Compare commits

..

1 Commits

Author SHA1 Message Date
NewSoupVi
8cfdf4b340 Core: Remove broken unused code from Options.py
"Unused" is a baseless assertion, but this code path has been crashing on the first statement for 6 months and noone's complained
2024-08-12 16:55:03 +02:00
251 changed files with 15759 additions and 71859 deletions

1
.gitattributes vendored
View File

@@ -1 +0,0 @@
worlds/blasphemous/region_data.py linguist-generated=true

View File

@@ -37,13 +37,12 @@ jobs:
- {version: '3.9'} - {version: '3.9'}
- {version: '3.10'} - {version: '3.10'}
- {version: '3.11'} - {version: '3.11'}
- {version: '3.12'}
include: include:
- python: {version: '3.8'} # win7 compat - python: {version: '3.8'} # win7 compat
os: windows-latest os: windows-latest
- python: {version: '3.12'} # current - python: {version: '3.11'} # current
os: windows-latest os: windows-latest
- python: {version: '3.12'} # current - python: {version: '3.11'} # current
os: macos-latest os: macos-latest
steps: steps:
@@ -71,7 +70,7 @@ jobs:
os: os:
- ubuntu-latest - ubuntu-latest
python: python:
- {version: '3.12'} # current - {version: '3.11'} # current
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View File

@@ -11,10 +11,8 @@ from argparse import Namespace
from collections import Counter, deque from collections import Counter, deque
from collections.abc import Collection, MutableSequence from collections.abc import Collection, MutableSequence
from enum import IntEnum, IntFlag from enum import IntEnum, IntFlag
from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple, from typing import Any, Callable, Dict, Iterable, Iterator, List, Mapping, NamedTuple, Optional, Set, Tuple, \
Optional, Protocol, Set, Tuple, Union, Type) TypedDict, Union, Type, ClassVar
from typing_extensions import NotRequired, TypedDict
import NetUtils import NetUtils
import Options import Options
@@ -24,16 +22,16 @@ if typing.TYPE_CHECKING:
from worlds import AutoWorld from worlds import AutoWorld
class Group(TypedDict): class Group(TypedDict, total=False):
name: str name: str
game: str game: str
world: "AutoWorld.World" world: "AutoWorld.World"
players: AbstractSet[int] players: Set[int]
item_pool: NotRequired[Set[str]] item_pool: Set[str]
replacement_items: NotRequired[Dict[int, Optional[str]]] replacement_items: Dict[int, Optional[str]]
local_items: NotRequired[Set[str]] local_items: Set[str]
non_local_items: NotRequired[Set[str]] non_local_items: Set[str]
link_replacement: NotRequired[bool] link_replacement: bool
class ThreadBarrierProxy: class ThreadBarrierProxy:
@@ -50,11 +48,6 @@ class ThreadBarrierProxy:
"Please use multiworld.per_slot_randoms[player] or randomize ahead of output.") "Please use multiworld.per_slot_randoms[player] or randomize ahead of output.")
class HasNameAndPlayer(Protocol):
name: str
player: int
class MultiWorld(): class MultiWorld():
debug_types = False debug_types = False
player_name: Dict[int, str] player_name: Dict[int, str]
@@ -163,7 +156,7 @@ class MultiWorld():
self.start_inventory_from_pool: Dict[int, Options.StartInventoryPool] = {} self.start_inventory_from_pool: Dict[int, Options.StartInventoryPool] = {}
for player in range(1, players + 1): for player in range(1, players + 1):
def set_player_attr(attr: str, val) -> None: def set_player_attr(attr, val):
self.__dict__.setdefault(attr, {})[player] = val self.__dict__.setdefault(attr, {})[player] = val
set_player_attr('plando_items', []) set_player_attr('plando_items', [])
set_player_attr('plando_texts', {}) set_player_attr('plando_texts', {})
@@ -172,13 +165,13 @@ class MultiWorld():
set_player_attr('completion_condition', lambda state: True) set_player_attr('completion_condition', lambda state: True)
self.worlds = {} self.worlds = {}
self.per_slot_randoms = Utils.DeprecateDict("Using per_slot_randoms is now deprecated. Please use the " 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 self.plando_options = PlandoOptions.none
def get_all_ids(self) -> Tuple[int, ...]: def get_all_ids(self) -> Tuple[int, ...]:
return self.player_ids + tuple(self.groups) return self.player_ids + tuple(self.groups)
def add_group(self, name: str, game: str, players: AbstractSet[int] = frozenset()) -> Tuple[int, Group]: def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tuple[int, Group]:
"""Create a group with name and return the assigned player ID and 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.""" If a group of this name already exists, the set of players is extended instead of creating a new one."""
from worlds import AutoWorld from worlds import AutoWorld
@@ -202,7 +195,7 @@ class MultiWorld():
return new_id, new_group return new_id, new_group
def get_player_groups(self, player: int) -> Set[int]: def get_player_groups(self, player) -> Set[int]:
return {group_id for group_id, group in self.groups.items() if player in group["players"]} 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): def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optional[str] = None):
@@ -265,7 +258,7 @@ class MultiWorld():
"link_replacement": replacement_prio.index(item_link["link_replacement"]), "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 current_item_name_groups = AutoWorld.AutoWorldRegister.world_types[item_link["game"]].item_name_groups
pool = set() pool = set()
local_items = set() local_items = set()
@@ -395,7 +388,7 @@ class MultiWorld():
return tuple(world for player, world in self.worlds.items() if return tuple(world for player, world in self.worlds.items() if
player not in self.groups and self.game[player] == game_name) player not in self.groups and self.game[player] == game_name)
def get_name_string_for_object(self, obj: HasNameAndPlayer) -> str: def get_name_string_for_object(self, obj) -> str:
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_name(obj.player)})' 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: def get_player_name(self, player: int) -> str:
@@ -437,7 +430,7 @@ class MultiWorld():
subworld = self.worlds[player] subworld = self.worlds[player]
for item in subworld.get_pre_fill_items(): for item in subworld.get_pre_fill_items():
subworld.collect(ret, item) subworld.collect(ret, item)
ret.sweep_for_advancements() ret.sweep_for_events()
if use_cache: if use_cache:
self._all_state = ret self._all_state = ret
@@ -446,7 +439,7 @@ class MultiWorld():
def get_items(self) -> List[Item]: def get_items(self) -> List[Item]:
return [loc.item for loc in self.get_filled_locations()] + self.itempool return [loc.item for loc in self.get_filled_locations()] + self.itempool
def find_item_locations(self, item: str, player: int, resolve_group_locations: bool = False) -> List[Location]: def find_item_locations(self, item, player: int, resolve_group_locations: bool = False) -> List[Location]:
if resolve_group_locations: if resolve_group_locations:
player_groups = self.get_player_groups(player) player_groups = self.get_player_groups(player)
return [location for location in self.get_locations() if return [location for location in self.get_locations() if
@@ -455,7 +448,7 @@ class MultiWorld():
return [location for location in self.get_locations() if return [location for location in self.get_locations() if
location.item and location.item.name == item and location.item.player == player] location.item and location.item.name == item and location.item.player == player]
def find_item(self, item: str, player: int) -> Location: def find_item(self, item, player: int) -> Location:
return next(location for location in self.get_locations() if return next(location for location in self.get_locations() if
location.item and location.item.name == item and location.item.player == player) location.item and location.item.name == item and location.item.player == player)
@@ -548,9 +541,9 @@ class MultiWorld():
return True return True
state = starting_state.copy() state = starting_state.copy()
else: else:
state = CollectionState(self) if self.has_beaten_game(self.state):
if self.has_beaten_game(state):
return True return True
state = CollectionState(self)
prog_locations = {location for location in self.get_locations() if location.item prog_locations = {location for location in self.get_locations() if location.item
and location.item.advancement and location not in state.locations_checked} and location.item.advancement and location not in state.locations_checked}
@@ -623,7 +616,8 @@ class MultiWorld():
def location_relevant(location: Location) -> bool: def location_relevant(location: Location) -> bool:
"""Determine if this location is relevant to sweep.""" """Determine if this location is relevant to sweep."""
return location.player in players["full"] or location.advancement return location.progress_type != LocationProgressType.EXCLUDED \
and (location.player in players["full"] or location.advancement)
def all_done() -> bool: def all_done() -> bool:
"""Check if all access rules are fulfilled""" """Check if all access rules are fulfilled"""
@@ -668,7 +662,7 @@ class CollectionState():
multiworld: MultiWorld multiworld: MultiWorld
reachable_regions: Dict[int, Set[Region]] reachable_regions: Dict[int, Set[Region]]
blocked_connections: Dict[int, Set[Entrance]] blocked_connections: Dict[int, Set[Entrance]]
advancements: Set[Location] events: Set[Location]
path: Dict[Union[Region, Entrance], PathValue] path: Dict[Union[Region, Entrance], PathValue]
locations_checked: Set[Location] locations_checked: Set[Location]
stale: Dict[int, bool] stale: Dict[int, bool]
@@ -680,7 +674,7 @@ class CollectionState():
self.multiworld = parent self.multiworld = parent
self.reachable_regions = {player: set() for player in parent.get_all_ids()} 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.blocked_connections = {player: set() for player in parent.get_all_ids()}
self.advancements = set() self.events = set()
self.path = {} self.path = {}
self.locations_checked = set() self.locations_checked = set()
self.stale = {player: True for player in parent.get_all_ids()} self.stale = {player: True for player in parent.get_all_ids()}
@@ -692,25 +686,17 @@ class CollectionState():
def update_reachable_regions(self, player: int): def update_reachable_regions(self, player: int):
self.stale[player] = False self.stale[player] = False
world: AutoWorld.World = self.multiworld.worlds[player]
reachable_regions = self.reachable_regions[player] reachable_regions = self.reachable_regions[player]
blocked_connections = self.blocked_connections[player]
queue = deque(self.blocked_connections[player]) queue = deque(self.blocked_connections[player])
start: Region = world.get_region(world.origin_region_name) start = self.multiworld.get_region("Menu", player)
# init on first call - this can't be done on construction since the regions don't exist yet # init on first call - this can't be done on construction since the regions don't exist yet
if start not in reachable_regions: if start not in reachable_regions:
reachable_regions.add(start) reachable_regions.add(start)
self.blocked_connections[player].update(start.exits) blocked_connections.update(start.exits)
queue.extend(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 # run BFS on all connections, and keep track of those blocked by missing items
while queue: while queue:
connection = queue.popleft() connection = queue.popleft()
@@ -730,29 +716,6 @@ class CollectionState():
if new_entrance in blocked_connections and new_entrance not in queue: if new_entrance in blocked_connections and new_entrance not in queue:
queue.append(new_entrance) 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: def copy(self) -> CollectionState:
ret = CollectionState(self.multiworld) ret = CollectionState(self.multiworld)
ret.prog_items = {player: counter.copy() for player, counter in self.prog_items.items()} ret.prog_items = {player: counter.copy() for player, counter in self.prog_items.items()}
@@ -760,7 +723,7 @@ class CollectionState():
self.reachable_regions.items()} self.reachable_regions.items()}
ret.blocked_connections = {player: entrance_set.copy() for player, entrance_set in ret.blocked_connections = {player: entrance_set.copy() for player, entrance_set in
self.blocked_connections.items()} self.blocked_connections.items()}
ret.advancements = self.advancements.copy() ret.events = self.events.copy()
ret.path = self.path.copy() ret.path = self.path.copy()
ret.locations_checked = self.locations_checked.copy() ret.locations_checked = self.locations_checked.copy()
for function in self.additional_copy_functions: for function in self.additional_copy_functions:
@@ -793,24 +756,19 @@ class CollectionState():
return self.multiworld.get_region(spot, player).can_reach(self) return self.multiworld.get_region(spot, player).can_reach(self)
def sweep_for_events(self, locations: Optional[Iterable[Location]] = None) -> None: 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: if locations is None:
locations = self.multiworld.get_filled_locations() locations = self.multiworld.get_filled_locations()
reachable_advancements = True reachable_events = True
# since the loop has a good chance to run more than once, only filter the advancements once # 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.advancements} locations = {location for location in locations if location.advancement and location not in self.events}
while reachable_advancements: while reachable_events:
reachable_advancements = {location for location in locations if location.can_reach(self)} reachable_events = {location for location in locations if location.can_reach(self)}
locations -= reachable_advancements locations -= reachable_events
for advancement in reachable_advancements: for event in reachable_events:
self.advancements.add(advancement) self.events.add(event)
assert isinstance(advancement.item, Item), "tried to collect Event with no Item" assert isinstance(event.item, Item), "tried to collect Event with no Item"
self.collect(advancement.item, True, advancement) self.collect(event.item, True, event)
# item name related # item name related
def has(self, item: str, player: int, count: int = 1) -> bool: def has(self, item: str, player: int, count: int = 1) -> bool:
@@ -844,7 +802,7 @@ class CollectionState():
if found >= count: if found >= count:
return True return True
return False return False
def has_from_list_unique(self, items: Iterable[str], player: int, count: int) -> bool: 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. """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.""" Ignores duplicates of the same item."""
@@ -859,7 +817,7 @@ class CollectionState():
def count_from_list(self, items: Iterable[str], player: int) -> int: def count_from_list(self, items: Iterable[str], player: int) -> int:
"""Returns the cumulative count of items from a list present in state.""" """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) 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: 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.""" """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) return sum(self.prog_items[player][item_name] > 0 for item_name in items)
@@ -905,16 +863,20 @@ class CollectionState():
) )
# Item related # Item related
def collect(self, item: Item, prevent_sweep: bool = False, location: Optional[Location] = None) -> bool: def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool:
if location: if location:
self.locations_checked.add(location) self.locations_checked.add(location)
changed = self.multiworld.worlds[item.player].collect(self, item) 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 self.stale[item.player] = True
if changed and not prevent_sweep: if changed and not event:
self.sweep_for_advancements() self.sweep_for_events()
return changed return changed
@@ -938,7 +900,7 @@ class Entrance:
addresses = None addresses = None
target = None target = None
def __init__(self, player: int, name: str = "", parent: Optional[Region] = None) -> None: def __init__(self, player: int, name: str = '', parent: Region = None):
self.name = name self.name = name
self.parent_region = parent self.parent_region = parent
self.player = player self.player = player
@@ -958,6 +920,9 @@ class Entrance:
region.entrances.append(self) region.entrances.append(self)
def __repr__(self): def __repr__(self):
return self.__str__()
def __str__(self):
multiworld = self.parent_region.multiworld if self.parent_region else None 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})' return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
@@ -1083,7 +1048,7 @@ class Region:
self.locations.append(location_type(self.player, location, address, self)) self.locations.append(location_type(self.player, location, address, self))
def connect(self, connecting_region: Region, name: Optional[str] = None, def connect(self, connecting_region: Region, name: Optional[str] = None,
rule: Optional[Callable[[CollectionState], bool]] = None) -> Entrance: rule: Optional[Callable[[CollectionState], bool]] = None) -> entrance_type:
""" """
Connects this Region to another Region, placing the provided rule on the connection. Connects this Region to another Region, placing the provided rule on the connection.
@@ -1123,6 +1088,9 @@ class Region:
rules[connecting_region] if rules and connecting_region in rules else None) rules[connecting_region] if rules and connecting_region in rules else None)
def __repr__(self): 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})' return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})'
@@ -1141,9 +1109,9 @@ class Location:
locked: bool = False locked: bool = False
show_in_spoiler: bool = True show_in_spoiler: bool = True
progress_type: LocationProgressType = LocationProgressType.DEFAULT progress_type: LocationProgressType = LocationProgressType.DEFAULT
always_allow: Callable[[CollectionState, Item], bool] = staticmethod(lambda state, item: False) always_allow = staticmethod(lambda state, item: False)
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True) access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
item_rule: Callable[[Item], bool] = staticmethod(lambda item: True) item_rule = staticmethod(lambda item: True)
item: Optional[Item] = None item: Optional[Item] = None
def __init__(self, player: int, name: str = '', address: Optional[int] = None, parent: Optional[Region] = None): def __init__(self, player: int, name: str = '', address: Optional[int] = None, parent: Optional[Region] = None):
@@ -1152,15 +1120,11 @@ class Location:
self.address = address self.address = address
self.parent_region = parent self.parent_region = parent
def can_fill(self, state: CollectionState, item: Item, check_access: bool = True) -> bool: def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
return (( return ((self.always_allow(state, item) and item.name not in state.multiworld.worlds[item.player].options.non_local_items)
self.always_allow(state, item) or ((self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful))
and item.name not in state.multiworld.worlds[item.player].options.non_local_items and self.item_rule(item)
) or ( and (not check_access or self.can_reach(state))))
(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: def can_reach(self, state: CollectionState) -> bool:
# Region.can_reach is just a cache lookup, so placing it first for faster abort on average # Region.can_reach is just a cache lookup, so placing it first for faster abort on average
@@ -1175,6 +1139,9 @@ class Location:
self.locked = True self.locked = True
def __repr__(self): 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 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})' return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
@@ -1196,7 +1163,7 @@ class Location:
@property @property
def native_item(self) -> bool: def native_item(self) -> bool:
"""Returns True if the item in this location matches game.""" """Returns True if the item in this location matches game."""
return self.item is not None and self.item.game == self.game return self.item and self.item.game == self.game
@property @property
def hint_text(self) -> str: def hint_text(self) -> str:
@@ -1204,26 +1171,13 @@ class Location:
class ItemClassification(IntFlag): class ItemClassification(IntFlag):
filler = 0b0000 filler = 0b0000 # aka trash, as in filler items like ammo, currency etc,
""" 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
progression = 0b0001 trap = 0b0100 # detrimental or entirely useless (nothing) item
""" Item that is logically relevant. skip_balancing = 0b1000 # should technically never occur on its own
Protects this item from being placed on excluded or unreachable locations. """ # Item that is logically relevant, but progression balancing should not touch.
# Typically currency or other counted items.
useful = 0b0010
""" Item that is especially useful.
Protects this item from being placed on excluded or unreachable locations.
When combined with another flag like "progression", it means "an especially useful progression item". """
trap = 0b0100
""" Item that is detrimental in some way. """
skip_balancing = 0b1000
""" should technically never occur on its own
Item that is logically relevant, but progression balancing should not touch.
Typically currency or other counted items. """
progression_skip_balancing = 0b1001 # only progression gets balanced progression_skip_balancing = 0b1001 # only progression gets balanced
def as_flag(self) -> int: def as_flag(self) -> int:
@@ -1292,6 +1246,9 @@ class Item:
return hash((self.name, self.player)) return hash((self.name, self.player))
def __repr__(self) -> str: 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: 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 self.location.parent_region.multiworld.get_name_string_for_object(self)
return f"{self.name} (Player {self.player})" return f"{self.name} (Player {self.player})"
@@ -1369,9 +1326,9 @@ class Spoiler:
# in the second phase, we cull each sphere such that the game is still beatable, # 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 # reducing each range of influence to the bare minimum required inside it
restore_later: Dict[Location, Item] = {} restore_later = {}
for num, sphere in reversed(tuple(enumerate(collection_spheres))): for num, sphere in reversed(tuple(enumerate(collection_spheres))):
to_delete: Set[Location] = set() to_delete = set()
for location in sphere: for location in sphere:
# we remove the item at location and check if game is still beatable # 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, logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name,
@@ -1389,7 +1346,7 @@ class Spoiler:
sphere -= to_delete sphere -= to_delete
# second phase, sphere 0 # second phase, sphere 0
removed_precollected: List[Item] = [] removed_precollected = []
for item in (i for i in chain.from_iterable(multiworld.precollected_items.values()) if i.advancement): 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) logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
multiworld.precollected_items[item.player].remove(item) multiworld.precollected_items[item.player].remove(item)
@@ -1470,7 +1427,7 @@ class Spoiler:
# Maybe move the big bomb over to the Event system instead? # Maybe move the big bomb over to the Event system instead?
if any(exit_path == 'Pyramid Fairy' for path in self.paths.values() if any(exit_path == 'Pyramid Fairy' for path in self.paths.values()
for (_, exit_path) in path): for (_, exit_path) in path):
if multiworld.worlds[player].options.mode != 'inverted': if multiworld.mode[player] != 'inverted':
self.paths[str(multiworld.get_region('Big Bomb Shop', player))] = \ self.paths[str(multiworld.get_region('Big Bomb Shop', player))] = \
get_path(state, multiworld.get_region('Big Bomb Shop', player)) get_path(state, multiworld.get_region('Big Bomb Shop', player))
else: else:
@@ -1542,9 +1499,9 @@ class Spoiler:
if self.paths: if self.paths:
outfile.write('\n\nPaths:\n\n') outfile.write('\n\nPaths:\n\n')
path_listings: List[str] = [] path_listings = []
for location, path in sorted(self.paths.items()): for location, path in sorted(self.paths.items()):
path_lines: List[str] = [] path_lines = []
for region, exit in path: for region, exit in path:
if exit is not None: if exit is not None:
path_lines.append("{} -> {}".format(region, exit)) path_lines.append("{} -> {}".format(region, exit))

View File

@@ -252,7 +252,7 @@ class CommonContext:
starting_reconnect_delay: int = 5 starting_reconnect_delay: int = 5
current_reconnect_delay: int = starting_reconnect_delay current_reconnect_delay: int = starting_reconnect_delay
command_processor: typing.Type[CommandProcessor] = ClientCommandProcessor command_processor: typing.Type[CommandProcessor] = ClientCommandProcessor
ui: typing.Optional["kvui.GameManager"] = None ui = None
ui_task: typing.Optional["asyncio.Task[None]"] = None ui_task: typing.Optional["asyncio.Task[None]"] = None
input_task: typing.Optional["asyncio.Task[None]"] = None input_task: typing.Optional["asyncio.Task[None]"] = None
keep_alive_task: typing.Optional["asyncio.Task[None]"] = None keep_alive_task: typing.Optional["asyncio.Task[None]"] = None
@@ -662,19 +662,17 @@ class CommonContext:
logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True}) logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True})
self._messagebox_connection_loss = self.gui_error(msg, exc_info[1]) self._messagebox_connection_loss = self.gui_error(msg, exc_info[1])
def make_gui(self) -> typing.Type["kvui.GameManager"]: def run_gui(self):
"""To return the Kivy App class needed for run_gui so it can be overridden before being built""" """Import kivy UI system and start running it as self.ui_task."""
from kvui import GameManager from kvui import GameManager
class TextManager(GameManager): class TextManager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago Text Client" base_title = "Archipelago Text Client"
return TextManager self.ui = TextManager(self)
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") self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
def run_cli(self): def run_cli(self):
@@ -996,7 +994,7 @@ def get_base_parser(description: typing.Optional[str] = None):
return parser return parser
def run_as_textclient(*args): def run_as_textclient():
class TextContext(CommonContext): class TextContext(CommonContext):
# Text Mode to use !hint and such with games that have no text entry # Text Mode to use !hint and such with games that have no text entry
tags = CommonContext.tags | {"TextOnly"} tags = CommonContext.tags | {"TextOnly"}
@@ -1035,18 +1033,15 @@ def run_as_textclient(*args):
parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.") 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('--name', default=None, help="Slot Name to connect as.")
parser.add_argument("url", nargs="?", help="Archipelago connection url") parser.add_argument("url", nargs="?", help="Archipelago connection url")
args = parser.parse_args(args) args = parser.parse_args()
if args.url: if args.url:
url = urllib.parse.urlparse(args.url) url = urllib.parse.urlparse(args.url)
if url.scheme == "archipelago": args.connect = url.netloc
args.connect = url.netloc if url.username:
if url.username: args.name = urllib.parse.unquote(url.username)
args.name = urllib.parse.unquote(url.username) if url.password:
if url.password: args.password = urllib.parse.unquote(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() colorama.init()
@@ -1056,4 +1051,4 @@ def run_as_textclient(*args):
if __name__ == '__main__': if __name__ == '__main__':
logging.getLogger().setLevel(logging.INFO) # force log-level to work around log level resetting to WARNING logging.getLogger().setLevel(logging.INFO) # force log-level to work around log level resetting to WARNING
run_as_textclient(*sys.argv[1:]) # default value for parse_args run_as_textclient()

33
Fill.py
View File

@@ -12,12 +12,7 @@ from worlds.generic.Rules import add_item_rule
class FillError(RuntimeError): class FillError(RuntimeError):
def __init__(self, *args: typing.Union[str, typing.Any], **kwargs) -> None: pass
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: def _log_fill_progress(name: str, placed: int, total_items: int) -> None:
@@ -29,7 +24,7 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item]
new_state = base_state.copy() new_state = base_state.copy()
for item in itempool: for item in itempool:
new_state.collect(item, True) new_state.collect(item, True)
new_state.sweep_for_advancements(locations=locations) new_state.sweep_for_events(locations=locations)
return new_state return new_state
@@ -217,7 +212,7 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
f"Unfilled locations:\n" f"Unfilled locations:\n"
f"{', '.join(str(location) for location in locations)}\n" f"{', '.join(str(location) for location in locations)}\n"
f"Already placed {len(placements)}:\n" f"Already placed {len(placements)}:\n"
f"{', '.join(str(place) for place in placements)}", multiworld=multiworld) f"{', '.join(str(place) for place in placements)}")
item_pool.extend(unplaced_items) item_pool.extend(unplaced_items)
@@ -304,7 +299,7 @@ def remaining_fill(multiworld: MultiWorld,
f"Unfilled locations:\n" f"Unfilled locations:\n"
f"{', '.join(str(location) for location in locations)}\n" f"{', '.join(str(location) for location in locations)}\n"
f"Already placed {len(placements)}:\n" f"Already placed {len(placements)}:\n"
f"{', '.join(str(place) for place in placements)}", multiworld=multiworld) f"{', '.join(str(place) for place in placements)}")
itempool.extend(unplaced_items) itempool.extend(unplaced_items)
@@ -329,8 +324,8 @@ def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, lo
pool.append(location.item) pool.append(location.item)
state.remove(location.item) state.remove(location.item)
location.item = None location.item = None
if location in state.advancements: if location in state.events:
state.advancements.remove(location) state.events.remove(location)
locations.append(location) locations.append(location)
if pool and locations: if pool and locations:
locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY) locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY)
@@ -363,7 +358,7 @@ def distribute_early_items(multiworld: MultiWorld,
early_priority_locations: typing.List[Location] = [] early_priority_locations: typing.List[Location] = []
loc_indexes_to_remove: typing.Set[int] = set() loc_indexes_to_remove: typing.Set[int] = set()
base_state = multiworld.state.copy() base_state = multiworld.state.copy()
base_state.sweep_for_advancements(locations=(loc for loc in multiworld.get_filled_locations() if loc.address is None)) base_state.sweep_for_events(locations=(loc for loc in multiworld.get_filled_locations() if loc.address is None))
for i, loc in enumerate(fill_locations): for i, loc in enumerate(fill_locations):
if loc.can_reach(base_state): if loc.can_reach(base_state):
if loc.progress_type == LocationProgressType.PRIORITY: if loc.progress_type == LocationProgressType.PRIORITY:
@@ -511,8 +506,7 @@ def distribute_items_restrictive(multiworld: MultiWorld,
if progitempool: if progitempool:
raise FillError( raise FillError(
f"Not enough locations for progression items. " 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) accessibility_corrections(multiworld, multiworld.state, defaultlocations)
@@ -529,8 +523,7 @@ def distribute_items_restrictive(multiworld: MultiWorld,
if excludedlocations: if excludedlocations:
raise FillError( raise FillError(
f"Not enough filler items for excluded locations. " f"Not enough filler items for excluded locations. "
f"There are {len(excludedlocations)} more excluded locations than excludable items.", f"There are {len(excludedlocations)} more excluded locations than filler or trap items."
multiworld=multiworld,
) )
restitempool = filleritempool + usefulitempool restitempool = filleritempool + usefulitempool
@@ -558,7 +551,7 @@ def flood_items(multiworld: MultiWorld) -> None:
progress_done = False progress_done = False
# sweep once to pick up preplaced items # sweep once to pick up preplaced items
multiworld.state.sweep_for_advancements() multiworld.state.sweep_for_events()
# fill multiworld from top of itempool while we can # fill multiworld from top of itempool while we can
while not progress_done: while not progress_done:
@@ -596,7 +589,7 @@ def flood_items(multiworld: MultiWorld) -> None:
if candidate_item_to_place is not None: if candidate_item_to_place is not None:
item_to_place = candidate_item_to_place item_to_place = candidate_item_to_place
else: else:
raise FillError('No more progress items left to place.', multiworld=multiworld) raise FillError('No more progress items left to place.')
# find item to replace with progress item # find item to replace with progress item
location_list = multiworld.get_reachable_locations() location_list = multiworld.get_reachable_locations()
@@ -746,7 +739,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
), items_to_test): ), items_to_test):
reducing_state.collect(location.item, True, location) reducing_state.collect(location.item, True, location)
reducing_state.sweep_for_advancements(locations=locations_to_test) reducing_state.sweep_for_events(locations=locations_to_test)
if multiworld.has_beaten_game(balancing_state): if multiworld.has_beaten_game(balancing_state):
if not multiworld.has_beaten_game(reducing_state): if not multiworld.has_beaten_game(reducing_state):
@@ -829,7 +822,7 @@ def distribute_planned(multiworld: MultiWorld) -> None:
warn(warning, force) warn(warning, force)
swept_state = multiworld.state.copy() swept_state = multiworld.state.copy()
swept_state.sweep_for_advancements() swept_state.sweep_for_events()
reachable = frozenset(multiworld.get_reachable_locations(swept_state)) reachable = frozenset(multiworld.get_reachable_locations(swept_state))
early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list) early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
non_early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list) non_early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)

View File

@@ -155,7 +155,6 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
erargs.outputpath = args.outputpath erargs.outputpath = args.outputpath
erargs.skip_prog_balancing = args.skip_prog_balancing erargs.skip_prog_balancing = args.skip_prog_balancing
erargs.skip_output = args.skip_output erargs.skip_output = args.skip_output
erargs.name = {}
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \ settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None) {fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None)
@@ -203,7 +202,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 if path == args.weights_file_path: # if name came from the weights file, just use base player name
erargs.name[player] = f"Player{player}" erargs.name[player] = f"Player{player}"
elif player not in erargs.name: # if name was not specified, generate it from filename elif not erargs.name[player]: # if name was not specified, generate it from filename
erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0] erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
erargs.name[player] = handle_name(erargs.name[player], player, name_counter) erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
@@ -512,7 +511,7 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
continue continue
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers.") 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: if PlandoOptions.items in plando_options:
ret.plando_items = copy.deepcopy(game_weights.get("plando_items", [])) ret.plando_items = game_weights.get("plando_items", [])
if ret.game == "A Link to the Past": if ret.game == "A Link to the Past":
roll_alttp_settings(ret, game_weights) roll_alttp_settings(ret, game_weights)

View File

@@ -1,9 +0,0 @@
if __name__ == '__main__':
import ModuleUpdate
ModuleUpdate.update()
import Utils
Utils.init_logging("KH1Client", exception_logger="Client")
from worlds.kh1.Client import launch
launch()

View File

@@ -16,11 +16,10 @@ import multiprocessing
import shlex import shlex
import subprocess import subprocess
import sys import sys
import urllib.parse
import webbrowser import webbrowser
from os.path import isfile from os.path import isfile
from shutil import which from shutil import which
from typing import Callable, Optional, Sequence, Tuple, Union from typing import Callable, Sequence, Union, Optional
import Utils import Utils
import settings import settings
@@ -108,81 +107,7 @@ components.extend([
]) ])
def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None: def identify(path: Union[None, str]):
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: if path is None:
return None, None return None, None
for component in components: for component in components:
@@ -341,7 +266,7 @@ def run_gui():
if file and component: if file and component:
run_component(component, file) run_component(component, file)
else: else:
logging.warning(f"unable to identify component for {file}") logging.warning(f"unable to identify component for {filename}")
def _stop(self, *largs): def _stop(self, *largs):
# ran into what appears to be https://groups.google.com/g/kivy-users/c/saWDLoYCSZ4 with PyCharm. # ran into what appears to be https://groups.google.com/g/kivy-users/c/saWDLoYCSZ4 with PyCharm.
@@ -374,24 +299,20 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
elif not args: elif not args:
args = {} args = {}
path = args.get("Patch|Game|Component|url", None) if args.get("Patch|Game|Component", None) is not None:
if path is not None: file, component = identify(args["Patch|Game|Component"])
if path.startswith("archipelago://"):
handle_uri(path, args.get("args", ()))
return
file, component = identify(path)
if file: if file:
args['file'] = file args['file'] = file
if component: if component:
args['component'] = component args['component'] = component
if not component: if not component:
logging.warning(f"Could not identify Component responsible for {path}") logging.warning(f"Could not identify Component responsible for {args['Patch|Game|Component']}")
if args["update_settings"]: if args["update_settings"]:
update_settings() update_settings()
if "file" in args: if 'file' in args:
run_component(args["component"], args["file"], *args["args"]) run_component(args["component"], args["file"], *args["args"])
elif "component" in args: elif 'component' in args:
run_component(args["component"], *args["args"]) run_component(args["component"], *args["args"])
elif not args["update_settings"]: elif not args["update_settings"]:
run_gui() run_gui()
@@ -401,16 +322,12 @@ if __name__ == '__main__':
init_logging('Launcher') init_logging('Launcher')
Utils.freeze_support() Utils.freeze_support()
multiprocessing.set_start_method("spawn") # if launched process uses kivy, fork won't work multiprocessing.set_start_method("spawn") # if launched process uses kivy, fork won't work
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(description='Archipelago Launcher')
description='Archipelago Launcher',
usage="[-h] [--update_settings] [Patch|Game|Component] [-- component args here]"
)
run_group = parser.add_argument_group("Run") run_group = parser.add_argument_group("Run")
run_group.add_argument("--update_settings", action="store_true", run_group.add_argument("--update_settings", action="store_true",
help="Update host.yaml and exit.") help="Update host.yaml and exit.")
run_group.add_argument("Patch|Game|Component|url", type=str, nargs="?", run_group.add_argument("Patch|Game|Component", type=str, nargs="?",
help="Pass either a patch file, a generated game, the component name to run, or a url to " help="Pass either a patch file, a generated game or the name of a component to run.")
"connect with.")
run_group.add_argument("args", nargs="*", run_group.add_argument("args", nargs="*",
help="Arguments to pass to component.") help="Arguments to pass to component.")
main(parser.parse_args()) main(parser.parse_args())

View File

@@ -14,7 +14,7 @@ import tkinter as tk
from argparse import Namespace from argparse import Namespace
from concurrent.futures import as_completed, ThreadPoolExecutor from concurrent.futures import as_completed, ThreadPoolExecutor
from glob import glob from glob import glob
from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox, Button, Radiobutton, LEFT, X, BOTH, TOP, LabelFrame, \ from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox, Button, Radiobutton, LEFT, X, TOP, LabelFrame, \
IntVar, Checkbutton, E, W, OptionMenu, Toplevel, BOTTOM, RIGHT, font as font, PhotoImage IntVar, Checkbutton, E, W, OptionMenu, Toplevel, BOTTOM, RIGHT, font as font, PhotoImage
from tkinter.constants import DISABLED, NORMAL from tkinter.constants import DISABLED, NORMAL
from urllib.parse import urlparse from urllib.parse import urlparse
@@ -29,8 +29,7 @@ from Utils import output_path, local_path, user_path, open_file, get_cert_none_s
GAME_ALTTP = "A Link to the Past" GAME_ALTTP = "A Link to the Past"
WINDOW_MIN_HEIGHT = 525
WINDOW_MIN_WIDTH = 425
class AdjusterWorld(object): class AdjusterWorld(object):
def __init__(self, sprite_pool): def __init__(self, sprite_pool):
@@ -243,17 +242,16 @@ def adjustGUI():
from argparse import Namespace from argparse import Namespace
from Utils import __version__ as MWVersion from Utils import __version__ as MWVersion
adjustWindow = Tk() adjustWindow = Tk()
adjustWindow.minsize(WINDOW_MIN_WIDTH, WINDOW_MIN_HEIGHT)
adjustWindow.wm_title("Archipelago %s LttP Adjuster" % MWVersion) adjustWindow.wm_title("Archipelago %s LttP Adjuster" % MWVersion)
set_icon(adjustWindow) set_icon(adjustWindow)
rom_options_frame, rom_vars, set_sprite = get_rom_options_frame(adjustWindow) rom_options_frame, rom_vars, set_sprite = get_rom_options_frame(adjustWindow)
bottomFrame2 = Frame(adjustWindow, padx=8, pady=2) bottomFrame2 = Frame(adjustWindow)
romFrame, romVar = get_rom_frame(adjustWindow) romFrame, romVar = get_rom_frame(adjustWindow)
romDialogFrame = Frame(adjustWindow, padx=8, pady=2) romDialogFrame = Frame(adjustWindow)
baseRomLabel2 = Label(romDialogFrame, text='Rom to adjust') baseRomLabel2 = Label(romDialogFrame, text='Rom to adjust')
romVar2 = StringVar() romVar2 = StringVar()
romEntry2 = Entry(romDialogFrame, textvariable=romVar2) romEntry2 = Entry(romDialogFrame, textvariable=romVar2)
@@ -263,9 +261,9 @@ def adjustGUI():
romVar2.set(rom) romVar2.set(rom)
romSelectButton2 = Button(romDialogFrame, text='Select Rom', command=RomSelect2) romSelectButton2 = Button(romDialogFrame, text='Select Rom', command=RomSelect2)
romDialogFrame.pack(side=TOP, expand=False, fill=X) romDialogFrame.pack(side=TOP, expand=True, fill=X)
baseRomLabel2.pack(side=LEFT, expand=False, fill=X, padx=(0, 8)) baseRomLabel2.pack(side=LEFT)
romEntry2.pack(side=LEFT, expand=True, fill=BOTH, pady=1) romEntry2.pack(side=LEFT, expand=True, fill=X)
romSelectButton2.pack(side=LEFT) romSelectButton2.pack(side=LEFT)
def adjustRom(): def adjustRom():
@@ -333,11 +331,12 @@ def adjustGUI():
messagebox.showinfo(title="Success", message="Settings saved to persistent storage") messagebox.showinfo(title="Success", message="Settings saved to persistent storage")
adjustButton = Button(bottomFrame2, text='Adjust Rom', command=adjustRom) adjustButton = Button(bottomFrame2, text='Adjust Rom', command=adjustRom)
rom_options_frame.pack(side=TOP, padx=8, pady=8, fill=BOTH, expand=True) rom_options_frame.pack(side=TOP)
adjustButton.pack(side=LEFT, padx=(5,5)) adjustButton.pack(side=LEFT, padx=(5,5))
saveButton = Button(bottomFrame2, text='Save Settings', command=saveGUISettings) saveButton = Button(bottomFrame2, text='Save Settings', command=saveGUISettings)
saveButton.pack(side=LEFT, padx=(5,5)) saveButton.pack(side=LEFT, padx=(5,5))
bottomFrame2.pack(side=TOP, pady=(5,5)) bottomFrame2.pack(side=TOP, pady=(5,5))
tkinter_center_window(adjustWindow) tkinter_center_window(adjustWindow)
@@ -577,7 +576,7 @@ class AttachTooltip(object):
def get_rom_frame(parent=None): def get_rom_frame(parent=None):
adjuster_settings = get_adjuster_settings(GAME_ALTTP) adjuster_settings = get_adjuster_settings(GAME_ALTTP)
romFrame = Frame(parent, padx=8, pady=8) romFrame = Frame(parent)
baseRomLabel = Label(romFrame, text='LttP Base Rom: ') baseRomLabel = Label(romFrame, text='LttP Base Rom: ')
romVar = StringVar(value=adjuster_settings.baserom) romVar = StringVar(value=adjuster_settings.baserom)
romEntry = Entry(romFrame, textvariable=romVar) romEntry = Entry(romFrame, textvariable=romVar)
@@ -597,19 +596,20 @@ def get_rom_frame(parent=None):
romSelectButton = Button(romFrame, text='Select Rom', command=RomSelect) romSelectButton = Button(romFrame, text='Select Rom', command=RomSelect)
baseRomLabel.pack(side=LEFT) baseRomLabel.pack(side=LEFT)
romEntry.pack(side=LEFT, expand=True, fill=BOTH, pady=1) romEntry.pack(side=LEFT, expand=True, fill=X)
romSelectButton.pack(side=LEFT) romSelectButton.pack(side=LEFT)
romFrame.pack(side=TOP, fill=X) romFrame.pack(side=TOP, expand=True, fill=X)
return romFrame, romVar return romFrame, romVar
def get_rom_options_frame(parent=None): def get_rom_options_frame(parent=None):
adjuster_settings = get_adjuster_settings(GAME_ALTTP) adjuster_settings = get_adjuster_settings(GAME_ALTTP)
romOptionsFrame = LabelFrame(parent, text="Rom options", padx=8, pady=8) romOptionsFrame = LabelFrame(parent, text="Rom options")
romOptionsFrame.columnconfigure(0, weight=1)
romOptionsFrame.columnconfigure(1, weight=1)
for i in range(5): for i in range(5):
romOptionsFrame.rowconfigure(i, weight=0, pad=4) romOptionsFrame.rowconfigure(i, weight=1)
vars = Namespace() vars = Namespace()
vars.MusicVar = IntVar() vars.MusicVar = IntVar()
@@ -660,7 +660,7 @@ def get_rom_options_frame(parent=None):
spriteSelectButton = Button(spriteDialogFrame, text='...', command=SpriteSelect) spriteSelectButton = Button(spriteDialogFrame, text='...', command=SpriteSelect)
baseSpriteLabel.pack(side=LEFT) baseSpriteLabel.pack(side=LEFT)
spriteEntry.pack(side=LEFT, expand=True, fill=X) spriteEntry.pack(side=LEFT)
spriteSelectButton.pack(side=LEFT) spriteSelectButton.pack(side=LEFT)
oofDialogFrame = Frame(romOptionsFrame) oofDialogFrame = Frame(romOptionsFrame)

View File

@@ -11,8 +11,7 @@ from typing import Dict, List, Optional, Set, Tuple, Union
import worlds import worlds
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region
from Fill import FillError, balance_multiworld_progression, distribute_items_restrictive, distribute_planned, \ from Fill import balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items
flood_items
from Options import StartInventoryPool from Options import StartInventoryPool
from Utils import __version__, output_path, version_tuple, get_settings from Utils import __version__, output_path, version_tuple, get_settings
from settings import get_settings from settings import get_settings
@@ -101,7 +100,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
multiworld.early_items[player][item_name] = max(0, early-count) multiworld.early_items[player][item_name] = max(0, early-count)
remaining_count = count-early remaining_count = count-early
if remaining_count > 0: if remaining_count > 0:
local_early = multiworld.local_early_items[player].get(item_name, 0) local_early = multiworld.early_local_items[player].get(item_name, 0)
if local_early: if local_early:
multiworld.early_items[player][item_name] = max(0, local_early - remaining_count) multiworld.early_items[player][item_name] = max(0, local_early - remaining_count)
del local_early del local_early
@@ -347,7 +346,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
output_file_futures.append(pool.submit(write_multidata)) output_file_futures.append(pool.submit(write_multidata))
if not check_accessibility_task.result(): if not check_accessibility_task.result():
if not multiworld.can_beat_game(): if not multiworld.can_beat_game():
raise FillError("Game appears as unbeatable. Aborting.", multiworld=multiworld) raise Exception("Game appears as unbeatable. Aborting.")
else: else:
logger.warning("Location Accessibility requirements not fulfilled.") logger.warning("Location Accessibility requirements not fulfilled.")

View File

@@ -75,13 +75,13 @@ def update(yes: bool = False, force: bool = False) -> None:
if not update_ran: if not update_ran:
update_ran = True update_ran = True
install_pkg_resources(yes=yes)
import pkg_resources
if force: if force:
update_command() update_command()
return return
install_pkg_resources(yes=yes)
import pkg_resources
prev = "" # if a line ends in \ we store here and merge later prev = "" # if a line ends in \ we store here and merge later
for req_file in requirements_files: for req_file in requirements_files:
path = os.path.join(os.path.dirname(sys.argv[0]), req_file) path = os.path.join(os.path.dirname(sys.argv[0]), req_file)

View File

@@ -67,21 +67,6 @@ def update_dict(dictionary, entries):
return dictionary 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 # functions callable on storable data on the server by clients
modify_functions = { modify_functions = {
# generic: # generic:
@@ -566,9 +551,6 @@ class Context:
self.logger.info(f"Saving failed. Retry in {self.auto_save_interval} seconds.") self.logger.info(f"Saving failed. Retry in {self.auto_save_interval} seconds.")
else: else:
self.save_dirty = False 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 = threading.Thread(target=save_regularly, daemon=True)
self.auto_saver_thread.start() self.auto_saver_thread.start()
@@ -1009,7 +991,7 @@ def collect_player(ctx: Context, team: int, slot: int, is_group: bool = False):
collect_player(ctx, team, group, True) collect_player(ctx, team, group, True)
def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[typing.Tuple[int, int]]: def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]:
return ctx.locations.get_remaining(ctx.location_checks, team, slot) return ctx.locations.get_remaining(ctx.location_checks, team, slot)
@@ -1221,10 +1203,6 @@ class CommonCommandProcessor(CommandProcessor):
timer = int(seconds, 10) timer = int(seconds, 10)
except ValueError: except ValueError:
timer = 10 timer = 10
else:
if timer > 60 * 60:
raise ValueError(f"{timer} is invalid. Maximum is 1 hour.")
async_start(countdown(self.ctx, timer)) async_start(countdown(self.ctx, timer))
return True return True
@@ -1372,10 +1350,10 @@ class ClientMessageProcessor(CommonCommandProcessor):
def _cmd_remaining(self) -> bool: def _cmd_remaining(self) -> bool:
"""List remaining items in your game, but not their location or recipient""" """List remaining items in your game, but not their location or recipient"""
if self.ctx.remaining_mode == "enabled": if self.ctx.remaining_mode == "enabled":
rest_locations = get_remaining(self.ctx, self.client.team, self.client.slot) remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
if rest_locations: if remaining_item_ids:
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[slot]][item_id] self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[self.client.slot]][item_id]
for slot, item_id in rest_locations)) for item_id in remaining_item_ids))
else: else:
self.output("No remaining items found.") self.output("No remaining items found.")
return True return True
@@ -1385,10 +1363,10 @@ class ClientMessageProcessor(CommonCommandProcessor):
return False return False
else: # is goal else: # is goal
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL: if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
rest_locations = get_remaining(self.ctx, self.client.team, self.client.slot) remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
if rest_locations: if remaining_item_ids:
self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[slot]][item_id] self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[self.client.slot]][item_id]
for slot, item_id in rest_locations)) for item_id in remaining_item_ids))
else: else:
self.output("No remaining items found.") self.output("No remaining items found.")
return True return True
@@ -2061,8 +2039,6 @@ class ServerCommandProcessor(CommonCommandProcessor):
item_name, usable, response = get_intended_text(item_name, names) item_name, usable, response = get_intended_text(item_name, names)
if usable: if usable:
amount: int = int(amount) 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))] new_items = [NetworkItem(names[item_name], -1, 0) for _ in range(int(amount))]
send_items_to(self.ctx, team, slot, *new_items) send_items_to(self.ctx, team, slot, *new_items)

View File

@@ -79,7 +79,6 @@ class NetworkItem(typing.NamedTuple):
item: int item: int
location: int location: int
player: int player: int
""" Sending player, except in LocationInfo (from LocationScouts), where it is the receiving player. """
flags: int = 0 flags: int = 0
@@ -398,12 +397,12 @@ class _LocationStore(dict, typing.MutableMapping[int, typing.Dict[int, typing.Tu
location_id not in checked] location_id not in checked]
def get_remaining(self, state: typing.Dict[typing.Tuple[int, int], typing.Set[int]], team: int, slot: int def get_remaining(self, state: typing.Dict[typing.Tuple[int, int], typing.Set[int]], team: int, slot: int
) -> typing.List[typing.Tuple[int, int]]: ) -> typing.List[int]:
checked = state[team, slot] checked = state[team, slot]
player_locations = self[slot] player_locations = self[slot]
return sorted([(player_locations[location_id][1], player_locations[location_id][0]) for return sorted([player_locations[location_id][0] for
location_id in player_locations if location_id in player_locations if
location_id not in checked]) location_id not in checked])
if typing.TYPE_CHECKING: # type-check with pure python implementation until we have a typing stub if typing.TYPE_CHECKING: # type-check with pure python implementation until we have a typing stub

View File

@@ -973,19 +973,7 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys):
if random.random() < float(text.get("percentage", 100)/100): if random.random() < float(text.get("percentage", 100)/100):
at = text.get("at", None) at = text.get("at", None)
if at is not 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", []) 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): if isinstance(given_text, str):
given_text = [given_text] given_text = [given_text]
texts.append(PlandoText( texts.append(PlandoText(
@@ -993,8 +981,6 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys):
given_text, given_text,
text.get("percentage", 100) text.get("percentage", 100)
)) ))
else:
raise OptionError("\"at\" must be a valid string or weighted list of strings!")
elif isinstance(text, PlandoText): elif isinstance(text, PlandoText):
if random.random() < float(text.percentage/100): if random.random() < float(text.percentage/100):
texts.append(text) texts.append(text)

View File

@@ -73,9 +73,6 @@ Currently, the following games are supported:
* Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006 * Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006
* A Hat in Time * A Hat in Time
* Old School Runescape * Old School Runescape
* Kingdom Hearts 1
* Mega Man 2
* Yacht Dice
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). 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 Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

View File

@@ -1,4 +1,3 @@
import argparse
import os import os
import multiprocessing import multiprocessing
import logging import logging
@@ -32,15 +31,6 @@ def get_app() -> "Flask":
import yaml import yaml
app.config.from_file(configpath, yaml.safe_load) app.config.from_file(configpath, yaml.safe_load)
logging.info(f"Updated config from {configpath}") 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"]: if not app.config["HOST_ADDRESS"]:
logging.info("Getting public IP, as HOST_ADDRESS is empty.") logging.info("Getting public IP, as HOST_ADDRESS is empty.")
app.config["HOST_ADDRESS"] = Utils.get_public_ipv4() app.config["HOST_ADDRESS"] = Utils.get_public_ipv4()

View File

@@ -72,14 +72,6 @@ class WebHostContext(Context):
self.video = {} self.video = {}
self.tags = ["AP", "WebHost"] 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): def _load_game_data(self):
for key, value in self.static_server_data.items(): 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 # NOTE: attributes are mutable and shared, so they will have to be copied before being modified
@@ -257,7 +249,6 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
ctx = WebHostContext(static_server_data, logger) ctx = WebHostContext(static_server_data, logger)
ctx.load(room_id) ctx.load(room_id)
ctx.init_save() ctx.init_save()
assert ctx.server is None
try: try:
ctx.server = websockets.serve( ctx.server = websockets.serve(
functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context) functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
@@ -288,7 +279,6 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
ctx.auto_shutdown = Room.get(id=room_id).timeout ctx.auto_shutdown = Room.get(id=room_id).timeout
if ctx.saving: if ctx.saving:
setattr(asyncio.current_task(), "save", lambda: ctx._save(True)) setattr(asyncio.current_task(), "save", lambda: ctx._save(True))
assert ctx.shutdown_task is None
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, [])) ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
await ctx.shutdown_task await ctx.shutdown_task
@@ -335,7 +325,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
def run(self): def run(self):
while 1: while 1:
next_room = rooms_to_run.get(block=True, timeout=None) next_room = rooms_to_run.get(block=True, timeout=None)
gc.collect() gc.collect(0)
task = asyncio.run_coroutine_threadsafe(start_room(next_room), loop) task = asyncio.run_coroutine_threadsafe(start_room(next_room), loop)
self._tasks.append(task) self._tasks.append(task)
task.add_done_callback(self._done) task.add_done_callback(self._done)

View File

@@ -1,11 +1,10 @@
flask>=3.0.3 flask>=3.0.3
werkzeug>=3.0.4 werkzeug>=3.0.3
pony>=0.7.19 pony>=0.7.17
waitress>=3.0.0 waitress>=3.0.0
Flask-Caching>=2.3.0 Flask-Caching>=2.3.0
Flask-Compress>=1.15 Flask-Compress>=1.15
Flask-Limiter>=3.8.0 Flask-Limiter>=3.7.0
bokeh>=3.1.1; python_version <= '3.8' bokeh>=3.1.1; python_version <= '3.8'
bokeh>=3.4.3; python_version == '3.9' bokeh>=3.4.1; python_version >= '3.9'
bokeh>=3.5.2; python_version >= '3.10'
markupsafe>=2.1.5 markupsafe>=2.1.5

View File

@@ -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: You may also find developer documentation in the `docs` folder:
[/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs). [/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs).
If you have more questions, feel free to ask in the **#ap-world-dev** channel on our Discord. If you have more questions, feel free to ask in the **#archipelago-dev** channel on our Discord.

View File

@@ -22,7 +22,7 @@
{% for patch in room.seed.slots|list|sort(attribute="player_id") %} {% for patch in room.seed.slots|list|sort(attribute="player_id") %}
<tr> <tr>
<td>{{ patch.player_id }}</td> <td>{{ patch.player_id }}</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 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>{{ patch.game }}</td> <td>{{ patch.game }}</td>
<td> <td>
{% if patch.data %} {% if patch.data %}

View File

@@ -69,7 +69,7 @@
</tbody> </tbody>
</table> </table>
{% else %} {% else %}
You have not generated any seeds yet! You have no generated any seeds yet!
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@@ -138,7 +138,7 @@
id="{{ option_name }}-{{ key }}" id="{{ option_name }}-{{ key }}"
name="{{ option_name }}||{{ key }}" name="{{ option_name }}||{{ key }}"
value="1" value="1"
{{ "checked" if key in option.default }} checked="{{ "checked" if key in option.default else "" }}"
/> />
<label for="{{ option_name }}-{{ key }}"> <label for="{{ option_name }}-{{ key }}">
{{ key }} {{ key }}

View File

@@ -287,15 +287,15 @@ cdef class LocationStore:
entry in self.entries[start:start + count] if entry in self.entries[start:start + count] if
entry.location not in checked] entry.location not in checked]
def get_remaining(self, state: State, team: int, slot: int) -> List[Tuple[int, int]]: def get_remaining(self, state: State, team: int, slot: int) -> List[int]:
cdef LocationEntry* entry cdef LocationEntry* entry
cdef ap_player_t sender = slot cdef ap_player_t sender = slot
cdef size_t start = self.sender_index[sender].start cdef size_t start = self.sender_index[sender].start
cdef size_t count = self.sender_index[sender].count cdef size_t count = self.sender_index[sender].count
cdef set checked = state[team, slot] cdef set checked = state[team, slot]
return sorted([(entry.receiver, entry.item) for return sorted([entry.item for
entry in self.entries[start:start+count] if entry in self.entries[start:start+count] if
entry.location not in checked]) entry.location not in checked])
@cython.auto_pickle(False) @cython.auto_pickle(False)

View File

@@ -46,7 +46,7 @@
/worlds/clique/ @ThePhar /worlds/clique/ @ThePhar
# Dark Souls III # Dark Souls III
/worlds/dark_souls_3/ @Marechal-L @nex3 /worlds/dark_souls_3/ @Marechal-L
# Donkey Kong Country 3 # Donkey Kong Country 3
/worlds/dkc3/ @PoryGone /worlds/dkc3/ @PoryGone
@@ -78,9 +78,6 @@
# Kirby's Dream Land 3 # Kirby's Dream Land 3
/worlds/kdl3/ @Silvris /worlds/kdl3/ @Silvris
# Kingdom Hearts
/worlds/kh1/ @gaithern
# Kingdom Hearts 2 # Kingdom Hearts 2
/worlds/kh2/ @JaredWeakStrike /worlds/kh2/ @JaredWeakStrike
@@ -106,9 +103,6 @@
# Minecraft # Minecraft
/worlds/minecraft/ @KonoTyran @espeon65536 /worlds/minecraft/ @KonoTyran @espeon65536
# Mega Man 2
/worlds/mm2/ @Silvris
# MegaMan Battle Network 3 # MegaMan Battle Network 3
/worlds/mmbn3/ @digiholic /worlds/mmbn3/ @digiholic
@@ -118,6 +112,9 @@
# Noita # Noita
/worlds/noita/ @ScipioWright @heinermann /worlds/noita/ @ScipioWright @heinermann
# Ocarina of Time
/worlds/oot/ @espeon65536
# Old School Runescape # Old School Runescape
/worlds/osrs @digiholic /worlds/osrs @digiholic
@@ -199,9 +196,6 @@
# The Witness # The Witness
/worlds/witness/ @NewSoupVi @blastron /worlds/witness/ @NewSoupVi @blastron
# Yacht Dice
/worlds/yachtdice/ @spinerak
# Yoshi's Island # Yoshi's Island
/worlds/yoshisisland/ @PinkSwitch /worlds/yoshisisland/ @PinkSwitch
@@ -227,9 +221,6 @@
# Links Awakening DX # Links Awakening DX
# /worlds/ladx/ # /worlds/ladx/
# Ocarina of Time
# /worlds/oot/
## Disabled Unmaintained Worlds ## Disabled Unmaintained Worlds
# The following worlds in this repo are currently unmaintained and disabled as they do not work in core. If you are # The following worlds in this repo are currently unmaintained and disabled as they do not work in core. If you are

View File

@@ -510,7 +510,7 @@ In JSON this may look like:
| ----- | ----- | | ----- | ----- |
| 0 | Nothing special about this item | | 0 | Nothing special about this item |
| 0b001 | If set, indicates the item can unlock logical advancement | | 0b001 | If set, indicates the item can unlock logical advancement |
| 0b010 | If set, indicates the item is especially useful | | 0b010 | If set, indicates the item is important but not in a way that unlocks advancement |
| 0b100 | If set, indicates the item is a trap | | 0b100 | If set, indicates the item is a trap |
### JSONMessagePart ### JSONMessagePart
@@ -702,18 +702,14 @@ 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. | | checksum | str | A checksum hash of this game's data. |
### Tags ### 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 | | Name | Notes |
|-----------|--------------------------------------------------------------------------------------------------------------------------------------| |------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| AP | Signifies that this client is a reference client, its usefulness is mostly in debugging to compare client behaviours more easily. | | 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. | | 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 | 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. |
| Tracker | Indicates the client is a tracker, made to track instead of sending locations. Special join/leave message,¹ `game` is optional.² | | 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. |
| 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 ### 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: A special kind of Bounce packet that can be supported by any AP game. It targets the tag "DeathLink" and carries the following data:

View File

@@ -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 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`. 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 - All options with a fixed set of possible values (i.e. those which inherit from `Toggle`, `(Text)Choice` or
`(Named)Range`) support `random` as a generic option. `random` chooses from any of the available values for that `(Named/Special)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`. 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 However, you can override `from_text` and handle `text == "random"` to customize its behavior or
implement it for additional option types. implement it for additional option types.
@@ -129,23 +129,6 @@ class Difficulty(Choice):
default = 1 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 ### Option Groups
Options may be categorized into groups for display on the WebHost. Option groups are displayed in the order specified 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 by your world on the player-options and weighted-options pages. In the generated template files, there will be a comment

View File

@@ -8,7 +8,7 @@ use that version. These steps are for developers or platforms without compiled r
What you'll need: What you'll need:
* [Python 3.8.7 or newer](https://www.python.org/downloads/), not the Windows Store version * [Python 3.8.7 or newer](https://www.python.org/downloads/), not the Windows Store version
* Python 3.12.x is currently the newest supported version * **Python 3.12 is currently unsupported**
* pip: included in downloads from python.org, separate in many Linux distributions * pip: included in downloads from python.org, separate in many Linux distributions
* Matching C compiler * Matching C compiler
* possibly optional, read operating system specific sections * possibly optional, read operating system specific sections
@@ -31,14 +31,14 @@ After this, you should be able to run the programs.
Recommended steps Recommended steps
* Download and install a "Windows installer (64-bit)" from the [Python download page](https://www.python.org/downloads) * Download and install a "Windows installer (64-bit)" from the [Python download page](https://www.python.org/downloads)
* [read above](#General) which versions are supported * **Python 3.12 is currently unsupported**
* **Optional**: Download and install Visual Studio Build Tools from * **Optional**: Download and install Visual Studio Build Tools from
[Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/). [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. * 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. 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 * Build tools are not required if all modules are installed pre-compiled. Pre-compiled modules are pinned on
[Discord in #ap-core-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808) [Discord in #archipelago-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808)
* It is recommended to use [PyCharm IDE](https://www.jetbrains.com/pycharm/) * 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 * Run Generate.py which will prompt installation of missing modules, press enter to confirm

View File

@@ -248,8 +248,7 @@ will all have the same ID. Name must not be numeric (must contain at least 1 let
Other classifications include: Other classifications include:
* `filler`: a regular item or trash item * `filler`: a regular item or trash item
* `useful`: item that is especially useful. Cannot be placed on excluded or unreachable locations. When combined with * `useful`: generally quite useful, but not required for anything logical. Cannot be placed on excluded locations
another flag like "progression", it means "an especially useful progression item".
* `trap`: negative impact on the player * `trap`: negative impact on the player
* `skip_balancing`: denotes that an item should not be moved to an earlier sphere for the purpose of balancing (to be * `skip_balancing`: denotes that an item should not be moved to an earlier sphere for the purpose of balancing (to be
combined with `progression`; see below) combined with `progression`; see below)
@@ -304,31 +303,6 @@ generation (entrance randomization).
An access rule is a function that returns `True` or `False` for a `Location` or `Entrance` based on the current `state` 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). (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 ### 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 An item rule is a function that returns `True` or `False` for a `Location` based on a single item. It can be used to
@@ -656,7 +630,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 Custom methods can be defined for your logic rules. The access rule that ultimately gets assigned to the Location or
Entrance should be Entrance should be
a [`CollectionRule`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L10). a [`CollectionRule`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L9).
Typically, this is done by defining a lambda expression on demand at the relevant bit, typically calling other 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. 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). For an example, see [The Messenger](/worlds/messenger/rules.py).

View File

@@ -26,17 +26,8 @@ Unless these are shared between multiple people, we expect the following from ea
### Adding a World ### Adding a World
When we merge your world into the core Archipelago repository, you automatically become world maintainer unless you 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). 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.
### 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 ### Getting Voted
@@ -44,7 +35,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. 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. 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. The time limit is 1 week, but can end early if the majority is reached earlier.
Voting shall be conducted on Discord in #ap-core-dev. Voting shall be conducted on Discord in #archipelago-dev.
## Dropping out ## Dropping out
@@ -60,7 +51,7 @@ for example when they become unreachable.
For a vote to pass, the majority of participating core maintainers must vote in the affirmative. 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 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. made their case or was pinged and has been unreachable for more than 2 weeks already.
Voting shall be conducted on Discord in #ap-core-dev. Commits that are a direct result of the voting shall include Voting shall be conducted on Discord in #archipelago-dev. Commits that are a direct result of the voting shall include
date, voting members and final result in the commit message. date, voting members and final result in the commit message.
## Handling of Unmaintained Worlds ## Handling of Unmaintained Worlds

View File

@@ -186,11 +186,6 @@ Root: HKCR; Subkey: "{#MyAppName}cv64patch"; ValueData: "Arc
Root: HKCR; Subkey: "{#MyAppName}cv64patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}cv64patch\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: "{#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: ".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"; 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: ""; Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: "";
@@ -228,8 +223,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"; ValueData: "Archipegalo Protocol"; Flags: uninsdeletekey;
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: ""; Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: "";
Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoLauncher.exe,0"; Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoTextClient.exe,0";
Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1"""; Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoTextClient.exe"" ""%1""";
[Code] [Code]
// See: https://stackoverflow.com/a/51614652/2287576 // See: https://stackoverflow.com/a/51614652/2287576

View File

@@ -5,8 +5,6 @@ import typing
import re import re
from collections import deque from collections import deque
assert "kivy" not in sys.modules, "kvui should be imported before kivy for frozen compatibility"
if sys.platform == "win32": if sys.platform == "win32":
import ctypes import ctypes

View File

@@ -1,14 +1,14 @@
colorama>=0.4.6 colorama>=0.4.6
websockets>=13.0.1 websockets>=12.0
PyYAML>=6.0.2 PyYAML>=6.0.1
jellyfish>=1.1.0 jellyfish>=1.0.3
jinja2>=3.1.4 jinja2>=3.1.4
schema>=0.7.7 schema>=0.7.7
kivy>=2.3.0 kivy>=2.3.0
bsdiff4>=1.2.4 bsdiff4>=1.2.4
platformdirs>=4.2.2 platformdirs>=4.2.2
certifi>=2024.8.30 certifi>=2024.6.2
cython>=3.0.11 cython>=3.0.10
cymem>=2.0.8 cymem>=2.0.8
orjson>=3.10.7 orjson>=3.10.3
typing_extensions>=4.12.2 typing_extensions>=4.12.1

View File

@@ -23,8 +23,8 @@ class TestBase(unittest.TestCase):
state = CollectionState(self.multiworld) state = CollectionState(self.multiworld)
for item in items: for item in items:
item.classification = ItemClassification.progression item.classification = ItemClassification.progression
state.collect(item, prevent_sweep=True) state.collect(item, event=True)
state.sweep_for_advancements() state.sweep_for_events()
state.update_reachable_regions(1) state.update_reachable_regions(1)
self._state_cache[self.multiworld, tuple(items)] = state self._state_cache[self.multiworld, tuple(items)] = state
return state return state
@@ -221,8 +221,8 @@ class WorldTestBase(unittest.TestCase):
if isinstance(items, Item): if isinstance(items, Item):
items = (items,) items = (items,)
for item in items: for item in items:
if item.location and item.advancement and item.location in self.multiworld.state.advancements: if item.location and item.advancement and item.location in self.multiworld.state.events:
self.multiworld.state.advancements.remove(item.location) self.multiworld.state.events.remove(item.location)
self.multiworld.state.remove(item) self.multiworld.state.remove(item)
def can_reach_location(self, location: str) -> bool: def can_reach_location(self, location: str) -> bool:
@@ -293,11 +293,13 @@ class WorldTestBase(unittest.TestCase):
if not (self.run_default_tests and self.constructed): if not (self.run_default_tests and self.constructed):
return return
with self.subTest("Game", game=self.game, seed=self.multiworld.seed): 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) state = self.multiworld.get_all_state(False)
for location in self.multiworld.get_locations(): for location in self.multiworld.get_locations():
with self.subTest("Location should be reached", location=location.name): if location.name not in excluded:
reachable = location.can_reach(state) with self.subTest("Location should be reached", location=location.name):
self.assertTrue(reachable, f"{location.name} unreachable") reachable = location.can_reach(state)
self.assertTrue(reachable, f"{location.name} unreachable")
with self.subTest("Beatable"): with self.subTest("Beatable"):
self.multiworld.state = state self.multiworld.state = state
self.assertBeatable(True) self.assertBeatable(True)

View File

@@ -192,7 +192,7 @@ class TestFillRestrictive(unittest.TestCase):
location_pool = player1.locations[1:] + player2.locations location_pool = player1.locations[1:] + player2.locations
item_pool = player1.prog_items[:-1] + player2.prog_items item_pool = player1.prog_items[:-1] + player2.prog_items
fill_restrictive(multiworld, multiworld.state, location_pool, item_pool) fill_restrictive(multiworld, multiworld.state, location_pool, item_pool)
multiworld.state.sweep_for_advancements() # collect everything multiworld.state.sweep_for_events() # collect everything
# all of player2's locations and items should be accessible (not all of player1's) # all of player2's locations and items should be accessible (not all of player1's)
for item in player2.prog_items: for item in player2.prog_items:
@@ -443,8 +443,8 @@ class TestFillRestrictive(unittest.TestCase):
item = player1.prog_items[0] item = player1.prog_items[0]
item.code = None item.code = None
location.place_locked_item(item) location.place_locked_item(item)
multiworld.state.sweep_for_advancements() multiworld.state.sweep_for_events()
multiworld.state.sweep_for_advancements() multiworld.state.sweep_for_events()
self.assertTrue(multiworld.state.prog_items[item.player][item.name], "Sweep did not collect - Test flawed") 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") self.assertEqual(multiworld.state.prog_items[item.player][item.name], 1, "Sweep collected multiple times")

View File

@@ -14,18 +14,6 @@ class TestBase(unittest.TestCase):
"Desert Northern Cliffs", # on top of mountain, only reachable via OWG "Desert Northern Cliffs", # on top of mountain, only reachable via OWG
"Dark Death Mountain Bunny Descent Area" # OWG Mountain descent "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": { "Ocarina of Time": {
"Prelude of Light Warp", # Prelude is not progression by default "Prelude of Light Warp", # Prelude is not progression by default
"Serenade of Water Warp", # Serenade is not progression by default "Serenade of Water Warp", # Serenade is not progression by default
@@ -49,10 +37,12 @@ class TestBase(unittest.TestCase):
unreachable_regions = self.default_settings_unreachable_regions.get(game_name, set()) unreachable_regions = self.default_settings_unreachable_regions.get(game_name, set())
with self.subTest("Game", game=game_name): with self.subTest("Game", game=game_name):
multiworld = setup_solo_multiworld(world_type) multiworld = setup_solo_multiworld(world_type)
excluded = multiworld.worlds[1].options.exclude_locations.value
state = multiworld.get_all_state(False) state = multiworld.get_all_state(False)
for location in multiworld.get_locations(): for location in multiworld.get_locations():
with self.subTest("Location should be reached", location=location.name): if location.name not in excluded:
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(): for region in multiworld.get_regions():
if region.name in unreachable_regions: if region.name in unreachable_regions:

View File

@@ -55,7 +55,7 @@ class TestAllGamesMultiworld(MultiworldTestBase):
all_worlds = list(AutoWorldRegister.world_types.values()) all_worlds = list(AutoWorldRegister.world_types.values())
self.multiworld = setup_multiworld(all_worlds, ()) self.multiworld = setup_multiworld(all_worlds, ())
for world in self.multiworld.worlds.values(): for world in self.multiworld.worlds.values():
world.options.accessibility.value = Accessibility.option_full world.options.accessibility.value = Accessibility.option_locations
self.assertSteps(gen_steps) self.assertSteps(gen_steps)
with self.subTest("filling multiworld", seed=self.multiworld.seed): with self.subTest("filling multiworld", seed=self.multiworld.seed):
distribute_items_restrictive(self.multiworld) distribute_items_restrictive(self.multiworld)
@@ -66,8 +66,8 @@ class TestAllGamesMultiworld(MultiworldTestBase):
class TestTwoPlayerMulti(MultiworldTestBase): class TestTwoPlayerMulti(MultiworldTestBase):
def test_two_player_single_game_fills(self) -> None: def test_two_player_single_game_fills(self) -> None:
"""Tests that a multiworld of two players for each registered game world can generate.""" """Tests that a multiworld of two players for each registered game world can generate."""
for world_type in AutoWorldRegister.world_types.values(): for world in AutoWorldRegister.world_types.values():
self.multiworld = setup_multiworld([world_type, world_type], ()) self.multiworld = setup_multiworld([world, world], ())
for world in self.multiworld.worlds.values(): for world in self.multiworld.worlds.values():
world.options.accessibility.value = Accessibility.option_full world.options.accessibility.value = Accessibility.option_full
self.assertSteps(gen_steps) self.assertSteps(gen_steps)

View File

@@ -130,9 +130,9 @@ class Base:
def test_get_remaining(self) -> None: def test_get_remaining(self) -> None:
self.assertEqual(self.store.get_remaining(full_state, 0, 1), []) self.assertEqual(self.store.get_remaining(full_state, 0, 1), [])
self.assertEqual(self.store.get_remaining(one_state, 0, 1), [(1, 13), (2, 21)]) self.assertEqual(self.store.get_remaining(one_state, 0, 1), [13, 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, 1), [13, 21, 22])
self.assertEqual(self.store.get_remaining(empty_state, 0, 3), [(4, 99)]) self.assertEqual(self.store.get_remaining(empty_state, 0, 3), [99])
def test_location_set_intersection(self) -> None: def test_location_set_intersection(self) -> None:
locations = {10, 11, 12} locations = {10, 11, 12}

View File

@@ -292,14 +292,6 @@ class World(metaclass=AutoWorldRegister):
web: ClassVar[WebWorld] = WebWorld() web: ClassVar[WebWorld] = WebWorld()
"""see WebWorld for options""" """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" multiworld: "MultiWorld"
"""autoset on creation. The MultiWorld object for the currently generating multiworld.""" """autoset on creation. The MultiWorld object for the currently generating multiworld."""
player: int player: int

View File

@@ -26,13 +26,10 @@ class Component:
cli: bool cli: bool
func: Optional[Callable] func: Optional[Callable]
file_identifier: Optional[Callable[[str], bool]] 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, 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, 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.display_name = display_name
self.script_name = script_name self.script_name = script_name
self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None
@@ -48,8 +45,6 @@ class Component:
Type.ADJUSTER if "Adjuster" in display_name else Type.MISC) Type.ADJUSTER if "Adjuster" in display_name else Type.MISC)
self.func = func self.func = func
self.file_identifier = file_identifier self.file_identifier = file_identifier
self.game_name = game_name
self.supports_uri = supports_uri
def handles_file(self, path: str): def handles_file(self, path: str):
return self.file_identifier(path) if self.file_identifier else False return self.file_identifier(path) if self.file_identifier else False
@@ -61,10 +56,10 @@ class Component:
processes = weakref.WeakSet() processes = weakref.WeakSet()
def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()) -> None: def launch_subprocess(func: Callable, name: str = None):
global processes global processes
import multiprocessing import multiprocessing
process = multiprocessing.Process(target=func, name=name, args=args) process = multiprocessing.Process(target=func, name=name)
process.start() process.start()
processes.add(process) processes.add(process)
@@ -83,9 +78,9 @@ class SuffixIdentifier:
return False return False
def launch_textclient(*args): def launch_textclient():
import CommonClient import CommonClient
launch_subprocess(CommonClient.run_as_textclient, name="TextClient", args=args) launch_subprocess(CommonClient.run_as_textclient, name="TextClient")
def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]: def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]:
@@ -137,8 +132,7 @@ def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, path
break break
if found_already_loaded: if found_already_loaded:
raise Exception(f"Installed APWorld successfully, but '{module_name}' is already loaded,\n" raise Exception(f"Installed APWorld successfully, but '{module_name}' is already loaded,\n"
"so a Launcher restart is required to use the new installation.\n" "so a Launcher restart is required to use the new installation.")
"If the Launcher is not open, no action needs to be taken.")
world_source = worlds.WorldSource(str(target), is_zip=True) world_source = worlds.WorldSource(str(target), is_zip=True)
bisect.insort(worlds.world_sources, world_source) bisect.insort(worlds.world_sources, world_source)
world_source.load() world_source.load()

View File

@@ -59,10 +59,14 @@ class BizHawkClientContext(CommonContext):
self.bizhawk_ctx = BizHawkContext() self.bizhawk_ctx = BizHawkContext()
self.watcher_timeout = 0.5 self.watcher_timeout = 0.5
def make_gui(self): def run_gui(self):
ui = super().make_gui() from kvui import GameManager
ui.base_title = "Archipelago BizHawk Client"
return ui 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 on_package(self, cmd, args): def on_package(self, cmd, args):
if cmd == "Connected": if cmd == "Connected":

View File

@@ -968,35 +968,40 @@ def get_act_by_number(world: "HatInTimeWorld", chapter_name: str, num: int) -> R
def create_thug_shops(world: "HatInTimeWorld"): def create_thug_shops(world: "HatInTimeWorld"):
min_items: int = world.options.NyakuzaThugMinShopItems.value min_items: int = world.options.NyakuzaThugMinShopItems.value
max_items: int = world.options.NyakuzaThugMaxShopItems.value max_items: int = world.options.NyakuzaThugMaxShopItems.value
count = -1
thug_location_counts: Dict[str, int] = {} step = 0
old_name = ""
for key, data in shop_locations.items(): for key, data in shop_locations.items():
thug_name = data.nyakuza_thug if data.nyakuza_thug == "":
if thug_name == "":
# Different shop type.
continue continue
if thug_name not in world.nyakuza_thug_items: if old_name != "" and old_name == data.nyakuza_thug:
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 continue
location_count = thug_location_counts.setdefault(thug_name, 0) try:
if location_count >= shop_item_count: if world.nyakuza_thug_items[data.nyakuza_thug] <= 0:
# Already created all the locations for this thug. continue
continue except KeyError:
pass
# Create the shop location. if count == -1:
region = world.multiworld.get_region(data.region, world.player) count = world.random.randint(min_items, max_items)
loc = HatInTimeLocation(world.player, key, data.id, region) world.nyakuza_thug_items.setdefault(data.nyakuza_thug, count)
region.locations.append(loc) if count <= 0:
world.shop_locs.append(loc.name) continue
thug_location_counts[thug_name] = location_count + 1
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
def create_events(world: "HatInTimeWorld") -> int: def create_events(world: "HatInTimeWorld") -> int:

View File

@@ -381,8 +381,8 @@ def set_moderate_rules(world: "HatInTimeWorld"):
lambda state: can_use_hat(state, world, HatType.ICE), "or") lambda state: can_use_hat(state, world, HatType.ICE), "or")
# Moderate: Clock Tower Chest + Ruined Tower with nothing # Moderate: Clock Tower Chest + Ruined Tower with nothing
set_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True) add_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) add_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 # 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: 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(): if world.is_dlc1():
# Moderate: clear Rock the Boat without Ice Hat # Moderate: clear Rock the Boat without Ice Hat
set_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), lambda state: True) add_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) add_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), lambda state: True)
# Moderate: clear Deep Sea without Ice Hat # Moderate: clear Deep Sea without Ice Hat
set_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player), set_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player),
@@ -855,9 +855,6 @@ def set_rift_rules(world: "HatInTimeWorld", regions: Dict[str, Region]):
for entrance in regions["Time Rift - Alpine Skyline"].entrances: for entrance in regions["Time Rift - Alpine Skyline"].entrances:
add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon")) 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(): if world.is_dlc1():
for entrance in regions["Time Rift - Balcony"].entrances: for entrance in regions["Time Rift - Balcony"].entrances:
@@ -936,9 +933,6 @@ def set_default_rift_rules(world: "HatInTimeWorld"):
for entrance in world.multiworld.get_region("Time Rift - Alpine Skyline", world.player).entrances: 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")) 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(): if world.is_dlc1():
for entrance in world.multiworld.get_region("Time Rift - Balcony", world.player).entrances: for entrance in world.multiworld.get_region("Time Rift - Balcony", world.player).entrances:

View File

@@ -248,7 +248,7 @@ def fill_dungeons_restrictive(multiworld: MultiWorld):
pass pass
for item in pre_fill_items: for item in pre_fill_items:
multiworld.worlds[item.player].collect(all_state_base, item) multiworld.worlds[item.player].collect(all_state_base, item)
all_state_base.sweep_for_advancements() all_state_base.sweep_for_events()
# Remove completion condition so that minimal-accessibility worlds place keys properly # Remove completion condition so that minimal-accessibility worlds place keys properly
for player in {item.player for item in in_dungeon_items}: 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])) all_state_base.remove(item_factory(key_data[3], multiworld.worlds[player]))
loc = multiworld.get_location(key_loc, player) loc = multiworld.get_location(key_loc, player)
if loc in all_state_base.advancements: if loc in all_state_base.events:
all_state_base.advancements.remove(loc) all_state_base.events.remove(loc)
fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, lock=True, allow_excluded=True, fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, lock=True, allow_excluded=True,
name="LttP Dungeon Items") name="LttP Dungeon Items")

View File

@@ -682,7 +682,7 @@ def get_pool_core(world, player: int):
if 'triforce_hunt' in goal: if 'triforce_hunt' in goal:
if world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_extra: if world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_extra:
treasure_hunt_total = (world.triforce_pieces_required[player].value treasure_hunt_total = (world.triforce_pieces_available[player].value
+ world.triforce_pieces_extra[player].value) + world.triforce_pieces_extra[player].value)
elif world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_percentage: elif world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_percentage:
percentage = float(world.triforce_pieces_percentage[player].value) / 100 percentage = float(world.triforce_pieces_percentage[player].value) / 100

View File

@@ -728,7 +728,7 @@ class ALttPPlandoConnections(PlandoConnections):
entrances = set([connection[0] for connection in ( entrances = set([connection[0] for connection in (
*default_connections, *default_dungeon_connections, *inverted_default_connections, *default_connections, *default_dungeon_connections, *inverted_default_connections,
*inverted_default_dungeon_connections)]) *inverted_default_dungeon_connections)])
exits = set([connection[0] for connection in ( exits = set([connection[1] for connection in (
*default_connections, *default_dungeon_connections, *inverted_default_connections, *default_connections, *default_dungeon_connections, *inverted_default_connections,
*inverted_default_dungeon_connections)]) *inverted_default_dungeon_connections)])

View File

@@ -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)) 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), set_rule(multiworld.get_location('Thieves\' Town - Blind\'s Cell', player),
lambda state: state._lttp_has_key('Small Key (Thieves Town)', player)) lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
if multiworld.accessibility[player] != 'full' and not multiworld.key_drop_shuffle[player]: if multiworld.accessibility[player] != 'locations' 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_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 - 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), 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))) 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 # this seemed to be causing generation failure, disable for now
# if world.accessibility[player] != 'full': # if world.accessibility[player] != 'locations':
# 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)) # 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. # 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.

View File

@@ -356,8 +356,6 @@ class ALTTPWorld(World):
self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group] self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group]
if option == "original_dungeon": if option == "original_dungeon":
self.dungeon_specific_item_names |= self.item_name_groups[option.item_name_group] 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] self.difficulty_requirements = difficulties[multiworld.item_pool[player].current_key]

View File

@@ -2,8 +2,8 @@
## Configuration ## Configuration
1. All plando options are enabled by default, except for "items plando" which has to be enabled before it can be used (opt-in). 1. Plando features have to be enabled first, before they can be used (opt-in).
2. To enable it, go to your installation directory (Windows default: `C:\ProgramData\Archipelago`), then open the host.yaml 2. To do so, go to your installation directory (Windows default: `C:\ProgramData\Archipelago`), then open the host.yaml
file with a text editor. 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 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` to `bosses, items, texts, connections`
@@ -66,7 +66,6 @@ boss_shuffle:
- ignored if only one world is generated - ignored if only one world is generated
- can be a number, to target that slot in the multiworld - can be a number, to target that slot in the multiworld
- can be a name, to target that player's world - 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 true, to target any other player's world
- can be false, to target own world and is the default - can be false, to target own world and is the default
- can be null, to target a random world - can be null, to target a random world
@@ -133,15 +132,17 @@ plando_items:
### Texts ### Texts
- This module is disabled by default.
- Has the options `text`, `at`, and `percentage` - 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% - percentage is the percentage chance for this text to be placed, can be omitted entirely for 100%
- text is the text to be placed. - text is the text to be placed.
- can be weighted.
- `\n` is a newline. - `\n` is a newline.
- `@` is the entered player's name. - `@` is the entered player's name.
- Warning: Text Mapper does not support full unicode. - Warning: Text Mapper does not support full unicode.
- [Alphabet](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Text.py#L758) - [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. - 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) - [List of targets](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Text.py#L1499)
#### Example #### Example
@@ -161,6 +162,7 @@ and `uncle_dying_sewer`, then places the text "This is a plando. You've been war
### Connections ### Connections
- This module is disabled by default.
- Has the options `percentage`, `entrance`, `exit` and `direction`. - Has the options `percentage`, `entrance`, `exit` and `direction`.
- All options support subweights - All options support subweights
- percentage is the percentage chance for this to be connected, can be omitted entirely for 100% - percentage is the percentage chance for this to be connected, can be omitted entirely for 100%

View File

@@ -54,7 +54,7 @@ class TestDungeon(LTTPTestBase):
for item in items: for item in items:
item.classification = ItemClassification.progression item.classification = ItemClassification.progression
state.collect(item, prevent_sweep=True) # prevent_sweep=True prevents running sweep_for_advancements() and picking up state.collect(item, event=True) # event=True prevents running sweep_for_events() and picking up
state.sweep_for_advancements() # key drop keys repeatedly state.sweep_for_events() # 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}")

View File

@@ -1,60 +0,0 @@
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")

View File

@@ -4,7 +4,7 @@ from BaseClasses import Tutorial
from ..AutoWorld import WebWorld, World from ..AutoWorld import WebWorld, World
class AP_SudokuWebWorld(WebWorld): class AP_SudokuWebWorld(WebWorld):
options_page = False options_page = "games/Sudoku/info/en"
theme = 'partyTime' theme = 'partyTime'
setup_en = Tutorial( setup_en = Tutorial(

View File

@@ -1,7 +1,9 @@
# APSudoku Setup Guide # APSudoku Setup Guide
## Required Software ## Required Software
- [APSudoku](https://github.com/APSudoku/APSudoku) - [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.
## General Concept ## General Concept
@@ -11,33 +13,25 @@ Does not need to be added at the start of a seed, as it does not create any slot
## Installation Procedures ## Installation Procedures
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. Go to the latest release from the [APSudoku Releases page](https://github.com/EmilyV99/APSudoku/releases). Download and extract the `APSudoku.zip` file.
## Joining a MultiWorld Game ## Joining a MultiWorld Game
1. Run the APSudoku executable. 1. Run APSudoku.exe
2. Under `Settings` &rarr; `Connection` at the top-right: 2. Under the 'Archipelago' tab at the top-right:
- Enter the server address and port number - Enter the server url & port number
- Enter the name of the slot you wish to connect to - Enter the name of the slot you wish to connect to
- Enter the room password (optional) - Enter the room password (optional)
- Select DeathLink related settings (optional) - Select DeathLink related settings (optional)
- Press `Connect` - Press connect
4. Under the `Sudoku` tab 3. Go back to the 'Sudoku' tab
- Choose puzzle difficulty - Click the various '?' buttons for information on how to play / control
- Click `Start` to generate a puzzle 4. Choose puzzle difficulty
5. Try to solve the Sudoku. Click `Check` when done 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` &rarr; `Sudoku`, and can change the colors used under `Settings` &rarr; `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 ## DeathLink Support
If `DeathLink` is enabled when you click `Connect`: 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). - 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).
- Your life count is customizable (default 0). Dying with 0 lives left kills linked players AND resets your puzzle. - Life count 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. - On receiving a DeathLink from another player, your puzzle resets.

View File

@@ -1,19 +0,0 @@
{
"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]"
]
}

View File

@@ -637,35 +637,52 @@ item_table: List[ItemDict] = [
'classification': ItemClassification.filler} '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]] = { group_table: Dict[str, Set[str]] = {
"wounds" : {"Holy Wound of Attrition", "wounds" : ["Holy Wound of Attrition",
"Holy Wound of Contrition", "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", "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 Second Refuge",
"Mark of the Third Refuge"}, "Mark of the Third Refuge"],
"tirso" : {"Bouquet of Rosemary", "tirso" : ["Bouquet of Rosemary",
"Incense Garlic", "Incense Garlic",
"Olive Seeds", "Olive Seeds",
"Dried Clove", "Dried Clove",
"Sooty Garlic", "Sooty Garlic",
"Bouquet of Thyme"}, "Bouquet of Thyme"],
"tentudia": {"Tentudia's Carnal Remains", "tentudia": ["Tentudia's Carnal Remains",
"Remains of Tentudia's Hair", "Remains of Tentudia's Hair",
"Tentudia's Skeletal Remains"}, "Tentudia's Skeletal Remains"],
"egg" : {"Melted Golden Coins", "egg" : ["Melted Golden Coins",
"Torn Bridal Ribbon", "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", "Jaw of Ashgan, the Inquisitor",
"Cervical vertebra of Zicher, the Brewmaster", "Cervical vertebra of Zicher, the Brewmaster",
"Clavicle of Dalhuisen, the Schoolchild", "Clavicle of Dalhuisen, the Schoolchild",
@@ -708,14 +725,14 @@ group_table: Dict[str, Set[str]] = {
"Scaphoid of Fierce, the Leper", "Scaphoid of Fierce, the Leper",
"Anklebone of Weston, the Pilgrim", "Anklebone of Weston, the Pilgrim",
"Calcaneum of Persian, the Bandit", "Calcaneum of Persian, the Bandit",
"Navicular of Kahnnyhoo, the Murderer"}, "Navicular of Kahnnyhoo, the Murderer"],
"power" : {"Life Upgrade", "power" : ["Life Upgrade",
"Fervour Upgrade", "Fervour Upgrade",
"Empty Bile Vessel", "Empty Bile Vessel",
"Quicksilver"}, "Quicksilver"],
"prayer" : {"Seguiriya to your Eyes like Stars", "prayer" : ["Seguiriya to your Eyes like Stars",
"Debla of the Lights", "Debla of the Lights",
"Saeta Dolorosa", "Saeta Dolorosa",
"Campanillero to the Sons of the Aurora", "Campanillero to the Sons of the Aurora",
@@ -729,17 +746,10 @@ group_table: Dict[str, Set[str]] = {
"Romance to the Crimson Mist", "Romance to the Crimson Mist",
"Zambra to the Resplendent Crown", "Zambra to the Resplendent Crown",
"Cantina of the Blue Rose", "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_list: List[str] = [ tears_set: Set[str] = [
"Tears of Atonement (500)", "Tears of Atonement (500)",
"Tears of Atonement (625)", "Tears of Atonement (625)",
"Tears of Atonement (750)", "Tears of Atonement (750)",
@@ -762,16 +772,16 @@ tears_list: List[str] = [
"Tears of Atonement (30000)" "Tears of Atonement (30000)"
] ]
reliquary_set: Set[str] = { reliquary_set: Set[str] = [
"Reliquary of the Fervent Heart", "Reliquary of the Fervent Heart",
"Reliquary of the Suffering Heart", "Reliquary of the Suffering Heart",
"Reliquary of the Sorrowful Heart" "Reliquary of the Sorrowful Heart"
} ]
skill_set: Set[str] = { skill_set: Set[str] = [
"Combo Skill", "Combo Skill",
"Charged Skill", "Charged Skill",
"Ranged Skill", "Ranged Skill",
"Dive Skill", "Dive Skill",
"Lunge Skill" "Lunge Skill"
} ]

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,4 @@
from dataclasses import dataclass from Options import Choice, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions, OptionGroup
import random import random
@@ -21,30 +20,23 @@ class ChoiceIsRandom(Choice):
class PrieDieuWarp(DefaultOnToggle): 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" display_name = "Unlock Fast Travel"
class SkipCutscenes(DefaultOnToggle): class SkipCutscenes(DefaultOnToggle):
""" """Automatically skips most cutscenes."""
Automatically skips most cutscenes.
"""
display_name = "Auto Skip Cutscenes" display_name = "Auto Skip Cutscenes"
class CorpseHints(DefaultOnToggle): 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" display_name = "Corpse Hints"
class Difficulty(Choice): class Difficulty(Choice):
""" """Adjusts the overall difficulty of the randomizer, including upgrades required to defeat bosses
Adjusts the overall difficulty of the randomizer, including upgrades required to defeat bosses and advanced movement tricks or glitches. and advanced movement tricks or glitches."""
"""
display_name = "Difficulty" display_name = "Difficulty"
option_easy = 0 option_easy = 0
option_normal = 1 option_normal = 1
@@ -53,18 +45,15 @@ class Difficulty(Choice):
class Penitence(Toggle): 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" display_name = "Penitence"
class StartingLocation(ChoiceIsRandom): class StartingLocation(ChoiceIsRandom):
""" """Choose where to start the randomizer. Note that some starting locations cannot be chosen with certain
Choose where to start the randomizer. Note that some starting locations cannot be chosen with certain other options. other options.
Specifically, Brotherhood and Mourning And Havoc cannot be chosen if Shuffle Dash is enabled, and Grievance Ascends
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. cannot be chosen if Shuffle Wall Climb is enabled."""
"""
display_name = "Starting Location" display_name = "Starting Location"
option_brotherhood = 0 option_brotherhood = 0
option_albero = 1 option_albero = 1
@@ -77,15 +66,10 @@ class StartingLocation(ChoiceIsRandom):
class Ending(Choice): 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. Talking to Tirso in Albero will tell you the selected ending for the current game.
Ending A: Collect all thorn upgrades. 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" display_name = "Ending"
option_any_ending = 0 option_any_ending = 0
option_ending_a = 1 option_ending_a = 1
@@ -94,18 +78,14 @@ class Ending(Choice):
class SkipLongQuests(Toggle): class SkipLongQuests(Toggle):
""" """Ensures that the rewards for long quests will be filler items.
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\""""
Affected locations: "Albero: Donate 50000 Tears", "Ossuary: 11th reward", "AtTotS: Miriam's gift", "TSC: Jocinero's final reward"
"""
display_name = "Skip Long Quests" display_name = "Skip Long Quests"
class ThornShuffle(Choice): 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" display_name = "Shuffle Thorn"
option_anywhere = 0 option_anywhere = 0
option_local_only = 1 option_local_only = 1
@@ -114,68 +94,50 @@ class ThornShuffle(Choice):
class DashShuffle(Toggle): 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" display_name = "Shuffle Dash"
class WallClimbShuffle(Toggle): 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" display_name = "Shuffle Wall Climb"
class ReliquaryShuffle(DefaultOnToggle): 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" display_name = "Shuffle Penitence Rewards"
class CustomItem1(Toggle): class CustomItem1(Toggle):
""" """Adds the custom relic Boots of Pleading into the item pool, which grants the ability to fall onto spikes
Adds the custom relic Boots of Pleading into the item pool, which grants the ability to fall onto spikes and survive. and survive.
Must have the \"Blasphemous-Boots-of-Pleading\" mod installed to connect to a multiworld."""
Must have the "Boots of Pleading" mod installed to connect to a multiworld.
"""
display_name = "Boots of Pleading" display_name = "Boots of Pleading"
class CustomItem2(Toggle): class CustomItem2(Toggle):
""" """Adds the custom relic Purified Hand of the Nun into the item pool, which grants the ability to jump
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. a second time in mid-air.
Must have the \"Blasphemous-Double-Jump\" mod installed to connect to a multiworld."""
Must have the "Double Jump" mod installed to connect to a multiworld.
"""
display_name = "Purified Hand of the Nun" display_name = "Purified Hand of the Nun"
class StartWheel(Toggle): 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" display_name = "Start with Wheel"
class SkillRando(Toggle): 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" display_name = "Skill Randomizer"
class EnemyRando(Choice): class EnemyRando(Choice):
""" """Randomizes the enemies that appear in each room.
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.
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. 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" display_name = "Enemy Randomizer"
option_disabled = 0 option_disabled = 0
option_shuffled = 1 option_shuffled = 1
@@ -184,75 +146,43 @@ class EnemyRando(Choice):
class EnemyGroups(DefaultOnToggle): 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) (Weak, normal, large, flying)
Has no effect if Enemy Randomizer is disabled."""
Has no effect if Enemy Randomizer is disabled.
"""
display_name = "Enemy Groups" display_name = "Enemy Groups"
class EnemyScaling(DefaultOnToggle): class EnemyScaling(DefaultOnToggle):
""" """Randomized enemies will have their stats increased or decreased depending on the area they appear in.
Randomized enemies will have their stats increased or decreased depending on the area they appear in. Has no effect if Enemy Randomizer is disabled."""
Has no effect if Enemy Randomizer is disabled.
"""
display_name = "Enemy Scaling" display_name = "Enemy Scaling"
class BlasphemousDeathLink(DeathLink): class BlasphemousDeathLink(DeathLink):
""" """When you die, everyone dies. The reverse is also true.
When you die, everyone dies. The reverse is also true. Note that Guilt Fragments will not appear when killed by Death Link."""
Note that Guilt Fragments will not appear when killed by Death Link.
"""
@dataclass blasphemous_options = {
class BlasphemousOptions(PerGameCommonOptions): "prie_dieu_warp": PrieDieuWarp,
prie_dieu_warp: PrieDieuWarp "skip_cutscenes": SkipCutscenes,
skip_cutscenes: SkipCutscenes "corpse_hints": CorpseHints,
corpse_hints: CorpseHints "difficulty": Difficulty,
difficulty: Difficulty "penitence": Penitence,
penitence: Penitence "starting_location": StartingLocation,
starting_location: StartingLocation "ending": Ending,
ending: Ending "skip_long_quests": SkipLongQuests,
skip_long_quests: SkipLongQuests "thorn_shuffle" : ThornShuffle,
thorn_shuffle: ThornShuffle "dash_shuffle": DashShuffle,
dash_shuffle: DashShuffle "wall_climb_shuffle": WallClimbShuffle,
wall_climb_shuffle: WallClimbShuffle "reliquary_shuffle": ReliquaryShuffle,
reliquary_shuffle: ReliquaryShuffle "boots_of_pleading": CustomItem1,
boots_of_pleading: CustomItem1 "purified_hand": CustomItem2,
purified_hand: CustomItem2 "start_wheel": StartWheel,
start_wheel: StartWheel "skill_randomizer": SkillRando,
skill_randomizer: SkillRando "enemy_randomizer": EnemyRando,
enemy_randomizer: EnemyRando "enemy_groups": EnemyGroups,
enemy_groups: EnemyGroups "enemy_scaling": EnemyScaling,
enemy_scaling: EnemyScaling "death_link": BlasphemousDeathLink,
death_link: BlasphemousDeathLink "start_inventory": StartInventoryPool
}
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
])
]

View File

@@ -1,582 +0,0 @@
# 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())

5405
worlds/blasphemous/Rooms.py Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -8,12 +8,12 @@ unrandomized_dict: Dict[str, str] = {
} }
junk_locations: Set[str] = { junk_locations: Set[str] = [
"Albero: Donate 50000 Tears", "Albero: Donate 50000 Tears",
"Ossuary: 11th reward", "Ossuary: 11th reward",
"AtTotS: Miriam's gift", "AtTotS: Miriam's gift",
"TSC: Jocinero's final reward" "TSC: Jocinero's final reward"
} ]
thorn_set: Set[str] = { thorn_set: Set[str] = {
@@ -44,4 +44,4 @@ skill_dict: Dict[str, str] = {
"Skill 5, Tier 1": "Lunge Skill", "Skill 5, Tier 1": "Lunge Skill",
"Skill 5, Tier 2": "Lunge Skill", "Skill 5, Tier 2": "Lunge Skill",
"Skill 5, Tier 3": "Lunge Skill", "Skill 5, Tier 3": "Lunge Skill",
} }

View File

@@ -1,15 +1,15 @@
from typing import Dict, List, Set, Any from typing import Dict, List, Set, Any
from collections import Counter from collections import Counter
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification
from Options import OptionError
from worlds.AutoWorld import World, WebWorld from worlds.AutoWorld import World, WebWorld
from .Items import base_id, item_table, group_table, tears_list, reliquary_set from .Items import base_id, item_table, group_table, tears_set, reliquary_set, event_table
from .Locations import location_names from .Locations import location_table
from .Rules import BlasRules from .Rooms import room_table, door_table
from worlds.generic.Rules import set_rule from .Rules import rules
from .Options import BlasphemousOptions, blas_option_groups from worlds.generic.Rules import set_rule, add_rule
from .Options import blasphemous_options
from .Vanilla import unrandomized_dict, junk_locations, thorn_set, skill_dict from .Vanilla import unrandomized_dict, junk_locations, thorn_set, skill_dict
from .region_data import regions, locations
class BlasphemousWeb(WebWorld): class BlasphemousWeb(WebWorld):
theme = "stone" theme = "stone"
@@ -21,33 +21,39 @@ class BlasphemousWeb(WebWorld):
"setup/en", "setup/en",
["TRPG"] ["TRPG"]
)] )]
option_groups = blas_option_groups
class BlasphemousWorld(World): class BlasphemousWorld(World):
""" """
Blasphemous is a challenging Metroidvania set in the cursed land of Cvstodia. Play as the Penitent One, trapped 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 its terrible fate in your quest to break in an endless cycle of death and rebirth, and free the world from it's terrible fate in your quest to break
your eternal damnation! your eternal damnation!
""" """
game = "Blasphemous" game: str = "Blasphemous"
web = BlasphemousWeb() web = BlasphemousWeb()
item_name_to_id = {item["name"]: (base_id + index) for index, item in enumerate(item_table)} item_name_to_id = {item["name"]: (base_id + index) for index, item in enumerate(item_table)}
location_name_to_id = {loc: (base_id + index) for index, loc in enumerate(location_names.values())} 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}
item_name_groups = group_table item_name_groups = group_table
options_dataclass = BlasphemousOptions option_definitions = blasphemous_options
options: BlasphemousOptions
required_client_version = (0, 4, 7) required_client_version = (0, 4, 2)
def __init__(self, multiworld, player): def __init__(self, multiworld, player):
super(BlasphemousWorld, self).__init__(multiworld, player) super(BlasphemousWorld, self).__init__(multiworld, player)
self.start_room: str = "D17Z01S01" self.start_room: str = "D17Z01S01"
self.disabled_locations: List[str] = [] 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)
def create_item(self, name: str) -> "BlasphemousItem": def create_item(self, name: str) -> "BlasphemousItem":
@@ -62,56 +68,64 @@ class BlasphemousWorld(World):
def get_filler_item_name(self) -> str: def get_filler_item_name(self) -> str:
return self.random.choice(tears_list) return self.multiworld.random.choice(tears_set)
def generate_early(self): def generate_early(self):
if not self.options.starting_location.randomized: world = self.multiworld
if self.options.starting_location == "mourning_havoc" and self.options.difficulty < 2: player = self.player
raise OptionError(f"[Blasphemous - '{self.player_name}'] "
f"{self.options.starting_location} cannot be chosen if Difficulty is lower than Hard.")
if (self.options.starting_location == "brotherhood" or self.options.starting_location == "mourning_havoc") \ if not world.starting_location[player].randomized:
and self.options.dash_shuffle: if world.starting_location[player].value == 6 and world.difficulty[player].value < 2:
raise OptionError(f"[Blasphemous - '{self.player_name}'] " raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
f"{self.options.starting_location} cannot be chosen if Shuffle Dash is enabled.") " 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 == "grievance" and self.options.wall_climb_shuffle: if world.starting_location[player].value == 3 and world.wall_climb_shuffle[player]:
raise OptionError(f"[Blasphemous - '{self.player_name}'] " raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
f"{self.options.starting_location} cannot be chosen if Shuffle Wall Climb is enabled.") " cannot be chosen if Shuffle Wall Climb is enabled.")
else: else:
locations: List[int] = [ 0, 1, 2, 3, 4, 5, 6 ] locations: List[int] = [ 0, 1, 2, 3, 4, 5, 6 ]
invalid: bool = False
if self.options.difficulty < 2: if world.difficulty[player].value < 2:
locations.remove(6) locations.remove(6)
if self.options.dash_shuffle: if world.dash_shuffle[player]:
locations.remove(0) locations.remove(0)
if 6 in locations: if 6 in locations:
locations.remove(6) locations.remove(6)
if self.options.wall_climb_shuffle: if world.wall_climb_shuffle[player]:
locations.remove(3) locations.remove(3)
if self.options.starting_location.value not in locations: if world.starting_location[player].value == 6 and world.difficulty[player].value < 2:
self.options.starting_location.value = self.random.choice(locations) 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 not self.options.dash_shuffle: if not world.dash_shuffle[player]:
self.multiworld.push_precollected(self.create_item("Dash Ability")) world.push_precollected(self.create_item("Dash Ability"))
if not self.options.wall_climb_shuffle: if not world.wall_climb_shuffle[player]:
self.multiworld.push_precollected(self.create_item("Wall Climb Ability")) world.push_precollected(self.create_item("Wall Climb Ability"))
if not self.options.boots_of_pleading: if world.skip_long_quests[player]:
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: for loc in junk_locations:
self.options.exclude_locations.value.add(loc) world.exclude_locations[player].value.add(loc)
start_rooms: Dict[int, str] = { start_rooms: Dict[int, str] = {
0: "D17Z01S01", 0: "D17Z01S01",
@@ -123,10 +137,13 @@ class BlasphemousWorld(World):
6: "D20Z02S09" 6: "D20Z02S09"
} }
self.start_room = start_rooms[self.options.starting_location.value] self.start_room = start_rooms[world.starting_location[player].value]
def create_items(self): def create_items(self):
world = self.multiworld
player = self.player
removed: int = 0 removed: int = 0
to_remove: List[str] = [ to_remove: List[str] = [
"Tears of Atonement (250)", "Tears of Atonement (250)",
@@ -139,46 +156,46 @@ class BlasphemousWorld(World):
skipped_items = [] skipped_items = []
junk: int = 0 junk: int = 0
for item, count in self.options.start_inventory.value.items(): for item, count in world.start_inventory[player].value.items():
for _ in range(count): for _ in range(count):
skipped_items.append(item) skipped_items.append(item)
junk += 1 junk += 1
skipped_items.extend(unrandomized_dict.values()) skipped_items.extend(unrandomized_dict.values())
if self.options.thorn_shuffle == "vanilla": if world.thorn_shuffle[player] == 2:
for _ in range(8): for i in range(8):
skipped_items.append("Thorn Upgrade") skipped_items.append("Thorn Upgrade")
if self.options.dash_shuffle: if world.dash_shuffle[player]:
skipped_items.append(to_remove[removed]) skipped_items.append(to_remove[removed])
removed += 1 removed += 1
elif not self.options.dash_shuffle: elif not world.dash_shuffle[player]:
skipped_items.append("Dash Ability") skipped_items.append("Dash Ability")
if self.options.wall_climb_shuffle: if world.wall_climb_shuffle[player]:
skipped_items.append(to_remove[removed]) skipped_items.append(to_remove[removed])
removed += 1 removed += 1
elif not self.options.wall_climb_shuffle: elif not world.wall_climb_shuffle[player]:
skipped_items.append("Wall Climb Ability") skipped_items.append("Wall Climb Ability")
if not self.options.reliquary_shuffle: if not world.reliquary_shuffle[player]:
skipped_items.extend(reliquary_set) skipped_items.extend(reliquary_set)
elif self.options.reliquary_shuffle: elif world.reliquary_shuffle[player]:
for _ in range(3): for i in range(3):
skipped_items.append(to_remove[removed]) skipped_items.append(to_remove[removed])
removed += 1 removed += 1
if not self.options.boots_of_pleading: if not world.boots_of_pleading[player]:
skipped_items.append("Boots of Pleading") skipped_items.append("Boots of Pleading")
if not self.options.purified_hand: if not world.purified_hand[player]:
skipped_items.append("Purified Hand of the Nun") skipped_items.append("Purified Hand of the Nun")
if self.options.start_wheel: if world.start_wheel[player]:
skipped_items.append("The Young Mason's Wheel") skipped_items.append("The Young Mason's Wheel")
if not self.options.skill_randomizer: if not world.skill_randomizer[player]:
skipped_items.extend(skill_dict.values()) skipped_items.extend(skill_dict.values())
counter = Counter(skipped_items) counter = Counter(skipped_items)
@@ -191,140 +208,184 @@ class BlasphemousWorld(World):
if count <= 0: if count <= 0:
continue continue
else: else:
for _ in range(count): for i in range(count):
pool.append(self.create_item(item["name"])) pool.append(self.create_item(item["name"]))
for _ in range(junk): for _ in range(junk):
pool.append(self.create_item(self.get_filler_item_name())) pool.append(self.create_item(self.get_filler_item_name()))
self.multiworld.itempool += pool world.itempool += pool
def pre_fill(self): def pre_fill(self):
world = self.multiworld
player = self.player
self.place_items_from_dict(unrandomized_dict) self.place_items_from_dict(unrandomized_dict)
if self.options.thorn_shuffle == "vanilla": if world.thorn_shuffle[player] == 2:
self.place_items_from_set(thorn_set, "Thorn Upgrade") self.place_items_from_set(thorn_set, "Thorn Upgrade")
if self.options.start_wheel: if world.start_wheel[player]:
self.get_location("Beginning gift").place_locked_item(self.create_item("The Young Mason's Wheel")) world.get_location("Beginning gift", player)\
.place_locked_item(self.create_item("The Young Mason's Wheel"))
if not self.options.skill_randomizer: if not world.skill_randomizer[player]:
self.place_items_from_dict(skill_dict) self.place_items_from_dict(skill_dict)
if self.options.thorn_shuffle == "local_only": if world.thorn_shuffle[player] == 1:
self.options.local_items.value.add("Thorn Upgrade") world.local_items[player].value.add("Thorn Upgrade")
def place_items_from_set(self, location_set: Set[str], name: str): def place_items_from_set(self, location_set: Set[str], name: str):
for loc in location_set: for loc in location_set:
self.get_location(loc).place_locked_item(self.create_item(name)) self.multiworld.get_location(loc, self.player)\
.place_locked_item(self.create_item(name))
def place_items_from_dict(self, option_dict: Dict[str, str]): def place_items_from_dict(self, option_dict: Dict[str, str]):
for loc, item in option_dict.items(): for loc, item in option_dict.items():
self.get_location(loc).place_locked_item(self.create_item(item)) self.multiworld.get_location(loc, self.player)\
.place_locked_item(self.create_item(item))
def create_regions(self) -> None: def create_regions(self) -> None:
multiworld = self.multiworld
player = self.player 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")
victory = Location(player, "His Holiness Escribar", None, self.get_region("D07Z01S03[W]")) menu_region = Region("Menu", player, world)
victory.place_locked_item(self.create_event("Victory")) misc_region = Region("Misc", player, world)
self.get_region("D07Z01S03[W]").locations.append(victory) world.regions += [menu_region, misc_region]
if self.options.ending == "ending_a": 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.place_locked_item(self.create_event("Victory"))
world.get_region("D07Z01S03", player).locations.append(victory)
if world.ending[self.player].value == 1:
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8)) set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8))
elif self.options.ending == "ending_c": elif world.ending[self.player].value == 2:
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8) and set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8) and
state.has("Holy Wound of Abnegation", player)) state.has("Holy Wound of Abnegation", player))
multiworld.completion_condition[self.player] = lambda state: state.has("Victory", 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)
def fill_slot_data(self) -> Dict[str, Any]: def fill_slot_data(self) -> Dict[str, Any]:
slot_data: Dict[str, Any] = {} slot_data: Dict[str, Any] = {}
locations = []
doors: Dict[str, str] = {} doors: Dict[str, str] = {}
world = self.multiworld
player = self.player
thorns: bool = True thorns: bool = True
if self.options.thorn_shuffle == "vanilla": if world.thorn_shuffle[player].value == 2:
thorns = False 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 = { config = {
"LogicDifficulty": self.options.difficulty.value, "LogicDifficulty": world.difficulty[player].value,
"StartingLocation": self.options.starting_location.value, "StartingLocation": world.starting_location[player].value,
"VersionCreated": "AP", "VersionCreated": "AP",
"UnlockTeleportation": bool(self.options.prie_dieu_warp.value), "UnlockTeleportation": bool(world.prie_dieu_warp[player].value),
"AllowHints": bool(self.options.corpse_hints.value), "AllowHints": bool(world.corpse_hints[player].value),
"AllowPenitence": bool(self.options.penitence.value), "AllowPenitence": bool(world.penitence[player].value),
"ShuffleReliquaries": bool(self.options.reliquary_shuffle.value), "ShuffleReliquaries": bool(world.reliquary_shuffle[player].value),
"ShuffleBootsOfPleading": bool(self.options.boots_of_pleading.value), "ShuffleBootsOfPleading": bool(world.boots_of_pleading[player].value),
"ShufflePurifiedHand": bool(self.options.purified_hand.value), "ShufflePurifiedHand": bool(world.purified_hand[player].value),
"ShuffleDash": bool(self.options.dash_shuffle.value), "ShuffleDash": bool(world.dash_shuffle[player].value),
"ShuffleWallClimb": bool(self.options.wall_climb_shuffle.value), "ShuffleWallClimb": bool(world.wall_climb_shuffle[player].value),
"ShuffleSwordSkills": bool(self.options.wall_climb_shuffle.value), "ShuffleSwordSkills": bool(world.skill_randomizer[player].value),
"ShuffleThorns": thorns, "ShuffleThorns": thorns,
"JunkLongQuests": bool(self.options.skip_long_quests.value), "JunkLongQuests": bool(world.skip_long_quests[player].value),
"StartWithWheel": bool(self.options.start_wheel.value), "StartWithWheel": bool(world.start_wheel[player].value),
"EnemyShuffleType": self.options.enemy_randomizer.value, "EnemyShuffleType": world.enemy_randomizer[player].value,
"MaintainClass": bool(self.options.enemy_groups.value), "MaintainClass": bool(world.enemy_groups[player].value),
"AreaScaling": bool(self.options.enemy_scaling.value), "AreaScaling": bool(world.enemy_scaling[player].value),
"BossShuffleType": 0, "BossShuffleType": 0,
"DoorShuffleType": 0 "DoorShuffleType": 0
} }
slot_data = { slot_data = {
"locationinfo": [{"gameId": loc, "apId": (base_id + index)} for index, loc in enumerate(location_names)], "locations": locations,
"doors": doors, "doors": doors,
"cfg": config, "cfg": config,
"ending": self.options.ending.value, "ending": world.ending[self.player].value,
"death_link": bool(self.options.death_link.value) "death_link": bool(world.death_link[self.player].value)
} }
return slot_data return slot_data

View File

@@ -1,17 +1,48 @@
# Blasphemous Multiworld Setup Guide # Blasphemous Multiworld Setup Guide
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. ## Useful Links
You will need the [Multiworld](https://github.com/BrandenEK/Blasphemous.Randomizer.Multiworld) mod to play an Archipelago randomizer. 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)
Some optional mods are also recommended: Optional:
- [Rando Map](https://github.com/BrandenEK/Blasphemous.Randomizer.MapTracker) - In-game map tracker: [GitHub](https://github.com/BrandenEK/Blasphemous-Rando-Map)
- [Boots of Pleading](https://github.com/BrandenEK/Blasphemous.BootsOfPleading) (Required if the "Boots of Pleading" option is enabled) - Quick Prie Dieu warp mod: [GitHub](https://github.com/BadMagic100/Blasphemous-PrieWarp)
- [Double Jump](https://github.com/BrandenEK/Blasphemous.DoubleJump) (Required if the "Purified Hand of the Nun" option is enabled) - Boots of Pleading mod: [GitHub](https://github.com/BrandenEK/Blasphemous-Boots-of-Pleading)
- Double Jump mod: [GitHub](https://github.com/BrandenEK/Blasphemous-Double-Jump)
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. ## Mod Installer (Recommended)
After connecting, there are some commands you can use in the console, which can be opened by pressing backslash `\`: 1. Download the [Mod Installer](https://github.com/BrandenEK/Blasphemous-Mod-Installer),
- `ap status` - Display connection status. and point it to your install directory for Blasphemous.
- `ap say [message]` - Send a message to the server.
- `ap hint [item]` - Request a hint for an item from the server. 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.**

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +0,0 @@
from test.bases import WorldTestBase
from .. import BlasphemousWorld
class BlasphemousTestBase(WorldTestBase):
game = "Blasphemous"
world: BlasphemousWorld

View File

@@ -1,56 +0,0 @@
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)

View File

@@ -1,135 +0,0 @@
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"
}

View File

@@ -28,7 +28,7 @@ An Example `AP.json` file:
``` ```
{ {
"Url": "archipelago.gg:12345", "Url": "archipelago:12345",
"SlotName": "Maddy", "SlotName": "Maddy",
"Password": "" "Password": ""
} }

View File

@@ -44,15 +44,15 @@ class ChecksFinderWorld(World):
self.multiworld.regions += [menu, board] self.multiworld.regions += [menu, board]
def create_items(self): def create_items(self):
# Generate list of items # Generate item pool
items_to_create = [] itempool = []
# Add the map width and height stuff # Add the map width and height stuff
items_to_create += ["Map Width"] * 5 # 10 - 5 itempool += ["Map Width"] * 5 # 10 - 5
items_to_create += ["Map Height"] * 5 # 10 - 5 itempool += ["Map Height"] * 5 # 10 - 5
# Add the map bombs # Add the map bombs
items_to_create += ["Map Bombs"] * 15 # 20 - 5 itempool += ["Map Bombs"] * 15 # 20 - 5
# Convert list into real items # Convert itempool into real items
itempool = [self.create_item(item) for item in items_to_create] itempool = [self.create_item(item) for item in itempool]
self.multiworld.itempool += itempool self.multiworld.itempool += itempool

View File

@@ -238,6 +238,15 @@ class DS3ItemData:
ds3_code = cast(int, self.ds3_code) + level, ds3_code = cast(int, self.ds3_code) + level,
filler = False, 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): class DarkSouls3Item(Item):

View File

@@ -1252,9 +1252,6 @@ class DarkSouls3World(World):
lambda item: not item.advancement 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": if self.options.excluded_location_behavior == "allow_useful":
self.options.exclude_locations.value.clear() self.options.exclude_locations.value.clear()
@@ -1295,10 +1292,10 @@ class DarkSouls3World(World):
locations = location if isinstance(location, list) else [location] locations = location if isinstance(location, list) else [location]
for location in locations: for location in locations:
data = location_dictionary[location] data = location_dictionary[location]
if data.dlc and not self.options.enable_dlc: continue if data.dlc and not self.options.enable_dlc: return
if data.ngp and not self.options.enable_ngp: continue if data.ngp and not self.options.enable_ngp: return
if not self._is_location_available(location): continue if not self._is_location_available(location): return
if isinstance(rule, str): if isinstance(rule, str):
assert item_dictionary[rule].classification == ItemClassification.progression assert item_dictionary[rule].classification == ItemClassification.progression
rule = lambda state, item=rule: state.has(item, self.player) rule = lambda state, item=rule: state.has(item, self.player)
@@ -1507,19 +1504,16 @@ class DarkSouls3World(World):
# We include all the items the game knows about so that users can manually request items # 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 # 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. # practice `item_dictionary.values()` doesn't include upgraded or infused weapons.
items_by_name = { all_items = {
location.item.name: cast(DarkSouls3Item, location.item).data cast(DarkSouls3Item, location.item).data
for location in self.multiworld.get_filled_locations() for location in self.multiworld.get_filled_locations()
# item.code None is used for events, which we want to skip # 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 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] = {} ap_ids_to_ds3_ids: Dict[str, int] = {}
item_counts: Dict[str, int] = {} item_counts: Dict[str, int] = {}
for item in items_by_name.values(): for item in all_items:
if item.ap_code is None: continue 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.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 if item.count != 1: item_counts[str(item.ap_code)] = item.count

View File

@@ -101,7 +101,6 @@ class Factorio(World):
tech_tree_layout_prerequisites: typing.Dict[FactorioScienceLocation, typing.Set[FactorioScienceLocation]] tech_tree_layout_prerequisites: typing.Dict[FactorioScienceLocation, typing.Set[FactorioScienceLocation]]
tech_mix: int = 0 tech_mix: int = 0
skip_silo: bool = False skip_silo: bool = False
origin_region_name = "Nauvis"
science_locations: typing.List[FactorioScienceLocation] science_locations: typing.List[FactorioScienceLocation]
settings: typing.ClassVar[FactorioSettings] settings: typing.ClassVar[FactorioSettings]
@@ -126,6 +125,9 @@ class Factorio(World):
def create_regions(self): def create_regions(self):
player = self.player player = self.player
random = self.multiworld.random 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) nauvis = Region("Nauvis", player, self.multiworld)
location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \ location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \
@@ -182,7 +184,8 @@ class Factorio(World):
event = FactorioItem(f"Automated {ingredient}", ItemClassification.progression, None, player) event = FactorioItem(f"Automated {ingredient}", ItemClassification.progression, None, player)
location.place_locked_item(event) location.place_locked_item(event)
self.multiworld.regions.append(nauvis) crash.connect(nauvis)
self.multiworld.regions += [menu, nauvis]
def create_items(self) -> None: def create_items(self) -> None:
player = self.player player = self.player

View File

@@ -131,8 +131,8 @@ guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en)
the location without using any hint points. the location without using any hint points.
* `start_location_hints` is the same as `start_hints` but for locations, allowing you to hint for the item contained * `start_location_hints` is the same as `start_hints` but for locations, allowing you to hint for the item contained
there without using any hint points. there without using any hint points.
* `exclude_locations` lets you define any locations that you don't want to do and prevents items classified as * `exclude_locations` lets you define any locations that you don't want to do and forces a filler or trap item which
"progression" or "useful" from being placed on them. isn't necessary for progression into these locations.
* `priority_locations` lets you define any locations that you want to do and forces a progression item into these * `priority_locations` lets you define any locations that you want to do and forces a progression item into these
locations. locations.
* `item_links` allows players to link their items into a group with the same item link name and game. The items declared * `item_links` allows players to link their items into a group with the same item link name and game. The items declared

View File

@@ -601,11 +601,11 @@ class HKWorld(World):
if change: if change:
for effect_name, effect_value in item_effects.get(item.name, {}).items(): for effect_name, effect_value in item_effects.get(item.name, {}).items():
state.prog_items[item.player][effect_name] += effect_value state.prog_items[item.player][effect_name] += effect_value
if item.name in {"Left_Mothwing_Cloak", "Right_Mothwing_Cloak"}: if item.name in {"Left_Mothwing_Cloak", "Right_Mothwing_Cloak"}:
if state.prog_items[item.player].get('RIGHTDASH', 0) and \ if state.prog_items[item.player].get('RIGHTDASH', 0) and \
state.prog_items[item.player].get('LEFTDASH', 0): state.prog_items[item.player].get('LEFTDASH', 0):
(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"]) = \ (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) ([max(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"])] * 2)
return change return change
def remove(self, state, item: HKItem) -> bool: def remove(self, state, item: HKItem) -> bool:

View File

@@ -1,9 +1,5 @@
import struct 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 = { kirby_flavor_presets = {
1: { 1: {
@@ -227,23 +223,6 @@ kirby_flavor_presets = {
"14": "E6E6FA", "14": "E6E6FA",
"15": "976FBD", "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 = { gooey_flavor_presets = {
@@ -419,21 +398,21 @@ gooey_target_palettes = {
} }
def get_kirby_palette(world: "KDL3World") -> Optional[Dict[str, str]]: def get_kirby_palette(world):
palette = world.options.kirby_flavor_preset.value palette = world.options.kirby_flavor_preset.value
if palette == KirbyFlavorPreset.option_custom: if palette == KirbyFlavorPreset.option_custom:
return world.options.kirby_flavor.value return world.options.kirby_flavor.value
return kirby_flavor_presets.get(palette, None) return kirby_flavor_presets.get(palette, None)
def get_gooey_palette(world: "KDL3World") -> Optional[Dict[str, str]]: def get_gooey_palette(world):
palette = world.options.gooey_flavor_preset.value palette = world.options.gooey_flavor_preset.value
if palette == GooeyFlavorPreset.option_custom: if palette == GooeyFlavorPreset.option_custom:
return world.options.gooey_flavor.value return world.options.gooey_flavor.value
return gooey_flavor_presets.get(palette, None) return gooey_flavor_presets.get(palette, None)
def rgb888_to_bgr555(red: int, green: int, blue: int) -> bytes: def rgb888_to_bgr555(red, green, blue) -> bytes:
red = red >> 3 red = red >> 3
green = green >> 3 green = green >> 3
blue = blue >> 3 blue = blue >> 3
@@ -441,15 +420,15 @@ def rgb888_to_bgr555(red: int, green: int, blue: int) -> bytes:
return struct.pack("H", outcol) return struct.pack("H", outcol)
def get_palette_bytes(palette: Dict[str, str], target: List[str], offset: int, factor: float) -> bytes: def get_palette_bytes(palette, target, offset, factor):
output_data = bytearray() output_data = bytearray()
for color in target: for color in target:
hexcol = palette[color] hexcol = palette[color]
if hexcol.startswith("#"): if hexcol.startswith("#"):
hexcol = hexcol.replace("#", "") hexcol = hexcol.replace("#", "")
colint = int(hexcol, 16) colint = int(hexcol, 16)
col: Tuple[int, ...] = ((colint & 0xFF0000) >> 16, (colint & 0xFF00) >> 8, colint & 0xFF) col = ((colint & 0xFF0000) >> 16, (colint & 0xFF00) >> 8, colint & 0xFF)
col = tuple(int(int(factor*x) + offset) for x in col) col = tuple(int(int(factor*x) + offset) for x in col)
byte_data = rgb888_to_bgr555(col[0], col[1], col[2]) byte_data = rgb888_to_bgr555(col[0], col[1], col[2])
output_data.extend(bytearray(byte_data)) output_data.extend(bytearray(byte_data))
return bytes(output_data) return output_data

View File

@@ -11,13 +11,13 @@ from MultiServer import mark_raw
from NetUtils import ClientStatus, color from NetUtils import ClientStatus, color
from Utils import async_start from Utils import async_start
from worlds.AutoSNIClient import SNIClient from worlds.AutoSNIClient import SNIClient
from .locations import boss_locations from .Locations import boss_locations
from .gifting import kdl3_gifting_options, kdl3_trap_gifts, kdl3_gifts, update_object, pop_object, initialize_giftboxes 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 .ClientAddrs import consumable_addrs, star_addrs
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from SNIClient import SNIClientCommandProcessor, SNIContext from SNIClient import SNIClientCommandProcessor
snes_logger = logging.getLogger("SNES") snes_logger = logging.getLogger("SNES")
@@ -81,16 +81,17 @@ deathlink_messages = defaultdict(lambda: " was defeated.", {
@mark_raw @mark_raw
def cmd_gift(self: "SNIClientCommandProcessor") -> None: def cmd_gift(self: "SNIClientCommandProcessor"):
"""Toggles gifting for the current game.""" """Toggles gifting for the current game."""
handler = self.ctx.client_handler if not getattr(self.ctx, "gifting", None):
assert isinstance(handler, KDL3SNIClient) self.ctx.gifting = True
handler.gifting = not handler.gifting else:
self.output(f"Gifting set to {handler.gifting}") self.ctx.gifting = not self.ctx.gifting
self.output(f"Gifting set to {self.ctx.gifting}")
async_start(update_object(self.ctx, f"Giftboxes;{self.ctx.team}", { async_start(update_object(self.ctx, f"Giftboxes;{self.ctx.team}", {
f"{self.ctx.slot}": f"{self.ctx.slot}":
{ {
"IsOpen": handler.gifting, "IsOpen": self.ctx.gifting,
**kdl3_gifting_options **kdl3_gifting_options
} }
})) }))
@@ -99,17 +100,16 @@ def cmd_gift(self: "SNIClientCommandProcessor") -> None:
class KDL3SNIClient(SNIClient): class KDL3SNIClient(SNIClient):
game = "Kirby's Dream Land 3" game = "Kirby's Dream Land 3"
patch_suffix = ".apkdl3" patch_suffix = ".apkdl3"
levels: typing.Dict[int, typing.List[int]] = {} levels = None
consumables: typing.Optional[bool] = None consumables = None
stars: typing.Optional[bool] = None stars = None
item_queue: typing.List[int] = [] item_queue: typing.List = []
initialize_gifting: bool = False initialize_gifting = False
gifting: bool = False
giftbox_key: str = "" giftbox_key: str = ""
motherbox_key: str = "" motherbox_key: str = ""
client_random: random.Random = random.Random() client_random: random.Random = random.Random()
async def deathlink_kill_player(self, ctx: "SNIContext") -> None: async def deathlink_kill_player(self, ctx) -> None:
from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read
game_state = await snes_read(ctx, KDL3_GAME_STATE, 1) game_state = await snes_read(ctx, KDL3_GAME_STATE, 1)
if game_state[0] == 0xFF: if game_state[0] == 0xFF:
@@ -131,7 +131,7 @@ class KDL3SNIClient(SNIClient):
ctx.death_state = DeathState.dead ctx.death_state = DeathState.dead
ctx.last_death_link = time.time() ctx.last_death_link = time.time()
async def validate_rom(self, ctx: "SNIContext") -> bool: async def validate_rom(self, ctx) -> bool:
from SNIClient import snes_read from SNIClient import snes_read
rom_name = await snes_read(ctx, KDL3_ROMNAME, 0x15) 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": 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.game = self.game
ctx.rom = rom_name ctx.rom = rom_name
ctx.items_handling = 0b101 # default local items with remote start inventory ctx.items_handling = 0b111 # always remote items
ctx.allow_collect = True ctx.allow_collect = True
if "gift" not in ctx.command_processor.commands: if "gift" not in ctx.command_processor.commands:
ctx.command_processor.commands["gift"] = cmd_gift ctx.command_processor.commands["gift"] = cmd_gift
@@ -149,10 +149,9 @@ class KDL3SNIClient(SNIClient):
death_link = await snes_read(ctx, KDL3_DEATH_LINK_ADDR, 1) death_link = await snes_read(ctx, KDL3_DEATH_LINK_ADDR, 1)
if death_link: if death_link:
await ctx.update_death_link(bool(death_link[0] & 0b1)) await ctx.update_death_link(bool(death_link[0] & 0b1))
ctx.items_handling |= (death_link[0] & 0b10) # set local items if enabled
return True return True
async def pop_item(self, ctx: "SNIContext", in_stage: bool) -> None: async def pop_item(self, ctx, in_stage):
from SNIClient import snes_buffered_write, snes_read from SNIClient import snes_buffered_write, snes_read
if len(self.item_queue) > 0: if len(self.item_queue) > 0:
item = self.item_queue.pop() item = self.item_queue.pop()
@@ -169,8 +168,8 @@ class KDL3SNIClient(SNIClient):
else: else:
self.item_queue.append(item) # no more slots, get it next go around self.item_queue.append(item) # no more slots, get it next go around
async def pop_gift(self, ctx: "SNIContext") -> None: async def pop_gift(self, ctx):
if self.giftbox_key in ctx.stored_data and ctx.stored_data[self.giftbox_key]: if ctx.stored_data[self.giftbox_key]:
from SNIClient import snes_read, snes_buffered_write from SNIClient import snes_read, snes_buffered_write
key, gift = ctx.stored_data[self.giftbox_key].popitem() key, gift = ctx.stored_data[self.giftbox_key].popitem()
await pop_object(ctx, self.giftbox_key, key) await pop_object(ctx, self.giftbox_key, key)
@@ -215,7 +214,7 @@ class KDL3SNIClient(SNIClient):
quality = min(10, quality * 2) quality = min(10, quality * 2)
else: else:
# it's not really edible, but he'll eat it anyway # it's not really edible, but he'll eat it anyway
quality = self.client_random.choices(range(0, 2), [75, 25])[0] quality = self.client_random.choices(range(0, 2), {0: 75, 1: 25})[0]
kirby_hp = await snes_read(ctx, KDL3_KIRBY_HP, 1) kirby_hp = await snes_read(ctx, KDL3_KIRBY_HP, 1)
gooey_hp = await snes_read(ctx, KDL3_KIRBY_HP + 2, 1) gooey_hp = await snes_read(ctx, KDL3_KIRBY_HP + 2, 1)
snes_buffered_write(ctx, KDL3_SOUND_FX, bytes([0x26])) snes_buffered_write(ctx, KDL3_SOUND_FX, bytes([0x26]))
@@ -225,8 +224,7 @@ class KDL3SNIClient(SNIClient):
else: else:
snes_buffered_write(ctx, KDL3_KIRBY_HP, struct.pack("H", min(kirby_hp[0] + quality, 10))) snes_buffered_write(ctx, KDL3_KIRBY_HP, struct.pack("H", min(kirby_hp[0] + quality, 10)))
async def pick_gift_recipient(self, ctx: "SNIContext", gift: int) -> None: async def pick_gift_recipient(self, ctx, gift):
assert ctx.slot
if gift != 4: if gift != 4:
gift_base = kdl3_gifts[gift] gift_base = kdl3_gifts[gift]
else: else:
@@ -240,7 +238,7 @@ class KDL3SNIClient(SNIClient):
if desire > most_applicable: if desire > most_applicable:
most_applicable = desire most_applicable = desire
most_applicable_slot = int(slot) most_applicable_slot = int(slot)
elif most_applicable_slot != ctx.slot and most_applicable == -1 and info["AcceptsAnyGift"]: elif most_applicable_slot == ctx.slot and info["AcceptsAnyGift"]:
# only send to ourselves if no one else will take it # only send to ourselves if no one else will take it
most_applicable_slot = int(slot) most_applicable_slot = int(slot)
# print(most_applicable, most_applicable_slot) # print(most_applicable, most_applicable_slot)
@@ -259,7 +257,7 @@ class KDL3SNIClient(SNIClient):
item_uuid: item, item_uuid: item,
}) })
async def game_watcher(self, ctx: "SNIContext") -> None: async def game_watcher(self, ctx) -> None:
try: try:
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
rom = await snes_read(ctx, KDL3_ROMNAME, 0x15) rom = await snes_read(ctx, KDL3_ROMNAME, 0x15)
@@ -280,12 +278,11 @@ class KDL3SNIClient(SNIClient):
await initialize_giftboxes(ctx, self.giftbox_key, self.motherbox_key, bool(enable_gifting[0])) await initialize_giftboxes(ctx, self.giftbox_key, self.motherbox_key, bool(enable_gifting[0]))
self.initialize_gifting = True self.initialize_gifting = True
# can't check debug anymore, without going and copying the value. might be important later. # can't check debug anymore, without going and copying the value. might be important later.
if not self.levels: if self.levels is None:
self.levels = dict() self.levels = dict()
for i in range(5): for i in range(5):
level_data = await snes_read(ctx, KDL3_LEVEL_ADDR + (14 * i), 14) level_data = await snes_read(ctx, KDL3_LEVEL_ADDR + (14 * i), 14)
self.levels[i] = [int.from_bytes(level_data[idx:idx+1], "little") self.levels[i] = unpack("HHHHHHH", level_data)
for idx in range(0, len(level_data), 2)]
self.levels[5] = [0x0205, # Hyper Zone self.levels[5] = [0x0205, # Hyper Zone
0, # MG-5, can't send from here 0, # MG-5, can't send from here
0x0300, # Boss Butch 0x0300, # Boss Butch
@@ -374,7 +371,7 @@ class KDL3SNIClient(SNIClient):
stages_raw = await snes_read(ctx, KDL3_COMPLETED_STAGES, 60) stages_raw = await snes_read(ctx, KDL3_COMPLETED_STAGES, 60)
stages = struct.unpack("HHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", stages_raw) stages = struct.unpack("HHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", stages_raw)
for i in range(30): for i in range(30):
loc_id = 0x770000 + i loc_id = 0x770000 + i + 1
if stages[i] == 1 and loc_id not in ctx.checked_locations: if stages[i] == 1 and loc_id not in ctx.checked_locations:
new_checks.append(loc_id) new_checks.append(loc_id)
elif loc_id in ctx.checked_locations: elif loc_id in ctx.checked_locations:
@@ -384,8 +381,8 @@ class KDL3SNIClient(SNIClient):
heart_stars = await snes_read(ctx, KDL3_HEART_STARS, 35) heart_stars = await snes_read(ctx, KDL3_HEART_STARS, 35)
for i in range(5): for i in range(5):
start_ind = i * 7 start_ind = i * 7
for j in range(6): for j in range(1, 7):
level_ind = start_ind + j level_ind = start_ind + j - 1
loc_id = 0x770100 + (6 * i) + j loc_id = 0x770100 + (6 * i) + j
if heart_stars[level_ind] and loc_id not in ctx.checked_locations: if heart_stars[level_ind] and loc_id not in ctx.checked_locations:
new_checks.append(loc_id) new_checks.append(loc_id)
@@ -404,9 +401,6 @@ class KDL3SNIClient(SNIClient):
if star not in ctx.checked_locations and stars[star_addrs[star]] == 0x01: if star not in ctx.checked_locations and stars[star_addrs[star]] == 0x01:
new_checks.append(star) new_checks.append(star)
if not game_state:
return
if game_state[0] != 0xFF: if game_state[0] != 0xFF:
await self.pop_gift(ctx) await self.pop_gift(ctx)
await self.pop_item(ctx, game_state[0] != 0xFF) await self.pop_item(ctx, game_state[0] != 0xFF)
@@ -414,7 +408,7 @@ class KDL3SNIClient(SNIClient):
# boss status # boss status
boss_flag_bytes = await snes_read(ctx, KDL3_BOSS_STATUS, 2) boss_flag_bytes = await snes_read(ctx, KDL3_BOSS_STATUS, 2)
boss_flag = int.from_bytes(boss_flag_bytes, "little") boss_flag = unpack("H", boss_flag_bytes)[0]
for bitmask, boss in zip(range(1, 11, 2), boss_locations.keys()): 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: if boss_flag & (1 << bitmask) > 0 and boss not in ctx.checked_locations:
new_checks.append(boss) new_checks.append(boss)

View File

@@ -1,11 +1,8 @@
# Small subfile to handle gifting info such as desired traits and giftbox management # Small subfile to handle gifting info such as desired traits and giftbox management
import typing 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([ await ctx.send_msgs([
{ {
"cmd": "Set", "cmd": "Set",
@@ -19,7 +16,7 @@ async def update_object(ctx: "SNIContext", key: str, value: typing.Dict[str, typ
]) ])
async def pop_object(ctx: "SNIContext", key: str, value: str) -> None: async def pop_object(ctx, key: str, value: str):
await ctx.send_msgs([ await ctx.send_msgs([
{ {
"cmd": "Set", "cmd": "Set",
@@ -33,14 +30,14 @@ async def pop_object(ctx: "SNIContext", key: str, value: str) -> None:
]) ])
async def initialize_giftboxes(ctx: "SNIContext", giftbox_key: str, motherbox_key: str, is_open: bool) -> None: async def initialize_giftboxes(ctx, giftbox_key: str, motherbox_key: str, is_open: bool):
ctx.set_notify(motherbox_key, giftbox_key) ctx.set_notify(motherbox_key, giftbox_key)
await update_object(ctx, f"Giftboxes;{ctx.team}", {f"{ctx.slot}": await update_object(ctx, f"Giftboxes;{ctx.team}", {f"{ctx.slot}":
{ {
"IsOpen": is_open, "IsOpen": is_open,
**kdl3_gifting_options **kdl3_gifting_options
}}) }})
ctx.client_handler.gifting = is_open ctx.gifting = is_open
kdl3_gifting_options = { kdl3_gifting_options = {

View File

@@ -77,9 +77,9 @@ filler_item_weights = {
} }
star_item_weights = { star_item_weights = {
"Little Star": 16, "Little Star": 4,
"Medium Star": 8, "Medium Star": 2,
"Big Star": 4 "Big Star": 1
} }
total_filler_weights = { total_filler_weights = {
@@ -102,4 +102,4 @@ item_names = {
"Animal Friend": set(animal_friend_table), "Animal Friend": set(animal_friend_table),
} }
lookup_item_to_id: typing.Dict[str, int] = {item_name: data.code for item_name, data in item_table.items() if data.code} lookup_name_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
View File

@@ -0,0 +1,940 @@
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
}

View File

@@ -1,5 +1,3 @@
from typing import List
grass_land_1_a1 = "Grass Land 1 - Animal 1" # Nago grass_land_1_a1 = "Grass Land 1 - Animal 1" # Nago
grass_land_1_a2 = "Grass Land 1 - Animal 2" # Rick grass_land_1_a2 = "Grass Land 1 - Animal 2" # Rick
grass_land_2_a1 = "Grass Land 2 - Animal 1" # ChuChu grass_land_2_a1 = "Grass Land 2 - Animal 1" # ChuChu
@@ -199,12 +197,3 @@ animal_friend_spawns = {
iceberg_6_a5: "ChuChu Spawn", iceberg_6_a5: "ChuChu Spawn",
iceberg_6_a6: "Nago 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]
]

View File

@@ -809,7 +809,7 @@ vanilla_enemies = {'Waddle Dee': 'No Ability',
enemy_restrictive: List[Tuple[List[str], List[str]]] = [ enemy_restrictive: List[Tuple[List[str], List[str]]] = [
# abilities, enemies, set_all (False to set any) # abilities, enemies, set_all (False to set any)
(["Stone Ability"], ["Rocky", "Sparky", "Babut", "Squishy", ]), # Ribbon Field 5 - 7 (["Burning Ability", "Stone Ability"], ["Rocky", "Sparky", "Babut", "Squishy", ]), # Ribbon Field 5 - 7
# Sand Canyon 6 # Sand Canyon 6
(["Parasol Ability", "Cutter Ability"], ['Bukiset (Parasol)', 'Bukiset (Cutter)']), (["Parasol Ability", "Cutter Ability"], ['Bukiset (Parasol)', 'Bukiset (Cutter)']),
(["Spark Ability", "Clean Ability"], ['Bukiset (Spark)', 'Bukiset (Clean)']), (["Spark Ability", "Clean Ability"], ['Bukiset (Spark)', 'Bukiset (Clean)']),

View File

@@ -1,21 +1,13 @@
import random import random
from dataclasses import dataclass from dataclasses import dataclass
from typing import List
from Options import DeathLinkMixin, Choice, Toggle, OptionDict, Range, PlandoBosses, DefaultOnToggle, \ from Options import DeathLink, Choice, Toggle, OptionDict, Range, PlandoBosses, DefaultOnToggle, \
PerGameCommonOptions, Visibility, NamedRange, OptionGroup, PlandoConnections PerGameCommonOptions, PlandoConnections
from .names import location_name from .Names import LocationName
class RemoteItems(DefaultOnToggle):
"""
Enables receiving items from your own world, primarily for co-op play.
"""
display_name = "Remote Items"
class KDL3PlandoConnections(PlandoConnections): class KDL3PlandoConnections(PlandoConnections):
entrances = exits = {f"{i} {j}" for i in location_name.level_names for j in range(1, 7)} entrances = exits = {f"{i} {j}" for i in LocationName.level_names for j in range(1, 7)}
class Goal(Choice): class Goal(Choice):
@@ -38,7 +30,6 @@ class Goal(Choice):
return cls.name_lookup[value].upper() return cls.name_lookup[value].upper()
return super().get_option_name(value) return super().get_option_name(value)
class GoalSpeed(Choice): class GoalSpeed(Choice):
""" """
Normal: the goal is unlocked after purifying the five bosses Normal: the goal is unlocked after purifying the five bosses
@@ -49,14 +40,13 @@ class GoalSpeed(Choice):
option_fast = 1 option_fast = 1
class MaxHeartStars(Range): class TotalHeartStars(Range):
""" """
Maximum number of heart stars to include in the pool of items. 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" display_name = "Max Heart Stars"
range_start = 5 # set to 5 so strict bosses does not degrade range_start = 5 # set to 5 so strict bosses does not degrade
range_end = 99 # previously set to 50, set to highest it can be should there be less locations than heart stars range_end = 50 # 30 default locations + 30 stage clears + 5 bosses - 14 progression items = 51, so round down
default = 30 default = 30
@@ -94,9 +84,9 @@ class BossShuffle(PlandoBosses):
Singularity: All (non-Zero) bosses will be replaced with a single boss Singularity: All (non-Zero) bosses will be replaced with a single boss
Supports plando placement. Supports plando placement.
""" """
bosses = frozenset(location_name.boss_names.keys()) bosses = frozenset(LocationName.boss_names.keys())
locations = frozenset(location_name.level_names.keys()) locations = frozenset(LocationName.level_names.keys())
duplicate_bosses = True duplicate_bosses = True
@@ -288,8 +278,7 @@ class KirbyFlavorPreset(Choice):
option_orange = 11 option_orange = 11
option_lime = 12 option_lime = 12
option_lavender = 13 option_lavender = 13
option_miku = 14 option_custom = 14
option_custom = 15
default = 0 default = 0
@classmethod @classmethod
@@ -307,7 +296,6 @@ 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 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. "15", with their values being an HTML hex color.
""" """
display_name = "Custom Kirby Flavor"
default = { default = {
"1": "B01810", "1": "B01810",
"2": "F0E0E8", "2": "F0E0E8",
@@ -325,7 +313,6 @@ class KirbyFlavor(OptionDict):
"14": "F8F8F8", "14": "F8F8F8",
"15": "B03830", "15": "B03830",
} }
visibility = Visibility.template | Visibility.spoiler # likely never supported on guis
class GooeyFlavorPreset(Choice): class GooeyFlavorPreset(Choice):
@@ -365,7 +352,6 @@ 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 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. "15", with their values being an HTML hex color.
""" """
display_name = "Custom Gooey Flavor"
default = { default = {
"1": "000808", "1": "000808",
"2": "102838", "2": "102838",
@@ -377,7 +363,6 @@ class GooeyFlavor(OptionDict):
"8": "D0C0C0", "8": "D0C0C0",
"9": "F8F8F8", "9": "F8F8F8",
} }
visibility = Visibility.template | Visibility.spoiler # likely never supported on guis
class MusicShuffle(Choice): class MusicShuffle(Choice):
@@ -417,27 +402,14 @@ class Gifting(Toggle):
display_name = "Gifting" 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 @dataclass
class KDL3Options(PerGameCommonOptions, DeathLinkMixin): class KDL3Options(PerGameCommonOptions):
remote_items: RemoteItems
plando_connections: KDL3PlandoConnections plando_connections: KDL3PlandoConnections
death_link: DeathLink
game_language: GameLanguage game_language: GameLanguage
goal: Goal goal: Goal
goal_speed: GoalSpeed goal_speed: GoalSpeed
max_heart_stars: MaxHeartStars total_heart_stars: TotalHeartStars
heart_stars_required: HeartStarsRequired heart_stars_required: HeartStarsRequired
filler_percentage: FillerPercentage filler_percentage: FillerPercentage
trap_percentage: TrapPercentage trap_percentage: TrapPercentage
@@ -463,17 +435,3 @@ class KDL3Options(PerGameCommonOptions, DeathLinkMixin):
gooey_flavor: GooeyFlavor gooey_flavor: GooeyFlavor
music_shuffle: MusicShuffle music_shuffle: MusicShuffle
virtual_console: VirtualConsoleChanges 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, ]),
]

View File

@@ -25,7 +25,6 @@ all_random = {
"ow_boss_requirement": "random", "ow_boss_requirement": "random",
"boss_requirement_random": "random", "boss_requirement_random": "random",
"consumables": "random", "consumables": "random",
"starsanity": "random",
"kirby_flavor_preset": "random", "kirby_flavor_preset": "random",
"gooey_flavor_preset": "random", "gooey_flavor_preset": "random",
"music_shuffle": "random", "music_shuffle": "random",

View File

@@ -1,62 +1,60 @@
import orjson import orjson
import os import os
from pkgutil import get_data from pkgutil import get_data
from copy import deepcopy
from typing import TYPE_CHECKING, List, Dict, Optional, Union, Callable from typing import TYPE_CHECKING, List, Dict, Optional, Union
from BaseClasses import Region, CollectionState from BaseClasses import Region
from worlds.generic.Rules import add_item_rule from worlds.generic.Rules import add_item_rule
from .locations import KDL3Location from .Locations import KDL3Location
from .names import location_name from .Names import LocationName
from .options import BossShuffle from .Options import BossShuffle
from .room import KDL3Room from .Room import KDL3Room
if TYPE_CHECKING: if TYPE_CHECKING:
from . import KDL3World from . import KDL3World
default_levels = { default_levels = {
1: [0x770000, 0x770001, 0x770002, 0x770003, 0x770004, 0x770005, 0x770200], 1: [0x770001, 0x770002, 0x770003, 0x770004, 0x770005, 0x770006, 0x770200],
2: [0x770006, 0x770007, 0x770008, 0x770009, 0x77000A, 0x77000B, 0x770201], 2: [0x770007, 0x770008, 0x770009, 0x77000A, 0x77000B, 0x77000C, 0x770201],
3: [0x77000C, 0x77000D, 0x77000E, 0x77000F, 0x770010, 0x770011, 0x770202], 3: [0x77000D, 0x77000E, 0x77000F, 0x770010, 0x770011, 0x770012, 0x770202],
4: [0x770012, 0x770013, 0x770014, 0x770015, 0x770016, 0x770017, 0x770203], 4: [0x770013, 0x770014, 0x770015, 0x770016, 0x770017, 0x770018, 0x770203],
5: [0x770018, 0x770019, 0x77001A, 0x77001B, 0x77001C, 0x77001D, 0x770204], 5: [0x770019, 0x77001A, 0x77001B, 0x77001C, 0x77001D, 0x77001E, 0x770204],
} }
first_stage_blacklist = { first_stage_blacklist = {
# We want to confirm that the first stage can be completed without any items # We want to confirm that the first stage can be completed without any items
0x77000A, # 2-5 needs Kine 0x77000B, # 2-5 needs Kine
0x770010, # 3-5 needs Cutter 0x770011, # 3-5 needs Cutter
0x77001B, # 5-4 needs Burning 0x77001C, # 5-4 needs Burning
} }
first_world_limit = { first_world_limit = {
# We need to limit the number of very restrictive stages in level 1 on solo gens # 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 *first_stage_blacklist, # all three of the blacklist stages need 2+ items for both checks
0x770006,
0x770007, 0x770007,
0x770012, 0x770008,
0x77001D, 0x770013,
0x77001E,
} }
def generate_valid_level(world: "KDL3World", level: int, stage: int, def generate_valid_level(world: "KDL3World", level: int, stage: int,
possible_stages: List[int], placed_stages: List[Optional[int]]) -> int: possible_stages: List[int], placed_stages: List[int]):
new_stage = world.random.choice(possible_stages) new_stage = world.random.choice(possible_stages)
if level == 1: if level == 1:
if stage == 0 and new_stage in first_stage_blacklist: 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) 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 elif (not (world.multiworld.players > 1 or world.options.consumables or world.options.starsanity) and
new_stage in first_world_limit and new_stage in first_world_limit and
sum(p_stage in first_world_limit for p_stage in placed_stages) sum(p_stage in first_world_limit for p_stage in placed_stages)
>= (2 if world.options.open_world else 1)): >= (2 if world.options.open_world else 1)):
return generate_valid_level(world, level, stage, possible_stages, placed_stages) return generate_valid_level(world, level, stage, possible_stages, placed_stages)
return new_stage return new_stage
def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]) -> None: def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]):
level_names = {location_name.level_names[level]: level for level in location_name.level_names} level_names = {LocationName.level_names[level]: level for level in LocationName.level_names}
room_data = orjson.loads(get_data(__name__, os.path.join("data", "Rooms.json"))) room_data = orjson.loads(get_data(__name__, os.path.join("data", "Rooms.json")))
rooms: Dict[str, KDL3Room] = dict() rooms: Dict[str, KDL3Room] = dict()
for room_entry in room_data: for room_entry in room_data:
@@ -65,7 +63,7 @@ def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]) -> None
room_entry["default_exits"], room_entry["animal_pointers"], room_entry["enemies"], room_entry["default_exits"], room_entry["animal_pointers"], room_entry["enemies"],
room_entry["entity_load"], room_entry["consumables"], room_entry["consumables_pointer"]) 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 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 if (not any(x in location for x in ["1-Up", "Maxim"]) or
world.options.consumables.value) and ("Star" not in location world.options.consumables.value) and ("Star" not in location
or world.options.starsanity.value)}, or world.options.starsanity.value)},
@@ -85,8 +83,8 @@ def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]) -> None
if room.stage == 7: if room.stage == 7:
first_rooms[0x770200 + room.level - 1] = room first_rooms[0x770200 + room.level - 1] = room
else: else:
first_rooms[0x770000 + ((room.level - 1) * 6) + room.stage - 1] = room first_rooms[0x770000 + ((room.level - 1) * 6) + room.stage] = room
exits: Dict[str, Callable[[CollectionState], bool]] = dict() exits = dict()
for def_exit in room.default_exits: for def_exit in room.default_exits:
target = f"{level_names[room.level]} {room.stage} - {def_exit['room']}" target = f"{level_names[room.level]} {room.stage} - {def_exit['room']}"
access_rule = tuple(def_exit["access_rule"]) access_rule = tuple(def_exit["access_rule"])
@@ -117,54 +115,50 @@ def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]) -> None
if world.options.open_world: if world.options.open_world:
level_regions[level].add_exits([first_rooms[0x770200 + level - 1].name]) level_regions[level].add_exits([first_rooms[0x770200 + level - 1].name])
else: 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]) .parent_region.add_exits([first_rooms[0x770200 + level - 1].name])
def generate_valid_levels(world: "KDL3World", shuffle_mode: int) -> Dict[int, List[int]]: def generate_valid_levels(world: "KDL3World", enforce_world: bool, enforce_pattern: bool) -> dict:
if shuffle_mode: levels: Dict[int, List[Optional[int]]] = {
levels: Dict[int, List[Optional[int]]] = { 1: [None] * 7,
1: [None] * 7, 2: [None] * 7,
2: [None] * 7, 3: [None] * 7,
3: [None] * 7, 4: [None] * 7,
4: [None] * 7, 5: [None] * 7,
5: [None] * 7, }
}
possible_stages = [default_levels[level][stage] for level in default_levels for stage in range(6)] possible_stages = [default_levels[level][stage] for level in default_levels for stage in range(6)]
if world.options.plando_connections: if world.options.plando_connections:
for connection in world.options.plando_connections: for connection in world.options.plando_connections:
try: try:
entrance_world, entrance_stage = connection.entrance.rsplit(" ", 1) entrance_world, entrance_stage = connection.entrance.rsplit(" ", 1)
stage_world, stage_stage = connection.exit.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] new_stage = default_levels[LocationName.level_names[stage_world.strip()]][int(stage_stage) - 1]
levels[location_name.level_names[entrance_world.strip()]][int(entrance_stage) - 1] = new_stage levels[LocationName.level_names[entrance_world.strip()]][int(entrance_stage) - 1] = new_stage
possible_stages.remove(new_stage) possible_stages.remove(new_stage)
except Exception: except Exception:
raise Exception( raise Exception(
f"Invalid connection: {connection.entrance} =>" f"Invalid connection: {connection.entrance} =>"
f" {connection.exit} for player {world.player} ({world.player_name})") f" {connection.exit} for player {world.player} ({world.player_name})")
for level in range(1, 6): for level in range(1, 6):
for stage in range(6): for stage in range(6):
# Randomize bosses separately # Randomize bosses separately
try:
if levels[level][stage] is None: if levels[level][stage] is None:
stage_candidates = [candidate for candidate in possible_stages stage_candidates = [candidate for candidate in possible_stages
if (shuffle_mode == 1 and candidate in default_levels[level]) if (enforce_world and candidate in default_levels[level])
or (shuffle_mode == 2 and (candidate & 0x00FFFF) % 6 == stage) or (enforce_pattern and ((candidate - 1) & 0x00FFFF) % 6 == stage)
or (shuffle_mode == 3) or (enforce_pattern == enforce_world)
] ]
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]) new_stage = generate_valid_level(world, level, stage, stage_candidates, levels[level])
possible_stages.remove(new_stage) possible_stages.remove(new_stage)
levels[level][stage] = new_stage levels[level][stage] = new_stage
else: except Exception:
levels = deepcopy(default_levels) raise Exception(f"Failed to find valid stage for {level}-{stage}. Remaining Stages:{possible_stages}")
for level in levels:
levels[level][6] = None
# now handle bosses # now handle bosses
boss_shuffle: Union[int, str] = world.options.boss_shuffle.value boss_shuffle: Union[int, str] = world.options.boss_shuffle.value
plando_bosses = [] plando_bosses = []
@@ -174,17 +168,17 @@ def generate_valid_levels(world: "KDL3World", shuffle_mode: int) -> Dict[int, Li
boss_shuffle = BossShuffle.options[options.pop()] boss_shuffle = BossShuffle.options[options.pop()]
for option in options: for option in options:
if "-" in option: if "-" in option:
loc, plando_boss = option.split("-") loc, boss = option.split("-")
loc = loc.title() loc = loc.title()
plando_boss = plando_boss.title() boss = boss.title()
levels[location_name.level_names[loc]][6] = location_name.boss_names[plando_boss] levels[LocationName.level_names[loc]][6] = LocationName.boss_names[boss]
plando_bosses.append(location_name.boss_names[plando_boss]) plando_bosses.append(LocationName.boss_names[boss])
else: else:
option = option.title() option = option.title()
for level in levels: for level in levels:
if levels[level][6] is None: if levels[level][6] is None:
levels[level][6] = location_name.boss_names[option] levels[level][6] = LocationName.boss_names[option]
plando_bosses.append(location_name.boss_names[option]) plando_bosses.append(LocationName.boss_names[option])
if boss_shuffle > 0: if boss_shuffle > 0:
if boss_shuffle == BossShuffle.option_full: if boss_shuffle == BossShuffle.option_full:
@@ -229,14 +223,15 @@ def create_levels(world: "KDL3World") -> None:
5: level5, 5: level5,
} }
level_shuffle = world.options.stage_shuffle.value level_shuffle = world.options.stage_shuffle.value
if hasattr(world.multiworld, "re_gen_passthrough"): if level_shuffle != 0:
world.player_levels = getattr(world.multiworld, "re_gen_passthrough")["Kirby's Dream Land 3"]["player_levels"] world.player_levels = generate_valid_levels(
else: world,
world.player_levels = generate_valid_levels(world, level_shuffle) level_shuffle == 1,
level_shuffle == 2)
generate_rooms(world, levels) generate_rooms(world, levels)
level6.add_locations({location_name.goals[world.options.goal.value]: None}, KDL3Location) level6.add_locations({LocationName.goals[world.options.goal]: None}, KDL3Location)
menu.connect(level1, "Start Game") menu.connect(level1, "Start Game")
level1.connect(level2, "To Level 2") level1.connect(level2, "To Level 2")

577
worlds/kdl3/Rom.py Normal file
View File

@@ -0,0 +1,577 @@
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

95
worlds/kdl3/Room.py Normal file
View File

@@ -0,0 +1,95 @@
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)

View File

@@ -1,7 +1,7 @@
from worlds.generic.Rules import set_rule, add_rule from worlds.generic.Rules import set_rule, add_rule
from .names import location_name, enemy_abilities, animal_friend_spawns from .Names import LocationName, EnemyAbilities
from .locations import location_table from .Locations import location_table
from .options import GoalSpeed from .Options import GoalSpeed
import typing import typing
if typing.TYPE_CHECKING: 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, def can_reach_boss(state: "CollectionState", player: int, level: int, open_world: int,
ow_boss_req: int, player_levels: typing.Dict[int, typing.List[int]]) -> bool: ow_boss_req: int, player_levels: typing.Dict[int, typing.Dict[int, int]]):
if open_world: if open_world:
return state.has(f"{location_name.level_names_inverse[level]} - Stage Completion", player, ow_boss_req) return state.has(f"{LocationName.level_names_inverse[level]} - Stage Completion", player, ow_boss_req)
else: else:
return state.can_reach(location_table[player_levels[level][5]], "Location", player) 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]) -> bool: def can_assemble_rob(state: "CollectionState", player: int, copy_abilities: typing.Dict[str, str]):
# check animal requirements # check animal requirements
if not (can_reach_coo(state, player) and can_reach_kine(state, player)): if not (can_reach_coo(state, player) and can_reach_kine(state, player)):
return False return False
for abilities, bukisets in enemy_abilities.enemy_restrictive[1:5]: for abilities, bukisets in EnemyAbilities.enemy_restrictive[1:5]:
iterator = iter(x for x in bukisets if copy_abilities[x] in abilities) iterator = iter(x for x in bukisets if copy_abilities[x] in abilities)
target_bukiset = next(iterator, None) target_bukiset = next(iterator, None)
can_reach = False 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) 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]) -> bool: def can_fix_angel_wings(state: "CollectionState", player: int, copy_abilities: typing.Dict[str, str]):
can_reach = True can_reach = True
for enemy in {"Sparky", "Blocky", "Jumper Shoot", "Yuki", "Sir Kibble", "Haboki", "Boboo", "Captain Stitch"}: 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) 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: def set_rules(world: "KDL3World") -> None:
# Level 1 # Level 1
set_rule(world.multiworld.get_location(location_name.grass_land_muchi, world.player), set_rule(world.multiworld.get_location(LocationName.grass_land_muchi, world.player),
lambda state: can_reach_chuchu(state, world.player)) lambda state: can_reach_chuchu(state, world.player))
set_rule(world.multiworld.get_location(location_name.grass_land_chao, world.player), set_rule(world.multiworld.get_location(LocationName.grass_land_chao, world.player),
lambda state: can_reach_stone(state, world.player)) lambda state: can_reach_stone(state, world.player))
set_rule(world.multiworld.get_location(location_name.grass_land_mine, world.player), set_rule(world.multiworld.get_location(LocationName.grass_land_mine, world.player),
lambda state: can_reach_kine(state, world.player)) lambda state: can_reach_kine(state, world.player))
# Level 2 # Level 2
set_rule(world.multiworld.get_location(location_name.ripple_field_5, world.player), set_rule(world.multiworld.get_location(LocationName.ripple_field_5, world.player),
lambda state: can_reach_kine(state, world.player)) lambda state: can_reach_kine(state, world.player))
set_rule(world.multiworld.get_location(location_name.ripple_field_kamuribana, world.player), set_rule(world.multiworld.get_location(LocationName.ripple_field_kamuribana, world.player),
lambda state: can_reach_pitch(state, world.player) and can_reach_clean(state, world.player)) lambda state: can_reach_pitch(state, world.player) and can_reach_clean(state, world.player))
set_rule(world.multiworld.get_location(location_name.ripple_field_bakasa, world.player), set_rule(world.multiworld.get_location(LocationName.ripple_field_bakasa, world.player),
lambda state: can_reach_kine(state, world.player) and can_reach_parasol(state, world.player)) lambda state: can_reach_kine(state, world.player) and can_reach_parasol(state, world.player))
set_rule(world.multiworld.get_location(location_name.ripple_field_toad, world.player), set_rule(world.multiworld.get_location(LocationName.ripple_field_toad, world.player),
lambda state: can_reach_needle(state, world.player)) lambda state: can_reach_needle(state, world.player))
set_rule(world.multiworld.get_location(location_name.ripple_field_mama_pitch, world.player), set_rule(world.multiworld.get_location(LocationName.ripple_field_mama_pitch, world.player),
lambda state: (can_reach_pitch(state, world.player) and lambda state: (can_reach_pitch(state, world.player) and
can_reach_kine(state, world.player) and can_reach_kine(state, world.player) and
can_reach_burning(state, world.player) and can_reach_burning(state, world.player) and
can_reach_stone(state, world.player))) can_reach_stone(state, world.player)))
# Level 3 # Level 3
set_rule(world.multiworld.get_location(location_name.sand_canyon_5, world.player), set_rule(world.multiworld.get_location(LocationName.sand_canyon_5, world.player),
lambda state: can_reach_cutter(state, world.player)) lambda state: can_reach_cutter(state, world.player))
set_rule(world.multiworld.get_location(location_name.sand_canyon_auntie, world.player), set_rule(world.multiworld.get_location(LocationName.sand_canyon_auntie, world.player),
lambda state: can_reach_clean(state, world.player)) lambda state: can_reach_clean(state, world.player))
set_rule(world.multiworld.get_location(location_name.sand_canyon_nyupun, world.player), set_rule(world.multiworld.get_location(LocationName.sand_canyon_nyupun, world.player),
lambda state: can_reach_chuchu(state, world.player) and can_reach_cutter(state, world.player)) lambda state: can_reach_chuchu(state, world.player) and can_reach_cutter(state, world.player))
set_rule(world.multiworld.get_location(location_name.sand_canyon_rob, world.player), set_rule(world.multiworld.get_location(LocationName.sand_canyon_rob, world.player),
lambda state: can_assemble_rob(state, world.player, world.copy_abilities) lambda state: can_assemble_rob(state, world.player, world.copy_abilities)
) )
# Level 4 # Level 4
set_rule(world.multiworld.get_location(location_name.cloudy_park_hibanamodoki, world.player), set_rule(world.multiworld.get_location(LocationName.cloudy_park_hibanamodoki, world.player),
lambda state: can_reach_coo(state, world.player) and can_reach_clean(state, world.player)) lambda state: can_reach_coo(state, world.player) and can_reach_clean(state, world.player))
set_rule(world.multiworld.get_location(location_name.cloudy_park_piyokeko, world.player), set_rule(world.multiworld.get_location(LocationName.cloudy_park_piyokeko, world.player),
lambda state: can_reach_needle(state, world.player)) lambda state: can_reach_needle(state, world.player))
set_rule(world.multiworld.get_location(location_name.cloudy_park_mikarin, world.player), set_rule(world.multiworld.get_location(LocationName.cloudy_park_mikarin, world.player),
lambda state: can_reach_coo(state, world.player)) lambda state: can_reach_coo(state, world.player))
set_rule(world.multiworld.get_location(location_name.cloudy_park_pick, world.player), set_rule(world.multiworld.get_location(LocationName.cloudy_park_pick, world.player),
lambda state: can_reach_rick(state, world.player)) lambda state: can_reach_rick(state, world.player))
# Level 5 # Level 5
set_rule(world.multiworld.get_location(location_name.iceberg_4, world.player), set_rule(world.multiworld.get_location(LocationName.iceberg_4, world.player),
lambda state: can_reach_burning(state, world.player)) lambda state: can_reach_burning(state, world.player))
set_rule(world.multiworld.get_location(location_name.iceberg_kogoesou, world.player), set_rule(world.multiworld.get_location(LocationName.iceberg_kogoesou, world.player),
lambda state: can_reach_burning(state, world.player)) lambda state: can_reach_burning(state, world.player))
set_rule(world.multiworld.get_location(location_name.iceberg_samus, world.player), set_rule(world.multiworld.get_location(LocationName.iceberg_samus, world.player),
lambda state: can_reach_ice(state, world.player)) lambda state: can_reach_ice(state, world.player))
set_rule(world.multiworld.get_location(location_name.iceberg_name, world.player), set_rule(world.multiworld.get_location(LocationName.iceberg_name, world.player),
lambda state: (can_reach_coo(state, world.player) and lambda state: (can_reach_coo(state, world.player) and
can_reach_burning(state, world.player) and can_reach_burning(state, world.player) and
can_reach_chuchu(state, world.player))) can_reach_chuchu(state, world.player)))
# ChuChu is guaranteed here, but we use this for consistency # ChuChu is guaranteed here, but we use this for consistency
set_rule(world.multiworld.get_location(location_name.iceberg_shiro, world.player), set_rule(world.multiworld.get_location(LocationName.iceberg_shiro, world.player),
lambda state: can_reach_nago(state, world.player)) lambda state: can_reach_nago(state, world.player))
set_rule(world.multiworld.get_location(location_name.iceberg_angel, world.player), set_rule(world.multiworld.get_location(LocationName.iceberg_angel, world.player),
lambda state: can_fix_angel_wings(state, world.player, world.copy_abilities)) lambda state: can_fix_angel_wings(state, world.player, world.copy_abilities))
# Consumables # Consumables
if world.options.consumables: if world.options.consumables:
set_rule(world.multiworld.get_location(location_name.grass_land_1_u1, world.player), set_rule(world.multiworld.get_location(LocationName.grass_land_1_u1, world.player),
lambda state: can_reach_parasol(state, world.player)) lambda state: can_reach_parasol(state, world.player))
set_rule(world.multiworld.get_location(location_name.grass_land_1_m1, world.player), set_rule(world.multiworld.get_location(LocationName.grass_land_1_m1, world.player),
lambda state: can_reach_spark(state, world.player)) lambda state: can_reach_spark(state, world.player))
set_rule(world.multiworld.get_location(location_name.grass_land_2_u1, world.player), set_rule(world.multiworld.get_location(LocationName.grass_land_2_u1, world.player),
lambda state: can_reach_needle(state, world.player)) lambda state: can_reach_needle(state, world.player))
set_rule(world.multiworld.get_location(location_name.ripple_field_2_u1, world.player), set_rule(world.multiworld.get_location(LocationName.ripple_field_2_u1, world.player),
lambda state: can_reach_kine(state, world.player)) lambda state: can_reach_kine(state, world.player))
set_rule(world.multiworld.get_location(location_name.ripple_field_2_m1, world.player), set_rule(world.multiworld.get_location(LocationName.ripple_field_2_m1, world.player),
lambda state: can_reach_kine(state, world.player)) lambda state: can_reach_kine(state, world.player))
set_rule(world.multiworld.get_location(location_name.ripple_field_3_u1, world.player), set_rule(world.multiworld.get_location(LocationName.ripple_field_3_u1, world.player),
lambda state: can_reach_cutter(state, world.player) or can_reach_spark(state, world.player)) lambda state: can_reach_cutter(state, world.player) or can_reach_spark(state, world.player))
set_rule(world.multiworld.get_location(location_name.ripple_field_4_u1, world.player), set_rule(world.multiworld.get_location(LocationName.ripple_field_4_u1, world.player),
lambda state: can_reach_stone(state, world.player)) lambda state: can_reach_stone(state, world.player))
set_rule(world.multiworld.get_location(location_name.ripple_field_4_m2, world.player), set_rule(world.multiworld.get_location(LocationName.ripple_field_4_m2, world.player),
lambda state: can_reach_stone(state, world.player)) lambda state: can_reach_stone(state, world.player))
set_rule(world.multiworld.get_location(location_name.ripple_field_5_m1, world.player), set_rule(world.multiworld.get_location(LocationName.ripple_field_5_m1, world.player),
lambda state: can_reach_kine(state, world.player)) lambda state: can_reach_kine(state, world.player))
set_rule(world.multiworld.get_location(location_name.ripple_field_5_u1, world.player), set_rule(world.multiworld.get_location(LocationName.ripple_field_5_u1, world.player),
lambda state: (can_reach_kine(state, world.player) and lambda state: (can_reach_kine(state, world.player) and
can_reach_burning(state, world.player) and can_reach_burning(state, world.player) and
can_reach_stone(state, world.player))) can_reach_stone(state, world.player)))
set_rule(world.multiworld.get_location(location_name.ripple_field_5_m2, world.player), set_rule(world.multiworld.get_location(LocationName.ripple_field_5_m2, world.player),
lambda state: (can_reach_kine(state, world.player) and lambda state: (can_reach_kine(state, world.player) and
can_reach_burning(state, world.player) and can_reach_burning(state, world.player) and
can_reach_stone(state, world.player))) can_reach_stone(state, world.player)))
set_rule(world.multiworld.get_location(location_name.sand_canyon_4_u1, world.player), set_rule(world.multiworld.get_location(LocationName.sand_canyon_4_u1, world.player),
lambda state: can_reach_clean(state, world.player)) lambda state: can_reach_clean(state, world.player))
set_rule(world.multiworld.get_location(location_name.sand_canyon_4_m2, world.player), set_rule(world.multiworld.get_location(LocationName.sand_canyon_4_m2, world.player),
lambda state: can_reach_needle(state, world.player)) lambda state: can_reach_needle(state, world.player))
set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u2, world.player), set_rule(world.multiworld.get_location(LocationName.sand_canyon_5_u2, world.player),
lambda state: can_reach_ice(state, world.player) and lambda state: can_reach_ice(state, world.player) and
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player) (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_chuchu(state, world.player) or can_reach_pitch(state, world.player)
or can_reach_nago(state, world.player))) or can_reach_nago(state, world.player)))
set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u3, world.player), set_rule(world.multiworld.get_location(LocationName.sand_canyon_5_u3, world.player),
lambda state: can_reach_ice(state, world.player) and lambda state: can_reach_ice(state, world.player) and
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player) (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_chuchu(state, world.player) or can_reach_pitch(state, world.player)
or can_reach_nago(state, world.player))) or can_reach_nago(state, world.player)))
set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u4, world.player), set_rule(world.multiworld.get_location(LocationName.sand_canyon_5_u4, world.player),
lambda state: can_reach_ice(state, world.player) and lambda state: can_reach_ice(state, world.player) and
(can_reach_rick(state, world.player) or can_reach_coo(state, world.player) (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_chuchu(state, world.player) or can_reach_pitch(state, world.player)
or can_reach_nago(state, world.player))) or can_reach_nago(state, world.player)))
set_rule(world.multiworld.get_location(location_name.cloudy_park_6_u1, world.player), set_rule(world.multiworld.get_location(LocationName.cloudy_park_6_u1, world.player),
lambda state: can_reach_cutter(state, world.player)) lambda state: can_reach_cutter(state, world.player))
if world.options.starsanity: if world.options.starsanity:
@@ -274,57 +274,50 @@ def set_rules(world: "KDL3World") -> None:
# copy ability access edge cases # 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 # 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 # and eaten by inhaling while falling on top of them
set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_2_E3, world.player), set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_2_E3, world.player),
lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_3_E6, world.player), set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_3_E6, world.player),
lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, 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 # Ripple Field 4 E5, E7, and E8 are doable, but too strict to leave in logic
set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_4_E5, world.player), set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_4_E5, world.player),
lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_4_E7, world.player), set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_4_E7, world.player),
lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_4_E8, world.player), set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_4_E8, world.player),
lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_5_E1, world.player), set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_5_E1, world.player),
lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_5_E2, world.player), set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_5_E2, world.player),
lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_5_E3, world.player), set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_5_E3, world.player),
lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_5_E4, world.player), set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_5_E4, world.player),
lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
set_rule(world.multiworld.get_location(enemy_abilities.Sand_Canyon_4_E7, world.player), set_rule(world.multiworld.get_location(EnemyAbilities.Sand_Canyon_4_E7, world.player),
lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
set_rule(world.multiworld.get_location(enemy_abilities.Sand_Canyon_4_E8, world.player), set_rule(world.multiworld.get_location(EnemyAbilities.Sand_Canyon_4_E8, world.player),
lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
set_rule(world.multiworld.get_location(enemy_abilities.Sand_Canyon_4_E9, world.player), set_rule(world.multiworld.get_location(EnemyAbilities.Sand_Canyon_4_E9, world.player),
lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player))
set_rule(world.multiworld.get_location(enemy_abilities.Sand_Canyon_4_E10, world.player), set_rule(world.multiworld.get_location(EnemyAbilities.Sand_Canyon_4_E10, world.player),
lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, 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", for boss_flag, purification, i in zip(["Level 1 Boss - Purified", "Level 2 Boss - Purified",
"Level 3 Boss - Purified", "Level 4 Boss - Purified", "Level 3 Boss - Purified", "Level 4 Boss - Purified",
"Level 5 Boss - Purified"], "Level 5 Boss - Purified"],
[location_name.grass_land_whispy, location_name.ripple_field_acro, [LocationName.grass_land_whispy, LocationName.ripple_field_acro,
location_name.sand_canyon_poncon, location_name.cloudy_park_ado, LocationName.sand_canyon_poncon, LocationName.cloudy_park_ado,
location_name.iceberg_dedede], LocationName.iceberg_dedede],
range(1, 6)): range(1, 6)):
set_rule(world.multiworld.get_location(boss_flag, world.player), set_rule(world.multiworld.get_location(boss_flag, world.player),
lambda state, x=i: (state.has("Heart Star", world.player, world.boss_requirements[x - 1]) lambda state, i=i: (state.has("Heart Star", world.player, world.boss_requirements[i - 1])
and can_reach_boss(state, world.player, x, and can_reach_boss(state, world.player, i,
world.options.open_world.value, world.options.open_world.value,
world.options.ow_boss_requirement.value, world.options.ow_boss_requirement.value,
world.player_levels))) world.player_levels)))
set_rule(world.multiworld.get_location(purification, world.player), set_rule(world.multiworld.get_location(purification, world.player),
lambda state, x=i: (state.has("Heart Star", world.player, world.boss_requirements[x - 1]) lambda state, i=i: (state.has("Heart Star", world.player, world.boss_requirements[i - 1])
and can_reach_boss(state, world.player, x, and can_reach_boss(state, world.player, i,
world.options.open_world.value, world.options.open_world.value,
world.options.ow_boss_requirement.value, world.options.ow_boss_requirement.value,
world.player_levels))) world.player_levels)))
@@ -334,12 +327,12 @@ def set_rules(world: "KDL3World") -> None:
for level in range(2, 6): for level in range(2, 6):
set_rule(world.multiworld.get_entrance(f"To Level {level}", world.player), set_rule(world.multiworld.get_entrance(f"To Level {level}", world.player),
lambda state, x=level: state.has(f"Level {x - 1} Boss Defeated", world.player)) lambda state, i=level: state.has(f"Level {i - 1} Boss Defeated", world.player))
if world.options.strict_bosses: if world.options.strict_bosses:
for level in range(2, 6): for level in range(2, 6):
add_rule(world.multiworld.get_entrance(f"To Level {level}", world.player), add_rule(world.multiworld.get_entrance(f"To Level {level}", world.player),
lambda state, x=level: state.has(f"Level {x - 1} Boss Purified", world.player)) lambda state, i=level: state.has(f"Level {i - 1} Boss Purified", world.player))
if world.options.goal_speed == GoalSpeed.option_normal: if world.options.goal_speed == GoalSpeed.option_normal:
add_rule(world.multiworld.get_entrance("To Level 6", world.player), add_rule(world.multiworld.get_entrance("To Level 6", world.player),

View File

@@ -1,25 +1,25 @@
import logging import logging
import typing
from BaseClasses import Tutorial, ItemClassification, MultiWorld, CollectionState, Item from BaseClasses import Tutorial, ItemClassification, MultiWorld
from Fill import fill_restrictive from Fill import fill_restrictive
from Options import PerGameCommonOptions from Options import PerGameCommonOptions
from worlds.AutoWorld import World, WebWorld from worlds.AutoWorld import World, WebWorld
from .items import item_table, item_names, copy_ability_table, animal_friend_table, filler_item_weights, KDL3Item, \ 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,\ trap_item_table, copy_ability_access_table, star_item_weights, total_filler_weights
lookup_item_to_id from .Locations import location_table, KDL3Location, level_consumables, consumable_locations, star_locations
from .locations import location_table, KDL3Location, level_consumables, consumable_locations, star_locations from .Names.AnimalFriendSpawns import animal_friend_spawns
from .names.animal_friend_spawns import animal_friend_spawns, problematic_sets from .Names.EnemyAbilities import vanilla_enemies, enemy_mapping, enemy_restrictive
from .names.enemy_abilities import vanilla_enemies, enemy_mapping, enemy_restrictive from .Regions import create_levels, default_levels
from .regions import create_levels, default_levels from .Options import KDL3Options
from .options import KDL3Options, kdl3_option_groups from .Presets import kdl3_options_presets
from .presets import kdl3_options_presets from .Names import LocationName
from .names import location_name from .Room import KDL3Room
from .room import KDL3Room from .Rules import set_rules
from .rules import set_rules from .Rom import KDL3DeltaPatch, get_base_rom_path, RomData, patch_rom, KDL3JHASH, KDL3UHASH
from .rom import KDL3ProcedurePatch, get_base_rom_path, patch_rom, KDL3JHASH, KDL3UHASH from .Client import KDL3SNIClient
from .client import KDL3SNIClient
from typing import Dict, TextIO, Optional, List, Any, Mapping, ClassVar, Type from typing import Dict, TextIO, Optional, List
import os import os
import math import math
import threading import threading
@@ -53,7 +53,6 @@ class KDL3WebWorld(WebWorld):
) )
] ]
options_presets = kdl3_options_presets options_presets = kdl3_options_presets
option_groups = kdl3_option_groups
class KDL3World(World): class KDL3World(World):
@@ -62,35 +61,35 @@ class KDL3World(World):
""" """
game = "Kirby's Dream Land 3" game = "Kirby's Dream Land 3"
options_dataclass: ClassVar[Type[PerGameCommonOptions]] = KDL3Options options_dataclass: typing.ClassVar[typing.Type[PerGameCommonOptions]] = KDL3Options
options: KDL3Options options: KDL3Options
item_name_to_id = lookup_item_to_id item_name_to_id = {item: item_table[item].code for item in item_table}
location_name_to_id = {location_table[location]: location for location in location_table} location_name_to_id = {location_table[location]: location for location in location_table}
item_name_groups = item_names item_name_groups = item_names
web = KDL3WebWorld() web = KDL3WebWorld()
settings: ClassVar[KDL3Settings] settings: typing.ClassVar[KDL3Settings]
def __init__(self, multiworld: MultiWorld, player: int): def __init__(self, multiworld: MultiWorld, player: int):
self.rom_name: bytes = bytes() self.rom_name = None
self.rom_name_available_event = threading.Event() self.rom_name_available_event = threading.Event()
super().__init__(multiworld, player) super().__init__(multiworld, player)
self.copy_abilities: Dict[str, str] = vanilla_enemies.copy() self.copy_abilities: Dict[str, str] = vanilla_enemies.copy()
self.required_heart_stars: int = 0 # we fill this during create_items self.required_heart_stars: int = 0 # we fill this during create_items
self.boss_requirements: List[int] = [] self.boss_requirements: Dict[int, int] = dict()
self.player_levels = default_levels.copy() self.player_levels = default_levels.copy()
self.stage_shuffle_enabled = False self.stage_shuffle_enabled = False
self.boss_butch_bosses: List[Optional[bool]] = [] self.boss_butch_bosses: List[Optional[bool]] = list()
self.rooms: List[KDL3Room] = [] 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}")
create_regions = create_levels create_regions = create_levels
def generate_early(self) -> None: def create_item(self, name: str, force_non_progression=False) -> KDL3Item:
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] item = item_table[name]
classification = ItemClassification.filler classification = ItemClassification.filler
if item.progression and not force_non_progression: if item.progression and not force_non_progression:
@@ -100,7 +99,7 @@ class KDL3World(World):
classification = ItemClassification.trap classification = ItemClassification.trap
return KDL3Item(name, classification, item.code, self.player) return KDL3Item(name, classification, item.code, self.player)
def get_filler_item_name(self, include_stars: bool = True) -> str: def get_filler_item_name(self, include_stars=True) -> str:
if include_stars: if include_stars:
return self.random.choices(list(total_filler_weights.keys()), return self.random.choices(list(total_filler_weights.keys()),
weights=list(total_filler_weights.values()))[0] weights=list(total_filler_weights.values()))[0]
@@ -113,8 +112,8 @@ class KDL3World(World):
self.options.slow_trap_weight.value, self.options.slow_trap_weight.value,
self.options.ability_trap_weight.value])[0] self.options.ability_trap_weight.value])[0]
def get_restrictive_copy_ability_placement(self, copy_ability: str, enemies_to_set: List[str], def get_restrictive_copy_ability_placement(self, copy_ability: str, enemies_to_set: typing.List[str],
level: int, stage: int) -> Optional[str]: level: int, stage: int):
valid_rooms = [room for room in self.rooms if (room.level < level) 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 or (room.level == level and room.stage < stage)] # leave out the stage in question to avoid edge
valid_enemies = set() valid_enemies = set()
@@ -125,10 +124,6 @@ class KDL3World(World):
return None # a valid enemy got placed by a more restrictive placement 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])) 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: def pre_fill(self) -> None:
if self.options.copy_ability_randomization: if self.options.copy_ability_randomization:
# randomize copy abilities # randomize copy abilities
@@ -201,40 +196,21 @@ class KDL3World(World):
else: else:
animal_base = ["Rick Spawn", "Kine Spawn", "Coo Spawn", "Nago Spawn", "ChuChu Spawn", "Pitch Spawn"] animal_base = ["Rick Spawn", "Kine Spawn", "Coo Spawn", "Nago Spawn", "ChuChu Spawn", "Pitch Spawn"]
animal_pool = [self.random.choice(animal_base) animal_pool = [self.random.choice(animal_base)
for _ in range(len(animal_friend_spawns) - 10)] for _ in range(len(animal_friend_spawns) - 9)]
# have to guarantee one of each animal # have to guarantee one of each animal
animal_pool.extend(animal_base) animal_pool.extend(animal_base)
if guaranteed_animal == "Kine Spawn": if guaranteed_animal == "Kine Spawn":
animal_pool.append("Coo Spawn") animal_pool.append("Coo Spawn")
else: else:
animal_pool.append("Kine Spawn") 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] locations = [self.multiworld.get_location(spawn, self.player) for spawn in spawns]
items: List[Item] = [self.create_item(animal) for animal in animal_pool] items = [self.create_item(animal) for animal in animal_pool]
allstate = CollectionState(self.multiworld) allstate = self.multiworld.get_all_state(False)
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) self.random.shuffle(locations)
fill_restrictive(self.multiworld, allstate, locations, items, True, True) 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: else:
animal_friends = animal_friend_spawns.copy() animal_friends = animal_friend_spawns.copy()
for animal in animal_friends: for animal in animal_friends:
@@ -249,20 +225,21 @@ class KDL3World(World):
remaining_items = len(location_table) - len(itempool) remaining_items = len(location_table) - len(itempool)
if not self.options.consumables: if not self.options.consumables:
remaining_items -= len(consumable_locations) remaining_items -= len(consumable_locations)
if not self.options.starsanity: remaining_items -= len(star_locations)
remaining_items -= len(star_locations) if self.options.starsanity:
max_heart_stars = self.options.max_heart_stars.value # star fill, keep consumable pool locked to consumable and fill 767 stars specifically
if max_heart_stars > remaining_items: star_items = list(star_item_weights.keys())
max_heart_stars = remaining_items 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
# ensure at least 1 heart star required per world # ensure at least 1 heart star required per world
required_heart_stars = min(max(int(max_heart_stars * required_percentage), 5), 99) required_heart_stars = max(int(total_heart_stars * required_percentage), 5)
filler_items = remaining_items - required_heart_stars filler_items = total_heart_stars - required_heart_stars
converted_heart_stars = math.floor((max_heart_stars - required_heart_stars) * (self.options.filler_percentage / 100.0)) filler_amount = math.floor(filler_items * (self.options.filler_percentage / 100.0))
non_required_heart_stars = max_heart_stars - converted_heart_stars - required_heart_stars trap_amount = math.floor(filler_amount * (self.options.trap_percentage / 100.0))
filler_items -= non_required_heart_stars filler_amount -= trap_amount
trap_amount = math.floor(filler_items * (self.options.trap_percentage / 100.0)) non_required_heart_stars = filler_items - filler_amount - trap_amount
filler_items -= trap_amount
self.required_heart_stars = required_heart_stars self.required_heart_stars = required_heart_stars
# handle boss requirements here # handle boss requirements here
requirements = [required_heart_stars] requirements = [required_heart_stars]
@@ -284,8 +261,8 @@ class KDL3World(World):
requirements.insert(i - 1, quotient * i) requirements.insert(i - 1, quotient * i)
self.boss_requirements = requirements self.boss_requirements = requirements
itempool.extend([self.create_item("Heart Star") for _ in range(required_heart_stars)]) itempool.extend([self.create_item("Heart Star") for _ in range(required_heart_stars)])
itempool.extend([self.create_item(self.get_filler_item_name(bool(self.options.starsanity.value))) itempool.extend([self.create_item(self.get_filler_item_name(False))
for _ in range(filler_items)]) for _ in range(filler_amount + (remaining_items - total_heart_stars))])
itempool.extend([self.create_item(self.get_trap_item_name()) itempool.extend([self.create_item(self.get_trap_item_name())
for _ in range(trap_amount)]) for _ in range(trap_amount)])
itempool.extend([self.create_item("Heart Star", True) for _ in range(non_required_heart_stars)]) itempool.extend([self.create_item("Heart Star", True) for _ in range(non_required_heart_stars)])
@@ -296,15 +273,15 @@ class KDL3World(World):
self.multiworld.get_location(location_table[self.player_levels[level][stage]] self.multiworld.get_location(location_table[self.player_levels[level][stage]]
.replace("Complete", "Stage Completion"), self.player) \ .replace("Complete", "Stage Completion"), self.player) \
.place_locked_item(KDL3Item( .place_locked_item(KDL3Item(
f"{location_name.level_names_inverse[level]} - Stage Completion", f"{LocationName.level_names_inverse[level]} - Stage Completion",
ItemClassification.progression, None, self.player)) ItemClassification.progression, None, self.player))
set_rules = set_rules set_rules = set_rules
def generate_basic(self) -> None: def generate_basic(self) -> None:
self.stage_shuffle_enabled = self.options.stage_shuffle > 0 self.stage_shuffle_enabled = self.options.stage_shuffle > 0
goal = self.options.goal.value goal = self.options.goal
goal_location = self.multiworld.get_location(location_name.goals[goal], self.player) goal_location = self.multiworld.get_location(LocationName.goals[goal], self.player)
goal_location.place_locked_item(KDL3Item("Love-Love Rod", ItemClassification.progression, None, self.player)) goal_location.place_locked_item(KDL3Item("Love-Love Rod", ItemClassification.progression, None, self.player))
for level in range(1, 6): for level in range(1, 6):
self.multiworld.get_location(f"Level {level} Boss - Defeated", self.player) \ self.multiworld.get_location(f"Level {level} Boss - Defeated", self.player) \
@@ -323,65 +300,60 @@ class KDL3World(World):
else: else:
self.boss_butch_bosses = [False for _ in range(6)] self.boss_butch_bosses = [False for _ in range(6)]
def generate_output(self, output_directory: str) -> None: def generate_output(self, output_directory: str):
rom_path = ""
try: try:
patch = KDL3ProcedurePatch() rom = RomData(get_base_rom_path())
patch_rom(self, patch) patch_rom(self, rom)
self.rom_name = patch.name 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
patch.write(os.path.join(output_directory, patch = KDL3DeltaPatch(os.path.splitext(rom_path)[0] + KDL3DeltaPatch.patch_file_ending, player=self.player,
f"{self.multiworld.get_out_file_name_base(self.player)}{patch.patch_file_ending}")) player_name=self.multiworld.player_name[self.player], patched_path=rom_path)
patch.write()
except Exception: except Exception:
raise raise
finally: finally:
self.rom_name_available_event.set() # make sure threading continues and errors are collected 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[str, Any]) -> None: def modify_multidata(self, multidata: dict):
# wait for self.rom_name to be available. # wait for self.rom_name to be available.
self.rom_name_available_event.wait() self.rom_name_available_event.wait()
assert isinstance(self.rom_name, bytes)
rom_name = getattr(self, "rom_name", None) 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 # we skip in case of error, so that the original error in the output thread is the one that gets raised
if rom_name: if rom_name:
new_name = base64.b64encode(self.rom_name).decode() new_name = base64.b64encode(bytes(self.rom_name)).decode()
multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]] 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: def write_spoiler(self, spoiler_handle: TextIO) -> None:
if self.stage_shuffle_enabled: if self.stage_shuffle_enabled:
spoiler_handle.write(f"\nLevel Layout ({self.multiworld.get_player_name(self.player)}):\n") spoiler_handle.write(f"\nLevel Layout ({self.multiworld.get_player_name(self.player)}):\n")
for level in location_name.level_names: for level in LocationName.level_names:
for stage, i in zip(self.player_levels[location_name.level_names[level]], range(1, 7)): for stage, i in zip(self.player_levels[LocationName.level_names[level]], range(1, 7)):
spoiler_handle.write(f"{level} {i}: {location_table[stage].replace(' - Complete', '')}\n") spoiler_handle.write(f"{level} {i}: {location_table[stage].replace(' - Complete', '')}\n")
if self.options.animal_randomization: if self.options.animal_randomization:
spoiler_handle.write(f"\nAnimal Friends ({self.multiworld.get_player_name(self.player)}):\n") spoiler_handle.write(f"\nAnimal Friends ({self.multiworld.get_player_name(self.player)}):\n")
for lvl in self.player_levels: for level in self.player_levels:
for stage in range(6): for stage in range(6):
rooms = [room for room in self.rooms if room.level == lvl and room.stage == stage] rooms = [room for room in self.rooms if room.level == level and room.stage == stage]
animals = [] animals = []
for room in rooms: for room in rooms:
animals.extend([location.item.name.replace(" Spawn", "") animals.extend([location.item.name.replace(" Spawn", "")
for location in room.locations if "Animal" in location.name 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[level][stage]].replace(' - Complete','')}"
spoiler_handle.write(f"{location_table[self.player_levels[lvl][stage]].replace(' - Complete','')}"
f": {', '.join(animals)}\n") f": {', '.join(animals)}\n")
if self.options.copy_ability_randomization: if self.options.copy_ability_randomization:
spoiler_handle.write(f"\nCopy Abilities ({self.multiworld.get_player_name(self.player)}):\n") spoiler_handle.write(f"\nCopy Abilities ({self.multiworld.get_player_name(self.player)}):\n")
for enemy in self.copy_abilities: for enemy in self.copy_abilities:
spoiler_handle.write(f"{enemy}: {self.copy_abilities[enemy].replace('No Ability', 'None').replace(' Ability', '')}\n") 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]]) -> None: def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]):
if self.stage_shuffle_enabled: if self.stage_shuffle_enabled:
regions = {location_name.level_names[level]: level for level in location_name.level_names} regions = {LocationName.level_names[level]: level for level in LocationName.level_names}
level_hint_data = {} level_hint_data = {}
for level in regions: for level in regions:
for stage in range(7): for stage in range(7):
@@ -389,6 +361,6 @@ class KDL3World(World):
self.player).name.replace(" - Complete", "") self.player).name.replace(" - Complete", "")
stage_regions = [room for room in self.rooms if stage_name in room.name] stage_regions = [room for room in self.rooms if stage_name in room.name]
for region in stage_regions: for region in stage_regions:
for location in [location for location in list(region.get_locations()) if location.address]: for location in [location for location in region.locations if location.address]:
level_hint_data[location.address] = f"{regions[level]} {stage + 1 if stage < 6 else 'Boss'}" level_hint_data[location.address] = f"{regions[level]} {stage + 1 if stage < 6 else 'Boss'}"
hint_data[self.player] = level_hint_data hint_data[self.player] = level_hint_data

View File

@@ -1,940 +0,0 @@
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
}

View File

@@ -1,602 +0,0 @@
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

View File

@@ -1,133 +0,0 @@
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"))

View File

@@ -58,10 +58,6 @@ org $01AFC8
org $01B013 org $01B013
SEC ; Remove Dedede Bad Ending SEC ; Remove Dedede Bad Ending
org $01B050
JSL HookBossPurify
NOP
org $02B7B0 ; Zero unlock org $02B7B0 ; Zero unlock
LDA $80A0 LDA $80A0
CMP #$0001 CMP #$0001
@@ -164,6 +160,7 @@ CopyAbilityAnimalOverride:
STA $39DF, X STA $39DF, X
RTL RTL
org $079A00
HeartStarCheck: HeartStarCheck:
TXA TXA
CMP #$0000 ; is this level 1 CMP #$0000 ; is this level 1
@@ -204,6 +201,7 @@ HeartStarCheck:
SEC SEC
RTL RTL
org $079A80
OpenWorldUnlock: OpenWorldUnlock:
PHX PHX
LDX $900E ; Are we on open world? LDX $900E ; Are we on open world?
@@ -226,6 +224,7 @@ OpenWorldUnlock:
PLX PLX
RTL RTL
org $079B00
MainLoopHook: MainLoopHook:
STA $D4 STA $D4
INC $3524 INC $3524
@@ -240,18 +239,16 @@ MainLoopHook:
BEQ .Return ; return if we are BEQ .Return ; return if we are
LDA $5541 ; gooey status LDA $5541 ; gooey status
BPL .Slowness ; gooey is already spawned BPL .Slowness ; gooey is already spawned
LDA $39D1 ; is kirby alive?
BEQ .Slowness ; branch if he isn't
; maybe BMI here too?
LDA $8080 LDA $8080
CMP #$0000 ; did we get a gooey trap CMP #$0000 ; did we get a gooey trap
BEQ .Slowness ; branch if we did not BEQ .Slowness ; branch if we did not
JSL GooeySpawn JSL GooeySpawn
DEC $8080 STZ $8080
.Slowness: .Slowness:
LDA $8082 ; slowness LDA $8082 ; slowness
BEQ .Eject ; are we under the effects of a slowness trap BEQ .Eject ; are we under the effects of a slowness trap
DEC $8082 ; dec by 1 each frame DEC
STA $8082 ; dec by 1 each frame
.Eject: .Eject:
PHX PHX
PHY PHY
@@ -261,13 +258,14 @@ MainLoopHook:
BEQ .PullVars ; branch if we haven't received eject BEQ .PullVars ; branch if we haven't received eject
LDA #$2000 ; select button press LDA #$2000 ; select button press
STA $60C1 ; write to controller mirror STA $60C1 ; write to controller mirror
DEC $8084 STZ $8084
.PullVars: .PullVars:
PLY PLY
PLX PLX
.Return: .Return:
RTL RTL
org $079B80
HeartStarGraphicFix: HeartStarGraphicFix:
LDA #$0000 LDA #$0000
PHX PHX
@@ -290,7 +288,7 @@ HeartStarGraphicFix:
ASL ASL
TAX TAX
LDA $07D080, X ; table of original stage number LDA $07D080, X ; table of original stage number
CMP #$0002 ; is the current stage a minigame stage? CMP #$0003 ; is the current stage a minigame stage?
BEQ .ReturnTrue ; branch if so BEQ .ReturnTrue ; branch if so
CLC CLC
BRA .Return BRA .Return
@@ -301,6 +299,7 @@ HeartStarGraphicFix:
PLX PLX
RTL RTL
org $079BF0
ParseItemQueue: ParseItemQueue:
; Local item queue parsing ; Local item queue parsing
NOP NOP
@@ -337,6 +336,8 @@ ParseItemQueue:
AND #$000F AND #$000F
ASL ASL
TAY TAY
LDA $8080,Y
BNE .LoopCheck
JSL .ApplyNegative JSL .ApplyNegative
RTL RTL
.ApplyAbility: .ApplyAbility:
@@ -417,73 +418,35 @@ ParseItemQueue:
CPY #$0005 CPY #$0005
BCS .PlayNone BCS .PlayNone
LDA $8080,Y LDA $8080,Y
CPY #$0002 BNE .Return
BNE .Increment
CLC
LDA #$0384 LDA #$0384
ADC $8080, Y
BVC .PlayNegative
LDA #$FFFF
.PlayNegative:
STA $8080,Y STA $8080,Y
LDA #$00A7 LDA #$00A7
BRA .PlaySFXLong BRA .PlaySFXLong
.Increment:
INC
STA $8080, Y
BRA .PlayNegative
.PlayNone: .PlayNone:
LDA #$0000 LDA #$0000
BRA .PlaySFXLong BRA .PlaySFXLong
org $079D00
AnimalFriendSpawn: AnimalFriendSpawn:
PHA PHA
CPX #$0002 ; is this an animal friend? CPX #$0002 ; is this an animal friend?
BNE .Return BNE .Return
XBA XBA
PHA 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 ASL
TAY TAY
PLA PLA
INC INC
CMP $8000, Y ; do we have this animal friend CMP $8000, Y ; do we have this animal friend
BEQ .Return ; we have this animal friend BEQ .Return ; we have this animal friend
.False:
INX INX
.Return: .Return:
PLY PLY
LDA #$9999 LDA #$9999
RTL RTL
.AlreadySpawned:
PLA
PLX
ASL
TAY
PLA
BRA .False
org $079E00
WriteBWRAM: WriteBWRAM:
LDY #$6001 ;starting addr LDY #$6001 ;starting addr
LDA #$1FFE ;bytes to write LDA #$1FFE ;bytes to write
@@ -516,6 +479,7 @@ WriteBWRAM:
.Return: .Return:
RTL RTL
org $079E80
ConsumableSet: ConsumableSet:
PHA PHA
PHX PHX
@@ -543,6 +507,7 @@ ConsumableSet:
ASL ASL
TAX TAX
LDA $07D020, X ; current stage LDA $07D020, X ; current stage
DEC
ASL #6 ASL #6
TAX TAX
PLA PLA
@@ -554,16 +519,8 @@ ConsumableSet:
BRA .LoopHead ; return to loop head BRA .LoopHead ; return to loop head
.ApplyCheck: .ApplyCheck:
LDA $A000, X ; consumables index LDA $A000, X ; consumables index
PHA
ORA #$0001 ORA #$0001
STA $A000, X STA $A000, X
PLA
AND #$00FF
BNE .Return
TXA
ORA #$1000
JSL ApplyLocalCheck
.Return:
PLY PLY
PLX PLX
PLA PLA
@@ -571,6 +528,7 @@ ConsumableSet:
AND #$00FF AND #$00FF
RTL RTL
org $079F00
NormalGoalSet: NormalGoalSet:
PHX PHX
LDA $07D012 LDA $07D012
@@ -591,6 +549,7 @@ NormalGoalSet:
STA $5AC1 ; cutscene STA $5AC1 ; cutscene
RTL RTL
org $079F80
FinalIcebergFix: FinalIcebergFix:
PHX PHX
PHY PHY
@@ -613,7 +572,7 @@ FinalIcebergFix:
ASL ASL
TAX TAX
LDA $07D020, X LDA $07D020, X
CMP #$001D CMP #$001E
BEQ .ReturnTrue BEQ .ReturnTrue
CLC CLC
BRA .Return BRA .Return
@@ -624,6 +583,7 @@ FinalIcebergFix:
PLX PLX
RTL RTL
org $07A000
StrictBosses: StrictBosses:
PHX PHX
LDA $901E ; Do we have strict bosses enabled? LDA $901E ; Do we have strict bosses enabled?
@@ -650,6 +610,7 @@ StrictBosses:
LDA $53CD LDA $53CD
RTL RTL
org $07A030
NintenHalken: NintenHalken:
LDX #$0005 LDX #$0005
.Halken: .Halken:
@@ -667,6 +628,7 @@ NintenHalken:
LDA #$0001 LDA #$0001
RTL RTL
org $07A080
StageCompleteSet: StageCompleteSet:
PHX PHX
LDA $5AC1 ; completed stage cutscene LDA $5AC1 ; completed stage cutscene
@@ -694,17 +656,9 @@ StageCompleteSet:
ASL ASL
TAX TAX
LDA $9020, X ; load the stage we completed LDA $9020, X ; load the stage we completed
DEC
ASL ASL
TAX TAX
PHX
LDA $8200, X
AND #$00FF
BNE .ApplyClear
TXA
LSR
JSL ApplyLocalCheck
.ApplyClear:
PLX
LDA #$0001 LDA #$0001
ORA $8200, X ORA $8200, X
STA $8200, X STA $8200, X
@@ -714,6 +668,7 @@ StageCompleteSet:
CMP $53CB CMP $53CB
RTL RTL
org $07A100
OpenWorldBossUnlock: OpenWorldBossUnlock:
PHX PHX
PHY PHY
@@ -744,6 +699,7 @@ OpenWorldBossUnlock:
.LoopStage: .LoopStage:
PLX PLX
LDY $9020, X ; get stage id LDY $9020, X ; get stage id
DEY
INX INX
INX INX
PHA PHA
@@ -776,6 +732,7 @@ OpenWorldBossUnlock:
PLX PLX
RTL RTL
org $07A180
GooeySpawn: GooeySpawn:
PHY PHY
PHX PHX
@@ -811,6 +768,7 @@ GooeySpawn:
PLY PLY
RTL RTL
org $07A200
SpeedTrap: SpeedTrap:
PHX PHX
LDX $8082 ; do we have slowness LDX $8082 ; do we have slowness
@@ -822,6 +780,7 @@ SpeedTrap:
EOR #$FFFF EOR #$FFFF
RTL RTL
org $07A280
HeartStarVisual: HeartStarVisual:
CPX #$0000 CPX #$0000
BEQ .SkipInx BEQ .SkipInx
@@ -885,6 +844,7 @@ HeartStarVisual:
.Return: .Return:
RTL RTL
org $07A300
LoadFont: LoadFont:
JSL $00D29F ; play sfx JSL $00D29F ; play sfx
PHX PHX
@@ -955,6 +915,7 @@ LoadFont:
PLX PLX
RTL RTL
org $07A380
HeartStarVisual2: HeartStarVisual2:
LDA #$2C80 LDA #$2C80
STA $0000, Y STA $0000, Y
@@ -1068,12 +1029,14 @@ HeartStarVisual2:
STA $0000, Y STA $0000, Y
RTL RTL
org $07A480
HeartStarSelectFix: HeartStarSelectFix:
PHX PHX
TXA TXA
ASL ASL
TAX TAX
LDA $9020, X LDA $9020, X
DEC
TAX TAX
.LoopHead: .LoopHead:
CMP #$0006 CMP #$0006
@@ -1088,31 +1051,15 @@ HeartStarSelectFix:
AND #$00FF AND #$00FF
RTL RTL
org $07A500
HeartStarCutsceneFix: HeartStarCutsceneFix:
TAX TAX
LDA $53D3 LDA $53D3
DEC DEC
STA $5AC3 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 RTL
org $07A510
GiftGiving: GiftGiving:
CMP #$0008 CMP #$0008
.This: .This:
@@ -1128,6 +1075,7 @@ GiftGiving:
PLX PLX
JML $CABC18 JML $CABC18
org $07A550
PauseMenu: PauseMenu:
JSL $00D29F JSL $00D29F
PHX PHX
@@ -1188,6 +1136,7 @@ PauseMenu:
PLX PLX
RTL RTL
org $07A600
StarsSet: StarsSet:
PHA PHA
PHX PHX
@@ -1217,6 +1166,7 @@ StarsSet:
ASL ASL
TAX TAX
LDA $07D020, X LDA $07D020, X
DEC
ASL ASL
ASL ASL
ASL ASL
@@ -1233,15 +1183,8 @@ StarsSet:
BRA .2LoopHead BRA .2LoopHead
.2LoopEnd: .2LoopEnd:
LDA $B000, X LDA $B000, X
PHA
ORA #$0001 ORA #$0001
STA $B000, X STA $B000, X
PLA
AND #$00FF
BNE .Return
TXA
ORA #$2000
JSL ApplyLocalCheck
.Return: .Return:
PLY PLY
PLX PLX
@@ -1256,48 +1199,6 @@ StarsSet:
STA $39D7 STA $39D7
BRA .Return 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 org $07C000
db "KDL3_BASEPATCH_ARCHI" db "KDL3_BASEPATCH_ARCHI"
@@ -1333,7 +1234,4 @@ org $07E040
db $3A, $01 db $3A, $01
db $3B, $05 db $3B, $05
db $3C, $05 db $3C, $05
db $3D, $05 db $3D, $05
org $07F000
incbin "APPauseIcons.dat"

View File

@@ -6,8 +6,6 @@ from test.bases import WorldTestBase
from test.general import gen_steps from test.general import gen_steps
from worlds import AutoWorld from worlds import AutoWorld
from worlds.AutoWorld import call_all 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): class KDL3TestBase(WorldTestBase):

View File

@@ -5,12 +5,12 @@ class TestFastGoal(KDL3TestBase):
options = { options = {
"open_world": False, "open_world": False,
"goal_speed": "fast", "goal_speed": "fast",
"max_heart_stars": 30, "total_heart_stars": 30,
"heart_stars_required": 50, "heart_stars_required": 50,
"filler_percentage": 0, "filler_percentage": 0,
} }
def test_goal(self) -> None: def test_goal(self):
self.assertBeatable(False) self.assertBeatable(False)
heart_stars = self.get_items_by_name("Heart Star") heart_stars = self.get_items_by_name("Heart Star")
self.collect(heart_stars[0:14]) self.collect(heart_stars[0:14])
@@ -30,12 +30,12 @@ class TestNormalGoal(KDL3TestBase):
options = { options = {
"open_world": False, "open_world": False,
"goal_speed": "normal", "goal_speed": "normal",
"max_heart_stars": 30, "total_heart_stars": 30,
"heart_stars_required": 50, "heart_stars_required": 50,
"filler_percentage": 0, "filler_percentage": 0,
} }
def test_goal(self) -> None: def test_goal(self):
self.assertBeatable(False) self.assertBeatable(False)
heart_stars = self.get_items_by_name("Heart Star") heart_stars = self.get_items_by_name("Heart Star")
self.collect(heart_stars[0:14]) 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.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed))
self.assertBeatable(True) self.assertBeatable(True)
def test_kine(self) -> None: def test_kine(self):
self.collect_by_name(["Cutter", "Burning", "Heart Star"]) self.collect_by_name(["Cutter", "Burning", "Heart Star"])
self.assertBeatable(False) self.assertBeatable(False)
def test_cutter(self) -> None: def test_cutter(self):
self.collect_by_name(["Kine", "Burning", "Heart Star"]) self.collect_by_name(["Kine", "Burning", "Heart Star"])
self.assertBeatable(False) self.assertBeatable(False)
def test_burning(self) -> None: def test_burning(self):
self.collect_by_name(["Cutter", "Kine", "Heart Star"]) self.collect_by_name(["Cutter", "Kine", "Heart Star"])
self.assertBeatable(False) self.assertBeatable(False)

View File

@@ -1,6 +1,6 @@
from . import KDL3TestBase from . import KDL3TestBase
from ..names import location_name
from Options import PlandoConnection from Options import PlandoConnection
from ..Names import LocationName
import typing import typing
@@ -12,31 +12,31 @@ class TestLocations(KDL3TestBase):
# these ensure we can always reach all stages physically # these ensure we can always reach all stages physically
} }
def test_simple_heart_stars(self) -> None: def test_simple_heart_stars(self):
self.run_location_test(location_name.grass_land_muchi, ["ChuChu"]) self.run_location_test(LocationName.grass_land_muchi, ["ChuChu"])
self.run_location_test(location_name.grass_land_chao, ["Stone"]) self.run_location_test(LocationName.grass_land_chao, ["Stone"])
self.run_location_test(location_name.grass_land_mine, ["Kine"]) self.run_location_test(LocationName.grass_land_mine, ["Kine"])
self.run_location_test(location_name.ripple_field_kamuribana, ["Pitch", "Clean"]) self.run_location_test(LocationName.ripple_field_kamuribana, ["Pitch", "Clean"])
self.run_location_test(location_name.ripple_field_bakasa, ["Kine", "Parasol"]) self.run_location_test(LocationName.ripple_field_bakasa, ["Kine", "Parasol"])
self.run_location_test(location_name.ripple_field_toad, ["Needle"]) self.run_location_test(LocationName.ripple_field_toad, ["Needle"])
self.run_location_test(location_name.ripple_field_mama_pitch, ["Pitch", "Kine", "Burning", "Stone"]) self.run_location_test(LocationName.ripple_field_mama_pitch, ["Pitch", "Kine", "Burning", "Stone"])
self.run_location_test(location_name.sand_canyon_auntie, ["Clean"]) self.run_location_test(LocationName.sand_canyon_auntie, ["Clean"])
self.run_location_test(location_name.sand_canyon_nyupun, ["ChuChu", "Cutter"]) self.run_location_test(LocationName.sand_canyon_nyupun, ["ChuChu", "Cutter"])
self.run_location_test(location_name.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Spark", "Ice"]) self.run_location_test(LocationName.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(LocationName.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(LocationName.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(LocationName.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Clean", "Needle"]),
self.run_location_test(location_name.cloudy_park_hibanamodoki, ["Coo", "Clean"]) self.run_location_test(LocationName.cloudy_park_hibanamodoki, ["Coo", "Clean"])
self.run_location_test(location_name.cloudy_park_piyokeko, ["Needle"]) self.run_location_test(LocationName.cloudy_park_piyokeko, ["Needle"])
self.run_location_test(location_name.cloudy_park_mikarin, ["Coo"]) self.run_location_test(LocationName.cloudy_park_mikarin, ["Coo"])
self.run_location_test(location_name.cloudy_park_pick, ["Rick"]) self.run_location_test(LocationName.cloudy_park_pick, ["Rick"])
self.run_location_test(location_name.iceberg_kogoesou, ["Burning"]) self.run_location_test(LocationName.iceberg_kogoesou, ["Burning"])
self.run_location_test(location_name.iceberg_samus, ["Ice"]) self.run_location_test(LocationName.iceberg_samus, ["Ice"])
self.run_location_test(location_name.iceberg_name, ["Burning", "Coo", "ChuChu"]) self.run_location_test(LocationName.iceberg_name, ["Burning", "Coo", "ChuChu"])
self.run_location_test(location_name.iceberg_angel, ["Cutter", "Burning", "Spark", "Parasol", "Needle", "Clean", self.run_location_test(LocationName.iceberg_angel, ["Cutter", "Burning", "Spark", "Parasol", "Needle", "Clean",
"Stone", "Ice"]) "Stone", "Ice"])
def run_location_test(self, location: str, itempool: typing.List[str]) -> None: def run_location_test(self, location: str, itempool: typing.List[str]):
items = itempool.copy() items = itempool.copy()
while len(itempool) > 0: while len(itempool) > 0:
self.assertFalse(self.can_reach_location(location), str(self.multiworld.seed)) self.assertFalse(self.can_reach_location(location), str(self.multiworld.seed))
@@ -57,7 +57,7 @@ class TestShiro(KDL3TestBase):
"plando_options": "connections" "plando_options": "connections"
} }
def test_shiro(self) -> None: def test_shiro(self):
self.assertFalse(self.can_reach_location("Iceberg 5 - Shiro"), str(self.multiworld.seed)) self.assertFalse(self.can_reach_location("Iceberg 5 - Shiro"), str(self.multiworld.seed))
self.collect_by_name("Nago") self.collect_by_name("Nago")
self.assertFalse(self.can_reach_location("Iceberg 5 - Shiro"), str(self.multiworld.seed)) self.assertFalse(self.can_reach_location("Iceberg 5 - Shiro"), str(self.multiworld.seed))

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