mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-10 09:33:46 -07:00
Compare commits
115 Commits
NewSoupVi-
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a69c73eed | ||
|
|
e6250fbcd8 | ||
|
|
d09295e726 | ||
|
|
7621889b8b | ||
|
|
c9f1a21bd2 | ||
|
|
874392756b | ||
|
|
7ff201e32c | ||
|
|
170aedba8f | ||
|
|
09c7f5f909 | ||
|
|
4aab317665 | ||
|
|
e52ce0149a | ||
|
|
5a5162c9d3 | ||
|
|
cf375cbcc4 | ||
|
|
6d6d35d598 | ||
|
|
05b257adf9 | ||
|
|
cabfef669a | ||
|
|
e4a5ed1cc4 | ||
|
|
5021997df0 | ||
|
|
d90cf0db65 | ||
|
|
dad228cd4a | ||
|
|
a652108472 | ||
|
|
5348f693fe | ||
|
|
b8c2e14e8b | ||
|
|
430b71a092 | ||
|
|
a40744e6db | ||
|
|
d802f9652a | ||
|
|
cbdb4d7ce3 | ||
|
|
691ce6a248 | ||
|
|
f9fc6944d3 | ||
|
|
e984583e5e | ||
|
|
7e03a87608 | ||
|
|
456bc481a3 | ||
|
|
b4752cd32d | ||
|
|
ceec51b9e1 | ||
|
|
d3312287a8 | ||
|
|
d65863ffa2 | ||
|
|
b8d7ef24f7 | ||
|
|
b2949dfbe8 | ||
|
|
2aa0653b6d | ||
|
|
d63efa5846 | ||
|
|
765721888a | ||
|
|
73701292b5 | ||
|
|
3ab71daa8d | ||
|
|
6f46397185 | ||
|
|
1a41e1acc8 | ||
|
|
34a3b5f058 | ||
|
|
456b4adaa1 | ||
|
|
fc8462f4e9 | ||
|
|
499dad53b1 | ||
|
|
8a809be67a | ||
|
|
7e0219c214 | ||
|
|
b37bb60891 | ||
|
|
f81335d614 | ||
|
|
8ed466bf24 | ||
|
|
920cffda2d | ||
|
|
b1be597451 | ||
|
|
08dc7e522e | ||
|
|
0f64bd08e1 | ||
|
|
d52827ebd2 | ||
|
|
0e55ddc7cf | ||
|
|
ab5b986716 | ||
|
|
97c313c1c4 | ||
|
|
701a7faa71 | ||
|
|
9a4e84efdc | ||
|
|
906b23088c | ||
|
|
0fb69dce33 | ||
|
|
e99f027b42 | ||
|
|
dddffa1660 | ||
|
|
83367c6946 | ||
|
|
0fcca25870 | ||
|
|
d1a7fd7da1 | ||
|
|
5c5f2ffc94 | ||
|
|
6f617e302d | ||
|
|
35c9061c9c | ||
|
|
e61d521ba8 | ||
|
|
6efa065867 | ||
|
|
56dbba6a31 | ||
|
|
43cb9611fb | ||
|
|
64b654d42e | ||
|
|
74aab81f79 | ||
|
|
f390b33c17 | ||
|
|
31852801c9 | ||
|
|
e35addf5b2 | ||
|
|
3cdcb8c455 | ||
|
|
48c6a6fb4c | ||
|
|
eaa8156061 | ||
|
|
54a7bb5664 | ||
|
|
0e6e359747 | ||
|
|
c4e7b6ca82 | ||
|
|
f253dffc07 | ||
|
|
c010c8c938 | ||
|
|
1e8a8e7482 | ||
|
|
182f7e24e5 | ||
|
|
9277cb39ef | ||
|
|
28a9709516 | ||
|
|
49a5b52774 | ||
|
|
2b1802ccee | ||
|
|
f5218faea7 | ||
|
|
81092247c6 | ||
|
|
ca96e7e294 | ||
|
|
c014c5a54a | ||
|
|
e9c863dffd | ||
|
|
7eda4c47f8 | ||
|
|
474a3181c6 | ||
|
|
4af6927e23 | ||
|
|
06df072095 | ||
|
|
56aabe51b8 | ||
|
|
5e5f24cdd2 | ||
|
|
9fbaa6050f | ||
|
|
0af31c71e0 | ||
|
|
169da1b1e0 | ||
|
|
8e7ea06f39 | ||
|
|
96d48a923a | ||
|
|
dcaa2f7b97 | ||
|
|
50330cf32f |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
worlds/blasphemous/region_data.py linguist-generated=true
|
||||
7
.github/workflows/unittests.yml
vendored
7
.github/workflows/unittests.yml
vendored
@@ -37,12 +37,13 @@ jobs:
|
||||
- {version: '3.9'}
|
||||
- {version: '3.10'}
|
||||
- {version: '3.11'}
|
||||
- {version: '3.12'}
|
||||
include:
|
||||
- python: {version: '3.8'} # win7 compat
|
||||
os: windows-latest
|
||||
- python: {version: '3.11'} # current
|
||||
- python: {version: '3.12'} # current
|
||||
os: windows-latest
|
||||
- python: {version: '3.11'} # current
|
||||
- python: {version: '3.12'} # current
|
||||
os: macos-latest
|
||||
|
||||
steps:
|
||||
@@ -70,7 +71,7 @@ jobs:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
python:
|
||||
- {version: '3.11'} # current
|
||||
- {version: '3.12'} # current
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
189
BaseClasses.py
189
BaseClasses.py
@@ -11,8 +11,10 @@ from argparse import Namespace
|
||||
from collections import Counter, deque
|
||||
from collections.abc import Collection, MutableSequence
|
||||
from enum import IntEnum, IntFlag
|
||||
from typing import Any, Callable, Dict, Iterable, Iterator, List, Mapping, NamedTuple, Optional, Set, Tuple, \
|
||||
TypedDict, Union, Type, ClassVar
|
||||
from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple,
|
||||
Optional, Protocol, Set, Tuple, Union, Type)
|
||||
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
|
||||
import NetUtils
|
||||
import Options
|
||||
@@ -22,16 +24,16 @@ if typing.TYPE_CHECKING:
|
||||
from worlds import AutoWorld
|
||||
|
||||
|
||||
class Group(TypedDict, total=False):
|
||||
class Group(TypedDict):
|
||||
name: str
|
||||
game: str
|
||||
world: "AutoWorld.World"
|
||||
players: Set[int]
|
||||
item_pool: Set[str]
|
||||
replacement_items: Dict[int, Optional[str]]
|
||||
local_items: Set[str]
|
||||
non_local_items: Set[str]
|
||||
link_replacement: bool
|
||||
players: AbstractSet[int]
|
||||
item_pool: NotRequired[Set[str]]
|
||||
replacement_items: NotRequired[Dict[int, Optional[str]]]
|
||||
local_items: NotRequired[Set[str]]
|
||||
non_local_items: NotRequired[Set[str]]
|
||||
link_replacement: NotRequired[bool]
|
||||
|
||||
|
||||
class ThreadBarrierProxy:
|
||||
@@ -48,6 +50,11 @@ class ThreadBarrierProxy:
|
||||
"Please use multiworld.per_slot_randoms[player] or randomize ahead of output.")
|
||||
|
||||
|
||||
class HasNameAndPlayer(Protocol):
|
||||
name: str
|
||||
player: int
|
||||
|
||||
|
||||
class MultiWorld():
|
||||
debug_types = False
|
||||
player_name: Dict[int, str]
|
||||
@@ -156,7 +163,7 @@ class MultiWorld():
|
||||
self.start_inventory_from_pool: Dict[int, Options.StartInventoryPool] = {}
|
||||
|
||||
for player in range(1, players + 1):
|
||||
def set_player_attr(attr, val):
|
||||
def set_player_attr(attr: str, val) -> None:
|
||||
self.__dict__.setdefault(attr, {})[player] = val
|
||||
set_player_attr('plando_items', [])
|
||||
set_player_attr('plando_texts', {})
|
||||
@@ -165,13 +172,13 @@ class MultiWorld():
|
||||
set_player_attr('completion_condition', lambda state: True)
|
||||
self.worlds = {}
|
||||
self.per_slot_randoms = Utils.DeprecateDict("Using per_slot_randoms is now deprecated. Please use the "
|
||||
"world's random object instead (usually self.random)")
|
||||
"world's random object instead (usually self.random)")
|
||||
self.plando_options = PlandoOptions.none
|
||||
|
||||
def get_all_ids(self) -> Tuple[int, ...]:
|
||||
return self.player_ids + tuple(self.groups)
|
||||
|
||||
def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tuple[int, Group]:
|
||||
def add_group(self, name: str, game: str, players: AbstractSet[int] = frozenset()) -> Tuple[int, Group]:
|
||||
"""Create a group with name and return the assigned player ID and group.
|
||||
If a group of this name already exists, the set of players is extended instead of creating a new one."""
|
||||
from worlds import AutoWorld
|
||||
@@ -195,7 +202,7 @@ class MultiWorld():
|
||||
|
||||
return new_id, new_group
|
||||
|
||||
def get_player_groups(self, player) -> Set[int]:
|
||||
def get_player_groups(self, player: int) -> Set[int]:
|
||||
return {group_id for group_id, group in self.groups.items() if player in group["players"]}
|
||||
|
||||
def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optional[str] = None):
|
||||
@@ -258,7 +265,7 @@ class MultiWorld():
|
||||
"link_replacement": replacement_prio.index(item_link["link_replacement"]),
|
||||
}
|
||||
|
||||
for name, item_link in item_links.items():
|
||||
for _name, item_link in item_links.items():
|
||||
current_item_name_groups = AutoWorld.AutoWorldRegister.world_types[item_link["game"]].item_name_groups
|
||||
pool = set()
|
||||
local_items = set()
|
||||
@@ -328,6 +335,13 @@ class MultiWorld():
|
||||
for item_name, item_count in next(iter(common_item_count.values())).items():
|
||||
for _ in range(item_count):
|
||||
new_item = group["world"].create_item(item_name)
|
||||
|
||||
# Make sure this item is picked up even if it is not progression
|
||||
# (Monkeypatching a property is kind of hard, this is one of the only ways)
|
||||
class AdvancementTrue(new_item.__class__):
|
||||
advancement = True
|
||||
new_item.__class__ = AdvancementTrue
|
||||
|
||||
# mangle together all original classification bits
|
||||
new_item.classification |= classifications[item_name]
|
||||
new_itempool.append(new_item)
|
||||
@@ -388,7 +402,7 @@ class MultiWorld():
|
||||
return tuple(world for player, world in self.worlds.items() if
|
||||
player not in self.groups and self.game[player] == game_name)
|
||||
|
||||
def get_name_string_for_object(self, obj) -> str:
|
||||
def get_name_string_for_object(self, obj: HasNameAndPlayer) -> str:
|
||||
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_name(obj.player)})'
|
||||
|
||||
def get_player_name(self, player: int) -> str:
|
||||
@@ -430,7 +444,7 @@ class MultiWorld():
|
||||
subworld = self.worlds[player]
|
||||
for item in subworld.get_pre_fill_items():
|
||||
subworld.collect(ret, item)
|
||||
ret.sweep_for_events()
|
||||
ret.sweep_for_advancements()
|
||||
|
||||
if use_cache:
|
||||
self._all_state = ret
|
||||
@@ -439,7 +453,7 @@ class MultiWorld():
|
||||
def get_items(self) -> List[Item]:
|
||||
return [loc.item for loc in self.get_filled_locations()] + self.itempool
|
||||
|
||||
def find_item_locations(self, item, player: int, resolve_group_locations: bool = False) -> List[Location]:
|
||||
def find_item_locations(self, item: str, player: int, resolve_group_locations: bool = False) -> List[Location]:
|
||||
if resolve_group_locations:
|
||||
player_groups = self.get_player_groups(player)
|
||||
return [location for location in self.get_locations() if
|
||||
@@ -448,7 +462,7 @@ class MultiWorld():
|
||||
return [location for location in self.get_locations() if
|
||||
location.item and location.item.name == item and location.item.player == player]
|
||||
|
||||
def find_item(self, item, player: int) -> Location:
|
||||
def find_item(self, item: str, player: int) -> Location:
|
||||
return next(location for location in self.get_locations() if
|
||||
location.item and location.item.name == item and location.item.player == player)
|
||||
|
||||
@@ -541,9 +555,9 @@ class MultiWorld():
|
||||
return True
|
||||
state = starting_state.copy()
|
||||
else:
|
||||
if self.has_beaten_game(self.state):
|
||||
return True
|
||||
state = CollectionState(self)
|
||||
if self.has_beaten_game(state):
|
||||
return True
|
||||
prog_locations = {location for location in self.get_locations() if location.item
|
||||
and location.item.advancement and location not in state.locations_checked}
|
||||
|
||||
@@ -616,8 +630,7 @@ class MultiWorld():
|
||||
|
||||
def location_relevant(location: Location) -> bool:
|
||||
"""Determine if this location is relevant to sweep."""
|
||||
return location.progress_type != LocationProgressType.EXCLUDED \
|
||||
and (location.player in players["full"] or location.advancement)
|
||||
return location.player in players["full"] or location.advancement
|
||||
|
||||
def all_done() -> bool:
|
||||
"""Check if all access rules are fulfilled"""
|
||||
@@ -662,7 +675,7 @@ class CollectionState():
|
||||
multiworld: MultiWorld
|
||||
reachable_regions: Dict[int, Set[Region]]
|
||||
blocked_connections: Dict[int, Set[Entrance]]
|
||||
events: Set[Location]
|
||||
advancements: Set[Location]
|
||||
path: Dict[Union[Region, Entrance], PathValue]
|
||||
locations_checked: Set[Location]
|
||||
stale: Dict[int, bool]
|
||||
@@ -674,7 +687,7 @@ class CollectionState():
|
||||
self.multiworld = parent
|
||||
self.reachable_regions = {player: set() for player in parent.get_all_ids()}
|
||||
self.blocked_connections = {player: set() for player in parent.get_all_ids()}
|
||||
self.events = set()
|
||||
self.advancements = set()
|
||||
self.path = {}
|
||||
self.locations_checked = set()
|
||||
self.stale = {player: True for player in parent.get_all_ids()}
|
||||
@@ -686,17 +699,25 @@ class CollectionState():
|
||||
|
||||
def update_reachable_regions(self, player: int):
|
||||
self.stale[player] = False
|
||||
world: AutoWorld.World = self.multiworld.worlds[player]
|
||||
reachable_regions = self.reachable_regions[player]
|
||||
blocked_connections = self.blocked_connections[player]
|
||||
queue = deque(self.blocked_connections[player])
|
||||
start = self.multiworld.get_region("Menu", player)
|
||||
start: Region = world.get_region(world.origin_region_name)
|
||||
|
||||
# init on first call - this can't be done on construction since the regions don't exist yet
|
||||
if start not in reachable_regions:
|
||||
reachable_regions.add(start)
|
||||
blocked_connections.update(start.exits)
|
||||
self.blocked_connections[player].update(start.exits)
|
||||
queue.extend(start.exits)
|
||||
|
||||
if world.explicit_indirect_conditions:
|
||||
self._update_reachable_regions_explicit_indirect_conditions(player, queue)
|
||||
else:
|
||||
self._update_reachable_regions_auto_indirect_conditions(player, queue)
|
||||
|
||||
def _update_reachable_regions_explicit_indirect_conditions(self, player: int, queue: deque):
|
||||
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
|
||||
while queue:
|
||||
connection = queue.popleft()
|
||||
@@ -716,6 +737,29 @@ class CollectionState():
|
||||
if new_entrance in blocked_connections and new_entrance not in queue:
|
||||
queue.append(new_entrance)
|
||||
|
||||
def _update_reachable_regions_auto_indirect_conditions(self, player: int, queue: deque):
|
||||
reachable_regions = self.reachable_regions[player]
|
||||
blocked_connections = self.blocked_connections[player]
|
||||
new_connection: bool = True
|
||||
# run BFS on all connections, and keep track of those blocked by missing items
|
||||
while new_connection:
|
||||
new_connection = False
|
||||
while queue:
|
||||
connection = queue.popleft()
|
||||
new_region = connection.connected_region
|
||||
if new_region in reachable_regions:
|
||||
blocked_connections.remove(connection)
|
||||
elif connection.can_reach(self):
|
||||
assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region"
|
||||
reachable_regions.add(new_region)
|
||||
blocked_connections.remove(connection)
|
||||
blocked_connections.update(new_region.exits)
|
||||
queue.extend(new_region.exits)
|
||||
self.path[new_region] = (new_region.name, self.path.get(connection, None))
|
||||
new_connection = True
|
||||
# sweep for indirect connections, mostly Entrance.can_reach(unrelated_Region)
|
||||
queue.extend(blocked_connections)
|
||||
|
||||
def copy(self) -> CollectionState:
|
||||
ret = CollectionState(self.multiworld)
|
||||
ret.prog_items = {player: counter.copy() for player, counter in self.prog_items.items()}
|
||||
@@ -723,7 +767,7 @@ class CollectionState():
|
||||
self.reachable_regions.items()}
|
||||
ret.blocked_connections = {player: entrance_set.copy() for player, entrance_set in
|
||||
self.blocked_connections.items()}
|
||||
ret.events = self.events.copy()
|
||||
ret.advancements = self.advancements.copy()
|
||||
ret.path = self.path.copy()
|
||||
ret.locations_checked = self.locations_checked.copy()
|
||||
for function in self.additional_copy_functions:
|
||||
@@ -756,19 +800,24 @@ class CollectionState():
|
||||
return self.multiworld.get_region(spot, player).can_reach(self)
|
||||
|
||||
def sweep_for_events(self, locations: Optional[Iterable[Location]] = None) -> None:
|
||||
Utils.deprecate("sweep_for_events has been renamed to sweep_for_advancements. The functionality is the same. "
|
||||
"Please switch over to sweep_for_advancements.")
|
||||
return self.sweep_for_advancements(locations)
|
||||
|
||||
def sweep_for_advancements(self, locations: Optional[Iterable[Location]] = None) -> None:
|
||||
if locations is None:
|
||||
locations = self.multiworld.get_filled_locations()
|
||||
reachable_events = True
|
||||
# since the loop has a good chance to run more than once, only filter the events once
|
||||
locations = {location for location in locations if location.advancement and location not in self.events}
|
||||
reachable_advancements = True
|
||||
# since the loop has a good chance to run more than once, only filter the advancements once
|
||||
locations = {location for location in locations if location.advancement and location not in self.advancements}
|
||||
|
||||
while reachable_events:
|
||||
reachable_events = {location for location in locations if location.can_reach(self)}
|
||||
locations -= reachable_events
|
||||
for event in reachable_events:
|
||||
self.events.add(event)
|
||||
assert isinstance(event.item, Item), "tried to collect Event with no Item"
|
||||
self.collect(event.item, True, event)
|
||||
while reachable_advancements:
|
||||
reachable_advancements = {location for location in locations if location.can_reach(self)}
|
||||
locations -= reachable_advancements
|
||||
for advancement in reachable_advancements:
|
||||
self.advancements.add(advancement)
|
||||
assert isinstance(advancement.item, Item), "tried to collect Event with no Item"
|
||||
self.collect(advancement.item, True, advancement)
|
||||
|
||||
# item name related
|
||||
def has(self, item: str, player: int, count: int = 1) -> bool:
|
||||
@@ -802,7 +851,7 @@ class CollectionState():
|
||||
if found >= count:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def has_from_list_unique(self, items: Iterable[str], player: int, count: int) -> bool:
|
||||
"""Returns True if the state contains at least `count` items matching any of the item names from a list.
|
||||
Ignores duplicates of the same item."""
|
||||
@@ -817,7 +866,7 @@ class CollectionState():
|
||||
def count_from_list(self, items: Iterable[str], player: int) -> int:
|
||||
"""Returns the cumulative count of items from a list present in state."""
|
||||
return sum(self.prog_items[player][item_name] for item_name in items)
|
||||
|
||||
|
||||
def count_from_list_unique(self, items: Iterable[str], player: int) -> int:
|
||||
"""Returns the cumulative count of items from a list present in state. Ignores duplicates of the same item."""
|
||||
return sum(self.prog_items[player][item_name] > 0 for item_name in items)
|
||||
@@ -863,20 +912,16 @@ class CollectionState():
|
||||
)
|
||||
|
||||
# Item related
|
||||
def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool:
|
||||
def collect(self, item: Item, prevent_sweep: bool = False, location: Optional[Location] = None) -> bool:
|
||||
if location:
|
||||
self.locations_checked.add(location)
|
||||
|
||||
changed = self.multiworld.worlds[item.player].collect(self, item)
|
||||
|
||||
if not changed and event:
|
||||
self.prog_items[item.player][item.name] += 1
|
||||
changed = True
|
||||
|
||||
self.stale[item.player] = True
|
||||
|
||||
if changed and not event:
|
||||
self.sweep_for_events()
|
||||
if changed and not prevent_sweep:
|
||||
self.sweep_for_advancements()
|
||||
|
||||
return changed
|
||||
|
||||
@@ -900,7 +945,7 @@ class Entrance:
|
||||
addresses = None
|
||||
target = None
|
||||
|
||||
def __init__(self, player: int, name: str = '', parent: Region = None):
|
||||
def __init__(self, player: int, name: str = "", parent: Optional[Region] = None) -> None:
|
||||
self.name = name
|
||||
self.parent_region = parent
|
||||
self.player = player
|
||||
@@ -920,9 +965,6 @@ class Entrance:
|
||||
region.entrances.append(self)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self):
|
||||
multiworld = self.parent_region.multiworld if self.parent_region else None
|
||||
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
|
||||
|
||||
@@ -1048,7 +1090,7 @@ class Region:
|
||||
self.locations.append(location_type(self.player, location, address, self))
|
||||
|
||||
def connect(self, connecting_region: Region, name: Optional[str] = None,
|
||||
rule: Optional[Callable[[CollectionState], bool]] = None) -> entrance_type:
|
||||
rule: Optional[Callable[[CollectionState], bool]] = None) -> Entrance:
|
||||
"""
|
||||
Connects this Region to another Region, placing the provided rule on the connection.
|
||||
|
||||
@@ -1088,9 +1130,6 @@ class Region:
|
||||
rules[connecting_region] if rules and connecting_region in rules else None)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self):
|
||||
return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})'
|
||||
|
||||
|
||||
@@ -1109,9 +1148,9 @@ class Location:
|
||||
locked: bool = False
|
||||
show_in_spoiler: bool = True
|
||||
progress_type: LocationProgressType = LocationProgressType.DEFAULT
|
||||
always_allow = staticmethod(lambda state, item: False)
|
||||
always_allow: Callable[[CollectionState, Item], bool] = staticmethod(lambda state, item: False)
|
||||
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
|
||||
item_rule = staticmethod(lambda item: True)
|
||||
item_rule: Callable[[Item], bool] = staticmethod(lambda item: True)
|
||||
item: Optional[Item] = None
|
||||
|
||||
def __init__(self, player: int, name: str = '', address: Optional[int] = None, parent: Optional[Region] = None):
|
||||
@@ -1120,11 +1159,15 @@ class Location:
|
||||
self.address = address
|
||||
self.parent_region = parent
|
||||
|
||||
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
|
||||
return ((self.always_allow(state, item) and item.name not in state.multiworld.worlds[item.player].options.non_local_items)
|
||||
or ((self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful))
|
||||
and self.item_rule(item)
|
||||
and (not check_access or self.can_reach(state))))
|
||||
def can_fill(self, state: CollectionState, item: Item, check_access: bool = True) -> bool:
|
||||
return ((
|
||||
self.always_allow(state, item)
|
||||
and item.name not in state.multiworld.worlds[item.player].options.non_local_items
|
||||
) or (
|
||||
(self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful))
|
||||
and self.item_rule(item)
|
||||
and (not check_access or self.can_reach(state))
|
||||
))
|
||||
|
||||
def can_reach(self, state: CollectionState) -> bool:
|
||||
# Region.can_reach is just a cache lookup, so placing it first for faster abort on average
|
||||
@@ -1139,9 +1182,6 @@ class Location:
|
||||
self.locked = True
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self):
|
||||
multiworld = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None
|
||||
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
|
||||
|
||||
@@ -1163,7 +1203,7 @@ class Location:
|
||||
@property
|
||||
def native_item(self) -> bool:
|
||||
"""Returns True if the item in this location matches game."""
|
||||
return self.item and self.item.game == self.game
|
||||
return self.item is not None and self.item.game == self.game
|
||||
|
||||
@property
|
||||
def hint_text(self) -> str:
|
||||
@@ -1174,7 +1214,7 @@ class ItemClassification(IntFlag):
|
||||
filler = 0b0000 # aka trash, as in filler items like ammo, currency etc,
|
||||
progression = 0b0001 # Item that is logically relevant
|
||||
useful = 0b0010 # Item that is generally quite useful, but not required for anything logical
|
||||
trap = 0b0100 # detrimental or entirely useless (nothing) item
|
||||
trap = 0b0100 # detrimental item
|
||||
skip_balancing = 0b1000 # should technically never occur on its own
|
||||
# Item that is logically relevant, but progression balancing should not touch.
|
||||
# Typically currency or other counted items.
|
||||
@@ -1246,9 +1286,6 @@ class Item:
|
||||
return hash((self.name, self.player))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.location and self.location.parent_region and self.location.parent_region.multiworld:
|
||||
return self.location.parent_region.multiworld.get_name_string_for_object(self)
|
||||
return f"{self.name} (Player {self.player})"
|
||||
@@ -1326,9 +1363,9 @@ class Spoiler:
|
||||
|
||||
# in the second phase, we cull each sphere such that the game is still beatable,
|
||||
# reducing each range of influence to the bare minimum required inside it
|
||||
restore_later = {}
|
||||
restore_later: Dict[Location, Item] = {}
|
||||
for num, sphere in reversed(tuple(enumerate(collection_spheres))):
|
||||
to_delete = set()
|
||||
to_delete: Set[Location] = set()
|
||||
for location in sphere:
|
||||
# we remove the item at location and check if game is still beatable
|
||||
logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name,
|
||||
@@ -1346,7 +1383,7 @@ class Spoiler:
|
||||
sphere -= to_delete
|
||||
|
||||
# second phase, sphere 0
|
||||
removed_precollected = []
|
||||
removed_precollected: List[Item] = []
|
||||
for item in (i for i in chain.from_iterable(multiworld.precollected_items.values()) if i.advancement):
|
||||
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
|
||||
multiworld.precollected_items[item.player].remove(item)
|
||||
@@ -1427,7 +1464,7 @@ class Spoiler:
|
||||
# Maybe move the big bomb over to the Event system instead?
|
||||
if any(exit_path == 'Pyramid Fairy' for path in self.paths.values()
|
||||
for (_, exit_path) in path):
|
||||
if multiworld.mode[player] != 'inverted':
|
||||
if multiworld.worlds[player].options.mode != 'inverted':
|
||||
self.paths[str(multiworld.get_region('Big Bomb Shop', player))] = \
|
||||
get_path(state, multiworld.get_region('Big Bomb Shop', player))
|
||||
else:
|
||||
@@ -1499,9 +1536,9 @@ class Spoiler:
|
||||
|
||||
if self.paths:
|
||||
outfile.write('\n\nPaths:\n\n')
|
||||
path_listings = []
|
||||
path_listings: List[str] = []
|
||||
for location, path in sorted(self.paths.items()):
|
||||
path_lines = []
|
||||
path_lines: List[str] = []
|
||||
for region, exit in path:
|
||||
if exit is not None:
|
||||
path_lines.append("{} -> {}".format(region, exit))
|
||||
|
||||
@@ -252,7 +252,7 @@ class CommonContext:
|
||||
starting_reconnect_delay: int = 5
|
||||
current_reconnect_delay: int = starting_reconnect_delay
|
||||
command_processor: typing.Type[CommandProcessor] = ClientCommandProcessor
|
||||
ui = None
|
||||
ui: typing.Optional["kvui.GameManager"] = None
|
||||
ui_task: typing.Optional["asyncio.Task[None]"] = None
|
||||
input_task: typing.Optional["asyncio.Task[None]"] = None
|
||||
keep_alive_task: typing.Optional["asyncio.Task[None]"] = None
|
||||
@@ -662,17 +662,19 @@ class CommonContext:
|
||||
logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True})
|
||||
self._messagebox_connection_loss = self.gui_error(msg, exc_info[1])
|
||||
|
||||
def run_gui(self):
|
||||
"""Import kivy UI system and start running it as self.ui_task."""
|
||||
def make_gui(self) -> typing.Type["kvui.GameManager"]:
|
||||
"""To return the Kivy App class needed for run_gui so it can be overridden before being built"""
|
||||
from kvui import GameManager
|
||||
|
||||
class TextManager(GameManager):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago")
|
||||
]
|
||||
base_title = "Archipelago Text Client"
|
||||
|
||||
self.ui = TextManager(self)
|
||||
return TextManager
|
||||
|
||||
def run_gui(self):
|
||||
"""Import kivy UI system from make_gui() and start running it as self.ui_task."""
|
||||
ui_class = self.make_gui()
|
||||
self.ui = ui_class(self)
|
||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||
|
||||
def run_cli(self):
|
||||
@@ -994,7 +996,7 @@ def get_base_parser(description: typing.Optional[str] = None):
|
||||
return parser
|
||||
|
||||
|
||||
def run_as_textclient():
|
||||
def run_as_textclient(*args):
|
||||
class TextContext(CommonContext):
|
||||
# Text Mode to use !hint and such with games that have no text entry
|
||||
tags = CommonContext.tags | {"TextOnly"}
|
||||
@@ -1033,15 +1035,18 @@ def run_as_textclient():
|
||||
parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.")
|
||||
parser.add_argument('--name', default=None, help="Slot Name to connect as.")
|
||||
parser.add_argument("url", nargs="?", help="Archipelago connection url")
|
||||
args = parser.parse_args()
|
||||
args = parser.parse_args(args)
|
||||
|
||||
if args.url:
|
||||
url = urllib.parse.urlparse(args.url)
|
||||
args.connect = url.netloc
|
||||
if url.username:
|
||||
args.name = urllib.parse.unquote(url.username)
|
||||
if url.password:
|
||||
args.password = urllib.parse.unquote(url.password)
|
||||
if url.scheme == "archipelago":
|
||||
args.connect = url.netloc
|
||||
if url.username:
|
||||
args.name = urllib.parse.unquote(url.username)
|
||||
if url.password:
|
||||
args.password = urllib.parse.unquote(url.password)
|
||||
else:
|
||||
parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281")
|
||||
|
||||
colorama.init()
|
||||
|
||||
@@ -1051,4 +1056,4 @@ def run_as_textclient():
|
||||
|
||||
if __name__ == '__main__':
|
||||
logging.getLogger().setLevel(logging.INFO) # force log-level to work around log level resetting to WARNING
|
||||
run_as_textclient()
|
||||
run_as_textclient(*sys.argv[1:]) # default value for parse_args
|
||||
|
||||
33
Fill.py
33
Fill.py
@@ -12,7 +12,12 @@ from worlds.generic.Rules import add_item_rule
|
||||
|
||||
|
||||
class FillError(RuntimeError):
|
||||
pass
|
||||
def __init__(self, *args: typing.Union[str, typing.Any], **kwargs) -> None:
|
||||
if "multiworld" in kwargs and isinstance(args[0], str):
|
||||
placements = (args[0] + f"\nAll Placements:\n" +
|
||||
f"{[(loc, loc.item) for loc in kwargs['multiworld'].get_filled_locations()]}")
|
||||
args = (placements, *args[1:])
|
||||
super().__init__(*args)
|
||||
|
||||
|
||||
def _log_fill_progress(name: str, placed: int, total_items: int) -> None:
|
||||
@@ -24,7 +29,7 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item]
|
||||
new_state = base_state.copy()
|
||||
for item in itempool:
|
||||
new_state.collect(item, True)
|
||||
new_state.sweep_for_events(locations=locations)
|
||||
new_state.sweep_for_advancements(locations=locations)
|
||||
return new_state
|
||||
|
||||
|
||||
@@ -212,7 +217,7 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
f"Unfilled locations:\n"
|
||||
f"{', '.join(str(location) for location in locations)}\n"
|
||||
f"Already placed {len(placements)}:\n"
|
||||
f"{', '.join(str(place) for place in placements)}")
|
||||
f"{', '.join(str(place) for place in placements)}", multiworld=multiworld)
|
||||
|
||||
item_pool.extend(unplaced_items)
|
||||
|
||||
@@ -299,7 +304,7 @@ def remaining_fill(multiworld: MultiWorld,
|
||||
f"Unfilled locations:\n"
|
||||
f"{', '.join(str(location) for location in locations)}\n"
|
||||
f"Already placed {len(placements)}:\n"
|
||||
f"{', '.join(str(place) for place in placements)}")
|
||||
f"{', '.join(str(place) for place in placements)}", multiworld=multiworld)
|
||||
|
||||
itempool.extend(unplaced_items)
|
||||
|
||||
@@ -324,8 +329,8 @@ def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, lo
|
||||
pool.append(location.item)
|
||||
state.remove(location.item)
|
||||
location.item = None
|
||||
if location in state.events:
|
||||
state.events.remove(location)
|
||||
if location in state.advancements:
|
||||
state.advancements.remove(location)
|
||||
locations.append(location)
|
||||
if pool and locations:
|
||||
locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY)
|
||||
@@ -358,7 +363,7 @@ def distribute_early_items(multiworld: MultiWorld,
|
||||
early_priority_locations: typing.List[Location] = []
|
||||
loc_indexes_to_remove: typing.Set[int] = set()
|
||||
base_state = multiworld.state.copy()
|
||||
base_state.sweep_for_events(locations=(loc for loc in multiworld.get_filled_locations() if loc.address is None))
|
||||
base_state.sweep_for_advancements(locations=(loc for loc in multiworld.get_filled_locations() if loc.address is None))
|
||||
for i, loc in enumerate(fill_locations):
|
||||
if loc.can_reach(base_state):
|
||||
if loc.progress_type == LocationProgressType.PRIORITY:
|
||||
@@ -506,7 +511,8 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
||||
if progitempool:
|
||||
raise FillError(
|
||||
f"Not enough locations for progression items. "
|
||||
f"There are {len(progitempool)} more progression items than there are available locations."
|
||||
f"There are {len(progitempool)} more progression items than there are available locations.",
|
||||
multiworld=multiworld,
|
||||
)
|
||||
accessibility_corrections(multiworld, multiworld.state, defaultlocations)
|
||||
|
||||
@@ -523,7 +529,8 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
||||
if excludedlocations:
|
||||
raise FillError(
|
||||
f"Not enough filler items for excluded locations. "
|
||||
f"There are {len(excludedlocations)} more excluded locations than filler or trap items."
|
||||
f"There are {len(excludedlocations)} more excluded locations than filler or trap items.",
|
||||
multiworld=multiworld,
|
||||
)
|
||||
|
||||
restitempool = filleritempool + usefulitempool
|
||||
@@ -551,7 +558,7 @@ def flood_items(multiworld: MultiWorld) -> None:
|
||||
progress_done = False
|
||||
|
||||
# sweep once to pick up preplaced items
|
||||
multiworld.state.sweep_for_events()
|
||||
multiworld.state.sweep_for_advancements()
|
||||
|
||||
# fill multiworld from top of itempool while we can
|
||||
while not progress_done:
|
||||
@@ -589,7 +596,7 @@ def flood_items(multiworld: MultiWorld) -> None:
|
||||
if candidate_item_to_place is not None:
|
||||
item_to_place = candidate_item_to_place
|
||||
else:
|
||||
raise FillError('No more progress items left to place.')
|
||||
raise FillError('No more progress items left to place.', multiworld=multiworld)
|
||||
|
||||
# find item to replace with progress item
|
||||
location_list = multiworld.get_reachable_locations()
|
||||
@@ -739,7 +746,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
||||
), items_to_test):
|
||||
reducing_state.collect(location.item, True, location)
|
||||
|
||||
reducing_state.sweep_for_events(locations=locations_to_test)
|
||||
reducing_state.sweep_for_advancements(locations=locations_to_test)
|
||||
|
||||
if multiworld.has_beaten_game(balancing_state):
|
||||
if not multiworld.has_beaten_game(reducing_state):
|
||||
@@ -822,7 +829,7 @@ def distribute_planned(multiworld: MultiWorld) -> None:
|
||||
warn(warning, force)
|
||||
|
||||
swept_state = multiworld.state.copy()
|
||||
swept_state.sweep_for_events()
|
||||
swept_state.sweep_for_advancements()
|
||||
reachable = frozenset(multiworld.get_reachable_locations(swept_state))
|
||||
early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
|
||||
non_early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
|
||||
|
||||
@@ -155,6 +155,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
|
||||
erargs.outputpath = args.outputpath
|
||||
erargs.skip_prog_balancing = args.skip_prog_balancing
|
||||
erargs.skip_output = args.skip_output
|
||||
erargs.name = {}
|
||||
|
||||
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
|
||||
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None)
|
||||
@@ -202,7 +203,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
|
||||
|
||||
if path == args.weights_file_path: # if name came from the weights file, just use base player name
|
||||
erargs.name[player] = f"Player{player}"
|
||||
elif not erargs.name[player]: # if name was not specified, generate it from filename
|
||||
elif player not in erargs.name: # if name was not specified, generate it from filename
|
||||
erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
|
||||
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
|
||||
|
||||
@@ -511,7 +512,7 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
||||
continue
|
||||
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers.")
|
||||
if PlandoOptions.items in plando_options:
|
||||
ret.plando_items = game_weights.get("plando_items", [])
|
||||
ret.plando_items = copy.deepcopy(game_weights.get("plando_items", []))
|
||||
if ret.game == "A Link to the Past":
|
||||
roll_alttp_settings(ret, game_weights)
|
||||
|
||||
|
||||
9
KH1Client.py
Normal file
9
KH1Client.py
Normal file
@@ -0,0 +1,9 @@
|
||||
if __name__ == '__main__':
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
|
||||
import Utils
|
||||
Utils.init_logging("KH1Client", exception_logger="Client")
|
||||
|
||||
from worlds.kh1.Client import launch
|
||||
launch()
|
||||
105
Launcher.py
105
Launcher.py
@@ -16,10 +16,11 @@ import multiprocessing
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.parse
|
||||
import webbrowser
|
||||
from os.path import isfile
|
||||
from shutil import which
|
||||
from typing import Callable, Sequence, Union, Optional
|
||||
from typing import Callable, Optional, Sequence, Tuple, Union
|
||||
|
||||
import Utils
|
||||
import settings
|
||||
@@ -107,7 +108,81 @@ components.extend([
|
||||
])
|
||||
|
||||
|
||||
def identify(path: Union[None, str]):
|
||||
def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
|
||||
url = urllib.parse.urlparse(path)
|
||||
queries = urllib.parse.parse_qs(url.query)
|
||||
launch_args = (path, *launch_args)
|
||||
client_component = None
|
||||
text_client_component = None
|
||||
if "game" in queries:
|
||||
game = queries["game"][0]
|
||||
else: # TODO around 0.6.0 - this is for pre this change webhost uri's
|
||||
game = "Archipelago"
|
||||
for component in components:
|
||||
if component.supports_uri and component.game_name == game:
|
||||
client_component = component
|
||||
elif component.display_name == "Text Client":
|
||||
text_client_component = component
|
||||
|
||||
from kvui import App, Button, BoxLayout, Label, Clock, Window
|
||||
|
||||
class Popup(App):
|
||||
timer_label: Label
|
||||
remaining_time: Optional[int]
|
||||
|
||||
def __init__(self):
|
||||
self.title = "Connect to Multiworld"
|
||||
self.icon = r"data/icon.png"
|
||||
super().__init__()
|
||||
|
||||
def build(self):
|
||||
layout = BoxLayout(orientation="vertical")
|
||||
|
||||
if client_component is None:
|
||||
self.remaining_time = 7
|
||||
label_text = (f"A game client able to parse URIs was not detected for {game}.\n"
|
||||
f"Launching Text Client in 7 seconds...")
|
||||
self.timer_label = Label(text=label_text)
|
||||
layout.add_widget(self.timer_label)
|
||||
Clock.schedule_interval(self.update_label, 1)
|
||||
else:
|
||||
layout.add_widget(Label(text="Select client to open and connect with."))
|
||||
button_row = BoxLayout(orientation="horizontal", size_hint=(1, 0.4))
|
||||
|
||||
text_client_button = Button(
|
||||
text=text_client_component.display_name,
|
||||
on_release=lambda *args: run_component(text_client_component, *launch_args)
|
||||
)
|
||||
button_row.add_widget(text_client_button)
|
||||
|
||||
game_client_button = Button(
|
||||
text=client_component.display_name,
|
||||
on_release=lambda *args: run_component(client_component, *launch_args)
|
||||
)
|
||||
button_row.add_widget(game_client_button)
|
||||
|
||||
layout.add_widget(button_row)
|
||||
|
||||
return layout
|
||||
|
||||
def update_label(self, dt):
|
||||
if self.remaining_time > 1:
|
||||
# countdown the timer and string replace the number
|
||||
self.remaining_time -= 1
|
||||
self.timer_label.text = self.timer_label.text.replace(
|
||||
str(self.remaining_time + 1), str(self.remaining_time)
|
||||
)
|
||||
else:
|
||||
# our timer is finished so launch text client and close down
|
||||
run_component(text_client_component, *launch_args)
|
||||
Clock.unschedule(self.update_label)
|
||||
App.get_running_app().stop()
|
||||
Window.close()
|
||||
|
||||
Popup().run()
|
||||
|
||||
|
||||
def identify(path: Union[None, str]) -> Tuple[Union[None, str], Union[None, Component]]:
|
||||
if path is None:
|
||||
return None, None
|
||||
for component in components:
|
||||
@@ -266,7 +341,7 @@ def run_gui():
|
||||
if file and component:
|
||||
run_component(component, file)
|
||||
else:
|
||||
logging.warning(f"unable to identify component for {filename}")
|
||||
logging.warning(f"unable to identify component for {file}")
|
||||
|
||||
def _stop(self, *largs):
|
||||
# ran into what appears to be https://groups.google.com/g/kivy-users/c/saWDLoYCSZ4 with PyCharm.
|
||||
@@ -299,20 +374,24 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
|
||||
elif not args:
|
||||
args = {}
|
||||
|
||||
if args.get("Patch|Game|Component", None) is not None:
|
||||
file, component = identify(args["Patch|Game|Component"])
|
||||
path = args.get("Patch|Game|Component|url", None)
|
||||
if path is not None:
|
||||
if path.startswith("archipelago://"):
|
||||
handle_uri(path, args.get("args", ()))
|
||||
return
|
||||
file, component = identify(path)
|
||||
if file:
|
||||
args['file'] = file
|
||||
if component:
|
||||
args['component'] = component
|
||||
if not component:
|
||||
logging.warning(f"Could not identify Component responsible for {args['Patch|Game|Component']}")
|
||||
logging.warning(f"Could not identify Component responsible for {path}")
|
||||
|
||||
if args["update_settings"]:
|
||||
update_settings()
|
||||
if 'file' in args:
|
||||
if "file" in args:
|
||||
run_component(args["component"], args["file"], *args["args"])
|
||||
elif 'component' in args:
|
||||
elif "component" in args:
|
||||
run_component(args["component"], *args["args"])
|
||||
elif not args["update_settings"]:
|
||||
run_gui()
|
||||
@@ -322,12 +401,16 @@ if __name__ == '__main__':
|
||||
init_logging('Launcher')
|
||||
Utils.freeze_support()
|
||||
multiprocessing.set_start_method("spawn") # if launched process uses kivy, fork won't work
|
||||
parser = argparse.ArgumentParser(description='Archipelago Launcher')
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Archipelago Launcher',
|
||||
usage="[-h] [--update_settings] [Patch|Game|Component] [-- component args here]"
|
||||
)
|
||||
run_group = parser.add_argument_group("Run")
|
||||
run_group.add_argument("--update_settings", action="store_true",
|
||||
help="Update host.yaml and exit.")
|
||||
run_group.add_argument("Patch|Game|Component", type=str, nargs="?",
|
||||
help="Pass either a patch file, a generated game or the name of a component to run.")
|
||||
run_group.add_argument("Patch|Game|Component|url", type=str, nargs="?",
|
||||
help="Pass either a patch file, a generated game, the component name to run, or a url to "
|
||||
"connect with.")
|
||||
run_group.add_argument("args", nargs="*",
|
||||
help="Arguments to pass to component.")
|
||||
main(parser.parse_args())
|
||||
|
||||
@@ -14,7 +14,7 @@ import tkinter as tk
|
||||
from argparse import Namespace
|
||||
from concurrent.futures import as_completed, ThreadPoolExecutor
|
||||
from glob import glob
|
||||
from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox, Button, Radiobutton, LEFT, X, TOP, LabelFrame, \
|
||||
from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox, Button, Radiobutton, LEFT, X, BOTH, TOP, LabelFrame, \
|
||||
IntVar, Checkbutton, E, W, OptionMenu, Toplevel, BOTTOM, RIGHT, font as font, PhotoImage
|
||||
from tkinter.constants import DISABLED, NORMAL
|
||||
from urllib.parse import urlparse
|
||||
@@ -29,7 +29,8 @@ from Utils import output_path, local_path, user_path, open_file, get_cert_none_s
|
||||
|
||||
|
||||
GAME_ALTTP = "A Link to the Past"
|
||||
|
||||
WINDOW_MIN_HEIGHT = 525
|
||||
WINDOW_MIN_WIDTH = 425
|
||||
|
||||
class AdjusterWorld(object):
|
||||
def __init__(self, sprite_pool):
|
||||
@@ -242,16 +243,17 @@ def adjustGUI():
|
||||
from argparse import Namespace
|
||||
from Utils import __version__ as MWVersion
|
||||
adjustWindow = Tk()
|
||||
adjustWindow.minsize(WINDOW_MIN_WIDTH, WINDOW_MIN_HEIGHT)
|
||||
adjustWindow.wm_title("Archipelago %s LttP Adjuster" % MWVersion)
|
||||
set_icon(adjustWindow)
|
||||
|
||||
rom_options_frame, rom_vars, set_sprite = get_rom_options_frame(adjustWindow)
|
||||
|
||||
bottomFrame2 = Frame(adjustWindow)
|
||||
bottomFrame2 = Frame(adjustWindow, padx=8, pady=2)
|
||||
|
||||
romFrame, romVar = get_rom_frame(adjustWindow)
|
||||
|
||||
romDialogFrame = Frame(adjustWindow)
|
||||
romDialogFrame = Frame(adjustWindow, padx=8, pady=2)
|
||||
baseRomLabel2 = Label(romDialogFrame, text='Rom to adjust')
|
||||
romVar2 = StringVar()
|
||||
romEntry2 = Entry(romDialogFrame, textvariable=romVar2)
|
||||
@@ -261,9 +263,9 @@ def adjustGUI():
|
||||
romVar2.set(rom)
|
||||
|
||||
romSelectButton2 = Button(romDialogFrame, text='Select Rom', command=RomSelect2)
|
||||
romDialogFrame.pack(side=TOP, expand=True, fill=X)
|
||||
baseRomLabel2.pack(side=LEFT)
|
||||
romEntry2.pack(side=LEFT, expand=True, fill=X)
|
||||
romDialogFrame.pack(side=TOP, expand=False, fill=X)
|
||||
baseRomLabel2.pack(side=LEFT, expand=False, fill=X, padx=(0, 8))
|
||||
romEntry2.pack(side=LEFT, expand=True, fill=BOTH, pady=1)
|
||||
romSelectButton2.pack(side=LEFT)
|
||||
|
||||
def adjustRom():
|
||||
@@ -331,12 +333,11 @@ def adjustGUI():
|
||||
messagebox.showinfo(title="Success", message="Settings saved to persistent storage")
|
||||
|
||||
adjustButton = Button(bottomFrame2, text='Adjust Rom', command=adjustRom)
|
||||
rom_options_frame.pack(side=TOP)
|
||||
rom_options_frame.pack(side=TOP, padx=8, pady=8, fill=BOTH, expand=True)
|
||||
adjustButton.pack(side=LEFT, padx=(5,5))
|
||||
|
||||
saveButton = Button(bottomFrame2, text='Save Settings', command=saveGUISettings)
|
||||
saveButton.pack(side=LEFT, padx=(5,5))
|
||||
|
||||
bottomFrame2.pack(side=TOP, pady=(5,5))
|
||||
|
||||
tkinter_center_window(adjustWindow)
|
||||
@@ -576,7 +577,7 @@ class AttachTooltip(object):
|
||||
def get_rom_frame(parent=None):
|
||||
adjuster_settings = get_adjuster_settings(GAME_ALTTP)
|
||||
|
||||
romFrame = Frame(parent)
|
||||
romFrame = Frame(parent, padx=8, pady=8)
|
||||
baseRomLabel = Label(romFrame, text='LttP Base Rom: ')
|
||||
romVar = StringVar(value=adjuster_settings.baserom)
|
||||
romEntry = Entry(romFrame, textvariable=romVar)
|
||||
@@ -596,20 +597,19 @@ def get_rom_frame(parent=None):
|
||||
romSelectButton = Button(romFrame, text='Select Rom', command=RomSelect)
|
||||
|
||||
baseRomLabel.pack(side=LEFT)
|
||||
romEntry.pack(side=LEFT, expand=True, fill=X)
|
||||
romEntry.pack(side=LEFT, expand=True, fill=BOTH, pady=1)
|
||||
romSelectButton.pack(side=LEFT)
|
||||
romFrame.pack(side=TOP, expand=True, fill=X)
|
||||
romFrame.pack(side=TOP, fill=X)
|
||||
|
||||
return romFrame, romVar
|
||||
|
||||
def get_rom_options_frame(parent=None):
|
||||
adjuster_settings = get_adjuster_settings(GAME_ALTTP)
|
||||
|
||||
romOptionsFrame = LabelFrame(parent, text="Rom options")
|
||||
romOptionsFrame.columnconfigure(0, weight=1)
|
||||
romOptionsFrame.columnconfigure(1, weight=1)
|
||||
romOptionsFrame = LabelFrame(parent, text="Rom options", padx=8, pady=8)
|
||||
|
||||
for i in range(5):
|
||||
romOptionsFrame.rowconfigure(i, weight=1)
|
||||
romOptionsFrame.rowconfigure(i, weight=0, pad=4)
|
||||
vars = Namespace()
|
||||
|
||||
vars.MusicVar = IntVar()
|
||||
@@ -660,7 +660,7 @@ def get_rom_options_frame(parent=None):
|
||||
spriteSelectButton = Button(spriteDialogFrame, text='...', command=SpriteSelect)
|
||||
|
||||
baseSpriteLabel.pack(side=LEFT)
|
||||
spriteEntry.pack(side=LEFT)
|
||||
spriteEntry.pack(side=LEFT, expand=True, fill=X)
|
||||
spriteSelectButton.pack(side=LEFT)
|
||||
|
||||
oofDialogFrame = Frame(romOptionsFrame)
|
||||
|
||||
7
Main.py
7
Main.py
@@ -11,7 +11,8 @@ from typing import Dict, List, Optional, Set, Tuple, Union
|
||||
|
||||
import worlds
|
||||
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region
|
||||
from Fill import balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items
|
||||
from Fill import FillError, balance_multiworld_progression, distribute_items_restrictive, distribute_planned, \
|
||||
flood_items
|
||||
from Options import StartInventoryPool
|
||||
from Utils import __version__, output_path, version_tuple, get_settings
|
||||
from settings import get_settings
|
||||
@@ -100,7 +101,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
multiworld.early_items[player][item_name] = max(0, early-count)
|
||||
remaining_count = count-early
|
||||
if remaining_count > 0:
|
||||
local_early = multiworld.early_local_items[player].get(item_name, 0)
|
||||
local_early = multiworld.local_early_items[player].get(item_name, 0)
|
||||
if local_early:
|
||||
multiworld.early_items[player][item_name] = max(0, local_early - remaining_count)
|
||||
del local_early
|
||||
@@ -346,7 +347,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
output_file_futures.append(pool.submit(write_multidata))
|
||||
if not check_accessibility_task.result():
|
||||
if not multiworld.can_beat_game():
|
||||
raise Exception("Game appears as unbeatable. Aborting.")
|
||||
raise FillError("Game appears as unbeatable. Aborting.", multiworld=multiworld)
|
||||
else:
|
||||
logger.warning("Location Accessibility requirements not fulfilled.")
|
||||
|
||||
|
||||
@@ -75,13 +75,13 @@ def update(yes: bool = False, force: bool = False) -> None:
|
||||
if not update_ran:
|
||||
update_ran = True
|
||||
|
||||
install_pkg_resources(yes=yes)
|
||||
import pkg_resources
|
||||
|
||||
if force:
|
||||
update_command()
|
||||
return
|
||||
|
||||
install_pkg_resources(yes=yes)
|
||||
import pkg_resources
|
||||
|
||||
prev = "" # if a line ends in \ we store here and merge later
|
||||
for req_file in requirements_files:
|
||||
path = os.path.join(os.path.dirname(sys.argv[0]), req_file)
|
||||
|
||||
@@ -67,6 +67,21 @@ def update_dict(dictionary, entries):
|
||||
return dictionary
|
||||
|
||||
|
||||
def queue_gc():
|
||||
import gc
|
||||
from threading import Thread
|
||||
|
||||
gc_thread: typing.Optional[Thread] = getattr(queue_gc, "_thread", None)
|
||||
def async_collect():
|
||||
time.sleep(2)
|
||||
setattr(queue_gc, "_thread", None)
|
||||
gc.collect()
|
||||
if not gc_thread:
|
||||
gc_thread = Thread(target=async_collect)
|
||||
setattr(queue_gc, "_thread", gc_thread)
|
||||
gc_thread.start()
|
||||
|
||||
|
||||
# functions callable on storable data on the server by clients
|
||||
modify_functions = {
|
||||
# generic:
|
||||
@@ -551,6 +566,9 @@ class Context:
|
||||
self.logger.info(f"Saving failed. Retry in {self.auto_save_interval} seconds.")
|
||||
else:
|
||||
self.save_dirty = False
|
||||
if not atexit_save: # if atexit is used, that keeps a reference anyway
|
||||
queue_gc()
|
||||
|
||||
self.auto_saver_thread = threading.Thread(target=save_regularly, daemon=True)
|
||||
self.auto_saver_thread.start()
|
||||
|
||||
@@ -991,7 +1009,7 @@ def collect_player(ctx: Context, team: int, slot: int, is_group: bool = False):
|
||||
collect_player(ctx, team, group, True)
|
||||
|
||||
|
||||
def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]:
|
||||
def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[typing.Tuple[int, int]]:
|
||||
return ctx.locations.get_remaining(ctx.location_checks, team, slot)
|
||||
|
||||
|
||||
@@ -1203,6 +1221,10 @@ class CommonCommandProcessor(CommandProcessor):
|
||||
timer = int(seconds, 10)
|
||||
except ValueError:
|
||||
timer = 10
|
||||
else:
|
||||
if timer > 60 * 60:
|
||||
raise ValueError(f"{timer} is invalid. Maximum is 1 hour.")
|
||||
|
||||
async_start(countdown(self.ctx, timer))
|
||||
return True
|
||||
|
||||
@@ -1350,10 +1372,10 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
def _cmd_remaining(self) -> bool:
|
||||
"""List remaining items in your game, but not their location or recipient"""
|
||||
if self.ctx.remaining_mode == "enabled":
|
||||
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
|
||||
if remaining_item_ids:
|
||||
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[self.client.slot]][item_id]
|
||||
for item_id in remaining_item_ids))
|
||||
rest_locations = get_remaining(self.ctx, self.client.team, self.client.slot)
|
||||
if rest_locations:
|
||||
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[slot]][item_id]
|
||||
for slot, item_id in rest_locations))
|
||||
else:
|
||||
self.output("No remaining items found.")
|
||||
return True
|
||||
@@ -1363,10 +1385,10 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
return False
|
||||
else: # is goal
|
||||
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
|
||||
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
|
||||
if remaining_item_ids:
|
||||
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[self.client.slot]][item_id]
|
||||
for item_id in remaining_item_ids))
|
||||
rest_locations = get_remaining(self.ctx, self.client.team, self.client.slot)
|
||||
if rest_locations:
|
||||
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[slot]][item_id]
|
||||
for slot, item_id in rest_locations))
|
||||
else:
|
||||
self.output("No remaining items found.")
|
||||
return True
|
||||
@@ -2039,6 +2061,8 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
item_name, usable, response = get_intended_text(item_name, names)
|
||||
if usable:
|
||||
amount: int = int(amount)
|
||||
if amount > 100:
|
||||
raise ValueError(f"{amount} is invalid. Maximum is 100.")
|
||||
new_items = [NetworkItem(names[item_name], -1, 0) for _ in range(int(amount))]
|
||||
send_items_to(self.ctx, team, slot, *new_items)
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ class NetworkItem(typing.NamedTuple):
|
||||
item: int
|
||||
location: int
|
||||
player: int
|
||||
""" Sending player, except in LocationInfo (from LocationScouts), where it is the receiving player. """
|
||||
flags: int = 0
|
||||
|
||||
|
||||
@@ -397,12 +398,12 @@ class _LocationStore(dict, typing.MutableMapping[int, typing.Dict[int, typing.Tu
|
||||
location_id not in checked]
|
||||
|
||||
def get_remaining(self, state: typing.Dict[typing.Tuple[int, int], typing.Set[int]], team: int, slot: int
|
||||
) -> typing.List[int]:
|
||||
) -> typing.List[typing.Tuple[int, int]]:
|
||||
checked = state[team, slot]
|
||||
player_locations = self[slot]
|
||||
return sorted([player_locations[location_id][0] for
|
||||
location_id in player_locations if
|
||||
location_id not in checked])
|
||||
return sorted([(player_locations[location_id][1], player_locations[location_id][0]) for
|
||||
location_id in player_locations if
|
||||
location_id not in checked])
|
||||
|
||||
|
||||
if typing.TYPE_CHECKING: # type-check with pure python implementation until we have a typing stub
|
||||
|
||||
14
Options.py
14
Options.py
@@ -973,7 +973,19 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys):
|
||||
if random.random() < float(text.get("percentage", 100)/100):
|
||||
at = text.get("at", None)
|
||||
if at is not None:
|
||||
if isinstance(at, dict):
|
||||
if at:
|
||||
at = random.choices(list(at.keys()),
|
||||
weights=list(at.values()), k=1)[0]
|
||||
else:
|
||||
raise OptionError("\"at\" must be a valid string or weighted list of strings!")
|
||||
given_text = text.get("text", [])
|
||||
if isinstance(given_text, dict):
|
||||
if not given_text:
|
||||
given_text = []
|
||||
else:
|
||||
given_text = random.choices(list(given_text.keys()),
|
||||
weights=list(given_text.values()), k=1)
|
||||
if isinstance(given_text, str):
|
||||
given_text = [given_text]
|
||||
texts.append(PlandoText(
|
||||
@@ -981,6 +993,8 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys):
|
||||
given_text,
|
||||
text.get("percentage", 100)
|
||||
))
|
||||
else:
|
||||
raise OptionError("\"at\" must be a valid string or weighted list of strings!")
|
||||
elif isinstance(text, PlandoText):
|
||||
if random.random() < float(text.percentage/100):
|
||||
texts.append(text)
|
||||
|
||||
@@ -73,6 +73,9 @@ Currently, the following games are supported:
|
||||
* Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006
|
||||
* A Hat in Time
|
||||
* Old School Runescape
|
||||
* Kingdom Hearts 1
|
||||
* Mega Man 2
|
||||
* Yacht Dice
|
||||
|
||||
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
|
||||
|
||||
10
WebHost.py
10
WebHost.py
@@ -1,3 +1,4 @@
|
||||
import argparse
|
||||
import os
|
||||
import multiprocessing
|
||||
import logging
|
||||
@@ -31,6 +32,15 @@ def get_app() -> "Flask":
|
||||
import yaml
|
||||
app.config.from_file(configpath, yaml.safe_load)
|
||||
logging.info(f"Updated config from {configpath}")
|
||||
# inside get_app() so it's usable in systems like gunicorn, which do not run WebHost.py, but import it.
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--config_override', default=None,
|
||||
help="Path to yaml config file that overrules config.yaml.")
|
||||
args = parser.parse_known_args()[0]
|
||||
if args.config_override:
|
||||
import yaml
|
||||
app.config.from_file(os.path.abspath(args.config_override), yaml.safe_load)
|
||||
logging.info(f"Updated config from {args.config_override}")
|
||||
if not app.config["HOST_ADDRESS"]:
|
||||
logging.info("Getting public IP, as HOST_ADDRESS is empty.")
|
||||
app.config["HOST_ADDRESS"] = Utils.get_public_ipv4()
|
||||
|
||||
@@ -72,6 +72,14 @@ class WebHostContext(Context):
|
||||
self.video = {}
|
||||
self.tags = ["AP", "WebHost"]
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
import psutil
|
||||
from Utils import format_SI_prefix
|
||||
self.logger.debug(f"Context destroyed, Mem: {format_SI_prefix(psutil.Process().memory_info().rss, 1024)}iB")
|
||||
except ImportError:
|
||||
self.logger.debug("Context destroyed")
|
||||
|
||||
def _load_game_data(self):
|
||||
for key, value in self.static_server_data.items():
|
||||
# NOTE: attributes are mutable and shared, so they will have to be copied before being modified
|
||||
@@ -249,6 +257,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
ctx = WebHostContext(static_server_data, logger)
|
||||
ctx.load(room_id)
|
||||
ctx.init_save()
|
||||
assert ctx.server is None
|
||||
try:
|
||||
ctx.server = websockets.serve(
|
||||
functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
|
||||
@@ -279,6 +288,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
ctx.auto_shutdown = Room.get(id=room_id).timeout
|
||||
if ctx.saving:
|
||||
setattr(asyncio.current_task(), "save", lambda: ctx._save(True))
|
||||
assert ctx.shutdown_task is None
|
||||
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
|
||||
await ctx.shutdown_task
|
||||
|
||||
@@ -325,7 +335,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
def run(self):
|
||||
while 1:
|
||||
next_room = rooms_to_run.get(block=True, timeout=None)
|
||||
gc.collect(0)
|
||||
gc.collect()
|
||||
task = asyncio.run_coroutine_threadsafe(start_room(next_room), loop)
|
||||
self._tasks.append(task)
|
||||
task.add_done_callback(self._done)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
flask>=3.0.3
|
||||
werkzeug>=3.0.3
|
||||
pony>=0.7.17
|
||||
werkzeug>=3.0.4
|
||||
pony>=0.7.19
|
||||
waitress>=3.0.0
|
||||
Flask-Caching>=2.3.0
|
||||
Flask-Compress>=1.15
|
||||
Flask-Limiter>=3.7.0
|
||||
Flask-Limiter>=3.8.0
|
||||
bokeh>=3.1.1; python_version <= '3.8'
|
||||
bokeh>=3.4.1; python_version >= '3.9'
|
||||
bokeh>=3.4.3; python_version == '3.9'
|
||||
bokeh>=3.5.2; python_version >= '3.10'
|
||||
markupsafe>=2.1.5
|
||||
|
||||
@@ -77,4 +77,4 @@ There, you will find examples of games in the `worlds` folder:
|
||||
You may also find developer documentation in the `docs` folder:
|
||||
[/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs).
|
||||
|
||||
If you have more questions, feel free to ask in the **#archipelago-dev** channel on our Discord.
|
||||
If you have more questions, feel free to ask in the **#ap-world-dev** channel on our Discord.
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{% for patch in room.seed.slots|list|sort(attribute="player_id") %}
|
||||
<tr>
|
||||
<td>{{ patch.player_id }}</td>
|
||||
<td data-tooltip="Connect via TextClient"><a href="archipelago://{{ patch.player_name | e}}:None@{{ config['HOST_ADDRESS'] }}:{{ room.last_port }}">{{ patch.player_name }}</a></td>
|
||||
<td data-tooltip="Connect via Game Client"><a href="archipelago://{{ patch.player_name | e}}:None@{{ config['HOST_ADDRESS'] }}:{{ room.last_port }}?game={{ patch.game }}&room={{ room.id | suuid }}">{{ patch.player_name }}</a></td>
|
||||
<td>{{ patch.game }}</td>
|
||||
<td>
|
||||
{% if patch.data %}
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
You have no generated any seeds yet!
|
||||
You have not generated any seeds yet!
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
id="{{ option_name }}-{{ key }}"
|
||||
name="{{ option_name }}||{{ key }}"
|
||||
value="1"
|
||||
checked="{{ "checked" if key in option.default else "" }}"
|
||||
{{ "checked" if key in option.default }}
|
||||
/>
|
||||
<label for="{{ option_name }}-{{ key }}">
|
||||
{{ key }}
|
||||
|
||||
@@ -287,15 +287,15 @@ cdef class LocationStore:
|
||||
entry in self.entries[start:start + count] if
|
||||
entry.location not in checked]
|
||||
|
||||
def get_remaining(self, state: State, team: int, slot: int) -> List[int]:
|
||||
def get_remaining(self, state: State, team: int, slot: int) -> List[Tuple[int, int]]:
|
||||
cdef LocationEntry* entry
|
||||
cdef ap_player_t sender = slot
|
||||
cdef size_t start = self.sender_index[sender].start
|
||||
cdef size_t count = self.sender_index[sender].count
|
||||
cdef set checked = state[team, slot]
|
||||
return sorted([entry.item for
|
||||
entry in self.entries[start:start+count] if
|
||||
entry.location not in checked])
|
||||
return sorted([(entry.receiver, entry.item) for
|
||||
entry in self.entries[start:start+count] if
|
||||
entry.location not in checked])
|
||||
|
||||
|
||||
@cython.auto_pickle(False)
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
/worlds/clique/ @ThePhar
|
||||
|
||||
# Dark Souls III
|
||||
/worlds/dark_souls_3/ @Marechal-L
|
||||
/worlds/dark_souls_3/ @Marechal-L @nex3
|
||||
|
||||
# Donkey Kong Country 3
|
||||
/worlds/dkc3/ @PoryGone
|
||||
@@ -78,6 +78,9 @@
|
||||
# Kirby's Dream Land 3
|
||||
/worlds/kdl3/ @Silvris
|
||||
|
||||
# Kingdom Hearts
|
||||
/worlds/kh1/ @gaithern
|
||||
|
||||
# Kingdom Hearts 2
|
||||
/worlds/kh2/ @JaredWeakStrike
|
||||
|
||||
@@ -103,6 +106,9 @@
|
||||
# Minecraft
|
||||
/worlds/minecraft/ @KonoTyran @espeon65536
|
||||
|
||||
# Mega Man 2
|
||||
/worlds/mm2/ @Silvris
|
||||
|
||||
# MegaMan Battle Network 3
|
||||
/worlds/mmbn3/ @digiholic
|
||||
|
||||
@@ -112,9 +118,6 @@
|
||||
# Noita
|
||||
/worlds/noita/ @ScipioWright @heinermann
|
||||
|
||||
# Ocarina of Time
|
||||
/worlds/oot/ @espeon65536
|
||||
|
||||
# Old School Runescape
|
||||
/worlds/osrs @digiholic
|
||||
|
||||
@@ -196,6 +199,9 @@
|
||||
# The Witness
|
||||
/worlds/witness/ @NewSoupVi @blastron
|
||||
|
||||
# Yacht Dice
|
||||
/worlds/yachtdice/ @spinerak
|
||||
|
||||
# Yoshi's Island
|
||||
/worlds/yoshisisland/ @PinkSwitch
|
||||
|
||||
@@ -221,6 +227,9 @@
|
||||
# Links Awakening DX
|
||||
# /worlds/ladx/
|
||||
|
||||
# Ocarina of Time
|
||||
# /worlds/oot/
|
||||
|
||||
## Disabled Unmaintained Worlds
|
||||
|
||||
# The following worlds in this repo are currently unmaintained and disabled as they do not work in core. If you are
|
||||
|
||||
@@ -702,14 +702,18 @@ GameData is a **dict** but contains these keys and values. It's broken out into
|
||||
| checksum | str | A checksum hash of this game's data. |
|
||||
|
||||
### Tags
|
||||
Tags are represented as a list of strings, the common Client tags follow:
|
||||
Tags are represented as a list of strings, the common client tags follow:
|
||||
|
||||
| Name | Notes |
|
||||
|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| AP | Signifies that this client is a reference client, its usefulness is mostly in debugging to compare client behaviours more easily. |
|
||||
| DeathLink | Client participates in the DeathLink mechanic, therefore will send and receive DeathLink bounce packets |
|
||||
| Tracker | Tells the server that this client will not send locations and is actually a Tracker. When specified and used with empty or null `game` in [Connect](#connect), game and game's version validation will be skipped. |
|
||||
| TextOnly | Tells the server that this client will not send locations and is intended for chat. When specified and used with empty or null `game` in [Connect](#connect), game and game's version validation will be skipped. |
|
||||
| Name | Notes |
|
||||
|-----------|--------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| AP | Signifies that this client is a reference client, its usefulness is mostly in debugging to compare client behaviours more easily. |
|
||||
| DeathLink | Client participates in the DeathLink mechanic, therefore will send and receive DeathLink bounce packets. |
|
||||
| HintGame | Indicates the client is a hint game, made to send hints instead of locations. Special join/leave message,¹ `game` is optional.² |
|
||||
| Tracker | Indicates the client is a tracker, made to track instead of sending locations. Special join/leave message,¹ `game` is optional.² |
|
||||
| TextOnly | Indicates the client is a basic client, made to chat instead of sending locations. Special join/leave message,¹ `game` is optional.² |
|
||||
|
||||
¹: When connecting or disconnecting, the chat message shows e.g. "tracking".\
|
||||
²: Allows `game` to be empty or null in [Connect](#connect). Game and version validation will then be skipped.
|
||||
|
||||
### DeathLink
|
||||
A special kind of Bounce packet that can be supported by any AP game. It targets the tag "DeathLink" and carries the following data:
|
||||
|
||||
@@ -24,7 +24,7 @@ display as `Value1` on the webhost.
|
||||
files, and both will resolve as `value1`. This should be used when changing options around, i.e. changing a Toggle to a
|
||||
Choice, and defining `alias_true = option_full`.
|
||||
- All options with a fixed set of possible values (i.e. those which inherit from `Toggle`, `(Text)Choice` or
|
||||
`(Named/Special)Range`) support `random` as a generic option. `random` chooses from any of the available values for that
|
||||
`(Named)Range`) support `random` as a generic option. `random` chooses from any of the available values for that
|
||||
option, and is reserved by AP. You can set this as your default value, but you cannot define your own `option_random`.
|
||||
However, you can override `from_text` and handle `text == "random"` to customize its behavior or
|
||||
implement it for additional option types.
|
||||
@@ -129,6 +129,23 @@ class Difficulty(Choice):
|
||||
default = 1
|
||||
```
|
||||
|
||||
### Option Visibility
|
||||
Every option has a Visibility IntFlag, defaulting to `all` (`0b1111`). This lets you choose where the option will be
|
||||
displayed. This only impacts where options are displayed, not how they can be used. Hidden options are still valid
|
||||
options in a yaml. The flags are as follows:
|
||||
* `none` (`0b0000`): This option is not shown anywhere
|
||||
* `template` (`0b0001`): This option shows up in template yamls
|
||||
* `simple_ui` (`0b0010`): This option shows up on the options page
|
||||
* `complex_ui` (`0b0100`): This option shows up on the advanced/weighted options page
|
||||
* `spoiler` (`0b1000`): This option shows up in spoiler logs
|
||||
|
||||
```python
|
||||
from Options import Choice, Visibility
|
||||
|
||||
class HiddenChoiceOption(Choice):
|
||||
visibility = Visibility.none
|
||||
```
|
||||
|
||||
### Option Groups
|
||||
Options may be categorized into groups for display on the WebHost. Option groups are displayed in the order specified
|
||||
by your world on the player-options and weighted-options pages. In the generated template files, there will be a comment
|
||||
|
||||
@@ -8,7 +8,7 @@ use that version. These steps are for developers or platforms without compiled r
|
||||
|
||||
What you'll need:
|
||||
* [Python 3.8.7 or newer](https://www.python.org/downloads/), not the Windows Store version
|
||||
* **Python 3.12 is currently unsupported**
|
||||
* Python 3.12.x is currently the newest supported version
|
||||
* pip: included in downloads from python.org, separate in many Linux distributions
|
||||
* Matching C compiler
|
||||
* possibly optional, read operating system specific sections
|
||||
@@ -31,14 +31,14 @@ After this, you should be able to run the programs.
|
||||
|
||||
Recommended steps
|
||||
* Download and install a "Windows installer (64-bit)" from the [Python download page](https://www.python.org/downloads)
|
||||
* **Python 3.12 is currently unsupported**
|
||||
* [read above](#General) which versions are supported
|
||||
|
||||
* **Optional**: Download and install Visual Studio Build Tools from
|
||||
[Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/).
|
||||
* Refer to [Windows Compilers on the python wiki](https://wiki.python.org/moin/WindowsCompilers) for details.
|
||||
Generally, selecting the box for "Desktop Development with C++" will provide what you need.
|
||||
* Build tools are not required if all modules are installed pre-compiled. Pre-compiled modules are pinned on
|
||||
[Discord in #archipelago-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808)
|
||||
[Discord in #ap-core-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808)
|
||||
|
||||
* It is recommended to use [PyCharm IDE](https://www.jetbrains.com/pycharm/)
|
||||
* Run Generate.py which will prompt installation of missing modules, press enter to confirm
|
||||
|
||||
@@ -303,6 +303,31 @@ generation (entrance randomization).
|
||||
An access rule is a function that returns `True` or `False` for a `Location` or `Entrance` based on the current `state`
|
||||
(items that have been collected).
|
||||
|
||||
The two possible ways to make a [CollectionRule](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L10) are:
|
||||
- `def rule(state: CollectionState) -> bool:`
|
||||
- `lambda state: ... boolean expression ...`
|
||||
|
||||
An access rule can be assigned through `set_rule(location, rule)`.
|
||||
|
||||
Access rules usually check for one of two things.
|
||||
- Items that have been collected (e.g. `state.has("Sword", player)`)
|
||||
- Locations, Regions or Entrances that have been reached (e.g. `state.can_reach_region("Boss Room")`)
|
||||
|
||||
Keep in mind that entrances and locations implicitly check for the accessibility of their parent region, so you do not need to check explicitly for it.
|
||||
|
||||
#### An important note on Entrance access rules:
|
||||
When using `state.can_reach` within an entrance access condition, you must also use `multiworld.register_indirect_condition`.
|
||||
|
||||
For efficiency reasons, every time reachable regions are searched, every entrance is only checked once in a somewhat non-deterministic order.
|
||||
This is fine when checking for items using `state.has`, because items do not change during a region sweep.
|
||||
However, `state.can_reach` checks for the very same thing we are updating: Regions.
|
||||
This can lead to non-deterministic behavior and, in the worst case, even generation failures.
|
||||
Even doing `state.can_reach_location` or `state.can_reach_entrance` is problematic, as these functions call `state.can_reach_region` on the respective parent region.
|
||||
|
||||
**Therefore, it is considered unsafe to perform `state.can_reach` from within an access condition for an entrance**, unless you are checking for something that sits in the source region of the entrance.
|
||||
You can use `multiworld.register_indirect_condition(region, entrance)` to explicitly tell the generator that, when a given region becomes accessible, it is necessary to re-check a specific entrance.
|
||||
You **must** use `multiworld.register_indirect_condition` if you perform this kind of `can_reach` from an entrance access rule, unless you have a **very** good technical understanding of the relevant code and can reason why it will never lead to problems in your case.
|
||||
|
||||
### Item Rules
|
||||
|
||||
An item rule is a function that returns `True` or `False` for a `Location` based on a single item. It can be used to
|
||||
@@ -630,7 +655,7 @@ def set_rules(self) -> None:
|
||||
|
||||
Custom methods can be defined for your logic rules. The access rule that ultimately gets assigned to the Location or
|
||||
Entrance should be
|
||||
a [`CollectionRule`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L9).
|
||||
a [`CollectionRule`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L10).
|
||||
Typically, this is done by defining a lambda expression on demand at the relevant bit, typically calling other
|
||||
functions, but this can also be achieved by defining a method with the appropriate format and assigning it directly.
|
||||
For an example, see [The Messenger](/worlds/messenger/rules.py).
|
||||
|
||||
@@ -26,8 +26,17 @@ Unless these are shared between multiple people, we expect the following from ea
|
||||
### Adding a World
|
||||
|
||||
When we merge your world into the core Archipelago repository, you automatically become world maintainer unless you
|
||||
nominate someone else (i.e. there are multiple devs). You can define who is allowed to approve changes to your world
|
||||
in the [CODEOWNERS](/docs/CODEOWNERS) document.
|
||||
nominate someone else (i.e. there are multiple devs).
|
||||
|
||||
### Being added as a maintainer to an existing implementation
|
||||
|
||||
At any point, a world maintainer can approve the addition of another maintainer to their world.
|
||||
In order to do this, either an existing maintainer or the new maintainer must open a PR updating the
|
||||
[CODEOWNERS](/docs/CODEOWNERS) file.
|
||||
This change must be approved by all existing maintainers of the affected world, the new maintainer candidate, and
|
||||
one core maintainer.
|
||||
To help the core team review the change, information about the new maintainer and their contributions should be
|
||||
included in the PR description.
|
||||
|
||||
### Getting Voted
|
||||
|
||||
@@ -35,7 +44,7 @@ When a world is unmaintained, the [core maintainers](https://github.com/orgs/Arc
|
||||
can vote for a new maintainer if there is a candidate.
|
||||
For a vote to pass, the majority of participating core maintainers must vote in the affirmative.
|
||||
The time limit is 1 week, but can end early if the majority is reached earlier.
|
||||
Voting shall be conducted on Discord in #archipelago-dev.
|
||||
Voting shall be conducted on Discord in #ap-core-dev.
|
||||
|
||||
## Dropping out
|
||||
|
||||
@@ -51,7 +60,7 @@ for example when they become unreachable.
|
||||
For a vote to pass, the majority of participating core maintainers must vote in the affirmative.
|
||||
The time limit is 2 weeks, but can end early if the majority is reached earlier AND the world maintainer was pinged and
|
||||
made their case or was pinged and has been unreachable for more than 2 weeks already.
|
||||
Voting shall be conducted on Discord in #archipelago-dev. Commits that are a direct result of the voting shall include
|
||||
Voting shall be conducted on Discord in #ap-core-dev. Commits that are a direct result of the voting shall include
|
||||
date, voting members and final result in the commit message.
|
||||
|
||||
## Handling of Unmaintained Worlds
|
||||
|
||||
@@ -186,6 +186,11 @@ Root: HKCR; Subkey: "{#MyAppName}cv64patch"; ValueData: "Arc
|
||||
Root: HKCR; Subkey: "{#MyAppName}cv64patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}cv64patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apmm2"; ValueData: "{#MyAppName}mm2patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mm2patch"; ValueData: "Archipelago Mega Man 2 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mm2patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mm2patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: "";
|
||||
@@ -223,8 +228,8 @@ Root: HKCR; Subkey: "{#MyAppName}worlddata\shell\open\command"; ValueData: """{a
|
||||
|
||||
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueData: "Archipegalo Protocol"; Flags: uninsdeletekey;
|
||||
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: "";
|
||||
Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoTextClient.exe,0";
|
||||
Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoTextClient.exe"" ""%1""";
|
||||
Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoLauncher.exe,0";
|
||||
Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1""";
|
||||
|
||||
[Code]
|
||||
// See: https://stackoverflow.com/a/51614652/2287576
|
||||
|
||||
2
kvui.py
2
kvui.py
@@ -5,6 +5,8 @@ import typing
|
||||
import re
|
||||
from collections import deque
|
||||
|
||||
assert "kivy" not in sys.modules, "kvui should be imported before kivy for frozen compatibility"
|
||||
|
||||
if sys.platform == "win32":
|
||||
import ctypes
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
colorama>=0.4.6
|
||||
websockets>=12.0
|
||||
PyYAML>=6.0.1
|
||||
jellyfish>=1.0.3
|
||||
websockets>=13.0.1
|
||||
PyYAML>=6.0.2
|
||||
jellyfish>=1.1.0
|
||||
jinja2>=3.1.4
|
||||
schema>=0.7.7
|
||||
kivy>=2.3.0
|
||||
bsdiff4>=1.2.4
|
||||
platformdirs>=4.2.2
|
||||
certifi>=2024.6.2
|
||||
cython>=3.0.10
|
||||
certifi>=2024.8.30
|
||||
cython>=3.0.11
|
||||
cymem>=2.0.8
|
||||
orjson>=3.10.3
|
||||
typing_extensions>=4.12.1
|
||||
orjson>=3.10.7
|
||||
typing_extensions>=4.12.2
|
||||
|
||||
@@ -23,8 +23,8 @@ class TestBase(unittest.TestCase):
|
||||
state = CollectionState(self.multiworld)
|
||||
for item in items:
|
||||
item.classification = ItemClassification.progression
|
||||
state.collect(item, event=True)
|
||||
state.sweep_for_events()
|
||||
state.collect(item, prevent_sweep=True)
|
||||
state.sweep_for_advancements()
|
||||
state.update_reachable_regions(1)
|
||||
self._state_cache[self.multiworld, tuple(items)] = state
|
||||
return state
|
||||
@@ -221,8 +221,8 @@ class WorldTestBase(unittest.TestCase):
|
||||
if isinstance(items, Item):
|
||||
items = (items,)
|
||||
for item in items:
|
||||
if item.location and item.advancement and item.location in self.multiworld.state.events:
|
||||
self.multiworld.state.events.remove(item.location)
|
||||
if item.location and item.advancement and item.location in self.multiworld.state.advancements:
|
||||
self.multiworld.state.advancements.remove(item.location)
|
||||
self.multiworld.state.remove(item)
|
||||
|
||||
def can_reach_location(self, location: str) -> bool:
|
||||
@@ -293,13 +293,11 @@ class WorldTestBase(unittest.TestCase):
|
||||
if not (self.run_default_tests and self.constructed):
|
||||
return
|
||||
with self.subTest("Game", game=self.game, seed=self.multiworld.seed):
|
||||
excluded = self.multiworld.worlds[self.player].options.exclude_locations.value
|
||||
state = self.multiworld.get_all_state(False)
|
||||
for location in self.multiworld.get_locations():
|
||||
if location.name not in excluded:
|
||||
with self.subTest("Location should be reached", location=location.name):
|
||||
reachable = location.can_reach(state)
|
||||
self.assertTrue(reachable, f"{location.name} unreachable")
|
||||
with self.subTest("Location should be reached", location=location.name):
|
||||
reachable = location.can_reach(state)
|
||||
self.assertTrue(reachable, f"{location.name} unreachable")
|
||||
with self.subTest("Beatable"):
|
||||
self.multiworld.state = state
|
||||
self.assertBeatable(True)
|
||||
|
||||
@@ -192,7 +192,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
location_pool = player1.locations[1:] + player2.locations
|
||||
item_pool = player1.prog_items[:-1] + player2.prog_items
|
||||
fill_restrictive(multiworld, multiworld.state, location_pool, item_pool)
|
||||
multiworld.state.sweep_for_events() # collect everything
|
||||
multiworld.state.sweep_for_advancements() # collect everything
|
||||
|
||||
# all of player2's locations and items should be accessible (not all of player1's)
|
||||
for item in player2.prog_items:
|
||||
@@ -443,8 +443,8 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
item = player1.prog_items[0]
|
||||
item.code = None
|
||||
location.place_locked_item(item)
|
||||
multiworld.state.sweep_for_events()
|
||||
multiworld.state.sweep_for_events()
|
||||
multiworld.state.sweep_for_advancements()
|
||||
multiworld.state.sweep_for_advancements()
|
||||
self.assertTrue(multiworld.state.prog_items[item.player][item.name], "Sweep did not collect - Test flawed")
|
||||
self.assertEqual(multiworld.state.prog_items[item.player][item.name], 1, "Sweep collected multiple times")
|
||||
|
||||
|
||||
@@ -14,6 +14,18 @@ class TestBase(unittest.TestCase):
|
||||
"Desert Northern Cliffs", # on top of mountain, only reachable via OWG
|
||||
"Dark Death Mountain Bunny Descent Area" # OWG Mountain descent
|
||||
},
|
||||
# These Blasphemous regions are not reachable with default options
|
||||
"Blasphemous": {
|
||||
"D01Z04S13[SE]", # difficulty must be hard
|
||||
"D01Z05S25[E]", # difficulty must be hard
|
||||
"D02Z02S05[W]", # difficulty must be hard and purified_hand must be true
|
||||
"D04Z01S06[E]", # purified_hand must be true
|
||||
"D04Z02S02[NE]", # difficulty must be hard and purified_hand must be true
|
||||
"D05Z01S11[SW]", # difficulty must be hard
|
||||
"D06Z01S08[N]", # difficulty must be hard and purified_hand must be true
|
||||
"D20Z02S11[NW]", # difficulty must be hard
|
||||
"D20Z02S11[E]", # difficulty must be hard
|
||||
},
|
||||
"Ocarina of Time": {
|
||||
"Prelude of Light Warp", # Prelude is not progression by default
|
||||
"Serenade of Water Warp", # Serenade is not progression by default
|
||||
@@ -37,12 +49,10 @@ class TestBase(unittest.TestCase):
|
||||
unreachable_regions = self.default_settings_unreachable_regions.get(game_name, set())
|
||||
with self.subTest("Game", game=game_name):
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
excluded = multiworld.worlds[1].options.exclude_locations.value
|
||||
state = multiworld.get_all_state(False)
|
||||
for location in multiworld.get_locations():
|
||||
if location.name not in excluded:
|
||||
with self.subTest("Location should be reached", location=location.name):
|
||||
self.assertTrue(location.can_reach(state), f"{location.name} unreachable")
|
||||
with self.subTest("Location should be reached", location=location.name):
|
||||
self.assertTrue(location.can_reach(state), f"{location.name} unreachable")
|
||||
|
||||
for region in multiworld.get_regions():
|
||||
if region.name in unreachable_regions:
|
||||
|
||||
@@ -55,7 +55,7 @@ class TestAllGamesMultiworld(MultiworldTestBase):
|
||||
all_worlds = list(AutoWorldRegister.world_types.values())
|
||||
self.multiworld = setup_multiworld(all_worlds, ())
|
||||
for world in self.multiworld.worlds.values():
|
||||
world.options.accessibility.value = Accessibility.option_locations
|
||||
world.options.accessibility.value = Accessibility.option_full
|
||||
self.assertSteps(gen_steps)
|
||||
with self.subTest("filling multiworld", seed=self.multiworld.seed):
|
||||
distribute_items_restrictive(self.multiworld)
|
||||
@@ -66,8 +66,8 @@ class TestAllGamesMultiworld(MultiworldTestBase):
|
||||
class TestTwoPlayerMulti(MultiworldTestBase):
|
||||
def test_two_player_single_game_fills(self) -> None:
|
||||
"""Tests that a multiworld of two players for each registered game world can generate."""
|
||||
for world in AutoWorldRegister.world_types.values():
|
||||
self.multiworld = setup_multiworld([world, world], ())
|
||||
for world_type in AutoWorldRegister.world_types.values():
|
||||
self.multiworld = setup_multiworld([world_type, world_type], ())
|
||||
for world in self.multiworld.worlds.values():
|
||||
world.options.accessibility.value = Accessibility.option_full
|
||||
self.assertSteps(gen_steps)
|
||||
|
||||
@@ -130,9 +130,9 @@ class Base:
|
||||
|
||||
def test_get_remaining(self) -> None:
|
||||
self.assertEqual(self.store.get_remaining(full_state, 0, 1), [])
|
||||
self.assertEqual(self.store.get_remaining(one_state, 0, 1), [13, 21])
|
||||
self.assertEqual(self.store.get_remaining(empty_state, 0, 1), [13, 21, 22])
|
||||
self.assertEqual(self.store.get_remaining(empty_state, 0, 3), [99])
|
||||
self.assertEqual(self.store.get_remaining(one_state, 0, 1), [(1, 13), (2, 21)])
|
||||
self.assertEqual(self.store.get_remaining(empty_state, 0, 1), [(1, 13), (2, 21), (2, 22)])
|
||||
self.assertEqual(self.store.get_remaining(empty_state, 0, 3), [(4, 99)])
|
||||
|
||||
def test_location_set_intersection(self) -> None:
|
||||
locations = {10, 11, 12}
|
||||
|
||||
@@ -292,6 +292,14 @@ class World(metaclass=AutoWorldRegister):
|
||||
web: ClassVar[WebWorld] = WebWorld()
|
||||
"""see WebWorld for options"""
|
||||
|
||||
origin_region_name: str = "Menu"
|
||||
"""Name of the Region from which accessibility is tested."""
|
||||
|
||||
explicit_indirect_conditions: bool = True
|
||||
"""If True, the world implementation is supposed to use MultiWorld.register_indirect_condition() correctly.
|
||||
If False, everything is rechecked at every step, which is slower computationally,
|
||||
but may be desirable in complex/dynamic worlds."""
|
||||
|
||||
multiworld: "MultiWorld"
|
||||
"""autoset on creation. The MultiWorld object for the currently generating multiworld."""
|
||||
player: int
|
||||
|
||||
@@ -26,10 +26,13 @@ class Component:
|
||||
cli: bool
|
||||
func: Optional[Callable]
|
||||
file_identifier: Optional[Callable[[str], bool]]
|
||||
game_name: Optional[str]
|
||||
supports_uri: Optional[bool]
|
||||
|
||||
def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None,
|
||||
cli: bool = False, icon: str = 'icon', component_type: Optional[Type] = None,
|
||||
func: Optional[Callable] = None, file_identifier: Optional[Callable[[str], bool]] = None):
|
||||
func: Optional[Callable] = None, file_identifier: Optional[Callable[[str], bool]] = None,
|
||||
game_name: Optional[str] = None, supports_uri: Optional[bool] = False):
|
||||
self.display_name = display_name
|
||||
self.script_name = script_name
|
||||
self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None
|
||||
@@ -45,6 +48,8 @@ class Component:
|
||||
Type.ADJUSTER if "Adjuster" in display_name else Type.MISC)
|
||||
self.func = func
|
||||
self.file_identifier = file_identifier
|
||||
self.game_name = game_name
|
||||
self.supports_uri = supports_uri
|
||||
|
||||
def handles_file(self, path: str):
|
||||
return self.file_identifier(path) if self.file_identifier else False
|
||||
@@ -56,10 +61,10 @@ class Component:
|
||||
processes = weakref.WeakSet()
|
||||
|
||||
|
||||
def launch_subprocess(func: Callable, name: str = None):
|
||||
def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()) -> None:
|
||||
global processes
|
||||
import multiprocessing
|
||||
process = multiprocessing.Process(target=func, name=name)
|
||||
process = multiprocessing.Process(target=func, name=name, args=args)
|
||||
process.start()
|
||||
processes.add(process)
|
||||
|
||||
@@ -78,9 +83,9 @@ class SuffixIdentifier:
|
||||
return False
|
||||
|
||||
|
||||
def launch_textclient():
|
||||
def launch_textclient(*args):
|
||||
import CommonClient
|
||||
launch_subprocess(CommonClient.run_as_textclient, name="TextClient")
|
||||
launch_subprocess(CommonClient.run_as_textclient, name="TextClient", args=args)
|
||||
|
||||
|
||||
def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]:
|
||||
@@ -132,7 +137,8 @@ def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, path
|
||||
break
|
||||
if found_already_loaded:
|
||||
raise Exception(f"Installed APWorld successfully, but '{module_name}' is already loaded,\n"
|
||||
"so a Launcher restart is required to use the new installation.")
|
||||
"so a Launcher restart is required to use the new installation.\n"
|
||||
"If the Launcher is not open, no action needs to be taken.")
|
||||
world_source = worlds.WorldSource(str(target), is_zip=True)
|
||||
bisect.insort(worlds.world_sources, world_source)
|
||||
world_source.load()
|
||||
|
||||
@@ -59,14 +59,10 @@ class BizHawkClientContext(CommonContext):
|
||||
self.bizhawk_ctx = BizHawkContext()
|
||||
self.watcher_timeout = 0.5
|
||||
|
||||
def run_gui(self):
|
||||
from kvui import GameManager
|
||||
|
||||
class BizHawkManager(GameManager):
|
||||
base_title = "Archipelago BizHawk Client"
|
||||
|
||||
self.ui = BizHawkManager(self)
|
||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||
def make_gui(self):
|
||||
ui = super().make_gui()
|
||||
ui.base_title = "Archipelago BizHawk Client"
|
||||
return ui
|
||||
|
||||
def on_package(self, cmd, args):
|
||||
if cmd == "Connected":
|
||||
|
||||
@@ -968,40 +968,35 @@ def get_act_by_number(world: "HatInTimeWorld", chapter_name: str, num: int) -> R
|
||||
def create_thug_shops(world: "HatInTimeWorld"):
|
||||
min_items: int = world.options.NyakuzaThugMinShopItems.value
|
||||
max_items: int = world.options.NyakuzaThugMaxShopItems.value
|
||||
count = -1
|
||||
step = 0
|
||||
old_name = ""
|
||||
|
||||
thug_location_counts: Dict[str, int] = {}
|
||||
|
||||
for key, data in shop_locations.items():
|
||||
if data.nyakuza_thug == "":
|
||||
thug_name = data.nyakuza_thug
|
||||
if thug_name == "":
|
||||
# Different shop type.
|
||||
continue
|
||||
|
||||
if old_name != "" and old_name == data.nyakuza_thug:
|
||||
if thug_name not in world.nyakuza_thug_items:
|
||||
shop_item_count = world.random.randint(min_items, max_items)
|
||||
world.nyakuza_thug_items[thug_name] = shop_item_count
|
||||
else:
|
||||
shop_item_count = world.nyakuza_thug_items[thug_name]
|
||||
|
||||
if shop_item_count <= 0:
|
||||
continue
|
||||
|
||||
try:
|
||||
if world.nyakuza_thug_items[data.nyakuza_thug] <= 0:
|
||||
continue
|
||||
except KeyError:
|
||||
pass
|
||||
location_count = thug_location_counts.setdefault(thug_name, 0)
|
||||
if location_count >= shop_item_count:
|
||||
# Already created all the locations for this thug.
|
||||
continue
|
||||
|
||||
if count == -1:
|
||||
count = world.random.randint(min_items, max_items)
|
||||
world.nyakuza_thug_items.setdefault(data.nyakuza_thug, count)
|
||||
if count <= 0:
|
||||
continue
|
||||
|
||||
if count >= 1:
|
||||
region = world.multiworld.get_region(data.region, world.player)
|
||||
loc = HatInTimeLocation(world.player, key, data.id, region)
|
||||
region.locations.append(loc)
|
||||
world.shop_locs.append(loc.name)
|
||||
|
||||
step += 1
|
||||
if step >= count:
|
||||
old_name = data.nyakuza_thug
|
||||
step = 0
|
||||
count = -1
|
||||
# Create the shop location.
|
||||
region = world.multiworld.get_region(data.region, world.player)
|
||||
loc = HatInTimeLocation(world.player, key, data.id, region)
|
||||
region.locations.append(loc)
|
||||
world.shop_locs.append(loc.name)
|
||||
thug_location_counts[thug_name] = location_count + 1
|
||||
|
||||
|
||||
def create_events(world: "HatInTimeWorld") -> int:
|
||||
|
||||
@@ -381,8 +381,8 @@ def set_moderate_rules(world: "HatInTimeWorld"):
|
||||
lambda state: can_use_hat(state, world, HatType.ICE), "or")
|
||||
|
||||
# Moderate: Clock Tower Chest + Ruined Tower with nothing
|
||||
add_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True)
|
||||
add_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), lambda state: True)
|
||||
set_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True)
|
||||
set_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), lambda state: True)
|
||||
|
||||
# Moderate: enter and clear The Subcon Well without Hookshot and without hitting the bell
|
||||
for loc in world.multiworld.get_region("The Subcon Well", world.player).locations:
|
||||
@@ -432,8 +432,8 @@ def set_moderate_rules(world: "HatInTimeWorld"):
|
||||
|
||||
if world.is_dlc1():
|
||||
# Moderate: clear Rock the Boat without Ice Hat
|
||||
add_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), lambda state: True)
|
||||
add_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), lambda state: True)
|
||||
set_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), lambda state: True)
|
||||
set_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), lambda state: True)
|
||||
|
||||
# Moderate: clear Deep Sea without Ice Hat
|
||||
set_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player),
|
||||
@@ -855,6 +855,9 @@ def set_rift_rules(world: "HatInTimeWorld", regions: Dict[str, Region]):
|
||||
|
||||
for entrance in regions["Time Rift - Alpine Skyline"].entrances:
|
||||
add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon"))
|
||||
if entrance.parent_region.name == "Alpine Free Roam":
|
||||
add_rule(entrance,
|
||||
lambda state: can_use_hookshot(state, world) and can_hit(state, world, umbrella_only=True))
|
||||
|
||||
if world.is_dlc1():
|
||||
for entrance in regions["Time Rift - Balcony"].entrances:
|
||||
@@ -933,6 +936,9 @@ def set_default_rift_rules(world: "HatInTimeWorld"):
|
||||
|
||||
for entrance in world.multiworld.get_region("Time Rift - Alpine Skyline", world.player).entrances:
|
||||
add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon"))
|
||||
if entrance.parent_region.name == "Alpine Free Roam":
|
||||
add_rule(entrance,
|
||||
lambda state: can_use_hookshot(state, world) and can_hit(state, world, umbrella_only=True))
|
||||
|
||||
if world.is_dlc1():
|
||||
for entrance in world.multiworld.get_region("Time Rift - Balcony", world.player).entrances:
|
||||
|
||||
@@ -248,7 +248,7 @@ def fill_dungeons_restrictive(multiworld: MultiWorld):
|
||||
pass
|
||||
for item in pre_fill_items:
|
||||
multiworld.worlds[item.player].collect(all_state_base, item)
|
||||
all_state_base.sweep_for_events()
|
||||
all_state_base.sweep_for_advancements()
|
||||
|
||||
# Remove completion condition so that minimal-accessibility worlds place keys properly
|
||||
for player in {item.player for item in in_dungeon_items}:
|
||||
@@ -262,8 +262,8 @@ def fill_dungeons_restrictive(multiworld: MultiWorld):
|
||||
all_state_base.remove(item_factory(key_data[3], multiworld.worlds[player]))
|
||||
loc = multiworld.get_location(key_loc, player)
|
||||
|
||||
if loc in all_state_base.events:
|
||||
all_state_base.events.remove(loc)
|
||||
if loc in all_state_base.advancements:
|
||||
all_state_base.advancements.remove(loc)
|
||||
fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, lock=True, allow_excluded=True,
|
||||
name="LttP Dungeon Items")
|
||||
|
||||
|
||||
@@ -682,7 +682,7 @@ def get_pool_core(world, player: int):
|
||||
if 'triforce_hunt' in goal:
|
||||
|
||||
if world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_extra:
|
||||
treasure_hunt_total = (world.triforce_pieces_available[player].value
|
||||
treasure_hunt_total = (world.triforce_pieces_required[player].value
|
||||
+ world.triforce_pieces_extra[player].value)
|
||||
elif world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_percentage:
|
||||
percentage = float(world.triforce_pieces_percentage[player].value) / 100
|
||||
|
||||
@@ -728,7 +728,7 @@ class ALttPPlandoConnections(PlandoConnections):
|
||||
entrances = set([connection[0] for connection in (
|
||||
*default_connections, *default_dungeon_connections, *inverted_default_connections,
|
||||
*inverted_default_dungeon_connections)])
|
||||
exits = set([connection[1] for connection in (
|
||||
exits = set([connection[0] for connection in (
|
||||
*default_connections, *default_dungeon_connections, *inverted_default_connections,
|
||||
*inverted_default_dungeon_connections)])
|
||||
|
||||
|
||||
@@ -412,7 +412,7 @@ def global_rules(multiworld: MultiWorld, player: int):
|
||||
lambda state: ((state._lttp_has_key('Small Key (Thieves Town)', player, 3)) or (location_item_name(state, 'Thieves\' Town - Big Chest', player) == ("Small Key (Thieves Town)", player)) and state._lttp_has_key('Small Key (Thieves Town)', player, 2)) and state.has('Hammer', player))
|
||||
set_rule(multiworld.get_location('Thieves\' Town - Blind\'s Cell', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
|
||||
if multiworld.accessibility[player] != 'locations' and not multiworld.key_drop_shuffle[player]:
|
||||
if multiworld.accessibility[player] != 'full' and not multiworld.key_drop_shuffle[player]:
|
||||
set_always_allow(multiworld.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player)
|
||||
set_rule(multiworld.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3))
|
||||
set_rule(multiworld.get_location('Thieves\' Town - Spike Switch Pot Key', player),
|
||||
@@ -547,7 +547,7 @@ def global_rules(multiworld: MultiWorld, player: int):
|
||||
location_item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 6)))
|
||||
|
||||
# this seemed to be causing generation failure, disable for now
|
||||
# if world.accessibility[player] != 'locations':
|
||||
# if world.accessibility[player] != 'full':
|
||||
# set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state._lttp_has_key('Small Key (Ganons Tower)', player, 7) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player))
|
||||
|
||||
# It is possible to need more than 6 keys to get through this entrance if you spend keys elsewhere. We reflect this in the chest requirements.
|
||||
|
||||
@@ -356,6 +356,8 @@ class ALTTPWorld(World):
|
||||
self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group]
|
||||
if option == "original_dungeon":
|
||||
self.dungeon_specific_item_names |= self.item_name_groups[option.item_name_group]
|
||||
else:
|
||||
self.options.local_items.value |= self.dungeon_local_item_names
|
||||
|
||||
self.difficulty_requirements = difficulties[multiworld.item_pool[player].current_key]
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
## Configuration
|
||||
|
||||
1. Plando features have to be enabled first, before they can be used (opt-in).
|
||||
2. To do so, go to your installation directory (Windows default: `C:\ProgramData\Archipelago`), then open the host.yaml
|
||||
1. All plando options are enabled by default, except for "items plando" which has to be enabled before it can be used (opt-in).
|
||||
2. To enable it, go to your installation directory (Windows default: `C:\ProgramData\Archipelago`), then open the host.yaml
|
||||
file with a text editor.
|
||||
3. In it, you're looking for the option key `plando_options`. To enable all plando modules you can set the value
|
||||
to `bosses, items, texts, connections`
|
||||
@@ -66,6 +66,7 @@ boss_shuffle:
|
||||
- ignored if only one world is generated
|
||||
- can be a number, to target that slot in the multiworld
|
||||
- can be a name, to target that player's world
|
||||
- can be a list of names, to target those players' worlds
|
||||
- can be true, to target any other player's world
|
||||
- can be false, to target own world and is the default
|
||||
- can be null, to target a random world
|
||||
@@ -132,17 +133,15 @@ plando_items:
|
||||
|
||||
### Texts
|
||||
|
||||
- This module is disabled by default.
|
||||
- Has the options `text`, `at`, and `percentage`
|
||||
- All of these options support subweights
|
||||
- percentage is the percentage chance for this text to be placed, can be omitted entirely for 100%
|
||||
- text is the text to be placed.
|
||||
- can be weighted.
|
||||
- `\n` is a newline.
|
||||
- `@` is the entered player's name.
|
||||
- Warning: Text Mapper does not support full unicode.
|
||||
- [Alphabet](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Text.py#L758)
|
||||
- at is the location within the game to attach the text to.
|
||||
- can be weighted.
|
||||
- [List of targets](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Text.py#L1499)
|
||||
|
||||
#### Example
|
||||
@@ -162,7 +161,6 @@ and `uncle_dying_sewer`, then places the text "This is a plando. You've been war
|
||||
|
||||
### Connections
|
||||
|
||||
- This module is disabled by default.
|
||||
- Has the options `percentage`, `entrance`, `exit` and `direction`.
|
||||
- All options support subweights
|
||||
- percentage is the percentage chance for this to be connected, can be omitted entirely for 100%
|
||||
|
||||
@@ -54,7 +54,7 @@ class TestDungeon(LTTPTestBase):
|
||||
|
||||
for item in items:
|
||||
item.classification = ItemClassification.progression
|
||||
state.collect(item, event=True) # event=True prevents running sweep_for_events() and picking up
|
||||
state.sweep_for_events() # key drop keys repeatedly
|
||||
state.collect(item, prevent_sweep=True) # prevent_sweep=True prevents running sweep_for_advancements() and picking up
|
||||
state.sweep_for_advancements() # key drop keys repeatedly
|
||||
|
||||
self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access, f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}")
|
||||
self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access, f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}")
|
||||
|
||||
60
worlds/alttp/test/options/test_dungeon_fill.py
Normal file
60
worlds/alttp/test/options/test_dungeon_fill.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from unittest import TestCase
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from test.general import gen_steps, setup_multiworld
|
||||
from worlds.AutoWorld import call_all
|
||||
from worlds.generic.Rules import locality_rules
|
||||
from ... import ALTTPWorld
|
||||
from ...Options import DungeonItem
|
||||
|
||||
|
||||
class DungeonFillTestBase(TestCase):
|
||||
multiworld: MultiWorld
|
||||
world_1: ALTTPWorld
|
||||
world_2: ALTTPWorld
|
||||
options = (
|
||||
"big_key_shuffle",
|
||||
"small_key_shuffle",
|
||||
"key_drop_shuffle",
|
||||
"compass_shuffle",
|
||||
"map_shuffle",
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
self.multiworld = setup_multiworld([ALTTPWorld, ALTTPWorld], ())
|
||||
self.world_1 = self.multiworld.worlds[1]
|
||||
self.world_2 = self.multiworld.worlds[2]
|
||||
|
||||
def generate_with_options(self, option_value: int):
|
||||
for option in self.options:
|
||||
getattr(self.world_1.options, option).value = getattr(self.world_2.options, option).value = option_value
|
||||
|
||||
for step in gen_steps:
|
||||
call_all(self.multiworld, step)
|
||||
# this is where locality rules are set in normal generation which we need to verify this test
|
||||
if step == "set_rules":
|
||||
locality_rules(self.multiworld)
|
||||
|
||||
def test_original_dungeons(self):
|
||||
self.generate_with_options(DungeonItem.option_original_dungeon)
|
||||
for location in self.multiworld.get_filled_locations():
|
||||
with (self.subTest(location=location)):
|
||||
if location.parent_region.dungeon is None:
|
||||
self.assertIs(location.item.dungeon, None)
|
||||
else:
|
||||
self.assertEqual(location.player, location.item.player,
|
||||
f"{location.item} does not belong to {location}'s player")
|
||||
if location.item.dungeon is None:
|
||||
continue
|
||||
self.assertIs(location.item.dungeon, location.parent_region.dungeon,
|
||||
f"{location.item} was not placed in its original dungeon.")
|
||||
|
||||
def test_own_dungeons(self):
|
||||
self.generate_with_options(DungeonItem.option_own_dungeons)
|
||||
for location in self.multiworld.get_filled_locations():
|
||||
with self.subTest(location=location):
|
||||
if location.parent_region.dungeon is None:
|
||||
self.assertIs(location.item.dungeon, None)
|
||||
else:
|
||||
self.assertEqual(location.player, location.item.player,
|
||||
f"{location.item} does not belong to {location}'s player")
|
||||
@@ -4,7 +4,7 @@ from BaseClasses import Tutorial
|
||||
from ..AutoWorld import WebWorld, World
|
||||
|
||||
class AP_SudokuWebWorld(WebWorld):
|
||||
options_page = "games/Sudoku/info/en"
|
||||
options_page = False
|
||||
theme = 'partyTime'
|
||||
|
||||
setup_en = Tutorial(
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
# APSudoku Setup Guide
|
||||
|
||||
## Required Software
|
||||
- [APSudoku](https://github.com/EmilyV99/APSudoku)
|
||||
- Windows (most tested on Win10)
|
||||
- Other platforms might be able to build from source themselves; and may be included in the future.
|
||||
- [APSudoku](https://github.com/APSudoku/APSudoku)
|
||||
|
||||
## General Concept
|
||||
|
||||
@@ -13,25 +11,33 @@ Does not need to be added at the start of a seed, as it does not create any slot
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
Go to the latest release from the [APSudoku Releases page](https://github.com/EmilyV99/APSudoku/releases). Download and extract the `APSudoku.zip` file.
|
||||
Go to the latest release from the [APSudoku Releases page](https://github.com/APSudoku/APSudoku/releases/latest). Download and extract the appropriate file for your platform.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
1. Run APSudoku.exe
|
||||
2. Under the 'Archipelago' tab at the top-right:
|
||||
- Enter the server url & port number
|
||||
1. Run the APSudoku executable.
|
||||
2. Under `Settings` → `Connection` at the top-right:
|
||||
- Enter the server address and port number
|
||||
- Enter the name of the slot you wish to connect to
|
||||
- Enter the room password (optional)
|
||||
- Select DeathLink related settings (optional)
|
||||
- Press connect
|
||||
3. Go back to the 'Sudoku' tab
|
||||
- Click the various '?' buttons for information on how to play / control
|
||||
4. Choose puzzle difficulty
|
||||
5. Try to solve the Sudoku. Click 'Check' when done.
|
||||
- Press `Connect`
|
||||
4. Under the `Sudoku` tab
|
||||
- Choose puzzle difficulty
|
||||
- Click `Start` to generate a puzzle
|
||||
5. Try to solve the Sudoku. Click `Check` when done
|
||||
- A correct solution rewards you with 1 hint for a location in the world you are connected to
|
||||
- An incorrect solution has no penalty, unless DeathLink is enabled (see below)
|
||||
|
||||
Info:
|
||||
- You can set various settings under `Settings` → `Sudoku`, and can change the colors used under `Settings` → `Theme`.
|
||||
- While connected, you can view the `Console` and `Hints` tabs for standard TextClient-like features
|
||||
- You can also use the `Tracking` tab to view either a basic tracker or a valid [GodotAP tracker pack](https://github.com/EmilyV99/GodotAP/blob/main/tracker_packs/GET_PACKS.md)
|
||||
- While connected, the number of "unhinted" locations for your slot is shown in the upper-left of the the `Sudoku` tab. (If this reads 0, no further hints can be earned for this slot, as every locations is already hinted)
|
||||
- Click the various `?` buttons for information on controls/how to play
|
||||
## DeathLink Support
|
||||
|
||||
If 'DeathLink' is enabled when you click 'Connect':
|
||||
- Lose a life if you check an incorrect puzzle (not an _incomplete_ puzzle- if any cells are empty, you get off with a warning), or quit a puzzle without solving it (including disconnecting).
|
||||
- Life count customizable (default 0). Dying with 0 lives left kills linked players AND resets your puzzle.
|
||||
If `DeathLink` is enabled when you click `Connect`:
|
||||
- Lose a life if you check an incorrect puzzle (not an _incomplete_ puzzle- if any cells are empty, you get off with a warning), or if you quit a puzzle without solving it (including disconnecting).
|
||||
- Your life count is customizable (default 0). Dying with 0 lives left kills linked players AND resets your puzzle.
|
||||
- On receiving a DeathLink from another player, your puzzle resets.
|
||||
|
||||
19
worlds/blasphemous/ExtractorConfig.json
Normal file
19
worlds/blasphemous/ExtractorConfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"type": "WorldDefinition",
|
||||
"configuration": "./output/StringWorldDefinition.json",
|
||||
"emptyRegionsToKeep": [
|
||||
"D17Z01S01",
|
||||
"D01Z02S01",
|
||||
"D02Z03S09",
|
||||
"D03Z03S11",
|
||||
"D04Z03S01",
|
||||
"D06Z01S09",
|
||||
"D20Z02S09",
|
||||
"D09Z01S09[Cell24]",
|
||||
"D09Z01S08[Cell7]",
|
||||
"D09Z01S08[Cell18]",
|
||||
"D09BZ01S01[Cell24]",
|
||||
"D09BZ01S01[Cell17]",
|
||||
"D09BZ01S01[Cell19]"
|
||||
]
|
||||
}
|
||||
@@ -637,52 +637,35 @@ item_table: List[ItemDict] = [
|
||||
'classification': ItemClassification.filler}
|
||||
]
|
||||
|
||||
event_table: Dict[str, str] = {
|
||||
"OpenedDCGateW": "D01Z05S24",
|
||||
"OpenedDCGateE": "D01Z05S12",
|
||||
"OpenedDCLadder": "D01Z05S20",
|
||||
"OpenedWOTWCave": "D02Z01S06",
|
||||
"RodeGOTPElevator": "D02Z02S11",
|
||||
"OpenedConventLadder": "D02Z03S11",
|
||||
"BrokeJondoBellW": "D03Z02S09",
|
||||
"BrokeJondoBellE": "D03Z02S05",
|
||||
"OpenedMOMLadder": "D04Z02S06",
|
||||
"OpenedTSCGate": "D05Z02S11",
|
||||
"OpenedARLadder": "D06Z01S23",
|
||||
"BrokeBOTTCStatue": "D08Z01S02",
|
||||
"OpenedWOTHPGate": "D09Z01S05",
|
||||
"OpenedBOTSSLadder": "D17Z01S04"
|
||||
}
|
||||
|
||||
group_table: Dict[str, Set[str]] = {
|
||||
"wounds" : ["Holy Wound of Attrition",
|
||||
"wounds" : {"Holy Wound of Attrition",
|
||||
"Holy Wound of Contrition",
|
||||
"Holy Wound of Compunction"],
|
||||
"Holy Wound of Compunction"},
|
||||
|
||||
"masks" : ["Deformed Mask of Orestes",
|
||||
"masks" : {"Deformed Mask of Orestes",
|
||||
"Mirrored Mask of Dolphos",
|
||||
"Embossed Mask of Crescente"],
|
||||
"Embossed Mask of Crescente"},
|
||||
|
||||
"marks" : ["Mark of the First Refuge",
|
||||
"marks" : {"Mark of the First Refuge",
|
||||
"Mark of the Second Refuge",
|
||||
"Mark of the Third Refuge"],
|
||||
"Mark of the Third Refuge"},
|
||||
|
||||
"tirso" : ["Bouquet of Rosemary",
|
||||
"tirso" : {"Bouquet of Rosemary",
|
||||
"Incense Garlic",
|
||||
"Olive Seeds",
|
||||
"Dried Clove",
|
||||
"Sooty Garlic",
|
||||
"Bouquet of Thyme"],
|
||||
"Bouquet of Thyme"},
|
||||
|
||||
"tentudia": ["Tentudia's Carnal Remains",
|
||||
"tentudia": {"Tentudia's Carnal Remains",
|
||||
"Remains of Tentudia's Hair",
|
||||
"Tentudia's Skeletal Remains"],
|
||||
"Tentudia's Skeletal Remains"},
|
||||
|
||||
"egg" : ["Melted Golden Coins",
|
||||
"egg" : {"Melted Golden Coins",
|
||||
"Torn Bridal Ribbon",
|
||||
"Black Grieving Veil"],
|
||||
"Black Grieving Veil"},
|
||||
|
||||
"bones" : ["Parietal bone of Lasser, the Inquisitor",
|
||||
"bones" : {"Parietal bone of Lasser, the Inquisitor",
|
||||
"Jaw of Ashgan, the Inquisitor",
|
||||
"Cervical vertebra of Zicher, the Brewmaster",
|
||||
"Clavicle of Dalhuisen, the Schoolchild",
|
||||
@@ -725,14 +708,14 @@ group_table: Dict[str, Set[str]] = {
|
||||
"Scaphoid of Fierce, the Leper",
|
||||
"Anklebone of Weston, the Pilgrim",
|
||||
"Calcaneum of Persian, the Bandit",
|
||||
"Navicular of Kahnnyhoo, the Murderer"],
|
||||
"Navicular of Kahnnyhoo, the Murderer"},
|
||||
|
||||
"power" : ["Life Upgrade",
|
||||
"power" : {"Life Upgrade",
|
||||
"Fervour Upgrade",
|
||||
"Empty Bile Vessel",
|
||||
"Quicksilver"],
|
||||
"Quicksilver"},
|
||||
|
||||
"prayer" : ["Seguiriya to your Eyes like Stars",
|
||||
"prayer" : {"Seguiriya to your Eyes like Stars",
|
||||
"Debla of the Lights",
|
||||
"Saeta Dolorosa",
|
||||
"Campanillero to the Sons of the Aurora",
|
||||
@@ -746,10 +729,17 @@ group_table: Dict[str, Set[str]] = {
|
||||
"Romance to the Crimson Mist",
|
||||
"Zambra to the Resplendent Crown",
|
||||
"Cantina of the Blue Rose",
|
||||
"Mirabras of the Return to Port"]
|
||||
"Mirabras of the Return to Port"},
|
||||
|
||||
"toe" : {"Little Toe made of Limestone",
|
||||
"Big Toe made of Limestone",
|
||||
"Fourth Toe made of Limestone"},
|
||||
|
||||
"eye" : {"Severed Right Eye of the Traitor",
|
||||
"Broken Left Eye of the Traitor"}
|
||||
}
|
||||
|
||||
tears_set: Set[str] = [
|
||||
tears_list: List[str] = [
|
||||
"Tears of Atonement (500)",
|
||||
"Tears of Atonement (625)",
|
||||
"Tears of Atonement (750)",
|
||||
@@ -772,16 +762,16 @@ tears_set: Set[str] = [
|
||||
"Tears of Atonement (30000)"
|
||||
]
|
||||
|
||||
reliquary_set: Set[str] = [
|
||||
reliquary_set: Set[str] = {
|
||||
"Reliquary of the Fervent Heart",
|
||||
"Reliquary of the Suffering Heart",
|
||||
"Reliquary of the Sorrowful Heart"
|
||||
]
|
||||
}
|
||||
|
||||
skill_set: Set[str] = [
|
||||
skill_set: Set[str] = {
|
||||
"Combo Skill",
|
||||
"Charged Skill",
|
||||
"Ranged Skill",
|
||||
"Dive Skill",
|
||||
"Lunge Skill"
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool
|
||||
from dataclasses import dataclass
|
||||
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions, OptionGroup
|
||||
import random
|
||||
|
||||
|
||||
@@ -20,23 +21,30 @@ class ChoiceIsRandom(Choice):
|
||||
|
||||
|
||||
class PrieDieuWarp(DefaultOnToggle):
|
||||
"""Automatically unlocks the ability to warp between Prie Dieu shrines."""
|
||||
"""
|
||||
Automatically unlocks the ability to warp between Prie Dieu shrines.
|
||||
"""
|
||||
display_name = "Unlock Fast Travel"
|
||||
|
||||
|
||||
class SkipCutscenes(DefaultOnToggle):
|
||||
"""Automatically skips most cutscenes."""
|
||||
"""
|
||||
Automatically skips most cutscenes.
|
||||
"""
|
||||
display_name = "Auto Skip Cutscenes"
|
||||
|
||||
|
||||
class CorpseHints(DefaultOnToggle):
|
||||
"""Changes the 34 corpses in game to give various hints about item locations."""
|
||||
"""
|
||||
Changes the 34 corpses in game to give various hints about item locations.
|
||||
"""
|
||||
display_name = "Corpse Hints"
|
||||
|
||||
|
||||
class Difficulty(Choice):
|
||||
"""Adjusts the overall difficulty of the randomizer, including upgrades required to defeat bosses
|
||||
and advanced movement tricks or glitches."""
|
||||
"""
|
||||
Adjusts the overall difficulty of the randomizer, including upgrades required to defeat bosses and advanced movement tricks or glitches.
|
||||
"""
|
||||
display_name = "Difficulty"
|
||||
option_easy = 0
|
||||
option_normal = 1
|
||||
@@ -45,15 +53,18 @@ class Difficulty(Choice):
|
||||
|
||||
|
||||
class Penitence(Toggle):
|
||||
"""Allows one of the three Penitences to be chosen at the beginning of the game."""
|
||||
"""
|
||||
Allows one of the three Penitences to be chosen at the beginning of the game.
|
||||
"""
|
||||
display_name = "Penitence"
|
||||
|
||||
|
||||
class StartingLocation(ChoiceIsRandom):
|
||||
"""Choose where to start the randomizer. Note that some starting locations cannot be chosen with certain
|
||||
other options.
|
||||
Specifically, Brotherhood and Mourning And Havoc cannot be chosen if Shuffle Dash is enabled, and Grievance Ascends
|
||||
cannot be chosen if Shuffle Wall Climb is enabled."""
|
||||
"""
|
||||
Choose where to start the randomizer. Note that some starting locations cannot be chosen with certain other options.
|
||||
|
||||
Specifically, Brotherhood and Mourning And Havoc cannot be chosen if Shuffle Dash is enabled, and Grievance Ascends cannot be chosen if Shuffle Wall Climb is enabled.
|
||||
"""
|
||||
display_name = "Starting Location"
|
||||
option_brotherhood = 0
|
||||
option_albero = 1
|
||||
@@ -66,10 +77,15 @@ class StartingLocation(ChoiceIsRandom):
|
||||
|
||||
|
||||
class Ending(Choice):
|
||||
"""Choose which ending is required to complete the game.
|
||||
"""
|
||||
Choose which ending is required to complete the game.
|
||||
|
||||
Talking to Tirso in Albero will tell you the selected ending for the current game.
|
||||
|
||||
Ending A: Collect all thorn upgrades.
|
||||
Ending C: Collect all thorn upgrades and the Holy Wound of Abnegation."""
|
||||
|
||||
Ending C: Collect all thorn upgrades and the Holy Wound of Abnegation.
|
||||
"""
|
||||
display_name = "Ending"
|
||||
option_any_ending = 0
|
||||
option_ending_a = 1
|
||||
@@ -78,14 +94,18 @@ class Ending(Choice):
|
||||
|
||||
|
||||
class SkipLongQuests(Toggle):
|
||||
"""Ensures that the rewards for long quests will be filler items.
|
||||
Affected locations: \"Albero: Donate 50000 Tears\", \"Ossuary: 11th reward\", \"AtTotS: Miriam's gift\",
|
||||
\"TSC: Jocinero's final reward\""""
|
||||
"""
|
||||
Ensures that the rewards for long quests will be filler items.
|
||||
|
||||
Affected locations: "Albero: Donate 50000 Tears", "Ossuary: 11th reward", "AtTotS: Miriam's gift", "TSC: Jocinero's final reward"
|
||||
"""
|
||||
display_name = "Skip Long Quests"
|
||||
|
||||
|
||||
class ThornShuffle(Choice):
|
||||
"""Shuffles the Thorn given by Deogracias and all Thorn upgrades into the item pool."""
|
||||
"""
|
||||
Shuffles the Thorn given by Deogracias and all Thorn upgrades into the item pool.
|
||||
"""
|
||||
display_name = "Shuffle Thorn"
|
||||
option_anywhere = 0
|
||||
option_local_only = 1
|
||||
@@ -94,50 +114,68 @@ class ThornShuffle(Choice):
|
||||
|
||||
|
||||
class DashShuffle(Toggle):
|
||||
"""Turns the ability to dash into an item that must be found in the multiworld."""
|
||||
"""
|
||||
Turns the ability to dash into an item that must be found in the multiworld.
|
||||
"""
|
||||
display_name = "Shuffle Dash"
|
||||
|
||||
|
||||
class WallClimbShuffle(Toggle):
|
||||
"""Turns the ability to climb walls with your sword into an item that must be found in the multiworld."""
|
||||
"""
|
||||
Turns the ability to climb walls with your sword into an item that must be found in the multiworld.
|
||||
"""
|
||||
display_name = "Shuffle Wall Climb"
|
||||
|
||||
|
||||
class ReliquaryShuffle(DefaultOnToggle):
|
||||
"""Adds the True Torment exclusive Reliquary rosary beads into the item pool."""
|
||||
"""
|
||||
Adds the True Torment exclusive Reliquary rosary beads into the item pool.
|
||||
"""
|
||||
display_name = "Shuffle Penitence Rewards"
|
||||
|
||||
|
||||
class CustomItem1(Toggle):
|
||||
"""Adds the custom relic Boots of Pleading into the item pool, which grants the ability to fall onto spikes
|
||||
and survive.
|
||||
Must have the \"Blasphemous-Boots-of-Pleading\" mod installed to connect to a multiworld."""
|
||||
"""
|
||||
Adds the custom relic Boots of Pleading into the item pool, which grants the ability to fall onto spikes and survive.
|
||||
|
||||
Must have the "Boots of Pleading" mod installed to connect to a multiworld.
|
||||
"""
|
||||
display_name = "Boots of Pleading"
|
||||
|
||||
|
||||
class CustomItem2(Toggle):
|
||||
"""Adds the custom relic Purified Hand of the Nun into the item pool, which grants the ability to jump
|
||||
a second time in mid-air.
|
||||
Must have the \"Blasphemous-Double-Jump\" mod installed to connect to a multiworld."""
|
||||
"""
|
||||
Adds the custom relic Purified Hand of the Nun into the item pool, which grants the ability to jump a second time in mid-air.
|
||||
|
||||
Must have the "Double Jump" mod installed to connect to a multiworld.
|
||||
"""
|
||||
display_name = "Purified Hand of the Nun"
|
||||
|
||||
|
||||
class StartWheel(Toggle):
|
||||
"""Changes the beginning gift to The Young Mason's Wheel."""
|
||||
"""
|
||||
Changes the beginning gift to The Young Mason's Wheel.
|
||||
"""
|
||||
display_name = "Start with Wheel"
|
||||
|
||||
|
||||
class SkillRando(Toggle):
|
||||
"""Randomizes the abilities from the skill tree into the item pool."""
|
||||
"""
|
||||
Randomizes the abilities from the skill tree into the item pool.
|
||||
"""
|
||||
display_name = "Skill Randomizer"
|
||||
|
||||
|
||||
class EnemyRando(Choice):
|
||||
"""Randomizes the enemies that appear in each room.
|
||||
Shuffled: Enemies will be shuffled amongst each other, but can only appear as many times as they do in
|
||||
a standard game.
|
||||
"""
|
||||
Randomizes the enemies that appear in each room.
|
||||
|
||||
Shuffled: Enemies will be shuffled amongst each other, but can only appear as many times as they do in a standard game.
|
||||
|
||||
Randomized: Every enemy is completely random, and can appear any number of times.
|
||||
Some enemies will never be randomized."""
|
||||
|
||||
Some enemies will never be randomized.
|
||||
"""
|
||||
display_name = "Enemy Randomizer"
|
||||
option_disabled = 0
|
||||
option_shuffled = 1
|
||||
@@ -146,43 +184,75 @@ class EnemyRando(Choice):
|
||||
|
||||
|
||||
class EnemyGroups(DefaultOnToggle):
|
||||
"""Randomized enemies will chosen from sets of specific groups.
|
||||
"""
|
||||
Randomized enemies will be chosen from sets of specific groups.
|
||||
|
||||
(Weak, normal, large, flying)
|
||||
Has no effect if Enemy Randomizer is disabled."""
|
||||
|
||||
Has no effect if Enemy Randomizer is disabled.
|
||||
"""
|
||||
display_name = "Enemy Groups"
|
||||
|
||||
|
||||
class EnemyScaling(DefaultOnToggle):
|
||||
"""Randomized enemies will have their stats increased or decreased depending on the area they appear in.
|
||||
Has no effect if Enemy Randomizer is disabled."""
|
||||
"""
|
||||
Randomized enemies will have their stats increased or decreased depending on the area they appear in.
|
||||
|
||||
Has no effect if Enemy Randomizer is disabled.
|
||||
"""
|
||||
display_name = "Enemy Scaling"
|
||||
|
||||
|
||||
class BlasphemousDeathLink(DeathLink):
|
||||
"""When you die, everyone dies. The reverse is also true.
|
||||
Note that Guilt Fragments will not appear when killed by Death Link."""
|
||||
"""
|
||||
When you die, everyone dies. The reverse is also true.
|
||||
|
||||
Note that Guilt Fragments will not appear when killed by Death Link.
|
||||
"""
|
||||
|
||||
|
||||
blasphemous_options = {
|
||||
"prie_dieu_warp": PrieDieuWarp,
|
||||
"skip_cutscenes": SkipCutscenes,
|
||||
"corpse_hints": CorpseHints,
|
||||
"difficulty": Difficulty,
|
||||
"penitence": Penitence,
|
||||
"starting_location": StartingLocation,
|
||||
"ending": Ending,
|
||||
"skip_long_quests": SkipLongQuests,
|
||||
"thorn_shuffle" : ThornShuffle,
|
||||
"dash_shuffle": DashShuffle,
|
||||
"wall_climb_shuffle": WallClimbShuffle,
|
||||
"reliquary_shuffle": ReliquaryShuffle,
|
||||
"boots_of_pleading": CustomItem1,
|
||||
"purified_hand": CustomItem2,
|
||||
"start_wheel": StartWheel,
|
||||
"skill_randomizer": SkillRando,
|
||||
"enemy_randomizer": EnemyRando,
|
||||
"enemy_groups": EnemyGroups,
|
||||
"enemy_scaling": EnemyScaling,
|
||||
"death_link": BlasphemousDeathLink,
|
||||
"start_inventory": StartInventoryPool
|
||||
}
|
||||
@dataclass
|
||||
class BlasphemousOptions(PerGameCommonOptions):
|
||||
prie_dieu_warp: PrieDieuWarp
|
||||
skip_cutscenes: SkipCutscenes
|
||||
corpse_hints: CorpseHints
|
||||
difficulty: Difficulty
|
||||
penitence: Penitence
|
||||
starting_location: StartingLocation
|
||||
ending: Ending
|
||||
skip_long_quests: SkipLongQuests
|
||||
thorn_shuffle: ThornShuffle
|
||||
dash_shuffle: DashShuffle
|
||||
wall_climb_shuffle: WallClimbShuffle
|
||||
reliquary_shuffle: ReliquaryShuffle
|
||||
boots_of_pleading: CustomItem1
|
||||
purified_hand: CustomItem2
|
||||
start_wheel: StartWheel
|
||||
skill_randomizer: SkillRando
|
||||
enemy_randomizer: EnemyRando
|
||||
enemy_groups: EnemyGroups
|
||||
enemy_scaling: EnemyScaling
|
||||
death_link: BlasphemousDeathLink
|
||||
|
||||
|
||||
blas_option_groups = [
|
||||
OptionGroup("Quality of Life", [
|
||||
PrieDieuWarp,
|
||||
SkipCutscenes,
|
||||
CorpseHints,
|
||||
SkipLongQuests,
|
||||
StartWheel
|
||||
]),
|
||||
OptionGroup("Moveset", [
|
||||
DashShuffle,
|
||||
WallClimbShuffle,
|
||||
SkillRando,
|
||||
CustomItem1,
|
||||
CustomItem2
|
||||
]),
|
||||
OptionGroup("Enemy Randomizer", [
|
||||
EnemyRando,
|
||||
EnemyGroups,
|
||||
EnemyScaling
|
||||
])
|
||||
]
|
||||
|
||||
582
worlds/blasphemous/Preprocessor.py
Normal file
582
worlds/blasphemous/Preprocessor.py
Normal file
@@ -0,0 +1,582 @@
|
||||
# Preprocessor to convert Blasphemous Randomizer logic into a StringWorldDefinition for use with APHKLogicExtractor
|
||||
# https://github.com/BrandenEK/Blasphemous.Randomizer
|
||||
# https://github.com/ArchipelagoMW-HollowKnight/APHKLogicExtractor
|
||||
|
||||
|
||||
import json, requests, argparse
|
||||
from typing import List, Dict, Any
|
||||
|
||||
|
||||
def load_resource_local(file: str) -> List[Dict[str, Any]]:
|
||||
print(f"Reading from {file}")
|
||||
loaded = []
|
||||
with open(file, encoding="utf-8") as f:
|
||||
loaded = read_json(f.readlines())
|
||||
f.close()
|
||||
|
||||
return loaded
|
||||
|
||||
|
||||
def load_resource_from_web(url: str) -> List[Dict[str, Any]]:
|
||||
req = requests.get(url, timeout=1)
|
||||
print(f"Reading from {url}")
|
||||
req.encoding = "utf-8"
|
||||
lines: List[str] = []
|
||||
for line in req.text.splitlines():
|
||||
while "\t" in line:
|
||||
line = line[1::]
|
||||
if line != "":
|
||||
lines.append(line)
|
||||
return read_json(lines)
|
||||
|
||||
|
||||
def read_json(lines: List[str]) -> List[Dict[str, Any]]:
|
||||
loaded = []
|
||||
creating_object: bool = False
|
||||
obj: str = ""
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if "{" in stripped:
|
||||
creating_object = True
|
||||
obj += stripped
|
||||
continue
|
||||
elif "}," in stripped or "}" in stripped and "]" in lines[lines.index(line)+1]:
|
||||
creating_object = False
|
||||
obj += "}"
|
||||
#print(f"obj = {obj}")
|
||||
loaded.append(json.loads(obj))
|
||||
obj = ""
|
||||
continue
|
||||
|
||||
if not creating_object:
|
||||
continue
|
||||
else:
|
||||
try:
|
||||
if "}," in lines[lines.index(line)+1] and stripped[-1] == ",":
|
||||
obj += stripped[:-1]
|
||||
else:
|
||||
obj += stripped
|
||||
except IndexError:
|
||||
obj += stripped
|
||||
|
||||
return loaded
|
||||
|
||||
|
||||
def get_room_from_door(door: str) -> str:
|
||||
return door[:door.find("[")]
|
||||
|
||||
|
||||
def preprocess_logic(is_door: bool, id: str, logic: str) -> str:
|
||||
if id in logic and not is_door:
|
||||
index: int = logic.find(id)
|
||||
logic = logic[:index] + logic[index+len(id)+4:]
|
||||
|
||||
while ">=" in logic:
|
||||
index: int = logic.find(">=")
|
||||
logic = logic[:index-1] + logic[index+3:]
|
||||
|
||||
while ">" in logic:
|
||||
index: int = logic.find(">")
|
||||
count = int(logic[index+2])
|
||||
count += 1
|
||||
logic = logic[:index-1] + str(count) + logic[index+3:]
|
||||
|
||||
while "<=" in logic:
|
||||
index: int = logic.find("<=")
|
||||
logic = logic[:index-1] + logic[index+3:]
|
||||
|
||||
while "<" in logic:
|
||||
index: int = logic.find("<")
|
||||
count = int(logic[index+2])
|
||||
count += 1
|
||||
logic = logic[:index-1] + str(count) + logic[index+3:]
|
||||
|
||||
#print(logic)
|
||||
return logic
|
||||
|
||||
|
||||
def build_logic_conditions(logic: str) -> List[List[str]]:
|
||||
all_conditions: List[List[str]] = []
|
||||
|
||||
parts = logic.split()
|
||||
sub_part: str = ""
|
||||
current_index: int = 0
|
||||
parens: int = -1
|
||||
current_condition: List[str] = []
|
||||
parens_conditions: List[List[List[str]]] = []
|
||||
|
||||
for index, part in enumerate(parts):
|
||||
#print(current_index, index, parens, part)
|
||||
|
||||
# skip parts that have already been handled
|
||||
if index < current_index:
|
||||
continue
|
||||
|
||||
# break loop if reached final part
|
||||
try:
|
||||
parts[index+1]
|
||||
except IndexError:
|
||||
#print("INDEXERROR", part)
|
||||
if parens < 0:
|
||||
current_condition.append(part)
|
||||
if len(parens_conditions) > 0:
|
||||
for i in parens_conditions:
|
||||
for j in i:
|
||||
all_conditions.append(j + current_condition)
|
||||
else:
|
||||
all_conditions.append(current_condition)
|
||||
break
|
||||
|
||||
#print(current_condition, parens, sub_part)
|
||||
|
||||
# prepare for subcondition
|
||||
if "(" in part:
|
||||
# keep track of nested parentheses
|
||||
if parens == -1:
|
||||
parens = 0
|
||||
for char in part:
|
||||
if char == "(":
|
||||
parens += 1
|
||||
|
||||
# add to sub part
|
||||
if sub_part == "":
|
||||
sub_part = part
|
||||
else:
|
||||
sub_part += f" {part}"
|
||||
#if not ")" in part:
|
||||
continue
|
||||
|
||||
# end of subcondition
|
||||
if ")" in part:
|
||||
# read every character in case of multiple closing parentheses
|
||||
for char in part:
|
||||
if char == ")":
|
||||
parens -= 1
|
||||
|
||||
sub_part += f" {part}"
|
||||
|
||||
# if reached end of parentheses, handle subcondition
|
||||
if parens == 0:
|
||||
#print(current_condition, sub_part)
|
||||
parens = -1
|
||||
|
||||
try:
|
||||
parts[index+1]
|
||||
except IndexError:
|
||||
#print("END OF LOGIC")
|
||||
if len(parens_conditions) > 0:
|
||||
parens_conditions.append(build_logic_subconditions(current_condition, sub_part))
|
||||
#print("PARENS:", parens_conditions)
|
||||
|
||||
temp_conditions: List[List[str]] = []
|
||||
|
||||
for i in parens_conditions[0]:
|
||||
for j in parens_conditions[1]:
|
||||
temp_conditions.append(i + j)
|
||||
|
||||
parens_conditions.pop(0)
|
||||
parens_conditions.pop(0)
|
||||
|
||||
while len(parens_conditions) > 0:
|
||||
temp_conditions2 = temp_conditions
|
||||
temp_conditions = []
|
||||
for k in temp_conditions2:
|
||||
for l in parens_conditions[0]:
|
||||
temp_conditions.append(k + l)
|
||||
|
||||
parens_conditions.pop(0)
|
||||
|
||||
#print("TEMP:", remove_duplicates(temp_conditions))
|
||||
all_conditions += temp_conditions
|
||||
else:
|
||||
all_conditions += build_logic_subconditions(current_condition, sub_part)
|
||||
else:
|
||||
#print("NEXT PARTS:", parts[index+1], parts[index+2])
|
||||
if parts[index+1] == "&&":
|
||||
parens_conditions.append(build_logic_subconditions(current_condition, sub_part))
|
||||
#print("PARENS:", parens_conditions)
|
||||
else:
|
||||
if len(parens_conditions) > 0:
|
||||
parens_conditions.append(build_logic_subconditions(current_condition, sub_part))
|
||||
#print("PARENS:", parens_conditions)
|
||||
|
||||
temp_conditions: List[List[str]] = []
|
||||
|
||||
for i in parens_conditions[0]:
|
||||
for j in parens_conditions[1]:
|
||||
temp_conditions.append(i + j)
|
||||
|
||||
parens_conditions.pop(0)
|
||||
parens_conditions.pop(0)
|
||||
|
||||
while len(parens_conditions) > 0:
|
||||
temp_conditions2 = temp_conditions
|
||||
temp_conditions = []
|
||||
for k in temp_conditions2:
|
||||
for l in parens_conditions[0]:
|
||||
temp_conditions.append(k + l)
|
||||
|
||||
parens_conditions.pop(0)
|
||||
|
||||
#print("TEMP:", remove_duplicates(temp_conditions))
|
||||
all_conditions += temp_conditions
|
||||
else:
|
||||
all_conditions += build_logic_subconditions(current_condition, sub_part)
|
||||
|
||||
current_index = index+2
|
||||
|
||||
current_condition = []
|
||||
sub_part = ""
|
||||
|
||||
continue
|
||||
|
||||
# collect all parts until reaching end of parentheses
|
||||
if parens > 0:
|
||||
sub_part += f" {part}"
|
||||
continue
|
||||
|
||||
current_condition.append(part)
|
||||
|
||||
# continue with current condition
|
||||
if parts[index+1] == "&&":
|
||||
current_index = index+2
|
||||
continue
|
||||
|
||||
# add condition to list and start new one
|
||||
elif parts[index+1] == "||":
|
||||
if len(parens_conditions) > 0:
|
||||
for i in parens_conditions:
|
||||
for j in i:
|
||||
all_conditions.append(j + current_condition)
|
||||
parens_conditions = []
|
||||
else:
|
||||
all_conditions.append(current_condition)
|
||||
current_condition = []
|
||||
current_index = index+2
|
||||
continue
|
||||
|
||||
return remove_duplicates(all_conditions)
|
||||
|
||||
|
||||
def build_logic_subconditions(current_condition: List[str], subcondition: str) -> List[List[str]]:
|
||||
#print("STARTED SUBCONDITION", current_condition, subcondition)
|
||||
subconditions = build_logic_conditions(subcondition[1:-1])
|
||||
final_conditions = []
|
||||
|
||||
for condition in subconditions:
|
||||
final_condition = current_condition + condition
|
||||
final_conditions.append(final_condition)
|
||||
|
||||
#print("ENDED SUBCONDITION")
|
||||
#print(final_conditions)
|
||||
return final_conditions
|
||||
|
||||
|
||||
def remove_duplicates(conditions: List[List[str]]) -> List[List[str]]:
|
||||
final_conditions: List[List[str]] = []
|
||||
for condition in conditions:
|
||||
final_conditions.append(list(dict.fromkeys(condition)))
|
||||
|
||||
return final_conditions
|
||||
|
||||
|
||||
def handle_door_visibility(door: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if door.get("visibilityFlags") == None:
|
||||
return door
|
||||
else:
|
||||
flags: List[str] = str(door.get("visibilityFlags")).split(", ")
|
||||
#print(flags)
|
||||
temp_flags: List[str] = []
|
||||
this_door: bool = False
|
||||
#required_doors: str = ""
|
||||
|
||||
if "ThisDoor" in flags:
|
||||
this_door = True
|
||||
|
||||
#if "requiredDoors" in flags:
|
||||
# required_doors: str = " || ".join(door.get("requiredDoors"))
|
||||
|
||||
if "DoubleJump" in flags:
|
||||
temp_flags.append("DoubleJump")
|
||||
|
||||
if "NormalLogic" in flags:
|
||||
temp_flags.append("NormalLogic")
|
||||
|
||||
if "NormalLogicAndDoubleJump" in flags:
|
||||
temp_flags.append("NormalLogicAndDoubleJump")
|
||||
|
||||
if "HardLogic" in flags:
|
||||
temp_flags.append("HardLogic")
|
||||
|
||||
if "HardLogicAndDoubleJump" in flags:
|
||||
temp_flags.append("HardLogicAndDoubleJump")
|
||||
|
||||
if "EnemySkips" in flags:
|
||||
temp_flags.append("EnemySkips")
|
||||
|
||||
if "EnemySkipsAndDoubleJump" in flags:
|
||||
temp_flags.append("EnemySkipsAndDoubleJump")
|
||||
|
||||
# remove duplicates
|
||||
temp_flags = list(dict.fromkeys(temp_flags))
|
||||
|
||||
original_logic: str = door.get("logic")
|
||||
temp_logic: str = ""
|
||||
|
||||
if this_door:
|
||||
temp_logic = door.get("id")
|
||||
|
||||
if temp_flags != []:
|
||||
if temp_logic != "":
|
||||
temp_logic += " || "
|
||||
temp_logic += ' && '.join(temp_flags)
|
||||
|
||||
if temp_logic != "" and original_logic != None:
|
||||
if len(original_logic.split()) == 1:
|
||||
if len(temp_logic.split()) == 1:
|
||||
door["logic"] = f"{temp_logic} && {original_logic}"
|
||||
else:
|
||||
door["logic"] = f"({temp_logic}) && {original_logic}"
|
||||
else:
|
||||
if len(temp_logic.split()) == 1:
|
||||
door["logic"] = f"{temp_logic} && ({original_logic})"
|
||||
else:
|
||||
door["logic"] = f"({temp_logic}) && ({original_logic})"
|
||||
elif temp_logic != "" and original_logic == None:
|
||||
door["logic"] = temp_logic
|
||||
|
||||
return door
|
||||
|
||||
|
||||
def get_state_provider_for_condition(condition: List[str]) -> str:
|
||||
for item in condition:
|
||||
if (item[0] == "D" and item[3] == "Z" and item[6] == "S")\
|
||||
or (item[0] == "D" and item[3] == "B" and item[4] == "Z" and item[7] == "S"):
|
||||
return item
|
||||
return None
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-l', '--local', action="store_true", help="Use local files in the same directory instead of reading resource files from the BrandenEK/Blasphemous-Randomizer repository.")
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
def main(args: argparse.Namespace):
|
||||
doors = []
|
||||
locations = []
|
||||
|
||||
if (args.local):
|
||||
doors = load_resource_local("doors.json")
|
||||
locations = load_resource_local("locations_items.json")
|
||||
|
||||
else:
|
||||
doors = load_resource_from_web("https://raw.githubusercontent.com/BrandenEK/Blasphemous-Randomizer/main/resources/data/Randomizer/doors.json")
|
||||
locations = load_resource_from_web("https://raw.githubusercontent.com/BrandenEK/Blasphemous-Randomizer/main/resources/data/Randomizer/locations_items.json")
|
||||
|
||||
original_connections: Dict[str, str] = {}
|
||||
rooms: Dict[str, List[str]] = {}
|
||||
output: Dict[str, Any] = {}
|
||||
logic_objects: List[Dict[str, Any]] = []
|
||||
|
||||
for door in doors:
|
||||
if door.get("originalDoor") != None:
|
||||
if not door.get("id") in original_connections:
|
||||
original_connections[door.get("id")] = door.get("originalDoor")
|
||||
original_connections[door.get("originalDoor")] = door.get("id")
|
||||
|
||||
room: str = get_room_from_door(door.get("originalDoor"))
|
||||
if not room in rooms.keys():
|
||||
rooms[room] = [door.get("id")]
|
||||
else:
|
||||
rooms[room].append(door.get("id"))
|
||||
|
||||
def flip_doors_in_condition(condition: List[str]) -> List[str]:
|
||||
new_condition = []
|
||||
for item in condition:
|
||||
if item in original_connections:
|
||||
new_condition.append(original_connections[item])
|
||||
else:
|
||||
new_condition.append(item)
|
||||
|
||||
return new_condition
|
||||
|
||||
for room in rooms.keys():
|
||||
obj = {
|
||||
"Name": room,
|
||||
"Logic": [],
|
||||
"Handling": "Default"
|
||||
}
|
||||
|
||||
for door in rooms[room]:
|
||||
logic = {
|
||||
"StateProvider": door,
|
||||
"Conditions": [],
|
||||
"StateModifiers": []
|
||||
}
|
||||
obj["Logic"].append(logic)
|
||||
|
||||
logic_objects.append(obj)
|
||||
|
||||
for door in doors:
|
||||
if door.get("direction") == 5:
|
||||
continue
|
||||
|
||||
handling: str = "Transition"
|
||||
if "Cell" in door.get("id"):
|
||||
handling = "Default"
|
||||
obj = {
|
||||
"Name": door.get("id"),
|
||||
"Logic": [],
|
||||
"Handling": handling
|
||||
}
|
||||
|
||||
visibility_flags: List[str] = []
|
||||
if door.get("visibilityFlags") != None:
|
||||
visibility_flags = str(door.get("visibilityFlags")).split(", ")
|
||||
if "1" in visibility_flags:
|
||||
visibility_flags.remove("1")
|
||||
visibility_flags.append("ThisDoor")
|
||||
|
||||
required_doors: List[str] = []
|
||||
if door.get("requiredDoors"):
|
||||
required_doors = door.get("requiredDoors")
|
||||
|
||||
if len(visibility_flags) > 0:
|
||||
for flag in visibility_flags:
|
||||
if flag == "RequiredDoors":
|
||||
continue
|
||||
|
||||
if flag == "ThisDoor":
|
||||
flag = original_connections[door.get("id")]
|
||||
|
||||
if door.get("logic") != None:
|
||||
logic: str = door.get("logic")
|
||||
logic = f"{flag} && ({logic})"
|
||||
logic = preprocess_logic(True, door.get("id"), logic)
|
||||
conditions = build_logic_conditions(logic)
|
||||
for condition in conditions:
|
||||
condition = flip_doors_in_condition(condition)
|
||||
state_provider: str = get_room_from_door(door.get("id"))
|
||||
|
||||
if get_state_provider_for_condition(condition) != None:
|
||||
state_provider = get_state_provider_for_condition(condition)
|
||||
condition.remove(state_provider)
|
||||
|
||||
logic = {
|
||||
"StateProvider": state_provider,
|
||||
"Conditions": condition,
|
||||
"StateModifiers": []
|
||||
}
|
||||
obj["Logic"].append(logic)
|
||||
else:
|
||||
logic = {
|
||||
"StateProvider": get_room_from_door(door.get("id")),
|
||||
"Conditions": [flag],
|
||||
"StateModifiers": []
|
||||
}
|
||||
obj["Logic"].append(logic)
|
||||
|
||||
if "RequiredDoors" in visibility_flags:
|
||||
for d in required_doors:
|
||||
flipped = original_connections[d]
|
||||
if door.get("logic") != None:
|
||||
logic: str = preprocess_logic(True, door.get("id"), door.get("logic"))
|
||||
conditions = build_logic_conditions(logic)
|
||||
for condition in conditions:
|
||||
condition = flip_doors_in_condition(condition)
|
||||
state_provider: str = flipped
|
||||
|
||||
if flipped in condition:
|
||||
condition.remove(flipped)
|
||||
|
||||
logic = {
|
||||
"StateProvider": state_provider,
|
||||
"Conditions": condition,
|
||||
"StateModifiers": []
|
||||
}
|
||||
obj["Logic"].append(logic)
|
||||
else:
|
||||
logic = {
|
||||
"StateProvider": flipped,
|
||||
"Conditions": [],
|
||||
"StateModifiers": []
|
||||
}
|
||||
obj["Logic"].append(logic)
|
||||
|
||||
else:
|
||||
if door.get("logic") != None:
|
||||
logic: str = preprocess_logic(True, door.get("id"), door.get("logic"))
|
||||
conditions = build_logic_conditions(logic)
|
||||
for condition in conditions:
|
||||
condition = flip_doors_in_condition(condition)
|
||||
stateProvider: str = get_room_from_door(door.get("id"))
|
||||
|
||||
if get_state_provider_for_condition(condition) != None:
|
||||
stateProvider = get_state_provider_for_condition(condition)
|
||||
condition.remove(stateProvider)
|
||||
|
||||
logic = {
|
||||
"StateProvider": stateProvider,
|
||||
"Conditions": condition,
|
||||
"StateModifiers": []
|
||||
}
|
||||
obj["Logic"].append(logic)
|
||||
else:
|
||||
logic = {
|
||||
"StateProvider": get_room_from_door(door.get("id")),
|
||||
"Conditions": [],
|
||||
"StateModifiers": []
|
||||
}
|
||||
obj["Logic"].append(logic)
|
||||
|
||||
logic_objects.append(obj)
|
||||
|
||||
for location in locations:
|
||||
obj = {
|
||||
"Name": location.get("id"),
|
||||
"Logic": [],
|
||||
"Handling": "Location"
|
||||
}
|
||||
|
||||
if location.get("logic") != None:
|
||||
for condition in build_logic_conditions(preprocess_logic(False, location.get("id"), location.get("logic"))):
|
||||
condition = flip_doors_in_condition(condition)
|
||||
stateProvider: str = location.get("room")
|
||||
|
||||
if get_state_provider_for_condition(condition) != None:
|
||||
stateProvider = get_state_provider_for_condition(condition)
|
||||
condition.remove(stateProvider)
|
||||
|
||||
if stateProvider == "Initial":
|
||||
stateProvider = None
|
||||
|
||||
logic = {
|
||||
"StateProvider": stateProvider,
|
||||
"Conditions": condition,
|
||||
"StateModifiers": []
|
||||
}
|
||||
obj["Logic"].append(logic)
|
||||
else:
|
||||
stateProvider: str = location.get("room")
|
||||
if stateProvider == "Initial":
|
||||
stateProvider = None
|
||||
logic = {
|
||||
"StateProvider": stateProvider,
|
||||
"Conditions": [],
|
||||
"StateModifiers": []
|
||||
}
|
||||
obj["Logic"].append(logic)
|
||||
|
||||
logic_objects.append(obj)
|
||||
|
||||
output["LogicObjects"] = logic_objects
|
||||
|
||||
with open("StringWorldDefinition.json", "w") as file:
|
||||
print("Writing to StringWorldDefinition.json")
|
||||
file.write(json.dumps(output, indent=4))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(parse_args())
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,12 +8,12 @@ unrandomized_dict: Dict[str, str] = {
|
||||
}
|
||||
|
||||
|
||||
junk_locations: Set[str] = [
|
||||
junk_locations: Set[str] = {
|
||||
"Albero: Donate 50000 Tears",
|
||||
"Ossuary: 11th reward",
|
||||
"AtTotS: Miriam's gift",
|
||||
"TSC: Jocinero's final reward"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
thorn_set: Set[str] = {
|
||||
@@ -44,4 +44,4 @@ skill_dict: Dict[str, str] = {
|
||||
"Skill 5, Tier 1": "Lunge Skill",
|
||||
"Skill 5, Tier 2": "Lunge Skill",
|
||||
"Skill 5, Tier 3": "Lunge Skill",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
from typing import Dict, List, Set, Any
|
||||
from collections import Counter
|
||||
from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification
|
||||
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification
|
||||
from Options import OptionError
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from .Items import base_id, item_table, group_table, tears_set, reliquary_set, event_table
|
||||
from .Locations import location_table
|
||||
from .Rooms import room_table, door_table
|
||||
from .Rules import rules
|
||||
from worlds.generic.Rules import set_rule, add_rule
|
||||
from .Options import blasphemous_options
|
||||
from .Items import base_id, item_table, group_table, tears_list, reliquary_set
|
||||
from .Locations import location_names
|
||||
from .Rules import BlasRules
|
||||
from worlds.generic.Rules import set_rule
|
||||
from .Options import BlasphemousOptions, blas_option_groups
|
||||
from .Vanilla import unrandomized_dict, junk_locations, thorn_set, skill_dict
|
||||
|
||||
from .region_data import regions, locations
|
||||
|
||||
class BlasphemousWeb(WebWorld):
|
||||
theme = "stone"
|
||||
@@ -21,39 +21,33 @@ class BlasphemousWeb(WebWorld):
|
||||
"setup/en",
|
||||
["TRPG"]
|
||||
)]
|
||||
option_groups = blas_option_groups
|
||||
|
||||
|
||||
class BlasphemousWorld(World):
|
||||
"""
|
||||
Blasphemous is a challenging Metroidvania set in the cursed land of Cvstodia. Play as the Penitent One, trapped
|
||||
in an endless cycle of death and rebirth, and free the world from it's terrible fate in your quest to break
|
||||
in an endless cycle of death and rebirth, and free the world from its terrible fate in your quest to break
|
||||
your eternal damnation!
|
||||
"""
|
||||
|
||||
game: str = "Blasphemous"
|
||||
game = "Blasphemous"
|
||||
web = BlasphemousWeb()
|
||||
|
||||
item_name_to_id = {item["name"]: (base_id + index) for index, item in enumerate(item_table)}
|
||||
location_name_to_id = {loc["name"]: (base_id + index) for index, loc in enumerate(location_table)}
|
||||
location_name_to_game_id = {loc["name"]: loc["game_id"] for loc in location_table}
|
||||
location_name_to_id = {loc: (base_id + index) for index, loc in enumerate(location_names.values())}
|
||||
|
||||
item_name_groups = group_table
|
||||
option_definitions = blasphemous_options
|
||||
options_dataclass = BlasphemousOptions
|
||||
options: BlasphemousOptions
|
||||
|
||||
required_client_version = (0, 4, 2)
|
||||
required_client_version = (0, 4, 7)
|
||||
|
||||
|
||||
def __init__(self, multiworld, player):
|
||||
super(BlasphemousWorld, self).__init__(multiworld, player)
|
||||
self.start_room: str = "D17Z01S01"
|
||||
self.door_connections: Dict[str, str] = {}
|
||||
|
||||
|
||||
def set_rules(self):
|
||||
rules(self)
|
||||
for door in door_table:
|
||||
add_rule(self.multiworld.get_location(door["Id"], self.player),
|
||||
lambda state: state.can_reach(self.get_connected_door(door["Id"])), self.player)
|
||||
self.disabled_locations: List[str] = []
|
||||
|
||||
|
||||
def create_item(self, name: str) -> "BlasphemousItem":
|
||||
@@ -68,64 +62,56 @@ class BlasphemousWorld(World):
|
||||
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.multiworld.random.choice(tears_set)
|
||||
return self.random.choice(tears_list)
|
||||
|
||||
|
||||
def generate_early(self):
|
||||
world = self.multiworld
|
||||
player = self.player
|
||||
if not self.options.starting_location.randomized:
|
||||
if self.options.starting_location == "mourning_havoc" and self.options.difficulty < 2:
|
||||
raise OptionError(f"[Blasphemous - '{self.player_name}'] "
|
||||
f"{self.options.starting_location} cannot be chosen if Difficulty is lower than Hard.")
|
||||
|
||||
if not world.starting_location[player].randomized:
|
||||
if world.starting_location[player].value == 6 and world.difficulty[player].value < 2:
|
||||
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
|
||||
" cannot be chosen if Difficulty is lower than Hard.")
|
||||
|
||||
if (world.starting_location[player].value == 0 or world.starting_location[player].value == 6) \
|
||||
and world.dash_shuffle[player]:
|
||||
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
|
||||
" cannot be chosen if Shuffle Dash is enabled.")
|
||||
if (self.options.starting_location == "brotherhood" or self.options.starting_location == "mourning_havoc") \
|
||||
and self.options.dash_shuffle:
|
||||
raise OptionError(f"[Blasphemous - '{self.player_name}'] "
|
||||
f"{self.options.starting_location} cannot be chosen if Shuffle Dash is enabled.")
|
||||
|
||||
if world.starting_location[player].value == 3 and world.wall_climb_shuffle[player]:
|
||||
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
|
||||
" cannot be chosen if Shuffle Wall Climb is enabled.")
|
||||
if self.options.starting_location == "grievance" and self.options.wall_climb_shuffle:
|
||||
raise OptionError(f"[Blasphemous - '{self.player_name}'] "
|
||||
f"{self.options.starting_location} cannot be chosen if Shuffle Wall Climb is enabled.")
|
||||
else:
|
||||
locations: List[int] = [ 0, 1, 2, 3, 4, 5, 6 ]
|
||||
invalid: bool = False
|
||||
|
||||
if world.difficulty[player].value < 2:
|
||||
if self.options.difficulty < 2:
|
||||
locations.remove(6)
|
||||
|
||||
if world.dash_shuffle[player]:
|
||||
if self.options.dash_shuffle:
|
||||
locations.remove(0)
|
||||
if 6 in locations:
|
||||
locations.remove(6)
|
||||
|
||||
if world.wall_climb_shuffle[player]:
|
||||
if self.options.wall_climb_shuffle:
|
||||
locations.remove(3)
|
||||
|
||||
if world.starting_location[player].value == 6 and world.difficulty[player].value < 2:
|
||||
invalid = True
|
||||
|
||||
if (world.starting_location[player].value == 0 or world.starting_location[player].value == 6) \
|
||||
and world.dash_shuffle[player]:
|
||||
invalid = True
|
||||
|
||||
if world.starting_location[player].value == 3 and world.wall_climb_shuffle[player]:
|
||||
invalid = True
|
||||
|
||||
if invalid:
|
||||
world.starting_location[player].value = world.random.choice(locations)
|
||||
if self.options.starting_location.value not in locations:
|
||||
self.options.starting_location.value = self.random.choice(locations)
|
||||
|
||||
|
||||
if not world.dash_shuffle[player]:
|
||||
world.push_precollected(self.create_item("Dash Ability"))
|
||||
if not self.options.dash_shuffle:
|
||||
self.multiworld.push_precollected(self.create_item("Dash Ability"))
|
||||
|
||||
if not world.wall_climb_shuffle[player]:
|
||||
world.push_precollected(self.create_item("Wall Climb Ability"))
|
||||
if not self.options.wall_climb_shuffle:
|
||||
self.multiworld.push_precollected(self.create_item("Wall Climb Ability"))
|
||||
|
||||
if world.skip_long_quests[player]:
|
||||
if not self.options.boots_of_pleading:
|
||||
self.disabled_locations.append("RE401")
|
||||
|
||||
if not self.options.purified_hand:
|
||||
self.disabled_locations.append("RE402")
|
||||
|
||||
if self.options.skip_long_quests:
|
||||
for loc in junk_locations:
|
||||
world.exclude_locations[player].value.add(loc)
|
||||
self.options.exclude_locations.value.add(loc)
|
||||
|
||||
start_rooms: Dict[int, str] = {
|
||||
0: "D17Z01S01",
|
||||
@@ -137,13 +123,10 @@ class BlasphemousWorld(World):
|
||||
6: "D20Z02S09"
|
||||
}
|
||||
|
||||
self.start_room = start_rooms[world.starting_location[player].value]
|
||||
self.start_room = start_rooms[self.options.starting_location.value]
|
||||
|
||||
|
||||
def create_items(self):
|
||||
world = self.multiworld
|
||||
player = self.player
|
||||
|
||||
removed: int = 0
|
||||
to_remove: List[str] = [
|
||||
"Tears of Atonement (250)",
|
||||
@@ -156,46 +139,46 @@ class BlasphemousWorld(World):
|
||||
skipped_items = []
|
||||
junk: int = 0
|
||||
|
||||
for item, count in world.start_inventory[player].value.items():
|
||||
for item, count in self.options.start_inventory.value.items():
|
||||
for _ in range(count):
|
||||
skipped_items.append(item)
|
||||
junk += 1
|
||||
|
||||
skipped_items.extend(unrandomized_dict.values())
|
||||
|
||||
if world.thorn_shuffle[player] == 2:
|
||||
for i in range(8):
|
||||
if self.options.thorn_shuffle == "vanilla":
|
||||
for _ in range(8):
|
||||
skipped_items.append("Thorn Upgrade")
|
||||
|
||||
if world.dash_shuffle[player]:
|
||||
if self.options.dash_shuffle:
|
||||
skipped_items.append(to_remove[removed])
|
||||
removed += 1
|
||||
elif not world.dash_shuffle[player]:
|
||||
elif not self.options.dash_shuffle:
|
||||
skipped_items.append("Dash Ability")
|
||||
|
||||
if world.wall_climb_shuffle[player]:
|
||||
if self.options.wall_climb_shuffle:
|
||||
skipped_items.append(to_remove[removed])
|
||||
removed += 1
|
||||
elif not world.wall_climb_shuffle[player]:
|
||||
elif not self.options.wall_climb_shuffle:
|
||||
skipped_items.append("Wall Climb Ability")
|
||||
|
||||
if not world.reliquary_shuffle[player]:
|
||||
if not self.options.reliquary_shuffle:
|
||||
skipped_items.extend(reliquary_set)
|
||||
elif world.reliquary_shuffle[player]:
|
||||
for i in range(3):
|
||||
elif self.options.reliquary_shuffle:
|
||||
for _ in range(3):
|
||||
skipped_items.append(to_remove[removed])
|
||||
removed += 1
|
||||
|
||||
if not world.boots_of_pleading[player]:
|
||||
if not self.options.boots_of_pleading:
|
||||
skipped_items.append("Boots of Pleading")
|
||||
|
||||
if not world.purified_hand[player]:
|
||||
if not self.options.purified_hand:
|
||||
skipped_items.append("Purified Hand of the Nun")
|
||||
|
||||
if world.start_wheel[player]:
|
||||
if self.options.start_wheel:
|
||||
skipped_items.append("The Young Mason's Wheel")
|
||||
|
||||
if not world.skill_randomizer[player]:
|
||||
if not self.options.skill_randomizer:
|
||||
skipped_items.extend(skill_dict.values())
|
||||
|
||||
counter = Counter(skipped_items)
|
||||
@@ -208,184 +191,140 @@ class BlasphemousWorld(World):
|
||||
if count <= 0:
|
||||
continue
|
||||
else:
|
||||
for i in range(count):
|
||||
for _ in range(count):
|
||||
pool.append(self.create_item(item["name"]))
|
||||
|
||||
for _ in range(junk):
|
||||
pool.append(self.create_item(self.get_filler_item_name()))
|
||||
|
||||
world.itempool += pool
|
||||
self.multiworld.itempool += pool
|
||||
|
||||
|
||||
def pre_fill(self):
|
||||
world = self.multiworld
|
||||
player = self.player
|
||||
|
||||
self.place_items_from_dict(unrandomized_dict)
|
||||
|
||||
if world.thorn_shuffle[player] == 2:
|
||||
if self.options.thorn_shuffle == "vanilla":
|
||||
self.place_items_from_set(thorn_set, "Thorn Upgrade")
|
||||
|
||||
if world.start_wheel[player]:
|
||||
world.get_location("Beginning gift", player)\
|
||||
.place_locked_item(self.create_item("The Young Mason's Wheel"))
|
||||
if self.options.start_wheel:
|
||||
self.get_location("Beginning gift").place_locked_item(self.create_item("The Young Mason's Wheel"))
|
||||
|
||||
if not world.skill_randomizer[player]:
|
||||
if not self.options.skill_randomizer:
|
||||
self.place_items_from_dict(skill_dict)
|
||||
|
||||
if world.thorn_shuffle[player] == 1:
|
||||
world.local_items[player].value.add("Thorn Upgrade")
|
||||
if self.options.thorn_shuffle == "local_only":
|
||||
self.options.local_items.value.add("Thorn Upgrade")
|
||||
|
||||
|
||||
def place_items_from_set(self, location_set: Set[str], name: str):
|
||||
for loc in location_set:
|
||||
self.multiworld.get_location(loc, self.player)\
|
||||
.place_locked_item(self.create_item(name))
|
||||
self.get_location(loc).place_locked_item(self.create_item(name))
|
||||
|
||||
|
||||
def place_items_from_dict(self, option_dict: Dict[str, str]):
|
||||
for loc, item in option_dict.items():
|
||||
self.multiworld.get_location(loc, self.player)\
|
||||
.place_locked_item(self.create_item(item))
|
||||
self.get_location(loc).place_locked_item(self.create_item(item))
|
||||
|
||||
|
||||
def create_regions(self) -> None:
|
||||
multiworld = self.multiworld
|
||||
player = self.player
|
||||
world = self.multiworld
|
||||
|
||||
created_regions: List[str] = []
|
||||
|
||||
for r in regions:
|
||||
multiworld.regions.append(Region(r["name"], player, multiworld))
|
||||
created_regions.append(r["name"])
|
||||
|
||||
self.get_region("Menu").add_exits({self.start_room: "New Game"})
|
||||
|
||||
blas_logic = BlasRules(self)
|
||||
|
||||
for r in regions:
|
||||
region = self.get_region(r["name"])
|
||||
|
||||
for e in r["exits"]:
|
||||
region.add_exits({e["target"]}, {e["target"]: blas_logic.load_rule(True, r["name"], e)})
|
||||
|
||||
for l in [l for l in r["locations"] if l not in self.disabled_locations]:
|
||||
region.add_locations({location_names[l]: self.location_name_to_id[location_names[l]]}, BlasphemousLocation)
|
||||
|
||||
for t in r["transitions"]:
|
||||
if t == r["name"]:
|
||||
continue
|
||||
|
||||
if t in created_regions:
|
||||
region.add_exits({t})
|
||||
else:
|
||||
multiworld.regions.append(Region(t, player, multiworld))
|
||||
created_regions.append(t)
|
||||
region.add_exits({t})
|
||||
|
||||
|
||||
for l in [l for l in locations if l["name"] not in self.disabled_locations]:
|
||||
location = self.get_location(location_names[l["name"]])
|
||||
set_rule(location, blas_logic.load_rule(False, l["name"], l))
|
||||
|
||||
for rname, ename in blas_logic.indirect_conditions:
|
||||
self.multiworld.register_indirect_condition(self.get_region(rname), self.get_entrance(ename))
|
||||
#from Utils import visualize_regions
|
||||
#visualize_regions(self.get_region("Menu"), "blasphemous_regions.puml")
|
||||
|
||||
menu_region = Region("Menu", player, world)
|
||||
misc_region = Region("Misc", player, world)
|
||||
world.regions += [menu_region, misc_region]
|
||||
|
||||
for room in room_table:
|
||||
region = Region(room, player, world)
|
||||
world.regions.append(region)
|
||||
|
||||
menu_region.add_exits({self.start_room: "New Game"})
|
||||
world.get_region(self.start_room, player).add_exits({"Misc": "Misc"})
|
||||
|
||||
for door in door_table:
|
||||
if door.get("OriginalDoor") is None:
|
||||
continue
|
||||
else:
|
||||
if not door["Id"] in self.door_connections.keys():
|
||||
self.door_connections[door["Id"]] = door["OriginalDoor"]
|
||||
self.door_connections[door["OriginalDoor"]] = door["Id"]
|
||||
|
||||
parent_region: Region = self.get_room_from_door(door["Id"])
|
||||
target_region: Region = self.get_room_from_door(door["OriginalDoor"])
|
||||
parent_region.add_exits({
|
||||
target_region.name: door["Id"]
|
||||
}, {
|
||||
target_region.name: lambda x: door.get("VisibilityFlags") != 1
|
||||
})
|
||||
|
||||
for index, loc in enumerate(location_table):
|
||||
if not world.boots_of_pleading[player] and loc["name"] == "BotSS: 2nd meeting with Redento":
|
||||
continue
|
||||
if not world.purified_hand[player] and loc["name"] == "MoM: Western room ledge":
|
||||
continue
|
||||
|
||||
region: Region = world.get_region(loc["room"], player)
|
||||
region.add_locations({loc["name"]: base_id + index})
|
||||
#id = base_id + location_table.index(loc)
|
||||
#reg.locations.append(BlasphemousLocation(player, loc["name"], id, reg))
|
||||
|
||||
for e, r in event_table.items():
|
||||
region: Region = world.get_region(r, player)
|
||||
event = BlasphemousLocation(player, e, None, region)
|
||||
event.show_in_spoiler = False
|
||||
event.place_locked_item(self.create_event(e))
|
||||
region.locations.append(event)
|
||||
|
||||
for door in door_table:
|
||||
region: Region = self.get_room_from_door(self.door_connections[door["Id"]])
|
||||
event = BlasphemousLocation(player, door["Id"], None, region)
|
||||
event.show_in_spoiler = False
|
||||
event.place_locked_item(self.create_event(door["Id"]))
|
||||
region.locations.append(event)
|
||||
|
||||
victory = Location(player, "His Holiness Escribar", None, world.get_region("D07Z01S03", player))
|
||||
victory = Location(player, "His Holiness Escribar", None, self.get_region("D07Z01S03[W]"))
|
||||
victory.place_locked_item(self.create_event("Victory"))
|
||||
world.get_region("D07Z01S03", player).locations.append(victory)
|
||||
self.get_region("D07Z01S03[W]").locations.append(victory)
|
||||
|
||||
if world.ending[self.player].value == 1:
|
||||
if self.options.ending == "ending_a":
|
||||
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8))
|
||||
elif world.ending[self.player].value == 2:
|
||||
elif self.options.ending == "ending_c":
|
||||
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8) and
|
||||
state.has("Holy Wound of Abnegation", player))
|
||||
|
||||
world.completion_condition[self.player] = lambda state: state.has("Victory", player)
|
||||
|
||||
|
||||
def get_room_from_door(self, door: str) -> Region:
|
||||
return self.multiworld.get_region(door.split("[")[0], self.player)
|
||||
|
||||
|
||||
def get_connected_door(self, door: str) -> Entrance:
|
||||
return self.multiworld.get_entrance(self.door_connections[door], self.player)
|
||||
multiworld.completion_condition[self.player] = lambda state: state.has("Victory", player)
|
||||
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
slot_data: Dict[str, Any] = {}
|
||||
locations = []
|
||||
doors: Dict[str, str] = {}
|
||||
|
||||
world = self.multiworld
|
||||
player = self.player
|
||||
thorns: bool = True
|
||||
|
||||
if world.thorn_shuffle[player].value == 2:
|
||||
if self.options.thorn_shuffle == "vanilla":
|
||||
thorns = False
|
||||
|
||||
for loc in world.get_filled_locations(player):
|
||||
if loc.item.code == None:
|
||||
continue
|
||||
else:
|
||||
data = {
|
||||
"id": self.location_name_to_game_id[loc.name],
|
||||
"ap_id": loc.address,
|
||||
"name": loc.item.name,
|
||||
"player_name": world.player_name[loc.item.player],
|
||||
"type": int(loc.item.classification)
|
||||
}
|
||||
|
||||
locations.append(data)
|
||||
|
||||
config = {
|
||||
"LogicDifficulty": world.difficulty[player].value,
|
||||
"StartingLocation": world.starting_location[player].value,
|
||||
"LogicDifficulty": self.options.difficulty.value,
|
||||
"StartingLocation": self.options.starting_location.value,
|
||||
"VersionCreated": "AP",
|
||||
|
||||
"UnlockTeleportation": bool(world.prie_dieu_warp[player].value),
|
||||
"AllowHints": bool(world.corpse_hints[player].value),
|
||||
"AllowPenitence": bool(world.penitence[player].value),
|
||||
"UnlockTeleportation": bool(self.options.prie_dieu_warp.value),
|
||||
"AllowHints": bool(self.options.corpse_hints.value),
|
||||
"AllowPenitence": bool(self.options.penitence.value),
|
||||
|
||||
"ShuffleReliquaries": bool(world.reliquary_shuffle[player].value),
|
||||
"ShuffleBootsOfPleading": bool(world.boots_of_pleading[player].value),
|
||||
"ShufflePurifiedHand": bool(world.purified_hand[player].value),
|
||||
"ShuffleDash": bool(world.dash_shuffle[player].value),
|
||||
"ShuffleWallClimb": bool(world.wall_climb_shuffle[player].value),
|
||||
"ShuffleReliquaries": bool(self.options.reliquary_shuffle.value),
|
||||
"ShuffleBootsOfPleading": bool(self.options.boots_of_pleading.value),
|
||||
"ShufflePurifiedHand": bool(self.options.purified_hand.value),
|
||||
"ShuffleDash": bool(self.options.dash_shuffle.value),
|
||||
"ShuffleWallClimb": bool(self.options.wall_climb_shuffle.value),
|
||||
|
||||
"ShuffleSwordSkills": bool(world.skill_randomizer[player].value),
|
||||
"ShuffleSwordSkills": bool(self.options.wall_climb_shuffle.value),
|
||||
"ShuffleThorns": thorns,
|
||||
"JunkLongQuests": bool(world.skip_long_quests[player].value),
|
||||
"StartWithWheel": bool(world.start_wheel[player].value),
|
||||
"JunkLongQuests": bool(self.options.skip_long_quests.value),
|
||||
"StartWithWheel": bool(self.options.start_wheel.value),
|
||||
|
||||
"EnemyShuffleType": world.enemy_randomizer[player].value,
|
||||
"MaintainClass": bool(world.enemy_groups[player].value),
|
||||
"AreaScaling": bool(world.enemy_scaling[player].value),
|
||||
"EnemyShuffleType": self.options.enemy_randomizer.value,
|
||||
"MaintainClass": bool(self.options.enemy_groups.value),
|
||||
"AreaScaling": bool(self.options.enemy_scaling.value),
|
||||
|
||||
"BossShuffleType": 0,
|
||||
"DoorShuffleType": 0
|
||||
}
|
||||
|
||||
slot_data = {
|
||||
"locations": locations,
|
||||
"locationinfo": [{"gameId": loc, "apId": (base_id + index)} for index, loc in enumerate(location_names)],
|
||||
"doors": doors,
|
||||
"cfg": config,
|
||||
"ending": world.ending[self.player].value,
|
||||
"death_link": bool(world.death_link[self.player].value)
|
||||
"ending": self.options.ending.value,
|
||||
"death_link": bool(self.options.death_link.value)
|
||||
}
|
||||
|
||||
return slot_data
|
||||
|
||||
@@ -1,48 +1,17 @@
|
||||
# Blasphemous Multiworld Setup Guide
|
||||
|
||||
## Useful Links
|
||||
It is recommended to use the [Mod Installer](https://github.com/BrandenEK/Blasphemous.Modding.Installer) to handle installing and updating mods. If you would prefer to install mods manually, instructions can also be found at the Mod Installer repository.
|
||||
|
||||
Required:
|
||||
- Blasphemous: [Steam](https://store.steampowered.com/app/774361/Blasphemous/)
|
||||
- The GOG version of Blasphemous will also work.
|
||||
- Blasphemous Mod Installer: [GitHub](https://github.com/BrandenEK/Blasphemous-Mod-Installer)
|
||||
- Blasphemous Modding API: [GitHub](https://github.com/BrandenEK/Blasphemous-Modding-API)
|
||||
- Blasphemous Randomizer: [GitHub](https://github.com/BrandenEK/Blasphemous-Randomizer)
|
||||
- Blasphemous Multiworld: [GitHub](https://github.com/BrandenEK/Blasphemous-Multiworld)
|
||||
You will need the [Multiworld](https://github.com/BrandenEK/Blasphemous.Randomizer.Multiworld) mod to play an Archipelago randomizer.
|
||||
|
||||
Optional:
|
||||
- In-game map tracker: [GitHub](https://github.com/BrandenEK/Blasphemous-Rando-Map)
|
||||
- Quick Prie Dieu warp mod: [GitHub](https://github.com/BadMagic100/Blasphemous-PrieWarp)
|
||||
- Boots of Pleading mod: [GitHub](https://github.com/BrandenEK/Blasphemous-Boots-of-Pleading)
|
||||
- Double Jump mod: [GitHub](https://github.com/BrandenEK/Blasphemous-Double-Jump)
|
||||
Some optional mods are also recommended:
|
||||
- [Rando Map](https://github.com/BrandenEK/Blasphemous.Randomizer.MapTracker)
|
||||
- [Boots of Pleading](https://github.com/BrandenEK/Blasphemous.BootsOfPleading) (Required if the "Boots of Pleading" option is enabled)
|
||||
- [Double Jump](https://github.com/BrandenEK/Blasphemous.DoubleJump) (Required if the "Purified Hand of the Nun" option is enabled)
|
||||
|
||||
## Mod Installer (Recommended)
|
||||
To connect to a multiworld: Choose a save file and enter the address, your name, and the password (if the server has one) into the menu.
|
||||
|
||||
1. Download the [Mod Installer](https://github.com/BrandenEK/Blasphemous-Mod-Installer),
|
||||
and point it to your install directory for Blasphemous.
|
||||
|
||||
2. Install the `Modding API`, `Randomizer`, and `Multiworld` mods. Optionally, you can also install the
|
||||
`Rando Map`, `PrieWarp`, `Boots of Pleading`, and `Double Jump` mods, and set up the PopTracker pack if desired.
|
||||
|
||||
3. Start Blasphemous. To verfy that the mods are working, look for a version number for both
|
||||
the Randomizer and Multiworld on the title screen.
|
||||
|
||||
## Manual Installation
|
||||
|
||||
1. Download the [Modding API](https://github.com/BrandenEK/Blasphemous-Modding-API/releases), and follow
|
||||
the [installation instructions](https://github.com/BrandenEK/Blasphemous-Modding-API#installation) on the GitHub page.
|
||||
|
||||
2. After the Modding API has been installed, download the
|
||||
[Randomizer](https://github.com/BrandenEK/Blasphemous-Randomizer/releases) and
|
||||
[Multiworld](https://github.com/BrandenEK/Blasphemous-Multiworld/releases) archives, and extract the contents of both
|
||||
into the `Modding` folder. Then, add any desired additional mods.
|
||||
|
||||
3. Start Blasphemous. To verfy that the mods are working, look for a version number for both
|
||||
the Randomizer and Multiworld on the title screen.
|
||||
|
||||
## Connecting
|
||||
|
||||
To connect to an Archipelago server, open the in-game console by pressing backslash `\` and use
|
||||
the command `multiworld connect [address:port] [name] [password]`.
|
||||
The port and password are both optional - if no port is provided then the default port of 38281 is used.
|
||||
**Make sure to connect to the server before attempting to start a new save file.**
|
||||
After connecting, there are some commands you can use in the console, which can be opened by pressing backslash `\`:
|
||||
- `ap status` - Display connection status.
|
||||
- `ap say [message]` - Send a message to the server.
|
||||
- `ap hint [item]` - Request a hint for an item from the server.
|
||||
48070
worlds/blasphemous/region_data.py
generated
Normal file
48070
worlds/blasphemous/region_data.py
generated
Normal file
File diff suppressed because it is too large
Load Diff
7
worlds/blasphemous/test/__init__.py
Normal file
7
worlds/blasphemous/test/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from test.bases import WorldTestBase
|
||||
from .. import BlasphemousWorld
|
||||
|
||||
|
||||
class BlasphemousTestBase(WorldTestBase):
|
||||
game = "Blasphemous"
|
||||
world: BlasphemousWorld
|
||||
56
worlds/blasphemous/test/test_background_zones.py
Normal file
56
worlds/blasphemous/test/test_background_zones.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from . import BlasphemousTestBase
|
||||
from ..Locations import location_names
|
||||
|
||||
|
||||
class BotSSGauntletTest(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "albero",
|
||||
"wall_climb_shuffle": True,
|
||||
"dash_shuffle": True
|
||||
}
|
||||
|
||||
@property
|
||||
def run_default_tests(self) -> bool:
|
||||
return False
|
||||
|
||||
def test_botss_gauntlet(self) -> None:
|
||||
self.assertAccessDependency([location_names["CO25"]], [["Dash Ability", "Wall Climb Ability"]], True)
|
||||
|
||||
|
||||
class BackgroundZonesTest(BlasphemousTestBase):
|
||||
@property
|
||||
def run_default_tests(self) -> bool:
|
||||
return False
|
||||
|
||||
def test_dc_shroud(self) -> None:
|
||||
self.assertAccessDependency([location_names["RB03"]], [["Shroud of Dreamt Sins"]], True)
|
||||
|
||||
def test_wothp_bronze_cells(self) -> None:
|
||||
bronze_locations = [
|
||||
location_names["QI70"],
|
||||
location_names["RESCUED_CHERUB_03"]
|
||||
]
|
||||
|
||||
self.assertAccessDependency(bronze_locations, [["Key of the Secular"]], True)
|
||||
|
||||
def test_wothp_silver_cells(self) -> None:
|
||||
silver_locations = [
|
||||
location_names["CO24"],
|
||||
location_names["RESCUED_CHERUB_34"],
|
||||
location_names["CO37"],
|
||||
location_names["RESCUED_CHERUB_04"]
|
||||
]
|
||||
|
||||
self.assertAccessDependency(silver_locations, [["Key of the Scribe"]], True)
|
||||
|
||||
def test_wothp_gold_cells(self) -> None:
|
||||
gold_locations = [
|
||||
location_names["QI51"],
|
||||
location_names["CO26"],
|
||||
location_names["CO02"]
|
||||
]
|
||||
|
||||
self.assertAccessDependency(gold_locations, [["Key of the Inquisitor"]], True)
|
||||
|
||||
def test_wothp_quirce(self) -> None:
|
||||
self.assertAccessDependency([location_names["BS14"]], [["Key of the Secular", "Key of the Scribe", "Key of the Inquisitor"]], True)
|
||||
135
worlds/blasphemous/test/test_starting_locations.py
Normal file
135
worlds/blasphemous/test/test_starting_locations.py
Normal file
@@ -0,0 +1,135 @@
|
||||
from . import BlasphemousTestBase
|
||||
|
||||
|
||||
class TestBrotherhoodEasy(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "brotherhood",
|
||||
"difficulty": "easy"
|
||||
}
|
||||
|
||||
|
||||
class TestBrotherhoodNormal(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "brotherhood",
|
||||
"difficulty": "normal"
|
||||
}
|
||||
|
||||
|
||||
class TestBrotherhoodHard(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "brotherhood",
|
||||
"difficulty": "hard"
|
||||
}
|
||||
|
||||
|
||||
class TestAlberoEasy(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "albero",
|
||||
"difficulty": "easy"
|
||||
}
|
||||
|
||||
|
||||
class TestAlberoNormal(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "albero",
|
||||
"difficulty": "normal"
|
||||
}
|
||||
|
||||
|
||||
class TestAlberoHard(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "albero",
|
||||
"difficulty": "hard"
|
||||
}
|
||||
|
||||
|
||||
class TestConventEasy(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "convent",
|
||||
"difficulty": "easy"
|
||||
}
|
||||
|
||||
|
||||
class TestConventNormal(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "convent",
|
||||
"difficulty": "normal"
|
||||
}
|
||||
|
||||
|
||||
class TestConventHard(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "convent",
|
||||
"difficulty": "hard"
|
||||
}
|
||||
|
||||
|
||||
class TestGrievanceEasy(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "grievance",
|
||||
"difficulty": "easy"
|
||||
}
|
||||
|
||||
|
||||
class TestGrievanceNormal(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "grievance",
|
||||
"difficulty": "normal"
|
||||
}
|
||||
|
||||
|
||||
class TestGrievanceHard(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "grievance",
|
||||
"difficulty": "hard"
|
||||
}
|
||||
|
||||
|
||||
class TestKnotOfWordsEasy(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "knot_of_words",
|
||||
"difficulty": "easy"
|
||||
}
|
||||
|
||||
|
||||
class TestKnotOfWordsNormal(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "knot_of_words",
|
||||
"difficulty": "normal"
|
||||
}
|
||||
|
||||
|
||||
class TestKnotOfWordsHard(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "knot_of_words",
|
||||
"difficulty": "hard"
|
||||
}
|
||||
|
||||
|
||||
class TestRooftopsEasy(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "rooftops",
|
||||
"difficulty": "easy"
|
||||
}
|
||||
|
||||
|
||||
class TestRooftopsNormal(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "rooftops",
|
||||
"difficulty": "normal"
|
||||
}
|
||||
|
||||
|
||||
class TestRooftopsHard(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "rooftops",
|
||||
"difficulty": "hard"
|
||||
}
|
||||
|
||||
|
||||
# mourning and havoc can't be selected on easy or normal. hard only
|
||||
class TestMourningHavocHard(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "mourning_havoc",
|
||||
"difficulty": "hard"
|
||||
}
|
||||
@@ -28,7 +28,7 @@ An Example `AP.json` file:
|
||||
|
||||
```
|
||||
{
|
||||
"Url": "archipelago:12345",
|
||||
"Url": "archipelago.gg:12345",
|
||||
"SlotName": "Maddy",
|
||||
"Password": ""
|
||||
}
|
||||
|
||||
@@ -44,15 +44,15 @@ class ChecksFinderWorld(World):
|
||||
self.multiworld.regions += [menu, board]
|
||||
|
||||
def create_items(self):
|
||||
# Generate item pool
|
||||
itempool = []
|
||||
# Generate list of items
|
||||
items_to_create = []
|
||||
# Add the map width and height stuff
|
||||
itempool += ["Map Width"] * 5 # 10 - 5
|
||||
itempool += ["Map Height"] * 5 # 10 - 5
|
||||
items_to_create += ["Map Width"] * 5 # 10 - 5
|
||||
items_to_create += ["Map Height"] * 5 # 10 - 5
|
||||
# Add the map bombs
|
||||
itempool += ["Map Bombs"] * 15 # 20 - 5
|
||||
# Convert itempool into real items
|
||||
itempool = [self.create_item(item) for item in itempool]
|
||||
items_to_create += ["Map Bombs"] * 15 # 20 - 5
|
||||
# Convert list into real items
|
||||
itempool = [self.create_item(item) for item in items_to_create]
|
||||
|
||||
self.multiworld.itempool += itempool
|
||||
|
||||
|
||||
@@ -238,15 +238,6 @@ class DS3ItemData:
|
||||
ds3_code = cast(int, self.ds3_code) + level,
|
||||
filler = False,
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return (self.name, self.ds3_code).__hash__()
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if isinstance(other, self.__class__):
|
||||
return self.name == other.name and self.ds3_code == other.ds3_code
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class DarkSouls3Item(Item):
|
||||
|
||||
@@ -1252,6 +1252,9 @@ class DarkSouls3World(World):
|
||||
lambda item: not item.advancement
|
||||
)
|
||||
|
||||
# Prevent the player from prioritizing and "excluding" the same location
|
||||
self.options.priority_locations.value -= allow_useful_locations
|
||||
|
||||
if self.options.excluded_location_behavior == "allow_useful":
|
||||
self.options.exclude_locations.value.clear()
|
||||
|
||||
@@ -1292,10 +1295,10 @@ class DarkSouls3World(World):
|
||||
locations = location if isinstance(location, list) else [location]
|
||||
for location in locations:
|
||||
data = location_dictionary[location]
|
||||
if data.dlc and not self.options.enable_dlc: return
|
||||
if data.ngp and not self.options.enable_ngp: return
|
||||
if data.dlc and not self.options.enable_dlc: continue
|
||||
if data.ngp and not self.options.enable_ngp: continue
|
||||
|
||||
if not self._is_location_available(location): return
|
||||
if not self._is_location_available(location): continue
|
||||
if isinstance(rule, str):
|
||||
assert item_dictionary[rule].classification == ItemClassification.progression
|
||||
rule = lambda state, item=rule: state.has(item, self.player)
|
||||
@@ -1504,16 +1507,19 @@ class DarkSouls3World(World):
|
||||
# We include all the items the game knows about so that users can manually request items
|
||||
# that aren't randomized, and then we _also_ include all the items that are placed in
|
||||
# practice `item_dictionary.values()` doesn't include upgraded or infused weapons.
|
||||
all_items = {
|
||||
cast(DarkSouls3Item, location.item).data
|
||||
items_by_name = {
|
||||
location.item.name: cast(DarkSouls3Item, location.item).data
|
||||
for location in self.multiworld.get_filled_locations()
|
||||
# item.code None is used for events, which we want to skip
|
||||
if location.item.code is not None and location.item.player == self.player
|
||||
}.union(item_dictionary.values())
|
||||
}
|
||||
for item in item_dictionary.values():
|
||||
if item.name not in items_by_name:
|
||||
items_by_name[item.name] = item
|
||||
|
||||
ap_ids_to_ds3_ids: Dict[str, int] = {}
|
||||
item_counts: Dict[str, int] = {}
|
||||
for item in all_items:
|
||||
for item in items_by_name.values():
|
||||
if item.ap_code is None: continue
|
||||
if item.ds3_code: ap_ids_to_ds3_ids[str(item.ap_code)] = item.ds3_code
|
||||
if item.count != 1: item_counts[str(item.ap_code)] = item.count
|
||||
|
||||
@@ -101,6 +101,7 @@ class Factorio(World):
|
||||
tech_tree_layout_prerequisites: typing.Dict[FactorioScienceLocation, typing.Set[FactorioScienceLocation]]
|
||||
tech_mix: int = 0
|
||||
skip_silo: bool = False
|
||||
origin_region_name = "Nauvis"
|
||||
science_locations: typing.List[FactorioScienceLocation]
|
||||
|
||||
settings: typing.ClassVar[FactorioSettings]
|
||||
@@ -125,9 +126,6 @@ class Factorio(World):
|
||||
def create_regions(self):
|
||||
player = self.player
|
||||
random = self.multiworld.random
|
||||
menu = Region("Menu", player, self.multiworld)
|
||||
crash = Entrance(player, "Crash Land", menu)
|
||||
menu.exits.append(crash)
|
||||
nauvis = Region("Nauvis", player, self.multiworld)
|
||||
|
||||
location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \
|
||||
@@ -184,8 +182,7 @@ class Factorio(World):
|
||||
event = FactorioItem(f"Automated {ingredient}", ItemClassification.progression, None, player)
|
||||
location.place_locked_item(event)
|
||||
|
||||
crash.connect(nauvis)
|
||||
self.multiworld.regions += [menu, nauvis]
|
||||
self.multiworld.regions.append(nauvis)
|
||||
|
||||
def create_items(self) -> None:
|
||||
player = self.player
|
||||
|
||||
@@ -601,11 +601,11 @@ class HKWorld(World):
|
||||
if change:
|
||||
for effect_name, effect_value in item_effects.get(item.name, {}).items():
|
||||
state.prog_items[item.player][effect_name] += effect_value
|
||||
if item.name in {"Left_Mothwing_Cloak", "Right_Mothwing_Cloak"}:
|
||||
if state.prog_items[item.player].get('RIGHTDASH', 0) and \
|
||||
state.prog_items[item.player].get('LEFTDASH', 0):
|
||||
(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"]) = \
|
||||
([max(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"])] * 2)
|
||||
if item.name in {"Left_Mothwing_Cloak", "Right_Mothwing_Cloak"}:
|
||||
if state.prog_items[item.player].get('RIGHTDASH', 0) and \
|
||||
state.prog_items[item.player].get('LEFTDASH', 0):
|
||||
(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"]) = \
|
||||
([max(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"])] * 2)
|
||||
return change
|
||||
|
||||
def remove(self, state, item: HKItem) -> bool:
|
||||
|
||||
@@ -1,940 +0,0 @@
|
||||
import typing
|
||||
from BaseClasses import Location, Region
|
||||
from .Names import LocationName
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .Room import KDL3Room
|
||||
|
||||
|
||||
class KDL3Location(Location):
|
||||
game: str = "Kirby's Dream Land 3"
|
||||
room: typing.Optional["KDL3Room"] = None
|
||||
|
||||
def __init__(self, player: int, name: str, address: typing.Optional[int], parent: typing.Union[Region, None]):
|
||||
super().__init__(player, name, address, parent)
|
||||
if not address:
|
||||
self.show_in_spoiler = False
|
||||
|
||||
|
||||
stage_locations = {
|
||||
0x770001: LocationName.grass_land_1,
|
||||
0x770002: LocationName.grass_land_2,
|
||||
0x770003: LocationName.grass_land_3,
|
||||
0x770004: LocationName.grass_land_4,
|
||||
0x770005: LocationName.grass_land_5,
|
||||
0x770006: LocationName.grass_land_6,
|
||||
0x770007: LocationName.ripple_field_1,
|
||||
0x770008: LocationName.ripple_field_2,
|
||||
0x770009: LocationName.ripple_field_3,
|
||||
0x77000A: LocationName.ripple_field_4,
|
||||
0x77000B: LocationName.ripple_field_5,
|
||||
0x77000C: LocationName.ripple_field_6,
|
||||
0x77000D: LocationName.sand_canyon_1,
|
||||
0x77000E: LocationName.sand_canyon_2,
|
||||
0x77000F: LocationName.sand_canyon_3,
|
||||
0x770010: LocationName.sand_canyon_4,
|
||||
0x770011: LocationName.sand_canyon_5,
|
||||
0x770012: LocationName.sand_canyon_6,
|
||||
0x770013: LocationName.cloudy_park_1,
|
||||
0x770014: LocationName.cloudy_park_2,
|
||||
0x770015: LocationName.cloudy_park_3,
|
||||
0x770016: LocationName.cloudy_park_4,
|
||||
0x770017: LocationName.cloudy_park_5,
|
||||
0x770018: LocationName.cloudy_park_6,
|
||||
0x770019: LocationName.iceberg_1,
|
||||
0x77001A: LocationName.iceberg_2,
|
||||
0x77001B: LocationName.iceberg_3,
|
||||
0x77001C: LocationName.iceberg_4,
|
||||
0x77001D: LocationName.iceberg_5,
|
||||
0x77001E: LocationName.iceberg_6,
|
||||
}
|
||||
|
||||
heart_star_locations = {
|
||||
0x770101: LocationName.grass_land_tulip,
|
||||
0x770102: LocationName.grass_land_muchi,
|
||||
0x770103: LocationName.grass_land_pitcherman,
|
||||
0x770104: LocationName.grass_land_chao,
|
||||
0x770105: LocationName.grass_land_mine,
|
||||
0x770106: LocationName.grass_land_pierre,
|
||||
0x770107: LocationName.ripple_field_kamuribana,
|
||||
0x770108: LocationName.ripple_field_bakasa,
|
||||
0x770109: LocationName.ripple_field_elieel,
|
||||
0x77010A: LocationName.ripple_field_toad,
|
||||
0x77010B: LocationName.ripple_field_mama_pitch,
|
||||
0x77010C: LocationName.ripple_field_hb002,
|
||||
0x77010D: LocationName.sand_canyon_mushrooms,
|
||||
0x77010E: LocationName.sand_canyon_auntie,
|
||||
0x77010F: LocationName.sand_canyon_caramello,
|
||||
0x770110: LocationName.sand_canyon_hikari,
|
||||
0x770111: LocationName.sand_canyon_nyupun,
|
||||
0x770112: LocationName.sand_canyon_rob,
|
||||
0x770113: LocationName.cloudy_park_hibanamodoki,
|
||||
0x770114: LocationName.cloudy_park_piyokeko,
|
||||
0x770115: LocationName.cloudy_park_mrball,
|
||||
0x770116: LocationName.cloudy_park_mikarin,
|
||||
0x770117: LocationName.cloudy_park_pick,
|
||||
0x770118: LocationName.cloudy_park_hb007,
|
||||
0x770119: LocationName.iceberg_kogoesou,
|
||||
0x77011A: LocationName.iceberg_samus,
|
||||
0x77011B: LocationName.iceberg_kawasaki,
|
||||
0x77011C: LocationName.iceberg_name,
|
||||
0x77011D: LocationName.iceberg_shiro,
|
||||
0x77011E: LocationName.iceberg_angel,
|
||||
}
|
||||
|
||||
boss_locations = {
|
||||
0x770200: LocationName.grass_land_whispy,
|
||||
0x770201: LocationName.ripple_field_acro,
|
||||
0x770202: LocationName.sand_canyon_poncon,
|
||||
0x770203: LocationName.cloudy_park_ado,
|
||||
0x770204: LocationName.iceberg_dedede,
|
||||
}
|
||||
|
||||
consumable_locations = {
|
||||
0x770300: LocationName.grass_land_1_u1,
|
||||
0x770301: LocationName.grass_land_1_m1,
|
||||
0x770302: LocationName.grass_land_2_u1,
|
||||
0x770303: LocationName.grass_land_3_u1,
|
||||
0x770304: LocationName.grass_land_3_m1,
|
||||
0x770305: LocationName.grass_land_4_m1,
|
||||
0x770306: LocationName.grass_land_4_u1,
|
||||
0x770307: LocationName.grass_land_4_m2,
|
||||
0x770308: LocationName.grass_land_4_m3,
|
||||
0x770309: LocationName.grass_land_6_u1,
|
||||
0x77030A: LocationName.grass_land_6_u2,
|
||||
0x77030B: LocationName.ripple_field_2_u1,
|
||||
0x77030C: LocationName.ripple_field_2_m1,
|
||||
0x77030D: LocationName.ripple_field_3_m1,
|
||||
0x77030E: LocationName.ripple_field_3_u1,
|
||||
0x77030F: LocationName.ripple_field_4_m2,
|
||||
0x770310: LocationName.ripple_field_4_u1,
|
||||
0x770311: LocationName.ripple_field_4_m1,
|
||||
0x770312: LocationName.ripple_field_5_u1,
|
||||
0x770313: LocationName.ripple_field_5_m2,
|
||||
0x770314: LocationName.ripple_field_5_m1,
|
||||
0x770315: LocationName.sand_canyon_1_u1,
|
||||
0x770316: LocationName.sand_canyon_2_u1,
|
||||
0x770317: LocationName.sand_canyon_2_m1,
|
||||
0x770318: LocationName.sand_canyon_4_m1,
|
||||
0x770319: LocationName.sand_canyon_4_u1,
|
||||
0x77031A: LocationName.sand_canyon_4_m2,
|
||||
0x77031B: LocationName.sand_canyon_5_u1,
|
||||
0x77031C: LocationName.sand_canyon_5_u3,
|
||||
0x77031D: LocationName.sand_canyon_5_m1,
|
||||
0x77031E: LocationName.sand_canyon_5_u4,
|
||||
0x77031F: LocationName.sand_canyon_5_u2,
|
||||
0x770320: LocationName.cloudy_park_1_m1,
|
||||
0x770321: LocationName.cloudy_park_1_u1,
|
||||
0x770322: LocationName.cloudy_park_4_u1,
|
||||
0x770323: LocationName.cloudy_park_4_m1,
|
||||
0x770324: LocationName.cloudy_park_5_m1,
|
||||
0x770325: LocationName.cloudy_park_6_u1,
|
||||
0x770326: LocationName.iceberg_3_m1,
|
||||
0x770327: LocationName.iceberg_5_u1,
|
||||
0x770328: LocationName.iceberg_5_u2,
|
||||
0x770329: LocationName.iceberg_5_u3,
|
||||
0x77032A: LocationName.iceberg_6_m1,
|
||||
0x77032B: LocationName.iceberg_6_u1,
|
||||
}
|
||||
|
||||
level_consumables = {
|
||||
1: [0, 1],
|
||||
2: [2],
|
||||
3: [3, 4],
|
||||
4: [5, 6, 7, 8],
|
||||
6: [9, 10],
|
||||
8: [11, 12],
|
||||
9: [13, 14],
|
||||
10: [15, 16, 17],
|
||||
11: [18, 19, 20],
|
||||
13: [21],
|
||||
14: [22, 23],
|
||||
16: [24, 25, 26],
|
||||
17: [27, 28, 29, 30, 31],
|
||||
19: [32, 33],
|
||||
22: [34, 35],
|
||||
23: [36],
|
||||
24: [37],
|
||||
27: [38],
|
||||
29: [39, 40, 41],
|
||||
30: [42, 43],
|
||||
}
|
||||
|
||||
star_locations = {
|
||||
0x770401: LocationName.grass_land_1_s1,
|
||||
0x770402: LocationName.grass_land_1_s2,
|
||||
0x770403: LocationName.grass_land_1_s3,
|
||||
0x770404: LocationName.grass_land_1_s4,
|
||||
0x770405: LocationName.grass_land_1_s5,
|
||||
0x770406: LocationName.grass_land_1_s6,
|
||||
0x770407: LocationName.grass_land_1_s7,
|
||||
0x770408: LocationName.grass_land_1_s8,
|
||||
0x770409: LocationName.grass_land_1_s9,
|
||||
0x77040a: LocationName.grass_land_1_s10,
|
||||
0x77040b: LocationName.grass_land_1_s11,
|
||||
0x77040c: LocationName.grass_land_1_s12,
|
||||
0x77040d: LocationName.grass_land_1_s13,
|
||||
0x77040e: LocationName.grass_land_1_s14,
|
||||
0x77040f: LocationName.grass_land_1_s15,
|
||||
0x770410: LocationName.grass_land_1_s16,
|
||||
0x770411: LocationName.grass_land_1_s17,
|
||||
0x770412: LocationName.grass_land_1_s18,
|
||||
0x770413: LocationName.grass_land_1_s19,
|
||||
0x770414: LocationName.grass_land_1_s20,
|
||||
0x770415: LocationName.grass_land_1_s21,
|
||||
0x770416: LocationName.grass_land_1_s22,
|
||||
0x770417: LocationName.grass_land_1_s23,
|
||||
0x770418: LocationName.grass_land_2_s1,
|
||||
0x770419: LocationName.grass_land_2_s2,
|
||||
0x77041a: LocationName.grass_land_2_s3,
|
||||
0x77041b: LocationName.grass_land_2_s4,
|
||||
0x77041c: LocationName.grass_land_2_s5,
|
||||
0x77041d: LocationName.grass_land_2_s6,
|
||||
0x77041e: LocationName.grass_land_2_s7,
|
||||
0x77041f: LocationName.grass_land_2_s8,
|
||||
0x770420: LocationName.grass_land_2_s9,
|
||||
0x770421: LocationName.grass_land_2_s10,
|
||||
0x770422: LocationName.grass_land_2_s11,
|
||||
0x770423: LocationName.grass_land_2_s12,
|
||||
0x770424: LocationName.grass_land_2_s13,
|
||||
0x770425: LocationName.grass_land_2_s14,
|
||||
0x770426: LocationName.grass_land_2_s15,
|
||||
0x770427: LocationName.grass_land_2_s16,
|
||||
0x770428: LocationName.grass_land_2_s17,
|
||||
0x770429: LocationName.grass_land_2_s18,
|
||||
0x77042a: LocationName.grass_land_2_s19,
|
||||
0x77042b: LocationName.grass_land_2_s20,
|
||||
0x77042c: LocationName.grass_land_2_s21,
|
||||
0x77042d: LocationName.grass_land_3_s1,
|
||||
0x77042e: LocationName.grass_land_3_s2,
|
||||
0x77042f: LocationName.grass_land_3_s3,
|
||||
0x770430: LocationName.grass_land_3_s4,
|
||||
0x770431: LocationName.grass_land_3_s5,
|
||||
0x770432: LocationName.grass_land_3_s6,
|
||||
0x770433: LocationName.grass_land_3_s7,
|
||||
0x770434: LocationName.grass_land_3_s8,
|
||||
0x770435: LocationName.grass_land_3_s9,
|
||||
0x770436: LocationName.grass_land_3_s10,
|
||||
0x770437: LocationName.grass_land_3_s11,
|
||||
0x770438: LocationName.grass_land_3_s12,
|
||||
0x770439: LocationName.grass_land_3_s13,
|
||||
0x77043a: LocationName.grass_land_3_s14,
|
||||
0x77043b: LocationName.grass_land_3_s15,
|
||||
0x77043c: LocationName.grass_land_3_s16,
|
||||
0x77043d: LocationName.grass_land_3_s17,
|
||||
0x77043e: LocationName.grass_land_3_s18,
|
||||
0x77043f: LocationName.grass_land_3_s19,
|
||||
0x770440: LocationName.grass_land_3_s20,
|
||||
0x770441: LocationName.grass_land_3_s21,
|
||||
0x770442: LocationName.grass_land_3_s22,
|
||||
0x770443: LocationName.grass_land_3_s23,
|
||||
0x770444: LocationName.grass_land_3_s24,
|
||||
0x770445: LocationName.grass_land_3_s25,
|
||||
0x770446: LocationName.grass_land_3_s26,
|
||||
0x770447: LocationName.grass_land_3_s27,
|
||||
0x770448: LocationName.grass_land_3_s28,
|
||||
0x770449: LocationName.grass_land_3_s29,
|
||||
0x77044a: LocationName.grass_land_3_s30,
|
||||
0x77044b: LocationName.grass_land_3_s31,
|
||||
0x77044c: LocationName.grass_land_4_s1,
|
||||
0x77044d: LocationName.grass_land_4_s2,
|
||||
0x77044e: LocationName.grass_land_4_s3,
|
||||
0x77044f: LocationName.grass_land_4_s4,
|
||||
0x770450: LocationName.grass_land_4_s5,
|
||||
0x770451: LocationName.grass_land_4_s6,
|
||||
0x770452: LocationName.grass_land_4_s7,
|
||||
0x770453: LocationName.grass_land_4_s8,
|
||||
0x770454: LocationName.grass_land_4_s9,
|
||||
0x770455: LocationName.grass_land_4_s10,
|
||||
0x770456: LocationName.grass_land_4_s11,
|
||||
0x770457: LocationName.grass_land_4_s12,
|
||||
0x770458: LocationName.grass_land_4_s13,
|
||||
0x770459: LocationName.grass_land_4_s14,
|
||||
0x77045a: LocationName.grass_land_4_s15,
|
||||
0x77045b: LocationName.grass_land_4_s16,
|
||||
0x77045c: LocationName.grass_land_4_s17,
|
||||
0x77045d: LocationName.grass_land_4_s18,
|
||||
0x77045e: LocationName.grass_land_4_s19,
|
||||
0x77045f: LocationName.grass_land_4_s20,
|
||||
0x770460: LocationName.grass_land_4_s21,
|
||||
0x770461: LocationName.grass_land_4_s22,
|
||||
0x770462: LocationName.grass_land_4_s23,
|
||||
0x770463: LocationName.grass_land_4_s24,
|
||||
0x770464: LocationName.grass_land_4_s25,
|
||||
0x770465: LocationName.grass_land_4_s26,
|
||||
0x770466: LocationName.grass_land_4_s27,
|
||||
0x770467: LocationName.grass_land_4_s28,
|
||||
0x770468: LocationName.grass_land_4_s29,
|
||||
0x770469: LocationName.grass_land_4_s30,
|
||||
0x77046a: LocationName.grass_land_4_s31,
|
||||
0x77046b: LocationName.grass_land_4_s32,
|
||||
0x77046c: LocationName.grass_land_4_s33,
|
||||
0x77046d: LocationName.grass_land_4_s34,
|
||||
0x77046e: LocationName.grass_land_4_s35,
|
||||
0x77046f: LocationName.grass_land_4_s36,
|
||||
0x770470: LocationName.grass_land_4_s37,
|
||||
0x770471: LocationName.grass_land_5_s1,
|
||||
0x770472: LocationName.grass_land_5_s2,
|
||||
0x770473: LocationName.grass_land_5_s3,
|
||||
0x770474: LocationName.grass_land_5_s4,
|
||||
0x770475: LocationName.grass_land_5_s5,
|
||||
0x770476: LocationName.grass_land_5_s6,
|
||||
0x770477: LocationName.grass_land_5_s7,
|
||||
0x770478: LocationName.grass_land_5_s8,
|
||||
0x770479: LocationName.grass_land_5_s9,
|
||||
0x77047a: LocationName.grass_land_5_s10,
|
||||
0x77047b: LocationName.grass_land_5_s11,
|
||||
0x77047c: LocationName.grass_land_5_s12,
|
||||
0x77047d: LocationName.grass_land_5_s13,
|
||||
0x77047e: LocationName.grass_land_5_s14,
|
||||
0x77047f: LocationName.grass_land_5_s15,
|
||||
0x770480: LocationName.grass_land_5_s16,
|
||||
0x770481: LocationName.grass_land_5_s17,
|
||||
0x770482: LocationName.grass_land_5_s18,
|
||||
0x770483: LocationName.grass_land_5_s19,
|
||||
0x770484: LocationName.grass_land_5_s20,
|
||||
0x770485: LocationName.grass_land_5_s21,
|
||||
0x770486: LocationName.grass_land_5_s22,
|
||||
0x770487: LocationName.grass_land_5_s23,
|
||||
0x770488: LocationName.grass_land_5_s24,
|
||||
0x770489: LocationName.grass_land_5_s25,
|
||||
0x77048a: LocationName.grass_land_5_s26,
|
||||
0x77048b: LocationName.grass_land_5_s27,
|
||||
0x77048c: LocationName.grass_land_5_s28,
|
||||
0x77048d: LocationName.grass_land_5_s29,
|
||||
0x77048e: LocationName.grass_land_6_s1,
|
||||
0x77048f: LocationName.grass_land_6_s2,
|
||||
0x770490: LocationName.grass_land_6_s3,
|
||||
0x770491: LocationName.grass_land_6_s4,
|
||||
0x770492: LocationName.grass_land_6_s5,
|
||||
0x770493: LocationName.grass_land_6_s6,
|
||||
0x770494: LocationName.grass_land_6_s7,
|
||||
0x770495: LocationName.grass_land_6_s8,
|
||||
0x770496: LocationName.grass_land_6_s9,
|
||||
0x770497: LocationName.grass_land_6_s10,
|
||||
0x770498: LocationName.grass_land_6_s11,
|
||||
0x770499: LocationName.grass_land_6_s12,
|
||||
0x77049a: LocationName.grass_land_6_s13,
|
||||
0x77049b: LocationName.grass_land_6_s14,
|
||||
0x77049c: LocationName.grass_land_6_s15,
|
||||
0x77049d: LocationName.grass_land_6_s16,
|
||||
0x77049e: LocationName.grass_land_6_s17,
|
||||
0x77049f: LocationName.grass_land_6_s18,
|
||||
0x7704a0: LocationName.grass_land_6_s19,
|
||||
0x7704a1: LocationName.grass_land_6_s20,
|
||||
0x7704a2: LocationName.grass_land_6_s21,
|
||||
0x7704a3: LocationName.grass_land_6_s22,
|
||||
0x7704a4: LocationName.grass_land_6_s23,
|
||||
0x7704a5: LocationName.grass_land_6_s24,
|
||||
0x7704a6: LocationName.grass_land_6_s25,
|
||||
0x7704a7: LocationName.grass_land_6_s26,
|
||||
0x7704a8: LocationName.grass_land_6_s27,
|
||||
0x7704a9: LocationName.grass_land_6_s28,
|
||||
0x7704aa: LocationName.grass_land_6_s29,
|
||||
0x7704ab: LocationName.ripple_field_1_s1,
|
||||
0x7704ac: LocationName.ripple_field_1_s2,
|
||||
0x7704ad: LocationName.ripple_field_1_s3,
|
||||
0x7704ae: LocationName.ripple_field_1_s4,
|
||||
0x7704af: LocationName.ripple_field_1_s5,
|
||||
0x7704b0: LocationName.ripple_field_1_s6,
|
||||
0x7704b1: LocationName.ripple_field_1_s7,
|
||||
0x7704b2: LocationName.ripple_field_1_s8,
|
||||
0x7704b3: LocationName.ripple_field_1_s9,
|
||||
0x7704b4: LocationName.ripple_field_1_s10,
|
||||
0x7704b5: LocationName.ripple_field_1_s11,
|
||||
0x7704b6: LocationName.ripple_field_1_s12,
|
||||
0x7704b7: LocationName.ripple_field_1_s13,
|
||||
0x7704b8: LocationName.ripple_field_1_s14,
|
||||
0x7704b9: LocationName.ripple_field_1_s15,
|
||||
0x7704ba: LocationName.ripple_field_1_s16,
|
||||
0x7704bb: LocationName.ripple_field_1_s17,
|
||||
0x7704bc: LocationName.ripple_field_1_s18,
|
||||
0x7704bd: LocationName.ripple_field_1_s19,
|
||||
0x7704be: LocationName.ripple_field_2_s1,
|
||||
0x7704bf: LocationName.ripple_field_2_s2,
|
||||
0x7704c0: LocationName.ripple_field_2_s3,
|
||||
0x7704c1: LocationName.ripple_field_2_s4,
|
||||
0x7704c2: LocationName.ripple_field_2_s5,
|
||||
0x7704c3: LocationName.ripple_field_2_s6,
|
||||
0x7704c4: LocationName.ripple_field_2_s7,
|
||||
0x7704c5: LocationName.ripple_field_2_s8,
|
||||
0x7704c6: LocationName.ripple_field_2_s9,
|
||||
0x7704c7: LocationName.ripple_field_2_s10,
|
||||
0x7704c8: LocationName.ripple_field_2_s11,
|
||||
0x7704c9: LocationName.ripple_field_2_s12,
|
||||
0x7704ca: LocationName.ripple_field_2_s13,
|
||||
0x7704cb: LocationName.ripple_field_2_s14,
|
||||
0x7704cc: LocationName.ripple_field_2_s15,
|
||||
0x7704cd: LocationName.ripple_field_2_s16,
|
||||
0x7704ce: LocationName.ripple_field_2_s17,
|
||||
0x7704cf: LocationName.ripple_field_3_s1,
|
||||
0x7704d0: LocationName.ripple_field_3_s2,
|
||||
0x7704d1: LocationName.ripple_field_3_s3,
|
||||
0x7704d2: LocationName.ripple_field_3_s4,
|
||||
0x7704d3: LocationName.ripple_field_3_s5,
|
||||
0x7704d4: LocationName.ripple_field_3_s6,
|
||||
0x7704d5: LocationName.ripple_field_3_s7,
|
||||
0x7704d6: LocationName.ripple_field_3_s8,
|
||||
0x7704d7: LocationName.ripple_field_3_s9,
|
||||
0x7704d8: LocationName.ripple_field_3_s10,
|
||||
0x7704d9: LocationName.ripple_field_3_s11,
|
||||
0x7704da: LocationName.ripple_field_3_s12,
|
||||
0x7704db: LocationName.ripple_field_3_s13,
|
||||
0x7704dc: LocationName.ripple_field_3_s14,
|
||||
0x7704dd: LocationName.ripple_field_3_s15,
|
||||
0x7704de: LocationName.ripple_field_3_s16,
|
||||
0x7704df: LocationName.ripple_field_3_s17,
|
||||
0x7704e0: LocationName.ripple_field_3_s18,
|
||||
0x7704e1: LocationName.ripple_field_3_s19,
|
||||
0x7704e2: LocationName.ripple_field_3_s20,
|
||||
0x7704e3: LocationName.ripple_field_3_s21,
|
||||
0x7704e4: LocationName.ripple_field_4_s1,
|
||||
0x7704e5: LocationName.ripple_field_4_s2,
|
||||
0x7704e6: LocationName.ripple_field_4_s3,
|
||||
0x7704e7: LocationName.ripple_field_4_s4,
|
||||
0x7704e8: LocationName.ripple_field_4_s5,
|
||||
0x7704e9: LocationName.ripple_field_4_s6,
|
||||
0x7704ea: LocationName.ripple_field_4_s7,
|
||||
0x7704eb: LocationName.ripple_field_4_s8,
|
||||
0x7704ec: LocationName.ripple_field_4_s9,
|
||||
0x7704ed: LocationName.ripple_field_4_s10,
|
||||
0x7704ee: LocationName.ripple_field_4_s11,
|
||||
0x7704ef: LocationName.ripple_field_4_s12,
|
||||
0x7704f0: LocationName.ripple_field_4_s13,
|
||||
0x7704f1: LocationName.ripple_field_4_s14,
|
||||
0x7704f2: LocationName.ripple_field_4_s15,
|
||||
0x7704f3: LocationName.ripple_field_4_s16,
|
||||
0x7704f4: LocationName.ripple_field_4_s17,
|
||||
0x7704f5: LocationName.ripple_field_4_s18,
|
||||
0x7704f6: LocationName.ripple_field_4_s19,
|
||||
0x7704f7: LocationName.ripple_field_4_s20,
|
||||
0x7704f8: LocationName.ripple_field_4_s21,
|
||||
0x7704f9: LocationName.ripple_field_4_s22,
|
||||
0x7704fa: LocationName.ripple_field_4_s23,
|
||||
0x7704fb: LocationName.ripple_field_4_s24,
|
||||
0x7704fc: LocationName.ripple_field_4_s25,
|
||||
0x7704fd: LocationName.ripple_field_4_s26,
|
||||
0x7704fe: LocationName.ripple_field_4_s27,
|
||||
0x7704ff: LocationName.ripple_field_4_s28,
|
||||
0x770500: LocationName.ripple_field_4_s29,
|
||||
0x770501: LocationName.ripple_field_4_s30,
|
||||
0x770502: LocationName.ripple_field_4_s31,
|
||||
0x770503: LocationName.ripple_field_4_s32,
|
||||
0x770504: LocationName.ripple_field_4_s33,
|
||||
0x770505: LocationName.ripple_field_4_s34,
|
||||
0x770506: LocationName.ripple_field_4_s35,
|
||||
0x770507: LocationName.ripple_field_4_s36,
|
||||
0x770508: LocationName.ripple_field_4_s37,
|
||||
0x770509: LocationName.ripple_field_4_s38,
|
||||
0x77050a: LocationName.ripple_field_4_s39,
|
||||
0x77050b: LocationName.ripple_field_4_s40,
|
||||
0x77050c: LocationName.ripple_field_4_s41,
|
||||
0x77050d: LocationName.ripple_field_4_s42,
|
||||
0x77050e: LocationName.ripple_field_4_s43,
|
||||
0x77050f: LocationName.ripple_field_4_s44,
|
||||
0x770510: LocationName.ripple_field_4_s45,
|
||||
0x770511: LocationName.ripple_field_4_s46,
|
||||
0x770512: LocationName.ripple_field_4_s47,
|
||||
0x770513: LocationName.ripple_field_4_s48,
|
||||
0x770514: LocationName.ripple_field_4_s49,
|
||||
0x770515: LocationName.ripple_field_4_s50,
|
||||
0x770516: LocationName.ripple_field_4_s51,
|
||||
0x770517: LocationName.ripple_field_5_s1,
|
||||
0x770518: LocationName.ripple_field_5_s2,
|
||||
0x770519: LocationName.ripple_field_5_s3,
|
||||
0x77051a: LocationName.ripple_field_5_s4,
|
||||
0x77051b: LocationName.ripple_field_5_s5,
|
||||
0x77051c: LocationName.ripple_field_5_s6,
|
||||
0x77051d: LocationName.ripple_field_5_s7,
|
||||
0x77051e: LocationName.ripple_field_5_s8,
|
||||
0x77051f: LocationName.ripple_field_5_s9,
|
||||
0x770520: LocationName.ripple_field_5_s10,
|
||||
0x770521: LocationName.ripple_field_5_s11,
|
||||
0x770522: LocationName.ripple_field_5_s12,
|
||||
0x770523: LocationName.ripple_field_5_s13,
|
||||
0x770524: LocationName.ripple_field_5_s14,
|
||||
0x770525: LocationName.ripple_field_5_s15,
|
||||
0x770526: LocationName.ripple_field_5_s16,
|
||||
0x770527: LocationName.ripple_field_5_s17,
|
||||
0x770528: LocationName.ripple_field_5_s18,
|
||||
0x770529: LocationName.ripple_field_5_s19,
|
||||
0x77052a: LocationName.ripple_field_5_s20,
|
||||
0x77052b: LocationName.ripple_field_5_s21,
|
||||
0x77052c: LocationName.ripple_field_5_s22,
|
||||
0x77052d: LocationName.ripple_field_5_s23,
|
||||
0x77052e: LocationName.ripple_field_5_s24,
|
||||
0x77052f: LocationName.ripple_field_5_s25,
|
||||
0x770530: LocationName.ripple_field_5_s26,
|
||||
0x770531: LocationName.ripple_field_5_s27,
|
||||
0x770532: LocationName.ripple_field_5_s28,
|
||||
0x770533: LocationName.ripple_field_5_s29,
|
||||
0x770534: LocationName.ripple_field_5_s30,
|
||||
0x770535: LocationName.ripple_field_5_s31,
|
||||
0x770536: LocationName.ripple_field_5_s32,
|
||||
0x770537: LocationName.ripple_field_5_s33,
|
||||
0x770538: LocationName.ripple_field_5_s34,
|
||||
0x770539: LocationName.ripple_field_5_s35,
|
||||
0x77053a: LocationName.ripple_field_5_s36,
|
||||
0x77053b: LocationName.ripple_field_5_s37,
|
||||
0x77053c: LocationName.ripple_field_5_s38,
|
||||
0x77053d: LocationName.ripple_field_5_s39,
|
||||
0x77053e: LocationName.ripple_field_5_s40,
|
||||
0x77053f: LocationName.ripple_field_5_s41,
|
||||
0x770540: LocationName.ripple_field_5_s42,
|
||||
0x770541: LocationName.ripple_field_5_s43,
|
||||
0x770542: LocationName.ripple_field_5_s44,
|
||||
0x770543: LocationName.ripple_field_5_s45,
|
||||
0x770544: LocationName.ripple_field_5_s46,
|
||||
0x770545: LocationName.ripple_field_5_s47,
|
||||
0x770546: LocationName.ripple_field_5_s48,
|
||||
0x770547: LocationName.ripple_field_5_s49,
|
||||
0x770548: LocationName.ripple_field_5_s50,
|
||||
0x770549: LocationName.ripple_field_5_s51,
|
||||
0x77054a: LocationName.ripple_field_6_s1,
|
||||
0x77054b: LocationName.ripple_field_6_s2,
|
||||
0x77054c: LocationName.ripple_field_6_s3,
|
||||
0x77054d: LocationName.ripple_field_6_s4,
|
||||
0x77054e: LocationName.ripple_field_6_s5,
|
||||
0x77054f: LocationName.ripple_field_6_s6,
|
||||
0x770550: LocationName.ripple_field_6_s7,
|
||||
0x770551: LocationName.ripple_field_6_s8,
|
||||
0x770552: LocationName.ripple_field_6_s9,
|
||||
0x770553: LocationName.ripple_field_6_s10,
|
||||
0x770554: LocationName.ripple_field_6_s11,
|
||||
0x770555: LocationName.ripple_field_6_s12,
|
||||
0x770556: LocationName.ripple_field_6_s13,
|
||||
0x770557: LocationName.ripple_field_6_s14,
|
||||
0x770558: LocationName.ripple_field_6_s15,
|
||||
0x770559: LocationName.ripple_field_6_s16,
|
||||
0x77055a: LocationName.ripple_field_6_s17,
|
||||
0x77055b: LocationName.ripple_field_6_s18,
|
||||
0x77055c: LocationName.ripple_field_6_s19,
|
||||
0x77055d: LocationName.ripple_field_6_s20,
|
||||
0x77055e: LocationName.ripple_field_6_s21,
|
||||
0x77055f: LocationName.ripple_field_6_s22,
|
||||
0x770560: LocationName.ripple_field_6_s23,
|
||||
0x770561: LocationName.sand_canyon_1_s1,
|
||||
0x770562: LocationName.sand_canyon_1_s2,
|
||||
0x770563: LocationName.sand_canyon_1_s3,
|
||||
0x770564: LocationName.sand_canyon_1_s4,
|
||||
0x770565: LocationName.sand_canyon_1_s5,
|
||||
0x770566: LocationName.sand_canyon_1_s6,
|
||||
0x770567: LocationName.sand_canyon_1_s7,
|
||||
0x770568: LocationName.sand_canyon_1_s8,
|
||||
0x770569: LocationName.sand_canyon_1_s9,
|
||||
0x77056a: LocationName.sand_canyon_1_s10,
|
||||
0x77056b: LocationName.sand_canyon_1_s11,
|
||||
0x77056c: LocationName.sand_canyon_1_s12,
|
||||
0x77056d: LocationName.sand_canyon_1_s13,
|
||||
0x77056e: LocationName.sand_canyon_1_s14,
|
||||
0x77056f: LocationName.sand_canyon_1_s15,
|
||||
0x770570: LocationName.sand_canyon_1_s16,
|
||||
0x770571: LocationName.sand_canyon_1_s17,
|
||||
0x770572: LocationName.sand_canyon_1_s18,
|
||||
0x770573: LocationName.sand_canyon_1_s19,
|
||||
0x770574: LocationName.sand_canyon_1_s20,
|
||||
0x770575: LocationName.sand_canyon_1_s21,
|
||||
0x770576: LocationName.sand_canyon_1_s22,
|
||||
0x770577: LocationName.sand_canyon_2_s1,
|
||||
0x770578: LocationName.sand_canyon_2_s2,
|
||||
0x770579: LocationName.sand_canyon_2_s3,
|
||||
0x77057a: LocationName.sand_canyon_2_s4,
|
||||
0x77057b: LocationName.sand_canyon_2_s5,
|
||||
0x77057c: LocationName.sand_canyon_2_s6,
|
||||
0x77057d: LocationName.sand_canyon_2_s7,
|
||||
0x77057e: LocationName.sand_canyon_2_s8,
|
||||
0x77057f: LocationName.sand_canyon_2_s9,
|
||||
0x770580: LocationName.sand_canyon_2_s10,
|
||||
0x770581: LocationName.sand_canyon_2_s11,
|
||||
0x770582: LocationName.sand_canyon_2_s12,
|
||||
0x770583: LocationName.sand_canyon_2_s13,
|
||||
0x770584: LocationName.sand_canyon_2_s14,
|
||||
0x770585: LocationName.sand_canyon_2_s15,
|
||||
0x770586: LocationName.sand_canyon_2_s16,
|
||||
0x770587: LocationName.sand_canyon_2_s17,
|
||||
0x770588: LocationName.sand_canyon_2_s18,
|
||||
0x770589: LocationName.sand_canyon_2_s19,
|
||||
0x77058a: LocationName.sand_canyon_2_s20,
|
||||
0x77058b: LocationName.sand_canyon_2_s21,
|
||||
0x77058c: LocationName.sand_canyon_2_s22,
|
||||
0x77058d: LocationName.sand_canyon_2_s23,
|
||||
0x77058e: LocationName.sand_canyon_2_s24,
|
||||
0x77058f: LocationName.sand_canyon_2_s25,
|
||||
0x770590: LocationName.sand_canyon_2_s26,
|
||||
0x770591: LocationName.sand_canyon_2_s27,
|
||||
0x770592: LocationName.sand_canyon_2_s28,
|
||||
0x770593: LocationName.sand_canyon_2_s29,
|
||||
0x770594: LocationName.sand_canyon_2_s30,
|
||||
0x770595: LocationName.sand_canyon_2_s31,
|
||||
0x770596: LocationName.sand_canyon_2_s32,
|
||||
0x770597: LocationName.sand_canyon_2_s33,
|
||||
0x770598: LocationName.sand_canyon_2_s34,
|
||||
0x770599: LocationName.sand_canyon_2_s35,
|
||||
0x77059a: LocationName.sand_canyon_2_s36,
|
||||
0x77059b: LocationName.sand_canyon_2_s37,
|
||||
0x77059c: LocationName.sand_canyon_2_s38,
|
||||
0x77059d: LocationName.sand_canyon_2_s39,
|
||||
0x77059e: LocationName.sand_canyon_2_s40,
|
||||
0x77059f: LocationName.sand_canyon_2_s41,
|
||||
0x7705a0: LocationName.sand_canyon_2_s42,
|
||||
0x7705a1: LocationName.sand_canyon_2_s43,
|
||||
0x7705a2: LocationName.sand_canyon_2_s44,
|
||||
0x7705a3: LocationName.sand_canyon_2_s45,
|
||||
0x7705a4: LocationName.sand_canyon_2_s46,
|
||||
0x7705a5: LocationName.sand_canyon_2_s47,
|
||||
0x7705a6: LocationName.sand_canyon_2_s48,
|
||||
0x7705a7: LocationName.sand_canyon_3_s1,
|
||||
0x7705a8: LocationName.sand_canyon_3_s2,
|
||||
0x7705a9: LocationName.sand_canyon_3_s3,
|
||||
0x7705aa: LocationName.sand_canyon_3_s4,
|
||||
0x7705ab: LocationName.sand_canyon_3_s5,
|
||||
0x7705ac: LocationName.sand_canyon_3_s6,
|
||||
0x7705ad: LocationName.sand_canyon_3_s7,
|
||||
0x7705ae: LocationName.sand_canyon_3_s8,
|
||||
0x7705af: LocationName.sand_canyon_3_s9,
|
||||
0x7705b0: LocationName.sand_canyon_3_s10,
|
||||
0x7705b1: LocationName.sand_canyon_4_s1,
|
||||
0x7705b2: LocationName.sand_canyon_4_s2,
|
||||
0x7705b3: LocationName.sand_canyon_4_s3,
|
||||
0x7705b4: LocationName.sand_canyon_4_s4,
|
||||
0x7705b5: LocationName.sand_canyon_4_s5,
|
||||
0x7705b6: LocationName.sand_canyon_4_s6,
|
||||
0x7705b7: LocationName.sand_canyon_4_s7,
|
||||
0x7705b8: LocationName.sand_canyon_4_s8,
|
||||
0x7705b9: LocationName.sand_canyon_4_s9,
|
||||
0x7705ba: LocationName.sand_canyon_4_s10,
|
||||
0x7705bb: LocationName.sand_canyon_4_s11,
|
||||
0x7705bc: LocationName.sand_canyon_4_s12,
|
||||
0x7705bd: LocationName.sand_canyon_4_s13,
|
||||
0x7705be: LocationName.sand_canyon_4_s14,
|
||||
0x7705bf: LocationName.sand_canyon_4_s15,
|
||||
0x7705c0: LocationName.sand_canyon_4_s16,
|
||||
0x7705c1: LocationName.sand_canyon_4_s17,
|
||||
0x7705c2: LocationName.sand_canyon_4_s18,
|
||||
0x7705c3: LocationName.sand_canyon_4_s19,
|
||||
0x7705c4: LocationName.sand_canyon_4_s20,
|
||||
0x7705c5: LocationName.sand_canyon_4_s21,
|
||||
0x7705c6: LocationName.sand_canyon_4_s22,
|
||||
0x7705c7: LocationName.sand_canyon_4_s23,
|
||||
0x7705c8: LocationName.sand_canyon_5_s1,
|
||||
0x7705c9: LocationName.sand_canyon_5_s2,
|
||||
0x7705ca: LocationName.sand_canyon_5_s3,
|
||||
0x7705cb: LocationName.sand_canyon_5_s4,
|
||||
0x7705cc: LocationName.sand_canyon_5_s5,
|
||||
0x7705cd: LocationName.sand_canyon_5_s6,
|
||||
0x7705ce: LocationName.sand_canyon_5_s7,
|
||||
0x7705cf: LocationName.sand_canyon_5_s8,
|
||||
0x7705d0: LocationName.sand_canyon_5_s9,
|
||||
0x7705d1: LocationName.sand_canyon_5_s10,
|
||||
0x7705d2: LocationName.sand_canyon_5_s11,
|
||||
0x7705d3: LocationName.sand_canyon_5_s12,
|
||||
0x7705d4: LocationName.sand_canyon_5_s13,
|
||||
0x7705d5: LocationName.sand_canyon_5_s14,
|
||||
0x7705d6: LocationName.sand_canyon_5_s15,
|
||||
0x7705d7: LocationName.sand_canyon_5_s16,
|
||||
0x7705d8: LocationName.sand_canyon_5_s17,
|
||||
0x7705d9: LocationName.sand_canyon_5_s18,
|
||||
0x7705da: LocationName.sand_canyon_5_s19,
|
||||
0x7705db: LocationName.sand_canyon_5_s20,
|
||||
0x7705dc: LocationName.sand_canyon_5_s21,
|
||||
0x7705dd: LocationName.sand_canyon_5_s22,
|
||||
0x7705de: LocationName.sand_canyon_5_s23,
|
||||
0x7705df: LocationName.sand_canyon_5_s24,
|
||||
0x7705e0: LocationName.sand_canyon_5_s25,
|
||||
0x7705e1: LocationName.sand_canyon_5_s26,
|
||||
0x7705e2: LocationName.sand_canyon_5_s27,
|
||||
0x7705e3: LocationName.sand_canyon_5_s28,
|
||||
0x7705e4: LocationName.sand_canyon_5_s29,
|
||||
0x7705e5: LocationName.sand_canyon_5_s30,
|
||||
0x7705e6: LocationName.sand_canyon_5_s31,
|
||||
0x7705e7: LocationName.sand_canyon_5_s32,
|
||||
0x7705e8: LocationName.sand_canyon_5_s33,
|
||||
0x7705e9: LocationName.sand_canyon_5_s34,
|
||||
0x7705ea: LocationName.sand_canyon_5_s35,
|
||||
0x7705eb: LocationName.sand_canyon_5_s36,
|
||||
0x7705ec: LocationName.sand_canyon_5_s37,
|
||||
0x7705ed: LocationName.sand_canyon_5_s38,
|
||||
0x7705ee: LocationName.sand_canyon_5_s39,
|
||||
0x7705ef: LocationName.sand_canyon_5_s40,
|
||||
0x7705f0: LocationName.cloudy_park_1_s1,
|
||||
0x7705f1: LocationName.cloudy_park_1_s2,
|
||||
0x7705f2: LocationName.cloudy_park_1_s3,
|
||||
0x7705f3: LocationName.cloudy_park_1_s4,
|
||||
0x7705f4: LocationName.cloudy_park_1_s5,
|
||||
0x7705f5: LocationName.cloudy_park_1_s6,
|
||||
0x7705f6: LocationName.cloudy_park_1_s7,
|
||||
0x7705f7: LocationName.cloudy_park_1_s8,
|
||||
0x7705f8: LocationName.cloudy_park_1_s9,
|
||||
0x7705f9: LocationName.cloudy_park_1_s10,
|
||||
0x7705fa: LocationName.cloudy_park_1_s11,
|
||||
0x7705fb: LocationName.cloudy_park_1_s12,
|
||||
0x7705fc: LocationName.cloudy_park_1_s13,
|
||||
0x7705fd: LocationName.cloudy_park_1_s14,
|
||||
0x7705fe: LocationName.cloudy_park_1_s15,
|
||||
0x7705ff: LocationName.cloudy_park_1_s16,
|
||||
0x770600: LocationName.cloudy_park_1_s17,
|
||||
0x770601: LocationName.cloudy_park_1_s18,
|
||||
0x770602: LocationName.cloudy_park_1_s19,
|
||||
0x770603: LocationName.cloudy_park_1_s20,
|
||||
0x770604: LocationName.cloudy_park_1_s21,
|
||||
0x770605: LocationName.cloudy_park_1_s22,
|
||||
0x770606: LocationName.cloudy_park_1_s23,
|
||||
0x770607: LocationName.cloudy_park_2_s1,
|
||||
0x770608: LocationName.cloudy_park_2_s2,
|
||||
0x770609: LocationName.cloudy_park_2_s3,
|
||||
0x77060a: LocationName.cloudy_park_2_s4,
|
||||
0x77060b: LocationName.cloudy_park_2_s5,
|
||||
0x77060c: LocationName.cloudy_park_2_s6,
|
||||
0x77060d: LocationName.cloudy_park_2_s7,
|
||||
0x77060e: LocationName.cloudy_park_2_s8,
|
||||
0x77060f: LocationName.cloudy_park_2_s9,
|
||||
0x770610: LocationName.cloudy_park_2_s10,
|
||||
0x770611: LocationName.cloudy_park_2_s11,
|
||||
0x770612: LocationName.cloudy_park_2_s12,
|
||||
0x770613: LocationName.cloudy_park_2_s13,
|
||||
0x770614: LocationName.cloudy_park_2_s14,
|
||||
0x770615: LocationName.cloudy_park_2_s15,
|
||||
0x770616: LocationName.cloudy_park_2_s16,
|
||||
0x770617: LocationName.cloudy_park_2_s17,
|
||||
0x770618: LocationName.cloudy_park_2_s18,
|
||||
0x770619: LocationName.cloudy_park_2_s19,
|
||||
0x77061a: LocationName.cloudy_park_2_s20,
|
||||
0x77061b: LocationName.cloudy_park_2_s21,
|
||||
0x77061c: LocationName.cloudy_park_2_s22,
|
||||
0x77061d: LocationName.cloudy_park_2_s23,
|
||||
0x77061e: LocationName.cloudy_park_2_s24,
|
||||
0x77061f: LocationName.cloudy_park_2_s25,
|
||||
0x770620: LocationName.cloudy_park_2_s26,
|
||||
0x770621: LocationName.cloudy_park_2_s27,
|
||||
0x770622: LocationName.cloudy_park_2_s28,
|
||||
0x770623: LocationName.cloudy_park_2_s29,
|
||||
0x770624: LocationName.cloudy_park_2_s30,
|
||||
0x770625: LocationName.cloudy_park_2_s31,
|
||||
0x770626: LocationName.cloudy_park_2_s32,
|
||||
0x770627: LocationName.cloudy_park_2_s33,
|
||||
0x770628: LocationName.cloudy_park_2_s34,
|
||||
0x770629: LocationName.cloudy_park_2_s35,
|
||||
0x77062a: LocationName.cloudy_park_2_s36,
|
||||
0x77062b: LocationName.cloudy_park_2_s37,
|
||||
0x77062c: LocationName.cloudy_park_2_s38,
|
||||
0x77062d: LocationName.cloudy_park_2_s39,
|
||||
0x77062e: LocationName.cloudy_park_2_s40,
|
||||
0x77062f: LocationName.cloudy_park_2_s41,
|
||||
0x770630: LocationName.cloudy_park_2_s42,
|
||||
0x770631: LocationName.cloudy_park_2_s43,
|
||||
0x770632: LocationName.cloudy_park_2_s44,
|
||||
0x770633: LocationName.cloudy_park_2_s45,
|
||||
0x770634: LocationName.cloudy_park_2_s46,
|
||||
0x770635: LocationName.cloudy_park_2_s47,
|
||||
0x770636: LocationName.cloudy_park_2_s48,
|
||||
0x770637: LocationName.cloudy_park_2_s49,
|
||||
0x770638: LocationName.cloudy_park_2_s50,
|
||||
0x770639: LocationName.cloudy_park_2_s51,
|
||||
0x77063a: LocationName.cloudy_park_2_s52,
|
||||
0x77063b: LocationName.cloudy_park_2_s53,
|
||||
0x77063c: LocationName.cloudy_park_2_s54,
|
||||
0x77063d: LocationName.cloudy_park_3_s1,
|
||||
0x77063e: LocationName.cloudy_park_3_s2,
|
||||
0x77063f: LocationName.cloudy_park_3_s3,
|
||||
0x770640: LocationName.cloudy_park_3_s4,
|
||||
0x770641: LocationName.cloudy_park_3_s5,
|
||||
0x770642: LocationName.cloudy_park_3_s6,
|
||||
0x770643: LocationName.cloudy_park_3_s7,
|
||||
0x770644: LocationName.cloudy_park_3_s8,
|
||||
0x770645: LocationName.cloudy_park_3_s9,
|
||||
0x770646: LocationName.cloudy_park_3_s10,
|
||||
0x770647: LocationName.cloudy_park_3_s11,
|
||||
0x770648: LocationName.cloudy_park_3_s12,
|
||||
0x770649: LocationName.cloudy_park_3_s13,
|
||||
0x77064a: LocationName.cloudy_park_3_s14,
|
||||
0x77064b: LocationName.cloudy_park_3_s15,
|
||||
0x77064c: LocationName.cloudy_park_3_s16,
|
||||
0x77064d: LocationName.cloudy_park_3_s17,
|
||||
0x77064e: LocationName.cloudy_park_3_s18,
|
||||
0x77064f: LocationName.cloudy_park_3_s19,
|
||||
0x770650: LocationName.cloudy_park_3_s20,
|
||||
0x770651: LocationName.cloudy_park_3_s21,
|
||||
0x770652: LocationName.cloudy_park_3_s22,
|
||||
0x770653: LocationName.cloudy_park_4_s1,
|
||||
0x770654: LocationName.cloudy_park_4_s2,
|
||||
0x770655: LocationName.cloudy_park_4_s3,
|
||||
0x770656: LocationName.cloudy_park_4_s4,
|
||||
0x770657: LocationName.cloudy_park_4_s5,
|
||||
0x770658: LocationName.cloudy_park_4_s6,
|
||||
0x770659: LocationName.cloudy_park_4_s7,
|
||||
0x77065a: LocationName.cloudy_park_4_s8,
|
||||
0x77065b: LocationName.cloudy_park_4_s9,
|
||||
0x77065c: LocationName.cloudy_park_4_s10,
|
||||
0x77065d: LocationName.cloudy_park_4_s11,
|
||||
0x77065e: LocationName.cloudy_park_4_s12,
|
||||
0x77065f: LocationName.cloudy_park_4_s13,
|
||||
0x770660: LocationName.cloudy_park_4_s14,
|
||||
0x770661: LocationName.cloudy_park_4_s15,
|
||||
0x770662: LocationName.cloudy_park_4_s16,
|
||||
0x770663: LocationName.cloudy_park_4_s17,
|
||||
0x770664: LocationName.cloudy_park_4_s18,
|
||||
0x770665: LocationName.cloudy_park_4_s19,
|
||||
0x770666: LocationName.cloudy_park_4_s20,
|
||||
0x770667: LocationName.cloudy_park_4_s21,
|
||||
0x770668: LocationName.cloudy_park_4_s22,
|
||||
0x770669: LocationName.cloudy_park_4_s23,
|
||||
0x77066a: LocationName.cloudy_park_4_s24,
|
||||
0x77066b: LocationName.cloudy_park_4_s25,
|
||||
0x77066c: LocationName.cloudy_park_4_s26,
|
||||
0x77066d: LocationName.cloudy_park_4_s27,
|
||||
0x77066e: LocationName.cloudy_park_4_s28,
|
||||
0x77066f: LocationName.cloudy_park_4_s29,
|
||||
0x770670: LocationName.cloudy_park_4_s30,
|
||||
0x770671: LocationName.cloudy_park_4_s31,
|
||||
0x770672: LocationName.cloudy_park_4_s32,
|
||||
0x770673: LocationName.cloudy_park_4_s33,
|
||||
0x770674: LocationName.cloudy_park_4_s34,
|
||||
0x770675: LocationName.cloudy_park_4_s35,
|
||||
0x770676: LocationName.cloudy_park_4_s36,
|
||||
0x770677: LocationName.cloudy_park_4_s37,
|
||||
0x770678: LocationName.cloudy_park_4_s38,
|
||||
0x770679: LocationName.cloudy_park_4_s39,
|
||||
0x77067a: LocationName.cloudy_park_4_s40,
|
||||
0x77067b: LocationName.cloudy_park_4_s41,
|
||||
0x77067c: LocationName.cloudy_park_4_s42,
|
||||
0x77067d: LocationName.cloudy_park_4_s43,
|
||||
0x77067e: LocationName.cloudy_park_4_s44,
|
||||
0x77067f: LocationName.cloudy_park_4_s45,
|
||||
0x770680: LocationName.cloudy_park_4_s46,
|
||||
0x770681: LocationName.cloudy_park_4_s47,
|
||||
0x770682: LocationName.cloudy_park_4_s48,
|
||||
0x770683: LocationName.cloudy_park_4_s49,
|
||||
0x770684: LocationName.cloudy_park_4_s50,
|
||||
0x770685: LocationName.cloudy_park_5_s1,
|
||||
0x770686: LocationName.cloudy_park_5_s2,
|
||||
0x770687: LocationName.cloudy_park_5_s3,
|
||||
0x770688: LocationName.cloudy_park_5_s4,
|
||||
0x770689: LocationName.cloudy_park_5_s5,
|
||||
0x77068a: LocationName.cloudy_park_5_s6,
|
||||
0x77068b: LocationName.cloudy_park_6_s1,
|
||||
0x77068c: LocationName.cloudy_park_6_s2,
|
||||
0x77068d: LocationName.cloudy_park_6_s3,
|
||||
0x77068e: LocationName.cloudy_park_6_s4,
|
||||
0x77068f: LocationName.cloudy_park_6_s5,
|
||||
0x770690: LocationName.cloudy_park_6_s6,
|
||||
0x770691: LocationName.cloudy_park_6_s7,
|
||||
0x770692: LocationName.cloudy_park_6_s8,
|
||||
0x770693: LocationName.cloudy_park_6_s9,
|
||||
0x770694: LocationName.cloudy_park_6_s10,
|
||||
0x770695: LocationName.cloudy_park_6_s11,
|
||||
0x770696: LocationName.cloudy_park_6_s12,
|
||||
0x770697: LocationName.cloudy_park_6_s13,
|
||||
0x770698: LocationName.cloudy_park_6_s14,
|
||||
0x770699: LocationName.cloudy_park_6_s15,
|
||||
0x77069a: LocationName.cloudy_park_6_s16,
|
||||
0x77069b: LocationName.cloudy_park_6_s17,
|
||||
0x77069c: LocationName.cloudy_park_6_s18,
|
||||
0x77069d: LocationName.cloudy_park_6_s19,
|
||||
0x77069e: LocationName.cloudy_park_6_s20,
|
||||
0x77069f: LocationName.cloudy_park_6_s21,
|
||||
0x7706a0: LocationName.cloudy_park_6_s22,
|
||||
0x7706a1: LocationName.cloudy_park_6_s23,
|
||||
0x7706a2: LocationName.cloudy_park_6_s24,
|
||||
0x7706a3: LocationName.cloudy_park_6_s25,
|
||||
0x7706a4: LocationName.cloudy_park_6_s26,
|
||||
0x7706a5: LocationName.cloudy_park_6_s27,
|
||||
0x7706a6: LocationName.cloudy_park_6_s28,
|
||||
0x7706a7: LocationName.cloudy_park_6_s29,
|
||||
0x7706a8: LocationName.cloudy_park_6_s30,
|
||||
0x7706a9: LocationName.cloudy_park_6_s31,
|
||||
0x7706aa: LocationName.cloudy_park_6_s32,
|
||||
0x7706ab: LocationName.cloudy_park_6_s33,
|
||||
0x7706ac: LocationName.iceberg_1_s1,
|
||||
0x7706ad: LocationName.iceberg_1_s2,
|
||||
0x7706ae: LocationName.iceberg_1_s3,
|
||||
0x7706af: LocationName.iceberg_1_s4,
|
||||
0x7706b0: LocationName.iceberg_1_s5,
|
||||
0x7706b1: LocationName.iceberg_1_s6,
|
||||
0x7706b2: LocationName.iceberg_2_s1,
|
||||
0x7706b3: LocationName.iceberg_2_s2,
|
||||
0x7706b4: LocationName.iceberg_2_s3,
|
||||
0x7706b5: LocationName.iceberg_2_s4,
|
||||
0x7706b6: LocationName.iceberg_2_s5,
|
||||
0x7706b7: LocationName.iceberg_2_s6,
|
||||
0x7706b8: LocationName.iceberg_2_s7,
|
||||
0x7706b9: LocationName.iceberg_2_s8,
|
||||
0x7706ba: LocationName.iceberg_2_s9,
|
||||
0x7706bb: LocationName.iceberg_2_s10,
|
||||
0x7706bc: LocationName.iceberg_2_s11,
|
||||
0x7706bd: LocationName.iceberg_2_s12,
|
||||
0x7706be: LocationName.iceberg_2_s13,
|
||||
0x7706bf: LocationName.iceberg_2_s14,
|
||||
0x7706c0: LocationName.iceberg_2_s15,
|
||||
0x7706c1: LocationName.iceberg_2_s16,
|
||||
0x7706c2: LocationName.iceberg_2_s17,
|
||||
0x7706c3: LocationName.iceberg_2_s18,
|
||||
0x7706c4: LocationName.iceberg_2_s19,
|
||||
0x7706c5: LocationName.iceberg_3_s1,
|
||||
0x7706c6: LocationName.iceberg_3_s2,
|
||||
0x7706c7: LocationName.iceberg_3_s3,
|
||||
0x7706c8: LocationName.iceberg_3_s4,
|
||||
0x7706c9: LocationName.iceberg_3_s5,
|
||||
0x7706ca: LocationName.iceberg_3_s6,
|
||||
0x7706cb: LocationName.iceberg_3_s7,
|
||||
0x7706cc: LocationName.iceberg_3_s8,
|
||||
0x7706cd: LocationName.iceberg_3_s9,
|
||||
0x7706ce: LocationName.iceberg_3_s10,
|
||||
0x7706cf: LocationName.iceberg_3_s11,
|
||||
0x7706d0: LocationName.iceberg_3_s12,
|
||||
0x7706d1: LocationName.iceberg_3_s13,
|
||||
0x7706d2: LocationName.iceberg_3_s14,
|
||||
0x7706d3: LocationName.iceberg_3_s15,
|
||||
0x7706d4: LocationName.iceberg_3_s16,
|
||||
0x7706d5: LocationName.iceberg_3_s17,
|
||||
0x7706d6: LocationName.iceberg_3_s18,
|
||||
0x7706d7: LocationName.iceberg_3_s19,
|
||||
0x7706d8: LocationName.iceberg_3_s20,
|
||||
0x7706d9: LocationName.iceberg_3_s21,
|
||||
0x7706da: LocationName.iceberg_4_s1,
|
||||
0x7706db: LocationName.iceberg_4_s2,
|
||||
0x7706dc: LocationName.iceberg_4_s3,
|
||||
0x7706dd: LocationName.iceberg_5_s1,
|
||||
0x7706de: LocationName.iceberg_5_s2,
|
||||
0x7706df: LocationName.iceberg_5_s3,
|
||||
0x7706e0: LocationName.iceberg_5_s4,
|
||||
0x7706e1: LocationName.iceberg_5_s5,
|
||||
0x7706e2: LocationName.iceberg_5_s6,
|
||||
0x7706e3: LocationName.iceberg_5_s7,
|
||||
0x7706e4: LocationName.iceberg_5_s8,
|
||||
0x7706e5: LocationName.iceberg_5_s9,
|
||||
0x7706e6: LocationName.iceberg_5_s10,
|
||||
0x7706e7: LocationName.iceberg_5_s11,
|
||||
0x7706e8: LocationName.iceberg_5_s12,
|
||||
0x7706e9: LocationName.iceberg_5_s13,
|
||||
0x7706ea: LocationName.iceberg_5_s14,
|
||||
0x7706eb: LocationName.iceberg_5_s15,
|
||||
0x7706ec: LocationName.iceberg_5_s16,
|
||||
0x7706ed: LocationName.iceberg_5_s17,
|
||||
0x7706ee: LocationName.iceberg_5_s18,
|
||||
0x7706ef: LocationName.iceberg_5_s19,
|
||||
0x7706f0: LocationName.iceberg_5_s20,
|
||||
0x7706f1: LocationName.iceberg_5_s21,
|
||||
0x7706f2: LocationName.iceberg_5_s22,
|
||||
0x7706f3: LocationName.iceberg_5_s23,
|
||||
0x7706f4: LocationName.iceberg_5_s24,
|
||||
0x7706f5: LocationName.iceberg_5_s25,
|
||||
0x7706f6: LocationName.iceberg_5_s26,
|
||||
0x7706f7: LocationName.iceberg_5_s27,
|
||||
0x7706f8: LocationName.iceberg_5_s28,
|
||||
0x7706f9: LocationName.iceberg_5_s29,
|
||||
0x7706fa: LocationName.iceberg_5_s30,
|
||||
0x7706fb: LocationName.iceberg_5_s31,
|
||||
0x7706fc: LocationName.iceberg_5_s32,
|
||||
0x7706fd: LocationName.iceberg_5_s33,
|
||||
0x7706fe: LocationName.iceberg_5_s34,
|
||||
0x7706ff: LocationName.iceberg_6_s1,
|
||||
|
||||
}
|
||||
|
||||
location_table = {
|
||||
**stage_locations,
|
||||
**heart_star_locations,
|
||||
**boss_locations,
|
||||
**consumable_locations,
|
||||
**star_locations
|
||||
}
|
||||
@@ -1,577 +0,0 @@
|
||||
import typing
|
||||
from pkgutil import get_data
|
||||
|
||||
import Utils
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
import hashlib
|
||||
import os
|
||||
import struct
|
||||
|
||||
import settings
|
||||
from worlds.Files import APDeltaPatch
|
||||
from .Aesthetics import get_palette_bytes, kirby_target_palettes, get_kirby_palette, gooey_target_palettes, \
|
||||
get_gooey_palette
|
||||
from .Compression import hal_decompress
|
||||
import bsdiff4
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import KDL3World
|
||||
|
||||
KDL3UHASH = "201e7658f6194458a3869dde36bf8ec2"
|
||||
KDL3JHASH = "b2f2d004ea640c3db66df958fce122b2"
|
||||
|
||||
level_pointers = {
|
||||
0x770001: 0x0084,
|
||||
0x770002: 0x009C,
|
||||
0x770003: 0x00B8,
|
||||
0x770004: 0x00D8,
|
||||
0x770005: 0x0104,
|
||||
0x770006: 0x0124,
|
||||
0x770007: 0x014C,
|
||||
0x770008: 0x0170,
|
||||
0x770009: 0x0190,
|
||||
0x77000A: 0x01B0,
|
||||
0x77000B: 0x01E8,
|
||||
0x77000C: 0x0218,
|
||||
0x77000D: 0x024C,
|
||||
0x77000E: 0x0270,
|
||||
0x77000F: 0x02A0,
|
||||
0x770010: 0x02C4,
|
||||
0x770011: 0x02EC,
|
||||
0x770012: 0x0314,
|
||||
0x770013: 0x03CC,
|
||||
0x770014: 0x0404,
|
||||
0x770015: 0x042C,
|
||||
0x770016: 0x044C,
|
||||
0x770017: 0x0478,
|
||||
0x770018: 0x049C,
|
||||
0x770019: 0x04E4,
|
||||
0x77001A: 0x0504,
|
||||
0x77001B: 0x0530,
|
||||
0x77001C: 0x0554,
|
||||
0x77001D: 0x05A8,
|
||||
0x77001E: 0x0640,
|
||||
0x770200: 0x0148,
|
||||
0x770201: 0x0248,
|
||||
0x770202: 0x03C8,
|
||||
0x770203: 0x04E0,
|
||||
0x770204: 0x06A4,
|
||||
0x770205: 0x06A8,
|
||||
}
|
||||
|
||||
bb_bosses = {
|
||||
0x770200: 0xED85F1,
|
||||
0x770201: 0xF01360,
|
||||
0x770202: 0xEDA3DF,
|
||||
0x770203: 0xEDC2B9,
|
||||
0x770204: 0xED7C3F,
|
||||
0x770205: 0xEC29D2,
|
||||
}
|
||||
|
||||
level_sprites = {
|
||||
0x19B2C6: 1827,
|
||||
0x1A195C: 1584,
|
||||
0x19F6F3: 1679,
|
||||
0x19DC8B: 1717,
|
||||
0x197900: 1872
|
||||
}
|
||||
|
||||
stage_tiles = {
|
||||
0: [
|
||||
0, 1, 2,
|
||||
16, 17, 18,
|
||||
32, 33, 34,
|
||||
48, 49, 50
|
||||
],
|
||||
1: [
|
||||
3, 4, 5,
|
||||
19, 20, 21,
|
||||
35, 36, 37,
|
||||
51, 52, 53
|
||||
],
|
||||
2: [
|
||||
6, 7, 8,
|
||||
22, 23, 24,
|
||||
38, 39, 40,
|
||||
54, 55, 56
|
||||
],
|
||||
3: [
|
||||
9, 10, 11,
|
||||
25, 26, 27,
|
||||
41, 42, 43,
|
||||
57, 58, 59,
|
||||
],
|
||||
4: [
|
||||
12, 13, 64,
|
||||
28, 29, 65,
|
||||
44, 45, 66,
|
||||
60, 61, 67
|
||||
],
|
||||
5: [
|
||||
14, 15, 68,
|
||||
30, 31, 69,
|
||||
46, 47, 70,
|
||||
62, 63, 71
|
||||
]
|
||||
}
|
||||
|
||||
heart_star_address = 0x2D0000
|
||||
heart_star_size = 456
|
||||
consumable_address = 0x2F91DD
|
||||
consumable_size = 698
|
||||
|
||||
stage_palettes = [0x60964, 0x60B64, 0x60D64, 0x60F64, 0x61164]
|
||||
|
||||
music_choices = [
|
||||
2, # Boss 1
|
||||
3, # Boss 2 (Unused)
|
||||
4, # Boss 3 (Miniboss)
|
||||
7, # Dedede
|
||||
9, # Event 2 (used once)
|
||||
10, # Field 1
|
||||
11, # Field 2
|
||||
12, # Field 3
|
||||
13, # Field 4
|
||||
14, # Field 5
|
||||
15, # Field 6
|
||||
16, # Field 7
|
||||
17, # Field 8
|
||||
18, # Field 9
|
||||
19, # Field 10
|
||||
20, # Field 11
|
||||
21, # Field 12 (Gourmet Race)
|
||||
23, # Dark Matter in the Hyper Zone
|
||||
24, # Zero
|
||||
25, # Level 1
|
||||
26, # Level 2
|
||||
27, # Level 4
|
||||
28, # Level 3
|
||||
29, # Heart Star Failed
|
||||
30, # Level 5
|
||||
31, # Minigame
|
||||
38, # Animal Friend 1
|
||||
39, # Animal Friend 2
|
||||
40, # Animal Friend 3
|
||||
]
|
||||
# extra room pointers we don't want to track other than for music
|
||||
room_pointers = [
|
||||
3079990, # Zero
|
||||
2983409, # BB Whispy
|
||||
3150688, # BB Acro
|
||||
2991071, # BB PonCon
|
||||
2998969, # BB Ado
|
||||
2980927, # BB Dedede
|
||||
2894290 # BB Zero
|
||||
]
|
||||
|
||||
enemy_remap = {
|
||||
"Waddle Dee": 0,
|
||||
"Bronto Burt": 2,
|
||||
"Rocky": 3,
|
||||
"Bobo": 5,
|
||||
"Chilly": 6,
|
||||
"Poppy Bros Jr.": 7,
|
||||
"Sparky": 8,
|
||||
"Polof": 9,
|
||||
"Broom Hatter": 11,
|
||||
"Cappy": 12,
|
||||
"Bouncy": 13,
|
||||
"Nruff": 15,
|
||||
"Glunk": 16,
|
||||
"Togezo": 18,
|
||||
"Kabu": 19,
|
||||
"Mony": 20,
|
||||
"Blipper": 21,
|
||||
"Squishy": 22,
|
||||
"Gabon": 24,
|
||||
"Oro": 25,
|
||||
"Galbo": 26,
|
||||
"Sir Kibble": 27,
|
||||
"Nidoo": 28,
|
||||
"Kany": 29,
|
||||
"Sasuke": 30,
|
||||
"Yaban": 32,
|
||||
"Boten": 33,
|
||||
"Coconut": 34,
|
||||
"Doka": 35,
|
||||
"Icicle": 36,
|
||||
"Pteran": 39,
|
||||
"Loud": 40,
|
||||
"Como": 41,
|
||||
"Klinko": 42,
|
||||
"Babut": 43,
|
||||
"Wappa": 44,
|
||||
"Mariel": 45,
|
||||
"Tick": 48,
|
||||
"Apolo": 49,
|
||||
"Popon Ball": 50,
|
||||
"KeKe": 51,
|
||||
"Magoo": 53,
|
||||
"Raft Waddle Dee": 57,
|
||||
"Madoo": 58,
|
||||
"Corori": 60,
|
||||
"Kapar": 67,
|
||||
"Batamon": 68,
|
||||
"Peran": 72,
|
||||
"Bobin": 73,
|
||||
"Mopoo": 74,
|
||||
"Gansan": 75,
|
||||
"Bukiset (Burning)": 76,
|
||||
"Bukiset (Stone)": 77,
|
||||
"Bukiset (Ice)": 78,
|
||||
"Bukiset (Needle)": 79,
|
||||
"Bukiset (Clean)": 80,
|
||||
"Bukiset (Parasol)": 81,
|
||||
"Bukiset (Spark)": 82,
|
||||
"Bukiset (Cutter)": 83,
|
||||
"Waddle Dee Drawing": 84,
|
||||
"Bronto Burt Drawing": 85,
|
||||
"Bouncy Drawing": 86,
|
||||
"Kabu (Dekabu)": 87,
|
||||
"Wapod": 88,
|
||||
"Propeller": 89,
|
||||
"Dogon": 90,
|
||||
"Joe": 91
|
||||
}
|
||||
|
||||
miniboss_remap = {
|
||||
"Captain Stitch": 0,
|
||||
"Yuki": 1,
|
||||
"Blocky": 2,
|
||||
"Jumper Shoot": 3,
|
||||
"Boboo": 4,
|
||||
"Haboki": 5
|
||||
}
|
||||
|
||||
ability_remap = {
|
||||
"No Ability": 0,
|
||||
"Burning Ability": 1,
|
||||
"Stone Ability": 2,
|
||||
"Ice Ability": 3,
|
||||
"Needle Ability": 4,
|
||||
"Clean Ability": 5,
|
||||
"Parasol Ability": 6,
|
||||
"Spark Ability": 7,
|
||||
"Cutter Ability": 8,
|
||||
}
|
||||
|
||||
|
||||
class RomData:
|
||||
def __init__(self, file: str, name: typing.Optional[str] = None):
|
||||
self.file = bytearray()
|
||||
self.read_from_file(file)
|
||||
self.name = name
|
||||
|
||||
def read_byte(self, offset: int):
|
||||
return self.file[offset]
|
||||
|
||||
def read_bytes(self, offset: int, length: int):
|
||||
return self.file[offset:offset + length]
|
||||
|
||||
def write_byte(self, offset: int, value: int):
|
||||
self.file[offset] = value
|
||||
|
||||
def write_bytes(self, offset: int, values: typing.Sequence) -> None:
|
||||
self.file[offset:offset + len(values)] = values
|
||||
|
||||
def write_to_file(self, file: str):
|
||||
with open(file, 'wb') as outfile:
|
||||
outfile.write(self.file)
|
||||
|
||||
def read_from_file(self, file: str):
|
||||
with open(file, 'rb') as stream:
|
||||
self.file = bytearray(stream.read())
|
||||
|
||||
def apply_patch(self, patch: bytes):
|
||||
self.file = bytearray(bsdiff4.patch(bytes(self.file), patch))
|
||||
|
||||
def write_crc(self):
|
||||
crc = (sum(self.file[:0x7FDC] + self.file[0x7FE0:]) + 0x01FE) & 0xFFFF
|
||||
inv = crc ^ 0xFFFF
|
||||
self.write_bytes(0x7FDC, [inv & 0xFF, (inv >> 8) & 0xFF, crc & 0xFF, (crc >> 8) & 0xFF])
|
||||
|
||||
|
||||
def handle_level_sprites(stages, sprites, palettes):
|
||||
palette_by_level = list()
|
||||
for palette in palettes:
|
||||
palette_by_level.extend(palette[10:16])
|
||||
for i in range(5):
|
||||
for j in range(6):
|
||||
palettes[i][10 + j] = palette_by_level[stages[i][j] - 1]
|
||||
palettes[i] = [x for palette in palettes[i] for x in palette]
|
||||
tiles_by_level = list()
|
||||
for spritesheet in sprites:
|
||||
decompressed = hal_decompress(spritesheet)
|
||||
tiles = [decompressed[i:i + 32] for i in range(0, 2304, 32)]
|
||||
tiles_by_level.extend([[tiles[x] for x in stage_tiles[stage]] for stage in stage_tiles])
|
||||
for world in range(5):
|
||||
levels = [stages[world][x] - 1 for x in range(6)]
|
||||
world_tiles: typing.List[typing.Optional[bytes]] = [None for _ in range(72)]
|
||||
for i in range(6):
|
||||
for x in range(12):
|
||||
world_tiles[stage_tiles[i][x]] = tiles_by_level[levels[i]][x]
|
||||
sprites[world] = list()
|
||||
for tile in world_tiles:
|
||||
sprites[world].extend(tile)
|
||||
# insert our fake compression
|
||||
sprites[world][0:0] = [0xe3, 0xff]
|
||||
sprites[world][1026:1026] = [0xe3, 0xff]
|
||||
sprites[world][2052:2052] = [0xe0, 0xff]
|
||||
sprites[world].append(0xff)
|
||||
return sprites, palettes
|
||||
|
||||
|
||||
def write_heart_star_sprites(rom: RomData):
|
||||
compressed = rom.read_bytes(heart_star_address, heart_star_size)
|
||||
decompressed = hal_decompress(compressed)
|
||||
patch = get_data(__name__, os.path.join("data", "APHeartStar.bsdiff4"))
|
||||
patched = bytearray(bsdiff4.patch(decompressed, patch))
|
||||
rom.write_bytes(0x1AF7DF, patched)
|
||||
patched[0:0] = [0xE3, 0xFF]
|
||||
patched.append(0xFF)
|
||||
rom.write_bytes(0x1CD000, patched)
|
||||
rom.write_bytes(0x3F0EBF, [0x00, 0xD0, 0x39])
|
||||
|
||||
|
||||
def write_consumable_sprites(rom: RomData, consumables: bool, stars: bool):
|
||||
compressed = rom.read_bytes(consumable_address, consumable_size)
|
||||
decompressed = hal_decompress(compressed)
|
||||
patched = bytearray(decompressed)
|
||||
if consumables:
|
||||
patch = get_data(__name__, os.path.join("data", "APConsumable.bsdiff4"))
|
||||
patched = bytearray(bsdiff4.patch(bytes(patched), patch))
|
||||
if stars:
|
||||
patch = get_data(__name__, os.path.join("data", "APStars.bsdiff4"))
|
||||
patched = bytearray(bsdiff4.patch(bytes(patched), patch))
|
||||
patched[0:0] = [0xE3, 0xFF]
|
||||
patched.append(0xFF)
|
||||
rom.write_bytes(0x1CD500, patched)
|
||||
rom.write_bytes(0x3F0DAE, [0x00, 0xD5, 0x39])
|
||||
|
||||
|
||||
class KDL3DeltaPatch(APDeltaPatch):
|
||||
hash = [KDL3UHASH, KDL3JHASH]
|
||||
game = "Kirby's Dream Land 3"
|
||||
patch_file_ending = ".apkdl3"
|
||||
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
return get_base_rom_bytes()
|
||||
|
||||
def patch(self, target: str):
|
||||
super().patch(target)
|
||||
rom = RomData(target)
|
||||
target_language = rom.read_byte(0x3C020)
|
||||
rom.write_byte(0x7FD9, target_language)
|
||||
write_heart_star_sprites(rom)
|
||||
if rom.read_bytes(0x3D014, 1)[0] > 0:
|
||||
stages = [struct.unpack("HHHHHHH", rom.read_bytes(0x3D020 + x * 14, 14)) for x in range(5)]
|
||||
palettes = [rom.read_bytes(full_pal, 512) for full_pal in stage_palettes]
|
||||
palettes = [[palette[i:i + 32] for i in range(0, 512, 32)] for palette in palettes]
|
||||
sprites = [rom.read_bytes(offset, level_sprites[offset]) for offset in level_sprites]
|
||||
sprites, palettes = handle_level_sprites(stages, sprites, palettes)
|
||||
for addr, palette in zip(stage_palettes, palettes):
|
||||
rom.write_bytes(addr, palette)
|
||||
for addr, level_sprite in zip([0x1CA000, 0x1CA920, 0x1CB230, 0x1CBB40, 0x1CC450], sprites):
|
||||
rom.write_bytes(addr, level_sprite)
|
||||
rom.write_bytes(0x460A, [0x00, 0xA0, 0x39, 0x20, 0xA9, 0x39, 0x30, 0xB2, 0x39, 0x40, 0xBB, 0x39,
|
||||
0x50, 0xC4, 0x39])
|
||||
write_consumable_sprites(rom, rom.read_byte(0x3D018) > 0, rom.read_byte(0x3D01A) > 0)
|
||||
rom_name = rom.read_bytes(0x3C000, 21)
|
||||
rom.write_bytes(0x7FC0, rom_name)
|
||||
rom.write_crc()
|
||||
rom.write_to_file(target)
|
||||
|
||||
|
||||
def patch_rom(world: "KDL3World", rom: RomData):
|
||||
rom.apply_patch(get_data(__name__, os.path.join("data", "kdl3_basepatch.bsdiff4")))
|
||||
tiles = get_data(__name__, os.path.join("data", "APPauseIcons.dat"))
|
||||
rom.write_bytes(0x3F000, tiles)
|
||||
|
||||
# Write open world patch
|
||||
if world.options.open_world:
|
||||
rom.write_bytes(0x143C7, [0xAD, 0xC1, 0x5A, 0xCD, 0xC1, 0x5A, ])
|
||||
# changes the stage flag function to compare $5AC1 to $5AC1,
|
||||
# always running the "new stage" function
|
||||
# This has further checks present for bosses already, so we just
|
||||
# need to handle regular stages
|
||||
# write check for boss to be unlocked
|
||||
|
||||
if world.options.consumables:
|
||||
# reroute maxim tomatoes to use the 1-UP function, then null out the function
|
||||
rom.write_bytes(0x3002F, [0x37, 0x00])
|
||||
rom.write_bytes(0x30037, [0xA9, 0x26, 0x00, # LDA #$0026
|
||||
0x22, 0x27, 0xD9, 0x00, # JSL $00D927
|
||||
0xA4, 0xD2, # LDY $D2
|
||||
0x6B, # RTL
|
||||
0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, # NOP #10
|
||||
])
|
||||
|
||||
# stars handling is built into the rom, so no changes there
|
||||
|
||||
rooms = world.rooms
|
||||
if world.options.music_shuffle > 0:
|
||||
if world.options.music_shuffle == 1:
|
||||
shuffled_music = music_choices.copy()
|
||||
world.random.shuffle(shuffled_music)
|
||||
music_map = dict(zip(music_choices, shuffled_music))
|
||||
# Avoid putting star twinkle in the pool
|
||||
music_map[5] = world.random.choice(music_choices)
|
||||
# Heart Star music doesn't work on regular stages
|
||||
music_map[8] = world.random.choice(music_choices)
|
||||
for room in rooms:
|
||||
room.music = music_map[room.music]
|
||||
for room in room_pointers:
|
||||
old_music = rom.read_byte(room + 2)
|
||||
rom.write_byte(room + 2, music_map[old_music])
|
||||
for i in range(5):
|
||||
# level themes
|
||||
old_music = rom.read_byte(0x133F2 + i)
|
||||
rom.write_byte(0x133F2 + i, music_map[old_music])
|
||||
# Zero
|
||||
rom.write_byte(0x9AE79, music_map[0x18])
|
||||
# Heart Star success and fail
|
||||
rom.write_byte(0x4A388, music_map[0x08])
|
||||
rom.write_byte(0x4A38D, music_map[0x1D])
|
||||
elif world.options.music_shuffle == 2:
|
||||
for room in rooms:
|
||||
room.music = world.random.choice(music_choices)
|
||||
for room in room_pointers:
|
||||
rom.write_byte(room + 2, world.random.choice(music_choices))
|
||||
for i in range(5):
|
||||
# level themes
|
||||
rom.write_byte(0x133F2 + i, world.random.choice(music_choices))
|
||||
# Zero
|
||||
rom.write_byte(0x9AE79, world.random.choice(music_choices))
|
||||
# Heart Star success and fail
|
||||
rom.write_byte(0x4A388, world.random.choice(music_choices))
|
||||
rom.write_byte(0x4A38D, world.random.choice(music_choices))
|
||||
|
||||
for room in rooms:
|
||||
room.patch(rom)
|
||||
|
||||
if world.options.virtual_console in [1, 3]:
|
||||
# Flash Reduction
|
||||
rom.write_byte(0x9AE68, 0x10)
|
||||
rom.write_bytes(0x9AE8E, [0x08, 0x00, 0x22, 0x5D, 0xF7, 0x00, 0xA2, 0x08, ])
|
||||
rom.write_byte(0x9AEA1, 0x08)
|
||||
rom.write_byte(0x9AEC9, 0x01)
|
||||
rom.write_bytes(0x9AED2, [0xA9, 0x1F])
|
||||
rom.write_byte(0x9AEE1, 0x08)
|
||||
|
||||
if world.options.virtual_console in [2, 3]:
|
||||
# Hyper Zone BB colors
|
||||
rom.write_bytes(0x2C5E16, [0xEE, 0x1B, 0x18, 0x5B, 0xD3, 0x4A, 0xF4, 0x3B, ])
|
||||
rom.write_bytes(0x2C8217, [0xFF, 0x1E, ])
|
||||
|
||||
# boss requirements
|
||||
rom.write_bytes(0x3D000, struct.pack("HHHHH", world.boss_requirements[0], world.boss_requirements[1],
|
||||
world.boss_requirements[2], world.boss_requirements[3],
|
||||
world.boss_requirements[4]))
|
||||
rom.write_bytes(0x3D00A, struct.pack("H", world.required_heart_stars if world.options.goal_speed == 1 else 0xFFFF))
|
||||
rom.write_byte(0x3D00C, world.options.goal_speed.value)
|
||||
rom.write_byte(0x3D00E, world.options.open_world.value)
|
||||
rom.write_byte(0x3D010, world.options.death_link.value)
|
||||
rom.write_byte(0x3D012, world.options.goal.value)
|
||||
rom.write_byte(0x3D014, world.options.stage_shuffle.value)
|
||||
rom.write_byte(0x3D016, world.options.ow_boss_requirement.value)
|
||||
rom.write_byte(0x3D018, world.options.consumables.value)
|
||||
rom.write_byte(0x3D01A, world.options.starsanity.value)
|
||||
rom.write_byte(0x3D01C, world.options.gifting.value if world.multiworld.players > 1 else 0)
|
||||
rom.write_byte(0x3D01E, world.options.strict_bosses.value)
|
||||
# don't write gifting for solo game, since there's no one to send anything to
|
||||
|
||||
for level in world.player_levels:
|
||||
for i in range(len(world.player_levels[level])):
|
||||
rom.write_bytes(0x3F002E + ((level - 1) * 14) + (i * 2),
|
||||
struct.pack("H", level_pointers[world.player_levels[level][i]]))
|
||||
rom.write_bytes(0x3D020 + (level - 1) * 14 + (i * 2),
|
||||
struct.pack("H", world.player_levels[level][i] & 0x00FFFF))
|
||||
if (i == 0) or (i > 0 and i % 6 != 0):
|
||||
rom.write_bytes(0x3D080 + (level - 1) * 12 + (i * 2),
|
||||
struct.pack("H", (world.player_levels[level][i] & 0x00FFFF) % 6))
|
||||
|
||||
for i in range(6):
|
||||
if world.boss_butch_bosses[i]:
|
||||
rom.write_bytes(0x3F0000 + (level_pointers[0x770200 + i]), struct.pack("I", bb_bosses[0x770200 + i]))
|
||||
|
||||
# copy ability shuffle
|
||||
if world.options.copy_ability_randomization.value > 0:
|
||||
for enemy in world.copy_abilities:
|
||||
if enemy in miniboss_remap:
|
||||
rom.write_bytes(0xB417E + (miniboss_remap[enemy] << 1),
|
||||
struct.pack("H", ability_remap[world.copy_abilities[enemy]]))
|
||||
else:
|
||||
rom.write_bytes(0xB3CAC + (enemy_remap[enemy] << 1),
|
||||
struct.pack("H", ability_remap[world.copy_abilities[enemy]]))
|
||||
# following only needs done on non-door rando
|
||||
# incredibly lucky this follows the same order (including 5E == star block)
|
||||
rom.write_byte(0x2F77EA, 0x5E + (ability_remap[world.copy_abilities["Sparky"]] << 1))
|
||||
rom.write_byte(0x2F7811, 0x5E + (ability_remap[world.copy_abilities["Sparky"]] << 1))
|
||||
rom.write_byte(0x2F9BC4, 0x5E + (ability_remap[world.copy_abilities["Blocky"]] << 1))
|
||||
rom.write_byte(0x2F9BEB, 0x5E + (ability_remap[world.copy_abilities["Blocky"]] << 1))
|
||||
rom.write_byte(0x2FAC06, 0x5E + (ability_remap[world.copy_abilities["Jumper Shoot"]] << 1))
|
||||
rom.write_byte(0x2FAC2D, 0x5E + (ability_remap[world.copy_abilities["Jumper Shoot"]] << 1))
|
||||
rom.write_byte(0x2F9E7B, 0x5E + (ability_remap[world.copy_abilities["Yuki"]] << 1))
|
||||
rom.write_byte(0x2F9EA2, 0x5E + (ability_remap[world.copy_abilities["Yuki"]] << 1))
|
||||
rom.write_byte(0x2FA951, 0x5E + (ability_remap[world.copy_abilities["Sir Kibble"]] << 1))
|
||||
rom.write_byte(0x2FA978, 0x5E + (ability_remap[world.copy_abilities["Sir Kibble"]] << 1))
|
||||
rom.write_byte(0x2FA132, 0x5E + (ability_remap[world.copy_abilities["Haboki"]] << 1))
|
||||
rom.write_byte(0x2FA159, 0x5E + (ability_remap[world.copy_abilities["Haboki"]] << 1))
|
||||
rom.write_byte(0x2FA3E8, 0x5E + (ability_remap[world.copy_abilities["Boboo"]] << 1))
|
||||
rom.write_byte(0x2FA40F, 0x5E + (ability_remap[world.copy_abilities["Boboo"]] << 1))
|
||||
rom.write_byte(0x2F90E2, 0x5E + (ability_remap[world.copy_abilities["Captain Stitch"]] << 1))
|
||||
rom.write_byte(0x2F9109, 0x5E + (ability_remap[world.copy_abilities["Captain Stitch"]] << 1))
|
||||
|
||||
if world.options.copy_ability_randomization == 2:
|
||||
for enemy in enemy_remap:
|
||||
# we just won't include it for minibosses
|
||||
rom.write_bytes(0xB3E40 + (enemy_remap[enemy] << 1), struct.pack("h", world.random.randint(-1, 2)))
|
||||
|
||||
# write jumping goal
|
||||
rom.write_bytes(0x94F8, struct.pack("H", world.options.jumping_target))
|
||||
rom.write_bytes(0x944E, struct.pack("H", world.options.jumping_target))
|
||||
|
||||
from Utils import __version__
|
||||
rom.name = bytearray(
|
||||
f'KDL3{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0', 'utf8')[:21]
|
||||
rom.name.extend([0] * (21 - len(rom.name)))
|
||||
rom.write_bytes(0x3C000, rom.name)
|
||||
rom.write_byte(0x3C020, world.options.game_language.value)
|
||||
|
||||
# handle palette
|
||||
if world.options.kirby_flavor_preset.value != 0:
|
||||
for addr in kirby_target_palettes:
|
||||
target = kirby_target_palettes[addr]
|
||||
palette = get_kirby_palette(world)
|
||||
rom.write_bytes(addr, get_palette_bytes(palette, target[0], target[1], target[2]))
|
||||
|
||||
if world.options.gooey_flavor_preset.value != 0:
|
||||
for addr in gooey_target_palettes:
|
||||
target = gooey_target_palettes[addr]
|
||||
palette = get_gooey_palette(world)
|
||||
rom.write_bytes(addr, get_palette_bytes(palette, target[0], target[1], target[2]))
|
||||
|
||||
|
||||
def get_base_rom_bytes() -> bytes:
|
||||
rom_file: str = get_base_rom_path()
|
||||
base_rom_bytes: Optional[bytes] = getattr(get_base_rom_bytes, "base_rom_bytes", None)
|
||||
if not base_rom_bytes:
|
||||
base_rom_bytes = bytes(Utils.read_snes_rom(open(rom_file, "rb")))
|
||||
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if basemd5.hexdigest() not in {KDL3UHASH, KDL3JHASH}:
|
||||
raise Exception("Supplied Base Rom does not match known MD5 for US or JP release. "
|
||||
"Get the correct game and version, then dump it")
|
||||
get_base_rom_bytes.base_rom_bytes = base_rom_bytes
|
||||
return base_rom_bytes
|
||||
|
||||
|
||||
def get_base_rom_path(file_name: str = "") -> str:
|
||||
options: settings.Settings = settings.get_settings()
|
||||
if not file_name:
|
||||
file_name = options["kdl3_options"]["rom_file"]
|
||||
if not os.path.exists(file_name):
|
||||
file_name = Utils.user_path(file_name)
|
||||
return file_name
|
||||
@@ -1,95 +0,0 @@
|
||||
import struct
|
||||
import typing
|
||||
from BaseClasses import Region, ItemClassification
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .Rom import RomData
|
||||
|
||||
animal_map = {
|
||||
"Rick Spawn": 0,
|
||||
"Kine Spawn": 1,
|
||||
"Coo Spawn": 2,
|
||||
"Nago Spawn": 3,
|
||||
"ChuChu Spawn": 4,
|
||||
"Pitch Spawn": 5
|
||||
}
|
||||
|
||||
|
||||
class KDL3Room(Region):
|
||||
pointer: int = 0
|
||||
level: int = 0
|
||||
stage: int = 0
|
||||
room: int = 0
|
||||
music: int = 0
|
||||
default_exits: typing.List[typing.Dict[str, typing.Union[int, typing.List[str]]]]
|
||||
animal_pointers: typing.List[int]
|
||||
enemies: typing.List[str]
|
||||
entity_load: typing.List[typing.List[int]]
|
||||
consumables: typing.List[typing.Dict[str, typing.Union[int, str]]]
|
||||
|
||||
def __init__(self, name, player, multiworld, hint, level, stage, room, pointer, music, default_exits,
|
||||
animal_pointers, enemies, entity_load, consumables, consumable_pointer):
|
||||
super().__init__(name, player, multiworld, hint)
|
||||
self.level = level
|
||||
self.stage = stage
|
||||
self.room = room
|
||||
self.pointer = pointer
|
||||
self.music = music
|
||||
self.default_exits = default_exits
|
||||
self.animal_pointers = animal_pointers
|
||||
self.enemies = enemies
|
||||
self.entity_load = entity_load
|
||||
self.consumables = consumables
|
||||
self.consumable_pointer = consumable_pointer
|
||||
|
||||
def patch(self, rom: "RomData"):
|
||||
rom.write_byte(self.pointer + 2, self.music)
|
||||
animals = [x.item.name for x in self.locations if "Animal" in x.name]
|
||||
if len(animals) > 0:
|
||||
for current_animal, address in zip(animals, self.animal_pointers):
|
||||
rom.write_byte(self.pointer + address + 7, animal_map[current_animal])
|
||||
if self.multiworld.worlds[self.player].options.consumables:
|
||||
load_len = len(self.entity_load)
|
||||
for consumable in self.consumables:
|
||||
location = next(x for x in self.locations if x.name == consumable["name"])
|
||||
assert location.item
|
||||
is_progression = location.item.classification & ItemClassification.progression
|
||||
if load_len == 8:
|
||||
# edge case, there is exactly 1 room with 8 entities and only 1 consumable among them
|
||||
if not (any(x in self.entity_load for x in [[0, 22], [1, 22]])
|
||||
and any(x in self.entity_load for x in [[2, 22], [3, 22]])):
|
||||
replacement_target = self.entity_load.index(
|
||||
next(x for x in self.entity_load if x in [[0, 22], [1, 22], [2, 22], [3, 22]]))
|
||||
if is_progression:
|
||||
vtype = 0
|
||||
else:
|
||||
vtype = 2
|
||||
rom.write_byte(self.pointer + 88 + (replacement_target * 2), vtype)
|
||||
self.entity_load[replacement_target] = [vtype, 22]
|
||||
else:
|
||||
if is_progression:
|
||||
# we need to see if 1-ups are in our load list
|
||||
if any(x not in self.entity_load for x in [[0, 22], [1, 22]]):
|
||||
self.entity_load.append([0, 22])
|
||||
else:
|
||||
if any(x not in self.entity_load for x in [[2, 22], [3, 22]]):
|
||||
# edge case: if (1, 22) is in, we need to load (3, 22) instead
|
||||
if [1, 22] in self.entity_load:
|
||||
self.entity_load.append([3, 22])
|
||||
else:
|
||||
self.entity_load.append([2, 22])
|
||||
if load_len < len(self.entity_load):
|
||||
rom.write_bytes(self.pointer + 88 + (load_len * 2), bytes(self.entity_load[load_len]))
|
||||
rom.write_bytes(self.pointer + 104 + (load_len * 2),
|
||||
bytes(struct.pack("H", self.consumable_pointer)))
|
||||
if is_progression:
|
||||
if [1, 22] in self.entity_load:
|
||||
vtype = 1
|
||||
else:
|
||||
vtype = 0
|
||||
else:
|
||||
if [3, 22] in self.entity_load:
|
||||
vtype = 3
|
||||
else:
|
||||
vtype = 2
|
||||
rom.write_byte(self.pointer + consumable["pointer"] + 7, vtype)
|
||||
@@ -1,25 +1,25 @@
|
||||
import logging
|
||||
import typing
|
||||
|
||||
from BaseClasses import Tutorial, ItemClassification, MultiWorld
|
||||
from BaseClasses import Tutorial, ItemClassification, MultiWorld, CollectionState, Item
|
||||
from Fill import fill_restrictive
|
||||
from Options import PerGameCommonOptions
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from .Items import item_table, item_names, copy_ability_table, animal_friend_table, filler_item_weights, KDL3Item, \
|
||||
trap_item_table, copy_ability_access_table, star_item_weights, total_filler_weights
|
||||
from .Locations import location_table, KDL3Location, level_consumables, consumable_locations, star_locations
|
||||
from .Names.AnimalFriendSpawns import animal_friend_spawns
|
||||
from .Names.EnemyAbilities import vanilla_enemies, enemy_mapping, enemy_restrictive
|
||||
from .Regions import create_levels, default_levels
|
||||
from .Options import KDL3Options
|
||||
from .Presets import kdl3_options_presets
|
||||
from .Names import LocationName
|
||||
from .Room import KDL3Room
|
||||
from .Rules import set_rules
|
||||
from .Rom import KDL3DeltaPatch, get_base_rom_path, RomData, patch_rom, KDL3JHASH, KDL3UHASH
|
||||
from .Client import KDL3SNIClient
|
||||
from .items import item_table, item_names, copy_ability_table, animal_friend_table, filler_item_weights, KDL3Item, \
|
||||
trap_item_table, copy_ability_access_table, star_item_weights, total_filler_weights, animal_friend_spawn_table,\
|
||||
lookup_item_to_id
|
||||
from .locations import location_table, KDL3Location, level_consumables, consumable_locations, star_locations
|
||||
from .names.animal_friend_spawns import animal_friend_spawns, problematic_sets
|
||||
from .names.enemy_abilities import vanilla_enemies, enemy_mapping, enemy_restrictive
|
||||
from .regions import create_levels, default_levels
|
||||
from .options import KDL3Options, kdl3_option_groups
|
||||
from .presets import kdl3_options_presets
|
||||
from .names import location_name
|
||||
from .room import KDL3Room
|
||||
from .rules import set_rules
|
||||
from .rom import KDL3ProcedurePatch, get_base_rom_path, patch_rom, KDL3JHASH, KDL3UHASH
|
||||
from .client import KDL3SNIClient
|
||||
|
||||
from typing import Dict, TextIO, Optional, List
|
||||
from typing import Dict, TextIO, Optional, List, Any, Mapping, ClassVar, Type
|
||||
import os
|
||||
import math
|
||||
import threading
|
||||
@@ -53,6 +53,7 @@ class KDL3WebWorld(WebWorld):
|
||||
)
|
||||
]
|
||||
options_presets = kdl3_options_presets
|
||||
option_groups = kdl3_option_groups
|
||||
|
||||
|
||||
class KDL3World(World):
|
||||
@@ -61,35 +62,35 @@ class KDL3World(World):
|
||||
"""
|
||||
|
||||
game = "Kirby's Dream Land 3"
|
||||
options_dataclass: typing.ClassVar[typing.Type[PerGameCommonOptions]] = KDL3Options
|
||||
options_dataclass: ClassVar[Type[PerGameCommonOptions]] = KDL3Options
|
||||
options: KDL3Options
|
||||
item_name_to_id = {item: item_table[item].code for item in item_table}
|
||||
item_name_to_id = lookup_item_to_id
|
||||
location_name_to_id = {location_table[location]: location for location in location_table}
|
||||
item_name_groups = item_names
|
||||
web = KDL3WebWorld()
|
||||
settings: typing.ClassVar[KDL3Settings]
|
||||
settings: ClassVar[KDL3Settings]
|
||||
|
||||
def __init__(self, multiworld: MultiWorld, player: int):
|
||||
self.rom_name = None
|
||||
self.rom_name: bytes = bytes()
|
||||
self.rom_name_available_event = threading.Event()
|
||||
super().__init__(multiworld, player)
|
||||
self.copy_abilities: Dict[str, str] = vanilla_enemies.copy()
|
||||
self.required_heart_stars: int = 0 # we fill this during create_items
|
||||
self.boss_requirements: Dict[int, int] = dict()
|
||||
self.boss_requirements: List[int] = []
|
||||
self.player_levels = default_levels.copy()
|
||||
self.stage_shuffle_enabled = False
|
||||
self.boss_butch_bosses: List[Optional[bool]] = list()
|
||||
self.rooms: Optional[List[KDL3Room]] = None
|
||||
|
||||
@classmethod
|
||||
def stage_assert_generate(cls, multiworld: MultiWorld) -> None:
|
||||
rom_file: str = get_base_rom_path()
|
||||
if not os.path.exists(rom_file):
|
||||
raise FileNotFoundError(f"Could not find base ROM for {cls.game}: {rom_file}")
|
||||
self.boss_butch_bosses: List[Optional[bool]] = []
|
||||
self.rooms: List[KDL3Room] = []
|
||||
|
||||
create_regions = create_levels
|
||||
|
||||
def create_item(self, name: str, force_non_progression=False) -> KDL3Item:
|
||||
def generate_early(self) -> None:
|
||||
if self.options.total_heart_stars != -1:
|
||||
logger.warning(f"Kirby's Dream Land 3 ({self.player_name}): Use of \"total_heart_stars\" is deprecated. "
|
||||
f"Please use \"max_heart_stars\" instead.")
|
||||
self.options.max_heart_stars.value = self.options.total_heart_stars.value
|
||||
|
||||
def create_item(self, name: str, force_non_progression: bool = False) -> KDL3Item:
|
||||
item = item_table[name]
|
||||
classification = ItemClassification.filler
|
||||
if item.progression and not force_non_progression:
|
||||
@@ -99,7 +100,7 @@ class KDL3World(World):
|
||||
classification = ItemClassification.trap
|
||||
return KDL3Item(name, classification, item.code, self.player)
|
||||
|
||||
def get_filler_item_name(self, include_stars=True) -> str:
|
||||
def get_filler_item_name(self, include_stars: bool = True) -> str:
|
||||
if include_stars:
|
||||
return self.random.choices(list(total_filler_weights.keys()),
|
||||
weights=list(total_filler_weights.values()))[0]
|
||||
@@ -112,8 +113,8 @@ class KDL3World(World):
|
||||
self.options.slow_trap_weight.value,
|
||||
self.options.ability_trap_weight.value])[0]
|
||||
|
||||
def get_restrictive_copy_ability_placement(self, copy_ability: str, enemies_to_set: typing.List[str],
|
||||
level: int, stage: int):
|
||||
def get_restrictive_copy_ability_placement(self, copy_ability: str, enemies_to_set: List[str],
|
||||
level: int, stage: int) -> Optional[str]:
|
||||
valid_rooms = [room for room in self.rooms if (room.level < level)
|
||||
or (room.level == level and room.stage < stage)] # leave out the stage in question to avoid edge
|
||||
valid_enemies = set()
|
||||
@@ -124,6 +125,10 @@ class KDL3World(World):
|
||||
return None # a valid enemy got placed by a more restrictive placement
|
||||
return self.random.choice(sorted([enemy for enemy in valid_enemies if enemy not in placed_enemies]))
|
||||
|
||||
def get_pre_fill_items(self) -> List[Item]:
|
||||
return [self.create_item(item)
|
||||
for item in [*copy_ability_access_table.keys(), *animal_friend_spawn_table.keys()]]
|
||||
|
||||
def pre_fill(self) -> None:
|
||||
if self.options.copy_ability_randomization:
|
||||
# randomize copy abilities
|
||||
@@ -196,21 +201,40 @@ class KDL3World(World):
|
||||
else:
|
||||
animal_base = ["Rick Spawn", "Kine Spawn", "Coo Spawn", "Nago Spawn", "ChuChu Spawn", "Pitch Spawn"]
|
||||
animal_pool = [self.random.choice(animal_base)
|
||||
for _ in range(len(animal_friend_spawns) - 9)]
|
||||
for _ in range(len(animal_friend_spawns) - 10)]
|
||||
# have to guarantee one of each animal
|
||||
animal_pool.extend(animal_base)
|
||||
if guaranteed_animal == "Kine Spawn":
|
||||
animal_pool.append("Coo Spawn")
|
||||
else:
|
||||
animal_pool.append("Kine Spawn")
|
||||
# Weird fill hack, this forces ChuChu to be the last animal friend placed
|
||||
# If Kine is ever the last animal friend placed, he will cause fill errors on closed world
|
||||
animal_pool.sort()
|
||||
locations = [self.multiworld.get_location(spawn, self.player) for spawn in spawns]
|
||||
items = [self.create_item(animal) for animal in animal_pool]
|
||||
allstate = self.multiworld.get_all_state(False)
|
||||
items: List[Item] = [self.create_item(animal) for animal in animal_pool]
|
||||
allstate = CollectionState(self.multiworld)
|
||||
for item in [*copy_ability_table, *animal_friend_table, *["Heart Star" for _ in range(99)]]:
|
||||
self.collect(allstate, self.create_item(item))
|
||||
self.random.shuffle(locations)
|
||||
fill_restrictive(self.multiworld, allstate, locations, items, True, True)
|
||||
|
||||
# Need to ensure all of these are unique items, and replace them if they aren't
|
||||
for spawns in problematic_sets:
|
||||
placed = [self.get_location(spawn).item for spawn in spawns]
|
||||
placed_names = set([item.name for item in placed])
|
||||
if len(placed_names) != len(placed):
|
||||
# have a duplicate
|
||||
animals = []
|
||||
for spawn in spawns:
|
||||
spawn_location = self.get_location(spawn)
|
||||
if spawn_location.item.name not in animals:
|
||||
animals.append(spawn_location.item.name)
|
||||
else:
|
||||
new_animal = self.random.choice([x for x in ["Rick Spawn", "Coo Spawn", "Kine Spawn",
|
||||
"ChuChu Spawn", "Nago Spawn", "Pitch Spawn"]
|
||||
if x not in placed_names and x not in animals])
|
||||
spawn_location.item = None
|
||||
spawn_location.place_locked_item(self.create_item(new_animal))
|
||||
animals.append(new_animal)
|
||||
# logically, this should be sound pre-ER. May need to adjust around it with ER in the future
|
||||
else:
|
||||
animal_friends = animal_friend_spawns.copy()
|
||||
for animal in animal_friends:
|
||||
@@ -225,21 +249,20 @@ class KDL3World(World):
|
||||
remaining_items = len(location_table) - len(itempool)
|
||||
if not self.options.consumables:
|
||||
remaining_items -= len(consumable_locations)
|
||||
remaining_items -= len(star_locations)
|
||||
if self.options.starsanity:
|
||||
# star fill, keep consumable pool locked to consumable and fill 767 stars specifically
|
||||
star_items = list(star_item_weights.keys())
|
||||
star_weights = list(star_item_weights.values())
|
||||
itempool.extend([self.create_item(item) for item in self.random.choices(star_items, weights=star_weights,
|
||||
k=767)])
|
||||
total_heart_stars = self.options.total_heart_stars
|
||||
if not self.options.starsanity:
|
||||
remaining_items -= len(star_locations)
|
||||
max_heart_stars = self.options.max_heart_stars.value
|
||||
if max_heart_stars > remaining_items:
|
||||
max_heart_stars = remaining_items
|
||||
# ensure at least 1 heart star required per world
|
||||
required_heart_stars = max(int(total_heart_stars * required_percentage), 5)
|
||||
filler_items = total_heart_stars - required_heart_stars
|
||||
filler_amount = math.floor(filler_items * (self.options.filler_percentage / 100.0))
|
||||
trap_amount = math.floor(filler_amount * (self.options.trap_percentage / 100.0))
|
||||
filler_amount -= trap_amount
|
||||
non_required_heart_stars = filler_items - filler_amount - trap_amount
|
||||
required_heart_stars = min(max(int(max_heart_stars * required_percentage), 5), 99)
|
||||
filler_items = remaining_items - required_heart_stars
|
||||
converted_heart_stars = math.floor((max_heart_stars - required_heart_stars) * (self.options.filler_percentage / 100.0))
|
||||
non_required_heart_stars = max_heart_stars - converted_heart_stars - required_heart_stars
|
||||
filler_items -= non_required_heart_stars
|
||||
trap_amount = math.floor(filler_items * (self.options.trap_percentage / 100.0))
|
||||
|
||||
filler_items -= trap_amount
|
||||
self.required_heart_stars = required_heart_stars
|
||||
# handle boss requirements here
|
||||
requirements = [required_heart_stars]
|
||||
@@ -261,8 +284,8 @@ class KDL3World(World):
|
||||
requirements.insert(i - 1, quotient * i)
|
||||
self.boss_requirements = requirements
|
||||
itempool.extend([self.create_item("Heart Star") for _ in range(required_heart_stars)])
|
||||
itempool.extend([self.create_item(self.get_filler_item_name(False))
|
||||
for _ in range(filler_amount + (remaining_items - total_heart_stars))])
|
||||
itempool.extend([self.create_item(self.get_filler_item_name(bool(self.options.starsanity.value)))
|
||||
for _ in range(filler_items)])
|
||||
itempool.extend([self.create_item(self.get_trap_item_name())
|
||||
for _ in range(trap_amount)])
|
||||
itempool.extend([self.create_item("Heart Star", True) for _ in range(non_required_heart_stars)])
|
||||
@@ -273,15 +296,15 @@ class KDL3World(World):
|
||||
self.multiworld.get_location(location_table[self.player_levels[level][stage]]
|
||||
.replace("Complete", "Stage Completion"), self.player) \
|
||||
.place_locked_item(KDL3Item(
|
||||
f"{LocationName.level_names_inverse[level]} - Stage Completion",
|
||||
f"{location_name.level_names_inverse[level]} - Stage Completion",
|
||||
ItemClassification.progression, None, self.player))
|
||||
|
||||
set_rules = set_rules
|
||||
|
||||
def generate_basic(self) -> None:
|
||||
self.stage_shuffle_enabled = self.options.stage_shuffle > 0
|
||||
goal = self.options.goal
|
||||
goal_location = self.multiworld.get_location(LocationName.goals[goal], self.player)
|
||||
goal = self.options.goal.value
|
||||
goal_location = self.multiworld.get_location(location_name.goals[goal], self.player)
|
||||
goal_location.place_locked_item(KDL3Item("Love-Love Rod", ItemClassification.progression, None, self.player))
|
||||
for level in range(1, 6):
|
||||
self.multiworld.get_location(f"Level {level} Boss - Defeated", self.player) \
|
||||
@@ -300,60 +323,65 @@ class KDL3World(World):
|
||||
else:
|
||||
self.boss_butch_bosses = [False for _ in range(6)]
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
rom_path = ""
|
||||
def generate_output(self, output_directory: str) -> None:
|
||||
try:
|
||||
rom = RomData(get_base_rom_path())
|
||||
patch_rom(self, rom)
|
||||
patch = KDL3ProcedurePatch()
|
||||
patch_rom(self, patch)
|
||||
|
||||
rom_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc")
|
||||
rom.write_to_file(rom_path)
|
||||
self.rom_name = rom.name
|
||||
self.rom_name = patch.name
|
||||
|
||||
patch = KDL3DeltaPatch(os.path.splitext(rom_path)[0] + KDL3DeltaPatch.patch_file_ending, player=self.player,
|
||||
player_name=self.multiworld.player_name[self.player], patched_path=rom_path)
|
||||
patch.write()
|
||||
patch.write(os.path.join(output_directory,
|
||||
f"{self.multiworld.get_out_file_name_base(self.player)}{patch.patch_file_ending}"))
|
||||
except Exception:
|
||||
raise
|
||||
finally:
|
||||
self.rom_name_available_event.set() # make sure threading continues and errors are collected
|
||||
if os.path.exists(rom_path):
|
||||
os.unlink(rom_path)
|
||||
|
||||
def modify_multidata(self, multidata: dict):
|
||||
def modify_multidata(self, multidata: Dict[str, Any]) -> None:
|
||||
# wait for self.rom_name to be available.
|
||||
self.rom_name_available_event.wait()
|
||||
assert isinstance(self.rom_name, bytes)
|
||||
rom_name = getattr(self, "rom_name", None)
|
||||
# we skip in case of error, so that the original error in the output thread is the one that gets raised
|
||||
if rom_name:
|
||||
new_name = base64.b64encode(bytes(self.rom_name)).decode()
|
||||
new_name = base64.b64encode(self.rom_name).decode()
|
||||
multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]]
|
||||
|
||||
def fill_slot_data(self) -> Mapping[str, Any]:
|
||||
# UT support
|
||||
return {"player_levels": self.player_levels}
|
||||
|
||||
def interpret_slot_data(self, slot_data: Mapping[str, Any]):
|
||||
# UT support
|
||||
player_levels = {int(key): value for key, value in slot_data["player_levels"].items()}
|
||||
return {"player_levels": player_levels}
|
||||
|
||||
def write_spoiler(self, spoiler_handle: TextIO) -> None:
|
||||
if self.stage_shuffle_enabled:
|
||||
spoiler_handle.write(f"\nLevel Layout ({self.multiworld.get_player_name(self.player)}):\n")
|
||||
for level in LocationName.level_names:
|
||||
for stage, i in zip(self.player_levels[LocationName.level_names[level]], range(1, 7)):
|
||||
for level in location_name.level_names:
|
||||
for stage, i in zip(self.player_levels[location_name.level_names[level]], range(1, 7)):
|
||||
spoiler_handle.write(f"{level} {i}: {location_table[stage].replace(' - Complete', '')}\n")
|
||||
if self.options.animal_randomization:
|
||||
spoiler_handle.write(f"\nAnimal Friends ({self.multiworld.get_player_name(self.player)}):\n")
|
||||
for level in self.player_levels:
|
||||
for lvl in self.player_levels:
|
||||
for stage in range(6):
|
||||
rooms = [room for room in self.rooms if room.level == level and room.stage == stage]
|
||||
rooms = [room for room in self.rooms if room.level == lvl and room.stage == stage]
|
||||
animals = []
|
||||
for room in rooms:
|
||||
animals.extend([location.item.name.replace(" Spawn", "")
|
||||
for location in room.locations if "Animal" in location.name])
|
||||
spoiler_handle.write(f"{location_table[self.player_levels[level][stage]].replace(' - Complete','')}"
|
||||
for location in room.locations if "Animal" in location.name
|
||||
and location.item is not None])
|
||||
spoiler_handle.write(f"{location_table[self.player_levels[lvl][stage]].replace(' - Complete','')}"
|
||||
f": {', '.join(animals)}\n")
|
||||
if self.options.copy_ability_randomization:
|
||||
spoiler_handle.write(f"\nCopy Abilities ({self.multiworld.get_player_name(self.player)}):\n")
|
||||
for enemy in self.copy_abilities:
|
||||
spoiler_handle.write(f"{enemy}: {self.copy_abilities[enemy].replace('No Ability', 'None').replace(' Ability', '')}\n")
|
||||
|
||||
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]):
|
||||
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None:
|
||||
if self.stage_shuffle_enabled:
|
||||
regions = {LocationName.level_names[level]: level for level in LocationName.level_names}
|
||||
regions = {location_name.level_names[level]: level for level in location_name.level_names}
|
||||
level_hint_data = {}
|
||||
for level in regions:
|
||||
for stage in range(7):
|
||||
@@ -361,6 +389,6 @@ class KDL3World(World):
|
||||
self.player).name.replace(" - Complete", "")
|
||||
stage_regions = [room for room in self.rooms if stage_name in room.name]
|
||||
for region in stage_regions:
|
||||
for location in [location for location in region.locations if location.address]:
|
||||
for location in [location for location in list(region.get_locations()) if location.address]:
|
||||
level_hint_data[location.address] = f"{regions[level]} {stage + 1 if stage < 6 else 'Boss'}"
|
||||
hint_data[self.player] = level_hint_data
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import struct
|
||||
from .Options import KirbyFlavorPreset, GooeyFlavorPreset
|
||||
from .options import KirbyFlavorPreset, GooeyFlavorPreset
|
||||
from typing import TYPE_CHECKING, Optional, Dict, List, Tuple
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import KDL3World
|
||||
|
||||
kirby_flavor_presets = {
|
||||
1: {
|
||||
@@ -223,6 +227,23 @@ kirby_flavor_presets = {
|
||||
"14": "E6E6FA",
|
||||
"15": "976FBD",
|
||||
},
|
||||
14: {
|
||||
"1": "373B3E",
|
||||
"2": "98d5d3",
|
||||
"3": "1aa5ab",
|
||||
"4": "168f95",
|
||||
"5": "4f5559",
|
||||
"6": "1dbac2",
|
||||
"7": "137a7f",
|
||||
"8": "093a3c",
|
||||
"9": "86cecb",
|
||||
"10": "a0afbc",
|
||||
"11": "62bfbb",
|
||||
"12": "50b8b4",
|
||||
"13": "bec8d1",
|
||||
"14": "bce4e2",
|
||||
"15": "91a2b1",
|
||||
}
|
||||
}
|
||||
|
||||
gooey_flavor_presets = {
|
||||
@@ -398,21 +419,21 @@ gooey_target_palettes = {
|
||||
}
|
||||
|
||||
|
||||
def get_kirby_palette(world):
|
||||
def get_kirby_palette(world: "KDL3World") -> Optional[Dict[str, str]]:
|
||||
palette = world.options.kirby_flavor_preset.value
|
||||
if palette == KirbyFlavorPreset.option_custom:
|
||||
return world.options.kirby_flavor.value
|
||||
return kirby_flavor_presets.get(palette, None)
|
||||
|
||||
|
||||
def get_gooey_palette(world):
|
||||
def get_gooey_palette(world: "KDL3World") -> Optional[Dict[str, str]]:
|
||||
palette = world.options.gooey_flavor_preset.value
|
||||
if palette == GooeyFlavorPreset.option_custom:
|
||||
return world.options.gooey_flavor.value
|
||||
return gooey_flavor_presets.get(palette, None)
|
||||
|
||||
|
||||
def rgb888_to_bgr555(red, green, blue) -> bytes:
|
||||
def rgb888_to_bgr555(red: int, green: int, blue: int) -> bytes:
|
||||
red = red >> 3
|
||||
green = green >> 3
|
||||
blue = blue >> 3
|
||||
@@ -420,15 +441,15 @@ def rgb888_to_bgr555(red, green, blue) -> bytes:
|
||||
return struct.pack("H", outcol)
|
||||
|
||||
|
||||
def get_palette_bytes(palette, target, offset, factor):
|
||||
def get_palette_bytes(palette: Dict[str, str], target: List[str], offset: int, factor: float) -> bytes:
|
||||
output_data = bytearray()
|
||||
for color in target:
|
||||
hexcol = palette[color]
|
||||
if hexcol.startswith("#"):
|
||||
hexcol = hexcol.replace("#", "")
|
||||
colint = int(hexcol, 16)
|
||||
col = ((colint & 0xFF0000) >> 16, (colint & 0xFF00) >> 8, colint & 0xFF)
|
||||
col: Tuple[int, ...] = ((colint & 0xFF0000) >> 16, (colint & 0xFF00) >> 8, colint & 0xFF)
|
||||
col = tuple(int(int(factor*x) + offset) for x in col)
|
||||
byte_data = rgb888_to_bgr555(col[0], col[1], col[2])
|
||||
output_data.extend(bytearray(byte_data))
|
||||
return output_data
|
||||
return bytes(output_data)
|
||||
@@ -11,13 +11,13 @@ from MultiServer import mark_raw
|
||||
from NetUtils import ClientStatus, color
|
||||
from Utils import async_start
|
||||
from worlds.AutoSNIClient import SNIClient
|
||||
from .Locations import boss_locations
|
||||
from .Gifting import kdl3_gifting_options, kdl3_trap_gifts, kdl3_gifts, update_object, pop_object, initialize_giftboxes
|
||||
from .ClientAddrs import consumable_addrs, star_addrs
|
||||
from .locations import boss_locations
|
||||
from .gifting import kdl3_gifting_options, kdl3_trap_gifts, kdl3_gifts, update_object, pop_object, initialize_giftboxes
|
||||
from .client_addrs import consumable_addrs, star_addrs
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from SNIClient import SNIClientCommandProcessor
|
||||
from SNIClient import SNIClientCommandProcessor, SNIContext
|
||||
|
||||
snes_logger = logging.getLogger("SNES")
|
||||
|
||||
@@ -81,17 +81,16 @@ deathlink_messages = defaultdict(lambda: " was defeated.", {
|
||||
|
||||
|
||||
@mark_raw
|
||||
def cmd_gift(self: "SNIClientCommandProcessor"):
|
||||
def cmd_gift(self: "SNIClientCommandProcessor") -> None:
|
||||
"""Toggles gifting for the current game."""
|
||||
if not getattr(self.ctx, "gifting", None):
|
||||
self.ctx.gifting = True
|
||||
else:
|
||||
self.ctx.gifting = not self.ctx.gifting
|
||||
self.output(f"Gifting set to {self.ctx.gifting}")
|
||||
handler = self.ctx.client_handler
|
||||
assert isinstance(handler, KDL3SNIClient)
|
||||
handler.gifting = not handler.gifting
|
||||
self.output(f"Gifting set to {handler.gifting}")
|
||||
async_start(update_object(self.ctx, f"Giftboxes;{self.ctx.team}", {
|
||||
f"{self.ctx.slot}":
|
||||
{
|
||||
"IsOpen": self.ctx.gifting,
|
||||
"IsOpen": handler.gifting,
|
||||
**kdl3_gifting_options
|
||||
}
|
||||
}))
|
||||
@@ -100,16 +99,17 @@ def cmd_gift(self: "SNIClientCommandProcessor"):
|
||||
class KDL3SNIClient(SNIClient):
|
||||
game = "Kirby's Dream Land 3"
|
||||
patch_suffix = ".apkdl3"
|
||||
levels = None
|
||||
consumables = None
|
||||
stars = None
|
||||
item_queue: typing.List = []
|
||||
initialize_gifting = False
|
||||
levels: typing.Dict[int, typing.List[int]] = {}
|
||||
consumables: typing.Optional[bool] = None
|
||||
stars: typing.Optional[bool] = None
|
||||
item_queue: typing.List[int] = []
|
||||
initialize_gifting: bool = False
|
||||
gifting: bool = False
|
||||
giftbox_key: str = ""
|
||||
motherbox_key: str = ""
|
||||
client_random: random.Random = random.Random()
|
||||
|
||||
async def deathlink_kill_player(self, ctx) -> None:
|
||||
async def deathlink_kill_player(self, ctx: "SNIContext") -> None:
|
||||
from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read
|
||||
game_state = await snes_read(ctx, KDL3_GAME_STATE, 1)
|
||||
if game_state[0] == 0xFF:
|
||||
@@ -131,7 +131,7 @@ class KDL3SNIClient(SNIClient):
|
||||
ctx.death_state = DeathState.dead
|
||||
ctx.last_death_link = time.time()
|
||||
|
||||
async def validate_rom(self, ctx) -> bool:
|
||||
async def validate_rom(self, ctx: "SNIContext") -> bool:
|
||||
from SNIClient import snes_read
|
||||
rom_name = await snes_read(ctx, KDL3_ROMNAME, 0x15)
|
||||
if rom_name is None or rom_name == bytes([0] * 0x15) or rom_name[:4] != b"KDL3":
|
||||
@@ -141,7 +141,7 @@ class KDL3SNIClient(SNIClient):
|
||||
|
||||
ctx.game = self.game
|
||||
ctx.rom = rom_name
|
||||
ctx.items_handling = 0b111 # always remote items
|
||||
ctx.items_handling = 0b101 # default local items with remote start inventory
|
||||
ctx.allow_collect = True
|
||||
if "gift" not in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands["gift"] = cmd_gift
|
||||
@@ -149,9 +149,10 @@ class KDL3SNIClient(SNIClient):
|
||||
death_link = await snes_read(ctx, KDL3_DEATH_LINK_ADDR, 1)
|
||||
if death_link:
|
||||
await ctx.update_death_link(bool(death_link[0] & 0b1))
|
||||
ctx.items_handling |= (death_link[0] & 0b10) # set local items if enabled
|
||||
return True
|
||||
|
||||
async def pop_item(self, ctx, in_stage):
|
||||
async def pop_item(self, ctx: "SNIContext", in_stage: bool) -> None:
|
||||
from SNIClient import snes_buffered_write, snes_read
|
||||
if len(self.item_queue) > 0:
|
||||
item = self.item_queue.pop()
|
||||
@@ -168,8 +169,8 @@ class KDL3SNIClient(SNIClient):
|
||||
else:
|
||||
self.item_queue.append(item) # no more slots, get it next go around
|
||||
|
||||
async def pop_gift(self, ctx):
|
||||
if ctx.stored_data[self.giftbox_key]:
|
||||
async def pop_gift(self, ctx: "SNIContext") -> None:
|
||||
if self.giftbox_key in ctx.stored_data and ctx.stored_data[self.giftbox_key]:
|
||||
from SNIClient import snes_read, snes_buffered_write
|
||||
key, gift = ctx.stored_data[self.giftbox_key].popitem()
|
||||
await pop_object(ctx, self.giftbox_key, key)
|
||||
@@ -214,7 +215,7 @@ class KDL3SNIClient(SNIClient):
|
||||
quality = min(10, quality * 2)
|
||||
else:
|
||||
# it's not really edible, but he'll eat it anyway
|
||||
quality = self.client_random.choices(range(0, 2), {0: 75, 1: 25})[0]
|
||||
quality = self.client_random.choices(range(0, 2), [75, 25])[0]
|
||||
kirby_hp = await snes_read(ctx, KDL3_KIRBY_HP, 1)
|
||||
gooey_hp = await snes_read(ctx, KDL3_KIRBY_HP + 2, 1)
|
||||
snes_buffered_write(ctx, KDL3_SOUND_FX, bytes([0x26]))
|
||||
@@ -224,7 +225,8 @@ class KDL3SNIClient(SNIClient):
|
||||
else:
|
||||
snes_buffered_write(ctx, KDL3_KIRBY_HP, struct.pack("H", min(kirby_hp[0] + quality, 10)))
|
||||
|
||||
async def pick_gift_recipient(self, ctx, gift):
|
||||
async def pick_gift_recipient(self, ctx: "SNIContext", gift: int) -> None:
|
||||
assert ctx.slot
|
||||
if gift != 4:
|
||||
gift_base = kdl3_gifts[gift]
|
||||
else:
|
||||
@@ -238,7 +240,7 @@ class KDL3SNIClient(SNIClient):
|
||||
if desire > most_applicable:
|
||||
most_applicable = desire
|
||||
most_applicable_slot = int(slot)
|
||||
elif most_applicable_slot == ctx.slot and info["AcceptsAnyGift"]:
|
||||
elif most_applicable_slot != ctx.slot and most_applicable == -1 and info["AcceptsAnyGift"]:
|
||||
# only send to ourselves if no one else will take it
|
||||
most_applicable_slot = int(slot)
|
||||
# print(most_applicable, most_applicable_slot)
|
||||
@@ -257,7 +259,7 @@ class KDL3SNIClient(SNIClient):
|
||||
item_uuid: item,
|
||||
})
|
||||
|
||||
async def game_watcher(self, ctx) -> None:
|
||||
async def game_watcher(self, ctx: "SNIContext") -> None:
|
||||
try:
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
||||
rom = await snes_read(ctx, KDL3_ROMNAME, 0x15)
|
||||
@@ -278,11 +280,12 @@ class KDL3SNIClient(SNIClient):
|
||||
await initialize_giftboxes(ctx, self.giftbox_key, self.motherbox_key, bool(enable_gifting[0]))
|
||||
self.initialize_gifting = True
|
||||
# can't check debug anymore, without going and copying the value. might be important later.
|
||||
if self.levels is None:
|
||||
if not self.levels:
|
||||
self.levels = dict()
|
||||
for i in range(5):
|
||||
level_data = await snes_read(ctx, KDL3_LEVEL_ADDR + (14 * i), 14)
|
||||
self.levels[i] = unpack("HHHHHHH", level_data)
|
||||
self.levels[i] = [int.from_bytes(level_data[idx:idx+1], "little")
|
||||
for idx in range(0, len(level_data), 2)]
|
||||
self.levels[5] = [0x0205, # Hyper Zone
|
||||
0, # MG-5, can't send from here
|
||||
0x0300, # Boss Butch
|
||||
@@ -371,7 +374,7 @@ class KDL3SNIClient(SNIClient):
|
||||
stages_raw = await snes_read(ctx, KDL3_COMPLETED_STAGES, 60)
|
||||
stages = struct.unpack("HHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", stages_raw)
|
||||
for i in range(30):
|
||||
loc_id = 0x770000 + i + 1
|
||||
loc_id = 0x770000 + i
|
||||
if stages[i] == 1 and loc_id not in ctx.checked_locations:
|
||||
new_checks.append(loc_id)
|
||||
elif loc_id in ctx.checked_locations:
|
||||
@@ -381,8 +384,8 @@ class KDL3SNIClient(SNIClient):
|
||||
heart_stars = await snes_read(ctx, KDL3_HEART_STARS, 35)
|
||||
for i in range(5):
|
||||
start_ind = i * 7
|
||||
for j in range(1, 7):
|
||||
level_ind = start_ind + j - 1
|
||||
for j in range(6):
|
||||
level_ind = start_ind + j
|
||||
loc_id = 0x770100 + (6 * i) + j
|
||||
if heart_stars[level_ind] and loc_id not in ctx.checked_locations:
|
||||
new_checks.append(loc_id)
|
||||
@@ -401,6 +404,9 @@ class KDL3SNIClient(SNIClient):
|
||||
if star not in ctx.checked_locations and stars[star_addrs[star]] == 0x01:
|
||||
new_checks.append(star)
|
||||
|
||||
if not game_state:
|
||||
return
|
||||
|
||||
if game_state[0] != 0xFF:
|
||||
await self.pop_gift(ctx)
|
||||
await self.pop_item(ctx, game_state[0] != 0xFF)
|
||||
@@ -408,7 +414,7 @@ class KDL3SNIClient(SNIClient):
|
||||
|
||||
# boss status
|
||||
boss_flag_bytes = await snes_read(ctx, KDL3_BOSS_STATUS, 2)
|
||||
boss_flag = unpack("H", boss_flag_bytes)[0]
|
||||
boss_flag = int.from_bytes(boss_flag_bytes, "little")
|
||||
for bitmask, boss in zip(range(1, 11, 2), boss_locations.keys()):
|
||||
if boss_flag & (1 << bitmask) > 0 and boss not in ctx.checked_locations:
|
||||
new_checks.append(boss)
|
||||
Binary file not shown.
@@ -1,8 +1,11 @@
|
||||
# Small subfile to handle gifting info such as desired traits and giftbox management
|
||||
import typing
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from SNIClient import SNIContext
|
||||
|
||||
async def update_object(ctx, key: str, value: typing.Dict):
|
||||
|
||||
async def update_object(ctx: "SNIContext", key: str, value: typing.Dict[str, typing.Any]) -> None:
|
||||
await ctx.send_msgs([
|
||||
{
|
||||
"cmd": "Set",
|
||||
@@ -16,7 +19,7 @@ async def update_object(ctx, key: str, value: typing.Dict):
|
||||
])
|
||||
|
||||
|
||||
async def pop_object(ctx, key: str, value: str):
|
||||
async def pop_object(ctx: "SNIContext", key: str, value: str) -> None:
|
||||
await ctx.send_msgs([
|
||||
{
|
||||
"cmd": "Set",
|
||||
@@ -30,14 +33,14 @@ async def pop_object(ctx, key: str, value: str):
|
||||
])
|
||||
|
||||
|
||||
async def initialize_giftboxes(ctx, giftbox_key: str, motherbox_key: str, is_open: bool):
|
||||
async def initialize_giftboxes(ctx: "SNIContext", giftbox_key: str, motherbox_key: str, is_open: bool) -> None:
|
||||
ctx.set_notify(motherbox_key, giftbox_key)
|
||||
await update_object(ctx, f"Giftboxes;{ctx.team}", {f"{ctx.slot}":
|
||||
{
|
||||
"IsOpen": is_open,
|
||||
**kdl3_gifting_options
|
||||
}})
|
||||
ctx.gifting = is_open
|
||||
{
|
||||
"IsOpen": is_open,
|
||||
**kdl3_gifting_options
|
||||
}})
|
||||
ctx.client_handler.gifting = is_open
|
||||
|
||||
|
||||
kdl3_gifting_options = {
|
||||
@@ -77,9 +77,9 @@ filler_item_weights = {
|
||||
}
|
||||
|
||||
star_item_weights = {
|
||||
"Little Star": 4,
|
||||
"Medium Star": 2,
|
||||
"Big Star": 1
|
||||
"Little Star": 16,
|
||||
"Medium Star": 8,
|
||||
"Big Star": 4
|
||||
}
|
||||
|
||||
total_filler_weights = {
|
||||
@@ -102,4 +102,4 @@ item_names = {
|
||||
"Animal Friend": set(animal_friend_table),
|
||||
}
|
||||
|
||||
lookup_name_to_id: typing.Dict[str, int] = {item_name: data.code for item_name, data in item_table.items() if data.code}
|
||||
lookup_item_to_id: typing.Dict[str, int] = {item_name: data.code for item_name, data in item_table.items() if data.code}
|
||||
940
worlds/kdl3/locations.py
Normal file
940
worlds/kdl3/locations.py
Normal file
@@ -0,0 +1,940 @@
|
||||
import typing
|
||||
from BaseClasses import Location, Region
|
||||
from .names import location_name
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .room import KDL3Room
|
||||
|
||||
|
||||
class KDL3Location(Location):
|
||||
game: str = "Kirby's Dream Land 3"
|
||||
room: typing.Optional["KDL3Room"] = None
|
||||
|
||||
def __init__(self, player: int, name: str, address: typing.Optional[int], parent: typing.Union[Region, None]):
|
||||
super().__init__(player, name, address, parent)
|
||||
if not address:
|
||||
self.show_in_spoiler = False
|
||||
|
||||
|
||||
stage_locations = {
|
||||
0x770000: location_name.grass_land_1,
|
||||
0x770001: location_name.grass_land_2,
|
||||
0x770002: location_name.grass_land_3,
|
||||
0x770003: location_name.grass_land_4,
|
||||
0x770004: location_name.grass_land_5,
|
||||
0x770005: location_name.grass_land_6,
|
||||
0x770006: location_name.ripple_field_1,
|
||||
0x770007: location_name.ripple_field_2,
|
||||
0x770008: location_name.ripple_field_3,
|
||||
0x770009: location_name.ripple_field_4,
|
||||
0x77000A: location_name.ripple_field_5,
|
||||
0x77000B: location_name.ripple_field_6,
|
||||
0x77000C: location_name.sand_canyon_1,
|
||||
0x77000D: location_name.sand_canyon_2,
|
||||
0x77000E: location_name.sand_canyon_3,
|
||||
0x77000F: location_name.sand_canyon_4,
|
||||
0x770010: location_name.sand_canyon_5,
|
||||
0x770011: location_name.sand_canyon_6,
|
||||
0x770012: location_name.cloudy_park_1,
|
||||
0x770013: location_name.cloudy_park_2,
|
||||
0x770014: location_name.cloudy_park_3,
|
||||
0x770015: location_name.cloudy_park_4,
|
||||
0x770016: location_name.cloudy_park_5,
|
||||
0x770017: location_name.cloudy_park_6,
|
||||
0x770018: location_name.iceberg_1,
|
||||
0x770019: location_name.iceberg_2,
|
||||
0x77001A: location_name.iceberg_3,
|
||||
0x77001B: location_name.iceberg_4,
|
||||
0x77001C: location_name.iceberg_5,
|
||||
0x77001D: location_name.iceberg_6,
|
||||
}
|
||||
|
||||
heart_star_locations = {
|
||||
0x770100: location_name.grass_land_tulip,
|
||||
0x770101: location_name.grass_land_muchi,
|
||||
0x770102: location_name.grass_land_pitcherman,
|
||||
0x770103: location_name.grass_land_chao,
|
||||
0x770104: location_name.grass_land_mine,
|
||||
0x770105: location_name.grass_land_pierre,
|
||||
0x770106: location_name.ripple_field_kamuribana,
|
||||
0x770107: location_name.ripple_field_bakasa,
|
||||
0x770108: location_name.ripple_field_elieel,
|
||||
0x770109: location_name.ripple_field_toad,
|
||||
0x77010A: location_name.ripple_field_mama_pitch,
|
||||
0x77010B: location_name.ripple_field_hb002,
|
||||
0x77010C: location_name.sand_canyon_mushrooms,
|
||||
0x77010D: location_name.sand_canyon_auntie,
|
||||
0x77010E: location_name.sand_canyon_caramello,
|
||||
0x77010F: location_name.sand_canyon_hikari,
|
||||
0x770110: location_name.sand_canyon_nyupun,
|
||||
0x770111: location_name.sand_canyon_rob,
|
||||
0x770112: location_name.cloudy_park_hibanamodoki,
|
||||
0x770113: location_name.cloudy_park_piyokeko,
|
||||
0x770114: location_name.cloudy_park_mrball,
|
||||
0x770115: location_name.cloudy_park_mikarin,
|
||||
0x770116: location_name.cloudy_park_pick,
|
||||
0x770117: location_name.cloudy_park_hb007,
|
||||
0x770118: location_name.iceberg_kogoesou,
|
||||
0x770119: location_name.iceberg_samus,
|
||||
0x77011A: location_name.iceberg_kawasaki,
|
||||
0x77011B: location_name.iceberg_name,
|
||||
0x77011C: location_name.iceberg_shiro,
|
||||
0x77011D: location_name.iceberg_angel,
|
||||
}
|
||||
|
||||
boss_locations = {
|
||||
0x770200: location_name.grass_land_whispy,
|
||||
0x770201: location_name.ripple_field_acro,
|
||||
0x770202: location_name.sand_canyon_poncon,
|
||||
0x770203: location_name.cloudy_park_ado,
|
||||
0x770204: location_name.iceberg_dedede,
|
||||
}
|
||||
|
||||
consumable_locations = {
|
||||
0x770300: location_name.grass_land_1_u1,
|
||||
0x770301: location_name.grass_land_1_m1,
|
||||
0x770302: location_name.grass_land_2_u1,
|
||||
0x770303: location_name.grass_land_3_u1,
|
||||
0x770304: location_name.grass_land_3_m1,
|
||||
0x770305: location_name.grass_land_4_m1,
|
||||
0x770306: location_name.grass_land_4_u1,
|
||||
0x770307: location_name.grass_land_4_m2,
|
||||
0x770308: location_name.grass_land_4_m3,
|
||||
0x770309: location_name.grass_land_6_u1,
|
||||
0x77030A: location_name.grass_land_6_u2,
|
||||
0x77030B: location_name.ripple_field_2_u1,
|
||||
0x77030C: location_name.ripple_field_2_m1,
|
||||
0x77030D: location_name.ripple_field_3_m1,
|
||||
0x77030E: location_name.ripple_field_3_u1,
|
||||
0x77030F: location_name.ripple_field_4_m2,
|
||||
0x770310: location_name.ripple_field_4_u1,
|
||||
0x770311: location_name.ripple_field_4_m1,
|
||||
0x770312: location_name.ripple_field_5_u1,
|
||||
0x770313: location_name.ripple_field_5_m2,
|
||||
0x770314: location_name.ripple_field_5_m1,
|
||||
0x770315: location_name.sand_canyon_1_u1,
|
||||
0x770316: location_name.sand_canyon_2_u1,
|
||||
0x770317: location_name.sand_canyon_2_m1,
|
||||
0x770318: location_name.sand_canyon_4_m1,
|
||||
0x770319: location_name.sand_canyon_4_u1,
|
||||
0x77031A: location_name.sand_canyon_4_m2,
|
||||
0x77031B: location_name.sand_canyon_5_u1,
|
||||
0x77031C: location_name.sand_canyon_5_u3,
|
||||
0x77031D: location_name.sand_canyon_5_m1,
|
||||
0x77031E: location_name.sand_canyon_5_u4,
|
||||
0x77031F: location_name.sand_canyon_5_u2,
|
||||
0x770320: location_name.cloudy_park_1_m1,
|
||||
0x770321: location_name.cloudy_park_1_u1,
|
||||
0x770322: location_name.cloudy_park_4_u1,
|
||||
0x770323: location_name.cloudy_park_4_m1,
|
||||
0x770324: location_name.cloudy_park_5_m1,
|
||||
0x770325: location_name.cloudy_park_6_u1,
|
||||
0x770326: location_name.iceberg_3_m1,
|
||||
0x770327: location_name.iceberg_5_u1,
|
||||
0x770328: location_name.iceberg_5_u2,
|
||||
0x770329: location_name.iceberg_5_u3,
|
||||
0x77032A: location_name.iceberg_6_m1,
|
||||
0x77032B: location_name.iceberg_6_u1,
|
||||
}
|
||||
|
||||
level_consumables = {
|
||||
1: [0, 1],
|
||||
2: [2],
|
||||
3: [3, 4],
|
||||
4: [5, 6, 7, 8],
|
||||
6: [9, 10],
|
||||
8: [11, 12],
|
||||
9: [13, 14],
|
||||
10: [15, 16, 17],
|
||||
11: [18, 19, 20],
|
||||
13: [21],
|
||||
14: [22, 23],
|
||||
16: [24, 25, 26],
|
||||
17: [27, 28, 29, 30, 31],
|
||||
19: [32, 33],
|
||||
22: [34, 35],
|
||||
23: [36],
|
||||
24: [37],
|
||||
27: [38],
|
||||
29: [39, 40, 41],
|
||||
30: [42, 43],
|
||||
}
|
||||
|
||||
star_locations = {
|
||||
0x770401: location_name.grass_land_1_s1,
|
||||
0x770402: location_name.grass_land_1_s2,
|
||||
0x770403: location_name.grass_land_1_s3,
|
||||
0x770404: location_name.grass_land_1_s4,
|
||||
0x770405: location_name.grass_land_1_s5,
|
||||
0x770406: location_name.grass_land_1_s6,
|
||||
0x770407: location_name.grass_land_1_s7,
|
||||
0x770408: location_name.grass_land_1_s8,
|
||||
0x770409: location_name.grass_land_1_s9,
|
||||
0x77040a: location_name.grass_land_1_s10,
|
||||
0x77040b: location_name.grass_land_1_s11,
|
||||
0x77040c: location_name.grass_land_1_s12,
|
||||
0x77040d: location_name.grass_land_1_s13,
|
||||
0x77040e: location_name.grass_land_1_s14,
|
||||
0x77040f: location_name.grass_land_1_s15,
|
||||
0x770410: location_name.grass_land_1_s16,
|
||||
0x770411: location_name.grass_land_1_s17,
|
||||
0x770412: location_name.grass_land_1_s18,
|
||||
0x770413: location_name.grass_land_1_s19,
|
||||
0x770414: location_name.grass_land_1_s20,
|
||||
0x770415: location_name.grass_land_1_s21,
|
||||
0x770416: location_name.grass_land_1_s22,
|
||||
0x770417: location_name.grass_land_1_s23,
|
||||
0x770418: location_name.grass_land_2_s1,
|
||||
0x770419: location_name.grass_land_2_s2,
|
||||
0x77041a: location_name.grass_land_2_s3,
|
||||
0x77041b: location_name.grass_land_2_s4,
|
||||
0x77041c: location_name.grass_land_2_s5,
|
||||
0x77041d: location_name.grass_land_2_s6,
|
||||
0x77041e: location_name.grass_land_2_s7,
|
||||
0x77041f: location_name.grass_land_2_s8,
|
||||
0x770420: location_name.grass_land_2_s9,
|
||||
0x770421: location_name.grass_land_2_s10,
|
||||
0x770422: location_name.grass_land_2_s11,
|
||||
0x770423: location_name.grass_land_2_s12,
|
||||
0x770424: location_name.grass_land_2_s13,
|
||||
0x770425: location_name.grass_land_2_s14,
|
||||
0x770426: location_name.grass_land_2_s15,
|
||||
0x770427: location_name.grass_land_2_s16,
|
||||
0x770428: location_name.grass_land_2_s17,
|
||||
0x770429: location_name.grass_land_2_s18,
|
||||
0x77042a: location_name.grass_land_2_s19,
|
||||
0x77042b: location_name.grass_land_2_s20,
|
||||
0x77042c: location_name.grass_land_2_s21,
|
||||
0x77042d: location_name.grass_land_3_s1,
|
||||
0x77042e: location_name.grass_land_3_s2,
|
||||
0x77042f: location_name.grass_land_3_s3,
|
||||
0x770430: location_name.grass_land_3_s4,
|
||||
0x770431: location_name.grass_land_3_s5,
|
||||
0x770432: location_name.grass_land_3_s6,
|
||||
0x770433: location_name.grass_land_3_s7,
|
||||
0x770434: location_name.grass_land_3_s8,
|
||||
0x770435: location_name.grass_land_3_s9,
|
||||
0x770436: location_name.grass_land_3_s10,
|
||||
0x770437: location_name.grass_land_3_s11,
|
||||
0x770438: location_name.grass_land_3_s12,
|
||||
0x770439: location_name.grass_land_3_s13,
|
||||
0x77043a: location_name.grass_land_3_s14,
|
||||
0x77043b: location_name.grass_land_3_s15,
|
||||
0x77043c: location_name.grass_land_3_s16,
|
||||
0x77043d: location_name.grass_land_3_s17,
|
||||
0x77043e: location_name.grass_land_3_s18,
|
||||
0x77043f: location_name.grass_land_3_s19,
|
||||
0x770440: location_name.grass_land_3_s20,
|
||||
0x770441: location_name.grass_land_3_s21,
|
||||
0x770442: location_name.grass_land_3_s22,
|
||||
0x770443: location_name.grass_land_3_s23,
|
||||
0x770444: location_name.grass_land_3_s24,
|
||||
0x770445: location_name.grass_land_3_s25,
|
||||
0x770446: location_name.grass_land_3_s26,
|
||||
0x770447: location_name.grass_land_3_s27,
|
||||
0x770448: location_name.grass_land_3_s28,
|
||||
0x770449: location_name.grass_land_3_s29,
|
||||
0x77044a: location_name.grass_land_3_s30,
|
||||
0x77044b: location_name.grass_land_3_s31,
|
||||
0x77044c: location_name.grass_land_4_s1,
|
||||
0x77044d: location_name.grass_land_4_s2,
|
||||
0x77044e: location_name.grass_land_4_s3,
|
||||
0x77044f: location_name.grass_land_4_s4,
|
||||
0x770450: location_name.grass_land_4_s5,
|
||||
0x770451: location_name.grass_land_4_s6,
|
||||
0x770452: location_name.grass_land_4_s7,
|
||||
0x770453: location_name.grass_land_4_s8,
|
||||
0x770454: location_name.grass_land_4_s9,
|
||||
0x770455: location_name.grass_land_4_s10,
|
||||
0x770456: location_name.grass_land_4_s11,
|
||||
0x770457: location_name.grass_land_4_s12,
|
||||
0x770458: location_name.grass_land_4_s13,
|
||||
0x770459: location_name.grass_land_4_s14,
|
||||
0x77045a: location_name.grass_land_4_s15,
|
||||
0x77045b: location_name.grass_land_4_s16,
|
||||
0x77045c: location_name.grass_land_4_s17,
|
||||
0x77045d: location_name.grass_land_4_s18,
|
||||
0x77045e: location_name.grass_land_4_s19,
|
||||
0x77045f: location_name.grass_land_4_s20,
|
||||
0x770460: location_name.grass_land_4_s21,
|
||||
0x770461: location_name.grass_land_4_s22,
|
||||
0x770462: location_name.grass_land_4_s23,
|
||||
0x770463: location_name.grass_land_4_s24,
|
||||
0x770464: location_name.grass_land_4_s25,
|
||||
0x770465: location_name.grass_land_4_s26,
|
||||
0x770466: location_name.grass_land_4_s27,
|
||||
0x770467: location_name.grass_land_4_s28,
|
||||
0x770468: location_name.grass_land_4_s29,
|
||||
0x770469: location_name.grass_land_4_s30,
|
||||
0x77046a: location_name.grass_land_4_s31,
|
||||
0x77046b: location_name.grass_land_4_s32,
|
||||
0x77046c: location_name.grass_land_4_s33,
|
||||
0x77046d: location_name.grass_land_4_s34,
|
||||
0x77046e: location_name.grass_land_4_s35,
|
||||
0x77046f: location_name.grass_land_4_s36,
|
||||
0x770470: location_name.grass_land_4_s37,
|
||||
0x770471: location_name.grass_land_5_s1,
|
||||
0x770472: location_name.grass_land_5_s2,
|
||||
0x770473: location_name.grass_land_5_s3,
|
||||
0x770474: location_name.grass_land_5_s4,
|
||||
0x770475: location_name.grass_land_5_s5,
|
||||
0x770476: location_name.grass_land_5_s6,
|
||||
0x770477: location_name.grass_land_5_s7,
|
||||
0x770478: location_name.grass_land_5_s8,
|
||||
0x770479: location_name.grass_land_5_s9,
|
||||
0x77047a: location_name.grass_land_5_s10,
|
||||
0x77047b: location_name.grass_land_5_s11,
|
||||
0x77047c: location_name.grass_land_5_s12,
|
||||
0x77047d: location_name.grass_land_5_s13,
|
||||
0x77047e: location_name.grass_land_5_s14,
|
||||
0x77047f: location_name.grass_land_5_s15,
|
||||
0x770480: location_name.grass_land_5_s16,
|
||||
0x770481: location_name.grass_land_5_s17,
|
||||
0x770482: location_name.grass_land_5_s18,
|
||||
0x770483: location_name.grass_land_5_s19,
|
||||
0x770484: location_name.grass_land_5_s20,
|
||||
0x770485: location_name.grass_land_5_s21,
|
||||
0x770486: location_name.grass_land_5_s22,
|
||||
0x770487: location_name.grass_land_5_s23,
|
||||
0x770488: location_name.grass_land_5_s24,
|
||||
0x770489: location_name.grass_land_5_s25,
|
||||
0x77048a: location_name.grass_land_5_s26,
|
||||
0x77048b: location_name.grass_land_5_s27,
|
||||
0x77048c: location_name.grass_land_5_s28,
|
||||
0x77048d: location_name.grass_land_5_s29,
|
||||
0x77048e: location_name.grass_land_6_s1,
|
||||
0x77048f: location_name.grass_land_6_s2,
|
||||
0x770490: location_name.grass_land_6_s3,
|
||||
0x770491: location_name.grass_land_6_s4,
|
||||
0x770492: location_name.grass_land_6_s5,
|
||||
0x770493: location_name.grass_land_6_s6,
|
||||
0x770494: location_name.grass_land_6_s7,
|
||||
0x770495: location_name.grass_land_6_s8,
|
||||
0x770496: location_name.grass_land_6_s9,
|
||||
0x770497: location_name.grass_land_6_s10,
|
||||
0x770498: location_name.grass_land_6_s11,
|
||||
0x770499: location_name.grass_land_6_s12,
|
||||
0x77049a: location_name.grass_land_6_s13,
|
||||
0x77049b: location_name.grass_land_6_s14,
|
||||
0x77049c: location_name.grass_land_6_s15,
|
||||
0x77049d: location_name.grass_land_6_s16,
|
||||
0x77049e: location_name.grass_land_6_s17,
|
||||
0x77049f: location_name.grass_land_6_s18,
|
||||
0x7704a0: location_name.grass_land_6_s19,
|
||||
0x7704a1: location_name.grass_land_6_s20,
|
||||
0x7704a2: location_name.grass_land_6_s21,
|
||||
0x7704a3: location_name.grass_land_6_s22,
|
||||
0x7704a4: location_name.grass_land_6_s23,
|
||||
0x7704a5: location_name.grass_land_6_s24,
|
||||
0x7704a6: location_name.grass_land_6_s25,
|
||||
0x7704a7: location_name.grass_land_6_s26,
|
||||
0x7704a8: location_name.grass_land_6_s27,
|
||||
0x7704a9: location_name.grass_land_6_s28,
|
||||
0x7704aa: location_name.grass_land_6_s29,
|
||||
0x7704ab: location_name.ripple_field_1_s1,
|
||||
0x7704ac: location_name.ripple_field_1_s2,
|
||||
0x7704ad: location_name.ripple_field_1_s3,
|
||||
0x7704ae: location_name.ripple_field_1_s4,
|
||||
0x7704af: location_name.ripple_field_1_s5,
|
||||
0x7704b0: location_name.ripple_field_1_s6,
|
||||
0x7704b1: location_name.ripple_field_1_s7,
|
||||
0x7704b2: location_name.ripple_field_1_s8,
|
||||
0x7704b3: location_name.ripple_field_1_s9,
|
||||
0x7704b4: location_name.ripple_field_1_s10,
|
||||
0x7704b5: location_name.ripple_field_1_s11,
|
||||
0x7704b6: location_name.ripple_field_1_s12,
|
||||
0x7704b7: location_name.ripple_field_1_s13,
|
||||
0x7704b8: location_name.ripple_field_1_s14,
|
||||
0x7704b9: location_name.ripple_field_1_s15,
|
||||
0x7704ba: location_name.ripple_field_1_s16,
|
||||
0x7704bb: location_name.ripple_field_1_s17,
|
||||
0x7704bc: location_name.ripple_field_1_s18,
|
||||
0x7704bd: location_name.ripple_field_1_s19,
|
||||
0x7704be: location_name.ripple_field_2_s1,
|
||||
0x7704bf: location_name.ripple_field_2_s2,
|
||||
0x7704c0: location_name.ripple_field_2_s3,
|
||||
0x7704c1: location_name.ripple_field_2_s4,
|
||||
0x7704c2: location_name.ripple_field_2_s5,
|
||||
0x7704c3: location_name.ripple_field_2_s6,
|
||||
0x7704c4: location_name.ripple_field_2_s7,
|
||||
0x7704c5: location_name.ripple_field_2_s8,
|
||||
0x7704c6: location_name.ripple_field_2_s9,
|
||||
0x7704c7: location_name.ripple_field_2_s10,
|
||||
0x7704c8: location_name.ripple_field_2_s11,
|
||||
0x7704c9: location_name.ripple_field_2_s12,
|
||||
0x7704ca: location_name.ripple_field_2_s13,
|
||||
0x7704cb: location_name.ripple_field_2_s14,
|
||||
0x7704cc: location_name.ripple_field_2_s15,
|
||||
0x7704cd: location_name.ripple_field_2_s16,
|
||||
0x7704ce: location_name.ripple_field_2_s17,
|
||||
0x7704cf: location_name.ripple_field_3_s1,
|
||||
0x7704d0: location_name.ripple_field_3_s2,
|
||||
0x7704d1: location_name.ripple_field_3_s3,
|
||||
0x7704d2: location_name.ripple_field_3_s4,
|
||||
0x7704d3: location_name.ripple_field_3_s5,
|
||||
0x7704d4: location_name.ripple_field_3_s6,
|
||||
0x7704d5: location_name.ripple_field_3_s7,
|
||||
0x7704d6: location_name.ripple_field_3_s8,
|
||||
0x7704d7: location_name.ripple_field_3_s9,
|
||||
0x7704d8: location_name.ripple_field_3_s10,
|
||||
0x7704d9: location_name.ripple_field_3_s11,
|
||||
0x7704da: location_name.ripple_field_3_s12,
|
||||
0x7704db: location_name.ripple_field_3_s13,
|
||||
0x7704dc: location_name.ripple_field_3_s14,
|
||||
0x7704dd: location_name.ripple_field_3_s15,
|
||||
0x7704de: location_name.ripple_field_3_s16,
|
||||
0x7704df: location_name.ripple_field_3_s17,
|
||||
0x7704e0: location_name.ripple_field_3_s18,
|
||||
0x7704e1: location_name.ripple_field_3_s19,
|
||||
0x7704e2: location_name.ripple_field_3_s20,
|
||||
0x7704e3: location_name.ripple_field_3_s21,
|
||||
0x7704e4: location_name.ripple_field_4_s1,
|
||||
0x7704e5: location_name.ripple_field_4_s2,
|
||||
0x7704e6: location_name.ripple_field_4_s3,
|
||||
0x7704e7: location_name.ripple_field_4_s4,
|
||||
0x7704e8: location_name.ripple_field_4_s5,
|
||||
0x7704e9: location_name.ripple_field_4_s6,
|
||||
0x7704ea: location_name.ripple_field_4_s7,
|
||||
0x7704eb: location_name.ripple_field_4_s8,
|
||||
0x7704ec: location_name.ripple_field_4_s9,
|
||||
0x7704ed: location_name.ripple_field_4_s10,
|
||||
0x7704ee: location_name.ripple_field_4_s11,
|
||||
0x7704ef: location_name.ripple_field_4_s12,
|
||||
0x7704f0: location_name.ripple_field_4_s13,
|
||||
0x7704f1: location_name.ripple_field_4_s14,
|
||||
0x7704f2: location_name.ripple_field_4_s15,
|
||||
0x7704f3: location_name.ripple_field_4_s16,
|
||||
0x7704f4: location_name.ripple_field_4_s17,
|
||||
0x7704f5: location_name.ripple_field_4_s18,
|
||||
0x7704f6: location_name.ripple_field_4_s19,
|
||||
0x7704f7: location_name.ripple_field_4_s20,
|
||||
0x7704f8: location_name.ripple_field_4_s21,
|
||||
0x7704f9: location_name.ripple_field_4_s22,
|
||||
0x7704fa: location_name.ripple_field_4_s23,
|
||||
0x7704fb: location_name.ripple_field_4_s24,
|
||||
0x7704fc: location_name.ripple_field_4_s25,
|
||||
0x7704fd: location_name.ripple_field_4_s26,
|
||||
0x7704fe: location_name.ripple_field_4_s27,
|
||||
0x7704ff: location_name.ripple_field_4_s28,
|
||||
0x770500: location_name.ripple_field_4_s29,
|
||||
0x770501: location_name.ripple_field_4_s30,
|
||||
0x770502: location_name.ripple_field_4_s31,
|
||||
0x770503: location_name.ripple_field_4_s32,
|
||||
0x770504: location_name.ripple_field_4_s33,
|
||||
0x770505: location_name.ripple_field_4_s34,
|
||||
0x770506: location_name.ripple_field_4_s35,
|
||||
0x770507: location_name.ripple_field_4_s36,
|
||||
0x770508: location_name.ripple_field_4_s37,
|
||||
0x770509: location_name.ripple_field_4_s38,
|
||||
0x77050a: location_name.ripple_field_4_s39,
|
||||
0x77050b: location_name.ripple_field_4_s40,
|
||||
0x77050c: location_name.ripple_field_4_s41,
|
||||
0x77050d: location_name.ripple_field_4_s42,
|
||||
0x77050e: location_name.ripple_field_4_s43,
|
||||
0x77050f: location_name.ripple_field_4_s44,
|
||||
0x770510: location_name.ripple_field_4_s45,
|
||||
0x770511: location_name.ripple_field_4_s46,
|
||||
0x770512: location_name.ripple_field_4_s47,
|
||||
0x770513: location_name.ripple_field_4_s48,
|
||||
0x770514: location_name.ripple_field_4_s49,
|
||||
0x770515: location_name.ripple_field_4_s50,
|
||||
0x770516: location_name.ripple_field_4_s51,
|
||||
0x770517: location_name.ripple_field_5_s1,
|
||||
0x770518: location_name.ripple_field_5_s2,
|
||||
0x770519: location_name.ripple_field_5_s3,
|
||||
0x77051a: location_name.ripple_field_5_s4,
|
||||
0x77051b: location_name.ripple_field_5_s5,
|
||||
0x77051c: location_name.ripple_field_5_s6,
|
||||
0x77051d: location_name.ripple_field_5_s7,
|
||||
0x77051e: location_name.ripple_field_5_s8,
|
||||
0x77051f: location_name.ripple_field_5_s9,
|
||||
0x770520: location_name.ripple_field_5_s10,
|
||||
0x770521: location_name.ripple_field_5_s11,
|
||||
0x770522: location_name.ripple_field_5_s12,
|
||||
0x770523: location_name.ripple_field_5_s13,
|
||||
0x770524: location_name.ripple_field_5_s14,
|
||||
0x770525: location_name.ripple_field_5_s15,
|
||||
0x770526: location_name.ripple_field_5_s16,
|
||||
0x770527: location_name.ripple_field_5_s17,
|
||||
0x770528: location_name.ripple_field_5_s18,
|
||||
0x770529: location_name.ripple_field_5_s19,
|
||||
0x77052a: location_name.ripple_field_5_s20,
|
||||
0x77052b: location_name.ripple_field_5_s21,
|
||||
0x77052c: location_name.ripple_field_5_s22,
|
||||
0x77052d: location_name.ripple_field_5_s23,
|
||||
0x77052e: location_name.ripple_field_5_s24,
|
||||
0x77052f: location_name.ripple_field_5_s25,
|
||||
0x770530: location_name.ripple_field_5_s26,
|
||||
0x770531: location_name.ripple_field_5_s27,
|
||||
0x770532: location_name.ripple_field_5_s28,
|
||||
0x770533: location_name.ripple_field_5_s29,
|
||||
0x770534: location_name.ripple_field_5_s30,
|
||||
0x770535: location_name.ripple_field_5_s31,
|
||||
0x770536: location_name.ripple_field_5_s32,
|
||||
0x770537: location_name.ripple_field_5_s33,
|
||||
0x770538: location_name.ripple_field_5_s34,
|
||||
0x770539: location_name.ripple_field_5_s35,
|
||||
0x77053a: location_name.ripple_field_5_s36,
|
||||
0x77053b: location_name.ripple_field_5_s37,
|
||||
0x77053c: location_name.ripple_field_5_s38,
|
||||
0x77053d: location_name.ripple_field_5_s39,
|
||||
0x77053e: location_name.ripple_field_5_s40,
|
||||
0x77053f: location_name.ripple_field_5_s41,
|
||||
0x770540: location_name.ripple_field_5_s42,
|
||||
0x770541: location_name.ripple_field_5_s43,
|
||||
0x770542: location_name.ripple_field_5_s44,
|
||||
0x770543: location_name.ripple_field_5_s45,
|
||||
0x770544: location_name.ripple_field_5_s46,
|
||||
0x770545: location_name.ripple_field_5_s47,
|
||||
0x770546: location_name.ripple_field_5_s48,
|
||||
0x770547: location_name.ripple_field_5_s49,
|
||||
0x770548: location_name.ripple_field_5_s50,
|
||||
0x770549: location_name.ripple_field_5_s51,
|
||||
0x77054a: location_name.ripple_field_6_s1,
|
||||
0x77054b: location_name.ripple_field_6_s2,
|
||||
0x77054c: location_name.ripple_field_6_s3,
|
||||
0x77054d: location_name.ripple_field_6_s4,
|
||||
0x77054e: location_name.ripple_field_6_s5,
|
||||
0x77054f: location_name.ripple_field_6_s6,
|
||||
0x770550: location_name.ripple_field_6_s7,
|
||||
0x770551: location_name.ripple_field_6_s8,
|
||||
0x770552: location_name.ripple_field_6_s9,
|
||||
0x770553: location_name.ripple_field_6_s10,
|
||||
0x770554: location_name.ripple_field_6_s11,
|
||||
0x770555: location_name.ripple_field_6_s12,
|
||||
0x770556: location_name.ripple_field_6_s13,
|
||||
0x770557: location_name.ripple_field_6_s14,
|
||||
0x770558: location_name.ripple_field_6_s15,
|
||||
0x770559: location_name.ripple_field_6_s16,
|
||||
0x77055a: location_name.ripple_field_6_s17,
|
||||
0x77055b: location_name.ripple_field_6_s18,
|
||||
0x77055c: location_name.ripple_field_6_s19,
|
||||
0x77055d: location_name.ripple_field_6_s20,
|
||||
0x77055e: location_name.ripple_field_6_s21,
|
||||
0x77055f: location_name.ripple_field_6_s22,
|
||||
0x770560: location_name.ripple_field_6_s23,
|
||||
0x770561: location_name.sand_canyon_1_s1,
|
||||
0x770562: location_name.sand_canyon_1_s2,
|
||||
0x770563: location_name.sand_canyon_1_s3,
|
||||
0x770564: location_name.sand_canyon_1_s4,
|
||||
0x770565: location_name.sand_canyon_1_s5,
|
||||
0x770566: location_name.sand_canyon_1_s6,
|
||||
0x770567: location_name.sand_canyon_1_s7,
|
||||
0x770568: location_name.sand_canyon_1_s8,
|
||||
0x770569: location_name.sand_canyon_1_s9,
|
||||
0x77056a: location_name.sand_canyon_1_s10,
|
||||
0x77056b: location_name.sand_canyon_1_s11,
|
||||
0x77056c: location_name.sand_canyon_1_s12,
|
||||
0x77056d: location_name.sand_canyon_1_s13,
|
||||
0x77056e: location_name.sand_canyon_1_s14,
|
||||
0x77056f: location_name.sand_canyon_1_s15,
|
||||
0x770570: location_name.sand_canyon_1_s16,
|
||||
0x770571: location_name.sand_canyon_1_s17,
|
||||
0x770572: location_name.sand_canyon_1_s18,
|
||||
0x770573: location_name.sand_canyon_1_s19,
|
||||
0x770574: location_name.sand_canyon_1_s20,
|
||||
0x770575: location_name.sand_canyon_1_s21,
|
||||
0x770576: location_name.sand_canyon_1_s22,
|
||||
0x770577: location_name.sand_canyon_2_s1,
|
||||
0x770578: location_name.sand_canyon_2_s2,
|
||||
0x770579: location_name.sand_canyon_2_s3,
|
||||
0x77057a: location_name.sand_canyon_2_s4,
|
||||
0x77057b: location_name.sand_canyon_2_s5,
|
||||
0x77057c: location_name.sand_canyon_2_s6,
|
||||
0x77057d: location_name.sand_canyon_2_s7,
|
||||
0x77057e: location_name.sand_canyon_2_s8,
|
||||
0x77057f: location_name.sand_canyon_2_s9,
|
||||
0x770580: location_name.sand_canyon_2_s10,
|
||||
0x770581: location_name.sand_canyon_2_s11,
|
||||
0x770582: location_name.sand_canyon_2_s12,
|
||||
0x770583: location_name.sand_canyon_2_s13,
|
||||
0x770584: location_name.sand_canyon_2_s14,
|
||||
0x770585: location_name.sand_canyon_2_s15,
|
||||
0x770586: location_name.sand_canyon_2_s16,
|
||||
0x770587: location_name.sand_canyon_2_s17,
|
||||
0x770588: location_name.sand_canyon_2_s18,
|
||||
0x770589: location_name.sand_canyon_2_s19,
|
||||
0x77058a: location_name.sand_canyon_2_s20,
|
||||
0x77058b: location_name.sand_canyon_2_s21,
|
||||
0x77058c: location_name.sand_canyon_2_s22,
|
||||
0x77058d: location_name.sand_canyon_2_s23,
|
||||
0x77058e: location_name.sand_canyon_2_s24,
|
||||
0x77058f: location_name.sand_canyon_2_s25,
|
||||
0x770590: location_name.sand_canyon_2_s26,
|
||||
0x770591: location_name.sand_canyon_2_s27,
|
||||
0x770592: location_name.sand_canyon_2_s28,
|
||||
0x770593: location_name.sand_canyon_2_s29,
|
||||
0x770594: location_name.sand_canyon_2_s30,
|
||||
0x770595: location_name.sand_canyon_2_s31,
|
||||
0x770596: location_name.sand_canyon_2_s32,
|
||||
0x770597: location_name.sand_canyon_2_s33,
|
||||
0x770598: location_name.sand_canyon_2_s34,
|
||||
0x770599: location_name.sand_canyon_2_s35,
|
||||
0x77059a: location_name.sand_canyon_2_s36,
|
||||
0x77059b: location_name.sand_canyon_2_s37,
|
||||
0x77059c: location_name.sand_canyon_2_s38,
|
||||
0x77059d: location_name.sand_canyon_2_s39,
|
||||
0x77059e: location_name.sand_canyon_2_s40,
|
||||
0x77059f: location_name.sand_canyon_2_s41,
|
||||
0x7705a0: location_name.sand_canyon_2_s42,
|
||||
0x7705a1: location_name.sand_canyon_2_s43,
|
||||
0x7705a2: location_name.sand_canyon_2_s44,
|
||||
0x7705a3: location_name.sand_canyon_2_s45,
|
||||
0x7705a4: location_name.sand_canyon_2_s46,
|
||||
0x7705a5: location_name.sand_canyon_2_s47,
|
||||
0x7705a6: location_name.sand_canyon_2_s48,
|
||||
0x7705a7: location_name.sand_canyon_3_s1,
|
||||
0x7705a8: location_name.sand_canyon_3_s2,
|
||||
0x7705a9: location_name.sand_canyon_3_s3,
|
||||
0x7705aa: location_name.sand_canyon_3_s4,
|
||||
0x7705ab: location_name.sand_canyon_3_s5,
|
||||
0x7705ac: location_name.sand_canyon_3_s6,
|
||||
0x7705ad: location_name.sand_canyon_3_s7,
|
||||
0x7705ae: location_name.sand_canyon_3_s8,
|
||||
0x7705af: location_name.sand_canyon_3_s9,
|
||||
0x7705b0: location_name.sand_canyon_3_s10,
|
||||
0x7705b1: location_name.sand_canyon_4_s1,
|
||||
0x7705b2: location_name.sand_canyon_4_s2,
|
||||
0x7705b3: location_name.sand_canyon_4_s3,
|
||||
0x7705b4: location_name.sand_canyon_4_s4,
|
||||
0x7705b5: location_name.sand_canyon_4_s5,
|
||||
0x7705b6: location_name.sand_canyon_4_s6,
|
||||
0x7705b7: location_name.sand_canyon_4_s7,
|
||||
0x7705b8: location_name.sand_canyon_4_s8,
|
||||
0x7705b9: location_name.sand_canyon_4_s9,
|
||||
0x7705ba: location_name.sand_canyon_4_s10,
|
||||
0x7705bb: location_name.sand_canyon_4_s11,
|
||||
0x7705bc: location_name.sand_canyon_4_s12,
|
||||
0x7705bd: location_name.sand_canyon_4_s13,
|
||||
0x7705be: location_name.sand_canyon_4_s14,
|
||||
0x7705bf: location_name.sand_canyon_4_s15,
|
||||
0x7705c0: location_name.sand_canyon_4_s16,
|
||||
0x7705c1: location_name.sand_canyon_4_s17,
|
||||
0x7705c2: location_name.sand_canyon_4_s18,
|
||||
0x7705c3: location_name.sand_canyon_4_s19,
|
||||
0x7705c4: location_name.sand_canyon_4_s20,
|
||||
0x7705c5: location_name.sand_canyon_4_s21,
|
||||
0x7705c6: location_name.sand_canyon_4_s22,
|
||||
0x7705c7: location_name.sand_canyon_4_s23,
|
||||
0x7705c8: location_name.sand_canyon_5_s1,
|
||||
0x7705c9: location_name.sand_canyon_5_s2,
|
||||
0x7705ca: location_name.sand_canyon_5_s3,
|
||||
0x7705cb: location_name.sand_canyon_5_s4,
|
||||
0x7705cc: location_name.sand_canyon_5_s5,
|
||||
0x7705cd: location_name.sand_canyon_5_s6,
|
||||
0x7705ce: location_name.sand_canyon_5_s7,
|
||||
0x7705cf: location_name.sand_canyon_5_s8,
|
||||
0x7705d0: location_name.sand_canyon_5_s9,
|
||||
0x7705d1: location_name.sand_canyon_5_s10,
|
||||
0x7705d2: location_name.sand_canyon_5_s11,
|
||||
0x7705d3: location_name.sand_canyon_5_s12,
|
||||
0x7705d4: location_name.sand_canyon_5_s13,
|
||||
0x7705d5: location_name.sand_canyon_5_s14,
|
||||
0x7705d6: location_name.sand_canyon_5_s15,
|
||||
0x7705d7: location_name.sand_canyon_5_s16,
|
||||
0x7705d8: location_name.sand_canyon_5_s17,
|
||||
0x7705d9: location_name.sand_canyon_5_s18,
|
||||
0x7705da: location_name.sand_canyon_5_s19,
|
||||
0x7705db: location_name.sand_canyon_5_s20,
|
||||
0x7705dc: location_name.sand_canyon_5_s21,
|
||||
0x7705dd: location_name.sand_canyon_5_s22,
|
||||
0x7705de: location_name.sand_canyon_5_s23,
|
||||
0x7705df: location_name.sand_canyon_5_s24,
|
||||
0x7705e0: location_name.sand_canyon_5_s25,
|
||||
0x7705e1: location_name.sand_canyon_5_s26,
|
||||
0x7705e2: location_name.sand_canyon_5_s27,
|
||||
0x7705e3: location_name.sand_canyon_5_s28,
|
||||
0x7705e4: location_name.sand_canyon_5_s29,
|
||||
0x7705e5: location_name.sand_canyon_5_s30,
|
||||
0x7705e6: location_name.sand_canyon_5_s31,
|
||||
0x7705e7: location_name.sand_canyon_5_s32,
|
||||
0x7705e8: location_name.sand_canyon_5_s33,
|
||||
0x7705e9: location_name.sand_canyon_5_s34,
|
||||
0x7705ea: location_name.sand_canyon_5_s35,
|
||||
0x7705eb: location_name.sand_canyon_5_s36,
|
||||
0x7705ec: location_name.sand_canyon_5_s37,
|
||||
0x7705ed: location_name.sand_canyon_5_s38,
|
||||
0x7705ee: location_name.sand_canyon_5_s39,
|
||||
0x7705ef: location_name.sand_canyon_5_s40,
|
||||
0x7705f0: location_name.cloudy_park_1_s1,
|
||||
0x7705f1: location_name.cloudy_park_1_s2,
|
||||
0x7705f2: location_name.cloudy_park_1_s3,
|
||||
0x7705f3: location_name.cloudy_park_1_s4,
|
||||
0x7705f4: location_name.cloudy_park_1_s5,
|
||||
0x7705f5: location_name.cloudy_park_1_s6,
|
||||
0x7705f6: location_name.cloudy_park_1_s7,
|
||||
0x7705f7: location_name.cloudy_park_1_s8,
|
||||
0x7705f8: location_name.cloudy_park_1_s9,
|
||||
0x7705f9: location_name.cloudy_park_1_s10,
|
||||
0x7705fa: location_name.cloudy_park_1_s11,
|
||||
0x7705fb: location_name.cloudy_park_1_s12,
|
||||
0x7705fc: location_name.cloudy_park_1_s13,
|
||||
0x7705fd: location_name.cloudy_park_1_s14,
|
||||
0x7705fe: location_name.cloudy_park_1_s15,
|
||||
0x7705ff: location_name.cloudy_park_1_s16,
|
||||
0x770600: location_name.cloudy_park_1_s17,
|
||||
0x770601: location_name.cloudy_park_1_s18,
|
||||
0x770602: location_name.cloudy_park_1_s19,
|
||||
0x770603: location_name.cloudy_park_1_s20,
|
||||
0x770604: location_name.cloudy_park_1_s21,
|
||||
0x770605: location_name.cloudy_park_1_s22,
|
||||
0x770606: location_name.cloudy_park_1_s23,
|
||||
0x770607: location_name.cloudy_park_2_s1,
|
||||
0x770608: location_name.cloudy_park_2_s2,
|
||||
0x770609: location_name.cloudy_park_2_s3,
|
||||
0x77060a: location_name.cloudy_park_2_s4,
|
||||
0x77060b: location_name.cloudy_park_2_s5,
|
||||
0x77060c: location_name.cloudy_park_2_s6,
|
||||
0x77060d: location_name.cloudy_park_2_s7,
|
||||
0x77060e: location_name.cloudy_park_2_s8,
|
||||
0x77060f: location_name.cloudy_park_2_s9,
|
||||
0x770610: location_name.cloudy_park_2_s10,
|
||||
0x770611: location_name.cloudy_park_2_s11,
|
||||
0x770612: location_name.cloudy_park_2_s12,
|
||||
0x770613: location_name.cloudy_park_2_s13,
|
||||
0x770614: location_name.cloudy_park_2_s14,
|
||||
0x770615: location_name.cloudy_park_2_s15,
|
||||
0x770616: location_name.cloudy_park_2_s16,
|
||||
0x770617: location_name.cloudy_park_2_s17,
|
||||
0x770618: location_name.cloudy_park_2_s18,
|
||||
0x770619: location_name.cloudy_park_2_s19,
|
||||
0x77061a: location_name.cloudy_park_2_s20,
|
||||
0x77061b: location_name.cloudy_park_2_s21,
|
||||
0x77061c: location_name.cloudy_park_2_s22,
|
||||
0x77061d: location_name.cloudy_park_2_s23,
|
||||
0x77061e: location_name.cloudy_park_2_s24,
|
||||
0x77061f: location_name.cloudy_park_2_s25,
|
||||
0x770620: location_name.cloudy_park_2_s26,
|
||||
0x770621: location_name.cloudy_park_2_s27,
|
||||
0x770622: location_name.cloudy_park_2_s28,
|
||||
0x770623: location_name.cloudy_park_2_s29,
|
||||
0x770624: location_name.cloudy_park_2_s30,
|
||||
0x770625: location_name.cloudy_park_2_s31,
|
||||
0x770626: location_name.cloudy_park_2_s32,
|
||||
0x770627: location_name.cloudy_park_2_s33,
|
||||
0x770628: location_name.cloudy_park_2_s34,
|
||||
0x770629: location_name.cloudy_park_2_s35,
|
||||
0x77062a: location_name.cloudy_park_2_s36,
|
||||
0x77062b: location_name.cloudy_park_2_s37,
|
||||
0x77062c: location_name.cloudy_park_2_s38,
|
||||
0x77062d: location_name.cloudy_park_2_s39,
|
||||
0x77062e: location_name.cloudy_park_2_s40,
|
||||
0x77062f: location_name.cloudy_park_2_s41,
|
||||
0x770630: location_name.cloudy_park_2_s42,
|
||||
0x770631: location_name.cloudy_park_2_s43,
|
||||
0x770632: location_name.cloudy_park_2_s44,
|
||||
0x770633: location_name.cloudy_park_2_s45,
|
||||
0x770634: location_name.cloudy_park_2_s46,
|
||||
0x770635: location_name.cloudy_park_2_s47,
|
||||
0x770636: location_name.cloudy_park_2_s48,
|
||||
0x770637: location_name.cloudy_park_2_s49,
|
||||
0x770638: location_name.cloudy_park_2_s50,
|
||||
0x770639: location_name.cloudy_park_2_s51,
|
||||
0x77063a: location_name.cloudy_park_2_s52,
|
||||
0x77063b: location_name.cloudy_park_2_s53,
|
||||
0x77063c: location_name.cloudy_park_2_s54,
|
||||
0x77063d: location_name.cloudy_park_3_s1,
|
||||
0x77063e: location_name.cloudy_park_3_s2,
|
||||
0x77063f: location_name.cloudy_park_3_s3,
|
||||
0x770640: location_name.cloudy_park_3_s4,
|
||||
0x770641: location_name.cloudy_park_3_s5,
|
||||
0x770642: location_name.cloudy_park_3_s6,
|
||||
0x770643: location_name.cloudy_park_3_s7,
|
||||
0x770644: location_name.cloudy_park_3_s8,
|
||||
0x770645: location_name.cloudy_park_3_s9,
|
||||
0x770646: location_name.cloudy_park_3_s10,
|
||||
0x770647: location_name.cloudy_park_3_s11,
|
||||
0x770648: location_name.cloudy_park_3_s12,
|
||||
0x770649: location_name.cloudy_park_3_s13,
|
||||
0x77064a: location_name.cloudy_park_3_s14,
|
||||
0x77064b: location_name.cloudy_park_3_s15,
|
||||
0x77064c: location_name.cloudy_park_3_s16,
|
||||
0x77064d: location_name.cloudy_park_3_s17,
|
||||
0x77064e: location_name.cloudy_park_3_s18,
|
||||
0x77064f: location_name.cloudy_park_3_s19,
|
||||
0x770650: location_name.cloudy_park_3_s20,
|
||||
0x770651: location_name.cloudy_park_3_s21,
|
||||
0x770652: location_name.cloudy_park_3_s22,
|
||||
0x770653: location_name.cloudy_park_4_s1,
|
||||
0x770654: location_name.cloudy_park_4_s2,
|
||||
0x770655: location_name.cloudy_park_4_s3,
|
||||
0x770656: location_name.cloudy_park_4_s4,
|
||||
0x770657: location_name.cloudy_park_4_s5,
|
||||
0x770658: location_name.cloudy_park_4_s6,
|
||||
0x770659: location_name.cloudy_park_4_s7,
|
||||
0x77065a: location_name.cloudy_park_4_s8,
|
||||
0x77065b: location_name.cloudy_park_4_s9,
|
||||
0x77065c: location_name.cloudy_park_4_s10,
|
||||
0x77065d: location_name.cloudy_park_4_s11,
|
||||
0x77065e: location_name.cloudy_park_4_s12,
|
||||
0x77065f: location_name.cloudy_park_4_s13,
|
||||
0x770660: location_name.cloudy_park_4_s14,
|
||||
0x770661: location_name.cloudy_park_4_s15,
|
||||
0x770662: location_name.cloudy_park_4_s16,
|
||||
0x770663: location_name.cloudy_park_4_s17,
|
||||
0x770664: location_name.cloudy_park_4_s18,
|
||||
0x770665: location_name.cloudy_park_4_s19,
|
||||
0x770666: location_name.cloudy_park_4_s20,
|
||||
0x770667: location_name.cloudy_park_4_s21,
|
||||
0x770668: location_name.cloudy_park_4_s22,
|
||||
0x770669: location_name.cloudy_park_4_s23,
|
||||
0x77066a: location_name.cloudy_park_4_s24,
|
||||
0x77066b: location_name.cloudy_park_4_s25,
|
||||
0x77066c: location_name.cloudy_park_4_s26,
|
||||
0x77066d: location_name.cloudy_park_4_s27,
|
||||
0x77066e: location_name.cloudy_park_4_s28,
|
||||
0x77066f: location_name.cloudy_park_4_s29,
|
||||
0x770670: location_name.cloudy_park_4_s30,
|
||||
0x770671: location_name.cloudy_park_4_s31,
|
||||
0x770672: location_name.cloudy_park_4_s32,
|
||||
0x770673: location_name.cloudy_park_4_s33,
|
||||
0x770674: location_name.cloudy_park_4_s34,
|
||||
0x770675: location_name.cloudy_park_4_s35,
|
||||
0x770676: location_name.cloudy_park_4_s36,
|
||||
0x770677: location_name.cloudy_park_4_s37,
|
||||
0x770678: location_name.cloudy_park_4_s38,
|
||||
0x770679: location_name.cloudy_park_4_s39,
|
||||
0x77067a: location_name.cloudy_park_4_s40,
|
||||
0x77067b: location_name.cloudy_park_4_s41,
|
||||
0x77067c: location_name.cloudy_park_4_s42,
|
||||
0x77067d: location_name.cloudy_park_4_s43,
|
||||
0x77067e: location_name.cloudy_park_4_s44,
|
||||
0x77067f: location_name.cloudy_park_4_s45,
|
||||
0x770680: location_name.cloudy_park_4_s46,
|
||||
0x770681: location_name.cloudy_park_4_s47,
|
||||
0x770682: location_name.cloudy_park_4_s48,
|
||||
0x770683: location_name.cloudy_park_4_s49,
|
||||
0x770684: location_name.cloudy_park_4_s50,
|
||||
0x770685: location_name.cloudy_park_5_s1,
|
||||
0x770686: location_name.cloudy_park_5_s2,
|
||||
0x770687: location_name.cloudy_park_5_s3,
|
||||
0x770688: location_name.cloudy_park_5_s4,
|
||||
0x770689: location_name.cloudy_park_5_s5,
|
||||
0x77068a: location_name.cloudy_park_5_s6,
|
||||
0x77068b: location_name.cloudy_park_6_s1,
|
||||
0x77068c: location_name.cloudy_park_6_s2,
|
||||
0x77068d: location_name.cloudy_park_6_s3,
|
||||
0x77068e: location_name.cloudy_park_6_s4,
|
||||
0x77068f: location_name.cloudy_park_6_s5,
|
||||
0x770690: location_name.cloudy_park_6_s6,
|
||||
0x770691: location_name.cloudy_park_6_s7,
|
||||
0x770692: location_name.cloudy_park_6_s8,
|
||||
0x770693: location_name.cloudy_park_6_s9,
|
||||
0x770694: location_name.cloudy_park_6_s10,
|
||||
0x770695: location_name.cloudy_park_6_s11,
|
||||
0x770696: location_name.cloudy_park_6_s12,
|
||||
0x770697: location_name.cloudy_park_6_s13,
|
||||
0x770698: location_name.cloudy_park_6_s14,
|
||||
0x770699: location_name.cloudy_park_6_s15,
|
||||
0x77069a: location_name.cloudy_park_6_s16,
|
||||
0x77069b: location_name.cloudy_park_6_s17,
|
||||
0x77069c: location_name.cloudy_park_6_s18,
|
||||
0x77069d: location_name.cloudy_park_6_s19,
|
||||
0x77069e: location_name.cloudy_park_6_s20,
|
||||
0x77069f: location_name.cloudy_park_6_s21,
|
||||
0x7706a0: location_name.cloudy_park_6_s22,
|
||||
0x7706a1: location_name.cloudy_park_6_s23,
|
||||
0x7706a2: location_name.cloudy_park_6_s24,
|
||||
0x7706a3: location_name.cloudy_park_6_s25,
|
||||
0x7706a4: location_name.cloudy_park_6_s26,
|
||||
0x7706a5: location_name.cloudy_park_6_s27,
|
||||
0x7706a6: location_name.cloudy_park_6_s28,
|
||||
0x7706a7: location_name.cloudy_park_6_s29,
|
||||
0x7706a8: location_name.cloudy_park_6_s30,
|
||||
0x7706a9: location_name.cloudy_park_6_s31,
|
||||
0x7706aa: location_name.cloudy_park_6_s32,
|
||||
0x7706ab: location_name.cloudy_park_6_s33,
|
||||
0x7706ac: location_name.iceberg_1_s1,
|
||||
0x7706ad: location_name.iceberg_1_s2,
|
||||
0x7706ae: location_name.iceberg_1_s3,
|
||||
0x7706af: location_name.iceberg_1_s4,
|
||||
0x7706b0: location_name.iceberg_1_s5,
|
||||
0x7706b1: location_name.iceberg_1_s6,
|
||||
0x7706b2: location_name.iceberg_2_s1,
|
||||
0x7706b3: location_name.iceberg_2_s2,
|
||||
0x7706b4: location_name.iceberg_2_s3,
|
||||
0x7706b5: location_name.iceberg_2_s4,
|
||||
0x7706b6: location_name.iceberg_2_s5,
|
||||
0x7706b7: location_name.iceberg_2_s6,
|
||||
0x7706b8: location_name.iceberg_2_s7,
|
||||
0x7706b9: location_name.iceberg_2_s8,
|
||||
0x7706ba: location_name.iceberg_2_s9,
|
||||
0x7706bb: location_name.iceberg_2_s10,
|
||||
0x7706bc: location_name.iceberg_2_s11,
|
||||
0x7706bd: location_name.iceberg_2_s12,
|
||||
0x7706be: location_name.iceberg_2_s13,
|
||||
0x7706bf: location_name.iceberg_2_s14,
|
||||
0x7706c0: location_name.iceberg_2_s15,
|
||||
0x7706c1: location_name.iceberg_2_s16,
|
||||
0x7706c2: location_name.iceberg_2_s17,
|
||||
0x7706c3: location_name.iceberg_2_s18,
|
||||
0x7706c4: location_name.iceberg_2_s19,
|
||||
0x7706c5: location_name.iceberg_3_s1,
|
||||
0x7706c6: location_name.iceberg_3_s2,
|
||||
0x7706c7: location_name.iceberg_3_s3,
|
||||
0x7706c8: location_name.iceberg_3_s4,
|
||||
0x7706c9: location_name.iceberg_3_s5,
|
||||
0x7706ca: location_name.iceberg_3_s6,
|
||||
0x7706cb: location_name.iceberg_3_s7,
|
||||
0x7706cc: location_name.iceberg_3_s8,
|
||||
0x7706cd: location_name.iceberg_3_s9,
|
||||
0x7706ce: location_name.iceberg_3_s10,
|
||||
0x7706cf: location_name.iceberg_3_s11,
|
||||
0x7706d0: location_name.iceberg_3_s12,
|
||||
0x7706d1: location_name.iceberg_3_s13,
|
||||
0x7706d2: location_name.iceberg_3_s14,
|
||||
0x7706d3: location_name.iceberg_3_s15,
|
||||
0x7706d4: location_name.iceberg_3_s16,
|
||||
0x7706d5: location_name.iceberg_3_s17,
|
||||
0x7706d6: location_name.iceberg_3_s18,
|
||||
0x7706d7: location_name.iceberg_3_s19,
|
||||
0x7706d8: location_name.iceberg_3_s20,
|
||||
0x7706d9: location_name.iceberg_3_s21,
|
||||
0x7706da: location_name.iceberg_4_s1,
|
||||
0x7706db: location_name.iceberg_4_s2,
|
||||
0x7706dc: location_name.iceberg_4_s3,
|
||||
0x7706dd: location_name.iceberg_5_s1,
|
||||
0x7706de: location_name.iceberg_5_s2,
|
||||
0x7706df: location_name.iceberg_5_s3,
|
||||
0x7706e0: location_name.iceberg_5_s4,
|
||||
0x7706e1: location_name.iceberg_5_s5,
|
||||
0x7706e2: location_name.iceberg_5_s6,
|
||||
0x7706e3: location_name.iceberg_5_s7,
|
||||
0x7706e4: location_name.iceberg_5_s8,
|
||||
0x7706e5: location_name.iceberg_5_s9,
|
||||
0x7706e6: location_name.iceberg_5_s10,
|
||||
0x7706e7: location_name.iceberg_5_s11,
|
||||
0x7706e8: location_name.iceberg_5_s12,
|
||||
0x7706e9: location_name.iceberg_5_s13,
|
||||
0x7706ea: location_name.iceberg_5_s14,
|
||||
0x7706eb: location_name.iceberg_5_s15,
|
||||
0x7706ec: location_name.iceberg_5_s16,
|
||||
0x7706ed: location_name.iceberg_5_s17,
|
||||
0x7706ee: location_name.iceberg_5_s18,
|
||||
0x7706ef: location_name.iceberg_5_s19,
|
||||
0x7706f0: location_name.iceberg_5_s20,
|
||||
0x7706f1: location_name.iceberg_5_s21,
|
||||
0x7706f2: location_name.iceberg_5_s22,
|
||||
0x7706f3: location_name.iceberg_5_s23,
|
||||
0x7706f4: location_name.iceberg_5_s24,
|
||||
0x7706f5: location_name.iceberg_5_s25,
|
||||
0x7706f6: location_name.iceberg_5_s26,
|
||||
0x7706f7: location_name.iceberg_5_s27,
|
||||
0x7706f8: location_name.iceberg_5_s28,
|
||||
0x7706f9: location_name.iceberg_5_s29,
|
||||
0x7706fa: location_name.iceberg_5_s30,
|
||||
0x7706fb: location_name.iceberg_5_s31,
|
||||
0x7706fc: location_name.iceberg_5_s32,
|
||||
0x7706fd: location_name.iceberg_5_s33,
|
||||
0x7706fe: location_name.iceberg_5_s34,
|
||||
0x7706ff: location_name.iceberg_6_s1,
|
||||
|
||||
}
|
||||
|
||||
location_table = {
|
||||
**stage_locations,
|
||||
**heart_star_locations,
|
||||
**boss_locations,
|
||||
**consumable_locations,
|
||||
**star_locations
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import List
|
||||
|
||||
grass_land_1_a1 = "Grass Land 1 - Animal 1" # Nago
|
||||
grass_land_1_a2 = "Grass Land 1 - Animal 2" # Rick
|
||||
grass_land_2_a1 = "Grass Land 2 - Animal 1" # ChuChu
|
||||
@@ -197,3 +199,12 @@ animal_friend_spawns = {
|
||||
iceberg_6_a5: "ChuChu Spawn",
|
||||
iceberg_6_a6: "Nago Spawn",
|
||||
}
|
||||
|
||||
problematic_sets: List[List[str]] = [
|
||||
# Animal groups that must be guaranteed unique. Potential for softlocks on future-ER if not.
|
||||
[ripple_field_4_a1, ripple_field_4_a2, ripple_field_4_a3],
|
||||
[sand_canyon_3_a1, sand_canyon_3_a2, sand_canyon_3_a3],
|
||||
[cloudy_park_6_a1, cloudy_park_6_a2, cloudy_park_6_a3],
|
||||
[iceberg_6_a1, iceberg_6_a2, iceberg_6_a3],
|
||||
[iceberg_6_a4, iceberg_6_a5, iceberg_6_a6]
|
||||
]
|
||||
@@ -809,7 +809,7 @@ vanilla_enemies = {'Waddle Dee': 'No Ability',
|
||||
|
||||
enemy_restrictive: List[Tuple[List[str], List[str]]] = [
|
||||
# abilities, enemies, set_all (False to set any)
|
||||
(["Burning Ability", "Stone Ability"], ["Rocky", "Sparky", "Babut", "Squishy", ]), # Ribbon Field 5 - 7
|
||||
(["Stone Ability"], ["Rocky", "Sparky", "Babut", "Squishy", ]), # Ribbon Field 5 - 7
|
||||
# Sand Canyon 6
|
||||
(["Parasol Ability", "Cutter Ability"], ['Bukiset (Parasol)', 'Bukiset (Cutter)']),
|
||||
(["Spark Ability", "Clean Ability"], ['Bukiset (Spark)', 'Bukiset (Clean)']),
|
||||
@@ -1,13 +1,21 @@
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
from Options import DeathLink, Choice, Toggle, OptionDict, Range, PlandoBosses, DefaultOnToggle, \
|
||||
PerGameCommonOptions, PlandoConnections
|
||||
from .Names import LocationName
|
||||
from Options import DeathLinkMixin, Choice, Toggle, OptionDict, Range, PlandoBosses, DefaultOnToggle, \
|
||||
PerGameCommonOptions, Visibility, NamedRange, OptionGroup, PlandoConnections
|
||||
from .names import location_name
|
||||
|
||||
|
||||
class RemoteItems(DefaultOnToggle):
|
||||
"""
|
||||
Enables receiving items from your own world, primarily for co-op play.
|
||||
"""
|
||||
display_name = "Remote Items"
|
||||
|
||||
|
||||
class KDL3PlandoConnections(PlandoConnections):
|
||||
entrances = exits = {f"{i} {j}" for i in LocationName.level_names for j in range(1, 7)}
|
||||
entrances = exits = {f"{i} {j}" for i in location_name.level_names for j in range(1, 7)}
|
||||
|
||||
|
||||
class Goal(Choice):
|
||||
@@ -30,6 +38,7 @@ class Goal(Choice):
|
||||
return cls.name_lookup[value].upper()
|
||||
return super().get_option_name(value)
|
||||
|
||||
|
||||
class GoalSpeed(Choice):
|
||||
"""
|
||||
Normal: the goal is unlocked after purifying the five bosses
|
||||
@@ -40,13 +49,14 @@ class GoalSpeed(Choice):
|
||||
option_fast = 1
|
||||
|
||||
|
||||
class TotalHeartStars(Range):
|
||||
class MaxHeartStars(Range):
|
||||
"""
|
||||
Maximum number of heart stars to include in the pool of items.
|
||||
If fewer available locations exist in the pool than this number, the number of available locations will be used instead.
|
||||
"""
|
||||
display_name = "Max Heart Stars"
|
||||
range_start = 5 # set to 5 so strict bosses does not degrade
|
||||
range_end = 50 # 30 default locations + 30 stage clears + 5 bosses - 14 progression items = 51, so round down
|
||||
range_end = 99 # previously set to 50, set to highest it can be should there be less locations than heart stars
|
||||
default = 30
|
||||
|
||||
|
||||
@@ -84,9 +94,9 @@ class BossShuffle(PlandoBosses):
|
||||
Singularity: All (non-Zero) bosses will be replaced with a single boss
|
||||
Supports plando placement.
|
||||
"""
|
||||
bosses = frozenset(LocationName.boss_names.keys())
|
||||
bosses = frozenset(location_name.boss_names.keys())
|
||||
|
||||
locations = frozenset(LocationName.level_names.keys())
|
||||
locations = frozenset(location_name.level_names.keys())
|
||||
|
||||
duplicate_bosses = True
|
||||
|
||||
@@ -278,7 +288,8 @@ class KirbyFlavorPreset(Choice):
|
||||
option_orange = 11
|
||||
option_lime = 12
|
||||
option_lavender = 13
|
||||
option_custom = 14
|
||||
option_miku = 14
|
||||
option_custom = 15
|
||||
default = 0
|
||||
|
||||
@classmethod
|
||||
@@ -296,6 +307,7 @@ class KirbyFlavor(OptionDict):
|
||||
A custom color for Kirby. To use a custom color, set the preset to Custom and then define a dict of keys from "1" to
|
||||
"15", with their values being an HTML hex color.
|
||||
"""
|
||||
display_name = "Custom Kirby Flavor"
|
||||
default = {
|
||||
"1": "B01810",
|
||||
"2": "F0E0E8",
|
||||
@@ -313,6 +325,7 @@ class KirbyFlavor(OptionDict):
|
||||
"14": "F8F8F8",
|
||||
"15": "B03830",
|
||||
}
|
||||
visibility = Visibility.template | Visibility.spoiler # likely never supported on guis
|
||||
|
||||
|
||||
class GooeyFlavorPreset(Choice):
|
||||
@@ -352,6 +365,7 @@ class GooeyFlavor(OptionDict):
|
||||
A custom color for Gooey. To use a custom color, set the preset to Custom and then define a dict of keys from "1" to
|
||||
"15", with their values being an HTML hex color.
|
||||
"""
|
||||
display_name = "Custom Gooey Flavor"
|
||||
default = {
|
||||
"1": "000808",
|
||||
"2": "102838",
|
||||
@@ -363,6 +377,7 @@ class GooeyFlavor(OptionDict):
|
||||
"8": "D0C0C0",
|
||||
"9": "F8F8F8",
|
||||
}
|
||||
visibility = Visibility.template | Visibility.spoiler # likely never supported on guis
|
||||
|
||||
|
||||
class MusicShuffle(Choice):
|
||||
@@ -402,14 +417,27 @@ class Gifting(Toggle):
|
||||
display_name = "Gifting"
|
||||
|
||||
|
||||
class TotalHeartStars(NamedRange):
|
||||
"""
|
||||
Deprecated. Use max_heart_stars instead. Supported for only one version.
|
||||
"""
|
||||
default = -1
|
||||
range_start = 5
|
||||
range_end = 99
|
||||
special_range_names = {
|
||||
"default": -1
|
||||
}
|
||||
visibility = Visibility.none
|
||||
|
||||
|
||||
@dataclass
|
||||
class KDL3Options(PerGameCommonOptions):
|
||||
class KDL3Options(PerGameCommonOptions, DeathLinkMixin):
|
||||
remote_items: RemoteItems
|
||||
plando_connections: KDL3PlandoConnections
|
||||
death_link: DeathLink
|
||||
game_language: GameLanguage
|
||||
goal: Goal
|
||||
goal_speed: GoalSpeed
|
||||
total_heart_stars: TotalHeartStars
|
||||
max_heart_stars: MaxHeartStars
|
||||
heart_stars_required: HeartStarsRequired
|
||||
filler_percentage: FillerPercentage
|
||||
trap_percentage: TrapPercentage
|
||||
@@ -435,3 +463,17 @@ class KDL3Options(PerGameCommonOptions):
|
||||
gooey_flavor: GooeyFlavor
|
||||
music_shuffle: MusicShuffle
|
||||
virtual_console: VirtualConsoleChanges
|
||||
|
||||
total_heart_stars: TotalHeartStars # remove in 2 versions
|
||||
|
||||
|
||||
kdl3_option_groups: List[OptionGroup] = [
|
||||
OptionGroup("Goal Options", [Goal, GoalSpeed, MaxHeartStars, HeartStarsRequired, JumpingTarget, ]),
|
||||
OptionGroup("World Options", [RemoteItems, StrictBosses, OpenWorld, OpenWorldBossRequirement, ConsumableChecks,
|
||||
StarChecks, FillerPercentage, TrapPercentage, GooeyTrapPercentage,
|
||||
SlowTrapPercentage, AbilityTrapPercentage, LevelShuffle, BossShuffle,
|
||||
AnimalRandomization, CopyAbilityRandomization, BossRequirementRandom,
|
||||
Gifting, ]),
|
||||
OptionGroup("Cosmetic Options", [GameLanguage, BossShuffleAllowBB, KirbyFlavorPreset, KirbyFlavor,
|
||||
GooeyFlavorPreset, GooeyFlavor, MusicShuffle, VirtualConsoleChanges, ]),
|
||||
]
|
||||
@@ -25,6 +25,7 @@ all_random = {
|
||||
"ow_boss_requirement": "random",
|
||||
"boss_requirement_random": "random",
|
||||
"consumables": "random",
|
||||
"starsanity": "random",
|
||||
"kirby_flavor_preset": "random",
|
||||
"gooey_flavor_preset": "random",
|
||||
"music_shuffle": "random",
|
||||
@@ -1,60 +1,62 @@
|
||||
import orjson
|
||||
import os
|
||||
from pkgutil import get_data
|
||||
from copy import deepcopy
|
||||
|
||||
from typing import TYPE_CHECKING, List, Dict, Optional, Union
|
||||
from BaseClasses import Region
|
||||
from typing import TYPE_CHECKING, List, Dict, Optional, Union, Callable
|
||||
from BaseClasses import Region, CollectionState
|
||||
from worlds.generic.Rules import add_item_rule
|
||||
from .Locations import KDL3Location
|
||||
from .Names import LocationName
|
||||
from .Options import BossShuffle
|
||||
from .Room import KDL3Room
|
||||
from .locations import KDL3Location
|
||||
from .names import location_name
|
||||
from .options import BossShuffle
|
||||
from .room import KDL3Room
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import KDL3World
|
||||
|
||||
default_levels = {
|
||||
1: [0x770001, 0x770002, 0x770003, 0x770004, 0x770005, 0x770006, 0x770200],
|
||||
2: [0x770007, 0x770008, 0x770009, 0x77000A, 0x77000B, 0x77000C, 0x770201],
|
||||
3: [0x77000D, 0x77000E, 0x77000F, 0x770010, 0x770011, 0x770012, 0x770202],
|
||||
4: [0x770013, 0x770014, 0x770015, 0x770016, 0x770017, 0x770018, 0x770203],
|
||||
5: [0x770019, 0x77001A, 0x77001B, 0x77001C, 0x77001D, 0x77001E, 0x770204],
|
||||
1: [0x770000, 0x770001, 0x770002, 0x770003, 0x770004, 0x770005, 0x770200],
|
||||
2: [0x770006, 0x770007, 0x770008, 0x770009, 0x77000A, 0x77000B, 0x770201],
|
||||
3: [0x77000C, 0x77000D, 0x77000E, 0x77000F, 0x770010, 0x770011, 0x770202],
|
||||
4: [0x770012, 0x770013, 0x770014, 0x770015, 0x770016, 0x770017, 0x770203],
|
||||
5: [0x770018, 0x770019, 0x77001A, 0x77001B, 0x77001C, 0x77001D, 0x770204],
|
||||
}
|
||||
|
||||
first_stage_blacklist = {
|
||||
# We want to confirm that the first stage can be completed without any items
|
||||
0x77000B, # 2-5 needs Kine
|
||||
0x770011, # 3-5 needs Cutter
|
||||
0x77001C, # 5-4 needs Burning
|
||||
0x77000A, # 2-5 needs Kine
|
||||
0x770010, # 3-5 needs Cutter
|
||||
0x77001B, # 5-4 needs Burning
|
||||
}
|
||||
|
||||
first_world_limit = {
|
||||
# We need to limit the number of very restrictive stages in level 1 on solo gens
|
||||
*first_stage_blacklist, # all three of the blacklist stages need 2+ items for both checks
|
||||
0x770006,
|
||||
0x770007,
|
||||
0x770008,
|
||||
0x770013,
|
||||
0x77001E,
|
||||
0x770012,
|
||||
0x77001D,
|
||||
|
||||
}
|
||||
|
||||
|
||||
def generate_valid_level(world: "KDL3World", level: int, stage: int,
|
||||
possible_stages: List[int], placed_stages: List[int]):
|
||||
possible_stages: List[int], placed_stages: List[Optional[int]]) -> int:
|
||||
new_stage = world.random.choice(possible_stages)
|
||||
if level == 1:
|
||||
if stage == 0 and new_stage in first_stage_blacklist:
|
||||
possible_stages.remove(new_stage)
|
||||
return generate_valid_level(world, level, stage, possible_stages, placed_stages)
|
||||
elif (not (world.multiworld.players > 1 or world.options.consumables or world.options.starsanity) and
|
||||
new_stage in first_world_limit and
|
||||
sum(p_stage in first_world_limit for p_stage in placed_stages)
|
||||
new_stage in first_world_limit and
|
||||
sum(p_stage in first_world_limit for p_stage in placed_stages)
|
||||
>= (2 if world.options.open_world else 1)):
|
||||
return generate_valid_level(world, level, stage, possible_stages, placed_stages)
|
||||
return new_stage
|
||||
|
||||
|
||||
def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]):
|
||||
level_names = {LocationName.level_names[level]: level for level in LocationName.level_names}
|
||||
def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]) -> None:
|
||||
level_names = {location_name.level_names[level]: level for level in location_name.level_names}
|
||||
room_data = orjson.loads(get_data(__name__, os.path.join("data", "Rooms.json")))
|
||||
rooms: Dict[str, KDL3Room] = dict()
|
||||
for room_entry in room_data:
|
||||
@@ -63,7 +65,7 @@ def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]):
|
||||
room_entry["default_exits"], room_entry["animal_pointers"], room_entry["enemies"],
|
||||
room_entry["entity_load"], room_entry["consumables"], room_entry["consumables_pointer"])
|
||||
room.add_locations({location: world.location_name_to_id[location] if location in world.location_name_to_id else
|
||||
None for location in room_entry["locations"]
|
||||
None for location in room_entry["locations"]
|
||||
if (not any(x in location for x in ["1-Up", "Maxim"]) or
|
||||
world.options.consumables.value) and ("Star" not in location
|
||||
or world.options.starsanity.value)},
|
||||
@@ -83,8 +85,8 @@ def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]):
|
||||
if room.stage == 7:
|
||||
first_rooms[0x770200 + room.level - 1] = room
|
||||
else:
|
||||
first_rooms[0x770000 + ((room.level - 1) * 6) + room.stage] = room
|
||||
exits = dict()
|
||||
first_rooms[0x770000 + ((room.level - 1) * 6) + room.stage - 1] = room
|
||||
exits: Dict[str, Callable[[CollectionState], bool]] = dict()
|
||||
for def_exit in room.default_exits:
|
||||
target = f"{level_names[room.level]} {room.stage} - {def_exit['room']}"
|
||||
access_rule = tuple(def_exit["access_rule"])
|
||||
@@ -115,50 +117,54 @@ def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]):
|
||||
if world.options.open_world:
|
||||
level_regions[level].add_exits([first_rooms[0x770200 + level - 1].name])
|
||||
else:
|
||||
world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][5]], world.player)\
|
||||
world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][5]], world.player) \
|
||||
.parent_region.add_exits([first_rooms[0x770200 + level - 1].name])
|
||||
|
||||
|
||||
def generate_valid_levels(world: "KDL3World", enforce_world: bool, enforce_pattern: bool) -> dict:
|
||||
levels: Dict[int, List[Optional[int]]] = {
|
||||
1: [None] * 7,
|
||||
2: [None] * 7,
|
||||
3: [None] * 7,
|
||||
4: [None] * 7,
|
||||
5: [None] * 7,
|
||||
}
|
||||
def generate_valid_levels(world: "KDL3World", shuffle_mode: int) -> Dict[int, List[int]]:
|
||||
if shuffle_mode:
|
||||
levels: Dict[int, List[Optional[int]]] = {
|
||||
1: [None] * 7,
|
||||
2: [None] * 7,
|
||||
3: [None] * 7,
|
||||
4: [None] * 7,
|
||||
5: [None] * 7,
|
||||
}
|
||||
|
||||
possible_stages = [default_levels[level][stage] for level in default_levels for stage in range(6)]
|
||||
if world.options.plando_connections:
|
||||
for connection in world.options.plando_connections:
|
||||
try:
|
||||
entrance_world, entrance_stage = connection.entrance.rsplit(" ", 1)
|
||||
stage_world, stage_stage = connection.exit.rsplit(" ", 1)
|
||||
new_stage = default_levels[LocationName.level_names[stage_world.strip()]][int(stage_stage) - 1]
|
||||
levels[LocationName.level_names[entrance_world.strip()]][int(entrance_stage) - 1] = new_stage
|
||||
possible_stages.remove(new_stage)
|
||||
possible_stages = [default_levels[level][stage] for level in default_levels for stage in range(6)]
|
||||
if world.options.plando_connections:
|
||||
for connection in world.options.plando_connections:
|
||||
try:
|
||||
entrance_world, entrance_stage = connection.entrance.rsplit(" ", 1)
|
||||
stage_world, stage_stage = connection.exit.rsplit(" ", 1)
|
||||
new_stage = default_levels[location_name.level_names[stage_world.strip()]][int(stage_stage) - 1]
|
||||
levels[location_name.level_names[entrance_world.strip()]][int(entrance_stage) - 1] = new_stage
|
||||
possible_stages.remove(new_stage)
|
||||
|
||||
except Exception:
|
||||
raise Exception(
|
||||
f"Invalid connection: {connection.entrance} =>"
|
||||
f" {connection.exit} for player {world.player} ({world.player_name})")
|
||||
except Exception:
|
||||
raise Exception(
|
||||
f"Invalid connection: {connection.entrance} =>"
|
||||
f" {connection.exit} for player {world.player} ({world.player_name})")
|
||||
|
||||
for level in range(1, 6):
|
||||
for stage in range(6):
|
||||
# Randomize bosses separately
|
||||
try:
|
||||
for level in range(1, 6):
|
||||
for stage in range(6):
|
||||
# Randomize bosses separately
|
||||
if levels[level][stage] is None:
|
||||
stage_candidates = [candidate for candidate in possible_stages
|
||||
if (enforce_world and candidate in default_levels[level])
|
||||
or (enforce_pattern and ((candidate - 1) & 0x00FFFF) % 6 == stage)
|
||||
or (enforce_pattern == enforce_world)
|
||||
if (shuffle_mode == 1 and candidate in default_levels[level])
|
||||
or (shuffle_mode == 2 and (candidate & 0x00FFFF) % 6 == stage)
|
||||
or (shuffle_mode == 3)
|
||||
]
|
||||
if not stage_candidates:
|
||||
raise Exception(
|
||||
f"Failed to find valid stage for {level}-{stage}. Remaining Stages:{possible_stages}")
|
||||
new_stage = generate_valid_level(world, level, stage, stage_candidates, levels[level])
|
||||
possible_stages.remove(new_stage)
|
||||
levels[level][stage] = new_stage
|
||||
except Exception:
|
||||
raise Exception(f"Failed to find valid stage for {level}-{stage}. Remaining Stages:{possible_stages}")
|
||||
|
||||
else:
|
||||
levels = deepcopy(default_levels)
|
||||
for level in levels:
|
||||
levels[level][6] = None
|
||||
# now handle bosses
|
||||
boss_shuffle: Union[int, str] = world.options.boss_shuffle.value
|
||||
plando_bosses = []
|
||||
@@ -168,17 +174,17 @@ def generate_valid_levels(world: "KDL3World", enforce_world: bool, enforce_patte
|
||||
boss_shuffle = BossShuffle.options[options.pop()]
|
||||
for option in options:
|
||||
if "-" in option:
|
||||
loc, boss = option.split("-")
|
||||
loc, plando_boss = option.split("-")
|
||||
loc = loc.title()
|
||||
boss = boss.title()
|
||||
levels[LocationName.level_names[loc]][6] = LocationName.boss_names[boss]
|
||||
plando_bosses.append(LocationName.boss_names[boss])
|
||||
plando_boss = plando_boss.title()
|
||||
levels[location_name.level_names[loc]][6] = location_name.boss_names[plando_boss]
|
||||
plando_bosses.append(location_name.boss_names[plando_boss])
|
||||
else:
|
||||
option = option.title()
|
||||
for level in levels:
|
||||
if levels[level][6] is None:
|
||||
levels[level][6] = LocationName.boss_names[option]
|
||||
plando_bosses.append(LocationName.boss_names[option])
|
||||
levels[level][6] = location_name.boss_names[option]
|
||||
plando_bosses.append(location_name.boss_names[option])
|
||||
|
||||
if boss_shuffle > 0:
|
||||
if boss_shuffle == BossShuffle.option_full:
|
||||
@@ -223,15 +229,14 @@ def create_levels(world: "KDL3World") -> None:
|
||||
5: level5,
|
||||
}
|
||||
level_shuffle = world.options.stage_shuffle.value
|
||||
if level_shuffle != 0:
|
||||
world.player_levels = generate_valid_levels(
|
||||
world,
|
||||
level_shuffle == 1,
|
||||
level_shuffle == 2)
|
||||
if hasattr(world.multiworld, "re_gen_passthrough"):
|
||||
world.player_levels = getattr(world.multiworld, "re_gen_passthrough")["Kirby's Dream Land 3"]["player_levels"]
|
||||
else:
|
||||
world.player_levels = generate_valid_levels(world, level_shuffle)
|
||||
|
||||
generate_rooms(world, levels)
|
||||
|
||||
level6.add_locations({LocationName.goals[world.options.goal]: None}, KDL3Location)
|
||||
level6.add_locations({location_name.goals[world.options.goal.value]: None}, KDL3Location)
|
||||
|
||||
menu.connect(level1, "Start Game")
|
||||
level1.connect(level2, "To Level 2")
|
||||
602
worlds/kdl3/rom.py
Normal file
602
worlds/kdl3/rom.py
Normal file
@@ -0,0 +1,602 @@
|
||||
import typing
|
||||
from pkgutil import get_data
|
||||
|
||||
import Utils
|
||||
from typing import Optional, TYPE_CHECKING, Tuple, Dict, List
|
||||
import hashlib
|
||||
import os
|
||||
import struct
|
||||
|
||||
import settings
|
||||
from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes, APPatchExtension
|
||||
from .aesthetics import get_palette_bytes, kirby_target_palettes, get_kirby_palette, gooey_target_palettes, \
|
||||
get_gooey_palette
|
||||
from .compression import hal_decompress
|
||||
import bsdiff4
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import KDL3World
|
||||
|
||||
KDL3UHASH = "201e7658f6194458a3869dde36bf8ec2"
|
||||
KDL3JHASH = "b2f2d004ea640c3db66df958fce122b2"
|
||||
|
||||
level_pointers = {
|
||||
0x770000: 0x0084,
|
||||
0x770001: 0x009C,
|
||||
0x770002: 0x00B8,
|
||||
0x770003: 0x00D8,
|
||||
0x770004: 0x0104,
|
||||
0x770005: 0x0124,
|
||||
0x770006: 0x014C,
|
||||
0x770007: 0x0170,
|
||||
0x770008: 0x0190,
|
||||
0x770009: 0x01B0,
|
||||
0x77000A: 0x01E8,
|
||||
0x77000B: 0x0218,
|
||||
0x77000C: 0x024C,
|
||||
0x77000D: 0x0270,
|
||||
0x77000E: 0x02A0,
|
||||
0x77000F: 0x02C4,
|
||||
0x770010: 0x02EC,
|
||||
0x770011: 0x0314,
|
||||
0x770012: 0x03CC,
|
||||
0x770013: 0x0404,
|
||||
0x770014: 0x042C,
|
||||
0x770015: 0x044C,
|
||||
0x770016: 0x0478,
|
||||
0x770017: 0x049C,
|
||||
0x770018: 0x04E4,
|
||||
0x770019: 0x0504,
|
||||
0x77001A: 0x0530,
|
||||
0x77001B: 0x0554,
|
||||
0x77001C: 0x05A8,
|
||||
0x77001D: 0x0640,
|
||||
0x770200: 0x0148,
|
||||
0x770201: 0x0248,
|
||||
0x770202: 0x03C8,
|
||||
0x770203: 0x04E0,
|
||||
0x770204: 0x06A4,
|
||||
0x770205: 0x06A8,
|
||||
}
|
||||
|
||||
bb_bosses = {
|
||||
0x770200: 0xED85F1,
|
||||
0x770201: 0xF01360,
|
||||
0x770202: 0xEDA3DF,
|
||||
0x770203: 0xEDC2B9,
|
||||
0x770204: 0xED7C3F,
|
||||
0x770205: 0xEC29D2,
|
||||
}
|
||||
|
||||
level_sprites = {
|
||||
0x19B2C6: 1827,
|
||||
0x1A195C: 1584,
|
||||
0x19F6F3: 1679,
|
||||
0x19DC8B: 1717,
|
||||
0x197900: 1872
|
||||
}
|
||||
|
||||
stage_tiles = {
|
||||
0: [
|
||||
0, 1, 2,
|
||||
16, 17, 18,
|
||||
32, 33, 34,
|
||||
48, 49, 50
|
||||
],
|
||||
1: [
|
||||
3, 4, 5,
|
||||
19, 20, 21,
|
||||
35, 36, 37,
|
||||
51, 52, 53
|
||||
],
|
||||
2: [
|
||||
6, 7, 8,
|
||||
22, 23, 24,
|
||||
38, 39, 40,
|
||||
54, 55, 56
|
||||
],
|
||||
3: [
|
||||
9, 10, 11,
|
||||
25, 26, 27,
|
||||
41, 42, 43,
|
||||
57, 58, 59,
|
||||
],
|
||||
4: [
|
||||
12, 13, 64,
|
||||
28, 29, 65,
|
||||
44, 45, 66,
|
||||
60, 61, 67
|
||||
],
|
||||
5: [
|
||||
14, 15, 68,
|
||||
30, 31, 69,
|
||||
46, 47, 70,
|
||||
62, 63, 71
|
||||
]
|
||||
}
|
||||
|
||||
heart_star_address = 0x2D0000
|
||||
heart_star_size = 456
|
||||
consumable_address = 0x2F91DD
|
||||
consumable_size = 698
|
||||
|
||||
stage_palettes = [0x60964, 0x60B64, 0x60D64, 0x60F64, 0x61164]
|
||||
|
||||
music_choices = [
|
||||
2, # Boss 1
|
||||
3, # Boss 2 (Unused)
|
||||
4, # Boss 3 (Miniboss)
|
||||
7, # Dedede
|
||||
9, # Event 2 (used once)
|
||||
10, # Field 1
|
||||
11, # Field 2
|
||||
12, # Field 3
|
||||
13, # Field 4
|
||||
14, # Field 5
|
||||
15, # Field 6
|
||||
16, # Field 7
|
||||
17, # Field 8
|
||||
18, # Field 9
|
||||
19, # Field 10
|
||||
20, # Field 11
|
||||
21, # Field 12 (Gourmet Race)
|
||||
23, # Dark Matter in the Hyper Zone
|
||||
24, # Zero
|
||||
25, # Level 1
|
||||
26, # Level 2
|
||||
27, # Level 4
|
||||
28, # Level 3
|
||||
29, # Heart Star Failed
|
||||
30, # Level 5
|
||||
31, # Minigame
|
||||
38, # Animal Friend 1
|
||||
39, # Animal Friend 2
|
||||
40, # Animal Friend 3
|
||||
]
|
||||
# extra room pointers we don't want to track other than for music
|
||||
room_music = {
|
||||
3079990: 23, # Zero
|
||||
2983409: 2, # BB Whispy
|
||||
3150688: 2, # BB Acro
|
||||
2991071: 2, # BB PonCon
|
||||
2998969: 2, # BB Ado
|
||||
2980927: 7, # BB Dedede
|
||||
2894290: 23 # BB Zero
|
||||
}
|
||||
|
||||
enemy_remap = {
|
||||
"Waddle Dee": 0,
|
||||
"Bronto Burt": 2,
|
||||
"Rocky": 3,
|
||||
"Bobo": 5,
|
||||
"Chilly": 6,
|
||||
"Poppy Bros Jr.": 7,
|
||||
"Sparky": 8,
|
||||
"Polof": 9,
|
||||
"Broom Hatter": 11,
|
||||
"Cappy": 12,
|
||||
"Bouncy": 13,
|
||||
"Nruff": 15,
|
||||
"Glunk": 16,
|
||||
"Togezo": 18,
|
||||
"Kabu": 19,
|
||||
"Mony": 20,
|
||||
"Blipper": 21,
|
||||
"Squishy": 22,
|
||||
"Gabon": 24,
|
||||
"Oro": 25,
|
||||
"Galbo": 26,
|
||||
"Sir Kibble": 27,
|
||||
"Nidoo": 28,
|
||||
"Kany": 29,
|
||||
"Sasuke": 30,
|
||||
"Yaban": 32,
|
||||
"Boten": 33,
|
||||
"Coconut": 34,
|
||||
"Doka": 35,
|
||||
"Icicle": 36,
|
||||
"Pteran": 39,
|
||||
"Loud": 40,
|
||||
"Como": 41,
|
||||
"Klinko": 42,
|
||||
"Babut": 43,
|
||||
"Wappa": 44,
|
||||
"Mariel": 45,
|
||||
"Tick": 48,
|
||||
"Apolo": 49,
|
||||
"Popon Ball": 50,
|
||||
"KeKe": 51,
|
||||
"Magoo": 53,
|
||||
"Raft Waddle Dee": 57,
|
||||
"Madoo": 58,
|
||||
"Corori": 60,
|
||||
"Kapar": 67,
|
||||
"Batamon": 68,
|
||||
"Peran": 72,
|
||||
"Bobin": 73,
|
||||
"Mopoo": 74,
|
||||
"Gansan": 75,
|
||||
"Bukiset (Burning)": 76,
|
||||
"Bukiset (Stone)": 77,
|
||||
"Bukiset (Ice)": 78,
|
||||
"Bukiset (Needle)": 79,
|
||||
"Bukiset (Clean)": 80,
|
||||
"Bukiset (Parasol)": 81,
|
||||
"Bukiset (Spark)": 82,
|
||||
"Bukiset (Cutter)": 83,
|
||||
"Waddle Dee Drawing": 84,
|
||||
"Bronto Burt Drawing": 85,
|
||||
"Bouncy Drawing": 86,
|
||||
"Kabu (Dekabu)": 87,
|
||||
"Wapod": 88,
|
||||
"Propeller": 89,
|
||||
"Dogon": 90,
|
||||
"Joe": 91
|
||||
}
|
||||
|
||||
miniboss_remap = {
|
||||
"Captain Stitch": 0,
|
||||
"Yuki": 1,
|
||||
"Blocky": 2,
|
||||
"Jumper Shoot": 3,
|
||||
"Boboo": 4,
|
||||
"Haboki": 5
|
||||
}
|
||||
|
||||
ability_remap = {
|
||||
"No Ability": 0,
|
||||
"Burning Ability": 1,
|
||||
"Stone Ability": 2,
|
||||
"Ice Ability": 3,
|
||||
"Needle Ability": 4,
|
||||
"Clean Ability": 5,
|
||||
"Parasol Ability": 6,
|
||||
"Spark Ability": 7,
|
||||
"Cutter Ability": 8,
|
||||
}
|
||||
|
||||
|
||||
class RomData:
|
||||
def __init__(self, file: bytes, name: typing.Optional[str] = None):
|
||||
self.file = bytearray(file)
|
||||
self.name = name
|
||||
|
||||
def read_byte(self, offset: int) -> int:
|
||||
return self.file[offset]
|
||||
|
||||
def read_bytes(self, offset: int, length: int) -> bytearray:
|
||||
return self.file[offset:offset + length]
|
||||
|
||||
def write_byte(self, offset: int, value: int) -> None:
|
||||
self.file[offset] = value
|
||||
|
||||
def write_bytes(self, offset: int, values: typing.Sequence[int]) -> None:
|
||||
self.file[offset:offset + len(values)] = values
|
||||
|
||||
def get_bytes(self) -> bytes:
|
||||
return bytes(self.file)
|
||||
|
||||
|
||||
def handle_level_sprites(stages: List[Tuple[int, ...]], sprites: List[bytearray], palettes: List[List[bytearray]]) \
|
||||
-> Tuple[List[bytearray], List[bytearray]]:
|
||||
palette_by_level = list()
|
||||
for palette in palettes:
|
||||
palette_by_level.extend(palette[10:16])
|
||||
out_palettes = list()
|
||||
for i in range(5):
|
||||
for j in range(6):
|
||||
palettes[i][10 + j] = palette_by_level[stages[i][j]]
|
||||
out_palettes.append(bytearray([x for palette in palettes[i] for x in palette]))
|
||||
tiles_by_level = list()
|
||||
for spritesheet in sprites:
|
||||
decompressed = hal_decompress(spritesheet)
|
||||
tiles = [decompressed[i:i + 32] for i in range(0, 2304, 32)]
|
||||
tiles_by_level.extend([[tiles[x] for x in stage_tiles[stage]] for stage in stage_tiles])
|
||||
out_sprites = list()
|
||||
for world in range(5):
|
||||
levels = [stages[world][x] for x in range(6)]
|
||||
world_tiles: typing.List[bytes] = [bytes() for _ in range(72)]
|
||||
for i in range(6):
|
||||
for x in range(12):
|
||||
world_tiles[stage_tiles[i][x]] = tiles_by_level[levels[i]][x]
|
||||
out_sprites.append(bytearray())
|
||||
for tile in world_tiles:
|
||||
out_sprites[world].extend(tile)
|
||||
# insert our fake compression
|
||||
out_sprites[world][0:0] = [0xe3, 0xff]
|
||||
out_sprites[world][1026:1026] = [0xe3, 0xff]
|
||||
out_sprites[world][2052:2052] = [0xe0, 0xff]
|
||||
out_sprites[world].append(0xff)
|
||||
return out_sprites, out_palettes
|
||||
|
||||
|
||||
def write_heart_star_sprites(rom: RomData) -> None:
|
||||
compressed = rom.read_bytes(heart_star_address, heart_star_size)
|
||||
decompressed = hal_decompress(compressed)
|
||||
patch = get_data(__name__, os.path.join("data", "APHeartStar.bsdiff4"))
|
||||
patched = bytearray(bsdiff4.patch(decompressed, patch))
|
||||
rom.write_bytes(0x1AF7DF, patched)
|
||||
patched[0:0] = [0xE3, 0xFF]
|
||||
patched.append(0xFF)
|
||||
rom.write_bytes(0x1CD000, patched)
|
||||
rom.write_bytes(0x3F0EBF, [0x00, 0xD0, 0x39])
|
||||
|
||||
|
||||
def write_consumable_sprites(rom: RomData, consumables: bool, stars: bool) -> None:
|
||||
compressed = rom.read_bytes(consumable_address, consumable_size)
|
||||
decompressed = hal_decompress(compressed)
|
||||
patched = bytearray(decompressed)
|
||||
if consumables:
|
||||
patch = get_data(__name__, os.path.join("data", "APConsumable.bsdiff4"))
|
||||
patched = bytearray(bsdiff4.patch(bytes(patched), patch))
|
||||
if stars:
|
||||
patch = get_data(__name__, os.path.join("data", "APStars.bsdiff4"))
|
||||
patched = bytearray(bsdiff4.patch(bytes(patched), patch))
|
||||
patched[0:0] = [0xE3, 0xFF]
|
||||
patched.append(0xFF)
|
||||
rom.write_bytes(0x1CD500, patched)
|
||||
rom.write_bytes(0x3F0DAE, [0x00, 0xD5, 0x39])
|
||||
|
||||
|
||||
class KDL3PatchExtensions(APPatchExtension):
|
||||
game = "Kirby's Dream Land 3"
|
||||
|
||||
@staticmethod
|
||||
def apply_post_patch(_: APProcedurePatch, rom: bytes) -> bytes:
|
||||
rom_data = RomData(rom)
|
||||
write_heart_star_sprites(rom_data)
|
||||
if rom_data.read_bytes(0x3D014, 1)[0] > 0:
|
||||
stages = [struct.unpack("HHHHHHH", rom_data.read_bytes(0x3D020 + x * 14, 14)) for x in range(5)]
|
||||
palettes = [rom_data.read_bytes(full_pal, 512) for full_pal in stage_palettes]
|
||||
read_palettes = [[palette[i:i + 32] for i in range(0, 512, 32)] for palette in palettes]
|
||||
sprites = [rom_data.read_bytes(offset, level_sprites[offset]) for offset in level_sprites]
|
||||
sprites, palettes = handle_level_sprites(stages, sprites, read_palettes)
|
||||
for addr, palette in zip(stage_palettes, palettes):
|
||||
rom_data.write_bytes(addr, palette)
|
||||
for addr, level_sprite in zip([0x1CA000, 0x1CA920, 0x1CB230, 0x1CBB40, 0x1CC450], sprites):
|
||||
rom_data.write_bytes(addr, level_sprite)
|
||||
rom_data.write_bytes(0x460A, [0x00, 0xA0, 0x39, 0x20, 0xA9, 0x39, 0x30, 0xB2, 0x39, 0x40, 0xBB, 0x39,
|
||||
0x50, 0xC4, 0x39])
|
||||
write_consumable_sprites(rom_data, rom_data.read_byte(0x3D018) > 0, rom_data.read_byte(0x3D01A) > 0)
|
||||
return rom_data.get_bytes()
|
||||
|
||||
|
||||
class KDL3ProcedurePatch(APProcedurePatch, APTokenMixin):
|
||||
hash = [KDL3UHASH, KDL3JHASH]
|
||||
game = "Kirby's Dream Land 3"
|
||||
patch_file_ending = ".apkdl3"
|
||||
procedure = [
|
||||
("apply_bsdiff4", ["kdl3_basepatch.bsdiff4"]),
|
||||
("apply_tokens", ["token_patch.bin"]),
|
||||
("apply_post_patch", []),
|
||||
("calc_snes_crc", [])
|
||||
]
|
||||
name: bytes # used to pass to __init__
|
||||
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
return get_base_rom_bytes()
|
||||
|
||||
|
||||
def patch_rom(world: "KDL3World", patch: KDL3ProcedurePatch) -> None:
|
||||
patch.write_file("kdl3_basepatch.bsdiff4",
|
||||
get_data(__name__, os.path.join("data", "kdl3_basepatch.bsdiff4")))
|
||||
|
||||
# Write open world patch
|
||||
if world.options.open_world:
|
||||
patch.write_token(APTokenTypes.WRITE, 0x143C7, bytes([0xAD, 0xC1, 0x5A, 0xCD, 0xC1, 0x5A, ]))
|
||||
# changes the stage flag function to compare $5AC1 to $5AC1,
|
||||
# always running the "new stage" function
|
||||
# This has further checks present for bosses already, so we just
|
||||
# need to handle regular stages
|
||||
# write check for boss to be unlocked
|
||||
|
||||
if world.options.consumables:
|
||||
# reroute maxim tomatoes to use the 1-UP function, then null out the function
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3002F, bytes([0x37, 0x00]))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x30037, bytes([0xA9, 0x26, 0x00, # LDA #$0026
|
||||
0x22, 0x27, 0xD9, 0x00, # JSL $00D927
|
||||
0xA4, 0xD2, # LDY $D2
|
||||
0x6B, # RTL
|
||||
0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA,
|
||||
0xEA, # NOP #10
|
||||
]))
|
||||
|
||||
# stars handling is built into the rom, so no changes there
|
||||
|
||||
rooms = world.rooms
|
||||
if world.options.music_shuffle > 0:
|
||||
if world.options.music_shuffle == 1:
|
||||
shuffled_music = music_choices.copy()
|
||||
world.random.shuffle(shuffled_music)
|
||||
music_map = dict(zip(music_choices, shuffled_music))
|
||||
# Avoid putting star twinkle in the pool
|
||||
music_map[5] = world.random.choice(music_choices)
|
||||
# Heart Star music doesn't work on regular stages
|
||||
music_map[8] = world.random.choice(music_choices)
|
||||
for room in rooms:
|
||||
room.music = music_map[room.music]
|
||||
for room_ptr in room_music:
|
||||
patch.write_token(APTokenTypes.WRITE, room_ptr + 2, bytes([music_map[room_music[room_ptr]]]))
|
||||
for i, old_music in zip(range(5), [25, 26, 28, 27, 30]):
|
||||
# level themes
|
||||
patch.write_token(APTokenTypes.WRITE, 0x133F2 + i, bytes([music_map[old_music]]))
|
||||
# Zero
|
||||
patch.write_token(APTokenTypes.WRITE, 0x9AE79, music_map[0x18].to_bytes(1, "little"))
|
||||
# Heart Star success and fail
|
||||
patch.write_token(APTokenTypes.WRITE, 0x4A388, music_map[0x08].to_bytes(1, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x4A38D, music_map[0x1D].to_bytes(1, "little"))
|
||||
elif world.options.music_shuffle == 2:
|
||||
for room in rooms:
|
||||
room.music = world.random.choice(music_choices)
|
||||
for room_ptr in room_music:
|
||||
patch.write_token(APTokenTypes.WRITE, room_ptr + 2,
|
||||
world.random.choice(music_choices).to_bytes(1, "little"))
|
||||
for i in range(5):
|
||||
# level themes
|
||||
patch.write_token(APTokenTypes.WRITE, 0x133F2 + i,
|
||||
world.random.choice(music_choices).to_bytes(1, "little"))
|
||||
# Zero
|
||||
patch.write_token(APTokenTypes.WRITE, 0x9AE79, world.random.choice(music_choices).to_bytes(1, "little"))
|
||||
# Heart Star success and fail
|
||||
patch.write_token(APTokenTypes.WRITE, 0x4A388, world.random.choice(music_choices).to_bytes(1, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x4A38D, world.random.choice(music_choices).to_bytes(1, "little"))
|
||||
|
||||
for room in rooms:
|
||||
room.patch(patch, bool(world.options.consumables.value), not bool(world.options.remote_items.value))
|
||||
|
||||
if world.options.virtual_console in [1, 3]:
|
||||
# Flash Reduction
|
||||
patch.write_token(APTokenTypes.WRITE, 0x9AE68, b"\x10")
|
||||
patch.write_token(APTokenTypes.WRITE, 0x9AE8E, bytes([0x08, 0x00, 0x22, 0x5D, 0xF7, 0x00, 0xA2, 0x08, ]))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x9AEA1, b"\x08")
|
||||
patch.write_token(APTokenTypes.WRITE, 0x9AEC9, b"\x01")
|
||||
patch.write_token(APTokenTypes.WRITE, 0x9AED2, bytes([0xA9, 0x1F]))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x9AEE1, b"\x08")
|
||||
|
||||
if world.options.virtual_console in [2, 3]:
|
||||
# Hyper Zone BB colors
|
||||
patch.write_token(APTokenTypes.WRITE, 0x2C5E16, bytes([0xEE, 0x1B, 0x18, 0x5B, 0xD3, 0x4A, 0xF4, 0x3B, ]))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x2C8217, bytes([0xFF, 0x1E, ]))
|
||||
|
||||
# boss requirements
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3D000,
|
||||
struct.pack("HHHHH", world.boss_requirements[0], world.boss_requirements[1],
|
||||
world.boss_requirements[2], world.boss_requirements[3],
|
||||
world.boss_requirements[4]))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3D00A,
|
||||
struct.pack("H", world.required_heart_stars if world.options.goal_speed == 1 else 0xFFFF))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3D00C, world.options.goal_speed.value.to_bytes(2, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3D00E, world.options.open_world.value.to_bytes(2, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3D010, ((world.options.remote_items.value << 1) +
|
||||
world.options.death_link.value).to_bytes(2, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3D012, world.options.goal.value.to_bytes(2, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3D014, world.options.stage_shuffle.value.to_bytes(2, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3D016, world.options.ow_boss_requirement.value.to_bytes(2, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3D018, world.options.consumables.value.to_bytes(2, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3D01A, world.options.starsanity.value.to_bytes(2, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3D01C, world.options.gifting.value.to_bytes(2, "little")
|
||||
if world.multiworld.players > 1 else bytes([0, 0]))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3D01E, world.options.strict_bosses.value.to_bytes(2, "little"))
|
||||
# don't write gifting for solo game, since there's no one to send anything to
|
||||
|
||||
for level in world.player_levels:
|
||||
for i in range(len(world.player_levels[level])):
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3F002E + ((level - 1) * 14) + (i * 2),
|
||||
struct.pack("H", level_pointers[world.player_levels[level][i]]))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3D020 + (level - 1) * 14 + (i * 2),
|
||||
struct.pack("H", world.player_levels[level][i] & 0x00FFFF))
|
||||
if (i == 0) or (i > 0 and i % 6 != 0):
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3D080 + (level - 1) * 12 + (i * 2),
|
||||
struct.pack("H", (world.player_levels[level][i] & 0x00FFFF) % 6))
|
||||
|
||||
for i in range(6):
|
||||
if world.boss_butch_bosses[i]:
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3F0000 + (level_pointers[0x770200 + i]),
|
||||
struct.pack("I", bb_bosses[0x770200 + i]))
|
||||
|
||||
# copy ability shuffle
|
||||
if world.options.copy_ability_randomization.value > 0:
|
||||
for enemy in world.copy_abilities:
|
||||
if enemy in miniboss_remap:
|
||||
patch.write_token(APTokenTypes.WRITE, 0xB417E + (miniboss_remap[enemy] << 1),
|
||||
struct.pack("H", ability_remap[world.copy_abilities[enemy]]))
|
||||
else:
|
||||
patch.write_token(APTokenTypes.WRITE, 0xB3CAC + (enemy_remap[enemy] << 1),
|
||||
struct.pack("H", ability_remap[world.copy_abilities[enemy]]))
|
||||
# following only needs done on non-door rando
|
||||
# incredibly lucky this follows the same order (including 5E == star block)
|
||||
patch.write_token(APTokenTypes.WRITE, 0x2F77EA,
|
||||
(0x5E + (ability_remap[world.copy_abilities["Sparky"]] << 1)).to_bytes(1, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x2F7811,
|
||||
(0x5E + (ability_remap[world.copy_abilities["Sparky"]] << 1)).to_bytes(1, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x2F9BC4,
|
||||
(0x5E + (ability_remap[world.copy_abilities["Blocky"]] << 1)).to_bytes(1, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x2F9BEB,
|
||||
(0x5E + (ability_remap[world.copy_abilities["Blocky"]] << 1)).to_bytes(1, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x2FAC06,
|
||||
(0x5E + (ability_remap[world.copy_abilities["Jumper Shoot"]] << 1)).to_bytes(1, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x2FAC2D,
|
||||
(0x5E + (ability_remap[world.copy_abilities["Jumper Shoot"]] << 1)).to_bytes(1, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x2F9E7B,
|
||||
(0x5E + (ability_remap[world.copy_abilities["Yuki"]] << 1)).to_bytes(1, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x2F9EA2,
|
||||
(0x5E + (ability_remap[world.copy_abilities["Yuki"]] << 1)).to_bytes(1, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x2FA951,
|
||||
(0x5E + (ability_remap[world.copy_abilities["Sir Kibble"]] << 1)).to_bytes(1, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x2FA978,
|
||||
(0x5E + (ability_remap[world.copy_abilities["Sir Kibble"]] << 1)).to_bytes(1, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x2FA132,
|
||||
(0x5E + (ability_remap[world.copy_abilities["Haboki"]] << 1)).to_bytes(1, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x2FA159,
|
||||
(0x5E + (ability_remap[world.copy_abilities["Haboki"]] << 1)).to_bytes(1, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x2FA3E8,
|
||||
(0x5E + (ability_remap[world.copy_abilities["Boboo"]] << 1)).to_bytes(1, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x2FA40F,
|
||||
(0x5E + (ability_remap[world.copy_abilities["Boboo"]] << 1)).to_bytes(1, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x2F90E2,
|
||||
(0x5E + (ability_remap[world.copy_abilities["Captain Stitch"]] << 1)).to_bytes(1, "little"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x2F9109,
|
||||
(0x5E + (ability_remap[world.copy_abilities["Captain Stitch"]] << 1)).to_bytes(1, "little"))
|
||||
|
||||
if world.options.copy_ability_randomization == 2:
|
||||
for enemy in enemy_remap:
|
||||
# we just won't include it for minibosses
|
||||
patch.write_token(APTokenTypes.WRITE, 0xB3E40 + (enemy_remap[enemy] << 1),
|
||||
struct.pack("h", world.random.randint(-1, 2)))
|
||||
|
||||
# write jumping goal
|
||||
patch.write_token(APTokenTypes.WRITE, 0x94F8, struct.pack("H", world.options.jumping_target))
|
||||
patch.write_token(APTokenTypes.WRITE, 0x944E, struct.pack("H", world.options.jumping_target))
|
||||
|
||||
from Utils import __version__
|
||||
patch_name = bytearray(
|
||||
f'KDL3{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0', 'utf8')[:21]
|
||||
patch_name.extend([0] * (21 - len(patch_name)))
|
||||
patch.name = bytes(patch_name)
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3C000, patch.name)
|
||||
patch.write_token(APTokenTypes.WRITE, 0x3C020, world.options.game_language.value.to_bytes(1, "little"))
|
||||
|
||||
patch.write_token(APTokenTypes.COPY, 0x7FC0, (21, 0x3C000))
|
||||
patch.write_token(APTokenTypes.COPY, 0x7FD9, (1, 0x3C020))
|
||||
|
||||
# handle palette
|
||||
if world.options.kirby_flavor_preset.value != 0:
|
||||
for addr in kirby_target_palettes:
|
||||
target = kirby_target_palettes[addr]
|
||||
palette = get_kirby_palette(world)
|
||||
if palette is not None:
|
||||
patch.write_token(APTokenTypes.WRITE, addr, get_palette_bytes(palette, target[0], target[1], target[2]))
|
||||
|
||||
if world.options.gooey_flavor_preset.value != 0:
|
||||
for addr in gooey_target_palettes:
|
||||
target = gooey_target_palettes[addr]
|
||||
palette = get_gooey_palette(world)
|
||||
if palette is not None:
|
||||
patch.write_token(APTokenTypes.WRITE, addr, get_palette_bytes(palette, target[0], target[1], target[2]))
|
||||
|
||||
patch.write_file("token_patch.bin", patch.get_token_binary())
|
||||
|
||||
|
||||
def get_base_rom_bytes() -> bytes:
|
||||
rom_file: str = get_base_rom_path()
|
||||
base_rom_bytes: Optional[bytes] = getattr(get_base_rom_bytes, "base_rom_bytes", None)
|
||||
if not base_rom_bytes:
|
||||
base_rom_bytes = bytes(Utils.read_snes_rom(open(rom_file, "rb")))
|
||||
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if basemd5.hexdigest() not in {KDL3UHASH, KDL3JHASH}:
|
||||
raise Exception("Supplied Base Rom does not match known MD5 for US or JP release. "
|
||||
"Get the correct game and version, then dump it")
|
||||
get_base_rom_bytes.base_rom_bytes = base_rom_bytes
|
||||
return base_rom_bytes
|
||||
|
||||
|
||||
def get_base_rom_path(file_name: str = "") -> str:
|
||||
options: settings.Settings = settings.get_settings()
|
||||
if not file_name:
|
||||
file_name = options["kdl3_options"]["rom_file"]
|
||||
if not os.path.exists(file_name):
|
||||
file_name = Utils.user_path(file_name)
|
||||
return file_name
|
||||
133
worlds/kdl3/room.py
Normal file
133
worlds/kdl3/room.py
Normal file
@@ -0,0 +1,133 @@
|
||||
import struct
|
||||
from typing import Optional, Dict, TYPE_CHECKING, List, Union
|
||||
from BaseClasses import Region, ItemClassification, MultiWorld
|
||||
from worlds.Files import APTokenTypes
|
||||
from .client_addrs import consumable_addrs, star_addrs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .rom import KDL3ProcedurePatch
|
||||
|
||||
animal_map = {
|
||||
"Rick Spawn": 0,
|
||||
"Kine Spawn": 1,
|
||||
"Coo Spawn": 2,
|
||||
"Nago Spawn": 3,
|
||||
"ChuChu Spawn": 4,
|
||||
"Pitch Spawn": 5
|
||||
}
|
||||
|
||||
|
||||
class KDL3Room(Region):
|
||||
pointer: int = 0
|
||||
level: int = 0
|
||||
stage: int = 0
|
||||
room: int = 0
|
||||
music: int = 0
|
||||
default_exits: List[Dict[str, Union[int, List[str]]]]
|
||||
animal_pointers: List[int]
|
||||
enemies: List[str]
|
||||
entity_load: List[List[int]]
|
||||
consumables: List[Dict[str, Union[int, str]]]
|
||||
|
||||
def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str], level: int,
|
||||
stage: int, room: int, pointer: int, music: int,
|
||||
default_exits: List[Dict[str, List[str]]],
|
||||
animal_pointers: List[int], enemies: List[str],
|
||||
entity_load: List[List[int]],
|
||||
consumables: List[Dict[str, Union[int, str]]], consumable_pointer: int) -> None:
|
||||
super().__init__(name, player, multiworld, hint)
|
||||
self.level = level
|
||||
self.stage = stage
|
||||
self.room = room
|
||||
self.pointer = pointer
|
||||
self.music = music
|
||||
self.default_exits = default_exits
|
||||
self.animal_pointers = animal_pointers
|
||||
self.enemies = enemies
|
||||
self.entity_load = entity_load
|
||||
self.consumables = consumables
|
||||
self.consumable_pointer = consumable_pointer
|
||||
|
||||
def patch(self, patch: "KDL3ProcedurePatch", consumables: bool, local_items: bool) -> None:
|
||||
patch.write_token(APTokenTypes.WRITE, self.pointer + 2, self.music.to_bytes(1, "little"))
|
||||
animals = [x.item.name for x in self.locations if "Animal" in x.name and x.item]
|
||||
if len(animals) > 0:
|
||||
for current_animal, address in zip(animals, self.animal_pointers):
|
||||
patch.write_token(APTokenTypes.WRITE, self.pointer + address + 7,
|
||||
animal_map[current_animal].to_bytes(1, "little"))
|
||||
if local_items:
|
||||
for location in self.get_locations():
|
||||
if location.item is None or location.item.player != self.player:
|
||||
continue
|
||||
item = location.item.code
|
||||
if item is None:
|
||||
continue
|
||||
item_idx = item & 0x00000F
|
||||
location_idx = location.address & 0xFFFF
|
||||
if location_idx & 0xF00 in (0x300, 0x400, 0x500, 0x600):
|
||||
# consumable or star, need remapped
|
||||
location_base = location_idx & 0xF00
|
||||
if location_base == 0x300:
|
||||
# consumable
|
||||
location_idx = consumable_addrs[location_idx & 0xFF] | 0x1000
|
||||
else:
|
||||
# star
|
||||
location_idx = star_addrs[location.address] | 0x2000
|
||||
if item & 0x000070 == 0:
|
||||
patch.write_token(APTokenTypes.WRITE, 0x4B000 + location_idx, bytes([item_idx | 0x10]))
|
||||
elif item & 0x000010 > 0:
|
||||
patch.write_token(APTokenTypes.WRITE, 0x4B000 + location_idx, bytes([item_idx | 0x20]))
|
||||
elif item & 0x000020 > 0:
|
||||
patch.write_token(APTokenTypes.WRITE, 0x4B000 + location_idx, bytes([item_idx | 0x40]))
|
||||
elif item & 0x000040 > 0:
|
||||
patch.write_token(APTokenTypes.WRITE, 0x4B000 + location_idx, bytes([item_idx | 0x80]))
|
||||
|
||||
if consumables:
|
||||
load_len = len(self.entity_load)
|
||||
for consumable in self.consumables:
|
||||
location = next(x for x in self.locations if x.name == consumable["name"])
|
||||
assert location.item is not None
|
||||
is_progression = location.item.classification & ItemClassification.progression
|
||||
if load_len == 8:
|
||||
# edge case, there is exactly 1 room with 8 entities and only 1 consumable among them
|
||||
if not (any(x in self.entity_load for x in [[0, 22], [1, 22]])
|
||||
and any(x in self.entity_load for x in [[2, 22], [3, 22]])):
|
||||
replacement_target = self.entity_load.index(
|
||||
next(x for x in self.entity_load if x in [[0, 22], [1, 22], [2, 22], [3, 22]]))
|
||||
if is_progression:
|
||||
vtype = 0
|
||||
else:
|
||||
vtype = 2
|
||||
patch.write_token(APTokenTypes.WRITE, self.pointer + 88 + (replacement_target * 2),
|
||||
vtype.to_bytes(1, "little"))
|
||||
self.entity_load[replacement_target] = [vtype, 22]
|
||||
else:
|
||||
if is_progression:
|
||||
# we need to see if 1-ups are in our load list
|
||||
if any(x not in self.entity_load for x in [[0, 22], [1, 22]]):
|
||||
self.entity_load.append([0, 22])
|
||||
else:
|
||||
if any(x not in self.entity_load for x in [[2, 22], [3, 22]]):
|
||||
# edge case: if (1, 22) is in, we need to load (3, 22) instead
|
||||
if [1, 22] in self.entity_load:
|
||||
self.entity_load.append([3, 22])
|
||||
else:
|
||||
self.entity_load.append([2, 22])
|
||||
if load_len < len(self.entity_load):
|
||||
patch.write_token(APTokenTypes.WRITE, self.pointer + 88 + (load_len * 2),
|
||||
bytes(self.entity_load[load_len]))
|
||||
patch.write_token(APTokenTypes.WRITE, self.pointer + 104 + (load_len * 2),
|
||||
bytes(struct.pack("H", self.consumable_pointer)))
|
||||
if is_progression:
|
||||
if [1, 22] in self.entity_load:
|
||||
vtype = 1
|
||||
else:
|
||||
vtype = 0
|
||||
else:
|
||||
if [3, 22] in self.entity_load:
|
||||
vtype = 3
|
||||
else:
|
||||
vtype = 2
|
||||
assert isinstance(consumable["pointer"], int)
|
||||
patch.write_token(APTokenTypes.WRITE, self.pointer + consumable["pointer"] + 7,
|
||||
vtype.to_bytes(1, "little"))
|
||||
@@ -1,7 +1,7 @@
|
||||
from worlds.generic.Rules import set_rule, add_rule
|
||||
from .Names import LocationName, EnemyAbilities
|
||||
from .Locations import location_table
|
||||
from .Options import GoalSpeed
|
||||
from .names import location_name, enemy_abilities, animal_friend_spawns
|
||||
from .locations import location_table
|
||||
from .options import GoalSpeed
|
||||
import typing
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
@@ -10,9 +10,9 @@ if typing.TYPE_CHECKING:
|
||||
|
||||
|
||||
def can_reach_boss(state: "CollectionState", player: int, level: int, open_world: int,
|
||||
ow_boss_req: int, player_levels: typing.Dict[int, typing.Dict[int, int]]):
|
||||
ow_boss_req: int, player_levels: typing.Dict[int, typing.List[int]]) -> bool:
|
||||
if open_world:
|
||||
return state.has(f"{LocationName.level_names_inverse[level]} - Stage Completion", player, ow_boss_req)
|
||||
return state.has(f"{location_name.level_names_inverse[level]} - Stage Completion", player, ow_boss_req)
|
||||
else:
|
||||
return state.can_reach(location_table[player_levels[level][5]], "Location", player)
|
||||
|
||||
@@ -86,11 +86,11 @@ ability_map: typing.Dict[str, typing.Callable[["CollectionState", int], bool]] =
|
||||
}
|
||||
|
||||
|
||||
def can_assemble_rob(state: "CollectionState", player: int, copy_abilities: typing.Dict[str, str]):
|
||||
def can_assemble_rob(state: "CollectionState", player: int, copy_abilities: typing.Dict[str, str]) -> bool:
|
||||
# check animal requirements
|
||||
if not (can_reach_coo(state, player) and can_reach_kine(state, player)):
|
||||
return False
|
||||
for abilities, bukisets in EnemyAbilities.enemy_restrictive[1:5]:
|
||||
for abilities, bukisets in enemy_abilities.enemy_restrictive[1:5]:
|
||||
iterator = iter(x for x in bukisets if copy_abilities[x] in abilities)
|
||||
target_bukiset = next(iterator, None)
|
||||
can_reach = False
|
||||
@@ -103,7 +103,7 @@ def can_assemble_rob(state: "CollectionState", player: int, copy_abilities: typi
|
||||
return can_reach_parasol(state, player) and can_reach_stone(state, player)
|
||||
|
||||
|
||||
def can_fix_angel_wings(state: "CollectionState", player: int, copy_abilities: typing.Dict[str, str]):
|
||||
def can_fix_angel_wings(state: "CollectionState", player: int, copy_abilities: typing.Dict[str, str]) -> bool:
|
||||
can_reach = True
|
||||
for enemy in {"Sparky", "Blocky", "Jumper Shoot", "Yuki", "Sir Kibble", "Haboki", "Boboo", "Captain Stitch"}:
|
||||
can_reach = can_reach & ability_map[copy_abilities[enemy]](state, player)
|
||||
@@ -112,114 +112,114 @@ def can_fix_angel_wings(state: "CollectionState", player: int, copy_abilities: t
|
||||
|
||||
def set_rules(world: "KDL3World") -> None:
|
||||
# Level 1
|
||||
set_rule(world.multiworld.get_location(LocationName.grass_land_muchi, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.grass_land_muchi, world.player),
|
||||
lambda state: can_reach_chuchu(state, world.player))
|
||||
set_rule(world.multiworld.get_location(LocationName.grass_land_chao, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.grass_land_chao, world.player),
|
||||
lambda state: can_reach_stone(state, world.player))
|
||||
set_rule(world.multiworld.get_location(LocationName.grass_land_mine, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.grass_land_mine, world.player),
|
||||
lambda state: can_reach_kine(state, world.player))
|
||||
|
||||
# Level 2
|
||||
set_rule(world.multiworld.get_location(LocationName.ripple_field_5, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.ripple_field_5, world.player),
|
||||
lambda state: can_reach_kine(state, world.player))
|
||||
set_rule(world.multiworld.get_location(LocationName.ripple_field_kamuribana, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.ripple_field_kamuribana, world.player),
|
||||
lambda state: can_reach_pitch(state, world.player) and can_reach_clean(state, world.player))
|
||||
set_rule(world.multiworld.get_location(LocationName.ripple_field_bakasa, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.ripple_field_bakasa, world.player),
|
||||
lambda state: can_reach_kine(state, world.player) and can_reach_parasol(state, world.player))
|
||||
set_rule(world.multiworld.get_location(LocationName.ripple_field_toad, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.ripple_field_toad, world.player),
|
||||
lambda state: can_reach_needle(state, world.player))
|
||||
set_rule(world.multiworld.get_location(LocationName.ripple_field_mama_pitch, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.ripple_field_mama_pitch, world.player),
|
||||
lambda state: (can_reach_pitch(state, world.player) and
|
||||
can_reach_kine(state, world.player) and
|
||||
can_reach_burning(state, world.player) and
|
||||
can_reach_stone(state, world.player)))
|
||||
|
||||
# Level 3
|
||||
set_rule(world.multiworld.get_location(LocationName.sand_canyon_5, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.sand_canyon_5, world.player),
|
||||
lambda state: can_reach_cutter(state, world.player))
|
||||
set_rule(world.multiworld.get_location(LocationName.sand_canyon_auntie, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.sand_canyon_auntie, world.player),
|
||||
lambda state: can_reach_clean(state, world.player))
|
||||
set_rule(world.multiworld.get_location(LocationName.sand_canyon_nyupun, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.sand_canyon_nyupun, world.player),
|
||||
lambda state: can_reach_chuchu(state, world.player) and can_reach_cutter(state, world.player))
|
||||
set_rule(world.multiworld.get_location(LocationName.sand_canyon_rob, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.sand_canyon_rob, world.player),
|
||||
lambda state: can_assemble_rob(state, world.player, world.copy_abilities)
|
||||
)
|
||||
|
||||
# Level 4
|
||||
set_rule(world.multiworld.get_location(LocationName.cloudy_park_hibanamodoki, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.cloudy_park_hibanamodoki, world.player),
|
||||
lambda state: can_reach_coo(state, world.player) and can_reach_clean(state, world.player))
|
||||
set_rule(world.multiworld.get_location(LocationName.cloudy_park_piyokeko, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.cloudy_park_piyokeko, world.player),
|
||||
lambda state: can_reach_needle(state, world.player))
|
||||
set_rule(world.multiworld.get_location(LocationName.cloudy_park_mikarin, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.cloudy_park_mikarin, world.player),
|
||||
lambda state: can_reach_coo(state, world.player))
|
||||
set_rule(world.multiworld.get_location(LocationName.cloudy_park_pick, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.cloudy_park_pick, world.player),
|
||||
lambda state: can_reach_rick(state, world.player))
|
||||
|
||||
# Level 5
|
||||
set_rule(world.multiworld.get_location(LocationName.iceberg_4, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.iceberg_4, world.player),
|
||||
lambda state: can_reach_burning(state, world.player))
|
||||
set_rule(world.multiworld.get_location(LocationName.iceberg_kogoesou, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.iceberg_kogoesou, world.player),
|
||||
lambda state: can_reach_burning(state, world.player))
|
||||
set_rule(world.multiworld.get_location(LocationName.iceberg_samus, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.iceberg_samus, world.player),
|
||||
lambda state: can_reach_ice(state, world.player))
|
||||
set_rule(world.multiworld.get_location(LocationName.iceberg_name, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.iceberg_name, world.player),
|
||||
lambda state: (can_reach_coo(state, world.player) and
|
||||
can_reach_burning(state, world.player) and
|
||||
can_reach_chuchu(state, world.player)))
|
||||
# ChuChu is guaranteed here, but we use this for consistency
|
||||
set_rule(world.multiworld.get_location(LocationName.iceberg_shiro, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.iceberg_shiro, world.player),
|
||||
lambda state: can_reach_nago(state, world.player))
|
||||
set_rule(world.multiworld.get_location(LocationName.iceberg_angel, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.iceberg_angel, world.player),
|
||||
lambda state: can_fix_angel_wings(state, world.player, world.copy_abilities))
|
||||
|
||||
# Consumables
|
||||
if world.options.consumables:
|
||||
set_rule(world.multiworld.get_location(LocationName.grass_land_1_u1, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.grass_land_1_u1, world.player),
|
||||
lambda state: can_reach_parasol(state, world.player))
|
||||
set_rule(world.multiworld.get_location(LocationName.grass_land_1_m1, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.grass_land_1_m1, world.player),
|
||||
lambda state: can_reach_spark(state, world.player))
|
||||
set_rule(world.multiworld.get_location(LocationName.grass_land_2_u1, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.grass_land_2_u1, world.player),
|
||||
lambda state: can_reach_needle(state, world.player))
|
||||
set_rule(world.multiworld.get_location(LocationName.ripple_field_2_u1, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.ripple_field_2_u1, world.player),
|
||||
lambda state: can_reach_kine(state, world.player))
|
||||
set_rule(world.multiworld.get_location(LocationName.ripple_field_2_m1, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.ripple_field_2_m1, world.player),
|
||||
lambda state: can_reach_kine(state, world.player))
|
||||
set_rule(world.multiworld.get_location(LocationName.ripple_field_3_u1, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.ripple_field_3_u1, world.player),
|
||||
lambda state: can_reach_cutter(state, world.player) or can_reach_spark(state, world.player))
|
||||
set_rule(world.multiworld.get_location(LocationName.ripple_field_4_u1, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.ripple_field_4_u1, world.player),
|
||||
lambda state: can_reach_stone(state, world.player))
|
||||
set_rule(world.multiworld.get_location(LocationName.ripple_field_4_m2, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.ripple_field_4_m2, world.player),
|
||||
lambda state: can_reach_stone(state, world.player))
|
||||
set_rule(world.multiworld.get_location(LocationName.ripple_field_5_m1, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.ripple_field_5_m1, world.player),
|
||||
lambda state: can_reach_kine(state, world.player))
|
||||
set_rule(world.multiworld.get_location(LocationName.ripple_field_5_u1, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.ripple_field_5_u1, world.player),
|
||||
lambda state: (can_reach_kine(state, world.player) and
|
||||
can_reach_burning(state, world.player) and
|
||||
can_reach_stone(state, world.player)))
|
||||
set_rule(world.multiworld.get_location(LocationName.ripple_field_5_m2, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.ripple_field_5_m2, world.player),
|
||||
lambda state: (can_reach_kine(state, world.player) and
|
||||
can_reach_burning(state, world.player) and
|
||||
can_reach_stone(state, world.player)))
|
||||
set_rule(world.multiworld.get_location(LocationName.sand_canyon_4_u1, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.sand_canyon_4_u1, world.player),
|
||||
lambda state: can_reach_clean(state, world.player))
|
||||
set_rule(world.multiworld.get_location(LocationName.sand_canyon_4_m2, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.sand_canyon_4_m2, world.player),
|
||||
lambda state: can_reach_needle(state, world.player))
|
||||
set_rule(world.multiworld.get_location(LocationName.sand_canyon_5_u2, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u2, world.player),
|
||||
lambda state: can_reach_ice(state, world.player) and
|
||||
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
|
||||
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
|
||||
or can_reach_nago(state, world.player)))
|
||||
set_rule(world.multiworld.get_location(LocationName.sand_canyon_5_u3, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u3, world.player),
|
||||
lambda state: can_reach_ice(state, world.player) and
|
||||
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
|
||||
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
|
||||
or can_reach_nago(state, world.player)))
|
||||
set_rule(world.multiworld.get_location(LocationName.sand_canyon_5_u4, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u4, world.player),
|
||||
lambda state: can_reach_ice(state, world.player) and
|
||||
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player)
|
||||
or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player)
|
||||
or can_reach_nago(state, world.player)))
|
||||
set_rule(world.multiworld.get_location(LocationName.cloudy_park_6_u1, world.player),
|
||||
set_rule(world.multiworld.get_location(location_name.cloudy_park_6_u1, world.player),
|
||||
lambda state: can_reach_cutter(state, world.player))
|
||||
|
||||
if world.options.starsanity:
|
||||
@@ -274,50 +274,57 @@ def set_rules(world: "KDL3World") -> None:
|
||||
# copy ability access edge cases
|
||||
# Kirby cannot eat enemies fully submerged in water. Vast majority of cases, the enemy can be brought to the surface
|
||||
# and eaten by inhaling while falling on top of them
|
||||
set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_2_E3, world.player),
|
||||
set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_2_E3, world.player),
|
||||
lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
|
||||
set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_3_E6, world.player),
|
||||
set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_3_E6, world.player),
|
||||
lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
|
||||
# Ripple Field 4 E5, E7, and E8 are doable, but too strict to leave in logic
|
||||
set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_4_E5, world.player),
|
||||
set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_4_E5, world.player),
|
||||
lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
|
||||
set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_4_E7, world.player),
|
||||
set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_4_E7, world.player),
|
||||
lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
|
||||
set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_4_E8, world.player),
|
||||
set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_4_E8, world.player),
|
||||
lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
|
||||
set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_5_E1, world.player),
|
||||
set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_5_E1, world.player),
|
||||
lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
|
||||
set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_5_E2, world.player),
|
||||
set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_5_E2, world.player),
|
||||
lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
|
||||
set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_5_E3, world.player),
|
||||
set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_5_E3, world.player),
|
||||
lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
|
||||
set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_5_E4, world.player),
|
||||
set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_5_E4, world.player),
|
||||
lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
|
||||
set_rule(world.multiworld.get_location(EnemyAbilities.Sand_Canyon_4_E7, world.player),
|
||||
set_rule(world.multiworld.get_location(enemy_abilities.Sand_Canyon_4_E7, world.player),
|
||||
lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
|
||||
set_rule(world.multiworld.get_location(EnemyAbilities.Sand_Canyon_4_E8, world.player),
|
||||
set_rule(world.multiworld.get_location(enemy_abilities.Sand_Canyon_4_E8, world.player),
|
||||
lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
|
||||
set_rule(world.multiworld.get_location(EnemyAbilities.Sand_Canyon_4_E9, world.player),
|
||||
set_rule(world.multiworld.get_location(enemy_abilities.Sand_Canyon_4_E9, world.player),
|
||||
lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
|
||||
set_rule(world.multiworld.get_location(EnemyAbilities.Sand_Canyon_4_E10, world.player),
|
||||
set_rule(world.multiworld.get_location(enemy_abilities.Sand_Canyon_4_E10, world.player),
|
||||
lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
|
||||
|
||||
# animal friend rules
|
||||
set_rule(world.multiworld.get_location(animal_friend_spawns.iceberg_4_a2, world.player),
|
||||
lambda state: can_reach_coo(state, world.player) and can_reach_burning(state, world.player))
|
||||
set_rule(world.multiworld.get_location(animal_friend_spawns.iceberg_4_a3, world.player),
|
||||
lambda state: can_reach_chuchu(state, world.player) and can_reach_coo(state, world.player)
|
||||
and can_reach_burning(state, world.player))
|
||||
|
||||
for boss_flag, purification, i in zip(["Level 1 Boss - Purified", "Level 2 Boss - Purified",
|
||||
"Level 3 Boss - Purified", "Level 4 Boss - Purified",
|
||||
"Level 5 Boss - Purified"],
|
||||
[LocationName.grass_land_whispy, LocationName.ripple_field_acro,
|
||||
LocationName.sand_canyon_poncon, LocationName.cloudy_park_ado,
|
||||
LocationName.iceberg_dedede],
|
||||
[location_name.grass_land_whispy, location_name.ripple_field_acro,
|
||||
location_name.sand_canyon_poncon, location_name.cloudy_park_ado,
|
||||
location_name.iceberg_dedede],
|
||||
range(1, 6)):
|
||||
set_rule(world.multiworld.get_location(boss_flag, world.player),
|
||||
lambda state, i=i: (state.has("Heart Star", world.player, world.boss_requirements[i - 1])
|
||||
and can_reach_boss(state, world.player, i,
|
||||
lambda state, x=i: (state.has("Heart Star", world.player, world.boss_requirements[x - 1])
|
||||
and can_reach_boss(state, world.player, x,
|
||||
world.options.open_world.value,
|
||||
world.options.ow_boss_requirement.value,
|
||||
world.player_levels)))
|
||||
set_rule(world.multiworld.get_location(purification, world.player),
|
||||
lambda state, i=i: (state.has("Heart Star", world.player, world.boss_requirements[i - 1])
|
||||
and can_reach_boss(state, world.player, i,
|
||||
lambda state, x=i: (state.has("Heart Star", world.player, world.boss_requirements[x - 1])
|
||||
and can_reach_boss(state, world.player, x,
|
||||
world.options.open_world.value,
|
||||
world.options.ow_boss_requirement.value,
|
||||
world.player_levels)))
|
||||
@@ -327,12 +334,12 @@ def set_rules(world: "KDL3World") -> None:
|
||||
|
||||
for level in range(2, 6):
|
||||
set_rule(world.multiworld.get_entrance(f"To Level {level}", world.player),
|
||||
lambda state, i=level: state.has(f"Level {i - 1} Boss Defeated", world.player))
|
||||
lambda state, x=level: state.has(f"Level {x - 1} Boss Defeated", world.player))
|
||||
|
||||
if world.options.strict_bosses:
|
||||
for level in range(2, 6):
|
||||
add_rule(world.multiworld.get_entrance(f"To Level {level}", world.player),
|
||||
lambda state, i=level: state.has(f"Level {i - 1} Boss Purified", world.player))
|
||||
lambda state, x=level: state.has(f"Level {x - 1} Boss Purified", world.player))
|
||||
|
||||
if world.options.goal_speed == GoalSpeed.option_normal:
|
||||
add_rule(world.multiworld.get_entrance("To Level 6", world.player),
|
||||
@@ -58,6 +58,10 @@ org $01AFC8
|
||||
org $01B013
|
||||
SEC ; Remove Dedede Bad Ending
|
||||
|
||||
org $01B050
|
||||
JSL HookBossPurify
|
||||
NOP
|
||||
|
||||
org $02B7B0 ; Zero unlock
|
||||
LDA $80A0
|
||||
CMP #$0001
|
||||
@@ -160,7 +164,6 @@ CopyAbilityAnimalOverride:
|
||||
STA $39DF, X
|
||||
RTL
|
||||
|
||||
org $079A00
|
||||
HeartStarCheck:
|
||||
TXA
|
||||
CMP #$0000 ; is this level 1
|
||||
@@ -201,7 +204,6 @@ HeartStarCheck:
|
||||
SEC
|
||||
RTL
|
||||
|
||||
org $079A80
|
||||
OpenWorldUnlock:
|
||||
PHX
|
||||
LDX $900E ; Are we on open world?
|
||||
@@ -224,7 +226,6 @@ OpenWorldUnlock:
|
||||
PLX
|
||||
RTL
|
||||
|
||||
org $079B00
|
||||
MainLoopHook:
|
||||
STA $D4
|
||||
INC $3524
|
||||
@@ -239,16 +240,18 @@ MainLoopHook:
|
||||
BEQ .Return ; return if we are
|
||||
LDA $5541 ; gooey status
|
||||
BPL .Slowness ; gooey is already spawned
|
||||
LDA $39D1 ; is kirby alive?
|
||||
BEQ .Slowness ; branch if he isn't
|
||||
; maybe BMI here too?
|
||||
LDA $8080
|
||||
CMP #$0000 ; did we get a gooey trap
|
||||
BEQ .Slowness ; branch if we did not
|
||||
JSL GooeySpawn
|
||||
STZ $8080
|
||||
DEC $8080
|
||||
.Slowness:
|
||||
LDA $8082 ; slowness
|
||||
BEQ .Eject ; are we under the effects of a slowness trap
|
||||
DEC
|
||||
STA $8082 ; dec by 1 each frame
|
||||
DEC $8082 ; dec by 1 each frame
|
||||
.Eject:
|
||||
PHX
|
||||
PHY
|
||||
@@ -258,14 +261,13 @@ MainLoopHook:
|
||||
BEQ .PullVars ; branch if we haven't received eject
|
||||
LDA #$2000 ; select button press
|
||||
STA $60C1 ; write to controller mirror
|
||||
STZ $8084
|
||||
DEC $8084
|
||||
.PullVars:
|
||||
PLY
|
||||
PLX
|
||||
.Return:
|
||||
RTL
|
||||
|
||||
org $079B80
|
||||
HeartStarGraphicFix:
|
||||
LDA #$0000
|
||||
PHX
|
||||
@@ -288,7 +290,7 @@ HeartStarGraphicFix:
|
||||
ASL
|
||||
TAX
|
||||
LDA $07D080, X ; table of original stage number
|
||||
CMP #$0003 ; is the current stage a minigame stage?
|
||||
CMP #$0002 ; is the current stage a minigame stage?
|
||||
BEQ .ReturnTrue ; branch if so
|
||||
CLC
|
||||
BRA .Return
|
||||
@@ -299,7 +301,6 @@ HeartStarGraphicFix:
|
||||
PLX
|
||||
RTL
|
||||
|
||||
org $079BF0
|
||||
ParseItemQueue:
|
||||
; Local item queue parsing
|
||||
NOP
|
||||
@@ -336,8 +337,6 @@ ParseItemQueue:
|
||||
AND #$000F
|
||||
ASL
|
||||
TAY
|
||||
LDA $8080,Y
|
||||
BNE .LoopCheck
|
||||
JSL .ApplyNegative
|
||||
RTL
|
||||
.ApplyAbility:
|
||||
@@ -418,35 +417,73 @@ ParseItemQueue:
|
||||
CPY #$0005
|
||||
BCS .PlayNone
|
||||
LDA $8080,Y
|
||||
BNE .Return
|
||||
CPY #$0002
|
||||
BNE .Increment
|
||||
CLC
|
||||
LDA #$0384
|
||||
ADC $8080, Y
|
||||
BVC .PlayNegative
|
||||
LDA #$FFFF
|
||||
.PlayNegative:
|
||||
STA $8080,Y
|
||||
LDA #$00A7
|
||||
BRA .PlaySFXLong
|
||||
.Increment:
|
||||
INC
|
||||
STA $8080, Y
|
||||
BRA .PlayNegative
|
||||
.PlayNone:
|
||||
LDA #$0000
|
||||
BRA .PlaySFXLong
|
||||
|
||||
org $079D00
|
||||
AnimalFriendSpawn:
|
||||
PHA
|
||||
CPX #$0002 ; is this an animal friend?
|
||||
BNE .Return
|
||||
XBA
|
||||
PHA
|
||||
PHX
|
||||
PHA
|
||||
LDX #$0000
|
||||
.CheckSpawned:
|
||||
LDA $05CA, X
|
||||
BNE .Continue
|
||||
LDA #$0002
|
||||
CMP $074A, X
|
||||
BNE .ContinueCheck
|
||||
PLA
|
||||
PHA
|
||||
XBA
|
||||
CMP $07CA, X
|
||||
BEQ .AlreadySpawned
|
||||
.ContinueCheck:
|
||||
INX
|
||||
INX
|
||||
BRA .CheckSpawned
|
||||
.Continue:
|
||||
PLA
|
||||
PLX
|
||||
ASL
|
||||
TAY
|
||||
PLA
|
||||
INC
|
||||
CMP $8000, Y ; do we have this animal friend
|
||||
BEQ .Return ; we have this animal friend
|
||||
.False:
|
||||
INX
|
||||
.Return:
|
||||
PLY
|
||||
LDA #$9999
|
||||
RTL
|
||||
.AlreadySpawned:
|
||||
PLA
|
||||
PLX
|
||||
ASL
|
||||
TAY
|
||||
PLA
|
||||
BRA .False
|
||||
|
||||
|
||||
org $079E00
|
||||
WriteBWRAM:
|
||||
LDY #$6001 ;starting addr
|
||||
LDA #$1FFE ;bytes to write
|
||||
@@ -479,7 +516,6 @@ WriteBWRAM:
|
||||
.Return:
|
||||
RTL
|
||||
|
||||
org $079E80
|
||||
ConsumableSet:
|
||||
PHA
|
||||
PHX
|
||||
@@ -507,7 +543,6 @@ ConsumableSet:
|
||||
ASL
|
||||
TAX
|
||||
LDA $07D020, X ; current stage
|
||||
DEC
|
||||
ASL #6
|
||||
TAX
|
||||
PLA
|
||||
@@ -519,8 +554,16 @@ ConsumableSet:
|
||||
BRA .LoopHead ; return to loop head
|
||||
.ApplyCheck:
|
||||
LDA $A000, X ; consumables index
|
||||
PHA
|
||||
ORA #$0001
|
||||
STA $A000, X
|
||||
PLA
|
||||
AND #$00FF
|
||||
BNE .Return
|
||||
TXA
|
||||
ORA #$1000
|
||||
JSL ApplyLocalCheck
|
||||
.Return:
|
||||
PLY
|
||||
PLX
|
||||
PLA
|
||||
@@ -528,7 +571,6 @@ ConsumableSet:
|
||||
AND #$00FF
|
||||
RTL
|
||||
|
||||
org $079F00
|
||||
NormalGoalSet:
|
||||
PHX
|
||||
LDA $07D012
|
||||
@@ -549,7 +591,6 @@ NormalGoalSet:
|
||||
STA $5AC1 ; cutscene
|
||||
RTL
|
||||
|
||||
org $079F80
|
||||
FinalIcebergFix:
|
||||
PHX
|
||||
PHY
|
||||
@@ -572,7 +613,7 @@ FinalIcebergFix:
|
||||
ASL
|
||||
TAX
|
||||
LDA $07D020, X
|
||||
CMP #$001E
|
||||
CMP #$001D
|
||||
BEQ .ReturnTrue
|
||||
CLC
|
||||
BRA .Return
|
||||
@@ -583,7 +624,6 @@ FinalIcebergFix:
|
||||
PLX
|
||||
RTL
|
||||
|
||||
org $07A000
|
||||
StrictBosses:
|
||||
PHX
|
||||
LDA $901E ; Do we have strict bosses enabled?
|
||||
@@ -610,7 +650,6 @@ StrictBosses:
|
||||
LDA $53CD
|
||||
RTL
|
||||
|
||||
org $07A030
|
||||
NintenHalken:
|
||||
LDX #$0005
|
||||
.Halken:
|
||||
@@ -628,7 +667,6 @@ NintenHalken:
|
||||
LDA #$0001
|
||||
RTL
|
||||
|
||||
org $07A080
|
||||
StageCompleteSet:
|
||||
PHX
|
||||
LDA $5AC1 ; completed stage cutscene
|
||||
@@ -656,9 +694,17 @@ StageCompleteSet:
|
||||
ASL
|
||||
TAX
|
||||
LDA $9020, X ; load the stage we completed
|
||||
DEC
|
||||
ASL
|
||||
TAX
|
||||
PHX
|
||||
LDA $8200, X
|
||||
AND #$00FF
|
||||
BNE .ApplyClear
|
||||
TXA
|
||||
LSR
|
||||
JSL ApplyLocalCheck
|
||||
.ApplyClear:
|
||||
PLX
|
||||
LDA #$0001
|
||||
ORA $8200, X
|
||||
STA $8200, X
|
||||
@@ -668,7 +714,6 @@ StageCompleteSet:
|
||||
CMP $53CB
|
||||
RTL
|
||||
|
||||
org $07A100
|
||||
OpenWorldBossUnlock:
|
||||
PHX
|
||||
PHY
|
||||
@@ -699,7 +744,6 @@ OpenWorldBossUnlock:
|
||||
.LoopStage:
|
||||
PLX
|
||||
LDY $9020, X ; get stage id
|
||||
DEY
|
||||
INX
|
||||
INX
|
||||
PHA
|
||||
@@ -732,7 +776,6 @@ OpenWorldBossUnlock:
|
||||
PLX
|
||||
RTL
|
||||
|
||||
org $07A180
|
||||
GooeySpawn:
|
||||
PHY
|
||||
PHX
|
||||
@@ -768,7 +811,6 @@ GooeySpawn:
|
||||
PLY
|
||||
RTL
|
||||
|
||||
org $07A200
|
||||
SpeedTrap:
|
||||
PHX
|
||||
LDX $8082 ; do we have slowness
|
||||
@@ -780,7 +822,6 @@ SpeedTrap:
|
||||
EOR #$FFFF
|
||||
RTL
|
||||
|
||||
org $07A280
|
||||
HeartStarVisual:
|
||||
CPX #$0000
|
||||
BEQ .SkipInx
|
||||
@@ -844,7 +885,6 @@ HeartStarVisual:
|
||||
.Return:
|
||||
RTL
|
||||
|
||||
org $07A300
|
||||
LoadFont:
|
||||
JSL $00D29F ; play sfx
|
||||
PHX
|
||||
@@ -915,7 +955,6 @@ LoadFont:
|
||||
PLX
|
||||
RTL
|
||||
|
||||
org $07A380
|
||||
HeartStarVisual2:
|
||||
LDA #$2C80
|
||||
STA $0000, Y
|
||||
@@ -1029,14 +1068,12 @@ HeartStarVisual2:
|
||||
STA $0000, Y
|
||||
RTL
|
||||
|
||||
org $07A480
|
||||
HeartStarSelectFix:
|
||||
PHX
|
||||
TXA
|
||||
ASL
|
||||
TAX
|
||||
LDA $9020, X
|
||||
DEC
|
||||
TAX
|
||||
.LoopHead:
|
||||
CMP #$0006
|
||||
@@ -1051,15 +1088,31 @@ HeartStarSelectFix:
|
||||
AND #$00FF
|
||||
RTL
|
||||
|
||||
org $07A500
|
||||
HeartStarCutsceneFix:
|
||||
TAX
|
||||
LDA $53D3
|
||||
DEC
|
||||
STA $5AC3
|
||||
LDA $53A7, X
|
||||
AND #$00FF
|
||||
BNE .Return
|
||||
PHX
|
||||
TXA
|
||||
.Loop:
|
||||
CMP #$0007
|
||||
BCC .Continue
|
||||
SEC
|
||||
SBC #$0007
|
||||
DEX
|
||||
BRA .Loop
|
||||
.Continue:
|
||||
TXA
|
||||
ORA #$0100
|
||||
JSL ApplyLocalCheck
|
||||
PLX
|
||||
.Return
|
||||
RTL
|
||||
|
||||
org $07A510
|
||||
GiftGiving:
|
||||
CMP #$0008
|
||||
.This:
|
||||
@@ -1075,7 +1128,6 @@ GiftGiving:
|
||||
PLX
|
||||
JML $CABC18
|
||||
|
||||
org $07A550
|
||||
PauseMenu:
|
||||
JSL $00D29F
|
||||
PHX
|
||||
@@ -1136,7 +1188,6 @@ PauseMenu:
|
||||
PLX
|
||||
RTL
|
||||
|
||||
org $07A600
|
||||
StarsSet:
|
||||
PHA
|
||||
PHX
|
||||
@@ -1166,7 +1217,6 @@ StarsSet:
|
||||
ASL
|
||||
TAX
|
||||
LDA $07D020, X
|
||||
DEC
|
||||
ASL
|
||||
ASL
|
||||
ASL
|
||||
@@ -1183,8 +1233,15 @@ StarsSet:
|
||||
BRA .2LoopHead
|
||||
.2LoopEnd:
|
||||
LDA $B000, X
|
||||
PHA
|
||||
ORA #$0001
|
||||
STA $B000, X
|
||||
PLA
|
||||
AND #$00FF
|
||||
BNE .Return
|
||||
TXA
|
||||
ORA #$2000
|
||||
JSL ApplyLocalCheck
|
||||
.Return:
|
||||
PLY
|
||||
PLX
|
||||
@@ -1199,6 +1256,48 @@ StarsSet:
|
||||
STA $39D7
|
||||
BRA .Return
|
||||
|
||||
ApplyLocalCheck:
|
||||
; args: A-address of check following $08B000
|
||||
TAX
|
||||
LDA $09B000, X
|
||||
AND #$00FF
|
||||
TAY
|
||||
LDX #$0000
|
||||
.Loop:
|
||||
LDA $C000, X
|
||||
BEQ .Apply
|
||||
INX
|
||||
INX
|
||||
CPX #$0010
|
||||
BCC .Loop
|
||||
BRA .Return ; this is dangerous, could lose a check here
|
||||
.Apply:
|
||||
TYA
|
||||
STA $C000, X
|
||||
.Return:
|
||||
RTL
|
||||
|
||||
HookBossPurify:
|
||||
ORA $B0
|
||||
STA $53D5
|
||||
LDA $B0
|
||||
LDX #$0000
|
||||
LSR
|
||||
.Loop:
|
||||
BIT #$0001
|
||||
BNE .Apply
|
||||
LSR
|
||||
LSR
|
||||
INX
|
||||
CPX #$0005
|
||||
BCS .Return
|
||||
BRA .Loop
|
||||
.Apply:
|
||||
TXA
|
||||
ORA #$0200
|
||||
JSL ApplyLocalCheck
|
||||
.Return:
|
||||
RTL
|
||||
|
||||
org $07C000
|
||||
db "KDL3_BASEPATCH_ARCHI"
|
||||
@@ -1234,4 +1333,7 @@ org $07E040
|
||||
db $3A, $01
|
||||
db $3B, $05
|
||||
db $3C, $05
|
||||
db $3D, $05
|
||||
db $3D, $05
|
||||
|
||||
org $07F000
|
||||
incbin "APPauseIcons.dat"
|
||||
@@ -6,6 +6,8 @@ from test.bases import WorldTestBase
|
||||
from test.general import gen_steps
|
||||
from worlds import AutoWorld
|
||||
from worlds.AutoWorld import call_all
|
||||
# mypy: ignore-errors
|
||||
# This is a copy of core code, and I'm not smart enough to solve the errors in here
|
||||
|
||||
|
||||
class KDL3TestBase(WorldTestBase):
|
||||
|
||||
@@ -5,12 +5,12 @@ class TestFastGoal(KDL3TestBase):
|
||||
options = {
|
||||
"open_world": False,
|
||||
"goal_speed": "fast",
|
||||
"total_heart_stars": 30,
|
||||
"max_heart_stars": 30,
|
||||
"heart_stars_required": 50,
|
||||
"filler_percentage": 0,
|
||||
}
|
||||
|
||||
def test_goal(self):
|
||||
def test_goal(self) -> None:
|
||||
self.assertBeatable(False)
|
||||
heart_stars = self.get_items_by_name("Heart Star")
|
||||
self.collect(heart_stars[0:14])
|
||||
@@ -30,12 +30,12 @@ class TestNormalGoal(KDL3TestBase):
|
||||
options = {
|
||||
"open_world": False,
|
||||
"goal_speed": "normal",
|
||||
"total_heart_stars": 30,
|
||||
"max_heart_stars": 30,
|
||||
"heart_stars_required": 50,
|
||||
"filler_percentage": 0,
|
||||
}
|
||||
|
||||
def test_goal(self):
|
||||
def test_goal(self) -> None:
|
||||
self.assertBeatable(False)
|
||||
heart_stars = self.get_items_by_name("Heart Star")
|
||||
self.collect(heart_stars[0:14])
|
||||
@@ -51,14 +51,14 @@ class TestNormalGoal(KDL3TestBase):
|
||||
self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed))
|
||||
self.assertBeatable(True)
|
||||
|
||||
def test_kine(self):
|
||||
def test_kine(self) -> None:
|
||||
self.collect_by_name(["Cutter", "Burning", "Heart Star"])
|
||||
self.assertBeatable(False)
|
||||
|
||||
def test_cutter(self):
|
||||
def test_cutter(self) -> None:
|
||||
self.collect_by_name(["Kine", "Burning", "Heart Star"])
|
||||
self.assertBeatable(False)
|
||||
|
||||
def test_burning(self):
|
||||
def test_burning(self) -> None:
|
||||
self.collect_by_name(["Cutter", "Kine", "Heart Star"])
|
||||
self.assertBeatable(False)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from . import KDL3TestBase
|
||||
from ..names import location_name
|
||||
from Options import PlandoConnection
|
||||
from ..Names import LocationName
|
||||
import typing
|
||||
|
||||
|
||||
@@ -12,31 +12,31 @@ class TestLocations(KDL3TestBase):
|
||||
# these ensure we can always reach all stages physically
|
||||
}
|
||||
|
||||
def test_simple_heart_stars(self):
|
||||
self.run_location_test(LocationName.grass_land_muchi, ["ChuChu"])
|
||||
self.run_location_test(LocationName.grass_land_chao, ["Stone"])
|
||||
self.run_location_test(LocationName.grass_land_mine, ["Kine"])
|
||||
self.run_location_test(LocationName.ripple_field_kamuribana, ["Pitch", "Clean"])
|
||||
self.run_location_test(LocationName.ripple_field_bakasa, ["Kine", "Parasol"])
|
||||
self.run_location_test(LocationName.ripple_field_toad, ["Needle"])
|
||||
self.run_location_test(LocationName.ripple_field_mama_pitch, ["Pitch", "Kine", "Burning", "Stone"])
|
||||
self.run_location_test(LocationName.sand_canyon_auntie, ["Clean"])
|
||||
self.run_location_test(LocationName.sand_canyon_nyupun, ["ChuChu", "Cutter"])
|
||||
self.run_location_test(LocationName.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Spark", "Ice"])
|
||||
self.run_location_test(LocationName.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Clean", "Ice"]),
|
||||
self.run_location_test(LocationName.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Spark", "Needle"]),
|
||||
self.run_location_test(LocationName.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Clean", "Needle"]),
|
||||
self.run_location_test(LocationName.cloudy_park_hibanamodoki, ["Coo", "Clean"])
|
||||
self.run_location_test(LocationName.cloudy_park_piyokeko, ["Needle"])
|
||||
self.run_location_test(LocationName.cloudy_park_mikarin, ["Coo"])
|
||||
self.run_location_test(LocationName.cloudy_park_pick, ["Rick"])
|
||||
self.run_location_test(LocationName.iceberg_kogoesou, ["Burning"])
|
||||
self.run_location_test(LocationName.iceberg_samus, ["Ice"])
|
||||
self.run_location_test(LocationName.iceberg_name, ["Burning", "Coo", "ChuChu"])
|
||||
self.run_location_test(LocationName.iceberg_angel, ["Cutter", "Burning", "Spark", "Parasol", "Needle", "Clean",
|
||||
def test_simple_heart_stars(self) -> None:
|
||||
self.run_location_test(location_name.grass_land_muchi, ["ChuChu"])
|
||||
self.run_location_test(location_name.grass_land_chao, ["Stone"])
|
||||
self.run_location_test(location_name.grass_land_mine, ["Kine"])
|
||||
self.run_location_test(location_name.ripple_field_kamuribana, ["Pitch", "Clean"])
|
||||
self.run_location_test(location_name.ripple_field_bakasa, ["Kine", "Parasol"])
|
||||
self.run_location_test(location_name.ripple_field_toad, ["Needle"])
|
||||
self.run_location_test(location_name.ripple_field_mama_pitch, ["Pitch", "Kine", "Burning", "Stone"])
|
||||
self.run_location_test(location_name.sand_canyon_auntie, ["Clean"])
|
||||
self.run_location_test(location_name.sand_canyon_nyupun, ["ChuChu", "Cutter"])
|
||||
self.run_location_test(location_name.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Spark", "Ice"])
|
||||
self.run_location_test(location_name.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Clean", "Ice"])
|
||||
self.run_location_test(location_name.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Spark", "Needle"])
|
||||
self.run_location_test(location_name.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Clean", "Needle"])
|
||||
self.run_location_test(location_name.cloudy_park_hibanamodoki, ["Coo", "Clean"])
|
||||
self.run_location_test(location_name.cloudy_park_piyokeko, ["Needle"])
|
||||
self.run_location_test(location_name.cloudy_park_mikarin, ["Coo"])
|
||||
self.run_location_test(location_name.cloudy_park_pick, ["Rick"])
|
||||
self.run_location_test(location_name.iceberg_kogoesou, ["Burning"])
|
||||
self.run_location_test(location_name.iceberg_samus, ["Ice"])
|
||||
self.run_location_test(location_name.iceberg_name, ["Burning", "Coo", "ChuChu"])
|
||||
self.run_location_test(location_name.iceberg_angel, ["Cutter", "Burning", "Spark", "Parasol", "Needle", "Clean",
|
||||
"Stone", "Ice"])
|
||||
|
||||
def run_location_test(self, location: str, itempool: typing.List[str]):
|
||||
def run_location_test(self, location: str, itempool: typing.List[str]) -> None:
|
||||
items = itempool.copy()
|
||||
while len(itempool) > 0:
|
||||
self.assertFalse(self.can_reach_location(location), str(self.multiworld.seed))
|
||||
@@ -57,7 +57,7 @@ class TestShiro(KDL3TestBase):
|
||||
"plando_options": "connections"
|
||||
}
|
||||
|
||||
def test_shiro(self):
|
||||
def test_shiro(self) -> None:
|
||||
self.assertFalse(self.can_reach_location("Iceberg 5 - Shiro"), str(self.multiworld.seed))
|
||||
self.collect_by_name("Nago")
|
||||
self.assertFalse(self.can_reach_location("Iceberg 5 - Shiro"), str(self.multiworld.seed))
|
||||
|
||||
@@ -1,47 +1,61 @@
|
||||
from typing import List, Tuple
|
||||
from typing import List, Tuple, Optional
|
||||
from . import KDL3TestBase
|
||||
from ..Room import KDL3Room
|
||||
from ..room import KDL3Room
|
||||
from ..names import animal_friend_spawns
|
||||
|
||||
|
||||
class TestCopyAbilityShuffle(KDL3TestBase):
|
||||
options = {
|
||||
"open_world": False,
|
||||
"goal_speed": "normal",
|
||||
"total_heart_stars": 30,
|
||||
"max_heart_stars": 30,
|
||||
"heart_stars_required": 50,
|
||||
"filler_percentage": 0,
|
||||
"copy_ability_randomization": "enabled",
|
||||
}
|
||||
|
||||
def test_goal(self):
|
||||
self.assertBeatable(False)
|
||||
heart_stars = self.get_items_by_name("Heart Star")
|
||||
self.collect(heart_stars[0:14])
|
||||
self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed))
|
||||
self.assertBeatable(False)
|
||||
self.collect(heart_stars[14:15])
|
||||
self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed))
|
||||
self.assertBeatable(False)
|
||||
self.collect_by_name(["Burning", "Cutter", "Kine"])
|
||||
self.assertBeatable(True)
|
||||
self.remove([self.get_item_by_name("Love-Love Rod")])
|
||||
self.collect(heart_stars)
|
||||
self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed))
|
||||
self.assertBeatable(True)
|
||||
def test_goal(self) -> None:
|
||||
try:
|
||||
self.assertBeatable(False)
|
||||
heart_stars = self.get_items_by_name("Heart Star")
|
||||
self.collect(heart_stars[0:14])
|
||||
self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed))
|
||||
self.assertBeatable(False)
|
||||
self.collect(heart_stars[14:15])
|
||||
self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed))
|
||||
self.assertBeatable(False)
|
||||
self.collect_by_name(["Burning", "Cutter", "Kine"])
|
||||
self.assertBeatable(True)
|
||||
self.remove([self.get_item_by_name("Love-Love Rod")])
|
||||
self.collect(heart_stars)
|
||||
self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed))
|
||||
self.assertBeatable(True)
|
||||
except AssertionError as ex:
|
||||
# if assert beatable fails, this will catch and print the seed
|
||||
raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex
|
||||
|
||||
def test_kine(self):
|
||||
self.collect_by_name(["Cutter", "Burning", "Heart Star"])
|
||||
self.assertBeatable(False)
|
||||
def test_kine(self) -> None:
|
||||
try:
|
||||
self.collect_by_name(["Cutter", "Burning", "Heart Star"])
|
||||
self.assertBeatable(False)
|
||||
except AssertionError as ex:
|
||||
raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex
|
||||
|
||||
def test_cutter(self):
|
||||
self.collect_by_name(["Kine", "Burning", "Heart Star"])
|
||||
self.assertBeatable(False)
|
||||
def test_cutter(self) -> None:
|
||||
try:
|
||||
self.collect_by_name(["Kine", "Burning", "Heart Star"])
|
||||
self.assertBeatable(False)
|
||||
except AssertionError as ex:
|
||||
raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex
|
||||
|
||||
def test_burning(self):
|
||||
self.collect_by_name(["Cutter", "Kine", "Heart Star"])
|
||||
self.assertBeatable(False)
|
||||
def test_burning(self) -> None:
|
||||
try:
|
||||
self.collect_by_name(["Cutter", "Kine", "Heart Star"])
|
||||
self.assertBeatable(False)
|
||||
except AssertionError as ex:
|
||||
raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex
|
||||
|
||||
def test_cutter_and_burning_reachable(self):
|
||||
def test_cutter_and_burning_reachable(self) -> None:
|
||||
rooms = self.multiworld.worlds[1].rooms
|
||||
copy_abilities = self.multiworld.worlds[1].copy_abilities
|
||||
sand_canyon_5 = self.multiworld.get_region("Sand Canyon 5 - 9", 1)
|
||||
@@ -63,7 +77,7 @@ class TestCopyAbilityShuffle(KDL3TestBase):
|
||||
else:
|
||||
self.fail("Could not reach Burning Ability before Iceberg 4!")
|
||||
|
||||
def test_valid_abilities_for_ROB(self):
|
||||
def test_valid_abilities_for_ROB(self) -> None:
|
||||
# there exists a subset of 4-7 abilities that will allow us access to ROB heart star on default settings
|
||||
self.collect_by_name(["Heart Star", "Kine", "Coo"]) # we will guaranteed need Coo, Kine, and Heart Stars to reach
|
||||
# first we need to identify our bukiset requirements
|
||||
@@ -74,13 +88,13 @@ class TestCopyAbilityShuffle(KDL3TestBase):
|
||||
({"Stone Ability", "Burning Ability"}, {'Bukiset (Stone)', 'Bukiset (Burning)'}),
|
||||
]
|
||||
copy_abilities = self.multiworld.worlds[1].copy_abilities
|
||||
required_abilities: List[Tuple[str]] = []
|
||||
required_abilities: List[List[str]] = []
|
||||
for abilities, bukisets in groups:
|
||||
potential_abilities: List[str] = list()
|
||||
for bukiset in bukisets:
|
||||
if copy_abilities[bukiset] in abilities:
|
||||
potential_abilities.append(copy_abilities[bukiset])
|
||||
required_abilities.append(tuple(potential_abilities))
|
||||
required_abilities.append(potential_abilities)
|
||||
collected_abilities = list()
|
||||
for group in required_abilities:
|
||||
self.assertFalse(len(group) == 0, str(self.multiworld.seed))
|
||||
@@ -103,91 +117,147 @@ class TestAnimalShuffle(KDL3TestBase):
|
||||
options = {
|
||||
"open_world": False,
|
||||
"goal_speed": "normal",
|
||||
"total_heart_stars": 30,
|
||||
"max_heart_stars": 30,
|
||||
"heart_stars_required": 50,
|
||||
"filler_percentage": 0,
|
||||
"animal_randomization": "full",
|
||||
}
|
||||
|
||||
def test_goal(self):
|
||||
self.assertBeatable(False)
|
||||
heart_stars = self.get_items_by_name("Heart Star")
|
||||
self.collect(heart_stars[0:14])
|
||||
self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed))
|
||||
self.assertBeatable(False)
|
||||
self.collect(heart_stars[14:15])
|
||||
self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed))
|
||||
self.assertBeatable(False)
|
||||
self.collect_by_name(["Burning", "Cutter", "Kine"])
|
||||
self.assertBeatable(True)
|
||||
self.remove([self.get_item_by_name("Love-Love Rod")])
|
||||
self.collect(heart_stars)
|
||||
self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed))
|
||||
self.assertBeatable(True)
|
||||
def test_goal(self) -> None:
|
||||
try:
|
||||
self.assertBeatable(False)
|
||||
heart_stars = self.get_items_by_name("Heart Star")
|
||||
self.collect(heart_stars[0:14])
|
||||
self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed))
|
||||
self.assertBeatable(False)
|
||||
self.collect(heart_stars[14:15])
|
||||
self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed))
|
||||
self.assertBeatable(False)
|
||||
self.collect_by_name(["Burning", "Cutter", "Kine"])
|
||||
self.assertBeatable(True)
|
||||
self.remove([self.get_item_by_name("Love-Love Rod")])
|
||||
self.collect(heart_stars)
|
||||
self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed))
|
||||
self.assertBeatable(True)
|
||||
except AssertionError as ex:
|
||||
# if assert beatable fails, this will catch and print the seed
|
||||
raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex
|
||||
|
||||
def test_kine(self):
|
||||
self.collect_by_name(["Cutter", "Burning", "Heart Star"])
|
||||
self.assertBeatable(False)
|
||||
def test_kine(self) -> None:
|
||||
try:
|
||||
self.collect_by_name(["Cutter", "Burning", "Heart Star"])
|
||||
self.assertBeatable(False)
|
||||
except AssertionError as ex:
|
||||
raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex
|
||||
|
||||
def test_cutter(self):
|
||||
self.collect_by_name(["Kine", "Burning", "Heart Star"])
|
||||
self.assertBeatable(False)
|
||||
def test_cutter(self) -> None:
|
||||
try:
|
||||
self.collect_by_name(["Kine", "Burning", "Heart Star"])
|
||||
self.assertBeatable(False)
|
||||
except AssertionError as ex:
|
||||
raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex
|
||||
|
||||
def test_burning(self):
|
||||
self.collect_by_name(["Cutter", "Kine", "Heart Star"])
|
||||
self.assertBeatable(False)
|
||||
def test_burning(self) -> None:
|
||||
try:
|
||||
self.collect_by_name(["Cutter", "Kine", "Heart Star"])
|
||||
self.assertBeatable(False)
|
||||
except AssertionError as ex:
|
||||
raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex
|
||||
|
||||
def test_locked_animals(self):
|
||||
self.assertTrue(self.multiworld.get_location("Ripple Field 5 - Animal 2", 1).item.name == "Pitch Spawn")
|
||||
self.assertTrue(self.multiworld.get_location("Iceberg 4 - Animal 1", 1).item.name == "ChuChu Spawn")
|
||||
self.assertTrue(self.multiworld.get_location("Sand Canyon 6 - Animal 1", 1).item.name in {"Kine Spawn", "Coo Spawn"})
|
||||
def test_locked_animals(self) -> None:
|
||||
ripple_field_5 = self.multiworld.get_location("Ripple Field 5 - Animal 2", 1)
|
||||
self.assertTrue(ripple_field_5.item is not None and ripple_field_5.item.name == "Pitch Spawn",
|
||||
f"Multiworld did not place Pitch, Seed: {self.multiworld.seed}")
|
||||
iceberg_4 = self.multiworld.get_location("Iceberg 4 - Animal 1", 1)
|
||||
self.assertTrue(iceberg_4.item is not None and iceberg_4.item.name == "ChuChu Spawn",
|
||||
f"Multiworld did not place ChuChu, Seed: {self.multiworld.seed}")
|
||||
sand_canyon_6 = self.multiworld.get_location("Sand Canyon 6 - Animal 1", 1)
|
||||
self.assertTrue(sand_canyon_6.item is not None and sand_canyon_6.item.name in
|
||||
{"Kine Spawn", "Coo Spawn"}, f"Multiworld did not place Coo/Kine, Seed: {self.multiworld.seed}")
|
||||
|
||||
def test_problematic(self) -> None:
|
||||
for spawns in animal_friend_spawns.problematic_sets:
|
||||
placed = [self.multiworld.get_location(spawn, 1).item for spawn in spawns]
|
||||
placed_names = set([item.name for item in placed])
|
||||
self.assertEqual(len(placed), len(placed_names),
|
||||
f"Duplicate animal placed in problematic locations:"
|
||||
f" {[spawn.location for spawn in placed]}, "
|
||||
f"Seed: {self.multiworld.seed}")
|
||||
|
||||
|
||||
class TestAllShuffle(KDL3TestBase):
|
||||
options = {
|
||||
"open_world": False,
|
||||
"goal_speed": "normal",
|
||||
"total_heart_stars": 30,
|
||||
"max_heart_stars": 30,
|
||||
"heart_stars_required": 50,
|
||||
"filler_percentage": 0,
|
||||
"animal_randomization": "full",
|
||||
"copy_ability_randomization": "enabled",
|
||||
}
|
||||
|
||||
def test_goal(self):
|
||||
self.assertBeatable(False)
|
||||
heart_stars = self.get_items_by_name("Heart Star")
|
||||
self.collect(heart_stars[0:14])
|
||||
self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed))
|
||||
self.assertBeatable(False)
|
||||
self.collect(heart_stars[14:15])
|
||||
self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed))
|
||||
self.assertBeatable(False)
|
||||
self.collect_by_name(["Burning", "Cutter", "Kine"])
|
||||
self.assertBeatable(True)
|
||||
self.remove([self.get_item_by_name("Love-Love Rod")])
|
||||
self.collect(heart_stars)
|
||||
self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed))
|
||||
self.assertBeatable(True)
|
||||
def test_goal(self) -> None:
|
||||
try:
|
||||
self.assertBeatable(False)
|
||||
heart_stars = self.get_items_by_name("Heart Star")
|
||||
self.collect(heart_stars[0:14])
|
||||
self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed))
|
||||
self.assertBeatable(False)
|
||||
self.collect(heart_stars[14:15])
|
||||
self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed))
|
||||
self.assertBeatable(False)
|
||||
self.collect_by_name(["Burning", "Cutter", "Kine"])
|
||||
self.assertBeatable(True)
|
||||
self.remove([self.get_item_by_name("Love-Love Rod")])
|
||||
self.collect(heart_stars)
|
||||
self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed))
|
||||
self.assertBeatable(True)
|
||||
except AssertionError as ex:
|
||||
# if assert beatable fails, this will catch and print the seed
|
||||
raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex
|
||||
|
||||
def test_kine(self):
|
||||
self.collect_by_name(["Cutter", "Burning", "Heart Star"])
|
||||
self.assertBeatable(False)
|
||||
def test_kine(self) -> None:
|
||||
try:
|
||||
self.collect_by_name(["Cutter", "Burning", "Heart Star"])
|
||||
self.assertBeatable(False)
|
||||
except AssertionError as ex:
|
||||
raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex
|
||||
|
||||
def test_cutter(self):
|
||||
self.collect_by_name(["Kine", "Burning", "Heart Star"])
|
||||
self.assertBeatable(False)
|
||||
def test_cutter(self) -> None:
|
||||
try:
|
||||
self.collect_by_name(["Kine", "Burning", "Heart Star"])
|
||||
self.assertBeatable(False)
|
||||
except AssertionError as ex:
|
||||
raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex
|
||||
|
||||
def test_burning(self):
|
||||
self.collect_by_name(["Cutter", "Kine", "Heart Star"])
|
||||
self.assertBeatable(False)
|
||||
def test_burning(self) -> None:
|
||||
try:
|
||||
self.collect_by_name(["Cutter", "Kine", "Heart Star"])
|
||||
self.assertBeatable(False)
|
||||
except AssertionError as ex:
|
||||
raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex
|
||||
|
||||
def test_locked_animals(self):
|
||||
self.assertTrue(self.multiworld.get_location("Ripple Field 5 - Animal 2", 1).item.name == "Pitch Spawn")
|
||||
self.assertTrue(self.multiworld.get_location("Iceberg 4 - Animal 1", 1).item.name == "ChuChu Spawn")
|
||||
self.assertTrue(self.multiworld.get_location("Sand Canyon 6 - Animal 1", 1).item.name in {"Kine Spawn", "Coo Spawn"})
|
||||
def test_locked_animals(self) -> None:
|
||||
ripple_field_5 = self.multiworld.get_location("Ripple Field 5 - Animal 2", 1)
|
||||
self.assertTrue(ripple_field_5.item is not None and ripple_field_5.item.name == "Pitch Spawn",
|
||||
f"Multiworld did not place Pitch, Seed: {self.multiworld.seed}")
|
||||
iceberg_4 = self.multiworld.get_location("Iceberg 4 - Animal 1", 1)
|
||||
self.assertTrue(iceberg_4.item is not None and iceberg_4.item.name == "ChuChu Spawn",
|
||||
f"Multiworld did not place ChuChu, Seed: {self.multiworld.seed}")
|
||||
sand_canyon_6 = self.multiworld.get_location("Sand Canyon 6 - Animal 1", 1)
|
||||
self.assertTrue(sand_canyon_6.item is not None and sand_canyon_6.item.name in
|
||||
{"Kine Spawn", "Coo Spawn"}, f"Multiworld did not place Coo/Kine, Seed: {self.multiworld.seed}")
|
||||
|
||||
def test_cutter_and_burning_reachable(self):
|
||||
def test_problematic(self) -> None:
|
||||
for spawns in animal_friend_spawns.problematic_sets:
|
||||
placed = [self.multiworld.get_location(spawn, 1).item for spawn in spawns]
|
||||
placed_names = set([item.name for item in placed])
|
||||
self.assertEqual(len(placed), len(placed_names),
|
||||
f"Duplicate animal placed in problematic locations:"
|
||||
f" {[spawn.location for spawn in placed]}, "
|
||||
f"Seed: {self.multiworld.seed}")
|
||||
|
||||
def test_cutter_and_burning_reachable(self) -> None:
|
||||
rooms = self.multiworld.worlds[1].rooms
|
||||
copy_abilities = self.multiworld.worlds[1].copy_abilities
|
||||
sand_canyon_5 = self.multiworld.get_region("Sand Canyon 5 - 9", 1)
|
||||
@@ -209,7 +279,7 @@ class TestAllShuffle(KDL3TestBase):
|
||||
else:
|
||||
self.fail("Could not reach Burning Ability before Iceberg 4!")
|
||||
|
||||
def test_valid_abilities_for_ROB(self):
|
||||
def test_valid_abilities_for_ROB(self) -> None:
|
||||
# there exists a subset of 4-7 abilities that will allow us access to ROB heart star on default settings
|
||||
self.collect_by_name(["Heart Star", "Kine", "Coo"]) # we will guaranteed need Coo, Kine, and Heart Stars to reach
|
||||
# first we need to identify our bukiset requirements
|
||||
@@ -220,13 +290,13 @@ class TestAllShuffle(KDL3TestBase):
|
||||
({"Stone Ability", "Burning Ability"}, {'Bukiset (Stone)', 'Bukiset (Burning)'}),
|
||||
]
|
||||
copy_abilities = self.multiworld.worlds[1].copy_abilities
|
||||
required_abilities: List[Tuple[str]] = []
|
||||
required_abilities: List[List[str]] = []
|
||||
for abilities, bukisets in groups:
|
||||
potential_abilities: List[str] = list()
|
||||
for bukiset in bukisets:
|
||||
if copy_abilities[bukiset] in abilities:
|
||||
potential_abilities.append(copy_abilities[bukiset])
|
||||
required_abilities.append(tuple(potential_abilities))
|
||||
required_abilities.append(potential_abilities)
|
||||
collected_abilities = list()
|
||||
for group in required_abilities:
|
||||
self.assertFalse(len(group) == 0, str(self.multiworld.seed))
|
||||
@@ -242,4 +312,4 @@ class TestAllShuffle(KDL3TestBase):
|
||||
self.collect_by_name(["Cutter"])
|
||||
|
||||
self.assertTrue(self.can_reach_location("Sand Canyon 6 - Professor Hector & R.O.B"),
|
||||
''.join(str(self.multiworld.seed)).join(collected_abilities))
|
||||
f"Seed: {self.multiworld.seed}, Collected: {collected_abilities}")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user