Merge branch 'main' into tunc-portal-direction-pairing

This commit is contained in:
Scipio Wright
2024-08-29 09:41:47 -04:00
committed by GitHub
128 changed files with 66528 additions and 11841 deletions

1
.gitattributes vendored Normal file
View File

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

View File

@@ -11,8 +11,10 @@ from argparse import Namespace
from collections import Counter, deque
from collections.abc import Collection, MutableSequence
from enum import IntEnum, IntFlag
from typing import Any, Callable, Dict, Iterable, Iterator, List, Mapping, NamedTuple, Optional, Set, Tuple, \
TypedDict, Union, Type, ClassVar
from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple,
Optional, Protocol, Set, Tuple, Union, Type)
from typing_extensions import NotRequired, TypedDict
import NetUtils
import Options
@@ -22,16 +24,16 @@ if typing.TYPE_CHECKING:
from worlds import AutoWorld
class Group(TypedDict, total=False):
class Group(TypedDict):
name: str
game: str
world: "AutoWorld.World"
players: Set[int]
item_pool: Set[str]
replacement_items: Dict[int, Optional[str]]
local_items: Set[str]
non_local_items: Set[str]
link_replacement: bool
players: AbstractSet[int]
item_pool: NotRequired[Set[str]]
replacement_items: NotRequired[Dict[int, Optional[str]]]
local_items: NotRequired[Set[str]]
non_local_items: NotRequired[Set[str]]
link_replacement: NotRequired[bool]
class ThreadBarrierProxy:
@@ -48,6 +50,11 @@ class ThreadBarrierProxy:
"Please use multiworld.per_slot_randoms[player] or randomize ahead of output.")
class HasNameAndPlayer(Protocol):
name: str
player: int
class MultiWorld():
debug_types = False
player_name: Dict[int, str]
@@ -156,7 +163,7 @@ class MultiWorld():
self.start_inventory_from_pool: Dict[int, Options.StartInventoryPool] = {}
for player in range(1, players + 1):
def set_player_attr(attr, val):
def set_player_attr(attr: str, val) -> None:
self.__dict__.setdefault(attr, {})[player] = val
set_player_attr('plando_items', [])
set_player_attr('plando_texts', {})
@@ -165,13 +172,13 @@ class MultiWorld():
set_player_attr('completion_condition', lambda state: True)
self.worlds = {}
self.per_slot_randoms = Utils.DeprecateDict("Using per_slot_randoms is now deprecated. Please use the "
"world's random object instead (usually self.random)")
"world's random object instead (usually self.random)")
self.plando_options = PlandoOptions.none
def get_all_ids(self) -> Tuple[int, ...]:
return self.player_ids + tuple(self.groups)
def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tuple[int, Group]:
def add_group(self, name: str, game: str, players: AbstractSet[int] = frozenset()) -> Tuple[int, Group]:
"""Create a group with name and return the assigned player ID and group.
If a group of this name already exists, the set of players is extended instead of creating a new one."""
from worlds import AutoWorld
@@ -195,7 +202,7 @@ class MultiWorld():
return new_id, new_group
def get_player_groups(self, player) -> Set[int]:
def get_player_groups(self, player: int) -> Set[int]:
return {group_id for group_id, group in self.groups.items() if player in group["players"]}
def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optional[str] = None):
@@ -258,7 +265,7 @@ class MultiWorld():
"link_replacement": replacement_prio.index(item_link["link_replacement"]),
}
for name, item_link in item_links.items():
for _name, item_link in item_links.items():
current_item_name_groups = AutoWorld.AutoWorldRegister.world_types[item_link["game"]].item_name_groups
pool = set()
local_items = set()
@@ -388,7 +395,7 @@ class MultiWorld():
return tuple(world for player, world in self.worlds.items() if
player not in self.groups and self.game[player] == game_name)
def get_name_string_for_object(self, obj) -> str:
def get_name_string_for_object(self, obj: HasNameAndPlayer) -> str:
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_name(obj.player)})'
def get_player_name(self, player: int) -> str:
@@ -430,7 +437,7 @@ class MultiWorld():
subworld = self.worlds[player]
for item in subworld.get_pre_fill_items():
subworld.collect(ret, item)
ret.sweep_for_events()
ret.sweep_for_advancements()
if use_cache:
self._all_state = ret
@@ -439,7 +446,7 @@ class MultiWorld():
def get_items(self) -> List[Item]:
return [loc.item for loc in self.get_filled_locations()] + self.itempool
def find_item_locations(self, item, player: int, resolve_group_locations: bool = False) -> List[Location]:
def find_item_locations(self, item: str, player: int, resolve_group_locations: bool = False) -> List[Location]:
if resolve_group_locations:
player_groups = self.get_player_groups(player)
return [location for location in self.get_locations() if
@@ -448,7 +455,7 @@ class MultiWorld():
return [location for location in self.get_locations() if
location.item and location.item.name == item and location.item.player == player]
def find_item(self, item, player: int) -> Location:
def find_item(self, item: str, player: int) -> Location:
return next(location for location in self.get_locations() if
location.item and location.item.name == item and location.item.player == player)
@@ -541,9 +548,9 @@ class MultiWorld():
return True
state = starting_state.copy()
else:
if self.has_beaten_game(self.state):
return True
state = CollectionState(self)
if self.has_beaten_game(state):
return True
prog_locations = {location for location in self.get_locations() if location.item
and location.item.advancement and location not in state.locations_checked}
@@ -661,7 +668,7 @@ class CollectionState():
multiworld: MultiWorld
reachable_regions: Dict[int, Set[Region]]
blocked_connections: Dict[int, Set[Entrance]]
events: Set[Location]
advancements: Set[Location]
path: Dict[Union[Region, Entrance], PathValue]
locations_checked: Set[Location]
stale: Dict[int, bool]
@@ -673,7 +680,7 @@ class CollectionState():
self.multiworld = parent
self.reachable_regions = {player: set() for player in parent.get_all_ids()}
self.blocked_connections = {player: set() for player in parent.get_all_ids()}
self.events = set()
self.advancements = set()
self.path = {}
self.locations_checked = set()
self.stale = {player: True for player in parent.get_all_ids()}
@@ -722,7 +729,7 @@ class CollectionState():
self.reachable_regions.items()}
ret.blocked_connections = {player: entrance_set.copy() for player, entrance_set in
self.blocked_connections.items()}
ret.events = self.events.copy()
ret.advancements = self.advancements.copy()
ret.path = self.path.copy()
ret.locations_checked = self.locations_checked.copy()
for function in self.additional_copy_functions:
@@ -755,19 +762,24 @@ class CollectionState():
return self.multiworld.get_region(spot, player).can_reach(self)
def sweep_for_events(self, locations: Optional[Iterable[Location]] = None) -> None:
Utils.deprecate("sweep_for_events has been renamed to sweep_for_advancements. The functionality is the same. "
"Please switch over to sweep_for_advancements.")
return self.sweep_for_advancements(locations)
def sweep_for_advancements(self, locations: Optional[Iterable[Location]] = None) -> None:
if locations is None:
locations = self.multiworld.get_filled_locations()
reachable_events = True
# since the loop has a good chance to run more than once, only filter the events once
locations = {location for location in locations if location.advancement and location not in self.events}
reachable_advancements = True
# since the loop has a good chance to run more than once, only filter the advancements once
locations = {location for location in locations if location.advancement and location not in self.advancements}
while reachable_events:
reachable_events = {location for location in locations if location.can_reach(self)}
locations -= reachable_events
for event in reachable_events:
self.events.add(event)
assert isinstance(event.item, Item), "tried to collect Event with no Item"
self.collect(event.item, True, event)
while reachable_advancements:
reachable_advancements = {location for location in locations if location.can_reach(self)}
locations -= reachable_advancements
for advancement in reachable_advancements:
self.advancements.add(advancement)
assert isinstance(advancement.item, Item), "tried to collect Event with no Item"
self.collect(advancement.item, True, advancement)
# item name related
def has(self, item: str, player: int, count: int = 1) -> bool:
@@ -801,7 +813,7 @@ class CollectionState():
if found >= count:
return True
return False
def has_from_list_unique(self, items: Iterable[str], player: int, count: int) -> bool:
"""Returns True if the state contains at least `count` items matching any of the item names from a list.
Ignores duplicates of the same item."""
@@ -816,7 +828,7 @@ class CollectionState():
def count_from_list(self, items: Iterable[str], player: int) -> int:
"""Returns the cumulative count of items from a list present in state."""
return sum(self.prog_items[player][item_name] for item_name in items)
def count_from_list_unique(self, items: Iterable[str], player: int) -> int:
"""Returns the cumulative count of items from a list present in state. Ignores duplicates of the same item."""
return sum(self.prog_items[player][item_name] > 0 for item_name in items)
@@ -871,7 +883,7 @@ class CollectionState():
self.stale[item.player] = True
if changed and not prevent_sweep:
self.sweep_for_events()
self.sweep_for_advancements()
return changed
@@ -895,7 +907,7 @@ class Entrance:
addresses = None
target = None
def __init__(self, player: int, name: str = '', parent: Region = None):
def __init__(self, player: int, name: str = "", parent: Optional[Region] = None) -> None:
self.name = name
self.parent_region = parent
self.player = player
@@ -915,9 +927,6 @@ class Entrance:
region.entrances.append(self)
def __repr__(self):
return self.__str__()
def __str__(self):
multiworld = self.parent_region.multiworld if self.parent_region else None
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
@@ -1043,7 +1052,7 @@ class Region:
self.locations.append(location_type(self.player, location, address, self))
def connect(self, connecting_region: Region, name: Optional[str] = None,
rule: Optional[Callable[[CollectionState], bool]] = None) -> entrance_type:
rule: Optional[Callable[[CollectionState], bool]] = None) -> Entrance:
"""
Connects this Region to another Region, placing the provided rule on the connection.
@@ -1083,9 +1092,6 @@ class Region:
rules[connecting_region] if rules and connecting_region in rules else None)
def __repr__(self):
return self.__str__()
def __str__(self):
return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})'
@@ -1104,9 +1110,9 @@ class Location:
locked: bool = False
show_in_spoiler: bool = True
progress_type: LocationProgressType = LocationProgressType.DEFAULT
always_allow = staticmethod(lambda state, item: False)
always_allow: Callable[[CollectionState, Item], bool] = staticmethod(lambda state, item: False)
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
item_rule = staticmethod(lambda item: True)
item_rule: Callable[[Item], bool] = staticmethod(lambda item: True)
item: Optional[Item] = None
def __init__(self, player: int, name: str = '', address: Optional[int] = None, parent: Optional[Region] = None):
@@ -1115,11 +1121,15 @@ class Location:
self.address = address
self.parent_region = parent
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
return ((self.always_allow(state, item) and item.name not in state.multiworld.worlds[item.player].options.non_local_items)
or ((self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful))
and self.item_rule(item)
and (not check_access or self.can_reach(state))))
def can_fill(self, state: CollectionState, item: Item, check_access: bool = True) -> bool:
return ((
self.always_allow(state, item)
and item.name not in state.multiworld.worlds[item.player].options.non_local_items
) or (
(self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful))
and self.item_rule(item)
and (not check_access or self.can_reach(state))
))
def can_reach(self, state: CollectionState) -> bool:
# Region.can_reach is just a cache lookup, so placing it first for faster abort on average
@@ -1134,9 +1144,6 @@ class Location:
self.locked = True
def __repr__(self):
return self.__str__()
def __str__(self):
multiworld = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
@@ -1158,7 +1165,7 @@ class Location:
@property
def native_item(self) -> bool:
"""Returns True if the item in this location matches game."""
return self.item and self.item.game == self.game
return self.item is not None and self.item.game == self.game
@property
def hint_text(self) -> str:
@@ -1241,9 +1248,6 @@ class Item:
return hash((self.name, self.player))
def __repr__(self) -> str:
return self.__str__()
def __str__(self) -> str:
if self.location and self.location.parent_region and self.location.parent_region.multiworld:
return self.location.parent_region.multiworld.get_name_string_for_object(self)
return f"{self.name} (Player {self.player})"
@@ -1321,9 +1325,9 @@ class Spoiler:
# in the second phase, we cull each sphere such that the game is still beatable,
# reducing each range of influence to the bare minimum required inside it
restore_later = {}
restore_later: Dict[Location, Item] = {}
for num, sphere in reversed(tuple(enumerate(collection_spheres))):
to_delete = set()
to_delete: Set[Location] = set()
for location in sphere:
# we remove the item at location and check if game is still beatable
logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name,
@@ -1341,7 +1345,7 @@ class Spoiler:
sphere -= to_delete
# second phase, sphere 0
removed_precollected = []
removed_precollected: List[Item] = []
for item in (i for i in chain.from_iterable(multiworld.precollected_items.values()) if i.advancement):
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
multiworld.precollected_items[item.player].remove(item)
@@ -1494,9 +1498,9 @@ class Spoiler:
if self.paths:
outfile.write('\n\nPaths:\n\n')
path_listings = []
path_listings: List[str] = []
for location, path in sorted(self.paths.items()):
path_lines = []
path_lines: List[str] = []
for region, exit in path:
if exit is not None:
path_lines.append("{} -> {}".format(region, exit))

View File

@@ -252,7 +252,7 @@ class CommonContext:
starting_reconnect_delay: int = 5
current_reconnect_delay: int = starting_reconnect_delay
command_processor: typing.Type[CommandProcessor] = ClientCommandProcessor
ui = None
ui: typing.Optional["kvui.GameManager"] = None
ui_task: typing.Optional["asyncio.Task[None]"] = None
input_task: typing.Optional["asyncio.Task[None]"] = None
keep_alive_task: typing.Optional["asyncio.Task[None]"] = None

14
Fill.py
View File

@@ -29,7 +29,7 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item]
new_state = base_state.copy()
for item in itempool:
new_state.collect(item, True)
new_state.sweep_for_events(locations=locations)
new_state.sweep_for_advancements(locations=locations)
return new_state
@@ -329,8 +329,8 @@ def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, lo
pool.append(location.item)
state.remove(location.item)
location.item = None
if location in state.events:
state.events.remove(location)
if location in state.advancements:
state.advancements.remove(location)
locations.append(location)
if pool and locations:
locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY)
@@ -363,7 +363,7 @@ def distribute_early_items(multiworld: MultiWorld,
early_priority_locations: typing.List[Location] = []
loc_indexes_to_remove: typing.Set[int] = set()
base_state = multiworld.state.copy()
base_state.sweep_for_events(locations=(loc for loc in multiworld.get_filled_locations() if loc.address is None))
base_state.sweep_for_advancements(locations=(loc for loc in multiworld.get_filled_locations() if loc.address is None))
for i, loc in enumerate(fill_locations):
if loc.can_reach(base_state):
if loc.progress_type == LocationProgressType.PRIORITY:
@@ -558,7 +558,7 @@ def flood_items(multiworld: MultiWorld) -> None:
progress_done = False
# sweep once to pick up preplaced items
multiworld.state.sweep_for_events()
multiworld.state.sweep_for_advancements()
# fill multiworld from top of itempool while we can
while not progress_done:
@@ -746,7 +746,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
), items_to_test):
reducing_state.collect(location.item, True, location)
reducing_state.sweep_for_events(locations=locations_to_test)
reducing_state.sweep_for_advancements(locations=locations_to_test)
if multiworld.has_beaten_game(balancing_state):
if not multiworld.has_beaten_game(reducing_state):
@@ -829,7 +829,7 @@ def distribute_planned(multiworld: MultiWorld) -> None:
warn(warning, force)
swept_state = multiworld.state.copy()
swept_state.sweep_for_events()
swept_state.sweep_for_advancements()
reachable = frozenset(multiworld.get_reachable_locations(swept_state))
early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
non_early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)

View File

@@ -511,7 +511,7 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
continue
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers.")
if PlandoOptions.items in plando_options:
ret.plando_items = game_weights.get("plando_items", [])
ret.plando_items = copy.deepcopy(game_weights.get("plando_items", []))
if ret.game == "A Link to the Past":
roll_alttp_settings(ret, game_weights)

9
KH1Client.py Normal file
View File

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

View File

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

View File

@@ -79,6 +79,7 @@ class NetworkItem(typing.NamedTuple):
item: int
location: int
player: int
""" Sending player, except in LocationInfo (from LocationScouts), where it is the receiving player. """
flags: int = 0

View File

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

View File

@@ -78,6 +78,9 @@
# Kirby's Dream Land 3
/worlds/kdl3/ @Silvris
# Kingdom Hearts
/worlds/kh1/ @gaithern
# Kingdom Hearts 2
/worlds/kh2/ @JaredWeakStrike
@@ -103,6 +106,9 @@
# Minecraft
/worlds/minecraft/ @KonoTyran @espeon65536
# Mega Man 2
/worlds/mm2/ @Silvris
# MegaMan Battle Network 3
/worlds/mmbn3/ @digiholic
@@ -196,6 +202,9 @@
# The Witness
/worlds/witness/ @NewSoupVi @blastron
# Yacht Dice
/worlds/yachtdice/ @spinerak
# Yoshi's Island
/worlds/yoshisisland/ @PinkSwitch

View File

@@ -186,6 +186,11 @@ Root: HKCR; Subkey: "{#MyAppName}cv64patch"; ValueData: "Arc
Root: HKCR; Subkey: "{#MyAppName}cv64patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}cv64patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".apmm2"; ValueData: "{#MyAppName}mm2patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}mm2patch"; ValueData: "Archipelago Mega Man 2 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}mm2patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}mm2patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: "";

View File

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

View File

@@ -24,7 +24,7 @@ class TestBase(unittest.TestCase):
for item in items:
item.classification = ItemClassification.progression
state.collect(item, prevent_sweep=True)
state.sweep_for_events()
state.sweep_for_advancements()
state.update_reachable_regions(1)
self._state_cache[self.multiworld, tuple(items)] = state
return state
@@ -221,8 +221,8 @@ class WorldTestBase(unittest.TestCase):
if isinstance(items, Item):
items = (items,)
for item in items:
if item.location and item.advancement and item.location in self.multiworld.state.events:
self.multiworld.state.events.remove(item.location)
if item.location and item.advancement and item.location in self.multiworld.state.advancements:
self.multiworld.state.advancements.remove(item.location)
self.multiworld.state.remove(item)
def can_reach_location(self, location: str) -> bool:

View File

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

View File

@@ -14,6 +14,18 @@ class TestBase(unittest.TestCase):
"Desert Northern Cliffs", # on top of mountain, only reachable via OWG
"Dark Death Mountain Bunny Descent Area" # OWG Mountain descent
},
# These Blasphemous regions are not reachable with default options
"Blasphemous": {
"D01Z04S13[SE]", # difficulty must be hard
"D01Z05S25[E]", # difficulty must be hard
"D02Z02S05[W]", # difficulty must be hard and purified_hand must be true
"D04Z01S06[E]", # purified_hand must be true
"D04Z02S02[NE]", # difficulty must be hard and purified_hand must be true
"D05Z01S11[SW]", # difficulty must be hard
"D06Z01S08[N]", # difficulty must be hard and purified_hand must be true
"D20Z02S11[NW]", # difficulty must be hard
"D20Z02S11[E]", # difficulty must be hard
},
"Ocarina of Time": {
"Prelude of Light Warp", # Prelude is not progression by default
"Serenade of Water Warp", # Serenade is not progression by default

View File

@@ -132,7 +132,8 @@ def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, path
break
if found_already_loaded:
raise Exception(f"Installed APWorld successfully, but '{module_name}' is already loaded,\n"
"so a Launcher restart is required to use the new installation.")
"so a Launcher restart is required to use the new installation.\n"
"If the Launcher is not open, no action needs to be taken.")
world_source = worlds.WorldSource(str(target), is_zip=True)
bisect.insort(worlds.world_sources, world_source)
world_source.load()

View File

@@ -381,8 +381,8 @@ def set_moderate_rules(world: "HatInTimeWorld"):
lambda state: can_use_hat(state, world, HatType.ICE), "or")
# Moderate: Clock Tower Chest + Ruined Tower with nothing
add_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True)
add_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), lambda state: True)
set_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True)
set_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), lambda state: True)
# Moderate: enter and clear The Subcon Well without Hookshot and without hitting the bell
for loc in world.multiworld.get_region("The Subcon Well", world.player).locations:
@@ -432,8 +432,8 @@ def set_moderate_rules(world: "HatInTimeWorld"):
if world.is_dlc1():
# Moderate: clear Rock the Boat without Ice Hat
add_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), lambda state: True)
add_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), lambda state: True)
set_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), lambda state: True)
set_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), lambda state: True)
# Moderate: clear Deep Sea without Ice Hat
set_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player),
@@ -855,6 +855,9 @@ def set_rift_rules(world: "HatInTimeWorld", regions: Dict[str, Region]):
for entrance in regions["Time Rift - Alpine Skyline"].entrances:
add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon"))
if entrance.parent_region.name == "Alpine Free Roam":
add_rule(entrance,
lambda state: can_use_hookshot(state, world) and can_hit(state, world, umbrella_only=True))
if world.is_dlc1():
for entrance in regions["Time Rift - Balcony"].entrances:
@@ -933,6 +936,9 @@ def set_default_rift_rules(world: "HatInTimeWorld"):
for entrance in world.multiworld.get_region("Time Rift - Alpine Skyline", world.player).entrances:
add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon"))
if entrance.parent_region.name == "Alpine Free Roam":
add_rule(entrance,
lambda state: can_use_hookshot(state, world) and can_hit(state, world, umbrella_only=True))
if world.is_dlc1():
for entrance in world.multiworld.get_region("Time Rift - Balcony", world.player).entrances:

View File

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

View File

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

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))
set_rule(multiworld.get_location('Thieves\' Town - Blind\'s Cell', player),
lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
if multiworld.accessibility[player] != 'locations' and not multiworld.key_drop_shuffle[player]:
if multiworld.accessibility[player] != 'full' and not multiworld.key_drop_shuffle[player]:
set_always_allow(multiworld.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player)
set_rule(multiworld.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3))
set_rule(multiworld.get_location('Thieves\' Town - Spike Switch Pot Key', player),
@@ -547,7 +547,7 @@ def global_rules(multiworld: MultiWorld, player: int):
location_item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 6)))
# this seemed to be causing generation failure, disable for now
# if world.accessibility[player] != 'locations':
# if world.accessibility[player] != 'full':
# set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state._lttp_has_key('Small Key (Ganons Tower)', player, 7) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player))
# It is possible to need more than 6 keys to get through this entrance if you spend keys elsewhere. We reflect this in the chest requirements.

View File

@@ -356,6 +356,8 @@ class ALTTPWorld(World):
self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group]
if option == "original_dungeon":
self.dungeon_specific_item_names |= self.item_name_groups[option.item_name_group]
else:
self.options.local_items.value |= self.dungeon_local_item_names
self.difficulty_requirements = difficulties[multiworld.item_pool[player].current_key]

View File

@@ -54,7 +54,7 @@ class TestDungeon(LTTPTestBase):
for item in items:
item.classification = ItemClassification.progression
state.collect(item, prevent_sweep=True) # prevent_sweep=True prevents running sweep_for_events() and picking up
state.sweep_for_events() # key drop keys repeatedly
state.collect(item, prevent_sweep=True) # prevent_sweep=True prevents running sweep_for_advancements() and picking up
state.sweep_for_advancements() # key drop keys repeatedly
self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access, f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}")

View File

@@ -0,0 +1,60 @@
from unittest import TestCase
from BaseClasses import MultiWorld
from test.general import gen_steps, setup_multiworld
from worlds.AutoWorld import call_all
from worlds.generic.Rules import locality_rules
from ... import ALTTPWorld
from ...Options import DungeonItem
class DungeonFillTestBase(TestCase):
multiworld: MultiWorld
world_1: ALTTPWorld
world_2: ALTTPWorld
options = (
"big_key_shuffle",
"small_key_shuffle",
"key_drop_shuffle",
"compass_shuffle",
"map_shuffle",
)
def setUp(self):
self.multiworld = setup_multiworld([ALTTPWorld, ALTTPWorld], ())
self.world_1 = self.multiworld.worlds[1]
self.world_2 = self.multiworld.worlds[2]
def generate_with_options(self, option_value: int):
for option in self.options:
getattr(self.world_1.options, option).value = getattr(self.world_2.options, option).value = option_value
for step in gen_steps:
call_all(self.multiworld, step)
# this is where locality rules are set in normal generation which we need to verify this test
if step == "set_rules":
locality_rules(self.multiworld)
def test_original_dungeons(self):
self.generate_with_options(DungeonItem.option_original_dungeon)
for location in self.multiworld.get_filled_locations():
with (self.subTest(location=location)):
if location.parent_region.dungeon is None:
self.assertIs(location.item.dungeon, None)
else:
self.assertEqual(location.player, location.item.player,
f"{location.item} does not belong to {location}'s player")
if location.item.dungeon is None:
continue
self.assertIs(location.item.dungeon, location.parent_region.dungeon,
f"{location.item} was not placed in its original dungeon.")
def test_own_dungeons(self):
self.generate_with_options(DungeonItem.option_own_dungeons)
for location in self.multiworld.get_filled_locations():
with self.subTest(location=location):
if location.parent_region.dungeon is None:
self.assertIs(location.item.dungeon, None)
else:
self.assertEqual(location.player, location.item.player,
f"{location.item} does not belong to {location}'s player")

View File

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

View File

@@ -1,9 +1,7 @@
# APSudoku Setup Guide
## Required Software
- [APSudoku](https://github.com/EmilyV99/APSudoku)
- Windows (most tested on Win10)
- Other platforms might be able to build from source themselves; and may be included in the future.
- [APSudoku](https://github.com/APSudoku/APSudoku)
## General Concept
@@ -13,25 +11,33 @@ Does not need to be added at the start of a seed, as it does not create any slot
## Installation Procedures
Go to the latest release from the [APSudoku Releases page](https://github.com/EmilyV99/APSudoku/releases). Download and extract the `APSudoku.zip` file.
Go to the latest release from the [APSudoku Releases page](https://github.com/APSudoku/APSudoku/releases/latest). Download and extract the appropriate file for your platform.
## Joining a MultiWorld Game
1. Run APSudoku.exe
2. Under the 'Archipelago' tab at the top-right:
- Enter the server url & port number
1. Run the APSudoku executable.
2. Under `Settings` → `Connection` at the top-right:
- Enter the server address and port number
- Enter the name of the slot you wish to connect to
- Enter the room password (optional)
- Select DeathLink related settings (optional)
- Press connect
3. Go back to the 'Sudoku' tab
- Click the various '?' buttons for information on how to play / control
4. Choose puzzle difficulty
5. Try to solve the Sudoku. Click 'Check' when done.
- Press `Connect`
4. Under the `Sudoku` tab
- Choose puzzle difficulty
- Click `Start` to generate a puzzle
5. Try to solve the Sudoku. Click `Check` when done
- A correct solution rewards you with 1 hint for a location in the world you are connected to
- An incorrect solution has no penalty, unless DeathLink is enabled (see below)
Info:
- You can set various settings under `Settings` → `Sudoku`, and can change the colors used under `Settings` → `Theme`.
- While connected, you can view the `Console` and `Hints` tabs for standard TextClient-like features
- You can also use the `Tracking` tab to view either a basic tracker or a valid [GodotAP tracker pack](https://github.com/EmilyV99/GodotAP/blob/main/tracker_packs/GET_PACKS.md)
- While connected, the number of "unhinted" locations for your slot is shown in the upper-left of the the `Sudoku` tab. (If this reads 0, no further hints can be earned for this slot, as every locations is already hinted)
- Click the various `?` buttons for information on controls/how to play
## DeathLink Support
If 'DeathLink' is enabled when you click 'Connect':
- Lose a life if you check an incorrect puzzle (not an _incomplete_ puzzle- if any cells are empty, you get off with a warning), or quit a puzzle without solving it (including disconnecting).
- Life count customizable (default 0). Dying with 0 lives left kills linked players AND resets your puzzle.
If `DeathLink` is enabled when you click `Connect`:
- Lose a life if you check an incorrect puzzle (not an _incomplete_ puzzle- if any cells are empty, you get off with a warning), or if you quit a puzzle without solving it (including disconnecting).
- Your life count is customizable (default 0). Dying with 0 lives left kills linked players AND resets your puzzle.
- On receiving a DeathLink from another player, your puzzle resets.

View File

@@ -0,0 +1,19 @@
{
"type": "WorldDefinition",
"configuration": "./output/StringWorldDefinition.json",
"emptyRegionsToKeep": [
"D17Z01S01",
"D01Z02S01",
"D02Z03S09",
"D03Z03S11",
"D04Z03S01",
"D06Z01S09",
"D20Z02S09",
"D09Z01S09[Cell24]",
"D09Z01S08[Cell7]",
"D09Z01S08[Cell18]",
"D09BZ01S01[Cell24]",
"D09BZ01S01[Cell17]",
"D09BZ01S01[Cell19]"
]
}

View File

@@ -637,52 +637,35 @@ item_table: List[ItemDict] = [
'classification': ItemClassification.filler}
]
event_table: Dict[str, str] = {
"OpenedDCGateW": "D01Z05S24",
"OpenedDCGateE": "D01Z05S12",
"OpenedDCLadder": "D01Z05S20",
"OpenedWOTWCave": "D02Z01S06",
"RodeGOTPElevator": "D02Z02S11",
"OpenedConventLadder": "D02Z03S11",
"BrokeJondoBellW": "D03Z02S09",
"BrokeJondoBellE": "D03Z02S05",
"OpenedMOMLadder": "D04Z02S06",
"OpenedTSCGate": "D05Z02S11",
"OpenedARLadder": "D06Z01S23",
"BrokeBOTTCStatue": "D08Z01S02",
"OpenedWOTHPGate": "D09Z01S05",
"OpenedBOTSSLadder": "D17Z01S04"
}
group_table: Dict[str, Set[str]] = {
"wounds" : ["Holy Wound of Attrition",
"wounds" : {"Holy Wound of Attrition",
"Holy Wound of Contrition",
"Holy Wound of Compunction"],
"Holy Wound of Compunction"},
"masks" : ["Deformed Mask of Orestes",
"masks" : {"Deformed Mask of Orestes",
"Mirrored Mask of Dolphos",
"Embossed Mask of Crescente"],
"Embossed Mask of Crescente"},
"marks" : ["Mark of the First Refuge",
"marks" : {"Mark of the First Refuge",
"Mark of the Second Refuge",
"Mark of the Third Refuge"],
"Mark of the Third Refuge"},
"tirso" : ["Bouquet of Rosemary",
"tirso" : {"Bouquet of Rosemary",
"Incense Garlic",
"Olive Seeds",
"Dried Clove",
"Sooty Garlic",
"Bouquet of Thyme"],
"Bouquet of Thyme"},
"tentudia": ["Tentudia's Carnal Remains",
"tentudia": {"Tentudia's Carnal Remains",
"Remains of Tentudia's Hair",
"Tentudia's Skeletal Remains"],
"Tentudia's Skeletal Remains"},
"egg" : ["Melted Golden Coins",
"egg" : {"Melted Golden Coins",
"Torn Bridal Ribbon",
"Black Grieving Veil"],
"Black Grieving Veil"},
"bones" : ["Parietal bone of Lasser, the Inquisitor",
"bones" : {"Parietal bone of Lasser, the Inquisitor",
"Jaw of Ashgan, the Inquisitor",
"Cervical vertebra of Zicher, the Brewmaster",
"Clavicle of Dalhuisen, the Schoolchild",
@@ -725,14 +708,14 @@ group_table: Dict[str, Set[str]] = {
"Scaphoid of Fierce, the Leper",
"Anklebone of Weston, the Pilgrim",
"Calcaneum of Persian, the Bandit",
"Navicular of Kahnnyhoo, the Murderer"],
"Navicular of Kahnnyhoo, the Murderer"},
"power" : ["Life Upgrade",
"power" : {"Life Upgrade",
"Fervour Upgrade",
"Empty Bile Vessel",
"Quicksilver"],
"Quicksilver"},
"prayer" : ["Seguiriya to your Eyes like Stars",
"prayer" : {"Seguiriya to your Eyes like Stars",
"Debla of the Lights",
"Saeta Dolorosa",
"Campanillero to the Sons of the Aurora",
@@ -746,10 +729,17 @@ group_table: Dict[str, Set[str]] = {
"Romance to the Crimson Mist",
"Zambra to the Resplendent Crown",
"Cantina of the Blue Rose",
"Mirabras of the Return to Port"]
"Mirabras of the Return to Port"},
"toe" : {"Little Toe made of Limestone",
"Big Toe made of Limestone",
"Fourth Toe made of Limestone"},
"eye" : {"Severed Right Eye of the Traitor",
"Broken Left Eye of the Traitor"}
}
tears_set: Set[str] = [
tears_list: List[str] = [
"Tears of Atonement (500)",
"Tears of Atonement (625)",
"Tears of Atonement (750)",
@@ -772,16 +762,16 @@ tears_set: Set[str] = [
"Tears of Atonement (30000)"
]
reliquary_set: Set[str] = [
reliquary_set: Set[str] = {
"Reliquary of the Fervent Heart",
"Reliquary of the Suffering Heart",
"Reliquary of the Sorrowful Heart"
]
}
skill_set: Set[str] = [
skill_set: Set[str] = {
"Combo Skill",
"Charged Skill",
"Ranged Skill",
"Dive Skill",
"Lunge Skill"
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool
from dataclasses import dataclass
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions, OptionGroup
import random
@@ -20,23 +21,30 @@ class ChoiceIsRandom(Choice):
class PrieDieuWarp(DefaultOnToggle):
"""Automatically unlocks the ability to warp between Prie Dieu shrines."""
"""
Automatically unlocks the ability to warp between Prie Dieu shrines.
"""
display_name = "Unlock Fast Travel"
class SkipCutscenes(DefaultOnToggle):
"""Automatically skips most cutscenes."""
"""
Automatically skips most cutscenes.
"""
display_name = "Auto Skip Cutscenes"
class CorpseHints(DefaultOnToggle):
"""Changes the 34 corpses in game to give various hints about item locations."""
"""
Changes the 34 corpses in game to give various hints about item locations.
"""
display_name = "Corpse Hints"
class Difficulty(Choice):
"""Adjusts the overall difficulty of the randomizer, including upgrades required to defeat bosses
and advanced movement tricks or glitches."""
"""
Adjusts the overall difficulty of the randomizer, including upgrades required to defeat bosses and advanced movement tricks or glitches.
"""
display_name = "Difficulty"
option_easy = 0
option_normal = 1
@@ -45,15 +53,18 @@ class Difficulty(Choice):
class Penitence(Toggle):
"""Allows one of the three Penitences to be chosen at the beginning of the game."""
"""
Allows one of the three Penitences to be chosen at the beginning of the game.
"""
display_name = "Penitence"
class StartingLocation(ChoiceIsRandom):
"""Choose where to start the randomizer. Note that some starting locations cannot be chosen with certain
other options.
Specifically, Brotherhood and Mourning And Havoc cannot be chosen if Shuffle Dash is enabled, and Grievance Ascends
cannot be chosen if Shuffle Wall Climb is enabled."""
"""
Choose where to start the randomizer. Note that some starting locations cannot be chosen with certain other options.
Specifically, Brotherhood and Mourning And Havoc cannot be chosen if Shuffle Dash is enabled, and Grievance Ascends cannot be chosen if Shuffle Wall Climb is enabled.
"""
display_name = "Starting Location"
option_brotherhood = 0
option_albero = 1
@@ -66,10 +77,15 @@ class StartingLocation(ChoiceIsRandom):
class Ending(Choice):
"""Choose which ending is required to complete the game.
"""
Choose which ending is required to complete the game.
Talking to Tirso in Albero will tell you the selected ending for the current game.
Ending A: Collect all thorn upgrades.
Ending C: Collect all thorn upgrades and the Holy Wound of Abnegation."""
Ending C: Collect all thorn upgrades and the Holy Wound of Abnegation.
"""
display_name = "Ending"
option_any_ending = 0
option_ending_a = 1
@@ -78,14 +94,18 @@ class Ending(Choice):
class SkipLongQuests(Toggle):
"""Ensures that the rewards for long quests will be filler items.
Affected locations: \"Albero: Donate 50000 Tears\", \"Ossuary: 11th reward\", \"AtTotS: Miriam's gift\",
\"TSC: Jocinero's final reward\""""
"""
Ensures that the rewards for long quests will be filler items.
Affected locations: "Albero: Donate 50000 Tears", "Ossuary: 11th reward", "AtTotS: Miriam's gift", "TSC: Jocinero's final reward"
"""
display_name = "Skip Long Quests"
class ThornShuffle(Choice):
"""Shuffles the Thorn given by Deogracias and all Thorn upgrades into the item pool."""
"""
Shuffles the Thorn given by Deogracias and all Thorn upgrades into the item pool.
"""
display_name = "Shuffle Thorn"
option_anywhere = 0
option_local_only = 1
@@ -94,50 +114,68 @@ class ThornShuffle(Choice):
class DashShuffle(Toggle):
"""Turns the ability to dash into an item that must be found in the multiworld."""
"""
Turns the ability to dash into an item that must be found in the multiworld.
"""
display_name = "Shuffle Dash"
class WallClimbShuffle(Toggle):
"""Turns the ability to climb walls with your sword into an item that must be found in the multiworld."""
"""
Turns the ability to climb walls with your sword into an item that must be found in the multiworld.
"""
display_name = "Shuffle Wall Climb"
class ReliquaryShuffle(DefaultOnToggle):
"""Adds the True Torment exclusive Reliquary rosary beads into the item pool."""
"""
Adds the True Torment exclusive Reliquary rosary beads into the item pool.
"""
display_name = "Shuffle Penitence Rewards"
class CustomItem1(Toggle):
"""Adds the custom relic Boots of Pleading into the item pool, which grants the ability to fall onto spikes
and survive.
Must have the \"Blasphemous-Boots-of-Pleading\" mod installed to connect to a multiworld."""
"""
Adds the custom relic Boots of Pleading into the item pool, which grants the ability to fall onto spikes and survive.
Must have the "Boots of Pleading" mod installed to connect to a multiworld.
"""
display_name = "Boots of Pleading"
class CustomItem2(Toggle):
"""Adds the custom relic Purified Hand of the Nun into the item pool, which grants the ability to jump
a second time in mid-air.
Must have the \"Blasphemous-Double-Jump\" mod installed to connect to a multiworld."""
"""
Adds the custom relic Purified Hand of the Nun into the item pool, which grants the ability to jump a second time in mid-air.
Must have the "Double Jump" mod installed to connect to a multiworld.
"""
display_name = "Purified Hand of the Nun"
class StartWheel(Toggle):
"""Changes the beginning gift to The Young Mason's Wheel."""
"""
Changes the beginning gift to The Young Mason's Wheel.
"""
display_name = "Start with Wheel"
class SkillRando(Toggle):
"""Randomizes the abilities from the skill tree into the item pool."""
"""
Randomizes the abilities from the skill tree into the item pool.
"""
display_name = "Skill Randomizer"
class EnemyRando(Choice):
"""Randomizes the enemies that appear in each room.
Shuffled: Enemies will be shuffled amongst each other, but can only appear as many times as they do in
a standard game.
"""
Randomizes the enemies that appear in each room.
Shuffled: Enemies will be shuffled amongst each other, but can only appear as many times as they do in a standard game.
Randomized: Every enemy is completely random, and can appear any number of times.
Some enemies will never be randomized."""
Some enemies will never be randomized.
"""
display_name = "Enemy Randomizer"
option_disabled = 0
option_shuffled = 1
@@ -146,43 +184,75 @@ class EnemyRando(Choice):
class EnemyGroups(DefaultOnToggle):
"""Randomized enemies will chosen from sets of specific groups.
"""
Randomized enemies will be chosen from sets of specific groups.
(Weak, normal, large, flying)
Has no effect if Enemy Randomizer is disabled."""
Has no effect if Enemy Randomizer is disabled.
"""
display_name = "Enemy Groups"
class EnemyScaling(DefaultOnToggle):
"""Randomized enemies will have their stats increased or decreased depending on the area they appear in.
Has no effect if Enemy Randomizer is disabled."""
"""
Randomized enemies will have their stats increased or decreased depending on the area they appear in.
Has no effect if Enemy Randomizer is disabled.
"""
display_name = "Enemy Scaling"
class BlasphemousDeathLink(DeathLink):
"""When you die, everyone dies. The reverse is also true.
Note that Guilt Fragments will not appear when killed by Death Link."""
"""
When you die, everyone dies. The reverse is also true.
Note that Guilt Fragments will not appear when killed by Death Link.
"""
blasphemous_options = {
"prie_dieu_warp": PrieDieuWarp,
"skip_cutscenes": SkipCutscenes,
"corpse_hints": CorpseHints,
"difficulty": Difficulty,
"penitence": Penitence,
"starting_location": StartingLocation,
"ending": Ending,
"skip_long_quests": SkipLongQuests,
"thorn_shuffle" : ThornShuffle,
"dash_shuffle": DashShuffle,
"wall_climb_shuffle": WallClimbShuffle,
"reliquary_shuffle": ReliquaryShuffle,
"boots_of_pleading": CustomItem1,
"purified_hand": CustomItem2,
"start_wheel": StartWheel,
"skill_randomizer": SkillRando,
"enemy_randomizer": EnemyRando,
"enemy_groups": EnemyGroups,
"enemy_scaling": EnemyScaling,
"death_link": BlasphemousDeathLink,
"start_inventory": StartInventoryPool
}
@dataclass
class BlasphemousOptions(PerGameCommonOptions):
prie_dieu_warp: PrieDieuWarp
skip_cutscenes: SkipCutscenes
corpse_hints: CorpseHints
difficulty: Difficulty
penitence: Penitence
starting_location: StartingLocation
ending: Ending
skip_long_quests: SkipLongQuests
thorn_shuffle: ThornShuffle
dash_shuffle: DashShuffle
wall_climb_shuffle: WallClimbShuffle
reliquary_shuffle: ReliquaryShuffle
boots_of_pleading: CustomItem1
purified_hand: CustomItem2
start_wheel: StartWheel
skill_randomizer: SkillRando
enemy_randomizer: EnemyRando
enemy_groups: EnemyGroups
enemy_scaling: EnemyScaling
death_link: BlasphemousDeathLink
blas_option_groups = [
OptionGroup("Quality of Life", [
PrieDieuWarp,
SkipCutscenes,
CorpseHints,
SkipLongQuests,
StartWheel
]),
OptionGroup("Moveset", [
DashShuffle,
WallClimbShuffle,
SkillRando,
CustomItem1,
CustomItem2
]),
OptionGroup("Enemy Randomizer", [
EnemyRando,
EnemyGroups,
EnemyScaling
])
]

View File

@@ -0,0 +1,582 @@
# Preprocessor to convert Blasphemous Randomizer logic into a StringWorldDefinition for use with APHKLogicExtractor
# https://github.com/BrandenEK/Blasphemous.Randomizer
# https://github.com/ArchipelagoMW-HollowKnight/APHKLogicExtractor
import json, requests, argparse
from typing import List, Dict, Any
def load_resource_local(file: str) -> List[Dict[str, Any]]:
print(f"Reading from {file}")
loaded = []
with open(file, encoding="utf-8") as f:
loaded = read_json(f.readlines())
f.close()
return loaded
def load_resource_from_web(url: str) -> List[Dict[str, Any]]:
req = requests.get(url, timeout=1)
print(f"Reading from {url}")
req.encoding = "utf-8"
lines: List[str] = []
for line in req.text.splitlines():
while "\t" in line:
line = line[1::]
if line != "":
lines.append(line)
return read_json(lines)
def read_json(lines: List[str]) -> List[Dict[str, Any]]:
loaded = []
creating_object: bool = False
obj: str = ""
for line in lines:
stripped = line.strip()
if "{" in stripped:
creating_object = True
obj += stripped
continue
elif "}," in stripped or "}" in stripped and "]" in lines[lines.index(line)+1]:
creating_object = False
obj += "}"
#print(f"obj = {obj}")
loaded.append(json.loads(obj))
obj = ""
continue
if not creating_object:
continue
else:
try:
if "}," in lines[lines.index(line)+1] and stripped[-1] == ",":
obj += stripped[:-1]
else:
obj += stripped
except IndexError:
obj += stripped
return loaded
def get_room_from_door(door: str) -> str:
return door[:door.find("[")]
def preprocess_logic(is_door: bool, id: str, logic: str) -> str:
if id in logic and not is_door:
index: int = logic.find(id)
logic = logic[:index] + logic[index+len(id)+4:]
while ">=" in logic:
index: int = logic.find(">=")
logic = logic[:index-1] + logic[index+3:]
while ">" in logic:
index: int = logic.find(">")
count = int(logic[index+2])
count += 1
logic = logic[:index-1] + str(count) + logic[index+3:]
while "<=" in logic:
index: int = logic.find("<=")
logic = logic[:index-1] + logic[index+3:]
while "<" in logic:
index: int = logic.find("<")
count = int(logic[index+2])
count += 1
logic = logic[:index-1] + str(count) + logic[index+3:]
#print(logic)
return logic
def build_logic_conditions(logic: str) -> List[List[str]]:
all_conditions: List[List[str]] = []
parts = logic.split()
sub_part: str = ""
current_index: int = 0
parens: int = -1
current_condition: List[str] = []
parens_conditions: List[List[List[str]]] = []
for index, part in enumerate(parts):
#print(current_index, index, parens, part)
# skip parts that have already been handled
if index < current_index:
continue
# break loop if reached final part
try:
parts[index+1]
except IndexError:
#print("INDEXERROR", part)
if parens < 0:
current_condition.append(part)
if len(parens_conditions) > 0:
for i in parens_conditions:
for j in i:
all_conditions.append(j + current_condition)
else:
all_conditions.append(current_condition)
break
#print(current_condition, parens, sub_part)
# prepare for subcondition
if "(" in part:
# keep track of nested parentheses
if parens == -1:
parens = 0
for char in part:
if char == "(":
parens += 1
# add to sub part
if sub_part == "":
sub_part = part
else:
sub_part += f" {part}"
#if not ")" in part:
continue
# end of subcondition
if ")" in part:
# read every character in case of multiple closing parentheses
for char in part:
if char == ")":
parens -= 1
sub_part += f" {part}"
# if reached end of parentheses, handle subcondition
if parens == 0:
#print(current_condition, sub_part)
parens = -1
try:
parts[index+1]
except IndexError:
#print("END OF LOGIC")
if len(parens_conditions) > 0:
parens_conditions.append(build_logic_subconditions(current_condition, sub_part))
#print("PARENS:", parens_conditions)
temp_conditions: List[List[str]] = []
for i in parens_conditions[0]:
for j in parens_conditions[1]:
temp_conditions.append(i + j)
parens_conditions.pop(0)
parens_conditions.pop(0)
while len(parens_conditions) > 0:
temp_conditions2 = temp_conditions
temp_conditions = []
for k in temp_conditions2:
for l in parens_conditions[0]:
temp_conditions.append(k + l)
parens_conditions.pop(0)
#print("TEMP:", remove_duplicates(temp_conditions))
all_conditions += temp_conditions
else:
all_conditions += build_logic_subconditions(current_condition, sub_part)
else:
#print("NEXT PARTS:", parts[index+1], parts[index+2])
if parts[index+1] == "&&":
parens_conditions.append(build_logic_subconditions(current_condition, sub_part))
#print("PARENS:", parens_conditions)
else:
if len(parens_conditions) > 0:
parens_conditions.append(build_logic_subconditions(current_condition, sub_part))
#print("PARENS:", parens_conditions)
temp_conditions: List[List[str]] = []
for i in parens_conditions[0]:
for j in parens_conditions[1]:
temp_conditions.append(i + j)
parens_conditions.pop(0)
parens_conditions.pop(0)
while len(parens_conditions) > 0:
temp_conditions2 = temp_conditions
temp_conditions = []
for k in temp_conditions2:
for l in parens_conditions[0]:
temp_conditions.append(k + l)
parens_conditions.pop(0)
#print("TEMP:", remove_duplicates(temp_conditions))
all_conditions += temp_conditions
else:
all_conditions += build_logic_subconditions(current_condition, sub_part)
current_index = index+2
current_condition = []
sub_part = ""
continue
# collect all parts until reaching end of parentheses
if parens > 0:
sub_part += f" {part}"
continue
current_condition.append(part)
# continue with current condition
if parts[index+1] == "&&":
current_index = index+2
continue
# add condition to list and start new one
elif parts[index+1] == "||":
if len(parens_conditions) > 0:
for i in parens_conditions:
for j in i:
all_conditions.append(j + current_condition)
parens_conditions = []
else:
all_conditions.append(current_condition)
current_condition = []
current_index = index+2
continue
return remove_duplicates(all_conditions)
def build_logic_subconditions(current_condition: List[str], subcondition: str) -> List[List[str]]:
#print("STARTED SUBCONDITION", current_condition, subcondition)
subconditions = build_logic_conditions(subcondition[1:-1])
final_conditions = []
for condition in subconditions:
final_condition = current_condition + condition
final_conditions.append(final_condition)
#print("ENDED SUBCONDITION")
#print(final_conditions)
return final_conditions
def remove_duplicates(conditions: List[List[str]]) -> List[List[str]]:
final_conditions: List[List[str]] = []
for condition in conditions:
final_conditions.append(list(dict.fromkeys(condition)))
return final_conditions
def handle_door_visibility(door: Dict[str, Any]) -> Dict[str, Any]:
if door.get("visibilityFlags") == None:
return door
else:
flags: List[str] = str(door.get("visibilityFlags")).split(", ")
#print(flags)
temp_flags: List[str] = []
this_door: bool = False
#required_doors: str = ""
if "ThisDoor" in flags:
this_door = True
#if "requiredDoors" in flags:
# required_doors: str = " || ".join(door.get("requiredDoors"))
if "DoubleJump" in flags:
temp_flags.append("DoubleJump")
if "NormalLogic" in flags:
temp_flags.append("NormalLogic")
if "NormalLogicAndDoubleJump" in flags:
temp_flags.append("NormalLogicAndDoubleJump")
if "HardLogic" in flags:
temp_flags.append("HardLogic")
if "HardLogicAndDoubleJump" in flags:
temp_flags.append("HardLogicAndDoubleJump")
if "EnemySkips" in flags:
temp_flags.append("EnemySkips")
if "EnemySkipsAndDoubleJump" in flags:
temp_flags.append("EnemySkipsAndDoubleJump")
# remove duplicates
temp_flags = list(dict.fromkeys(temp_flags))
original_logic: str = door.get("logic")
temp_logic: str = ""
if this_door:
temp_logic = door.get("id")
if temp_flags != []:
if temp_logic != "":
temp_logic += " || "
temp_logic += ' && '.join(temp_flags)
if temp_logic != "" and original_logic != None:
if len(original_logic.split()) == 1:
if len(temp_logic.split()) == 1:
door["logic"] = f"{temp_logic} && {original_logic}"
else:
door["logic"] = f"({temp_logic}) && {original_logic}"
else:
if len(temp_logic.split()) == 1:
door["logic"] = f"{temp_logic} && ({original_logic})"
else:
door["logic"] = f"({temp_logic}) && ({original_logic})"
elif temp_logic != "" and original_logic == None:
door["logic"] = temp_logic
return door
def get_state_provider_for_condition(condition: List[str]) -> str:
for item in condition:
if (item[0] == "D" and item[3] == "Z" and item[6] == "S")\
or (item[0] == "D" and item[3] == "B" and item[4] == "Z" and item[7] == "S"):
return item
return None
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument('-l', '--local', action="store_true", help="Use local files in the same directory instead of reading resource files from the BrandenEK/Blasphemous-Randomizer repository.")
args = parser.parse_args()
return args
def main(args: argparse.Namespace):
doors = []
locations = []
if (args.local):
doors = load_resource_local("doors.json")
locations = load_resource_local("locations_items.json")
else:
doors = load_resource_from_web("https://raw.githubusercontent.com/BrandenEK/Blasphemous-Randomizer/main/resources/data/Randomizer/doors.json")
locations = load_resource_from_web("https://raw.githubusercontent.com/BrandenEK/Blasphemous-Randomizer/main/resources/data/Randomizer/locations_items.json")
original_connections: Dict[str, str] = {}
rooms: Dict[str, List[str]] = {}
output: Dict[str, Any] = {}
logic_objects: List[Dict[str, Any]] = []
for door in doors:
if door.get("originalDoor") != None:
if not door.get("id") in original_connections:
original_connections[door.get("id")] = door.get("originalDoor")
original_connections[door.get("originalDoor")] = door.get("id")
room: str = get_room_from_door(door.get("originalDoor"))
if not room in rooms.keys():
rooms[room] = [door.get("id")]
else:
rooms[room].append(door.get("id"))
def flip_doors_in_condition(condition: List[str]) -> List[str]:
new_condition = []
for item in condition:
if item in original_connections:
new_condition.append(original_connections[item])
else:
new_condition.append(item)
return new_condition
for room in rooms.keys():
obj = {
"Name": room,
"Logic": [],
"Handling": "Default"
}
for door in rooms[room]:
logic = {
"StateProvider": door,
"Conditions": [],
"StateModifiers": []
}
obj["Logic"].append(logic)
logic_objects.append(obj)
for door in doors:
if door.get("direction") == 5:
continue
handling: str = "Transition"
if "Cell" in door.get("id"):
handling = "Default"
obj = {
"Name": door.get("id"),
"Logic": [],
"Handling": handling
}
visibility_flags: List[str] = []
if door.get("visibilityFlags") != None:
visibility_flags = str(door.get("visibilityFlags")).split(", ")
if "1" in visibility_flags:
visibility_flags.remove("1")
visibility_flags.append("ThisDoor")
required_doors: List[str] = []
if door.get("requiredDoors"):
required_doors = door.get("requiredDoors")
if len(visibility_flags) > 0:
for flag in visibility_flags:
if flag == "RequiredDoors":
continue
if flag == "ThisDoor":
flag = original_connections[door.get("id")]
if door.get("logic") != None:
logic: str = door.get("logic")
logic = f"{flag} && ({logic})"
logic = preprocess_logic(True, door.get("id"), logic)
conditions = build_logic_conditions(logic)
for condition in conditions:
condition = flip_doors_in_condition(condition)
state_provider: str = get_room_from_door(door.get("id"))
if get_state_provider_for_condition(condition) != None:
state_provider = get_state_provider_for_condition(condition)
condition.remove(state_provider)
logic = {
"StateProvider": state_provider,
"Conditions": condition,
"StateModifiers": []
}
obj["Logic"].append(logic)
else:
logic = {
"StateProvider": get_room_from_door(door.get("id")),
"Conditions": [flag],
"StateModifiers": []
}
obj["Logic"].append(logic)
if "RequiredDoors" in visibility_flags:
for d in required_doors:
flipped = original_connections[d]
if door.get("logic") != None:
logic: str = preprocess_logic(True, door.get("id"), door.get("logic"))
conditions = build_logic_conditions(logic)
for condition in conditions:
condition = flip_doors_in_condition(condition)
state_provider: str = flipped
if flipped in condition:
condition.remove(flipped)
logic = {
"StateProvider": state_provider,
"Conditions": condition,
"StateModifiers": []
}
obj["Logic"].append(logic)
else:
logic = {
"StateProvider": flipped,
"Conditions": [],
"StateModifiers": []
}
obj["Logic"].append(logic)
else:
if door.get("logic") != None:
logic: str = preprocess_logic(True, door.get("id"), door.get("logic"))
conditions = build_logic_conditions(logic)
for condition in conditions:
condition = flip_doors_in_condition(condition)
stateProvider: str = get_room_from_door(door.get("id"))
if get_state_provider_for_condition(condition) != None:
stateProvider = get_state_provider_for_condition(condition)
condition.remove(stateProvider)
logic = {
"StateProvider": stateProvider,
"Conditions": condition,
"StateModifiers": []
}
obj["Logic"].append(logic)
else:
logic = {
"StateProvider": get_room_from_door(door.get("id")),
"Conditions": [],
"StateModifiers": []
}
obj["Logic"].append(logic)
logic_objects.append(obj)
for location in locations:
obj = {
"Name": location.get("id"),
"Logic": [],
"Handling": "Location"
}
if location.get("logic") != None:
for condition in build_logic_conditions(preprocess_logic(False, location.get("id"), location.get("logic"))):
condition = flip_doors_in_condition(condition)
stateProvider: str = location.get("room")
if get_state_provider_for_condition(condition) != None:
stateProvider = get_state_provider_for_condition(condition)
condition.remove(stateProvider)
if stateProvider == "Initial":
stateProvider = None
logic = {
"StateProvider": stateProvider,
"Conditions": condition,
"StateModifiers": []
}
obj["Logic"].append(logic)
else:
stateProvider: str = location.get("room")
if stateProvider == "Initial":
stateProvider = None
logic = {
"StateProvider": stateProvider,
"Conditions": [],
"StateModifiers": []
}
obj["Logic"].append(logic)
logic_objects.append(obj)
output["LogicObjects"] = logic_objects
with open("StringWorldDefinition.json", "w") as file:
print("Writing to StringWorldDefinition.json")
file.write(json.dumps(output, indent=4))
if __name__ == "__main__":
main(parse_args())

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,48 +1,17 @@
# Blasphemous Multiworld Setup Guide
## Useful Links
It is recommended to use the [Mod Installer](https://github.com/BrandenEK/Blasphemous.Modding.Installer) to handle installing and updating mods. If you would prefer to install mods manually, instructions can also be found at the Mod Installer repository.
Required:
- Blasphemous: [Steam](https://store.steampowered.com/app/774361/Blasphemous/)
- The GOG version of Blasphemous will also work.
- Blasphemous Mod Installer: [GitHub](https://github.com/BrandenEK/Blasphemous-Mod-Installer)
- Blasphemous Modding API: [GitHub](https://github.com/BrandenEK/Blasphemous-Modding-API)
- Blasphemous Randomizer: [GitHub](https://github.com/BrandenEK/Blasphemous-Randomizer)
- Blasphemous Multiworld: [GitHub](https://github.com/BrandenEK/Blasphemous-Multiworld)
You will need the [Multiworld](https://github.com/BrandenEK/Blasphemous.Randomizer.Multiworld) mod to play an Archipelago randomizer.
Optional:
- In-game map tracker: [GitHub](https://github.com/BrandenEK/Blasphemous-Rando-Map)
- Quick Prie Dieu warp mod: [GitHub](https://github.com/BadMagic100/Blasphemous-PrieWarp)
- Boots of Pleading mod: [GitHub](https://github.com/BrandenEK/Blasphemous-Boots-of-Pleading)
- Double Jump mod: [GitHub](https://github.com/BrandenEK/Blasphemous-Double-Jump)
Some optional mods are also recommended:
- [Rando Map](https://github.com/BrandenEK/Blasphemous.Randomizer.MapTracker)
- [Boots of Pleading](https://github.com/BrandenEK/Blasphemous.BootsOfPleading) (Required if the "Boots of Pleading" option is enabled)
- [Double Jump](https://github.com/BrandenEK/Blasphemous.DoubleJump) (Required if the "Purified Hand of the Nun" option is enabled)
## Mod Installer (Recommended)
To connect to a multiworld: Choose a save file and enter the address, your name, and the password (if the server has one) into the menu.
1. Download the [Mod Installer](https://github.com/BrandenEK/Blasphemous-Mod-Installer),
and point it to your install directory for Blasphemous.
2. Install the `Modding API`, `Randomizer`, and `Multiworld` mods. Optionally, you can also install the
`Rando Map`, `PrieWarp`, `Boots of Pleading`, and `Double Jump` mods, and set up the PopTracker pack if desired.
3. Start Blasphemous. To verfy that the mods are working, look for a version number for both
the Randomizer and Multiworld on the title screen.
## Manual Installation
1. Download the [Modding API](https://github.com/BrandenEK/Blasphemous-Modding-API/releases), and follow
the [installation instructions](https://github.com/BrandenEK/Blasphemous-Modding-API#installation) on the GitHub page.
2. After the Modding API has been installed, download the
[Randomizer](https://github.com/BrandenEK/Blasphemous-Randomizer/releases) and
[Multiworld](https://github.com/BrandenEK/Blasphemous-Multiworld/releases) archives, and extract the contents of both
into the `Modding` folder. Then, add any desired additional mods.
3. Start Blasphemous. To verfy that the mods are working, look for a version number for both
the Randomizer and Multiworld on the title screen.
## Connecting
To connect to an Archipelago server, open the in-game console by pressing backslash `\` and use
the command `multiworld connect [address:port] [name] [password]`.
The port and password are both optional - if no port is provided then the default port of 38281 is used.
**Make sure to connect to the server before attempting to start a new save file.**
After connecting, there are some commands you can use in the console, which can be opened by pressing backslash `\`:
- `ap status` - Display connection status.
- `ap say [message]` - Send a message to the server.
- `ap hint [item]` - Request a hint for an item from the server.

48070
worlds/blasphemous/region_data.py generated Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,56 @@
from . import BlasphemousTestBase
from ..Locations import location_names
class BotSSGauntletTest(BlasphemousTestBase):
options = {
"starting_location": "albero",
"wall_climb_shuffle": True,
"dash_shuffle": True
}
@property
def run_default_tests(self) -> bool:
return False
def test_botss_gauntlet(self) -> None:
self.assertAccessDependency([location_names["CO25"]], [["Dash Ability", "Wall Climb Ability"]], True)
class BackgroundZonesTest(BlasphemousTestBase):
@property
def run_default_tests(self) -> bool:
return False
def test_dc_shroud(self) -> None:
self.assertAccessDependency([location_names["RB03"]], [["Shroud of Dreamt Sins"]], True)
def test_wothp_bronze_cells(self) -> None:
bronze_locations = [
location_names["QI70"],
location_names["RESCUED_CHERUB_03"]
]
self.assertAccessDependency(bronze_locations, [["Key of the Secular"]], True)
def test_wothp_silver_cells(self) -> None:
silver_locations = [
location_names["CO24"],
location_names["RESCUED_CHERUB_34"],
location_names["CO37"],
location_names["RESCUED_CHERUB_04"]
]
self.assertAccessDependency(silver_locations, [["Key of the Scribe"]], True)
def test_wothp_gold_cells(self) -> None:
gold_locations = [
location_names["QI51"],
location_names["CO26"],
location_names["CO02"]
]
self.assertAccessDependency(gold_locations, [["Key of the Inquisitor"]], True)
def test_wothp_quirce(self) -> None:
self.assertAccessDependency([location_names["BS14"]], [["Key of the Secular", "Key of the Scribe", "Key of the Inquisitor"]], True)

View File

@@ -0,0 +1,135 @@
from . import BlasphemousTestBase
class TestBrotherhoodEasy(BlasphemousTestBase):
options = {
"starting_location": "brotherhood",
"difficulty": "easy"
}
class TestBrotherhoodNormal(BlasphemousTestBase):
options = {
"starting_location": "brotherhood",
"difficulty": "normal"
}
class TestBrotherhoodHard(BlasphemousTestBase):
options = {
"starting_location": "brotherhood",
"difficulty": "hard"
}
class TestAlberoEasy(BlasphemousTestBase):
options = {
"starting_location": "albero",
"difficulty": "easy"
}
class TestAlberoNormal(BlasphemousTestBase):
options = {
"starting_location": "albero",
"difficulty": "normal"
}
class TestAlberoHard(BlasphemousTestBase):
options = {
"starting_location": "albero",
"difficulty": "hard"
}
class TestConventEasy(BlasphemousTestBase):
options = {
"starting_location": "convent",
"difficulty": "easy"
}
class TestConventNormal(BlasphemousTestBase):
options = {
"starting_location": "convent",
"difficulty": "normal"
}
class TestConventHard(BlasphemousTestBase):
options = {
"starting_location": "convent",
"difficulty": "hard"
}
class TestGrievanceEasy(BlasphemousTestBase):
options = {
"starting_location": "grievance",
"difficulty": "easy"
}
class TestGrievanceNormal(BlasphemousTestBase):
options = {
"starting_location": "grievance",
"difficulty": "normal"
}
class TestGrievanceHard(BlasphemousTestBase):
options = {
"starting_location": "grievance",
"difficulty": "hard"
}
class TestKnotOfWordsEasy(BlasphemousTestBase):
options = {
"starting_location": "knot_of_words",
"difficulty": "easy"
}
class TestKnotOfWordsNormal(BlasphemousTestBase):
options = {
"starting_location": "knot_of_words",
"difficulty": "normal"
}
class TestKnotOfWordsHard(BlasphemousTestBase):
options = {
"starting_location": "knot_of_words",
"difficulty": "hard"
}
class TestRooftopsEasy(BlasphemousTestBase):
options = {
"starting_location": "rooftops",
"difficulty": "easy"
}
class TestRooftopsNormal(BlasphemousTestBase):
options = {
"starting_location": "rooftops",
"difficulty": "normal"
}
class TestRooftopsHard(BlasphemousTestBase):
options = {
"starting_location": "rooftops",
"difficulty": "hard"
}
# mourning and havoc can't be selected on easy or normal. hard only
class TestMourningHavocHard(BlasphemousTestBase):
options = {
"starting_location": "mourning_havoc",
"difficulty": "hard"
}

View File

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

View File

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

258
worlds/kh1/Client.py Normal file
View File

@@ -0,0 +1,258 @@
from __future__ import annotations
import os
import json
import sys
import asyncio
import shutil
import logging
import re
import time
from calendar import timegm
import ModuleUpdate
ModuleUpdate.update()
import Utils
death_link = False
item_num = 1
logger = logging.getLogger("Client")
if __name__ == "__main__":
Utils.init_logging("KH1Client", exception_logger="Client")
from NetUtils import NetworkItem, ClientStatus
from CommonClient import gui_enabled, logger, get_base_parser, ClientCommandProcessor, \
CommonContext, server_loop
def check_stdin() -> None:
if Utils.is_windows and sys.stdin:
print("WARNING: Console input is not routed reliably on Windows, use the GUI instead.")
class KH1ClientCommandProcessor(ClientCommandProcessor):
def _cmd_deathlink(self):
"""Toggles Deathlink"""
global death_link
if death_link:
death_link = False
self.output(f"Death Link turned off")
else:
death_link = True
self.output(f"Death Link turned on")
class KH1Context(CommonContext):
command_processor: int = KH1ClientCommandProcessor
game = "Kingdom Hearts"
items_handling = 0b111 # full remote
def __init__(self, server_address, password):
super(KH1Context, self).__init__(server_address, password)
self.send_index: int = 0
self.syncing = False
self.awaiting_bridge = False
# self.game_communication_path: files go in this path to pass data between us and the actual game
if "localappdata" in os.environ:
self.game_communication_path = os.path.expandvars(r"%localappdata%/KH1FM")
else:
self.game_communication_path = os.path.expandvars(r"$HOME/KH1FM")
if not os.path.exists(self.game_communication_path):
os.makedirs(self.game_communication_path)
for root, dirs, files in os.walk(self.game_communication_path):
for file in files:
if file.find("obtain") <= -1:
os.remove(root+"/"+file)
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(KH1Context, self).server_auth(password_requested)
await self.get_username()
await self.send_connect()
async def connection_closed(self):
await super(KH1Context, self).connection_closed()
for root, dirs, files in os.walk(self.game_communication_path):
for file in files:
if file.find("obtain") <= -1:
os.remove(root + "/" + file)
global item_num
item_num = 1
@property
def endpoints(self):
if self.server:
return [self.server]
else:
return []
async def shutdown(self):
await super(KH1Context, self).shutdown()
for root, dirs, files in os.walk(self.game_communication_path):
for file in files:
if file.find("obtain") <= -1:
os.remove(root+"/"+file)
global item_num
item_num = 1
def on_package(self, cmd: str, args: dict):
if cmd in {"Connected"}:
if not os.path.exists(self.game_communication_path):
os.makedirs(self.game_communication_path)
for ss in self.checked_locations:
filename = f"send{ss}"
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
f.close()
#Handle Slot Data
for key in list(args['slot_data'].keys()):
with open(os.path.join(self.game_communication_path, key + ".cfg"), 'w') as f:
f.write(str(args['slot_data'][key]))
f.close()
###Support Legacy Games
if "Required Reports" in list(args['slot_data'].keys()) and "required_reports_eotw" not in list(args['slot_data'].keys()):
reports_required = args['slot_data']["Required Reports"]
with open(os.path.join(self.game_communication_path, "required_reports.cfg"), 'w') as f:
f.write(str(reports_required))
f.close()
###End Support Legacy Games
#End Handle Slot Data
if cmd in {"ReceivedItems"}:
start_index = args["index"]
if start_index != len(self.items_received):
global item_num
for item in args['items']:
found = False
item_filename = f"AP_{str(item_num)}.item"
for filename in os.listdir(self.game_communication_path):
if filename == item_filename:
found = True
if not found:
with open(os.path.join(self.game_communication_path, item_filename), 'w') as f:
f.write(str(NetworkItem(*item).item) + "\n" + str(NetworkItem(*item).location) + "\n" + str(NetworkItem(*item).player))
f.close()
item_num = item_num + 1
if cmd in {"RoomUpdate"}:
if "checked_locations" in args:
for ss in self.checked_locations:
filename = f"send{ss}"
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
f.close()
if cmd in {"PrintJSON"} and "type" in args:
if args["type"] == "ItemSend":
item = args["item"]
networkItem = NetworkItem(*item)
recieverID = args["receiving"]
senderID = networkItem.player
locationID = networkItem.location
if recieverID != self.slot and senderID == self.slot:
itemName = self.item_names.lookup_in_slot(networkItem.item, recieverID)
itemCategory = networkItem.flags
recieverName = self.player_names[recieverID]
filename = "sent"
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
f.write(
re.sub('[^A-Za-z0-9 ]+', '',str(itemName))[:15] + "\n"
+ re.sub('[^A-Za-z0-9 ]+', '',str(recieverName))[:6] + "\n"
+ str(itemCategory) + "\n"
+ str(locationID))
f.close()
def on_deathlink(self, data: dict[str, object]):
self.last_death_link = max(data["time"], self.last_death_link)
text = data.get("cause", "")
if text:
logger.info(f"DeathLink: {text}")
else:
logger.info(f"DeathLink: Received from {data['source']}")
with open(os.path.join(self.game_communication_path, 'dlreceive'), 'w') as f:
f.write(str(int(data["time"])))
f.close()
def run_gui(self):
"""Import kivy UI system and start running it as self.ui_task."""
from kvui import GameManager
class KH1Manager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago KH1 Client"
self.ui = KH1Manager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
async def game_watcher(ctx: KH1Context):
from .Locations import lookup_id_to_name
while not ctx.exit_event.is_set():
global death_link
if death_link and "DeathLink" not in ctx.tags:
await ctx.update_death_link(death_link)
if not death_link and "DeathLink" in ctx.tags:
await ctx.update_death_link(death_link)
if ctx.syncing == True:
sync_msg = [{'cmd': 'Sync'}]
if ctx.locations_checked:
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
await ctx.send_msgs(sync_msg)
ctx.syncing = False
sending = []
victory = False
for root, dirs, files in os.walk(ctx.game_communication_path):
for file in files:
if file.find("send") > -1:
st = file.split("send", -1)[1]
if st != "nil":
sending = sending+[(int(st))]
if file.find("victory") > -1:
victory = True
if file.find("dlsend") > -1 and "DeathLink" in ctx.tags:
st = file.split("dlsend", -1)[1]
if st != "nil":
if timegm(time.strptime(st, '%Y%m%d%H%M%S')) > ctx.last_death_link and int(time.time()) % int(timegm(time.strptime(st, '%Y%m%d%H%M%S'))) < 10:
await ctx.send_death(death_text = "Sora was defeated!")
if file.find("insynthshop") > -1:
await ctx.send_msgs([{
"cmd": "LocationScouts",
"locations": [2656401,2656402,2656403,2656404,2656405,2656406],
"create_as_hint": 2
}])
ctx.locations_checked = sending
message = [{"cmd": 'LocationChecks', "locations": sending}]
await ctx.send_msgs(message)
if not ctx.finished_game and victory:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
await asyncio.sleep(0.1)
def launch():
async def main(args):
ctx = KH1Context(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
progression_watcher = asyncio.create_task(
game_watcher(ctx), name="KH1ProgressionWatcher")
await ctx.exit_event.wait()
ctx.server_address = None
await progression_watcher
await ctx.shutdown()
import colorama
parser = get_base_parser(description="KH1 Client, for text interfacing.")
args, rest = parser.parse_known_args()
colorama.init()
asyncio.run(main(args))
colorama.deinit()

532
worlds/kh1/Items.py Normal file
View File

@@ -0,0 +1,532 @@
from typing import Dict, NamedTuple, Optional, Set
from BaseClasses import Item, ItemClassification
class KH1Item(Item):
game: str = "Kingdom Hearts"
class KH1ItemData(NamedTuple):
category: str
code: int
classification: ItemClassification = ItemClassification.filler
max_quantity: int = 1
weight: int = 1
def get_items_by_category(category: str) -> Dict[str, KH1ItemData]:
item_dict: Dict[str, KH1ItemData] = {}
for name, data in item_table.items():
if data.category == category:
item_dict.setdefault(name, data)
return item_dict
item_table: Dict[str, KH1ItemData] = {
"Victory": KH1ItemData("VIC", code = 264_0000, classification = ItemClassification.progression, ),
"Potion": KH1ItemData("Item", code = 264_1001, classification = ItemClassification.filler, ),
"Hi-Potion": KH1ItemData("Item", code = 264_1002, classification = ItemClassification.filler, ),
"Ether": KH1ItemData("Item", code = 264_1003, classification = ItemClassification.filler, ),
"Elixir": KH1ItemData("Item", code = 264_1004, classification = ItemClassification.filler, ),
#"B05": KH1ItemData("Item", code = 264_1005, classification = ItemClassification.filler, ),
"Mega-Potion": KH1ItemData("Item", code = 264_1006, classification = ItemClassification.filler, ),
"Mega-Ether": KH1ItemData("Item", code = 264_1007, classification = ItemClassification.filler, ),
"Megalixir": KH1ItemData("Item", code = 264_1008, classification = ItemClassification.filler, ),
#"Fury Stone": KH1ItemData("Synthesis", code = 264_1009, classification = ItemClassification.filler, ),
#"Power Stone": KH1ItemData("Synthesis", code = 264_1010, classification = ItemClassification.filler, ),
#"Energy Stone": KH1ItemData("Synthesis", code = 264_1011, classification = ItemClassification.filler, ),
#"Blazing Stone": KH1ItemData("Synthesis", code = 264_1012, classification = ItemClassification.filler, ),
#"Frost Stone": KH1ItemData("Synthesis", code = 264_1013, classification = ItemClassification.filler, ),
#"Lightning Stone": KH1ItemData("Synthesis", code = 264_1014, classification = ItemClassification.filler, ),
#"Dazzling Stone": KH1ItemData("Synthesis", code = 264_1015, classification = ItemClassification.filler, ),
#"Stormy Stone": KH1ItemData("Synthesis", code = 264_1016, classification = ItemClassification.filler, ),
"Protect Chain": KH1ItemData("Accessory", code = 264_1017, classification = ItemClassification.useful, ),
"Protera Chain": KH1ItemData("Accessory", code = 264_1018, classification = ItemClassification.useful, ),
"Protega Chain": KH1ItemData("Accessory", code = 264_1019, classification = ItemClassification.useful, ),
"Fire Ring": KH1ItemData("Accessory", code = 264_1020, classification = ItemClassification.useful, ),
"Fira Ring": KH1ItemData("Accessory", code = 264_1021, classification = ItemClassification.useful, ),
"Firaga Ring": KH1ItemData("Accessory", code = 264_1022, classification = ItemClassification.useful, ),
"Blizzard Ring": KH1ItemData("Accessory", code = 264_1023, classification = ItemClassification.useful, ),
"Blizzara Ring": KH1ItemData("Accessory", code = 264_1024, classification = ItemClassification.useful, ),
"Blizzaga Ring": KH1ItemData("Accessory", code = 264_1025, classification = ItemClassification.useful, ),
"Thunder Ring": KH1ItemData("Accessory", code = 264_1026, classification = ItemClassification.useful, ),
"Thundara Ring": KH1ItemData("Accessory", code = 264_1027, classification = ItemClassification.useful, ),
"Thundaga Ring": KH1ItemData("Accessory", code = 264_1028, classification = ItemClassification.useful, ),
"Ability Stud": KH1ItemData("Accessory", code = 264_1029, classification = ItemClassification.useful, ),
"Guard Earring": KH1ItemData("Accessory", code = 264_1030, classification = ItemClassification.useful, ),
"Master Earring": KH1ItemData("Accessory", code = 264_1031, classification = ItemClassification.useful, ),
"Chaos Ring": KH1ItemData("Accessory", code = 264_1032, classification = ItemClassification.useful, ),
"Dark Ring": KH1ItemData("Accessory", code = 264_1033, classification = ItemClassification.useful, ),
"Element Ring": KH1ItemData("Accessory", code = 264_1034, classification = ItemClassification.useful, ),
"Three Stars": KH1ItemData("Accessory", code = 264_1035, classification = ItemClassification.useful, ),
"Power Chain": KH1ItemData("Accessory", code = 264_1036, classification = ItemClassification.useful, ),
"Golem Chain": KH1ItemData("Accessory", code = 264_1037, classification = ItemClassification.useful, ),
"Titan Chain": KH1ItemData("Accessory", code = 264_1038, classification = ItemClassification.useful, ),
"Energy Bangle": KH1ItemData("Accessory", code = 264_1039, classification = ItemClassification.useful, ),
"Angel Bangle": KH1ItemData("Accessory", code = 264_1040, classification = ItemClassification.useful, ),
"Gaia Bangle": KH1ItemData("Accessory", code = 264_1041, classification = ItemClassification.useful, ),
"Magic Armlet": KH1ItemData("Accessory", code = 264_1042, classification = ItemClassification.useful, ),
"Rune Armlet": KH1ItemData("Accessory", code = 264_1043, classification = ItemClassification.useful, ),
"Atlas Armlet": KH1ItemData("Accessory", code = 264_1044, classification = ItemClassification.useful, ),
"Heartguard": KH1ItemData("Accessory", code = 264_1045, classification = ItemClassification.useful, ),
"Ribbon": KH1ItemData("Accessory", code = 264_1046, classification = ItemClassification.useful, ),
"Crystal Crown": KH1ItemData("Accessory", code = 264_1047, classification = ItemClassification.useful, ),
"Brave Warrior": KH1ItemData("Accessory", code = 264_1048, classification = ItemClassification.useful, ),
"Ifrit's Horn": KH1ItemData("Accessory", code = 264_1049, classification = ItemClassification.useful, ),
"Inferno Band": KH1ItemData("Accessory", code = 264_1050, classification = ItemClassification.useful, ),
"White Fang": KH1ItemData("Accessory", code = 264_1051, classification = ItemClassification.useful, ),
"Ray of Light": KH1ItemData("Accessory", code = 264_1052, classification = ItemClassification.useful, ),
"Holy Circlet": KH1ItemData("Accessory", code = 264_1053, classification = ItemClassification.useful, ),
"Raven's Claw": KH1ItemData("Accessory", code = 264_1054, classification = ItemClassification.useful, ),
"Omega Arts": KH1ItemData("Accessory", code = 264_1055, classification = ItemClassification.useful, ),
"EXP Earring": KH1ItemData("Accessory", code = 264_1056, classification = ItemClassification.useful, ),
#"A41": KH1ItemData("Accessory", code = 264_1057, classification = ItemClassification.useful, ),
"EXP Ring": KH1ItemData("Accessory", code = 264_1058, classification = ItemClassification.useful, ),
"EXP Bracelet": KH1ItemData("Accessory", code = 264_1059, classification = ItemClassification.useful, ),
"EXP Necklace": KH1ItemData("Accessory", code = 264_1060, classification = ItemClassification.useful, ),
"Firagun Band": KH1ItemData("Accessory", code = 264_1061, classification = ItemClassification.useful, ),
"Blizzagun Band": KH1ItemData("Accessory", code = 264_1062, classification = ItemClassification.useful, ),
"Thundagun Band": KH1ItemData("Accessory", code = 264_1063, classification = ItemClassification.useful, ),
"Ifrit Belt": KH1ItemData("Accessory", code = 264_1064, classification = ItemClassification.useful, ),
"Shiva Belt": KH1ItemData("Accessory", code = 264_1065, classification = ItemClassification.useful, ),
"Ramuh Belt": KH1ItemData("Accessory", code = 264_1066, classification = ItemClassification.useful, ),
"Moogle Badge": KH1ItemData("Accessory", code = 264_1067, classification = ItemClassification.useful, ),
"Cosmic Arts": KH1ItemData("Accessory", code = 264_1068, classification = ItemClassification.useful, ),
"Royal Crown": KH1ItemData("Accessory", code = 264_1069, classification = ItemClassification.useful, ),
"Prime Cap": KH1ItemData("Accessory", code = 264_1070, classification = ItemClassification.useful, ),
"Obsidian Ring": KH1ItemData("Accessory", code = 264_1071, classification = ItemClassification.useful, ),
#"A56": KH1ItemData("Accessory", code = 264_1072, classification = ItemClassification.filler, ),
#"A57": KH1ItemData("Accessory", code = 264_1073, classification = ItemClassification.filler, ),
#"A58": KH1ItemData("Accessory", code = 264_1074, classification = ItemClassification.filler, ),
#"A59": KH1ItemData("Accessory", code = 264_1075, classification = ItemClassification.filler, ),
#"A60": KH1ItemData("Accessory", code = 264_1076, classification = ItemClassification.filler, ),
#"A61": KH1ItemData("Accessory", code = 264_1077, classification = ItemClassification.filler, ),
#"A62": KH1ItemData("Accessory", code = 264_1078, classification = ItemClassification.filler, ),
#"A63": KH1ItemData("Accessory", code = 264_1079, classification = ItemClassification.filler, ),
#"A64": KH1ItemData("Accessory", code = 264_1080, classification = ItemClassification.filler, ),
#"Kingdom Key": KH1ItemData("Keyblades", code = 264_1081, classification = ItemClassification.useful, ),
#"Dream Sword": KH1ItemData("Keyblades", code = 264_1082, classification = ItemClassification.useful, ),
#"Dream Shield": KH1ItemData("Keyblades", code = 264_1083, classification = ItemClassification.useful, ),
#"Dream Rod": KH1ItemData("Keyblades", code = 264_1084, classification = ItemClassification.useful, ),
"Wooden Sword": KH1ItemData("Keyblades", code = 264_1085, classification = ItemClassification.useful, ),
"Jungle King": KH1ItemData("Keyblades", code = 264_1086, classification = ItemClassification.progression, ),
"Three Wishes": KH1ItemData("Keyblades", code = 264_1087, classification = ItemClassification.progression, ),
"Fairy Harp": KH1ItemData("Keyblades", code = 264_1088, classification = ItemClassification.progression, ),
"Pumpkinhead": KH1ItemData("Keyblades", code = 264_1089, classification = ItemClassification.progression, ),
"Crabclaw": KH1ItemData("Keyblades", code = 264_1090, classification = ItemClassification.useful, ),
"Divine Rose": KH1ItemData("Keyblades", code = 264_1091, classification = ItemClassification.progression, ),
"Spellbinder": KH1ItemData("Keyblades", code = 264_1092, classification = ItemClassification.useful, ),
"Olympia": KH1ItemData("Keyblades", code = 264_1093, classification = ItemClassification.progression, ),
"Lionheart": KH1ItemData("Keyblades", code = 264_1094, classification = ItemClassification.progression, ),
"Metal Chocobo": KH1ItemData("Keyblades", code = 264_1095, classification = ItemClassification.useful, ),
"Oathkeeper": KH1ItemData("Keyblades", code = 264_1096, classification = ItemClassification.progression, ),
"Oblivion": KH1ItemData("Keyblades", code = 264_1097, classification = ItemClassification.progression, ),
"Lady Luck": KH1ItemData("Keyblades", code = 264_1098, classification = ItemClassification.progression, ),
"Wishing Star": KH1ItemData("Keyblades", code = 264_1099, classification = ItemClassification.progression, ),
"Ultima Weapon": KH1ItemData("Keyblades", code = 264_1100, classification = ItemClassification.useful, ),
"Diamond Dust": KH1ItemData("Keyblades", code = 264_1101, classification = ItemClassification.useful, ),
"One-Winged Angel": KH1ItemData("Keyblades", code = 264_1102, classification = ItemClassification.useful, ),
#"Mage's Staff": KH1ItemData("Weapons", code = 264_1103, classification = ItemClassification.filler, ),
"Morning Star": KH1ItemData("Weapons", code = 264_1104, classification = ItemClassification.useful, ),
"Shooting Star": KH1ItemData("Weapons", code = 264_1105, classification = ItemClassification.useful, ),
"Magus Staff": KH1ItemData("Weapons", code = 264_1106, classification = ItemClassification.useful, ),
"Wisdom Staff": KH1ItemData("Weapons", code = 264_1107, classification = ItemClassification.useful, ),
"Warhammer": KH1ItemData("Weapons", code = 264_1108, classification = ItemClassification.useful, ),
"Silver Mallet": KH1ItemData("Weapons", code = 264_1109, classification = ItemClassification.useful, ),
"Grand Mallet": KH1ItemData("Weapons", code = 264_1110, classification = ItemClassification.useful, ),
"Lord Fortune": KH1ItemData("Weapons", code = 264_1111, classification = ItemClassification.useful, ),
"Violetta": KH1ItemData("Weapons", code = 264_1112, classification = ItemClassification.useful, ),
"Dream Rod (Donald)": KH1ItemData("Weapons", code = 264_1113, classification = ItemClassification.useful, ),
"Save the Queen": KH1ItemData("Weapons", code = 264_1114, classification = ItemClassification.useful, ),
"Wizard's Relic": KH1ItemData("Weapons", code = 264_1115, classification = ItemClassification.useful, ),
"Meteor Strike": KH1ItemData("Weapons", code = 264_1116, classification = ItemClassification.useful, ),
"Fantasista": KH1ItemData("Weapons", code = 264_1117, classification = ItemClassification.useful, ),
#"Unused (Donald)": KH1ItemData("Weapons", code = 264_1118, classification = ItemClassification.filler, ),
#"Knight's Shield": KH1ItemData("Weapons", code = 264_1119, classification = ItemClassification.filler, ),
"Mythril Shield": KH1ItemData("Weapons", code = 264_1120, classification = ItemClassification.useful, ),
"Onyx Shield": KH1ItemData("Weapons", code = 264_1121, classification = ItemClassification.useful, ),
"Stout Shield": KH1ItemData("Weapons", code = 264_1122, classification = ItemClassification.useful, ),
"Golem Shield": KH1ItemData("Weapons", code = 264_1123, classification = ItemClassification.useful, ),
"Adamant Shield": KH1ItemData("Weapons", code = 264_1124, classification = ItemClassification.useful, ),
"Smasher": KH1ItemData("Weapons", code = 264_1125, classification = ItemClassification.useful, ),
"Gigas Fist": KH1ItemData("Weapons", code = 264_1126, classification = ItemClassification.useful, ),
"Genji Shield": KH1ItemData("Weapons", code = 264_1127, classification = ItemClassification.useful, ),
"Herc's Shield": KH1ItemData("Weapons", code = 264_1128, classification = ItemClassification.useful, ),
"Dream Shield (Goofy)": KH1ItemData("Weapons", code = 264_1129, classification = ItemClassification.useful, ),
"Save the King": KH1ItemData("Weapons", code = 264_1130, classification = ItemClassification.useful, ),
"Defender": KH1ItemData("Weapons", code = 264_1131, classification = ItemClassification.useful, ),
"Mighty Shield": KH1ItemData("Weapons", code = 264_1132, classification = ItemClassification.useful, ),
"Seven Elements": KH1ItemData("Weapons", code = 264_1133, classification = ItemClassification.useful, ),
#"Unused (Goofy)": KH1ItemData("Weapons", code = 264_1134, classification = ItemClassification.filler, ),
#"Spear": KH1ItemData("Weapons", code = 264_1135, classification = ItemClassification.filler, ),
#"No Weapon": KH1ItemData("Weapons", code = 264_1136, classification = ItemClassification.filler, ),
#"Genie": KH1ItemData("Weapons", code = 264_1137, classification = ItemClassification.filler, ),
#"No Weapon": KH1ItemData("Weapons", code = 264_1138, classification = ItemClassification.filler, ),
#"No Weapon": KH1ItemData("Weapons", code = 264_1139, classification = ItemClassification.filler, ),
#"Tinker Bell": KH1ItemData("Weapons", code = 264_1140, classification = ItemClassification.filler, ),
#"Claws": KH1ItemData("Weapons", code = 264_1141, classification = ItemClassification.filler, ),
"Tent": KH1ItemData("Camping", code = 264_1142, classification = ItemClassification.filler, ),
"Camping Set": KH1ItemData("Camping", code = 264_1143, classification = ItemClassification.filler, ),
"Cottage": KH1ItemData("Camping", code = 264_1144, classification = ItemClassification.filler, ),
#"C04": KH1ItemData("Camping", code = 264_1145, classification = ItemClassification.filler, ),
#"C05": KH1ItemData("Camping", code = 264_1146, classification = ItemClassification.filler, ),
#"C06": KH1ItemData("Camping", code = 264_1147, classification = ItemClassification.filler, ),
#"C07": KH1ItemData("Camping", code = 264_1148, classification = ItemClassification.filler, ),
"Ansem's Report 11": KH1ItemData("Reports", code = 264_1149, classification = ItemClassification.progression, ),
"Ansem's Report 12": KH1ItemData("Reports", code = 264_1150, classification = ItemClassification.progression, ),
"Ansem's Report 13": KH1ItemData("Reports", code = 264_1151, classification = ItemClassification.progression, ),
"Power Up": KH1ItemData("Stat Ups", code = 264_1152, classification = ItemClassification.filler, ),
"Defense Up": KH1ItemData("Stat Ups", code = 264_1153, classification = ItemClassification.filler, ),
"AP Up": KH1ItemData("Stat Ups", code = 264_1154, classification = ItemClassification.filler, ),
#"Serenity Power": KH1ItemData("Synthesis", code = 264_1155, classification = ItemClassification.filler, ),
#"Dark Matter": KH1ItemData("Synthesis", code = 264_1156, classification = ItemClassification.filler, ),
#"Mythril Stone": KH1ItemData("Synthesis", code = 264_1157, classification = ItemClassification.filler, ),
"Fire Arts": KH1ItemData("Key", code = 264_1158, classification = ItemClassification.progression, ),
"Blizzard Arts": KH1ItemData("Key", code = 264_1159, classification = ItemClassification.progression, ),
"Thunder Arts": KH1ItemData("Key", code = 264_1160, classification = ItemClassification.progression, ),
"Cure Arts": KH1ItemData("Key", code = 264_1161, classification = ItemClassification.progression, ),
"Gravity Arts": KH1ItemData("Key", code = 264_1162, classification = ItemClassification.progression, ),
"Stop Arts": KH1ItemData("Key", code = 264_1163, classification = ItemClassification.progression, ),
"Aero Arts": KH1ItemData("Key", code = 264_1164, classification = ItemClassification.progression, ),
#"Shiitank Rank": KH1ItemData("Synthesis", code = 264_1165, classification = ItemClassification.filler, ),
#"Matsutake Rank": KH1ItemData("Synthesis", code = 264_1166, classification = ItemClassification.filler, ),
#"Mystery Mold": KH1ItemData("Synthesis", code = 264_1167, classification = ItemClassification.filler, ),
"Ansem's Report 1": KH1ItemData("Reports", code = 264_1168, classification = ItemClassification.progression, ),
"Ansem's Report 2": KH1ItemData("Reports", code = 264_1169, classification = ItemClassification.progression, ),
"Ansem's Report 3": KH1ItemData("Reports", code = 264_1170, classification = ItemClassification.progression, ),
"Ansem's Report 4": KH1ItemData("Reports", code = 264_1171, classification = ItemClassification.progression, ),
"Ansem's Report 5": KH1ItemData("Reports", code = 264_1172, classification = ItemClassification.progression, ),
"Ansem's Report 6": KH1ItemData("Reports", code = 264_1173, classification = ItemClassification.progression, ),
"Ansem's Report 7": KH1ItemData("Reports", code = 264_1174, classification = ItemClassification.progression, ),
"Ansem's Report 8": KH1ItemData("Reports", code = 264_1175, classification = ItemClassification.progression, ),
"Ansem's Report 9": KH1ItemData("Reports", code = 264_1176, classification = ItemClassification.progression, ),
"Ansem's Report 10": KH1ItemData("Reports", code = 264_1177, classification = ItemClassification.progression, ),
#"Khama Vol. 8": KH1ItemData("Key", code = 264_1178, classification = ItemClassification.progression, ),
#"Salegg Vol. 6": KH1ItemData("Key", code = 264_1179, classification = ItemClassification.progression, ),
#"Azal Vol. 3": KH1ItemData("Key", code = 264_1180, classification = ItemClassification.progression, ),
#"Mava Vol. 3": KH1ItemData("Key", code = 264_1181, classification = ItemClassification.progression, ),
#"Mava Vol. 6": KH1ItemData("Key", code = 264_1182, classification = ItemClassification.progression, ),
"Theon Vol. 6": KH1ItemData("Key", code = 264_1183, classification = ItemClassification.progression, ),
#"Nahara Vol. 5": KH1ItemData("Key", code = 264_1184, classification = ItemClassification.progression, ),
#"Hafet Vol. 4": KH1ItemData("Key", code = 264_1185, classification = ItemClassification.progression, ),
"Empty Bottle": KH1ItemData("Key", code = 264_1186, classification = ItemClassification.progression, max_quantity = 6 ),
#"Old Book": KH1ItemData("Key", code = 264_1187, classification = ItemClassification.progression, ),
"Emblem Piece (Flame)": KH1ItemData("Key", code = 264_1188, classification = ItemClassification.progression, ),
"Emblem Piece (Chest)": KH1ItemData("Key", code = 264_1189, classification = ItemClassification.progression, ),
"Emblem Piece (Statue)": KH1ItemData("Key", code = 264_1190, classification = ItemClassification.progression, ),
"Emblem Piece (Fountain)": KH1ItemData("Key", code = 264_1191, classification = ItemClassification.progression, ),
#"Log": KH1ItemData("Key", code = 264_1192, classification = ItemClassification.progression, ),
#"Cloth": KH1ItemData("Key", code = 264_1193, classification = ItemClassification.progression, ),
#"Rope": KH1ItemData("Key", code = 264_1194, classification = ItemClassification.progression, ),
#"Seagull Egg": KH1ItemData("Key", code = 264_1195, classification = ItemClassification.progression, ),
#"Fish": KH1ItemData("Key", code = 264_1196, classification = ItemClassification.progression, ),
#"Mushroom": KH1ItemData("Key", code = 264_1197, classification = ItemClassification.progression, ),
#"Coconut": KH1ItemData("Key", code = 264_1198, classification = ItemClassification.progression, ),
#"Drinking Water": KH1ItemData("Key", code = 264_1199, classification = ItemClassification.progression, ),
#"Navi-G Piece 1": KH1ItemData("Key", code = 264_1200, classification = ItemClassification.progression, ),
#"Navi-G Piece 2": KH1ItemData("Key", code = 264_1201, classification = ItemClassification.progression, ),
#"Navi-Gummi Unused": KH1ItemData("Key", code = 264_1202, classification = ItemClassification.progression, ),
#"Navi-G Piece 3": KH1ItemData("Key", code = 264_1203, classification = ItemClassification.progression, ),
#"Navi-G Piece 4": KH1ItemData("Key", code = 264_1204, classification = ItemClassification.progression, ),
#"Navi-Gummi": KH1ItemData("Key", code = 264_1205, classification = ItemClassification.progression, ),
#"Watergleam": KH1ItemData("Key", code = 264_1206, classification = ItemClassification.progression, ),
#"Naturespark": KH1ItemData("Key", code = 264_1207, classification = ItemClassification.progression, ),
#"Fireglow": KH1ItemData("Key", code = 264_1208, classification = ItemClassification.progression, ),
#"Earthshine": KH1ItemData("Key", code = 264_1209, classification = ItemClassification.progression, ),
"Crystal Trident": KH1ItemData("Key", code = 264_1210, classification = ItemClassification.progression, ),
"Postcard": KH1ItemData("Key", code = 264_1211, classification = ItemClassification.progression, max_quantity = 10),
"Torn Page 1": KH1ItemData("Torn Pages", code = 264_1212, classification = ItemClassification.progression, ),
"Torn Page 2": KH1ItemData("Torn Pages", code = 264_1213, classification = ItemClassification.progression, ),
"Torn Page 3": KH1ItemData("Torn Pages", code = 264_1214, classification = ItemClassification.progression, ),
"Torn Page 4": KH1ItemData("Torn Pages", code = 264_1215, classification = ItemClassification.progression, ),
"Torn Page 5": KH1ItemData("Torn Pages", code = 264_1216, classification = ItemClassification.progression, ),
"Slides": KH1ItemData("Key", code = 264_1217, classification = ItemClassification.progression, ),
#"Slide 2": KH1ItemData("Key", code = 264_1218, classification = ItemClassification.progression, ),
#"Slide 3": KH1ItemData("Key", code = 264_1219, classification = ItemClassification.progression, ),
#"Slide 4": KH1ItemData("Key", code = 264_1220, classification = ItemClassification.progression, ),
#"Slide 5": KH1ItemData("Key", code = 264_1221, classification = ItemClassification.progression, ),
#"Slide 6": KH1ItemData("Key", code = 264_1222, classification = ItemClassification.progression, ),
"Footprints": KH1ItemData("Key", code = 264_1223, classification = ItemClassification.progression, ),
#"Claw Marks": KH1ItemData("Key", code = 264_1224, classification = ItemClassification.progression, ),
#"Stench": KH1ItemData("Key", code = 264_1225, classification = ItemClassification.progression, ),
#"Antenna": KH1ItemData("Key", code = 264_1226, classification = ItemClassification.progression, ),
"Forget-Me-Not": KH1ItemData("Key", code = 264_1227, classification = ItemClassification.progression, ),
"Jack-In-The-Box": KH1ItemData("Key", code = 264_1228, classification = ItemClassification.progression, ),
"Entry Pass": KH1ItemData("Key", code = 264_1229, classification = ItemClassification.progression, ),
#"Hero License": KH1ItemData("Key", code = 264_1230, classification = ItemClassification.progression, ),
#"Pretty Stone": KH1ItemData("Synthesis", code = 264_1231, classification = ItemClassification.filler, ),
#"N41": KH1ItemData("Synthesis", code = 264_1232, classification = ItemClassification.filler, ),
#"Lucid Shard": KH1ItemData("Synthesis", code = 264_1233, classification = ItemClassification.filler, ),
#"Lucid Gem": KH1ItemData("Synthesis", code = 264_1234, classification = ItemClassification.filler, ),
#"Lucid Crystal": KH1ItemData("Synthesis", code = 264_1235, classification = ItemClassification.filler, ),
#"Spirit Shard": KH1ItemData("Synthesis", code = 264_1236, classification = ItemClassification.filler, ),
#"Spirit Gem": KH1ItemData("Synthesis", code = 264_1237, classification = ItemClassification.filler, ),
#"Power Shard": KH1ItemData("Synthesis", code = 264_1238, classification = ItemClassification.filler, ),
#"Power Gem": KH1ItemData("Synthesis", code = 264_1239, classification = ItemClassification.filler, ),
#"Power Crystal": KH1ItemData("Synthesis", code = 264_1240, classification = ItemClassification.filler, ),
#"Blaze Shard": KH1ItemData("Synthesis", code = 264_1241, classification = ItemClassification.filler, ),
#"Blaze Gem": KH1ItemData("Synthesis", code = 264_1242, classification = ItemClassification.filler, ),
#"Frost Shard": KH1ItemData("Synthesis", code = 264_1243, classification = ItemClassification.filler, ),
#"Frost Gem": KH1ItemData("Synthesis", code = 264_1244, classification = ItemClassification.filler, ),
#"Thunder Shard": KH1ItemData("Synthesis", code = 264_1245, classification = ItemClassification.filler, ),
#"Thunder Gem": KH1ItemData("Synthesis", code = 264_1246, classification = ItemClassification.filler, ),
#"Shiny Crystal": KH1ItemData("Synthesis", code = 264_1247, classification = ItemClassification.filler, ),
#"Bright Shard": KH1ItemData("Synthesis", code = 264_1248, classification = ItemClassification.filler, ),
#"Bright Gem": KH1ItemData("Synthesis", code = 264_1249, classification = ItemClassification.filler, ),
#"Bright Crystal": KH1ItemData("Synthesis", code = 264_1250, classification = ItemClassification.filler, ),
#"Mystery Goo": KH1ItemData("Synthesis", code = 264_1251, classification = ItemClassification.filler, ),
#"Gale": KH1ItemData("Synthesis", code = 264_1252, classification = ItemClassification.filler, ),
#"Mythril Shard": KH1ItemData("Synthesis", code = 264_1253, classification = ItemClassification.filler, ),
#"Mythril": KH1ItemData("Synthesis", code = 264_1254, classification = ItemClassification.filler, ),
#"Orichalcum": KH1ItemData("Synthesis", code = 264_1255, classification = ItemClassification.filler, ),
"High Jump": KH1ItemData("Shared Abilities", code = 264_2001, classification = ItemClassification.progression, ),
"Mermaid Kick": KH1ItemData("Shared Abilities", code = 264_2002, classification = ItemClassification.progression, ),
"Progressive Glide": KH1ItemData("Shared Abilities", code = 264_2003, classification = ItemClassification.progression, max_quantity = 2 ),
#"Superglide": KH1ItemData("Shared Abilities", code = 264_2004, classification = ItemClassification.progression, ),
"Puppy 01": KH1ItemData("Puppies", code = 264_2101, classification = ItemClassification.progression, ),
"Puppy 02": KH1ItemData("Puppies", code = 264_2102, classification = ItemClassification.progression, ),
"Puppy 03": KH1ItemData("Puppies", code = 264_2103, classification = ItemClassification.progression, ),
"Puppy 04": KH1ItemData("Puppies", code = 264_2104, classification = ItemClassification.progression, ),
"Puppy 05": KH1ItemData("Puppies", code = 264_2105, classification = ItemClassification.progression, ),
"Puppy 06": KH1ItemData("Puppies", code = 264_2106, classification = ItemClassification.progression, ),
"Puppy 07": KH1ItemData("Puppies", code = 264_2107, classification = ItemClassification.progression, ),
"Puppy 08": KH1ItemData("Puppies", code = 264_2108, classification = ItemClassification.progression, ),
"Puppy 09": KH1ItemData("Puppies", code = 264_2109, classification = ItemClassification.progression, ),
"Puppy 10": KH1ItemData("Puppies", code = 264_2110, classification = ItemClassification.progression, ),
"Puppy 11": KH1ItemData("Puppies", code = 264_2111, classification = ItemClassification.progression, ),
"Puppy 12": KH1ItemData("Puppies", code = 264_2112, classification = ItemClassification.progression, ),
"Puppy 13": KH1ItemData("Puppies", code = 264_2113, classification = ItemClassification.progression, ),
"Puppy 14": KH1ItemData("Puppies", code = 264_2114, classification = ItemClassification.progression, ),
"Puppy 15": KH1ItemData("Puppies", code = 264_2115, classification = ItemClassification.progression, ),
"Puppy 16": KH1ItemData("Puppies", code = 264_2116, classification = ItemClassification.progression, ),
"Puppy 17": KH1ItemData("Puppies", code = 264_2117, classification = ItemClassification.progression, ),
"Puppy 18": KH1ItemData("Puppies", code = 264_2118, classification = ItemClassification.progression, ),
"Puppy 19": KH1ItemData("Puppies", code = 264_2119, classification = ItemClassification.progression, ),
"Puppy 20": KH1ItemData("Puppies", code = 264_2120, classification = ItemClassification.progression, ),
"Puppy 21": KH1ItemData("Puppies", code = 264_2121, classification = ItemClassification.progression, ),
"Puppy 22": KH1ItemData("Puppies", code = 264_2122, classification = ItemClassification.progression, ),
"Puppy 23": KH1ItemData("Puppies", code = 264_2123, classification = ItemClassification.progression, ),
"Puppy 24": KH1ItemData("Puppies", code = 264_2124, classification = ItemClassification.progression, ),
"Puppy 25": KH1ItemData("Puppies", code = 264_2125, classification = ItemClassification.progression, ),
"Puppy 26": KH1ItemData("Puppies", code = 264_2126, classification = ItemClassification.progression, ),
"Puppy 27": KH1ItemData("Puppies", code = 264_2127, classification = ItemClassification.progression, ),
"Puppy 28": KH1ItemData("Puppies", code = 264_2128, classification = ItemClassification.progression, ),
"Puppy 29": KH1ItemData("Puppies", code = 264_2129, classification = ItemClassification.progression, ),
"Puppy 30": KH1ItemData("Puppies", code = 264_2130, classification = ItemClassification.progression, ),
"Puppy 31": KH1ItemData("Puppies", code = 264_2131, classification = ItemClassification.progression, ),
"Puppy 32": KH1ItemData("Puppies", code = 264_2132, classification = ItemClassification.progression, ),
"Puppy 33": KH1ItemData("Puppies", code = 264_2133, classification = ItemClassification.progression, ),
"Puppy 34": KH1ItemData("Puppies", code = 264_2134, classification = ItemClassification.progression, ),
"Puppy 35": KH1ItemData("Puppies", code = 264_2135, classification = ItemClassification.progression, ),
"Puppy 36": KH1ItemData("Puppies", code = 264_2136, classification = ItemClassification.progression, ),
"Puppy 37": KH1ItemData("Puppies", code = 264_2137, classification = ItemClassification.progression, ),
"Puppy 38": KH1ItemData("Puppies", code = 264_2138, classification = ItemClassification.progression, ),
"Puppy 39": KH1ItemData("Puppies", code = 264_2139, classification = ItemClassification.progression, ),
"Puppy 40": KH1ItemData("Puppies", code = 264_2140, classification = ItemClassification.progression, ),
"Puppy 41": KH1ItemData("Puppies", code = 264_2141, classification = ItemClassification.progression, ),
"Puppy 42": KH1ItemData("Puppies", code = 264_2142, classification = ItemClassification.progression, ),
"Puppy 43": KH1ItemData("Puppies", code = 264_2143, classification = ItemClassification.progression, ),
"Puppy 44": KH1ItemData("Puppies", code = 264_2144, classification = ItemClassification.progression, ),
"Puppy 45": KH1ItemData("Puppies", code = 264_2145, classification = ItemClassification.progression, ),
"Puppy 46": KH1ItemData("Puppies", code = 264_2146, classification = ItemClassification.progression, ),
"Puppy 47": KH1ItemData("Puppies", code = 264_2147, classification = ItemClassification.progression, ),
"Puppy 48": KH1ItemData("Puppies", code = 264_2148, classification = ItemClassification.progression, ),
"Puppy 49": KH1ItemData("Puppies", code = 264_2149, classification = ItemClassification.progression, ),
"Puppy 50": KH1ItemData("Puppies", code = 264_2150, classification = ItemClassification.progression, ),
"Puppy 51": KH1ItemData("Puppies", code = 264_2151, classification = ItemClassification.progression, ),
"Puppy 52": KH1ItemData("Puppies", code = 264_2152, classification = ItemClassification.progression, ),
"Puppy 53": KH1ItemData("Puppies", code = 264_2153, classification = ItemClassification.progression, ),
"Puppy 54": KH1ItemData("Puppies", code = 264_2154, classification = ItemClassification.progression, ),
"Puppy 55": KH1ItemData("Puppies", code = 264_2155, classification = ItemClassification.progression, ),
"Puppy 56": KH1ItemData("Puppies", code = 264_2156, classification = ItemClassification.progression, ),
"Puppy 57": KH1ItemData("Puppies", code = 264_2157, classification = ItemClassification.progression, ),
"Puppy 58": KH1ItemData("Puppies", code = 264_2158, classification = ItemClassification.progression, ),
"Puppy 59": KH1ItemData("Puppies", code = 264_2159, classification = ItemClassification.progression, ),
"Puppy 60": KH1ItemData("Puppies", code = 264_2160, classification = ItemClassification.progression, ),
"Puppy 61": KH1ItemData("Puppies", code = 264_2161, classification = ItemClassification.progression, ),
"Puppy 62": KH1ItemData("Puppies", code = 264_2162, classification = ItemClassification.progression, ),
"Puppy 63": KH1ItemData("Puppies", code = 264_2163, classification = ItemClassification.progression, ),
"Puppy 64": KH1ItemData("Puppies", code = 264_2164, classification = ItemClassification.progression, ),
"Puppy 65": KH1ItemData("Puppies", code = 264_2165, classification = ItemClassification.progression, ),
"Puppy 66": KH1ItemData("Puppies", code = 264_2166, classification = ItemClassification.progression, ),
"Puppy 67": KH1ItemData("Puppies", code = 264_2167, classification = ItemClassification.progression, ),
"Puppy 68": KH1ItemData("Puppies", code = 264_2168, classification = ItemClassification.progression, ),
"Puppy 69": KH1ItemData("Puppies", code = 264_2169, classification = ItemClassification.progression, ),
"Puppy 70": KH1ItemData("Puppies", code = 264_2170, classification = ItemClassification.progression, ),
"Puppy 71": KH1ItemData("Puppies", code = 264_2171, classification = ItemClassification.progression, ),
"Puppy 72": KH1ItemData("Puppies", code = 264_2172, classification = ItemClassification.progression, ),
"Puppy 73": KH1ItemData("Puppies", code = 264_2173, classification = ItemClassification.progression, ),
"Puppy 74": KH1ItemData("Puppies", code = 264_2174, classification = ItemClassification.progression, ),
"Puppy 75": KH1ItemData("Puppies", code = 264_2175, classification = ItemClassification.progression, ),
"Puppy 76": KH1ItemData("Puppies", code = 264_2176, classification = ItemClassification.progression, ),
"Puppy 77": KH1ItemData("Puppies", code = 264_2177, classification = ItemClassification.progression, ),
"Puppy 78": KH1ItemData("Puppies", code = 264_2178, classification = ItemClassification.progression, ),
"Puppy 79": KH1ItemData("Puppies", code = 264_2179, classification = ItemClassification.progression, ),
"Puppy 80": KH1ItemData("Puppies", code = 264_2180, classification = ItemClassification.progression, ),
"Puppy 81": KH1ItemData("Puppies", code = 264_2181, classification = ItemClassification.progression, ),
"Puppy 82": KH1ItemData("Puppies", code = 264_2182, classification = ItemClassification.progression, ),
"Puppy 83": KH1ItemData("Puppies", code = 264_2183, classification = ItemClassification.progression, ),
"Puppy 84": KH1ItemData("Puppies", code = 264_2184, classification = ItemClassification.progression, ),
"Puppy 85": KH1ItemData("Puppies", code = 264_2185, classification = ItemClassification.progression, ),
"Puppy 86": KH1ItemData("Puppies", code = 264_2186, classification = ItemClassification.progression, ),
"Puppy 87": KH1ItemData("Puppies", code = 264_2187, classification = ItemClassification.progression, ),
"Puppy 88": KH1ItemData("Puppies", code = 264_2188, classification = ItemClassification.progression, ),
"Puppy 89": KH1ItemData("Puppies", code = 264_2189, classification = ItemClassification.progression, ),
"Puppy 90": KH1ItemData("Puppies", code = 264_2190, classification = ItemClassification.progression, ),
"Puppy 91": KH1ItemData("Puppies", code = 264_2191, classification = ItemClassification.progression, ),
"Puppy 92": KH1ItemData("Puppies", code = 264_2192, classification = ItemClassification.progression, ),
"Puppy 93": KH1ItemData("Puppies", code = 264_2193, classification = ItemClassification.progression, ),
"Puppy 94": KH1ItemData("Puppies", code = 264_2194, classification = ItemClassification.progression, ),
"Puppy 95": KH1ItemData("Puppies", code = 264_2195, classification = ItemClassification.progression, ),
"Puppy 96": KH1ItemData("Puppies", code = 264_2196, classification = ItemClassification.progression, ),
"Puppy 97": KH1ItemData("Puppies", code = 264_2197, classification = ItemClassification.progression, ),
"Puppy 98": KH1ItemData("Puppies", code = 264_2198, classification = ItemClassification.progression, ),
"Puppy 99": KH1ItemData("Puppies", code = 264_2199, classification = ItemClassification.progression, ),
"Puppies 01-03": KH1ItemData("Puppies", code = 264_2201, classification = ItemClassification.progression, ),
"Puppies 04-06": KH1ItemData("Puppies", code = 264_2202, classification = ItemClassification.progression, ),
"Puppies 07-09": KH1ItemData("Puppies", code = 264_2203, classification = ItemClassification.progression, ),
"Puppies 10-12": KH1ItemData("Puppies", code = 264_2204, classification = ItemClassification.progression, ),
"Puppies 13-15": KH1ItemData("Puppies", code = 264_2205, classification = ItemClassification.progression, ),
"Puppies 16-18": KH1ItemData("Puppies", code = 264_2206, classification = ItemClassification.progression, ),
"Puppies 19-21": KH1ItemData("Puppies", code = 264_2207, classification = ItemClassification.progression, ),
"Puppies 22-24": KH1ItemData("Puppies", code = 264_2208, classification = ItemClassification.progression, ),
"Puppies 25-27": KH1ItemData("Puppies", code = 264_2209, classification = ItemClassification.progression, ),
"Puppies 28-30": KH1ItemData("Puppies", code = 264_2210, classification = ItemClassification.progression, ),
"Puppies 31-33": KH1ItemData("Puppies", code = 264_2211, classification = ItemClassification.progression, ),
"Puppies 34-36": KH1ItemData("Puppies", code = 264_2212, classification = ItemClassification.progression, ),
"Puppies 37-39": KH1ItemData("Puppies", code = 264_2213, classification = ItemClassification.progression, ),
"Puppies 40-42": KH1ItemData("Puppies", code = 264_2214, classification = ItemClassification.progression, ),
"Puppies 43-45": KH1ItemData("Puppies", code = 264_2215, classification = ItemClassification.progression, ),
"Puppies 46-48": KH1ItemData("Puppies", code = 264_2216, classification = ItemClassification.progression, ),
"Puppies 49-51": KH1ItemData("Puppies", code = 264_2217, classification = ItemClassification.progression, ),
"Puppies 52-54": KH1ItemData("Puppies", code = 264_2218, classification = ItemClassification.progression, ),
"Puppies 55-57": KH1ItemData("Puppies", code = 264_2219, classification = ItemClassification.progression, ),
"Puppies 58-60": KH1ItemData("Puppies", code = 264_2220, classification = ItemClassification.progression, ),
"Puppies 61-63": KH1ItemData("Puppies", code = 264_2221, classification = ItemClassification.progression, ),
"Puppies 64-66": KH1ItemData("Puppies", code = 264_2222, classification = ItemClassification.progression, ),
"Puppies 67-69": KH1ItemData("Puppies", code = 264_2223, classification = ItemClassification.progression, ),
"Puppies 70-72": KH1ItemData("Puppies", code = 264_2224, classification = ItemClassification.progression, ),
"Puppies 73-75": KH1ItemData("Puppies", code = 264_2225, classification = ItemClassification.progression, ),
"Puppies 76-78": KH1ItemData("Puppies", code = 264_2226, classification = ItemClassification.progression, ),
"Puppies 79-81": KH1ItemData("Puppies", code = 264_2227, classification = ItemClassification.progression, ),
"Puppies 82-84": KH1ItemData("Puppies", code = 264_2228, classification = ItemClassification.progression, ),
"Puppies 85-87": KH1ItemData("Puppies", code = 264_2229, classification = ItemClassification.progression, ),
"Puppies 88-90": KH1ItemData("Puppies", code = 264_2230, classification = ItemClassification.progression, ),
"Puppies 91-93": KH1ItemData("Puppies", code = 264_2231, classification = ItemClassification.progression, ),
"Puppies 94-96": KH1ItemData("Puppies", code = 264_2232, classification = ItemClassification.progression, ),
"Puppies 97-99": KH1ItemData("Puppies", code = 264_2233, classification = ItemClassification.progression, ),
"All Puppies": KH1ItemData("Puppies", code = 264_2240, classification = ItemClassification.progression, ),
"Treasure Magnet": KH1ItemData("Abilities", code = 264_3005, classification = ItemClassification.useful, max_quantity = 2 ),
"Combo Plus": KH1ItemData("Abilities", code = 264_3006, classification = ItemClassification.useful, max_quantity = 4 ),
"Air Combo Plus": KH1ItemData("Abilities", code = 264_3007, classification = ItemClassification.useful, max_quantity = 2 ),
"Critical Plus": KH1ItemData("Abilities", code = 264_3008, classification = ItemClassification.useful, max_quantity = 3 ),
#"Second Wind": KH1ItemData("Abilities", code = 264_3009, classification = ItemClassification.useful, ),
"Scan": KH1ItemData("Abilities", code = 264_3010, classification = ItemClassification.useful, ),
"Sonic Blade": KH1ItemData("Abilities", code = 264_3011, classification = ItemClassification.useful, ),
"Ars Arcanum": KH1ItemData("Abilities", code = 264_3012, classification = ItemClassification.useful, ),
"Strike Raid": KH1ItemData("Abilities", code = 264_3013, classification = ItemClassification.useful, ),
"Ragnarok": KH1ItemData("Abilities", code = 264_3014, classification = ItemClassification.useful, ),
"Trinity Limit": KH1ItemData("Abilities", code = 264_3015, classification = ItemClassification.useful, ),
"Cheer": KH1ItemData("Abilities", code = 264_3016, classification = ItemClassification.useful, ),
"Vortex": KH1ItemData("Abilities", code = 264_3017, classification = ItemClassification.useful, ),
"Aerial Sweep": KH1ItemData("Abilities", code = 264_3018, classification = ItemClassification.useful, ),
"Counterattack": KH1ItemData("Abilities", code = 264_3019, classification = ItemClassification.useful, ),
"Blitz": KH1ItemData("Abilities", code = 264_3020, classification = ItemClassification.useful, ),
"Guard": KH1ItemData("Abilities", code = 264_3021, classification = ItemClassification.progression, ),
"Dodge Roll": KH1ItemData("Abilities", code = 264_3022, classification = ItemClassification.progression, ),
"MP Haste": KH1ItemData("Abilities", code = 264_3023, classification = ItemClassification.useful, ),
"MP Rage": KH1ItemData("Abilities", code = 264_3024, classification = ItemClassification.progression, ),
"Second Chance": KH1ItemData("Abilities", code = 264_3025, classification = ItemClassification.progression, ),
"Berserk": KH1ItemData("Abilities", code = 264_3026, classification = ItemClassification.useful, ),
"Jackpot": KH1ItemData("Abilities", code = 264_3027, classification = ItemClassification.useful, ),
"Lucky Strike": KH1ItemData("Abilities", code = 264_3028, classification = ItemClassification.useful, ),
#"Charge": KH1ItemData("Abilities", code = 264_3029, classification = ItemClassification.useful, ),
#"Rocket": KH1ItemData("Abilities", code = 264_3030, classification = ItemClassification.useful, ),
#"Tornado": KH1ItemData("Abilities", code = 264_3031, classification = ItemClassification.useful, ),
#"MP Gift": KH1ItemData("Abilities", code = 264_3032, classification = ItemClassification.useful, ),
#"Raging Boar": KH1ItemData("Abilities", code = 264_3033, classification = ItemClassification.useful, ),
#"Asp's Bite": KH1ItemData("Abilities", code = 264_3034, classification = ItemClassification.useful, ),
#"Healing Herb": KH1ItemData("Abilities", code = 264_3035, classification = ItemClassification.useful, ),
#"Wind Armor": KH1ItemData("Abilities", code = 264_3036, classification = ItemClassification.useful, ),
#"Crescent": KH1ItemData("Abilities", code = 264_3037, classification = ItemClassification.useful, ),
#"Sandstorm": KH1ItemData("Abilities", code = 264_3038, classification = ItemClassification.useful, ),
#"Applause!": KH1ItemData("Abilities", code = 264_3039, classification = ItemClassification.useful, ),
#"Blazing Fury": KH1ItemData("Abilities", code = 264_3040, classification = ItemClassification.useful, ),
#"Icy Terror": KH1ItemData("Abilities", code = 264_3041, classification = ItemClassification.useful, ),
#"Bolts of Sorrow": KH1ItemData("Abilities", code = 264_3042, classification = ItemClassification.useful, ),
#"Ghostly Scream": KH1ItemData("Abilities", code = 264_3043, classification = ItemClassification.useful, ),
#"Humming Bird": KH1ItemData("Abilities", code = 264_3044, classification = ItemClassification.useful, ),
#"Time-Out": KH1ItemData("Abilities", code = 264_3045, classification = ItemClassification.useful, ),
#"Storm's Eye": KH1ItemData("Abilities", code = 264_3046, classification = ItemClassification.useful, ),
#"Ferocious Lunge": KH1ItemData("Abilities", code = 264_3047, classification = ItemClassification.useful, ),
#"Furious Bellow": KH1ItemData("Abilities", code = 264_3048, classification = ItemClassification.useful, ),
#"Spiral Wave": KH1ItemData("Abilities", code = 264_3049, classification = ItemClassification.useful, ),
#"Thunder Potion": KH1ItemData("Abilities", code = 264_3050, classification = ItemClassification.useful, ),
#"Cure Potion": KH1ItemData("Abilities", code = 264_3051, classification = ItemClassification.useful, ),
#"Aero Potion": KH1ItemData("Abilities", code = 264_3052, classification = ItemClassification.useful, ),
"Slapshot": KH1ItemData("Abilities", code = 264_3053, classification = ItemClassification.useful, ),
"Sliding Dash": KH1ItemData("Abilities", code = 264_3054, classification = ItemClassification.useful, ),
"Hurricane Blast": KH1ItemData("Abilities", code = 264_3055, classification = ItemClassification.useful, ),
"Ripple Drive": KH1ItemData("Abilities", code = 264_3056, classification = ItemClassification.useful, ),
"Stun Impact": KH1ItemData("Abilities", code = 264_3057, classification = ItemClassification.useful, ),
"Gravity Break": KH1ItemData("Abilities", code = 264_3058, classification = ItemClassification.useful, ),
"Zantetsuken": KH1ItemData("Abilities", code = 264_3059, classification = ItemClassification.useful, ),
"Tech Boost": KH1ItemData("Abilities", code = 264_3060, classification = ItemClassification.useful, max_quantity = 4 ),
"Encounter Plus": KH1ItemData("Abilities", code = 264_3061, classification = ItemClassification.useful, ),
"Leaf Bracer": KH1ItemData("Abilities", code = 264_3062, classification = ItemClassification.progression, ),
#"Evolution": KH1ItemData("Abilities", code = 264_3063, classification = ItemClassification.useful, ),
"EXP Zero": KH1ItemData("Abilities", code = 264_3064, classification = ItemClassification.useful, ),
"Combo Master": KH1ItemData("Abilities", code = 264_3065, classification = ItemClassification.progression, ),
"Max HP Increase": KH1ItemData("Level Up", code = 264_4001, classification = ItemClassification.useful, max_quantity = 15),
"Max MP Increase": KH1ItemData("Level Up", code = 264_4002, classification = ItemClassification.useful, max_quantity = 15),
"Max AP Increase": KH1ItemData("Level Up", code = 264_4003, classification = ItemClassification.useful, max_quantity = 15),
"Strength Increase": KH1ItemData("Level Up", code = 264_4004, classification = ItemClassification.useful, max_quantity = 15),
"Defense Increase": KH1ItemData("Level Up", code = 264_4005, classification = ItemClassification.useful, max_quantity = 15),
"Accessory Slot Increase": KH1ItemData("Limited Level Up", code = 264_4006, classification = ItemClassification.useful, max_quantity = 15),
"Item Slot Increase": KH1ItemData("Limited Level Up", code = 264_4007, classification = ItemClassification.useful, max_quantity = 15),
"Dumbo": KH1ItemData("Summons", code = 264_5000, classification = ItemClassification.progression, ),
"Bambi": KH1ItemData("Summons", code = 264_5001, classification = ItemClassification.progression, ),
"Genie": KH1ItemData("Summons", code = 264_5002, classification = ItemClassification.progression, ),
"Tinker Bell": KH1ItemData("Summons", code = 264_5003, classification = ItemClassification.progression, ),
"Mushu": KH1ItemData("Summons", code = 264_5004, classification = ItemClassification.progression, ),
"Simba": KH1ItemData("Summons", code = 264_5005, classification = ItemClassification.progression, ),
"Progressive Fire": KH1ItemData("Magic", code = 264_6001, classification = ItemClassification.progression, max_quantity = 3 ),
"Progressive Blizzard": KH1ItemData("Magic", code = 264_6002, classification = ItemClassification.progression, max_quantity = 3 ),
"Progressive Thunder": KH1ItemData("Magic", code = 264_6003, classification = ItemClassification.progression, max_quantity = 3 ),
"Progressive Cure": KH1ItemData("Magic", code = 264_6004, classification = ItemClassification.progression, max_quantity = 3 ),
"Progressive Gravity": KH1ItemData("Magic", code = 264_6005, classification = ItemClassification.progression, max_quantity = 3 ),
"Progressive Stop": KH1ItemData("Magic", code = 264_6006, classification = ItemClassification.progression, max_quantity = 3 ),
"Progressive Aero": KH1ItemData("Magic", code = 264_6007, classification = ItemClassification.progression, max_quantity = 3 ),
#"Traverse Town": KH1ItemData("Worlds", code = 264_7001, classification = ItemClassification.progression, ),
"Wonderland": KH1ItemData("Worlds", code = 264_7002, classification = ItemClassification.progression, ),
"Olympus Coliseum": KH1ItemData("Worlds", code = 264_7003, classification = ItemClassification.progression, ),
"Deep Jungle": KH1ItemData("Worlds", code = 264_7004, classification = ItemClassification.progression, ),
"Agrabah": KH1ItemData("Worlds", code = 264_7005, classification = ItemClassification.progression, ),
"Halloween Town": KH1ItemData("Worlds", code = 264_7006, classification = ItemClassification.progression, ),
"Atlantica": KH1ItemData("Worlds", code = 264_7007, classification = ItemClassification.progression, ),
"Neverland": KH1ItemData("Worlds", code = 264_7008, classification = ItemClassification.progression, ),
"Hollow Bastion": KH1ItemData("Worlds", code = 264_7009, classification = ItemClassification.progression, ),
"End of the World": KH1ItemData("Worlds", code = 264_7010, classification = ItemClassification.progression, ),
"Monstro": KH1ItemData("Worlds", code = 264_7011, classification = ItemClassification.progression, ),
"Blue Trinity": KH1ItemData("Trinities", code = 264_8001, classification = ItemClassification.progression, ),
"Red Trinity": KH1ItemData("Trinities", code = 264_8002, classification = ItemClassification.progression, ),
"Green Trinity": KH1ItemData("Trinities", code = 264_8003, classification = ItemClassification.progression, ),
"Yellow Trinity": KH1ItemData("Trinities", code = 264_8004, classification = ItemClassification.progression, ),
"White Trinity": KH1ItemData("Trinities", code = 264_8005, classification = ItemClassification.progression, ),
"Phil Cup": KH1ItemData("Cups", code = 264_9001, classification = ItemClassification.progression, ),
"Pegasus Cup": KH1ItemData("Cups", code = 264_9002, classification = ItemClassification.progression, ),
"Hercules Cup": KH1ItemData("Cups", code = 264_9003, classification = ItemClassification.progression, ),
#"Hades Cup": KH1ItemData("Cups", code = 264_9004, classification = ItemClassification.progression, ),
}
event_item_table: Dict[str, KH1ItemData] = {}
#Make item categories
item_name_groups: Dict[str, Set[str]] = {}
for item in item_table.keys():
category = item_table[item].category
if category not in item_name_groups.keys():
item_name_groups[category] = set()
item_name_groups[category].add(item)

590
worlds/kh1/Locations.py Normal file
View File

@@ -0,0 +1,590 @@
from typing import Dict, NamedTuple, Optional, Set
import typing
from BaseClasses import Location
class KH1Location(Location):
game: str = "Kingdom Hearts"
class KH1LocationData(NamedTuple):
category: str
code: int
def get_locations_by_category(category: str) -> Dict[str, KH1LocationData]:
location_dict: Dict[str, KH1LocationData] = {}
for name, data in location_table.items():
if data.category == category:
location_dict.setdefault(name, data)
return location_dict
location_table: Dict[str, KH1LocationData] = {
#"Destiny Islands Chest": KH1LocationData("Destiny Islands", 265_0011), missable
"Traverse Town 1st District Candle Puzzle Chest": KH1LocationData("Traverse Town", 265_0211),
"Traverse Town 1st District Accessory Shop Roof Chest": KH1LocationData("Traverse Town", 265_0212),
"Traverse Town 2nd District Boots and Shoes Awning Chest": KH1LocationData("Traverse Town", 265_0213),
"Traverse Town 2nd District Rooftop Chest": KH1LocationData("Traverse Town", 265_0214),
"Traverse Town 2nd District Gizmo Shop Facade Chest": KH1LocationData("Traverse Town", 265_0251),
"Traverse Town Alleyway Balcony Chest": KH1LocationData("Traverse Town", 265_0252),
"Traverse Town Alleyway Blue Room Awning Chest": KH1LocationData("Traverse Town", 265_0253),
"Traverse Town Alleyway Corner Chest": KH1LocationData("Traverse Town", 265_0254),
"Traverse Town Green Room Clock Puzzle Chest": KH1LocationData("Traverse Town", 265_0292),
"Traverse Town Green Room Table Chest": KH1LocationData("Traverse Town", 265_0293),
"Traverse Town Red Room Chest": KH1LocationData("Traverse Town", 265_0294),
"Traverse Town Mystical House Yellow Trinity Chest": KH1LocationData("Traverse Town", 265_0331),
"Traverse Town Accessory Shop Chest": KH1LocationData("Traverse Town", 265_0332),
"Traverse Town Secret Waterway White Trinity Chest": KH1LocationData("Traverse Town", 265_0333),
"Traverse Town Geppetto's House Chest": KH1LocationData("Traverse Town", 265_0334),
"Traverse Town Item Workshop Right Chest": KH1LocationData("Traverse Town", 265_0371),
"Traverse Town 1st District Blue Trinity Balcony Chest": KH1LocationData("Traverse Town", 265_0411),
"Traverse Town Mystical House Glide Chest": KH1LocationData("Traverse Town", 265_0891),
"Traverse Town Alleyway Behind Crates Chest": KH1LocationData("Traverse Town", 265_0892),
"Traverse Town Item Workshop Left Chest": KH1LocationData("Traverse Town", 265_0893),
"Traverse Town Secret Waterway Near Stairs Chest": KH1LocationData("Traverse Town", 265_0894),
"Wonderland Rabbit Hole Green Trinity Chest": KH1LocationData("Wonderland", 265_0931),
"Wonderland Rabbit Hole Defeat Heartless 1 Chest": KH1LocationData("Wonderland", 265_0932),
"Wonderland Rabbit Hole Defeat Heartless 2 Chest": KH1LocationData("Wonderland", 265_0933),
"Wonderland Rabbit Hole Defeat Heartless 3 Chest": KH1LocationData("Wonderland", 265_0934),
"Wonderland Bizarre Room Green Trinity Chest": KH1LocationData("Wonderland", 265_0971),
"Wonderland Queen's Castle Hedge Left Red Chest": KH1LocationData("Wonderland", 265_1011),
"Wonderland Queen's Castle Hedge Right Blue Chest": KH1LocationData("Wonderland", 265_1012),
"Wonderland Queen's Castle Hedge Right Red Chest": KH1LocationData("Wonderland", 265_1013),
"Wonderland Lotus Forest Thunder Plant Chest": KH1LocationData("Wonderland", 265_1014),
"Wonderland Lotus Forest Through the Painting Thunder Plant Chest": KH1LocationData("Wonderland", 265_1051),
"Wonderland Lotus Forest Glide Chest": KH1LocationData("Wonderland", 265_1052),
"Wonderland Lotus Forest Nut Chest": KH1LocationData("Wonderland", 265_1053),
"Wonderland Lotus Forest Corner Chest": KH1LocationData("Wonderland", 265_1054),
"Wonderland Bizarre Room Lamp Chest": KH1LocationData("Wonderland", 265_1091),
"Wonderland Tea Party Garden Above Lotus Forest Entrance 2nd Chest": KH1LocationData("Wonderland", 265_1093),
"Wonderland Tea Party Garden Above Lotus Forest Entrance 1st Chest": KH1LocationData("Wonderland", 265_1094),
"Wonderland Tea Party Garden Bear and Clock Puzzle Chest": KH1LocationData("Wonderland", 265_1131),
"Wonderland Tea Party Garden Across From Bizarre Room Entrance Chest": KH1LocationData("Wonderland", 265_1132),
"Wonderland Lotus Forest Through the Painting White Trinity Chest": KH1LocationData("Wonderland", 265_1133),
"Deep Jungle Tree House Beneath Tree House Chest": KH1LocationData("Deep Jungle", 265_1213),
"Deep Jungle Tree House Rooftop Chest": KH1LocationData("Deep Jungle", 265_1214),
"Deep Jungle Hippo's Lagoon Center Chest": KH1LocationData("Deep Jungle", 265_1251),
"Deep Jungle Hippo's Lagoon Left Chest": KH1LocationData("Deep Jungle", 265_1252),
"Deep Jungle Hippo's Lagoon Right Chest": KH1LocationData("Deep Jungle", 265_1253),
"Deep Jungle Vines Chest": KH1LocationData("Deep Jungle", 265_1291),
"Deep Jungle Vines 2 Chest": KH1LocationData("Deep Jungle", 265_1292),
"Deep Jungle Climbing Trees Blue Trinity Chest": KH1LocationData("Deep Jungle", 265_1293),
"Deep Jungle Tunnel Chest": KH1LocationData("Deep Jungle", 265_1331),
"Deep Jungle Cavern of Hearts White Trinity Chest": KH1LocationData("Deep Jungle", 265_1332),
"Deep Jungle Camp Blue Trinity Chest": KH1LocationData("Deep Jungle", 265_1333),
"Deep Jungle Tent Chest": KH1LocationData("Deep Jungle", 265_1334),
"Deep Jungle Waterfall Cavern Low Chest": KH1LocationData("Deep Jungle", 265_1371),
"Deep Jungle Waterfall Cavern Middle Chest": KH1LocationData("Deep Jungle", 265_1372),
"Deep Jungle Waterfall Cavern High Wall Chest": KH1LocationData("Deep Jungle", 265_1373),
"Deep Jungle Waterfall Cavern High Middle Chest": KH1LocationData("Deep Jungle", 265_1374),
"Deep Jungle Cliff Right Cliff Left Chest": KH1LocationData("Deep Jungle", 265_1411),
"Deep Jungle Cliff Right Cliff Right Chest": KH1LocationData("Deep Jungle", 265_1412),
"Deep Jungle Tree House Suspended Boat Chest": KH1LocationData("Deep Jungle", 265_1413),
"100 Acre Wood Meadow Inside Log Chest": KH1LocationData("100 Acre Wood", 265_1654),
"100 Acre Wood Bouncing Spot Left Cliff Chest": KH1LocationData("100 Acre Wood", 265_1691),
"100 Acre Wood Bouncing Spot Right Tree Alcove Chest": KH1LocationData("100 Acre Wood", 265_1692),
"100 Acre Wood Bouncing Spot Under Giant Pot Chest": KH1LocationData("100 Acre Wood", 265_1693),
"Agrabah Plaza By Storage Chest": KH1LocationData("Agrabah", 265_1972),
"Agrabah Plaza Raised Terrace Chest": KH1LocationData("Agrabah", 265_1973),
"Agrabah Plaza Top Corner Chest": KH1LocationData("Agrabah", 265_1974),
"Agrabah Alley Chest": KH1LocationData("Agrabah", 265_2011),
"Agrabah Bazaar Across Windows Chest": KH1LocationData("Agrabah", 265_2012),
"Agrabah Bazaar High Corner Chest": KH1LocationData("Agrabah", 265_2013),
"Agrabah Main Street Right Palace Entrance Chest": KH1LocationData("Agrabah", 265_2014),
"Agrabah Main Street High Above Alley Entrance Chest": KH1LocationData("Agrabah", 265_2051),
"Agrabah Main Street High Above Palace Gates Entrance Chest": KH1LocationData("Agrabah", 265_2052),
"Agrabah Palace Gates Low Chest": KH1LocationData("Agrabah", 265_2053),
"Agrabah Palace Gates High Opposite Palace Chest": KH1LocationData("Agrabah", 265_2054),
"Agrabah Palace Gates High Close to Palace Chest": KH1LocationData("Agrabah", 265_2091),
"Agrabah Storage Green Trinity Chest": KH1LocationData("Agrabah", 265_2092),
"Agrabah Storage Behind Barrel Chest": KH1LocationData("Agrabah", 265_2093),
"Agrabah Cave of Wonders Entrance Left Chest": KH1LocationData("Agrabah", 265_2094),
"Agrabah Cave of Wonders Entrance Tall Tower Chest": KH1LocationData("Agrabah", 265_2131),
"Agrabah Cave of Wonders Hall High Left Chest": KH1LocationData("Agrabah", 265_2132),
"Agrabah Cave of Wonders Hall Near Bottomless Hall Chest": KH1LocationData("Agrabah", 265_2133),
"Agrabah Cave of Wonders Bottomless Hall Raised Platform Chest": KH1LocationData("Agrabah", 265_2134),
"Agrabah Cave of Wonders Bottomless Hall Pillar Chest": KH1LocationData("Agrabah", 265_2171),
"Agrabah Cave of Wonders Bottomless Hall Across Chasm Chest": KH1LocationData("Agrabah", 265_2172),
"Agrabah Cave of Wonders Treasure Room Across Platforms Chest": KH1LocationData("Agrabah", 265_2173),
"Agrabah Cave of Wonders Treasure Room Small Treasure Pile Chest": KH1LocationData("Agrabah", 265_2174),
"Agrabah Cave of Wonders Treasure Room Large Treasure Pile Chest": KH1LocationData("Agrabah", 265_2211),
"Agrabah Cave of Wonders Treasure Room Above Fire Chest": KH1LocationData("Agrabah", 265_2212),
"Agrabah Cave of Wonders Relic Chamber Jump from Stairs Chest": KH1LocationData("Agrabah", 265_2213),
"Agrabah Cave of Wonders Relic Chamber Stairs Chest": KH1LocationData("Agrabah", 265_2214),
"Agrabah Cave of Wonders Dark Chamber Abu Gem Chest": KH1LocationData("Agrabah", 265_2251),
"Agrabah Cave of Wonders Dark Chamber Across from Relic Chamber Entrance Chest": KH1LocationData("Agrabah", 265_2252),
"Agrabah Cave of Wonders Dark Chamber Bridge Chest": KH1LocationData("Agrabah", 265_2253),
"Agrabah Cave of Wonders Dark Chamber Near Save Chest": KH1LocationData("Agrabah", 265_2254),
"Agrabah Cave of Wonders Silent Chamber Blue Trinity Chest": KH1LocationData("Agrabah", 265_2291),
"Agrabah Cave of Wonders Hidden Room Right Chest": KH1LocationData("Agrabah", 265_2292),
"Agrabah Cave of Wonders Hidden Room Left Chest": KH1LocationData("Agrabah", 265_2293),
"Agrabah Aladdin's House Main Street Entrance Chest": KH1LocationData("Agrabah", 265_2294),
"Agrabah Aladdin's House Plaza Entrance Chest": KH1LocationData("Agrabah", 265_2331),
"Agrabah Cave of Wonders Entrance White Trinity Chest": KH1LocationData("Agrabah", 265_2332),
"Monstro Chamber 6 Other Platform Chest": KH1LocationData("Monstro", 265_2413),
"Monstro Chamber 6 Platform Near Chamber 5 Entrance Chest": KH1LocationData("Monstro", 265_2414),
"Monstro Chamber 6 Raised Area Near Chamber 1 Entrance Chest": KH1LocationData("Monstro", 265_2451),
"Monstro Chamber 6 Low Chest": KH1LocationData("Monstro", 265_2452),
"Atlantica Sunken Ship In Flipped Boat Chest": KH1LocationData("Atlantica", 265_2531),
"Atlantica Sunken Ship Seabed Chest": KH1LocationData("Atlantica", 265_2532),
"Atlantica Sunken Ship Inside Ship Chest": KH1LocationData("Atlantica", 265_2533),
"Atlantica Ariel's Grotto High Chest": KH1LocationData("Atlantica", 265_2534),
"Atlantica Ariel's Grotto Middle Chest": KH1LocationData("Atlantica", 265_2571),
"Atlantica Ariel's Grotto Low Chest": KH1LocationData("Atlantica", 265_2572),
"Atlantica Ursula's Lair Use Fire on Urchin Chest": KH1LocationData("Atlantica", 265_2573),
"Atlantica Undersea Gorge Jammed by Ariel's Grotto Chest": KH1LocationData("Atlantica", 265_2574),
"Atlantica Triton's Palace White Trinity Chest": KH1LocationData("Atlantica", 265_2611),
"Halloween Town Moonlight Hill White Trinity Chest": KH1LocationData("Halloween Town", 265_3014),
"Halloween Town Bridge Under Bridge": KH1LocationData("Halloween Town", 265_3051),
"Halloween Town Boneyard Tombstone Puzzle Chest": KH1LocationData("Halloween Town", 265_3052),
"Halloween Town Bridge Right of Gate Chest": KH1LocationData("Halloween Town", 265_3053),
"Halloween Town Cemetery Behind Grave Chest": KH1LocationData("Halloween Town", 265_3054),
"Halloween Town Cemetery By Cat Shape Chest": KH1LocationData("Halloween Town", 265_3091),
"Halloween Town Cemetery Between Graves Chest": KH1LocationData("Halloween Town", 265_3092),
"Halloween Town Oogie's Manor Lower Iron Cage Chest": KH1LocationData("Halloween Town", 265_3093),
"Halloween Town Oogie's Manor Upper Iron Cage Chest": KH1LocationData("Halloween Town", 265_3094),
"Halloween Town Oogie's Manor Hollow Chest": KH1LocationData("Halloween Town", 265_3131),
"Halloween Town Oogie's Manor Grounds Red Trinity Chest": KH1LocationData("Halloween Town", 265_3132),
"Halloween Town Guillotine Square High Tower Chest": KH1LocationData("Halloween Town", 265_3133),
"Halloween Town Guillotine Square Pumpkin Structure Left Chest": KH1LocationData("Halloween Town", 265_3134),
"Halloween Town Oogie's Manor Entrance Steps Chest": KH1LocationData("Halloween Town", 265_3171),
"Halloween Town Oogie's Manor Inside Entrance Chest": KH1LocationData("Halloween Town", 265_3172),
"Halloween Town Bridge Left of Gate Chest": KH1LocationData("Halloween Town", 265_3291),
"Halloween Town Cemetery By Striped Grave Chest": KH1LocationData("Halloween Town", 265_3292),
"Halloween Town Guillotine Square Under Jack's House Stairs Chest": KH1LocationData("Halloween Town", 265_3293),
"Halloween Town Guillotine Square Pumpkin Structure Right Chest": KH1LocationData("Halloween Town", 265_3294),
"Olympus Coliseum Coliseum Gates Left Behind Columns Chest": KH1LocationData("Olympus Coliseum", 265_3332),
"Olympus Coliseum Coliseum Gates Right Blue Trinity Chest": KH1LocationData("Olympus Coliseum", 265_3333),
"Olympus Coliseum Coliseum Gates Left Blue Trinity Chest": KH1LocationData("Olympus Coliseum", 265_3334),
"Olympus Coliseum Coliseum Gates White Trinity Chest": KH1LocationData("Olympus Coliseum", 265_3371),
"Olympus Coliseum Coliseum Gates Blizzara Chest": KH1LocationData("Olympus Coliseum", 265_3372),
"Olympus Coliseum Coliseum Gates Blizzaga Chest": KH1LocationData("Olympus Coliseum", 265_3373),
"Monstro Mouth Boat Deck Chest": KH1LocationData("Monstro", 265_3454),
"Monstro Mouth High Platform Boat Side Chest": KH1LocationData("Monstro", 265_3491),
"Monstro Mouth High Platform Across from Boat Chest": KH1LocationData("Monstro", 265_3492),
"Monstro Mouth Near Ship Chest": KH1LocationData("Monstro", 265_3493),
"Monstro Mouth Green Trinity Top of Boat Chest": KH1LocationData("Monstro", 265_3494),
"Monstro Chamber 2 Ground Chest": KH1LocationData("Monstro", 265_3534),
"Monstro Chamber 2 Platform Chest": KH1LocationData("Monstro", 265_3571),
"Monstro Chamber 5 Platform Chest": KH1LocationData("Monstro", 265_3613),
"Monstro Chamber 3 Ground Chest": KH1LocationData("Monstro", 265_3614),
"Monstro Chamber 3 Platform Above Chamber 2 Entrance Chest": KH1LocationData("Monstro", 265_3651),
"Monstro Chamber 3 Near Chamber 6 Entrance Chest": KH1LocationData("Monstro", 265_3652),
"Monstro Chamber 3 Platform Near Chamber 6 Entrance Chest": KH1LocationData("Monstro", 265_3653),
"Monstro Mouth High Platform Near Teeth Chest": KH1LocationData("Monstro", 265_3732),
"Monstro Chamber 5 Atop Barrel Chest": KH1LocationData("Monstro", 265_3733),
"Monstro Chamber 5 Low 2nd Chest": KH1LocationData("Monstro", 265_3734),
"Monstro Chamber 5 Low 1st Chest": KH1LocationData("Monstro", 265_3771),
"Neverland Pirate Ship Deck White Trinity Chest": KH1LocationData("Neverland", 265_3772),
"Neverland Pirate Ship Crows Nest Chest": KH1LocationData("Neverland", 265_3773),
"Neverland Hold Yellow Trinity Right Blue Chest": KH1LocationData("Neverland", 265_3774),
"Neverland Hold Yellow Trinity Left Blue Chest": KH1LocationData("Neverland", 265_3811),
"Neverland Galley Chest": KH1LocationData("Neverland", 265_3812),
"Neverland Cabin Chest": KH1LocationData("Neverland", 265_3813),
"Neverland Hold Flight 1st Chest": KH1LocationData("Neverland", 265_3814),
"Neverland Clock Tower Chest": KH1LocationData("Neverland", 265_4014),
"Neverland Hold Flight 2nd Chest": KH1LocationData("Neverland", 265_4051),
"Neverland Hold Yellow Trinity Green Chest": KH1LocationData("Neverland", 265_4052),
"Neverland Captain's Cabin Chest": KH1LocationData("Neverland", 265_4053),
"Hollow Bastion Rising Falls Water's Surface Chest": KH1LocationData("Hollow Bastion", 265_4054),
"Hollow Bastion Rising Falls Under Water 1st Chest": KH1LocationData("Hollow Bastion", 265_4091),
"Hollow Bastion Rising Falls Under Water 2nd Chest": KH1LocationData("Hollow Bastion", 265_4092),
"Hollow Bastion Rising Falls Floating Platform Near Save Chest": KH1LocationData("Hollow Bastion", 265_4093),
"Hollow Bastion Rising Falls Floating Platform Near Bubble Chest": KH1LocationData("Hollow Bastion", 265_4094),
"Hollow Bastion Rising Falls High Platform Chest": KH1LocationData("Hollow Bastion", 265_4131),
"Hollow Bastion Castle Gates Gravity Chest": KH1LocationData("Hollow Bastion", 265_4132),
"Hollow Bastion Castle Gates Freestanding Pillar Chest": KH1LocationData("Hollow Bastion", 265_4133),
"Hollow Bastion Castle Gates High Pillar Chest": KH1LocationData("Hollow Bastion", 265_4134),
"Hollow Bastion Great Crest Lower Chest": KH1LocationData("Hollow Bastion", 265_4171),
"Hollow Bastion Great Crest After Battle Platform Chest": KH1LocationData("Hollow Bastion", 265_4172),
"Hollow Bastion High Tower 2nd Gravity Chest": KH1LocationData("Hollow Bastion", 265_4173),
"Hollow Bastion High Tower 1st Gravity Chest": KH1LocationData("Hollow Bastion", 265_4174),
"Hollow Bastion High Tower Above Sliding Blocks Chest": KH1LocationData("Hollow Bastion", 265_4211),
"Hollow Bastion Library Top of Bookshelf Chest": KH1LocationData("Hollow Bastion", 265_4213),
"Hollow Bastion Library 1st Floor Turn the Carousel Chest": KH1LocationData("Hollow Bastion", 265_4214),
"Hollow Bastion Library Top of Bookshelf Turn the Carousel Chest": KH1LocationData("Hollow Bastion", 265_4251),
"Hollow Bastion Library 2nd Floor Turn the Carousel 1st Chest": KH1LocationData("Hollow Bastion", 265_4252),
"Hollow Bastion Library 2nd Floor Turn the Carousel 2nd Chest": KH1LocationData("Hollow Bastion", 265_4253),
"Hollow Bastion Lift Stop Library Node After High Tower Switch Gravity Chest": KH1LocationData("Hollow Bastion", 265_4254),
"Hollow Bastion Lift Stop Library Node Gravity Chest": KH1LocationData("Hollow Bastion", 265_4291),
"Hollow Bastion Lift Stop Under High Tower Sliding Blocks Chest": KH1LocationData("Hollow Bastion", 265_4292),
"Hollow Bastion Lift Stop Outside Library Gravity Chest": KH1LocationData("Hollow Bastion", 265_4293),
"Hollow Bastion Lift Stop Heartless Sigil Door Gravity Chest": KH1LocationData("Hollow Bastion", 265_4294),
"Hollow Bastion Base Level Bubble Under the Wall Platform Chest": KH1LocationData("Hollow Bastion", 265_4331),
"Hollow Bastion Base Level Platform Near Entrance Chest": KH1LocationData("Hollow Bastion", 265_4332),
"Hollow Bastion Base Level Near Crystal Switch Chest": KH1LocationData("Hollow Bastion", 265_4333),
"Hollow Bastion Waterway Near Save Chest": KH1LocationData("Hollow Bastion", 265_4334),
"Hollow Bastion Waterway Blizzard on Bubble Chest": KH1LocationData("Hollow Bastion", 265_4371),
"Hollow Bastion Waterway Unlock Passage from Base Level Chest": KH1LocationData("Hollow Bastion", 265_4372),
"Hollow Bastion Dungeon By Candles Chest": KH1LocationData("Hollow Bastion", 265_4373),
"Hollow Bastion Dungeon Corner Chest": KH1LocationData("Hollow Bastion", 265_4374),
"Hollow Bastion Grand Hall Steps Right Side Chest": KH1LocationData("Hollow Bastion", 265_4454),
"Hollow Bastion Grand Hall Oblivion Chest": KH1LocationData("Hollow Bastion", 265_4491),
"Hollow Bastion Grand Hall Left of Gate Chest": KH1LocationData("Hollow Bastion", 265_4492),
#"Hollow Bastion Entrance Hall Push the Statue Chest": KH1LocationData("Hollow Bastion", 265_4493), --handled later
"Hollow Bastion Entrance Hall Left of Emblem Door Chest": KH1LocationData("Hollow Bastion", 265_4212),
"Hollow Bastion Rising Falls White Trinity Chest": KH1LocationData("Hollow Bastion", 265_4494),
"End of the World Final Dimension 1st Chest": KH1LocationData("End of the World", 265_4531),
"End of the World Final Dimension 2nd Chest": KH1LocationData("End of the World", 265_4532),
"End of the World Final Dimension 3rd Chest": KH1LocationData("End of the World", 265_4533),
"End of the World Final Dimension 4th Chest": KH1LocationData("End of the World", 265_4534),
"End of the World Final Dimension 5th Chest": KH1LocationData("End of the World", 265_4571),
"End of the World Final Dimension 6th Chest": KH1LocationData("End of the World", 265_4572),
"End of the World Final Dimension 10th Chest": KH1LocationData("End of the World", 265_4573),
"End of the World Final Dimension 9th Chest": KH1LocationData("End of the World", 265_4574),
"End of the World Final Dimension 8th Chest": KH1LocationData("End of the World", 265_4611),
"End of the World Final Dimension 7th Chest": KH1LocationData("End of the World", 265_4612),
"End of the World Giant Crevasse 3rd Chest": KH1LocationData("End of the World", 265_4613),
"End of the World Giant Crevasse 5th Chest": KH1LocationData("End of the World", 265_4614),
"End of the World Giant Crevasse 1st Chest": KH1LocationData("End of the World", 265_4651),
"End of the World Giant Crevasse 4th Chest": KH1LocationData("End of the World", 265_4652),
"End of the World Giant Crevasse 2nd Chest": KH1LocationData("End of the World", 265_4653),
"End of the World World Terminus Traverse Town Chest": KH1LocationData("End of the World", 265_4654),
"End of the World World Terminus Wonderland Chest": KH1LocationData("End of the World", 265_4691),
"End of the World World Terminus Olympus Coliseum Chest": KH1LocationData("End of the World", 265_4692),
"End of the World World Terminus Deep Jungle Chest": KH1LocationData("End of the World", 265_4693),
"End of the World World Terminus Agrabah Chest": KH1LocationData("End of the World", 265_4694),
"End of the World World Terminus Atlantica Chest": KH1LocationData("End of the World", 265_4731),
"End of the World World Terminus Halloween Town Chest": KH1LocationData("End of the World", 265_4732),
"End of the World World Terminus Neverland Chest": KH1LocationData("End of the World", 265_4733),
"End of the World World Terminus 100 Acre Wood Chest": KH1LocationData("End of the World", 265_4734),
#"End of the World World Terminus Hollow Bastion Chest": KH1LocationData("End of the World", 265_4771),
"End of the World Final Rest Chest": KH1LocationData("End of the World", 265_4772),
"Monstro Chamber 6 White Trinity Chest": KH1LocationData("End of the World", 265_5092),
#"Awakening Chest": KH1LocationData("Awakening", 265_5093), missable
"Traverse Town Defeat Guard Armor Dodge Roll Event": KH1LocationData("Traverse Town", 265_6011),
"Traverse Town Defeat Guard Armor Fire Event": KH1LocationData("Traverse Town", 265_6012),
"Traverse Town Defeat Guard Armor Blue Trinity Event": KH1LocationData("Traverse Town", 265_6013),
"Traverse Town Leon Secret Waterway Earthshine Event": KH1LocationData("Traverse Town", 265_6014),
"Traverse Town Kairi Secret Waterway Oathkeeper Event": KH1LocationData("Traverse Town", 265_6015),
"Traverse Town Defeat Guard Armor Brave Warrior Event": KH1LocationData("Traverse Town", 265_6016),
"Deep Jungle Defeat Sabor White Fang Event": KH1LocationData("Deep Jungle", 265_6021),
"Deep Jungle Defeat Clayton Cure Event": KH1LocationData("Deep Jungle", 265_6022),
"Deep Jungle Seal Keyhole Jungle King Event": KH1LocationData("Deep Jungle", 265_6023),
"Deep Jungle Seal Keyhole Red Trinity Event": KH1LocationData("Deep Jungle", 265_6024),
"Olympus Coliseum Clear Phil's Training Thunder Event": KH1LocationData("Olympus Coliseum", 265_6031),
"Olympus Coliseum Defeat Cerberus Inferno Band Event": KH1LocationData("Olympus Coliseum", 265_6033),
"Wonderland Defeat Trickmaster Blizzard Event": KH1LocationData("Wonderland", 265_6041),
"Wonderland Defeat Trickmaster Ifrit's Horn Event": KH1LocationData("Wonderland", 265_6042),
"Agrabah Defeat Pot Centipede Ray of Light Event": KH1LocationData("Agrabah", 265_6051),
"Agrabah Defeat Jafar Blizzard Event": KH1LocationData("Agrabah", 265_6052),
"Agrabah Defeat Jafar Genie Fire Event": KH1LocationData("Agrabah", 265_6053),
"Agrabah Seal Keyhole Genie Event": KH1LocationData("Agrabah", 265_6054),
"Agrabah Seal Keyhole Three Wishes Event": KH1LocationData("Agrabah", 265_6055),
"Agrabah Seal Keyhole Green Trinity Event": KH1LocationData("Agrabah", 265_6056),
"Monstro Defeat Parasite Cage I Goofy Cheer Event": KH1LocationData("Monstro", 265_6061),
"Monstro Defeat Parasite Cage II Stop Event": KH1LocationData("Monstro", 265_6062),
"Atlantica Defeat Ursula I Mermaid Kick Event": KH1LocationData("Atlantica", 265_6071),
"Atlantica Defeat Ursula II Thunder Event": KH1LocationData("Atlantica", 265_6072),
"Atlantica Seal Keyhole Crabclaw Event": KH1LocationData("Atlantica", 265_6073),
"Halloween Town Defeat Oogie Boogie Holy Circlet Event": KH1LocationData("Halloween Town", 265_6081),
"Halloween Town Defeat Oogie's Manor Gravity Event": KH1LocationData("Halloween Town", 265_6082),
"Halloween Town Seal Keyhole Pumpkinhead Event": KH1LocationData("Halloween Town", 265_6083),
"Neverland Defeat Anti Sora Raven's Claw Event": KH1LocationData("Neverland", 265_6091),
"Neverland Encounter Hook Cure Event": KH1LocationData("Neverland", 265_6092),
"Neverland Seal Keyhole Fairy Harp Event": KH1LocationData("Neverland", 265_6093),
"Neverland Seal Keyhole Tinker Bell Event": KH1LocationData("Neverland", 265_6094),
"Neverland Seal Keyhole Glide Event": KH1LocationData("Neverland", 265_6095),
"Neverland Defeat Phantom Stop Event": KH1LocationData("Neverland", 265_6096),
"Neverland Defeat Captain Hook Ars Arcanum Event": KH1LocationData("Neverland", 265_6097),
"Hollow Bastion Defeat Riku I White Trinity Event": KH1LocationData("Hollow Bastion", 265_6101),
"Hollow Bastion Defeat Maleficent Donald Cheer Event": KH1LocationData("Hollow Bastion", 265_6102),
"Hollow Bastion Defeat Dragon Maleficent Fireglow Event": KH1LocationData("Hollow Bastion", 265_6103),
"Hollow Bastion Defeat Riku II Ragnarok Event": KH1LocationData("Hollow Bastion", 265_6104),
"Hollow Bastion Defeat Behemoth Omega Arts Event": KH1LocationData("Hollow Bastion", 265_6105),
"Hollow Bastion Speak to Princesses Fire Event": KH1LocationData("Hollow Bastion", 265_6106),
"End of the World Defeat Chernabog Superglide Event": KH1LocationData("End of the World", 265_6111),
"Traverse Town Mail Postcard 01 Event": KH1LocationData("Traverse Town", 265_6120),
"Traverse Town Mail Postcard 02 Event": KH1LocationData("Traverse Town", 265_6121),
"Traverse Town Mail Postcard 03 Event": KH1LocationData("Traverse Town", 265_6122),
"Traverse Town Mail Postcard 04 Event": KH1LocationData("Traverse Town", 265_6123),
"Traverse Town Mail Postcard 05 Event": KH1LocationData("Traverse Town", 265_6124),
"Traverse Town Mail Postcard 06 Event": KH1LocationData("Traverse Town", 265_6125),
"Traverse Town Mail Postcard 07 Event": KH1LocationData("Traverse Town", 265_6126),
"Traverse Town Mail Postcard 08 Event": KH1LocationData("Traverse Town", 265_6127),
"Traverse Town Mail Postcard 09 Event": KH1LocationData("Traverse Town", 265_6128),
"Traverse Town Mail Postcard 10 Event": KH1LocationData("Traverse Town", 265_6129),
"Traverse Town Defeat Opposite Armor Aero Event": KH1LocationData("Traverse Town", 265_6131),
"Atlantica Undersea Gorge Blizzard Clam": KH1LocationData("Atlantica", 265_6201),
"Atlantica Undersea Gorge Ocean Floor Clam": KH1LocationData("Atlantica", 265_6202),
"Atlantica Undersea Valley Higher Cave Clam": KH1LocationData("Atlantica", 265_6203),
"Atlantica Undersea Valley Lower Cave Clam": KH1LocationData("Atlantica", 265_6204),
"Atlantica Undersea Valley Fire Clam": KH1LocationData("Atlantica", 265_6205),
"Atlantica Undersea Valley Wall Clam": KH1LocationData("Atlantica", 265_6206),
"Atlantica Undersea Valley Pillar Clam": KH1LocationData("Atlantica", 265_6207),
"Atlantica Undersea Valley Ocean Floor Clam": KH1LocationData("Atlantica", 265_6208),
"Atlantica Triton's Palace Thunder Clam": KH1LocationData("Atlantica", 265_6209),
"Atlantica Triton's Palace Wall Right Clam": KH1LocationData("Atlantica", 265_6210),
"Atlantica Triton's Palace Near Path Clam": KH1LocationData("Atlantica", 265_6211),
"Atlantica Triton's Palace Wall Left Clam": KH1LocationData("Atlantica", 265_6212),
"Atlantica Cavern Nook Clam": KH1LocationData("Atlantica", 265_6213),
"Atlantica Below Deck Clam": KH1LocationData("Atlantica", 265_6214),
"Atlantica Undersea Garden Clam": KH1LocationData("Atlantica", 265_6215),
"Atlantica Undersea Cave Clam": KH1LocationData("Atlantica", 265_6216),
#"Traverse Town Magician's Study Turn in Naturespark": KH1LocationData("Traverse Town", 265_6300),
#"Traverse Town Magician's Study Turn in Watergleam": KH1LocationData("Traverse Town", 265_6301),
#"Traverse Town Magician's Study Turn in Fireglow": KH1LocationData("Traverse Town", 265_6302),
#"Traverse Town Magician's Study Turn in all Summon Gems": KH1LocationData("Traverse Town", 265_6303),
"Traverse Town Geppetto's House Geppetto Reward 1": KH1LocationData("Traverse Town", 265_6304),
"Traverse Town Geppetto's House Geppetto Reward 2": KH1LocationData("Traverse Town", 265_6305),
"Traverse Town Geppetto's House Geppetto Reward 3": KH1LocationData("Traverse Town", 265_6306),
"Traverse Town Geppetto's House Geppetto Reward 4": KH1LocationData("Traverse Town", 265_6307),
"Traverse Town Geppetto's House Geppetto Reward 5": KH1LocationData("Traverse Town", 265_6308),
"Traverse Town Geppetto's House Geppetto All Summons Reward": KH1LocationData("Traverse Town", 265_6309),
"Traverse Town Geppetto's House Talk to Pinocchio": KH1LocationData("Traverse Town", 265_6310),
"Traverse Town Magician's Study Obtained All Arts Items": KH1LocationData("Traverse Town", 265_6311),
"Traverse Town Magician's Study Obtained All LV1 Magic": KH1LocationData("Traverse Town", 265_6312),
"Traverse Town Magician's Study Obtained All LV3 Magic": KH1LocationData("Traverse Town", 265_6313),
"Traverse Town Piano Room Return 10 Puppies": KH1LocationData("Traverse Town", 265_6314),
"Traverse Town Piano Room Return 20 Puppies": KH1LocationData("Traverse Town", 265_6315),
"Traverse Town Piano Room Return 30 Puppies": KH1LocationData("Traverse Town", 265_6316),
"Traverse Town Piano Room Return 40 Puppies": KH1LocationData("Traverse Town", 265_6317),
"Traverse Town Piano Room Return 50 Puppies Reward 1": KH1LocationData("Traverse Town", 265_6318),
"Traverse Town Piano Room Return 50 Puppies Reward 2": KH1LocationData("Traverse Town", 265_6319),
"Traverse Town Piano Room Return 60 Puppies": KH1LocationData("Traverse Town", 265_6320),
"Traverse Town Piano Room Return 70 Puppies": KH1LocationData("Traverse Town", 265_6321),
"Traverse Town Piano Room Return 80 Puppies": KH1LocationData("Traverse Town", 265_6322),
"Traverse Town Piano Room Return 90 Puppies": KH1LocationData("Traverse Town", 265_6324),
"Traverse Town Piano Room Return 99 Puppies Reward 1": KH1LocationData("Traverse Town", 265_6326),
"Traverse Town Piano Room Return 99 Puppies Reward 2": KH1LocationData("Traverse Town", 265_6327),
"Olympus Coliseum Cloud Sonic Blade Event": KH1LocationData("Olympus Coliseum", 265_6032), #Had to change the way we send this check, not changing location_id
"Olympus Coliseum Defeat Sephiroth One-Winged Angel Event": KH1LocationData("Olympus Coliseum", 265_6328),
"Olympus Coliseum Defeat Ice Titan Diamond Dust Event": KH1LocationData("Olympus Coliseum", 265_6329),
"Olympus Coliseum Gates Purple Jar After Defeating Hades": KH1LocationData("Olympus Coliseum", 265_6330),
"Halloween Town Guillotine Square Ring Jack's Doorbell 3 Times": KH1LocationData("Halloween Town", 265_6331),
#"Neverland Clock Tower 01:00 Door": KH1LocationData("Neverland", 265_6332),
#"Neverland Clock Tower 02:00 Door": KH1LocationData("Neverland", 265_6333),
#"Neverland Clock Tower 03:00 Door": KH1LocationData("Neverland", 265_6334),
#"Neverland Clock Tower 04:00 Door": KH1LocationData("Neverland", 265_6335),
#"Neverland Clock Tower 05:00 Door": KH1LocationData("Neverland", 265_6336),
#"Neverland Clock Tower 06:00 Door": KH1LocationData("Neverland", 265_6337),
#"Neverland Clock Tower 07:00 Door": KH1LocationData("Neverland", 265_6338),
#"Neverland Clock Tower 08:00 Door": KH1LocationData("Neverland", 265_6339),
#"Neverland Clock Tower 09:00 Door": KH1LocationData("Neverland", 265_6340),
#"Neverland Clock Tower 10:00 Door": KH1LocationData("Neverland", 265_6341),
#"Neverland Clock Tower 11:00 Door": KH1LocationData("Neverland", 265_6342),
#"Neverland Clock Tower 12:00 Door": KH1LocationData("Neverland", 265_6343),
"Neverland Hold Aero Chest": KH1LocationData("Neverland", 265_6344),
"100 Acre Wood Bouncing Spot Turn in Rare Nut 1": KH1LocationData("100 Acre Wood", 265_6345),
"100 Acre Wood Bouncing Spot Turn in Rare Nut 2": KH1LocationData("100 Acre Wood", 265_6346),
"100 Acre Wood Bouncing Spot Turn in Rare Nut 3": KH1LocationData("100 Acre Wood", 265_6347),
"100 Acre Wood Bouncing Spot Turn in Rare Nut 4": KH1LocationData("100 Acre Wood", 265_6348),
"100 Acre Wood Bouncing Spot Turn in Rare Nut 5": KH1LocationData("100 Acre Wood", 265_6349),
"100 Acre Wood Pooh's House Owl Cheer": KH1LocationData("100 Acre Wood", 265_6350),
"100 Acre Wood Convert Torn Page 1": KH1LocationData("100 Acre Wood", 265_6351),
"100 Acre Wood Convert Torn Page 2": KH1LocationData("100 Acre Wood", 265_6352),
"100 Acre Wood Convert Torn Page 3": KH1LocationData("100 Acre Wood", 265_6353),
"100 Acre Wood Convert Torn Page 4": KH1LocationData("100 Acre Wood", 265_6354),
"100 Acre Wood Convert Torn Page 5": KH1LocationData("100 Acre Wood", 265_6355),
"100 Acre Wood Pooh's House Start Fire": KH1LocationData("100 Acre Wood", 265_6356),
"100 Acre Wood Pooh's Room Cabinet": KH1LocationData("100 Acre Wood", 265_6357),
"100 Acre Wood Pooh's Room Chimney": KH1LocationData("100 Acre Wood", 265_6358),
"100 Acre Wood Bouncing Spot Break Log": KH1LocationData("100 Acre Wood", 265_6359),
"100 Acre Wood Bouncing Spot Fall Through Top of Tree Next to Pooh": KH1LocationData("100 Acre Wood", 265_6360),
"Deep Jungle Camp Hi-Potion Experiment": KH1LocationData("Deep Jungle", 265_6361),
"Deep Jungle Camp Ether Experiment": KH1LocationData("Deep Jungle", 265_6362),
"Deep Jungle Camp Replication Experiment": KH1LocationData("Deep Jungle", 265_6363),
"Deep Jungle Cliff Save Gorillas": KH1LocationData("Deep Jungle", 265_6364),
"Deep Jungle Tree House Save Gorillas": KH1LocationData("Deep Jungle", 265_6365),
"Deep Jungle Camp Save Gorillas": KH1LocationData("Deep Jungle", 265_6366),
"Deep Jungle Bamboo Thicket Save Gorillas": KH1LocationData("Deep Jungle", 265_6367),
"Deep Jungle Climbing Trees Save Gorillas": KH1LocationData("Deep Jungle", 265_6368),
"Olympus Coliseum Olympia Chest": KH1LocationData("Olympus Coliseum", 265_6369),
"Deep Jungle Jungle Slider 10 Fruits": KH1LocationData("Deep Jungle", 265_6370),
"Deep Jungle Jungle Slider 20 Fruits": KH1LocationData("Deep Jungle", 265_6371),
"Deep Jungle Jungle Slider 30 Fruits": KH1LocationData("Deep Jungle", 265_6372),
"Deep Jungle Jungle Slider 40 Fruits": KH1LocationData("Deep Jungle", 265_6373),
"Deep Jungle Jungle Slider 50 Fruits": KH1LocationData("Deep Jungle", 265_6374),
"Traverse Town 1st District Speak with Cid Event": KH1LocationData("Traverse Town", 265_6375),
"Wonderland Bizarre Room Read Book": KH1LocationData("Wonderland", 265_6376),
"Olympus Coliseum Coliseum Gates Green Trinity": KH1LocationData("Olympus Coliseum", 265_6377),
"Agrabah Defeat Kurt Zisa Zantetsuken Event": KH1LocationData("Agrabah", 265_6378),
"Hollow Bastion Defeat Unknown EXP Necklace Event": KH1LocationData("Hollow Bastion", 265_6379),
"Olympus Coliseum Coliseum Gates Hero's License Event": KH1LocationData("Olympus Coliseum", 265_6380),
"Atlantica Sunken Ship Crystal Trident Event": KH1LocationData("Atlantica", 265_6381),
"Halloween Town Graveyard Forget-Me-Not Event": KH1LocationData("Halloween Town", 265_6382),
"Deep Jungle Tent Protect-G Event": KH1LocationData("Deep Jungle", 265_6383),
"Deep Jungle Cavern of Hearts Navi-G Piece Event": KH1LocationData("Deep Jungle", 265_6384),
"Wonderland Bizarre Room Navi-G Piece Event": KH1LocationData("Wonderland", 265_6385),
"Olympus Coliseum Coliseum Gates Entry Pass Event": KH1LocationData("Olympus Coliseum", 265_6386),
"Traverse Town Synth Log": KH1LocationData("Traverse Town", 265_6401),
"Traverse Town Synth Cloth": KH1LocationData("Traverse Town", 265_6402),
"Traverse Town Synth Rope": KH1LocationData("Traverse Town", 265_6403),
"Traverse Town Synth Seagull Egg": KH1LocationData("Traverse Town", 265_6404),
"Traverse Town Synth Fish": KH1LocationData("Traverse Town", 265_6405),
"Traverse Town Synth Mushroom": KH1LocationData("Traverse Town", 265_6406),
"Traverse Town Item Shop Postcard": KH1LocationData("Traverse Town", 265_6500),
"Traverse Town 1st District Safe Postcard": KH1LocationData("Traverse Town", 265_6501),
"Traverse Town Gizmo Shop Postcard 1": KH1LocationData("Traverse Town", 265_6502),
"Traverse Town Gizmo Shop Postcard 2": KH1LocationData("Traverse Town", 265_6503),
"Traverse Town Item Workshop Postcard": KH1LocationData("Traverse Town", 265_6504),
"Traverse Town 3rd District Balcony Postcard": KH1LocationData("Traverse Town", 265_6505),
"Traverse Town Geppetto's House Postcard": KH1LocationData("Traverse Town", 265_6506),
"Halloween Town Lab Torn Page": KH1LocationData("Halloween Town", 265_6508),
"Hollow Bastion Entrance Hall Emblem Piece (Flame)": KH1LocationData("Hollow Bastion", 265_6516),
"Hollow Bastion Entrance Hall Emblem Piece (Chest)": KH1LocationData("Hollow Bastion", 265_6517),
"Hollow Bastion Entrance Hall Emblem Piece (Statue)": KH1LocationData("Hollow Bastion", 265_6518),
"Hollow Bastion Entrance Hall Emblem Piece (Fountain)": KH1LocationData("Hollow Bastion", 265_6519),
#"Traverse Town 1st District Leon Gift": KH1LocationData("Traverse Town", 265_6520),
#"Traverse Town 1st District Aerith Gift": KH1LocationData("Traverse Town", 265_6521),
"Hollow Bastion Library Speak to Belle Divine Rose": KH1LocationData("Hollow Bastion", 265_6522),
"Hollow Bastion Library Speak to Aerith Cure": KH1LocationData("Hollow Bastion", 265_6523),
"Agrabah Defeat Jafar Genie Ansem's Report 1": KH1LocationData("Agrabah", 265_7018),
"Hollow Bastion Speak with Aerith Ansem's Report 2": KH1LocationData("Hollow Bastion", 265_7017),
"Atlantica Defeat Ursula II Ansem's Report 3": KH1LocationData("Atlantica", 265_7016),
"Hollow Bastion Speak with Aerith Ansem's Report 4": KH1LocationData("Hollow Bastion", 265_7015),
"Hollow Bastion Defeat Maleficent Ansem's Report 5": KH1LocationData("Hollow Bastion", 265_7014),
"Hollow Bastion Speak with Aerith Ansem's Report 6": KH1LocationData("Hollow Bastion", 265_7013),
"Halloween Town Defeat Oogie Boogie Ansem's Report 7": KH1LocationData("Halloween Town", 265_7012),
"Olympus Coliseum Defeat Hades Ansem's Report 8": KH1LocationData("Olympus Coliseum", 265_7011),
"Neverland Defeat Hook Ansem's Report 9": KH1LocationData("Neverland", 265_7028),
"Hollow Bastion Speak with Aerith Ansem's Report 10": KH1LocationData("Hollow Bastion", 265_7027),
"Agrabah Defeat Kurt Zisa Ansem's Report 11": KH1LocationData("Agrabah", 265_7026),
"Olympus Coliseum Defeat Sephiroth Ansem's Report 12": KH1LocationData("Olympus Coliseum", 265_7025),
"Hollow Bastion Defeat Unknown Ansem's Report 13": KH1LocationData("Hollow Bastion", 265_7024),
"Level 001": KH1LocationData("Levels", 265_8001),
"Level 002": KH1LocationData("Levels", 265_8002),
"Level 003": KH1LocationData("Levels", 265_8003),
"Level 004": KH1LocationData("Levels", 265_8004),
"Level 005": KH1LocationData("Levels", 265_8005),
"Level 006": KH1LocationData("Levels", 265_8006),
"Level 007": KH1LocationData("Levels", 265_8007),
"Level 008": KH1LocationData("Levels", 265_8008),
"Level 009": KH1LocationData("Levels", 265_8009),
"Level 010": KH1LocationData("Levels", 265_8010),
"Level 011": KH1LocationData("Levels", 265_8011),
"Level 012": KH1LocationData("Levels", 265_8012),
"Level 013": KH1LocationData("Levels", 265_8013),
"Level 014": KH1LocationData("Levels", 265_8014),
"Level 015": KH1LocationData("Levels", 265_8015),
"Level 016": KH1LocationData("Levels", 265_8016),
"Level 017": KH1LocationData("Levels", 265_8017),
"Level 018": KH1LocationData("Levels", 265_8018),
"Level 019": KH1LocationData("Levels", 265_8019),
"Level 020": KH1LocationData("Levels", 265_8020),
"Level 021": KH1LocationData("Levels", 265_8021),
"Level 022": KH1LocationData("Levels", 265_8022),
"Level 023": KH1LocationData("Levels", 265_8023),
"Level 024": KH1LocationData("Levels", 265_8024),
"Level 025": KH1LocationData("Levels", 265_8025),
"Level 026": KH1LocationData("Levels", 265_8026),
"Level 027": KH1LocationData("Levels", 265_8027),
"Level 028": KH1LocationData("Levels", 265_8028),
"Level 029": KH1LocationData("Levels", 265_8029),
"Level 030": KH1LocationData("Levels", 265_8030),
"Level 031": KH1LocationData("Levels", 265_8031),
"Level 032": KH1LocationData("Levels", 265_8032),
"Level 033": KH1LocationData("Levels", 265_8033),
"Level 034": KH1LocationData("Levels", 265_8034),
"Level 035": KH1LocationData("Levels", 265_8035),
"Level 036": KH1LocationData("Levels", 265_8036),
"Level 037": KH1LocationData("Levels", 265_8037),
"Level 038": KH1LocationData("Levels", 265_8038),
"Level 039": KH1LocationData("Levels", 265_8039),
"Level 040": KH1LocationData("Levels", 265_8040),
"Level 041": KH1LocationData("Levels", 265_8041),
"Level 042": KH1LocationData("Levels", 265_8042),
"Level 043": KH1LocationData("Levels", 265_8043),
"Level 044": KH1LocationData("Levels", 265_8044),
"Level 045": KH1LocationData("Levels", 265_8045),
"Level 046": KH1LocationData("Levels", 265_8046),
"Level 047": KH1LocationData("Levels", 265_8047),
"Level 048": KH1LocationData("Levels", 265_8048),
"Level 049": KH1LocationData("Levels", 265_8049),
"Level 050": KH1LocationData("Levels", 265_8050),
"Level 051": KH1LocationData("Levels", 265_8051),
"Level 052": KH1LocationData("Levels", 265_8052),
"Level 053": KH1LocationData("Levels", 265_8053),
"Level 054": KH1LocationData("Levels", 265_8054),
"Level 055": KH1LocationData("Levels", 265_8055),
"Level 056": KH1LocationData("Levels", 265_8056),
"Level 057": KH1LocationData("Levels", 265_8057),
"Level 058": KH1LocationData("Levels", 265_8058),
"Level 059": KH1LocationData("Levels", 265_8059),
"Level 060": KH1LocationData("Levels", 265_8060),
"Level 061": KH1LocationData("Levels", 265_8061),
"Level 062": KH1LocationData("Levels", 265_8062),
"Level 063": KH1LocationData("Levels", 265_8063),
"Level 064": KH1LocationData("Levels", 265_8064),
"Level 065": KH1LocationData("Levels", 265_8065),
"Level 066": KH1LocationData("Levels", 265_8066),
"Level 067": KH1LocationData("Levels", 265_8067),
"Level 068": KH1LocationData("Levels", 265_8068),
"Level 069": KH1LocationData("Levels", 265_8069),
"Level 070": KH1LocationData("Levels", 265_8070),
"Level 071": KH1LocationData("Levels", 265_8071),
"Level 072": KH1LocationData("Levels", 265_8072),
"Level 073": KH1LocationData("Levels", 265_8073),
"Level 074": KH1LocationData("Levels", 265_8074),
"Level 075": KH1LocationData("Levels", 265_8075),
"Level 076": KH1LocationData("Levels", 265_8076),
"Level 077": KH1LocationData("Levels", 265_8077),
"Level 078": KH1LocationData("Levels", 265_8078),
"Level 079": KH1LocationData("Levels", 265_8079),
"Level 080": KH1LocationData("Levels", 265_8080),
"Level 081": KH1LocationData("Levels", 265_8081),
"Level 082": KH1LocationData("Levels", 265_8082),
"Level 083": KH1LocationData("Levels", 265_8083),
"Level 084": KH1LocationData("Levels", 265_8084),
"Level 085": KH1LocationData("Levels", 265_8085),
"Level 086": KH1LocationData("Levels", 265_8086),
"Level 087": KH1LocationData("Levels", 265_8087),
"Level 088": KH1LocationData("Levels", 265_8088),
"Level 089": KH1LocationData("Levels", 265_8089),
"Level 090": KH1LocationData("Levels", 265_8090),
"Level 091": KH1LocationData("Levels", 265_8091),
"Level 092": KH1LocationData("Levels", 265_8092),
"Level 093": KH1LocationData("Levels", 265_8093),
"Level 094": KH1LocationData("Levels", 265_8094),
"Level 095": KH1LocationData("Levels", 265_8095),
"Level 096": KH1LocationData("Levels", 265_8096),
"Level 097": KH1LocationData("Levels", 265_8097),
"Level 098": KH1LocationData("Levels", 265_8098),
"Level 099": KH1LocationData("Levels", 265_8099),
"Level 100": KH1LocationData("Levels", 265_8100),
"Complete Phil Cup": KH1LocationData("Olympus Coliseum", 265_9001),
"Complete Phil Cup Solo": KH1LocationData("Olympus Coliseum", 265_9002),
"Complete Phil Cup Time Trial": KH1LocationData("Olympus Coliseum", 265_9003),
"Complete Pegasus Cup": KH1LocationData("Olympus Coliseum", 265_9004),
"Complete Pegasus Cup Solo": KH1LocationData("Olympus Coliseum", 265_9005),
"Complete Pegasus Cup Time Trial": KH1LocationData("Olympus Coliseum", 265_9006),
"Complete Hercules Cup": KH1LocationData("Olympus Coliseum", 265_9007),
"Complete Hercules Cup Solo": KH1LocationData("Olympus Coliseum", 265_9008),
"Complete Hercules Cup Time Trial": KH1LocationData("Olympus Coliseum", 265_9009),
"Complete Hades Cup": KH1LocationData("Olympus Coliseum", 265_9010),
"Complete Hades Cup Solo": KH1LocationData("Olympus Coliseum", 265_9011),
"Complete Hades Cup Time Trial": KH1LocationData("Olympus Coliseum", 265_9012),
"Hades Cup Defeat Cloud and Leon Event": KH1LocationData("Olympus Coliseum", 265_9013),
"Hades Cup Defeat Yuffie Event": KH1LocationData("Olympus Coliseum", 265_9014),
"Hades Cup Defeat Cerberus Event": KH1LocationData("Olympus Coliseum", 265_9015),
"Hades Cup Defeat Behemoth Event": KH1LocationData("Olympus Coliseum", 265_9016),
"Hades Cup Defeat Hades Event": KH1LocationData("Olympus Coliseum", 265_9017),
"Hercules Cup Defeat Cloud Event": KH1LocationData("Olympus Coliseum", 265_9018),
"Hercules Cup Yellow Trinity Event": KH1LocationData("Olympus Coliseum", 265_9019),
"Final Ansem": KH1LocationData("Final", 265_9999)
}
event_location_table: Dict[str, KH1LocationData] = {}
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in location_table.items() if data.code}
#Make location categories
location_name_groups: Dict[str, Set[str]] = {}
for location in location_table.keys():
category = location_table[location].category
if category not in location_name_groups.keys():
location_name_groups[category] = set()
location_name_groups[category].add(location)

445
worlds/kh1/Options.py Normal file
View File

@@ -0,0 +1,445 @@
from dataclasses import dataclass
from Options import NamedRange, Choice, Range, Toggle, DefaultOnToggle, PerGameCommonOptions, StartInventoryPool, OptionGroup
class StrengthIncrease(Range):
"""
Determines the number of Strength Increases to add to the multiworld.
The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld.
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random.
"""
display_name = "STR Increases"
range_start = 0
range_end = 100
default = 24
class DefenseIncrease(Range):
"""
Determines the number of Defense Increases to add to the multiworld.
The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld.
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random.
"""
display_name = "DEF Increases"
range_start = 0
range_end = 100
default = 24
class HPIncrease(Range):
"""
Determines the number of HP Increases to add to the multiworld.
The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld.
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random.
"""
display_name = "HP Increases"
range_start = 0
range_end = 100
default = 23
class APIncrease(Range):
"""
Determines the number of AP Increases to add to the multiworld.
The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld.
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random.
"""
display_name = "AP Increases"
range_start = 0
range_end = 100
default = 18
class MPIncrease(Range):
"""
Determines the number of MP Increases to add to the multiworld.
The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld.
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random.
"""
display_name = "MP Increases"
range_start = 0
range_end = 20
default = 7
class AccessorySlotIncrease(Range):
"""
Determines the number of Accessory Slot Increases to add to the multiworld.
The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld.
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random.
"""
display_name = "Accessory Slot Increases"
range_start = 0
range_end = 6
default = 1
class ItemSlotIncrease(Range):
"""
Determines the number of Item Slot Increases to add to the multiworld.
The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld.
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random.
"""
display_name = "Item Slot Increases"
range_start = 0
range_end = 5
default = 3
class Atlantica(Toggle):
"""
Toggle whether to include checks in Atlantica.
"""
display_name = "Atlantica"
class HundredAcreWood(Toggle):
"""
Toggle whether to include checks in the 100 Acre Wood.
"""
display_name = "100 Acre Wood"
class SuperBosses(Toggle):
"""
Toggle whether to include checks behind Super Bosses.
"""
display_name = "Super Bosses"
class Cups(Toggle):
"""
Toggle whether to include checks behind completing Phil, Pegasus, Hercules, or Hades cups.
Please note that the cup items will still appear in the multiworld even if toggled off, as they are required to challenge Sephiroth.
"""
display_name = "Cups"
class Goal(Choice):
"""
Determines when victory is achieved in your playthrough.
Sephiroth: Defeat Sephiroth
Unknown: Defeat Unknown
Postcards: Turn in all 10 postcards in Traverse Town
Final Ansem: Enter End of the World and defeat Ansem as normal
Puppies: Rescue and return all 99 puppies in Traverse Town
Final Rest: Open the chest in End of the World Final Rest
"""
display_name = "Goal"
option_sephiroth = 0
option_unknown = 1
option_postcards = 2
option_final_ansem = 3
option_puppies = 4
option_final_rest = 5
default = 3
class EndoftheWorldUnlock(Choice):
"""Determines how End of the World is unlocked.
Item: You can receive an item called "End of the World" which unlocks the world
Reports: A certain amount of reports are required to unlock End of the World, which is defined in your options"""
display_name = "End of the World Unlock"
option_item = 0
option_reports = 1
default = 1
class FinalRestDoor(Choice):
"""Determines what conditions need to be met to manifest the door in Final Rest, allowing the player to challenge Ansem.
Reports: A certain number of Ansem's Reports are required, determined by the "Reports to Open Final Rest Door" option
Puppies: Having all 99 puppies is required
Postcards: Turning in all 10 postcards is required
Superbosses: Defeating Sephiroth, Unknown, Kurt Zisa, and Phantom are required
"""
display_name = "Final Rest Door"
option_reports = 0
option_puppies = 1
option_postcards = 2
option_superbosses = 3
class Puppies(Choice):
"""
Determines how dalmatian puppies are shuffled into the pool.
Full: All puppies are in one location
Triplets: Puppies are found in triplets just as they are in the base game
Individual: One puppy can be found per location
"""
display_name = "Puppies"
option_full = 0
option_triplets = 1
option_individual = 2
default = 1
class EXPMultiplier(NamedRange):
"""
Determines the multiplier to apply to EXP gained.
"""
display_name = "EXP Multiplier"
default = 16
range_start = default // 4
range_end = 128
special_range_names = {
"0.25x": int(default // 4),
"0.5x": int(default // 2),
"1x": default,
"2x": default * 2,
"3x": default * 3,
"4x": default * 4,
"8x": default * 8,
}
class RequiredReportsEotW(Range):
"""
If End of the World Unlock is set to "Reports", determines the number of Ansem's Reports required to open End of the World.
"""
display_name = "Reports to Open End of the World"
default = 4
range_start = 0
range_end = 13
class RequiredReportsDoor(Range):
"""
If Final Rest Door is set to "Reports", determines the number of Ansem's Reports required to manifest the door in Final Rest to challenge Ansem.
"""
display_name = "Reports to Open Final Rest Door"
default = 4
range_start = 0
range_end = 13
class ReportsInPool(Range):
"""
Determines the number of Ansem's Reports in the item pool.
"""
display_name = "Reports in Pool"
default = 4
range_start = 0
range_end = 13
class RandomizeKeybladeStats(DefaultOnToggle):
"""
Determines whether Keyblade stats should be randomized.
"""
display_name = "Randomize Keyblade Stats"
class KeybladeMinStrength(Range):
"""
Determines the minimum STR bonus a keyblade can have.
"""
display_name = "Keyblade Minimum STR Bonus"
default = 3
range_start = 0
range_end = 20
class KeybladeMaxStrength(Range):
"""
Determines the maximum STR bonus a keyblade can have.
"""
display_name = "Keyblade Maximum STR Bonus"
default = 14
range_start = 0
range_end = 20
class KeybladeMinMP(Range):
"""
Determines the minimum MP bonus a keyblade can have.
"""
display_name = "Keyblade Minimum MP Bonus"
default = -2
range_start = -2
range_end = 5
class KeybladeMaxMP(Range):
"""
Determines the maximum MP bonus a keyblade can have.
"""
display_name = "Keyblade Maximum MP Bonus"
default = 3
range_start = -2
range_end = 5
class LevelChecks(Range):
"""
Determines the maximum level for which checks can be obtained.
"""
display_name = "Level Checks"
default = 100
range_start = 0
range_end = 100
class ForceStatsOnLevels(NamedRange):
"""
If this value is less than the value for Level Checks, this determines the minimum level from which only stat ups are obtained at level up locations.
For example, if you want to be able to find any multiworld item from levels 1-50, then just stat ups for levels 51-100, set this value to 51.
"""
display_name = "Force Stats on Level Starting From"
default = 1
range_start = 1
range_end = 101
special_range_names = {
"none": 101,
"multiworld-to-level-50": 51,
"all": 1
}
class BadStartingWeapons(Toggle):
"""
Forces Kingdom Key, Dream Sword, Dream Shield, and Dream Staff to have bad stats.
"""
display_name = "Bad Starting Weapons"
class DonaldDeathLink(Toggle):
"""
If Donald is KO'ed, so is Sora. If Death Link is toggled on in your client, this will send a death to everyone.
"""
display_name = "Donald Death Link"
class GoofyDeathLink(Toggle):
"""
If Goofy is KO'ed, so is Sora. If Death Link is toggled on in your client, this will send a death to everyone.
"""
display_name = "Goofy Death Link"
class KeybladesUnlockChests(Toggle):
"""
If toggled on, the player is required to have a certain keyblade to open chests in certain worlds.
TT - Lionheart
WL - Lady Luck
OC - Olympia
DJ - Jungle King
AG - Three Wishes
MS - Wishing Star
HT - Pumpkinhead
NL - Fairy Harp
HB - Divine Rose
EotW - Oblivion
HAW - Oathkeeper
Note: Does not apply to Atlantica, the emblem and carousel chests in Hollow Bastion, or the Aero chest in Neverland currently.
"""
display_name = "Keyblades Unlock Chests"
class InteractInBattle(Toggle):
"""
Allow Sora to talk to people, examine objects, and open chests in battle.
"""
display_name = "Interact in Battle"
class AdvancedLogic(Toggle):
"""
If on, logic may expect you to do advanced skips like using Combo Master, Dumbo, and other unusual methods to reach locations.
"""
display_name = "Advanced Logic"
class ExtraSharedAbilities(Toggle):
"""
If on, adds extra shared abilities to the pool. These can stack, so multiple high jumps make you jump higher and multiple glides make you superglide faster.
"""
display_name = "Extra Shared Abilities"
class EXPZeroInPool(Toggle):
"""
If on, adds EXP Zero ability to the item pool. This is redundant if you are planning on playing on Proud.
"""
display_name = "EXP Zero in Pool"
class VanillaEmblemPieces(DefaultOnToggle):
"""
If on, the Hollow Bastion emblem pieces are in their vanilla locations.
"""
display_name = "Vanilla Emblem Pieces"
class StartingWorlds(Range):
"""
Number of random worlds to start with in addition to Traverse Town, which is always available. Will only consider Atlantica if toggled, and will only consider End of the World if its unlock is set to "Item".
"""
display_name = "Starting Worlds"
default = 0
range_start = 0
range_end = 10
@dataclass
class KH1Options(PerGameCommonOptions):
goal: Goal
end_of_the_world_unlock: EndoftheWorldUnlock
final_rest_door: FinalRestDoor
required_reports_eotw: RequiredReportsEotW
required_reports_door: RequiredReportsDoor
reports_in_pool: ReportsInPool
super_bosses: SuperBosses
atlantica: Atlantica
hundred_acre_wood: HundredAcreWood
cups: Cups
puppies: Puppies
starting_worlds: StartingWorlds
keyblades_unlock_chests: KeybladesUnlockChests
interact_in_battle: InteractInBattle
exp_multiplier: EXPMultiplier
advanced_logic: AdvancedLogic
extra_shared_abilities: ExtraSharedAbilities
exp_zero_in_pool: EXPZeroInPool
vanilla_emblem_pieces: VanillaEmblemPieces
donald_death_link: DonaldDeathLink
goofy_death_link: GoofyDeathLink
randomize_keyblade_stats: RandomizeKeybladeStats
bad_starting_weapons: BadStartingWeapons
keyblade_min_str: KeybladeMinStrength
keyblade_max_str: KeybladeMaxStrength
keyblade_min_mp: KeybladeMinMP
keyblade_max_mp: KeybladeMaxMP
level_checks: LevelChecks
force_stats_on_levels: ForceStatsOnLevels
strength_increase: StrengthIncrease
defense_increase: DefenseIncrease
hp_increase: HPIncrease
ap_increase: APIncrease
mp_increase: MPIncrease
accessory_slot_increase: AccessorySlotIncrease
item_slot_increase: ItemSlotIncrease
start_inventory_from_pool: StartInventoryPool
kh1_option_groups = [
OptionGroup("Goal", [
Goal,
EndoftheWorldUnlock,
FinalRestDoor,
RequiredReportsDoor,
RequiredReportsEotW,
ReportsInPool,
]),
OptionGroup("Locations", [
SuperBosses,
Atlantica,
Cups,
HundredAcreWood,
VanillaEmblemPieces,
]),
OptionGroup("Levels", [
EXPMultiplier,
LevelChecks,
ForceStatsOnLevels,
StrengthIncrease,
DefenseIncrease,
HPIncrease,
APIncrease,
MPIncrease,
AccessorySlotIncrease,
ItemSlotIncrease,
]),
OptionGroup("Keyblades", [
KeybladesUnlockChests,
RandomizeKeybladeStats,
BadStartingWeapons,
KeybladeMaxStrength,
KeybladeMinStrength,
KeybladeMaxMP,
KeybladeMinMP,
]),
OptionGroup("Misc", [
StartingWorlds,
Puppies,
InteractInBattle,
AdvancedLogic,
ExtraSharedAbilities,
EXPZeroInPool,
DonaldDeathLink,
GoofyDeathLink,
])
]

177
worlds/kh1/Presets.py Normal file
View File

@@ -0,0 +1,177 @@
from typing import Any, Dict
from .Options import *
kh1_option_presets: Dict[str, Dict[str, Any]] = {
# Standard playthrough where your goal is to defeat Ansem, reaching him by acquiring enough reports.
"Final Ansem": {
"goal": Goal.option_final_ansem,
"end_of_the_world_unlock": EndoftheWorldUnlock.option_reports,
"final_rest_door": FinalRestDoor.option_reports,
"required_reports_eotw": 7,
"required_reports_door": 10,
"reports_in_pool": 13,
"super_bosses": False,
"atlantica": False,
"hundred_acre_wood": False,
"cups": False,
"vanilla_emblem_pieces": True,
"exp_multiplier": 48,
"level_checks": 100,
"force_stats_on_levels": 1,
"strength_increase": 24,
"defense_increase": 24,
"hp_increase": 23,
"ap_increase": 18,
"mp_increase": 7,
"accessory_slot_increase": 1,
"item_slot_increase": 3,
"keyblades_unlock_chests": False,
"randomize_keyblade_stats": True,
"bad_starting_weapons": False,
"keyblade_max_str": 14,
"keyblade_min_str": 3,
"keyblade_max_mp": 3,
"keyblade_min_mp": -2,
"puppies": Puppies.option_triplets,
"starting_worlds": 0,
"interact_in_battle": False,
"advanced_logic": False,
"extra_shared_abilities": False,
"exp_zero_in_pool": False,
"donald_death_link": False,
"goofy_death_link": False
},
# Puppies are found individually, and the goal is to return them all.
"Puppy Hunt": {
"goal": Goal.option_puppies,
"end_of_the_world_unlock": EndoftheWorldUnlock.option_item,
"final_rest_door": FinalRestDoor.option_puppies,
"required_reports_eotw": 13,
"required_reports_door": 13,
"reports_in_pool": 13,
"super_bosses": False,
"atlantica": False,
"hundred_acre_wood": False,
"cups": False,
"vanilla_emblem_pieces": True,
"exp_multiplier": 48,
"level_checks": 100,
"force_stats_on_levels": 1,
"strength_increase": 24,
"defense_increase": 24,
"hp_increase": 23,
"ap_increase": 18,
"mp_increase": 7,
"accessory_slot_increase": 1,
"item_slot_increase": 3,
"keyblades_unlock_chests": False,
"randomize_keyblade_stats": True,
"bad_starting_weapons": False,
"keyblade_max_str": 14,
"keyblade_min_str": 3,
"keyblade_max_mp": 3,
"keyblade_min_mp": -2,
"puppies": Puppies.option_individual,
"starting_worlds": 0,
"interact_in_battle": False,
"advanced_logic": False,
"extra_shared_abilities": False,
"exp_zero_in_pool": False,
"donald_death_link": False,
"goofy_death_link": False
},
# Advanced playthrough with most settings on.
"Advanced": {
"goal": Goal.option_final_ansem,
"end_of_the_world_unlock": EndoftheWorldUnlock.option_reports,
"final_rest_door": FinalRestDoor.option_reports,
"required_reports_eotw": 7,
"required_reports_door": 10,
"reports_in_pool": 13,
"super_bosses": True,
"atlantica": True,
"hundred_acre_wood": True,
"cups": True,
"vanilla_emblem_pieces": False,
"exp_multiplier": 48,
"level_checks": 100,
"force_stats_on_levels": 1,
"strength_increase": 24,
"defense_increase": 24,
"hp_increase": 23,
"ap_increase": 18,
"mp_increase": 7,
"accessory_slot_increase": 1,
"item_slot_increase": 3,
"keyblades_unlock_chests": True,
"randomize_keyblade_stats": True,
"bad_starting_weapons": True,
"keyblade_max_str": 14,
"keyblade_min_str": 3,
"keyblade_max_mp": 3,
"keyblade_min_mp": -2,
"puppies": Puppies.option_triplets,
"starting_worlds": 0,
"interact_in_battle": True,
"advanced_logic": True,
"extra_shared_abilities": True,
"exp_zero_in_pool": True,
"donald_death_link": False,
"goofy_death_link": False
},
# Playthrough meant to enhance the level 1 experience.
"Level 1": {
"goal": Goal.option_final_ansem,
"end_of_the_world_unlock": EndoftheWorldUnlock.option_reports,
"final_rest_door": FinalRestDoor.option_reports,
"required_reports_eotw": 7,
"required_reports_door": 10,
"reports_in_pool": 13,
"super_bosses": False,
"atlantica": False,
"hundred_acre_wood": False,
"cups": False,
"vanilla_emblem_pieces": True,
"exp_multiplier": 16,
"level_checks": 0,
"force_stats_on_levels": 101,
"strength_increase": 0,
"defense_increase": 0,
"hp_increase": 0,
"mp_increase": 0,
"accessory_slot_increase": 6,
"item_slot_increase": 5,
"keyblades_unlock_chests": False,
"randomize_keyblade_stats": True,
"bad_starting_weapons": False,
"keyblade_max_str": 14,
"keyblade_min_str": 3,
"keyblade_max_mp": 3,
"keyblade_min_mp": -2,
"puppies": Puppies.option_triplets,
"starting_worlds": 0,
"interact_in_battle": False,
"advanced_logic": False,
"extra_shared_abilities": False,
"exp_zero_in_pool": False,
"donald_death_link": False,
"goofy_death_link": False
}
}

516
worlds/kh1/Regions.py Normal file
View File

@@ -0,0 +1,516 @@
from typing import Dict, List, NamedTuple, Optional
from BaseClasses import MultiWorld, Region, Entrance
from .Locations import KH1Location, location_table
class KH1RegionData(NamedTuple):
locations: List[str]
region_exits: Optional[List[str]]
def create_regions(multiworld: MultiWorld, player: int, options):
regions: Dict[str, KH1RegionData] = {
"Menu": KH1RegionData([], ["Awakening", "Levels"]),
"Awakening": KH1RegionData([], ["Destiny Islands"]),
"Destiny Islands": KH1RegionData([], ["Traverse Town"]),
"Traverse Town": KH1RegionData([], ["World Map"]),
"Wonderland": KH1RegionData([], []),
"Olympus Coliseum": KH1RegionData([], []),
"Deep Jungle": KH1RegionData([], []),
"Agrabah": KH1RegionData([], []),
"Monstro": KH1RegionData([], []),
"Atlantica": KH1RegionData([], []),
"Halloween Town": KH1RegionData([], []),
"Neverland": KH1RegionData([], []),
"Hollow Bastion": KH1RegionData([], []),
"End of the World": KH1RegionData([], []),
"100 Acre Wood": KH1RegionData([], []),
"Levels": KH1RegionData([], []),
"World Map": KH1RegionData([], ["Wonderland", "Olympus Coliseum", "Deep Jungle",
"Agrabah", "Monstro", "Atlantica",
"Halloween Town", "Neverland", "Hollow Bastion",
"End of the World", "100 Acre Wood"])
}
# Set up locations
regions["Agrabah"].locations.append("Agrabah Aladdin's House Main Street Entrance Chest")
regions["Agrabah"].locations.append("Agrabah Aladdin's House Plaza Entrance Chest")
regions["Agrabah"].locations.append("Agrabah Alley Chest")
regions["Agrabah"].locations.append("Agrabah Bazaar Across Windows Chest")
regions["Agrabah"].locations.append("Agrabah Bazaar High Corner Chest")
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Bottomless Hall Across Chasm Chest")
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Bottomless Hall Pillar Chest")
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Bottomless Hall Raised Platform Chest")
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Dark Chamber Abu Gem Chest")
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Dark Chamber Across from Relic Chamber Entrance Chest")
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Dark Chamber Bridge Chest")
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Dark Chamber Near Save Chest")
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Entrance Left Chest")
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Entrance Tall Tower Chest")
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Entrance White Trinity Chest")
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Hall High Left Chest")
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Hall Near Bottomless Hall Chest")
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Hidden Room Left Chest")
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Hidden Room Right Chest")
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Relic Chamber Jump from Stairs Chest")
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Relic Chamber Stairs Chest")
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Silent Chamber Blue Trinity Chest")
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Treasure Room Above Fire Chest")
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Treasure Room Across Platforms Chest")
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Treasure Room Large Treasure Pile Chest")
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Treasure Room Small Treasure Pile Chest")
regions["Agrabah"].locations.append("Agrabah Defeat Jafar Blizzard Event")
regions["Agrabah"].locations.append("Agrabah Defeat Jafar Genie Ansem's Report 1")
regions["Agrabah"].locations.append("Agrabah Defeat Jafar Genie Fire Event")
regions["Agrabah"].locations.append("Agrabah Defeat Pot Centipede Ray of Light Event")
regions["Agrabah"].locations.append("Agrabah Main Street High Above Alley Entrance Chest")
regions["Agrabah"].locations.append("Agrabah Main Street High Above Palace Gates Entrance Chest")
regions["Agrabah"].locations.append("Agrabah Main Street Right Palace Entrance Chest")
regions["Agrabah"].locations.append("Agrabah Palace Gates High Close to Palace Chest")
regions["Agrabah"].locations.append("Agrabah Palace Gates High Opposite Palace Chest")
regions["Agrabah"].locations.append("Agrabah Palace Gates Low Chest")
regions["Agrabah"].locations.append("Agrabah Plaza By Storage Chest")
regions["Agrabah"].locations.append("Agrabah Plaza Raised Terrace Chest")
regions["Agrabah"].locations.append("Agrabah Plaza Top Corner Chest")
regions["Agrabah"].locations.append("Agrabah Seal Keyhole Genie Event")
regions["Agrabah"].locations.append("Agrabah Seal Keyhole Green Trinity Event")
regions["Agrabah"].locations.append("Agrabah Seal Keyhole Three Wishes Event")
regions["Agrabah"].locations.append("Agrabah Storage Behind Barrel Chest")
regions["Agrabah"].locations.append("Agrabah Storage Green Trinity Chest")
regions["Deep Jungle"].locations.append("Deep Jungle Bamboo Thicket Save Gorillas")
regions["Deep Jungle"].locations.append("Deep Jungle Camp Blue Trinity Chest")
regions["Deep Jungle"].locations.append("Deep Jungle Camp Ether Experiment")
regions["Deep Jungle"].locations.append("Deep Jungle Camp Hi-Potion Experiment")
regions["Deep Jungle"].locations.append("Deep Jungle Camp Replication Experiment")
regions["Deep Jungle"].locations.append("Deep Jungle Camp Save Gorillas")
regions["Deep Jungle"].locations.append("Deep Jungle Cavern of Hearts Navi-G Piece Event")
regions["Deep Jungle"].locations.append("Deep Jungle Cavern of Hearts White Trinity Chest")
regions["Deep Jungle"].locations.append("Deep Jungle Cliff Right Cliff Left Chest")
regions["Deep Jungle"].locations.append("Deep Jungle Cliff Right Cliff Right Chest")
regions["Deep Jungle"].locations.append("Deep Jungle Cliff Save Gorillas")
regions["Deep Jungle"].locations.append("Deep Jungle Climbing Trees Blue Trinity Chest")
regions["Deep Jungle"].locations.append("Deep Jungle Climbing Trees Save Gorillas")
regions["Deep Jungle"].locations.append("Deep Jungle Defeat Clayton Cure Event")
regions["Deep Jungle"].locations.append("Deep Jungle Defeat Sabor White Fang Event")
regions["Deep Jungle"].locations.append("Deep Jungle Hippo's Lagoon Center Chest")
regions["Deep Jungle"].locations.append("Deep Jungle Hippo's Lagoon Left Chest")
regions["Deep Jungle"].locations.append("Deep Jungle Hippo's Lagoon Right Chest")
regions["Deep Jungle"].locations.append("Deep Jungle Jungle Slider 10 Fruits")
regions["Deep Jungle"].locations.append("Deep Jungle Jungle Slider 20 Fruits")
regions["Deep Jungle"].locations.append("Deep Jungle Jungle Slider 30 Fruits")
regions["Deep Jungle"].locations.append("Deep Jungle Jungle Slider 40 Fruits")
regions["Deep Jungle"].locations.append("Deep Jungle Jungle Slider 50 Fruits")
regions["Deep Jungle"].locations.append("Deep Jungle Seal Keyhole Jungle King Event")
regions["Deep Jungle"].locations.append("Deep Jungle Seal Keyhole Red Trinity Event")
regions["Deep Jungle"].locations.append("Deep Jungle Tent Chest")
regions["Deep Jungle"].locations.append("Deep Jungle Tent Protect-G Event")
regions["Deep Jungle"].locations.append("Deep Jungle Tree House Beneath Tree House Chest")
regions["Deep Jungle"].locations.append("Deep Jungle Tree House Rooftop Chest")
regions["Deep Jungle"].locations.append("Deep Jungle Tree House Save Gorillas")
regions["Deep Jungle"].locations.append("Deep Jungle Tree House Suspended Boat Chest")
regions["Deep Jungle"].locations.append("Deep Jungle Tunnel Chest")
regions["Deep Jungle"].locations.append("Deep Jungle Vines 2 Chest")
regions["Deep Jungle"].locations.append("Deep Jungle Vines Chest")
regions["Deep Jungle"].locations.append("Deep Jungle Waterfall Cavern High Middle Chest")
regions["Deep Jungle"].locations.append("Deep Jungle Waterfall Cavern High Wall Chest")
regions["Deep Jungle"].locations.append("Deep Jungle Waterfall Cavern Low Chest")
regions["Deep Jungle"].locations.append("Deep Jungle Waterfall Cavern Middle Chest")
regions["End of the World"].locations.append("End of the World Defeat Chernabog Superglide Event")
regions["End of the World"].locations.append("End of the World Final Dimension 10th Chest")
regions["End of the World"].locations.append("End of the World Final Dimension 1st Chest")
regions["End of the World"].locations.append("End of the World Final Dimension 2nd Chest")
regions["End of the World"].locations.append("End of the World Final Dimension 3rd Chest")
regions["End of the World"].locations.append("End of the World Final Dimension 4th Chest")
regions["End of the World"].locations.append("End of the World Final Dimension 5th Chest")
regions["End of the World"].locations.append("End of the World Final Dimension 6th Chest")
regions["End of the World"].locations.append("End of the World Final Dimension 7th Chest")
regions["End of the World"].locations.append("End of the World Final Dimension 8th Chest")
regions["End of the World"].locations.append("End of the World Final Dimension 9th Chest")
regions["End of the World"].locations.append("End of the World Final Rest Chest")
regions["End of the World"].locations.append("End of the World Giant Crevasse 1st Chest")
regions["End of the World"].locations.append("End of the World Giant Crevasse 2nd Chest")
regions["End of the World"].locations.append("End of the World Giant Crevasse 3rd Chest")
regions["End of the World"].locations.append("End of the World Giant Crevasse 4th Chest")
regions["End of the World"].locations.append("End of the World Giant Crevasse 5th Chest")
regions["End of the World"].locations.append("End of the World World Terminus 100 Acre Wood Chest")
regions["End of the World"].locations.append("End of the World World Terminus Agrabah Chest")
regions["End of the World"].locations.append("End of the World World Terminus Atlantica Chest")
regions["End of the World"].locations.append("End of the World World Terminus Deep Jungle Chest")
regions["End of the World"].locations.append("End of the World World Terminus Halloween Town Chest")
#regions["End of the World"].locations.append("End of the World World Terminus Hollow Bastion Chest")
regions["End of the World"].locations.append("End of the World World Terminus Neverland Chest")
regions["End of the World"].locations.append("End of the World World Terminus Olympus Coliseum Chest")
regions["End of the World"].locations.append("End of the World World Terminus Traverse Town Chest")
regions["End of the World"].locations.append("End of the World World Terminus Wonderland Chest")
regions["Halloween Town"].locations.append("Halloween Town Boneyard Tombstone Puzzle Chest")
regions["Halloween Town"].locations.append("Halloween Town Bridge Left of Gate Chest")
regions["Halloween Town"].locations.append("Halloween Town Bridge Right of Gate Chest")
regions["Halloween Town"].locations.append("Halloween Town Bridge Under Bridge")
regions["Halloween Town"].locations.append("Halloween Town Cemetery Behind Grave Chest")
regions["Halloween Town"].locations.append("Halloween Town Cemetery Between Graves Chest")
regions["Halloween Town"].locations.append("Halloween Town Cemetery By Cat Shape Chest")
regions["Halloween Town"].locations.append("Halloween Town Cemetery By Striped Grave Chest")
regions["Halloween Town"].locations.append("Halloween Town Defeat Oogie Boogie Ansem's Report 7")
regions["Halloween Town"].locations.append("Halloween Town Defeat Oogie Boogie Holy Circlet Event")
regions["Halloween Town"].locations.append("Halloween Town Defeat Oogie's Manor Gravity Event")
regions["Halloween Town"].locations.append("Halloween Town Graveyard Forget-Me-Not Event")
regions["Halloween Town"].locations.append("Halloween Town Guillotine Square High Tower Chest")
regions["Halloween Town"].locations.append("Halloween Town Guillotine Square Pumpkin Structure Left Chest")
regions["Halloween Town"].locations.append("Halloween Town Guillotine Square Pumpkin Structure Right Chest")
regions["Halloween Town"].locations.append("Halloween Town Guillotine Square Ring Jack's Doorbell 3 Times")
regions["Halloween Town"].locations.append("Halloween Town Guillotine Square Under Jack's House Stairs Chest")
regions["Halloween Town"].locations.append("Halloween Town Lab Torn Page")
regions["Halloween Town"].locations.append("Halloween Town Moonlight Hill White Trinity Chest")
regions["Halloween Town"].locations.append("Halloween Town Oogie's Manor Entrance Steps Chest")
regions["Halloween Town"].locations.append("Halloween Town Oogie's Manor Grounds Red Trinity Chest")
regions["Halloween Town"].locations.append("Halloween Town Oogie's Manor Hollow Chest")
regions["Halloween Town"].locations.append("Halloween Town Oogie's Manor Inside Entrance Chest")
regions["Halloween Town"].locations.append("Halloween Town Oogie's Manor Lower Iron Cage Chest")
regions["Halloween Town"].locations.append("Halloween Town Oogie's Manor Upper Iron Cage Chest")
regions["Halloween Town"].locations.append("Halloween Town Seal Keyhole Pumpkinhead Event")
regions["Hollow Bastion"].locations.append("Hollow Bastion Base Level Bubble Under the Wall Platform Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Base Level Near Crystal Switch Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Base Level Platform Near Entrance Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Castle Gates Freestanding Pillar Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Castle Gates Gravity Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Castle Gates High Pillar Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Behemoth Omega Arts Event")
regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Dragon Maleficent Fireglow Event")
regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Maleficent Ansem's Report 5")
regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Maleficent Donald Cheer Event")
regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Riku I White Trinity Event")
regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Riku II Ragnarok Event")
regions["Hollow Bastion"].locations.append("Hollow Bastion Dungeon By Candles Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Dungeon Corner Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Entrance Hall Emblem Piece (Chest)")
regions["Hollow Bastion"].locations.append("Hollow Bastion Entrance Hall Emblem Piece (Flame)")
regions["Hollow Bastion"].locations.append("Hollow Bastion Entrance Hall Emblem Piece (Fountain)")
regions["Hollow Bastion"].locations.append("Hollow Bastion Entrance Hall Emblem Piece (Statue)")
regions["Hollow Bastion"].locations.append("Hollow Bastion Entrance Hall Left of Emblem Door Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Grand Hall Left of Gate Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Grand Hall Oblivion Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Grand Hall Steps Right Side Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Great Crest After Battle Platform Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Great Crest Lower Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion High Tower 1st Gravity Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion High Tower 2nd Gravity Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion High Tower Above Sliding Blocks Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Library 1st Floor Turn the Carousel Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Library 2nd Floor Turn the Carousel 1st Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Library 2nd Floor Turn the Carousel 2nd Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Library Speak to Aerith Cure")
regions["Hollow Bastion"].locations.append("Hollow Bastion Library Speak to Belle Divine Rose")
regions["Hollow Bastion"].locations.append("Hollow Bastion Library Top of Bookshelf Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Library Top of Bookshelf Turn the Carousel Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Lift Stop Heartless Sigil Door Gravity Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Lift Stop Library Node After High Tower Switch Gravity Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Lift Stop Library Node Gravity Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Lift Stop Outside Library Gravity Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Lift Stop Under High Tower Sliding Blocks Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Rising Falls Floating Platform Near Bubble Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Rising Falls Floating Platform Near Save Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Rising Falls High Platform Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Rising Falls Under Water 1st Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Rising Falls Under Water 2nd Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Rising Falls Water's Surface Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Rising Falls White Trinity Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Speak to Princesses Fire Event")
regions["Hollow Bastion"].locations.append("Hollow Bastion Speak with Aerith Ansem's Report 10")
regions["Hollow Bastion"].locations.append("Hollow Bastion Speak with Aerith Ansem's Report 2")
regions["Hollow Bastion"].locations.append("Hollow Bastion Speak with Aerith Ansem's Report 4")
regions["Hollow Bastion"].locations.append("Hollow Bastion Speak with Aerith Ansem's Report 6")
regions["Hollow Bastion"].locations.append("Hollow Bastion Waterway Blizzard on Bubble Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Waterway Near Save Chest")
regions["Hollow Bastion"].locations.append("Hollow Bastion Waterway Unlock Passage from Base Level Chest")
regions["Monstro"].locations.append("Monstro Chamber 2 Ground Chest")
regions["Monstro"].locations.append("Monstro Chamber 2 Platform Chest")
regions["Monstro"].locations.append("Monstro Chamber 3 Ground Chest")
regions["Monstro"].locations.append("Monstro Chamber 3 Near Chamber 6 Entrance Chest")
regions["Monstro"].locations.append("Monstro Chamber 3 Platform Above Chamber 2 Entrance Chest")
regions["Monstro"].locations.append("Monstro Chamber 3 Platform Near Chamber 6 Entrance Chest")
regions["Monstro"].locations.append("Monstro Chamber 5 Atop Barrel Chest")
regions["Monstro"].locations.append("Monstro Chamber 5 Low 1st Chest")
regions["Monstro"].locations.append("Monstro Chamber 5 Low 2nd Chest")
regions["Monstro"].locations.append("Monstro Chamber 5 Platform Chest")
regions["Monstro"].locations.append("Monstro Chamber 6 Low Chest")
regions["Monstro"].locations.append("Monstro Chamber 6 Other Platform Chest")
regions["Monstro"].locations.append("Monstro Chamber 6 Platform Near Chamber 5 Entrance Chest")
regions["Monstro"].locations.append("Monstro Chamber 6 Raised Area Near Chamber 1 Entrance Chest")
regions["Monstro"].locations.append("Monstro Chamber 6 White Trinity Chest")
regions["Monstro"].locations.append("Monstro Defeat Parasite Cage I Goofy Cheer Event")
regions["Monstro"].locations.append("Monstro Defeat Parasite Cage II Stop Event")
regions["Monstro"].locations.append("Monstro Mouth Boat Deck Chest")
regions["Monstro"].locations.append("Monstro Mouth Green Trinity Top of Boat Chest")
regions["Monstro"].locations.append("Monstro Mouth High Platform Across from Boat Chest")
regions["Monstro"].locations.append("Monstro Mouth High Platform Boat Side Chest")
regions["Monstro"].locations.append("Monstro Mouth High Platform Near Teeth Chest")
regions["Monstro"].locations.append("Monstro Mouth Near Ship Chest")
regions["Neverland"].locations.append("Neverland Cabin Chest")
regions["Neverland"].locations.append("Neverland Captain's Cabin Chest")
#regions["Neverland"].locations.append("Neverland Clock Tower 01:00 Door")
#regions["Neverland"].locations.append("Neverland Clock Tower 02:00 Door")
#regions["Neverland"].locations.append("Neverland Clock Tower 03:00 Door")
#regions["Neverland"].locations.append("Neverland Clock Tower 04:00 Door")
#regions["Neverland"].locations.append("Neverland Clock Tower 05:00 Door")
#regions["Neverland"].locations.append("Neverland Clock Tower 06:00 Door")
#regions["Neverland"].locations.append("Neverland Clock Tower 07:00 Door")
#regions["Neverland"].locations.append("Neverland Clock Tower 08:00 Door")
#regions["Neverland"].locations.append("Neverland Clock Tower 09:00 Door")
#regions["Neverland"].locations.append("Neverland Clock Tower 10:00 Door")
#regions["Neverland"].locations.append("Neverland Clock Tower 11:00 Door")
#regions["Neverland"].locations.append("Neverland Clock Tower 12:00 Door")
regions["Neverland"].locations.append("Neverland Clock Tower Chest")
regions["Neverland"].locations.append("Neverland Defeat Anti Sora Raven's Claw Event")
regions["Neverland"].locations.append("Neverland Defeat Captain Hook Ars Arcanum Event")
regions["Neverland"].locations.append("Neverland Defeat Hook Ansem's Report 9")
regions["Neverland"].locations.append("Neverland Encounter Hook Cure Event")
regions["Neverland"].locations.append("Neverland Galley Chest")
regions["Neverland"].locations.append("Neverland Hold Aero Chest")
regions["Neverland"].locations.append("Neverland Hold Flight 1st Chest")
regions["Neverland"].locations.append("Neverland Hold Flight 2nd Chest")
regions["Neverland"].locations.append("Neverland Hold Yellow Trinity Green Chest")
regions["Neverland"].locations.append("Neverland Hold Yellow Trinity Left Blue Chest")
regions["Neverland"].locations.append("Neverland Hold Yellow Trinity Right Blue Chest")
regions["Neverland"].locations.append("Neverland Pirate Ship Crows Nest Chest")
regions["Neverland"].locations.append("Neverland Pirate Ship Deck White Trinity Chest")
regions["Neverland"].locations.append("Neverland Seal Keyhole Fairy Harp Event")
regions["Neverland"].locations.append("Neverland Seal Keyhole Glide Event")
regions["Neverland"].locations.append("Neverland Seal Keyhole Tinker Bell Event")
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Clear Phil's Training Thunder Event")
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Cloud Sonic Blade Event")
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Coliseum Gates Blizzaga Chest")
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Coliseum Gates Blizzara Chest")
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Coliseum Gates Entry Pass Event")
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Coliseum Gates Green Trinity")
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Coliseum Gates Hero's License Event")
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Coliseum Gates Left Behind Columns Chest")
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Coliseum Gates Left Blue Trinity Chest")
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Coliseum Gates Right Blue Trinity Chest")
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Coliseum Gates White Trinity Chest")
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Defeat Cerberus Inferno Band Event")
regions["Traverse Town"].locations.append("Traverse Town 1st District Accessory Shop Roof Chest")
#regions["Traverse Town"].locations.append("Traverse Town 1st District Aerith Gift")
regions["Traverse Town"].locations.append("Traverse Town 1st District Blue Trinity Balcony Chest")
regions["Traverse Town"].locations.append("Traverse Town 1st District Candle Puzzle Chest")
#regions["Traverse Town"].locations.append("Traverse Town 1st District Leon Gift")
regions["Traverse Town"].locations.append("Traverse Town 1st District Safe Postcard")
regions["Traverse Town"].locations.append("Traverse Town 1st District Speak with Cid Event")
regions["Traverse Town"].locations.append("Traverse Town 2nd District Boots and Shoes Awning Chest")
regions["Traverse Town"].locations.append("Traverse Town 2nd District Gizmo Shop Facade Chest")
regions["Traverse Town"].locations.append("Traverse Town 2nd District Rooftop Chest")
regions["Traverse Town"].locations.append("Traverse Town 3rd District Balcony Postcard")
regions["Traverse Town"].locations.append("Traverse Town Accessory Shop Chest")
regions["Traverse Town"].locations.append("Traverse Town Alleyway Balcony Chest")
regions["Traverse Town"].locations.append("Traverse Town Alleyway Behind Crates Chest")
regions["Traverse Town"].locations.append("Traverse Town Alleyway Blue Room Awning Chest")
regions["Traverse Town"].locations.append("Traverse Town Alleyway Corner Chest")
regions["Traverse Town"].locations.append("Traverse Town Defeat Guard Armor Blue Trinity Event")
regions["Traverse Town"].locations.append("Traverse Town Defeat Guard Armor Brave Warrior Event")
regions["Traverse Town"].locations.append("Traverse Town Defeat Guard Armor Dodge Roll Event")
regions["Traverse Town"].locations.append("Traverse Town Defeat Guard Armor Fire Event")
regions["Traverse Town"].locations.append("Traverse Town Defeat Opposite Armor Aero Event")
regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Chest")
regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Geppetto All Summons Reward")
regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Geppetto Reward 1")
regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Geppetto Reward 2")
regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Geppetto Reward 3")
regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Geppetto Reward 4")
regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Geppetto Reward 5")
regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Postcard")
regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Talk to Pinocchio")
regions["Traverse Town"].locations.append("Traverse Town Gizmo Shop Postcard 1")
regions["Traverse Town"].locations.append("Traverse Town Gizmo Shop Postcard 2")
regions["Traverse Town"].locations.append("Traverse Town Green Room Clock Puzzle Chest")
regions["Traverse Town"].locations.append("Traverse Town Green Room Table Chest")
regions["Traverse Town"].locations.append("Traverse Town Item Shop Postcard")
regions["Traverse Town"].locations.append("Traverse Town Item Workshop Left Chest")
regions["Traverse Town"].locations.append("Traverse Town Item Workshop Postcard")
regions["Traverse Town"].locations.append("Traverse Town Item Workshop Right Chest")
regions["Traverse Town"].locations.append("Traverse Town Kairi Secret Waterway Oathkeeper Event")
regions["Traverse Town"].locations.append("Traverse Town Leon Secret Waterway Earthshine Event")
regions["Traverse Town"].locations.append("Traverse Town Magician's Study Obtained All Arts Items")
regions["Traverse Town"].locations.append("Traverse Town Magician's Study Obtained All LV1 Magic")
regions["Traverse Town"].locations.append("Traverse Town Magician's Study Obtained All LV3 Magic")
regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 01 Event")
regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 02 Event")
regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 03 Event")
regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 04 Event")
regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 05 Event")
regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 06 Event")
regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 07 Event")
regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 08 Event")
regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 09 Event")
regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 10 Event")
regions["Traverse Town"].locations.append("Traverse Town Mystical House Glide Chest")
regions["Traverse Town"].locations.append("Traverse Town Mystical House Yellow Trinity Chest")
regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 10 Puppies")
regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 20 Puppies")
regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 30 Puppies")
regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 40 Puppies")
regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 50 Puppies Reward 1")
regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 50 Puppies Reward 2")
regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 60 Puppies")
regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 70 Puppies")
regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 80 Puppies")
regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 90 Puppies")
regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 99 Puppies Reward 1")
regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 99 Puppies Reward 2")
regions["Traverse Town"].locations.append("Traverse Town Red Room Chest")
regions["Traverse Town"].locations.append("Traverse Town Secret Waterway Near Stairs Chest")
regions["Traverse Town"].locations.append("Traverse Town Secret Waterway White Trinity Chest")
regions["Traverse Town"].locations.append("Traverse Town Synth Cloth")
regions["Traverse Town"].locations.append("Traverse Town Synth Fish")
regions["Traverse Town"].locations.append("Traverse Town Synth Log")
regions["Traverse Town"].locations.append("Traverse Town Synth Mushroom")
regions["Traverse Town"].locations.append("Traverse Town Synth Rope")
regions["Traverse Town"].locations.append("Traverse Town Synth Seagull Egg")
regions["Wonderland"].locations.append("Wonderland Bizarre Room Green Trinity Chest")
regions["Wonderland"].locations.append("Wonderland Bizarre Room Lamp Chest")
regions["Wonderland"].locations.append("Wonderland Bizarre Room Navi-G Piece Event")
regions["Wonderland"].locations.append("Wonderland Bizarre Room Read Book")
regions["Wonderland"].locations.append("Wonderland Defeat Trickmaster Blizzard Event")
regions["Wonderland"].locations.append("Wonderland Defeat Trickmaster Ifrit's Horn Event")
regions["Wonderland"].locations.append("Wonderland Lotus Forest Corner Chest")
regions["Wonderland"].locations.append("Wonderland Lotus Forest Glide Chest")
regions["Wonderland"].locations.append("Wonderland Lotus Forest Nut Chest")
regions["Wonderland"].locations.append("Wonderland Lotus Forest Through the Painting Thunder Plant Chest")
regions["Wonderland"].locations.append("Wonderland Lotus Forest Through the Painting White Trinity Chest")
regions["Wonderland"].locations.append("Wonderland Lotus Forest Thunder Plant Chest")
regions["Wonderland"].locations.append("Wonderland Queen's Castle Hedge Left Red Chest")
regions["Wonderland"].locations.append("Wonderland Queen's Castle Hedge Right Blue Chest")
regions["Wonderland"].locations.append("Wonderland Queen's Castle Hedge Right Red Chest")
regions["Wonderland"].locations.append("Wonderland Rabbit Hole Defeat Heartless 1 Chest")
regions["Wonderland"].locations.append("Wonderland Rabbit Hole Defeat Heartless 2 Chest")
regions["Wonderland"].locations.append("Wonderland Rabbit Hole Defeat Heartless 3 Chest")
regions["Wonderland"].locations.append("Wonderland Rabbit Hole Green Trinity Chest")
regions["Wonderland"].locations.append("Wonderland Tea Party Garden Above Lotus Forest Entrance 1st Chest")
regions["Wonderland"].locations.append("Wonderland Tea Party Garden Above Lotus Forest Entrance 2nd Chest")
regions["Wonderland"].locations.append("Wonderland Tea Party Garden Across From Bizarre Room Entrance Chest")
regions["Wonderland"].locations.append("Wonderland Tea Party Garden Bear and Clock Puzzle Chest")
if options.hundred_acre_wood:
regions["100 Acre Wood"].locations.append("100 Acre Wood Meadow Inside Log Chest")
regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Left Cliff Chest")
regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Right Tree Alcove Chest")
regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Under Giant Pot Chest")
regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Turn in Rare Nut 1")
regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Turn in Rare Nut 2")
regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Turn in Rare Nut 3")
regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Turn in Rare Nut 4")
regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Turn in Rare Nut 5")
regions["100 Acre Wood"].locations.append("100 Acre Wood Pooh's House Owl Cheer")
regions["100 Acre Wood"].locations.append("100 Acre Wood Convert Torn Page 1")
regions["100 Acre Wood"].locations.append("100 Acre Wood Convert Torn Page 2")
regions["100 Acre Wood"].locations.append("100 Acre Wood Convert Torn Page 3")
regions["100 Acre Wood"].locations.append("100 Acre Wood Convert Torn Page 4")
regions["100 Acre Wood"].locations.append("100 Acre Wood Convert Torn Page 5")
regions["100 Acre Wood"].locations.append("100 Acre Wood Pooh's House Start Fire")
regions["100 Acre Wood"].locations.append("100 Acre Wood Pooh's Room Cabinet")
regions["100 Acre Wood"].locations.append("100 Acre Wood Pooh's Room Chimney")
regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Break Log")
regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Fall Through Top of Tree Next to Pooh")
if options.atlantica:
regions["Atlantica"].locations.append("Atlantica Sunken Ship In Flipped Boat Chest")
regions["Atlantica"].locations.append("Atlantica Sunken Ship Seabed Chest")
regions["Atlantica"].locations.append("Atlantica Sunken Ship Inside Ship Chest")
regions["Atlantica"].locations.append("Atlantica Ariel's Grotto High Chest")
regions["Atlantica"].locations.append("Atlantica Ariel's Grotto Middle Chest")
regions["Atlantica"].locations.append("Atlantica Ariel's Grotto Low Chest")
regions["Atlantica"].locations.append("Atlantica Ursula's Lair Use Fire on Urchin Chest")
regions["Atlantica"].locations.append("Atlantica Undersea Gorge Jammed by Ariel's Grotto Chest")
regions["Atlantica"].locations.append("Atlantica Triton's Palace White Trinity Chest")
regions["Atlantica"].locations.append("Atlantica Defeat Ursula I Mermaid Kick Event")
regions["Atlantica"].locations.append("Atlantica Defeat Ursula II Thunder Event")
regions["Atlantica"].locations.append("Atlantica Seal Keyhole Crabclaw Event")
regions["Atlantica"].locations.append("Atlantica Undersea Gorge Blizzard Clam")
regions["Atlantica"].locations.append("Atlantica Undersea Gorge Ocean Floor Clam")
regions["Atlantica"].locations.append("Atlantica Undersea Valley Higher Cave Clam")
regions["Atlantica"].locations.append("Atlantica Undersea Valley Lower Cave Clam")
regions["Atlantica"].locations.append("Atlantica Undersea Valley Fire Clam")
regions["Atlantica"].locations.append("Atlantica Undersea Valley Wall Clam")
regions["Atlantica"].locations.append("Atlantica Undersea Valley Pillar Clam")
regions["Atlantica"].locations.append("Atlantica Undersea Valley Ocean Floor Clam")
regions["Atlantica"].locations.append("Atlantica Triton's Palace Thunder Clam")
regions["Atlantica"].locations.append("Atlantica Triton's Palace Wall Right Clam")
regions["Atlantica"].locations.append("Atlantica Triton's Palace Near Path Clam")
regions["Atlantica"].locations.append("Atlantica Triton's Palace Wall Left Clam")
regions["Atlantica"].locations.append("Atlantica Cavern Nook Clam")
regions["Atlantica"].locations.append("Atlantica Below Deck Clam")
regions["Atlantica"].locations.append("Atlantica Undersea Garden Clam")
regions["Atlantica"].locations.append("Atlantica Undersea Cave Clam")
regions["Atlantica"].locations.append("Atlantica Sunken Ship Crystal Trident Event")
regions["Atlantica"].locations.append("Atlantica Defeat Ursula II Ansem's Report 3")
if options.cups:
regions["Olympus Coliseum"].locations.append("Complete Phil Cup")
regions["Olympus Coliseum"].locations.append("Complete Phil Cup Solo")
regions["Olympus Coliseum"].locations.append("Complete Phil Cup Time Trial")
regions["Olympus Coliseum"].locations.append("Complete Pegasus Cup")
regions["Olympus Coliseum"].locations.append("Complete Pegasus Cup Solo")
regions["Olympus Coliseum"].locations.append("Complete Pegasus Cup Time Trial")
regions["Olympus Coliseum"].locations.append("Complete Hercules Cup")
regions["Olympus Coliseum"].locations.append("Complete Hercules Cup Solo")
regions["Olympus Coliseum"].locations.append("Complete Hercules Cup Time Trial")
regions["Olympus Coliseum"].locations.append("Complete Hades Cup")
regions["Olympus Coliseum"].locations.append("Complete Hades Cup Solo")
regions["Olympus Coliseum"].locations.append("Complete Hades Cup Time Trial")
regions["Olympus Coliseum"].locations.append("Hades Cup Defeat Cloud and Leon Event")
regions["Olympus Coliseum"].locations.append("Hades Cup Defeat Yuffie Event")
regions["Olympus Coliseum"].locations.append("Hades Cup Defeat Cerberus Event")
regions["Olympus Coliseum"].locations.append("Hades Cup Defeat Behemoth Event")
regions["Olympus Coliseum"].locations.append("Hades Cup Defeat Hades Event")
regions["Olympus Coliseum"].locations.append("Hercules Cup Defeat Cloud Event")
regions["Olympus Coliseum"].locations.append("Hercules Cup Yellow Trinity Event")
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Defeat Hades Ansem's Report 8")
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Olympia Chest")
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Defeat Ice Titan Diamond Dust Event")
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Gates Purple Jar After Defeating Hades")
if options.super_bosses:
regions["Neverland"].locations.append("Neverland Defeat Phantom Stop Event")
regions["Agrabah"].locations.append("Agrabah Defeat Kurt Zisa Zantetsuken Event")
regions["Agrabah"].locations.append("Agrabah Defeat Kurt Zisa Ansem's Report 11")
if options.super_bosses or options.goal.current_key == "sephiroth":
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Defeat Sephiroth Ansem's Report 12")
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Defeat Sephiroth One-Winged Angel Event")
if options.super_bosses or options.goal.current_key == "unknown":
regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Unknown Ansem's Report 13")
regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Unknown EXP Necklace Event")
for i in range(options.level_checks):
regions["Levels"].locations.append("Level " + str(i+1).rjust(3, '0'))
if options.goal.current_key == "final_ansem":
regions["End of the World"].locations.append("Final Ansem")
# Set up the regions correctly.
for name, data in regions.items():
multiworld.regions.append(create_region(multiworld, player, name, data))
multiworld.get_entrance("Awakening", player).connect(multiworld.get_region("Awakening", player))
multiworld.get_entrance("Destiny Islands", player).connect(multiworld.get_region("Destiny Islands", player))
multiworld.get_entrance("Traverse Town", player).connect(multiworld.get_region("Traverse Town", player))
multiworld.get_entrance("Wonderland", player).connect(multiworld.get_region("Wonderland", player))
multiworld.get_entrance("Olympus Coliseum", player).connect(multiworld.get_region("Olympus Coliseum", player))
multiworld.get_entrance("Deep Jungle", player).connect(multiworld.get_region("Deep Jungle", player))
multiworld.get_entrance("Agrabah", player).connect(multiworld.get_region("Agrabah", player))
multiworld.get_entrance("Monstro", player).connect(multiworld.get_region("Monstro", player))
multiworld.get_entrance("Atlantica", player).connect(multiworld.get_region("Atlantica", player))
multiworld.get_entrance("Halloween Town", player).connect(multiworld.get_region("Halloween Town", player))
multiworld.get_entrance("Neverland", player).connect(multiworld.get_region("Neverland", player))
multiworld.get_entrance("Hollow Bastion", player).connect(multiworld.get_region("Hollow Bastion", player))
multiworld.get_entrance("End of the World", player).connect(multiworld.get_region("End of the World", player))
multiworld.get_entrance("100 Acre Wood", player).connect(multiworld.get_region("100 Acre Wood", player))
multiworld.get_entrance("World Map", player).connect(multiworld.get_region("World Map", player))
multiworld.get_entrance("Levels", player).connect(multiworld.get_region("Levels", player))
def create_region(multiworld: MultiWorld, player: int, name: str, data: KH1RegionData):
region = Region(name, player, multiworld)
if data.locations:
for loc_name in data.locations:
loc_data = location_table.get(loc_name)
location = KH1Location(player, loc_name, loc_data.code if loc_data else None, region)
region.locations.append(location)
if data.region_exits:
for exit in data.region_exits:
entrance = Entrance(player, exit, region)
region.exits.append(entrance)
return region

1948
worlds/kh1/Rules.py Normal file

File diff suppressed because it is too large Load Diff

282
worlds/kh1/__init__.py Normal file
View File

@@ -0,0 +1,282 @@
import logging
from typing import List
from BaseClasses import Tutorial
from worlds.AutoWorld import WebWorld, World
from .Items import KH1Item, KH1ItemData, event_item_table, get_items_by_category, item_table, item_name_groups
from .Locations import KH1Location, location_table, get_locations_by_category, location_name_groups
from .Options import KH1Options, kh1_option_groups
from .Regions import create_regions
from .Rules import set_rules
from .Presets import kh1_option_presets
from worlds.LauncherComponents import Component, components, Type, launch_subprocess
def launch_client():
from .Client import launch
launch_subprocess(launch, name="KH1 Client")
components.append(Component("KH1 Client", "KH1Client", func=launch_client, component_type=Type.CLIENT))
class KH1Web(WebWorld):
theme = "ocean"
tutorials = [Tutorial(
"Multiworld Setup Guide",
"A guide to setting up the Kingdom Hearts Randomizer software on your computer."
"This guide covers single-player, multiworld, and related software.",
"English",
"kh1_en.md",
"kh1/en",
["Gicu"]
)]
option_groups = kh1_option_groups
options_presets = kh1_option_presets
class KH1World(World):
"""
Kingdom Hearts is an action RPG following Sora on his journey
through many worlds to find Riku and Kairi.
"""
game = "Kingdom Hearts"
options_dataclass = KH1Options
options: KH1Options
topology_present = True
web = KH1Web()
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = {name: data.code for name, data in location_table.items()}
item_name_groups = item_name_groups
location_name_groups = location_name_groups
fillers = {}
fillers.update(get_items_by_category("Item"))
fillers.update(get_items_by_category("Camping"))
fillers.update(get_items_by_category("Stat Ups"))
def create_items(self):
self.place_predetermined_items()
# Handle starting worlds
starting_worlds = []
if self.options.starting_worlds > 0:
possible_starting_worlds = ["Wonderland", "Olympus Coliseum", "Deep Jungle", "Agrabah", "Monstro", "Halloween Town", "Neverland", "Hollow Bastion"]
if self.options.atlantica:
possible_starting_worlds.append("Atlantica")
if self.options.end_of_the_world_unlock == "item":
possible_starting_worlds.append("End of the World")
starting_worlds = self.random.sample(possible_starting_worlds, min(self.options.starting_worlds.value, len(possible_starting_worlds)))
for starting_world in starting_worlds:
self.multiworld.push_precollected(self.create_item(starting_world))
item_pool: List[KH1Item] = []
possible_level_up_item_pool = []
level_up_item_pool = []
# Calculate Level Up Items
# Fill pool with mandatory items
for _ in range(self.options.item_slot_increase):
level_up_item_pool.append("Item Slot Increase")
for _ in range(self.options.accessory_slot_increase):
level_up_item_pool.append("Accessory Slot Increase")
# Create other pool
for _ in range(self.options.strength_increase):
possible_level_up_item_pool.append("Strength Increase")
for _ in range(self.options.defense_increase):
possible_level_up_item_pool.append("Defense Increase")
for _ in range(self.options.hp_increase):
possible_level_up_item_pool.append("Max HP Increase")
for _ in range(self.options.mp_increase):
possible_level_up_item_pool.append("Max MP Increase")
for _ in range(self.options.ap_increase):
possible_level_up_item_pool.append("Max AP Increase")
# Fill remaining pool with items from other pool
self.random.shuffle(possible_level_up_item_pool)
level_up_item_pool = level_up_item_pool + possible_level_up_item_pool[:(100 - len(level_up_item_pool))]
level_up_locations = list(get_locations_by_category("Levels").keys())
self.random.shuffle(level_up_item_pool)
current_level_for_placing_stats = self.options.force_stats_on_levels.value
while len(level_up_item_pool) > 0 and current_level_for_placing_stats <= self.options.level_checks:
self.get_location(level_up_locations[current_level_for_placing_stats - 1]).place_locked_item(self.create_item(level_up_item_pool.pop()))
current_level_for_placing_stats += 1
# Calculate prefilled locations and items
prefilled_items = []
if self.options.vanilla_emblem_pieces:
prefilled_items = prefilled_items + ["Emblem Piece (Flame)", "Emblem Piece (Chest)", "Emblem Piece (Fountain)", "Emblem Piece (Statue)"]
total_locations = len(self.multiworld.get_unfilled_locations(self.player))
non_filler_item_categories = ["Key", "Magic", "Worlds", "Trinities", "Cups", "Summons", "Abilities", "Shared Abilities", "Keyblades", "Accessory", "Weapons", "Puppies"]
if self.options.hundred_acre_wood:
non_filler_item_categories.append("Torn Pages")
for name, data in item_table.items():
quantity = data.max_quantity
if data.category not in non_filler_item_categories:
continue
if name in starting_worlds:
continue
if data.category == "Puppies":
if self.options.puppies == "triplets" and "-" in name:
item_pool += [self.create_item(name) for _ in range(quantity)]
if self.options.puppies == "individual" and "Puppy" in name:
item_pool += [self.create_item(name) for _ in range(0, quantity)]
if self.options.puppies == "full" and name == "All Puppies":
item_pool += [self.create_item(name) for _ in range(0, quantity)]
elif name == "Atlantica":
if self.options.atlantica:
item_pool += [self.create_item(name) for _ in range(0, quantity)]
elif name == "Mermaid Kick":
if self.options.atlantica:
if self.options.extra_shared_abilities:
item_pool += [self.create_item(name) for _ in range(0, 2)]
else:
item_pool += [self.create_item(name) for _ in range(0, quantity)]
elif name == "Crystal Trident":
if self.options.atlantica:
item_pool += [self.create_item(name) for _ in range(0, quantity)]
elif name == "High Jump":
if self.options.extra_shared_abilities:
item_pool += [self.create_item(name) for _ in range(0, 3)]
else:
item_pool += [self.create_item(name) for _ in range(0, quantity)]
elif name == "Progressive Glide":
if self.options.extra_shared_abilities:
item_pool += [self.create_item(name) for _ in range(0, 4)]
else:
item_pool += [self.create_item(name) for _ in range(0, quantity)]
elif name == "End of the World":
if self.options.end_of_the_world_unlock.current_key == "item":
item_pool += [self.create_item(name) for _ in range(0, quantity)]
elif name == "EXP Zero":
if self.options.exp_zero_in_pool:
item_pool += [self.create_item(name) for _ in range(0, quantity)]
elif name not in prefilled_items:
item_pool += [self.create_item(name) for _ in range(0, quantity)]
for i in range(self.determine_reports_in_pool()):
item_pool += [self.create_item("Ansem's Report " + str(i+1))]
while len(item_pool) < total_locations and len(level_up_item_pool) > 0:
item_pool += [self.create_item(level_up_item_pool.pop())]
# Fill any empty locations with filler items.
while len(item_pool) < total_locations:
item_pool.append(self.create_item(self.get_filler_item_name()))
self.multiworld.itempool += item_pool
def place_predetermined_items(self) -> None:
goal_dict = {
"sephiroth": "Olympus Coliseum Defeat Sephiroth Ansem's Report 12",
"unknown": "Hollow Bastion Defeat Unknown Ansem's Report 13",
"postcards": "Traverse Town Mail Postcard 10 Event",
"final_ansem": "Final Ansem",
"puppies": "Traverse Town Piano Room Return 99 Puppies Reward 2",
"final_rest": "End of the World Final Rest Chest"
}
self.get_location(goal_dict[self.options.goal.current_key]).place_locked_item(self.create_item("Victory"))
if self.options.vanilla_emblem_pieces:
self.get_location("Hollow Bastion Entrance Hall Emblem Piece (Flame)").place_locked_item(self.create_item("Emblem Piece (Flame)"))
self.get_location("Hollow Bastion Entrance Hall Emblem Piece (Statue)").place_locked_item(self.create_item("Emblem Piece (Statue)"))
self.get_location("Hollow Bastion Entrance Hall Emblem Piece (Fountain)").place_locked_item(self.create_item("Emblem Piece (Fountain)"))
self.get_location("Hollow Bastion Entrance Hall Emblem Piece (Chest)").place_locked_item(self.create_item("Emblem Piece (Chest)"))
def get_filler_item_name(self) -> str:
weights = [data.weight for data in self.fillers.values()]
return self.random.choices([filler for filler in self.fillers.keys()], weights)[0]
def fill_slot_data(self) -> dict:
slot_data = {"xpmult": int(self.options.exp_multiplier)/16,
"required_reports_eotw": self.determine_reports_required_to_open_end_of_the_world(),
"required_reports_door": self.determine_reports_required_to_open_final_rest_door(),
"door": self.options.final_rest_door.current_key,
"seed": self.multiworld.seed_name,
"advanced_logic": bool(self.options.advanced_logic),
"hundred_acre_wood": bool(self.options.hundred_acre_wood),
"atlantica": bool(self.options.atlantica),
"goal": str(self.options.goal.current_key)}
if self.options.randomize_keyblade_stats:
min_str_bonus = min(self.options.keyblade_min_str.value, self.options.keyblade_max_str.value)
max_str_bonus = max(self.options.keyblade_min_str.value, self.options.keyblade_max_str.value)
self.options.keyblade_min_str.value = min_str_bonus
self.options.keyblade_max_str.value = max_str_bonus
min_mp_bonus = min(self.options.keyblade_min_mp.value, self.options.keyblade_max_mp.value)
max_mp_bonus = max(self.options.keyblade_min_mp.value, self.options.keyblade_max_mp.value)
self.options.keyblade_min_mp.value = min_mp_bonus
self.options.keyblade_max_mp.value = max_mp_bonus
slot_data["keyblade_stats"] = ""
for i in range(22):
if i < 4 and self.options.bad_starting_weapons:
slot_data["keyblade_stats"] = slot_data["keyblade_stats"] + "1,0,"
else:
str_bonus = int(self.random.randint(min_str_bonus, max_str_bonus))
mp_bonus = int(self.random.randint(min_mp_bonus, max_mp_bonus))
slot_data["keyblade_stats"] = slot_data["keyblade_stats"] + str(str_bonus) + "," + str(mp_bonus) + ","
slot_data["keyblade_stats"] = slot_data["keyblade_stats"][:-1]
if self.options.donald_death_link:
slot_data["donalddl"] = ""
if self.options.goofy_death_link:
slot_data["goofydl"] = ""
if self.options.keyblades_unlock_chests:
slot_data["chestslocked"] = ""
else:
slot_data["chestsunlocked"] = ""
if self.options.interact_in_battle:
slot_data["interactinbattle"] = ""
return slot_data
def create_item(self, name: str) -> KH1Item:
data = item_table[name]
return KH1Item(name, data.classification, data.code, self.player)
def create_event(self, name: str) -> KH1Item:
data = event_item_table[name]
return KH1Item(name, data.classification, data.code, self.player)
def set_rules(self):
set_rules(self)
def create_regions(self):
create_regions(self.multiworld, self.player, self.options)
def generate_early(self):
value_names = ["Reports to Open End of the World", "Reports to Open Final Rest Door", "Reports in Pool"]
initial_report_settings = [self.options.required_reports_eotw.value, self.options.required_reports_door.value, self.options.reports_in_pool.value]
self.change_numbers_of_reports_to_consider()
new_report_settings = [self.options.required_reports_eotw.value, self.options.required_reports_door.value, self.options.reports_in_pool.value]
for i in range(3):
if initial_report_settings[i] != new_report_settings[i]:
logging.info(f"{self.player_name}'s value {initial_report_settings[i]} for \"{value_names[i]}\" was invalid\n"
f"Setting \"{value_names[i]}\" value to {new_report_settings[i]}")
def change_numbers_of_reports_to_consider(self) -> None:
if self.options.end_of_the_world_unlock == "reports" and self.options.final_rest_door == "reports":
self.options.required_reports_eotw.value, self.options.required_reports_door.value, self.options.reports_in_pool.value = sorted(
[self.options.required_reports_eotw.value, self.options.required_reports_door.value, self.options.reports_in_pool.value])
elif self.options.end_of_the_world_unlock == "reports":
self.options.required_reports_eotw.value, self.options.reports_in_pool.value = sorted(
[self.options.required_reports_eotw.value, self.options.reports_in_pool.value])
elif self.options.final_rest_door == "reports":
self.options.required_reports_door.value, self.options.reports_in_pool.value = sorted(
[self.options.required_reports_door.value, self.options.reports_in_pool.value])
def determine_reports_in_pool(self) -> int:
if self.options.end_of_the_world_unlock == "reports" or self.options.final_rest_door == "reports":
return self.options.reports_in_pool.value
return 0
def determine_reports_required_to_open_end_of_the_world(self) -> int:
if self.options.end_of_the_world_unlock == "reports":
return self.options.required_reports_eotw.value
return 14
def determine_reports_required_to_open_final_rest_door(self) -> int:
if self.options.final_rest_door == "reports":
return self.options.required_reports_door.value
return 14

View File

@@ -0,0 +1,88 @@
# Kingdom Hearts (PC)
## Where is the options page?
The [player options page for this game](../player-options) contains most of the options you need to
configure and export a config file.
## What does randomization do to this game?
The Kingdom Hearts AP Randomizer randomizes most rewards in the game and adds several items which are used to unlock worlds, Olympus Coliseum cups, and world progression.
Worlds can only be accessed by finding the corresponding item. For example, you need to find the `Monstro` item to enter Monstro.
The default goal is to enter End of the World and defeat Final Ansem.
## What items and locations get shuffled?
### Items
Any weapon, accessory, spell, trinity, summon, world, key item, stat up, consumable, or ability can be found in any location.
### Locations
Locations the player can find items include chests, event rewards, Atlantica clams, level up rewards, 101 Dalmatian rewards, and postcard rewards.
## Which items can be in another player's world?
Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to limit
certain items to your own world.
## When the player receives an item, what happens?
When the player receives an item, your client will display a message displaying the item you have obtained. You will also see a notification in the "LEVEL UP" box.
## What do I do if I encounter a bug with the game?
Please reach out to Gicu#7034 on Discord.
## How do I progress in a certain world?
### The evidence boxes aren't spawning in Wonderland.
Find `Footprints` in the multiworld.
### I can't enter any cups in Olympus Coliseum.
Firstly, find `Entry Pass` in the multiworld. Additionally, `Phil Cup`, `Pegasus Cup`, and `Hercules Cup` are all multiworld items. Finding all 3 grant you access to the Hades Cup and the Platinum Match. Clearing all cups lets you challenge Ice Titan.
### The slides aren't spawning in Deep Jungle.
Find `Slides` in the multiworld.
### I can't progress in Atlantica.
Find `Crystal Trident` in the multiworld.
### I can't progress in Halloween Town.
Find `Forget-Me-Not` and `Jack-in-the-Box` in the multiworld.
### The Hollow Bastion Library is missing a book.
Find `Theon Vol. 6` in the multiworld.
## How do I enter the End of the World?
You can enter End of the World by obtaining a number of Ansem's Reports or by finding `End of the World` in the multiworld, depending on your options.
## Credits
This is a collaborative effort from several individuals in the Kingdom Hearts community, but most of all, denhonator.
Denho's original KH rando laid the foundation for the work here and makes everything here possible, so thank you Denho for such a blast of a randomizer.
Other credits include:
Sonicshadowsilver2 for their work finding many memory addresses, working to identify and resolve bugs, and converting the code base to the latest EGS update.
Shananas and the rest of the OpenKH team for providing such an amazing tool for us to utilize on this project.
TopazTK for their work on the `Show Prompt` method and Krujo for their implementation of the method in AP.
JaredWeakStrike for helping clean up my mess of code.
KSX for their `Interact in Battle` code.
RavSpect for their title screen image edit.
SunCatMC for their work on ChecksFinder, which I used as a basis for game-to-client communication.
ThePhar for their work on Rogue Legacy AP, which I used as a basis for the apworld creation.

54
worlds/kh1/docs/kh1_en.md Normal file
View File

@@ -0,0 +1,54 @@
# Kingdom Hearts Randomizer Setup Guide
## Setting up the required mods
BEFORE MODDING, PLEASE INSTALL AND RUN KH1 AT LEAST ONCE.
1. Install OpenKH and the LUA Backend
Download the [latest release of OpenKH](https://github.com/OpenKH/OpenKh/releases/tag/latest)
Extract the files to a directory of your choosing.
Open `OpenKh.Tools.ModsManager.exe` and run first time set up
When prompted for game edition, choose `PC Release`, select which platform you're using (EGS or Steam), navigate to your `Kingdom Hearts I.5 + II.5` installation folder in the path box and click `Next`
When prompted, install Panacea, then click `Next`
When prompted, check KH1 plus any other AP game you play and click `Install and configure LUA backend`, then click `Next`
Extracting game data for KH1 is unnecessary, but you may want to extract data for KH2 if you plan on playing KH2 AP
Click `Finish`
2. Open `OpenKh.Tools.ModsManager.exe`
3. Click the drop-down menu at the top-right and choose `Kingdom Hearts 1`
4. Click `Mods>Install a New Mod`
5. In `Add a new mod from GitHub` paste `gaithern/KH-1FM-AP-LUA`
6. Click `Install`
7. Navigate to Mod Loader and click `Build and Run`
## Configuring your YAML file
### What is a YAML file and why do I need one?
Your YAML file contains a set of configuration options which provide the generator with information about how it should
generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy
an experience customized for their taste, and different players in the same multiworld can all have different options.
### Where do I get a YAML file?
you can customize your settings by visiting the [Kingdom Hearts Options Page](/games/Kingdom%20Hearts/player-options).
## Connect to the MultiWorld
For first-time players, it is recommended to open your KH1 Client first before opening the game.
On the title screen, open your KH1 Client and connect to your multiworld.

View File

@@ -0,0 +1,5 @@
from test.bases import WorldTestBase
class KH1TestBase(WorldTestBase):
game = "Kingdom Hearts"

View File

@@ -0,0 +1,33 @@
from . import KH1TestBase
class TestDefault(KH1TestBase):
options = {}
class TestSephiroth(KH1TestBase):
options = {
"Goal": 0,
}
class TestUnknown(KH1TestBase):
options = {
"Goal": 1,
}
class TestPostcards(KH1TestBase):
options = {
"Goal": 2,
}
class TestFinalAnsem(KH1TestBase):
options = {
"Goal": 3,
}
class TestPuppies(KH1TestBase):
options = {
"Goal": 4,
}
class TestFinalRest(KH1TestBase):
options = {
"Goal": 5,
}

View File

@@ -280,6 +280,8 @@ def generateRom(args, world: "LinksAwakeningWorld"):
name = "Your"
else:
name = f"{world.multiworld.player_name[location.item.player]}'s"
# filter out { and } since they cause issues with string.format later on
name = name.replace("{", "").replace("}", "")
if isinstance(location, LinksAwakeningLocation):
location_name = location.ladxr_item.metadata.name
@@ -288,7 +290,9 @@ def generateRom(args, world: "LinksAwakeningWorld"):
hint = f"{name} {location.item} is at {location_name}"
if location.player != world.player:
hint += f" in {world.multiworld.player_name[location.player]}'s world"
# filter out { and } since they cause issues with string.format later on
player_name = world.multiworld.player_name[location.player].replace("{", "").replace("}", "")
hint += f" in {player_name}'s world"
# Cap hint size at 85
# Realistically we could go bigger but let's be safe instead

View File

@@ -18,7 +18,8 @@ class ShopItem(ItemInfo):
mw_text = ""
if multiworld:
mw_text = f" for player {rom.player_names[multiworld - 1].encode('ascii', 'replace').decode()}"
# filter out { and } since they cause issues with string.format later on
mw_text = mw_text.replace("{", "").replace("}", "")
if self.custom_item_name:
name = self.custom_item_name

View File

@@ -85,7 +85,7 @@ class LingoWorld(World):
state.collect(self.create_item(self.player_logic.forced_good_item), True)
all_locations = self.multiworld.get_locations(self.player)
state.sweep_for_events(locations=all_locations)
state.sweep_for_advancements(locations=all_locations)
unreachable_locations = [location for location in all_locations
if not state.can_reach_location(location.name, self.player)]

View File

@@ -1,10 +1,14 @@
from math import ceil
from typing import List
from BaseClasses import MultiWorld, Item
from worlds.AutoWorld import World
from BaseClasses import Item
from . import Constants
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from . import MinecraftWorld
def get_junk_item_names(rand, k: int) -> str:
junk_weights = Constants.item_info["junk_weights"]
@@ -14,39 +18,38 @@ def get_junk_item_names(rand, k: int) -> str:
k=k)
return junk
def build_item_pool(mc_world: World) -> List[Item]:
multiworld = mc_world.multiworld
player = mc_world.player
def build_item_pool(world: "MinecraftWorld") -> List[Item]:
multiworld = world.multiworld
player = world.player
itempool = []
total_location_count = len(multiworld.get_unfilled_locations(player))
required_pool = Constants.item_info["required_pool"]
junk_weights = Constants.item_info["junk_weights"]
# Add required progression items
for item_name, num in required_pool.items():
itempool += [mc_world.create_item(item_name) for _ in range(num)]
itempool += [world.create_item(item_name) for _ in range(num)]
# Add structure compasses
if multiworld.structure_compasses[player]:
compasses = [name for name in mc_world.item_name_to_id if "Structure Compass" in name]
if world.options.structure_compasses:
compasses = [name for name in world.item_name_to_id if "Structure Compass" in name]
for item_name in compasses:
itempool.append(mc_world.create_item(item_name))
itempool.append(world.create_item(item_name))
# Dragon egg shards
if multiworld.egg_shards_required[player] > 0:
num = multiworld.egg_shards_available[player]
itempool += [mc_world.create_item("Dragon Egg Shard") for _ in range(num)]
if world.options.egg_shards_required > 0:
num = world.options.egg_shards_available
itempool += [world.create_item("Dragon Egg Shard") for _ in range(num)]
# Bee traps
bee_trap_percentage = multiworld.bee_traps[player] * 0.01
bee_trap_percentage = world.options.bee_traps * 0.01
if bee_trap_percentage > 0:
bee_trap_qty = ceil(bee_trap_percentage * (total_location_count - len(itempool)))
itempool += [mc_world.create_item("Bee Trap") for _ in range(bee_trap_qty)]
itempool += [world.create_item("Bee Trap") for _ in range(bee_trap_qty)]
# Fill remaining itempool with randomly generated junk
junk = get_junk_item_names(multiworld.random, total_location_count - len(itempool))
itempool += [mc_world.create_item(name) for name in junk]
junk = get_junk_item_names(world.random, total_location_count - len(itempool))
itempool += [world.create_item(name) for name in junk]
return itempool

View File

@@ -1,6 +1,7 @@
import typing
from Options import Choice, Option, Toggle, DefaultOnToggle, Range, OptionList, DeathLink, PlandoConnections
from Options import Choice, Toggle, DefaultOnToggle, Range, OptionList, DeathLink, PlandoConnections, \
PerGameCommonOptions
from .Constants import region_info
from dataclasses import dataclass
class AdvancementGoal(Range):
@@ -55,7 +56,7 @@ class StructureCompasses(DefaultOnToggle):
display_name = "Structure Compasses"
class BeeTraps(Range):
class BeeTraps(Range):
"""Replaces a percentage of junk items with bee traps, which spawn multiple angered bees around every player when
received."""
display_name = "Bee Trap Percentage"
@@ -94,7 +95,20 @@ class SendDefeatedMobs(Toggle):
class StartingItems(OptionList):
"""Start with these items. Each entry should be of this format: {item: "item_name", amount: #, nbt: "nbt_string"}"""
"""Start with these items. Each entry should be of this format: {item: "item_name", amount: #}
`item` can include components, and should be in an identical format to a `/give` command with
`"` escaped for json reasons.
`amount` is optional and will default to 1 if omitted.
example:
```
starting_items: [
{ "item": "minecraft:stick[minecraft:custom_name=\"{'text':'pointy stick'}\"]" },
{ "item": "minecraft:arrow[minecraft:rarity=epic]", amount: 64 }
]
```
"""
display_name = "Starting Items"
@@ -109,22 +123,21 @@ class MCPlandoConnections(PlandoConnections):
return True
minecraft_options: typing.Dict[str, type(Option)] = {
"plando_connections": MCPlandoConnections,
"advancement_goal": AdvancementGoal,
"egg_shards_required": EggShardsRequired,
"egg_shards_available": EggShardsAvailable,
"required_bosses": BossGoal,
@dataclass
class MinecraftOptions(PerGameCommonOptions):
plando_connections: MCPlandoConnections
advancement_goal: AdvancementGoal
egg_shards_required: EggShardsRequired
egg_shards_available: EggShardsAvailable
required_bosses: BossGoal
shuffle_structures: ShuffleStructures
structure_compasses: StructureCompasses
"shuffle_structures": ShuffleStructures,
"structure_compasses": StructureCompasses,
"combat_difficulty": CombatDifficulty,
"include_hard_advancements": HardAdvancements,
"include_unreasonable_advancements": UnreasonableAdvancements,
"include_postgame_advancements": PostgameAdvancements,
"bee_traps": BeeTraps,
"send_defeated_mobs": SendDefeatedMobs,
"death_link": DeathLink,
"starting_items": StartingItems,
}
combat_difficulty: CombatDifficulty
include_hard_advancements: HardAdvancements
include_unreasonable_advancements: UnreasonableAdvancements
include_postgame_advancements: PostgameAdvancements
bee_traps: BeeTraps
send_defeated_mobs: SendDefeatedMobs
death_link: DeathLink
starting_items: StartingItems

View File

@@ -1,276 +1,471 @@
import typing
from collections.abc import Callable
from BaseClasses import CollectionState
from worlds.generic.Rules import exclusion_rules
from worlds.AutoWorld import World
from . import Constants
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from . import MinecraftWorld
# Helper functions
# moved from logicmixin
def has_iron_ingots(state: CollectionState, player: int) -> bool:
def has_iron_ingots(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
return state.has('Progressive Tools', player) and state.has('Progressive Resource Crafting', player)
def has_copper_ingots(state: CollectionState, player: int) -> bool:
def has_copper_ingots(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
return state.has('Progressive Tools', player) and state.has('Progressive Resource Crafting', player)
def has_gold_ingots(state: CollectionState, player: int) -> bool:
return state.has('Progressive Resource Crafting', player) and (state.has('Progressive Tools', player, 2) or state.can_reach('The Nether', 'Region', player))
def has_diamond_pickaxe(state: CollectionState, player: int) -> bool:
return state.has('Progressive Tools', player, 3) and has_iron_ingots(state, player)
def has_gold_ingots(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
return (state.has('Progressive Resource Crafting', player)
and (
state.has('Progressive Tools', player, 2)
or state.can_reach_region('The Nether', player)
)
)
def craft_crossbow(state: CollectionState, player: int) -> bool:
return state.has('Archery', player) and has_iron_ingots(state, player)
def has_bottle(state: CollectionState, player: int) -> bool:
def has_diamond_pickaxe(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
return state.has('Progressive Tools', player, 3) and has_iron_ingots(world, state, player)
def craft_crossbow(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
return state.has('Archery', player) and has_iron_ingots(world, state, player)
def has_bottle(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
return state.has('Bottles', player) and state.has('Progressive Resource Crafting', player)
def has_spyglass(state: CollectionState, player: int) -> bool:
return has_copper_ingots(state, player) and state.has('Spyglass', player) and can_adventure(state, player)
def can_enchant(state: CollectionState, player: int) -> bool:
return state.has('Enchanting', player) and has_diamond_pickaxe(state, player) # mine obsidian and lapis
def has_spyglass(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
return (has_copper_ingots(world, state, player)
and state.has('Spyglass', player)
and can_adventure(world, state, player)
)
def can_use_anvil(state: CollectionState, player: int) -> bool:
return state.has('Enchanting', player) and state.has('Progressive Resource Crafting', player, 2) and has_iron_ingots(state, player)
def fortress_loot(state: CollectionState, player: int) -> bool: # saddles, blaze rods, wither skulls
return state.can_reach('Nether Fortress', 'Region', player) and basic_combat(state, player)
def can_enchant(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
return state.has('Enchanting', player) and has_diamond_pickaxe(world, state, player) # mine obsidian and lapis
def can_brew_potions(state: CollectionState, player: int) -> bool:
return state.has('Blaze Rods', player) and state.has('Brewing', player) and has_bottle(state, player)
def can_piglin_trade(state: CollectionState, player: int) -> bool:
return has_gold_ingots(state, player) and (
state.can_reach('The Nether', 'Region', player) or
state.can_reach('Bastion Remnant', 'Region', player))
def can_use_anvil(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
return (state.has('Enchanting', player)
and state.has('Progressive Resource Crafting', player,2)
and has_iron_ingots(world, state, player)
)
def overworld_villager(state: CollectionState, player: int) -> bool:
def fortress_loot(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: # saddles, blaze rods, wither skulls
return state.can_reach_region('Nether Fortress', player) and basic_combat(world, state, player)
def can_brew_potions(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
return state.has('Blaze Rods', player) and state.has('Brewing', player) and has_bottle(world, state, player)
def can_piglin_trade(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
return (has_gold_ingots(world, state, player)
and (
state.can_reach_region('The Nether', player)
or state.can_reach_region('Bastion Remnant', player)
))
def overworld_villager(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
village_region = state.multiworld.get_region('Village', player).entrances[0].parent_region.name
if village_region == 'The Nether': # 2 options: cure zombie villager or build portal in village
return (state.can_reach('Zombie Doctor', 'Location', player) or
(has_diamond_pickaxe(state, player) and state.can_reach('Village', 'Region', player)))
if village_region == 'The Nether': # 2 options: cure zombie villager or build portal in village
return (state.can_reach_location('Zombie Doctor', player)
or (
has_diamond_pickaxe(world, state, player)
and state.can_reach_region('Village', player)
))
elif village_region == 'The End':
return state.can_reach('Zombie Doctor', 'Location', player)
return state.can_reach('Village', 'Region', player)
return state.can_reach_location('Zombie Doctor', player)
return state.can_reach_region('Village', player)
def enter_stronghold(state: CollectionState, player: int) -> bool:
def enter_stronghold(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
return state.has('Blaze Rods', player) and state.has('Brewing', player) and state.has('3 Ender Pearls', player)
# Difficulty-dependent functions
def combat_difficulty(state: CollectionState, player: int) -> bool:
return state.multiworld.combat_difficulty[player].current_key
def combat_difficulty(world: "MinecraftWorld", state: CollectionState, player: int) -> str:
return world.options.combat_difficulty.current_key
def can_adventure(state: CollectionState, player: int) -> bool:
death_link_check = not state.multiworld.death_link[player] or state.has('Bed', player)
if combat_difficulty(state, player) == 'easy':
return state.has('Progressive Weapons', player, 2) and has_iron_ingots(state, player) and death_link_check
elif combat_difficulty(state, player) == 'hard':
def can_adventure(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
death_link_check = not world.options.death_link or state.has('Bed', player)
if combat_difficulty(world, state, player) == 'easy':
return state.has('Progressive Weapons', player, 2) and has_iron_ingots(world, state, player) and death_link_check
elif combat_difficulty(world, state, player) == 'hard':
return True
return (state.has('Progressive Weapons', player) and death_link_check and
(state.has('Progressive Resource Crafting', player) or state.has('Campfire', player)))
return (state.has('Progressive Weapons', player) and death_link_check and
(state.has('Progressive Resource Crafting', player) or state.has('Campfire', player)))
def basic_combat(state: CollectionState, player: int) -> bool:
if combat_difficulty(state, player) == 'easy':
return state.has('Progressive Weapons', player, 2) and state.has('Progressive Armor', player) and \
state.has('Shield', player) and has_iron_ingots(state, player)
elif combat_difficulty(state, player) == 'hard':
def basic_combat(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
if combat_difficulty(world, state, player) == 'easy':
return (state.has('Progressive Weapons', player, 2)
and state.has('Progressive Armor', player)
and state.has('Shield', player)
and has_iron_ingots(world, state, player)
)
elif combat_difficulty(world, state, player) == 'hard':
return True
return state.has('Progressive Weapons', player) and (state.has('Progressive Armor', player) or state.has('Shield', player)) and has_iron_ingots(state, player)
return (state.has('Progressive Weapons', player)
and (
state.has('Progressive Armor', player)
or state.has('Shield', player)
)
and has_iron_ingots(world, state, player)
)
def complete_raid(state: CollectionState, player: int) -> bool:
reach_regions = state.can_reach('Village', 'Region', player) and state.can_reach('Pillager Outpost', 'Region', player)
if combat_difficulty(state, player) == 'easy':
return reach_regions and \
state.has('Progressive Weapons', player, 3) and state.has('Progressive Armor', player, 2) and \
state.has('Shield', player) and state.has('Archery', player) and \
state.has('Progressive Tools', player, 2) and has_iron_ingots(state, player)
elif combat_difficulty(state, player) == 'hard': # might be too hard?
return reach_regions and state.has('Progressive Weapons', player, 2) and has_iron_ingots(state, player) and \
(state.has('Progressive Armor', player) or state.has('Shield', player))
return reach_regions and state.has('Progressive Weapons', player, 2) and has_iron_ingots(state, player) and \
state.has('Progressive Armor', player) and state.has('Shield', player)
def can_kill_wither(state: CollectionState, player: int) -> bool:
normal_kill = state.has("Progressive Weapons", player, 3) and state.has("Progressive Armor", player, 2) and can_brew_potions(state, player) and can_enchant(state, player)
if combat_difficulty(state, player) == 'easy':
return fortress_loot(state, player) and normal_kill and state.has('Archery', player)
elif combat_difficulty(state, player) == 'hard': # cheese kill using bedrock ceilings
return fortress_loot(state, player) and (normal_kill or state.can_reach('The Nether', 'Region', player) or state.can_reach('The End', 'Region', player))
return fortress_loot(state, player) and normal_kill
def complete_raid(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
reach_regions = (state.can_reach_region('Village', player)
and state.can_reach_region('Pillager Outpost', player))
if combat_difficulty(world, state, player) == 'easy':
return (reach_regions
and state.has('Progressive Weapons', player, 3)
and state.has('Progressive Armor', player, 2)
and state.has('Shield', player)
and state.has('Archery', player)
and state.has('Progressive Tools', player, 2)
and has_iron_ingots(world, state, player)
)
elif combat_difficulty(world, state, player) == 'hard': # might be too hard?
return (reach_regions
and state.has('Progressive Weapons', player, 2)
and has_iron_ingots(world, state, player)
and (
state.has('Progressive Armor', player)
or state.has('Shield', player)
)
)
return (reach_regions
and state.has('Progressive Weapons', player, 2)
and has_iron_ingots(world, state, player)
and state.has('Progressive Armor', player)
and state.has('Shield', player)
)
def can_respawn_ender_dragon(state: CollectionState, player: int) -> bool:
return state.can_reach('The Nether', 'Region', player) and state.can_reach('The End', 'Region', player) and \
state.has('Progressive Resource Crafting', player) # smelt sand into glass
def can_kill_ender_dragon(state: CollectionState, player: int) -> bool:
if combat_difficulty(state, player) == 'easy':
return state.has("Progressive Weapons", player, 3) and state.has("Progressive Armor", player, 2) and \
state.has('Archery', player) and can_brew_potions(state, player) and can_enchant(state, player)
if combat_difficulty(state, player) == 'hard':
return (state.has('Progressive Weapons', player, 2) and state.has('Progressive Armor', player)) or \
(state.has('Progressive Weapons', player, 1) and state.has('Bed', player))
return state.has('Progressive Weapons', player, 2) and state.has('Progressive Armor', player) and state.has('Archery', player)
def can_kill_wither(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
normal_kill = (state.has("Progressive Weapons", player, 3)
and state.has("Progressive Armor", player, 2)
and can_brew_potions(world, state, player)
and can_enchant(world, state, player)
)
if combat_difficulty(world, state, player) == 'easy':
return (fortress_loot(world, state, player)
and normal_kill
and state.has('Archery', player)
)
elif combat_difficulty(world, state, player) == 'hard': # cheese kill using bedrock ceilings
return (fortress_loot(world, state, player)
and (
normal_kill
or state.can_reach_region('The Nether', player)
or state.can_reach_region('The End', player)
)
)
def has_structure_compass(state: CollectionState, entrance_name: str, player: int) -> bool:
if not state.multiworld.structure_compasses[player]:
return fortress_loot(world, state, player) and normal_kill
def can_respawn_ender_dragon(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
return (state.can_reach_region('The Nether', player)
and state.can_reach_region('The End', player)
and state.has('Progressive Resource Crafting', player) # smelt sand into glass
)
def can_kill_ender_dragon(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
if combat_difficulty(world, state, player) == 'easy':
return (state.has("Progressive Weapons", player, 3)
and state.has("Progressive Armor", player, 2)
and state.has('Archery', player)
and can_brew_potions(world, state, player)
and can_enchant(world, state, player)
)
if combat_difficulty(world, state, player) == 'hard':
return (
(
state.has('Progressive Weapons', player, 2)
and state.has('Progressive Armor', player)
) or (
state.has('Progressive Weapons', player, 1)
and state.has('Bed', player) # who needs armor when you can respawn right outside the chamber
)
)
return (state.has('Progressive Weapons', player, 2)
and state.has('Progressive Armor', player)
and state.has('Archery', player)
)
def has_structure_compass(world: "MinecraftWorld", state: CollectionState, entrance_name: str, player: int) -> bool:
if not world.options.structure_compasses:
return True
return state.has(f"Structure Compass ({state.multiworld.get_entrance(entrance_name, player).connected_region.name})", player)
def get_rules_lookup(player: int):
rules_lookup: typing.Dict[str, typing.List[Callable[[CollectionState], bool]]] = {
def get_rules_lookup(world, player: int):
rules_lookup = {
"entrances": {
"Nether Portal": lambda state: (state.has('Flint and Steel', player) and
(state.has('Bucket', player) or state.has('Progressive Tools', player, 3)) and
has_iron_ingots(state, player)),
"End Portal": lambda state: enter_stronghold(state, player) and state.has('3 Ender Pearls', player, 4),
"Overworld Structure 1": lambda state: (can_adventure(state, player) and has_structure_compass(state, "Overworld Structure 1", player)),
"Overworld Structure 2": lambda state: (can_adventure(state, player) and has_structure_compass(state, "Overworld Structure 2", player)),
"Nether Structure 1": lambda state: (can_adventure(state, player) and has_structure_compass(state, "Nether Structure 1", player)),
"Nether Structure 2": lambda state: (can_adventure(state, player) and has_structure_compass(state, "Nether Structure 2", player)),
"The End Structure": lambda state: (can_adventure(state, player) and has_structure_compass(state, "The End Structure", player)),
"Nether Portal": lambda state: state.has('Flint and Steel', player)
and (
state.has('Bucket', player)
or state.has('Progressive Tools', player, 3)
)
and has_iron_ingots(world, state, player),
"End Portal": lambda state: enter_stronghold(world, state, player)
and state.has('3 Ender Pearls', player, 4),
"Overworld Structure 1": lambda state: can_adventure(world, state, player)
and has_structure_compass(world, state, "Overworld Structure 1", player),
"Overworld Structure 2": lambda state: can_adventure(world, state, player)
and has_structure_compass(world, state, "Overworld Structure 2", player),
"Nether Structure 1": lambda state: can_adventure(world, state, player)
and has_structure_compass(world, state, "Nether Structure 1", player),
"Nether Structure 2": lambda state: can_adventure(world, state, player)
and has_structure_compass(world, state, "Nether Structure 2", player),
"The End Structure": lambda state: can_adventure(world, state, player)
and has_structure_compass(world, state, "The End Structure", player),
},
"locations": {
"Ender Dragon": lambda state: can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player),
"Wither": lambda state: can_kill_wither(state, player),
"Blaze Rods": lambda state: fortress_loot(state, player),
"Who is Cutting Onions?": lambda state: can_piglin_trade(state, player),
"Oh Shiny": lambda state: can_piglin_trade(state, player),
"Suit Up": lambda state: state.has("Progressive Armor", player) and has_iron_ingots(state, player),
"Very Very Frightening": lambda state: (state.has("Channeling Book", player) and
can_use_anvil(state, player) and can_enchant(state, player) and overworld_villager(state, player)),
"Hot Stuff": lambda state: state.has("Bucket", player) and has_iron_ingots(state, player),
"Free the End": lambda state: can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player),
"A Furious Cocktail": lambda state: (can_brew_potions(state, player) and
state.has("Fishing Rod", player) and # Water Breathing
state.can_reach("The Nether", "Region", player) and # Regeneration, Fire Resistance, gold nuggets
state.can_reach("Village", "Region", player) and # Night Vision, Invisibility
state.can_reach("Bring Home the Beacon", "Location", player)), # Resistance
"Bring Home the Beacon": lambda state: (can_kill_wither(state, player) and
has_diamond_pickaxe(state, player) and state.has("Progressive Resource Crafting", player, 2)),
"Not Today, Thank You": lambda state: state.has("Shield", player) and has_iron_ingots(state, player),
"Isn't It Iron Pick": lambda state: state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player),
"Local Brewery": lambda state: can_brew_potions(state, player),
"The Next Generation": lambda state: can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player),
"Ender Dragon": lambda state: can_respawn_ender_dragon(world, state, player)
and can_kill_ender_dragon(world, state, player),
"Wither": lambda state: can_kill_wither(world, state, player),
"Blaze Rods": lambda state: fortress_loot(world, state, player),
"Who is Cutting Onions?": lambda state: can_piglin_trade(world, state, player),
"Oh Shiny": lambda state: can_piglin_trade(world, state, player),
"Suit Up": lambda state: state.has("Progressive Armor", player)
and has_iron_ingots(world, state, player),
"Very Very Frightening": lambda state: state.has("Channeling Book", player)
and can_use_anvil(world, state, player)
and can_enchant(world, state, player)
and overworld_villager(world, state, player),
"Hot Stuff": lambda state: state.has("Bucket", player)
and has_iron_ingots(world, state, player),
"Free the End": lambda state: can_respawn_ender_dragon(world, state, player)
and can_kill_ender_dragon(world, state, player),
"A Furious Cocktail": lambda state: (can_brew_potions(world, state, player)
and state.has("Fishing Rod", player) # Water Breathing
and state.can_reach_region("The Nether", player) # Regeneration, Fire Resistance, gold nuggets
and state.can_reach_region("Village", player) # Night Vision, Invisibility
and state.can_reach_location("Bring Home the Beacon", player)),
# Resistance
"Bring Home the Beacon": lambda state: can_kill_wither(world, state, player)
and has_diamond_pickaxe(world, state, player)
and state.has("Progressive Resource Crafting", player, 2),
"Not Today, Thank You": lambda state: state.has("Shield", player)
and has_iron_ingots(world, state, player),
"Isn't It Iron Pick": lambda state: state.has("Progressive Tools", player, 2)
and has_iron_ingots(world, state, player),
"Local Brewery": lambda state: can_brew_potions(world, state, player),
"The Next Generation": lambda state: can_respawn_ender_dragon(world, state, player)
and can_kill_ender_dragon(world, state, player),
"Fishy Business": lambda state: state.has("Fishing Rod", player),
"This Boat Has Legs": lambda state: ((fortress_loot(state, player) or complete_raid(state, player)) and
state.has("Saddle", player) and state.has("Fishing Rod", player)),
"This Boat Has Legs": lambda state: (
fortress_loot(world, state, player)
or complete_raid(world, state, player)
)
and state.has("Saddle", player)
and state.has("Fishing Rod", player),
"Sniper Duel": lambda state: state.has("Archery", player),
"Great View From Up Here": lambda state: basic_combat(state, player),
"How Did We Get Here?": lambda state: (can_brew_potions(state, player) and
has_gold_ingots(state, player) and # Absorption
state.can_reach('End City', 'Region', player) and # Levitation
state.can_reach('The Nether', 'Region', player) and # potion ingredients
state.has("Fishing Rod", player) and state.has("Archery",player) and # Pufferfish, Nautilus Shells; spectral arrows
state.can_reach("Bring Home the Beacon", "Location", player) and # Haste
state.can_reach("Hero of the Village", "Location", player)), # Bad Omen, Hero of the Village
"Bullseye": lambda state: (state.has("Archery", player) and state.has("Progressive Tools", player, 2) and
has_iron_ingots(state, player)),
"Spooky Scary Skeleton": lambda state: basic_combat(state, player),
"Two by Two": lambda state: has_iron_ingots(state, player) and state.has("Bucket", player) and can_adventure(state, player),
"Two Birds, One Arrow": lambda state: craft_crossbow(state, player) and can_enchant(state, player),
"Who's the Pillager Now?": lambda state: craft_crossbow(state, player),
"Great View From Up Here": lambda state: basic_combat(world, state, player),
"How Did We Get Here?": lambda state: (can_brew_potions(world, state, player)
and has_gold_ingots(world, state, player) # Absorption
and state.can_reach_region('End City', player) # Levitation
and state.can_reach_region('The Nether', player) # potion ingredients
and state.has("Fishing Rod", player) # Pufferfish, Nautilus Shells; spectral arrows
and state.has("Archery", player)
and state.can_reach_location("Bring Home the Beacon", player) # Haste
and state.can_reach_location("Hero of the Village", player)), # Bad Omen, Hero of the Village
"Bullseye": lambda state: state.has("Archery", player)
and state.has("Progressive Tools", player, 2)
and has_iron_ingots(world, state, player),
"Spooky Scary Skeleton": lambda state: basic_combat(world, state, player),
"Two by Two": lambda state: has_iron_ingots(world, state, player)
and state.has("Bucket", player)
and can_adventure(world, state, player),
"Two Birds, One Arrow": lambda state: craft_crossbow(world, state, player)
and can_enchant(world, state, player),
"Who's the Pillager Now?": lambda state: craft_crossbow(world, state, player),
"Getting an Upgrade": lambda state: state.has("Progressive Tools", player),
"Tactical Fishing": lambda state: state.has("Bucket", player) and has_iron_ingots(state, player),
"Zombie Doctor": lambda state: can_brew_potions(state, player) and has_gold_ingots(state, player),
"Ice Bucket Challenge": lambda state: has_diamond_pickaxe(state, player),
"Into Fire": lambda state: basic_combat(state, player),
"War Pigs": lambda state: basic_combat(state, player),
"Tactical Fishing": lambda state: state.has("Bucket", player)
and has_iron_ingots(world, state, player),
"Zombie Doctor": lambda state: can_brew_potions(world, state, player)
and has_gold_ingots(world, state, player),
"Ice Bucket Challenge": lambda state: has_diamond_pickaxe(world, state, player),
"Into Fire": lambda state: basic_combat(world, state, player),
"War Pigs": lambda state: basic_combat(world, state, player),
"Take Aim": lambda state: state.has("Archery", player),
"Total Beelocation": lambda state: state.has("Silk Touch Book", player) and can_use_anvil(state, player) and can_enchant(state, player),
"Arbalistic": lambda state: (craft_crossbow(state, player) and state.has("Piercing IV Book", player) and
can_use_anvil(state, player) and can_enchant(state, player)),
"The End... Again...": lambda state: can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player),
"Acquire Hardware": lambda state: has_iron_ingots(state, player),
"Not Quite \"Nine\" Lives": lambda state: can_piglin_trade(state, player) and state.has("Progressive Resource Crafting", player, 2),
"Cover Me With Diamonds": lambda state: (state.has("Progressive Armor", player, 2) and
state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player)),
"Sky's the Limit": lambda state: basic_combat(state, player),
"Hired Help": lambda state: state.has("Progressive Resource Crafting", player, 2) and has_iron_ingots(state, player),
"Sweet Dreams": lambda state: state.has("Bed", player) or state.can_reach('Village', 'Region', player),
"You Need a Mint": lambda state: can_respawn_ender_dragon(state, player) and has_bottle(state, player),
"Monsters Hunted": lambda state: (can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player) and
can_kill_wither(state, player) and state.has("Fishing Rod", player)),
"Enchanter": lambda state: can_enchant(state, player),
"Voluntary Exile": lambda state: basic_combat(state, player),
"Eye Spy": lambda state: enter_stronghold(state, player),
"Serious Dedication": lambda state: (can_brew_potions(state, player) and state.has("Bed", player) and
has_diamond_pickaxe(state, player) and has_gold_ingots(state, player)),
"Postmortal": lambda state: complete_raid(state, player),
"Adventuring Time": lambda state: can_adventure(state, player),
"Hero of the Village": lambda state: complete_raid(state, player),
"Hidden in the Depths": lambda state: can_brew_potions(state, player) and state.has("Bed", player) and has_diamond_pickaxe(state, player),
"Beaconator": lambda state: (can_kill_wither(state, player) and has_diamond_pickaxe(state, player) and
state.has("Progressive Resource Crafting", player, 2)),
"Withering Heights": lambda state: can_kill_wither(state, player),
"A Balanced Diet": lambda state: (has_bottle(state, player) and has_gold_ingots(state, player) and # honey bottle; gapple
state.has("Progressive Resource Crafting", player, 2) and state.can_reach('The End', 'Region', player)), # notch apple, chorus fruit
"Subspace Bubble": lambda state: has_diamond_pickaxe(state, player),
"Country Lode, Take Me Home": lambda state: state.can_reach("Hidden in the Depths", "Location", player) and has_gold_ingots(state, player),
"Bee Our Guest": lambda state: state.has("Campfire", player) and has_bottle(state, player),
"Uneasy Alliance": lambda state: has_diamond_pickaxe(state, player) and state.has('Fishing Rod', player),
"Diamonds!": lambda state: state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player),
"A Throwaway Joke": lambda state: can_adventure(state, player),
"Sticky Situation": lambda state: state.has("Campfire", player) and has_bottle(state, player),
"Ol' Betsy": lambda state: craft_crossbow(state, player),
"Cover Me in Debris": lambda state: (state.has("Progressive Armor", player, 2) and
state.has("8 Netherite Scrap", player, 2) and state.has("Progressive Resource Crafting", player) and
has_diamond_pickaxe(state, player) and has_iron_ingots(state, player) and
can_brew_potions(state, player) and state.has("Bed", player)),
"Total Beelocation": lambda state: state.has("Silk Touch Book", player)
and can_use_anvil(world, state, player)
and can_enchant(world, state, player),
"Arbalistic": lambda state: (craft_crossbow(world, state, player)
and state.has("Piercing IV Book", player)
and can_use_anvil(world, state, player)
and can_enchant(world, state, player)
),
"The End... Again...": lambda state: can_respawn_ender_dragon(world, state, player)
and can_kill_ender_dragon(world, state, player),
"Acquire Hardware": lambda state: has_iron_ingots(world, state, player),
"Not Quite \"Nine\" Lives": lambda state: can_piglin_trade(world, state, player)
and state.has("Progressive Resource Crafting", player, 2),
"Cover Me With Diamonds": lambda state: state.has("Progressive Armor", player, 2)
and state.has("Progressive Tools", player, 2)
and has_iron_ingots(world, state, player),
"Sky's the Limit": lambda state: basic_combat(world, state, player),
"Hired Help": lambda state: state.has("Progressive Resource Crafting", player, 2)
and has_iron_ingots(world, state, player),
"Sweet Dreams": lambda state: state.has("Bed", player)
or state.can_reach_region('Village', player),
"You Need a Mint": lambda state: can_respawn_ender_dragon(world, state, player)
and has_bottle(world, state, player),
"Monsters Hunted": lambda state: (can_respawn_ender_dragon(world, state, player)
and can_kill_ender_dragon(world, state, player)
and can_kill_wither(world, state, player)
and state.has("Fishing Rod", player)),
"Enchanter": lambda state: can_enchant(world, state, player),
"Voluntary Exile": lambda state: basic_combat(world, state, player),
"Eye Spy": lambda state: enter_stronghold(world, state, player),
"Serious Dedication": lambda state: (can_brew_potions(world, state, player)
and state.has("Bed", player)
and has_diamond_pickaxe(world, state, player)
and has_gold_ingots(world, state, player)),
"Postmortal": lambda state: complete_raid(world, state, player),
"Adventuring Time": lambda state: can_adventure(world, state, player),
"Hero of the Village": lambda state: complete_raid(world, state, player),
"Hidden in the Depths": lambda state: can_brew_potions(world, state, player)
and state.has("Bed", player)
and has_diamond_pickaxe(world, state, player),
"Beaconator": lambda state: (can_kill_wither(world, state, player)
and has_diamond_pickaxe(world, state, player)
and state.has("Progressive Resource Crafting", player, 2)),
"Withering Heights": lambda state: can_kill_wither(world, state, player),
"A Balanced Diet": lambda state: (has_bottle(world, state, player)
and has_gold_ingots(world, state, player)
and state.has("Progressive Resource Crafting", player, 2)
and state.can_reach_region('The End', player)),
# notch apple, chorus fruit
"Subspace Bubble": lambda state: has_diamond_pickaxe(world, state, player),
"Country Lode, Take Me Home": lambda state: state.can_reach_location("Hidden in the Depths", player)
and has_gold_ingots(world, state, player),
"Bee Our Guest": lambda state: state.has("Campfire", player)
and has_bottle(world, state, player),
"Uneasy Alliance": lambda state: has_diamond_pickaxe(world, state, player)
and state.has('Fishing Rod', player),
"Diamonds!": lambda state: state.has("Progressive Tools", player, 2)
and has_iron_ingots(world, state, player),
"A Throwaway Joke": lambda state: can_adventure(world, state, player),
"Sticky Situation": lambda state: state.has("Campfire", player)
and has_bottle(world, state, player),
"Ol' Betsy": lambda state: craft_crossbow(world, state, player),
"Cover Me in Debris": lambda state: state.has("Progressive Armor", player, 2)
and state.has("8 Netherite Scrap", player, 2)
and state.has("Progressive Resource Crafting", player)
and has_diamond_pickaxe(world, state, player)
and has_iron_ingots(world, state, player)
and can_brew_potions(world, state, player)
and state.has("Bed", player),
"Hot Topic": lambda state: state.has("Progressive Resource Crafting", player),
"The Lie": lambda state: has_iron_ingots(state, player) and state.has("Bucket", player),
"On a Rail": lambda state: has_iron_ingots(state, player) and state.has('Progressive Tools', player, 2),
"When Pigs Fly": lambda state: ((fortress_loot(state, player) or complete_raid(state, player)) and
state.has("Saddle", player) and state.has("Fishing Rod", player) and can_adventure(state, player)),
"Overkill": lambda state: (can_brew_potions(state, player) and
(state.has("Progressive Weapons", player) or state.can_reach('The Nether', 'Region', player))),
"The Lie": lambda state: has_iron_ingots(world, state, player)
and state.has("Bucket", player),
"On a Rail": lambda state: has_iron_ingots(world, state, player)
and state.has('Progressive Tools', player, 2),
"When Pigs Fly": lambda state: (
fortress_loot(world, state, player)
or complete_raid(world, state, player)
)
and state.has("Saddle", player)
and state.has("Fishing Rod", player)
and can_adventure(world, state, player),
"Overkill": lambda state: can_brew_potions(world, state, player)
and (
state.has("Progressive Weapons", player)
or state.can_reach_region('The Nether', player)
),
"Librarian": lambda state: state.has("Enchanting", player),
"Overpowered": lambda state: (has_iron_ingots(state, player) and
state.has('Progressive Tools', player, 2) and basic_combat(state, player)),
"Wax On": lambda state: (has_copper_ingots(state, player) and state.has('Campfire', player) and
state.has('Progressive Resource Crafting', player, 2)),
"Wax Off": lambda state: (has_copper_ingots(state, player) and state.has('Campfire', player) and
state.has('Progressive Resource Crafting', player, 2)),
"The Cutest Predator": lambda state: has_iron_ingots(state, player) and state.has('Bucket', player),
"The Healing Power of Friendship": lambda state: has_iron_ingots(state, player) and state.has('Bucket', player),
"Is It a Bird?": lambda state: has_spyglass(state, player) and can_adventure(state, player),
"Is It a Balloon?": lambda state: has_spyglass(state, player),
"Is It a Plane?": lambda state: has_spyglass(state, player) and can_respawn_ender_dragon(state, player),
"Surge Protector": lambda state: (state.has("Channeling Book", player) and
can_use_anvil(state, player) and can_enchant(state, player) and overworld_villager(state, player)),
"Light as a Rabbit": lambda state: can_adventure(state, player) and has_iron_ingots(state, player) and state.has('Bucket', player),
"Glow and Behold!": lambda state: can_adventure(state, player),
"Whatever Floats Your Goat!": lambda state: can_adventure(state, player),
"Caves & Cliffs": lambda state: has_iron_ingots(state, player) and state.has('Bucket', player) and state.has('Progressive Tools', player, 2),
"Feels like home": lambda state: (has_iron_ingots(state, player) and state.has('Bucket', player) and state.has('Fishing Rod', player) and
(fortress_loot(state, player) or complete_raid(state, player)) and state.has("Saddle", player)),
"Sound of Music": lambda state: state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player) and basic_combat(state, player),
"Star Trader": lambda state: (has_iron_ingots(state, player) and state.has('Bucket', player) and
(state.can_reach("The Nether", 'Region', player) or
state.can_reach("Nether Fortress", 'Region', player) or # soul sand for water elevator
can_piglin_trade(state, player)) and
overworld_villager(state, player)),
"Birthday Song": lambda state: state.can_reach("The Lie", "Location", player) and state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player),
"Bukkit Bukkit": lambda state: state.has("Bucket", player) and has_iron_ingots(state, player) and can_adventure(state, player),
"It Spreads": lambda state: can_adventure(state, player) and has_iron_ingots(state, player) and state.has("Progressive Tools", player, 2),
"Sneak 100": lambda state: can_adventure(state, player) and has_iron_ingots(state, player) and state.has("Progressive Tools", player, 2),
"When the Squad Hops into Town": lambda state: can_adventure(state, player) and state.has("Lead", player),
"With Our Powers Combined!": lambda state: can_adventure(state, player) and state.has("Lead", player),
"Overpowered": lambda state: has_iron_ingots(world, state, player)
and state.has('Progressive Tools', player, 2)
and basic_combat(world, state, player),
"Wax On": lambda state: has_copper_ingots(world, state, player)
and state.has('Campfire', player)
and state.has('Progressive Resource Crafting', player, 2),
"Wax Off": lambda state: has_copper_ingots(world, state, player)
and state.has('Campfire', player)
and state.has('Progressive Resource Crafting', player, 2),
"The Cutest Predator": lambda state: has_iron_ingots(world, state, player)
and state.has('Bucket', player),
"The Healing Power of Friendship": lambda state: has_iron_ingots(world, state, player)
and state.has('Bucket', player),
"Is It a Bird?": lambda state: has_spyglass(world, state, player)
and can_adventure(world, state, player),
"Is It a Balloon?": lambda state: has_spyglass(world, state, player),
"Is It a Plane?": lambda state: has_spyglass(world, state, player)
and can_respawn_ender_dragon(world, state, player),
"Surge Protector": lambda state: state.has("Channeling Book", player)
and can_use_anvil(world, state, player)
and can_enchant(world, state, player)
and overworld_villager(world, state, player),
"Light as a Rabbit": lambda state: can_adventure(world, state, player)
and has_iron_ingots(world, state, player)
and state.has('Bucket', player),
"Glow and Behold!": lambda state: can_adventure(world, state, player),
"Whatever Floats Your Goat!": lambda state: can_adventure(world, state, player),
"Caves & Cliffs": lambda state: has_iron_ingots(world, state, player)
and state.has('Bucket', player)
and state.has('Progressive Tools', player, 2),
"Feels like home": lambda state: has_iron_ingots(world, state, player)
and state.has('Bucket', player)
and state.has('Fishing Rod', player)
and (
fortress_loot(world, state, player)
or complete_raid(world, state, player)
)
and state.has("Saddle", player),
"Sound of Music": lambda state: state.has("Progressive Tools", player, 2)
and has_iron_ingots(world, state, player)
and basic_combat(world, state, player),
"Star Trader": lambda state: has_iron_ingots(world, state, player)
and state.has('Bucket', player)
and (
state.can_reach_region("The Nether", player) # soul sand in nether
or state.can_reach_region("Nether Fortress", player) # soul sand in fortress if not in nether for water elevator
or can_piglin_trade(world, state, player) # piglins give soul sand
)
and overworld_villager(world, state, player),
"Birthday Song": lambda state: state.can_reach_location("The Lie", player)
and state.has("Progressive Tools", player, 2)
and has_iron_ingots(world, state, player),
"Bukkit Bukkit": lambda state: state.has("Bucket", player)
and has_iron_ingots(world, state, player)
and can_adventure(world, state, player),
"It Spreads": lambda state: can_adventure(world, state, player)
and has_iron_ingots(world, state, player)
and state.has("Progressive Tools", player, 2),
"Sneak 100": lambda state: can_adventure(world, state, player)
and has_iron_ingots(world, state, player)
and state.has("Progressive Tools", player, 2),
"When the Squad Hops into Town": lambda state: can_adventure(world, state, player)
and state.has("Lead", player),
"With Our Powers Combined!": lambda state: can_adventure(world, state, player)
and state.has("Lead", player),
}
}
return rules_lookup
def set_rules(mc_world: World) -> None:
multiworld = mc_world.multiworld
player = mc_world.player
def set_rules(self: "MinecraftWorld") -> None:
multiworld = self.multiworld
player = self.player
rules_lookup = get_rules_lookup(player)
rules_lookup = get_rules_lookup(self, player)
# Set entrance rules
for entrance_name, rule in rules_lookup["entrances"].items():
@@ -281,33 +476,33 @@ def set_rules(mc_world: World) -> None:
multiworld.get_location(location_name, player).access_rule = rule
# Set rules surrounding completion
bosses = multiworld.required_bosses[player]
bosses = self.options.required_bosses
postgame_advancements = set()
if bosses.dragon:
postgame_advancements.update(Constants.exclusion_info["ender_dragon"])
if bosses.wither:
postgame_advancements.update(Constants.exclusion_info["wither"])
def location_count(state: CollectionState) -> bool:
def location_count(state: CollectionState) -> int:
return len([location for location in multiworld.get_locations(player) if
location.address != None and
location.can_reach(state)])
location.address is not None and
location.can_reach(state)])
def defeated_bosses(state: CollectionState) -> bool:
return ((not bosses.dragon or state.has("Ender Dragon", player))
and (not bosses.wither or state.has("Wither", player)))
and (not bosses.wither or state.has("Wither", player)))
egg_shards = min(multiworld.egg_shards_required[player], multiworld.egg_shards_available[player])
completion_requirements = lambda state: (location_count(state) >= multiworld.advancement_goal[player]
and state.has("Dragon Egg Shard", player, egg_shards))
egg_shards = min(self.options.egg_shards_required.value, self.options.egg_shards_available.value)
completion_requirements = lambda state: (location_count(state) >= self.options.advancement_goal
and state.has("Dragon Egg Shard", player, egg_shards))
multiworld.completion_condition[player] = lambda state: completion_requirements(state) and defeated_bosses(state)
# Set exclusions on hard/unreasonable/postgame
excluded_advancements = set()
if not multiworld.include_hard_advancements[player]:
if not self.options.include_hard_advancements:
excluded_advancements.update(Constants.exclusion_info["hard"])
if not multiworld.include_unreasonable_advancements[player]:
if not self.options.include_unreasonable_advancements:
excluded_advancements.update(Constants.exclusion_info["unreasonable"])
if not multiworld.include_postgame_advancements[player]:
if not self.options.include_postgame_advancements:
excluded_advancements.update(postgame_advancements)
exclusion_rules(multiworld, player, excluded_advancements)

View File

@@ -1,17 +1,19 @@
from worlds.AutoWorld import World
from . import Constants
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from . import MinecraftWorld
def shuffle_structures(mc_world: World) -> None:
multiworld = mc_world.multiworld
player = mc_world.player
def shuffle_structures(self: "MinecraftWorld") -> None:
multiworld = self.multiworld
player = self.player
default_connections = Constants.region_info["default_connections"]
illegal_connections = Constants.region_info["illegal_connections"]
# Get all unpaired exits and all regions without entrances (except the Menu)
# This function is destructive on these lists.
exits = [exit.name for r in multiworld.regions if r.player == player for exit in r.exits if exit.connected_region == None]
exits = [exit.name for r in multiworld.regions if r.player == player for exit in r.exits if exit.connected_region is None]
structs = [r.name for r in multiworld.regions if r.player == player and r.entrances == [] and r.name != 'Menu']
exits_spoiler = exits[:] # copy the original order for the spoiler log
@@ -26,19 +28,19 @@ def shuffle_structures(mc_world: World) -> None:
raise Exception(f"Invalid connection: {exit} => {struct} for player {player} ({multiworld.player_name[player]})")
# Connect plando structures first
if multiworld.plando_connections[player]:
for conn in multiworld.plando_connections[player]:
if self.options.plando_connections:
for conn in self.plando_connections:
set_pair(conn.entrance, conn.exit)
# The algorithm tries to place the most restrictive structures first. This algorithm always works on the
# relatively small set of restrictions here, but does not work on all possible inputs with valid configurations.
if multiworld.shuffle_structures[player]:
if self.options.shuffle_structures:
structs.sort(reverse=True, key=lambda s: len(illegal_connections.get(s, [])))
for struct in structs[:]:
try:
exit = multiworld.random.choice([e for e in exits if e not in illegal_connections.get(struct, [])])
exit = self.random.choice([e for e in exits if e not in illegal_connections.get(struct, [])])
except IndexError:
raise Exception(f"No valid structure placements remaining for player {player} ({multiworld.player_name[player]})")
raise Exception(f"No valid structure placements remaining for player {player} ({self.player_name})")
set_pair(exit, struct)
else: # write remaining default connections
for (exit, struct) in default_connections:
@@ -49,9 +51,9 @@ def shuffle_structures(mc_world: World) -> None:
try:
assert len(exits) == len(structs) == 0
except AssertionError:
raise Exception(f"Failed to connect all Minecraft structures for player {player} ({multiworld.player_name[player]})")
raise Exception(f"Failed to connect all Minecraft structures for player {player} ({self.player_name})")
for exit in exits_spoiler:
multiworld.get_entrance(exit, player).connect(multiworld.get_region(pairs[exit], player))
if multiworld.shuffle_structures[player] or multiworld.plando_connections[player]:
if self.options.shuffle_structures or self.options.plando_connections:
multiworld.spoiler.set_entrance(exit, pairs[exit], 'entrance', player)

View File

@@ -9,7 +9,7 @@ from BaseClasses import Region, Entrance, Item, Tutorial, ItemClassification, Lo
from worlds.AutoWorld import World, WebWorld
from . import Constants
from .Options import minecraft_options
from .Options import MinecraftOptions
from .Structures import shuffle_structures
from .ItemPool import build_item_pool, get_junk_item_names
from .Rules import set_rules
@@ -83,8 +83,9 @@ class MinecraftWorld(World):
structures, and materials to create a portal to another world. Defeat the Ender Dragon, and claim
victory!
"""
game: str = "Minecraft"
option_definitions = minecraft_options
game = "Minecraft"
options_dataclass = MinecraftOptions
options: MinecraftOptions
settings: typing.ClassVar[MinecraftSettings]
topology_present = True
web = MinecraftWebWorld()
@@ -95,20 +96,20 @@ class MinecraftWorld(World):
def _get_mc_data(self) -> Dict[str, Any]:
exits = [connection[0] for connection in Constants.region_info["default_connections"]]
return {
'world_seed': self.multiworld.per_slot_randoms[self.player].getrandbits(32),
'world_seed': self.random.getrandbits(32),
'seed_name': self.multiworld.seed_name,
'player_name': self.multiworld.get_player_name(self.player),
'player_name': self.player_name,
'player_id': self.player,
'client_version': client_version,
'structures': {exit: self.multiworld.get_entrance(exit, self.player).connected_region.name for exit in exits},
'advancement_goal': self.multiworld.advancement_goal[self.player].value,
'egg_shards_required': min(self.multiworld.egg_shards_required[self.player].value,
self.multiworld.egg_shards_available[self.player].value),
'egg_shards_available': self.multiworld.egg_shards_available[self.player].value,
'required_bosses': self.multiworld.required_bosses[self.player].current_key,
'MC35': bool(self.multiworld.send_defeated_mobs[self.player].value),
'death_link': bool(self.multiworld.death_link[self.player].value),
'starting_items': str(self.multiworld.starting_items[self.player].value),
'advancement_goal': self.options.advancement_goal.value,
'egg_shards_required': min(self.options.egg_shards_required.value,
self.options.egg_shards_available.value),
'egg_shards_available': self.options.egg_shards_available.value,
'required_bosses': self.options.required_bosses.current_key,
'MC35': bool(self.options.send_defeated_mobs.value),
'death_link': bool(self.options.death_link.value),
'starting_items': json.dumps(self.options.starting_items.value),
'race': self.multiworld.is_race,
}
@@ -129,7 +130,7 @@ class MinecraftWorld(World):
loc.place_locked_item(self.create_event_item(event_name))
region.locations.append(loc)
def create_event_item(self, name: str) -> None:
def create_event_item(self, name: str) -> Item:
item = self.create_item(name)
item.classification = ItemClassification.progression
return item
@@ -176,15 +177,10 @@ class MinecraftWorld(World):
f.write(b64encode(bytes(json.dumps(data), 'utf-8')))
def fill_slot_data(self) -> dict:
slot_data = self._get_mc_data()
for option_name in minecraft_options:
option = getattr(self.multiworld, option_name)[self.player]
if slot_data.get(option_name, None) is None and type(option.value) in {str, int}:
slot_data[option_name] = int(option.value)
return slot_data
return self._get_mc_data()
def get_filler_item_name(self) -> str:
return get_junk_item_names(self.multiworld.random, 1)[0]
return get_junk_item_names(self.random, 1)[0]
class MinecraftLocation(Location):

View File

@@ -1,19 +1,19 @@
from . import MCTestBase
from ..Constants import region_info
from ..Options import minecraft_options
from .. import Options
from BaseClasses import ItemClassification
class AdvancementTestBase(MCTestBase):
options = {
"advancement_goal": minecraft_options["advancement_goal"].range_end
"advancement_goal": Options.AdvancementGoal.range_end
}
# beatability test implicit
class ShardTestBase(MCTestBase):
options = {
"egg_shards_required": minecraft_options["egg_shards_required"].range_end,
"egg_shards_available": minecraft_options["egg_shards_available"].range_end
"egg_shards_required": Options.EggShardsRequired.range_end,
"egg_shards_available": Options.EggShardsAvailable.range_end
}
# check that itempool is not overfilled with shards
@@ -29,7 +29,7 @@ class CompassTestBase(MCTestBase):
class NoBeeTestBase(MCTestBase):
options = {
"bee_traps": 0
"bee_traps": Options.BeeTraps.range_start
}
# With no bees, there are no traps in the pool
@@ -40,7 +40,7 @@ class NoBeeTestBase(MCTestBase):
class AllBeeTestBase(MCTestBase):
options = {
"bee_traps": 100
"bee_traps": Options.BeeTraps.range_end
}
# With max bees, there are no filler items, only bee traps

View File

@@ -1,5 +1,5 @@
from test.TestBase import TestBase, WorldTestBase
from .. import MinecraftWorld
from test.bases import TestBase, WorldTestBase
from .. import MinecraftWorld, MinecraftOptions
class MCTestBase(WorldTestBase, TestBase):

290
worlds/mm2/__init__.py Normal file
View File

@@ -0,0 +1,290 @@
import hashlib
import logging
from copy import deepcopy
from typing import Dict, Any, TYPE_CHECKING, Optional, Sequence, Tuple, ClassVar, List
from BaseClasses import Tutorial, ItemClassification, MultiWorld, Item, Location
from worlds.AutoWorld import World, WebWorld
from .names import (dr_wily, heat_man_stage, air_man_stage, wood_man_stage, bubble_man_stage, quick_man_stage,
flash_man_stage, metal_man_stage, crash_man_stage)
from .items import (item_table, item_names, MM2Item, filler_item_weights, robot_master_weapon_table,
stage_access_table, item_item_table, lookup_item_to_id)
from .locations import (MM2Location, mm2_regions, MM2Region, energy_pickups, etank_1ups, lookup_location_to_id,
location_groups)
from .rom import patch_rom, MM2ProcedurePatch, MM2LCHASH, PROTEUSHASH, MM2VCHASH, MM2NESHASH
from .options import MM2Options, Consumables
from .client import MegaMan2Client
from .rules import set_rules, weapon_damage, robot_masters, weapons_to_name, minimum_weakness_requirement
import os
import threading
import base64
import settings
logger = logging.getLogger("Mega Man 2")
if TYPE_CHECKING:
from BaseClasses import CollectionState
class MM2Settings(settings.Group):
class RomFile(settings.UserFilePath):
"""File name of the MM2 EN rom"""
description = "Mega Man 2 ROM File"
copy_to: Optional[str] = "Mega Man 2 (USA).nes"
md5s = [MM2NESHASH, MM2VCHASH, MM2LCHASH, PROTEUSHASH]
def browse(self: settings.T,
filetypes: Optional[Sequence[Tuple[str, Sequence[str]]]] = None,
**kwargs: Any) -> Optional[settings.T]:
if not filetypes:
file_types = [("NES", [".nes"]), ("Program", [".exe"])] # LC1 is only a windows executable, no linux
return super().browse(file_types, **kwargs)
else:
return super().browse(filetypes, **kwargs)
@classmethod
def validate(cls, path: str) -> None:
"""Try to open and validate file against hashes"""
with open(path, "rb", buffering=0) as f:
try:
f.seek(0)
if f.read(4) == b"NES\x1A":
f.seek(16)
else:
f.seek(0)
cls._validate_stream_hashes(f)
base_rom_bytes = f.read()
basemd5 = hashlib.md5()
basemd5.update(base_rom_bytes)
if basemd5.hexdigest() == PROTEUSHASH:
# we need special behavior here
cls.copy_to = None
except ValueError:
raise ValueError(f"File hash does not match for {path}")
rom_file: RomFile = RomFile(RomFile.copy_to)
class MM2WebWorld(WebWorld):
theme = "partyTime"
tutorials = [
Tutorial(
"Multiworld Setup Guide",
"A guide to setting up the Mega Man 2 randomizer connected to an Archipelago Multiworld.",
"English",
"setup_en.md",
"setup/en",
["Silvris"]
)
]
class MM2World(World):
"""
In the year 200X, following his prior defeat by Mega Man, the evil Dr. Wily has returned to take over the world with
his own group of Robot Masters. Mega Man once again sets out to defeat the eight Robot Masters and stop Dr. Wily.
"""
game = "Mega Man 2"
settings: ClassVar[MM2Settings]
options_dataclass = MM2Options
options: MM2Options
item_name_to_id = lookup_item_to_id
location_name_to_id = lookup_location_to_id
item_name_groups = item_names
location_name_groups = location_groups
web = MM2WebWorld()
rom_name: bytearray
world_version: Tuple[int, int, int] = (0, 3, 1)
wily_5_weapons: Dict[int, List[int]]
def __init__(self, world: MultiWorld, player: int):
self.rom_name = bytearray()
self.rom_name_available_event = threading.Event()
super().__init__(world, player)
self.weapon_damage = deepcopy(weapon_damage)
self.wily_5_weapons = {}
def create_regions(self) -> None:
menu = MM2Region("Menu", self.player, self.multiworld)
self.multiworld.regions.append(menu)
for region in mm2_regions:
stage = MM2Region(region, self.player, self.multiworld)
required_items = mm2_regions[region][0]
locations = mm2_regions[region][1]
prev_stage = mm2_regions[region][2]
if prev_stage is None:
menu.connect(stage, f"To {region}",
lambda state, items=required_items: state.has_all(items, self.player))
else:
old_stage = self.get_region(prev_stage)
old_stage.connect(stage, f"To {region}",
lambda state, items=required_items: state.has_all(items, self.player))
stage.add_locations(locations, MM2Location)
for location in stage.get_locations():
if location.address is None and location.name != dr_wily:
location.place_locked_item(MM2Item(location.name, ItemClassification.progression,
None, self.player))
if region in etank_1ups and self.options.consumables in (Consumables.option_1up_etank,
Consumables.option_all):
stage.add_locations(etank_1ups[region], MM2Location)
if region in energy_pickups and self.options.consumables in (Consumables.option_weapon_health,
Consumables.option_all):
stage.add_locations(energy_pickups[region], MM2Location)
self.multiworld.regions.append(stage)
def create_item(self, name: str) -> MM2Item:
item = item_table[name]
classification = ItemClassification.filler
if item.progression:
classification = ItemClassification.progression_skip_balancing \
if item.skip_balancing else ItemClassification.progression
if item.useful:
classification |= ItemClassification.useful
return MM2Item(name, classification, item.code, self.player)
def get_filler_item_name(self) -> str:
return self.random.choices(list(filler_item_weights.keys()),
weights=list(filler_item_weights.values()))[0]
def create_items(self) -> None:
itempool = []
# grab first robot master
robot_master = self.item_id_to_name[0x880101 + self.options.starting_robot_master.value]
self.multiworld.push_precollected(self.create_item(robot_master))
itempool.extend([self.create_item(name) for name in stage_access_table.keys()
if name != robot_master])
itempool.extend([self.create_item(name) for name in robot_master_weapon_table.keys()])
itempool.extend([self.create_item(name) for name in item_item_table.keys()])
total_checks = 24
if self.options.consumables in (Consumables.option_1up_etank,
Consumables.option_all):
total_checks += 20
if self.options.consumables in (Consumables.option_weapon_health,
Consumables.option_all):
total_checks += 27
remaining = total_checks - len(itempool)
itempool.extend([self.create_item(name)
for name in self.random.choices(list(filler_item_weights.keys()),
weights=list(filler_item_weights.values()),
k=remaining)])
self.multiworld.itempool += itempool
set_rules = set_rules
def generate_early(self) -> None:
if (not self.options.yoku_jumps
and self.options.starting_robot_master == "heat_man") or \
(not self.options.enable_lasers
and self.options.starting_robot_master == "quick_man"):
robot_master_pool = [1, 2, 3, 5, 6, 7, ]
if self.options.yoku_jumps:
robot_master_pool.append(0)
if self.options.enable_lasers:
robot_master_pool.append(4)
self.options.starting_robot_master.value = self.random.choice(robot_master_pool)
logger.warning(
f"Mega Man 2 ({self.player_name}): "
f"Incompatible starting Robot Master, changing to "
f"{self.options.starting_robot_master.current_key.replace('_', ' ').title()}")
def generate_basic(self) -> None:
goal_location = self.get_location(dr_wily)
goal_location.place_locked_item(MM2Item("Victory", ItemClassification.progression, None, self.player))
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
def fill_hook(self,
progitempool: List["Item"],
usefulitempool: List["Item"],
filleritempool: List["Item"],
fill_locations: List["Location"]) -> None:
# on a solo gen, fill can try to force Wily into sphere 2, but for most generations this is impossible
# since MM2 can have a 2 item sphere 1, and 3 items are required for Wily
if self.multiworld.players > 1:
return # Don't need to change anything on a multi gen, fill should be able to solve it with a 4 sphere 1
rbm_to_item = {
0: heat_man_stage,
1: air_man_stage,
2: wood_man_stage,
3: bubble_man_stage,
4: quick_man_stage,
5: flash_man_stage,
6: metal_man_stage,
7: crash_man_stage
}
affected_rbm = [2, 3] # Wood and Bubble will always have this happen
possible_rbm = [1, 5] # Air and Flash are always valid targets, due to Item 2/3 receive
if self.options.consumables:
possible_rbm.append(6) # Metal has 3 consumables
possible_rbm.append(7) # Crash has 3 consumables
if self.options.enable_lasers:
possible_rbm.append(4) # Quick has a lot of consumables, but needs logical time stopper if not enabled
else:
affected_rbm.extend([6, 7]) # only two checks on non consumables
if self.options.yoku_jumps:
possible_rbm.append(0) # Heat has 3 locations always, but might need 2 items logically
if self.options.starting_robot_master.value in affected_rbm:
rbm_names = list(map(lambda s: rbm_to_item[s], possible_rbm))
valid_second = [item for item in progitempool
if item.name in rbm_names
and item.player == self.player]
placed_item = self.random.choice(valid_second)
rbm_defeated = (f"{robot_masters[self.options.starting_robot_master.value].replace(' Defeated', '')}"
f" - Defeated")
rbm_location = self.get_location(rbm_defeated)
rbm_location.place_locked_item(placed_item)
progitempool.remove(placed_item)
fill_locations.remove(rbm_location)
target_rbm = (placed_item.code & 0xF) - 1
if self.options.strict_weakness or (self.options.random_weakness
and not (self.weapon_damage[0][target_rbm] > 0)):
# we need to find a weakness for this boss
weaknesses = [weapon for weapon in range(1, 9)
if self.weapon_damage[weapon][target_rbm] >= minimum_weakness_requirement[weapon]]
weapons = list(map(lambda s: weapons_to_name[s], weaknesses))
valid_weapons = [item for item in progitempool
if item.name in weapons
and item.player == self.player]
placed_weapon = self.random.choice(valid_weapons)
weapon_name = next(name for name, idx in lookup_location_to_id.items()
if idx == 0x880101 + self.options.starting_robot_master.value)
weapon_location = self.get_location(weapon_name)
weapon_location.place_locked_item(placed_weapon)
progitempool.remove(placed_weapon)
fill_locations.remove(weapon_location)
def generate_output(self, output_directory: str) -> None:
try:
patch = MM2ProcedurePatch(player=self.player, player_name=self.player_name)
patch_rom(self, patch)
self.rom_name = patch.name
patch.write(os.path.join(output_directory,
f"{self.multiworld.get_out_file_name_base(self.player)}{patch.patch_file_ending}"))
except Exception:
raise
finally:
self.rom_name_available_event.set() # make sure threading continues and errors are collected
def fill_slot_data(self) -> Dict[str, Any]:
return {
"death_link": self.options.death_link.value,
"weapon_damage": self.weapon_damage,
"wily_5_weapons": self.wily_5_weapons,
}
def interpret_slot_data(self, slot_data: Dict[str, Any]) -> Dict[str, Any]:
local_weapon = {int(key): value for key, value in slot_data["weapon_damage"].items()}
local_wily = {int(key): value for key, value in slot_data["wily_5_weapons"].items()}
return {"weapon_damage": local_weapon, "wily_5_weapons": local_wily}
def modify_multidata(self, multidata: Dict[str, Any]) -> None:
# wait for self.rom_name to be available.
self.rom_name_available_event.wait()
rom_name = getattr(self, "rom_name", None)
# we skip in case of error, so that the original error in the output thread is the one that gets raised
if rom_name:
new_name = base64.b64encode(bytes(self.rom_name)).decode()
multidata["connect_names"][new_name] = multidata["connect_names"][self.player_name]

562
worlds/mm2/client.py Normal file
View File

@@ -0,0 +1,562 @@
import logging
import time
from enum import IntEnum
from base64 import b64encode
from typing import TYPE_CHECKING, Dict, Tuple, List, Optional, Any
from NetUtils import ClientStatus, color, NetworkItem
from worlds._bizhawk.client import BizHawkClient
if TYPE_CHECKING:
from worlds._bizhawk.context import BizHawkClientContext, BizHawkClientCommandProcessor
nes_logger = logging.getLogger("NES")
logger = logging.getLogger("Client")
MM2_ROBOT_MASTERS_UNLOCKED = 0x8A
MM2_ROBOT_MASTERS_DEFEATED = 0x8B
MM2_ITEMS_ACQUIRED = 0x8C
MM2_LAST_WILY = 0x8D
MM2_RECEIVED_ITEMS = 0x8E
MM2_DEATHLINK = 0x8F
MM2_ENERGYLINK = 0x90
MM2_RBM_STROBE = 0x91
MM2_WEAPONS_UNLOCKED = 0x9A
MM2_ITEMS_UNLOCKED = 0x9B
MM2_WEAPON_ENERGY = 0x9C
MM2_E_TANKS = 0xA7
MM2_LIVES = 0xA8
MM2_DIFFICULTY = 0xCB
MM2_HEALTH = 0x6C0
MM2_COMPLETED_STAGES = 0x770
MM2_CONSUMABLES = 0x780
MM2_SFX_QUEUE = 0x580
MM2_SFX_STROBE = 0x66
MM2_CONSUMABLE_TABLE: Dict[int, Tuple[int, int]] = {
# Item: (byte offset, bit mask)
0x880201: (0, 8),
0x880202: (16, 1),
0x880203: (16, 2),
0x880204: (16, 4),
0x880205: (16, 8),
0x880206: (16, 16),
0x880207: (16, 32),
0x880208: (16, 64),
0x880209: (16, 128),
0x88020A: (20, 1),
0x88020B: (20, 4),
0x88020C: (20, 64),
0x88020D: (21, 1),
0x88020E: (21, 2),
0x88020F: (21, 4),
0x880210: (24, 1),
0x880211: (24, 2),
0x880212: (24, 4),
0x880213: (28, 1),
0x880214: (28, 2),
0x880215: (28, 4),
0x880216: (33, 4),
0x880217: (33, 8),
0x880218: (37, 8),
0x880219: (37, 16),
0x88021A: (38, 1),
0x88021B: (38, 2),
0x880227: (38, 4),
0x880228: (38, 32),
0x880229: (38, 128),
0x88022A: (39, 4),
0x88022B: (39, 2),
0x88022C: (39, 1),
0x88022D: (38, 64),
0x88022E: (38, 16),
0x88022F: (38, 8),
0x88021C: (39, 32),
0x88021D: (39, 64),
0x88021E: (39, 128),
0x88021F: (41, 16),
0x880220: (42, 2),
0x880221: (42, 4),
0x880222: (42, 8),
0x880223: (46, 1),
0x880224: (46, 2),
0x880225: (46, 4),
0x880226: (46, 8),
}
class MM2EnergyLinkType(IntEnum):
Life = 0
AtomicFire = 1
AirShooter = 2
LeafShield = 3
BubbleLead = 4
QuickBoomerang = 5
TimeStopper = 6
MetalBlade = 7
CrashBomber = 8
Item1 = 9
Item2 = 10
Item3 = 11
OneUP = 12
request_to_name: Dict[str, str] = {
"HP": "health",
"AF": "Atomic Fire energy",
"AS": "Air Shooter energy",
"LS": "Leaf Shield energy",
"BL": "Bubble Lead energy",
"QB": "Quick Boomerang energy",
"TS": "Time Stopper energy",
"MB": "Metal Blade energy",
"CB": "Crash Bomber energy",
"I1": "Item 1 energy",
"I2": "Item 2 energy",
"I3": "Item 3 energy",
"1U": "lives"
}
HP_EXCHANGE_RATE = 500000000
WEAPON_EXCHANGE_RATE = 250000000
ONEUP_EXCHANGE_RATE = 14000000000
def cmd_pool(self: "BizHawkClientCommandProcessor") -> None:
"""Check the current pool of EnergyLink, and requestable refills from it."""
if self.ctx.game != "Mega Man 2":
logger.warning("This command can only be used when playing Mega Man 2.")
return
if not self.ctx.server or not self.ctx.slot:
logger.warning("You must be connected to a server to use this command.")
return
energylink = self.ctx.stored_data.get(f"EnergyLink{self.ctx.team}", 0)
health_points = energylink // HP_EXCHANGE_RATE
weapon_points = energylink // WEAPON_EXCHANGE_RATE
lives = energylink // ONEUP_EXCHANGE_RATE
logger.info(f"Healing available: {health_points}\n"
f"Weapon refill available: {weapon_points}\n"
f"Lives available: {lives}")
def cmd_request(self: "BizHawkClientCommandProcessor", amount: str, target: str) -> None:
from worlds._bizhawk.context import BizHawkClientContext
"""Request a refill from EnergyLink."""
if self.ctx.game != "Mega Man 2":
logger.warning("This command can only be used when playing Mega Man 2.")
return
if not self.ctx.server or not self.ctx.slot:
logger.warning("You must be connected to a server to use this command.")
return
valid_targets: Dict[str, MM2EnergyLinkType] = {
"HP": MM2EnergyLinkType.Life,
"AF": MM2EnergyLinkType.AtomicFire,
"AS": MM2EnergyLinkType.AirShooter,
"LS": MM2EnergyLinkType.LeafShield,
"BL": MM2EnergyLinkType.BubbleLead,
"QB": MM2EnergyLinkType.QuickBoomerang,
"TS": MM2EnergyLinkType.TimeStopper,
"MB": MM2EnergyLinkType.MetalBlade,
"CB": MM2EnergyLinkType.CrashBomber,
"I1": MM2EnergyLinkType.Item1,
"I2": MM2EnergyLinkType.Item2,
"I3": MM2EnergyLinkType.Item3,
"1U": MM2EnergyLinkType.OneUP
}
if target.upper() not in valid_targets:
logger.warning(f"Unrecognized target {target.upper()}. Available targets: {', '.join(valid_targets.keys())}")
return
ctx = self.ctx
assert isinstance(ctx, BizHawkClientContext)
client = ctx.client_handler
assert isinstance(client, MegaMan2Client)
client.refill_queue.append((valid_targets[target.upper()], int(amount)))
logger.info(f"Restoring {amount} {request_to_name[target.upper()]}.")
def cmd_autoheal(self) -> None:
"""Enable auto heal from EnergyLink."""
if self.ctx.game != "Mega Man 2":
logger.warning("This command can only be used when playing Mega Man 2.")
return
if not self.ctx.server or not self.ctx.slot:
logger.warning("You must be connected to a server to use this command.")
return
else:
assert isinstance(self.ctx.client_handler, MegaMan2Client)
if self.ctx.client_handler.auto_heal:
self.ctx.client_handler.auto_heal = False
logger.info(f"Auto healing disabled.")
else:
self.ctx.client_handler.auto_heal = True
logger.info(f"Auto healing enabled.")
def get_sfx_writes(sfx: int) -> Tuple[Tuple[int, bytes, str], ...]:
return (MM2_SFX_QUEUE, sfx.to_bytes(1, 'little'), "RAM"), (MM2_SFX_STROBE, 0x01.to_bytes(1, "little"), "RAM")
class MegaMan2Client(BizHawkClient):
game = "Mega Man 2"
system = "NES"
patch_suffix = ".apmm2"
item_queue: List[NetworkItem] = []
pending_death_link: bool = False
# default to true, as we don't want to send a deathlink until Mega Man's HP is initialized once
sending_death_link: bool = True
death_link: bool = False
energy_link: bool = False
rom: Optional[bytes] = None
weapon_energy: int = 0
health_energy: int = 0
auto_heal: bool = False
refill_queue: List[Tuple[MM2EnergyLinkType, int]] = []
last_wily: Optional[int] = None # default to wily 1
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
from worlds._bizhawk import RequestFailedError, read
from . import MM2World
try:
game_name, version = (await read(ctx.bizhawk_ctx, [(0x3FFB0, 21, "PRG ROM"),
(0x3FFC8, 3, "PRG ROM")]))
if game_name[:3] != b"MM2" or version != bytes(MM2World.world_version):
if game_name[:3] == b"MM2":
# I think this is an easier check than the other?
older_version = "0.2.1" if version == b"\xFF\xFF\xFF" else f"{version[0]}.{version[1]}.{version[2]}"
logger.warning(f"This Mega Man 2 patch was generated for an different version of the apworld. "
f"Please use that version to connect instead.\n"
f"Patch version: ({older_version})\n"
f"Client version: ({'.'.join([str(i) for i in MM2World.world_version])})")
if "pool" in ctx.command_processor.commands:
ctx.command_processor.commands.pop("pool")
if "request" in ctx.command_processor.commands:
ctx.command_processor.commands.pop("request")
if "autoheal" in ctx.command_processor.commands:
ctx.command_processor.commands.pop("autoheal")
return False
except UnicodeDecodeError:
return False
except RequestFailedError:
return False # Should verify on the next pass
ctx.game = self.game
self.rom = game_name
ctx.items_handling = 0b111
ctx.want_slot_data = False
deathlink = (await read(ctx.bizhawk_ctx, [(0x3FFC5, 1, "PRG ROM")]))[0][0]
if deathlink & 0x01:
self.death_link = True
if deathlink & 0x02:
self.energy_link = True
if self.energy_link:
if "pool" not in ctx.command_processor.commands:
ctx.command_processor.commands["pool"] = cmd_pool
if "request" not in ctx.command_processor.commands:
ctx.command_processor.commands["request"] = cmd_request
if "autoheal" not in ctx.command_processor.commands:
ctx.command_processor.commands["autoheal"] = cmd_autoheal
return True
async def set_auth(self, ctx: "BizHawkClientContext") -> None:
if self.rom:
ctx.auth = b64encode(self.rom).decode()
def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: Dict[str, Any]) -> None:
if cmd == "Bounced":
if "tags" in args:
assert ctx.slot is not None
if "DeathLink" in args["tags"] and args["data"]["source"] != ctx.slot_info[ctx.slot].name:
self.on_deathlink(ctx)
elif cmd == "Retrieved":
if f"MM2_LAST_WILY_{ctx.team}_{ctx.slot}" in args["keys"]:
self.last_wily = args["keys"][f"MM2_LAST_WILY_{ctx.team}_{ctx.slot}"]
elif cmd == "Connected":
if self.energy_link:
ctx.set_notify(f"EnergyLink{ctx.team}")
if ctx.ui:
ctx.ui.enable_energy_link()
async def send_deathlink(self, ctx: "BizHawkClientContext") -> None:
self.sending_death_link = True
ctx.last_death_link = time.time()
await ctx.send_death("Mega Man was defeated.")
def on_deathlink(self, ctx: "BizHawkClientContext") -> None:
ctx.last_death_link = time.time()
self.pending_death_link = True
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
from worlds._bizhawk import read, write
if ctx.server is None:
return
if ctx.slot is None:
return
# get our relevant bytes
robot_masters_unlocked, robot_masters_defeated, items_acquired, \
weapons_unlocked, items_unlocked, items_received, \
completed_stages, consumable_checks, \
e_tanks, lives, weapon_energy, health, difficulty, death_link_status, \
energy_link_packet, last_wily = await read(ctx.bizhawk_ctx, [
(MM2_ROBOT_MASTERS_UNLOCKED, 1, "RAM"),
(MM2_ROBOT_MASTERS_DEFEATED, 1, "RAM"),
(MM2_ITEMS_ACQUIRED, 1, "RAM"),
(MM2_WEAPONS_UNLOCKED, 1, "RAM"),
(MM2_ITEMS_UNLOCKED, 1, "RAM"),
(MM2_RECEIVED_ITEMS, 1, "RAM"),
(MM2_COMPLETED_STAGES, 0xE, "RAM"),
(MM2_CONSUMABLES, 52, "RAM"),
(MM2_E_TANKS, 1, "RAM"),
(MM2_LIVES, 1, "RAM"),
(MM2_WEAPON_ENERGY, 11, "RAM"),
(MM2_HEALTH, 1, "RAM"),
(MM2_DIFFICULTY, 1, "RAM"),
(MM2_DEATHLINK, 1, "RAM"),
(MM2_ENERGYLINK, 1, "RAM"),
(MM2_LAST_WILY, 1, "RAM"),
])
if difficulty[0] not in (0, 1):
return # Game is not initialized
if not ctx.finished_game and completed_stages[0xD] != 0:
# this sets on credits fade, no real better way to do this
await ctx.send_msgs([{
"cmd": "StatusUpdate",
"status": ClientStatus.CLIENT_GOAL
}])
writes = []
# deathlink
if self.death_link:
await ctx.update_death_link(self.death_link)
if self.pending_death_link:
writes.append((MM2_DEATHLINK, bytes([0x01]), "RAM"))
self.pending_death_link = False
self.sending_death_link = True
if "DeathLink" in ctx.tags and ctx.last_death_link + 1 < time.time():
if health[0] == 0x00 and not self.sending_death_link:
await self.send_deathlink(ctx)
elif health[0] != 0x00 and not death_link_status[0]:
self.sending_death_link = False
if self.last_wily != last_wily[0]:
if self.last_wily is None:
# revalidate last wily from data storage
await ctx.send_msgs([{"cmd": "Set", "key": f"MM2_LAST_WILY_{ctx.team}_{ctx.slot}", "operations": [
{"operation": "default", "value": 8}
]}])
await ctx.send_msgs([{"cmd": "Get", "keys": [f"MM2_LAST_WILY_{ctx.team}_{ctx.slot}"]}])
elif last_wily[0] == 0:
writes.append((MM2_LAST_WILY, self.last_wily.to_bytes(1, "little"), "RAM"))
else:
# correct our setting
self.last_wily = last_wily[0]
await ctx.send_msgs([{"cmd": "Set", "key": f"MM2_LAST_WILY_{ctx.team}_{ctx.slot}", "operations": [
{"operation": "replace", "value": self.last_wily}
]}])
# handle receiving items
recv_amount = items_received[0]
if recv_amount < len(ctx.items_received):
item = ctx.items_received[recv_amount]
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
color(ctx.item_names.lookup_in_game(item.item), 'red', 'bold'),
color(ctx.player_names[item.player], 'yellow'),
ctx.location_names.lookup_in_slot(item.location, item.player), recv_amount, len(ctx.items_received)))
if item.item & 0x130 == 0:
# Robot Master Weapon
new_weapons = weapons_unlocked[0] | (1 << ((item.item & 0xF) - 1))
writes.append((MM2_WEAPONS_UNLOCKED, new_weapons.to_bytes(1, 'little'), "RAM"))
writes.extend(get_sfx_writes(0x21))
elif item.item & 0x30 == 0:
# Robot Master Stage Access
new_stages = robot_masters_unlocked[0] & ~(1 << ((item.item & 0xF) - 1))
writes.append((MM2_ROBOT_MASTERS_UNLOCKED, new_stages.to_bytes(1, 'little'), "RAM"))
writes.extend(get_sfx_writes(0x3a))
writes.append((MM2_RBM_STROBE, b"\x01", "RAM"))
elif item.item & 0x20 == 0:
# Items
new_items = items_unlocked[0] | (1 << ((item.item & 0xF) - 1))
writes.append((MM2_ITEMS_UNLOCKED, new_items.to_bytes(1, 'little'), "RAM"))
writes.extend(get_sfx_writes(0x21))
else:
# append to the queue, so we handle it later
self.item_queue.append(item)
recv_amount += 1
writes.append((MM2_RECEIVED_ITEMS, recv_amount.to_bytes(1, 'little'), "RAM"))
if energy_link_packet[0]:
pickup = energy_link_packet[0]
if pickup in (0x76, 0x77):
# Health pickups
if pickup == 0x77:
value = 2
else:
value = 10
exchange_rate = HP_EXCHANGE_RATE
elif pickup in (0x78, 0x79):
# Weapon Energy
if pickup == 0x79:
value = 2
else:
value = 10
exchange_rate = WEAPON_EXCHANGE_RATE
elif pickup == 0x7B:
# 1-Up
value = 1
exchange_rate = ONEUP_EXCHANGE_RATE
else:
# if we managed to pickup something else, we should just fall through
value = 0
exchange_rate = 0
contribution = (value * exchange_rate) >> 1
if contribution:
await ctx.send_msgs([{
"cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations":
[{"operation": "add", "value": contribution},
{"operation": "max", "value": 0}]}])
logger.info(f"Deposited {contribution / HP_EXCHANGE_RATE} health into the pool.")
writes.append((MM2_ENERGYLINK, 0x00.to_bytes(1, "little"), "RAM"))
if self.weapon_energy:
# Weapon Energy
# We parse the whole thing to spread it as thin as possible
current_energy = self.weapon_energy
weapon_energy = bytearray(weapon_energy)
for i, weapon in zip(range(len(weapon_energy)), weapon_energy):
if weapon < 0x1C:
missing = 0x1C - weapon
if missing > self.weapon_energy:
missing = self.weapon_energy
self.weapon_energy -= missing
weapon_energy[i] = weapon + missing
if not self.weapon_energy:
writes.append((MM2_WEAPON_ENERGY, weapon_energy, "RAM"))
break
else:
if current_energy != self.weapon_energy:
writes.append((MM2_WEAPON_ENERGY, weapon_energy, "RAM"))
if self.health_energy or self.auto_heal:
# Health Energy
# We save this if the player has not taken any damage
current_health = health[0]
if 0 < current_health < 0x1C:
health_diff = 0x1C - current_health
if self.health_energy:
if health_diff > self.health_energy:
health_diff = self.health_energy
self.health_energy -= health_diff
else:
pool = ctx.stored_data.get(f"EnergyLink{ctx.team}", 0)
if health_diff * HP_EXCHANGE_RATE > pool:
health_diff = int(pool // HP_EXCHANGE_RATE)
await ctx.send_msgs([{
"cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations":
[{"operation": "add", "value": -health_diff * HP_EXCHANGE_RATE},
{"operation": "max", "value": 0}]}])
current_health += health_diff
writes.append((MM2_HEALTH, current_health.to_bytes(1, 'little'), "RAM"))
if self.refill_queue:
refill_type, refill_amount = self.refill_queue.pop()
if refill_type == MM2EnergyLinkType.Life:
exchange_rate = HP_EXCHANGE_RATE
elif refill_type == MM2EnergyLinkType.OneUP:
exchange_rate = ONEUP_EXCHANGE_RATE
else:
exchange_rate = WEAPON_EXCHANGE_RATE
pool = ctx.stored_data.get(f"EnergyLink{ctx.team}", 0)
request = exchange_rate * refill_amount
if request > pool:
logger.warning(
f"Not enough energy to fulfill the request. Maximum request: {pool // exchange_rate}")
else:
await ctx.send_msgs([{
"cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations":
[{"operation": "add", "value": -request},
{"operation": "max", "value": 0}]}])
if refill_type == MM2EnergyLinkType.Life:
refill_ptr = MM2_HEALTH
elif refill_type == MM2EnergyLinkType.OneUP:
refill_ptr = MM2_LIVES
else:
refill_ptr = MM2_WEAPON_ENERGY - 1 + refill_type
current_value = (await read(ctx.bizhawk_ctx, [(refill_ptr, 1, "RAM")]))[0][0]
new_value = min(0x1C if refill_type != MM2EnergyLinkType.OneUP else 99, current_value + refill_amount)
writes.append((refill_ptr, new_value.to_bytes(1, "little"), "RAM"))
if len(self.item_queue):
item = self.item_queue.pop(0)
idx = item.item & 0xF
if idx == 0:
# 1-Up
current_lives = lives[0]
if current_lives > 99:
self.item_queue.append(item)
else:
current_lives += 1
writes.append((MM2_LIVES, current_lives.to_bytes(1, 'little'), "RAM"))
writes.extend(get_sfx_writes(0x42))
elif idx == 1:
self.weapon_energy += 0xE
writes.extend(get_sfx_writes(0x28))
elif idx == 2:
self.health_energy += 0xE
writes.extend(get_sfx_writes(0x28))
elif idx == 3:
# E-Tank
# visuals only allow 4, but we're gonna go up to 9 anyway? May change
current_tanks = e_tanks[0]
if current_tanks < 9:
current_tanks += 1
writes.append((MM2_E_TANKS, current_tanks.to_bytes(1, 'little'), "RAM"))
writes.extend(get_sfx_writes(0x42))
else:
self.item_queue.append(item)
await write(ctx.bizhawk_ctx, writes)
new_checks = []
# check for locations
for i in range(8):
flag = 1 << i
if robot_masters_defeated[0] & flag:
wep_id = 0x880101 + i
if wep_id not in ctx.checked_locations:
new_checks.append(wep_id)
for i in range(3):
flag = 1 << i
if items_acquired[0] & flag:
itm_id = 0x880111 + i
if itm_id not in ctx.checked_locations:
new_checks.append(itm_id)
for i in range(0xD):
rbm_id = 0x880001 + i
if completed_stages[i] != 0:
if rbm_id not in ctx.checked_locations:
new_checks.append(rbm_id)
for consumable in MM2_CONSUMABLE_TABLE:
if consumable not in ctx.checked_locations:
is_checked = consumable_checks[MM2_CONSUMABLE_TABLE[consumable][0]] \
& MM2_CONSUMABLE_TABLE[consumable][1]
if is_checked:
new_checks.append(consumable)
for new_check_id in new_checks:
ctx.locations_checked.add(new_check_id)
location = ctx.location_names.lookup_in_game(new_check_id)
nes_logger.info(
f'New Check: {location} ({len(ctx.locations_checked)}/'
f'{len(ctx.missing_locations) + len(ctx.checked_locations)})')
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}])

276
worlds/mm2/color.py Normal file
View File

@@ -0,0 +1,276 @@
from typing import Dict, Tuple, List, TYPE_CHECKING, Union
from . import names
from zlib import crc32
import struct
import logging
if TYPE_CHECKING:
from . import MM2World
from .rom import MM2ProcedurePatch
HTML_TO_NES: Dict[str, int] = {
"SNOW": 0x20,
"LINEN": 0x36,
"SEASHELL": 0x36,
"AZURE": 0x3C,
"LAVENDER": 0x33,
"WHITE": 0x30,
"BLACK": 0x0F,
"GREY": 0x00,
"GRAY": 0x00,
"ROYALBLUE": 0x12,
"BLUE": 0x11,
"SKYBLUE": 0x21,
"LIGHTBLUE": 0x31,
"TURQUOISE": 0x2B,
"CYAN": 0x2C,
"AQUAMARINE": 0x3B,
"DARKGREEN": 0x0A,
"GREEN": 0x1A,
"YELLOW": 0x28,
"GOLD": 0x28,
"WHEAT": 0x37,
"TAN": 0x37,
"CHOCOLATE": 0x07,
"BROWN": 0x07,
"SALMON": 0x26,
"ORANGE": 0x27,
"CORAL": 0x36,
"TOMATO": 0x16,
"RED": 0x16,
"PINK": 0x25,
"MAROON": 0x06,
"MAGENTA": 0x24,
"FUSCHIA": 0x24,
"VIOLET": 0x24,
"PLUM": 0x33,
"PURPLE": 0x14,
"THISTLE": 0x34,
"DARKBLUE": 0x01,
"SILVER": 0x10,
"NAVY": 0x02,
"TEAL": 0x1C,
"OLIVE": 0x18,
"LIME": 0x2A,
"AQUA": 0x2C,
# can add more as needed
}
MM2_COLORS: Dict[str, Tuple[int, int]] = {
names.atomic_fire: (0x28, 0x15),
names.air_shooter: (0x20, 0x11),
names.leaf_shield: (0x20, 0x19),
names.bubble_lead: (0x20, 0x00),
names.time_stopper: (0x34, 0x25),
names.quick_boomerang: (0x34, 0x14),
names.metal_blade: (0x37, 0x18),
names.crash_bomber: (0x20, 0x26),
names.item_1: (0x20, 0x16),
names.item_2: (0x20, 0x16),
names.item_3: (0x20, 0x16),
names.heat_man_stage: (0x28, 0x15),
names.air_man_stage: (0x28, 0x11),
names.wood_man_stage: (0x36, 0x17),
names.bubble_man_stage: (0x30, 0x19),
names.quick_man_stage: (0x28, 0x15),
names.flash_man_stage: (0x30, 0x12),
names.metal_man_stage: (0x28, 0x15),
names.crash_man_stage: (0x30, 0x16)
}
MM2_KNOWN_COLORS: Dict[str, Tuple[int, int]] = {
**MM2_COLORS,
# Street Fighter, technically
"Hadouken": (0x3C, 0x11),
"Shoryuken": (0x38, 0x16),
# X Series
"Z-Saber": (0x20, 0x16),
# X1
"Homing Torpedo": (0x3D, 0x37),
"Chameleon Sting": (0x3B, 0x1A),
"Rolling Shield": (0x3A, 0x25),
"Fire Wave": (0x37, 0x26),
"Storm Tornado": (0x34, 0x14),
"Electric Spark": (0x3D, 0x28),
"Boomerang Cutter": (0x3B, 0x2D),
"Shotgun Ice": (0x28, 0x2C),
# X2
"Crystal Hunter": (0x33, 0x21),
"Bubble Splash": (0x35, 0x28),
"Spin Wheel": (0x34, 0x1B),
"Silk Shot": (0x3B, 0x27),
"Sonic Slicer": (0x27, 0x01),
"Strike Chain": (0x30, 0x23),
"Magnet Mine": (0x28, 0x2D),
"Speed Burner": (0x31, 0x16),
# X3
"Acid Burst": (0x28, 0x2A),
"Tornado Fang": (0x28, 0x2C),
"Triad Thunder": (0x2B, 0x23),
"Spinning Blade": (0x20, 0x16),
"Ray Splasher": (0x28, 0x17),
"Gravity Well": (0x38, 0x14),
"Parasitic Bomb": (0x31, 0x28),
"Frost Shield": (0x23, 0x2C),
}
palette_pointers: Dict[str, List[int]] = {
"Mega Buster": [0x3D314],
"Atomic Fire": [0x3D318],
"Air Shooter": [0x3D31C],
"Leaf Shield": [0x3D320],
"Bubble Lead": [0x3D324],
"Quick Boomerang": [0x3D328],
"Time Stopper": [0x3D32C],
"Metal Blade": [0x3D330],
"Crash Bomber": [0x3D334],
"Item 1": [0x3D338],
"Item 2": [0x3D33C],
"Item 3": [0x3D340],
"Heat Man": [0x34B6, 0x344F7],
"Air Man": [0x74B6, 0x344FF],
"Wood Man": [0xB4EC, 0x34507],
"Bubble Man": [0xF4B6, 0x3450F],
"Quick Man": [0x134C8, 0x34517],
"Flash Man": [0x174B6, 0x3451F],
"Metal Man": [0x1B4A4, 0x34527],
"Crash Man": [0x1F4EC, 0x3452F],
}
def add_color_to_mm2(name: str, color: Tuple[int, int]) -> None:
"""
Add a color combo for Mega Man 2 to recognize as the color to display for a given item.
For information on available colors: https://www.nesdev.org/wiki/PPU_palettes#2C02
"""
MM2_KNOWN_COLORS[name] = validate_colors(*color)
def extrapolate_color(color: int) -> Tuple[int, int]:
if color > 0x1F:
color_1 = color
color_2 = color_1 - 0x10
else:
color_2 = color
color_1 = color_2 + 0x10
return color_1, color_2
def validate_colors(color_1: int, color_2: int, allow_match: bool = False) -> Tuple[int, int]:
# Black should be reserved for outlines, a gray should suffice
if color_1 in [0x0D, 0x0E, 0x0F, 0x1E, 0x2E, 0x3E, 0x1F, 0x2F, 0x3F]:
color_1 = 0x10
if color_2 in [0x0D, 0x0E, 0x0F, 0x1E, 0x2E, 0x3E, 0x1F, 0x2F, 0x3F]:
color_2 = 0x10
# one final check, make sure we don't have two matching
if not allow_match and color_1 == color_2:
color_1 = 0x30 # color 1 to white works with about any paired color
return color_1, color_2
def get_colors_for_item(name: str) -> Tuple[int, int]:
if name in MM2_KNOWN_COLORS:
return MM2_KNOWN_COLORS[name]
check_colors = {color: color in name.upper().replace(" ", "") for color in HTML_TO_NES}
colors = [color for color in check_colors if check_colors[color]]
if colors:
# we have at least one color pattern matched
if len(colors) > 1:
# we have at least 2
color_1 = HTML_TO_NES[colors[0]]
color_2 = HTML_TO_NES[colors[1]]
else:
color_1, color_2 = extrapolate_color(HTML_TO_NES[colors[0]])
else:
# generate hash
crc_hash = crc32(name.encode("utf-8"))
hash_color = struct.pack("I", crc_hash)
color_1 = hash_color[0] % 0x3F
color_2 = hash_color[1] % 0x3F
if color_1 < color_2:
temp = color_1
color_1 = color_2
color_2 = temp
color_1, color_2 = validate_colors(color_1, color_2)
return color_1, color_2
def parse_color(colors: List[str]) -> Tuple[int, int]:
color_a = colors[0]
if color_a.startswith("$"):
color_1 = int(color_a[1:], 16)
else:
# assume it's in our list of colors
color_1 = HTML_TO_NES[color_a.upper()]
if len(colors) == 1:
color_1, color_2 = extrapolate_color(color_1)
else:
color_b = colors[1]
if color_b.startswith("$"):
color_2 = int(color_b[1:], 16)
else:
color_2 = HTML_TO_NES[color_b.upper()]
return color_1, color_2
def write_palette_shuffle(world: "MM2World", rom: "MM2ProcedurePatch") -> None:
palette_shuffle: Union[int, str] = world.options.palette_shuffle.value
palettes_to_write: Dict[str, Tuple[int, int]] = {}
if isinstance(palette_shuffle, str):
color_sets = palette_shuffle.split(";")
if len(color_sets) == 1:
palette_shuffle = world.options.palette_shuffle.option_none
# singularity is more correct, but this is faster
else:
palette_shuffle = world.options.palette_shuffle.options[color_sets.pop()]
for color_set in color_sets:
if "-" in color_set:
character, color = color_set.split("-")
if character.title() not in palette_pointers:
logging.warning(f"Player {world.multiworld.get_player_name(world.player)} "
f"attempted to set color for unrecognized option {character}")
colors = color.split("|")
real_colors = validate_colors(*parse_color(colors), allow_match=True)
palettes_to_write[character.title()] = real_colors
else:
# If color is provided with no character, assume singularity
colors = color_set.split("|")
real_colors = validate_colors(*parse_color(colors), allow_match=True)
for character in palette_pointers:
palettes_to_write[character] = real_colors
# Now we handle the real values
if palette_shuffle == 1:
shuffled_colors = list(MM2_COLORS.values())
shuffled_colors.append((0x2C, 0x11)) # Mega Buster
world.random.shuffle(shuffled_colors)
for character in palette_pointers:
if character not in palettes_to_write:
palettes_to_write[character] = shuffled_colors.pop()
elif palette_shuffle > 1:
if palette_shuffle == 2:
for character in palette_pointers:
if character not in palettes_to_write:
real_colors = validate_colors(world.random.randint(0, 0x3F), world.random.randint(0, 0x3F))
palettes_to_write[character] = real_colors
else:
# singularity
real_colors = validate_colors(world.random.randint(0, 0x3F), world.random.randint(0, 0x3F))
for character in palette_pointers:
if character not in palettes_to_write:
palettes_to_write[character] = real_colors
for character in palettes_to_write:
for pointer in palette_pointers[character]:
rom.write_bytes(pointer, bytes(palettes_to_write[character]))
if character == "Atomic Fire":
# special case, we need to update Atomic Fire's flashing routine
rom.write_byte(0x3DE4A, palettes_to_write[character][1])
rom.write_byte(0x3DE4C, palettes_to_write[character][1])

Binary file not shown.

View File

@@ -0,0 +1,114 @@
# Mega Man 2
## Where is the options page?
The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
Weapons received from Robot Masters, access to each individual stage, and Items from Dr. Light are randomized
into the multiworld. Access to the Wily Stages is locked behind receiving Item 1, 2, and 3. The game is completed when
viewing the ending sequence after defeating the Alien.
## What Mega Man 2 items can appear in other players' worlds?
- Robot Master weapons
- Robot Master Access Codes (stage access)
- Items 1/2/3
- 1-Ups
- E-Tanks
- Health Energy (L)
- Weapon Energy (L)
## What is considered a location check in Mega Man 2?
- The defeat of a Robot Master or Wily Boss
- Receiving a weapon or item from Dr. Light
- Optionally, 1-Ups and E-Tanks present within stages
- Optionally, Weapon and Health Energy pickups present within stages
## When the player receives an item, what happens?
A sound effect will play based on the type of item received, and the effects of the item will be immediately applied,
such as unlocking the use of a weapon mid-stage. If the effects of the item cannot be fully applied (such as receiving
Health Energy while at full health), the leftover amount is withheld until it can be applied.
## What is EnergyLink?
EnergyLink is an energy storage supported by certain games that is shared across all worlds in a multiworld. In Mega Man
2, when enabled, drops from enemies are not applied directly to Mega Man and are instead deposited into the EnergyLink.
Half of the energy that would be gained is lost upon transfer to the EnergyLink.
Energy from the EnergyLink storage can be converted into health, weapon energy, and lives at different conversion rates.
You can find out how much of each type you can pull using the `/pool` command in the client. Additionally, you can have it
automatically pull from the EnergyLink storage to keep Mega Man healed using the `/autoheal` command in the client.
Finally, you can use the `/request` command to request a certain type of energy from the storage.
## Plando Palettes
The palette shuffle option supports specifying a specific palette for a given weapon/Robot Master. The format for doing
so is `Character-Color1|Color2;Option`. Character is the individual that this should apply to, and can only be one of
the following:
- Mega Buster
- Atomic Fire
- Air Shooter
- Leaf Shield
- Bubble Lead
- Quick Boomerang
- Time Stopper
- Metal Blade
- Crash Bomber
- Item 1
- Item 2
- Item 3
- Heat Man
- Air Man
- Wood Man
- Bubble Man
- Quick Man
- Flash Man
- Metal Man
- Crash Man
Colors attempt to map a list of HTML-defined colors to what the NES can render. A full list of applicable colors can be
found [here](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/mm2/color.py#L11). Alternatively, colors can
be supplied directly using `$xx` format. A full list of NES colors can be found [here](https://www.nesdev.org/wiki/PPU_palettes#2C02).
You can also pass only one color (such as `Mega Buster-Red`) and it will interpret a second color based off of the color
given. Additionally, passing only colors (such as `Red|Blue`) and not any specific boss/weapon will apply that color to
all weapons/bosses that did not have a prior color specified.
The option is the method to be used to set the palettes of the remaining bosses/weapons, and will not overwrite any
plando placements.
## Plando Weaknesses
Plando Weaknesses allows you to override the amount of damage a boss should take from a given weapon, ignoring prior
weaknesses generated by strict/random weakness options. Formatting for this is as follows:
```yaml
plando_weakness:
Air Man:
Atomic Fire: 0
Bubble Lead: 4
```
This would cause Air Man to take 4 damage from Bubble Lead, and 0 from Atomic Fire.
Note: it is possible that plando weakness is not be respected should the plando create a situation in which the game
becomes impossible to complete. In this situation, the damage would be boosted to the minimum required to defeat the
Robot Master.
## Unique Local Commands
- `/pool` Only present with EnergyLink, prints the max amount of each type of request that could be fulfilled.
- `/autoheal` Only present with EnergyLink, will automatically drain energy from the EnergyLink in order to
restore Mega Man's health.
- `/request <amount> <type>` Only present with EnergyLink, sends a request of a certain type of energy to be pulled from
the EnergyLink. Types are as follows:
- `HP` Health
- `AF` Atomic Fire
- `AS` Air Shooter
- `LS` Leaf Shield
- `BL` Bubble Lead
- `QB` Quick Boomerang
- `TS` Time Stopper
- `MB` Metal Blade
- `CB` Crash Bomber
- `I1` Item 1
- `I2` Item 2
- `I3` Item 3
- `1U` Lives

View File

@@ -0,0 +1,53 @@
# Mega Man 2 Setup Guide
## Required Software
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- An English Mega Man 2 ROM. Alternatively, the [Mega Man Legacy Collection](https://store.steampowered.com/app/363440/Mega_Man_Legacy_Collection/) on Steam.
- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 or later
### Configuring Bizhawk
Once you have installed BizHawk, open `EmuHawk.exe` and change the following settings:
- If you're using BizHawk 2.7 or 2.8, go to `Config > Customize`. On the Advanced tab, switch the Lua Core from
`NLua+KopiLua` to `Lua+LuaInterface`, then restart EmuHawk. (If you're using BizHawk 2.9, you can skip this step.)
- Under `Config > Customize`, check the "Run in background" option to prevent disconnecting from the client while you're
tabbed out of EmuHawk.
- Open a `.nes` file in EmuHawk and go to `Config > Controllers…` to configure your inputs. If you can't click
`Controllers…`, load any `.nes` ROM first.
- Consider clearing keybinds in `Config > Hotkeys…` if you don't intend to use them. Select the keybind and press Esc to
clear it.
## Generating and Patching a Game
1. Create your options file (YAML). You can make one on the
[Mega Man 2 options page](../../../games/Mega%20Man%202/player-options).
2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game).
This will generate an output file for you. Your patch file will have the `.apmm2` file extension.
3. Open `ArchipelagoLauncher.exe`
4. Select "Open Patch" on the left side and select your patch file.
5. If this is your first time patching, you will be prompted to locate your vanilla ROM. If you are using the Legacy
Collection, provide `Proteus.exe` in place of your rom.
6. A patched `.nes` file will be created in the same place as the patch file.
7. On your first time opening a patch with BizHawk Client, you will also be asked to locate `EmuHawk.exe` in your
BizHawk install.
## Connecting to a Server
By default, opening a patch file will do steps 1-5 below for you automatically. Even so, keep them in your memory just
in case you have to close and reopen a window mid-game for some reason.
1. Mega Man 2 uses Archipelago's BizHawk Client. If the client isn't still open from when you patched your game,
you can re-open it from the launcher.
2. Ensure EmuHawk is running the patched ROM.
3. In EmuHawk, go to `Tools > Lua Console`. This window must stay open while playing.
4. In the Lua Console window, go to `Script > Open Script…`.
5. Navigate to your Archipelago install folder and open `data/lua/connector_bizhawk_generic.lua`.
6. The emulator and client will eventually connect to each other. The BizHawk Client window should indicate that it
connected and recognized Mega Man 2.
7. To connect the client to the server, enter your room's address and port (e.g. `archipelago.gg:38281`) into the
top text field of the client and click Connect.
You should now be able to receive and send items. You'll need to do these steps every time you want to reconnect. It is
perfectly safe to make progress offline; everything will re-sync when you reconnect.

72
worlds/mm2/items.py Normal file
View File

@@ -0,0 +1,72 @@
from BaseClasses import Item
from typing import NamedTuple, Dict
from . import names
class ItemData(NamedTuple):
code: int
progression: bool
useful: bool = False # primarily use this for incredibly useful items of their class, like Metal Blade
skip_balancing: bool = False
class MM2Item(Item):
game = "Mega Man 2"
robot_master_weapon_table = {
names.atomic_fire: ItemData(0x880001, True),
names.air_shooter: ItemData(0x880002, True),
names.leaf_shield: ItemData(0x880003, True),
names.bubble_lead: ItemData(0x880004, True),
names.quick_boomerang: ItemData(0x880005, True),
names.time_stopper: ItemData(0x880006, True, True),
names.metal_blade: ItemData(0x880007, True, True),
names.crash_bomber: ItemData(0x880008, True),
}
stage_access_table = {
names.heat_man_stage: ItemData(0x880101, True),
names.air_man_stage: ItemData(0x880102, True),
names.wood_man_stage: ItemData(0x880103, True),
names.bubble_man_stage: ItemData(0x880104, True),
names.quick_man_stage: ItemData(0x880105, True),
names.flash_man_stage: ItemData(0x880106, True),
names.metal_man_stage: ItemData(0x880107, True),
names.crash_man_stage: ItemData(0x880108, True),
}
item_item_table = {
names.item_1: ItemData(0x880011, True, True, True),
names.item_2: ItemData(0x880012, True, True, True),
names.item_3: ItemData(0x880013, True, True, True)
}
filler_item_table = {
names.one_up: ItemData(0x880020, False),
names.weapon_energy: ItemData(0x880021, False),
names.health_energy: ItemData(0x880022, False),
names.e_tank: ItemData(0x880023, False, True),
}
filler_item_weights = {
names.one_up: 1,
names.weapon_energy: 4,
names.health_energy: 1,
names.e_tank: 2,
}
item_table = {
**robot_master_weapon_table,
**stage_access_table,
**item_item_table,
**filler_item_table,
}
item_names = {
"Weapons": {name for name in robot_master_weapon_table.keys()},
"Stages": {name for name in stage_access_table.keys()},
"Items": {name for name in item_item_table.keys()}
}
lookup_item_to_id: Dict[str, int] = {item_name: data.code for item_name, data in item_table.items()}

239
worlds/mm2/locations.py Normal file
View File

@@ -0,0 +1,239 @@
from BaseClasses import Location, Region
from typing import Dict, Tuple, Optional
from . import names
class MM2Location(Location):
game = "Mega Man 2"
class MM2Region(Region):
game = "Mega Man 2"
heat_man_locations: Dict[str, Optional[int]] = {
names.heat_man: 0x880001,
names.atomic_fire_get: 0x880101,
names.item_1_get: 0x880111,
}
air_man_locations: Dict[str, Optional[int]] = {
names.air_man: 0x880002,
names.air_shooter_get: 0x880102,
names.item_2_get: 0x880112
}
wood_man_locations: Dict[str, Optional[int]] = {
names.wood_man: 0x880003,
names.leaf_shield_get: 0x880103
}
bubble_man_locations: Dict[str, Optional[int]] = {
names.bubble_man: 0x880004,
names.bubble_lead_get: 0x880104
}
quick_man_locations: Dict[str, Optional[int]] = {
names.quick_man: 0x880005,
names.quick_boomerang_get: 0x880105,
}
flash_man_locations: Dict[str, Optional[int]] = {
names.flash_man: 0x880006,
names.time_stopper_get: 0x880106,
names.item_3_get: 0x880113,
}
metal_man_locations: Dict[str, Optional[int]] = {
names.metal_man: 0x880007,
names.metal_blade_get: 0x880107
}
crash_man_locations: Dict[str, Optional[int]] = {
names.crash_man: 0x880008,
names.crash_bomber_get: 0x880108
}
wily_1_locations: Dict[str, Optional[int]] = {
names.wily_1: 0x880009,
names.wily_stage_1: None
}
wily_2_locations: Dict[str, Optional[int]] = {
names.wily_2: 0x88000A,
names.wily_stage_2: None
}
wily_3_locations: Dict[str, Optional[int]] = {
names.wily_3: 0x88000B,
names.wily_stage_3: None
}
wily_4_locations: Dict[str, Optional[int]] = {
names.wily_4: 0x88000C,
names.wily_stage_4: None
}
wily_5_locations: Dict[str, Optional[int]] = {
names.wily_5: 0x88000D,
names.wily_stage_5: None
}
wily_6_locations: Dict[str, Optional[int]] = {
names.dr_wily: None
}
etank_1ups: Dict[str, Dict[str, Optional[int]]] = {
"Heat Man Stage": {
names.heat_man_c1: 0x880201,
},
"Quick Man Stage": {
names.quick_man_c1: 0x880202,
names.quick_man_c2: 0x880203,
names.quick_man_c3: 0x880204,
names.quick_man_c7: 0x880208,
},
"Flash Man Stage": {
names.flash_man_c2: 0x88020B,
names.flash_man_c6: 0x88020F,
},
"Metal Man Stage": {
names.metal_man_c1: 0x880210,
names.metal_man_c2: 0x880211,
names.metal_man_c3: 0x880212,
},
"Crash Man Stage": {
names.crash_man_c2: 0x880214,
names.crash_man_c3: 0x880215,
},
"Wily Stage 1": {
names.wily_1_c1: 0x880216,
},
"Wily Stage 2": {
names.wily_2_c3: 0x88021A,
names.wily_2_c4: 0x88021B,
names.wily_2_c5: 0x88021C,
names.wily_2_c6: 0x88021D,
},
"Wily Stage 3": {
names.wily_3_c2: 0x880220,
},
"Wily Stage 4": {
names.wily_4_c3: 0x880225,
names.wily_4_c4: 0x880226,
}
}
energy_pickups: Dict[str, Dict[str, Optional[int]]] = {
"Quick Man Stage": {
names.quick_man_c4: 0x880205,
names.quick_man_c5: 0x880206,
names.quick_man_c6: 0x880207,
names.quick_man_c8: 0x880209,
},
"Flash Man Stage": {
names.flash_man_c1: 0x88020A,
names.flash_man_c3: 0x88020C,
names.flash_man_c4: 0x88020D,
names.flash_man_c5: 0x88020E,
},
"Crash Man Stage": {
names.crash_man_c1: 0x880213,
},
"Wily Stage 1": {
names.wily_1_c2: 0x880217,
},
"Wily Stage 2": {
names.wily_2_c1: 0x880218,
names.wily_2_c2: 0x880219,
names.wily_2_c7: 0x88021E,
names.wily_2_c8: 0x880227,
names.wily_2_c9: 0x880228,
names.wily_2_c10: 0x880229,
names.wily_2_c11: 0x88022A,
names.wily_2_c12: 0x88022B,
names.wily_2_c13: 0x88022C,
names.wily_2_c14: 0x88022D,
names.wily_2_c15: 0x88022E,
names.wily_2_c16: 0x88022F,
},
"Wily Stage 3": {
names.wily_3_c1: 0x88021F,
names.wily_3_c3: 0x880221,
names.wily_3_c4: 0x880222,
},
"Wily Stage 4": {
names.wily_4_c1: 0x880223,
names.wily_4_c2: 0x880224,
}
}
mm2_regions: Dict[str, Tuple[Tuple[str, ...], Dict[str, Optional[int]], Optional[str]]] = {
"Heat Man Stage": ((names.heat_man_stage,), heat_man_locations, None),
"Air Man Stage": ((names.air_man_stage,), air_man_locations, None),
"Wood Man Stage": ((names.wood_man_stage,), wood_man_locations, None),
"Bubble Man Stage": ((names.bubble_man_stage,), bubble_man_locations, None),
"Quick Man Stage": ((names.quick_man_stage,), quick_man_locations, None),
"Flash Man Stage": ((names.flash_man_stage,), flash_man_locations, None),
"Metal Man Stage": ((names.metal_man_stage,), metal_man_locations, None),
"Crash Man Stage": ((names.crash_man_stage,), crash_man_locations, None),
"Wily Stage 1": ((names.item_1, names.item_2, names.item_3), wily_1_locations, None),
"Wily Stage 2": ((names.wily_stage_1,), wily_2_locations, "Wily Stage 1"),
"Wily Stage 3": ((names.wily_stage_2,), wily_3_locations, "Wily Stage 2"),
"Wily Stage 4": ((names.wily_stage_3,), wily_4_locations, "Wily Stage 3"),
"Wily Stage 5": ((names.wily_stage_4,), wily_5_locations, "Wily Stage 4"),
"Wily Stage 6": ((names.wily_stage_5,), wily_6_locations, "Wily Stage 5")
}
location_table: Dict[str, Optional[int]] = {
**heat_man_locations,
**air_man_locations,
**wood_man_locations,
**bubble_man_locations,
**quick_man_locations,
**flash_man_locations,
**metal_man_locations,
**crash_man_locations,
**wily_1_locations,
**wily_2_locations,
**wily_3_locations,
**wily_4_locations,
**wily_5_locations,
}
for table in etank_1ups:
location_table.update(etank_1ups[table])
for table in energy_pickups:
location_table.update(energy_pickups[table])
location_groups = {
"Get Equipped": {
names.atomic_fire_get,
names.air_shooter_get,
names.leaf_shield_get,
names.bubble_lead_get,
names.quick_boomerang_get,
names.time_stopper_get,
names.metal_blade_get,
names.crash_bomber_get,
names.item_1_get,
names.item_2_get,
names.item_3_get
},
"Heat Man Stage": {*heat_man_locations.keys(), *etank_1ups["Heat Man Stage"].keys()},
"Air Man Stage": {*air_man_locations.keys()},
"Wood Man Stage": {*wood_man_locations.keys()},
"Bubble Man Stage": {*bubble_man_locations.keys()},
"Quick Man Stage": {*quick_man_locations.keys(), *etank_1ups["Quick Man Stage"].keys(),
*energy_pickups["Quick Man Stage"].keys()},
"Flash Man Stage": {*flash_man_locations.keys(), *etank_1ups["Flash Man Stage"].keys(),
*energy_pickups["Flash Man Stage"].keys()},
"Metal Man Stage": {*metal_man_locations.keys(), *etank_1ups["Metal Man Stage"].keys()},
"Crash Man Stage": {*crash_man_locations.keys(), *etank_1ups["Crash Man Stage"].keys(),
*energy_pickups["Crash Man Stage"].keys()},
"Wily 2 Weapon Energy": {names.wily_2_c8, names.wily_2_c9, names.wily_2_c10, names.wily_2_c11, names.wily_2_c12,
names.wily_2_c13, names.wily_2_c14, names.wily_2_c15, names.wily_2_c16}
}
lookup_location_to_id: Dict[str, int] = {location: idx for location, idx in location_table.items() if idx is not None}

114
worlds/mm2/names.py Normal file
View File

@@ -0,0 +1,114 @@
# Robot Master Weapons
crash_bomber = "Crash Bomber"
metal_blade = "Metal Blade"
quick_boomerang = "Quick Boomerang"
bubble_lead = "Bubble Lead"
atomic_fire = "Atomic Fire"
leaf_shield = "Leaf Shield"
time_stopper = "Time Stopper"
air_shooter = "Air Shooter"
# Stage Entry
crash_man_stage = "Crash Man Access Codes"
metal_man_stage = "Metal Man Access Codes"
quick_man_stage = "Quick Man Access Codes"
bubble_man_stage = "Bubble Man Access Codes"
heat_man_stage = "Heat Man Access Codes"
wood_man_stage = "Wood Man Access Codes"
flash_man_stage = "Flash Man Access Codes"
air_man_stage = "Air Man Access Codes"
# The Items
item_1 = "Item 1 - Propeller"
item_2 = "Item 2 - Rocket"
item_3 = "Item 3 - Bouncy"
# Misc. Items
one_up = "1-Up"
weapon_energy = "Weapon Energy (L)"
health_energy = "Health Energy (L)"
e_tank = "E-Tank"
# Locations
crash_man = "Crash Man - Defeated"
metal_man = "Metal Man - Defeated"
quick_man = "Quick Man - Defeated"
bubble_man = "Bubble Man - Defeated"
heat_man = "Heat Man - Defeated"
wood_man = "Wood Man - Defeated"
flash_man = "Flash Man - Defeated"
air_man = "Air Man - Defeated"
crash_bomber_get = "Crash Bomber - Received"
metal_blade_get = "Metal Blade - Received"
quick_boomerang_get = "Quick Boomerang - Received"
bubble_lead_get = "Bubble Lead - Received"
atomic_fire_get = "Atomic Fire - Received"
leaf_shield_get = "Leaf Shield - Received"
time_stopper_get = "Time Stopper - Received"
air_shooter_get = "Air Shooter - Received"
item_1_get = "Item 1 - Received"
item_2_get = "Item 2 - Received"
item_3_get = "Item 3 - Received"
wily_1 = "Mecha Dragon - Defeated"
wily_2 = "Picopico-kun - Defeated"
wily_3 = "Guts Tank - Defeated"
wily_4 = "Boobeam Trap - Defeated"
wily_5 = "Wily Machine 2 - Defeated"
dr_wily = "Dr. Wily (Alien) - Defeated"
# Wily Stage Event Items
wily_stage_1 = "Wily Stage 1 - Completed"
wily_stage_2 = "Wily Stage 2 - Completed"
wily_stage_3 = "Wily Stage 3 - Completed"
wily_stage_4 = "Wily Stage 4 - Completed"
wily_stage_5 = "Wily Stage 5 - Completed"
# Consumable Locations
heat_man_c1 = "Heat Man Stage - 1-Up" # 3, requires Yoku jumps or Item 2
flash_man_c1 = "Flash Man Stage - Health Energy 1" # 0
flash_man_c2 = "Flash Man Stage - 1-Up" # 2, requires any Item
flash_man_c3 = "Flash Man Stage - Health Energy 2" # 6, requires Crash Bomber
flash_man_c4 = "Flash Man Stage - Weapon Energy 1" # 8, requires Crash Bomber
flash_man_c5 = "Flash Man Stage - Health Energy 3" # 9
flash_man_c6 = "Flash Man Stage - E-Tank" # 10
quick_man_c1 = "Quick Man Stage - 1-Up 1" # 0, needs any Item
quick_man_c2 = "Quick Man Stage - E-Tank" # 1, requires allow lasers or Time Stopper
quick_man_c3 = "Quick Man Stage - 1-Up 2" # 2, requires allow lasers or Time Stopper
quick_man_c4 = "Quick Man Stage - Weapon Energy 1" # 3, requires allow lasers or Time Stopper
quick_man_c5 = "Quick Man Stage - Weapon Energy 2" # 4, requires allow lasers or Time Stopper
quick_man_c6 = "Quick Man Stage - Health Energy" # 5, requires allow lasers or Time Stopper
quick_man_c7 = "Quick Man Stage - 1-Up 3" # 6, requires allow lasers or Time Stopper
quick_man_c8 = "Quick Man Stage - Weapon Energy 3" # 7, requires allow lasers or Time Stopper
metal_man_c1 = "Metal Man Stage - E-Tank 1" # 0
metal_man_c2 = "Metal Man Stage - 1-Up" # 1, needs Item 1/2
metal_man_c3 = "Metal Man Stage - E-Tank 2" # 2, needs Item 1/2 (without putting dying in logic at least)
crash_man_c1 = "Crash Man Stage - Health Energy" # 0
crash_man_c2 = "Crash Man Stage - E-Tank" # 1
crash_man_c3 = "Crash Man Stage - 1-Up" # 2, any Item
wily_1_c1 = "Wily Stage 1 - 1-Up" # 10
wily_1_c2 = "Wily Stage 1 - Weapon Energy 1" # 11
wily_2_c1 = "Wily Stage 2 - Weapon Energy 1" # 11
wily_2_c2 = "Wily Stage 2 - Weapon Energy 2" # 12
wily_2_c3 = "Wily Stage 2 - E-Tank 1" # 16
wily_2_c4 = "Wily Stage 2 - 1-Up 1" # 17
# 18 - 27 are all small weapon energies, might force these local junk?
wily_2_c8 = "Wily Stage 2 - Weapon Energy 3" # 18
wily_2_c9 = "Wily Stage 2 - Weapon Energy 4" # 19
wily_2_c10 = "Wily Stage 2 - Weapon Energy 5" # 20
wily_2_c11 = "Wily Stage 2 - Weapon Energy 6" # 21
wily_2_c12 = "Wily Stage 2 - Weapon Energy 7" # 22
wily_2_c13 = "Wily Stage 2 - Weapon Energy 8" # 23
wily_2_c14 = "Wily Stage 2 - Weapon Energy 9" # 24
wily_2_c15 = "Wily Stage 2 - Weapon Energy 10" # 25
wily_2_c16 = "Wily Stage 2 - Weapon Energy 11" # 26
wily_2_c5 = "Wily Stage 2 - 1-Up 2" # 29, requires Crash Bomber
wily_2_c6 = "Wily Stage 2 - E-Tank 2" # 30, requires Crash Bomber
wily_2_c7 = "Wily Stage 2 - Health Energy" # 31, item 2 (already required to reach wily 2)
wily_3_c1 = "Wily Stage 3 - Weapon Energy 1" # 12, requires Crash Bomber
wily_3_c2 = "Wily Stage 3 - E-Tank" # 17, requires Crash Bomber
wily_3_c3 = "Wily Stage 3 - Weapon Energy 2" # 18
wily_3_c4 = "Wily Stage 3 - Weapon Energy 3" # 19
wily_4_c1 = "Wily Stage 4 - Weapon Energy 1" # 16
wily_4_c2 = "Wily Stage 4 - Weapon Energy 2" # 17
wily_4_c3 = "Wily Stage 4 - 1-Up 1" # 18
wily_4_c4 = "Wily Stage 4 - E-Tank 1" # 19

229
worlds/mm2/options.py Normal file
View File

@@ -0,0 +1,229 @@
from dataclasses import dataclass
from Options import Choice, Toggle, DeathLink, DefaultOnToggle, TextChoice, Range, OptionDict, PerGameCommonOptions
from schema import Schema, And, Use, Optional
bosses = {
"Heat Man": 0,
"Air Man": 1,
"Wood Man": 2,
"Bubble Man": 3,
"Quick Man": 4,
"Flash Man": 5,
"Metal Man": 6,
"Crash Man": 7,
"Mecha Dragon": 8,
"Picopico-kun": 9,
"Guts Tank": 10,
"Boobeam Trap": 11,
"Wily Machine 2": 12,
"Alien": 13
}
weapons_to_id = {
"Mega Buster": 0,
"Atomic Fire": 1,
"Air Shooter": 2,
"Leaf Shield": 3,
"Bubble Lead": 4,
"Quick Boomerang": 5,
"Metal Blade": 7,
"Crash Bomber": 6,
"Time Stopper": 8,
}
class EnergyLink(Toggle):
"""
Enables EnergyLink support.
When enabled, pickups dropped from enemies are sent to the EnergyLink pool, and healing/weapon energy/1-Ups can
be requested from the EnergyLink pool.
Some of the energy sent to the pool will be lost on transfer.
"""
display_name = "EnergyLink"
class StartingRobotMaster(Choice):
"""
The initial stage unlocked at the start.
"""
display_name = "Starting Robot Master"
option_heat_man = 0
option_air_man = 1
option_wood_man = 2
option_bubble_man = 3
option_quick_man = 4
option_flash_man = 5
option_metal_man = 6
option_crash_man = 7
default = "random"
class YokuJumps(Toggle):
"""
When enabled, the player is expected to be able to perform the yoku block sequence in Heat Man's
stage without Item 2.
"""
display_name = "Yoku Block Jumps"
class EnableLasers(Toggle):
"""
When enabled, the player is expected to complete (and acquire items within) the laser sections of Quick Man's
stage without the Time Stopper.
"""
display_name = "Enable Lasers"
class Consumables(Choice):
"""
When enabled, e-tanks/1-ups/health/weapon energy will be added to the pool of items and included as checks.
E-Tanks and 1-Ups add 20 checks to the pool.
Weapon/Health Energy add 27 checks to the pool.
"""
display_name = "Consumables"
option_none = 0
option_1up_etank = 1
option_weapon_health = 2
option_all = 3
default = 1
alias_true = 3
alias_false = 0
@classmethod
def get_option_name(cls, value: int) -> str:
if value == 1:
return "1-Ups/E-Tanks"
if value == 2:
return "Weapon/Health Energy"
return super().get_option_name(value)
class Quickswap(DefaultOnToggle):
"""
When enabled, the player can quickswap through all received weapons by pressing Select.
"""
display_name = "Quickswap"
class PaletteShuffle(TextChoice):
"""
Change the color of Mega Man and the Robot Masters.
None: The palettes are unchanged.
Shuffled: Palette colors are shuffled amongst the robot masters.
Randomized: Random (usually good) palettes are generated for each robot master.
Singularity: one palette is generated and used for all robot masters.
Supports custom palettes using HTML named colors in the
following format: Mega Buster-Lavender|Violet;randomized
The first value is the character whose palette you'd like to define, then separated by - is a set of 2 colors for
that character. separate every color with a pipe, and separate every character as well as the remaining shuffle with
a semicolon.
"""
display_name = "Palette Shuffle"
option_none = 0
option_shuffled = 1
option_randomized = 2
option_singularity = 3
class EnemyWeaknesses(Toggle):
"""
Randomizes the damage dealt to enemies by weapons. Friender will always take damage from the buster.
"""
display_name = "Random Enemy Weaknesses"
class StrictWeaknesses(Toggle):
"""
Only your starting Robot Master will take damage from the Mega Buster, the rest must be defeated with weapons.
Weapons that only do 1-3 damage to bosses no longer deal damage (aside from Alien).
"""
display_name = "Strict Boss Weaknesses"
class RandomWeaknesses(Choice):
"""
None: Bosses will have their regular weaknesses.
Shuffled: Weapon damage will be shuffled amongst the weapons, so Metal Blade may do Bubble Lead damage.
Time Stopper will deplete half of a random Robot Master's HP.
Randomized: Weapon damage will be fully randomized.
"""
display_name = "Random Boss Weaknesses"
option_none = 0
option_shuffled = 1
option_randomized = 2
alias_false = 0
alias_true = 2
class Wily5Requirement(Range):
"""Change the number of Robot Masters that are required to be defeated for
the teleporter to the Wily Machine to appear."""
display_name = "Wily 5 Requirement"
default = 8
range_start = 1
range_end = 8
class WeaknessPlando(OptionDict):
"""
Specify specific damage numbers for boss damage. Can be used even without strict/random weaknesses.
plando_weakness:
Robot Master:
Weapon: Damage
"""
display_name = "Plando Weaknesses"
schema = Schema({
Optional(And(str, Use(str.title), lambda s: s in bosses)): {
And(str, Use(str.title), lambda s: s in weapons_to_id): And(int, lambda i: i in range(-1, 14))
}
})
default = {}
class ReduceFlashing(Choice):
"""
Reduce flashing seen in gameplay, such as the stage select and when defeating a Wily boss.
Virtual Console: increases length of most flashes, changes some flashes from white to a dark gray.
Minor: VC changes + decreasing the speed of Bubble/Metal Man stage animations.
Full: VC changes + further decreasing the brightness of most flashes and
disables stage animations for Metal/Bubble Man stages.
"""
display_name = "Reduce Flashing"
option_none = 0
option_virtual_console = 1
option_minor = 2
option_full = 3
default = 1
class RandomMusic(Choice):
"""
Vanilla: music is unchanged
Shuffled: stage and certain menu music is shuffled.
Randomized: stage and certain menu music is randomly selected
None: no music will play
"""
display_name = "Random Music"
option_vanilla = 0
option_shuffled = 1
option_randomized = 2
option_none = 3
@dataclass
class MM2Options(PerGameCommonOptions):
death_link: DeathLink
energy_link: EnergyLink
starting_robot_master: StartingRobotMaster
consumables: Consumables
yoku_jumps: YokuJumps
enable_lasers: EnableLasers
enemy_weakness: EnemyWeaknesses
strict_weakness: StrictWeaknesses
random_weakness: RandomWeaknesses
wily_5_requirement: Wily5Requirement
plando_weakness: WeaknessPlando
palette_shuffle: PaletteShuffle
quickswap: Quickswap
reduce_flashing: ReduceFlashing
random_music: RandomMusic

415
worlds/mm2/rom.py Normal file
View File

@@ -0,0 +1,415 @@
import pkgutil
from typing import Optional, TYPE_CHECKING, Iterable, Dict, Sequence
import hashlib
import Utils
import os
import settings
from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes
from . import names
from .rules import minimum_weakness_requirement
from .text import MM2TextEntry
from .color import get_colors_for_item, write_palette_shuffle
from .options import Consumables, ReduceFlashing, RandomMusic
if TYPE_CHECKING:
from . import MM2World
MM2LCHASH = "37f2c36ce7592f1e16b3434b3985c497"
PROTEUSHASH = "9ff045a3ca30018b6e874c749abb3ec4"
MM2NESHASH = "0527a0ee512f69e08b8db6dc97964632"
MM2VCHASH = "0c78dfe8e90fb8f3eed022ff01126ad3"
enemy_weakness_ptrs: Dict[int, int] = {
0: 0x3E9A8,
1: 0x3EA24,
2: 0x3EA9C,
3: 0x3EB14,
4: 0x3EB8C,
5: 0x3EC04,
6: 0x3EC7C,
7: 0x3ECF4,
}
enemy_addresses: Dict[str, int] = {
"Shrink": 0x00,
"M-445": 0x04,
"Claw": 0x08,
"Tanishi": 0x0A,
"Kerog": 0x0C,
"Petit Kerog": 0x0D,
"Anko": 0x0F,
"Batton": 0x16,
"Robitto": 0x17,
"Friender": 0x1C,
"Monking": 0x1D,
"Kukku": 0x1F,
"Telly": 0x22,
"Changkey Maker": 0x23,
"Changkey": 0x24,
"Pierrobot": 0x29,
"Fly Boy": 0x2C,
# "Crash Wall": 0x2D
# "Friender Wall": 0x2E
"Blocky": 0x31,
"Neo Metall": 0x34,
"Matasaburo": 0x36,
"Pipi": 0x38,
"Pipi Egg": 0x3A,
"Copipi": 0x3C,
"Kaminari Goro": 0x3E,
"Petit Goblin": 0x45,
"Springer": 0x46,
"Mole (Up)": 0x48,
"Mole (Down)": 0x49,
"Shotman (Left)": 0x4B,
"Shotman (Right)": 0x4C,
"Sniper Armor": 0x4E,
"Sniper Joe": 0x4F,
"Scworm": 0x50,
"Scworm Worm": 0x51,
"Picopico-kun": 0x6A,
"Boobeam Trap": 0x6D,
"Big Fish": 0x71
}
# addresses printed when assembling basepatch
consumables_ptr: int = 0x3F2FE
quickswap_ptr: int = 0x3F363
wily_5_ptr: int = 0x3F3A1
energylink_ptr: int = 0x3F46B
get_equipped_sound_ptr: int = 0x3F384
class RomData:
def __init__(self, file: bytes, name: 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: Sequence[int]) -> None:
self.file[offset:offset + len(values)] = values
def write_to_file(self, file: str) -> None:
with open(file, 'wb') as outfile:
outfile.write(self.file)
class MM2ProcedurePatch(APProcedurePatch, APTokenMixin):
hash = [MM2LCHASH, MM2NESHASH, MM2VCHASH]
game = "Mega Man 2"
patch_file_ending = ".apmm2"
result_file_ending = ".nes"
name: bytearray
procedure = [
("apply_bsdiff4", ["mm2_basepatch.bsdiff4"]),
("apply_tokens", ["token_patch.bin"]),
]
@classmethod
def get_source_data(cls) -> bytes:
return get_base_rom_bytes()
def write_byte(self, offset: int, value: int) -> None:
self.write_token(APTokenTypes.WRITE, offset, value.to_bytes(1, "little"))
def write_bytes(self, offset: int, value: Iterable[int]) -> None:
self.write_token(APTokenTypes.WRITE, offset, bytes(value))
def patch_rom(world: "MM2World", patch: MM2ProcedurePatch) -> None:
patch.write_file("mm2_basepatch.bsdiff4", pkgutil.get_data(__name__, os.path.join("data", "mm2_basepatch.bsdiff4")))
# text writing
patch.write_bytes(0x37E2A, MM2TextEntry("FOR ", 0xCB).resolve())
patch.write_bytes(0x37EAA, MM2TextEntry("GET EQUIPPED ", 0x0B).resolve())
patch.write_bytes(0x37EBA, MM2TextEntry("WITH ", 0x2B).resolve())
base_address = 0x3F650
color_address = 0x37F6C
for i, location in zip(range(11), [
names.atomic_fire_get,
names.air_shooter_get,
names.leaf_shield_get,
names.bubble_lead_get,
names.quick_boomerang_get,
names.time_stopper_get,
names.metal_blade_get,
names.crash_bomber_get,
names.item_1_get,
names.item_2_get,
names.item_3_get
]):
item = world.multiworld.get_location(location, world.player).item
if item:
if len(item.name) <= 14:
# we want to just place it in the center
first_str = ""
second_str = item.name
third_str = ""
elif len(item.name) <= 28:
# spread across second and third
first_str = ""
second_str = item.name[:14]
third_str = item.name[14:]
else:
# all three
first_str = item.name[:14]
second_str = item.name[14:28]
third_str = item.name[28:]
if len(third_str) > 16:
third_str = third_str[:16]
player_str = world.multiworld.get_player_name(item.player)
if len(player_str) > 14:
player_str = player_str[:14]
patch.write_bytes(base_address + (64 * i), MM2TextEntry(first_str, 0x4B).resolve())
patch.write_bytes(base_address + (64 * i) + 16, MM2TextEntry(second_str, 0x6B).resolve())
patch.write_bytes(base_address + (64 * i) + 32, MM2TextEntry(third_str, 0x8B).resolve())
patch.write_bytes(base_address + (64 * i) + 48, MM2TextEntry(player_str, 0xEB).resolve())
colors = get_colors_for_item(item.name)
if i > 7:
patch.write_bytes(color_address + 27 + ((i - 8) * 2), colors)
else:
patch.write_bytes(color_address + (i * 2), colors)
write_palette_shuffle(world, patch)
enemy_weaknesses: Dict[str, Dict[int, int]] = {}
if world.options.strict_weakness or world.options.random_weakness or world.options.plando_weakness:
# we need to write boss weaknesses
output = bytearray()
for weapon in world.weapon_damage:
if weapon == 8:
continue # Time Stopper is a special case
weapon_damage = [world.weapon_damage[weapon][i]
if world.weapon_damage[weapon][i] >= 0
else 256 + world.weapon_damage[weapon][i]
for i in range(14)]
output.extend(weapon_damage)
patch.write_bytes(0x2E952, bytes(output))
time_stopper_damage = world.weapon_damage[8]
time_offset = 0x2C03B
damage_table = {
4: 0xF,
3: 0x17,
2: 0x1E,
1: 0x25
}
for boss, damage in enumerate(time_stopper_damage):
if damage > 4:
damage = 4 # 4 is a guaranteed kill, no need to exceed
if damage <= 0:
patch.write_byte(time_offset + 14 + boss, 0)
else:
patch.write_byte(time_offset + 14 + boss, 1)
patch.write_byte(time_offset + boss, damage_table[damage])
if world.options.random_weakness:
wily_5_weaknesses = [i for i in range(8) if world.weapon_damage[i][12] > minimum_weakness_requirement[i]]
world.random.shuffle(wily_5_weaknesses)
if len(wily_5_weaknesses) >= 3:
weak1 = wily_5_weaknesses.pop()
weak2 = wily_5_weaknesses.pop()
weak3 = wily_5_weaknesses.pop()
elif len(wily_5_weaknesses) == 2:
weak1 = weak2 = wily_5_weaknesses.pop()
weak3 = wily_5_weaknesses.pop()
else:
weak1 = weak2 = weak3 = 0
patch.write_byte(0x2DA2E, weak1)
patch.write_byte(0x2DA32, weak2)
patch.write_byte(0x2DA3A, weak3)
enemy_weaknesses["Picopico-kun"] = {weapon: world.weapon_damage[weapon][9] for weapon in range(8)}
enemy_weaknesses["Boobeam Trap"] = {weapon: world.weapon_damage[weapon][11] for weapon in range(8)}
if world.options.enemy_weakness:
for enemy in enemy_addresses:
if enemy in ("Picopico-kun", "Boobeam Trap"):
continue
enemy_weaknesses[enemy] = {weapon: world.random.randint(-4, 4) for weapon in enemy_weakness_ptrs}
if enemy == "Friender":
# Friender has to be killed, need buster damage to not break logic
enemy_weaknesses[enemy][0] = max(enemy_weaknesses[enemy][0], 1)
for enemy, damage_table in enemy_weaknesses.items():
for weapon in enemy_weakness_ptrs:
if damage_table[weapon] < 0:
damage_table[weapon] = 256 + damage_table[weapon]
patch.write_byte(enemy_weakness_ptrs[weapon] + enemy_addresses[enemy], damage_table[weapon])
if world.options.quickswap:
patch.write_byte(quickswap_ptr + 1, 0x01)
if world.options.consumables != Consumables.option_all:
value_a = 0x7C
value_b = 0x76
if world.options.consumables == Consumables.option_1up_etank:
value_b = 0x7A
else:
value_a = 0x7A
patch.write_byte(consumables_ptr - 3, value_a)
patch.write_byte(consumables_ptr + 1, value_b)
patch.write_byte(wily_5_ptr + 1, world.options.wily_5_requirement.value)
if world.options.energy_link:
patch.write_byte(energylink_ptr + 1, 1)
if world.options.reduce_flashing:
if world.options.reduce_flashing.value == ReduceFlashing.option_virtual_console:
color = 0x2D # Dark Gray
speed = -1
elif world.options.reduce_flashing.value == ReduceFlashing.option_minor:
color = 0x2D
speed = 0x08
else:
color = 0x0F
speed = 0x00
patch.write_byte(0x2D1B0, color) # Change white to a dark gray, Mecha Dragon
patch.write_byte(0x2D397, 0x0F) # Longer flash time, Mecha Dragon kill
patch.write_byte(0x2D3A0, color) # Change white to a dark gray, Picopico-kun/Boobeam Trap
patch.write_byte(0x2D65F, color) # Change white to a dark gray, Guts Tank
patch.write_byte(0x2DA94, color) # Change white to a dark gray, Wily Machine
patch.write_byte(0x2DC97, color) # Change white to a dark gray, Alien
patch.write_byte(0x2DD68, 0x10) # Longer flash time, Alien kill
patch.write_bytes(0x2DF14, [0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA]) # Reduce final Alien flash to 1 big flash
patch.write_byte(0x34132, 0x08) # Longer flash time, Stage Select
if world.options.reduce_flashing.value == ReduceFlashing.option_full:
# reduce color of stage flashing
patch.write_bytes(0x344C9, [0x2D, 0x10, 0x00, 0x2D,
0x0F, 0x10, 0x2D, 0x00,
0x0F, 0x10, 0x2D, 0x00,
0x0F, 0x10, 0x2D, 0x00,
0x2D, 0x10, 0x2D, 0x00,
0x0F, 0x10, 0x2D, 0x00,
0x0F, 0x10, 0x2D, 0x00,
0x0F, 0x10, 0x2D, 0x00])
# remove wily castle flash
patch.write_byte(0x3596D, 0x0F)
if speed != -1:
patch.write_byte(0xFE01, speed) # Bubble Man Stage
patch.write_byte(0x1BE01, speed) # Metal Man Stage
if world.options.random_music:
if world.options.random_music == RandomMusic.option_none:
pool = [0xFF] * 20
# A couple of additional mutes we want here
patch.write_byte(0x37819, 0xFF) # Credits
patch.write_byte(0x378A4, 0xFF) # Credits #2
patch.write_byte(0x37149, 0xFF) # Game Over Jingle
patch.write_byte(0x341BA, 0xFF) # Robot Master Jingle
patch.write_byte(0x2E0B4, 0xFF) # Robot Master Defeated
patch.write_byte(0x35B78, 0xFF) # Wily Castle
patch.write_byte(0x2DFA5, 0xFF) # Wily Defeated
elif world.options.random_music == RandomMusic.option_shuffled:
pool = [0, 1, 2, 3, 4, 5, 6, 7, 8, 8, 9, 9, 9, 0x10, 0xC, 0xB, 0x17, 0x13, 0xE, 0xD]
world.random.shuffle(pool)
else:
pool = world.random.choices([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xB, 0xC, 0xD, 0xE, 0x10, 0x13, 0x17], k=20)
patch.write_bytes(0x381E0, pool[:13])
patch.write_byte(0x36318, pool[13]) # Game Start
patch.write_byte(0x37181, pool[13]) # Game Over
patch.write_byte(0x340AE, pool[14]) # RBM Select
patch.write_byte(0x39005, pool[15]) # Robot Master Battle
patch.write_byte(get_equipped_sound_ptr + 1, pool[16]) # Get Equipped, we actually hook this already lmao
patch.write_byte(0x3775A, pool[17]) # Epilogue
patch.write_byte(0x36089, pool[18]) # Intro
patch.write_byte(0x361F1, pool[19]) # Title
from Utils import __version__
patch.name = bytearray(f'MM2{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0',
'utf8')[:21]
patch.name.extend([0] * (21 - len(patch.name)))
patch.write_bytes(0x3FFC0, patch.name)
deathlink_byte = world.options.death_link.value | (world.options.energy_link.value << 1)
patch.write_byte(0x3FFD5, deathlink_byte)
patch.write_bytes(0x3FFD8, world.world_version)
version_map = {
"0": 0x90,
"1": 0x91,
"2": 0x92,
"3": 0x93,
"4": 0x94,
"5": 0x95,
"6": 0x96,
"7": 0x97,
"8": 0x98,
"9": 0x99,
".": 0xDC
}
patch.write_token(APTokenTypes.RLE, 0x36EE0, (11, 0))
patch.write_token(APTokenTypes.RLE, 0x36EEE, (25, 0))
# BY SILVRIS
patch.write_bytes(0x36EE0, [0xC2, 0xD9, 0xC0, 0xD3, 0xC9, 0xCC, 0xD6, 0xD2, 0xC9, 0xD3])
# ARCHIPELAGO x.x.x
patch.write_bytes(0x36EF2, [0xC1, 0xD2, 0xC3, 0xC8, 0xC9, 0xD0, 0xC5, 0xCC, 0xC1, 0xC7, 0xCF, 0xC0])
patch.write_bytes(0x36EFE, list(map(lambda c: version_map[c], __version__)))
patch.write_file("token_patch.bin", patch.get_token_binary())
header = b"\x4E\x45\x53\x1A\x10\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00"
def read_headerless_nes_rom(rom: bytes) -> bytes:
if rom[:4] == b"NES\x1A":
return rom[16:]
else:
return rom
def get_base_rom_bytes(file_name: str = "") -> bytes:
base_rom_bytes: Optional[bytes] = getattr(get_base_rom_bytes, "base_rom_bytes", None)
if not base_rom_bytes:
file_name = get_base_rom_path(file_name)
base_rom_bytes = read_headerless_nes_rom(bytes(open(file_name, "rb").read()))
basemd5 = hashlib.md5()
basemd5.update(base_rom_bytes)
if basemd5.hexdigest() == PROTEUSHASH:
base_rom_bytes = extract_mm2(base_rom_bytes)
basemd5 = hashlib.md5()
basemd5.update(base_rom_bytes)
if basemd5.hexdigest() not in {MM2LCHASH, MM2NESHASH, MM2VCHASH}:
print(basemd5.hexdigest())
raise Exception("Supplied Base Rom does not match known MD5 for US, LC, or US VC release. "
"Get the correct game and version, then dump it")
headered_rom = bytearray(base_rom_bytes)
headered_rom[0:0] = header
setattr(get_base_rom_bytes, "base_rom_bytes", bytes(headered_rom))
return bytes(headered_rom)
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["mm2_options"]["rom_file"]
if not os.path.exists(file_name):
file_name = Utils.user_path(file_name)
return file_name
PRG_OFFSET = 0x8ED70
PRG_SIZE = 0x40000
def extract_mm2(proteus: bytes) -> bytes:
mm2 = bytearray(proteus[PRG_OFFSET:PRG_OFFSET + PRG_SIZE])
return bytes(mm2)

321
worlds/mm2/rules.py Normal file
View File

@@ -0,0 +1,321 @@
from math import ceil
from typing import TYPE_CHECKING, Dict, List
from . import names
from .locations import heat_man_locations, air_man_locations, wood_man_locations, bubble_man_locations, \
quick_man_locations, flash_man_locations, metal_man_locations, crash_man_locations, wily_1_locations, \
wily_2_locations, wily_3_locations, wily_4_locations, wily_5_locations, wily_6_locations
from .options import bosses, weapons_to_id, Consumables, RandomWeaknesses
from worlds.generic.Rules import add_rule
if TYPE_CHECKING:
from . import MM2World
from BaseClasses import CollectionState
weapon_damage: Dict[int, List[int]] = {
0: [2, 2, 1, 1, 2, 2, 1, 1, 1, 7, 1, 0, 1, -1], # Mega Buster
1: [-1, 6, 0xE, 0, 0xA, 6, 4, 6, 8, 13, 8, 0, 0xE, -1], # Atomic Fire
2: [2, 0, 4, 0, 2, 0, 0, 0xA, 0, 0, 0, 0, 1, -1], # Air Shooter
3: [0, 8, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1], # Leaf Shield
4: [6, 0, 0, -1, 0, 2, 0, 1, 0, 14, 1, 0, 0, 1], # Bubble Lead
5: [2, 2, 0, 2, 0, 0, 4, 1, 1, 7, 2, 0, 1, -1], # Quick Boomerang
6: [-1, 0, 2, 2, 4, 3, 0, 0, 1, 0, 1, 0x14, 1, -1], # Crash Bomber
7: [1, 0, 2, 4, 0, 4, 0xE, 0, 0, 7, 0, 0, 1, -1], # Metal Blade
8: [0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0], # Time Stopper
}
weapons_to_name: Dict[int, str] = {
1: names.atomic_fire,
2: names.air_shooter,
3: names.leaf_shield,
4: names.bubble_lead,
5: names.quick_boomerang,
6: names.crash_bomber,
7: names.metal_blade,
8: names.time_stopper
}
minimum_weakness_requirement: Dict[int, int] = {
0: 1, # Mega Buster is free
1: 14, # 2 shots of Atomic Fire
2: 1, # 14 shots of Air Shooter, although you likely hit more than one shot
3: 4, # 9 uses of Leaf Shield, 3 ends up 1 damage off
4: 1, # 56 uses of Bubble Lead
5: 1, # 224 uses of Quick Boomerang
6: 4, # 7 uses of Crash Bomber
7: 1, # 112 uses of Metal Blade
8: 4, # 1 use of Time Stopper, but setting to 4 means we shave the entire HP bar
}
robot_masters: Dict[int, str] = {
0: "Heat Man Defeated",
1: "Air Man Defeated",
2: "Wood Man Defeated",
3: "Bubble Man Defeated",
4: "Quick Man Defeated",
5: "Flash Man Defeated",
6: "Metal Man Defeated",
7: "Crash Man Defeated"
}
weapon_costs = {
0: 0,
1: 10,
2: 2,
3: 3,
4: 0.5,
5: 0.125,
6: 4,
7: 0.25,
8: 7,
}
def can_defeat_enough_rbms(state: "CollectionState", player: int,
required: int, boss_requirements: Dict[int, List[int]]):
can_defeat = 0
for boss, reqs in boss_requirements.items():
if boss in robot_masters:
if state.has_all(map(lambda x: weapons_to_name[x], reqs), player):
can_defeat += 1
if can_defeat >= required:
return True
return False
def set_rules(world: "MM2World") -> None:
# most rules are set on region, so we only worry about rules required within stage access
# or rules variable on settings
if (hasattr(world.multiworld, "re_gen_passthrough")
and "Mega Man 2" in getattr(world.multiworld, "re_gen_passthrough")):
slot_data = getattr(world.multiworld, "re_gen_passthrough")["Mega Man 2"]
world.weapon_damage = slot_data["weapon_damage"]
world.wily_5_weapons = slot_data["wily_5_weapons"]
else:
if world.options.random_weakness == RandomWeaknesses.option_shuffled:
weapon_tables = [table for weapon, table in weapon_damage.items() if weapon not in (0, 8)]
world.random.shuffle(weapon_tables)
for i in range(1, 8):
world.weapon_damage[i] = weapon_tables.pop()
# alien must take minimum required damage from his weakness
alien_weakness = next(weapon for weapon in range(8) if world.weapon_damage[weapon][13] != -1)
world.weapon_damage[alien_weakness][13] = minimum_weakness_requirement[alien_weakness]
world.weapon_damage[8] = [0 for _ in range(14)]
world.weapon_damage[8][world.random.choice(range(8))] = 2
elif world.options.random_weakness == RandomWeaknesses.option_randomized:
world.weapon_damage = {i: [] for i in range(9)}
for boss in range(13):
for weapon in world.weapon_damage:
world.weapon_damage[weapon].append(min(14, max(-1, int(world.random.normalvariate(3, 3)))))
if not any([world.weapon_damage[weapon][boss] >= max(4, minimum_weakness_requirement[weapon])
for weapon in range(1, 7)]):
# failsafe, there should be at least one defined non-Buster weakness
weapon = world.random.randint(1, 7)
world.weapon_damage[weapon][boss] = world.random.randint(
max(4, minimum_weakness_requirement[weapon]), 14) # Force weakness
# special case, if boobeam trap has a weakness to Crash, it needs to be max damage
if world.weapon_damage[6][11] > 4:
world.weapon_damage[6][11] = 14
# handle the alien
boss = 13
for weapon in world.weapon_damage:
world.weapon_damage[weapon].append(-1)
weapon = world.random.choice(list(world.weapon_damage.keys()))
world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon]
if world.options.strict_weakness:
for weapon in weapon_damage:
for i in range(13):
if weapon == 0:
world.weapon_damage[weapon][i] = 0
elif i in (8, 12) and not world.options.random_weakness:
continue
# Mecha Dragon only has damage range of 0-1, so allow the 1
# Wily Machine needs all three weaknesses present, so allow
elif 4 > world.weapon_damage[weapon][i] > 0:
world.weapon_damage[weapon][i] = 0
# handle special cases
for boss in range(14):
for weapon in (1, 3, 6, 8):
if (0 < world.weapon_damage[weapon][boss] < minimum_weakness_requirement[weapon] and
not any(world.weapon_damage[i][boss] > 0 for i in range(1, 8) if i != weapon)):
# Weapon does not have enough possible ammo to kill the boss, raise the damage
if boss == 9:
if weapon != 3:
# Atomic Fire and Crash Bomber cannot be Picopico-kun's only weakness
world.weapon_damage[weapon][boss] = 0
weakness = world.random.choice((2, 3, 4, 5, 7, 8))
world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness]
elif boss == 11:
if weapon == 1:
# Atomic Fire cannot be Boobeam Trap's only weakness
world.weapon_damage[weapon][boss] = 0
weakness = world.random.choice((2, 3, 4, 5, 6, 7, 8))
world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness]
else:
world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon]
starting = world.options.starting_robot_master.value
world.weapon_damage[0][starting] = 1
for p_boss in world.options.plando_weakness:
for p_weapon in world.options.plando_weakness[p_boss]:
if world.options.plando_weakness[p_boss][p_weapon] < minimum_weakness_requirement[p_weapon] \
and not any(w != p_weapon
and world.weapon_damage[w][bosses[p_boss]] > minimum_weakness_requirement[w]
for w in world.weapon_damage):
# we need to replace this weakness
weakness = world.random.choice([key for key in world.weapon_damage if key != p_weapon])
world.weapon_damage[weakness][bosses[p_boss]] = minimum_weakness_requirement[weakness]
world.weapon_damage[weapons_to_id[p_weapon]][bosses[p_boss]] \
= world.options.plando_weakness[p_boss][p_weapon]
if world.weapon_damage[0][world.options.starting_robot_master.value] < 1:
world.weapon_damage[0][world.options.starting_robot_master.value] = weapon_damage[0][world.options.starting_robot_master.value]
# final special case
# There's a vanilla crash if Time Stopper kills Wily phase 1
# There's multiple fixes, but ensuring Wily cannot take Time Stopper damage is best
if world.weapon_damage[8][12] > 0:
world.weapon_damage[8][12] = 0
# weakness validation, it is better to confirm a completable seed than respect plando
boss_health = {boss: 0x1C if boss != 12 else 0x1C * 2 for boss in [*range(8), 12]}
weapon_energy = {key: float(0x1C) for key in weapon_costs}
weapon_boss = {boss: {weapon: world.weapon_damage[weapon][boss] for weapon in world.weapon_damage}
for boss in [*range(8), 12]}
flexibility = {
boss: (
sum(damage_value > 0 for damage_value in
weapon_damages.values()) # Amount of weapons that hit this boss
* sum(weapon_damages.values()) # Overall damage that those weapons do
)
for boss, weapon_damages in weapon_boss.items() if boss != 12
}
flexibility = sorted(flexibility, key=flexibility.get) # Fast way to sort dict by value
used_weapons = {i: set() for i in [*range(8), 12]}
for boss in [*flexibility, 12]:
boss_damage = weapon_boss[boss]
weapon_weight = {weapon: (weapon_energy[weapon] / damage) if damage else 0 for weapon, damage in
boss_damage.items() if weapon_energy[weapon] > 0}
if boss_damage[8]:
boss_damage[8] = 1.75 * boss_damage[8]
if any(boss_damage[i] > 0 for i in range(8)) and 8 in weapon_weight:
# We get exactly one use of Time Stopper during the rush
# So we want to make sure that use is absolutely needed
weapon_weight[8] = min(weapon_weight[8], 0.001)
while boss_health[boss] > 0:
if boss_damage[0] > 0:
boss_health[boss] = 0 # if we can buster, we should buster
continue
highest, wp = max(zip(weapon_weight.values(), weapon_weight.keys()))
uses = weapon_energy[wp] // weapon_costs[wp]
used_weapons[boss].add(wp)
if int(uses * boss_damage[wp]) > boss_health[boss]:
used = ceil(boss_health[boss] / boss_damage[wp])
weapon_energy[wp] -= weapon_costs[wp] * used
boss_health[boss] = 0
elif highest <= 0:
# we are out of weapons that can actually damage the boss
# so find the weapon that has the most uses, and apply that as an additional weakness
# it should be impossible to be out of energy, simply because even if every boss took 1 from
# Quick Boomerang and no other, it would only be 28 off from defeating all 9, which Metal Blade should
# be able to cover
wp, max_uses = max((weapon, weapon_energy[weapon] // weapon_costs[weapon]) for weapon in weapon_weight
if weapon != 0)
world.weapon_damage[wp][boss] = minimum_weakness_requirement[wp]
used = min(int(weapon_energy[wp] // weapon_costs[wp]),
ceil(boss_health[boss] // minimum_weakness_requirement[wp]))
weapon_energy[wp] -= weapon_costs[wp] * used
boss_health[boss] -= int(used * minimum_weakness_requirement[wp])
weapon_weight.pop(wp)
else:
# drain the weapon and continue
boss_health[boss] -= int(uses * boss_damage[wp])
weapon_energy[wp] -= weapon_costs[wp] * uses
weapon_weight.pop(wp)
world.wily_5_weapons = {boss: sorted(used_weapons[boss]) for boss in used_weapons}
for i, boss_locations in enumerate([
heat_man_locations,
air_man_locations,
wood_man_locations,
bubble_man_locations,
quick_man_locations,
flash_man_locations,
metal_man_locations,
crash_man_locations,
wily_1_locations,
wily_2_locations,
wily_3_locations,
wily_4_locations,
wily_5_locations,
wily_6_locations
]):
if world.weapon_damage[0][i] > 0:
continue # this can always be in logic
weapons = []
for weapon in range(1, 9):
if world.weapon_damage[weapon][i] > 0:
if world.weapon_damage[weapon][i] < minimum_weakness_requirement[weapon]:
continue # Atomic Fire can only be considered logical for bosses it can kill in 2 hits
weapons.append(weapons_to_name[weapon])
if not weapons:
raise Exception(f"Attempted to have boss {i} with no weakness! Seed: {world.multiworld.seed}")
for location in boss_locations:
if i == 12:
add_rule(world.get_location(location),
lambda state, weps=tuple(weapons): state.has_all(weps, world.player))
# TODO: when has_list gets added, check for a subset of possible weaknesses
else:
add_rule(world.get_location(location),
lambda state, weps=tuple(weapons): state.has_any(weps, world.player))
# Always require Crash Bomber for Boobeam Trap
add_rule(world.get_location(names.wily_4),
lambda state: state.has(names.crash_bomber, world.player))
add_rule(world.get_location(names.wily_stage_4),
lambda state: state.has(names.crash_bomber, world.player))
# Need to defeat x amount of robot masters for Wily 5
add_rule(world.get_location(names.wily_5),
lambda state: can_defeat_enough_rbms(state, world.player, world.options.wily_5_requirement.value,
world.wily_5_weapons))
add_rule(world.get_location(names.wily_stage_5),
lambda state: can_defeat_enough_rbms(state, world.player, world.options.wily_5_requirement.value,
world.wily_5_weapons))
if not world.options.yoku_jumps:
add_rule(world.get_entrance("To Heat Man Stage"),
lambda state: state.has(names.item_2, world.player))
if not world.options.enable_lasers:
add_rule(world.get_entrance("To Quick Man Stage"),
lambda state: state.has(names.time_stopper, world.player))
if world.options.consumables in (Consumables.option_1up_etank,
Consumables.option_all):
add_rule(world.get_location(names.flash_man_c2),
lambda state: state.has_any([names.item_1, names.item_2, names.item_3], world.player))
add_rule(world.get_location(names.quick_man_c1),
lambda state: state.has_any([names.item_1, names.item_2, names.item_3], world.player))
add_rule(world.get_location(names.metal_man_c2),
lambda state: state.has_any([names.item_1, names.item_2], world.player))
add_rule(world.get_location(names.metal_man_c3),
lambda state: state.has_any([names.item_1, names.item_2], world.player))
add_rule(world.get_location(names.crash_man_c3),
lambda state: state.has_any([names.item_1, names.item_2, names.item_3], world.player))
add_rule(world.get_location(names.wily_2_c5),
lambda state: state.has(names.crash_bomber, world.player))
add_rule(world.get_location(names.wily_2_c6),
lambda state: state.has(names.crash_bomber, world.player))
add_rule(world.get_location(names.wily_3_c2),
lambda state: state.has(names.crash_bomber, world.player))
if world.options.consumables in (Consumables.option_weapon_health,
Consumables.option_all):
add_rule(world.get_location(names.flash_man_c3),
lambda state: state.has(names.crash_bomber, world.player))
add_rule(world.get_location(names.flash_man_c4),
lambda state: state.has(names.crash_bomber, world.player))
add_rule(world.get_location(names.wily_3_c1),
lambda state: state.has(names.crash_bomber, world.player))

View File

@@ -0,0 +1,861 @@
norom
!headersize = 16
!controller_mirror = $23
!controller_flip = $27 ; only on first frame of input, used by crash man, etc
!current_stage = $2A
!received_stages = $8A
!completed_stages = $8B
!received_item_checks = $8C
!last_wily = $8D
!deathlink = $8F
!energylink_packet = $90
!rbm_strobe = $91
!received_weapons = $9A
!received_items = $9B
!current_weapon = $A9
!stage_completion = $0F70
!consumable_checks = $0F80
!CONTROLLER_SELECT = #$04
!CONTROLLER_SELECT_START = #$0C
!CONTROLLER_ALL_BUTTON = #$0F
!PpuControl_2000 = $2000
!PpuMask_2001 = $2001
!PpuAddr_2006 = $2006
!PpuData_2007 = $2007
!LOAD_BANK = $C000
macro org(address,bank)
if <bank> == $0F
org <address>-$C000+($4000*<bank>)+!headersize ; org sets the position in the output file to write to (in norom, at least)
base <address> ; base sets the position that all labels are relative to - this is necessary so labels will still start from $8000, instead of $0000 or somewhere
else
org <address>-$8000+($4000*<bank>)+!headersize
base <address>
endif
endmacro
%org($8400, $08)
incbin "mm2font.dat"
%org($A900, $09)
incbin "mm2titlefont.dat"
%org($807E, $0B)
FlashFixes:
CMP #$FF
BEQ FlashFixTarget1
CMP #$FF
BNE FlashFixTarget2
%org($8086, $0B)
FlashFixTarget1:
%org($808D, $0B)
FlashFixTarget2:
%org($8015, $0D)
ClearRefreshHook:
; if we're already doing a fresh load of the stage select
; we don't need to immediately refresh it
JSR ClearRefresh
NOP
%org($802B, $0D)
PatchFaceTiles:
LDA !received_stages
%org($8072, $0D)
PatchFaceSprites:
LDA !received_stages
%org($80CC, $0D)
CheckItemsForWily:
LDA !received_items
CMP #$07
%org($80D2, $0D)
LoadWily:
JSR GoToMostRecentWily
NOP
%org($80DC, $0D)
CheckAccessCodes:
LDA !received_stages
%org($8312, $0D)
HookStageSelect:
JSR RefreshRBMTiles
NOP
%org($A315, $0D)
RemoveWeaponClear:
NOP
NOP
NOP
NOP
;Adjust Password select flasher
%org($A32A, $0D)
LDX #$68
;Block password input
%org($A346, $0D)
EOR #$00
;Remove password text
%org($AF3A, $0D)
StartHeight:
db $AC ; set Start to center
%org($AF49, $0D)
PasswordText:
db $40, $40, $40, $40, $40, $40, $40, $40
%org($AF6C, $0D)
ContinueHeight:
db $AB ; split height between 2 remaining options
%org($AF77, $0D)
StageSelectHeight:
db $EB ; split between 2 remaining options
%org($AF88, $0D)
GameOverPasswordText:
db $40, $40, $40, $40, $40, $40, $40, $40
%org($AFA5, $0D)
GetEquippedPasswordText:
db $40, $40, $40, $40, $40, $40, $40, $40
%org($AFAE, $0D)
GetEquippedStageSelect:
db $26, $EA
%org($B195, $0D)
GameOverPasswordUp:
LDA #$01 ; originally 02, removing last option
%org($B19F, $0D)
GameOverPassword:
CMP #$02 ; originally 03, remove the last option
%org($B1ED, $0D)
FixupGameOverArrows:
db $68, $78
%org($BB74, $0D)
GetEquippedStage:
JSR StageGetEquipped
NOP #13
%org($BBD9, $0D)
GetEquippedDefault:
LDA #$01
%org($BC01, $0D)
GetEquippedPasswordRemove:
ORA #$01 ; originally EOR #$01, we always want 1 here
%org($BCF1, $0D)
GetEquippedItem:
ADC #$07
JSR ItemGetEquipped
JSR LoadItemsColor
NOP ; !!!! This is a load-bearing NOP. It gets branched to later in the function
LDX $FF
%org($BB08, $0D)
WilyProgress:
JSR StoreWilyProgress
NOP
%org($BF6F, $0D)
GetEquippedStageSelectHeight:
db $B8
%org($805B, $0E)
InitalizeStartingRBM:
LDA #$FF ; this does two things
STA !received_stages ; we're overwriting clearing e-tanks and setting RBM available to none
%org($8066, $0E)
BlockStartupAutoWily:
; presumably this would be called from password?
LDA #$00
%org($80A7, $0E)
StageLoad:
JMP CleanWily5
NOP
%org($8178, $0E)
Main1:
JSR MainLoopHook
NOP
%org($81DE, $0E)
Wily5Teleporter:
LDA $99
CMP #$01
BCC SkipSpawn
%org($81F9, $0E)
SkipSpawn:
; just present to fix the branch, if we try to branch raw it'll get confused
%org($822D, $0E)
Main2:
; believe used in the wily 5 refights?
JSR MainLoopHook
NOP
%org($842F, $0E)
Wily5Hook:
JMP Wily5Requirement
NOP
%org($C10D, $0F)
Deathlink:
JSR KillMegaMan
%org($C1BC, $0F)
RemoveETankLoss:
NOP
NOP
%org($C23C, $0F)
WriteStageComplete:
ORA !completed_stages
STA !completed_stages
%org($C243, $0F)
WriteReceiveItem:
ORA !received_item_checks
STA !received_item_checks
%org($C254, $0F)
BlockAutoWily:
; and this one is on return from stage?
LDA #$00
%org($C261, $0F)
WilyStageCompletion:
JSR StoreWilyStageCompletion
NOP
%org($E5AC, $0F)
NullDeathlink:
STA $8F ; we null his HP later in the process
NOP
%org($E5D1, $0F)
EnergylinkHook:
JSR Energylink
NOP #2 ; comment this out to enable item giving their usual reward alongside EL
%org($E5E8, $0F)
ConsumableHook:
JSR CheckConsumable
%org($F2E3, $0F)
CheckConsumable:
STA $0140, Y
TXA
PHA
LDA $AD ; the consumable value
CMP #$7C
BPL .Store
print "Consumables (replace 7a): ", hex(realbase())
CMP #$76
BMI .Store
LDA #$00
.Store:
STA $AD
LDA $2A
ASL
ASL
TAX
TYA
.LoopHead:
CMP #$08
BMI .GetFlag
INX
SBC #$08
BNE .LoopHead
.GetFlag:
TAY
LDA #$01
.Loop2Head:
CPY #$00
BEQ .Apply
ASL
DEY
BNE .Loop2Head
.Apply:
ORA !consumable_checks, X
STA !consumable_checks, X
PLA
TAX
RTS
GoToMostRecentWily:
LDA !controller_mirror
CMP !CONTROLLER_SELECT_START
BEQ .Default
LDA !last_wily
BNE .Store
.Default:
LDA #$08 ; wily stage 1
.Store:
STA !current_stage
RTS
StoreWilyStageCompletion:
LDA #$01
STA !stage_completion, X
INC !current_stage
LDA !current_stage
STA !last_wily
RTS
ReturnToGameOver:
LDA #$10
STA !PpuControl_2000
LDA #$06
STA !PpuMask_2001
JMP $C1BE ; specific code that loads game over
MainLoopHook:
LDA !controller_mirror
CMP !CONTROLLER_ALL_BUTTON
BNE .Next
JMP ReturnToGameOver
.Next:
LDA !deathlink
CMP #$01
BNE .Next2
JMP $E5A8 ; this kills the Mega Man
.Next2:
print "Quickswap:", hex(realbase())
LDA #$00 ; slot data, write in enable for quickswap
CMP #$01
BNE .Finally
LDA !controller_flip
AND !CONTROLLER_SELECT
BEQ .Finally
JMP Quickswap
.Finally:
LDA !controller_flip
AND #$08 ; this is checking for menu
RTS
StoreWilyProgress:
STA !current_stage
TXA
PHA
LDX !current_stage
LDA #$01
STA !stage_completion, X
PLA
TAX
print "Get Equipped Music: ", hex(realbase())
LDA #$17
RTS
KillMegaMan:
JSR $C051 ; this kills the mega man
LDA #$00
STA $06C0 ; set HP to zero so client can actually detect he died
RTS
Wily5Requirement:
LDA #$01
LDX #$08
LDY #$00
.LoopHead:
BIT $BC
BEQ .Skip
INY
.Skip:
DEX
ASL
CPX #$00
BNE .LoopHead
print "Wily 5 Requirement:", hex(realbase())
CPY #$08
BCS .SpawnTeleporter
JMP $8450
.SpawnTeleporter:
LDA #$FF
STA $BC
LDA #$01
STA $99
JMP $8433
CleanWily5:
LDA #$00
STA $BC
STA $99
JMP $80AB
LoadString:
STY $00
ASL
ASL
ASL
ASL
TAY
LDA $DB
ADC #$00
STA $C8
LDA #$40
STA $C9
LDA #$F6
CLC
ADC $C8
STA $CA
LDA ($C9), Y
STA $03B6
TYA
CLC
ADC #$01
TAY
LDA $CA
ADC #$00
STA $CA
LDA ($C9), Y
STA $03B7
TYA
CLC
ADC #$01
TAY
LDA $CA
ADC #$00
STA $CA
STY $FE
LDA #$0E
STA $FD
.LoopHead:
JSR $BD34
LDY $FE
CPY #$40
BNE .NotEqual
LDA $0420
BNE .Skip
.NotEqual:
LDA ($C9), Y
.Skip:
STA $03B8
INC $47
INC $03B7
LDA $FE
CLC
ADC #$01
STA $FE
LDA $CA
ADC #$00
STA $CA
DEC $FD
BNE .LoopHead
LDY $00
JSR $C0AB
RTS
StageGetEquipped:
LDA !current_stage
LDX #$00
BCS LoadGetEquipped
ItemGetEquipped:
LDX #$02
LoadGetEquipped:
STX $DB
ASL
ASL
PHA
SEC
JSR LoadString
PLA
ADC #$00
PHA
SEC
JSR LoadString
PLA
ADC #$00
PHA
SEC
JSR LoadString
LDA #$00
SEC
JSR $BD3E
PLA
ADC #$00
SEC
JSR LoadString
RTS
LoadItemsColor:
LDA #$7D
STA $FD
LDA $0420
AND #$0F
ASL
SEC
ADC #$1A
STA $FF
RTS
Energylink:
LSR $0420, X
print "Energylink: ", hex(realbase())
LDA #$00
BEQ .ApplyDrop
LDA $04E0, X
BEQ .ApplyDrop ; This is a stage pickup, and not an enemy drop
STY !energylink_packet
SEC
BCS .Return
.ApplyDrop:
STY $AD
.Return:
RTS
Quickswap:
LDX #$0F
.LoopHead:
LDA $0420, X
BMI .Return1 ; return if we have any weapon entities spawned
DEX
CPX #$01
BNE .LoopHead
LDX !current_weapon
BNE .DoQuickswap
LDX #$00
.DoQuickswap:
TYA
PHA
LDX !current_weapon
INX
CPX #$09
BPL .Items
LDA #$01
.Loop2Head:
DEX
BEQ .FoundTarget
ASL
CPX #$00
BNE .Loop2Head
.FoundTarget:
LDX !current_weapon
INX
.Loop3Head:
PHA
AND !received_weapons
BNE .CanSwap
PLA
INX
CPX #$09
BPL .Items
ASL
BNE .Loop3Head
.CanSwap:
PLA
SEC
BCS .ApplySwap
.Items:
TXA
PHA
SEC
SBC #$08
TAX
LDA #$01
.Loop4Head:
DEX
BEQ .CheckItem
ASL
CPX #$00
BNE .Loop4Head
.CheckItem:
TAY
PLA
TAX
TYA
.Loop5Head:
PHA
AND !received_items
BNE .CanSwap
PLA
INX
ASL
BNE .Loop5Head
LDX #$00
SEC
BCS .ApplySwap
.Return1:
RTS
.ApplySwap: ; $F408 on old rom
LDA #$0D
JSR !LOAD_BANK
; this is a bunch of boiler plate to make the swap work
LDA $B5
PHA
LDA $B6
PHA
LDA $B7
PHA
LDA $B8
PHA
LDA $B9
PHA
LDA $20
PHA
LDA $1F
PHA
;but wait, there's more
STX !current_weapon
JSR $CC6C
LDA $1A
PHA
LDX #$00
.Loop6Head:
STX $FD
CLC
LDA $52
ADC $957F, X
STA $08
LDA $53
ADC #$00
STA $09
LDA $08
LSR $09
ROR
LSR $09
ROR
STA $08
AND #$3F
STA $1A
CLC
LDA $09
ADC #$85
STA $09
LDA #$00
STA $1B
LDA $FD
CMP #$08
BCS .Past8
LDX $A9
LDA $9664, X
TAY
CPX #$09
BCC .LessThanNine
LDX #$00
BEQ .Apply
.LessThanNine:
LDX #$05
BNE .Apply
.Past8:
LDY #$90
LDX #$00
.Apply:
JSR $C760
JSR $C0AB ; iirc this is loading graphics?
LDX $FD
INX
CPX #$0F
BNE .Loop6Head
STX $FD
LDY #$90
LDX #$00
JSR $C760
JSR $D2ED
; two sections redacted here, might need to look at what they actually do?
PLA
STA $1A
PLA
STA $1F
PLA
STA $20
PLA
STA $B9
PLA
STA $B8
PLA
STA $B7
PLA
STA $B6
PLA
STA $B5
LDA #$00
STA $AC
STA $2C
STA $0680
STA $06A0
LDA #$1A
STA $0400
LDA #$03
STA $AA
LDA #$30
JSR $C051
.Finally:
LDA #$0E
JSR !LOAD_BANK
PLA
TAY
.Return:
RTS
RefreshRBMTiles:
; primarily just a copy of the startup RBM setup, we just do it again
; can't jump to it as it leads into the main loop
LDA !rbm_strobe
BNE .Update
JMP .NoUpdate
.Update:
LDA #$00
STA !rbm_strobe
LDA #$10
STA $F7
STA !PpuControl_2000
LDA #$06
STA $F8
STA !PpuMask_2001
JSR $847E
JSR $843C
LDX #$00
LDA $8A
STA $01
.TileLoop:
STX $00
LSR $01
BCC .SkipTile
LDA $8531,X
STA $09
LDA $8539,X
STA $08
LDX #$04
LDA #$00
.ClearBody:
LDA $09
STA !PpuAddr_2006
LDA $08
STA !PpuAddr_2006
LDY #$04
LDA #$00
.ClearLine:
STA !PpuData_2007
DEY
BNE .ClearLine
CLC
LDA $08
ADC #$20
STA $08
DEX
BNE .ClearBody
.SkipTile:
LDX $00
INX
CPX #$08
BNE .TileLoop
LDX #$1F
JSR $829E
JSR $8473
LDX #$00
LDA $8A
STA $02
LDY #$00
.SpriteLoop:
STX $01
LSR $02
BCS .SkipRBM
LDA $8605,X
STA $00
LDA $85FD,X
TAX
.WriteSprite:
LDA $8541,X
STA $0200,Y
INY
INX
DEC $00
BNE .WriteSprite
.SkipRBM:
LDX $01
INX
CPX #$08
BNE .SpriteLoop
JSR $A51D
LDA #$0C
JSR $C051
LDA #$00
STA $2A
STA $FD
JSR $C0AB
.NoUpdate:
LDA $1C
AND #$08
RTS
ClearRefresh:
LDA #$00
STA !rbm_strobe
LDA #$10
STA $F7
RTS
assert realbase() <= $03F650 ; This is the start of our text data, and we absolutely cannot go past this point (text takes too much room).
%org($F640, $0F)
db $25, $4B, "PLACEHOLDER_L1"
db $25, $6B, "PLACEHOLDER_L2"
db $25, $8B, "PLACEHOLDER_L3"
db $25, $EB, "PLACEHOLDER_PL"
db $25, $4B, "PLACEHOLDER_L1"
db $25, $6B, "PLACEHOLDER_L2"
db $25, $8B, "PLACEHOLDER_L3"
db $25, $EB, "PLACEHOLDER_PL"
db $25, $4B, "PLACEHOLDER_L1"
db $25, $6B, "PLACEHOLDER_L2"
db $25, $8B, "PLACEHOLDER_L3"
db $25, $EB, "PLACEHOLDER_PL"
db $25, $4B, "PLACEHOLDER_L1"
db $25, $6B, "PLACEHOLDER_L2"
db $25, $8B, "PLACEHOLDER_L3"
db $25, $EB, "PLACEHOLDER_PL"
db $25, $4B, "PLACEHOLDER_L1"
db $25, $6B, "PLACEHOLDER_L2"
db $25, $8B, "PLACEHOLDER_L3"
db $25, $EB, "PLACEHOLDER_PL"
db $25, $4B, "PLACEHOLDER_L1"
db $25, $6B, "PLACEHOLDER_L2"
db $25, $8B, "PLACEHOLDER_L3"
db $25, $EB, "PLACEHOLDER_PL"
db $25, $4B, "PLACEHOLDER_L1"
db $25, $6B, "PLACEHOLDER_L2"
db $25, $8B, "PLACEHOLDER_L3"
db $25, $EB, "PLACEHOLDER_PL"
db $25, $4B, "PLACEHOLDER_L1"
db $25, $6B, "PLACEHOLDER_L2"
db $25, $8B, "PLACEHOLDER_L3"
db $25, $EB, "PLACEHOLDER_PL"
db $25, $4B, "PLACEHOLDER_L1"
db $25, $6B, "PLACEHOLDER_L2"
db $25, $8B, "PLACEHOLDER_L3"
db $25, $EB, "PLACEHOLDER_PL"
db $25, $4B, "PLACEHOLDER_L1"
db $25, $6B, "PLACEHOLDER_L2"
db $25, $8B, "PLACEHOLDER_L3"
db $25, $EB, "PLACEHOLDER_PL"
db $25, $4B, "PLACEHOLDER_L1"
db $25, $6B, "PLACEHOLDER_L2"
db $25, $8B, "PLACEHOLDER_L3"
db $25, $EB, "PLACEHOLDER_PL"
%org($FFB0, $0F)
db "MM2_BASEPATCH_ARCHI "

BIN
worlds/mm2/src/mm2font.dat Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,5 @@
from test.bases import WorldTestBase
class MM2TestBase(WorldTestBase):
game = "Mega Man 2"

View File

@@ -0,0 +1,47 @@
from . import MM2TestBase
from ..locations import (quick_man_locations, heat_man_locations, wily_1_locations, wily_2_locations,
wily_3_locations, wily_4_locations, wily_5_locations, wily_6_locations,
energy_pickups, etank_1ups)
from ..names import *
class TestAccess(MM2TestBase):
options = {
"consumables": "all"
}
def test_time_stopper(self) -> None:
"""Optional based on Enable Lasers setting, confirm these are the locations affected"""
locations = [*quick_man_locations, *energy_pickups["Quick Man Stage"], *etank_1ups["Quick Man Stage"]]
items = [["Time Stopper"]]
self.assertAccessDependency(locations, items)
def test_item_2(self) -> None:
"""Optional based on Yoku Block setting, confirm these are the locations affected"""
locations = [*heat_man_locations, *etank_1ups["Heat Man Stage"]]
items = [["Item 2 - Rocket"]]
self.assertAccessDependency(locations, items, True)
def test_any_item(self) -> None:
locations = [flash_man_c2, quick_man_c1, crash_man_c3]
items = [["Item 1 - Propeller"], ["Item 2 - Rocket"], ["Item 3 - Bouncy"]]
self.assertAccessDependency(locations, items, True)
locations = [metal_man_c2, metal_man_c3]
items = [["Item 1 - Propeller"], ["Item 2 - Rocket"]]
self.assertAccessDependency(locations, items, True)
def test_all_items(self) -> None:
locations = [flash_man_c2, quick_man_c1, crash_man_c3, metal_man_c2, metal_man_c3, *heat_man_locations,
*etank_1ups["Heat Man Stage"], *wily_1_locations, *wily_2_locations, *wily_3_locations,
*wily_4_locations, *wily_5_locations, *wily_6_locations, *etank_1ups["Wily Stage 1"],
*etank_1ups["Wily Stage 2"], *etank_1ups["Wily Stage 3"], *etank_1ups["Wily Stage 4"],
*energy_pickups["Wily Stage 1"], *energy_pickups["Wily Stage 2"], *energy_pickups["Wily Stage 3"],
*energy_pickups["Wily Stage 4"]]
items = [["Item 1 - Propeller", "Item 2 - Rocket", "Item 3 - Bouncy"]]
self.assertAccessDependency(locations, items)
def test_crash_bomber(self) -> None:
locations = [flash_man_c3, flash_man_c4, wily_2_c5, wily_2_c6, wily_3_c1, wily_3_c2,
wily_4, wily_stage_4]
items = [["Crash Bomber"]]
self.assertAccessDependency(locations, items)

View File

@@ -0,0 +1,104 @@
from math import ceil
from . import MM2TestBase
from ..options import bosses
# Need to figure out how this test should work
def validate_wily_5(base: MM2TestBase) -> None:
world = base.multiworld.worlds[base.player]
weapon_damage = world.weapon_damage
weapon_costs = {
0: 0,
1: 10,
2: 2,
3: 3,
4: 0.5,
5: 0.125,
6: 4,
7: 0.25,
8: 7,
}
boss_health = {boss: 0x1C if boss != 12 else 0x1C * 2 for boss in [*range(8), 12]}
weapon_energy = {key: float(0x1C) for key in weapon_costs}
weapon_boss = {boss: {weapon: world.weapon_damage[weapon][boss] for weapon in world.weapon_damage}
for boss in [*range(8), 12]}
flexibility = {
boss: (
sum(damage_value > 0 for damage_value in
weapon_damages.values()) # Amount of weapons that hit this boss
* sum(weapon_damages.values()) # Overall damage that those weapons do
)
for boss, weapon_damages in weapon_boss.items() if boss != 12
}
flexibility = sorted(flexibility, key=flexibility.get) # Fast way to sort dict by value
used_weapons = {i: set() for i in [*range(8), 12]}
for boss in [*flexibility, 12]:
boss_damage = weapon_boss[boss]
weapon_weight = {weapon: (weapon_energy[weapon] / damage) if damage else 0 for weapon, damage in
boss_damage.items() if weapon_energy[weapon] > 0}
if boss_damage[8]:
boss_damage[8] = 1.75 * boss_damage[8]
if any(boss_damage[i] > 0 for i in range(8)) and 8 in weapon_weight:
# We get exactly one use of Time Stopper during the rush
# So we want to make sure that use is absolutely needed
weapon_weight[8] = min(weapon_weight[8], 0.001)
while boss_health[boss] > 0:
if boss_damage[0] > 0:
boss_health[boss] = 0 # if we can buster, we should buster
continue
highest, wp = max(zip(weapon_weight.values(), weapon_weight.keys()))
uses = weapon_energy[wp] // weapon_costs[wp]
used_weapons[boss].add(wp)
if int(uses * boss_damage[wp]) > boss_health[boss]:
used = ceil(boss_health[boss] / boss_damage[wp])
weapon_energy[wp] -= weapon_costs[wp] * used
boss_health[boss] = 0
elif highest <= 0:
# we are out of weapons that can actually damage the boss
base.fail(f"Ran out of weapon energy to damage "
f"{next(name for name in bosses if bosses[name] == boss)}\n"
f"Seed: {base.multiworld.seed}\n"
f"Damage Table: {weapon_damage}")
else:
# drain the weapon and continue
boss_health[boss] -= int(uses * boss_damage[wp])
weapon_energy[wp] -= weapon_costs[wp] * uses
weapon_weight.pop(wp)
class StrictWeaknessTests(MM2TestBase):
options = {
"strict_weakness": True,
"yoku_jumps": True,
"enable_lasers": True
}
def test_that_every_boss_has_a_weakness(self) -> None:
world = self.multiworld.worlds[self.player]
weapon_damage = world.weapon_damage
for boss in range(14):
if not any(weapon_damage[weapon][boss] for weapon in range(9)):
self.fail(f"Boss {boss} generated without weakness! Seed: {self.multiworld.seed}")
def test_wily_5(self) -> None:
validate_wily_5(self)
class RandomStrictWeaknessTests(MM2TestBase):
options = {
"strict_weakness": True,
"random_weakness": "randomized",
"yoku_jumps": True,
"enable_lasers": True
}
def test_that_every_boss_has_a_weakness(self) -> None:
world = self.multiworld.worlds[self.player]
weapon_damage = world.weapon_damage
for boss in range(14):
if not any(weapon_damage[weapon][boss] for weapon in range(9)):
self.fail(f"Boss {boss} generated without weakness! Seed: {self.multiworld.seed}")
def test_wily_5(self) -> None:
validate_wily_5(self)

90
worlds/mm2/text.py Normal file
View File

@@ -0,0 +1,90 @@
from typing import DefaultDict
from collections import defaultdict
MM2_WEAPON_ENCODING: DefaultDict[str, int] = defaultdict(lambda x: 0x6F, {
' ': 0x40,
'A': 0x41,
'B': 0x42,
'C': 0x43,
'D': 0x44,
'E': 0x45,
'F': 0x46,
'G': 0x47,
'H': 0x48,
'I': 0x49,
'J': 0x4A,
'K': 0x4B,
'L': 0x4C,
'M': 0x4D,
'N': 0x4E,
'O': 0x4F,
'P': 0x50,
'Q': 0x51,
'R': 0x52,
'S': 0x53,
'T': 0x54,
'U': 0x55,
'V': 0x56,
'W': 0x57,
'X': 0x58,
'Y': 0x59,
'Z': 0x5A,
# 0x5B is the small r in Dr Light
'.': 0x5C,
',': 0x5D,
'\'': 0x5E,
'!': 0x5F,
'(': 0x60,
')': 0x61,
'#': 0x62,
'$': 0x63,
'%': 0x64,
'&': 0x65,
'*': 0x66,
'+': 0x67,
'/': 0x68,
'\\': 0x69,
':': 0x6A,
';': 0x6B,
'<': 0x6C,
'>': 0x6D,
'=': 0x6E,
'?': 0x6F,
'@': 0x70,
'[': 0x71,
']': 0x72,
'^': 0x73,
'_': 0x74,
'`': 0x75,
'{': 0x76,
'}': 0x77,
'|': 0x78,
'~': 0x79,
'\"': 0x92,
'-': 0x94,
'0': 0xA0,
'1': 0xA1,
'2': 0xA2,
'3': 0xA3,
'4': 0xA4,
'5': 0xA5,
'6': 0xA6,
'7': 0xA7,
'8': 0xA8,
'9': 0xA9,
})
class MM2TextEntry:
def __init__(self, text: str = "", coords: int = 0x0B):
self.target_area: int = 0x25 # don't change
self.coords: int = coords # 0xYX, Y can only be increments of 0x20
self.text: str = text
def resolve(self) -> bytes:
data = bytearray()
data.append(self.target_area)
data.append(self.coords)
data.extend([MM2_WEAPON_ENCODING[x] for x in self.text.upper()])
data.extend([0x40] * (14 - len(self.text)))
return bytes(data)

View File

@@ -46,6 +46,8 @@ class MuseDashCollections:
"CHAOS Glitch",
"FM 17314 SUGAR RADIO",
"Yume Ou Mono Yo Secret",
"Echo over you... Secret",
"Tsukuyomi Ni Naru Replaced",
]
album_items: Dict[str, AlbumData] = {}

View File

@@ -553,7 +553,7 @@ NOVA|73-4|Happy Otaku Pack Vol.19|True|6|8|10|
Heaven's Gradius|73-5|Happy Otaku Pack Vol.19|True|6|8|10|
Ray Tuning|74-0|CHUNITHM COURSE MUSE|True|6|8|10|
World Vanquisher|74-1|CHUNITHM COURSE MUSE|True|6|8|10|11
Tsukuyomi Ni Naru|74-2|CHUNITHM COURSE MUSE|False|5|7|9|
Tsukuyomi Ni Naru Replaced|74-2|CHUNITHM COURSE MUSE|True|5|7|9|
The wheel to the right|74-3|CHUNITHM COURSE MUSE|True|5|7|9|11
Climax|74-4|CHUNITHM COURSE MUSE|True|4|8|11|11
Spider's Thread|74-5|CHUNITHM COURSE MUSE|True|5|8|10|12
@@ -567,3 +567,16 @@ SUPERHERO|75-0|Novice Rider Pack|False|2|4|7|
Highway_Summer|75-1|Novice Rider Pack|True|2|4|6|
Mx. Black Box|75-2|Novice Rider Pack|True|5|7|9|
Sweet Encounter|75-3|Novice Rider Pack|True|2|4|7|
Echo over you... Secret|0-55|Default Music|False|6|8|10|
Echo over you...|0-56|Default Music|False|1|4|0|
Tsukuyomi Ni Naru|74-6|CHUNITHM COURSE MUSE|True|5|8|10|
disco light|76-0|MUSE RADIO FM105|True|5|7|9|
room light feat.chancylemon|76-1|MUSE RADIO FM105|True|3|5|7|
Invisible|76-2|MUSE RADIO FM105|True|3|5|8|
Christmas Season-LLABB|76-3|MUSE RADIO FM105|True|1|4|7|
Hyouryu|77-0|Let's Rhythm Jam!|False|6|8|10|
The Whole Rest|77-1|Let's Rhythm Jam!|False|5|8|10|11
Hydra|77-2|Let's Rhythm Jam!|False|4|7|11|
Pastel Lines|77-3|Let's Rhythm Jam!|False|3|6|9|
LINK x LIN#S|77-4|Let's Rhythm Jam!|False|3|6|9|
Arcade ViruZ|77-5|Let's Rhythm Jam!|False|6|8|10|

View File

@@ -445,7 +445,7 @@ def shuffle_random_entrances(ootworld):
# Gather locations to keep reachable for validation
all_state = ootworld.get_state_with_complete_itempool()
all_state.sweep_for_events(locations=ootworld.get_locations())
all_state.sweep_for_advancements(locations=ootworld.get_locations())
locations_to_ensure_reachable = {loc for loc in world.get_reachable_locations(all_state, player) if not (loc.type == 'Drop' or (loc.type == 'Event' and 'Subrule' in loc.name))}
# Set entrance data for all entrances
@@ -791,8 +791,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
all_state = all_state_orig.copy()
none_state = none_state_orig.copy()
all_state.sweep_for_events(locations=ootworld.get_locations())
none_state.sweep_for_events(locations=ootworld.get_locations())
all_state.sweep_for_advancements(locations=ootworld.get_locations())
none_state.sweep_for_advancements(locations=ootworld.get_locations())
if ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances or ootworld.spawn_positions:
time_travel_state = none_state.copy()

View File

@@ -228,7 +228,7 @@ def set_shop_rules(ootworld):
def set_entrances_based_rules(ootworld):
all_state = ootworld.get_state_with_complete_itempool()
all_state.sweep_for_events(locations=ootworld.get_locations())
all_state.sweep_for_advancements(locations=ootworld.get_locations())
for location in filter(lambda location: location.type == 'Shop', ootworld.get_locations()):
# If a shop is not reachable as adult, it can't have Goron Tunic or Zora Tunic as child can't buy these

View File

@@ -847,7 +847,7 @@ class OOTWorld(World):
# Make sure to only kill actual internal events, not in-game "events"
all_state = self.get_state_with_complete_itempool()
all_locations = self.get_locations()
all_state.sweep_for_events(locations=all_locations)
all_state.sweep_for_advancements(locations=all_locations)
reachable = self.multiworld.get_reachable_locations(all_state, self.player)
unreachable = [loc for loc in all_locations if
(loc.internal or loc.type == 'Drop') and loc.address is None and loc.locked and loc not in reachable]
@@ -875,7 +875,7 @@ class OOTWorld(World):
state = base_state.copy()
for item in self.get_pre_fill_items():
self.collect(state, item)
state.sweep_for_events(locations=self.get_locations())
state.sweep_for_advancements(locations=self.get_locations())
return state
# Prefill shops, songs, and dungeon items
@@ -887,7 +887,7 @@ class OOTWorld(World):
state = CollectionState(self.multiworld)
for item in self.itempool:
self.collect(state, item)
state.sweep_for_events(locations=self.get_locations())
state.sweep_for_advancements(locations=self.get_locations())
# Place dungeon items
special_fill_types = ['GanonBossKey', 'BossKey', 'SmallKey', 'HideoutSmallKey', 'Map', 'Compass']

View File

@@ -56,8 +56,8 @@ class OSRSWorld(World):
locations_by_category: typing.Dict[str, typing.List[LocationRow]]
def __init__(self, world: MultiWorld, player: int):
super().__init__(world, player)
def __init__(self, multiworld: MultiWorld, player: int):
super().__init__(multiworld, player)
self.region_name_to_data = {}
self.location_name_to_data = {}

View File

@@ -137,6 +137,8 @@ class PokemonEmeraldClient(BizHawkClient):
previous_death_link: float
ignore_next_death_link: bool
current_map: Optional[int]
def __init__(self) -> None:
super().__init__()
self.local_checked_locations = set()
@@ -150,6 +152,7 @@ class PokemonEmeraldClient(BizHawkClient):
self.death_counter = None
self.previous_death_link = 0
self.ignore_next_death_link = False
self.current_map = None
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
from CommonClient import logger
@@ -243,6 +246,7 @@ class PokemonEmeraldClient(BizHawkClient):
sb1_address = int.from_bytes(guards["SAVE BLOCK 1"][1], "little")
sb2_address = int.from_bytes(guards["SAVE BLOCK 2"][1], "little")
await self.handle_tracker_info(ctx, guards)
await self.handle_death_link(ctx, guards)
await self.handle_received_items(ctx, guards)
await self.handle_wonder_trade(ctx, guards)
@@ -403,6 +407,30 @@ class PokemonEmeraldClient(BizHawkClient):
# Exit handler and return to main loop to reconnect
pass
async def handle_tracker_info(self, ctx: "BizHawkClientContext", guards: Dict[str, Tuple[int, bytes, str]]) -> None:
# Current map
sb1_address = int.from_bytes(guards["SAVE BLOCK 1"][1], "little")
read_result = await bizhawk.guarded_read(
ctx.bizhawk_ctx,
[(sb1_address + 0x4, 2, "System Bus")],
[guards["SAVE BLOCK 1"]]
)
if read_result is None: # Save block moved
return
current_map = int.from_bytes(read_result[0], "big")
if current_map != self.current_map:
self.current_map = current_map
await ctx.send_msgs([{
"cmd": "Bounce",
"slots": [ctx.slot],
"data": {
"type": "MapUpdate",
"mapId": current_map,
},
}])
async def handle_death_link(self, ctx: "BizHawkClientContext", guards: Dict[str, Tuple[int, bytes, str]]) -> None:
"""
Checks whether the player has died while connected and sends a death link if so. Queues a death link in the game

View File

@@ -114,6 +114,14 @@ class PokemonEmeraldProcedurePatch(APProcedurePatch, APTokenMixin):
def write_tokens(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePatch) -> None:
# TODO: Remove when the base patch is updated to include this change
# Moves an NPC to avoid overlapping people during trainersanity
patch.write_token(
APTokenTypes.WRITE,
0x53A298 + (0x18 * 7) + 4, # Space Center 1F event address + 8th event + 4-byte offset for x coord
struct.pack("<H", 11)
)
# Set free fly location
if world.options.free_fly_location:
patch.write_token(

View File

@@ -296,7 +296,7 @@ class PokemonRedBlueWorld(World):
if attempt > 1:
for mon in poke_data.pokemon_data.keys():
state.collect(self.create_item(mon), True)
state.sweep_for_events()
state.sweep_for_advancements()
self.multiworld.random.shuffle(badges)
self.multiworld.random.shuffle(badgelocs)
badgelocs_copy = badgelocs.copy()

View File

@@ -2439,7 +2439,7 @@ def door_shuffle(world, multiworld, player, badges, badge_locs):
state_copy = state.copy()
state_copy.collect(item, True)
state.sweep_for_events(locations=event_locations)
state.sweep_for_advancements(locations=event_locations)
new_reachable_entrances = len([entrance for entrance in entrances if entrance in reachable_entrances or
entrance.parent_region.can_reach(state_copy)])
return new_reachable_entrances > len(reachable_entrances)
@@ -2480,7 +2480,7 @@ def door_shuffle(world, multiworld, player, badges, badge_locs):
while entrances:
state.update_reachable_regions(player)
state.sweep_for_events(locations=event_locations)
state.sweep_for_advancements(locations=event_locations)
multiworld.random.shuffle(entrances)

View File

@@ -26,6 +26,7 @@ class FishItem:
fresh_water = (Region.farm, Region.forest, Region.town, Region.mountain)
ocean = (Region.beach,)
tide_pools = (Region.tide_pools,)
town_river = (Region.town,)
mountain_lake = (Region.mountain,)
forest_pond = (Region.forest,)
@@ -118,13 +119,13 @@ midnight_squid = create_fish(Fish.midnight_squid, night_market, season.winter, 5
spook_fish = create_fish(Fish.spook_fish, night_market, season.winter, 60)
angler = create_fish(Fish.angler, town_river, season.fall, 85, True, False)
crimsonfish = create_fish(Fish.crimsonfish, ocean, season.summer, 95, True, False)
crimsonfish = create_fish(Fish.crimsonfish, tide_pools, season.summer, 95, True, False)
glacierfish = create_fish(Fish.glacierfish, forest_river, season.winter, 100, True, False)
legend = create_fish(Fish.legend, mountain_lake, season.spring, 110, True, False)
mutant_carp = create_fish(Fish.mutant_carp, sewers, season.all_seasons, 80, True, False)
ms_angler = create_fish(Fish.ms_angler, town_river, season.fall, 85, True, True)
son_of_crimsonfish = create_fish(Fish.son_of_crimsonfish, ocean, season.summer, 95, True, True)
son_of_crimsonfish = create_fish(Fish.son_of_crimsonfish, tide_pools, season.summer, 95, True, True)
glacierfish_jr = create_fish(Fish.glacierfish_jr, forest_river, season.winter, 100, True, True)
legend_ii = create_fish(Fish.legend_ii, mountain_lake, season.spring, 110, True, True)
radioactive_carp = create_fish(Fish.radioactive_carp, sewers, season.all_seasons, 80, True, True)

View File

@@ -27,6 +27,7 @@ def collect_fishing_abilities(tester: SVTestBase):
tester.multiworld.state.collect(tester.world.create_item("Fall"), prevent_sweep=False)
tester.multiworld.state.collect(tester.world.create_item("Winter"), prevent_sweep=False)
tester.multiworld.state.collect(tester.world.create_item(Transportation.desert_obelisk), prevent_sweep=False)
tester.multiworld.state.collect(tester.world.create_item("Beach Bridge"), prevent_sweep=False)
tester.multiworld.state.collect(tester.world.create_item("Railroad Boulder Removed"), prevent_sweep=False)
tester.multiworld.state.collect(tester.world.create_item("Island North Turtle"), prevent_sweep=False)
tester.multiworld.state.collect(tester.world.create_item("Island West Turtle"), prevent_sweep=False)

View File

@@ -258,15 +258,19 @@ class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase):
def collect_lots_of_money(self):
self.multiworld.state.collect(self.world.create_item("Shipping Bin"), prevent_sweep=False)
required_prog_items = int(round(self.multiworld.worlds[self.player].total_progression_items * 0.25))
real_total_prog_items = self.multiworld.worlds[self.player].total_progression_items
required_prog_items = int(round(real_total_prog_items * 0.25))
for i in range(required_prog_items):
self.multiworld.state.collect(self.world.create_item("Stardrop"), prevent_sweep=False)
self.multiworld.worlds[self.player].total_progression_items = real_total_prog_items
def collect_all_the_money(self):
self.multiworld.state.collect(self.world.create_item("Shipping Bin"), prevent_sweep=False)
required_prog_items = int(round(self.multiworld.worlds[self.player].total_progression_items * 0.95))
real_total_prog_items = self.multiworld.worlds[self.player].total_progression_items
required_prog_items = int(round(real_total_prog_items * 0.95))
for i in range(required_prog_items):
self.multiworld.state.collect(self.world.create_item("Stardrop"), prevent_sweep=False)
self.multiworld.worlds[self.player].total_progression_items = real_total_prog_items
def collect_everything(self):
non_event_items = [item for item in self.multiworld.get_items() if item.code]

View File

@@ -0,0 +1,61 @@
from ...options import SeasonRandomization, Friendsanity, FriendsanityHeartSize, Fishsanity, ExcludeGingerIsland, SkillProgression, ToolProgression, \
ElevatorProgression, SpecialOrderLocations
from ...strings.fish_names import Fish
from ...test import SVTestBase
class TestNeedRegionToCatchFish(SVTestBase):
options = {
SeasonRandomization.internal_name: SeasonRandomization.option_disabled,
ElevatorProgression.internal_name: ElevatorProgression.option_vanilla,
SkillProgression.internal_name: SkillProgression.option_vanilla,
ToolProgression.internal_name: ToolProgression.option_vanilla,
Fishsanity.internal_name: Fishsanity.option_all,
ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi,
}
def test_catch_fish_requires_region_unlock(self):
fish_and_items = {
Fish.crimsonfish: ["Beach Bridge"],
Fish.void_salmon: ["Railroad Boulder Removed", "Dark Talisman"],
Fish.woodskip: ["Glittering Boulder Removed", "Progressive Weapon"], # For the ores to get the axe upgrades
Fish.mutant_carp: ["Rusty Key"],
Fish.slimejack: ["Railroad Boulder Removed", "Rusty Key"],
Fish.lionfish: ["Boat Repair"],
Fish.blue_discus: ["Island Obelisk", "Island West Turtle"],
Fish.stingray: ["Boat Repair", "Island Resort"],
Fish.ghostfish: ["Progressive Weapon"],
Fish.stonefish: ["Progressive Weapon"],
Fish.ice_pip: ["Progressive Weapon", "Progressive Weapon"],
Fish.lava_eel: ["Progressive Weapon", "Progressive Weapon", "Progressive Weapon"],
Fish.sandfish: ["Bus Repair"],
Fish.scorpion_carp: ["Desert Obelisk"],
# Starting the extended family quest requires having caught all the legendaries before, so they all have the rules of every other legendary
Fish.son_of_crimsonfish: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"],
Fish.radioactive_carp: ["Beach Bridge", "Rusty Key", "Boat Repair", "Island West Turtle", "Qi Walnut Room"],
Fish.glacierfish_jr: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"],
Fish.legend_ii: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"],
Fish.ms_angler: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"],
}
self.original_state = self.multiworld.state.copy()
for fish in fish_and_items:
with self.subTest(f"Region rules for {fish}"):
self.collect_all_the_money()
item_names = fish_and_items[fish]
location = self.multiworld.get_location(f"Fishsanity: {fish}", self.player)
self.assert_reach_location_false(location, self.multiworld.state)
items = []
for item_name in item_names:
items.append(self.collect(item_name))
with self.subTest(f"{fish} can be reached with {item_names}"):
self.assert_reach_location_true(location, self.multiworld.state)
for item_required in items:
self.multiworld.state = self.original_state.copy()
with self.subTest(f"{fish} requires {item_required.name}"):
for item_to_collect in items:
if item_to_collect.name != item_required.name:
self.collect(item_to_collect)
self.assert_reach_location_false(location, self.multiworld.state)
self.multiworld.state = self.original_state.copy()

View File

@@ -421,7 +421,8 @@ class TunicWorld(World):
except KeyError:
# logic bug, proceed with warning since it takes a long time to update AP
warning(f"{location.name} is not logically accessible for {self.player_name}. "
"Creating entrance hint Inaccessible. Please report this to the TUNIC rando devs.")
"Creating entrance hint Inaccessible. Please report this to the TUNIC rando devs. "
"If you are using Plando Items (excluding early locations), then this is likely the cause.")
hint_text = "Inaccessible"
else:
while connection != ("Menu", None):

View File

@@ -49,12 +49,14 @@ class V6World(World):
self.area_cost_map = {}
set_rules(self.multiworld, self.options, self.player, self.area_connections, self.area_cost_map)
def create_item(self, name: str) -> Item:
return V6Item(name, ItemClassification.progression, item_table[name], self.player)
def create_item(self, name: str, classification: ItemClassification = ItemClassification.filler) -> Item:
return V6Item(name, classification, item_table[name], self.player)
def create_items(self):
trinkets = [self.create_item("Trinket " + str(i+1).zfill(2)) for i in range(0,20)]
self.multiworld.itempool += trinkets
progtrinkets = [self.create_item("Trinket " + str(i+1).zfill(2), ItemClassification.progression) for i in range(0, (4 * self.options.door_cost.value))]
filltrinkets = [self.create_item("Trinket " + str(i+1).zfill(2)) for i in range((4 * self.options.door_cost.value), 20)]
self.multiworld.itempool += progtrinkets
self.multiworld.itempool += filltrinkets
def generate_basic(self):
musiclist_o = [1,2,3,4,9,12]

View File

@@ -15,7 +15,7 @@ from .data import static_locations as static_witness_locations
from .data import static_logic as static_witness_logic
from .data.item_definition_classes import DoorItemDefinition, ItemData
from .data.utils import get_audio_logs
from .hints import CompactItemData, create_all_hints, make_compact_hint_data, make_laser_hints
from .hints import CompactHintData, create_all_hints, make_compact_hint_data, make_laser_hints
from .locations import WitnessPlayerLocations
from .options import TheWitnessOptions, witness_option_groups
from .player_items import WitnessItem, WitnessPlayerItems
@@ -68,12 +68,14 @@ class WitnessWorld(World):
player_items: WitnessPlayerItems
player_regions: WitnessPlayerRegions
log_ids_to_hints: Dict[int, CompactItemData]
laser_ids_to_hints: Dict[int, CompactItemData]
log_ids_to_hints: Dict[int, CompactHintData]
laser_ids_to_hints: Dict[int, CompactHintData]
items_placed_early: List[str]
own_itempool: List[WitnessItem]
panel_hunt_required_count: int
def _get_slot_data(self) -> Dict[str, Any]:
return {
"seed": self.random.randrange(0, 1000000),
@@ -83,12 +85,14 @@ class WitnessWorld(World):
"door_hexes_in_the_pool": self.player_items.get_door_ids_in_pool(),
"symbols_not_in_the_game": self.player_items.get_symbol_ids_not_in_pool(),
"disabled_entities": [int(h, 16) for h in self.player_logic.COMPLETELY_DISABLED_ENTITIES],
"hunt_entities": [int(h, 16) for h in self.player_logic.HUNT_ENTITIES],
"log_ids_to_hints": self.log_ids_to_hints,
"laser_ids_to_hints": self.laser_ids_to_hints,
"progressive_item_lists": self.player_items.get_progressive_item_ids_in_pool(),
"obelisk_side_id_to_EPs": static_witness_logic.OBELISK_SIDE_ID_TO_EP_HEXES,
"precompleted_puzzles": [int(h, 16) for h in self.player_logic.EXCLUDED_LOCATIONS],
"precompleted_puzzles": [int(h, 16) for h in self.player_logic.EXCLUDED_ENTITIES],
"entity_to_name": static_witness_logic.ENTITY_ID_TO_NAME,
"panel_hunt_required_absolute": self.panel_hunt_required_count
}
def determine_sufficient_progression(self) -> None:
@@ -124,10 +128,10 @@ class WitnessWorld(World):
)
if not has_locally_relevant_progression and self.multiworld.players == 1:
warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have any progression"
warning(f"{self.player_name}'s Witness world doesn't have any progression"
f" items. Please turn on Symbol Shuffle, Door Shuffle or Laser Shuffle if that doesn't seem right.")
elif not interacts_sufficiently_with_multiworld and self.multiworld.players > 1:
raise OptionError(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have enough"
raise OptionError(f"{self.player_name}'s Witness world doesn't have enough"
f" progression items that can be placed in other players' worlds. Please turn on Symbol"
f" Shuffle, Door Shuffle, or Obelisk Keys.")
@@ -151,6 +155,13 @@ class WitnessWorld(World):
if self.options.shuffle_lasers == "local":
self.options.local_items.value |= self.item_name_groups["Lasers"]
if self.options.victory_condition == "panel_hunt":
total_panels = self.options.panel_hunt_total
required_percentage = self.options.panel_hunt_required_percentage
self.panel_hunt_required_count = round(total_panels * required_percentage / 100)
else:
self.panel_hunt_required_count = 0
def create_regions(self) -> None:
self.player_regions.create_regions(self, self.player_logic)
@@ -169,7 +180,7 @@ class WitnessWorld(World):
for event_location in self.player_locations.EVENT_LOCATION_TABLE:
item_obj = self.create_item(
self.player_logic.EVENT_ITEM_PAIRS[event_location]
self.player_logic.EVENT_ITEM_PAIRS[event_location][0]
)
location_obj = self.get_location(event_location)
location_obj.place_locked_item(item_obj)
@@ -178,12 +189,13 @@ class WitnessWorld(World):
event_locations.append(location_obj)
# Place other locked items
dog_puzzle_skip = self.create_item("Puzzle Skip")
self.get_location("Town Pet the Dog").place_locked_item(dog_puzzle_skip)
self.own_itempool.append(dog_puzzle_skip)
if self.options.shuffle_dog == "puzzle_skip":
dog_puzzle_skip = self.create_item("Puzzle Skip")
self.get_location("Town Pet the Dog").place_locked_item(dog_puzzle_skip)
self.items_placed_early.append("Puzzle Skip")
self.own_itempool.append(dog_puzzle_skip)
self.items_placed_early.append("Puzzle Skip")
if self.options.early_symbol_item:
# Pick an early item to place on the tutorial gate.
@@ -192,8 +204,11 @@ class WitnessWorld(World):
]
if early_items:
random_early_item = self.random.choice(early_items)
if self.options.puzzle_randomization == "sigma_expert":
# In Expert, only tag the item as early, rather than forcing it onto the gate.
if (
self.options.puzzle_randomization == "sigma_expert"
or self.options.victory_condition == "panel_hunt"
):
# In Expert and Panel Hunt, only tag the item as early, rather than forcing it onto the gate.
self.multiworld.local_early_items[self.player][random_early_item] = 1
else:
# Force the item onto the tutorial gate check and remove it from our random pool.
@@ -202,19 +217,22 @@ class WitnessWorld(World):
self.own_itempool.append(gate_item)
self.items_placed_early.append(random_early_item)
# There are some really restrictive settings in The Witness.
# There are some really restrictive options in The Witness.
# They are rarely played, but when they are, we add some extra sphere 1 locations.
# This is done both to prevent generation failures, but also to make the early game less linear.
# Only sweeps for events because having this behavior be random based on Tutorial Gate would be strange.
state = CollectionState(self.multiworld)
state.sweep_for_events(locations=event_locations)
state.sweep_for_advancements(locations=event_locations)
num_early_locs = sum(1 for loc in self.multiworld.get_reachable_locations(state, self.player) if loc.address)
num_early_locs = sum(
1 for loc in self.multiworld.get_reachable_locations(state, self.player)
if loc.address and not loc.item
)
# Adjust the needed size for sphere 1 based on how restrictive the settings are in terms of items
# Adjust the needed size for sphere 1 based on how restrictive the options are in terms of items
needed_size = 3
needed_size = 2
needed_size += self.options.puzzle_randomization == "sigma_expert"
needed_size += self.options.shuffle_symbols
needed_size += self.options.shuffle_doors > 0
@@ -236,9 +254,10 @@ class WitnessWorld(World):
self.player_locations.add_location_late(loc)
self.get_region(region).add_locations({loc: self.location_name_to_id[loc]})
player = self.multiworld.get_player_name(self.player)
warning(f"""Location "{loc}" had to be added to {player}'s world due to insufficient sphere 1 size.""")
warning(
f"""Location "{loc}" had to be added to {self.player_name}'s world
due to insufficient sphere 1 size."""
)
def create_items(self) -> None:
# Determine pool size.
@@ -275,7 +294,7 @@ class WitnessWorld(World):
self.multiworld.push_precollected(self.create_item(inventory_item_name))
if len(item_pool) > pool_size:
error(f"{self.multiworld.get_player_name(self.player)}'s Witness world has too few locations ({pool_size})"
error(f"{self.player_name}'s Witness world has too few locations ({pool_size})"
f" to place its necessary items ({len(item_pool)}).")
return
@@ -285,7 +304,7 @@ class WitnessWorld(World):
num_puzzle_skips = self.options.puzzle_skip_amount.value
if num_puzzle_skips > remaining_item_slots:
warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world has insufficient locations"
warning(f"{self.player_name}'s Witness world has insufficient locations"
f" to place all requested puzzle skips.")
num_puzzle_skips = remaining_item_slots
item_pool["Puzzle Skip"] = num_puzzle_skips
@@ -305,8 +324,8 @@ class WitnessWorld(World):
self.options.local_items.value.add(item_name)
def fill_slot_data(self) -> Dict[str, Any]:
self.log_ids_to_hints: Dict[int, CompactItemData] = {}
self.laser_ids_to_hints: Dict[int, CompactItemData] = {}
self.log_ids_to_hints: Dict[int, CompactHintData] = {}
self.laser_ids_to_hints: Dict[int, CompactHintData] = {}
already_hinted_locations = set()

View File

@@ -176,6 +176,7 @@ Door - 0x03444 (Vault Door) - 0x0CC7B
Door - 0x09FEE (Light Room Entry) - 0x0C339
158701 - 0x03608 (Laser Panel) - 0x012D7 & 0x0A15F - True
Laser - 0x012FB (Laser) - 0x03608
159804 - 0xFFD03 (Laser Activated + Redirected) - 0x09F98 & 0x012FB - True
159020 - 0x3351D (Sand Snake EP) - True - True
159030 - 0x0053C (Facade Right EP) - True - True
159031 - 0x00771 (Facade Left EP) - True - True
@@ -980,7 +981,7 @@ Mountainside Obelisk (Mountainside) - Entry - True:
159739 - 0x00367 (Obelisk) - True - True
Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085:
159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True
159550 - 0x28B91 (Thundercloud EP) - 0xFFD03 - True
158612 - 0x17C42 (Discard) - True - Triangles
158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Dots & Black/White Squares & Dots
Door - 0x00085 (Vault Door) - 0x002A6

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