mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-07 15:13:52 -08:00
Compare commits
164 Commits
NewSoupVi-
...
core_world
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18661d9a63 | ||
|
|
c194ebf7c5 | ||
|
|
48a59247d9 | ||
|
|
b372b02273 | ||
|
|
f26313367e | ||
|
|
a3e8f69909 | ||
|
|
922c7fe86a | ||
|
|
e49ba2ff6f | ||
|
|
61d5120f66 | ||
|
|
ff5402c410 | ||
|
|
fcccbfca65 | ||
|
|
2db5435474 | ||
|
|
eeb022fa0c | ||
|
|
b30b2ecb07 | ||
|
|
699ca8adf6 | ||
|
|
fefd790de6 | ||
|
|
d83da1b818 | ||
|
|
0de09cd794 | ||
|
|
48c201af19 | ||
|
|
b0300d3063 | ||
|
|
e0e34894a3 | ||
|
|
18e3a8911f | ||
|
|
c505b1c32c | ||
|
|
e22e434258 | ||
|
|
8b91f9ff72 | ||
|
|
fadcfbdfea | ||
|
|
3c4c294f9c | ||
|
|
27a7e538df | ||
|
|
cb0cadcc5f | ||
|
|
2e1035a29f | ||
|
|
21c7f3cd92 | ||
|
|
13b6a5f4b2 | ||
|
|
78e8082a6f | ||
|
|
1de91fab67 | ||
|
|
4ef5436559 | ||
|
|
f2a6a769b0 | ||
|
|
8a767bd2ad | ||
|
|
7df243b860 | ||
|
|
f35d91933b | ||
|
|
286769a0f3 | ||
|
|
1dd91ec85b | ||
|
|
6adeb8b95e | ||
|
|
3e0d42bf9e | ||
|
|
41e22dabda | ||
|
|
39e7ee315e | ||
|
|
3e032e6cd6 | ||
|
|
609f4af600 | ||
|
|
4c27e35445 | ||
|
|
b0c967c039 | ||
|
|
c51da00bfb | ||
|
|
f3389f5d8b | ||
|
|
3b1971be66 | ||
|
|
4cb518930c | ||
|
|
c835bff570 | ||
|
|
6ee02fc62d | ||
|
|
8095f922bc | ||
|
|
77e5f3733e | ||
|
|
c47687dd21 | ||
|
|
8662433142 | ||
|
|
5f073c2a76 | ||
|
|
c5d67dd97a | ||
|
|
9b421450b1 | ||
|
|
a6740e7be3 | ||
|
|
65ef35f1b4 | ||
|
|
520253e762 | ||
|
|
aa3614a32b | ||
|
|
94492c45cb | ||
|
|
8f261bb27c | ||
|
|
ddd08342c8 | ||
|
|
c7db213ee9 | ||
|
|
220248dd3d | ||
|
|
5932160f15 | ||
|
|
76e0619b79 | ||
|
|
646a52a2e7 | ||
|
|
e1322df8b0 | ||
|
|
092a9dc6bd | ||
|
|
9f71fe707f | ||
|
|
b8311a62e7 | ||
|
|
13830ff4cb | ||
|
|
c1b858b2cf | ||
|
|
a035ac579c | ||
|
|
20c10e33c4 | ||
|
|
a4e4ce1c72 | ||
|
|
983936af8c | ||
|
|
62dfeac441 | ||
|
|
b81e1a228a | ||
|
|
5899920e48 | ||
|
|
8dee460397 | ||
|
|
cda54e0bea | ||
|
|
0554bf4e2d | ||
|
|
b92803e77f | ||
|
|
69e83071ff | ||
|
|
875765e6dc | ||
|
|
db56e26df9 | ||
|
|
5a88641228 | ||
|
|
16559e7595 | ||
|
|
d594d5d4a7 | ||
|
|
e950a2fa58 | ||
|
|
1df38cb782 | ||
|
|
c6400b6673 | ||
|
|
dbf2325c01 | ||
|
|
dd5b25399a | ||
|
|
8178ee4e58 | ||
|
|
ad1b41ea81 | ||
|
|
efd8528db0 | ||
|
|
e54a15978f | ||
|
|
d78b9ded2d | ||
|
|
53e8130c9c | ||
|
|
55c70a5ba8 | ||
|
|
ebbdd7bfda | ||
|
|
863f161466 | ||
|
|
9305ecb3bc | ||
|
|
0002bb8e5a | ||
|
|
b42fb77451 | ||
|
|
5a8e166289 | ||
|
|
5fa719143c | ||
|
|
a906f139c3 | ||
|
|
56363ea7e7 | ||
|
|
01e1e1fe11 | ||
|
|
4477dc7a66 | ||
|
|
45994e344e | ||
|
|
51d5e1afae | ||
|
|
577b958c4d | ||
|
|
ce38d8ced6 | ||
|
|
d65fcf286d | ||
|
|
5a6a0b37d6 | ||
|
|
4a0a65d604 | ||
|
|
d25abfc305 | ||
|
|
0905e3ce32 | ||
|
|
ac84b272c5 | ||
|
|
e8a63abfa4 | ||
|
|
3fa2745c37 | ||
|
|
775065715d | ||
|
|
4e608b13ae | ||
|
|
886cc68051 | ||
|
|
146a314d22 | ||
|
|
18cf1bce36 | ||
|
|
f7e3f4e589 | ||
|
|
9f9765b78d | ||
|
|
8ae1a7da32 | ||
|
|
08ea3fe225 | ||
|
|
b81be6b4fc | ||
|
|
f1aca0fc46 | ||
|
|
d88fe99780 | ||
|
|
360a1384f2 | ||
|
|
d089b00ad5 | ||
|
|
c05a2adc38 | ||
|
|
7631242621 | ||
|
|
df48c3e718 | ||
|
|
9a755e64b2 | ||
|
|
34d362a003 | ||
|
|
b75cce5d41 | ||
|
|
a07faca2d9 | ||
|
|
8a1a715dc4 | ||
|
|
60a192b1b6 | ||
|
|
3b721e0365 | ||
|
|
3e16c20fce | ||
|
|
ec2c39e82f | ||
|
|
23d319247f | ||
|
|
c2c488410f | ||
|
|
8ea49e76db | ||
|
|
d834ecec6a | ||
|
|
f3000a89d4 | ||
|
|
aa2774a5d5 |
4
.github/pyright-config.json
vendored
4
.github/pyright-config.json
vendored
@@ -2,11 +2,15 @@
|
||||
"include": [
|
||||
"../BizHawkClient.py",
|
||||
"../Patch.py",
|
||||
"../rule_builder/cached_world.py",
|
||||
"../rule_builder/options.py",
|
||||
"../rule_builder/rules.py",
|
||||
"../test/param.py",
|
||||
"../test/general/test_groups.py",
|
||||
"../test/general/test_helpers.py",
|
||||
"../test/general/test_memory.py",
|
||||
"../test/general/test_names.py",
|
||||
"../test/general/test_rule_builder.py",
|
||||
"../test/multiworld/__init__.py",
|
||||
"../test/multiworld/test_multiworlds.py",
|
||||
"../test/netutils/__init__.py",
|
||||
|
||||
13
.github/workflows/build.yml
vendored
13
.github/workflows/build.yml
vendored
@@ -1,4 +1,5 @@
|
||||
# This workflow will build a release-like distribution when manually dispatched
|
||||
# This workflow will build a release-like distribution when manually dispatched:
|
||||
# a Windows x64 7zip, a Windows x64 Installer, a Linux AppImage and a Linux binary .tar.gz.
|
||||
|
||||
name: Build
|
||||
|
||||
@@ -24,10 +25,10 @@ env:
|
||||
# NOTE: since appimage/appimagetool and appimage/type2-runtime does not have tags anymore,
|
||||
# we check the sha256 and require manual intervention if it was updated.
|
||||
APPIMAGE_FORK: 'PopTracker'
|
||||
APPIMAGETOOL_VERSION: 'r-2025-10-19'
|
||||
APPIMAGETOOL_X86_64_HASH: '9493a6b253a01f84acb9c624c38810ecfa11d99daa829b952b0bff43113080f9'
|
||||
APPIMAGE_RUNTIME_VERSION: 'r-2025-08-11'
|
||||
APPIMAGE_RUNTIME_X86_64_HASH: 'e70ffa9b69b211574d0917adc482dd66f25a0083427b5945783965d55b0b0a8b'
|
||||
APPIMAGETOOL_VERSION: 'r-2025-11-18'
|
||||
APPIMAGETOOL_X86_64_HASH: '4577a452b30af2337123fbb383aea154b618e51ad5448c3b62085cbbbfbfd9a2'
|
||||
APPIMAGE_RUNTIME_VERSION: 'r-2025-11-07'
|
||||
APPIMAGE_RUNTIME_X86_64_HASH: '27ddd3f78e483fc5f7856e413d7c17092917f8c35bfe3318a0d378aa9435ad17'
|
||||
|
||||
permissions: # permissions required for attestation
|
||||
id-token: 'write'
|
||||
@@ -50,7 +51,7 @@ jobs:
|
||||
run: |
|
||||
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip
|
||||
Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force
|
||||
choco install innosetup --version=6.2.2 --allow-downgrade
|
||||
choco install innosetup --version=6.7.0 --allow-downgrade
|
||||
- name: Build
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -12,10 +12,10 @@ env:
|
||||
# NOTE: since appimage/appimagetool and appimage/type2-runtime does not have tags anymore,
|
||||
# we check the sha256 and require manual intervention if it was updated.
|
||||
APPIMAGE_FORK: 'PopTracker'
|
||||
APPIMAGETOOL_VERSION: 'r-2025-10-19'
|
||||
APPIMAGETOOL_X86_64_HASH: '9493a6b253a01f84acb9c624c38810ecfa11d99daa829b952b0bff43113080f9'
|
||||
APPIMAGE_RUNTIME_VERSION: 'r-2025-08-11'
|
||||
APPIMAGE_RUNTIME_X86_64_HASH: 'e70ffa9b69b211574d0917adc482dd66f25a0083427b5945783965d55b0b0a8b'
|
||||
APPIMAGETOOL_VERSION: 'r-2025-11-18'
|
||||
APPIMAGETOOL_X86_64_HASH: '4577a452b30af2337123fbb383aea154b618e51ad5448c3b62085cbbbfbfd9a2'
|
||||
APPIMAGE_RUNTIME_VERSION: 'r-2025-11-07'
|
||||
APPIMAGE_RUNTIME_X86_64_HASH: '27ddd3f78e483fc5f7856e413d7c17092917f8c35bfe3318a0d378aa9435ad17'
|
||||
|
||||
permissions: # permissions required for attestation
|
||||
id-token: 'write'
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -63,7 +63,10 @@ Output Logs/
|
||||
/installdelete.iss
|
||||
/data/user.kv
|
||||
/datapackage
|
||||
/datapackage_export.json
|
||||
/custom_worlds
|
||||
# stubgen output
|
||||
/out/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Build APWorld" type="PythonConfigurationType" factoryName="Python">
|
||||
<configuration default="false" name="Build APWorlds" type="PythonConfigurationType" factoryName="Python">
|
||||
<module name="Archipelago" />
|
||||
<option name="ENV_FILES" value="" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
@@ -12,8 +12,8 @@
|
||||
<option name="IS_MODULE_SDK" value="true" />
|
||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||
<option name="SCRIPT_NAME" value="$ContentRoot$/Launcher.py" />
|
||||
<option name="PARAMETERS" value="\"Build APWorlds\"" />
|
||||
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/Launcher.py" />
|
||||
<option name="PARAMETERS" value=""Build APWorlds"" />
|
||||
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||
<option name="EMULATE_TERMINAL" value="false" />
|
||||
<option name="MODULE_MODE" value="false" />
|
||||
@@ -8,10 +8,10 @@ import secrets
|
||||
import warnings
|
||||
from argparse import Namespace
|
||||
from collections import Counter, deque, defaultdict
|
||||
from collections.abc import Collection, MutableSequence
|
||||
from collections.abc import Callable, Collection, Iterable, Iterator, Mapping, MutableSequence, Set
|
||||
from enum import IntEnum, IntFlag
|
||||
from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Literal, Mapping, NamedTuple,
|
||||
Optional, Protocol, Set, Tuple, Union, TYPE_CHECKING, Literal, overload)
|
||||
from typing import (AbstractSet, Any, ClassVar, Dict, List, Literal, NamedTuple,
|
||||
Optional, Protocol, Tuple, Union, TYPE_CHECKING, overload)
|
||||
import dataclasses
|
||||
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
@@ -22,6 +22,7 @@ import Utils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from entrance_rando import ERPlacementState
|
||||
from rule_builder.rules import Rule
|
||||
from worlds import AutoWorld
|
||||
|
||||
|
||||
@@ -85,7 +86,7 @@ class MultiWorld():
|
||||
local_items: Dict[int, Options.LocalItems]
|
||||
non_local_items: Dict[int, Options.NonLocalItems]
|
||||
progression_balancing: Dict[int, Options.ProgressionBalancing]
|
||||
completion_condition: Dict[int, Callable[[CollectionState], bool]]
|
||||
completion_condition: Dict[int, CollectionRule]
|
||||
indirect_connections: Dict[Region, Set[Entrance]]
|
||||
exclude_locations: Dict[int, Options.ExcludeLocations]
|
||||
priority_locations: Dict[int, Options.PriorityLocations]
|
||||
@@ -766,7 +767,7 @@ class CollectionState():
|
||||
else:
|
||||
self._update_reachable_regions_auto_indirect_conditions(player, queue)
|
||||
|
||||
def _update_reachable_regions_explicit_indirect_conditions(self, player: int, queue: deque):
|
||||
def _update_reachable_regions_explicit_indirect_conditions(self, player: int, queue: deque[Entrance]):
|
||||
reachable_regions = self.reachable_regions[player]
|
||||
blocked_connections = self.blocked_connections[player]
|
||||
# run BFS on all connections, and keep track of those blocked by missing items
|
||||
@@ -784,13 +785,16 @@ class CollectionState():
|
||||
blocked_connections.update(new_region.exits)
|
||||
queue.extend(new_region.exits)
|
||||
self.path[new_region] = (new_region.name, self.path.get(connection, None))
|
||||
self.multiworld.worlds[player].reached_region(self, new_region)
|
||||
|
||||
# Retry connections if the new region can unblock them
|
||||
for new_entrance in self.multiworld.indirect_connections.get(new_region, set()):
|
||||
if new_entrance in blocked_connections and new_entrance not in queue:
|
||||
queue.append(new_entrance)
|
||||
entrances = self.multiworld.indirect_connections.get(new_region)
|
||||
if entrances is not None:
|
||||
relevant_entrances = entrances.intersection(blocked_connections)
|
||||
relevant_entrances.difference_update(queue)
|
||||
queue.extend(relevant_entrances)
|
||||
|
||||
def _update_reachable_regions_auto_indirect_conditions(self, player: int, queue: deque):
|
||||
def _update_reachable_regions_auto_indirect_conditions(self, player: int, queue: deque[Entrance]):
|
||||
reachable_regions = self.reachable_regions[player]
|
||||
blocked_connections = self.blocked_connections[player]
|
||||
new_connection: bool = True
|
||||
@@ -812,6 +816,7 @@ class CollectionState():
|
||||
queue.extend(new_region.exits)
|
||||
self.path[new_region] = (new_region.name, self.path.get(connection, None))
|
||||
new_connection = True
|
||||
self.multiworld.worlds[player].reached_region(self, new_region)
|
||||
# sweep for indirect connections, mostly Entrance.can_reach(unrelated_Region)
|
||||
queue.extend(blocked_connections)
|
||||
|
||||
@@ -1169,13 +1174,17 @@ class CollectionState():
|
||||
self.prog_items[player][item] = count
|
||||
|
||||
|
||||
CollectionRule = Callable[[CollectionState], bool]
|
||||
DEFAULT_COLLECTION_RULE: CollectionRule = staticmethod(lambda state: True)
|
||||
|
||||
|
||||
class EntranceType(IntEnum):
|
||||
ONE_WAY = 1
|
||||
TWO_WAY = 2
|
||||
|
||||
|
||||
class Entrance:
|
||||
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
|
||||
access_rule: CollectionRule = DEFAULT_COLLECTION_RULE
|
||||
hide_path: bool = False
|
||||
player: int
|
||||
name: str
|
||||
@@ -1362,7 +1371,7 @@ class Region:
|
||||
self,
|
||||
location_name: str,
|
||||
item_name: str | None = None,
|
||||
rule: Callable[[CollectionState], bool] | None = None,
|
||||
rule: CollectionRule | Rule[Any] | None = None,
|
||||
location_type: type[Location] | None = None,
|
||||
item_type: type[Item] | None = None,
|
||||
show_in_spoiler: bool = True,
|
||||
@@ -1390,7 +1399,7 @@ class Region:
|
||||
event_location = location_type(self.player, location_name, None, self)
|
||||
event_location.show_in_spoiler = show_in_spoiler
|
||||
if rule is not None:
|
||||
event_location.access_rule = rule
|
||||
self.multiworld.worlds[self.player].set_rule(event_location, rule)
|
||||
|
||||
event_item = item_type(item_name, ItemClassification.progression, None, self.player)
|
||||
|
||||
@@ -1401,7 +1410,7 @@ class Region:
|
||||
return event_item
|
||||
|
||||
def connect(self, connecting_region: Region, name: Optional[str] = None,
|
||||
rule: Optional[Callable[[CollectionState], bool]] = None) -> Entrance:
|
||||
rule: Optional[CollectionRule | Rule[Any]] = None) -> Entrance:
|
||||
"""
|
||||
Connects this Region to another Region, placing the provided rule on the connection.
|
||||
|
||||
@@ -1409,8 +1418,8 @@ class Region:
|
||||
:param name: name of the connection being created
|
||||
:param rule: callable to determine access of this connection to go from self to the exiting_region"""
|
||||
exit_ = self.create_exit(name if name else f"{self.name} -> {connecting_region.name}")
|
||||
if rule:
|
||||
exit_.access_rule = rule
|
||||
if rule is not None:
|
||||
self.multiworld.worlds[self.player].set_rule(exit_, rule)
|
||||
exit_.connect(connecting_region)
|
||||
return exit_
|
||||
|
||||
@@ -1435,7 +1444,7 @@ class Region:
|
||||
return entrance
|
||||
|
||||
def add_exits(self, exits: Iterable[str] | Mapping[str, str | None],
|
||||
rules: Mapping[str, Callable[[CollectionState], bool]] | None = None) -> List[Entrance]:
|
||||
rules: Mapping[str, CollectionRule | Rule[Any]] | None = None) -> List[Entrance]:
|
||||
"""
|
||||
Connects current region to regions in exit dictionary. Passed region names must exist first.
|
||||
|
||||
@@ -1474,7 +1483,7 @@ class Location:
|
||||
show_in_spoiler: bool = True
|
||||
progress_type: LocationProgressType = LocationProgressType.DEFAULT
|
||||
always_allow: Callable[[CollectionState, Item], bool] = staticmethod(lambda state, item: False)
|
||||
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
|
||||
access_rule: CollectionRule = DEFAULT_COLLECTION_RULE
|
||||
item_rule: Callable[[Item], bool] = staticmethod(lambda item: True)
|
||||
item: Optional[Item] = None
|
||||
|
||||
@@ -1551,7 +1560,7 @@ class ItemClassification(IntFlag):
|
||||
skip_balancing = 0b01000
|
||||
""" should technically never occur on its own
|
||||
Item that is logically relevant, but progression balancing should not touch.
|
||||
|
||||
|
||||
Possible reasons for why an item should not be pulled ahead by progression balancing:
|
||||
1. This item is quite insignificant, so pulling it earlier doesn't help (currency/etc.)
|
||||
2. It is important for the player experience that this item is evenly distributed in the seed (e.g. goal items) """
|
||||
@@ -1559,13 +1568,13 @@ class ItemClassification(IntFlag):
|
||||
deprioritized = 0b10000
|
||||
""" Should technically never occur on its own.
|
||||
Will not be considered for priority locations,
|
||||
unless Priority Locations Fill runs out of regular progression items before filling all priority locations.
|
||||
|
||||
unless Priority Locations Fill runs out of regular progression items before filling all priority locations.
|
||||
|
||||
Should be used for items that would feel bad for the player to find on a priority location.
|
||||
Usually, these are items that are plentiful or insignificant. """
|
||||
|
||||
progression_deprioritized_skip_balancing = 0b11001
|
||||
""" Since a common case of both skip_balancing and deprioritized is "insignificant progression",
|
||||
""" Since a common case of both skip_balancing and deprioritized is "insignificant progression",
|
||||
these items often want both flags. """
|
||||
|
||||
progression_skip_balancing = 0b01001 # only progression gets balanced
|
||||
|
||||
@@ -24,7 +24,7 @@ if __name__ == "__main__":
|
||||
from MultiServer import CommandProcessor, mark_raw
|
||||
from NetUtils import (Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot,
|
||||
RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes, HintStatus, SlotType)
|
||||
from Utils import Version, stream_input, async_start
|
||||
from Utils import gui_enabled, Version, stream_input, async_start
|
||||
from worlds import network_data_package, AutoWorldRegister
|
||||
import os
|
||||
import ssl
|
||||
@@ -35,9 +35,6 @@ if typing.TYPE_CHECKING:
|
||||
|
||||
logger = logging.getLogger("Client")
|
||||
|
||||
# without terminal, we have to use gui mode
|
||||
gui_enabled = not sys.stdout or "--nogui" not in sys.argv
|
||||
|
||||
|
||||
@Utils.cache_argsless
|
||||
def get_ssl_context():
|
||||
@@ -65,6 +62,8 @@ class ClientCommandProcessor(CommandProcessor):
|
||||
|
||||
def _cmd_exit(self) -> bool:
|
||||
"""Close connections and client"""
|
||||
if self.ctx.ui:
|
||||
self.ctx.ui.stop()
|
||||
self.ctx.exit_event.set()
|
||||
return True
|
||||
|
||||
@@ -323,7 +322,7 @@ class CommonContext:
|
||||
hint_cost: int | None
|
||||
"""Current Hint Cost per Hint from the server"""
|
||||
hint_points: int | None
|
||||
"""Current avaliable Hint Points from the server"""
|
||||
"""Current available Hint Points from the server"""
|
||||
player_names: dict[int, str]
|
||||
"""Current lookup of slot number to player display name from server (includes aliases)"""
|
||||
|
||||
|
||||
136
Generate.py
136
Generate.py
@@ -23,7 +23,7 @@ from BaseClasses import seeddigits, get_seed, PlandoOptions
|
||||
from Utils import parse_yamls, version_tuple, __version__, tuplize_version
|
||||
|
||||
|
||||
def mystery_argparse(argv: list[str] | None = None):
|
||||
def mystery_argparse(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
from settings import get_settings
|
||||
settings = get_settings()
|
||||
defaults = settings.generator
|
||||
@@ -68,7 +68,7 @@ def mystery_argparse(argv: list[str] | None = None):
|
||||
args.weights_file_path = os.path.join(args.player_files_path, args.weights_file_path)
|
||||
if not os.path.isabs(args.meta_file_path):
|
||||
args.meta_file_path = os.path.join(args.player_files_path, args.meta_file_path)
|
||||
args.plando: PlandoOptions = PlandoOptions.from_option_string(args.plando)
|
||||
args.plando = PlandoOptions.from_option_string(args.plando)
|
||||
|
||||
return args
|
||||
|
||||
@@ -119,9 +119,9 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
||||
else:
|
||||
meta_weights = None
|
||||
|
||||
|
||||
player_id = 1
|
||||
player_files = {}
|
||||
player_id: int = 1
|
||||
player_files: dict[int, str] = {}
|
||||
player_errors: list[str] = []
|
||||
for file in os.scandir(args.player_files_path):
|
||||
fname = file.name
|
||||
if file.is_file() and not fname.startswith(".") and not fname.lower().endswith(".ini") and \
|
||||
@@ -135,9 +135,13 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
||||
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
|
||||
logging.exception(f"Exception reading weights in file {fname}")
|
||||
player_errors.append(
|
||||
f"{len(player_errors) + 1}. "
|
||||
f"File {fname} is invalid. Please fix your yaml.\n{Utils.get_all_causes(e)}"
|
||||
)
|
||||
|
||||
# sort dict for consistent results across platforms:
|
||||
weights_cache = {key: value for key, value in sorted(weights_cache.items(), key=lambda k: k[0].casefold())}
|
||||
@@ -152,6 +156,10 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
||||
args.multi = max(player_id - 1, args.multi)
|
||||
|
||||
if args.multi == 0:
|
||||
if player_errors:
|
||||
errors = "\n\n".join(player_errors)
|
||||
raise ValueError(f"Encountered {len(player_errors)} error(s) in player files. "
|
||||
f"See logs for full tracebacks.\n\n{errors}")
|
||||
raise ValueError(
|
||||
"No individual player files found and number of players is 0. "
|
||||
"Provide individual player files or specify the number of players via host.yaml or --multi."
|
||||
@@ -161,6 +169,10 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
||||
f"{seed_name} Seed {seed} with plando: {args.plando}")
|
||||
|
||||
if not weights_cache:
|
||||
if player_errors:
|
||||
errors = "\n\n".join(player_errors)
|
||||
raise ValueError(f"Encountered {len(player_errors)} error(s) in player files. "
|
||||
f"See logs for full tracebacks.\n\n{errors}")
|
||||
raise Exception(f"No weights found. "
|
||||
f"Provide a general weights file ({args.weights_file_path}) or individual player files. "
|
||||
f"A mix is also permitted.")
|
||||
@@ -171,10 +183,6 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
||||
args.sprite_pool = dict.fromkeys(range(1, args.multi+1), None)
|
||||
args.name = {}
|
||||
|
||||
settings_cache: dict[str, tuple[argparse.Namespace, ...]] = \
|
||||
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None)
|
||||
for fname, yamls in weights_cache.items()}
|
||||
|
||||
if meta_weights:
|
||||
for category_name, category_dict in meta_weights.items():
|
||||
for key in category_dict:
|
||||
@@ -197,47 +205,85 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
||||
else:
|
||||
yaml[category_name][key] = option
|
||||
|
||||
player_path_cache = {}
|
||||
settings_cache: dict[str, tuple[argparse.Namespace, ...] | None] = {fname: None for fname in weights_cache}
|
||||
if args.sameoptions:
|
||||
for fname, yamls in weights_cache.items():
|
||||
try:
|
||||
settings_cache[fname] = tuple(roll_settings(yaml, args.plando) for yaml in yamls)
|
||||
except Exception as e:
|
||||
logging.exception(f"Exception reading settings in file {fname}")
|
||||
player_errors.append(
|
||||
f"{len(player_errors) + 1}. "
|
||||
f"File {fname} is invalid. Please fix your yaml.\n{Utils.get_all_causes(e)}"
|
||||
)
|
||||
# Exit early here to avoid throwing the same errors again later
|
||||
if player_errors:
|
||||
errors = "\n\n".join(player_errors)
|
||||
raise ValueError(f"Encountered {len(player_errors)} error(s) in player files. "
|
||||
f"See logs for full tracebacks.\n\n{errors}")
|
||||
|
||||
player_path_cache: dict[int, str] = {}
|
||||
for player in range(1, args.multi + 1):
|
||||
player_path_cache[player] = player_files.get(player, args.weights_file_path)
|
||||
name_counter = Counter()
|
||||
name_counter: Counter[str] = Counter()
|
||||
args.player_options = {}
|
||||
|
||||
player = 1
|
||||
while player <= args.multi:
|
||||
path = player_path_cache[player]
|
||||
if path:
|
||||
if not path:
|
||||
player_errors.append(f'No weights specified for player {player}')
|
||||
player += 1
|
||||
continue
|
||||
|
||||
for doc_index, yaml in enumerate(weights_cache[path]):
|
||||
name = yaml.get("name")
|
||||
try:
|
||||
settings: tuple[argparse.Namespace, ...] = settings_cache[path] if settings_cache[path] else \
|
||||
tuple(roll_settings(yaml, args.plando) for yaml in weights_cache[path])
|
||||
for settingsObject in settings:
|
||||
for k, v in vars(settingsObject).items():
|
||||
if v is not None:
|
||||
try:
|
||||
getattr(args, k)[player] = v
|
||||
except AttributeError:
|
||||
setattr(args, k, {player: v})
|
||||
except Exception as e:
|
||||
raise Exception(f"Error setting {k} to {v} for player {player}") from e
|
||||
# Use the cached settings object if it exists, otherwise roll settings within the try-catch
|
||||
# Invariant: settings_cache[path] and weights_cache[path] have the same length
|
||||
cached = settings_cache[path]
|
||||
settings_object: argparse.Namespace = (cached[doc_index] if cached else roll_settings(yaml, args.plando))
|
||||
|
||||
# name was not specified
|
||||
if player not in args.name:
|
||||
if path == args.weights_file_path:
|
||||
# weights file, so we need to make the name unique
|
||||
args.name[player] = f"Player{player}"
|
||||
else:
|
||||
# use the filename
|
||||
args.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
|
||||
args.name[player] = handle_name(args.name[player], player, name_counter)
|
||||
for k, v in vars(settings_object).items():
|
||||
if v is not None:
|
||||
try:
|
||||
getattr(args, k)[player] = v
|
||||
except AttributeError:
|
||||
setattr(args, k, {player: v})
|
||||
except Exception as e:
|
||||
raise Exception(f"Error setting {k} to {v} for player {player}") from e
|
||||
|
||||
# name was not specified
|
||||
if player not in args.name:
|
||||
if path == args.weights_file_path:
|
||||
# weights file, so we need to make the name unique
|
||||
args.name[player] = f"Player{player}"
|
||||
else:
|
||||
# use the filename
|
||||
args.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
|
||||
args.name[player] = handle_name(args.name[player], player, name_counter)
|
||||
|
||||
player += 1
|
||||
except Exception as e:
|
||||
raise ValueError(f"File {path} is invalid. Please fix your yaml.") from e
|
||||
else:
|
||||
raise RuntimeError(f'No weights specified for player {player}')
|
||||
logging.exception(f"Exception reading settings in file {path} document #{doc_index + 1} "
|
||||
f"(name: {args.name.get(player, name)})")
|
||||
player_errors.append(
|
||||
f"{len(player_errors) + 1}. "
|
||||
f"File {path} document #{doc_index + 1} (name: {args.name.get(player, name)}) is invalid. "
|
||||
f"Please fix your yaml.\n{Utils.get_all_causes(e)}")
|
||||
|
||||
# increment for each yaml document in the file
|
||||
player += 1
|
||||
|
||||
if len(set(name.lower() for name in args.name.values())) != len(args.name):
|
||||
raise Exception(f"Names have to be unique. Names: {Counter(name.lower() for name in args.name.values())}")
|
||||
player_errors.append(
|
||||
f"{len(player_errors) + 1}. "
|
||||
f"Names have to be unique. Names: {Counter(name.lower() for name in args.name.values())}"
|
||||
)
|
||||
|
||||
if player_errors:
|
||||
errors = "\n\n".join(player_errors)
|
||||
raise ValueError(f"Encountered {len(player_errors)} error(s) in player files. "
|
||||
f"See logs for full tracebacks.\n\n{errors}")
|
||||
|
||||
return args, seed
|
||||
|
||||
@@ -316,7 +362,7 @@ class SafeFormatter(string.Formatter):
|
||||
return kwargs.get(key, "{" + key + "}")
|
||||
|
||||
|
||||
def handle_name(name: str, player: int, name_counter: Counter):
|
||||
def handle_name(name: str, player: int, name_counter: Counter[str]):
|
||||
name_counter[name.lower()] += 1
|
||||
number = name_counter[name.lower()]
|
||||
new_name = "%".join([x.replace("%number%", "{number}").replace("%player%", "{player}") for x in name.split("%%")])
|
||||
@@ -347,7 +393,9 @@ def update_weights(weights: dict, new_weights: dict, update_type: str, name: str
|
||||
elif isinstance(new_value, list):
|
||||
cleaned_value.extend(new_value)
|
||||
elif isinstance(new_value, dict):
|
||||
cleaned_value = dict(Counter(cleaned_value) + Counter(new_value))
|
||||
counter_value = Counter(cleaned_value)
|
||||
counter_value.update(new_value)
|
||||
cleaned_value = dict(counter_value)
|
||||
else:
|
||||
raise Exception(f"Cannot apply merge to non-dict, set, or list type {option_name},"
|
||||
f" received {type(new_value).__name__}.")
|
||||
@@ -361,7 +409,9 @@ def update_weights(weights: dict, new_weights: dict, update_type: str, name: str
|
||||
for element in new_value:
|
||||
cleaned_value.remove(element)
|
||||
elif isinstance(new_value, dict):
|
||||
cleaned_value = dict(Counter(cleaned_value) - Counter(new_value))
|
||||
counter_value = Counter(cleaned_value)
|
||||
counter_value.subtract(new_value)
|
||||
cleaned_value = dict(counter_value)
|
||||
else:
|
||||
raise Exception(f"Cannot apply remove to non-dict, set, or list type {option_name},"
|
||||
f" received {type(new_value).__name__}.")
|
||||
@@ -450,7 +500,7 @@ def roll_triggers(weights: dict, triggers: list, valid_keys: set) -> dict:
|
||||
return weights
|
||||
|
||||
|
||||
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option), plando_options: PlandoOptions):
|
||||
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type[Options.Option], plando_options: PlandoOptions):
|
||||
try:
|
||||
if option_key in game_weights:
|
||||
if not option.supports_weighting:
|
||||
|
||||
16
Launcher.py
16
Launcher.py
@@ -31,6 +31,10 @@ import settings
|
||||
import Utils
|
||||
from Utils import (init_logging, is_frozen, is_linux, is_macos, is_windows, local_path, messagebox, open_filename,
|
||||
user_path)
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_logging('Launcher')
|
||||
|
||||
from worlds.LauncherComponents import Component, components, icon_paths, SuffixIdentifier, Type
|
||||
|
||||
|
||||
@@ -218,12 +222,17 @@ def launch(exe, in_terminal=False):
|
||||
|
||||
def create_shortcut(button: Any, component: Component) -> None:
|
||||
from pyshortcuts import make_shortcut
|
||||
script = sys.argv[0]
|
||||
wkdir = Utils.local_path()
|
||||
env = os.environ
|
||||
if "APPIMAGE" in env:
|
||||
script = env["ARGV0"]
|
||||
wkdir = None # defaults to ~ on Linux
|
||||
else:
|
||||
script = sys.argv[0]
|
||||
wkdir = Utils.local_path()
|
||||
|
||||
script = f"{script} \"{component.display_name}\""
|
||||
make_shortcut(script, name=f"Archipelago {component.display_name}", icon=local_path("data", "icon.ico"),
|
||||
startmenu=False, terminal=False, working_dir=wkdir)
|
||||
startmenu=False, terminal=False, working_dir=wkdir, noexe=Utils.is_frozen())
|
||||
button.menu.dismiss()
|
||||
|
||||
|
||||
@@ -488,7 +497,6 @@ def main(args: argparse.Namespace | dict | None = None):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_logging('Launcher')
|
||||
multiprocessing.freeze_support()
|
||||
multiprocessing.set_start_method("spawn") # if launched process uses kivy, fork won't work
|
||||
parser = argparse.ArgumentParser(
|
||||
|
||||
3
Main.py
3
Main.py
@@ -207,6 +207,9 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
|
||||
else:
|
||||
logger.info("Progression balancing skipped.")
|
||||
|
||||
AutoWorld.call_all(multiworld, "finalize_multiworld")
|
||||
AutoWorld.call_all(multiworld, "pre_output")
|
||||
|
||||
# we're about to output using multithreading, so we're removing the global random state to prevent accidental use
|
||||
multiworld.random.passthrough = False
|
||||
|
||||
|
||||
@@ -5,15 +5,16 @@ import multiprocessing
|
||||
import warnings
|
||||
|
||||
|
||||
if sys.platform in ("win32", "darwin") and sys.version_info < (3, 11, 9):
|
||||
if sys.platform in ("win32", "darwin") and not (3, 11, 9) <= sys.version_info < (3, 14, 0):
|
||||
# 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.11.9+ is supported.")
|
||||
raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. "
|
||||
"Official 3.11.9 through 3.13.x is supported.")
|
||||
elif sys.platform in ("win32", "darwin") and sys.version_info < (3, 11, 13):
|
||||
# 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, 11, 0):
|
||||
elif not (3, 11, 0) <= sys.version_info < (3, 14, 0):
|
||||
# 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.11.0+ is supported.")
|
||||
raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. 3.11.0 through 3.13.x is supported.")
|
||||
|
||||
# don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess)
|
||||
_skip_update = bool(
|
||||
|
||||
@@ -21,6 +21,7 @@ import time
|
||||
import typing
|
||||
import weakref
|
||||
import zlib
|
||||
from signal import SIGINT, SIGTERM
|
||||
|
||||
import ModuleUpdate
|
||||
|
||||
@@ -69,6 +70,12 @@ def remove_from_list(container, value):
|
||||
|
||||
|
||||
def pop_from_container(container, value):
|
||||
if isinstance(container, list) and isinstance(value, int) and len(container) <= value:
|
||||
return container
|
||||
|
||||
if isinstance(container, dict) and value not in container:
|
||||
return container
|
||||
|
||||
try:
|
||||
container.pop(value)
|
||||
except ValueError:
|
||||
@@ -490,10 +497,11 @@ class Context:
|
||||
|
||||
self.read_data = {}
|
||||
# there might be a better place to put this.
|
||||
self.read_data["race_mode"] = lambda: decoded_obj.get("race_mode", 0)
|
||||
race_mode = decoded_obj.get("race_mode", 0)
|
||||
self.read_data["race_mode"] = lambda: race_mode
|
||||
mdata_ver = decoded_obj["minimum_versions"]["server"]
|
||||
if mdata_ver > version_tuple:
|
||||
raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver},"
|
||||
raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver}, "
|
||||
f"however this server is of version {version_tuple}")
|
||||
self.generator_version = Version(*decoded_obj["version"])
|
||||
clients_ver = decoded_obj["minimum_versions"].get("clients", {})
|
||||
@@ -911,12 +919,6 @@ async def server(websocket: "ServerConnection", path: str = "/", ctx: Context =
|
||||
|
||||
|
||||
async def on_client_connected(ctx: Context, client: Client):
|
||||
players = []
|
||||
for team, clients in ctx.clients.items():
|
||||
for slot, connected_clients in clients.items():
|
||||
if connected_clients:
|
||||
name = ctx.player_names[team, slot]
|
||||
players.append(NetworkPlayer(team, slot, ctx.name_aliases.get((team, slot), name), name))
|
||||
games = {ctx.games[x] for x in range(1, len(ctx.games) + 1)}
|
||||
games.add("Archipelago")
|
||||
await ctx.send_msgs(client, [{
|
||||
@@ -1301,6 +1303,13 @@ class CommandMeta(type):
|
||||
commands.update(base.commands)
|
||||
commands.update({command_name[5:]: method for command_name, method in attrs.items() if
|
||||
command_name.startswith("_cmd_")})
|
||||
for command_name, method in commands.items():
|
||||
# wrap async def functions so they run on default asyncio loop
|
||||
if inspect.iscoroutinefunction(method):
|
||||
def _wrapper(self, *args, _method=method, **kwargs):
|
||||
return async_start(_method(self, *args, **kwargs))
|
||||
functools.update_wrapper(_wrapper, method)
|
||||
commands[command_name] = _wrapper
|
||||
return super(CommandMeta, cls).__new__(cls, name, bases, attrs)
|
||||
|
||||
|
||||
@@ -1364,7 +1373,10 @@ class CommandProcessor(metaclass=CommandMeta):
|
||||
argname += "=" + parameter.default
|
||||
argtext += argname
|
||||
argtext += " "
|
||||
doctext = '\n '.join(inspect.getdoc(method).split('\n'))
|
||||
method_doc = inspect.getdoc(method)
|
||||
if method_doc is None:
|
||||
method_doc = "(missing help text)"
|
||||
doctext = "\n ".join(method_doc.split("\n"))
|
||||
s += f"{self.marker}{command} {argtext}\n {doctext}\n"
|
||||
return s
|
||||
|
||||
@@ -2560,6 +2572,8 @@ async def console(ctx: Context):
|
||||
input_text = await queue.get()
|
||||
queue.task_done()
|
||||
ctx.commandprocessor(input_text)
|
||||
except asyncio.exceptions.CancelledError:
|
||||
ctx.logger.info("ConsoleTask cancelled")
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
@@ -2726,6 +2740,15 @@ async def main(args: argparse.Namespace):
|
||||
console_task = asyncio.create_task(console(ctx))
|
||||
if ctx.auto_shutdown:
|
||||
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, [console_task]))
|
||||
|
||||
def stop():
|
||||
for remove_signal in [SIGINT, SIGTERM]:
|
||||
asyncio.get_event_loop().remove_signal_handler(remove_signal)
|
||||
ctx.commandprocessor._cmd_exit()
|
||||
|
||||
for signal in [SIGINT, SIGTERM]:
|
||||
asyncio.get_event_loop().add_signal_handler(signal, stop)
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
console_task.cancel()
|
||||
if ctx.shutdown_task:
|
||||
|
||||
188
Options.py
188
Options.py
@@ -24,6 +24,39 @@ if typing.TYPE_CHECKING:
|
||||
import pathlib
|
||||
|
||||
|
||||
_RANDOM_OPTS = [
|
||||
"random", "random-low", "random-middle", "random-high",
|
||||
"random-range-low-<min>-<max>", "random-range-middle-<min>-<max>",
|
||||
"random-range-high-<min>-<max>", "random-range-<min>-<max>",
|
||||
]
|
||||
|
||||
|
||||
def triangular(lower: int, end: int, tri: float = 0.5) -> int:
|
||||
"""
|
||||
Integer triangular distribution for `lower` inclusive to `end` inclusive.
|
||||
|
||||
Expects `lower <= end` and `0.0 <= tri <= 1.0`. The result of other inputs is undefined.
|
||||
"""
|
||||
# Use the continuous range [lower, end + 1) to produce an integer result in [lower, end].
|
||||
# random.triangular is actually [a, b] and not [a, b), so there is a very small chance of getting exactly b even
|
||||
# when a != b, so ensure the result is never more than `end`.
|
||||
return min(end, math.floor(random.triangular(0.0, 1.0, tri) * (end - lower + 1) + lower))
|
||||
|
||||
|
||||
def random_weighted_range(text: str, range_start: int, range_end: int):
|
||||
if text == "random-low":
|
||||
return triangular(range_start, range_end, 0.0)
|
||||
elif text == "random-high":
|
||||
return triangular(range_start, range_end, 1.0)
|
||||
elif text == "random-middle":
|
||||
return triangular(range_start, range_end)
|
||||
elif text == "random":
|
||||
return random.randint(range_start, range_end)
|
||||
else:
|
||||
raise Exception(f"random text \"{text}\" did not resolve to a recognized pattern. "
|
||||
f"Acceptable values are: {', '.join(_RANDOM_OPTS)}.")
|
||||
|
||||
|
||||
def roll_percentage(percentage: int | float) -> bool:
|
||||
"""Roll a percentage chance.
|
||||
percentage is expected to be in range [0, 100]"""
|
||||
@@ -417,10 +450,12 @@ class Toggle(NumericOption):
|
||||
def from_text(cls, text: str) -> Toggle:
|
||||
if text == "random":
|
||||
return cls(random.choice(list(cls.name_lookup)))
|
||||
elif text.lower() in {"off", "0", "false", "none", "null", "no"}:
|
||||
elif text.lower() in {"off", "0", "false", "none", "null", "no", "disabled"}:
|
||||
return cls(0)
|
||||
else:
|
||||
elif text.lower() in {"on", "1", "true", "yes", "enabled"}:
|
||||
return cls(1)
|
||||
else:
|
||||
raise OptionError(f"Option {cls.__name__} does not support a value of {text}")
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Any):
|
||||
@@ -523,9 +558,9 @@ class Choice(NumericOption):
|
||||
|
||||
class TextChoice(Choice):
|
||||
"""Allows custom string input and offers choices. Choices will resolve to int and text will resolve to string"""
|
||||
value: typing.Union[str, int]
|
||||
value: str | int
|
||||
|
||||
def __init__(self, value: typing.Union[str, int]):
|
||||
def __init__(self, value: str | int):
|
||||
assert isinstance(value, str) or isinstance(value, int), \
|
||||
f"'{value}' is not a valid option for '{self.__class__.__name__}'"
|
||||
self.value = value
|
||||
@@ -546,7 +581,7 @@ class TextChoice(Choice):
|
||||
return cls(text)
|
||||
|
||||
@classmethod
|
||||
def get_option_name(cls, value: T) -> str:
|
||||
def get_option_name(cls, value: str | int) -> str:
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
return super().get_option_name(value)
|
||||
@@ -688,12 +723,6 @@ class Range(NumericOption):
|
||||
range_start = 0
|
||||
range_end = 1
|
||||
|
||||
_RANDOM_OPTS = [
|
||||
"random", "random-low", "random-middle", "random-high",
|
||||
"random-range-low-<min>-<max>", "random-range-middle-<min>-<max>",
|
||||
"random-range-high-<min>-<max>", "random-range-<min>-<max>",
|
||||
]
|
||||
|
||||
def __init__(self, value: int):
|
||||
if value < self.range_start:
|
||||
raise Exception(f"{value} is lower than minimum {self.range_start} for option {self.__class__.__name__}")
|
||||
@@ -742,25 +771,16 @@ class Range(NumericOption):
|
||||
|
||||
@classmethod
|
||||
def weighted_range(cls, text) -> Range:
|
||||
if text == "random-low":
|
||||
return cls(cls.triangular(cls.range_start, cls.range_end, 0.0))
|
||||
elif text == "random-high":
|
||||
return cls(cls.triangular(cls.range_start, cls.range_end, 1.0))
|
||||
elif text == "random-middle":
|
||||
return cls(cls.triangular(cls.range_start, cls.range_end))
|
||||
elif text.startswith("random-range-"):
|
||||
if text.startswith("random-range-"):
|
||||
return cls.custom_range(text)
|
||||
elif text == "random":
|
||||
return cls(random.randint(cls.range_start, cls.range_end))
|
||||
else:
|
||||
raise Exception(f"random text \"{text}\" did not resolve to a recognized pattern. "
|
||||
f"Acceptable values are: {', '.join(cls._RANDOM_OPTS)}.")
|
||||
return cls(random_weighted_range(text, cls.range_start, cls.range_end))
|
||||
|
||||
@classmethod
|
||||
def custom_range(cls, text) -> Range:
|
||||
textsplit = text.split("-")
|
||||
try:
|
||||
random_range = [int(textsplit[len(textsplit) - 2]), int(textsplit[len(textsplit) - 1])]
|
||||
random_range = [int(textsplit[-2]), int(textsplit[-1])]
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid random range {text} for option {cls.__name__}")
|
||||
random_range.sort()
|
||||
@@ -768,14 +788,9 @@ class Range(NumericOption):
|
||||
raise Exception(
|
||||
f"{random_range[0]}-{random_range[1]} is outside allowed range "
|
||||
f"{cls.range_start}-{cls.range_end} for option {cls.__name__}")
|
||||
if text.startswith("random-range-low"):
|
||||
return cls(cls.triangular(random_range[0], random_range[1], 0.0))
|
||||
elif text.startswith("random-range-middle"):
|
||||
return cls(cls.triangular(random_range[0], random_range[1]))
|
||||
elif text.startswith("random-range-high"):
|
||||
return cls(cls.triangular(random_range[0], random_range[1], 1.0))
|
||||
else:
|
||||
return cls(random.randint(random_range[0], random_range[1]))
|
||||
if textsplit[2] in ("low", "middle", "high"):
|
||||
return cls(random_weighted_range(f"{textsplit[0]}-{textsplit[2]}", *random_range))
|
||||
return cls(random_weighted_range("random", *random_range))
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Any) -> Range:
|
||||
@@ -790,18 +805,6 @@ class Range(NumericOption):
|
||||
def __str__(self) -> str:
|
||||
return str(self.value)
|
||||
|
||||
@staticmethod
|
||||
def triangular(lower: int, end: int, tri: float = 0.5) -> int:
|
||||
"""
|
||||
Integer triangular distribution for `lower` inclusive to `end` inclusive.
|
||||
|
||||
Expects `lower <= end` and `0.0 <= tri <= 1.0`. The result of other inputs is undefined.
|
||||
"""
|
||||
# Use the continuous range [lower, end + 1) to produce an integer result in [lower, end].
|
||||
# random.triangular is actually [a, b] and not [a, b), so there is a very small chance of getting exactly b even
|
||||
# when a != b, so ensure the result is never more than `end`.
|
||||
return min(end, math.floor(random.triangular(0.0, 1.0, tri) * (end - lower + 1) + lower))
|
||||
|
||||
|
||||
class NamedRange(Range):
|
||||
special_range_names: typing.Dict[str, int] = {}
|
||||
@@ -891,7 +894,7 @@ class VerifyKeys(metaclass=FreezeValidKeys):
|
||||
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
|
||||
@@ -906,7 +909,8 @@ class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mappin
|
||||
else:
|
||||
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
|
||||
|
||||
def get_option_name(self, value):
|
||||
@classmethod
|
||||
def get_option_name(cls, value):
|
||||
return ", ".join(f"{key}: {v}" for key, v in value.items())
|
||||
|
||||
def __getitem__(self, item: str) -> typing.Any:
|
||||
@@ -986,7 +990,8 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
|
||||
return cls(data)
|
||||
return cls.from_text(str(data))
|
||||
|
||||
def get_option_name(self, value):
|
||||
@classmethod
|
||||
def get_option_name(cls, value):
|
||||
return ", ".join(map(str, value))
|
||||
|
||||
def __contains__(self, item):
|
||||
@@ -996,13 +1001,19 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
|
||||
class OptionSet(Option[typing.Set[str]], VerifyKeys):
|
||||
default = frozenset()
|
||||
supports_weighting = False
|
||||
random_str: str | None
|
||||
|
||||
def __init__(self, value: typing.Iterable[str]):
|
||||
def __init__(self, value: typing.Iterable[str], random_str: str | None = None):
|
||||
self.value = set(deepcopy(value))
|
||||
self.random_str = random_str
|
||||
super(OptionSet, self).__init__()
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str):
|
||||
check_text = text.lower().split(",")
|
||||
if ((cls.valid_keys or cls.verify_item_name or cls.verify_location_name)
|
||||
and len(check_text) == 1 and check_text[0].startswith("random")):
|
||||
return cls((), check_text[0])
|
||||
return cls([option.strip() for option in text.split(",")])
|
||||
|
||||
@classmethod
|
||||
@@ -1011,7 +1022,37 @@ class OptionSet(Option[typing.Set[str]], VerifyKeys):
|
||||
return cls(data)
|
||||
return cls.from_text(str(data))
|
||||
|
||||
def get_option_name(self, value):
|
||||
def verify(self, world: typing.Type[World], player_name: str, plando_options: PlandoOptions) -> None:
|
||||
if self.random_str and not self.value:
|
||||
choice_list = sorted(self.valid_keys)
|
||||
if self.verify_item_name:
|
||||
choice_list.extend(sorted(world.item_names))
|
||||
if self.verify_location_name:
|
||||
choice_list.extend(sorted(world.location_names))
|
||||
if self.random_str.startswith("random-range-"):
|
||||
textsplit = self.random_str.split("-")
|
||||
try:
|
||||
random_range = [int(textsplit[-2]), int(textsplit[-1])]
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid random range {self.random_str} for option {self.__class__.__name__} "
|
||||
f"for player {player_name}")
|
||||
random_range.sort()
|
||||
if random_range[0] < 0 or random_range[1] > len(choice_list):
|
||||
raise Exception(
|
||||
f"{random_range[0]}-{random_range[1]} is outside allowed range "
|
||||
f"0-{len(choice_list)} for option {self.__class__.__name__} for player {player_name}")
|
||||
if textsplit[2] in ("low", "middle", "high"):
|
||||
choice_count = random_weighted_range(f"{textsplit[0]}-{textsplit[2]}",
|
||||
random_range[0], random_range[1])
|
||||
else:
|
||||
choice_count = random_weighted_range("random", random_range[0], random_range[1])
|
||||
else:
|
||||
choice_count = random_weighted_range(self.random_str, 0, len(choice_list))
|
||||
self.value = set(random.sample(choice_list, k=choice_count))
|
||||
super(Option, self).verify(world, player_name, plando_options)
|
||||
|
||||
@classmethod
|
||||
def get_option_name(cls, value):
|
||||
return ", ".join(sorted(value))
|
||||
|
||||
def __contains__(self, item):
|
||||
@@ -1545,6 +1586,7 @@ class PlandoItems(Option[typing.List[PlandoItem]]):
|
||||
default = ()
|
||||
supports_weighting = False
|
||||
display_name = "Plando Items"
|
||||
visibility = Visibility.template | Visibility.spoiler
|
||||
|
||||
def __init__(self, value: typing.Iterable[PlandoItem]) -> None:
|
||||
self.value = list(deepcopy(value))
|
||||
@@ -1655,7 +1697,7 @@ class PlandoItems(Option[typing.List[PlandoItem]]):
|
||||
def __len__(self) -> int:
|
||||
return len(self.value)
|
||||
|
||||
|
||||
|
||||
class Removed(FreeText):
|
||||
"""This Option has been Removed."""
|
||||
rich_text_doc = True
|
||||
@@ -1741,8 +1783,10 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
|
||||
from Utils import local_path, __version__
|
||||
|
||||
full_path: str
|
||||
preset_folder = os.path.join(target_folder, "Presets")
|
||||
|
||||
os.makedirs(target_folder, exist_ok=True)
|
||||
os.makedirs(preset_folder, exist_ok=True)
|
||||
|
||||
# clean out old
|
||||
for file in os.listdir(target_folder):
|
||||
@@ -1750,11 +1794,16 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
|
||||
if os.path.isfile(full_path) and full_path.endswith(".yaml"):
|
||||
os.unlink(full_path)
|
||||
|
||||
def dictify_range(option: Range):
|
||||
data = {option.default: 50}
|
||||
for file in os.listdir(preset_folder):
|
||||
full_path = os.path.join(preset_folder, file)
|
||||
if os.path.isfile(full_path) and full_path.endswith(".yaml"):
|
||||
os.unlink(full_path)
|
||||
|
||||
def dictify_range(option: Range, option_val: int | str):
|
||||
data = {option_val: 50}
|
||||
for sub_option in ["random", "random-low", "random-high",
|
||||
f"random-range-{option.range_start}-{option.range_end}"]:
|
||||
if sub_option != option.default:
|
||||
if sub_option != option_val:
|
||||
data[sub_option] = 0
|
||||
notes = {
|
||||
"random-low": "random value weighted towards lower values",
|
||||
@@ -1767,6 +1816,8 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
|
||||
if number in data:
|
||||
data[name] = data[number]
|
||||
del data[number]
|
||||
elif name in data:
|
||||
pass
|
||||
else:
|
||||
data[name] = 0
|
||||
|
||||
@@ -1782,20 +1833,27 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
|
||||
|
||||
for game_name, world in AutoWorldRegister.world_types.items():
|
||||
if not world.hidden or generate_hidden:
|
||||
presets = world.web.options_presets.copy()
|
||||
presets.update({"": {}})
|
||||
|
||||
option_groups = get_option_groups(world)
|
||||
|
||||
res = template.render(
|
||||
option_groups=option_groups,
|
||||
__version__=__version__,
|
||||
game=game_name,
|
||||
world_version=world.world_version.as_simple_string(),
|
||||
yaml_dump=yaml_dump_scalar,
|
||||
dictify_range=dictify_range,
|
||||
cleandoc=cleandoc,
|
||||
)
|
||||
|
||||
with open(os.path.join(target_folder, get_file_safe_name(game_name) + ".yaml"), "w", encoding="utf-8-sig") as f:
|
||||
f.write(res)
|
||||
for name, preset in presets.items():
|
||||
res = template.render(
|
||||
option_groups=option_groups,
|
||||
__version__=__version__,
|
||||
game=game_name,
|
||||
world_version=world.world_version.as_simple_string(),
|
||||
yaml_dump=yaml_dump_scalar,
|
||||
dictify_range=dictify_range,
|
||||
cleandoc=cleandoc,
|
||||
preset_name=name,
|
||||
preset=preset,
|
||||
)
|
||||
preset_name = f" - {name}" if name else ""
|
||||
with open(os.path.join(preset_folder if name else target_folder,
|
||||
get_file_safe_name(game_name + preset_name) + ".yaml"),
|
||||
"w", encoding="utf-8-sig") as f:
|
||||
f.write(res)
|
||||
|
||||
|
||||
def dump_player_options(multiworld: MultiWorld) -> None:
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
if __name__ == "__main__":
|
||||
import ModuleUpdate
|
||||
|
||||
ModuleUpdate.update()
|
||||
|
||||
|
||||
from kvui import (ThemedApp, ScrollBox, MainLayout, ContainerLayout, dp, Widget, MDBoxLayout, TooltipLabel, MDLabel,
|
||||
ToggleButton, MarkupDropdown, ResizableTextField)
|
||||
from kivy.clock import Clock
|
||||
from kivy.uix.behaviors.button import ButtonBehavior
|
||||
from kivymd.uix.behaviors import RotateBehavior
|
||||
from kivymd.uix.anchorlayout import MDAnchorLayout
|
||||
@@ -22,7 +29,7 @@ import webbrowser
|
||||
import re
|
||||
from urllib.parse import urlparse
|
||||
from worlds.AutoWorld import AutoWorldRegister, World
|
||||
from Options import (Option, Toggle, TextChoice, Choice, FreeText, NamedRange, Range, OptionSet, OptionList, Removed,
|
||||
from Options import (Option, Toggle, TextChoice, Choice, FreeText, NamedRange, Range, OptionSet, OptionList,
|
||||
OptionCounter, Visibility)
|
||||
|
||||
|
||||
@@ -263,55 +270,76 @@ class OptionsCreator(ThemedApp):
|
||||
self.options = {}
|
||||
super().__init__()
|
||||
|
||||
def export_options(self, button: Widget):
|
||||
if 0 < len(self.name_input.text) < 17 and self.current_game:
|
||||
file_name = Utils.save_filename("Export Options File As...", [("YAML", ["*.yaml"])],
|
||||
@staticmethod
|
||||
def show_result_snack(text: str) -> None:
|
||||
MDSnackbar(MDSnackbarText(text=text), y=dp(24), pos_hint={"center_x": 0.5}, size_hint_x=0.5).open()
|
||||
|
||||
def on_export_result(self, text: str | None) -> None:
|
||||
self.container.disabled = False
|
||||
if text is not None:
|
||||
Clock.schedule_once(lambda _: self.show_result_snack(text), 0)
|
||||
|
||||
def export_options_background(self, options: dict[str, typing.Any]) -> None:
|
||||
try:
|
||||
file_name = Utils.save_filename("Export Options File As...", [("YAML", [".yaml"])],
|
||||
Utils.get_file_safe_name(f"{self.name_input.text}.yaml"))
|
||||
except Exception:
|
||||
self.on_export_result("Could not open dialog. Already open?")
|
||||
raise
|
||||
|
||||
if not file_name:
|
||||
self.on_export_result(None) # No file selected. No need to show a message for this.
|
||||
return
|
||||
|
||||
try:
|
||||
with open(file_name, 'w') as f:
|
||||
f.write(Utils.dump(options, sort_keys=False))
|
||||
f.close()
|
||||
self.on_export_result("File saved successfully.")
|
||||
except Exception:
|
||||
self.on_export_result("Could not save file.")
|
||||
raise
|
||||
|
||||
def export_options(self, button: Widget) -> None:
|
||||
if 0 < len(self.name_input.text) < 17 and self.current_game:
|
||||
import threading
|
||||
options = {
|
||||
"name": self.name_input.text,
|
||||
"description": f"YAML generated by Archipelago {Utils.__version__}.",
|
||||
"game": self.current_game,
|
||||
self.current_game: {k: check_random(v) for k, v in self.options.items()}
|
||||
}
|
||||
try:
|
||||
with open(file_name, 'w') as f:
|
||||
f.write(Utils.dump(options, sort_keys=False))
|
||||
f.close()
|
||||
MDSnackbar(MDSnackbarText(text="File saved successfully."), y=dp(24), pos_hint={"center_x": 0.5},
|
||||
size_hint_x=0.5).open()
|
||||
except FileNotFoundError:
|
||||
MDSnackbar(MDSnackbarText(text="Saving cancelled."), y=dp(24), pos_hint={"center_x": 0.5},
|
||||
size_hint_x=0.5).open()
|
||||
threading.Thread(target=self.export_options_background, args=(options,), daemon=True).start()
|
||||
self.container.disabled = True
|
||||
elif not self.name_input.text:
|
||||
MDSnackbar(MDSnackbarText(text="Name must not be empty."), y=dp(24), pos_hint={"center_x": 0.5},
|
||||
size_hint_x=0.5).open()
|
||||
self.show_result_snack("Name must not be empty.")
|
||||
elif not self.current_game:
|
||||
MDSnackbar(MDSnackbarText(text="You must select a game to play."), y=dp(24), pos_hint={"center_x": 0.5},
|
||||
size_hint_x=0.5).open()
|
||||
self.show_result_snack("You must select a game to play.")
|
||||
else:
|
||||
MDSnackbar(MDSnackbarText(text="Name cannot be longer than 16 characters."), y=dp(24),
|
||||
pos_hint={"center_x": 0.5}, size_hint_x=0.5).open()
|
||||
self.show_result_snack("Name cannot be longer than 16 characters.")
|
||||
|
||||
def create_range(self, option: typing.Type[Range], name: str):
|
||||
def create_range(self, option: typing.Type[Range], name: str, bind=True):
|
||||
def update_text(range_box: VisualRange):
|
||||
self.options[name] = int(range_box.slider.value)
|
||||
range_box.tag.text = str(int(range_box.slider.value))
|
||||
return
|
||||
|
||||
box = VisualRange(option=option, name=name)
|
||||
box.slider.bind(on_touch_move=lambda _, _1: update_text(box))
|
||||
if bind:
|
||||
box.slider.bind(value=lambda _, _1: update_text(box))
|
||||
self.options[name] = option.default
|
||||
return box
|
||||
|
||||
def create_named_range(self, option: typing.Type[NamedRange], name: str):
|
||||
def set_to_custom(range_box: VisualNamedRange):
|
||||
if (not self.options[name] == range_box.range.slider.value) \
|
||||
and (not self.options[name] in option.special_range_names or
|
||||
range_box.range.slider.value != option.special_range_names[self.options[name]]):
|
||||
# we should validate the touch here,
|
||||
# but this is much cheaper
|
||||
range_box.range.tag.text = str(int(range_box.range.slider.value))
|
||||
if range_box.range.slider.value in option.special_range_names.values():
|
||||
value = next(key for key, val in option.special_range_names.items()
|
||||
if val == range_box.range.slider.value)
|
||||
self.options[name] = value
|
||||
set_button_text(box.choice, value.title())
|
||||
else:
|
||||
self.options[name] = int(range_box.range.slider.value)
|
||||
range_box.range.tag.text = str(int(range_box.range.slider.value))
|
||||
set_button_text(range_box.choice, "Custom")
|
||||
|
||||
def set_button_text(button: MDButton, text: str):
|
||||
@@ -320,7 +348,7 @@ class OptionsCreator(ThemedApp):
|
||||
def set_value(text: str, range_box: VisualNamedRange):
|
||||
range_box.range.slider.value = min(max(option.special_range_names[text.lower()], option.range_start),
|
||||
option.range_end)
|
||||
range_box.range.tag.text = str(int(range_box.range.slider.value))
|
||||
range_box.range.tag.text = str(option.special_range_names[text.lower()])
|
||||
set_button_text(range_box.choice, text)
|
||||
self.options[name] = text.lower()
|
||||
range_box.range.slider.dropdown.dismiss()
|
||||
@@ -329,8 +357,18 @@ class OptionsCreator(ThemedApp):
|
||||
# for some reason this fixes an issue causing some to not open
|
||||
box.range.slider.dropdown.open()
|
||||
|
||||
box = VisualNamedRange(option=option, name=name, range_widget=self.create_range(option, name))
|
||||
box.range.slider.bind(on_touch_move=lambda _, _2: set_to_custom(box))
|
||||
box = VisualNamedRange(option=option, name=name, range_widget=self.create_range(option, name, bind=False))
|
||||
default: int | str = option.default
|
||||
if default in option.special_range_names:
|
||||
# value can get mismatched in this case
|
||||
box.range.slider.value = min(max(option.special_range_names[default], option.range_start),
|
||||
option.range_end)
|
||||
box.range.tag.text = str(int(box.range.slider.value))
|
||||
elif default in option.special_range_names.values():
|
||||
# better visual
|
||||
default = next(key for key, val in option.special_range_names.items() if val == option.default)
|
||||
set_button_text(box.choice, default.title())
|
||||
box.range.slider.bind(value=lambda _, _2: set_to_custom(box))
|
||||
items = [
|
||||
{
|
||||
"text": choice.title(),
|
||||
@@ -340,7 +378,7 @@ class OptionsCreator(ThemedApp):
|
||||
]
|
||||
box.range.slider.dropdown = MDDropdownMenu(caller=box.choice, items=items)
|
||||
box.choice.bind(on_release=open_dropdown)
|
||||
self.options[name] = option.default
|
||||
self.options[name] = default
|
||||
return box
|
||||
|
||||
def create_free_text(self, option: typing.Type[FreeText] | typing.Type[TextChoice], name: str):
|
||||
@@ -365,7 +403,7 @@ class OptionsCreator(ThemedApp):
|
||||
# for some reason this fixes an issue causing some to not open
|
||||
dropdown.open()
|
||||
|
||||
default_random = option.default == "random"
|
||||
default_string = isinstance(option.default, str)
|
||||
main_button = VisualChoice(option=option, name=name)
|
||||
main_button.bind(on_release=open_dropdown)
|
||||
|
||||
@@ -377,7 +415,7 @@ class OptionsCreator(ThemedApp):
|
||||
for choice in option.name_lookup
|
||||
]
|
||||
dropdown = MDDropdownMenu(caller=main_button, items=items)
|
||||
self.options[name] = option.name_lookup[option.default] if not default_random else option.default
|
||||
self.options[name] = option.name_lookup[option.default] if not default_string else option.default
|
||||
return main_button
|
||||
|
||||
def create_text_choice(self, option: typing.Type[TextChoice], name: str):
|
||||
@@ -416,8 +454,12 @@ class OptionsCreator(ThemedApp):
|
||||
valid_keys = sorted(option.valid_keys)
|
||||
if option.verify_item_name:
|
||||
valid_keys += list(world.item_name_to_id.keys())
|
||||
if option.convert_name_groups:
|
||||
valid_keys += list(world.item_name_groups.keys())
|
||||
if option.verify_location_name:
|
||||
valid_keys += list(world.location_name_to_id.keys())
|
||||
if option.convert_name_groups:
|
||||
valid_keys += list(world.location_name_groups.keys())
|
||||
|
||||
if not issubclass(option, OptionCounter):
|
||||
def apply_changes(button):
|
||||
@@ -439,14 +481,6 @@ class OptionsCreator(ThemedApp):
|
||||
dialog.scrollbox.layout.spacing = dp(5)
|
||||
dialog.scrollbox.layout.padding = [0, dp(5), 0, 0]
|
||||
|
||||
if name not in self.options:
|
||||
# convert from non-mutable to mutable
|
||||
# We use list syntax even for sets, set behavior is enforced through GUI
|
||||
if issubclass(option, OptionCounter):
|
||||
self.options[name] = deepcopy(option.default)
|
||||
else:
|
||||
self.options[name] = sorted(option.default)
|
||||
|
||||
if issubclass(option, OptionCounter):
|
||||
for value in sorted(self.options[name]):
|
||||
dialog.add_set_item(value, self.options[name].get(value, None))
|
||||
@@ -460,6 +494,15 @@ class OptionsCreator(ThemedApp):
|
||||
def create_option_set_list_counter(self, option: typing.Type[OptionList] | typing.Type[OptionSet] |
|
||||
typing.Type[OptionCounter], name: str, world: typing.Type[World]):
|
||||
main_button = MDButton(MDButtonText(text="Edit"), on_release=lambda x: self.create_popup(option, name, world))
|
||||
|
||||
if name not in self.options:
|
||||
# convert from non-mutable to mutable
|
||||
# We use list syntax even for sets, set behavior is enforced through GUI
|
||||
if issubclass(option, OptionCounter):
|
||||
self.options[name] = deepcopy(option.default)
|
||||
else:
|
||||
self.options[name] = sorted(option.default)
|
||||
|
||||
return main_button
|
||||
|
||||
def create_option(self, option: typing.Type[Option], name: str, world: typing.Type[World]) -> Widget:
|
||||
@@ -498,8 +541,10 @@ class OptionsCreator(ThemedApp):
|
||||
self.options[name] = "random-" + str(self.options[name])
|
||||
else:
|
||||
self.options[name] = self.options[name].replace("random-", "")
|
||||
if self.options[name].isnumeric() or self.options[name] in ("True", "False"):
|
||||
self.options[name] = eval(self.options[name])
|
||||
if self.options[name].isnumeric():
|
||||
self.options[name] = int(self.options[name])
|
||||
elif self.options[name] in ("True", "False"):
|
||||
self.options[name] = self.options[name] == "True"
|
||||
|
||||
base_object = instance.parent.parent
|
||||
label_object = instance.parent
|
||||
@@ -560,8 +605,11 @@ class OptionsCreator(ThemedApp):
|
||||
groups[group].append((name, option))
|
||||
|
||||
for group, options in groups.items():
|
||||
options = [(name, option) for name, option in options
|
||||
if name and option.visibility & Visibility.simple_ui]
|
||||
if not options:
|
||||
continue # Game Options can be empty if every other option is in another group
|
||||
# Can also have an option group of options that should not render on simple ui
|
||||
group_item = MDExpansionPanel(size_hint_y=None)
|
||||
group_header = MDExpansionPanelHeader(MDListItem(MDListItemSupportingText(text=group),
|
||||
TrailingPressedIconButton(icon="chevron-right",
|
||||
@@ -583,8 +631,7 @@ class OptionsCreator(ThemedApp):
|
||||
group_box.layout.orientation = "vertical"
|
||||
group_box.layout.spacing = dp(3)
|
||||
for name, option in options:
|
||||
if name and option is not Removed and option.visibility & Visibility.simple_ui:
|
||||
group_content.add_widget(self.create_option(option, name, cls))
|
||||
group_content.add_widget(self.create_option(option, name, cls))
|
||||
expansion_box.layout.add_widget(group_item)
|
||||
self.option_layout.add_widget(expansion_box)
|
||||
self.game_label.text = f"Game: {self.current_game}"
|
||||
@@ -619,7 +666,7 @@ class OptionsCreator(ThemedApp):
|
||||
self.create_options_panel(world_btn)
|
||||
|
||||
for world, cls in sorted(AutoWorldRegister.world_types.items(), key=lambda x: x[0]):
|
||||
if world == "Archipelago":
|
||||
if cls.hidden:
|
||||
continue
|
||||
world_text = MDButtonText(text=world, size_hint_y=None, width=dp(150),
|
||||
pos_hint={"x": 0.03, "center_y": 0.5})
|
||||
|
||||
@@ -83,6 +83,8 @@ Currently, the following games are supported:
|
||||
* Celeste (Open World)
|
||||
* Choo-Choo Charles
|
||||
* APQuest
|
||||
* Satisfactory
|
||||
* EarthBound
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import asyncio
|
||||
import typing
|
||||
import bsdiff4
|
||||
@@ -15,6 +16,9 @@ from CommonClient import CommonContext, server_loop, \
|
||||
gui_enabled, ClientCommandProcessor, logger, get_base_parser
|
||||
from Utils import async_start
|
||||
|
||||
# Heartbeat for position sharing via bounces, in seconds
|
||||
UNDERTALE_STATUS_INTERVAL = 30.0
|
||||
UNDERTALE_ONLINE_TIMEOUT = 60.0
|
||||
|
||||
class UndertaleCommandProcessor(ClientCommandProcessor):
|
||||
def __init__(self, ctx):
|
||||
@@ -109,6 +113,11 @@ class UndertaleContext(CommonContext):
|
||||
self.completed_routes = {"pacifist": 0, "genocide": 0, "neutral": 0}
|
||||
# self.save_game_folder: files go in this path to pass data between us and the actual game
|
||||
self.save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE")
|
||||
self.last_sent_position: typing.Optional[tuple] = None
|
||||
self.last_room: typing.Optional[str] = None
|
||||
self.last_status_write: float = 0.0
|
||||
self.other_undertale_status: dict[int, dict] = {}
|
||||
|
||||
|
||||
def patch_game(self):
|
||||
with open(Utils.user_path("Undertale", "data.win"), "rb") as f:
|
||||
@@ -219,6 +228,9 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
||||
await ctx.send_msgs([{"cmd": "SetNotify", "keys": [str(ctx.slot)+" RoutesDone neutral",
|
||||
str(ctx.slot)+" RoutesDone pacifist",
|
||||
str(ctx.slot)+" RoutesDone genocide"]}])
|
||||
if any(info.game == "Undertale" and slot != ctx.slot
|
||||
for slot, info in ctx.slot_info.items()):
|
||||
ctx.set_notify("undertale_room_status")
|
||||
if args["slot_data"]["only_flakes"]:
|
||||
with open(os.path.join(ctx.save_game_folder, "GenoNoChest.flag"), "w") as f:
|
||||
f.close()
|
||||
@@ -263,6 +275,12 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
||||
if str(ctx.slot)+" RoutesDone pacifist" in args["keys"]:
|
||||
if args["keys"][str(ctx.slot) + " RoutesDone pacifist"] is not None:
|
||||
ctx.completed_routes["pacifist"] = args["keys"][str(ctx.slot)+" RoutesDone pacifist"]
|
||||
if "undertale_room_status" in args["keys"] and args["keys"]["undertale_room_status"]:
|
||||
status = args["keys"]["undertale_room_status"]
|
||||
ctx.other_undertale_status = {
|
||||
int(key): val for key, val in status.items()
|
||||
if int(key) != ctx.slot
|
||||
}
|
||||
elif cmd == "SetReply":
|
||||
if args["value"] is not None:
|
||||
if str(ctx.slot)+" RoutesDone pacifist" == args["key"]:
|
||||
@@ -271,17 +289,19 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
||||
ctx.completed_routes["genocide"] = args["value"]
|
||||
elif str(ctx.slot)+" RoutesDone neutral" == args["key"]:
|
||||
ctx.completed_routes["neutral"] = args["value"]
|
||||
if args.get("key") == "undertale_room_status" and args.get("value"):
|
||||
ctx.other_undertale_status = {
|
||||
int(key): val for key, val in args["value"].items()
|
||||
if int(key) != ctx.slot
|
||||
}
|
||||
elif cmd == "ReceivedItems":
|
||||
start_index = args["index"]
|
||||
|
||||
if start_index == 0:
|
||||
ctx.items_received = []
|
||||
elif start_index != len(ctx.items_received):
|
||||
sync_msg = [{"cmd": "Sync"}]
|
||||
if ctx.locations_checked:
|
||||
sync_msg.append({"cmd": "LocationChecks",
|
||||
"locations": list(ctx.locations_checked)})
|
||||
await ctx.send_msgs(sync_msg)
|
||||
await ctx.check_locations(ctx.locations_checked)
|
||||
await ctx.send_msgs([{"cmd": "Sync"}])
|
||||
if start_index == len(ctx.items_received):
|
||||
counter = -1
|
||||
placedWeapon = 0
|
||||
@@ -368,9 +388,8 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
||||
f.close()
|
||||
|
||||
elif cmd == "Bounced":
|
||||
tags = args.get("tags", [])
|
||||
if "Online" in tags:
|
||||
data = args.get("data", {})
|
||||
data = args.get("data", {})
|
||||
if "x" in data and "room" in data:
|
||||
if data["player"] != ctx.slot and data["player"] is not None:
|
||||
filename = f"FRISK" + str(data["player"]) + ".playerspot"
|
||||
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
||||
@@ -381,21 +400,63 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
||||
|
||||
async def multi_watcher(ctx: UndertaleContext):
|
||||
while not ctx.exit_event.is_set():
|
||||
path = ctx.save_game_folder
|
||||
for root, dirs, files in os.walk(path):
|
||||
for file in files:
|
||||
if "spots.mine" in file and "Online" in ctx.tags:
|
||||
with open(os.path.join(root, file), "r") as mine:
|
||||
this_x = mine.readline()
|
||||
this_y = mine.readline()
|
||||
this_room = mine.readline()
|
||||
this_sprite = mine.readline()
|
||||
this_frame = mine.readline()
|
||||
mine.close()
|
||||
message = [{"cmd": "Bounce", "tags": ["Online"],
|
||||
"data": {"player": ctx.slot, "x": this_x, "y": this_y, "room": this_room,
|
||||
"spr": this_sprite, "frm": this_frame}}]
|
||||
await ctx.send_msgs(message)
|
||||
if "Online" in ctx.tags and any(
|
||||
info.game == "Undertale" and slot != ctx.slot
|
||||
for slot, info in ctx.slot_info.items()):
|
||||
now = time.time()
|
||||
path = ctx.save_game_folder
|
||||
for root, dirs, files in os.walk(path):
|
||||
for file in files:
|
||||
if "spots.mine" in file:
|
||||
with open(os.path.join(root, file), "r") as mine:
|
||||
this_x = mine.readline()
|
||||
this_y = mine.readline()
|
||||
this_room = mine.readline()
|
||||
this_sprite = mine.readline()
|
||||
this_frame = mine.readline()
|
||||
|
||||
if this_room != ctx.last_room or \
|
||||
now - ctx.last_status_write >= UNDERTALE_STATUS_INTERVAL:
|
||||
ctx.last_room = this_room
|
||||
ctx.last_status_write = now
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Set",
|
||||
"key": "undertale_room_status",
|
||||
"default": {},
|
||||
"want_reply": False,
|
||||
"operations": [{"operation": "update",
|
||||
"value": {str(ctx.slot): {"room": this_room,
|
||||
"time": now}}}]
|
||||
}])
|
||||
|
||||
# If player was visible but timed out (heartbeat) or left the room, remove them.
|
||||
for slot, entry in ctx.other_undertale_status.items():
|
||||
if entry.get("room") != this_room or \
|
||||
now - entry.get("time", now) > UNDERTALE_ONLINE_TIMEOUT:
|
||||
playerspot = os.path.join(ctx.save_game_folder,
|
||||
f"FRISK{slot}.playerspot")
|
||||
if os.path.exists(playerspot):
|
||||
os.remove(playerspot)
|
||||
|
||||
current_position = (this_x, this_y, this_room, this_sprite, this_frame)
|
||||
if current_position == ctx.last_sent_position:
|
||||
continue
|
||||
|
||||
# Empty status dict = no data yet → send to bootstrap.
|
||||
online_in_room = any(
|
||||
entry.get("room") == this_room and
|
||||
now - entry.get("time", now) <= UNDERTALE_ONLINE_TIMEOUT
|
||||
for entry in ctx.other_undertale_status.values()
|
||||
)
|
||||
if ctx.other_undertale_status and not online_in_room:
|
||||
continue
|
||||
|
||||
message = [{"cmd": "Bounce", "games": ["Undertale"],
|
||||
"data": {"player": ctx.slot, "x": this_x, "y": this_y,
|
||||
"room": this_room, "spr": this_sprite,
|
||||
"frm": this_frame}}]
|
||||
await ctx.send_msgs(message)
|
||||
ctx.last_sent_position = current_position
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
@@ -409,10 +470,9 @@ async def game_watcher(ctx: UndertaleContext):
|
||||
for file in files:
|
||||
if ".item" in file:
|
||||
os.remove(os.path.join(root, file))
|
||||
sync_msg = [{"cmd": "Sync"}]
|
||||
if ctx.locations_checked:
|
||||
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
|
||||
await ctx.send_msgs(sync_msg)
|
||||
await ctx.check_locations(ctx.locations_checked)
|
||||
await ctx.send_msgs([{"cmd": "Sync"}])
|
||||
|
||||
ctx.syncing = False
|
||||
if ctx.got_deathlink:
|
||||
ctx.got_deathlink = False
|
||||
@@ -447,7 +507,7 @@ async def game_watcher(ctx: UndertaleContext):
|
||||
for l in lines:
|
||||
sending = sending+[(int(l.rstrip('\n')))+12000]
|
||||
finally:
|
||||
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": sending}])
|
||||
await ctx.check_locations(sending)
|
||||
if "victory" in file and str(ctx.route) in file:
|
||||
victory = True
|
||||
if ".playerspot" in file and "Online" not in ctx.tags:
|
||||
|
||||
178
Utils.py
178
Utils.py
@@ -22,6 +22,8 @@ from settings import Settings, get_settings
|
||||
from time import sleep
|
||||
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard
|
||||
from yaml import load, load_all, dump
|
||||
from pathspec import PathSpec, GitIgnoreSpec
|
||||
from typing_extensions import deprecated
|
||||
|
||||
try:
|
||||
from yaml import CLoader as UnsafeLoader, CSafeLoader as SafeLoader, CDumper as Dumper
|
||||
@@ -48,7 +50,7 @@ class Version(typing.NamedTuple):
|
||||
return ".".join(str(item) for item in self)
|
||||
|
||||
|
||||
__version__ = "0.6.5"
|
||||
__version__ = "0.6.7"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
@@ -314,6 +316,7 @@ def get_public_ipv6() -> str:
|
||||
return ip
|
||||
|
||||
|
||||
@deprecated("Utils.get_options() is deprecated. Use the settings API instead.")
|
||||
def get_options() -> Settings:
|
||||
deprecate("Utils.get_options() is deprecated. Use the settings API instead.")
|
||||
return get_settings()
|
||||
@@ -387,6 +390,14 @@ def store_data_package_for_checksum(game: str, data: typing.Dict[str, Any]) -> N
|
||||
logging.debug(f"Could not store data package: {e}")
|
||||
|
||||
|
||||
def read_apignore(filename: str | pathlib.Path) -> PathSpec | None:
|
||||
try:
|
||||
with open(filename) as ignore_file:
|
||||
return GitIgnoreSpec.from_lines(ignore_file)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
def get_default_adjuster_settings(game_name: str) -> Namespace:
|
||||
import LttPAdjuster
|
||||
adjuster_settings = Namespace()
|
||||
@@ -802,29 +813,32 @@ def open_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typin
|
||||
except tkinter.TclError:
|
||||
return None # GUI not available. None is the same as a user clicking "cancel"
|
||||
root.withdraw()
|
||||
return tkinter.filedialog.askopenfilename(title=title, filetypes=((t[0], ' '.join(t[1])) for t in filetypes),
|
||||
initialfile=suggest or None)
|
||||
try:
|
||||
return tkinter.filedialog.askopenfilename(
|
||||
title=title,
|
||||
filetypes=((t[0], ' '.join(t[1])) for t in filetypes),
|
||||
initialfile=suggest or None,
|
||||
)
|
||||
finally:
|
||||
root.destroy()
|
||||
|
||||
|
||||
def save_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typing.Iterable[str]]], suggest: str = "") \
|
||||
-> typing.Optional[str]:
|
||||
logging.info(f"Opening file save dialog for {title}.")
|
||||
|
||||
def run(*args: str):
|
||||
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
|
||||
|
||||
if is_linux:
|
||||
# prefer native dialog
|
||||
from shutil import which
|
||||
kdialog = which("kdialog")
|
||||
if kdialog:
|
||||
k_filters = '|'.join((f'{text} (*{" *".join(ext)})' for (text, ext) in filetypes))
|
||||
return run(kdialog, f"--title={title}", "--getsavefilename", suggest or ".", k_filters)
|
||||
return _run_for_stdout(kdialog, f"--title={title}", "--getsavefilename", suggest or ".", k_filters)
|
||||
zenity = which("zenity")
|
||||
if zenity:
|
||||
z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes)
|
||||
selection = (f"--filename={suggest}",) if suggest else ()
|
||||
return run(zenity, f"--title={title}", "--file-selection", "--save", *z_filters, *selection)
|
||||
return _run_for_stdout(zenity, f"--title={title}", "--file-selection", "--save", *z_filters, *selection)
|
||||
|
||||
# fall back to tk
|
||||
try:
|
||||
@@ -847,8 +861,14 @@ def save_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typin
|
||||
except tkinter.TclError:
|
||||
return None # GUI not available. None is the same as a user clicking "cancel"
|
||||
root.withdraw()
|
||||
return tkinter.filedialog.asksaveasfilename(title=title, filetypes=((t[0], ' '.join(t[1])) for t in filetypes),
|
||||
initialfile=suggest or None)
|
||||
try:
|
||||
return tkinter.filedialog.asksaveasfilename(
|
||||
title=title,
|
||||
filetypes=((t[0], ' '.join(t[1])) for t in filetypes),
|
||||
initialfile=suggest or None,
|
||||
)
|
||||
finally:
|
||||
root.destroy()
|
||||
|
||||
|
||||
def _mp_open_directory(res: "multiprocessing.Queue[typing.Optional[str]]", *args: Any) -> None:
|
||||
@@ -896,6 +916,13 @@ def open_directory(title: str, suggest: str = "") -> typing.Optional[str]:
|
||||
|
||||
|
||||
def messagebox(title: str, text: str, error: bool = False) -> None:
|
||||
if not gui_enabled:
|
||||
if error:
|
||||
logging.error(f"{title}: {text}")
|
||||
else:
|
||||
logging.info(f"{title}: {text}")
|
||||
return
|
||||
|
||||
if is_kivy_running():
|
||||
from kvui import MessageBox
|
||||
MessageBox(title, text, error).open()
|
||||
@@ -931,6 +958,9 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
|
||||
root.update()
|
||||
|
||||
|
||||
gui_enabled = not sys.stdout or "--nogui" not in sys.argv
|
||||
"""Checks if the user wanted no GUI mode and has a terminal to use it with."""
|
||||
|
||||
def title_sorted(data: typing.Iterable, key=None, ignore: typing.AbstractSet[str] = frozenset(("a", "the"))):
|
||||
"""Sorts a sequence of text ignoring typical articles like "a" or "the" in the beginning."""
|
||||
def sorter(element: Union[str, Dict[str, Any]]) -> str:
|
||||
@@ -975,6 +1005,7 @@ def async_start(co: Coroutine[None, None, typing.Any], name: Optional[str] = Non
|
||||
|
||||
|
||||
def deprecate(message: str, add_stacklevels: int = 0):
|
||||
"""also use typing_extensions.deprecated wherever you use this"""
|
||||
if __debug__:
|
||||
raise Exception(message)
|
||||
warnings.warn(message, stacklevel=2 + add_stacklevels)
|
||||
@@ -1039,6 +1070,7 @@ def _extend_freeze_support() -> None:
|
||||
multiprocessing.freeze_support = multiprocessing.spawn.freeze_support = _freeze_support if is_frozen() else _noop
|
||||
|
||||
|
||||
@deprecated("Use multiprocessing.freeze_support() instead")
|
||||
def freeze_support() -> None:
|
||||
"""This now only calls multiprocessing.freeze_support since we are patching freeze_support on module load."""
|
||||
import multiprocessing
|
||||
@@ -1050,9 +1082,18 @@ def freeze_support() -> None:
|
||||
_extend_freeze_support()
|
||||
|
||||
|
||||
def visualize_regions(root_region: Region, file_name: str, *,
|
||||
show_entrance_names: bool = False, show_locations: bool = True, show_other_regions: bool = True,
|
||||
linetype_ortho: bool = True, regions_to_highlight: set[Region] | None = None) -> None:
|
||||
def visualize_regions(
|
||||
root_region: Region,
|
||||
file_name: str,
|
||||
*,
|
||||
show_entrance_names: bool = False,
|
||||
show_locations: bool = True,
|
||||
show_other_regions: bool = True,
|
||||
linetype_ortho: bool = True,
|
||||
regions_to_highlight: set[Region] | None = None,
|
||||
entrance_highlighting: dict[int, int] | None = None,
|
||||
detail_other_regions: bool = False,
|
||||
auto_assign_colors: bool = False) -> None:
|
||||
"""Visualize the layout of a world as a PlantUML diagram.
|
||||
|
||||
:param root_region: The region from which to start the diagram from. (Usually the "Menu" region of your world.)
|
||||
@@ -1069,6 +1110,13 @@ def visualize_regions(root_region: Region, file_name: str, *,
|
||||
:param show_other_regions: (default True) If enabled, regions that can't be reached by traversing exits are shown.
|
||||
:param linetype_ortho: (default True) If enabled, orthogonal straight line parts will be used; otherwise polylines.
|
||||
:param regions_to_highlight: Regions that will be highlighted in green if they are reachable.
|
||||
:param entrance_highlighting: a mapping from your world's entrance randomization groups to RGB values, used to color
|
||||
your entrances
|
||||
:param detail_other_regions: (default False) If enabled, will fully visualize regions that aren't reachable
|
||||
from root_region.
|
||||
:param auto_assign_colors: (default False) If enabled, will automatically assign random colors to entrances of the
|
||||
same randomization group. Uses entrance_highlighting first, and only picks random colors for entrance groups
|
||||
not found in the passed-in map
|
||||
|
||||
Example usage in World code:
|
||||
from Utils import visualize_regions
|
||||
@@ -1094,6 +1142,34 @@ def visualize_regions(root_region: Region, file_name: str, *,
|
||||
regions: typing.Deque[Region] = deque((root_region,))
|
||||
multiworld: MultiWorld = root_region.multiworld
|
||||
|
||||
colors_used: set[int] = set()
|
||||
if entrance_highlighting:
|
||||
for color in entrance_highlighting.values():
|
||||
# filter the colors to their most-significant bits to avoid too similar colors
|
||||
colors_used.add(color & 0xF0F0F0)
|
||||
else:
|
||||
# assign an empty dict to not crash later
|
||||
# the parameter is optional for ease of use when you don't care about colors
|
||||
entrance_highlighting = {}
|
||||
|
||||
def select_color(group: int) -> int:
|
||||
# specifically spacing color indexes by three different prime numbers (3, 5, 7) for the RGB components to avoid
|
||||
# obvious cyclical color patterns
|
||||
COLOR_INDEX_SPACING: int = 0x357
|
||||
new_color_index: int = (group * COLOR_INDEX_SPACING) % 0x1000
|
||||
new_color = ((new_color_index & 0xF00) << 12) + \
|
||||
((new_color_index & 0xF0) << 8) + \
|
||||
((new_color_index & 0xF) << 4)
|
||||
while new_color in colors_used:
|
||||
# while this is technically unbounded, expected collisions are low. There are 4095 possible colors
|
||||
# and worlds are unlikely to get to anywhere close to that many entrance groups
|
||||
# intentionally not using multiworld.random to not affect output when debugging with this tool
|
||||
new_color_index += COLOR_INDEX_SPACING
|
||||
new_color = ((new_color_index & 0xF00) << 12) + \
|
||||
((new_color_index & 0xF0) << 8) + \
|
||||
((new_color_index & 0xF) << 4)
|
||||
return new_color
|
||||
|
||||
def fmt(obj: Union[Entrance, Item, Location, Region]) -> str:
|
||||
name = obj.name
|
||||
if isinstance(obj, Item):
|
||||
@@ -1113,18 +1189,28 @@ def visualize_regions(root_region: Region, file_name: str, *,
|
||||
|
||||
def visualize_exits(region: Region) -> None:
|
||||
for exit_ in region.exits:
|
||||
color_code: str = ""
|
||||
if exit_.randomization_group in entrance_highlighting:
|
||||
color_code = f" #{entrance_highlighting[exit_.randomization_group]:0>6X}"
|
||||
if exit_.connected_region:
|
||||
if show_entrance_names:
|
||||
uml.append(f"\"{fmt(region)}\" --> \"{fmt(exit_.connected_region)}\" : \"{fmt(exit_)}\"")
|
||||
uml.append(f"\"{fmt(region)}\" --> \"{fmt(exit_.connected_region)}\" : \"{fmt(exit_)}\"{color_code}")
|
||||
else:
|
||||
try:
|
||||
uml.remove(f"\"{fmt(exit_.connected_region)}\" --> \"{fmt(region)}\"")
|
||||
uml.append(f"\"{fmt(exit_.connected_region)}\" <--> \"{fmt(region)}\"")
|
||||
uml.remove(f"\"{fmt(exit_.connected_region)}\" --> \"{fmt(region)}\"{color_code}")
|
||||
uml.append(f"\"{fmt(exit_.connected_region)}\" <--> \"{fmt(region)}\"{color_code}")
|
||||
except ValueError:
|
||||
uml.append(f"\"{fmt(region)}\" --> \"{fmt(exit_.connected_region)}\"")
|
||||
uml.append(f"\"{fmt(region)}\" --> \"{fmt(exit_.connected_region)}\"{color_code}")
|
||||
else:
|
||||
uml.append(f"circle \"unconnected exit:\\n{fmt(exit_)}\"")
|
||||
uml.append(f"\"{fmt(region)}\" --> \"unconnected exit:\\n{fmt(exit_)}\"")
|
||||
uml.append(f"circle \"unconnected exit:\\n{fmt(exit_)}\" {color_code}")
|
||||
uml.append(f"\"{fmt(region)}\" --> \"unconnected exit:\\n{fmt(exit_)}\"{color_code}")
|
||||
for entrance in region.entrances:
|
||||
color_code: str = ""
|
||||
if entrance.randomization_group in entrance_highlighting:
|
||||
color_code = f" #{entrance_highlighting[entrance.randomization_group]:0>6X}"
|
||||
if not entrance.parent_region:
|
||||
uml.append(f"circle \"unconnected entrance:\\n{fmt(entrance)}\"{color_code}")
|
||||
uml.append(f"\"unconnected entrance:\\n{fmt(entrance)}\" --> \"{fmt(region)}\"{color_code}")
|
||||
|
||||
def visualize_locations(region: Region) -> None:
|
||||
any_lock = any(location.locked for location in region.locations)
|
||||
@@ -1145,9 +1231,27 @@ def visualize_regions(root_region: Region, file_name: str, *,
|
||||
if other_regions := [region for region in multiworld.get_regions(root_region.player) if region not in seen]:
|
||||
uml.append("package \"other regions\" <<Cloud>> {")
|
||||
for region in other_regions:
|
||||
uml.append(f"class \"{fmt(region)}\"")
|
||||
if detail_other_regions:
|
||||
visualize_region(region)
|
||||
else:
|
||||
uml.append(f"class \"{fmt(region)}\"")
|
||||
uml.append("}")
|
||||
|
||||
if auto_assign_colors:
|
||||
all_entrances: list[Entrance] = []
|
||||
for region in multiworld.get_regions(root_region.player):
|
||||
all_entrances.extend(region.entrances)
|
||||
all_entrances.extend(region.exits)
|
||||
all_groups: list[int] = sorted(set([entrance.randomization_group for entrance in all_entrances]))
|
||||
for group in all_groups:
|
||||
if group not in entrance_highlighting:
|
||||
if len(colors_used) >= 0x1000:
|
||||
# on the off chance someone makes 4096 different entrance groups, don't cycle forever
|
||||
break
|
||||
new_color: int = select_color(group)
|
||||
entrance_highlighting[group] = new_color
|
||||
colors_used.add(new_color)
|
||||
|
||||
uml.append("@startuml")
|
||||
uml.append("hide circle")
|
||||
uml.append("hide empty members")
|
||||
@@ -1158,7 +1262,7 @@ def visualize_regions(root_region: Region, file_name: str, *,
|
||||
seen.add(current_region)
|
||||
visualize_region(current_region)
|
||||
regions.extend(exit_.connected_region for exit_ in current_region.exits if exit_.connected_region)
|
||||
if show_other_regions:
|
||||
if show_other_regions or detail_other_regions:
|
||||
visualize_other_regions()
|
||||
uml.append("@enduml")
|
||||
|
||||
@@ -1222,3 +1326,35 @@ class DaemonThreadPoolExecutor(concurrent.futures.ThreadPoolExecutor):
|
||||
t.start()
|
||||
self._threads.add(t)
|
||||
# NOTE: don't add to _threads_queues so we don't block on shutdown
|
||||
|
||||
|
||||
def get_full_typename(t: type) -> str:
|
||||
"""Returns the full qualified name of a type, including its module (if not builtins)."""
|
||||
module = t.__module__
|
||||
if module and module != "builtins":
|
||||
return f"{module}.{t.__qualname__}"
|
||||
return t.__qualname__
|
||||
|
||||
|
||||
def get_all_causes(ex: Exception) -> str:
|
||||
"""Return a string describing the recursive causes of this exception.
|
||||
|
||||
:param ex: The exception to be described.
|
||||
:return A multiline string starting with the initial exception on the first line and each resulting exception
|
||||
on subsequent lines with progressive indentation.
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
Exception: Invalid value 'bad'.
|
||||
Which caused: Options.OptionError: Error generating option
|
||||
Which caused: ValueError: File bad.yaml is invalid.
|
||||
```
|
||||
"""
|
||||
cause = ex
|
||||
causes = [f"{get_full_typename(type(ex))}: {ex}"]
|
||||
while cause := cause.__cause__:
|
||||
causes.append(f"{get_full_typename(type(cause))}: {cause}")
|
||||
top = causes[-1]
|
||||
others = "".join(f"\n{' ' * (i + 1)}Which caused: {c}" for i, c in enumerate(reversed(causes[:-1])))
|
||||
return f"{top}{others}"
|
||||
|
||||
@@ -20,7 +20,8 @@ if typing.TYPE_CHECKING:
|
||||
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
|
||||
if not os.path.exists(configpath):
|
||||
# fall back to config.yaml in user_path if config does not exist in cwd to match settings.py
|
||||
configpath = os.path.abspath(Utils.user_path('config.yaml'))
|
||||
|
||||
|
||||
|
||||
@@ -1,46 +1,20 @@
|
||||
# WebHost
|
||||
|
||||
## Asset License
|
||||
|
||||
The image files used in the page design were specifically designed for archipelago.gg and are **not** covered by the top
|
||||
level LICENSE.
|
||||
See individual LICENSE files in `./static/static/**`.
|
||||
|
||||
You are only allowed to use them for personal use, testing and development.
|
||||
If the site is reachable over the internet, have a robots.txt in place (see `ASSET_RIGHTS` in `config.yaml`)
|
||||
and do not promote it publicly. Alternatively replace or remove the assets.
|
||||
|
||||
## Contribution Guidelines
|
||||
**Thank you for your interest in contributing to the Archipelago website!**
|
||||
Much of the content on the website is generated automatically, but there are some things
|
||||
that need a personal touch. For those things, we rely on contributions from both the core
|
||||
team and the community. The current primary maintainer of the website is Farrak Kilhn.
|
||||
He may be found on Discord as `Farrak Kilhn#0418`, or on GitHub as `LegendaryLinux`.
|
||||
|
||||
### Small Changes
|
||||
Little changes like adding a button or a couple new select elements are perfectly fine.
|
||||
Tweaks to style specific to a PR's content are also probably not a problem. For example, if
|
||||
you build a new page which needs two side by side tables, and you need to write a CSS file
|
||||
specific to your page, that is perfectly reasonable.
|
||||
Pages should preferably be rendered on the server side with Jinja. Features should work with noscript if feasible.
|
||||
Design changes have to fit the overall design.
|
||||
|
||||
### Content Additions
|
||||
Once you develop a new feature or add new content the website, make a pull request. It will
|
||||
be reviewed by the community and there will probably be some discussion around it. Depending
|
||||
on the size of the feature, and if new styles are required, there may be an additional step
|
||||
before the PR is accepted wherein Farrak works with the designer to implement styles.
|
||||
Introduction of JS dependencies should first be discussed on Discord or in a draft PR.
|
||||
|
||||
### Restrictions on Style Changes
|
||||
A professional designer is paid to develop the styles and assets for the Archipelago website.
|
||||
In an effort to maintain a consistent look and feel, pull requests which *exclusively*
|
||||
change site styles are rejected. Please note this applies to code which changes the overall
|
||||
look and feel of the site, not to small tweaks to CSS for your custom page. The intention
|
||||
behind these restrictions is to maintain a curated feel for the design of the site. If
|
||||
any PR affects the overall feel of the site but includes additive changes, there will
|
||||
likely be a conversation about how to implement those changes without compromising the
|
||||
curated site style. It is therefore worth noting there are a couple files which, if
|
||||
changed in your pull request, will cause it to draw additional scrutiny.
|
||||
|
||||
These closely guarded files are:
|
||||
- `globalStyles.css`
|
||||
- `islandFooter.css`
|
||||
- `landing.css`
|
||||
- `markdown.css`
|
||||
- `tooltip.css`
|
||||
|
||||
### Site Themes
|
||||
There are several themes available for game pages. It is possible to request a new theme in
|
||||
the `#art-and-design` channel on Discord. Because themes are created by the designer, they
|
||||
are not free, and take some time to create. Farrak works closely with the designer to implement
|
||||
these themes, and pays for the assets out of pocket. Therefore, only a couple themes per year
|
||||
are added. If a proposed theme seems like a cool idea and the community likes it, there is a
|
||||
good chance it will become a reality.
|
||||
See also [docs/style.md](/docs/style.md) for the style guide.
|
||||
|
||||
@@ -23,6 +23,17 @@ app.jinja_env.filters['any'] = any
|
||||
app.jinja_env.filters['all'] = all
|
||||
app.jinja_env.filters['get_file_safe_name'] = get_file_safe_name
|
||||
|
||||
# overwrites of flask default config
|
||||
app.config["DEBUG"] = False
|
||||
app.config["PORT"] = 80
|
||||
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
|
||||
app.config["MAX_CONTENT_LENGTH"] = 64 * 1024 * 1024 # 64 megabyte limit
|
||||
# if you want to deploy, make sure you have a non-guessable secret key
|
||||
app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
|
||||
app.config["SESSION_PERMANENT"] = True
|
||||
app.config["MAX_FORM_MEMORY_SIZE"] = 2 * 1024 * 1024 # 2 MB, needed for large option pages such as SC2
|
||||
|
||||
# custom config
|
||||
app.config["SELFHOST"] = True # application process is in charge of running the websites
|
||||
app.config["GENERATORS"] = 8 # maximum concurrent world gens
|
||||
app.config["HOSTERS"] = 8 # maximum concurrent room hosters
|
||||
@@ -30,19 +41,12 @@ app.config["SELFLAUNCH"] = True # application process is in charge of launching
|
||||
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
|
||||
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections
|
||||
app.config["SELFGEN"] = True # application process is in charge of scheduling Generations.
|
||||
app.config["DEBUG"] = False
|
||||
app.config["PORT"] = 80
|
||||
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
||||
app.config['MAX_CONTENT_LENGTH'] = 64 * 1024 * 1024 # 64 megabyte limit
|
||||
# if you want to deploy, make sure you have a non-guessable secret key
|
||||
app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
|
||||
# at what amount of worlds should scheduling be used, instead of rolling in the web-thread
|
||||
app.config["JOB_THRESHOLD"] = 1
|
||||
# after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable.
|
||||
app.config["JOB_TIME"] = 600
|
||||
# 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
|
||||
# archipelago.gg uses gunicorn + nginx; ignoring this option
|
||||
|
||||
@@ -2,10 +2,20 @@
|
||||
from typing import List, Tuple
|
||||
|
||||
from flask import Blueprint
|
||||
from flask_cors import CORS
|
||||
|
||||
from ..models import Seed, Slot
|
||||
|
||||
api_endpoints = Blueprint('api', __name__, url_prefix="/api")
|
||||
cors = CORS(api_endpoints, resources={
|
||||
r"/api/datapackage/*": {"origins": "*"},
|
||||
r"/api/datapackage": {"origins": "*"},
|
||||
r"/api/datapackage_checksum/*": {"origins": "*"},
|
||||
r"/api/room_status/*": {"origins": "*"},
|
||||
r"/api/tracker/*": {"origins": "*"},
|
||||
r"/api/static_tracker/*": {"origins": "*"},
|
||||
r"/api/slot_data_tracker/*": {"origins": "*"}
|
||||
})
|
||||
|
||||
|
||||
def get_players(seed: Seed) -> List[Tuple[str, str]]:
|
||||
|
||||
@@ -58,6 +58,12 @@ class PlayerLocationsTotal(TypedDict):
|
||||
total_locations: int
|
||||
|
||||
|
||||
class PlayerGame(TypedDict):
|
||||
team: int
|
||||
player: int
|
||||
game: str
|
||||
|
||||
|
||||
@api_endpoints.route("/tracker/<suuid:tracker>")
|
||||
@cache.memoize(timeout=60)
|
||||
def tracker_data(tracker: UUID) -> dict[str, Any]:
|
||||
@@ -80,7 +86,8 @@ def tracker_data(tracker: UUID) -> dict[str, Any]:
|
||||
"""Slot aliases of all players."""
|
||||
for team, players in all_players.items():
|
||||
for player in players:
|
||||
player_aliases.append({"team": team, "player": player, "alias": tracker_data.get_player_alias(team, player)})
|
||||
player_aliases.append(
|
||||
{"team": team, "player": player, "alias": tracker_data.get_player_alias(team, player)})
|
||||
|
||||
player_items_received: list[PlayerItemsReceived] = []
|
||||
"""Items received by each player."""
|
||||
@@ -94,7 +101,8 @@ def tracker_data(tracker: UUID) -> dict[str, Any]:
|
||||
for team, players in all_players.items():
|
||||
for player in players:
|
||||
player_checks_done.append(
|
||||
{"team": team, "player": player, "locations": sorted(tracker_data.get_player_checked_locations(team, player))})
|
||||
{"team": team, "player": player,
|
||||
"locations": sorted(tracker_data.get_player_checked_locations(team, player))})
|
||||
|
||||
total_checks_done: list[TeamTotalChecks] = [
|
||||
{"team": team, "checks_done": checks_done}
|
||||
@@ -144,7 +152,8 @@ def tracker_data(tracker: UUID) -> dict[str, Any]:
|
||||
"""The current client status for each player."""
|
||||
for team, players in all_players.items():
|
||||
for player in players:
|
||||
player_status.append({"team": team, "player": player, "status": tracker_data.get_player_client_status(team, player)})
|
||||
player_status.append(
|
||||
{"team": team, "player": player, "status": tracker_data.get_player_client_status(team, player)})
|
||||
|
||||
return {
|
||||
"aliases": player_aliases,
|
||||
@@ -207,12 +216,20 @@ def static_tracker_data(tracker: UUID) -> dict[str, Any]:
|
||||
player_locations_total.append(
|
||||
{"team": team, "player": player, "total_locations": len(tracker_data.get_player_locations(player))})
|
||||
|
||||
player_game: list[PlayerGame] = []
|
||||
"""The played game per player slot."""
|
||||
for team, players in all_players.items():
|
||||
for player in players:
|
||||
player_game.append({"team": team, "player": player, "game": tracker_data.get_player_game(player)})
|
||||
|
||||
return {
|
||||
"groups": groups,
|
||||
"datapackage": tracker_data._multidata["datapackage"],
|
||||
"player_locations_total": player_locations_total,
|
||||
"player_game": player_game,
|
||||
}
|
||||
|
||||
|
||||
# It should be exceedingly rare that slot data is needed, so it's separated out.
|
||||
@api_endpoints.route("/slot_data_tracker/<suuid:tracker>")
|
||||
@cache.memoize(timeout=300)
|
||||
|
||||
@@ -89,19 +89,24 @@ class WebHostContext(Context):
|
||||
setattr(self, key, value)
|
||||
self.non_hintable_names = collections.defaultdict(frozenset, self.non_hintable_names)
|
||||
|
||||
def listen_to_db_commands(self):
|
||||
async def listen_to_db_commands(self):
|
||||
cmdprocessor = DBCommandProcessor(self)
|
||||
|
||||
while not self.exit_event.is_set():
|
||||
with db_session:
|
||||
commands = select(command for command in Command if command.room.id == self.room_id)
|
||||
if commands:
|
||||
for command in commands:
|
||||
self.main_loop.call_soon_threadsafe(cmdprocessor, command.commandtext)
|
||||
command.delete()
|
||||
commit()
|
||||
del commands
|
||||
time.sleep(5)
|
||||
await self.main_loop.run_in_executor(None, self._process_db_commands, cmdprocessor)
|
||||
try:
|
||||
await asyncio.wait_for(self.exit_event.wait(), 5)
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
|
||||
def _process_db_commands(self, cmdprocessor):
|
||||
with db_session:
|
||||
commands = select(command for command in Command if command.room.id == self.room_id)
|
||||
if commands:
|
||||
for command in commands:
|
||||
self.main_loop.call_soon_threadsafe(cmdprocessor, command.commandtext)
|
||||
command.delete()
|
||||
commit()
|
||||
|
||||
@db_session
|
||||
def load(self, room_id: int):
|
||||
@@ -156,9 +161,9 @@ class WebHostContext(Context):
|
||||
with db_session:
|
||||
savegame_data = Room.get(id=self.room_id).multisave
|
||||
if savegame_data:
|
||||
self.set_save(restricted_loads(Room.get(id=self.room_id).multisave))
|
||||
self.set_save(restricted_loads(savegame_data))
|
||||
self._start_async_saving(atexit_save=False)
|
||||
threading.Thread(target=self.listen_to_db_commands, daemon=True).start()
|
||||
asyncio.create_task(self.listen_to_db_commands())
|
||||
|
||||
@db_session
|
||||
def _save(self, exit_save: bool = False) -> bool:
|
||||
@@ -229,6 +234,17 @@ def set_up_logging(room_id) -> logging.Logger:
|
||||
return logger
|
||||
|
||||
|
||||
def tear_down_logging(room_id):
|
||||
"""Close logging handling for a room."""
|
||||
logger_name = f"RoomLogger {room_id}"
|
||||
if logger_name in logging.Logger.manager.loggerDict:
|
||||
logger = logging.getLogger(logger_name)
|
||||
for handler in logger.handlers[:]:
|
||||
logger.removeHandler(handler)
|
||||
handler.close()
|
||||
del logging.Logger.manager.loggerDict[logger_name]
|
||||
|
||||
|
||||
def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
|
||||
host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue):
|
||||
@@ -325,7 +341,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
if ctx.saving:
|
||||
ctx._save()
|
||||
ctx._save(True)
|
||||
setattr(asyncio.current_task(), "save", None)
|
||||
except Exception as e:
|
||||
with db_session:
|
||||
@@ -336,19 +352,25 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
raise
|
||||
else:
|
||||
if ctx.saving:
|
||||
ctx._save()
|
||||
ctx._save(True)
|
||||
setattr(asyncio.current_task(), "save", None)
|
||||
finally:
|
||||
try:
|
||||
ctx.save_dirty = False # make sure the saving thread does not write to DB after final wakeup
|
||||
ctx.exit_event.set() # make sure the saving thread stops at some point
|
||||
# NOTE: async saving should probably be an async task and could be merged with shutdown_task
|
||||
|
||||
if ctx.server and hasattr(ctx.server, "ws_server"):
|
||||
ctx.server.ws_server.close()
|
||||
await ctx.server.ws_server.wait_closed()
|
||||
|
||||
with db_session:
|
||||
# ensure the Room does not spin up again on its own, minute of safety buffer
|
||||
room = Room.get(id=room_id)
|
||||
room.last_activity = datetime.datetime.utcnow() - \
|
||||
datetime.timedelta(minutes=1, seconds=room.timeout)
|
||||
del room
|
||||
tear_down_logging(room_id)
|
||||
logging.info(f"Shutting down room {room_id} on {name}.")
|
||||
finally:
|
||||
await asyncio.sleep(5)
|
||||
|
||||
@@ -128,8 +128,13 @@ def tutorial_landing():
|
||||
"authors": tutorial.authors,
|
||||
"language": tutorial.language
|
||||
}
|
||||
tutorials = {world_name: tutorials for world_name, tutorials in title_sorted(
|
||||
tutorials.items(), key=lambda element: "\x00" if element[0] == "Archipelago" else worlds[element[0]].game)}
|
||||
|
||||
worlds = dict(
|
||||
title_sorted(
|
||||
worlds.items(), key=lambda element: "\x00" if element[0] == "Archipelago" else worlds[element[0]].game
|
||||
)
|
||||
)
|
||||
|
||||
return render_template("tutorialLanding.html", worlds=worlds, tutorials=tutorials)
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ waitress>=3.0.2
|
||||
Flask-Caching>=2.3.0
|
||||
Flask-Compress==1.18 # pkg_resources can't resolve the "backports.zstd" dependency of >1.18, breaking ModuleUpdate.py
|
||||
Flask-Limiter>=3.12
|
||||
Flask-Cors>=6.0.2
|
||||
bokeh>=3.6.3
|
||||
markupsafe>=3.0.2
|
||||
setproctitle>=1.3.5
|
||||
|
||||
@@ -23,7 +23,7 @@ players to rely upon each other to complete their game.
|
||||
While a multiworld game traditionally requires all players to be playing the same game, a multi-game multiworld allows
|
||||
players to randomize any of the supported games, and send items between them. This allows players of different
|
||||
games to interact with one another in a single multiplayer environment. Archipelago supports multi-game multiworlds.
|
||||
Here is a list of our [Supported Games](https://archipelago.gg/games).
|
||||
Here is a list of our [Supported Games](/games).
|
||||
|
||||
## Can I generate a single-player game with Archipelago?
|
||||
|
||||
@@ -33,7 +33,7 @@ play, open the Settings Page, pick your settings, and click Generate Game.
|
||||
|
||||
## How do I get started?
|
||||
|
||||
We have a [Getting Started](https://archipelago.gg/tutorial/Archipelago/setup/en) guide that will help you get the
|
||||
We have a [Getting Started](/tutorial/Archipelago/setup/en) guide that will help you get the
|
||||
software set up. You can use that guide to learn how to generate multiworlds. There are also basic instructions for
|
||||
including multiple games, and hosting multiworlds on the website for ease and convenience.
|
||||
|
||||
@@ -57,7 +57,7 @@ their multiworld.
|
||||
|
||||
If a player must leave early, they can use Archipelago's release system. When a player releases their game, all items
|
||||
in that game belonging to other players are sent out automatically. This allows other players to continue to play
|
||||
uninterrupted. Here is a list of all of our [Server Commands](https://archipelago.gg/tutorial/Archipelago/commands/en).
|
||||
uninterrupted. Here is a list of all of our [Server Commands](/tutorial/Archipelago/commands/en).
|
||||
|
||||
## What happens if an item is placed somewhere it is impossible to get?
|
||||
|
||||
|
||||
@@ -55,6 +55,9 @@
|
||||
{{ OptionTitle(option_name, option) }}
|
||||
<div class="named-range-container">
|
||||
<select id="{{ option_name }}-select" name="{{ option_name }}" data-option-name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
|
||||
{% if option.default not in option.special_range_names.values() %}
|
||||
<option value="{{ option.default }}" selected>Default ({{ option.default }})</option>
|
||||
{% endif %}
|
||||
{% for key, val in option.special_range_names.items() %}
|
||||
{% if option.default == val %}
|
||||
<option value="{{ val }}" selected>{{ key|replace("_", " ")|title }} ({{ val }})</option>
|
||||
@@ -94,6 +97,9 @@
|
||||
<div class="text-choice-container">
|
||||
<div class="text-choice-wrapper">
|
||||
<select id="{{ option_name }}" name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
|
||||
{% if option.default not in option.options.values() %}
|
||||
<option value="{{ option.default }}" selected>Default ({{ option.default }})</option>
|
||||
{% endif %}
|
||||
{% for id, name in option.name_lookup.items()|sort %}
|
||||
{% if name != "random" %}
|
||||
{% if option.default == id %}
|
||||
|
||||
@@ -959,7 +959,7 @@ if "Timespinner" in network_data_package["games"]:
|
||||
|
||||
timespinner_location_ids = {
|
||||
"Present": list(range(1337000, 1337085)),
|
||||
"Past": list(range(1337086, 1337175)),
|
||||
"Past": list(range(1337086, 1337157)) + list(range(1337159, 1337175)),
|
||||
"Ancient Pyramid": [
|
||||
1337236,
|
||||
1337246, 1337247, 1337248, 1337249]
|
||||
|
||||
@@ -289,7 +289,7 @@ async def nes_sync_task(ctx: ZeldaContext):
|
||||
if not ctx.auth:
|
||||
ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0])
|
||||
if ctx.auth == '':
|
||||
logger.info("Invalid ROM detected. No player name built into the ROM. Please regenerate"
|
||||
logger.info("Invalid ROM detected. No player name built into the ROM. Please regenerate "
|
||||
"the ROM using the same link but adding your slot name")
|
||||
if ctx.awaiting_rom:
|
||||
await ctx.server_auth(False)
|
||||
|
||||
13
data/GLOBAL.apignore
Normal file
13
data/GLOBAL.apignore
Normal file
@@ -0,0 +1,13 @@
|
||||
# This file specifies patterns that are ignored by default for any world built with the "Build APWorlds" component.
|
||||
# These patterns can be overriden by a world-specific .apignore using !-prefixed patterns for negation.
|
||||
|
||||
# Auto-created folders
|
||||
__MACOSX
|
||||
.DS_Store
|
||||
__pycache__
|
||||
|
||||
# Unneeded files
|
||||
/archipelago.json
|
||||
/.apignore
|
||||
/.git
|
||||
/.gitignore
|
||||
@@ -28,7 +28,7 @@
|
||||
name: Player{number}
|
||||
|
||||
# Used to describe your yaml. Useful if you have multiple files.
|
||||
description: {{ yaml_dump("Default %s Template" % game) }}
|
||||
description: {{ yaml_dump("%s Preset for %s" % (preset_name, game)) if preset_name else yaml_dump("Default %s Template" % game) }}
|
||||
|
||||
game: {{ yaml_dump(game) }}
|
||||
requires:
|
||||
@@ -38,11 +38,11 @@ requires:
|
||||
{{ yaml_dump(game) }}: {{ world_version }} # Version of the world required for this yaml to work as expected.
|
||||
{%- endif %}
|
||||
|
||||
{%- macro range_option(option) %}
|
||||
{%- macro range_option(option, option_val) %}
|
||||
# You can define additional values between the minimum and maximum values.
|
||||
# Minimum value is {{ option.range_start }}
|
||||
# Maximum value is {{ option.range_end }}
|
||||
{%- set data, notes = dictify_range(option) %}
|
||||
{%- set data, notes = dictify_range(option, option_val) %}
|
||||
{%- for entry, default in data.items() %}
|
||||
{{ entry }}: {{ default }}{% if notes[entry] %} # {{ notes[entry] }}{% endif %}
|
||||
{%- endfor -%}
|
||||
@@ -56,6 +56,10 @@ requires:
|
||||
|
||||
{%- for option_key, option in group_options.items() %}
|
||||
{{ option_key }}:
|
||||
{%- set option_val = option.default %}
|
||||
{%- if option_key in preset %}
|
||||
{%- set option_val = preset[option_key] %}
|
||||
{%- endif -%}
|
||||
{%- if option.__doc__ %}
|
||||
# {{ cleandoc(option.__doc__)
|
||||
| trim
|
||||
@@ -69,25 +73,25 @@ requires:
|
||||
{%- endif -%}
|
||||
|
||||
{%- if option.range_start is defined and option.range_start is number %}
|
||||
{{- range_option(option) -}}
|
||||
{{- range_option(option, option_val) -}}
|
||||
|
||||
{%- elif option.options -%}
|
||||
{%- for suboption_option_id, sub_option_name in option.name_lookup.items() %}
|
||||
{{ yaml_dump(sub_option_name) }}: {% if suboption_option_id == option.default %}50{% else %}0{% endif %}
|
||||
{{ yaml_dump(sub_option_name) }}: {% if suboption_option_id == option_val or sub_option_name == option_val %}50{% else %}0{% endif %}
|
||||
{%- endfor -%}
|
||||
|
||||
{%- if option.name_lookup[option.default] not in option.options %}
|
||||
{{ yaml_dump(option.default) }}: 50
|
||||
|
||||
{%- if option.name_lookup[option_val] not in option.options and option_val not in option.options %}
|
||||
{{ yaml_dump(option_val) }}: 50
|
||||
{%- endif -%}
|
||||
|
||||
{%- elif option.default is string %}
|
||||
{{ yaml_dump(option.default) }}: 50
|
||||
{%- elif option_val is string %}
|
||||
{{ yaml_dump(option_val) }}: 50
|
||||
|
||||
{%- elif option.default is iterable and option.default is not mapping %}
|
||||
{{ option.default | list }}
|
||||
{%- elif option_val is iterable and option_val is not mapping %}
|
||||
{{ option_val | list }}
|
||||
|
||||
{%- else %}
|
||||
{{ yaml_dump(option.default) | indent(4, first=false) }}
|
||||
{{ yaml_dump(option_val) | indent(4, first=false) }}
|
||||
{%- endif -%}
|
||||
{{ "\n" }}
|
||||
{%- endfor %}
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
tag: tag
|
||||
MDLabel:
|
||||
id: tag
|
||||
text: str(this.option.default) if this.option.default != "random" else this.option.range_start
|
||||
text: str(this.option.default) if not isinstance(this.option.default, str) else str(this.option.range_start)
|
||||
MDSlider:
|
||||
id: slider
|
||||
min: this.option.range_start
|
||||
max: this.option.range_end
|
||||
value: min(max(this.option.default, this.option.range_start), this.option.range_end) if this.option.default != "random" else this.option.range_start
|
||||
value: min(max(this.option.default, this.option.range_start), this.option.range_end) if not isinstance(this.option.default, str) else this.option.range_start
|
||||
step: 1
|
||||
step_point_size: 0
|
||||
MDSliderHandle:
|
||||
@@ -23,7 +23,7 @@
|
||||
text: text
|
||||
MDButtonText:
|
||||
id: text
|
||||
text: this.option.get_option_name(this.option.default if this.option.default != "random" else list(this.option.options.values())[0])
|
||||
text: this.option.get_option_name(this.option.default if not isinstance(this.option.default, str) else list(this.option.options.values())[0])
|
||||
theme_text_color: "Primary"
|
||||
|
||||
<VisualNamedRange>:
|
||||
@@ -38,7 +38,7 @@
|
||||
text: text
|
||||
MDButtonText:
|
||||
id: text
|
||||
text: this.option.special_range_names.get(list(this.option.special_range_names.values()).index(this.option.default)) if this.option.default in this.option.special_range_names else "Custom"
|
||||
text: this.option.default.title() if this.option.default in this.option.special_range_names else "Custom"
|
||||
|
||||
<VisualFreeText>:
|
||||
multiline: False
|
||||
|
||||
@@ -70,6 +70,9 @@
|
||||
# DOOM II
|
||||
/worlds/doom_ii/ @Daivuk @KScl
|
||||
|
||||
# EarthBound
|
||||
/worlds/earthbound/ @PinkSwitch
|
||||
|
||||
# Factorio
|
||||
/worlds/factorio/ @Berserker66
|
||||
|
||||
@@ -176,8 +179,12 @@
|
||||
# Sonic Adventure 2 Battle
|
||||
/worlds/sa2b/ @PoryGone @RaspberrySpace
|
||||
|
||||
# Satisfactory
|
||||
/worlds/satisfactory/ @Jarno458 @budak7273
|
||||
|
||||
# Starcraft 2
|
||||
/worlds/sc2/ @Ziktofel
|
||||
# Note: @Ziktofel acts as a mentor
|
||||
/worlds/sc2/ @MatthewMarinets @Snarkie @SirChuckOfTheChuckles
|
||||
|
||||
# Super Metroid
|
||||
/worlds/sm/ @lordlou
|
||||
|
||||
@@ -17,7 +17,8 @@ it will not be detailed here.
|
||||
The client is an intermediary program between the game and the Archipelago server. This can either be a direct
|
||||
modification to the game, an external program, or both. This can be implemented in nearly any modern language, but it
|
||||
must fulfill a few requirements in order to function as expected. Libraries for most modern languages and the spec for
|
||||
various packets can be found in the [network protocol](/docs/network%20protocol.md) API reference document.
|
||||
various packets can be found in the [network protocol](/docs/network%20protocol.md) API reference document. Additional help with specific game
|
||||
engines and rom formats can be found in the #ap-modding-help channel in the [Discord](https://archipelago.gg/discord).
|
||||
|
||||
### Hard Requirements
|
||||
|
||||
@@ -139,8 +140,8 @@ if possible.
|
||||
|
||||
* An implementation of
|
||||
[get_filler_item_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L473)
|
||||
* By default, this function chooses any item name from `item_name_to_id`, so you want to limit it to only the true
|
||||
filler items.
|
||||
* By default, this function chooses any item name from `item_name_to_id`, which may include items you consider
|
||||
"non-repeatable".
|
||||
* An `options_dataclass` defining the options players have available to them
|
||||
* This should be accompanied by a type hint for `options` with the same class name
|
||||
* A [bug report page](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L220)
|
||||
|
||||
@@ -41,16 +41,18 @@ There are also the following optional fields:
|
||||
If the APWorld is packaged as an `.apworld` zip file, it also needs to have `version` and `compatible_version`,
|
||||
which refer to the version of the APContainer packaging scheme defined in [Files.py](../worlds/Files.py).
|
||||
These get automatically added to the `archipelago.json` of an .apworld if it is packaged using the
|
||||
["Build apworlds" launcher component](#build-apworlds-launcher-component),
|
||||
["Build APWorlds" launcher component](#build-apworlds-launcher-component),
|
||||
which is the correct way to package your `.apworld` as a world developer. Do not write these fields yourself.
|
||||
|
||||
### "Build apworlds" Launcher Component
|
||||
### "Build APWorlds" Launcher Component
|
||||
|
||||
In the Archipelago Launcher, there is a "Build apworlds" component that will package all world folders to `.apworld`,
|
||||
In the Archipelago Launcher, there is a "Build APWorlds" component that will package all world folders to `.apworld`,
|
||||
and add `archipelago.json` manifest files to them.
|
||||
These .apworld files will be output to `build/apworlds` (relative to the Archipelago root directory).
|
||||
The `archipelago.json` file in each .apworld will automatically include the appropriate
|
||||
`version` and `compatible_version`.
|
||||
`version` and `compatible_version`.
|
||||
The component can also be called from the command line to allow for specifying a certain list of worlds to build.
|
||||
For example, running `Launcher.py "Build APWorlds" -- "Game Name"` will build only the game called `Game Name`.
|
||||
|
||||
If a world folder has an `archipelago.json` in its root, any fields it contains will be carried over.
|
||||
So, a world folder with an `archipelago.json` that looks like this:
|
||||
@@ -79,10 +81,26 @@ will be packaged into an `.apworld` with a manifest file inside of it that looks
|
||||
|
||||
This is the recommended workflow for packaging your world to an `.apworld`.
|
||||
|
||||
## Extra Data
|
||||
### .apignore Exclusions
|
||||
|
||||
The zip can contain arbitrary files in addition what was specified above.
|
||||
By default, any additional files inside of the world folder will be packaged into the resulting `.apworld` archive and
|
||||
can then be read by the world. However, if there are any other files that aren't needed in the resulting `.apworld`, you
|
||||
can automatically prevent the build component from including them by specifying them in a file called `.apignore` inside
|
||||
the root of the world folder.
|
||||
|
||||
The `.apignore` file selects files in the same way as the `.gitignore` format with patterns separated by line describing
|
||||
which files to ignore. For example, an `.apignore` like this:
|
||||
|
||||
```gitignore
|
||||
*.iso
|
||||
scripts/
|
||||
!scripts/needed.py
|
||||
```
|
||||
|
||||
would ignore any `.iso` files and anything in the scripts folder except for `scripts/needed.py`.
|
||||
|
||||
Some exclusions are made by default for all worlds such as `__pycache__` folders. These are listed in the
|
||||
`GLOBAL.apignore` file inside of the `data` directory.
|
||||
|
||||
## Caveats
|
||||
|
||||
|
||||
@@ -6,6 +6,49 @@ including [Contributing](contributing.md), [Adding Games](<adding games.md>), an
|
||||
|
||||
---
|
||||
|
||||
### I've never added a game to Archipelago before. Should I start with the APWorld or the game client?
|
||||
|
||||
Strictly speaking, this is a false dichotomy: we do *not* recommend doing 100% of client work before the APWorld,
|
||||
or 100% of APWorld work before the client. It's important to iterate on both parts and test them together.
|
||||
However, the early iterations tend to be very similar for most games,
|
||||
so the typical recommendation for first-time AP developers is:
|
||||
|
||||
- Start with a proof-of-concept for [the game client](adding%20games.md#client)
|
||||
- Figure out how to interface with the game. Whether that means "modding" the game, or patching a ROM file,
|
||||
or developing a separate client program that edits the game's memory, or some other technique.
|
||||
- Figure out how to give items and detect locations in the actual game. Not every item and location,
|
||||
just one of each major type (e.g. opening a chest vs completing a sidequest) to prove all the items and locations
|
||||
you want can actually be implemented.
|
||||
- Figure out how to make a websocket connection to an AP server, possibly using a client library (see [Network Protocol](<network%20protocol.md>).
|
||||
To make absolutely sure this part works, you may want to test the connection by generating a multiworld
|
||||
with a different game, then making your client temporarily pretend to be that other game.
|
||||
- Next, make a "trivial" APWorld, i.e. an APWorld that always generates the same items and locations
|
||||
- If you've never done this before, likely the fastest approach is to copy-paste [APQuest](<../worlds/apquest>), and read the many
|
||||
comments in there until you understand how to edit the items and locations.
|
||||
- Then you can do your first "end-to-end test": generate a multiworld using your APWorld, [run a local server](<running%20from%20source.md>)
|
||||
to host it, connect to that local server from your game client, actually check a location in the game,
|
||||
and finally make sure the client successfully sent that location check to the AP server
|
||||
as well as received an item from it.
|
||||
|
||||
That's about where general recommendations end. What you should do next will depend entirely on your game
|
||||
(e.g. implement more items, write down logic rules, add client features, prototype a tracker, etc).
|
||||
If you're not sure, then this would be a good time to re-read [Adding Games](<adding%20games.md>), and [World API](<world%20api.md>).
|
||||
|
||||
There are a few assumptions in this recommendation worth stating explicitly, namely:
|
||||
|
||||
- If something you want to do is infeasible, you want to find out that it's infeasible as soon as possible, before
|
||||
you write a bunch of code assuming it could be done. That's why we recommend starting with the game client.
|
||||
- Getting an APWorld to generate whatever items/locations you want is always feasible, since items/locations are
|
||||
little more than id numbers and name strings during generation.
|
||||
- You generally want to get to an "end-to-end playable" prototype quickly. On top of all the technical challenges these
|
||||
docs describe, it's also important to check that a randomizer is *fun to play*, and figure out what features would be
|
||||
essential for a public release.
|
||||
- A first-time world developer may or may not be deeply familiar with Archipelago, but they're almost certainly familiar
|
||||
with the game they want to randomize. So judging whether your game client is working correctly might be significantly
|
||||
easier than judging if your APWorld is working.
|
||||
|
||||
---
|
||||
|
||||
### My game has a restrictive start that leads to fill errors
|
||||
|
||||
A "restrictive start" here means having a combination of very few sphere 1 locations and potentially requiring more
|
||||
@@ -140,3 +183,58 @@ So when the game itself does not follow this assumption, the options are:
|
||||
- For connections, any logical regions will still need to be reachable through other, *repeatable* connections
|
||||
- For locations, this may require game changes to remove the vanilla item if it affects logic
|
||||
- Decide that resetting the save file is part of the game's logic, and warn players about that
|
||||
|
||||
---
|
||||
|
||||
### What are "local" vs "remote" items, and what are the pros and cons of each?
|
||||
|
||||
First off, these terms can be misleading. Since the whole point of a multi-game multiworld randomizer is that some items
|
||||
are going to be placed in other slots (unless there's only one slot), the choice isn't really "local vs remote";
|
||||
it's "mixed local/remote vs all remote". You have to get "remote items" working to be an AP implementation at all, and
|
||||
it's often simpler to handle every item/location the same way, so you generally shouldn't worry about "local items"
|
||||
until you've finished more critical features.
|
||||
|
||||
Next, "local" and "remote" items confusingly refer to multiple concepts, so it's important to clearly separate them:
|
||||
|
||||
- Whether an item happens to get placed in the same slot it originates from, or a different slot. I'll call these
|
||||
"locally placed" and "remotely placed" items.
|
||||
- Whether an AP client implements location checking for locally placed items by skipping the usual AP server roundtrip
|
||||
(i.e. sending [LocationChecks](<network%20protocol.md#locationchecks>)
|
||||
then receiving [ReceivedItems](<network%20protocol.md#receiveditems>)
|
||||
) and directly giving the item to the player, or by doing the AP server roundtrip regardless. I'll call these
|
||||
"locally implemented" items and "remotely implemented" items.
|
||||
- Locally implementing items requires the AP client to know what the locally placed items were without asking an AP
|
||||
server (or else you'd effectively be doing remote items with extra steps). Typically, it gets that information from
|
||||
a patch file, which is one reason why games that already need a patch file are more likely to choose local items.
|
||||
- If items are remotely implemented, the AP client can use [location scouts](<network%20protocol.md#LocationScouts>)
|
||||
to learn what items are placed on what locations. Features that require this information are sometimes mistakenly
|
||||
assumed to require locally implemented items, but location scouts work just as well as patch file data.
|
||||
- [The `items_handling` bitflags in the Connect packet](<network%20protocol.md#items_handling-flags>).
|
||||
AP clients with remotely implemented items will typically set all three flags, including "from your own world".
|
||||
Clients with locally implemented items might set only the "from other worlds" flag.
|
||||
- Whether a local items client sets the "starting inventory" flag likely depends on other details. For example, if a ROM
|
||||
is being patched, and starting inventory can be added to that patch, then it makes sense to leave the flag unset.
|
||||
|
||||
When people talk about "local vs remote items" as a choice that world devs have to make, they mean deciding whether
|
||||
your client will locally or remotely implement the items which happen to be locally placed (or make both
|
||||
implementations, or let the player choose an implementation).
|
||||
|
||||
Theoretically, the biggest benefit of "local items" is that it allows a solo (single slot) multiworld to be played
|
||||
entirely offline, with no AP server, from start to finish. This is similar to a "standalone"/non-AP randomizer,
|
||||
except that you still get AP's player options, generation, etc. for free.
|
||||
For some games, there are also technical constraints that make certain items easier to implement locally,
|
||||
or less glitchy when implemented locally, as long as you're okay with never allowing these items to be placed remotely
|
||||
(or offering the player even more options).
|
||||
|
||||
The main downside (besides more implementation work) is that "local items" can't support "same slot co-op".
|
||||
That's when two players on two different machines connect to the same slot and play together.
|
||||
This only works if both players receive all the items for that slot, including ones found by the other player,
|
||||
which requires those items to be implemented remotely so the AP server can send them to all of that slot's clients.
|
||||
|
||||
So to recap:
|
||||
|
||||
- (All) remote items is often the simplest choice, since you have to implement remote items anyway.
|
||||
- Remote items enable same slot co-op.
|
||||
- Local items enable solo offline play.
|
||||
- If you want to support both solo offline play and same slot co-op,
|
||||
you might need to expose local vs remote items as an option to the player.
|
||||
|
||||
@@ -20,7 +20,7 @@ game contributions:
|
||||
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:
|
||||

|
||||

|
||||
|
||||
* **When reviewing PRs, please leave a message about what was done.**
|
||||
We don't have full test coverage, so manual testing can help.
|
||||
|
||||
@@ -225,7 +225,7 @@ Sent to clients after a client requested this message be sent to them, more info
|
||||
| games | list\[str\] | Optional. Game names this message is targeting |
|
||||
| slots | list\[int\] | Optional. Player slot IDs that this message is targeting |
|
||||
| tags | list\[str\] | Optional. Client [Tags](#Tags) this message is targeting |
|
||||
| data | dict | The data in the [Bounce](#Bounce) package copied |
|
||||
| data | dict | Optional. The data in the [Bounce](#Bounce) package copied |
|
||||
|
||||
### InvalidPacket
|
||||
Sent to clients if the server caught a problem with a packet. This only occurs for errors that are explicitly checked for.
|
||||
@@ -425,7 +425,7 @@ the server will forward the message to all those targets to which any one requir
|
||||
| games | list\[str\] | Optional. Game names that should receive this message |
|
||||
| slots | list\[int\] | Optional. Player IDs that should receive this message |
|
||||
| tags | list\[str\] | Optional. Client tags that should receive this message |
|
||||
| data | dict | Any data you want to send |
|
||||
| data | dict | Optional. Any data you want to send |
|
||||
|
||||
### Get
|
||||
Used to request a single or multiple values from the server's data storage, see the [Set](#Set) package for how to write values to the data storage. A Get package will be answered with a [Retrieved](#Retrieved) package.
|
||||
|
||||
@@ -269,7 +269,8 @@ placed on them.
|
||||
|
||||
### PriorityLocations
|
||||
Marks locations given here as `LocationProgressType.Priority` forcing progression items on them if any are available in
|
||||
the pool.
|
||||
the pool. Progression items without a deprioritized flag will be used first when filling priority_locations. Progression items with
|
||||
a deprioritized flag will be used next.
|
||||
|
||||
### ItemLinks
|
||||
Allows users to share their item pool with other players. Currently item links are per game. A link of one game between
|
||||
|
||||
482
docs/rule builder.md
Normal file
482
docs/rule builder.md
Normal file
@@ -0,0 +1,482 @@
|
||||
# Rule Builder
|
||||
|
||||
This document describes the API provided for the rule builder. Using this API provides you with with a simple interface to define rules and the following advantages:
|
||||
|
||||
- Rule classes that avoid all the common pitfalls
|
||||
- Logic optimization
|
||||
- Automatic result caching (opt-in)
|
||||
- Serialization/deserialization
|
||||
- Human-readable logic explanations for players
|
||||
|
||||
## Overview
|
||||
|
||||
The rule builder consists of 3 main parts:
|
||||
|
||||
1. The rules, which are classes that inherit from `rule_builder.rules.Rule`. These are what you write for your logic. They can be combined and take into account your world's options. There are a number of default rules listed below, and you can create as many custom rules for your world as needed. When assigning the rules to a location or entrance they must be resolved.
|
||||
1. Resolved rules, which are classes that inherit from `rule_builder.rules.Rule.Resolved`. These are the optimized rules specific to one player that are set as a location or entrance's access rule. You generally shouldn't be directly creating these but they'll be created when assigning rules to locations or entrances. These are what power the human-readable logic explanations.
|
||||
1. The optional rule builder world subclass `CachedRuleBuilderWorld`, which is a class your world can inherit from instead of `World`. It adds a caching system to the rules that will lazy evaluate and cache the result.
|
||||
|
||||
## Usage
|
||||
|
||||
For the most part the only difference in usage is instead of writing lambdas for your logic, you write static Rule objects. You then must use `world.set_rule` to assign the rule to a location or entrance.
|
||||
|
||||
```python
|
||||
# In your world's create_regions method
|
||||
location = MyWorldLocation(...)
|
||||
self.set_rule(location, Has("A Big Gun"))
|
||||
```
|
||||
|
||||
The rule builder comes with a number of rules by default:
|
||||
|
||||
- `True_`: Always returns true
|
||||
- `False_`: Always returns false
|
||||
- `And`: Checks that all child rules are true (also provided by `&` operator)
|
||||
- `Or`: Checks that at least one child rule is true (also provided by `|` operator)
|
||||
- `Has`: Checks that the player has the given item with the given count (default 1)
|
||||
- `HasAll`: Checks that the player has all given items
|
||||
- `HasAny`: Checks that the player has at least one of the given items
|
||||
- `HasAllCounts`: Checks that the player has all of the counts for the given items
|
||||
- `HasAnyCount`: Checks that the player has any of the counts for the given items
|
||||
- `HasFromList`: Checks that the player has some number of given items
|
||||
- `HasFromListUnique`: Checks that the player has some number of given items, ignoring duplicates of the same item
|
||||
- `HasGroup`: Checks that the player has some number of items from a given item group
|
||||
- `HasGroupUnique`: Checks that the player has some number of items from a given item group, ignoring duplicates of the same item
|
||||
- `CanReachLocation`: Checks that the player can logically reach the given location
|
||||
- `CanReachRegion`: Checks that the player can logically reach the given region
|
||||
- `CanReachEntrance`: Checks that the player can logically reach the given entrance
|
||||
|
||||
You can combine these rules together to describe the logic required for something. For example, to check if a player either has `Movement ability` or they have both `Key 1` and `Key 2`, you can do:
|
||||
|
||||
```python
|
||||
rule = Has("Movement ability") | HasAll("Key 1", "Key 2")
|
||||
```
|
||||
|
||||
> ⚠️ Composing rules with the `and` and `or` keywords will not work. You must use the bitwise `&` and `|` operators. In order to catch mistakes, the rule builder will not let you do boolean operations. As a consequence, in order to check if a rule is defined you must use `if rule is not None`.
|
||||
|
||||
### Assigning rules
|
||||
|
||||
When assigning the rule you must use the `set_rule` helper to correctly resolve and register the rule.
|
||||
|
||||
```python
|
||||
self.set_rule(location_or_entrance, rule)
|
||||
```
|
||||
|
||||
There is also a `create_entrance` helper that will resolve the rule, check if it's `False`, and if not create the entrance and set the rule. This allows you to skip creating entrances that will never be valid. You can also specify `force_creation=True` if you would like to create the entrance even if the rule is `False`.
|
||||
|
||||
```python
|
||||
self.create_entrance(from_region, to_region, rule)
|
||||
```
|
||||
|
||||
> ⚠️ If you use a `CanReachLocation` rule on an entrance, you will either have to create the locations first, or specify the location's parent region name with the `parent_region_name` argument of `CanReachLocation`.
|
||||
|
||||
You can also set a rule for your world's completion condition:
|
||||
|
||||
```python
|
||||
self.set_completion_rule(rule)
|
||||
```
|
||||
|
||||
### Restricting options
|
||||
|
||||
Every rule allows you to specify which options it's applicable for. You can provide the argument `options` which is an iterable of `OptionFilter` instances. Rules that pass the options check will be resolved as normal, and those that fail will be resolved as `False`.
|
||||
|
||||
If you want a comparison that isn't equals, you can specify with the `operator` argument. The following operators are allowed:
|
||||
|
||||
- `eq`: `==`
|
||||
- `ne`: `!=`
|
||||
- `gt`: `>`
|
||||
- `lt`: `<`
|
||||
- `ge`: `>=`
|
||||
- `le`: `<=`
|
||||
- `contains`: `in`
|
||||
|
||||
By default rules that are excluded by their options will default to `False`. If you want to default to `True` instead, you can specify `filtered_resolution=True` on your rule.
|
||||
|
||||
To check if the player can reach a switch, or if they've received the switch item if switches are randomized:
|
||||
|
||||
```python
|
||||
rule = (
|
||||
Has("Red switch", options=[OptionFilter(SwitchRando, 1)])
|
||||
| CanReachLocation("Red switch", options=[OptionFilter(SwitchRando, 0)])
|
||||
)
|
||||
```
|
||||
|
||||
To add an extra logic requirement on the easiest difficulty which is ignored for other difficulties:
|
||||
|
||||
```python
|
||||
rule = (
|
||||
# ...the rest of the logic
|
||||
& Has("QoL item", options=[OptionFilter(Difficulty, Difficulty.option_easy)], filtered_resolution=True)
|
||||
)
|
||||
```
|
||||
|
||||
If you would like to provide option filters when reusing or composing rules, you can use the `Filtered` helper rule:
|
||||
|
||||
```python
|
||||
common_rule = Has("A") | HasAny("B", "C")
|
||||
...
|
||||
rule = (
|
||||
Filtered(common_rule, options=[OptionFilter(Opt, 0)]),
|
||||
| Filtered(Has("X") | CanReachRegion("Y"), options=[OptionFilter(Opt, 1)]),
|
||||
)
|
||||
```
|
||||
|
||||
You can also use the & and | operators to apply options to rules:
|
||||
|
||||
```python
|
||||
common_rule = Has("A")
|
||||
easy_filter = [OptionFilter(Difficulty, Difficulty.option_easy)]
|
||||
common_rule_only_on_easy = common_rule & easy_filter
|
||||
common_rule_skipped_on_easy = common_rule | easy_filter
|
||||
```
|
||||
|
||||
## Enabling caching
|
||||
|
||||
The rule builder provides a `CachedRuleBuilderWorld` base class for your `World` class that enables caching on your rules.
|
||||
|
||||
```python
|
||||
class MyWorld(CachedRuleBuilderWorld):
|
||||
game = "My Game"
|
||||
```
|
||||
|
||||
If your world's logic is very simple and you don't have many nested rules, the caching system may have more overhead cost than time it saves. You'll have to benchmark your own world to see if it should be enabled or not.
|
||||
|
||||
### Item name mapping
|
||||
|
||||
If you have multiple real items that map to a single logic item, add a `item_mapping` class dict to your world that maps actual item names to real item names so the cache system knows what to invalidate.
|
||||
|
||||
For example, if you have multiple `Currency x<num>` items on locations, but your rules only check a singular logical `Currency` item, eg `Has("Currency", 1000)`, you'll want to map each numerical currency item to the single logical `Currency`.
|
||||
|
||||
```python
|
||||
class MyWorld(CachedRuleBuilderWorld):
|
||||
item_mapping = {
|
||||
"Currency x10": "Currency",
|
||||
"Currency x50": "Currency",
|
||||
"Currency x100": "Currency",
|
||||
"Currency x500": "Currency",
|
||||
}
|
||||
```
|
||||
|
||||
## Defining custom rules
|
||||
|
||||
You can create a custom rule by creating a class that inherits from `Rule` or any of the default rules. You must provide the game name as an argument to the class. It's recommended to use the `@dataclass` decorator to reduce boilerplate, and to also provide your world as a type argument to add correct type checking to the `_instantiate` method.
|
||||
|
||||
You must provide or inherit a `Resolved` child class that defines an `_evaluate` method. This class will automatically be converted into a frozen `dataclass`. If your world has caching enabled you may need to define one or more dependencies functions as outlined below.
|
||||
|
||||
To add a rule that checks if the user has enough mcguffins to goal, with a randomized requirement:
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass()
|
||||
class CanGoal(Rule["MyWorld"], game="My Game"):
|
||||
@override
|
||||
def _instantiate(self, world: "MyWorld") -> Rule.Resolved:
|
||||
# caching_enabled only needs to be passed in when your world inherits from CachedRuleBuilderWorld
|
||||
return self.Resolved(world.required_mcguffins, player=world.player, caching_enabled=True)
|
||||
|
||||
class Resolved(Rule.Resolved):
|
||||
goal: int
|
||||
|
||||
@override
|
||||
def _evaluate(self, state: CollectionState) -> bool:
|
||||
return state.has("McGuffin", self.player, count=self.goal)
|
||||
|
||||
@override
|
||||
def item_dependencies(self) -> dict[str, set[int]]:
|
||||
# this function is only required if you have caching enabled
|
||||
return {"McGuffin": {id(self)}}
|
||||
|
||||
@override
|
||||
def explain_json(self, state: CollectionState | None = None) -> list[JSONMessagePart]:
|
||||
# this method can be overridden to display custom explanations
|
||||
return [
|
||||
{"type": "text", "text": "Goal with "},
|
||||
{"type": "color", "color": "green" if state and self(state) else "salmon", "text": str(self.goal)},
|
||||
{"type": "text", "text": " McGuffins"},
|
||||
]
|
||||
```
|
||||
|
||||
Your custom rule can also resolve to builtin rules instead of needing to define your own:
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass()
|
||||
class ComplicatedFilter(Rule["MyWorld"], game="My Game"):
|
||||
def _instantiate(self, world: "MyWorld") -> Rule.Resolved:
|
||||
if world.some_precalculated_bool:
|
||||
return Has("Item 1").resolve(world)
|
||||
if world.options.some_option:
|
||||
return CanReachRegion("Region 1").resolve(world)
|
||||
return False_().resolve(world)
|
||||
```
|
||||
|
||||
### Item dependencies
|
||||
|
||||
If your world inherits from `CachedRuleBuilderWorld` and there are items that when collected will affect the result of your rule evaluation, it must define an `item_dependencies` function that returns a mapping of the item name to the id of your rule. These dependencies will be combined to inform the caching system. It may be worthwhile to define this function even when caching is disabled as more things may use it in the future.
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass()
|
||||
class MyRule(Rule["MyWorld"], game="My Game"):
|
||||
class Resolved(Rule.Resolved):
|
||||
item_name: str
|
||||
|
||||
@override
|
||||
def item_dependencies(self) -> dict[str, set[int]]:
|
||||
return {self.item_name: {id(self)}}
|
||||
```
|
||||
|
||||
All of the default `Has*` rules define this function already.
|
||||
|
||||
### Region dependencies
|
||||
|
||||
If your custom rule references other regions, it must define a `region_dependencies` function that returns a mapping of region names to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the caching system if applicable.
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass()
|
||||
class MyRule(Rule["MyWorld"], game="My Game"):
|
||||
class Resolved(Rule.Resolved):
|
||||
region_name: str
|
||||
|
||||
@override
|
||||
def region_dependencies(self) -> dict[str, set[int]]:
|
||||
return {self.region_name: {id(self)}}
|
||||
```
|
||||
|
||||
The default `CanReachLocation`, `CanReachRegion`, and `CanReachEntrance` rules define this function already.
|
||||
|
||||
### Location dependencies
|
||||
|
||||
If your custom rule references other locations, it must define a `location_dependencies` function that returns a mapping of the location name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the caching system if applicable.
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass()
|
||||
class MyRule(Rule["MyWorld"], game="My Game"):
|
||||
class Resolved(Rule.Resolved):
|
||||
location_name: str
|
||||
|
||||
@override
|
||||
def location_dependencies(self) -> dict[str, set[int]]:
|
||||
return {self.location_name: {id(self)}}
|
||||
```
|
||||
|
||||
The default `CanReachLocation` rule defines this function already.
|
||||
|
||||
### Entrance dependencies
|
||||
|
||||
If your custom rule references other entrances, it must define a `entrance_dependencies` function that returns a mapping of the entrance name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the caching system if applicable.
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass()
|
||||
class MyRule(Rule["MyWorld"], game="My Game"):
|
||||
class Resolved(Rule.Resolved):
|
||||
entrance_name: str
|
||||
|
||||
@override
|
||||
def entrance_dependencies(self) -> dict[str, set[int]]:
|
||||
return {self.entrance_name: {id(self)}}
|
||||
```
|
||||
|
||||
The default `CanReachEntrance` rule defines this function already.
|
||||
|
||||
### Rule explanations
|
||||
|
||||
Resolved rules have a default implementation for `explain_json` and `explain_str` functions. The former optionally accepts a `CollectionState` and returns a list of `JSONMessagePart` appropriate for `print_json` in a client. It will display a human-readable message that explains what the rule requires. The latter is similar but returns a string. It is useful when debugging. There is also a `__str__` method defined to check what a rule is without a state.
|
||||
|
||||
To implement a custom message with a custom rule, override the `explain_json` and/or `explain_str` method on your `Resolved` class:
|
||||
|
||||
```python
|
||||
class MyRule(Rule, game="My Game"):
|
||||
class Resolved(Rule.Resolved):
|
||||
@override
|
||||
def explain_json(self, state: CollectionState | None = None) -> list[JSONMessagePart]:
|
||||
has_item = state and state.has("growth spurt", self.player)
|
||||
color = "yellow"
|
||||
start = "You must be "
|
||||
if has_item:
|
||||
start = "You are "
|
||||
color = "green"
|
||||
elif state is not None:
|
||||
start = "You are not "
|
||||
color = "salmon"
|
||||
return [
|
||||
{"type": "text", "text": start},
|
||||
{"type": "color", "color": color, "text": "THIS"},
|
||||
{"type": "text", "text": " tall to beat the game"},
|
||||
]
|
||||
|
||||
@override
|
||||
def explain_str(self, state: CollectionState | None = None) -> str:
|
||||
if state is None:
|
||||
return str(self)
|
||||
if state.has("growth spurt", self.player):
|
||||
return "You ARE this tall and can beat the game"
|
||||
return "You are not THIS tall and cannot beat the game"
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return "You must be THIS tall to beat the game"
|
||||
```
|
||||
|
||||
### Cache control
|
||||
|
||||
By default your custom rule will work through the cache system as any other rule if caching is enabled. There are two class attributes on the `Resolved` class you can override to change this behavior.
|
||||
|
||||
- `force_recalculate`: Setting this to `True` will cause your custom rule to skip going through the caching system and always recalculate when being evaluated. When a rule with this flag enabled is composed with `And` or `Or` it will cause any parent rules to always force recalculate as well. Use this flag when it's difficult to determine when your rule should be marked as stale.
|
||||
- `skip_cache`: Setting this to `True` will also cause your custom rule to skip going through the caching system when being evaluated. However, it will **not** affect any other rules when composed with `And` or `Or`, so it must still define its `*_dependencies` functions as required. Use this flag when the evaluation of this rule is trivial and the overhead of the caching system will slow it down.
|
||||
|
||||
### Caveats
|
||||
|
||||
- Ensure you are passing `caching_enabled=True` in your `_instantiate` function when creating resolved rule instances if your world has opted into caching.
|
||||
- Resolved rules are forced to be frozen dataclasses. They and all their attributes must be immutable and hashable.
|
||||
- If your rule creates child rules ensure they are being resolved through the world rather than creating `Resolved` instances directly.
|
||||
|
||||
## Serialization
|
||||
|
||||
The rule builder is intended to be written first in Python for optimization and type safety. To facilitate exporting the rules to a client or tracker, rules have a `to_dict` method that returns a JSON-compatible dict. Since the location and entrance logic structure varies greatly from world to world, the actual JSON dumping is left up to the world dev.
|
||||
|
||||
The dict contains a `rule` key with the name of the rule, an `options` key with the rule's list of option filters, and an `args` key that contains any other arguments the individual rule has. For example, this is what a simple `Has` rule would look like:
|
||||
|
||||
```python
|
||||
{
|
||||
"rule": "Has",
|
||||
"options": [],
|
||||
"args": {
|
||||
"item_name": "Some item",
|
||||
"count": 1,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
For `And` and `Or` rules, instead of an `args` key, they have a `children` key containing a list of their child rules in the same serializable format:
|
||||
|
||||
```python
|
||||
{
|
||||
"rule": "And",
|
||||
"options": [],
|
||||
"children": [
|
||||
..., # each serialized rule
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
A full example is as follows:
|
||||
|
||||
```python
|
||||
rule = And(
|
||||
Has("a", options=[OptionFilter(ToggleOption, 0)]),
|
||||
Or(Has("b", count=2), CanReachRegion("c"), options=[OptionFilter(ToggleOption, 1)]),
|
||||
)
|
||||
assert rule.to_dict() == {
|
||||
"rule": "And",
|
||||
"options": [],
|
||||
"children": [
|
||||
{
|
||||
"rule": "Has",
|
||||
"options": [
|
||||
{
|
||||
"option": "worlds.my_world.options.ToggleOption",
|
||||
"value": 0,
|
||||
"operator": "eq",
|
||||
},
|
||||
],
|
||||
"args": {
|
||||
"item_name": "a",
|
||||
"count": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
"rule": "Or",
|
||||
"options": [
|
||||
{
|
||||
"option": "worlds.my_world.options.ToggleOption",
|
||||
"value": 1,
|
||||
"operator": "eq",
|
||||
},
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"rule": "Has",
|
||||
"options": [],
|
||||
"args": {
|
||||
"item_name": "b",
|
||||
"count": 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
"rule": "CanReachRegion",
|
||||
"options": [],
|
||||
"args": {
|
||||
"region_name": "c",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
### Custom serialization
|
||||
|
||||
To define a different format for your custom rules, override the `to_dict` function:
|
||||
|
||||
```python
|
||||
class BasicLogicRule(Rule, game="My Game"):
|
||||
items = ("one", "two")
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
# Return whatever format works best for you
|
||||
return {
|
||||
"logic": "basic",
|
||||
"items": self.items,
|
||||
}
|
||||
```
|
||||
|
||||
If your logic has been done in custom JSON first, you can define a `from_dict` class method on your rules to parse it correctly:
|
||||
|
||||
```python
|
||||
class BasicLogicRule(Rule, game="My Game"):
|
||||
@classmethod
|
||||
def from_dict(cls, data: Mapping[str, Any], world_cls: type[World]) -> Self:
|
||||
items = data.get("items", ())
|
||||
return cls(*items)
|
||||
```
|
||||
|
||||
## APIs
|
||||
|
||||
This section is provided for reference, refer to the above sections for examples.
|
||||
|
||||
### World API
|
||||
|
||||
These are properties and helpers that are available to you in your world.
|
||||
|
||||
#### Methods
|
||||
|
||||
- `rule_from_dict(data)`: Create a rule instance from a deserialized dict representation
|
||||
- `register_rule_builder_dependencies()`: Register all rules that depend on location or entrance access with the inherited dependencies, gets called automatically after set_rules
|
||||
- `set_rule(spot: Location | Entrance, rule: Rule)`: Resolve a rule, register its dependencies, and set it on the given location or entrance
|
||||
- `set_completion_rule(rule: Rule)`: Sets the completion condition for this world
|
||||
- `create_entrance(from_region: Region, to_region: Region, rule: Rule | None, name: str | None = None, force_creation: bool = False)`: Attempt to create an entrance from `from_region` to `to_region`, skipping creation if `rule` is defined and evaluates to `False_()` unless force_creation is `True`
|
||||
|
||||
#### CachedRuleBuilderWorld Properties
|
||||
|
||||
The following property is only available when inheriting from `CachedRuleBuilderWorld`
|
||||
|
||||
- `item_mapping: dict[str, str]`: A mapping of actual item name to logical item name
|
||||
|
||||
### Rule API
|
||||
|
||||
These are properties and helpers that you can use or override for custom rules.
|
||||
|
||||
- `_instantiate(world: World)`: Create a new resolved rule instance, override for custom rules as required
|
||||
- `to_dict()`: Create a JSON-compatible dict representation of this rule, override if you want to customize your rule's serialization
|
||||
- `from_dict(data, world_cls: type[World])`: Return a new rule instance from a deserialized representation, override if you've overridden `to_dict`
|
||||
- `__str__()`: Basic string representation of a rule, useful for debugging
|
||||
|
||||
#### Resolved rule API
|
||||
|
||||
- `player: int`: The slot this rule is resolved for
|
||||
- `_evaluate(state: CollectionState)`: Evaluate this rule against the given state, override this to define the logic for this rule
|
||||
- `item_dependencies()`: A mapping of item name to set of ids, override this if your custom rule depends on item collection
|
||||
- `region_dependencies()`: A mapping of region name to set of ids, override this if your custom rule depends on reaching regions
|
||||
- `location_dependencies()`: A mapping of location name to set of ids, override this if your custom rule depends on reaching locations
|
||||
- `entrance_dependencies()`: A mapping of entrance name to set of ids, override this if your custom rule depends on reaching entrances
|
||||
- `explain_json(state: CollectionState | None = None)`: Return a list of printJSON messages describing this rule's logic (and if state is defined its evaluation) in a human readable way, override to explain custom rules
|
||||
- `explain_str(state: CollectionState | None = None)`: Return a string describing this rule's logic (and if state is defined its evaluation) in a human readable way, override to explain custom rules, more useful for debugging
|
||||
- `__str__()`: A string describing this rule's logic without its evaluation, override to explain custom rules
|
||||
@@ -7,10 +7,9 @@ use that version. These steps are for developers or platforms without compiled r
|
||||
## General
|
||||
|
||||
What you'll need:
|
||||
* [Python 3.11.9 or newer](https://www.python.org/downloads/), not the Windows Store version
|
||||
* [Python 3.11.9 or newer but less than 3.14](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.13.x is currently the newest supported version
|
||||
* pip: included in downloads from python.org, separate in many Linux distributions
|
||||
* Matching C compiler
|
||||
* possibly optional, read operating system specific sections
|
||||
@@ -53,6 +52,32 @@ Recommended steps
|
||||
Refer to [Guide to Run Archipelago from Source Code on macOS](../worlds/generic/docs/mac_en.md).
|
||||
|
||||
|
||||
## Linux
|
||||
|
||||
If your Linux distribution ships a compatible Python version (see [General](#general)) and pip, you can use that,
|
||||
otherwise you may need to install Python from a 3rd party. Refer to documentation of your Linux distribution.
|
||||
|
||||
Installing a C compiler is usually optional. The package is typically named `gcc`, sometimes another package with the
|
||||
base build tools may be required, i.e. `build-essential` (Debian/Ubuntu) or `base-devel` (Arch).
|
||||
|
||||
After getting the source code, it is strongly recommended to create a
|
||||
[venv](https://docs.python.org/3/tutorial/venv.html) (Virtual Environment)
|
||||
by hand or using an IDE, such as PyCharm, because Archipelago requires specific versions of Python packages.
|
||||
|
||||
Run `python ModuleUpdate.py` in the project root to install packages, run `python Launcher.py` to run the Launcher.
|
||||
|
||||
### Building
|
||||
|
||||
Builds contain (almost) all dependencies to run Archipelago on any Linux distribution that is as new or newer than the
|
||||
one it was built on. Beware that currently only the oldest Ubuntu LTS available in GitHub actions is supported for that.
|
||||
This means the easiest way to generate a build is by running the `Build` action from GitHub actions instead of building
|
||||
locally. If you still want to, e.g. for local testing, you can by running
|
||||
|
||||
`python setup.py build_exe` to generate a binary distribution of Archipelago in `build/`. Or to generate an AppImage
|
||||
first generate the binary distribution and then run `python setup.py bdist_appimage` to populate `dist/`. You need to
|
||||
put an `appimagetool` into the directory you run the command from, rename it to `appimagetool` and make it executable.
|
||||
|
||||
|
||||
## Optional: A Link to the Past Enemizer
|
||||
|
||||
Only required to generate seeds that include A Link to the Past with certain options enabled. You will receive an
|
||||
|
||||
@@ -47,21 +47,27 @@
|
||||
|
||||
## HTML
|
||||
|
||||
* Indent with 2 spaces for new code.
|
||||
* Indent with 4 spaces for new code.
|
||||
* kebab-case for ids and classes.
|
||||
* Avoid using on* attributes (onclick, etc.).
|
||||
|
||||
## CSS
|
||||
## CSS / SCSS
|
||||
|
||||
* Indent with 2 spaces for new code.
|
||||
* Indent with 4 spaces for new code.
|
||||
* `{` on the same line as the selector.
|
||||
* No space between selector and `{`.
|
||||
* Space between selector and `{`.
|
||||
|
||||
## JS
|
||||
|
||||
* Indent with 2 spaces.
|
||||
* Indent `case` inside `switch ` with 2 spaces.
|
||||
* Use single quotes.
|
||||
* Indent with 4 spaces.
|
||||
* Indent `case` inside `switch ` with 4 spaces.
|
||||
* Prefer double quotation marks (`"`).
|
||||
* Semicolons are required after every statement.
|
||||
* Use [IIFEs](https://developer.mozilla.org/docs/Glossary/IIFE) to avoid polluting global scope.
|
||||
* Prefer to use [defer](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#defer)
|
||||
in script tags, which retains order of execution but does not block.
|
||||
* Avoid `<script async ...` in most cases, see [async and defer](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#async_and_defer).
|
||||
* Use addEventListener.
|
||||
|
||||
## KV
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ Archipelago has a rudimentary API that can be queried by endpoints. The API is a
|
||||
|
||||
The following API requests are formatted as: `https://<Archipelago URL>/api/<endpoint>`
|
||||
|
||||
The returned data will be formated in a combination of JSON lists or dicts, with their keys or values being notated in `blocks` (if applicable)
|
||||
The returned data will be formatted in a combination of JSON lists or dicts, with their keys or values being notated in `blocks` (if applicable)
|
||||
|
||||
Current endpoints:
|
||||
- Datapackage API
|
||||
@@ -24,13 +24,21 @@ Current endpoints:
|
||||
- [`/get_rooms`](#getrooms)
|
||||
- [`/get_seeds`](#getseeds)
|
||||
|
||||
## API Data Caching
|
||||
To reduce the strain on an Archipelago WebHost, many API endpoints will cache their data and only poll new data in timed intervals. Each endpoint has their own caching time related to the type of data being served. More dynamic data is refreshed more frequently, while static data is cached for longer.
|
||||
Each API endpoint will have their "Cache timer" listed under their definition (if any).
|
||||
API calls to these endpoints should not be faster than the listed timer. This will result in wasted processing for your client and (more importantly) the Archipelago WebHost, as the data will not be refreshed by the WebHost until the internal timer has elapsed.
|
||||
|
||||
|
||||
## Datapackage Endpoints
|
||||
These endpoints are used by applications to acquire a room's datapackage, and validate that they have the correct datapackage for use. Datapackages normally include, item IDs, location IDs, and name groupings, for a given room, and are essential for mapping IDs received from Archipelago to their correct items or locations.
|
||||
|
||||
### `/datapackage`
|
||||
<a name="datapackage"></a>
|
||||
|
||||
Fetches the current datapackage from the WebHost.
|
||||
**Cache timer: None**
|
||||
|
||||
You'll receive a dict named `games` that contains a named dict of every game and its data currently supported by Archipelago.
|
||||
Each game will have:
|
||||
- A checksum `checksum`
|
||||
@@ -40,7 +48,7 @@ Each game will have:
|
||||
- Location name to AP ID dict `location_name_to_id`
|
||||
|
||||
Example:
|
||||
```
|
||||
```json
|
||||
{
|
||||
"games": {
|
||||
...
|
||||
@@ -76,7 +84,10 @@ Example:
|
||||
|
||||
### `/datapackage/<string:checksum>`
|
||||
<a name="datapackagestringchecksum"></a>
|
||||
Fetches a single datapackage by checksum.
|
||||
|
||||
Fetches a single datapackage by checksum.
|
||||
**Cache timer: None**
|
||||
|
||||
Returns a dict of the game's data with:
|
||||
- A checksum `checksum`
|
||||
- A dict of item groups `item_name_groups`
|
||||
@@ -88,10 +99,13 @@ Its format will be identical to the whole-datapackage endpoint (`/datapackage`),
|
||||
|
||||
### `/datapackage_checksum`
|
||||
<a name="datapackagechecksum"></a>
|
||||
Fetches the checksums of the current static datapackages on the WebHost.
|
||||
|
||||
Fetches the checksums of the current static datapackages on the WebHost.
|
||||
**Cache timer: None**
|
||||
|
||||
You'll receive a dict with `game:checksum` key-value pairs for all the current officially supported games.
|
||||
Example:
|
||||
```
|
||||
```json
|
||||
{
|
||||
...
|
||||
"Donkey Kong Country 3":"f90acedcd958213f483a6a4c238e2a3faf92165e",
|
||||
@@ -108,6 +122,7 @@ These endpoints are used internally for the WebHost to generate games and valida
|
||||
<a name="generate"></a>
|
||||
Submits a game to the WebHost for generation.
|
||||
**This endpoint only accepts a POST HTTP request.**
|
||||
**Cache timer: None**
|
||||
|
||||
There are two ways to submit data for generation: With a file and with JSON.
|
||||
|
||||
@@ -116,7 +131,7 @@ Have your ZIP of yaml(s) or a single yaml, and submit a POST request to the `/ge
|
||||
If the options are valid, you'll be returned a successful generation response. (see [Generation Response](#generation-response))
|
||||
|
||||
Example using the python requests library:
|
||||
```
|
||||
```python
|
||||
file = {'file': open('Games.zip', 'rb')}
|
||||
req = requests.post("https://archipelago.gg/api/generate", files=file)
|
||||
```
|
||||
@@ -127,7 +142,7 @@ Finally, submit a POST request to the `/generate` endpoint.
|
||||
If the weighted options are valid, you'll be returned a successful generation response (see [Generation Response](#generation-response))
|
||||
|
||||
Example using the python requests library:
|
||||
```
|
||||
```python
|
||||
data = {"Test":{"game": "Factorio","name": "Test","Factorio": {}},}
|
||||
weights={"weights": data}
|
||||
req = requests.post("https://archipelago.gg/api/generate", json=weights)
|
||||
@@ -143,7 +158,7 @@ Upon successful generation, you'll be sent a JSON dict response detailing the ge
|
||||
- The API status page of the generation `wait_api_url` (see [Status Endpoint](#status))
|
||||
|
||||
Example:
|
||||
```
|
||||
```json
|
||||
{
|
||||
"detail": "19878f16-5a58-4b76-aab7-d6bf38be9463",
|
||||
"encoded": "GYePFlpYS3aqt9a_OL6UYw",
|
||||
@@ -167,12 +182,14 @@ If the generation detects a issue in generation, you'll be sent a dict with two
|
||||
- Detailed issue in `detail`
|
||||
|
||||
In the event of an unhandled server exception, you'll be provided a dict with a single key `text`:
|
||||
- Exception, `Uncought Exception: <error>` with a 500 status code
|
||||
- Exception, `Uncaught Exception: <error>` with a 500 status code
|
||||
|
||||
### `/status/<suuid:seed>`
|
||||
<a name="status"></a>
|
||||
Retrieves the status of the seed's generation.
|
||||
This endpoint will return a dict with a single key-vlaue pair. The key will always be `text`
|
||||
**Cache timer: None**
|
||||
|
||||
This endpoint will return a dict with a single key-value pair. The key will always be `text`
|
||||
The value will tell you the status of the generation:
|
||||
- Generation was completed: `Generation done` with a 201 status code
|
||||
- Generation request was not found: `Generation not found` with a 404 status code
|
||||
@@ -184,6 +201,8 @@ Endpoints to fetch information of the active WebHost room with the supplied room
|
||||
|
||||
### `/room_status/<suuid:room_id>`
|
||||
<a name="roomstatus"></a>
|
||||
**Cache timer: None**
|
||||
|
||||
Will provide a dict of room data with the following keys:
|
||||
- Tracker SUUID (`tracker`)
|
||||
- A list of players (`players`)
|
||||
@@ -192,10 +211,10 @@ Will provide a dict of room data with the following keys:
|
||||
- Last activity timestamp (`last_activity`)
|
||||
- The room timeout counter (`timeout`)
|
||||
- A list of downloads for files required for gameplay (`downloads`)
|
||||
- Each item is a dict containings the download URL and slot (`slot`, `download`)
|
||||
- Each item is a dict containing the download URL and slot (`slot`, `download`)
|
||||
|
||||
Example:
|
||||
```
|
||||
```json
|
||||
{
|
||||
"downloads": [
|
||||
{
|
||||
@@ -244,7 +263,7 @@ Example:
|
||||
]
|
||||
],
|
||||
"timeout": 7200,
|
||||
"tracker": "cf6989c0-4703-45d7-a317-2e5158431171"
|
||||
"tracker": "2gVkMQgISGScA8wsvDZg5A"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -254,17 +273,27 @@ can either be viewed while on a room tracker page, or from the [room's endpoint]
|
||||
|
||||
### `/tracker/<suuid:tracker>`
|
||||
<a name=tracker></a>
|
||||
**Cache timer: 60 seconds**
|
||||
|
||||
Will provide a dict of tracker data with the following keys:
|
||||
|
||||
- Each player's current alias (`aliases`)
|
||||
- Will return the name if there is none
|
||||
- A list of items each player has received as a NetworkItem (`player_items_received`)
|
||||
- A list of players current alias data (`aliases`)
|
||||
- Each item containing a dict with, their alias `alias`, their player number `player`, and their team `team`
|
||||
- `alias` will return `null` if there is no alias set
|
||||
- A list of items each player has received as a [NetworkItem](network%20protocol.md#networkitem) (`player_items_received`)
|
||||
- Each item containing a dict with, a list of NetworkItems `items`, their player number `player`, their team `team`
|
||||
- A list of checks done by each player as a list of the location id's (`player_checks_done`)
|
||||
- The total number of checks done by all players (`total_checks_done`)
|
||||
- Hints that players have used or received (`hints`)
|
||||
- The time of last activity of each player in RFC 1123 format (`activity_timers`)
|
||||
- The time of last active connection of each player in RFC 1123 format (`connection_timers`)
|
||||
- The current client status of each player (`player_status`)
|
||||
- Each item containing a dict with, a list of checked location id's `locations`, their player number `player`, and their team `team`
|
||||
- A list of the total number of checks done by all players (`total_checks_done`)
|
||||
- Each item will contain a dict with, the total checks done `checks_done`, and the team `team`
|
||||
- A list of [Hints](network%20protocol.md#hint) data that players have used or received (`hints`)
|
||||
- Each item containing a dict containing, a list of hint data `hints`, the player number `player`, and their team `team`
|
||||
- A list containing the last activity time for each player, formatted in RFC 1123 format (`activity_timers`)
|
||||
- Each item containing, last activity time `time`, their player number `player`, and their team `team`
|
||||
- A list containing the last connection time for each player, formatted in RFC 1123 format (`connection_timers`)
|
||||
- Each item containing, the time of their last connection `time`, their player number `player`, and their team `team`
|
||||
- A list of the current [ClientStatus](network%20protocol.md#clientstatus) of each player (`player_status`)
|
||||
- Each item will contain, their status `status`, their player number `player`, and their team `team`
|
||||
|
||||
Example:
|
||||
```json
|
||||
@@ -279,7 +308,12 @@ Example:
|
||||
"team": 0,
|
||||
"player": 2,
|
||||
"alias": "Slot_Name_2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"team": 0,
|
||||
"player": 3,
|
||||
"alias": null
|
||||
},
|
||||
],
|
||||
"player_items_received": [
|
||||
{
|
||||
@@ -378,13 +412,21 @@ Example:
|
||||
|
||||
### `/static_tracker/<suuid:tracker>`
|
||||
<a name=statictracker></a>
|
||||
**Cache timer: 300 seconds**
|
||||
|
||||
Will provide a dict of static tracker data with the following keys:
|
||||
|
||||
- item_link groups and their players (`groups`)
|
||||
- The datapackage hash for each game (`datapackage`)
|
||||
- A list of item_link groups and their member players (`groups`)
|
||||
- Each item containing a dict with, the slot registering the group `slot`, the item_link name `name`, and a list of members `members`
|
||||
- A dict of datapackage hashes for each game (`datapackage`)
|
||||
- Each item is a named dict of the game's name.
|
||||
- Each game contains two keys, the datapackage's checksum hash `checksum`, and the version `version`
|
||||
- This hash can then be sent to the datapackage API to receive the appropriate datapackage as necessary
|
||||
- The number of checks found vs. total checks available per player (`player_locations_total`)
|
||||
- A list of number of checks found vs. total checks available per player (`player_locations_total`)
|
||||
- Each list item contains a dict with three keys, the total locations for that slot `total_locations`, their player number `player`, and their team `team`
|
||||
- Same logic as the multitracker template: found = len(player_checks_done.locations) / total = player_locations_total.total_locations (all available checks).
|
||||
- The game each player is playing (`player_game`)
|
||||
- Provided as a list of objects with `team`, `player`, and `game`.
|
||||
|
||||
Example:
|
||||
```json
|
||||
@@ -409,10 +451,10 @@ Example:
|
||||
],
|
||||
"datapackage": {
|
||||
"Archipelago": {
|
||||
"checksum": "ac9141e9ad0318df2fa27da5f20c50a842afeecb",
|
||||
"checksum": "ac9141e9ad0318df2fa27da5f20c50a842afeecb"
|
||||
},
|
||||
"The Messenger": {
|
||||
"checksum": "6991cbcda7316b65bcb072667f3ee4c4cae71c0b",
|
||||
"checksum": "6991cbcda7316b65bcb072667f3ee4c4cae71c0b"
|
||||
}
|
||||
},
|
||||
"player_locations_total": [
|
||||
@@ -427,12 +469,29 @@ Example:
|
||||
"total_locations": 20
|
||||
}
|
||||
],
|
||||
"player_game": [
|
||||
{
|
||||
"team": 0,
|
||||
"player": 1,
|
||||
"game": "Archipelago"
|
||||
},
|
||||
{
|
||||
"team": 0,
|
||||
"player": 2,
|
||||
"game": "The Messenger"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `/slot_data_tracker/<suuid:tracker>`
|
||||
<a name=slotdatatracker></a>
|
||||
Will provide a list of each player's slot_data.
|
||||
Will provide a list of each player's slot_data.
|
||||
**Cache timer: 300 seconds**
|
||||
|
||||
Each list item will contain a dict with the player's data:
|
||||
- player slot number `player`
|
||||
- A named dict `slot_data` containing any set slot data for that player
|
||||
|
||||
Example:
|
||||
```json
|
||||
@@ -460,6 +519,8 @@ User endpoints can get room and seed details from the current session tokens (co
|
||||
### `/get_rooms`
|
||||
<a name="getrooms"></a>
|
||||
Retreives a list of all rooms currently owned by the session token.
|
||||
**Cache timer: None**
|
||||
|
||||
Each list item will contain a dict with the room's details:
|
||||
- Room SUUID (`room_id`)
|
||||
- Seed SUUID (`seed_id`)
|
||||
@@ -470,25 +531,25 @@ Each list item will contain a dict with the room's details:
|
||||
- Room tracker SUUID (`tracker`)
|
||||
|
||||
Example:
|
||||
```
|
||||
```json
|
||||
[
|
||||
{
|
||||
"creation_time": "Fri, 18 Apr 2025 19:46:53 GMT",
|
||||
"last_activity": "Fri, 18 Apr 2025 21:16:02 GMT",
|
||||
"last_port": 52122,
|
||||
"room_id": "90ae5f9b-177c-4df8-ac53-9629fc3bff7a",
|
||||
"seed_id": "efbd62c2-aaeb-4dda-88c3-f461c029cef6",
|
||||
"room_id": "0D30FgQaRcWivFsw9o8qzw",
|
||||
"seed_id": "TFjiarBgTsCj5-Jbe8u33A",
|
||||
"timeout": 7200,
|
||||
"tracker": "cf6989c0-4703-45d7-a317-2e5158431171"
|
||||
"tracker": "52BycvJhRe6knrYH8v4bag"
|
||||
},
|
||||
{
|
||||
"creation_time": "Fri, 18 Apr 2025 20:36:42 GMT",
|
||||
"last_activity": "Fri, 18 Apr 2025 20:36:46 GMT",
|
||||
"last_port": 56884,
|
||||
"room_id": "14465c05-d08e-4d28-96bd-916f994609d8",
|
||||
"seed_id": "a528e34c-3b4f-42a9-9f8f-00a4fd40bacb",
|
||||
"room_id": "LMCFchESSNyuqcY3GxkhwA",
|
||||
"seed_id": "CENtJMXCTGmkIYCzjB5Csg",
|
||||
"timeout": 7200,
|
||||
"tracker": "4e624bd8-32b6-42e4-9178-aa407f72751c"
|
||||
"tracker": "2gVkMQgISGScA8wsvDZg5A"
|
||||
}
|
||||
]
|
||||
```
|
||||
@@ -496,6 +557,8 @@ Example:
|
||||
### `/get_seeds`
|
||||
<a name="getseeds"></a>
|
||||
Retreives a list of all seeds currently owned by the session token.
|
||||
**Cache timer: None**
|
||||
|
||||
Each item in the list will contain a dict with the seed's details:
|
||||
- Seed SUUID (`seed_id`)
|
||||
- Creation timestamp (`creation_time`)
|
||||
@@ -503,7 +566,7 @@ Each item in the list will contain a dict with the seed's details:
|
||||
- Each item in the list will contain a list of the slot name and game
|
||||
|
||||
Example:
|
||||
```
|
||||
```json
|
||||
[
|
||||
{
|
||||
"creation_time": "Fri, 18 Apr 2025 19:46:52 GMT",
|
||||
@@ -529,7 +592,7 @@ Example:
|
||||
"Ocarina of Time"
|
||||
]
|
||||
],
|
||||
"seed_id": "efbd62c2-aaeb-4dda-88c3-f461c029cef6"
|
||||
"seed_id": "CENtJMXCTGmkIYCzjB5Csg"
|
||||
},
|
||||
{
|
||||
"creation_time": "Fri, 18 Apr 2025 20:36:39 GMT",
|
||||
@@ -551,7 +614,7 @@ Example:
|
||||
"Archipelago"
|
||||
]
|
||||
],
|
||||
"seed_id": "a528e34c-3b4f-42a9-9f8f-00a4fd40bacb"
|
||||
"seed_id": "TFjiarBgTsCj5-Jbe8u33A"
|
||||
}
|
||||
]
|
||||
```
|
||||
@@ -225,7 +225,10 @@ and has a classification. The name needs to be unique within each game and must
|
||||
letter or symbol). The ID needs to be unique across all locations within the game.
|
||||
Locations and items can share IDs, and locations can share IDs with other games' locations.
|
||||
|
||||
World-specific IDs must be in the range 1 to 2<sup>53</sup>-1; IDs ≤ 0 are global and reserved.
|
||||
World-specific IDs **must** be in the range 1 to 2<sup>53</sup>-1 (the largest integer that is "[safe](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER#description)"
|
||||
to store in a 64-bit float, and thus all popular programming languages can handle). IDs ≤ 0 are global and reserved.
|
||||
It's **recommended** to keep your IDs in the range 1 to 2<sup>31</sup>-1,
|
||||
so only 32-bit integers are needed to hold your IDs.
|
||||
|
||||
Classification is one of `LocationProgressType.DEFAULT`, `PRIORITY` or `EXCLUDED`.
|
||||
The Fill algorithm will force progression items to be placed at priority locations, giving a higher chance of them being
|
||||
@@ -767,6 +770,7 @@ class MyGameState(LogicMixin):
|
||||
new_state.mygame_defeatable_enemies = {
|
||||
player: enemies.copy() for player, enemies in self.mygame_defeatable_enemies.items()
|
||||
}
|
||||
return new_state
|
||||
```
|
||||
|
||||
After doing this, you can now access `state.mygame_defeatable_enemies[player]` from your access rules.
|
||||
|
||||
@@ -525,7 +525,7 @@ def randomize_entrances(
|
||||
|
||||
running_time = time.perf_counter() - start_time
|
||||
if running_time > 1.0:
|
||||
logging.info(f"Took {running_time:.4f} seconds during entrance randomization for player {world.player},"
|
||||
logging.info(f"Took {running_time:.4f} seconds during entrance randomization for player {world.player}, "
|
||||
f"named {world.multiworld.player_name[world.player]}")
|
||||
|
||||
return er_state
|
||||
|
||||
@@ -208,6 +208,11 @@ Root: HKCR; Subkey: "{#MyAppName}apcivvipatch"; ValueData: "
|
||||
Root: HKCR; Subkey: "{#MyAppName}apcivvipatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLauncher.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}apcivvipatch\shell\open\command"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apeb"; ValueData: "{#MyAppName}ebpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ebpatch"; ValueData: "Archipelago EarthBound Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ebpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ebpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: "";
|
||||
|
||||
12
kvui.py
12
kvui.py
@@ -19,6 +19,7 @@ os.environ["KIVY_NO_CONSOLELOG"] = "1"
|
||||
os.environ["KIVY_NO_FILELOG"] = "1"
|
||||
os.environ["KIVY_NO_ARGS"] = "1"
|
||||
os.environ["KIVY_LOG_ENABLE"] = "0"
|
||||
os.environ["SDL_MOUSE_FOCUS_CLICKTHROUGH"] = "1"
|
||||
|
||||
import Utils
|
||||
|
||||
@@ -35,6 +36,17 @@ Config.set("input", "mouse", "mouse,disable_multitouch")
|
||||
Config.set("kivy", "exit_on_escape", "0")
|
||||
Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers
|
||||
|
||||
# Workaround for Kivy issue #9226.
|
||||
# caused by kivy by default using probesysfs,
|
||||
# which assumes all multi touch deviecs are touch screens.
|
||||
# workaround provided by Snu of the kivy commmunity c:
|
||||
from kivy.utils import platform
|
||||
if platform == "linux":
|
||||
options = Config.options("input")
|
||||
for option in options:
|
||||
if Config.get("input", option) == "probesysfs":
|
||||
Config.remove_option("input", option)
|
||||
|
||||
# Workaround for an issue where importing kivy.core.window before loading sounds
|
||||
# will hang the whole application on Linux once the first sound is loaded.
|
||||
# kivymd imports kivy.core.window, so we have to do this before the first kivymd import.
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
colorama>=0.4.6
|
||||
websockets>=13.0.1,<14
|
||||
PyYAML>=6.0.2
|
||||
jellyfish>=1.1.3
|
||||
PyYAML>=6.0.3
|
||||
jellyfish>=1.2.1
|
||||
jinja2>=3.1.6
|
||||
schema>=0.7.7
|
||||
schema>=0.7.8
|
||||
kivy>=2.3.1
|
||||
bsdiff4>=1.2.6
|
||||
platformdirs>=4.3.6
|
||||
certifi>=2025.4.26
|
||||
cython>=3.0.12
|
||||
cymem>=2.0.11
|
||||
orjson>=3.10.15
|
||||
typing_extensions>=4.12.2
|
||||
pyshortcuts>=1.9.1
|
||||
platformdirs>=4.5.0
|
||||
certifi>=2025.11.12
|
||||
cython>=3.2.1
|
||||
cymem>=2.0.13
|
||||
orjson>=3.11.4
|
||||
typing_extensions>=4.15.0
|
||||
pyshortcuts>=1.9.6
|
||||
pathspec>=0.12.1
|
||||
kivymd @ git+https://github.com/kivymd/KivyMD@5ff9d0d
|
||||
kivymd>=2.0.1.dev0
|
||||
|
||||
# Legacy world dependencies that custom worlds rely on
|
||||
Pymem>=1.13.0
|
||||
|
||||
0
rule_builder/__init__.py
Normal file
0
rule_builder/__init__.py
Normal file
146
rule_builder/cached_world.py
Normal file
146
rule_builder/cached_world.py
Normal file
@@ -0,0 +1,146 @@
|
||||
from collections import defaultdict
|
||||
from typing import ClassVar, cast
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
from BaseClasses import CollectionState, Item, MultiWorld, Region
|
||||
from worlds.AutoWorld import LogicMixin, World
|
||||
|
||||
from .rules import Rule
|
||||
|
||||
|
||||
class CachedRuleBuilderWorld(World):
|
||||
"""A World subclass that provides helpers for interacting with the rule builder"""
|
||||
|
||||
rule_item_dependencies: dict[str, set[int]]
|
||||
"""A mapping of item name to set of rule ids"""
|
||||
|
||||
rule_region_dependencies: dict[str, set[int]]
|
||||
"""A mapping of region name to set of rule ids"""
|
||||
|
||||
rule_location_dependencies: dict[str, set[int]]
|
||||
"""A mapping of location name to set of rule ids"""
|
||||
|
||||
rule_entrance_dependencies: dict[str, set[int]]
|
||||
"""A mapping of entrance name to set of rule ids"""
|
||||
|
||||
item_mapping: ClassVar[dict[str, str]] = {}
|
||||
"""A mapping of actual item name to logical item name.
|
||||
Useful when there are multiple versions of a collected item but the logic only uses one. For example:
|
||||
item = Item("Currency x500"), rule = Has("Currency", count=1000), item_mapping = {"Currency x500": "Currency"}"""
|
||||
|
||||
rule_caching_enabled: ClassVar[bool] = True
|
||||
"""Flag to inform rules that the caching system for this world is enabled. It should not be overridden."""
|
||||
|
||||
def __init__(self, multiworld: MultiWorld, player: int) -> None:
|
||||
super().__init__(multiworld, player)
|
||||
self.rule_item_dependencies = defaultdict(set)
|
||||
self.rule_region_dependencies = defaultdict(set)
|
||||
self.rule_location_dependencies = defaultdict(set)
|
||||
self.rule_entrance_dependencies = defaultdict(set)
|
||||
|
||||
@override
|
||||
def register_rule_dependencies(self, resolved_rule: Rule.Resolved) -> None:
|
||||
for item_name, rule_ids in resolved_rule.item_dependencies().items():
|
||||
self.rule_item_dependencies[item_name] |= rule_ids
|
||||
for region_name, rule_ids in resolved_rule.region_dependencies().items():
|
||||
self.rule_region_dependencies[region_name] |= rule_ids
|
||||
for location_name, rule_ids in resolved_rule.location_dependencies().items():
|
||||
self.rule_location_dependencies[location_name] |= rule_ids
|
||||
for entrance_name, rule_ids in resolved_rule.entrance_dependencies().items():
|
||||
self.rule_entrance_dependencies[entrance_name] |= rule_ids
|
||||
|
||||
def register_rule_builder_dependencies(self) -> None:
|
||||
"""Register all rules that depend on locations or entrances with their dependencies"""
|
||||
for location_name, rule_ids in self.rule_location_dependencies.items():
|
||||
try:
|
||||
location = self.get_location(location_name)
|
||||
except KeyError:
|
||||
continue
|
||||
if not isinstance(location.access_rule, Rule.Resolved):
|
||||
continue
|
||||
for item_name in location.access_rule.item_dependencies():
|
||||
self.rule_item_dependencies[item_name] |= rule_ids
|
||||
for region_name in location.access_rule.region_dependencies():
|
||||
self.rule_region_dependencies[region_name] |= rule_ids
|
||||
|
||||
for entrance_name, rule_ids in self.rule_entrance_dependencies.items():
|
||||
try:
|
||||
entrance = self.get_entrance(entrance_name)
|
||||
except KeyError:
|
||||
continue
|
||||
if not isinstance(entrance.access_rule, Rule.Resolved):
|
||||
continue
|
||||
for item_name in entrance.access_rule.item_dependencies():
|
||||
self.rule_item_dependencies[item_name] |= rule_ids
|
||||
for region_name in entrance.access_rule.region_dependencies():
|
||||
self.rule_region_dependencies[region_name] |= rule_ids
|
||||
|
||||
@override
|
||||
def collect(self, state: CollectionState, item: Item) -> bool:
|
||||
changed = super().collect(state, item)
|
||||
if changed and self.rule_item_dependencies:
|
||||
player_results = cast(dict[int, bool], state.rule_builder_cache[self.player]) # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType]
|
||||
mapped_name = self.item_mapping.get(item.name, "")
|
||||
rule_ids = self.rule_item_dependencies[item.name] | self.rule_item_dependencies[mapped_name]
|
||||
for rule_id in rule_ids:
|
||||
if player_results.get(rule_id, None) is False:
|
||||
del player_results[rule_id]
|
||||
|
||||
return changed
|
||||
|
||||
@override
|
||||
def remove(self, state: CollectionState, item: Item) -> bool:
|
||||
changed = super().remove(state, item)
|
||||
if not changed:
|
||||
return changed
|
||||
|
||||
player_results = cast(dict[int, bool], state.rule_builder_cache[self.player]) # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType]
|
||||
if self.rule_item_dependencies:
|
||||
mapped_name = self.item_mapping.get(item.name, "")
|
||||
rule_ids = self.rule_item_dependencies[item.name] | self.rule_item_dependencies[mapped_name]
|
||||
for rule_id in rule_ids:
|
||||
player_results.pop(rule_id, None)
|
||||
|
||||
# clear all region dependent caches as none can be trusted
|
||||
if self.rule_region_dependencies:
|
||||
for rule_ids in self.rule_region_dependencies.values():
|
||||
for rule_id in rule_ids:
|
||||
player_results.pop(rule_id, None)
|
||||
|
||||
# clear all location dependent caches as they may have lost region access
|
||||
if self.rule_location_dependencies:
|
||||
for rule_ids in self.rule_location_dependencies.values():
|
||||
for rule_id in rule_ids:
|
||||
player_results.pop(rule_id, None)
|
||||
|
||||
# clear all entrance dependent caches as they may have lost region access
|
||||
if self.rule_entrance_dependencies:
|
||||
for rule_ids in self.rule_entrance_dependencies.values():
|
||||
for rule_id in rule_ids:
|
||||
player_results.pop(rule_id, None)
|
||||
|
||||
return changed
|
||||
|
||||
@override
|
||||
def reached_region(self, state: CollectionState, region: Region) -> None:
|
||||
super().reached_region(state, region)
|
||||
if self.rule_region_dependencies:
|
||||
player_results = cast(dict[int, bool], state.rule_builder_cache[self.player]) # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType]
|
||||
for rule_id in self.rule_region_dependencies[region.name]:
|
||||
player_results.pop(rule_id, None)
|
||||
|
||||
|
||||
class CachedRuleBuilderLogicMixin(LogicMixin):
|
||||
multiworld: MultiWorld # pyright: ignore[reportUninitializedInstanceVariable]
|
||||
rule_builder_cache: dict[int, dict[int, bool]] # pyright: ignore[reportUninitializedInstanceVariable]
|
||||
|
||||
def init_mixin(self, multiworld: "MultiWorld") -> None:
|
||||
players = multiworld.get_all_ids()
|
||||
self.rule_builder_cache = {player: {} for player in players}
|
||||
|
||||
def copy_mixin(self, new_state: "CachedRuleBuilderLogicMixin") -> "CachedRuleBuilderLogicMixin":
|
||||
new_state.rule_builder_cache = {
|
||||
player: player_results.copy() for player, player_results in self.rule_builder_cache.items()
|
||||
}
|
||||
return new_state
|
||||
91
rule_builder/options.py
Normal file
91
rule_builder/options.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import dataclasses
|
||||
import importlib
|
||||
import operator
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import Any, Final, Literal, Self, cast
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
from Options import CommonOptions, Option
|
||||
|
||||
Operator = Literal["eq", "ne", "gt", "lt", "ge", "le", "contains", "in"]
|
||||
|
||||
OPERATORS: Final[dict[Operator, Callable[..., bool]]] = {
|
||||
"eq": operator.eq,
|
||||
"ne": operator.ne,
|
||||
"gt": operator.gt,
|
||||
"lt": operator.lt,
|
||||
"ge": operator.ge,
|
||||
"le": operator.le,
|
||||
"contains": operator.contains,
|
||||
"in": operator.contains,
|
||||
}
|
||||
OPERATOR_STRINGS: Final[dict[Operator, str]] = {
|
||||
"eq": "==",
|
||||
"ne": "!=",
|
||||
"gt": ">",
|
||||
"lt": "<",
|
||||
"ge": ">=",
|
||||
"le": "<=",
|
||||
}
|
||||
REVERSE_OPERATORS: Final[tuple[Operator, ...]] = ("in",)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class OptionFilter:
|
||||
option: type[Option[Any]]
|
||||
value: Any
|
||||
operator: Operator = "eq"
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""Returns a JSON compatible dict representation of this option filter"""
|
||||
return {
|
||||
"option": f"{self.option.__module__}.{self.option.__name__}",
|
||||
"value": self.value,
|
||||
"operator": self.operator,
|
||||
}
|
||||
|
||||
def check(self, options: CommonOptions) -> bool:
|
||||
"""Tests the given options dataclass to see if it passes this option filter"""
|
||||
option_name = next(
|
||||
(name for name, cls in options.__class__.type_hints.items() if cls is self.option),
|
||||
None,
|
||||
)
|
||||
if option_name is None:
|
||||
raise ValueError(f"Cannot find option {self.option.__name__} in options class {options.__class__.__name__}")
|
||||
opt = cast(Option[Any] | None, getattr(options, option_name, None))
|
||||
if opt is None:
|
||||
raise ValueError(f"Invalid option: {option_name}")
|
||||
|
||||
fn = OPERATORS[self.operator]
|
||||
return fn(self.value, opt) if self.operator in REVERSE_OPERATORS else fn(opt, self.value)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> Self:
|
||||
"""Returns a new OptionFilter instance from a dict representation"""
|
||||
if "option" not in data or "value" not in data:
|
||||
raise ValueError("Missing required value and/or option")
|
||||
|
||||
option_path = data["option"]
|
||||
try:
|
||||
option_mod_name, option_cls_name = option_path.rsplit(".", 1)
|
||||
option_module = importlib.import_module(option_mod_name)
|
||||
option = getattr(option_module, option_cls_name, None)
|
||||
except (ValueError, ImportError) as e:
|
||||
raise ValueError(f"Cannot parse option '{option_path}'") from e
|
||||
if option is None or not issubclass(option, Option):
|
||||
raise ValueError(f"Invalid option '{option_path}' returns type '{option}' instead of Option subclass")
|
||||
|
||||
value = data["value"]
|
||||
operator = data.get("operator", "eq")
|
||||
return cls(option=cast(type[Option[Any]], option), value=value, operator=operator)
|
||||
|
||||
@classmethod
|
||||
def multiple_from_dict(cls, data: Iterable[dict[str, Any]]) -> tuple[Self, ...]:
|
||||
"""Returns a tuple of OptionFilters instances from an iterable of dict representations"""
|
||||
return tuple(cls.from_dict(o) for o in data)
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
op = OPERATOR_STRINGS.get(self.operator, self.operator)
|
||||
return f"{self.option.__name__} {op} {self.value}"
|
||||
1822
rule_builder/rules.py
Normal file
1822
rule_builder/rules.py
Normal file
File diff suppressed because it is too large
Load Diff
4
setup.py
4
setup.py
@@ -394,11 +394,11 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
|
||||
manifest = json.load(manifest_file)
|
||||
|
||||
assert "game" in manifest, (
|
||||
f"World directory {world_directory} has an archipelago.json manifest file, but it"
|
||||
f"World directory {world_directory} has an archipelago.json manifest file, but it "
|
||||
"does not define a \"game\"."
|
||||
)
|
||||
assert manifest["game"] == worldtype.game, (
|
||||
f"World directory {world_directory} has an archipelago.json manifest file, but value of the"
|
||||
f"World directory {world_directory} has an archipelago.json manifest file, but value of the "
|
||||
f"\"game\" field ({manifest['game']} does not equal the World class's game ({worldtype.game})."
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -248,6 +248,7 @@ class WorldTestBase(unittest.TestCase):
|
||||
with self.subTest("Game", game=self.game, seed=self.multiworld.seed):
|
||||
distribute_items_restrictive(self.multiworld)
|
||||
call_all(self.multiworld, "post_fill")
|
||||
call_all(self.multiworld, "finalize_multiworld")
|
||||
self.assertTrue(fulfills_accessibility(), "Collected all locations, but can't beat the game.")
|
||||
placed_items = [loc.item for loc in self.multiworld.get_locations() if loc.item and loc.item.code]
|
||||
self.assertLessEqual(len(self.multiworld.itempool), len(placed_items),
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import unittest
|
||||
from typing import Callable, Dict, Optional
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
from BaseClasses import CollectionState, MultiWorld, Region
|
||||
from BaseClasses import CollectionRule, MultiWorld, Region
|
||||
from rule_builder.rules import Has, Rule
|
||||
from test.general import TestWorld
|
||||
|
||||
|
||||
class TestHelpers(unittest.TestCase):
|
||||
@@ -16,6 +18,7 @@ class TestHelpers(unittest.TestCase):
|
||||
self.multiworld.game[self.player] = "helper_test_game"
|
||||
self.multiworld.player_name = {1: "Tester"}
|
||||
self.multiworld.set_seed()
|
||||
self.multiworld.worlds[self.player] = TestWorld(self.multiworld, self.player)
|
||||
|
||||
def test_region_helpers(self) -> None:
|
||||
"""Tests `Region.add_locations()` and `Region.add_exits()` have correct behavior"""
|
||||
@@ -46,8 +49,9 @@ class TestHelpers(unittest.TestCase):
|
||||
"TestRegion1": {"TestRegion3"}
|
||||
}
|
||||
|
||||
exit_rules: Dict[str, Callable[[CollectionState], bool]] = {
|
||||
"TestRegion1": lambda state: state.has("test_item", self.player)
|
||||
exit_rules: Dict[str, CollectionRule | Rule[Any]] = {
|
||||
"TestRegion1": lambda state: state.has("test_item", self.player),
|
||||
"TestRegion2": Has("test_item2"),
|
||||
}
|
||||
|
||||
self.multiworld.regions += [Region(region, self.player, self.multiworld, regions[region]) for region in regions]
|
||||
@@ -74,13 +78,17 @@ class TestHelpers(unittest.TestCase):
|
||||
self.assertTrue(f"{parent} -> {exit_reg}" in created_exit_names)
|
||||
if exit_reg in exit_rules:
|
||||
entrance_name = exit_name if exit_name else f"{parent} -> {exit_reg}"
|
||||
self.assertEqual(exit_rules[exit_reg],
|
||||
self.multiworld.get_entrance(entrance_name, self.player).access_rule)
|
||||
rule = exit_rules[exit_reg]
|
||||
if isinstance(rule, Rule):
|
||||
self.assertEqual(rule.resolve(self.multiworld.worlds[self.player]),
|
||||
self.multiworld.get_entrance(entrance_name, self.player).access_rule)
|
||||
else:
|
||||
self.assertEqual(rule, self.multiworld.get_entrance(entrance_name, self.player).access_rule)
|
||||
|
||||
for region in reg_exit_set:
|
||||
for region, exit_set in reg_exit_set.items():
|
||||
current_region = self.multiworld.get_region(region, self.player)
|
||||
current_region.add_exits(reg_exit_set[region])
|
||||
current_region.add_exits(exit_set)
|
||||
exit_names = {_exit.name for _exit in current_region.exits}
|
||||
for reg_exit in reg_exit_set[region]:
|
||||
for reg_exit in exit_set:
|
||||
self.assertTrue(f"{region} -> {reg_exit}" in exit_names,
|
||||
f"{region} -> {reg_exit} not in {exit_names}")
|
||||
|
||||
@@ -88,6 +88,7 @@ class TestIDs(unittest.TestCase):
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
distribute_items_restrictive(multiworld)
|
||||
call_all(multiworld, "post_fill")
|
||||
call_all(multiworld, "finalize_multiworld")
|
||||
datapackage = world_type.get_data_package_data()
|
||||
for item_group, item_names in datapackage["item_name_groups"].items():
|
||||
self.assertIsInstance(item_group, str,
|
||||
|
||||
@@ -46,6 +46,8 @@ class TestImplemented(unittest.TestCase):
|
||||
with self.subTest(game=game_name, seed=multiworld.seed):
|
||||
distribute_items_restrictive(multiworld)
|
||||
call_all(multiworld, "post_fill")
|
||||
call_all(multiworld, "finalize_multiworld")
|
||||
call_all(multiworld, "pre_output")
|
||||
for key, data in multiworld.worlds[1].fill_slot_data().items():
|
||||
self.assertIsInstance(key, str, "keys in slot data must be a string")
|
||||
convert_to_base_types(data) # only put base data types into slot data
|
||||
@@ -93,6 +95,7 @@ class TestImplemented(unittest.TestCase):
|
||||
with self.subTest(game=game_name, seed=multiworld.seed):
|
||||
distribute_items_restrictive(multiworld)
|
||||
call_all(multiworld, "post_fill")
|
||||
call_all(multiworld, "finalize_multiworld")
|
||||
|
||||
# Note: `multiworld.get_spheres()` iterates a set of locations, so the order that locations are checked
|
||||
# is nondeterministic and may vary between runs with the same seed.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import unittest
|
||||
from argparse import Namespace
|
||||
from collections import ChainMap
|
||||
from typing import Type
|
||||
|
||||
from BaseClasses import CollectionState, MultiWorld
|
||||
@@ -82,12 +83,13 @@ class TestBase(unittest.TestCase):
|
||||
|
||||
def test_items_in_datapackage(self):
|
||||
"""Test that any created items in the itempool are in the datapackage"""
|
||||
archipelago = AutoWorldRegister.world_types["Archipelago"]
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest("Game", game=game_name):
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
for item in multiworld.itempool:
|
||||
self.assertIn(item.name, world_type.item_name_to_id)
|
||||
|
||||
self.assertIn(item.name, ChainMap(world_type.item_name_to_id, archipelago.item_name_to_id))
|
||||
|
||||
def test_item_links(self) -> None:
|
||||
"""
|
||||
Tests item link creation by creating a multiworld of 2 worlds for every game and linking their items together.
|
||||
@@ -121,6 +123,7 @@ class TestBase(unittest.TestCase):
|
||||
call_all(multiworld, "pre_fill")
|
||||
distribute_items_restrictive(multiworld)
|
||||
call_all(multiworld, "post_fill")
|
||||
call_all(multiworld, "finalize_multiworld")
|
||||
self.assertTrue(multiworld.can_beat_game(CollectionState(multiworld)), f"seed = {multiworld.seed}")
|
||||
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import unittest
|
||||
|
||||
from BaseClasses import PlandoOptions
|
||||
from Options import Choice, ItemLinks, PlandoConnections, PlandoItems, PlandoTexts
|
||||
from Options import Choice, ItemLinks, OptionSet, PlandoConnections, PlandoItems, PlandoTexts
|
||||
from Utils import restricted_dumps
|
||||
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
|
||||
@@ -44,19 +45,19 @@ class TestOptions(unittest.TestCase):
|
||||
}],
|
||||
[{
|
||||
"name": "ItemLinkGroup",
|
||||
"item_pool": ["Hammer", "Bow"],
|
||||
"item_pool": ["Hammer", "Sword"],
|
||||
"link_replacement": False,
|
||||
"replacement_item": None,
|
||||
}]
|
||||
]
|
||||
# we really need some sort of test world but generic doesn't have enough items for this
|
||||
world = AutoWorldRegister.world_types["A Link to the Past"]
|
||||
world = AutoWorldRegister.world_types["APQuest"]
|
||||
plando_options = PlandoOptions.from_option_string("bosses")
|
||||
item_links = [ItemLinks.from_any(item_link_groups[0]), ItemLinks.from_any(item_link_groups[1])]
|
||||
for link in item_links:
|
||||
link.verify(world, "tester", plando_options)
|
||||
self.assertIn("Hammer", link.value[0]["item_pool"])
|
||||
self.assertIn("Bow", link.value[0]["item_pool"])
|
||||
self.assertIn("Sword", link.value[0]["item_pool"])
|
||||
|
||||
# TODO test that the group created using these options has the items
|
||||
|
||||
@@ -81,6 +82,19 @@ class TestOptions(unittest.TestCase):
|
||||
restricted_dumps(option.from_any(option.default))
|
||||
if issubclass(option, Choice) and option.default in option.name_lookup:
|
||||
restricted_dumps(option.from_text(option.name_lookup[option.default]))
|
||||
|
||||
def test_option_set_keys_random(self):
|
||||
"""Tests that option sets do not contain 'random' and its variants as valid keys"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
if game_name not in ("Archipelago", "Sudoku", "Super Metroid"):
|
||||
for option_key, option in world_type.options_dataclass.type_hints.items():
|
||||
if issubclass(option, OptionSet):
|
||||
with self.subTest(game=game_name, option=option_key):
|
||||
self.assertFalse(any(random_key in option.valid_keys for random_key in ("random",
|
||||
"random-high",
|
||||
"random-low")))
|
||||
for key in option.valid_keys:
|
||||
self.assertFalse("random-range" in key)
|
||||
|
||||
def test_pickle_dumps_plando(self):
|
||||
"""Test that plando options using containers of a custom type can be pickled"""
|
||||
|
||||
@@ -37,3 +37,23 @@ class TestPlayerOptions(unittest.TestCase):
|
||||
self.assertEqual(new_weights["dict_2"]["option_g"], 50)
|
||||
self.assertEqual(len(new_weights["set_1"]), 2)
|
||||
self.assertIn("option_d", new_weights["set_1"])
|
||||
|
||||
def test_update_dict_supports_negatives_and_zeroes(self):
|
||||
original_options = {
|
||||
"dict_1": {"a": 1, "b": -1},
|
||||
"dict_2": {"a": 1, "b": -1},
|
||||
}
|
||||
new_weights = Generate.update_weights(
|
||||
original_options,
|
||||
{
|
||||
"+dict_1": {"a": -2, "b": 2},
|
||||
"-dict_2": {"a": 1, "b": 2},
|
||||
},
|
||||
"Tested",
|
||||
"",
|
||||
)
|
||||
self.assertEqual(new_weights["dict_1"]["a"], -1)
|
||||
self.assertEqual(new_weights["dict_1"]["b"], 1)
|
||||
self.assertEqual(new_weights["dict_2"]["a"], 0)
|
||||
self.assertEqual(new_weights["dict_2"]["b"], -3)
|
||||
self.assertIn("a", new_weights["dict_2"])
|
||||
|
||||
1336
test/general/test_rule_builder.py
Normal file
1336
test/general/test_rule_builder.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -70,13 +70,13 @@ if __name__ == "__main__":
|
||||
empty_file = str(Path(tempdir) / "empty")
|
||||
open(empty_file, "w").close()
|
||||
sys.argv += ["--config_override", empty_file] # tests #5541
|
||||
multis = [["VVVVVV"], ["Temp World"], ["VVVVVV", "Temp World"]]
|
||||
multis = [["APQuest"], ["Temp World"], ["APQuest", "Temp World"]]
|
||||
p1_games: list[str] = []
|
||||
data_paths: list[Path | None] = []
|
||||
rooms: list[str] = []
|
||||
multidata: Path | None
|
||||
|
||||
copy_world("VVVVVV", "Temp World")
|
||||
copy_world("APQuest", "Temp World")
|
||||
try:
|
||||
for n, games in enumerate(multis, 1):
|
||||
print(f"Generating [{n}] {', '.join(games)} offline")
|
||||
|
||||
@@ -20,7 +20,7 @@ def copy(src: str, dst: str) -> None:
|
||||
src_cls = AutoWorldRegister.world_types[src]
|
||||
src_folder = Path(src_cls.__file__).parent
|
||||
worlds_folder = src_folder.parent
|
||||
if (not src_cls.__file__.endswith("__init__.py") or not src_folder.is_dir()
|
||||
if (not src_cls.__file__.endswith(("__init__.py", "world.py")) or not src_folder.is_dir()
|
||||
or not (worlds_folder / "generic").is_dir()):
|
||||
raise ValueError(f"Unsupported layout for copy_world from {src}")
|
||||
dst_folder = worlds_folder / dst_folder_name
|
||||
@@ -28,11 +28,14 @@ def copy(src: str, dst: str) -> None:
|
||||
raise ValueError(f"Destination {dst_folder} already exists")
|
||||
shutil.copytree(src_folder, dst_folder)
|
||||
_new_worlds[dst] = str(dst_folder)
|
||||
with open(dst_folder / "__init__.py", "r", encoding="utf-8-sig") as f:
|
||||
contents = f.read()
|
||||
contents = re.sub(r'game\s*(:\s*[a-zA-Z\[\]]+)?\s*=\s*[\'"]' + re.escape(src) + r'[\'"]', f'game = "{dst}"', contents)
|
||||
with open(dst_folder / "__init__.py", "w", encoding="utf-8") as f:
|
||||
f.write(contents)
|
||||
|
||||
for potential_world_class_file in ("__init__.py", "world.py"):
|
||||
with open(dst_folder / potential_world_class_file, "r", encoding="utf-8-sig") as f:
|
||||
contents = f.read()
|
||||
r_src = re.escape(src)
|
||||
contents = re.sub(r'game\s*(:\s*[a-zA-Z\[\]]+)?\s*=\s*[\'"]' + r_src + r'[\'"]', f'game = "{dst}"', contents)
|
||||
with open(dst_folder / "__init__.py", "w", encoding="utf-8") as f:
|
||||
f.write(contents)
|
||||
|
||||
|
||||
def delete(name: str) -> None:
|
||||
|
||||
@@ -61,6 +61,7 @@ class TestAllGamesMultiworld(MultiworldTestBase):
|
||||
with self.subTest("filling multiworld", seed=self.multiworld.seed):
|
||||
distribute_items_restrictive(self.multiworld)
|
||||
call_all(self.multiworld, "post_fill")
|
||||
call_all(self.multiworld, "finalize_multiworld")
|
||||
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
||||
|
||||
|
||||
@@ -78,4 +79,5 @@ class TestTwoPlayerMulti(MultiworldTestBase):
|
||||
with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed):
|
||||
distribute_items_restrictive(self.multiworld)
|
||||
call_all(self.multiworld, "post_fill")
|
||||
call_all(self.multiworld, "finalize_multiworld")
|
||||
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
||||
|
||||
@@ -25,31 +25,41 @@ class TestGenerateYamlTemplates(unittest.TestCase):
|
||||
if "World: with colon" in worlds.AutoWorld.AutoWorldRegister.world_types:
|
||||
del worlds.AutoWorld.AutoWorldRegister.world_types["World: with colon"]
|
||||
|
||||
|
||||
def test_name_with_colon(self) -> None:
|
||||
from Options import generate_yaml_templates
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
from worlds.AutoWorld import World
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
|
||||
class WebWorldWithColon(WebWorld):
|
||||
options_presets = {
|
||||
"Generic": {
|
||||
"progression_balancing": "disabled",
|
||||
"accessibility": "minimal",
|
||||
}
|
||||
}
|
||||
|
||||
class WorldWithColon(World):
|
||||
game = "World: with colon"
|
||||
item_name_to_id = {}
|
||||
location_name_to_id = {}
|
||||
web = WebWorldWithColon()
|
||||
|
||||
AutoWorldRegister.world_types = {WorldWithColon.game: WorldWithColon}
|
||||
with TemporaryDirectory(f"archipelago_{__name__}") as temp_dir:
|
||||
generate_yaml_templates(temp_dir)
|
||||
path: Path
|
||||
for path in Path(temp_dir).iterdir():
|
||||
self.assertTrue(path.is_file())
|
||||
self.assertTrue(path.suffix == ".yaml")
|
||||
with path.open(encoding="utf-8") as f:
|
||||
try:
|
||||
data = parse_yaml(f)
|
||||
except:
|
||||
f.seek(0)
|
||||
print(f"Error in {path.name}:\n{f.read()}")
|
||||
raise
|
||||
self.assertIn("game", data)
|
||||
self.assertIn(":", data["game"])
|
||||
self.assertIn(data["game"], data)
|
||||
self.assertIsInstance(data[data["game"]], dict)
|
||||
for path in Path(temp_dir).rglob("*"):
|
||||
if path.is_file():
|
||||
self.assertTrue(path.suffix == ".yaml")
|
||||
with path.open(encoding="utf-8") as f:
|
||||
try:
|
||||
data = parse_yaml(f)
|
||||
except:
|
||||
f.seek(0)
|
||||
print(f"Error in {path.name}:\n{f.read()}")
|
||||
raise
|
||||
self.assertIn("game", data)
|
||||
self.assertIn(":", data["game"])
|
||||
self.assertIn(data["game"], data)
|
||||
self.assertIsInstance(data[data["game"]], dict)
|
||||
|
||||
@@ -2,8 +2,8 @@ description: Almost blank test yaml
|
||||
name: Player{NUMBER}
|
||||
|
||||
game:
|
||||
Timespinner: 1 # what else
|
||||
APQuest: 1 # what else
|
||||
requires:
|
||||
version: 0.2.6
|
||||
Timespinner: {}
|
||||
APQuest: {}
|
||||
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
import logging
|
||||
import os
|
||||
from uuid import UUID, uuid4, uuid5
|
||||
|
||||
from flask import url_for
|
||||
|
||||
from WebHostLib.customserver import set_up_logging, tear_down_logging
|
||||
from . import TestBase
|
||||
|
||||
|
||||
def _cleanup_logger(room_id: UUID) -> None:
|
||||
from Utils import user_path
|
||||
tear_down_logging(room_id)
|
||||
try:
|
||||
os.unlink(user_path("logs", f"{room_id}.txt"))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
class TestHostFakeRoom(TestBase):
|
||||
room_id: UUID
|
||||
log_filename: str
|
||||
@@ -39,7 +50,7 @@ class TestHostFakeRoom(TestBase):
|
||||
|
||||
try:
|
||||
os.unlink(self.log_filename)
|
||||
except FileNotFoundError:
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def test_display_log_missing_full(self) -> None:
|
||||
@@ -191,3 +202,27 @@ class TestHostFakeRoom(TestBase):
|
||||
with db_session:
|
||||
commands = select(command for command in Command if command.room.id == self.room_id) # type: ignore
|
||||
self.assertNotIn("/help", (command.commandtext for command in commands))
|
||||
|
||||
def test_logger_teardown(self) -> None:
|
||||
"""Verify that room loggers are removed from the global logging manager."""
|
||||
from WebHostLib.customserver import tear_down_logging
|
||||
room_id = uuid4()
|
||||
self.addCleanup(_cleanup_logger, room_id)
|
||||
set_up_logging(room_id)
|
||||
self.assertIn(f"RoomLogger {room_id}", logging.Logger.manager.loggerDict)
|
||||
tear_down_logging(room_id)
|
||||
self.assertNotIn(f"RoomLogger {room_id}", logging.Logger.manager.loggerDict)
|
||||
|
||||
def test_handler_teardown(self) -> None:
|
||||
"""Verify that handlers for room loggers are closed by tear_down_logging."""
|
||||
from WebHostLib.customserver import tear_down_logging
|
||||
room_id = uuid4()
|
||||
self.addCleanup(_cleanup_logger, room_id)
|
||||
logger = set_up_logging(room_id)
|
||||
handlers = logger.handlers[:]
|
||||
self.assertGreater(len(handlers), 0)
|
||||
|
||||
tear_down_logging(room_id)
|
||||
for handler in handlers:
|
||||
if isinstance(handler, logging.FileHandler):
|
||||
self.assertTrue(handler.stream is None or handler.stream.closed)
|
||||
|
||||
@@ -2,7 +2,7 @@ import unittest
|
||||
|
||||
from BaseClasses import PlandoOptions
|
||||
from worlds import AutoWorldRegister
|
||||
from Options import OptionCounter, NamedRange, NumericOption, OptionList, OptionSet
|
||||
from Options import OptionCounter, NamedRange, NumericOption, OptionList, OptionSet, Visibility
|
||||
|
||||
|
||||
class TestOptionPresets(unittest.TestCase):
|
||||
@@ -19,6 +19,9 @@ class TestOptionPresets(unittest.TestCase):
|
||||
# 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)))
|
||||
if not (Visibility.complex_ui in option.visibility or Visibility.simple_ui in option.visibility):
|
||||
self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' is not "
|
||||
f"visible in any supported UI.")
|
||||
supported_types = [NumericOption, OptionSet, OptionList, OptionCounter]
|
||||
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}' "
|
||||
|
||||
82
typings/kivy/clock.pyi
Normal file
82
typings/kivy/clock.pyi
Normal file
@@ -0,0 +1,82 @@
|
||||
from _typeshed import Incomplete
|
||||
from kivy._clock import (
|
||||
ClockEvent as ClockEvent,
|
||||
ClockNotRunningError as ClockNotRunningError,
|
||||
CyClockBase as CyClockBase,
|
||||
CyClockBaseFree as CyClockBaseFree,
|
||||
FreeClockEvent as FreeClockEvent,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Clock",
|
||||
"ClockNotRunningError",
|
||||
"ClockEvent",
|
||||
"FreeClockEvent",
|
||||
"CyClockBase",
|
||||
"CyClockBaseFree",
|
||||
"triggered",
|
||||
"ClockBaseBehavior",
|
||||
"ClockBaseInterruptBehavior",
|
||||
"ClockBaseInterruptFreeBehavior",
|
||||
"ClockBase",
|
||||
"ClockBaseInterrupt",
|
||||
"ClockBaseFreeInterruptAll",
|
||||
"ClockBaseFreeInterruptOnly",
|
||||
"mainthread",
|
||||
]
|
||||
|
||||
class ClockBaseBehavior:
|
||||
MIN_SLEEP: float
|
||||
SLEEP_UNDERSHOOT: Incomplete
|
||||
def __init__(self, async_lib: str = "asyncio", **kwargs) -> None: ...
|
||||
def init_async_lib(self, lib) -> None: ...
|
||||
@property
|
||||
def frametime(self): ...
|
||||
@property
|
||||
def frames(self): ...
|
||||
@property
|
||||
def frames_displayed(self): ...
|
||||
def usleep(self, microseconds) -> None: ...
|
||||
def idle(self): ...
|
||||
async def async_idle(self): ...
|
||||
def tick(self) -> None: ...
|
||||
async def async_tick(self) -> None: ...
|
||||
def pre_idle(self) -> None: ...
|
||||
def post_idle(self, ts, current): ...
|
||||
def tick_draw(self) -> None: ...
|
||||
def get_fps(self): ...
|
||||
def get_rfps(self): ...
|
||||
def get_time(self): ...
|
||||
def get_boottime(self): ...
|
||||
time: Incomplete
|
||||
def handle_exception(self, e) -> None: ...
|
||||
|
||||
class ClockBaseInterruptBehavior(ClockBaseBehavior):
|
||||
interupt_next_only: bool
|
||||
def __init__(self, interupt_next_only: bool = False, **kwargs) -> None: ...
|
||||
def init_async_lib(self, lib) -> None: ...
|
||||
def usleep(self, microseconds) -> None: ...
|
||||
async def async_usleep(self, microseconds) -> None: ...
|
||||
def on_schedule(self, event) -> None: ...
|
||||
def idle(self): ...
|
||||
async def async_idle(self): ...
|
||||
|
||||
class ClockBaseInterruptFreeBehavior(ClockBaseInterruptBehavior):
|
||||
def __init__(self, **kwargs) -> None: ...
|
||||
def on_schedule(self, event): ...
|
||||
|
||||
class ClockBase(ClockBaseBehavior, CyClockBase):
|
||||
def __init__(self, **kwargs) -> None: ...
|
||||
def usleep(self, microseconds) -> None: ...
|
||||
|
||||
class ClockBaseInterrupt(ClockBaseInterruptBehavior, CyClockBase): ...
|
||||
class ClockBaseFreeInterruptAll(ClockBaseInterruptFreeBehavior, CyClockBaseFree): ...
|
||||
|
||||
class ClockBaseFreeInterruptOnly(ClockBaseInterruptFreeBehavior, CyClockBaseFree):
|
||||
def idle(self): ...
|
||||
async def async_idle(self): ...
|
||||
|
||||
def mainthread(func): ...
|
||||
def triggered(timeout: int = 0, interval: bool = False): ...
|
||||
|
||||
Clock: ClockBase
|
||||
@@ -1,14 +1,26 @@
|
||||
|
||||
from __future__ import annotations
|
||||
import abc
|
||||
from bisect import bisect_right
|
||||
from dataclasses import dataclass
|
||||
import enum
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, ClassVar, Dict, Iterable, Tuple, Any, Optional, Union, TypeGuard
|
||||
from typing import (TYPE_CHECKING, Any, ClassVar, Dict, Generic, Iterable,
|
||||
Optional, Sequence, Tuple, TypeGuard, TypeVar, Union)
|
||||
|
||||
|
||||
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from SNIClient import SNIContext
|
||||
|
||||
SNES_READ_CHUNK_SIZE = 2048
|
||||
"""
|
||||
note: SNI v0.0.101 currently has a bug where reads from
|
||||
RetroArch >2048 bytes will only return the last ~2048 bytes read.
|
||||
https://github.com/alttpo/sni/issues/51
|
||||
"""
|
||||
|
||||
component = Component('SNI Client', 'SNIClient', component_type=Type.CLIENT, file_identifier=SuffixIdentifier(".apsoe"),
|
||||
description="A client for connecting to SNES consoles via Super Nintendo Interface.")
|
||||
components.append(component)
|
||||
@@ -91,3 +103,119 @@ class SNIClient(abc.ABC, metaclass=AutoSNIClientRegister):
|
||||
def on_package(self, ctx: SNIContext, cmd: str, args: Dict[str, Any]) -> None:
|
||||
""" override this with code to handle packages from the server """
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True, order=True)
|
||||
class Read:
|
||||
""" snes memory read - address and size in bytes """
|
||||
address: int
|
||||
size: int
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class _MemRead:
|
||||
location: Read
|
||||
data: bytes
|
||||
|
||||
|
||||
_T_Enum = TypeVar("_T_Enum", bound=enum.Enum)
|
||||
|
||||
|
||||
class SnesData(Generic[_T_Enum]):
|
||||
_ranges: Sequence[_MemRead]
|
||||
""" sorted by address """
|
||||
|
||||
def __init__(self, ranges: Sequence[tuple[Read, bytes]]) -> None:
|
||||
self._ranges = [_MemRead(r, d) for r, d in ranges]
|
||||
|
||||
def get(self, read: _T_Enum) -> bytes:
|
||||
assert isinstance(read.value, Read), read.value
|
||||
address = read.value.address
|
||||
index = bisect_right(self._ranges, address, key=lambda r: r.location.address) - 1
|
||||
assert index >= 0, (self._ranges, read.value)
|
||||
mem_read = self._ranges[index]
|
||||
sub_index = address - mem_read.location.address
|
||||
return mem_read.data[sub_index:sub_index + read.value.size]
|
||||
|
||||
|
||||
class SnesReader(Generic[_T_Enum]):
|
||||
"""
|
||||
how to use:
|
||||
```
|
||||
from enum import Enum
|
||||
from worlds.AutoSNIClient import Read, SNIClient, SnesReader
|
||||
|
||||
class MyGameMemory(Enum):
|
||||
game_mode = Read(WRAM_START + 0x0998, 1)
|
||||
send_queue = Read(SEND_QUEUE_START, 8 * 127)
|
||||
...
|
||||
|
||||
snes_reader = SnesReader(MyGameMemory)
|
||||
|
||||
snes_data = await snes_reader.read(ctx)
|
||||
if snes_data is None:
|
||||
snes_logger.info("error reading from snes")
|
||||
return
|
||||
|
||||
game_mode = snes_data.get(MyGameMemory.game_mode)
|
||||
```
|
||||
"""
|
||||
_ranges: Sequence[Read]
|
||||
""" sorted by address """
|
||||
|
||||
def __init__(self, reads: type[_T_Enum]) -> None:
|
||||
self._ranges = self._make_ranges(reads)
|
||||
|
||||
@staticmethod
|
||||
def _make_ranges(reads: type[enum.Enum]) -> Sequence[Read]:
|
||||
|
||||
unprocessed_reads: list[Read] = []
|
||||
for e in reads:
|
||||
assert isinstance(e.value, Read), (reads.__name__, e, e.value)
|
||||
unprocessed_reads.append(e.value)
|
||||
unprocessed_reads.sort()
|
||||
|
||||
ranges: list[Read] = []
|
||||
for read in unprocessed_reads:
|
||||
# v end of the previous range
|
||||
if len(ranges) == 0 or read.address - (ranges[-1].address + ranges[-1].size) > 255:
|
||||
ranges.append(read)
|
||||
else: # combine with previous range
|
||||
chunk_address = ranges[-1].address
|
||||
assert read.address >= chunk_address, "sort() didn't work? or something"
|
||||
original_chunk_size = ranges[-1].size
|
||||
new_size = max((read.address + read.size) - chunk_address,
|
||||
original_chunk_size)
|
||||
ranges[-1] = Read(chunk_address, new_size)
|
||||
logging.debug(f"{len(ranges)=} {max(r.size for r in ranges)=}")
|
||||
return ranges
|
||||
|
||||
async def read(self, ctx: "SNIContext") -> SnesData[_T_Enum] | None:
|
||||
"""
|
||||
returns `None` if reading fails,
|
||||
otherwise returns the data for the registered `Enum`
|
||||
"""
|
||||
from SNIClient import snes_read
|
||||
|
||||
reads: list[tuple[Read, bytes]] = []
|
||||
for r in self._ranges:
|
||||
if r.size < SNES_READ_CHUNK_SIZE: # most common
|
||||
response = await snes_read(ctx, r.address, r.size)
|
||||
if response is None:
|
||||
return None
|
||||
reads.append((r, response))
|
||||
else: # big read
|
||||
# Problems were reported with big reads,
|
||||
# so we chunk it into smaller pieces.
|
||||
read_so_far = 0
|
||||
collection: list[bytes] = []
|
||||
while read_so_far < r.size:
|
||||
remaining_size = r.size - read_so_far
|
||||
chunk_size = min(SNES_READ_CHUNK_SIZE, remaining_size)
|
||||
response = await snes_read(ctx, r.address + read_so_far, chunk_size)
|
||||
if response is None:
|
||||
return None
|
||||
collection.append(response)
|
||||
read_so_far += chunk_size
|
||||
reads.append((r, b"".join(collection)))
|
||||
return SnesData(reads)
|
||||
|
||||
@@ -5,17 +5,18 @@ import logging
|
||||
import pathlib
|
||||
import sys
|
||||
import time
|
||||
from collections.abc import Callable, Iterable, Mapping
|
||||
from random import Random
|
||||
from dataclasses import make_dataclass
|
||||
from typing import (Any, Callable, ClassVar, Dict, FrozenSet, Iterable, List, Mapping, Optional, Set, TextIO, Tuple,
|
||||
from typing import (Any, ClassVar, Dict, FrozenSet, List, Optional, Self, Set, TextIO, Tuple,
|
||||
TYPE_CHECKING, Type, Union)
|
||||
|
||||
from Options import item_and_loc_options, ItemsAccessibility, OptionGroup, PerGameCommonOptions
|
||||
from BaseClasses import CollectionState
|
||||
from BaseClasses import CollectionState, Entrance
|
||||
from rule_builder.rules import CustomRuleRegister, Rule
|
||||
from Utils import Version
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from BaseClasses import MultiWorld, Item, Location, Tutorial, Region, Entrance
|
||||
from BaseClasses import CollectionRule, Item, Location, MultiWorld, Region, Tutorial
|
||||
from NetUtils import GamesPackage, MultiData
|
||||
from settings import Group
|
||||
|
||||
@@ -47,27 +48,31 @@ class AutoWorldRegister(type):
|
||||
def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoWorldRegister:
|
||||
if "web" in dct:
|
||||
assert isinstance(dct["web"], WebWorld), "WebWorld has to be instantiated."
|
||||
# filter out any events
|
||||
dct["item_name_to_id"] = {name: id for name, id in dct["item_name_to_id"].items() if id}
|
||||
dct["location_name_to_id"] = {name: id for name, id in dct["location_name_to_id"].items() if id}
|
||||
# build reverse lookups
|
||||
dct["item_id_to_name"] = {code: name for name, code in dct["item_name_to_id"].items()}
|
||||
dct["location_id_to_name"] = {code: name for name, code in dct["location_name_to_id"].items()}
|
||||
|
||||
# build rest
|
||||
dct["item_names"] = frozenset(dct["item_name_to_id"])
|
||||
dct["item_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set
|
||||
in dct.get("item_name_groups", {}).items()}
|
||||
dct["item_name_groups"]["Everything"] = dct["item_names"]
|
||||
|
||||
dct["location_names"] = frozenset(dct["location_name_to_id"])
|
||||
dct["location_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set
|
||||
in dct.get("location_name_groups", {}).items()}
|
||||
dct["location_name_groups"]["Everywhere"] = dct["location_names"]
|
||||
dct["all_item_and_group_names"] = frozenset(dct["item_names"] | set(dct.get("item_name_groups", {})))
|
||||
|
||||
# move away from get_required_client_version function
|
||||
if "game" in dct:
|
||||
assert "item_name_to_id" in dct, f"{name}: item_name_to_id is required"
|
||||
assert "location_name_to_id" in dct, f"{name}: location_name_to_id is required"
|
||||
|
||||
# filter out any events
|
||||
dct["item_name_to_id"] = {name: id for name, id in dct["item_name_to_id"].items() if id}
|
||||
dct["location_name_to_id"] = {name: id for name, id in dct["location_name_to_id"].items() if id}
|
||||
# build reverse lookups
|
||||
dct["item_id_to_name"] = {code: name for name, code in dct["item_name_to_id"].items()}
|
||||
dct["location_id_to_name"] = {code: name for name, code in dct["location_name_to_id"].items()}
|
||||
|
||||
# build rest
|
||||
dct["item_names"] = frozenset(dct["item_name_to_id"])
|
||||
dct["item_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set
|
||||
in dct.get("item_name_groups", {}).items()}
|
||||
dct["item_name_groups"]["Everything"] = dct["item_names"]
|
||||
|
||||
dct["location_names"] = frozenset(dct["location_name_to_id"])
|
||||
dct["location_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set
|
||||
in dct.get("location_name_groups", {}).items()}
|
||||
dct["location_name_groups"]["Everywhere"] = dct["location_names"]
|
||||
dct["all_item_and_group_names"] = frozenset(dct["item_names"] | set(dct.get("item_name_groups", {})))
|
||||
|
||||
# move away from get_required_client_version function
|
||||
assert "get_required_client_version" not in dct, f"{name}: required_client_version is an attribute now"
|
||||
# set minimum required_client_version from bases
|
||||
if "required_client_version" in dct and bases:
|
||||
@@ -173,7 +178,8 @@ def _timed_call(method: Callable[..., Any], *args: Any,
|
||||
|
||||
|
||||
def call_single(multiworld: "MultiWorld", method_name: str, player: int, *args: Any) -> Any:
|
||||
method = getattr(multiworld.worlds[player], method_name)
|
||||
world = multiworld.worlds[player]
|
||||
method = getattr(world, method_name)
|
||||
try:
|
||||
ret = _timed_call(method, *args, multiworld=multiworld, player=player)
|
||||
except Exception as e:
|
||||
@@ -184,6 +190,10 @@ def call_single(multiworld: "MultiWorld", method_name: str, player: int, *args:
|
||||
logging.error(message)
|
||||
raise e
|
||||
else:
|
||||
# Convenience for CachedRuleBuilderWorld users: Ensure that caching setup function is called
|
||||
# Can be removed once dependency system is improved
|
||||
if method_name == "set_rules" and hasattr(world, "register_rule_builder_dependencies"):
|
||||
call_single(multiworld, "register_rule_builder_dependencies", player)
|
||||
return ret
|
||||
|
||||
|
||||
@@ -353,7 +363,7 @@ class World(metaclass=AutoWorldRegister):
|
||||
|
||||
def __getattr__(self, item: str) -> Any:
|
||||
if item == "settings":
|
||||
return self.__class__.settings
|
||||
return getattr(self.__class__, item)
|
||||
raise AttributeError
|
||||
|
||||
# overridable methods that get called by Main.py, sorted by execution order
|
||||
@@ -420,6 +430,23 @@ class World(metaclass=AutoWorldRegister):
|
||||
This happens before progression balancing, so the items may not be in their final locations yet.
|
||||
"""
|
||||
|
||||
def finalize_multiworld(self) -> None:
|
||||
"""
|
||||
Optional Method that is called after fill and progression balancing.
|
||||
This is the last stage of generation where worlds may change logically relevant data,
|
||||
such as item placements and connections. To not break assumptions,
|
||||
only ever increase accessibility, never decrease it.
|
||||
"""
|
||||
pass
|
||||
|
||||
def pre_output(self):
|
||||
"""
|
||||
Optional method that is called before output generation.
|
||||
Items and connections are not meant to be moved anymore,
|
||||
anything that would affect logical spheres is forbidden at this point.
|
||||
"""
|
||||
pass
|
||||
|
||||
def generate_output(self, output_directory: str) -> None:
|
||||
"""
|
||||
This method gets called from a threadpool, do not use multiworld.random here.
|
||||
@@ -484,7 +511,14 @@ class World(metaclass=AutoWorldRegister):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
"""Called when the item pool needs to be filled with additional items to match location count."""
|
||||
"""
|
||||
Called when the item pool needs to be filled with additional items to match location count.
|
||||
|
||||
Any returned item name must be for a "repeatable" item, i.e. one that it's okay to generate arbitrarily many of.
|
||||
For most worlds this will be one or more of your filler items, but the classification of these items
|
||||
does not need to be ItemClassification.filler.
|
||||
The item name returned can be for a trap, useful, and/or progression item as long as it's repeatable.
|
||||
"""
|
||||
logging.warning(f"World {self} is generating a filler item without custom filler pool.")
|
||||
return self.random.choice(tuple(self.item_name_to_id.keys()))
|
||||
|
||||
@@ -538,6 +572,10 @@ class World(metaclass=AutoWorldRegister):
|
||||
return True
|
||||
return False
|
||||
|
||||
def reached_region(self, state: "CollectionState", region: "Region") -> None:
|
||||
"""Called when a region is newly reachable by the state."""
|
||||
pass
|
||||
|
||||
# following methods should not need to be overridden.
|
||||
def create_filler(self) -> "Item":
|
||||
return self.create_item(self.get_filler_item_name())
|
||||
@@ -586,6 +624,64 @@ class World(metaclass=AutoWorldRegister):
|
||||
res["checksum"] = data_package_checksum(res)
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def get_rule_cls(cls, name: str) -> type[Rule[Self]]:
|
||||
"""Returns the world-registered or default rule with the given name"""
|
||||
return CustomRuleRegister.get_rule_cls(cls.game, name)
|
||||
|
||||
@classmethod
|
||||
def rule_from_dict(cls, data: Mapping[str, Any]) -> Rule[Self]:
|
||||
"""Create a rule instance from a serialized dict representation"""
|
||||
name = data.get("rule", "")
|
||||
rule_class = cls.get_rule_cls(name)
|
||||
return rule_class.from_dict(data, cls)
|
||||
|
||||
def set_rule(self, spot: Location | Entrance, rule: CollectionRule | Rule[Any]) -> None:
|
||||
"""Sets an access rule for a location or entrance"""
|
||||
if isinstance(rule, Rule):
|
||||
rule = rule.resolve(self)
|
||||
self.register_rule_dependencies(rule)
|
||||
if isinstance(spot, Entrance):
|
||||
self._register_rule_indirects(rule, spot)
|
||||
spot.access_rule = rule
|
||||
|
||||
def set_completion_rule(self, rule: CollectionRule | Rule[Any]) -> None:
|
||||
"""Set the completion rule for this world"""
|
||||
if isinstance(rule, Rule):
|
||||
rule = rule.resolve(self)
|
||||
self.register_rule_dependencies(rule)
|
||||
self.multiworld.completion_condition[self.player] = rule
|
||||
|
||||
def create_entrance(
|
||||
self,
|
||||
from_region: Region,
|
||||
to_region: Region,
|
||||
rule: CollectionRule | Rule[Any] | None = None,
|
||||
name: str | None = None,
|
||||
force_creation: bool = False,
|
||||
) -> Entrance | None:
|
||||
"""Try to create an entrance between regions with the given rule,
|
||||
skipping it if the rule resolves to False (unless force_creation is True)"""
|
||||
if rule is not None and isinstance(rule, Rule):
|
||||
rule = rule.resolve(self)
|
||||
if rule.always_false and not force_creation:
|
||||
return None
|
||||
self.register_rule_dependencies(rule)
|
||||
|
||||
entrance = from_region.connect(to_region, name, rule=rule)
|
||||
if rule and isinstance(rule, Rule.Resolved):
|
||||
self._register_rule_indirects(rule, entrance)
|
||||
return entrance
|
||||
|
||||
def register_rule_dependencies(self, resolved_rule: Rule.Resolved) -> None:
|
||||
"""Hook for registering dependencies when a rule is assigned for this world"""
|
||||
pass
|
||||
|
||||
def _register_rule_indirects(self, resolved_rule: Rule.Resolved, entrance: Entrance) -> None:
|
||||
if self.explicit_indirect_conditions:
|
||||
for indirect_region in resolved_rule.region_dependencies().keys():
|
||||
self.multiworld.register_indirect_condition(self.get_region(indirect_region), entrance)
|
||||
|
||||
|
||||
# any methods attached to this can be used as part of CollectionState,
|
||||
# please use a prefix as all of them get clobbered together
|
||||
|
||||
@@ -5,7 +5,7 @@ import weakref
|
||||
from enum import Enum, auto
|
||||
from typing import Optional, Callable, List, Iterable, Tuple
|
||||
|
||||
from Utils import local_path, open_filename, is_frozen, is_kivy_running, open_file, user_path
|
||||
from Utils import local_path, open_filename, is_frozen, is_kivy_running, open_file, user_path, read_apignore
|
||||
|
||||
|
||||
class Type(Enum):
|
||||
@@ -247,7 +247,8 @@ components: List[Component] = [
|
||||
# MegaMan Battle Network 3
|
||||
Component('MMBN3 Client', 'MMBN3Client', file_identifier=SuffixIdentifier('.apbn3')),
|
||||
|
||||
Component("Export Datapackage", func=export_datapackage, component_type=Type.TOOL),
|
||||
Component("Export Datapackage", func=export_datapackage, component_type=Type.TOOL,
|
||||
description="Write item/location data for installed worlds to a file and open it."),
|
||||
]
|
||||
|
||||
|
||||
@@ -278,6 +279,10 @@ if not is_frozen():
|
||||
games = [(worldname, worldtype) for worldname, worldtype in AutoWorldRegister.world_types.items()
|
||||
if not worldtype.zip_path]
|
||||
|
||||
global_apignores = read_apignore(local_path("data", "GLOBAL.apignore"))
|
||||
if not global_apignores:
|
||||
raise RuntimeError("Could not read global apignore file for build component")
|
||||
|
||||
apworlds_folder = os.path.join("build", "apworlds")
|
||||
os.makedirs(apworlds_folder, exist_ok=True)
|
||||
for worldname, worldtype in games:
|
||||
@@ -291,11 +296,11 @@ if not is_frozen():
|
||||
manifest = json.load(manifest_file)
|
||||
|
||||
assert "game" in manifest, (
|
||||
f"World directory {world_directory} has an archipelago.json manifest file, but it"
|
||||
f"World directory {world_directory} has an archipelago.json manifest file, but it "
|
||||
"does not define a \"game\"."
|
||||
)
|
||||
assert manifest["game"] == worldtype.game, (
|
||||
f"World directory {world_directory} has an archipelago.json manifest file, but value of the"
|
||||
f"World directory {world_directory} has an archipelago.json manifest file, but value of the "
|
||||
f"\"game\" field ({manifest['game']} does not equal the World class's game ({worldtype.game})."
|
||||
)
|
||||
else:
|
||||
@@ -305,18 +310,17 @@ if not is_frozen():
|
||||
apworld = APWorldContainer(str(zip_path))
|
||||
apworld.game = worldtype.game
|
||||
manifest.update(apworld.get_manifest())
|
||||
apworld.manifest_path = f"{file_name}/archipelago.json"
|
||||
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED,
|
||||
compresslevel=9) as zf:
|
||||
for path in pathlib.Path(world_directory).rglob("*"):
|
||||
relative_path = os.path.join(*path.parts[path.parts.index("worlds") + 1:])
|
||||
if "__MACOSX" in relative_path or ".DS_STORE" in relative_path or "__pycache__" in relative_path:
|
||||
continue
|
||||
if not relative_path.endswith("archipelago.json"):
|
||||
zf.write(path, relative_path)
|
||||
apworld.manifest_path = os.path.join(file_name, "archipelago.json")
|
||||
|
||||
local_ignores = read_apignore(pathlib.Path(world_directory, ".apignore"))
|
||||
apignores = global_apignores + local_ignores if local_ignores else global_apignores
|
||||
|
||||
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
|
||||
for file in apignores.match_tree_files(world_directory, negate=True):
|
||||
zf.write(pathlib.Path(world_directory, file), pathlib.Path(file_name, file))
|
||||
|
||||
zf.writestr(apworld.manifest_path, json.dumps(manifest))
|
||||
open_folder(apworlds_folder)
|
||||
|
||||
|
||||
components.append(Component('Build APWorlds', func=_build_apworlds, cli=True,
|
||||
components.append(Component("Build APWorlds", func=_build_apworlds, cli=True,
|
||||
description="Build APWorlds from loose-file world folders."))
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import importlib
|
||||
import importlib.util
|
||||
import importlib.abc
|
||||
import importlib.machinery
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
import zipimport
|
||||
import time
|
||||
import dataclasses
|
||||
import json
|
||||
from typing import List
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import List, Sequence
|
||||
from zipfile import BadZipFile
|
||||
|
||||
from NetUtils import DataPackage
|
||||
from Utils import local_path, user_path, Version, version_tuple, tuplize_version
|
||||
from Utils import local_path, user_path, Version, version_tuple, tuplize_version, messagebox
|
||||
|
||||
local_folder = os.path.dirname(__file__)
|
||||
user_folder = user_path("worlds") if user_path() != local_path() else user_path("custom_worlds")
|
||||
@@ -20,14 +23,14 @@ try:
|
||||
except OSError: # can't access/write?
|
||||
user_folder = None
|
||||
|
||||
__all__ = {
|
||||
__all__ = [
|
||||
"network_data_package",
|
||||
"AutoWorldRegister",
|
||||
"world_sources",
|
||||
"local_folder",
|
||||
"user_folder",
|
||||
"failed_world_loads",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
failed_world_loads: List[str] = []
|
||||
@@ -53,21 +56,7 @@ class WorldSource:
|
||||
def load(self) -> bool:
|
||||
try:
|
||||
start = time.perf_counter()
|
||||
if self.is_zip:
|
||||
importer = zipimport.zipimporter(self.resolved_path)
|
||||
spec = importer.find_spec(os.path.basename(self.path).rsplit(".", 1)[0])
|
||||
assert spec, f"{self.path} is not a loadable module"
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
|
||||
mod.__package__ = f"worlds.{mod.__package__}"
|
||||
|
||||
mod.__name__ = f"worlds.{mod.__name__}"
|
||||
sys.modules[mod.__name__] = mod
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore", message="__package__ != __spec__.parent")
|
||||
importer.exec_module(mod)
|
||||
else:
|
||||
importlib.import_module(f".{self.path}", "worlds")
|
||||
importlib.import_module(f".{Path(self.path).stem}", "worlds")
|
||||
self.time_taken = time.perf_counter()-start
|
||||
return True
|
||||
|
||||
@@ -112,7 +101,6 @@ for world_source in world_sources:
|
||||
else:
|
||||
world_source.load()
|
||||
|
||||
|
||||
from .AutoWorld import AutoWorldRegister
|
||||
|
||||
for world_source in world_sources:
|
||||
@@ -157,6 +145,15 @@ if apworlds:
|
||||
logging.error(e)
|
||||
else:
|
||||
raise e
|
||||
except BadZipFile as e:
|
||||
err_message = (f"The world source {apworld_source.resolved_path} is not a valid zip. "
|
||||
"It is likely either corrupted, or was packaged incorrectly.")
|
||||
|
||||
if sys.stdout:
|
||||
raise RuntimeError(err_message) from e
|
||||
else:
|
||||
messagebox("Couldn't load worlds", err_message, error=True)
|
||||
sys.exit(1)
|
||||
|
||||
if apworld.minimum_ap_version and apworld.minimum_ap_version > version_tuple:
|
||||
fail_world(apworld.game,
|
||||
@@ -174,6 +171,16 @@ if apworlds:
|
||||
core_compatible.sort(
|
||||
key=lambda element: element[1].world_version if element[1].world_version else Version(0, 0, 0),
|
||||
reverse=True)
|
||||
|
||||
apworld_module_specs = {}
|
||||
class APWorldModuleFinder(importlib.abc.MetaPathFinder):
|
||||
def find_spec(
|
||||
self, fullname: str, _path: Sequence[str] | None, _target: ModuleType = None
|
||||
) -> importlib.machinery.ModuleSpec | None:
|
||||
return apworld_module_specs.get(fullname)
|
||||
|
||||
sys.meta_path.insert(0, APWorldModuleFinder())
|
||||
|
||||
for apworld_source, apworld in core_compatible:
|
||||
if apworld.game and apworld.game in AutoWorldRegister.world_types:
|
||||
fail_world(apworld.game,
|
||||
@@ -181,6 +188,12 @@ if apworlds:
|
||||
f"as its game {apworld.game} is already loaded.",
|
||||
add_as_failed_to_load=False)
|
||||
else:
|
||||
importer = zipimport.zipimporter(apworld_source.resolved_path)
|
||||
world_name = Path(apworld.path).stem
|
||||
|
||||
spec = importer.find_spec(f"worlds.{world_name}")
|
||||
apworld_module_specs[f"worlds.{world_name}"] = spec
|
||||
|
||||
apworld_source.load()
|
||||
if apworld.game in AutoWorldRegister.world_types:
|
||||
# world could fail to load at this point
|
||||
|
||||
@@ -2,4 +2,4 @@ mpyq>=0.2.5
|
||||
portpicker>=1.5.2
|
||||
aiohttp>=3.8.4
|
||||
loguru>=0.7.0
|
||||
protobuf==6.31.1
|
||||
protobuf==6.33.5
|
||||
|
||||
@@ -63,6 +63,9 @@ def is_location_valid(world: "HatInTimeWorld", location: str) -> bool:
|
||||
if not world.options.ShuffleStorybookPages and location in storybook_pages.keys():
|
||||
return False
|
||||
|
||||
if not world.options.ShuffleDirectorTokens and location in director_tokens.keys():
|
||||
return False
|
||||
|
||||
if not world.options.ShuffleActContracts and location in contract_locations.keys():
|
||||
return False
|
||||
|
||||
@@ -566,6 +569,34 @@ storybook_pages = {
|
||||
"Rumbi Factory - Page: Last Area": LocData(2000345883, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2),
|
||||
}
|
||||
|
||||
director_tokens = {
|
||||
"Murder on the Owl Express - Conductor Token: Cafeteria": LocData(2001104767, "Murder on the Owl Express"),
|
||||
"Murder on the Owl Express - Conductor Token: Recreational Room": LocData(2001104768, "Murder on the Owl Express"),
|
||||
"Picture Perfect - DJ Grooves Token: Cardboard Puppy": LocData(2001203990, "Picture Perfect"),
|
||||
"Picture Perfect - DJ Grooves Token: Card Guessing Game": LocData(2001203991, "Picture Perfect"),
|
||||
"Picture Perfect - DJ Grooves Token: Back Alley": LocData(2001203992, "Picture Perfect"),
|
||||
"Picture Perfect - DJ Grooves Token: Cooking Show": LocData(2001203993, "Picture Perfect"),
|
||||
"Picture Perfect - DJ Grooves Token: Pon Cluster": LocData(2001203987, "Picture Perfect"),
|
||||
"Train Rush - Time Bonus: 1st Room": LocData(2001305235, "Train Rush", hookshot=True),
|
||||
"Train Rush - Time Bonus: Falling Platform": LocData(2001305189, "Train Rush", hookshot=True),
|
||||
"Train Rush - Time Bonus: Acid Crates": LocData(2001305186, "Train Rush", hookshot=True),
|
||||
"Train Rush - Time Bonus: Balloon": LocData(2001305239, "Train Rush", hookshot=True),
|
||||
"Train Rush - Time Bonus: Ring of Fire": LocData(2001305237, "Train Rush", hookshot=True),
|
||||
"Train Rush - Time Bonus: Blue Panels": LocData(2001305236, "Train Rush", hookshot=True),
|
||||
"Train Rush - Time Bonus: Sinking Lava Platform": LocData(2001305234, "Train Rush", hookshot=True),
|
||||
"Train Rush - Time Bonus: Lava Panels 1": LocData(2001305193, "Train Rush", hookshot=True),
|
||||
"Train Rush - Time Bonus: Lava Panels 2": LocData(2001305190, "Train Rush", hookshot=True),
|
||||
"Train Rush - Time Bonus: Lava Panels 3": LocData(2001305238, "Train Rush", hookshot=True),
|
||||
"The Big Parade - DJ Grooves Token (1/8)": LocData(2001400000, "The Big Parade"),
|
||||
"The Big Parade - DJ Grooves Token (2/8)": LocData(2001400001, "The Big Parade"),
|
||||
"The Big Parade - DJ Grooves Token (3/8)": LocData(2001400002, "The Big Parade"),
|
||||
"The Big Parade - DJ Grooves Token (4/8)": LocData(2001400003, "The Big Parade"),
|
||||
"The Big Parade - DJ Grooves Token (5/8)": LocData(2001400004, "The Big Parade", hit_type=HitType.umbrella),
|
||||
"The Big Parade - DJ Grooves Token (6/8)": LocData(2001400005, "The Big Parade", hit_type=HitType.umbrella),
|
||||
"The Big Parade - DJ Grooves Token (7/8)": LocData(2001400006, "The Big Parade", hit_type=HitType.umbrella),
|
||||
"The Big Parade - DJ Grooves Token (8/8)": LocData(2001400007, "The Big Parade", hit_type=HitType.umbrella),
|
||||
}
|
||||
|
||||
shop_locations = {
|
||||
"Badge Seller - Item 1": LocData(2000301003, "Badge Seller"),
|
||||
"Badge Seller - Item 2": LocData(2000301004, "Badge Seller"),
|
||||
@@ -1050,6 +1081,7 @@ location_table = {
|
||||
**ahit_locations,
|
||||
**act_completions,
|
||||
**storybook_pages,
|
||||
**director_tokens,
|
||||
**contract_locations,
|
||||
**shop_locations,
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ def adjust_options(world: "HatInTimeWorld"):
|
||||
world.options.EndGoal.value = EndGoal.option_seal_the_deal
|
||||
world.options.ActRandomizer.value = 0
|
||||
world.options.ShuffleAlpineZiplines.value = 0
|
||||
world.options.ShuffleDirectorTokens.value = 0
|
||||
world.options.ShuffleSubconPaintings.value = 0
|
||||
world.options.ShuffleStorybookPages.value = 0
|
||||
world.options.ShuffleActContracts.value = 0
|
||||
@@ -219,6 +220,12 @@ class ShuffleStorybookPages(DefaultOnToggle):
|
||||
display_name = "Shuffle Storybook Pages"
|
||||
|
||||
|
||||
class ShuffleDirectorTokens(Toggle):
|
||||
"""If enabled, causes the Conductor/DJ Grooves tokens found in Chapter 2 levels to become item checks.
|
||||
NOTE: This also includes the time bonus pickups from Train Rush, since the level doesn't have any tokens."""
|
||||
display_name = "Shuffle Director Tokens"
|
||||
|
||||
|
||||
class ShuffleActContracts(DefaultOnToggle):
|
||||
"""If enabled, shuffle Snatcher's act contracts into the pool as items"""
|
||||
display_name = "Shuffle Contracts"
|
||||
@@ -658,6 +665,7 @@ class AHITOptions(PerGameCommonOptions):
|
||||
StartWithCompassBadge: StartWithCompassBadge
|
||||
CompassBadgeMode: CompassBadgeMode
|
||||
ShuffleStorybookPages: ShuffleStorybookPages
|
||||
ShuffleDirectorTokens: ShuffleDirectorTokens
|
||||
ShuffleActContracts: ShuffleActContracts
|
||||
ShuffleSubconPaintings: ShuffleSubconPaintings
|
||||
NoPaintingSkips: NoPaintingSkips
|
||||
@@ -722,7 +730,8 @@ class AHITOptions(PerGameCommonOptions):
|
||||
|
||||
|
||||
ahit_option_groups: Dict[str, List[Any]] = {
|
||||
"General Options": [EndGoal, ShuffleStorybookPages, ShuffleAlpineZiplines, ShuffleSubconPaintings,
|
||||
"General Options": [EndGoal, ShuffleStorybookPages, ShuffleDirectorTokens,
|
||||
ShuffleAlpineZiplines, ShuffleSubconPaintings,
|
||||
ShuffleActContracts, MinPonCost, MaxPonCost, BadgeSellerMinItems, BadgeSellerMaxItems,
|
||||
LogicDifficulty, NoPaintingSkips, CTRLogic],
|
||||
|
||||
@@ -759,6 +768,7 @@ slot_data_options: List[str] = [
|
||||
"StartWithCompassBadge",
|
||||
"CompassBadgeMode",
|
||||
"ShuffleStorybookPages",
|
||||
"ShuffleDirectorTokens",
|
||||
"ShuffleActContracts",
|
||||
"ShuffleSubconPaintings",
|
||||
"NoPaintingSkips",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from BaseClasses import Region, Entrance, ItemClassification, Location, LocationProgressType
|
||||
from .Types import ChapterIndex, Difficulty, HatInTimeLocation, HatInTimeItem
|
||||
from .Locations import location_table, storybook_pages, event_locs, is_location_valid, \
|
||||
shop_locations, TASKSANITY_START_ID, snatcher_coins, zero_jumps, zero_jumps_expert, zero_jumps_hard
|
||||
shop_locations, TASKSANITY_START_ID, snatcher_coins, zero_jumps, zero_jumps_expert, zero_jumps_hard, director_tokens
|
||||
from typing import TYPE_CHECKING, List, Dict, Optional
|
||||
from .Rules import set_rift_rules, get_difficulty
|
||||
from .Options import ActRandomizer, EndGoal
|
||||
@@ -859,6 +859,9 @@ def create_region(world: "HatInTimeWorld", name: str) -> Region:
|
||||
if key in storybook_pages.keys() and not world.options.ShuffleStorybookPages:
|
||||
continue
|
||||
|
||||
if key in director_tokens.keys() and not world.options.ShuffleDirectorTokens:
|
||||
continue
|
||||
|
||||
location = HatInTimeLocation(world.player, key, data.id, reg)
|
||||
reg.locations.append(location)
|
||||
if location.name in shop_locations:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,16 @@
|
||||
import collections
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from .Regions import create_lw_region, create_dw_region, create_cave_region, create_dungeon_region
|
||||
from .SubClasses import LTTPRegionType
|
||||
|
||||
|
||||
def create_inverted_regions(world, player):
|
||||
def create_inverted_regions(multiworld: MultiWorld, player: int):
|
||||
|
||||
world.regions += [
|
||||
create_dw_region(world, player, 'Menu', None,
|
||||
multiworld.regions += [
|
||||
create_dw_region(multiworld, player, 'Menu', None,
|
||||
['Links House S&Q', 'Dark Sanctuary S&Q', 'Old Man S&Q', 'Castle Ledge S&Q']),
|
||||
create_lw_region(world, player, 'Light World',
|
||||
create_lw_region(multiworld, player, 'Light World',
|
||||
['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', 'Purple Chest',
|
||||
'Bombos Tablet'],
|
||||
["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Kings Grave Outer Rocks', 'Dam',
|
||||
@@ -35,184 +36,184 @@ def create_inverted_regions(world, player):
|
||||
'Hyrule Castle Entrance (South)', 'Secret Passage Outer Bushes',
|
||||
'Bush Covered Lawn Outer Bushes',
|
||||
'Potion Shop Outer Bushes', 'Graveyard Cave Outer Bushes', 'Bomb Hut Outer Bushes']),
|
||||
create_lw_region(world, player, 'Bush Covered Lawn', None,
|
||||
create_lw_region(multiworld, player, 'Bush Covered Lawn', None,
|
||||
['Bush Covered House', 'Bush Covered Lawn Inner Bushes', 'Bush Covered Lawn Mirror Spot']),
|
||||
create_lw_region(world, player, 'Bomb Hut Area', None,
|
||||
create_lw_region(multiworld, player, 'Bomb Hut Area', None,
|
||||
['Light World Bomb Hut', 'Bomb Hut Inner Bushes', 'Bomb Hut Mirror Spot']),
|
||||
create_lw_region(world, player, 'Hyrule Castle Secret Entrance Area', None,
|
||||
create_lw_region(multiworld, player, 'Hyrule Castle Secret Entrance Area', None,
|
||||
['Hyrule Castle Secret Entrance Stairs', 'Secret Passage Inner Bushes']),
|
||||
create_lw_region(world, player, 'Death Mountain Entrance', None,
|
||||
create_lw_region(multiworld, player, 'Death Mountain Entrance', None,
|
||||
['Old Man Cave (West)', 'Death Mountain Entrance Drop', 'Bumper Cave Entrance Mirror Spot']),
|
||||
create_lw_region(world, player, 'Lake Hylia Central Island', None,
|
||||
create_lw_region(multiworld, player, 'Lake Hylia Central Island', None,
|
||||
['Capacity Upgrade', 'Lake Hylia Central Island Mirror Spot']),
|
||||
create_cave_region(world, player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top",
|
||||
create_cave_region(multiworld, player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top",
|
||||
"Blind\'s Hideout - Left",
|
||||
"Blind\'s Hideout - Right",
|
||||
"Blind\'s Hideout - Far Left",
|
||||
"Blind\'s Hideout - Far Right"]),
|
||||
create_lw_region(world, player, 'Northeast Light World', None,
|
||||
create_lw_region(multiworld, player, 'Northeast Light World', None,
|
||||
['Zoras River', 'Waterfall of Wishing Cave', 'Potion Shop Outer Rock', 'Catfish Mirror Spot',
|
||||
'Northeast Light World Warp']),
|
||||
create_lw_region(world, player, 'Waterfall of Wishing Cave', None,
|
||||
create_lw_region(multiworld, player, 'Waterfall of Wishing Cave', None,
|
||||
['Waterfall of Wishing', 'Northeast Light World Return']),
|
||||
create_lw_region(world, player, 'Potion Shop Area', None,
|
||||
create_lw_region(multiworld, player, 'Potion Shop Area', None,
|
||||
['Potion Shop', 'Potion Shop Inner Bushes', 'Potion Shop Inner Rock',
|
||||
'Potion Shop Mirror Spot', 'Potion Shop River Drop']),
|
||||
create_lw_region(world, player, 'Graveyard Cave Area', None,
|
||||
create_lw_region(multiworld, player, 'Graveyard Cave Area', None,
|
||||
['Graveyard Cave', 'Graveyard Cave Inner Bushes', 'Graveyard Cave Mirror Spot']),
|
||||
create_lw_region(world, player, 'River', None, ['Light World Pier', 'Potion Shop Pier']),
|
||||
create_cave_region(world, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit',
|
||||
create_lw_region(multiworld, player, 'River', None, ['Light World Pier', 'Potion Shop Pier']),
|
||||
create_cave_region(multiworld, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit',
|
||||
['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']),
|
||||
create_lw_region(world, player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']),
|
||||
create_cave_region(world, player, 'Waterfall of Wishing', 'a cave with two chests',
|
||||
create_lw_region(multiworld, player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']),
|
||||
create_cave_region(multiworld, player, 'Waterfall of Wishing', 'a cave with two chests',
|
||||
['Waterfall Fairy - Left', 'Waterfall Fairy - Right']),
|
||||
create_lw_region(world, player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']),
|
||||
create_cave_region(world, player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']),
|
||||
create_cave_region(world, player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']),
|
||||
create_cave_region(world, player, 'Dam', 'the dam', ['Floodgate', 'Floodgate Chest']),
|
||||
create_cave_region(world, player, 'Inverted Links House', 'your house', ['Link\'s House'],
|
||||
create_lw_region(multiworld, player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']),
|
||||
create_cave_region(multiworld, player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']),
|
||||
create_cave_region(multiworld, player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']),
|
||||
create_cave_region(multiworld, player, 'Dam', 'the dam', ['Floodgate', 'Floodgate Chest']),
|
||||
create_cave_region(multiworld, player, 'Inverted Links House', 'your house', ['Link\'s House'],
|
||||
['Inverted Links House Exit']),
|
||||
create_cave_region(world, player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']),
|
||||
create_cave_region(world, player, 'Tavern', 'the tavern', ['Kakariko Tavern']),
|
||||
create_cave_region(world, player, 'Elder House', 'a connector', None,
|
||||
create_cave_region(multiworld, player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']),
|
||||
create_cave_region(multiworld, player, 'Tavern', 'the tavern', ['Kakariko Tavern']),
|
||||
create_cave_region(multiworld, player, 'Elder House', 'a connector', None,
|
||||
['Elder House Exit (East)', 'Elder House Exit (West)']),
|
||||
create_cave_region(world, player, 'Snitch Lady (East)', 'a boring house'),
|
||||
create_cave_region(world, player, 'Snitch Lady (West)', 'a boring house'),
|
||||
create_cave_region(world, player, 'Bush Covered House', 'the grass man'),
|
||||
create_cave_region(world, player, 'Tavern (Front)', 'the tavern'),
|
||||
create_cave_region(world, player, 'Light World Bomb Hut', 'a restock room'),
|
||||
create_cave_region(world, player, 'Kakariko Shop', 'a common shop'),
|
||||
create_cave_region(world, player, 'Fortune Teller (Light)', 'a fortune teller'),
|
||||
create_cave_region(world, player, 'Lake Hylia Fortune Teller', 'a fortune teller'),
|
||||
create_cave_region(world, player, 'Lumberjack House', 'a boring house'),
|
||||
create_cave_region(world, player, 'Bonk Fairy (Light)', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Bonk Fairy (Dark)', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Lake Hylia Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Swamp Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Desert Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Dark Lake Hylia Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Dark Lake Hylia Ledge Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Dark Desert Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Chicken House', 'a house with a chest', ['Chicken House']),
|
||||
create_cave_region(world, player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']),
|
||||
create_cave_region(world, player, 'Sahasrahlas Hut', 'Sahasrahla',
|
||||
create_cave_region(multiworld, player, 'Snitch Lady (East)', 'a boring house'),
|
||||
create_cave_region(multiworld, player, 'Snitch Lady (West)', 'a boring house'),
|
||||
create_cave_region(multiworld, player, 'Bush Covered House', 'the grass man'),
|
||||
create_cave_region(multiworld, player, 'Tavern (Front)', 'the tavern'),
|
||||
create_cave_region(multiworld, player, 'Light World Bomb Hut', 'a restock room'),
|
||||
create_cave_region(multiworld, player, 'Kakariko Shop', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Fortune Teller (Light)', 'a fortune teller'),
|
||||
create_cave_region(multiworld, player, 'Lake Hylia Fortune Teller', 'a fortune teller'),
|
||||
create_cave_region(multiworld, player, 'Lumberjack House', 'a boring house'),
|
||||
create_cave_region(multiworld, player, 'Bonk Fairy (Light)', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Bonk Fairy (Dark)', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Lake Hylia Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Swamp Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Desert Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Dark Lake Hylia Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Dark Desert Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Chicken House', 'a house with a chest', ['Chicken House']),
|
||||
create_cave_region(multiworld, player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']),
|
||||
create_cave_region(multiworld, player, 'Sahasrahlas Hut', 'Sahasrahla',
|
||||
['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right',
|
||||
'Sahasrahla']),
|
||||
create_cave_region(world, player, 'Kakariko Well (top)', 'a drop\'s exit',
|
||||
create_cave_region(multiworld, player, 'Kakariko Well (top)', 'a drop\'s exit',
|
||||
['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle',
|
||||
'Kakariko Well - Right', 'Kakariko Well - Bottom'], ['Kakariko Well (top to bottom)']),
|
||||
create_cave_region(world, player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']),
|
||||
create_cave_region(world, player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']),
|
||||
create_lw_region(world, player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']),
|
||||
create_cave_region(world, player, 'Bat Cave (right)', 'a drop\'s exit', ['Magic Bat'], ['Bat Cave Door']),
|
||||
create_cave_region(world, player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']),
|
||||
create_cave_region(world, player, 'Sick Kids House', 'the sick kid', ['Sick Kid']),
|
||||
create_lw_region(world, player, 'Hobo Bridge', ['Hobo']),
|
||||
create_cave_region(world, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'],
|
||||
create_cave_region(multiworld, player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']),
|
||||
create_cave_region(multiworld, player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']),
|
||||
create_lw_region(multiworld, player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']),
|
||||
create_cave_region(multiworld, player, 'Bat Cave (right)', 'a drop\'s exit', ['Magic Bat'], ['Bat Cave Door']),
|
||||
create_cave_region(multiworld, player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']),
|
||||
create_cave_region(multiworld, player, 'Sick Kids House', 'the sick kid', ['Sick Kid']),
|
||||
create_lw_region(multiworld, player, 'Hobo Bridge', ['Hobo']),
|
||||
create_cave_region(multiworld, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'],
|
||||
['Lost Woods Hideout (top to bottom)']),
|
||||
create_cave_region(world, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None,
|
||||
create_cave_region(multiworld, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None,
|
||||
['Lost Woods Hideout Exit']),
|
||||
create_cave_region(world, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'],
|
||||
create_cave_region(multiworld, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'],
|
||||
['Lumberjack Tree (top to bottom)']),
|
||||
create_cave_region(world, player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']),
|
||||
create_cave_region(world, player, 'Cave 45', 'a cave with an item', ['Cave 45']),
|
||||
create_cave_region(world, player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']),
|
||||
create_cave_region(world, player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']),
|
||||
create_cave_region(world, player, 'Long Fairy Cave', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Mini Moldorm Cave', 'a bounty of five items',
|
||||
create_cave_region(multiworld, player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']),
|
||||
create_cave_region(multiworld, player, 'Cave 45', 'a cave with an item', ['Cave 45']),
|
||||
create_cave_region(multiworld, player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']),
|
||||
create_cave_region(multiworld, player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']),
|
||||
create_cave_region(multiworld, player, 'Long Fairy Cave', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Mini Moldorm Cave', 'a bounty of five items',
|
||||
['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right',
|
||||
'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Generous Guy']),
|
||||
create_cave_region(world, player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']),
|
||||
create_cave_region(world, player, 'Good Bee Cave', 'a cold bee'),
|
||||
create_cave_region(world, player, '20 Rupee Cave', 'a cave with some cash'),
|
||||
create_cave_region(world, player, 'Cave Shop (Lake Hylia)', 'a common shop'),
|
||||
create_cave_region(world, player, 'Cave Shop (Dark Death Mountain)', 'a common shop'),
|
||||
create_cave_region(world, player, 'Bonk Rock Cave', 'a cave with a chest', ['Bonk Rock Cave']),
|
||||
create_cave_region(world, player, 'Library', 'the library', ['Library']),
|
||||
create_cave_region(world, player, 'Kakariko Gamble Game', 'a game of chance'),
|
||||
create_cave_region(world, player, 'Potion Shop', 'the potion shop', ['Potion Shop']),
|
||||
create_lw_region(world, player, 'Lake Hylia Island', ['Lake Hylia Island']),
|
||||
create_cave_region(world, player, 'Capacity Upgrade', 'the queen of fairies', ['Capacity Upgrade Shop']),
|
||||
create_cave_region(world, player, 'Two Brothers House', 'a connector', None,
|
||||
create_cave_region(multiworld, player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']),
|
||||
create_cave_region(multiworld, player, 'Good Bee Cave', 'a cold bee'),
|
||||
create_cave_region(multiworld, player, '20 Rupee Cave', 'a cave with some cash'),
|
||||
create_cave_region(multiworld, player, 'Cave Shop (Lake Hylia)', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Cave Shop (Dark Death Mountain)', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Bonk Rock Cave', 'a cave with a chest', ['Bonk Rock Cave']),
|
||||
create_cave_region(multiworld, player, 'Library', 'the library', ['Library']),
|
||||
create_cave_region(multiworld, player, 'Kakariko Gamble Game', 'a game of chance'),
|
||||
create_cave_region(multiworld, player, 'Potion Shop', 'the potion shop', ['Potion Shop']),
|
||||
create_lw_region(multiworld, player, 'Lake Hylia Island', ['Lake Hylia Island']),
|
||||
create_cave_region(multiworld, player, 'Capacity Upgrade', 'the queen of fairies', ['Capacity Upgrade Shop']),
|
||||
create_cave_region(multiworld, player, 'Two Brothers House', 'a connector', None,
|
||||
['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']),
|
||||
create_lw_region(world, player, 'Maze Race Ledge', ['Maze Race'],
|
||||
create_lw_region(multiworld, player, 'Maze Race Ledge', ['Maze Race'],
|
||||
['Two Brothers House (West)', 'Maze Race Mirror Spot']),
|
||||
create_cave_region(world, player, '50 Rupee Cave', 'a cave with some cash'),
|
||||
create_lw_region(world, player, 'Desert Ledge', ['Desert Ledge'],
|
||||
create_cave_region(multiworld, player, '50 Rupee Cave', 'a cave with some cash'),
|
||||
create_lw_region(multiworld, player, 'Desert Ledge', ['Desert Ledge'],
|
||||
['Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (West)',
|
||||
'Desert Ledge Drop']),
|
||||
create_lw_region(world, player, 'Desert Palace Stairs', None,
|
||||
create_lw_region(multiworld, player, 'Desert Palace Stairs', None,
|
||||
['Desert Palace Entrance (South)', 'Desert Palace Stairs Mirror Spot']),
|
||||
create_lw_region(world, player, 'Desert Palace Lone Stairs', None,
|
||||
create_lw_region(multiworld, player, 'Desert Palace Lone Stairs', None,
|
||||
['Desert Palace Stairs Drop', 'Desert Palace Entrance (East)']),
|
||||
create_lw_region(world, player, 'Desert Palace Entrance (North) Spot', None,
|
||||
create_lw_region(multiworld, player, 'Desert Palace Entrance (North) Spot', None,
|
||||
['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks',
|
||||
'Desert Palace North Mirror Spot']),
|
||||
create_dungeon_region(world, player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
|
||||
create_dungeon_region(multiworld, player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
|
||||
['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)', 'Desert Palace East Wing']),
|
||||
create_dungeon_region(world, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
|
||||
create_dungeon_region(world, player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
|
||||
create_dungeon_region(world, player, 'Desert Palace North', 'Desert Palace',
|
||||
create_dungeon_region(multiworld, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
|
||||
create_dungeon_region(multiworld, player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
|
||||
create_dungeon_region(multiworld, player, 'Desert Palace North', 'Desert Palace',
|
||||
['Desert Palace - Desert Tiles 1 Pot Key', 'Desert Palace - Beamos Hall Pot Key',
|
||||
'Desert Palace - Desert Tiles 2 Pot Key',
|
||||
'Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']),
|
||||
create_dungeon_region(world, player, 'Eastern Palace', 'Eastern Palace',
|
||||
create_dungeon_region(multiworld, player, 'Eastern Palace', 'Eastern Palace',
|
||||
['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest',
|
||||
'Eastern Palace - Cannonball Chest',
|
||||
'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop',
|
||||
'Eastern Palace - Big Key Chest',
|
||||
'Eastern Palace - Map Chest', 'Eastern Palace - Boss', 'Eastern Palace - Prize'],
|
||||
['Eastern Palace Exit']),
|
||||
create_lw_region(world, player, 'Master Sword Meadow', ['Master Sword Pedestal']),
|
||||
create_cave_region(world, player, 'Lost Woods Gamble', 'a game of chance'),
|
||||
create_lw_region(world, player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Inverted Ganons Tower', 'Hyrule Castle Ledge Courtyard Drop', 'Inverted Pyramid Hole']),
|
||||
create_dungeon_region(world, player, 'Hyrule Castle', 'Hyrule Castle',
|
||||
create_lw_region(multiworld, player, 'Master Sword Meadow', ['Master Sword Pedestal']),
|
||||
create_cave_region(multiworld, player, 'Lost Woods Gamble', 'a game of chance'),
|
||||
create_lw_region(multiworld, player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Inverted Ganons Tower', 'Hyrule Castle Ledge Courtyard Drop', 'Inverted Pyramid Hole']),
|
||||
create_dungeon_region(multiworld, player, 'Hyrule Castle', 'Hyrule Castle',
|
||||
['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest',
|
||||
'Hyrule Castle - Zelda\'s Chest',
|
||||
'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop',
|
||||
'Hyrule Castle - Big Key Drop'],
|
||||
['Hyrule Castle Exit (East)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (South)',
|
||||
'Throne Room']),
|
||||
create_dungeon_region(world, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks
|
||||
create_dungeon_region(world, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']),
|
||||
create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit', None, ['Sanctuary Push Door', 'Sewers Back Door', 'Sewers Secret Room']),
|
||||
create_dungeon_region(world, player, 'Sewers Secret Room', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
|
||||
create_dungeon_region(multiworld, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks
|
||||
create_dungeon_region(multiworld, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']),
|
||||
create_dungeon_region(multiworld, player, 'Sewers', 'a drop\'s exit', None, ['Sanctuary Push Door', 'Sewers Back Door', 'Sewers Secret Room']),
|
||||
create_dungeon_region(multiworld, player, 'Sewers Secret Room', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
|
||||
'Sewers - Secret Room - Right']),
|
||||
create_dungeon_region(world, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']),
|
||||
create_dungeon_region(world, player, 'Inverted Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Inverted Agahnims Tower Exit']),
|
||||
create_dungeon_region(world, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None),
|
||||
create_cave_region(world, player, 'Old Man Cave', 'a connector', ['Old Man'],
|
||||
create_dungeon_region(multiworld, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Inverted Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Inverted Agahnims Tower Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None),
|
||||
create_cave_region(multiworld, player, 'Old Man Cave', 'a connector', ['Old Man'],
|
||||
['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']),
|
||||
create_cave_region(world, player, 'Old Man House', 'a connector', None,
|
||||
create_cave_region(multiworld, player, 'Old Man House', 'a connector', None,
|
||||
['Old Man House Exit (Bottom)', 'Old Man House Front to Back']),
|
||||
create_cave_region(world, player, 'Old Man House Back', 'a connector', None,
|
||||
create_cave_region(multiworld, player, 'Old Man House Back', 'a connector', None,
|
||||
['Old Man House Exit (Top)', 'Old Man House Back to Front']),
|
||||
create_lw_region(world, player, 'Death Mountain', None,
|
||||
create_lw_region(multiworld, player, 'Death Mountain', None,
|
||||
['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)',
|
||||
'Death Mountain Return Cave (East)', 'Spectacle Rock Cave',
|
||||
'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Broken Bridge (West)',
|
||||
'Death Mountain Mirror Spot']),
|
||||
create_cave_region(world, player, 'Death Mountain Return Cave', 'a connector', None,
|
||||
create_cave_region(multiworld, player, 'Death Mountain Return Cave', 'a connector', None,
|
||||
['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']),
|
||||
create_lw_region(world, player, 'Death Mountain Return Ledge', None,
|
||||
create_lw_region(multiworld, player, 'Death Mountain Return Ledge', None,
|
||||
['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)',
|
||||
'Bumper Cave Ledge Mirror Spot']),
|
||||
create_cave_region(world, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'],
|
||||
create_cave_region(multiworld, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'],
|
||||
['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']),
|
||||
create_cave_region(world, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None,
|
||||
create_cave_region(multiworld, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None,
|
||||
['Spectacle Rock Cave Exit']),
|
||||
create_cave_region(world, player, 'Spectacle Rock Cave (Peak)', 'a connector', None,
|
||||
create_cave_region(multiworld, player, 'Spectacle Rock Cave (Peak)', 'a connector', None,
|
||||
['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']),
|
||||
create_lw_region(world, player, 'East Death Mountain (Bottom)', None,
|
||||
create_lw_region(multiworld, player, 'East Death Mountain (Bottom)', None,
|
||||
['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)',
|
||||
'East Death Mountain Mirror Spot (Bottom)', 'Hookshot Fairy',
|
||||
'Fairy Ascension Rocks', 'Spiral Cave (Bottom)']),
|
||||
create_cave_region(world, player, 'Hookshot Fairy', 'fairies deep in a cave'),
|
||||
create_cave_region(world, player, 'Paradox Cave Front', 'a connector', None,
|
||||
create_cave_region(multiworld, player, 'Hookshot Fairy', 'fairies deep in a cave'),
|
||||
create_cave_region(multiworld, player, 'Paradox Cave Front', 'a connector', None,
|
||||
['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)',
|
||||
'Light World Death Mountain Shop']),
|
||||
create_cave_region(world, player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left',
|
||||
create_cave_region(multiworld, player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left',
|
||||
'Paradox Cave Lower - Left',
|
||||
'Paradox Cave Lower - Right',
|
||||
'Paradox Cave Lower - Far Right',
|
||||
@@ -220,273 +221,273 @@ def create_inverted_regions(world, player):
|
||||
'Paradox Cave Upper - Left',
|
||||
'Paradox Cave Upper - Right'],
|
||||
['Paradox Cave Push Block', 'Paradox Cave Bomb Jump']),
|
||||
create_cave_region(world, player, 'Paradox Cave', 'a connector', None,
|
||||
create_cave_region(multiworld, player, 'Paradox Cave', 'a connector', None,
|
||||
['Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Paradox Cave Drop']),
|
||||
create_cave_region(world, player, 'Light World Death Mountain Shop', 'a common shop'),
|
||||
create_lw_region(world, player, 'East Death Mountain (Top)', ['Floating Island'],
|
||||
create_cave_region(multiworld, player, 'Light World Death Mountain Shop', 'a common shop'),
|
||||
create_lw_region(multiworld, player, 'East Death Mountain (Top)', ['Floating Island'],
|
||||
['Paradox Cave (Top)', 'Death Mountain (Top)', 'Spiral Cave Ledge Access',
|
||||
'East Death Mountain Drop', 'East Death Mountain Mirror Spot (Top)',
|
||||
'Fairy Ascension Ledge Access', 'Mimic Cave Ledge Access',
|
||||
'Floating Island Mirror Spot']),
|
||||
create_lw_region(world, player, 'Spiral Cave Ledge', None,
|
||||
create_lw_region(multiworld, player, 'Spiral Cave Ledge', None,
|
||||
['Spiral Cave', 'Spiral Cave Ledge Drop', 'Dark Death Mountain Ledge Mirror Spot (West)']),
|
||||
create_lw_region(world, player, 'Mimic Cave Ledge', None,
|
||||
create_lw_region(multiworld, player, 'Mimic Cave Ledge', None,
|
||||
['Mimic Cave', 'Mimic Cave Ledge Drop', 'Dark Death Mountain Ledge Mirror Spot (East)']),
|
||||
create_cave_region(world, player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'],
|
||||
create_cave_region(multiworld, player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'],
|
||||
['Spiral Cave (top to bottom)', 'Spiral Cave Exit (Top)']),
|
||||
create_cave_region(world, player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']),
|
||||
create_lw_region(world, player, 'Fairy Ascension Plateau', None,
|
||||
create_cave_region(multiworld, player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']),
|
||||
create_lw_region(multiworld, player, 'Fairy Ascension Plateau', None,
|
||||
['Fairy Ascension Drop', 'Fairy Ascension Cave (Bottom)']),
|
||||
create_cave_region(world, player, 'Fairy Ascension Cave (Bottom)', 'a connector', None,
|
||||
create_cave_region(multiworld, player, 'Fairy Ascension Cave (Bottom)', 'a connector', None,
|
||||
['Fairy Ascension Cave Climb', 'Fairy Ascension Cave Exit (Bottom)']),
|
||||
create_cave_region(world, player, 'Fairy Ascension Cave (Drop)', 'a connector', None,
|
||||
create_cave_region(multiworld, player, 'Fairy Ascension Cave (Drop)', 'a connector', None,
|
||||
['Fairy Ascension Cave Pots']),
|
||||
create_cave_region(world, player, 'Fairy Ascension Cave (Top)', 'a connector', None,
|
||||
create_cave_region(multiworld, player, 'Fairy Ascension Cave (Top)', 'a connector', None,
|
||||
['Fairy Ascension Cave Exit (Top)', 'Fairy Ascension Cave Drop']),
|
||||
create_lw_region(world, player, 'Fairy Ascension Ledge', None,
|
||||
create_lw_region(multiworld, player, 'Fairy Ascension Ledge', None,
|
||||
['Fairy Ascension Ledge Drop', 'Fairy Ascension Cave (Top)', 'Laser Bridge Mirror Spot']),
|
||||
create_lw_region(world, player, 'Death Mountain (Top)', ['Ether Tablet', 'Spectacle Rock'],
|
||||
create_lw_region(multiworld, player, 'Death Mountain (Top)', ['Ether Tablet', 'Spectacle Rock'],
|
||||
['East Death Mountain (Top)', 'Tower of Hera', 'Death Mountain Drop',
|
||||
'Death Mountain (Top) Mirror Spot']),
|
||||
create_dw_region(world, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'],
|
||||
create_dw_region(multiworld, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'],
|
||||
['Bumper Cave Ledge Drop', 'Bumper Cave (Top)']),
|
||||
create_dungeon_region(world, player, 'Tower of Hera (Bottom)', 'Tower of Hera', ['Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest'], ['Tower of Hera Small Key Door', 'Tower of Hera Big Key Door', 'Tower of Hera Exit']),
|
||||
create_dungeon_region(world, player, 'Tower of Hera (Basement)', 'Tower of Hera', ['Tower of Hera - Big Key Chest']),
|
||||
create_dungeon_region(world, player, 'Tower of Hera (Top)', 'Tower of Hera', ['Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss', 'Tower of Hera - Prize']),
|
||||
create_dungeon_region(multiworld, player, 'Tower of Hera (Bottom)', 'Tower of Hera', ['Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest'], ['Tower of Hera Small Key Door', 'Tower of Hera Big Key Door', 'Tower of Hera Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Tower of Hera (Basement)', 'Tower of Hera', ['Tower of Hera - Big Key Chest']),
|
||||
create_dungeon_region(multiworld, player, 'Tower of Hera (Top)', 'Tower of Hera', ['Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss', 'Tower of Hera - Prize']),
|
||||
|
||||
create_dw_region(world, player, 'East Dark World', ['Pyramid'],
|
||||
create_dw_region(multiworld, player, 'East Dark World', ['Pyramid'],
|
||||
['Pyramid Fairy', 'South Dark World Bridge', 'Palace of Darkness',
|
||||
'Dark Lake Hylia Drop (East)',
|
||||
'Dark Lake Hylia Fairy', 'Palace of Darkness Hint', 'East Dark World Hint',
|
||||
'Northeast Dark World Broken Bridge Pass', 'East Dark World Teleporter', 'EDW Flute']),
|
||||
create_dw_region(world, player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']),
|
||||
create_dw_region(world, player, 'Northeast Dark World', None,
|
||||
create_dw_region(multiworld, player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']),
|
||||
create_dw_region(multiworld, player, 'Northeast Dark World', None,
|
||||
['West Dark World Gap', 'Dark World Potion Shop', 'East Dark World Broken Bridge Pass',
|
||||
'NEDW Flute', 'Dark Lake Hylia Teleporter', 'Catfish Entrance Rock']),
|
||||
create_cave_region(world, player, 'Palace of Darkness Hint', 'a storyteller'),
|
||||
create_cave_region(world, player, 'East Dark World Hint', 'a storyteller'),
|
||||
create_dw_region(world, player, 'South Dark World', ['Stumpy', 'Digging Game'],
|
||||
create_cave_region(multiworld, player, 'Palace of Darkness Hint', 'a storyteller'),
|
||||
create_cave_region(multiworld, player, 'East Dark World Hint', 'a storyteller'),
|
||||
create_dw_region(multiworld, player, 'South Dark World', ['Stumpy', 'Digging Game'],
|
||||
['Dark Lake Hylia Drop (South)', 'Hype Cave', 'Swamp Palace', 'Village of Outcasts Heavy Rock',
|
||||
'East Dark World Bridge', 'Inverted Links House', 'Archery Game', 'Bonk Fairy (Dark)',
|
||||
'Dark Lake Hylia Shop', 'South Dark World Teleporter', 'Post Aga Teleporter', 'SDW Flute']),
|
||||
create_cave_region(world, player, 'Inverted Big Bomb Shop', 'the bomb shop'),
|
||||
create_cave_region(world, player, 'Archery Game', 'a game of skill'),
|
||||
create_dw_region(world, player, 'Dark Lake Hylia', None,
|
||||
create_cave_region(multiworld, player, 'Inverted Big Bomb Shop', 'the bomb shop'),
|
||||
create_cave_region(multiworld, player, 'Archery Game', 'a game of skill'),
|
||||
create_dw_region(multiworld, player, 'Dark Lake Hylia', None,
|
||||
['East Dark World Pier', 'Dark Lake Hylia Ledge Pier', 'Ice Palace Missing Wall']),
|
||||
create_dw_region(world, player, 'Dark Lake Hylia Central Island', None,
|
||||
create_dw_region(multiworld, player, 'Dark Lake Hylia Central Island', None,
|
||||
['Dark Lake Hylia Shallows', 'Ice Palace', 'Dark Lake Hylia Central Island Teleporter']),
|
||||
create_dw_region(world, player, 'Dark Lake Hylia Ledge', None,
|
||||
create_dw_region(multiworld, player, 'Dark Lake Hylia Ledge', None,
|
||||
['Dark Lake Hylia Ledge Drop', 'Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Hint',
|
||||
'Dark Lake Hylia Ledge Spike Cave', 'DLHL Flute']),
|
||||
create_cave_region(world, player, 'Dark Lake Hylia Ledge Hint', 'a storyteller'),
|
||||
create_cave_region(world, player, 'Dark Lake Hylia Ledge Spike Cave', 'a spiky hint'),
|
||||
create_cave_region(world, player, 'Hype Cave', 'a bounty of five items',
|
||||
create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Hint', 'a storyteller'),
|
||||
create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Spike Cave', 'a spiky hint'),
|
||||
create_cave_region(multiworld, player, 'Hype Cave', 'a bounty of five items',
|
||||
['Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left',
|
||||
'Hype Cave - Bottom', 'Hype Cave - Generous Guy']),
|
||||
create_dw_region(world, player, 'West Dark World', ['Frog', 'Flute Activation Spot'],
|
||||
create_dw_region(multiworld, player, 'West Dark World', ['Frog', 'Flute Activation Spot'],
|
||||
['Village of Outcasts Drop', 'East Dark World River Pier', 'Brewery', 'C-Shaped House',
|
||||
'Chest Game', 'Thieves Town', 'Bumper Cave Entrance Rock',
|
||||
'Skull Woods Forest', 'Village of Outcasts Pegs', 'Village of Outcasts Eastern Rocks',
|
||||
'Red Shield Shop', 'Inverted Dark Sanctuary', 'Fortune Teller (Dark)',
|
||||
'Dark World Lumberjack Shop',
|
||||
'West Dark World Teleporter', 'WDW Flute']),
|
||||
create_dw_region(world, player, 'Dark Grassy Lawn', None,
|
||||
create_dw_region(multiworld, player, 'Dark Grassy Lawn', None,
|
||||
['Grassy Lawn Pegs', 'Village of Outcasts Shop', 'Dark Grassy Lawn Flute']),
|
||||
create_dw_region(world, player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'],
|
||||
create_dw_region(multiworld, player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'],
|
||||
['Dark World Hammer Peg Cave', 'Peg Area Rocks', 'Hammer Peg Area Flute']),
|
||||
create_dw_region(world, player, 'Bumper Cave Entrance', None,
|
||||
create_dw_region(multiworld, player, 'Bumper Cave Entrance', None,
|
||||
['Bumper Cave (Bottom)', 'Bumper Cave Entrance Drop']),
|
||||
create_cave_region(world, player, 'Fortune Teller (Dark)', 'a fortune teller'),
|
||||
create_cave_region(world, player, 'Village of Outcasts Shop', 'a common shop'),
|
||||
create_cave_region(world, player, 'Dark Lake Hylia Shop', 'a common shop'),
|
||||
create_cave_region(world, player, 'Dark World Lumberjack Shop', 'a common shop'),
|
||||
create_cave_region(world, player, 'Dark World Potion Shop', 'a common shop'),
|
||||
create_cave_region(world, player, 'Dark World Hammer Peg Cave', 'a cave with an item', ['Peg Cave']),
|
||||
create_cave_region(world, player, 'Pyramid Fairy', 'a cave with two chests',
|
||||
create_cave_region(multiworld, player, 'Fortune Teller (Dark)', 'a fortune teller'),
|
||||
create_cave_region(multiworld, player, 'Village of Outcasts Shop', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Dark Lake Hylia Shop', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Dark World Lumberjack Shop', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Dark World Potion Shop', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Dark World Hammer Peg Cave', 'a cave with an item', ['Peg Cave']),
|
||||
create_cave_region(multiworld, player, 'Pyramid Fairy', 'a cave with two chests',
|
||||
['Pyramid Fairy - Left', 'Pyramid Fairy - Right']),
|
||||
create_cave_region(world, player, 'Brewery', 'a house with a chest', ['Brewery']),
|
||||
create_cave_region(world, player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']),
|
||||
create_cave_region(world, player, 'Chest Game', 'a game of 16 chests', ['Chest Game']),
|
||||
create_cave_region(world, player, 'Red Shield Shop', 'the rare shop'),
|
||||
create_cave_region(world, player, 'Inverted Dark Sanctuary', 'a storyteller', None,
|
||||
create_cave_region(multiworld, player, 'Brewery', 'a house with a chest', ['Brewery']),
|
||||
create_cave_region(multiworld, player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']),
|
||||
create_cave_region(multiworld, player, 'Chest Game', 'a game of 16 chests', ['Chest Game']),
|
||||
create_cave_region(multiworld, player, 'Red Shield Shop', 'the rare shop'),
|
||||
create_cave_region(multiworld, player, 'Inverted Dark Sanctuary', 'a storyteller', None,
|
||||
['Inverted Dark Sanctuary Exit']),
|
||||
create_cave_region(world, player, 'Bumper Cave', 'a connector', None,
|
||||
create_cave_region(multiworld, player, 'Bumper Cave', 'a connector', None,
|
||||
['Bumper Cave Exit (Bottom)', 'Bumper Cave Exit (Top)']),
|
||||
create_dw_region(world, player, 'Skull Woods Forest', None,
|
||||
create_dw_region(multiworld, player, 'Skull Woods Forest', None,
|
||||
['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)',
|
||||
'Skull Woods First Section Hole (North)',
|
||||
'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)']),
|
||||
create_dw_region(world, player, 'Skull Woods Forest (West)', None,
|
||||
create_dw_region(multiworld, player, 'Skull Woods Forest (West)', None,
|
||||
['Skull Woods Second Section Hole', 'Skull Woods Second Section Door (West)',
|
||||
'Skull Woods Final Section']),
|
||||
create_dw_region(world, player, 'Dark Desert', None,
|
||||
create_dw_region(multiworld, player, 'Dark Desert', None,
|
||||
['Misery Mire', 'Mire Shed', 'Dark Desert Hint', 'Dark Desert Fairy', 'DD Flute']),
|
||||
create_dw_region(world, player, 'Dark Desert Ledge', None, ['Dark Desert Drop', 'Dark Desert Teleporter']),
|
||||
create_cave_region(world, player, 'Mire Shed', 'a cave with two chests',
|
||||
create_dw_region(multiworld, player, 'Dark Desert Ledge', None, ['Dark Desert Drop', 'Dark Desert Teleporter']),
|
||||
create_cave_region(multiworld, player, 'Mire Shed', 'a cave with two chests',
|
||||
['Mire Shed - Left', 'Mire Shed - Right']),
|
||||
create_cave_region(world, player, 'Dark Desert Hint', 'a storyteller'),
|
||||
create_dw_region(world, player, 'Dark Death Mountain', None,
|
||||
create_cave_region(multiworld, player, 'Dark Desert Hint', 'a storyteller'),
|
||||
create_dw_region(multiworld, player, 'Dark Death Mountain', None,
|
||||
['Dark Death Mountain Drop (East)', 'Inverted Agahnims Tower', 'Superbunny Cave (Top)',
|
||||
'Hookshot Cave', 'Turtle Rock',
|
||||
'Spike Cave', 'Dark Death Mountain Fairy', 'Dark Death Mountain Teleporter (West)',
|
||||
'Turtle Rock Tail Drop', 'DDM Flute']),
|
||||
create_dw_region(world, player, 'Dark Death Mountain Ledge', None,
|
||||
create_dw_region(multiworld, player, 'Dark Death Mountain Ledge', None,
|
||||
['Dark Death Mountain Ledge (East)', 'Dark Death Mountain Ledge (West)']),
|
||||
create_dw_region(world, player, 'Turtle Rock (Top)', None,
|
||||
create_dw_region(multiworld, player, 'Turtle Rock (Top)', None,
|
||||
['Dark Death Mountain Teleporter (East)', 'Turtle Rock Drop']),
|
||||
create_dw_region(world, player, 'Dark Death Mountain Isolated Ledge', None,
|
||||
create_dw_region(multiworld, player, 'Dark Death Mountain Isolated Ledge', None,
|
||||
['Turtle Rock Isolated Ledge Entrance']),
|
||||
create_dw_region(world, player, 'Dark Death Mountain (East Bottom)', None,
|
||||
create_dw_region(multiworld, player, 'Dark Death Mountain (East Bottom)', None,
|
||||
['Superbunny Cave (Bottom)', 'Cave Shop (Dark Death Mountain)',
|
||||
'Dark Death Mountain Teleporter (East Bottom)', 'EDDM Flute']),
|
||||
create_cave_region(world, player, 'Superbunny Cave (Top)', 'a connector',
|
||||
create_cave_region(multiworld, player, 'Superbunny Cave (Top)', 'a connector',
|
||||
['Superbunny Cave - Top', 'Superbunny Cave - Bottom'], ['Superbunny Cave Exit (Top)']),
|
||||
create_cave_region(world, player, 'Superbunny Cave (Bottom)', 'a connector', None,
|
||||
create_cave_region(multiworld, player, 'Superbunny Cave (Bottom)', 'a connector', None,
|
||||
['Superbunny Cave Climb', 'Superbunny Cave Exit (Bottom)']),
|
||||
create_cave_region(world, player, 'Spike Cave', 'Spike Cave', ['Spike Cave']),
|
||||
create_cave_region(world, player, 'Hookshot Cave', 'a connector',
|
||||
create_cave_region(multiworld, player, 'Spike Cave', 'Spike Cave', ['Spike Cave']),
|
||||
create_cave_region(multiworld, player, 'Hookshot Cave', 'a connector',
|
||||
['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right',
|
||||
'Hookshot Cave - Bottom Left'],
|
||||
['Hookshot Cave Exit (South)', 'Hookshot Cave Bomb Wall (South)']),
|
||||
create_cave_region(world, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)',
|
||||
create_cave_region(multiworld, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)',
|
||||
'Hookshot Cave Bomb Wall (North)']),
|
||||
create_dw_region(world, player, 'Death Mountain Floating Island (Dark World)', None,
|
||||
create_dw_region(multiworld, player, 'Death Mountain Floating Island (Dark World)', None,
|
||||
['Floating Island Drop', 'Hookshot Cave Back Entrance']),
|
||||
create_cave_region(world, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']),
|
||||
create_cave_region(multiworld, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']),
|
||||
|
||||
create_dungeon_region(world, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']),
|
||||
create_dungeon_region(world, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']),
|
||||
create_dungeon_region(world, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest', 'Swamp Palace - Pot Row Pot Key',
|
||||
create_dungeon_region(multiworld, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']),
|
||||
create_dungeon_region(multiworld, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest', 'Swamp Palace - Pot Row Pot Key',
|
||||
'Swamp Palace - Trench 1 Pot Key'], ['Swamp Palace (Center)']),
|
||||
create_dungeon_region(world, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key',
|
||||
create_dungeon_region(multiworld, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key',
|
||||
'Swamp Palace - Trench 2 Pot Key'], ['Swamp Palace (North)', 'Swamp Palace (West)']),
|
||||
create_dungeon_region(world, player, 'Swamp Palace (West)', 'Swamp Palace', ['Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest']),
|
||||
create_dungeon_region(world, player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
|
||||
create_dungeon_region(multiworld, player, 'Swamp Palace (West)', 'Swamp Palace', ['Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest']),
|
||||
create_dungeon_region(multiworld, player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
|
||||
'Swamp Palace - Waterway Pot Key', 'Swamp Palace - Waterfall Room',
|
||||
'Swamp Palace - Boss', 'Swamp Palace - Prize']),
|
||||
create_dungeon_region(world, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest',
|
||||
create_dungeon_region(multiworld, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest',
|
||||
'Thieves\' Town - Map Chest',
|
||||
'Thieves\' Town - Compass Chest',
|
||||
'Thieves\' Town - Ambush Chest'], ['Thieves Town Big Key Door', 'Thieves Town Exit']),
|
||||
create_dungeon_region(world, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic',
|
||||
create_dungeon_region(multiworld, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic',
|
||||
'Thieves\' Town - Big Chest',
|
||||
'Thieves\' Town - Hallway Pot Key',
|
||||
'Thieves\' Town - Spike Switch Pot Key',
|
||||
'Thieves\' Town - Blind\'s Cell'],
|
||||
['Blind Fight']),
|
||||
create_dungeon_region(world, player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']),
|
||||
create_dungeon_region(world, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']),
|
||||
create_dungeon_region(world, player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
|
||||
create_dungeon_region(world, player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']),
|
||||
create_dungeon_region(world, player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
|
||||
create_dungeon_region(world, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']),
|
||||
create_dungeon_region(world, player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest', 'Skull Woods - West Lobby Pot Key'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']),
|
||||
create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
|
||||
create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop', 'Skull Woods - Boss', 'Skull Woods - Prize']),
|
||||
create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Second Section)', 'Ice Palace Exit']),
|
||||
create_dungeon_region(world, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop'], ['Ice Palace (Main)']),
|
||||
create_dungeon_region(world, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest',
|
||||
create_dungeon_region(multiworld, player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest', 'Skull Woods - West Lobby Pot Key'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop', 'Skull Woods - Boss', 'Skull Woods - Prize']),
|
||||
create_dungeon_region(multiworld, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Second Section)', 'Ice Palace Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop'], ['Ice Palace (Main)']),
|
||||
create_dungeon_region(multiworld, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest',
|
||||
'Ice Palace - Many Pots Pot Key',
|
||||
'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'], ['Ice Palace (East)', 'Ice Palace (Kholdstare)']),
|
||||
create_dungeon_region(world, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']),
|
||||
create_dungeon_region(world, player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Hammer Block Key Drop']),
|
||||
create_dungeon_region(world, player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']),
|
||||
create_dungeon_region(world, player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']),
|
||||
create_dungeon_region(world, player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby',
|
||||
create_dungeon_region(multiworld, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']),
|
||||
create_dungeon_region(multiworld, player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Hammer Block Key Drop']),
|
||||
create_dungeon_region(multiworld, player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']),
|
||||
create_dungeon_region(multiworld, player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby',
|
||||
'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest',
|
||||
'Misery Mire - Spikes Pot Key', 'Misery Mire - Fishbone Pot Key',
|
||||
'Misery Mire - Conveyor Crystal Key Drop'], ['Misery Mire (West)', 'Misery Mire Big Key Door']),
|
||||
create_dungeon_region(world, player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']),
|
||||
create_dungeon_region(world, player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']),
|
||||
create_dungeon_region(world, player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left',
|
||||
create_dungeon_region(multiworld, player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']),
|
||||
create_dungeon_region(multiworld, player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']),
|
||||
create_dungeon_region(multiworld, player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left',
|
||||
'Turtle Rock - Roller Room - Right'],
|
||||
['Turtle Rock Entrance to Pokey Room', 'Turtle Rock Entrance Gap Reverse']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'],
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'],
|
||||
['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock',
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Second Section)', 'Turtle Rock',
|
||||
['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'],
|
||||
['Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door',
|
||||
'Turtle Rock Second Section Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
|
||||
'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'],
|
||||
['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Eye Bridge Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
|
||||
['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)', 'Palace of Darkness Big Key Door']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'],
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'],
|
||||
['Palace of Darkness Spike Statue Room Door', 'Palace of Darkness Maze Door']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']),
|
||||
create_dungeon_region(world, player, 'Inverted Ganons Tower (Entrance)', 'Ganon\'s Tower',
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']),
|
||||
create_dungeon_region(multiworld, player, 'Inverted Ganons Tower (Entrance)', 'Ganon\'s Tower',
|
||||
['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left',
|
||||
'Ganons Tower - Hope Room - Right', 'Ganons Tower - Conveyor Cross Pot Key'],
|
||||
['Ganons Tower (Tile Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower Big Key Door',
|
||||
'Inverted Ganons Tower Exit']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'],
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'],
|
||||
['Ganons Tower (Tile Room) Key Door']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower',
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower',
|
||||
['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right',
|
||||
'Ganons Tower - Compass Room - Bottom Left',
|
||||
'Ganons Tower - Compass Room - Bottom Right',
|
||||
'Ganons Tower - Conveyor Star Pits Pot Key'],
|
||||
['Ganons Tower (Bottom) (East)']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower',
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower',
|
||||
['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right',
|
||||
'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right',
|
||||
'Ganons Tower - Double Switch Pot Key'],
|
||||
['Ganons Tower (Map Room)', 'Ganons Tower (Double Switch Room)']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower',
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower',
|
||||
['Ganons Tower - Firesnake Room'], ['Ganons Tower (Firesnake Room)']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower',
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower',
|
||||
['Ganons Tower - Randomizer Room - Top Left',
|
||||
'Ganons Tower - Randomizer Room - Top Right',
|
||||
'Ganons Tower - Randomizer Room - Bottom Left',
|
||||
'Ganons Tower - Randomizer Room - Bottom Right'],
|
||||
['Ganons Tower (Bottom) (West)']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower',
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower',
|
||||
['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest',
|
||||
'Ganons Tower - Big Key Room - Left',
|
||||
'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower',
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower',
|
||||
['Ganons Tower - Mini Helmasaur Room - Left',
|
||||
'Ganons Tower - Mini Helmasaur Room - Right',
|
||||
'Ganons Tower - Pre-Moldorm Chest', 'Ganons Tower - Mini Helmasaur Key Drop'],
|
||||
['Ganons Tower Moldorm Door']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']),
|
||||
|
||||
create_dungeon_region(world, player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None),
|
||||
create_cave_region(world, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']),
|
||||
create_cave_region(world, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']),
|
||||
create_dw_region(world, player, 'Pyramid Ledge', None, ['Pyramid Drop']), # houlihan room exits here in inverted
|
||||
create_dungeon_region(multiworld, player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None),
|
||||
create_cave_region(multiworld, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']),
|
||||
create_cave_region(multiworld, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']),
|
||||
create_dw_region(multiworld, player, 'Pyramid Ledge', None, ['Pyramid Drop']), # houlihan room exits here in inverted
|
||||
|
||||
# to simplify flute connections
|
||||
create_cave_region(world, player, 'The Sky', 'A Dark Sky', None,
|
||||
create_cave_region(multiworld, player, 'The Sky', 'A Dark Sky', None,
|
||||
['DDM Landing', 'NEDW Landing', 'WDW Landing', 'SDW Landing', 'EDW Landing', 'DD Landing',
|
||||
'DLHL Landing']),
|
||||
|
||||
create_lw_region(world, player, 'Desert Northern Cliffs'),
|
||||
create_lw_region(world, player, 'Death Mountain Bunny Descent Area')
|
||||
create_lw_region(multiworld, player, 'Desert Northern Cliffs'),
|
||||
create_lw_region(multiworld, player, 'Death Mountain Bunny Descent Area')
|
||||
]
|
||||
|
||||
|
||||
def mark_dark_world_regions(world, player):
|
||||
def mark_dark_world_regions(multiworld: MultiWorld, player: int):
|
||||
# cross world caves may have some sections marked as both in_light_world, and in_dark_work.
|
||||
# That is ok. the bunny logic will check for this case and incorporate special rules.
|
||||
queue = collections.deque(region for region in world.get_regions(player) if region.type == LTTPRegionType.DarkWorld)
|
||||
queue = collections.deque(region for region in multiworld.get_regions(player) if region.type == LTTPRegionType.DarkWorld)
|
||||
seen = set(queue)
|
||||
while queue:
|
||||
current = queue.popleft()
|
||||
@@ -499,7 +500,7 @@ def mark_dark_world_regions(world, player):
|
||||
seen.add(exit.connected_region)
|
||||
queue.append(exit.connected_region)
|
||||
|
||||
queue = collections.deque(region for region in world.get_regions(player) if region.type == LTTPRegionType.LightWorld)
|
||||
queue = collections.deque(region for region in multiworld.get_regions(player) if region.type == LTTPRegionType.LightWorld)
|
||||
seen = set(queue)
|
||||
while queue:
|
||||
current = queue.popleft()
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
|
||||
from BaseClasses import ItemClassification
|
||||
from BaseClasses import ItemClassification, MultiWorld
|
||||
from Options import OptionError
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .SubClasses import ALttPLocation, LTTPRegion, LTTPRegionType
|
||||
from .Shops import TakeAny, total_shop_slots, set_up_shops, shop_table_by_location, ShopType
|
||||
@@ -14,6 +15,9 @@ from .Options import small_key_shuffle, compass_shuffle, big_key_shuffle, map_sh
|
||||
from .StateHelpers import has_triforce_pieces, has_melee_weapon
|
||||
from .Regions import key_drop_data
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import ALTTPWorld
|
||||
|
||||
# This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space.
|
||||
# Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided.
|
||||
|
||||
@@ -222,7 +226,7 @@ items_reduction_table = (
|
||||
)
|
||||
|
||||
|
||||
def generate_itempool(world):
|
||||
def generate_itempool(world: "ALTTPWorld"):
|
||||
player: int = world.player
|
||||
multiworld = world.multiworld
|
||||
|
||||
@@ -263,7 +267,6 @@ def generate_itempool(world):
|
||||
('Frog', 'Get Frog'),
|
||||
('Missing Smith', 'Return Smith'),
|
||||
('Floodgate', 'Open Floodgate'),
|
||||
('Agahnim 1', 'Beat Agahnim 1'),
|
||||
('Flute Activation Spot', 'Activated Flute'),
|
||||
('Capacity Upgrade Shop', 'Capacity Upgrade Shop')
|
||||
]
|
||||
@@ -532,7 +535,7 @@ take_any_locations_inverted.sort()
|
||||
take_any_locations.sort()
|
||||
|
||||
|
||||
def set_up_take_anys(multiworld, world, player):
|
||||
def set_up_take_anys(multiworld: MultiWorld, world: "ALTTPWorld", player: int):
|
||||
# these are references, do not modify these lists in-place
|
||||
if world.options.mode == 'inverted':
|
||||
take_any_locs = take_any_locations_inverted
|
||||
@@ -586,15 +589,15 @@ def set_up_take_anys(multiworld, world, player):
|
||||
location.place_locked_item(item_factory("Boss Heart Container", world))
|
||||
|
||||
|
||||
def get_pool_core(world, player: int):
|
||||
shuffle = world.worlds[player].options.entrance_shuffle.current_key
|
||||
difficulty = world.worlds[player].options.item_pool.current_key
|
||||
timer = world.worlds[player].options.timer.current_key
|
||||
goal = world.worlds[player].options.goal.current_key
|
||||
mode = world.worlds[player].options.mode.current_key
|
||||
swordless = world.worlds[player].options.swordless
|
||||
retro_bow = world.worlds[player].options.retro_bow
|
||||
logic = world.worlds[player].options.glitches_required
|
||||
def get_pool_core(multiworld: MultiWorld, player: int):
|
||||
shuffle = multiworld.worlds[player].options.entrance_shuffle.current_key
|
||||
difficulty = multiworld.worlds[player].options.item_pool.current_key
|
||||
timer = multiworld.worlds[player].options.timer.current_key
|
||||
goal = multiworld.worlds[player].options.goal.current_key
|
||||
mode = multiworld.worlds[player].options.mode.current_key
|
||||
swordless = multiworld.worlds[player].options.swordless
|
||||
retro_bow = multiworld.worlds[player].options.retro_bow
|
||||
logic = multiworld.worlds[player].options.glitches_required
|
||||
|
||||
pool = []
|
||||
placed_items = {}
|
||||
@@ -611,13 +614,13 @@ def get_pool_core(world, player: int):
|
||||
placed_items[loc] = item
|
||||
|
||||
# provide boots to major glitch dependent seeds
|
||||
if logic.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.worlds[player].options.glitch_boots:
|
||||
if logic.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and multiworld.worlds[player].options.glitch_boots:
|
||||
precollected_items.append('Pegasus Boots')
|
||||
pool.remove('Pegasus Boots')
|
||||
pool.append('Rupees (20)')
|
||||
want_progressives = world.worlds[player].options.progressive.want_progressives
|
||||
want_progressives = multiworld.worlds[player].options.progressive.want_progressives
|
||||
|
||||
if want_progressives(world.random):
|
||||
if want_progressives(multiworld.random):
|
||||
pool.extend(diff.progressiveglove)
|
||||
else:
|
||||
pool.extend(diff.basicglove)
|
||||
@@ -641,27 +644,27 @@ def get_pool_core(world, player: int):
|
||||
thisbottle = None
|
||||
for _ in range(diff.bottle_count):
|
||||
if not diff.same_bottle or not thisbottle:
|
||||
thisbottle = world.random.choice(diff.bottles)
|
||||
thisbottle = multiworld.random.choice(diff.bottles)
|
||||
pool.append(thisbottle)
|
||||
|
||||
if want_progressives(world.random):
|
||||
if want_progressives(multiworld.random):
|
||||
pool.extend(diff.progressiveshield)
|
||||
else:
|
||||
pool.extend(diff.basicshield)
|
||||
|
||||
if want_progressives(world.random):
|
||||
if want_progressives(multiworld.random):
|
||||
pool.extend(diff.progressivearmor)
|
||||
else:
|
||||
pool.extend(diff.basicarmor)
|
||||
|
||||
if want_progressives(world.random):
|
||||
if want_progressives(multiworld.random):
|
||||
pool.extend(diff.progressivemagic)
|
||||
else:
|
||||
pool.extend(diff.basicmagic)
|
||||
|
||||
if want_progressives(world.random):
|
||||
if want_progressives(multiworld.random):
|
||||
pool.extend(diff.progressivebow)
|
||||
world.worlds[player].has_progressive_bows = True
|
||||
multiworld.worlds[player].has_progressive_bows = True
|
||||
elif (swordless or logic == 'no_glitches'):
|
||||
swordless_bows = ['Bow', 'Silver Bow']
|
||||
if difficulty == "easy":
|
||||
@@ -673,7 +676,7 @@ def get_pool_core(world, player: int):
|
||||
if swordless:
|
||||
pool.extend(diff.swordless)
|
||||
else:
|
||||
progressive_swords = want_progressives(world.random)
|
||||
progressive_swords = want_progressives(multiworld.random)
|
||||
pool.extend(diff.progressivesword if progressive_swords else diff.basicsword)
|
||||
|
||||
extraitems = total_items_to_place - len(pool) - len(placed_items)
|
||||
@@ -689,29 +692,29 @@ def get_pool_core(world, player: int):
|
||||
additional_pieces_to_place = 0
|
||||
if 'triforce_hunt' in goal:
|
||||
|
||||
if world.worlds[player].options.triforce_pieces_mode.value == TriforcePiecesMode.option_extra:
|
||||
treasure_hunt_total = (world.worlds[player].options.triforce_pieces_required.value
|
||||
+ world.worlds[player].options.triforce_pieces_extra.value)
|
||||
elif world.worlds[player].options.triforce_pieces_mode.value == TriforcePiecesMode.option_percentage:
|
||||
percentage = float(world.worlds[player].options.triforce_pieces_percentage.value) / 100
|
||||
treasure_hunt_total = int(round(world.worlds[player].options.triforce_pieces_required.value * percentage, 0))
|
||||
if multiworld.worlds[player].options.triforce_pieces_mode.value == TriforcePiecesMode.option_extra:
|
||||
treasure_hunt_total = (multiworld.worlds[player].options.triforce_pieces_required.value
|
||||
+ multiworld.worlds[player].options.triforce_pieces_extra.value)
|
||||
elif multiworld.worlds[player].options.triforce_pieces_mode.value == TriforcePiecesMode.option_percentage:
|
||||
percentage = float(multiworld.worlds[player].options.triforce_pieces_percentage.value) / 100
|
||||
treasure_hunt_total = int(round(multiworld.worlds[player].options.triforce_pieces_required.value * percentage, 0))
|
||||
else: # available
|
||||
treasure_hunt_total = world.worlds[player].options.triforce_pieces_available.value
|
||||
treasure_hunt_total = multiworld.worlds[player].options.triforce_pieces_available.value
|
||||
|
||||
triforce_pieces = min(90, max(treasure_hunt_total, world.worlds[player].options.triforce_pieces_required.value))
|
||||
triforce_pieces = min(90, max(treasure_hunt_total, multiworld.worlds[player].options.triforce_pieces_required.value))
|
||||
|
||||
pieces_in_core = min(extraitems, triforce_pieces)
|
||||
additional_pieces_to_place = triforce_pieces - pieces_in_core
|
||||
pool.extend(["Triforce Piece"] * pieces_in_core)
|
||||
extraitems -= pieces_in_core
|
||||
treasure_hunt_required = world.worlds[player].options.triforce_pieces_required.value
|
||||
treasure_hunt_required = multiworld.worlds[player].options.triforce_pieces_required.value
|
||||
|
||||
for extra in diff.extras:
|
||||
if extraitems >= len(extra):
|
||||
pool.extend(extra)
|
||||
extraitems -= len(extra)
|
||||
elif extraitems > 0:
|
||||
pool.extend(world.random.sample(extra, extraitems))
|
||||
pool.extend(multiworld.random.sample(extra, extraitems))
|
||||
break
|
||||
else:
|
||||
break
|
||||
@@ -730,25 +733,25 @@ def get_pool_core(world, player: int):
|
||||
else:
|
||||
break
|
||||
|
||||
if world.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
|
||||
if multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
|
||||
pool.extend(diff.universal_keys)
|
||||
if mode == 'standard':
|
||||
if world.worlds[player].options.key_drop_shuffle:
|
||||
if multiworld.worlds[player].options.key_drop_shuffle:
|
||||
key_locations = ['Secret Passage', 'Hyrule Castle - Map Guard Key Drop']
|
||||
key_location = world.random.choice(key_locations)
|
||||
key_location = multiworld.random.choice(key_locations)
|
||||
key_locations.remove(key_location)
|
||||
place_item(key_location, "Small Key (Universal)")
|
||||
key_locations += ['Hyrule Castle - Boomerang Guard Key Drop', 'Hyrule Castle - Boomerang Chest',
|
||||
'Hyrule Castle - Map Chest']
|
||||
key_location = world.random.choice(key_locations)
|
||||
key_location = multiworld.random.choice(key_locations)
|
||||
key_locations.remove(key_location)
|
||||
place_item(key_location, "Small Key (Universal)")
|
||||
key_locations += ['Hyrule Castle - Big Key Drop', 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross']
|
||||
key_location = world.random.choice(key_locations)
|
||||
key_location = multiworld.random.choice(key_locations)
|
||||
key_locations.remove(key_location)
|
||||
place_item(key_location, "Small Key (Universal)")
|
||||
key_locations += ['Sewers - Key Rat Key Drop']
|
||||
key_location = world.random.choice(key_locations)
|
||||
key_location = multiworld.random.choice(key_locations)
|
||||
place_item(key_location, "Small Key (Universal)")
|
||||
pool = pool[:-3]
|
||||
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import ItemClassification as IC
|
||||
from BaseClasses import MultiWorld, ItemClassification as IC
|
||||
from worlds.AutoWorld import World
|
||||
|
||||
|
||||
def GetBeemizerItem(world, player: int, item):
|
||||
def GetBeemizerItem(multiworld: MultiWorld, player: int, item):
|
||||
item_name = item if isinstance(item, str) else item.name
|
||||
|
||||
if item_name not in trap_replaceable or player in world.groups:
|
||||
if item_name not in trap_replaceable or player in multiworld.groups:
|
||||
return item
|
||||
|
||||
# first roll - replaceable item should be replaced, within beemizer_total_chance
|
||||
if not world.worlds[player].options.beemizer_total_chance or world.random.random() > (world.worlds[player].options.beemizer_total_chance / 100):
|
||||
if not multiworld.worlds[player].options.beemizer_total_chance or multiworld.random.random() > (multiworld.worlds[player].options.beemizer_total_chance / 100):
|
||||
return item
|
||||
|
||||
# second roll - bee replacement should be trap, within beemizer_trap_chance
|
||||
if not world.worlds[player].options.beemizer_trap_chance or world.random.random() > (world.worlds[player].options.beemizer_trap_chance / 100):
|
||||
return "Bee" if isinstance(item, str) else world.create_item("Bee", player)
|
||||
if not multiworld.worlds[player].options.beemizer_trap_chance or multiworld.random.random() > (multiworld.worlds[player].options.beemizer_trap_chance / 100):
|
||||
return "Bee" if isinstance(item, str) else multiworld.create_item("Bee", player)
|
||||
else:
|
||||
return "Bee Trap" if isinstance(item, str) else world.create_item("Bee Trap", player)
|
||||
return "Bee Trap" if isinstance(item, str) else multiworld.create_item("Bee Trap", player)
|
||||
|
||||
|
||||
def item_factory(items: typing.Union[str, typing.Iterable[str]], world: World):
|
||||
|
||||
@@ -154,13 +154,13 @@ class OpenPyramid(Choice):
|
||||
alias_true = option_open
|
||||
alias_false = option_closed
|
||||
|
||||
def to_bool(self, world: MultiWorld, player: int) -> bool:
|
||||
def to_bool(self, multiworld: MultiWorld, player: int) -> bool:
|
||||
if self.value == self.option_goal:
|
||||
return world.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'}
|
||||
return multiworld.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'}
|
||||
elif self.value == self.option_auto:
|
||||
return world.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'} \
|
||||
and (world.worlds[player].options.entrance_shuffle.current_key in {'vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed'} or not
|
||||
world.shuffle_ganon)
|
||||
return multiworld.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'} \
|
||||
and (multiworld.worlds[player].options.entrance_shuffle.current_key in {'vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed'} or not
|
||||
multiworld.shuffle_ganon)
|
||||
elif self.value == self.option_open:
|
||||
return True
|
||||
else:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Helper functions to deliver entrance/exit/region sets to OWG rules.
|
||||
"""
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from .StateHelpers import can_lift_heavy_rocks, can_boots_clip_lw, can_boots_clip_dw, can_get_glitched_speed_dw
|
||||
|
||||
|
||||
@@ -200,7 +201,7 @@ def get_mirror_offset_spots_dw():
|
||||
yield ('Dark Death Mountain Offset Mirror', 'Dark Death Mountain (West Bottom)', 'East Dark World')
|
||||
|
||||
|
||||
def get_mirror_offset_spots_lw(player):
|
||||
def get_mirror_offset_spots_lw(player: int):
|
||||
"""
|
||||
Mirror shenanigans placing a mirror portal with a broken camera
|
||||
"""
|
||||
@@ -218,54 +219,54 @@ def get_invalid_bunny_revival_dungeons():
|
||||
yield 'Sanctuary'
|
||||
|
||||
|
||||
def overworld_glitch_connections(world, player):
|
||||
def overworld_glitch_connections(multiworld: MultiWorld, player: int):
|
||||
# Boots-accessible locations.
|
||||
create_owg_connections(player, world, get_boots_clip_exits_lw(world.worlds[player].options.mode == 'inverted'))
|
||||
create_owg_connections(player, world, get_boots_clip_exits_dw(world.worlds[player].options.mode == 'inverted', player))
|
||||
create_owg_connections(player, multiworld, get_boots_clip_exits_lw(multiworld.worlds[player].options.mode == 'inverted'))
|
||||
create_owg_connections(player, multiworld, get_boots_clip_exits_dw(multiworld.worlds[player].options.mode == 'inverted', player))
|
||||
|
||||
# Glitched speed drops.
|
||||
create_owg_connections(player, world, get_glitched_speed_drops_dw(world.worlds[player].options.mode == 'inverted'))
|
||||
create_owg_connections(player, multiworld, get_glitched_speed_drops_dw(multiworld.worlds[player].options.mode == 'inverted'))
|
||||
|
||||
# Mirror clip spots.
|
||||
if world.worlds[player].options.mode != 'inverted':
|
||||
create_owg_connections(player, world, get_mirror_clip_spots_dw())
|
||||
create_owg_connections(player, world, get_mirror_offset_spots_dw())
|
||||
if multiworld.worlds[player].options.mode != 'inverted':
|
||||
create_owg_connections(player, multiworld, get_mirror_clip_spots_dw())
|
||||
create_owg_connections(player, multiworld, get_mirror_offset_spots_dw())
|
||||
else:
|
||||
create_owg_connections(player, world, get_mirror_offset_spots_lw(player))
|
||||
create_owg_connections(player, multiworld, get_mirror_offset_spots_lw(player))
|
||||
|
||||
|
||||
def overworld_glitches_rules(world, player):
|
||||
def overworld_glitches_rules(multiworld: MultiWorld, player: int):
|
||||
|
||||
# Boots-accessible locations.
|
||||
set_owg_connection_rules(player, world, get_boots_clip_exits_lw(world.worlds[player].options.mode == 'inverted'), lambda state: can_boots_clip_lw(state, player))
|
||||
set_owg_connection_rules(player, world, get_boots_clip_exits_dw(world.worlds[player].options.mode == 'inverted', player), lambda state: can_boots_clip_dw(state, player))
|
||||
set_owg_connection_rules(player, multiworld, get_boots_clip_exits_lw(multiworld.worlds[player].options.mode == 'inverted'), lambda state: can_boots_clip_lw(state, player))
|
||||
set_owg_connection_rules(player, multiworld, get_boots_clip_exits_dw(multiworld.worlds[player].options.mode == 'inverted', player), lambda state: can_boots_clip_dw(state, player))
|
||||
|
||||
# Glitched speed drops.
|
||||
set_owg_connection_rules(player, world, get_glitched_speed_drops_dw(world.worlds[player].options.mode == 'inverted'), lambda state: can_get_glitched_speed_dw(state, player))
|
||||
set_owg_connection_rules(player, multiworld, get_glitched_speed_drops_dw(multiworld.worlds[player].options.mode == 'inverted'), lambda state: can_get_glitched_speed_dw(state, player))
|
||||
# Dark Death Mountain Ledge Clip Spot also accessible with mirror.
|
||||
if world.worlds[player].options.mode != 'inverted':
|
||||
add_alternate_rule(world.get_entrance('Dark Death Mountain Ledge Clip Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
if multiworld.worlds[player].options.mode != 'inverted':
|
||||
add_alternate_rule(multiworld.get_entrance('Dark Death Mountain Ledge Clip Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
|
||||
# Mirror clip spots.
|
||||
if world.worlds[player].options.mode != 'inverted':
|
||||
set_owg_connection_rules(player, world, get_mirror_clip_spots_dw(), lambda state: state.has('Magic Mirror', player))
|
||||
set_owg_connection_rules(player, world, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and can_boots_clip_lw(state, player))
|
||||
if multiworld.worlds[player].options.mode != 'inverted':
|
||||
set_owg_connection_rules(player, multiworld, get_mirror_clip_spots_dw(), lambda state: state.has('Magic Mirror', player))
|
||||
set_owg_connection_rules(player, multiworld, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and can_boots_clip_lw(state, player))
|
||||
else:
|
||||
set_owg_connection_rules(player, world, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and can_boots_clip_dw(state, player))
|
||||
set_owg_connection_rules(player, multiworld, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and can_boots_clip_dw(state, player))
|
||||
|
||||
# Regions that require the boots and some other stuff.
|
||||
if world.worlds[player].options.mode != 'inverted':
|
||||
world.get_entrance('Turtle Rock Teleporter', player).access_rule = lambda state: (can_boots_clip_lw(state, player) or can_lift_heavy_rocks(state, player)) and state.has('Hammer', player)
|
||||
add_alternate_rule(world.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Moon Pearl', player) or state.has('Pegasus Boots', player))
|
||||
if multiworld.worlds[player].options.mode != 'inverted':
|
||||
multiworld.get_entrance('Turtle Rock Teleporter', player).access_rule = lambda state: (can_boots_clip_lw(state, player) or can_lift_heavy_rocks(state, player)) and state.has('Hammer', player)
|
||||
add_alternate_rule(multiworld.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Moon Pearl', player) or state.has('Pegasus Boots', player))
|
||||
else:
|
||||
add_alternate_rule(world.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Moon Pearl', player))
|
||||
add_alternate_rule(multiworld.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Moon Pearl', player))
|
||||
|
||||
world.get_entrance('Dark Desert Teleporter', player).access_rule = lambda state: (state.has('Flute', player) or state.has('Pegasus Boots', player)) and can_lift_heavy_rocks(state, player)
|
||||
add_alternate_rule(world.get_entrance('Catfish Exit Rock', player), lambda state: can_boots_clip_dw(state, player))
|
||||
add_alternate_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: can_boots_clip_dw(state, player))
|
||||
multiworld.get_entrance('Dark Desert Teleporter', player).access_rule = lambda state: (state.has('Flute', player) or state.has('Pegasus Boots', player)) and can_lift_heavy_rocks(state, player)
|
||||
add_alternate_rule(multiworld.get_entrance('Catfish Exit Rock', player), lambda state: can_boots_clip_dw(state, player))
|
||||
add_alternate_rule(multiworld.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: can_boots_clip_dw(state, player))
|
||||
|
||||
# Zora's Ledge via waterwalk setup.
|
||||
add_alternate_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Pegasus Boots', player))
|
||||
add_alternate_rule(multiworld.get_location('Zora\'s Ledge', player), lambda state: state.has('Pegasus Boots', player))
|
||||
|
||||
|
||||
def add_alternate_rule(entrance, rule):
|
||||
@@ -273,22 +274,22 @@ def add_alternate_rule(entrance, rule):
|
||||
entrance.access_rule = lambda state: old_rule(state) or rule(state)
|
||||
|
||||
|
||||
def create_no_logic_connections(player, world, connections):
|
||||
def create_no_logic_connections(player: int, multiworld: MultiWorld, connections):
|
||||
for entrance, parent_region, target_region, *rule_override in connections:
|
||||
parent = world.get_region(parent_region, player)
|
||||
target = world.get_region(target_region, player)
|
||||
parent = multiworld.get_region(parent_region, player)
|
||||
target = multiworld.get_region(target_region, player)
|
||||
parent.connect(target, entrance)
|
||||
|
||||
|
||||
def create_owg_connections(player, world, connections):
|
||||
def create_owg_connections(player: int, multiworld: MultiWorld, connections):
|
||||
for entrance, parent_region, target_region, *rule_override in connections:
|
||||
parent = world.get_region(parent_region, player)
|
||||
target = world.get_region(target_region, player)
|
||||
parent = multiworld.get_region(parent_region, player)
|
||||
target = multiworld.get_region(target_region, player)
|
||||
parent.connect(target, entrance)
|
||||
|
||||
|
||||
def set_owg_connection_rules(player, world, connections, default_rule):
|
||||
def set_owg_connection_rules(player: int, multiworld: MultiWorld, connections, default_rule):
|
||||
for entrance, _, _, *rule_override in connections:
|
||||
connection = world.get_entrance(entrance, player)
|
||||
connection = multiworld.get_entrance(entrance, player)
|
||||
rule = rule_override[0] if len(rule_override) > 0 else default_rule
|
||||
connection.access_rule = rule
|
||||
|
||||
@@ -9,11 +9,11 @@ def is_main_entrance(entrance: LTTPEntrance) -> bool:
|
||||
return entrance.parent_region.type in {LTTPRegionType.DarkWorld, LTTPRegionType.LightWorld} if entrance.parent_region.type else True
|
||||
|
||||
|
||||
def create_regions(world, player):
|
||||
def create_regions(multiworld: MultiWorld, player: int):
|
||||
|
||||
world.regions += [
|
||||
create_lw_region(world, player, 'Menu', None, ['Links House S&Q', 'Sanctuary S&Q', 'Old Man S&Q']),
|
||||
create_lw_region(world, player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure',
|
||||
multiworld.regions += [
|
||||
create_lw_region(multiworld, player, 'Menu', None, ['Links House S&Q', 'Sanctuary S&Q', 'Old Man S&Q']),
|
||||
create_lw_region(multiworld, player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure',
|
||||
'Purple Chest', 'Flute Activation Spot'],
|
||||
["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Zoras River', 'Kings Grave Outer Rocks', 'Dam',
|
||||
'Links House', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut', 'Kakariko Well Drop', 'Kakariko Well Cave',
|
||||
@@ -24,122 +24,122 @@ def create_regions(world, player):
|
||||
'Elder House (East)', 'Elder House (West)', 'North Fairy Cave', 'North Fairy Cave Drop', 'Lost Woods Gamble', 'Snitch Lady (East)', 'Snitch Lady (West)', 'Tavern (Front)',
|
||||
'Bush Covered House', 'Light World Bomb Hut', 'Kakariko Shop', 'Long Fairy Cave', 'Good Bee Cave', '20 Rupee Cave', 'Cave Shop (Lake Hylia)', 'Waterfall of Wishing', 'Hyrule Castle Main Gate',
|
||||
'Bonk Fairy (Light)', '50 Rupee Cave', 'Fortune Teller (Light)', 'Lake Hylia Fairy', 'Light Hype Fairy', 'Desert Fairy', 'Lumberjack House', 'Lake Hylia Fortune Teller', 'Kakariko Gamble Game', 'Top of Pyramid']),
|
||||
create_lw_region(world, player, 'Death Mountain Entrance', None, ['Old Man Cave (West)', 'Death Mountain Entrance Drop']),
|
||||
create_lw_region(world, player, 'Lake Hylia Central Island', None, ['Capacity Upgrade', 'Lake Hylia Central Island Teleporter']),
|
||||
create_cave_region(world, player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top",
|
||||
create_lw_region(multiworld, player, 'Death Mountain Entrance', None, ['Old Man Cave (West)', 'Death Mountain Entrance Drop']),
|
||||
create_lw_region(multiworld, player, 'Lake Hylia Central Island', None, ['Capacity Upgrade', 'Lake Hylia Central Island Teleporter']),
|
||||
create_cave_region(multiworld, player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top",
|
||||
"Blind\'s Hideout - Left",
|
||||
"Blind\'s Hideout - Right",
|
||||
"Blind\'s Hideout - Far Left",
|
||||
"Blind\'s Hideout - Far Right"]),
|
||||
create_cave_region(world, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit', ['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']),
|
||||
create_lw_region(world, player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']),
|
||||
create_cave_region(world, player, 'Waterfall of Wishing', 'a cave with two chests', ['Waterfall Fairy - Left', 'Waterfall Fairy - Right']),
|
||||
create_lw_region(world, player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']),
|
||||
create_cave_region(world, player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']),
|
||||
create_cave_region(world, player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']),
|
||||
create_cave_region(world, player, 'Dam', 'the dam', ['Floodgate', 'Floodgate Chest']),
|
||||
create_cave_region(world, player, 'Links House', 'your house', ['Link\'s House'], ['Links House Exit']),
|
||||
create_cave_region(world, player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']),
|
||||
create_cave_region(world, player, 'Tavern', 'the tavern', ['Kakariko Tavern']),
|
||||
create_cave_region(world, player, 'Elder House', 'a connector', None, ['Elder House Exit (East)', 'Elder House Exit (West)']),
|
||||
create_cave_region(world, player, 'Snitch Lady (East)', 'a boring house'),
|
||||
create_cave_region(world, player, 'Snitch Lady (West)', 'a boring house'),
|
||||
create_cave_region(world, player, 'Bush Covered House', 'the grass man'),
|
||||
create_cave_region(world, player, 'Tavern (Front)', 'the tavern'),
|
||||
create_cave_region(world, player, 'Light World Bomb Hut', 'a restock room'),
|
||||
create_cave_region(world, player, 'Kakariko Shop', 'a common shop'),
|
||||
create_cave_region(world, player, 'Fortune Teller (Light)', 'a fortune teller'),
|
||||
create_cave_region(world, player, 'Lake Hylia Fortune Teller', 'a fortune teller'),
|
||||
create_cave_region(world, player, 'Lumberjack House', 'a boring house'),
|
||||
create_cave_region(world, player, 'Bonk Fairy (Light)', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Bonk Fairy (Dark)', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Lake Hylia Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Swamp Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Desert Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Dark Lake Hylia Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Dark Lake Hylia Ledge Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Dark Desert Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Chicken House', 'a house with a chest', ['Chicken House']),
|
||||
create_cave_region(world, player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']),
|
||||
create_cave_region(world, player, 'Sahasrahlas Hut', 'Sahasrahla', ['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right', 'Sahasrahla']),
|
||||
create_cave_region(world, player, 'Kakariko Well (top)', 'a drop\'s exit', ['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle',
|
||||
create_cave_region(multiworld, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit', ['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']),
|
||||
create_lw_region(multiworld, player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']),
|
||||
create_cave_region(multiworld, player, 'Waterfall of Wishing', 'a cave with two chests', ['Waterfall Fairy - Left', 'Waterfall Fairy - Right']),
|
||||
create_lw_region(multiworld, player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']),
|
||||
create_cave_region(multiworld, player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']),
|
||||
create_cave_region(multiworld, player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']),
|
||||
create_cave_region(multiworld, player, 'Dam', 'the dam', ['Floodgate', 'Floodgate Chest']),
|
||||
create_cave_region(multiworld, player, 'Links House', 'your house', ['Link\'s House'], ['Links House Exit']),
|
||||
create_cave_region(multiworld, player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']),
|
||||
create_cave_region(multiworld, player, 'Tavern', 'the tavern', ['Kakariko Tavern']),
|
||||
create_cave_region(multiworld, player, 'Elder House', 'a connector', None, ['Elder House Exit (East)', 'Elder House Exit (West)']),
|
||||
create_cave_region(multiworld, player, 'Snitch Lady (East)', 'a boring house'),
|
||||
create_cave_region(multiworld, player, 'Snitch Lady (West)', 'a boring house'),
|
||||
create_cave_region(multiworld, player, 'Bush Covered House', 'the grass man'),
|
||||
create_cave_region(multiworld, player, 'Tavern (Front)', 'the tavern'),
|
||||
create_cave_region(multiworld, player, 'Light World Bomb Hut', 'a restock room'),
|
||||
create_cave_region(multiworld, player, 'Kakariko Shop', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Fortune Teller (Light)', 'a fortune teller'),
|
||||
create_cave_region(multiworld, player, 'Lake Hylia Fortune Teller', 'a fortune teller'),
|
||||
create_cave_region(multiworld, player, 'Lumberjack House', 'a boring house'),
|
||||
create_cave_region(multiworld, player, 'Bonk Fairy (Light)', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Bonk Fairy (Dark)', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Lake Hylia Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Swamp Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Desert Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Dark Lake Hylia Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Dark Desert Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Chicken House', 'a house with a chest', ['Chicken House']),
|
||||
create_cave_region(multiworld, player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']),
|
||||
create_cave_region(multiworld, player, 'Sahasrahlas Hut', 'Sahasrahla', ['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right', 'Sahasrahla']),
|
||||
create_cave_region(multiworld, player, 'Kakariko Well (top)', 'a drop\'s exit', ['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle',
|
||||
'Kakariko Well - Right', 'Kakariko Well - Bottom'], ['Kakariko Well (top to bottom)']),
|
||||
create_cave_region(world, player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']),
|
||||
create_cave_region(world, player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']),
|
||||
create_lw_region(world, player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']),
|
||||
create_cave_region(world, player, 'Bat Cave (right)', 'a drop\'s exit', ['Magic Bat'], ['Bat Cave Door']),
|
||||
create_cave_region(world, player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']),
|
||||
create_cave_region(world, player, 'Sick Kids House', 'the sick kid', ['Sick Kid']),
|
||||
create_lw_region(world, player, 'Hobo Bridge', ['Hobo']),
|
||||
create_cave_region(world, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'], ['Lost Woods Hideout (top to bottom)']),
|
||||
create_cave_region(world, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None, ['Lost Woods Hideout Exit']),
|
||||
create_cave_region(world, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'], ['Lumberjack Tree (top to bottom)']),
|
||||
create_cave_region(world, player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']),
|
||||
create_lw_region(world, player, 'Cave 45 Ledge', None, ['Cave 45']),
|
||||
create_cave_region(world, player, 'Cave 45', 'a cave with an item', ['Cave 45']),
|
||||
create_lw_region(world, player, 'Graveyard Ledge', None, ['Graveyard Cave']),
|
||||
create_cave_region(world, player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']),
|
||||
create_cave_region(world, player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']),
|
||||
create_cave_region(world, player, 'Long Fairy Cave', 'a fairy fountain'),
|
||||
create_cave_region(world, player, 'Mini Moldorm Cave', 'a bounty of five items', ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right',
|
||||
create_cave_region(multiworld, player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']),
|
||||
create_cave_region(multiworld, player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']),
|
||||
create_lw_region(multiworld, player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']),
|
||||
create_cave_region(multiworld, player, 'Bat Cave (right)', 'a drop\'s exit', ['Magic Bat'], ['Bat Cave Door']),
|
||||
create_cave_region(multiworld, player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']),
|
||||
create_cave_region(multiworld, player, 'Sick Kids House', 'the sick kid', ['Sick Kid']),
|
||||
create_lw_region(multiworld, player, 'Hobo Bridge', ['Hobo']),
|
||||
create_cave_region(multiworld, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'], ['Lost Woods Hideout (top to bottom)']),
|
||||
create_cave_region(multiworld, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None, ['Lost Woods Hideout Exit']),
|
||||
create_cave_region(multiworld, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'], ['Lumberjack Tree (top to bottom)']),
|
||||
create_cave_region(multiworld, player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']),
|
||||
create_lw_region(multiworld, player, 'Cave 45 Ledge', None, ['Cave 45']),
|
||||
create_cave_region(multiworld, player, 'Cave 45', 'a cave with an item', ['Cave 45']),
|
||||
create_lw_region(multiworld, player, 'Graveyard Ledge', None, ['Graveyard Cave']),
|
||||
create_cave_region(multiworld, player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']),
|
||||
create_cave_region(multiworld, player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']),
|
||||
create_cave_region(multiworld, player, 'Long Fairy Cave', 'a fairy fountain'),
|
||||
create_cave_region(multiworld, player, 'Mini Moldorm Cave', 'a bounty of five items', ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right',
|
||||
'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Generous Guy']),
|
||||
create_cave_region(world, player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']),
|
||||
create_cave_region(world, player, 'Good Bee Cave', 'a cold bee'),
|
||||
create_cave_region(world, player, '20 Rupee Cave', 'a cave with some cash'),
|
||||
create_cave_region(world, player, 'Cave Shop (Lake Hylia)', 'a common shop'),
|
||||
create_cave_region(world, player, 'Cave Shop (Dark Death Mountain)', 'a common shop'),
|
||||
create_cave_region(world, player, 'Bonk Rock Cave', 'a cave with a chest', ['Bonk Rock Cave']),
|
||||
create_cave_region(world, player, 'Library', 'the library', ['Library']),
|
||||
create_cave_region(world, player, 'Kakariko Gamble Game', 'a game of chance'),
|
||||
create_cave_region(world, player, 'Potion Shop', 'the potion shop', ['Potion Shop']),
|
||||
create_lw_region(world, player, 'Lake Hylia Island', ['Lake Hylia Island']),
|
||||
create_cave_region(world, player, 'Capacity Upgrade', 'the queen of fairies', ['Capacity Upgrade Shop']),
|
||||
create_cave_region(world, player, 'Two Brothers House', 'a connector', None, ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']),
|
||||
create_lw_region(world, player, 'Maze Race Ledge', ['Maze Race'], ['Two Brothers House (West)']),
|
||||
create_cave_region(world, player, '50 Rupee Cave', 'a cave with some cash'),
|
||||
create_lw_region(world, player, 'Desert Ledge', ['Desert Ledge'], ['Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (West)']),
|
||||
create_lw_region(world, player, 'Desert Ledge (Northeast)', None, ['Checkerboard Cave']),
|
||||
create_lw_region(world, player, 'Desert Palace Stairs', None, ['Desert Palace Entrance (South)']),
|
||||
create_lw_region(world, player, 'Desert Palace Lone Stairs', None, ['Desert Palace Stairs Drop', 'Desert Palace Entrance (East)']),
|
||||
create_lw_region(world, player, 'Desert Palace Entrance (North) Spot', None, ['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks']),
|
||||
create_dungeon_region(world, player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
|
||||
create_cave_region(multiworld, player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']),
|
||||
create_cave_region(multiworld, player, 'Good Bee Cave', 'a cold bee'),
|
||||
create_cave_region(multiworld, player, '20 Rupee Cave', 'a cave with some cash'),
|
||||
create_cave_region(multiworld, player, 'Cave Shop (Lake Hylia)', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Cave Shop (Dark Death Mountain)', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Bonk Rock Cave', 'a cave with a chest', ['Bonk Rock Cave']),
|
||||
create_cave_region(multiworld, player, 'Library', 'the library', ['Library']),
|
||||
create_cave_region(multiworld, player, 'Kakariko Gamble Game', 'a game of chance'),
|
||||
create_cave_region(multiworld, player, 'Potion Shop', 'the potion shop', ['Potion Shop']),
|
||||
create_lw_region(multiworld, player, 'Lake Hylia Island', ['Lake Hylia Island']),
|
||||
create_cave_region(multiworld, player, 'Capacity Upgrade', 'the queen of fairies', ['Capacity Upgrade Shop']),
|
||||
create_cave_region(multiworld, player, 'Two Brothers House', 'a connector', None, ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']),
|
||||
create_lw_region(multiworld, player, 'Maze Race Ledge', ['Maze Race'], ['Two Brothers House (West)']),
|
||||
create_cave_region(multiworld, player, '50 Rupee Cave', 'a cave with some cash'),
|
||||
create_lw_region(multiworld, player, 'Desert Ledge', ['Desert Ledge'], ['Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (West)']),
|
||||
create_lw_region(multiworld, player, 'Desert Ledge (Northeast)', None, ['Checkerboard Cave']),
|
||||
create_lw_region(multiworld, player, 'Desert Palace Stairs', None, ['Desert Palace Entrance (South)']),
|
||||
create_lw_region(multiworld, player, 'Desert Palace Lone Stairs', None, ['Desert Palace Stairs Drop', 'Desert Palace Entrance (East)']),
|
||||
create_lw_region(multiworld, player, 'Desert Palace Entrance (North) Spot', None, ['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks']),
|
||||
create_dungeon_region(multiworld, player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
|
||||
['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)', 'Desert Palace East Wing']),
|
||||
create_dungeon_region(world, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
|
||||
create_dungeon_region(world, player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
|
||||
create_dungeon_region(world, player, 'Desert Palace North', 'Desert Palace', ['Desert Palace - Desert Tiles 1 Pot Key', 'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key',
|
||||
create_dungeon_region(multiworld, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
|
||||
create_dungeon_region(multiworld, player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
|
||||
create_dungeon_region(multiworld, player, 'Desert Palace North', 'Desert Palace', ['Desert Palace - Desert Tiles 1 Pot Key', 'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key',
|
||||
'Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']),
|
||||
create_dungeon_region(world, player, 'Eastern Palace', 'Eastern Palace', ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest', 'Eastern Palace - Cannonball Chest',
|
||||
create_dungeon_region(multiworld, player, 'Eastern Palace', 'Eastern Palace', ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest', 'Eastern Palace - Cannonball Chest',
|
||||
'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop', 'Eastern Palace - Big Key Chest',
|
||||
'Eastern Palace - Map Chest', 'Eastern Palace - Boss', 'Eastern Palace - Prize'], ['Eastern Palace Exit']),
|
||||
create_lw_region(world, player, 'Master Sword Meadow', ['Master Sword Pedestal']),
|
||||
create_cave_region(world, player, 'Lost Woods Gamble', 'a game of chance'),
|
||||
create_lw_region(world, player, 'Hyrule Castle Courtyard', None, ['Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Entrance (South)']),
|
||||
create_lw_region(world, player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Agahnims Tower', 'Hyrule Castle Ledge Courtyard Drop']),
|
||||
create_dungeon_region(world, player, 'Hyrule Castle', 'Hyrule Castle', ['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest',
|
||||
create_lw_region(multiworld, player, 'Master Sword Meadow', ['Master Sword Pedestal']),
|
||||
create_cave_region(multiworld, player, 'Lost Woods Gamble', 'a game of chance'),
|
||||
create_lw_region(multiworld, player, 'Hyrule Castle Courtyard', None, ['Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Entrance (South)']),
|
||||
create_lw_region(multiworld, player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Agahnims Tower', 'Hyrule Castle Ledge Courtyard Drop']),
|
||||
create_dungeon_region(multiworld, player, 'Hyrule Castle', 'Hyrule Castle', ['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest',
|
||||
'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop', 'Hyrule Castle - Big Key Drop'],
|
||||
['Hyrule Castle Exit (East)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (South)', 'Throne Room']),
|
||||
create_dungeon_region(world, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks
|
||||
create_dungeon_region(world, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']),
|
||||
create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit', None, ['Sanctuary Push Door', 'Sewers Back Door', 'Sewers Secret Room']),
|
||||
create_dungeon_region(world, player, 'Sewers Secret Room', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
|
||||
create_dungeon_region(multiworld, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks
|
||||
create_dungeon_region(multiworld, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']),
|
||||
create_dungeon_region(multiworld, player, 'Sewers', 'a drop\'s exit', None, ['Sanctuary Push Door', 'Sewers Back Door', 'Sewers Secret Room']),
|
||||
create_dungeon_region(multiworld, player, 'Sewers Secret Room', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
|
||||
'Sewers - Secret Room - Right']),
|
||||
create_dungeon_region(world, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']),
|
||||
create_dungeon_region(world, player, 'Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Agahnims Tower Exit']),
|
||||
create_dungeon_region(world, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None),
|
||||
create_cave_region(world, player, 'Old Man Cave', 'a connector', ['Old Man'], ['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']),
|
||||
create_cave_region(world, player, 'Old Man House', 'a connector', None, ['Old Man House Exit (Bottom)', 'Old Man House Front to Back']),
|
||||
create_cave_region(world, player, 'Old Man House Back', 'a connector', None, ['Old Man House Exit (Top)', 'Old Man House Back to Front']),
|
||||
create_lw_region(world, player, 'Death Mountain', None, ['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave', 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Broken Bridge (West)', 'Death Mountain Teleporter']),
|
||||
create_cave_region(world, player, 'Death Mountain Return Cave', 'a connector', None, ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']),
|
||||
create_lw_region(world, player, 'Death Mountain Return Ledge', None, ['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)']),
|
||||
create_cave_region(world, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'], ['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']),
|
||||
create_cave_region(world, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None, ['Spectacle Rock Cave Exit']),
|
||||
create_cave_region(world, player, 'Spectacle Rock Cave (Peak)', 'a connector', None, ['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']),
|
||||
create_lw_region(world, player, 'East Death Mountain (Bottom)', None, ['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'East Death Mountain Teleporter', 'Hookshot Fairy', 'Fairy Ascension Rocks', 'Spiral Cave (Bottom)']),
|
||||
create_cave_region(world, player, 'Hookshot Fairy', 'fairies deep in a cave'),
|
||||
create_cave_region(world, player, 'Paradox Cave Front', 'a connector', None, ['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)', 'Light World Death Mountain Shop']),
|
||||
create_cave_region(world, player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left',
|
||||
create_dungeon_region(multiworld, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Agahnims Tower Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None),
|
||||
create_cave_region(multiworld, player, 'Old Man Cave', 'a connector', ['Old Man'], ['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']),
|
||||
create_cave_region(multiworld, player, 'Old Man House', 'a connector', None, ['Old Man House Exit (Bottom)', 'Old Man House Front to Back']),
|
||||
create_cave_region(multiworld, player, 'Old Man House Back', 'a connector', None, ['Old Man House Exit (Top)', 'Old Man House Back to Front']),
|
||||
create_lw_region(multiworld, player, 'Death Mountain', None, ['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave', 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Broken Bridge (West)', 'Death Mountain Teleporter']),
|
||||
create_cave_region(multiworld, player, 'Death Mountain Return Cave', 'a connector', None, ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']),
|
||||
create_lw_region(multiworld, player, 'Death Mountain Return Ledge', None, ['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)']),
|
||||
create_cave_region(multiworld, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'], ['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']),
|
||||
create_cave_region(multiworld, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None, ['Spectacle Rock Cave Exit']),
|
||||
create_cave_region(multiworld, player, 'Spectacle Rock Cave (Peak)', 'a connector', None, ['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']),
|
||||
create_lw_region(multiworld, player, 'East Death Mountain (Bottom)', None, ['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'East Death Mountain Teleporter', 'Hookshot Fairy', 'Fairy Ascension Rocks', 'Spiral Cave (Bottom)']),
|
||||
create_cave_region(multiworld, player, 'Hookshot Fairy', 'fairies deep in a cave'),
|
||||
create_cave_region(multiworld, player, 'Paradox Cave Front', 'a connector', None, ['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)', 'Light World Death Mountain Shop']),
|
||||
create_cave_region(multiworld, player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left',
|
||||
'Paradox Cave Lower - Left',
|
||||
'Paradox Cave Lower - Right',
|
||||
'Paradox Cave Lower - Far Right',
|
||||
@@ -147,267 +147,267 @@ def create_regions(world, player):
|
||||
'Paradox Cave Upper - Left',
|
||||
'Paradox Cave Upper - Right'],
|
||||
['Paradox Cave Push Block', 'Paradox Cave Bomb Jump']),
|
||||
create_cave_region(world, player, 'Paradox Cave', 'a connector', None,
|
||||
create_cave_region(multiworld, player, 'Paradox Cave', 'a connector', None,
|
||||
['Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Paradox Cave Drop']),
|
||||
create_cave_region(world, player, 'Light World Death Mountain Shop', 'a common shop'),
|
||||
create_lw_region(world, player, 'East Death Mountain (Top)', None,
|
||||
create_cave_region(multiworld, player, 'Light World Death Mountain Shop', 'a common shop'),
|
||||
create_lw_region(multiworld, player, 'East Death Mountain (Top)', None,
|
||||
['Paradox Cave (Top)', 'Death Mountain (Top)', 'Spiral Cave Ledge Access',
|
||||
'East Death Mountain Drop', 'Turtle Rock Teleporter', 'Fairy Ascension Ledge']),
|
||||
create_lw_region(world, player, 'Spiral Cave Ledge', None, ['Spiral Cave', 'Spiral Cave Ledge Drop']),
|
||||
create_cave_region(world, player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'],
|
||||
create_lw_region(multiworld, player, 'Spiral Cave Ledge', None, ['Spiral Cave', 'Spiral Cave Ledge Drop']),
|
||||
create_cave_region(multiworld, player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'],
|
||||
['Spiral Cave (top to bottom)', 'Spiral Cave Exit (Top)']),
|
||||
create_cave_region(world, player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']),
|
||||
create_lw_region(world, player, 'Fairy Ascension Plateau', None,
|
||||
create_cave_region(multiworld, player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']),
|
||||
create_lw_region(multiworld, player, 'Fairy Ascension Plateau', None,
|
||||
['Fairy Ascension Drop', 'Fairy Ascension Cave (Bottom)']),
|
||||
create_cave_region(world, player, 'Fairy Ascension Cave (Bottom)', 'a connector', None,
|
||||
create_cave_region(multiworld, player, 'Fairy Ascension Cave (Bottom)', 'a connector', None,
|
||||
['Fairy Ascension Cave Climb', 'Fairy Ascension Cave Exit (Bottom)']),
|
||||
create_cave_region(world, player, 'Fairy Ascension Cave (Drop)', 'a connector', None,
|
||||
create_cave_region(multiworld, player, 'Fairy Ascension Cave (Drop)', 'a connector', None,
|
||||
['Fairy Ascension Cave Pots']),
|
||||
create_cave_region(world, player, 'Fairy Ascension Cave (Top)', 'a connector', None,
|
||||
create_cave_region(multiworld, player, 'Fairy Ascension Cave (Top)', 'a connector', None,
|
||||
['Fairy Ascension Cave Exit (Top)', 'Fairy Ascension Cave Drop']),
|
||||
create_lw_region(world, player, 'Fairy Ascension Ledge', None,
|
||||
create_lw_region(multiworld, player, 'Fairy Ascension Ledge', None,
|
||||
['Fairy Ascension Ledge Drop', 'Fairy Ascension Cave (Top)']),
|
||||
create_lw_region(world, player, 'Death Mountain (Top)', ['Ether Tablet'],
|
||||
create_lw_region(multiworld, player, 'Death Mountain (Top)', ['Ether Tablet'],
|
||||
['East Death Mountain (Top)', 'Tower of Hera', 'Death Mountain Drop']),
|
||||
create_lw_region(world, player, 'Spectacle Rock', ['Spectacle Rock'], ['Spectacle Rock Drop']),
|
||||
create_dungeon_region(world, player, 'Tower of Hera (Bottom)', 'Tower of Hera',
|
||||
create_lw_region(multiworld, player, 'Spectacle Rock', ['Spectacle Rock'], ['Spectacle Rock Drop']),
|
||||
create_dungeon_region(multiworld, player, 'Tower of Hera (Bottom)', 'Tower of Hera',
|
||||
['Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest'],
|
||||
['Tower of Hera Small Key Door', 'Tower of Hera Big Key Door', 'Tower of Hera Exit']),
|
||||
create_dungeon_region(world, player, 'Tower of Hera (Basement)', 'Tower of Hera',
|
||||
create_dungeon_region(multiworld, player, 'Tower of Hera (Basement)', 'Tower of Hera',
|
||||
['Tower of Hera - Big Key Chest']),
|
||||
create_dungeon_region(world, player, 'Tower of Hera (Top)', 'Tower of Hera',
|
||||
create_dungeon_region(multiworld, player, 'Tower of Hera (Top)', 'Tower of Hera',
|
||||
['Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss',
|
||||
'Tower of Hera - Prize']),
|
||||
|
||||
create_dw_region(world, player, 'East Dark World', ['Pyramid'],
|
||||
create_dw_region(multiworld, player, 'East Dark World', ['Pyramid'],
|
||||
['Pyramid Fairy', 'South Dark World Bridge', 'Palace of Darkness',
|
||||
'Dark Lake Hylia Drop (East)',
|
||||
'Hyrule Castle Ledge Mirror Spot', 'Dark Lake Hylia Fairy', 'Palace of Darkness Hint',
|
||||
'East Dark World Hint', 'Pyramid Hole', 'Northeast Dark World Broken Bridge Pass', ]),
|
||||
create_dw_region(world, player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']),
|
||||
create_dw_region(world, player, 'Northeast Dark World', None,
|
||||
create_dw_region(multiworld, player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']),
|
||||
create_dw_region(multiworld, player, 'Northeast Dark World', None,
|
||||
['West Dark World Gap', 'Dark World Potion Shop', 'East Dark World Broken Bridge Pass',
|
||||
'Catfish Entrance Rock', 'Dark Lake Hylia Teleporter']),
|
||||
create_cave_region(world, player, 'Palace of Darkness Hint', 'a storyteller'),
|
||||
create_cave_region(world, player, 'East Dark World Hint', 'a storyteller'),
|
||||
create_dw_region(world, player, 'South Dark World', ['Stumpy', 'Digging Game'],
|
||||
create_cave_region(multiworld, player, 'Palace of Darkness Hint', 'a storyteller'),
|
||||
create_cave_region(multiworld, player, 'East Dark World Hint', 'a storyteller'),
|
||||
create_dw_region(multiworld, player, 'South Dark World', ['Stumpy', 'Digging Game'],
|
||||
['Dark Lake Hylia Drop (South)', 'Hype Cave', 'Swamp Palace', 'Village of Outcasts Heavy Rock',
|
||||
'Maze Race Mirror Spot',
|
||||
'Cave 45 Mirror Spot', 'East Dark World Bridge', 'Big Bomb Shop', 'Archery Game',
|
||||
'Bonk Fairy (Dark)', 'Dark Lake Hylia Shop',
|
||||
'Bombos Tablet Mirror Spot']),
|
||||
create_lw_region(world, player, 'Bombos Tablet Ledge', ['Bombos Tablet']),
|
||||
create_cave_region(world, player, 'Big Bomb Shop', 'the bomb shop'),
|
||||
create_cave_region(world, player, 'Archery Game', 'a game of skill'),
|
||||
create_dw_region(world, player, 'Dark Lake Hylia', None,
|
||||
create_lw_region(multiworld, player, 'Bombos Tablet Ledge', ['Bombos Tablet']),
|
||||
create_cave_region(multiworld, player, 'Big Bomb Shop', 'the bomb shop'),
|
||||
create_cave_region(multiworld, player, 'Archery Game', 'a game of skill'),
|
||||
create_dw_region(multiworld, player, 'Dark Lake Hylia', None,
|
||||
['Lake Hylia Island Mirror Spot', 'East Dark World Pier', 'Dark Lake Hylia Ledge']),
|
||||
create_dw_region(world, player, 'Dark Lake Hylia Central Island', None,
|
||||
create_dw_region(multiworld, player, 'Dark Lake Hylia Central Island', None,
|
||||
['Ice Palace', 'Lake Hylia Central Island Mirror Spot']),
|
||||
create_dw_region(world, player, 'Dark Lake Hylia Ledge', None,
|
||||
create_dw_region(multiworld, player, 'Dark Lake Hylia Ledge', None,
|
||||
['Dark Lake Hylia Ledge Drop', 'Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Hint',
|
||||
'Dark Lake Hylia Ledge Spike Cave']),
|
||||
create_cave_region(world, player, 'Dark Lake Hylia Ledge Hint', 'a storyteller'),
|
||||
create_cave_region(world, player, 'Dark Lake Hylia Ledge Spike Cave', 'a spiky hint'),
|
||||
create_cave_region(world, player, 'Hype Cave', 'a bounty of five items',
|
||||
create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Hint', 'a storyteller'),
|
||||
create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Spike Cave', 'a spiky hint'),
|
||||
create_cave_region(multiworld, player, 'Hype Cave', 'a bounty of five items',
|
||||
['Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left',
|
||||
'Hype Cave - Bottom', 'Hype Cave - Generous Guy']),
|
||||
create_dw_region(world, player, 'West Dark World', ['Frog'],
|
||||
create_dw_region(multiworld, player, 'West Dark World', ['Frog'],
|
||||
['Village of Outcasts Drop', 'East Dark World River Pier', 'Brewery', 'C-Shaped House',
|
||||
'Chest Game', 'Thieves Town', 'Graveyard Ledge Mirror Spot', 'Kings Grave Mirror Spot',
|
||||
'Bumper Cave Entrance Rock',
|
||||
'Skull Woods Forest', 'Village of Outcasts Pegs', 'Village of Outcasts Eastern Rocks',
|
||||
'Red Shield Shop', 'Dark Sanctuary Hint', 'Fortune Teller (Dark)',
|
||||
'Dark World Lumberjack Shop']),
|
||||
create_dw_region(world, player, 'Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Village of Outcasts Shop']),
|
||||
create_dw_region(world, player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'],
|
||||
create_dw_region(multiworld, player, 'Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Village of Outcasts Shop']),
|
||||
create_dw_region(multiworld, player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'],
|
||||
['Bat Cave Drop Ledge Mirror Spot', 'Dark World Hammer Peg Cave', 'Peg Area Rocks']),
|
||||
create_dw_region(world, player, 'Bumper Cave Entrance', None,
|
||||
create_dw_region(multiworld, player, 'Bumper Cave Entrance', None,
|
||||
['Bumper Cave (Bottom)', 'Bumper Cave Entrance Mirror Spot', 'Bumper Cave Entrance Drop']),
|
||||
create_cave_region(world, player, 'Fortune Teller (Dark)', 'a fortune teller'),
|
||||
create_cave_region(world, player, 'Village of Outcasts Shop', 'a common shop'),
|
||||
create_cave_region(world, player, 'Dark Lake Hylia Shop', 'a common shop'),
|
||||
create_cave_region(world, player, 'Dark World Lumberjack Shop', 'a common shop'),
|
||||
create_cave_region(world, player, 'Dark World Potion Shop', 'a common shop'),
|
||||
create_cave_region(world, player, 'Dark World Hammer Peg Cave', 'a cave with an item', ['Peg Cave']),
|
||||
create_cave_region(world, player, 'Pyramid Fairy', 'a cave with two chests',
|
||||
create_cave_region(multiworld, player, 'Fortune Teller (Dark)', 'a fortune teller'),
|
||||
create_cave_region(multiworld, player, 'Village of Outcasts Shop', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Dark Lake Hylia Shop', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Dark World Lumberjack Shop', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Dark World Potion Shop', 'a common shop'),
|
||||
create_cave_region(multiworld, player, 'Dark World Hammer Peg Cave', 'a cave with an item', ['Peg Cave']),
|
||||
create_cave_region(multiworld, player, 'Pyramid Fairy', 'a cave with two chests',
|
||||
['Pyramid Fairy - Left', 'Pyramid Fairy - Right']),
|
||||
create_cave_region(world, player, 'Brewery', 'a house with a chest', ['Brewery']),
|
||||
create_cave_region(world, player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']),
|
||||
create_cave_region(world, player, 'Chest Game', 'a game of 16 chests', ['Chest Game']),
|
||||
create_cave_region(world, player, 'Red Shield Shop', 'the rare shop'),
|
||||
create_cave_region(world, player, 'Dark Sanctuary Hint', 'a storyteller'),
|
||||
create_cave_region(world, player, 'Bumper Cave', 'a connector', None,
|
||||
create_cave_region(multiworld, player, 'Brewery', 'a house with a chest', ['Brewery']),
|
||||
create_cave_region(multiworld, player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']),
|
||||
create_cave_region(multiworld, player, 'Chest Game', 'a game of 16 chests', ['Chest Game']),
|
||||
create_cave_region(multiworld, player, 'Red Shield Shop', 'the rare shop'),
|
||||
create_cave_region(multiworld, player, 'Dark Sanctuary Hint', 'a storyteller'),
|
||||
create_cave_region(multiworld, player, 'Bumper Cave', 'a connector', None,
|
||||
['Bumper Cave Exit (Bottom)', 'Bumper Cave Exit (Top)']),
|
||||
create_dw_region(world, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'],
|
||||
create_dw_region(multiworld, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'],
|
||||
['Bumper Cave Ledge Drop', 'Bumper Cave (Top)', 'Bumper Cave Ledge Mirror Spot']),
|
||||
create_dw_region(world, player, 'Skull Woods Forest', None,
|
||||
create_dw_region(multiworld, player, 'Skull Woods Forest', None,
|
||||
['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)',
|
||||
'Skull Woods First Section Hole (North)',
|
||||
'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)']),
|
||||
create_dw_region(world, player, 'Skull Woods Forest (West)', None,
|
||||
create_dw_region(multiworld, player, 'Skull Woods Forest (West)', None,
|
||||
['Skull Woods Second Section Hole', 'Skull Woods Second Section Door (West)',
|
||||
'Skull Woods Final Section']),
|
||||
create_dw_region(world, player, 'Dark Desert', None,
|
||||
create_dw_region(multiworld, player, 'Dark Desert', None,
|
||||
['Misery Mire', 'Mire Shed', 'Desert Ledge (Northeast) Mirror Spot',
|
||||
'Desert Ledge Mirror Spot', 'Desert Palace Stairs Mirror Spot',
|
||||
'Desert Palace Entrance (North) Mirror Spot', 'Dark Desert Hint', 'Dark Desert Fairy']),
|
||||
create_cave_region(world, player, 'Mire Shed', 'a cave with two chests',
|
||||
create_cave_region(multiworld, player, 'Mire Shed', 'a cave with two chests',
|
||||
['Mire Shed - Left', 'Mire Shed - Right']),
|
||||
create_cave_region(world, player, 'Dark Desert Hint', 'a storyteller'),
|
||||
create_dw_region(world, player, 'Dark Death Mountain (West Bottom)', None,
|
||||
create_cave_region(multiworld, player, 'Dark Desert Hint', 'a storyteller'),
|
||||
create_dw_region(multiworld, player, 'Dark Death Mountain (West Bottom)', None,
|
||||
['Spike Cave', 'Spectacle Rock Mirror Spot', 'Dark Death Mountain Fairy']),
|
||||
create_dw_region(world, player, 'Dark Death Mountain (Top)', None,
|
||||
create_dw_region(multiworld, player, 'Dark Death Mountain (Top)', None,
|
||||
['Dark Death Mountain Drop (East)', 'Dark Death Mountain Drop (West)', 'Ganons Tower',
|
||||
'Superbunny Cave (Top)',
|
||||
'Hookshot Cave', 'East Death Mountain (Top) Mirror Spot', 'Turtle Rock']),
|
||||
create_dw_region(world, player, 'Dark Death Mountain Ledge', None,
|
||||
create_dw_region(multiworld, player, 'Dark Death Mountain Ledge', None,
|
||||
['Dark Death Mountain Ledge (East)', 'Dark Death Mountain Ledge (West)',
|
||||
'Mimic Cave Mirror Spot', 'Spiral Cave Mirror Spot']),
|
||||
create_dw_region(world, player, 'Dark Death Mountain Isolated Ledge', None,
|
||||
create_dw_region(multiworld, player, 'Dark Death Mountain Isolated Ledge', None,
|
||||
['Isolated Ledge Mirror Spot', 'Turtle Rock Isolated Ledge Entrance']),
|
||||
create_dw_region(world, player, 'Dark Death Mountain (East Bottom)', None,
|
||||
create_dw_region(multiworld, player, 'Dark Death Mountain (East Bottom)', None,
|
||||
['Superbunny Cave (Bottom)', 'Cave Shop (Dark Death Mountain)',
|
||||
'Fairy Ascension Mirror Spot']),
|
||||
create_cave_region(world, player, 'Superbunny Cave (Top)', 'a connector',
|
||||
create_cave_region(multiworld, player, 'Superbunny Cave (Top)', 'a connector',
|
||||
['Superbunny Cave - Top', 'Superbunny Cave - Bottom'], ['Superbunny Cave Exit (Top)']),
|
||||
create_cave_region(world, player, 'Superbunny Cave (Bottom)', 'a connector', None,
|
||||
create_cave_region(multiworld, player, 'Superbunny Cave (Bottom)', 'a connector', None,
|
||||
['Superbunny Cave Climb', 'Superbunny Cave Exit (Bottom)']),
|
||||
create_cave_region(world, player, 'Spike Cave', 'Spike Cave', ['Spike Cave']),
|
||||
create_cave_region(world, player, 'Hookshot Cave', 'a connector',
|
||||
create_cave_region(multiworld, player, 'Spike Cave', 'Spike Cave', ['Spike Cave']),
|
||||
create_cave_region(multiworld, player, 'Hookshot Cave', 'a connector',
|
||||
['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right',
|
||||
'Hookshot Cave - Bottom Left'],
|
||||
['Hookshot Cave Exit (South)', 'Hookshot Cave Bomb Wall (South)']),
|
||||
create_cave_region(world, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)',
|
||||
create_cave_region(multiworld, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)',
|
||||
'Hookshot Cave Bomb Wall (North)']),
|
||||
create_dw_region(world, player, 'Death Mountain Floating Island (Dark World)', None,
|
||||
create_dw_region(multiworld, player, 'Death Mountain Floating Island (Dark World)', None,
|
||||
['Floating Island Drop', 'Hookshot Cave Back Entrance', 'Floating Island Mirror Spot']),
|
||||
create_lw_region(world, player, 'Death Mountain Floating Island (Light World)', ['Floating Island']),
|
||||
create_dw_region(world, player, 'Turtle Rock (Top)', None, ['Turtle Rock Drop']),
|
||||
create_lw_region(world, player, 'Mimic Cave Ledge', None, ['Mimic Cave']),
|
||||
create_cave_region(world, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']),
|
||||
create_lw_region(multiworld, player, 'Death Mountain Floating Island (Light World)', ['Floating Island']),
|
||||
create_dw_region(multiworld, player, 'Turtle Rock (Top)', None, ['Turtle Rock Drop']),
|
||||
create_lw_region(multiworld, player, 'Mimic Cave Ledge', None, ['Mimic Cave']),
|
||||
create_cave_region(multiworld, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']),
|
||||
|
||||
create_dungeon_region(world, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']),
|
||||
create_dungeon_region(world, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']),
|
||||
create_dungeon_region(world, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest', 'Swamp Palace - Pot Row Pot Key',
|
||||
create_dungeon_region(multiworld, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']),
|
||||
create_dungeon_region(multiworld, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest', 'Swamp Palace - Pot Row Pot Key',
|
||||
'Swamp Palace - Trench 1 Pot Key'], ['Swamp Palace (Center)']),
|
||||
create_dungeon_region(world, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key',
|
||||
create_dungeon_region(multiworld, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key',
|
||||
'Swamp Palace - Trench 2 Pot Key'], ['Swamp Palace (North)', 'Swamp Palace (West)']),
|
||||
create_dungeon_region(world, player, 'Swamp Palace (West)', 'Swamp Palace', ['Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest']),
|
||||
create_dungeon_region(world, player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
|
||||
create_dungeon_region(multiworld, player, 'Swamp Palace (West)', 'Swamp Palace', ['Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest']),
|
||||
create_dungeon_region(multiworld, player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
|
||||
'Swamp Palace - Waterway Pot Key', 'Swamp Palace - Waterfall Room',
|
||||
'Swamp Palace - Boss', 'Swamp Palace - Prize']),
|
||||
create_dungeon_region(world, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest',
|
||||
create_dungeon_region(multiworld, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest',
|
||||
'Thieves\' Town - Map Chest',
|
||||
'Thieves\' Town - Compass Chest',
|
||||
'Thieves\' Town - Ambush Chest'], ['Thieves Town Big Key Door', 'Thieves Town Exit']),
|
||||
create_dungeon_region(world, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic',
|
||||
create_dungeon_region(multiworld, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic',
|
||||
'Thieves\' Town - Big Chest',
|
||||
'Thieves\' Town - Hallway Pot Key',
|
||||
'Thieves\' Town - Spike Switch Pot Key',
|
||||
'Thieves\' Town - Blind\'s Cell'], ['Blind Fight']),
|
||||
create_dungeon_region(world, player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']),
|
||||
create_dungeon_region(world, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']),
|
||||
create_dungeon_region(world, player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
|
||||
create_dungeon_region(world, player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']),
|
||||
create_dungeon_region(world, player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
|
||||
create_dungeon_region(world, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']),
|
||||
create_dungeon_region(world, player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest', 'Skull Woods - West Lobby Pot Key'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']),
|
||||
create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
|
||||
create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop', 'Skull Woods - Boss', 'Skull Woods - Prize']),
|
||||
create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Second Section)', 'Ice Palace Exit']),
|
||||
create_dungeon_region(world, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop'], ['Ice Palace (Main)']),
|
||||
create_dungeon_region(world, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest',
|
||||
create_dungeon_region(multiworld, player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest', 'Skull Woods - West Lobby Pot Key'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop', 'Skull Woods - Boss', 'Skull Woods - Prize']),
|
||||
create_dungeon_region(multiworld, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Second Section)', 'Ice Palace Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop'], ['Ice Palace (Main)']),
|
||||
create_dungeon_region(multiworld, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest',
|
||||
'Ice Palace - Many Pots Pot Key',
|
||||
'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'], ['Ice Palace (East)', 'Ice Palace (Kholdstare)']),
|
||||
create_dungeon_region(world, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']),
|
||||
create_dungeon_region(world, player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Hammer Block Key Drop']),
|
||||
create_dungeon_region(world, player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']),
|
||||
create_dungeon_region(world, player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']),
|
||||
create_dungeon_region(world, player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby',
|
||||
create_dungeon_region(multiworld, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']),
|
||||
create_dungeon_region(multiworld, player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Hammer Block Key Drop']),
|
||||
create_dungeon_region(multiworld, player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']),
|
||||
create_dungeon_region(multiworld, player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby',
|
||||
'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest',
|
||||
'Misery Mire - Spikes Pot Key', 'Misery Mire - Fishbone Pot Key',
|
||||
'Misery Mire - Conveyor Crystal Key Drop'], ['Misery Mire (West)', 'Misery Mire Big Key Door']),
|
||||
create_dungeon_region(world, player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']),
|
||||
create_dungeon_region(world, player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']),
|
||||
create_dungeon_region(world, player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left',
|
||||
create_dungeon_region(multiworld, player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']),
|
||||
create_dungeon_region(multiworld, player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']),
|
||||
create_dungeon_region(multiworld, player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left',
|
||||
'Turtle Rock - Roller Room - Right'],
|
||||
['Turtle Rock Entrance to Pokey Room', 'Turtle Rock Entrance Gap Reverse']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'], ['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'], ['Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door', 'Turtle Rock Second Section Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'], ['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'], ['Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door', 'Turtle Rock Second Section Bomb Wall']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']),
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
|
||||
'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'],
|
||||
['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Eye Bridge Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
|
||||
create_dungeon_region(multiworld, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
|
||||
['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)', 'Palace of Darkness Big Key Door']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'],
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'],
|
||||
['Palace of Darkness Spike Statue Room Door', 'Palace of Darkness Maze Door']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Entrance)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left',
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']),
|
||||
create_dungeon_region(multiworld, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Entrance)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left',
|
||||
'Ganons Tower - Hope Room - Right', 'Ganons Tower - Conveyor Cross Pot Key'],
|
||||
['Ganons Tower (Tile Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower Big Key Door', 'Ganons Tower Exit']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'], ['Ganons Tower (Tile Room) Key Door']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower', ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right',
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'], ['Ganons Tower (Tile Room) Key Door']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower', ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right',
|
||||
'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right',
|
||||
'Ganons Tower - Conveyor Star Pits Pot Key'],
|
||||
['Ganons Tower (Bottom) (East)']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower', ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right',
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower', ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right',
|
||||
'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right',
|
||||
'Ganons Tower - Double Switch Pot Key'],
|
||||
['Ganons Tower (Map Room)', 'Ganons Tower (Double Switch Room)']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower', ['Ganons Tower - Firesnake Room'], ['Ganons Tower (Firesnake Room)']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower', ['Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right',
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower', ['Ganons Tower - Firesnake Room'], ['Ganons Tower (Firesnake Room)']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower', ['Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right',
|
||||
'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right'],
|
||||
['Ganons Tower (Bottom) (West)']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest', 'Ganons Tower - Big Key Room - Left',
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest', 'Ganons Tower - Big Key Room - Left',
|
||||
'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower', ['Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right',
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']),
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower', ['Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right',
|
||||
'Ganons Tower - Pre-Moldorm Chest', 'Ganons Tower - Mini Helmasaur Key Drop'], ['Ganons Tower Moldorm Door']),
|
||||
create_dungeon_region(world, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']),
|
||||
create_dungeon_region(world, player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None),
|
||||
create_cave_region(world, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']),
|
||||
create_cave_region(world, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']),
|
||||
create_dw_region(world, player, 'Pyramid Ledge', None, ['Pyramid Entrance', 'Pyramid Drop']),
|
||||
create_lw_region(world, player, 'Desert Northern Cliffs'),
|
||||
create_dw_region(world, player, 'Dark Death Mountain Bunny Descent Area')
|
||||
create_dungeon_region(multiworld, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']),
|
||||
create_dungeon_region(multiworld, player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None),
|
||||
create_cave_region(multiworld, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']),
|
||||
create_cave_region(multiworld, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']),
|
||||
create_dw_region(multiworld, player, 'Pyramid Ledge', None, ['Pyramid Entrance', 'Pyramid Drop']),
|
||||
create_lw_region(multiworld, player, 'Desert Northern Cliffs'),
|
||||
create_dw_region(multiworld, player, 'Dark Death Mountain Bunny Descent Area')
|
||||
]
|
||||
|
||||
|
||||
def create_lw_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
|
||||
return _create_region(world, player, name, LTTPRegionType.LightWorld, 'Light World', locations, exits)
|
||||
def create_lw_region(multiworld: MultiWorld, player: int, name: str, locations=None, exits=None):
|
||||
return _create_region(multiworld, player, name, LTTPRegionType.LightWorld, 'Light World', locations, exits)
|
||||
|
||||
|
||||
def create_dw_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
|
||||
return _create_region(world, player, name, LTTPRegionType.DarkWorld, 'Dark World', locations, exits)
|
||||
def create_dw_region(multiworld: MultiWorld, player: int, name: str, locations=None, exits=None):
|
||||
return _create_region(multiworld, player, name, LTTPRegionType.DarkWorld, 'Dark World', locations, exits)
|
||||
|
||||
|
||||
def create_cave_region(world: MultiWorld, player: int, name: str, hint: str, locations=None, exits=None):
|
||||
return _create_region(world, player, name, LTTPRegionType.Cave, hint, locations, exits)
|
||||
def create_cave_region(multiworld: MultiWorld, player: int, name: str, hint: str, locations=None, exits=None):
|
||||
return _create_region(multiworld, player, name, LTTPRegionType.Cave, hint, locations, exits)
|
||||
|
||||
|
||||
def create_dungeon_region(world: MultiWorld, player: int, name: str, hint: str, locations=None, exits=None):
|
||||
return _create_region(world, player, name, LTTPRegionType.Dungeon, hint, locations, exits)
|
||||
def create_dungeon_region(multiworld: MultiWorld, player: int, name: str, hint: str, locations=None, exits=None):
|
||||
return _create_region(multiworld, player, name, LTTPRegionType.Dungeon, hint, locations, exits)
|
||||
|
||||
|
||||
def _create_region(world: MultiWorld, player: int, name: str, type: LTTPRegionType, hint: str, locations=None,
|
||||
def _create_region(multiworld: MultiWorld, player: int, name: str, type: LTTPRegionType, hint: str, locations=None,
|
||||
exits=None):
|
||||
from .SubClasses import ALttPLocation
|
||||
ret = LTTPRegion(name, type, hint, player, world)
|
||||
ret = LTTPRegion(name, type, hint, player, multiworld)
|
||||
if exits:
|
||||
for exit in exits:
|
||||
ret.create_exit(exit)
|
||||
@@ -422,10 +422,10 @@ def _create_region(world: MultiWorld, player: int, name: str, type: LTTPRegionTy
|
||||
return ret
|
||||
|
||||
|
||||
def mark_light_world_regions(world, player: int):
|
||||
def mark_light_world_regions(multiworld: MultiWorld, player: int):
|
||||
# cross world caves may have some sections marked as both in_light_world, and in_dark_work.
|
||||
# That is ok. the bunny logic will check for this case and incorporate special rules.
|
||||
queue = collections.deque(region for region in world.get_regions(player) if region.type == LTTPRegionType.LightWorld)
|
||||
queue = collections.deque(region for region in multiworld.get_regions(player) if region.type == LTTPRegionType.LightWorld)
|
||||
seen = set(queue)
|
||||
while queue:
|
||||
current = queue.popleft()
|
||||
@@ -438,7 +438,7 @@ def mark_light_world_regions(world, player: int):
|
||||
seen.add(exit.connected_region)
|
||||
queue.append(exit.connected_region)
|
||||
|
||||
queue = collections.deque(region for region in world.get_regions(player) if region.type == LTTPRegionType.DarkWorld)
|
||||
queue = collections.deque(region for region in multiworld.get_regions(player) if region.type == LTTPRegionType.DarkWorld)
|
||||
seen = set(queue)
|
||||
while queue:
|
||||
current = queue.popleft()
|
||||
|
||||
@@ -19,7 +19,7 @@ import subprocess
|
||||
import threading
|
||||
import concurrent.futures
|
||||
import bsdiff4
|
||||
from typing import Collection, Optional, List, SupportsIndex
|
||||
from typing import Collection, Optional, List, SupportsIndex, TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState, Region, Location, MultiWorld
|
||||
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml, read_snes_rom
|
||||
@@ -39,6 +39,9 @@ from .Items import item_table, item_name_groups, progression_items
|
||||
from .EntranceShuffle import door_addresses
|
||||
from .Options import small_key_shuffle
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import ALTTPWorld
|
||||
|
||||
try:
|
||||
from maseya import z3pr
|
||||
from maseya.z3pr.palette_randomizer import build_offset_collections
|
||||
@@ -183,7 +186,7 @@ def check_enemizer(enemizercli):
|
||||
if getattr(check_enemizer, "done", None):
|
||||
return
|
||||
if not os.path.exists(enemizercli) and not os.path.exists(enemizercli + ".exe"):
|
||||
raise Exception(f"Enemizer not found at {enemizercli}, please install it."
|
||||
raise Exception(f"Enemizer not found at {enemizercli}, please install it. "
|
||||
f"Such as https://github.com/Ijwu/Enemizer/releases")
|
||||
|
||||
with check_lock:
|
||||
@@ -792,13 +795,13 @@ def get_nonnative_item_sprite(code: int) -> int:
|
||||
# https://discord.com/channels/731205301247803413/827141303330406408/852102450822905886
|
||||
|
||||
|
||||
def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
local_random = world.worlds[player].random
|
||||
local_world = world.worlds[player]
|
||||
def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
local_random = multiworld.worlds[player].random
|
||||
local_world = multiworld.worlds[player]
|
||||
|
||||
# patch items
|
||||
|
||||
for location in world.get_locations(player):
|
||||
for location in multiworld.get_locations(player):
|
||||
if location.address is None or location.shop_slot is not None:
|
||||
continue
|
||||
|
||||
@@ -852,7 +855,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
rom.write_byte(0x155C9, local_random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle
|
||||
|
||||
# patch entrance/exits/holes
|
||||
for region in world.get_regions(player):
|
||||
for region in multiworld.get_regions(player):
|
||||
for exit in region.exits:
|
||||
if exit.target is not None:
|
||||
if isinstance(exit.addresses, tuple):
|
||||
@@ -885,7 +888,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
rom.write_int16(0x15DB5 + 2 * offset, 0x0640)
|
||||
elif room_id == 0x00d6 and local_world.fix_trock_exit:
|
||||
rom.write_int16(0x15DB5 + 2 * offset, 0x0134)
|
||||
elif room_id == 0x000c and world.shuffle_ganon: # fix ganons tower exit point
|
||||
elif room_id == 0x000c and multiworld.shuffle_ganon: # fix ganons tower exit point
|
||||
rom.write_int16(0x15DB5 + 2 * offset, 0x00A4)
|
||||
else:
|
||||
rom.write_int16(0x15DB5 + 2 * offset, link_y)
|
||||
@@ -905,9 +908,9 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
# patch door table
|
||||
rom.write_byte(0xDBB73 + exit.addresses, exit.target)
|
||||
if local_world.options.mode == 'inverted':
|
||||
patch_shuffled_dark_sanc(world, rom, player)
|
||||
patch_shuffled_dark_sanc(multiworld, rom, player)
|
||||
|
||||
write_custom_shops(rom, world, player)
|
||||
write_custom_shops(rom, multiworld, player)
|
||||
|
||||
def credits_digit(num):
|
||||
# top: $54 is 1, 55 2, etc , so 57=4, 5C=9
|
||||
@@ -981,11 +984,11 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
if local_world.options.mode in ['open', 'inverted']:
|
||||
rom.write_byte(0x180032, 0x01) # open mode
|
||||
if local_world.options.mode == 'inverted':
|
||||
set_inverted_mode(world, player, rom)
|
||||
set_inverted_mode(multiworld, player, rom)
|
||||
elif local_world.options.mode == 'standard':
|
||||
rom.write_byte(0x180032, 0x00) # standard mode
|
||||
|
||||
uncle_location = world.get_location('Link\'s Uncle', player)
|
||||
uncle_location = multiworld.get_location('Link\'s Uncle', player)
|
||||
if uncle_location.item is None or uncle_location.item.name not in ['Master Sword', 'Tempered Sword',
|
||||
'Fighter Sword', 'Golden Sword',
|
||||
'Progressive Sword']:
|
||||
@@ -1280,7 +1283,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
|
||||
# set up goals for treasure hunt
|
||||
rom.write_int16(0x180163, max(0, local_world.treasure_hunt_required -
|
||||
sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece")))
|
||||
sum(1 for item in multiworld.precollected_items[player] if item.name == "Triforce Piece")))
|
||||
rom.write_bytes(0x180165, [0x0E, 0x28]) # Triforce Piece Sprite
|
||||
rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled)
|
||||
|
||||
@@ -1309,7 +1312,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
rom.write_bytes(0x50563, [0x3F, 0x14]) # disable below ganon chest
|
||||
rom.write_byte(0x50599, 0x00) # disable below ganon chest
|
||||
rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest
|
||||
rom.write_byte(0x18008B, 0x01 if local_world.options.open_pyramid.to_bool(world, player) else 0x00) # pre-open Pyramid Hole
|
||||
rom.write_byte(0x18008B, 0x01 if local_world.options.open_pyramid.to_bool(multiworld, player) else 0x00) # pre-open Pyramid Hole
|
||||
rom.write_byte(0x18008C, 0x01 if local_world.options.crystals_needed_for_gt == 0 else 0x00) # GT pre-opened if crystal requirement is 0
|
||||
rom.write_byte(0xF5D73, 0xF0) # bees are catchable
|
||||
rom.write_byte(0xF5F10, 0xF0) # bees are catchable
|
||||
@@ -1327,7 +1330,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
starting_max_bombs = 0 if local_world.options.bombless_start else 10
|
||||
starting_max_arrows = 30
|
||||
|
||||
startingstate = CollectionState(world)
|
||||
startingstate = CollectionState(multiworld)
|
||||
|
||||
if startingstate.has('Silver Bow', player):
|
||||
equip[0x340] = 1
|
||||
@@ -1375,7 +1378,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
equip[0x37B] = 1
|
||||
equip[0x36E] = 0x80
|
||||
|
||||
for item in world.precollected_items[player]:
|
||||
for item in multiworld.precollected_items[player]:
|
||||
|
||||
if item.name in {'Bow', 'Silver Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)',
|
||||
'Titans Mitts', 'Power Glove', 'Progressive Glove',
|
||||
@@ -1590,7 +1593,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
}
|
||||
|
||||
def get_reveal_bytes(itemName):
|
||||
locations = world.find_item_locations(itemName, player)
|
||||
locations = multiworld.find_item_locations(itemName, player)
|
||||
if len(locations) < 1:
|
||||
return 0x0000
|
||||
location = locations[0]
|
||||
@@ -1667,7 +1670,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
rom.write_byte(0x18004C, 0x01)
|
||||
|
||||
# set correct flag for hera basement item
|
||||
hera_basement = world.get_location('Tower of Hera - Basement Cage', player)
|
||||
hera_basement = multiworld.get_location('Tower of Hera - Basement Cage', player)
|
||||
if hera_basement.item is not None and hera_basement.item.name == 'Small Key (Tower of Hera)' and hera_basement.item.player == player:
|
||||
rom.write_byte(0x4E3BB, 0xE4)
|
||||
else:
|
||||
@@ -1684,27 +1687,26 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
rom.write_byte(0xFEE41, 0x2A) # bombable exit
|
||||
|
||||
if local_world.options.tile_shuffle:
|
||||
tile_set = TileSet.get_random_tile_set(world.worlds[player].random)
|
||||
tile_set = TileSet.get_random_tile_set(multiworld.worlds[player].random)
|
||||
rom.write_byte(0x4BA21, tile_set.get_speed())
|
||||
rom.write_byte(0x4BA1D, tile_set.get_len())
|
||||
rom.write_bytes(0x4BA2A, tile_set.get_bytes())
|
||||
|
||||
write_strings(rom, world, player)
|
||||
write_strings(rom, multiworld, player)
|
||||
|
||||
# remote items flag, does not currently work
|
||||
rom.write_byte(0x18637C, 0)
|
||||
|
||||
# set rom name
|
||||
# 21 bytes
|
||||
from Utils import __version__
|
||||
rom.name = bytearray(f'AP{__version__.replace(".", "")[0:3]}_{player}_{world.seed:11}\0', 'utf8')[:21]
|
||||
rom.name = bytearray(f'AP{local_world.world_version.as_simple_string().replace(".", "")[0:3]}_{player}_{multiworld.seed:11}\0', 'utf8')[:21]
|
||||
rom.name.extend([0] * (21 - len(rom.name)))
|
||||
rom.write_bytes(0x7FC0, rom.name)
|
||||
|
||||
# set player names
|
||||
encoded_players = world.players + len(world.groups)
|
||||
encoded_players = multiworld.players + len(multiworld.groups)
|
||||
for p in range(1, min(encoded_players, ROM_PLAYER_LIMIT) + 1):
|
||||
rom.write_bytes(0x195FFC + ((p - 1) * 32), hud_format_text(world.player_name[p]))
|
||||
rom.write_bytes(0x195FFC + ((p - 1) * 32), hud_format_text(multiworld.player_name[p]))
|
||||
if encoded_players > ROM_PLAYER_LIMIT:
|
||||
rom.write_bytes(0x195FFC + ((ROM_PLAYER_LIMIT - 1) * 32), hud_format_text("Archipelago"))
|
||||
|
||||
@@ -1723,9 +1725,9 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
return rom
|
||||
|
||||
|
||||
def patch_race_rom(rom, world, player):
|
||||
def patch_race_rom(rom: LocalRom, multiworld: MultiWorld, player: int):
|
||||
rom.write_bytes(0x180213, [0x01, 0x00]) # Tournament Seed
|
||||
rom.encrypt(world, player)
|
||||
rom.encrypt(multiworld, player)
|
||||
|
||||
|
||||
def get_price_data(price: int, price_type: int) -> List[int]:
|
||||
@@ -1738,8 +1740,8 @@ def get_price_data(price: int, price_type: int) -> List[int]:
|
||||
return int16_as_bytes(price)
|
||||
|
||||
|
||||
def write_custom_shops(rom, world, player):
|
||||
shops = sorted([shop for shop in world.worlds[player].shops if shop.custom], key=lambda shop: shop.sram_offset)
|
||||
def write_custom_shops(rom: LocalRom, multiworld: MultiWorld, player: int):
|
||||
shops = sorted([shop for shop in multiworld.worlds[player].shops if shop.custom], key=lambda shop: shop.sram_offset)
|
||||
|
||||
shop_data = bytearray()
|
||||
items_data = bytearray()
|
||||
@@ -1758,9 +1760,9 @@ def write_custom_shops(rom, world, player):
|
||||
slot = 0 if shop.type == ShopType.TakeAny else index
|
||||
if item is None:
|
||||
break
|
||||
if world.worlds[player].options.shop_item_slots or shop.type == ShopType.TakeAny:
|
||||
count_shop = (shop.region.name != 'Potion Shop' or world.worlds[player].options.include_witch_hut) and \
|
||||
(shop.region.name != 'Capacity Upgrade' or world.worlds[player].options.shuffle_capacity_upgrades)
|
||||
if multiworld.worlds[player].options.shop_item_slots or shop.type == ShopType.TakeAny:
|
||||
count_shop = (shop.region.name != 'Potion Shop' or multiworld.worlds[player].options.include_witch_hut) and \
|
||||
(shop.region.name != 'Capacity Upgrade' or multiworld.worlds[player].options.shuffle_capacity_upgrades)
|
||||
rom.write_byte(0x186560 + shop.sram_offset + slot, 1 if count_shop else 0)
|
||||
if item['item'] == 'Single Arrow' and item['player'] == 0:
|
||||
arrow_mask |= 1 << index
|
||||
@@ -1773,11 +1775,11 @@ def write_custom_shops(rom, world, player):
|
||||
price_data = get_price_data(item['price'], item["price_type"])
|
||||
replacement_price_data = get_price_data(item['replacement_price'], item['replacement_price_type'])
|
||||
slot = 0 if shop.type == ShopType.TakeAny else index
|
||||
if item['player'] and world.game[item['player']] != "A Link to the Past": # item not native to ALTTP
|
||||
item_code = get_nonnative_item_sprite(world.worlds[item['player']].item_name_to_id[item['item']])
|
||||
if item['player'] and multiworld.game[item['player']] != "A Link to the Past": # item not native to ALTTP
|
||||
item_code = get_nonnative_item_sprite(multiworld.worlds[item['player']].item_name_to_id[item['item']])
|
||||
else:
|
||||
item_code = item_table[item["item"]].item_code
|
||||
if item['item'] == 'Single Arrow' and item['player'] == 0 and world.worlds[player].options.retro_bow:
|
||||
if item['item'] == 'Single Arrow' and item['player'] == 0 and multiworld.worlds[player].options.retro_bow:
|
||||
rom.write_byte(0x186500 + shop.sram_offset + slot, arrow_mask)
|
||||
|
||||
item_data = [shop_id, item_code] + price_data + \
|
||||
@@ -1790,12 +1792,12 @@ def write_custom_shops(rom, world, player):
|
||||
items_data.extend([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
|
||||
rom.write_bytes(0x184900, items_data)
|
||||
|
||||
if world.worlds[player].options.retro_bow:
|
||||
if multiworld.worlds[player].options.retro_bow:
|
||||
retro_shop_slots.append(0xFF)
|
||||
rom.write_bytes(0x186540, retro_shop_slots)
|
||||
|
||||
|
||||
def hud_format_text(text):
|
||||
def hud_format_text(text: str):
|
||||
output = bytes()
|
||||
for char in text.lower():
|
||||
if 'a' <= char <= 'z':
|
||||
@@ -1812,7 +1814,7 @@ def hud_format_text(text):
|
||||
output += b'\x7f\x00'
|
||||
return output[:32]
|
||||
|
||||
def apply_oof_sfx(rom, oof: str):
|
||||
def apply_oof_sfx(rom: LocalRom, oof: str):
|
||||
with open(oof, 'rb') as stream:
|
||||
oof_bytes = bytearray(stream.read())
|
||||
|
||||
@@ -1862,9 +1864,10 @@ def apply_oof_sfx(rom, oof: str):
|
||||
rom.write_bytes(0x13000D, [0x00, 0x00, 0x00, 0x08])
|
||||
|
||||
|
||||
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):
|
||||
def apply_rom_settings(rom: LocalRom, beep: str, color: str, quickswap: bool, menuspeed: str, music: bool, sprite: str,
|
||||
oof: str, palettes_options: dict[str, str], world: "ALTTPWorld | None" = None, player: int = 1,
|
||||
allow_random_on_event: bool = False, reduceflashing: bool = False, triforcehud: str = None,
|
||||
deathlink: bool = False, allowcollect: bool = False):
|
||||
local_random = random if not world else world.worlds[player].random
|
||||
disable_music: bool = not music
|
||||
# enable instant item menu
|
||||
@@ -1948,7 +1951,7 @@ def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, spri
|
||||
rom.write_byte(0x180167, triforce_flag)
|
||||
|
||||
if z3pr:
|
||||
def buildAndRandomize(option_name, mode):
|
||||
def buildAndRandomize(option_name: str, mode: str):
|
||||
options = {
|
||||
option_name: True
|
||||
}
|
||||
@@ -2012,7 +2015,7 @@ def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, spri
|
||||
rom.write_crc()
|
||||
|
||||
|
||||
def restore_maseya_colors(rom, offsets_array):
|
||||
def restore_maseya_colors(rom: LocalRom, offsets_array: list[list[int]]):
|
||||
if not rom.orig_buffer:
|
||||
return
|
||||
for offsetC in offsets_array:
|
||||
@@ -2020,7 +2023,7 @@ def restore_maseya_colors(rom, offsets_array):
|
||||
rom.write_bytes(address, rom.orig_buffer[address:address + 2])
|
||||
|
||||
|
||||
def set_color(rom, address, color, shade):
|
||||
def set_color(rom: LocalRom, address: int, color: tuple[int, int, int], shade: int):
|
||||
r = round(min(color[0], 0xFF) * pow(0.8, shade) * 0x1F / 0xFF)
|
||||
g = round(min(color[1], 0xFF) * pow(0.8, shade) * 0x1F / 0xFF)
|
||||
b = round(min(color[2], 0xFF) * pow(0.8, shade) * 0x1F / 0xFF)
|
||||
@@ -2028,7 +2031,7 @@ def set_color(rom, address, color, shade):
|
||||
rom.write_bytes(address, ((b << 10) | (g << 5) | (r << 0)).to_bytes(2, byteorder='little', signed=False))
|
||||
|
||||
|
||||
def default_ow_palettes(rom):
|
||||
def default_ow_palettes(rom: LocalRom):
|
||||
if not rom.orig_buffer:
|
||||
return
|
||||
rom.write_bytes(0xDE604, rom.orig_buffer[0xDE604:0xDEBB4])
|
||||
@@ -2037,7 +2040,7 @@ def default_ow_palettes(rom):
|
||||
rom.write_bytes(address, rom.orig_buffer[address:address + 2])
|
||||
|
||||
|
||||
def randomize_ow_palettes(rom, local_random):
|
||||
def randomize_ow_palettes(rom: LocalRom, local_random: random.Random):
|
||||
grass, grass2, grass3, dirt, dirt2, water, clouds, dwdirt, \
|
||||
dwgrass, dwwater, dwdmdirt, dwdmgrass, dwdmclouds1, dwdmclouds2 = [[local_random.randint(60, 215) for _ in range(3)]
|
||||
for _ in range(14)]
|
||||
@@ -2113,7 +2116,7 @@ def randomize_ow_palettes(rom, local_random):
|
||||
set_color(rom, address, color, shade)
|
||||
|
||||
|
||||
def blackout_ow_palettes(rom):
|
||||
def blackout_ow_palettes(rom: LocalRom):
|
||||
rom.write_bytes(0xDE604, [0] * 0xC4)
|
||||
for i in range(0xDE6C8, 0xDE86C, 70):
|
||||
rom.write_bytes(i, [0] * 64)
|
||||
@@ -2124,13 +2127,13 @@ def blackout_ow_palettes(rom):
|
||||
rom.write_bytes(address, [0, 0])
|
||||
|
||||
|
||||
def default_uw_palettes(rom):
|
||||
def default_uw_palettes(rom: LocalRom):
|
||||
if not rom.orig_buffer:
|
||||
return
|
||||
rom.write_bytes(0xDD734, rom.orig_buffer[0xDD734:0xDE544])
|
||||
|
||||
|
||||
def randomize_uw_palettes(rom, local_random):
|
||||
def randomize_uw_palettes(rom: LocalRom, local_random: random.Random):
|
||||
for dungeon in range(20):
|
||||
wall, pot, chest, floor1, floor2, floor3 = [[local_random.randint(60, 240) for _ in range(3)] for _ in range(6)]
|
||||
|
||||
@@ -2177,7 +2180,7 @@ def randomize_uw_palettes(rom, local_random):
|
||||
set_color(rom, 0x0DD796 + (0xB4 * dungeon), floor3, 4)
|
||||
|
||||
|
||||
def blackout_uw_palettes(rom):
|
||||
def blackout_uw_palettes(rom: LocalRom):
|
||||
for i in range(0xDD734, 0xDE544, 180):
|
||||
rom.write_bytes(i, [0] * 38)
|
||||
rom.write_bytes(i + 44, [0] * 76)
|
||||
@@ -2188,25 +2191,25 @@ def get_hash_string(hash):
|
||||
return ", ".join([hash_alphabet[code & 0x1F] for code in hash])
|
||||
|
||||
|
||||
def write_string_to_rom(rom, target, string):
|
||||
def write_string_to_rom(rom: LocalRom, target: str, string: str):
|
||||
address, maxbytes = text_addresses[target]
|
||||
rom.write_bytes(address, MultiByteTextMapper.convert(string, maxbytes))
|
||||
|
||||
|
||||
def write_strings(rom, world, player):
|
||||
def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int):
|
||||
from . import ALTTPWorld
|
||||
local_random = world.worlds[player].random
|
||||
w: ALTTPWorld = world.worlds[player]
|
||||
local_random = multiworld.worlds[player].random
|
||||
w: ALTTPWorld = multiworld.worlds[player]
|
||||
|
||||
tt = TextTable()
|
||||
tt.removeUnwantedText()
|
||||
|
||||
# Let's keep this guy's text accurate to the shuffle setting.
|
||||
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
|
||||
if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
|
||||
tt['kakariko_flophouse_man_no_flippers'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.'
|
||||
tt['kakariko_flophouse_man'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.'
|
||||
|
||||
if world.worlds[player].options.mode == 'inverted':
|
||||
if multiworld.worlds[player].options.mode == 'inverted':
|
||||
tt['sign_village_of_outcasts'] = 'attention\nferal ducks sighted\nhiding in statues\n\nflute players beware\n'
|
||||
|
||||
def hint_text(dest, ped_hint=False):
|
||||
@@ -2218,45 +2221,45 @@ def write_strings(rom, world, player):
|
||||
hint = dest.hint_text
|
||||
if dest.player != player:
|
||||
if ped_hint:
|
||||
hint += f" for {world.player_name[dest.player]}!"
|
||||
hint += f" for {multiworld.player_name[dest.player]}!"
|
||||
elif isinstance(dest, (Region, Location)):
|
||||
hint += f" in {world.player_name[dest.player]}'s world"
|
||||
hint += f" in {multiworld.player_name[dest.player]}'s world"
|
||||
else:
|
||||
hint += f" for {world.player_name[dest.player]}"
|
||||
hint += f" for {multiworld.player_name[dest.player]}"
|
||||
return hint
|
||||
|
||||
if world.worlds[player].options.scams.gives_king_zora_hint:
|
||||
if multiworld.worlds[player].options.scams.gives_king_zora_hint:
|
||||
# Zora hint
|
||||
zora_location = world.get_location("King Zora", player)
|
||||
zora_location = multiworld.get_location("King Zora", player)
|
||||
tt['zora_tells_cost'] = f"You got 500 rupees to buy {hint_text(zora_location.item)}" \
|
||||
f"\n ≥ Duh\n Oh carp\n{{CHOICE}}"
|
||||
if world.worlds[player].options.scams.gives_bottle_merchant_hint:
|
||||
if multiworld.worlds[player].options.scams.gives_bottle_merchant_hint:
|
||||
# Bottle Vendor hint
|
||||
vendor_location = world.get_location("Bottle Merchant", player)
|
||||
vendor_location = multiworld.get_location("Bottle Merchant", player)
|
||||
tt['bottle_vendor_choice'] = f"I gots {hint_text(vendor_location.item)}\nYous gots 100 rupees?" \
|
||||
f"\n ≥ I want\n no way!\n{{CHOICE}}"
|
||||
|
||||
# First we write hints about entrances, some from the inconvenient list others from all reasonable entrances.
|
||||
if world.worlds[player].options.hints:
|
||||
if world.worlds[player].options.hints.value >= 2:
|
||||
if world.worlds[player].options.hints == "full":
|
||||
if multiworld.worlds[player].options.hints:
|
||||
if multiworld.worlds[player].options.hints.value >= 2:
|
||||
if multiworld.worlds[player].options.hints == "full":
|
||||
tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles have hints!'
|
||||
else:
|
||||
tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles can have hints!'
|
||||
hint_locations = HintLocations.copy()
|
||||
local_random.shuffle(hint_locations)
|
||||
all_entrances = list(world.get_entrances(player))
|
||||
all_entrances = list(multiworld.get_entrances(player))
|
||||
local_random.shuffle(all_entrances)
|
||||
|
||||
# First we take care of the one inconvenient dungeon in the appropriately simple shuffles.
|
||||
entrances_to_hint = {}
|
||||
entrances_to_hint.update(InconvenientDungeonEntrances)
|
||||
if world.shuffle_ganon:
|
||||
if world.worlds[player].options.mode == 'inverted':
|
||||
if multiworld.shuffle_ganon:
|
||||
if multiworld.worlds[player].options.mode == 'inverted':
|
||||
entrances_to_hint.update({'Inverted Ganons Tower': 'The sealed castle door'})
|
||||
else:
|
||||
entrances_to_hint.update({'Ganons Tower': 'Ganon\'s Tower'})
|
||||
if world.worlds[player].options.entrance_shuffle in ['simple', 'restricted']:
|
||||
if multiworld.worlds[player].options.entrance_shuffle in ['simple', 'restricted']:
|
||||
for entrance in all_entrances:
|
||||
if entrance.name in entrances_to_hint:
|
||||
this_hint = entrances_to_hint[entrance.name] + ' leads to ' + hint_text(
|
||||
@@ -2266,9 +2269,9 @@ def write_strings(rom, world, player):
|
||||
break
|
||||
# Now we write inconvenient locations for most shuffles and finish taking care of the less chaotic ones.
|
||||
entrances_to_hint.update(InconvenientOtherEntrances)
|
||||
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
hint_count = 0
|
||||
elif world.worlds[player].options.entrance_shuffle in ['simple', 'restricted']:
|
||||
elif multiworld.worlds[player].options.entrance_shuffle in ['simple', 'restricted']:
|
||||
hint_count = 2
|
||||
else:
|
||||
hint_count = 4
|
||||
@@ -2285,31 +2288,31 @@ def write_strings(rom, world, player):
|
||||
|
||||
# Next we handle hints for randomly selected other entrances,
|
||||
# curating the selection intelligently based on shuffle.
|
||||
if world.worlds[player].options.entrance_shuffle not in ['simple', 'restricted']:
|
||||
if multiworld.worlds[player].options.entrance_shuffle not in ['simple', 'restricted']:
|
||||
entrances_to_hint.update(ConnectorEntrances)
|
||||
entrances_to_hint.update(DungeonEntrances)
|
||||
if world.worlds[player].options.mode == 'inverted':
|
||||
if multiworld.worlds[player].options.mode == 'inverted':
|
||||
entrances_to_hint.update({'Inverted Agahnims Tower': 'The dark mountain tower'})
|
||||
else:
|
||||
entrances_to_hint.update({'Agahnims Tower': 'The sealed castle door'})
|
||||
elif world.worlds[player].options.entrance_shuffle == 'restricted':
|
||||
elif multiworld.worlds[player].options.entrance_shuffle == 'restricted':
|
||||
entrances_to_hint.update(ConnectorEntrances)
|
||||
entrances_to_hint.update(OtherEntrances)
|
||||
if world.worlds[player].options.mode == 'inverted':
|
||||
if multiworld.worlds[player].options.mode == 'inverted':
|
||||
entrances_to_hint.update({'Inverted Dark Sanctuary': 'The dark sanctuary cave'})
|
||||
entrances_to_hint.update({'Inverted Big Bomb Shop': 'The old hero\'s dark home'})
|
||||
entrances_to_hint.update({'Inverted Links House': 'The old hero\'s light home'})
|
||||
else:
|
||||
entrances_to_hint.update({'Dark Sanctuary Hint': 'The dark sanctuary cave'})
|
||||
entrances_to_hint.update({'Big Bomb Shop': 'The old bomb shop'})
|
||||
if world.worlds[player].options.entrance_shuffle != 'insanity':
|
||||
if multiworld.worlds[player].options.entrance_shuffle != 'insanity':
|
||||
entrances_to_hint.update(InsanityEntrances)
|
||||
if world.shuffle_ganon:
|
||||
if world.worlds[player].options.mode == 'inverted':
|
||||
if multiworld.shuffle_ganon:
|
||||
if multiworld.worlds[player].options.mode == 'inverted':
|
||||
entrances_to_hint.update({'Inverted Pyramid Entrance': 'The extra castle passage'})
|
||||
else:
|
||||
entrances_to_hint.update({'Pyramid Ledge': 'The pyramid ledge'})
|
||||
hint_count = 4 if world.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
|
||||
hint_count = 4 if multiworld.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
|
||||
'dungeons_crossed'] else 0
|
||||
for entrance in all_entrances:
|
||||
if entrance.name in entrances_to_hint:
|
||||
@@ -2324,77 +2327,77 @@ def write_strings(rom, world, player):
|
||||
|
||||
# Next we write a few hints for specific inconvenient locations. We don't make many because in entrance this is highly unpredictable.
|
||||
locations_to_hint = InconvenientLocations.copy()
|
||||
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
locations_to_hint.extend(InconvenientVanillaLocations)
|
||||
local_random.shuffle(locations_to_hint)
|
||||
hint_count = 3 if world.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
|
||||
hint_count = 3 if multiworld.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
|
||||
'dungeons_crossed'] else 5
|
||||
for location in locations_to_hint[:hint_count]:
|
||||
if location == 'Swamp Left':
|
||||
if local_random.randint(0, 1):
|
||||
first_item = hint_text(world.get_location('Swamp Palace - West Chest', player).item)
|
||||
second_item = hint_text(world.get_location('Swamp Palace - Big Key Chest', player).item)
|
||||
first_item = hint_text(multiworld.get_location('Swamp Palace - West Chest', player).item)
|
||||
second_item = hint_text(multiworld.get_location('Swamp Palace - Big Key Chest', player).item)
|
||||
else:
|
||||
second_item = hint_text(world.get_location('Swamp Palace - West Chest', player).item)
|
||||
first_item = hint_text(world.get_location('Swamp Palace - Big Key Chest', player).item)
|
||||
second_item = hint_text(multiworld.get_location('Swamp Palace - West Chest', player).item)
|
||||
first_item = hint_text(multiworld.get_location('Swamp Palace - Big Key Chest', player).item)
|
||||
this_hint = ('The westmost chests in Swamp Palace contain ' + first_item + ' and ' + second_item + '.')
|
||||
tt[hint_locations.pop(0)] = this_hint
|
||||
elif location == 'Mire Left':
|
||||
if local_random.randint(0, 1):
|
||||
first_item = hint_text(world.get_location('Misery Mire - Compass Chest', player).item)
|
||||
second_item = hint_text(world.get_location('Misery Mire - Big Key Chest', player).item)
|
||||
first_item = hint_text(multiworld.get_location('Misery Mire - Compass Chest', player).item)
|
||||
second_item = hint_text(multiworld.get_location('Misery Mire - Big Key Chest', player).item)
|
||||
else:
|
||||
second_item = hint_text(world.get_location('Misery Mire - Compass Chest', player).item)
|
||||
first_item = hint_text(world.get_location('Misery Mire - Big Key Chest', player).item)
|
||||
second_item = hint_text(multiworld.get_location('Misery Mire - Compass Chest', player).item)
|
||||
first_item = hint_text(multiworld.get_location('Misery Mire - Big Key Chest', player).item)
|
||||
this_hint = ('The westmost chests in Misery Mire contain ' + first_item + ' and ' + second_item + '.')
|
||||
tt[hint_locations.pop(0)] = this_hint
|
||||
elif location == 'Tower of Hera - Big Key Chest':
|
||||
this_hint = 'Waiting in the Tower of Hera basement leads to ' + hint_text(
|
||||
world.get_location(location, player).item) + '.'
|
||||
multiworld.get_location(location, player).item) + '.'
|
||||
tt[hint_locations.pop(0)] = this_hint
|
||||
elif location == 'Ganons Tower - Big Chest':
|
||||
this_hint = 'The big chest in Ganon\'s Tower contains ' + hint_text(
|
||||
world.get_location(location, player).item) + '.'
|
||||
multiworld.get_location(location, player).item) + '.'
|
||||
tt[hint_locations.pop(0)] = this_hint
|
||||
elif location == 'Thieves\' Town - Big Chest':
|
||||
this_hint = 'The big chest in Thieves\' Town contains ' + hint_text(
|
||||
world.get_location(location, player).item) + '.'
|
||||
multiworld.get_location(location, player).item) + '.'
|
||||
tt[hint_locations.pop(0)] = this_hint
|
||||
elif location == 'Ice Palace - Big Chest':
|
||||
this_hint = 'The big chest in Ice Palace contains ' + hint_text(
|
||||
world.get_location(location, player).item) + '.'
|
||||
multiworld.get_location(location, player).item) + '.'
|
||||
tt[hint_locations.pop(0)] = this_hint
|
||||
elif location == 'Eastern Palace - Big Key Chest':
|
||||
this_hint = 'The antifairy guarded chest in Eastern Palace contains ' + hint_text(
|
||||
world.get_location(location, player).item) + '.'
|
||||
multiworld.get_location(location, player).item) + '.'
|
||||
tt[hint_locations.pop(0)] = this_hint
|
||||
elif location == 'Sahasrahla':
|
||||
this_hint = 'Sahasrahla seeks a green pendant for ' + hint_text(
|
||||
world.get_location(location, player).item) + '.'
|
||||
multiworld.get_location(location, player).item) + '.'
|
||||
tt[hint_locations.pop(0)] = this_hint
|
||||
elif location == 'Graveyard Cave':
|
||||
this_hint = 'The cave north of the graveyard contains ' + hint_text(
|
||||
world.get_location(location, player).item) + '.'
|
||||
multiworld.get_location(location, player).item) + '.'
|
||||
tt[hint_locations.pop(0)] = this_hint
|
||||
else:
|
||||
this_hint = location + ' contains ' + hint_text(world.get_location(location, player).item) + '.'
|
||||
this_hint = location + ' contains ' + hint_text(multiworld.get_location(location, player).item) + '.'
|
||||
tt[hint_locations.pop(0)] = this_hint
|
||||
|
||||
# Lastly we write hints to show where certain interesting items are.
|
||||
items_to_hint = RelevantItems.copy()
|
||||
if world.worlds[player].options.small_key_shuffle.hints_useful:
|
||||
if multiworld.worlds[player].options.small_key_shuffle.hints_useful:
|
||||
items_to_hint |= item_name_groups["Small Keys"]
|
||||
if world.worlds[player].options.big_key_shuffle.hints_useful:
|
||||
if multiworld.worlds[player].options.big_key_shuffle.hints_useful:
|
||||
items_to_hint |= item_name_groups["Big Keys"]
|
||||
|
||||
if world.worlds[player].options.hints == "full":
|
||||
if multiworld.worlds[player].options.hints == "full":
|
||||
hint_count = len(hint_locations) # fill all remaining hint locations with Item hints.
|
||||
else:
|
||||
hint_count = 5 if world.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
|
||||
hint_count = 5 if multiworld.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
|
||||
'dungeons_crossed'] else 8
|
||||
hint_count = min(hint_count, len(items_to_hint), len(hint_locations))
|
||||
if hint_count:
|
||||
locations = world.find_items_in_locations(items_to_hint, player, True)
|
||||
locations = multiworld.find_items_in_locations(items_to_hint, player, True)
|
||||
local_random.shuffle(locations)
|
||||
# make locked locations less likely to appear as hint,
|
||||
# chances are the lock means the player already knows.
|
||||
@@ -2414,15 +2417,15 @@ def write_strings(rom, world, player):
|
||||
|
||||
# We still need the older hints of course. Those are done here.
|
||||
|
||||
silverarrows = world.find_item_locations('Silver Bow', player, True)
|
||||
silverarrows = multiworld.find_item_locations('Silver Bow', player, True)
|
||||
local_random.shuffle(silverarrows)
|
||||
silverarrow_hint = (
|
||||
' %s?' % hint_text(silverarrows[0]).replace('Ganon\'s', 'my')) if silverarrows else '?\nI think not!'
|
||||
tt['ganon_phase_3_no_silvers'] = 'Did you find the silver arrows%s' % silverarrow_hint
|
||||
tt['ganon_phase_3_no_silvers_alt'] = 'Did you find the silver arrows%s' % silverarrow_hint
|
||||
if world.worlds[player].has_progressive_bows and (w.difficulty_requirements.progressive_bow_limit >= 2 or (
|
||||
world.worlds[player].options.swordless or world.worlds[player].options.glitches_required == 'no_glitches')):
|
||||
prog_bow_locs = world.find_item_locations('Progressive Bow', player, True)
|
||||
if multiworld.worlds[player].has_progressive_bows and (w.difficulty_requirements.progressive_bow_limit >= 2 or (
|
||||
multiworld.worlds[player].options.swordless or multiworld.worlds[player].options.glitches_required == 'no_glitches')):
|
||||
prog_bow_locs = multiworld.find_item_locations('Progressive Bow', player, True)
|
||||
local_random.shuffle(prog_bow_locs)
|
||||
found_bow = False
|
||||
found_bow_alt = False
|
||||
@@ -2437,34 +2440,34 @@ def write_strings(rom, world, player):
|
||||
silverarrow_hint = (' %s?' % hint_text(bow_loc).replace('Ganon\'s', 'my'))
|
||||
tt[target] = 'Did you find the silver arrows%s' % silverarrow_hint
|
||||
|
||||
crystal5 = world.find_item('Crystal 5', player)
|
||||
crystal6 = world.find_item('Crystal 6', player)
|
||||
crystal5 = multiworld.find_item('Crystal 5', player)
|
||||
crystal6 = multiworld.find_item('Crystal 6', player)
|
||||
tt['bomb_shop'] = 'Big Bomb?\nMy supply is blocked until you clear %s and %s.' % (
|
||||
crystal5.hint_text, crystal6.hint_text)
|
||||
|
||||
greenpendant = world.find_item('Green Pendant', player)
|
||||
greenpendant = multiworld.find_item('Green Pendant', player)
|
||||
tt['sahasrahla_bring_courage'] = 'I lost my family heirloom in %s' % greenpendant.hint_text
|
||||
|
||||
if world.worlds[player].options.crystals_needed_for_gt == 1:
|
||||
if multiworld.worlds[player].options.crystals_needed_for_gt == 1:
|
||||
tt['sign_ganons_tower'] = 'You need a crystal to enter.'
|
||||
else:
|
||||
tt['sign_ganons_tower'] = f'You need {world.worlds[player].options.crystals_needed_for_gt} crystals to enter.'
|
||||
tt['sign_ganons_tower'] = f'You need {multiworld.worlds[player].options.crystals_needed_for_gt} crystals to enter.'
|
||||
|
||||
if world.worlds[player].options.goal == 'bosses':
|
||||
if multiworld.worlds[player].options.goal == 'bosses':
|
||||
tt['sign_ganon'] = 'You need to kill all bosses, Ganon last.'
|
||||
elif world.worlds[player].options.goal == 'ganon_pedestal':
|
||||
elif multiworld.worlds[player].options.goal == 'ganon_pedestal':
|
||||
tt['sign_ganon'] = 'You need to pull the pedestal to defeat Ganon.'
|
||||
elif world.worlds[player].options.goal == "ganon":
|
||||
if world.worlds[player].options.crystals_needed_for_ganon == 1:
|
||||
elif multiworld.worlds[player].options.goal == "ganon":
|
||||
if multiworld.worlds[player].options.crystals_needed_for_ganon == 1:
|
||||
tt['sign_ganon'] = 'You need a crystal to beat Ganon and have beaten Agahnim atop Ganons Tower.'
|
||||
else:
|
||||
tt['sign_ganon'] = f'You need {world.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon and ' \
|
||||
tt['sign_ganon'] = f'You need {multiworld.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon and ' \
|
||||
f'have beaten Agahnim atop Ganons Tower'
|
||||
else:
|
||||
if world.worlds[player].options.crystals_needed_for_ganon == 1:
|
||||
if multiworld.worlds[player].options.crystals_needed_for_ganon == 1:
|
||||
tt['sign_ganon'] = 'You need a crystal to beat Ganon.'
|
||||
else:
|
||||
tt['sign_ganon'] = f'You need {world.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon.'
|
||||
tt['sign_ganon'] = f'You need {multiworld.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon.'
|
||||
|
||||
tt['uncle_leaving_text'] = Uncle_texts[local_random.randint(0, len(Uncle_texts) - 1)]
|
||||
tt['end_triforce'] = "{NOBORDER}\n" + Triforce_texts[local_random.randint(0, len(Triforce_texts) - 1)]
|
||||
@@ -2475,12 +2478,12 @@ def write_strings(rom, world, player):
|
||||
tt['blind_by_the_light'] = Blind_texts[local_random.randint(0, len(Blind_texts) - 1)]
|
||||
|
||||
triforce_pieces_required = max(0, w.treasure_hunt_required -
|
||||
sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece"))
|
||||
sum(1 for item in multiworld.precollected_items[player] if item.name == "Triforce Piece"))
|
||||
|
||||
if world.worlds[player].options.goal in ['triforce_hunt', 'local_triforce_hunt']:
|
||||
if multiworld.worlds[player].options.goal in ['triforce_hunt', 'local_triforce_hunt']:
|
||||
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Get the Triforce Pieces.'
|
||||
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
|
||||
if world.worlds[player].options.goal == 'triforce_hunt' and world.players > 1:
|
||||
if multiworld.worlds[player].options.goal == 'triforce_hunt' and multiworld.players > 1:
|
||||
tt['sign_ganon'] = 'Go find the Triforce pieces with your friends... Ganon is invincible!'
|
||||
else:
|
||||
tt['sign_ganon'] = 'Go find the Triforce pieces... Ganon is invincible!'
|
||||
@@ -2494,7 +2497,7 @@ def write_strings(rom, world, player):
|
||||
"invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \
|
||||
"hidden in a hollow tree. If you bring\n%d Triforce piece out of %d, I can reassemble it." % \
|
||||
(triforce_pieces_required, w.treasure_hunt_total)
|
||||
elif world.worlds[player].options.goal in ['pedestal']:
|
||||
elif multiworld.worlds[player].options.goal in ['pedestal']:
|
||||
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Your goal is at the pedestal.'
|
||||
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
|
||||
tt['sign_ganon'] = 'You need to get to the pedestal... Ganon is invincible!'
|
||||
@@ -2503,44 +2506,44 @@ def write_strings(rom, world, player):
|
||||
tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!'
|
||||
tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!'
|
||||
if triforce_pieces_required > 1:
|
||||
if world.worlds[player].options.goal == 'ganon_triforce_hunt' and world.players > 1:
|
||||
if multiworld.worlds[player].options.goal == 'ganon_triforce_hunt' and multiworld.players > 1:
|
||||
tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d with your friends to defeat Ganon.' % \
|
||||
(triforce_pieces_required, w.treasure_hunt_total)
|
||||
elif world.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
|
||||
elif multiworld.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
|
||||
tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d to defeat Ganon.' % \
|
||||
(triforce_pieces_required, w.treasure_hunt_total)
|
||||
else:
|
||||
if world.worlds[player].options.goal == 'ganon_triforce_hunt' and world.players > 1:
|
||||
if multiworld.worlds[player].options.goal == 'ganon_triforce_hunt' and multiworld.players > 1:
|
||||
tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d with your friends to defeat Ganon.' % \
|
||||
(triforce_pieces_required, w.treasure_hunt_total)
|
||||
elif world.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
|
||||
elif multiworld.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
|
||||
tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d to defeat Ganon.' % \
|
||||
(triforce_pieces_required, w.treasure_hunt_total)
|
||||
|
||||
tt['kakariko_tavern_fisherman'] = TavernMan_texts[local_random.randint(0, len(TavernMan_texts) - 1)]
|
||||
|
||||
pedestalitem = world.get_location('Master Sword Pedestal', player).item
|
||||
pedestalitem = multiworld.get_location('Master Sword Pedestal', player).item
|
||||
pedestal_text = 'Some Hot Air' if pedestalitem is None else hint_text(pedestalitem,
|
||||
True) if pedestalitem.pedestal_hint_text is not None else 'Unknown Item'
|
||||
tt['mastersword_pedestal_translated'] = pedestal_text
|
||||
pedestal_credit_text = 'and the Hot Air' if pedestalitem is None else \
|
||||
w.pedestal_credit_texts.get(pedestalitem.code, 'and the Unknown Item')
|
||||
|
||||
etheritem = world.get_location('Ether Tablet', player).item
|
||||
etheritem = multiworld.get_location('Ether Tablet', player).item
|
||||
ether_text = 'Some Hot Air' if etheritem is None else hint_text(etheritem,
|
||||
True) if etheritem.pedestal_hint_text is not None else 'Unknown Item'
|
||||
tt['tablet_ether_book'] = ether_text
|
||||
bombositem = world.get_location('Bombos Tablet', player).item
|
||||
bombositem = multiworld.get_location('Bombos Tablet', player).item
|
||||
bombos_text = 'Some Hot Air' if bombositem is None else hint_text(bombositem,
|
||||
True) if bombositem.pedestal_hint_text is not None else 'Unknown Item'
|
||||
tt['tablet_bombos_book'] = bombos_text
|
||||
|
||||
# inverted spawn menu changes
|
||||
if world.worlds[player].options.mode == 'inverted':
|
||||
if multiworld.worlds[player].options.mode == 'inverted':
|
||||
tt['menu_start_2'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n{CHOICE3}"
|
||||
tt['menu_start_3'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n Mountain Cave\n{CHOICE2}"
|
||||
|
||||
for at, text, _ in world.worlds[player].options.plando_texts:
|
||||
for at, text, _ in multiworld.worlds[player].options.plando_texts:
|
||||
|
||||
if at not in tt:
|
||||
raise Exception(f"No text target \"{at}\" found.")
|
||||
@@ -2551,22 +2554,22 @@ def write_strings(rom, world, player):
|
||||
|
||||
credits = Credits()
|
||||
|
||||
sickkiditem = world.get_location('Sick Kid', player).item
|
||||
sickkiditem = multiworld.get_location('Sick Kid', player).item
|
||||
sickkiditem_text = local_random.choice(SickKid_texts) \
|
||||
if sickkiditem is None or sickkiditem.code not in w.sickkid_credit_texts \
|
||||
else w.sickkid_credit_texts[sickkiditem.code]
|
||||
|
||||
zoraitem = world.get_location('King Zora', player).item
|
||||
zoraitem = multiworld.get_location('King Zora', player).item
|
||||
zoraitem_text = local_random.choice(Zora_texts) \
|
||||
if zoraitem is None or zoraitem.code not in w.zora_credit_texts \
|
||||
else w.zora_credit_texts[zoraitem.code]
|
||||
|
||||
magicshopitem = world.get_location('Potion Shop', player).item
|
||||
magicshopitem = multiworld.get_location('Potion Shop', player).item
|
||||
magicshopitem_text = local_random.choice(MagicShop_texts) \
|
||||
if magicshopitem is None or magicshopitem.code not in w.magicshop_credit_texts \
|
||||
else w.magicshop_credit_texts[magicshopitem.code]
|
||||
|
||||
fluteboyitem = world.get_location('Flute Spot', player).item
|
||||
fluteboyitem = multiworld.get_location('Flute Spot', player).item
|
||||
fluteboyitem_text = local_random.choice(FluteBoy_texts) \
|
||||
if fluteboyitem is None or fluteboyitem.code not in w.fluteboy_credit_texts \
|
||||
else w.fluteboy_credit_texts[fluteboyitem.code]
|
||||
@@ -2595,7 +2598,7 @@ def write_strings(rom, world, player):
|
||||
rom.write_bytes(0x76CC0, [byte for p in pointers for byte in [p & 0xFF, p >> 8 & 0xFF]])
|
||||
|
||||
|
||||
def set_inverted_mode(world, player, rom):
|
||||
def set_inverted_mode(multiworld: MultiWorld, player: int, rom: LocalRom):
|
||||
rom.write_byte(snes_to_pc(0x0283E0), 0xF0) # residual portals
|
||||
rom.write_byte(snes_to_pc(0x02B34D), 0xF0)
|
||||
rom.write_byte(snes_to_pc(0x06DB78), 0x8B)
|
||||
@@ -2613,12 +2616,12 @@ def set_inverted_mode(world, player, rom):
|
||||
rom.write_byte(snes_to_pc(0x08D40C), 0xD0) # morph proof
|
||||
# the following bytes should only be written in vanilla
|
||||
# or they'll overwrite the randomizer's shuffles
|
||||
if world.worlds[player].options.entrance_shuffle == 'vanilla':
|
||||
if multiworld.worlds[player].options.entrance_shuffle == 'vanilla':
|
||||
rom.write_byte(0xDBB73 + 0x23, 0x37) # switch AT and GT
|
||||
rom.write_byte(0xDBB73 + 0x36, 0x24)
|
||||
rom.write_int16(0x15AEE + 2 * 0x38, 0x00E0)
|
||||
rom.write_int16(0x15AEE + 2 * 0x25, 0x000C)
|
||||
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
rom.write_byte(0x15B8C, 0x6C)
|
||||
rom.write_byte(0xDBB73 + 0x00, 0x53) # switch bomb shop and links house
|
||||
rom.write_byte(0xDBB73 + 0x52, 0x01)
|
||||
@@ -2676,7 +2679,7 @@ def set_inverted_mode(world, player, rom):
|
||||
rom.write_int16(snes_to_pc(0x02D9A6), 0x005A)
|
||||
rom.write_byte(snes_to_pc(0x02D9B3), 0x12)
|
||||
# keep the old man spawn point at old man house unless shuffle is vanilla
|
||||
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
|
||||
if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
|
||||
rom.write_bytes(snes_to_pc(0x308350), [0x00, 0x00, 0x01])
|
||||
rom.write_int16(snes_to_pc(0x02D8DE), 0x00F1)
|
||||
rom.write_bytes(snes_to_pc(0x02D910), [0x1F, 0x1E, 0x1F, 0x1F, 0x03, 0x02, 0x03, 0x03])
|
||||
@@ -2739,7 +2742,7 @@ def set_inverted_mode(world, player, rom):
|
||||
rom.write_int16s(snes_to_pc(0x1bb836), [0x001B, 0x001B, 0x001B])
|
||||
rom.write_int16(snes_to_pc(0x308300), 0x0140) # new pyramid hole entrance
|
||||
rom.write_int16(snes_to_pc(0x308320), 0x001B)
|
||||
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
rom.write_byte(snes_to_pc(0x308340), 0x7B)
|
||||
rom.write_int16(snes_to_pc(0x1af504), 0x148B)
|
||||
rom.write_int16(snes_to_pc(0x1af50c), 0x149B)
|
||||
@@ -2776,10 +2779,10 @@ def set_inverted_mode(world, player, rom):
|
||||
rom.write_bytes(snes_to_pc(0x1BC85A), [0x50, 0x0F, 0x82])
|
||||
rom.write_int16(0xDB96F + 2 * 0x35, 0x001B) # move pyramid exit door
|
||||
rom.write_int16(0xDBA71 + 2 * 0x35, 0x06A4)
|
||||
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
rom.write_byte(0xDBB73 + 0x35, 0x36)
|
||||
rom.write_byte(snes_to_pc(0x09D436), 0xF3) # remove castle gate warp
|
||||
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
rom.write_int16(0x15AEE + 2 * 0x37, 0x0010) # pyramid exit to new hc area
|
||||
rom.write_byte(0x15B8C + 0x37, 0x1B)
|
||||
rom.write_int16(0x15BDB + 2 * 0x37, 0x0418)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ from Utils import int16_as_bytes
|
||||
|
||||
from worlds.generic.Rules import add_rule
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from BaseClasses import CollectionState, Item, MultiWorld
|
||||
from .SubClasses import ALttPLocation
|
||||
|
||||
from .Items import item_name_groups
|
||||
@@ -159,7 +159,7 @@ shop_class_mapping = {ShopType.UpgradeShop: UpgradeShop,
|
||||
ShopType.TakeAny: TakeAny}
|
||||
|
||||
|
||||
def push_shop_inventories(multiworld):
|
||||
def push_shop_inventories(multiworld: MultiWorld):
|
||||
all_shops = []
|
||||
for world in multiworld.get_game_worlds(ALttPLocation.game):
|
||||
all_shops.extend(world.shops)
|
||||
@@ -183,7 +183,7 @@ def push_shop_inventories(multiworld):
|
||||
world.pushed_shop_inventories.set()
|
||||
|
||||
|
||||
def create_shops(multiworld, player: int):
|
||||
def create_shops(multiworld: MultiWorld, player: int):
|
||||
from .Options import RandomizeShopInventories
|
||||
player_shop_table = shop_table.copy()
|
||||
if multiworld.worlds[player].options.include_witch_hut:
|
||||
@@ -306,7 +306,7 @@ shop_generation_types = {
|
||||
}
|
||||
|
||||
|
||||
def set_up_shops(multiworld, player: int):
|
||||
def set_up_shops(multiworld: MultiWorld, player: int):
|
||||
from .Options import small_key_shuffle
|
||||
# TODO: move hard+ mode changes for shields here, utilizing the new shops
|
||||
|
||||
@@ -408,7 +408,7 @@ price_rate_display = {
|
||||
}
|
||||
|
||||
|
||||
def get_price_modifier(item) -> float:
|
||||
def get_price_modifier(item: Item) -> float:
|
||||
if item.game == "A Link to the Past":
|
||||
if any(x in item.name for x in
|
||||
['Compass', 'Map', 'Single Bomb', 'Single Arrow', 'Piece of Heart']):
|
||||
@@ -428,7 +428,7 @@ def get_price_modifier(item) -> float:
|
||||
return 0.25
|
||||
|
||||
|
||||
def get_price(multiworld, item, player: int, price_type=None):
|
||||
def get_price(multiworld: MultiWorld, item: Item, player: int, price_type=None):
|
||||
"""Converts a raw Rupee price into a special price type"""
|
||||
from .Options import small_key_shuffle
|
||||
if price_type:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from BaseClasses import MultiWorld, CollectionState
|
||||
from worlds.generic.Rules import set_rule, add_rule
|
||||
from .StateHelpers import can_bomb_clip, has_sword, has_beam_sword, has_fire_source, can_melt_things, has_misery_mire_medallion
|
||||
from .SubClasses import LTTPEntrance
|
||||
@@ -5,27 +6,27 @@ from .SubClasses import LTTPEntrance
|
||||
|
||||
# We actually need the logic to properly "mark" these regions as Light or Dark world.
|
||||
# Therefore we need to make these connections during the normal link_entrances stage, rather than during set_rules.
|
||||
def underworld_glitch_connections(world, player):
|
||||
specrock = world.get_region('Spectacle Rock Cave (Bottom)', player)
|
||||
mire = world.get_region('Misery Mire (West)', player)
|
||||
def underworld_glitch_connections(multiworld: MultiWorld, player: int):
|
||||
specrock = multiworld.get_region('Spectacle Rock Cave (Bottom)', player)
|
||||
mire = multiworld.get_region('Misery Mire (West)', player)
|
||||
|
||||
kikiskip = specrock.create_exit('Kiki Skip')
|
||||
mire_to_hera = mire.create_exit('Mire to Hera Clip')
|
||||
mire_to_swamp = mire.create_exit('Hera to Swamp Clip')
|
||||
|
||||
if world.worlds[player].fix_fake_world:
|
||||
kikiskip.connect(world.get_entrance('Palace of Darkness Exit', player).connected_region)
|
||||
mire_to_hera.connect(world.get_entrance('Tower of Hera Exit', player).connected_region)
|
||||
mire_to_swamp.connect(world.get_entrance('Swamp Palace Exit', player).connected_region)
|
||||
if multiworld.worlds[player].fix_fake_world:
|
||||
kikiskip.connect(multiworld.get_entrance('Palace of Darkness Exit', player).connected_region)
|
||||
mire_to_hera.connect(multiworld.get_entrance('Tower of Hera Exit', player).connected_region)
|
||||
mire_to_swamp.connect(multiworld.get_entrance('Swamp Palace Exit', player).connected_region)
|
||||
else:
|
||||
kikiskip.connect(world.get_region('Palace of Darkness (Entrance)', player))
|
||||
mire_to_hera.connect(world.get_region('Tower of Hera (Bottom)', player))
|
||||
mire_to_swamp.connect(world.get_region('Swamp Palace (Entrance)', player))
|
||||
kikiskip.connect(multiworld.get_region('Palace of Darkness (Entrance)', player))
|
||||
mire_to_hera.connect(multiworld.get_region('Tower of Hera (Bottom)', player))
|
||||
mire_to_swamp.connect(multiworld.get_region('Swamp Palace (Entrance)', player))
|
||||
|
||||
|
||||
# For some entrances, we need to fake having pearl, because we're in fake DW/LW.
|
||||
# This creates a copy of the input state that has Moon Pearl.
|
||||
def fake_pearl_state(state, player):
|
||||
def fake_pearl_state(state: CollectionState, player: int):
|
||||
if state.has('Moon Pearl', player):
|
||||
return state
|
||||
fake_state = state.copy()
|
||||
@@ -35,11 +36,11 @@ def fake_pearl_state(state, player):
|
||||
|
||||
# Sets the rules on where we can actually go using this clip.
|
||||
# Behavior differs based on what type of ER shuffle we're playing.
|
||||
def dungeon_reentry_rules(world, player, clip: LTTPEntrance, dungeon_region: str, dungeon_exit: str):
|
||||
fix_dungeon_exits = world.worlds[player].fix_palaceofdarkness_exit
|
||||
fix_fake_worlds = world.worlds[player].fix_fake_world
|
||||
def dungeon_reentry_rules(multiworld: MultiWorld, player: int, clip: LTTPEntrance, dungeon_region: str, dungeon_exit: str):
|
||||
fix_dungeon_exits = multiworld.worlds[player].fix_palaceofdarkness_exit
|
||||
fix_fake_worlds = multiworld.worlds[player].fix_fake_world
|
||||
|
||||
dungeon_entrance = [r for r in world.get_region(dungeon_region, player).entrances if r.name != clip.name][0]
|
||||
dungeon_entrance = [r for r in multiworld.get_region(dungeon_region, player).entrances if r.name != clip.name][0]
|
||||
if not fix_dungeon_exits: # vanilla, simple, restricted, dungeons_simple; should never have fake worlds fix
|
||||
# Dungeons are only shuffled among themselves. We need to check SW, MM, and AT because they can't be reentered trivially.
|
||||
if dungeon_entrance.name == 'Skull Woods Final Section':
|
||||
@@ -49,64 +50,64 @@ def dungeon_reentry_rules(world, player, clip: LTTPEntrance, dungeon_region: str
|
||||
elif dungeon_entrance.name == 'Agahnims Tower':
|
||||
add_rule(clip, lambda state: state.has('Cape', player) or has_beam_sword(state, player) or state.has('Beat Agahnim 1', player)) # kill/bypass barrier
|
||||
# Then we set a restriction on exiting the dungeon, so you can't leave unless you got in normally.
|
||||
add_rule(world.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state))
|
||||
add_rule(multiworld.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state))
|
||||
elif not fix_fake_worlds: # full, dungeons_full; fixed dungeon exits, but no fake worlds fix
|
||||
# Entry requires the entrance's requirements plus a fake pearl, but you don't gain logical access to the surrounding region.
|
||||
add_rule(clip, lambda state: dungeon_entrance.access_rule(fake_pearl_state(state, player)))
|
||||
# exiting restriction
|
||||
add_rule(world.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state))
|
||||
add_rule(multiworld.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state))
|
||||
# Otherwise, the shuffle type is crossed, dungeons_crossed, or insanity; all of these do not need additional rules on where we can go,
|
||||
# since the clip links directly to the exterior region.
|
||||
|
||||
|
||||
def underworld_glitches_rules(world, player):
|
||||
def underworld_glitches_rules(multiworld: MultiWorld, player: int):
|
||||
# Ice Palace Entrance Clip
|
||||
# This is the easiest one since it's a simple internal clip.
|
||||
# Need to also add melting to freezor chest since it's otherwise assumed.
|
||||
# Also can pick up the first jelly key from behind.
|
||||
add_rule(world.get_entrance('Ice Palace (Main)', player), lambda state: can_bomb_clip(state, world.get_region('Ice Palace (Entrance)', player), player), combine='or')
|
||||
add_rule(world.get_location('Ice Palace - Freezor Chest', player), lambda state: can_melt_things(state, player))
|
||||
add_rule(world.get_location('Ice Palace - Jelly Key Drop', player), lambda state: can_bomb_clip(state, world.get_region('Ice Palace (Entrance)', player), player), combine='or')
|
||||
add_rule(multiworld.get_entrance('Ice Palace (Main)', player), lambda state: can_bomb_clip(state, multiworld.get_region('Ice Palace (Entrance)', player), player), combine='or')
|
||||
add_rule(multiworld.get_location('Ice Palace - Freezor Chest', player), lambda state: can_melt_things(state, player))
|
||||
add_rule(multiworld.get_location('Ice Palace - Jelly Key Drop', player), lambda state: can_bomb_clip(state, multiworld.get_region('Ice Palace (Entrance)', player), player), combine='or')
|
||||
|
||||
|
||||
# Kiki Skip
|
||||
kikiskip = world.get_entrance('Kiki Skip', player)
|
||||
kikiskip = multiworld.get_entrance('Kiki Skip', player)
|
||||
set_rule(kikiskip, lambda state: can_bomb_clip(state, kikiskip.parent_region, player))
|
||||
dungeon_reentry_rules(world, player, kikiskip, 'Palace of Darkness (Entrance)', 'Palace of Darkness Exit')
|
||||
dungeon_reentry_rules(multiworld, player, kikiskip, 'Palace of Darkness (Entrance)', 'Palace of Darkness Exit')
|
||||
|
||||
|
||||
# Mire -> Hera -> Swamp
|
||||
# Using mire keys on other dungeon doors
|
||||
mire = world.get_region('Misery Mire (West)', player)
|
||||
mire = multiworld.get_region('Misery Mire (West)', player)
|
||||
mire_clip = lambda state: state.can_reach('Misery Mire (West)', 'Region', player) and can_bomb_clip(state, mire, player) and has_fire_source(state, player)
|
||||
hera_clip = lambda state: state.can_reach('Tower of Hera (Top)', 'Region', player) and can_bomb_clip(state, world.get_region('Tower of Hera (Top)', player), player)
|
||||
add_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: mire_clip(state) and state.has('Big Key (Misery Mire)', player), combine='or')
|
||||
add_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: mire_clip(state), combine='or')
|
||||
add_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: mire_clip(state) or hera_clip(state), combine='or')
|
||||
hera_clip = lambda state: state.can_reach('Tower of Hera (Top)', 'Region', player) and can_bomb_clip(state, multiworld.get_region('Tower of Hera (Top)', player), player)
|
||||
add_rule(multiworld.get_entrance('Tower of Hera Big Key Door', player), lambda state: mire_clip(state) and state.has('Big Key (Misery Mire)', player), combine='or')
|
||||
add_rule(multiworld.get_entrance('Swamp Palace Small Key Door', player), lambda state: mire_clip(state), combine='or')
|
||||
add_rule(multiworld.get_entrance('Swamp Palace (Center)', player), lambda state: mire_clip(state) or hera_clip(state), combine='or')
|
||||
|
||||
# Build the rule for SP moat.
|
||||
# We need to be able to s+q to old man, then go to either Mire or Hera at either Hera or GT.
|
||||
# First we require a certain type of entrance shuffle, then build the rule from its pieces.
|
||||
if not world.worlds[player].swamp_patch_required:
|
||||
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
if not multiworld.worlds[player].swamp_patch_required:
|
||||
if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||
rule_map = {
|
||||
'Misery Mire (Entrance)': (lambda state: True),
|
||||
'Tower of Hera (Bottom)': (lambda state: state.can_reach('Tower of Hera Big Key Door', 'Entrance', player))
|
||||
}
|
||||
inverted = world.worlds[player].options.mode == 'inverted'
|
||||
inverted = multiworld.worlds[player].options.mode == 'inverted'
|
||||
hera_rule = lambda state: (state.has('Moon Pearl', player) or not inverted) and \
|
||||
rule_map.get(world.get_entrance('Tower of Hera', player).connected_region.name, lambda state: False)(state)
|
||||
rule_map.get(multiworld.get_entrance('Tower of Hera', player).connected_region.name, lambda state: False)(state)
|
||||
gt_rule = lambda state: (state.has('Moon Pearl', player) or inverted) and \
|
||||
rule_map.get(world.get_entrance(('Ganons Tower' if not inverted else 'Inverted Ganons Tower'), player).connected_region.name, lambda state: False)(state)
|
||||
rule_map.get(multiworld.get_entrance(('Ganons Tower' if not inverted else 'Inverted Ganons Tower'), player).connected_region.name, lambda state: False)(state)
|
||||
mirrorless_moat_rule = lambda state: state.can_reach('Old Man S&Q', 'Entrance', player) and mire_clip(state) and (hera_rule(state) or gt_rule(state))
|
||||
add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player) or mirrorless_moat_rule(state))
|
||||
add_rule(multiworld.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player) or mirrorless_moat_rule(state))
|
||||
else:
|
||||
add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player))
|
||||
add_rule(multiworld.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player))
|
||||
|
||||
# Using the entrances for various ER types. Hera -> Swamp never matters because you can only logically traverse with the mire keys
|
||||
mire_to_hera = world.get_entrance('Mire to Hera Clip', player)
|
||||
mire_to_swamp = world.get_entrance('Hera to Swamp Clip', player)
|
||||
mire_to_hera = multiworld.get_entrance('Mire to Hera Clip', player)
|
||||
mire_to_swamp = multiworld.get_entrance('Hera to Swamp Clip', player)
|
||||
set_rule(mire_to_hera, mire_clip)
|
||||
set_rule(mire_to_swamp, lambda state: mire_clip(state) and state.has('Flippers', player))
|
||||
dungeon_reentry_rules(world, player, mire_to_hera, 'Tower of Hera (Bottom)', 'Tower of Hera Exit')
|
||||
dungeon_reentry_rules(world, player, mire_to_swamp, 'Swamp Palace (Entrance)', 'Swamp Palace Exit')
|
||||
dungeon_reentry_rules(multiworld, player, mire_to_hera, 'Tower of Hera (Bottom)', 'Tower of Hera Exit')
|
||||
dungeon_reentry_rules(multiworld, player, mire_to_swamp, 'Swamp Palace (Entrance)', 'Swamp Palace Exit')
|
||||
|
||||
6
worlds/alttp/archipelago.json
Normal file
6
worlds/alttp/archipelago.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"game": "A Link to the Past",
|
||||
"minimum_ap_version": "0.6.6",
|
||||
"world_version": "5.1.0",
|
||||
"authors": ["Berserker"]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"game": "APQuest",
|
||||
"minimum_ap_version": "0.6.4",
|
||||
"world_version": "1.0.0",
|
||||
"world_version": "1.0.1",
|
||||
"authors": ["NewSoupVi"]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from math import sqrt
|
||||
from random import choice, random
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import Any
|
||||
|
||||
from kivy.core.window import Keyboard, Window
|
||||
from kivy.graphics import Color, Triangle
|
||||
|
||||
@@ -2,9 +2,8 @@ import pkgutil
|
||||
from collections.abc import Buffer
|
||||
from enum import Enum
|
||||
from io import BytesIO
|
||||
from typing import Literal, NamedTuple, cast
|
||||
from typing import Literal, NamedTuple, Protocol, cast
|
||||
|
||||
from bokeh.protocol import Protocol
|
||||
from kivy.uix.image import CoreImage
|
||||
|
||||
from CommonClient import logger
|
||||
|
||||
@@ -16,10 +16,6 @@ def make_data_directory(dir_name: str) -> Path:
|
||||
gitignore = specific_data_directory / ".gitignore"
|
||||
|
||||
with open(gitignore, "w") as f:
|
||||
f.write(
|
||||
"""*
|
||||
!.gitignore
|
||||
"""
|
||||
)
|
||||
f.write("*\n")
|
||||
|
||||
return specific_data_directory
|
||||
|
||||
@@ -31,3 +31,21 @@ components.append(
|
||||
supports_uri=True,
|
||||
)
|
||||
)
|
||||
|
||||
# There are two optional parameters that are worth drawing attention to here: "game_name" and "supports_uri".
|
||||
# As you might know, on a room page on WebHost, clicking a slot name opens your locally installed Launcher
|
||||
# and asks you if you want to open a Text Client.
|
||||
# If you have "game_name" set on your Component, your user also gets the option to open that instead.
|
||||
# Furthermore, if you have "supports_uri" set to True, your Component will be passed a uri as an arg.
|
||||
# This uri contains the room url + port, the slot name, and the password.
|
||||
# You can process this uri arg to automatically connect the user to their slot without having to type anything.
|
||||
|
||||
# As you can see above, the APQuest client has both of these parameters set.
|
||||
# This means a user can click on the slot name of an APQuest slot on WebHost,
|
||||
# then click "APQuest Client" instead of "Text Client" in the Launcher popup, and after a few seconds,
|
||||
# they will be connected and playing the game without having to touch their keyboard once.
|
||||
|
||||
# Since a Component is just Python code, this doesn't just work with CommonClient-derived clients.
|
||||
# You could forward this uri arg to your standalone C++/Java/.NET/whatever client as well,
|
||||
# meaning just about every client can support this "Click on slot name -> Everything happens automatically" action.
|
||||
# The author would like to see more clients be aware of this feature and try to support it.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Benötigte Software
|
||||
|
||||
- [Archipelago](github.com/ArchipelagoMW/Archipelago/releases/latest)
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest)
|
||||
- Die [APQuest-apworld](https://github.com/NewSoupVi/Archipelago/releases),
|
||||
falls diese nicht mit deiner Version von Archipelago gebündelt ist.
|
||||
|
||||
@@ -11,16 +11,16 @@
|
||||
Zuerst brauchst du einen Raum, mit dem du dich verbinden kannst.
|
||||
Dafür musst du oder jemand den du kennst ein Spiel generieren.
|
||||
Dieser Schritt wird hier nicht erklärt, aber du kannst den
|
||||
[Archipelago Setup Guide](https://archipelago.gg/tutorial/Archipelago/setup_en#generating-a-game) lesen.
|
||||
[Archipelago Setup Guide](/tutorial/Archipelago/setup_en#generating-a-game) lesen.
|
||||
|
||||
Du musst außerdem [Archipelago](github.com/ArchipelagoMW/Archipelago/releases/latest) installiert haben
|
||||
Du musst außerdem [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest) installiert haben
|
||||
und die [APQuest apworld](https://github.com/NewSoupVi/Archipelago/releases) darin installieren.
|
||||
|
||||
Von hier ist es einfach, dich mit deinem Slot zu verbinden.
|
||||
|
||||
### Webhost-Raum
|
||||
|
||||
Wenn dein Raum auf einem WebHost läuft (z.B. [archipelago.gg](archipelago.gg))
|
||||
Wenn dein Raum auf einem WebHost läuft (z.B. [archipelago.gg](https://archipelago.gg))
|
||||
kannst du einfach auf deinen Namen in der Spielerliste klicken.
|
||||
Dies öffnet den Archipelago Launcher, welcher dich dann fragt,
|
||||
ob du den Text Client oder den APQuest Client öffnen willst.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Required Software
|
||||
|
||||
- [Archipelago](github.com/ArchipelagoMW/Archipelago/releases/latest)
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest)
|
||||
- [The APQuest apworld](https://github.com/NewSoupVi/Archipelago/releases),
|
||||
if not bundled with your version of Archipelago
|
||||
|
||||
@@ -10,16 +10,16 @@
|
||||
|
||||
First, you need a room to connect to. For this, you or someone you know has to generate a game.
|
||||
This will not be explained here,
|
||||
but you can check the [Archipelago Setup Guide](https://archipelago.gg/tutorial/Archipelago/setup_en#generating-a-game).
|
||||
but you can check the [Archipelago Setup Guide](/tutorial/Archipelago/setup_en#generating-a-game).
|
||||
|
||||
You also need to have [Archipelago](github.com/ArchipelagoMW/Archipelago/releases/latest) installed
|
||||
You also need to have [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest) installed
|
||||
and the [The APQuest apworld](https://github.com/NewSoupVi/Archipelago/releases) installed into Archipelago.
|
||||
|
||||
From here, connecting to your APQuest slot is easy. There are two scenarios.
|
||||
|
||||
### Webhost Room
|
||||
|
||||
If your room is hosted on a WebHost (e.g. [archipelago.gg](archipelago.gg)),
|
||||
If your room is hosted on a WebHost (e.g. [archipelago.gg](https://archipelago.gg)),
|
||||
you should be able to simply click on your name in the player list.
|
||||
This will open the Archipelago Launcher
|
||||
and ask you whether you want to connect with the Text Client or the APQuest Client.
|
||||
|
||||
@@ -158,11 +158,11 @@ class Game:
|
||||
if not self.gameboard.ready:
|
||||
return
|
||||
|
||||
if self.active_math_problem is not None:
|
||||
if input_key in DIGIT_INPUTS_TO_DIGITS:
|
||||
self.math_problem_input(DIGIT_INPUTS_TO_DIGITS[input_key])
|
||||
if input_key == Input.BACKSPACE:
|
||||
self.math_problem_delete()
|
||||
if input_key in DIGIT_INPUTS_TO_DIGITS:
|
||||
self.math_problem_input(DIGIT_INPUTS_TO_DIGITS[input_key])
|
||||
return
|
||||
if input_key == Input.BACKSPACE:
|
||||
self.math_problem_delete()
|
||||
return
|
||||
|
||||
if input_key == Input.LEFT:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from collections import Counter
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .events import Event, LocationClearedEvent, VictoryEvent
|
||||
from .gameboard import Gameboard
|
||||
|
||||
@@ -5,7 +5,8 @@ from typing import Any
|
||||
from worlds.AutoWorld import World
|
||||
|
||||
# Imports of your world's files must be relative.
|
||||
from . import items, locations, options, regions, rules, web_world
|
||||
from . import items, locations, regions, rules, web_world
|
||||
from . import options as apquest_options # rename due to a name conflict with World.options
|
||||
|
||||
# APQuest will go through all the parts of the world api one step at a time,
|
||||
# with many examples and comments across multiple files.
|
||||
@@ -36,8 +37,9 @@ class APQuestWorld(World):
|
||||
web = web_world.APQuestWebWorld()
|
||||
|
||||
# This is how we associate the options defined in our options.py with our world.
|
||||
options_dataclass = options.APQuestOptions
|
||||
options: options.APQuestOptions # Common mistake: This has to be a colon (:), not an equals sign (=).
|
||||
# (Note: options.py has been imported as "apquest_options" at the top of this file to avoid a name conflict)
|
||||
options_dataclass = apquest_options.APQuestOptions
|
||||
options: apquest_options.APQuestOptions # Common mistake: This has to be a colon (:), not an equals sign (=).
|
||||
|
||||
# Our world class must have a static location_name_to_id and item_name_to_id defined.
|
||||
# We define these in regions.py and items.py respectively, so we just set them here.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user