diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cb9fda69b4..4138f93f04 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ jobs: python-version: '3.8' - name: Download run-time dependencies run: | - Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/v0.0.81/sni-v0.0.81-windows-amd64.zip -OutFile sni.zip + Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-windows-amd64.zip -OutFile sni.zip Expand-Archive -Path sni.zip -DestinationPath SNI -Force Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/7.0.1/win-x64.zip -OutFile enemizer.zip Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force @@ -63,7 +63,7 @@ jobs: chmod a+rx appimagetool - name: Download run-time dependencies run: | - wget -nv https://github.com/alttpo/sni/releases/download/v0.0.81/sni-v0.0.81-manylinux2014-amd64.tar.xz + wget -nv https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-manylinux2014-amd64.tar.xz tar xf sni-*.tar.xz rm sni-*.tar.xz mv sni-* SNI diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6e5095cff2..aa82883ff1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,7 +51,7 @@ jobs: chmod a+rx appimagetool - name: Download run-time dependencies run: | - wget -nv https://github.com/alttpo/sni/releases/download/v0.0.81/sni-v0.0.81-manylinux2014-amd64.tar.xz + wget -nv https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-manylinux2014-amd64.tar.xz tar xf sni-*.tar.xz rm sni-*.tar.xz mv sni-* SNI diff --git a/.gitignore b/.gitignore index 9ae4aaba3e..6a0231c22b 100644 --- a/.gitignore +++ b/.gitignore @@ -117,6 +117,9 @@ target/ profile_default/ ipython_config.py +# vim editor +*.swp + # SageMath parsed files *.sage.py @@ -153,10 +156,17 @@ dmypy.json # Cython debug symbols cython_debug/ -#minecraft server stuff +# minecraft server stuff jdk*/ minecraft*/ minecraft_versions.json -#pyenv +# pyenv .python-version + +# OS General Files +.DS_Store +.AppleDouble +.LSOverride +Thumbs.db +[Dd]esktop.ini diff --git a/BaseClasses.py b/BaseClasses.py index a186404727..cea1d48e6f 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -126,7 +126,6 @@ class MultiWorld(): set_player_attr('beemizer_total_chance', 0) set_player_attr('beemizer_trap_chance', 0) set_player_attr('escape_assist', []) - set_player_attr('open_pyramid', False) set_player_attr('treasure_hunt_icon', 'Triforce Piece') set_player_attr('treasure_hunt_count', 0) set_player_attr('clock_mode', False) @@ -167,7 +166,7 @@ class MultiWorld(): self.player_types[new_id] = NetUtils.SlotType.group self._region_cache[new_id] = {} world_type = AutoWorld.AutoWorldRegister.world_types[game] - for option_key, option in world_type.options.items(): + for option_key, option in world_type.option_definitions.items(): getattr(self, option_key)[new_id] = option(option.default) for option_key, option in Options.common_options.items(): getattr(self, option_key)[new_id] = option(option.default) @@ -205,7 +204,7 @@ class MultiWorld(): for player in self.player_ids: self.custom_data[player] = {} world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]] - for option_key in world_type.options: + for option_key in world_type.option_definitions: setattr(self, option_key, getattr(args, option_key, {})) self.worlds[player] = world_type(self, player) @@ -385,25 +384,17 @@ class MultiWorld(): return self.worlds[player].create_item(item_name) def push_precollected(self, item: Item): - item.world = self self.precollected_items[item.player].append(item) self.state.collect(item, True) def push_item(self, location: Location, item: Item, collect: bool = True): - if not isinstance(location, Location): - raise RuntimeError( - 'Cannot assign item %s to invalid location %s (player %d).' % (item, location, item.player)) + assert location.can_fill(self.state, item, False), f"Cannot place {item} into {location}." + location.item = item + item.location = location + if collect: + self.state.collect(item, location.event, location) - if location.can_fill(self.state, item, False): - location.item = item - item.location = location - item.world = self # try to not have this here anymore - if collect: - self.state.collect(item, location.event, location) - - logging.debug('Placed %s at %s', item, location) - else: - raise RuntimeError('Cannot assign item %s to location %s.' % (item, location)) + logging.debug('Placed %s at %s', item, location) def get_entrances(self) -> List[Entrance]: if self._cached_entrances is None: @@ -1073,26 +1064,25 @@ class LocationProgressType(IntEnum): class Location: - # If given as integer, then this is the shop's inventory index - shop_slot: Optional[int] = None - shop_slot_disabled: bool = False + game: str = "Generic" + player: int + name: str + address: Optional[int] + parent_region: Optional[Region] event: bool = False locked: bool = False - game: str = "Generic" show_in_spoiler: bool = True - crystal: bool = False progress_type: LocationProgressType = LocationProgressType.DEFAULT always_allow = staticmethod(lambda item, state: False) access_rule = staticmethod(lambda state: True) item_rule = staticmethod(lambda item: True) item: Optional[Item] = None - parent_region: Optional[Region] - def __init__(self, player: int, name: str = '', address: int = None, parent=None): - self.name: str = name - self.address: Optional[int] = address + def __init__(self, player: int, name: str = '', address: Optional[int] = None, parent: Optional[Region] = None): + self.player = player + self.name = name + self.address = address self.parent_region = parent - self.player: int = player def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool: return self.always_allow(state, item) or (self.item_rule(item) and (not check_access or self.can_reach(state))) @@ -1109,7 +1099,6 @@ class Location: self.item = item item.location = self self.event = item.advancement - self.item.world = self.parent_region.world self.locked = True def __repr__(self): @@ -1154,39 +1143,28 @@ class ItemClassification(IntFlag): class Item: - location: Optional[Location] = None - world: Optional[MultiWorld] = None - code: Optional[int] = None # an item with ID None is called an Event, and does not get written to multidata - name: str game: str = "Generic" - type: str = None + __slots__ = ("name", "classification", "code", "player", "location") + name: str classification: ItemClassification - - # need to find a decent place for these to live and to allow other games to register texts if they want. - pedestal_credit_text: str = "and the Unknown Item" - sickkid_credit_text: Optional[str] = None - magicshop_credit_text: Optional[str] = None - zora_credit_text: Optional[str] = None - fluteboy_credit_text: Optional[str] = None - - # hopefully temporary attributes to satisfy legacy LttP code, proper implementation in subclass ALttPItem - smallkey: bool = False - bigkey: bool = False - map: bool = False - compass: bool = False + code: Optional[int] + """an item with code None is called an Event, and does not get written to multidata""" + player: int + location: Optional[Location] def __init__(self, name: str, classification: ItemClassification, code: Optional[int], player: int): self.name = name self.classification = classification self.player = player self.code = code + self.location = None @property - def hint_text(self): + def hint_text(self) -> str: return getattr(self, "_hint_text", self.name.replace("_", " ").replace("-", " ")) @property - def pedestal_hint_text(self): + def pedestal_hint_text(self) -> str: return getattr(self, "_pedestal_hint_text", self.name.replace("_", " ").replace("-", " ")) @property @@ -1212,7 +1190,7 @@ class Item: def __eq__(self, other): return self.name == other.name and self.player == other.player - def __lt__(self, other: Item): + def __lt__(self, other: Item) -> bool: if other.player != self.player: return other.player < self.player return self.name < other.name @@ -1220,11 +1198,13 @@ class Item: def __hash__(self): return hash((self.name, self.player)) - def __repr__(self): + def __repr__(self) -> str: return self.__str__() - def __str__(self): - return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})' + def __str__(self) -> str: + if self.location and self.location.parent_region and self.location.parent_region.world: + return self.location.parent_region.world.get_name_string_for_object(self) + return f"{self.name} (Player {self.player})" class Spoiler(): @@ -1408,7 +1388,7 @@ class Spoiler(): outfile.write('Game: %s\n' % self.world.game[player]) for f_option, option in Options.per_game_common_options.items(): write_option(f_option, option) - options = self.world.worlds[player].options + options = self.world.worlds[player].option_definitions if options: for f_option, option in options.items(): write_option(f_option, option) @@ -1431,8 +1411,6 @@ class Spoiler(): outfile.write('Entrance Shuffle: %s\n' % self.world.shuffle[player]) if self.world.shuffle[player] != "vanilla": outfile.write('Entrance Shuffle Seed %s\n' % self.world.worlds[player].er_seed) - outfile.write('Pyramid hole pre-opened: %s\n' % ( - 'Yes' if self.world.open_pyramid[player] else 'No')) outfile.write('Shop inventory shuffle: %s\n' % bool_to_text("i" in self.world.shop_shuffle[player])) outfile.write('Shop price shuffle: %s\n' % diff --git a/ChecksFinderClient.py b/ChecksFinderClient.py index 3bb96ceb6c..e774b3faa7 100644 --- a/ChecksFinderClient.py +++ b/ChecksFinderClient.py @@ -1,6 +1,8 @@ from __future__ import annotations import os +import sys import asyncio +import shutil import ModuleUpdate ModuleUpdate.update() @@ -32,6 +34,24 @@ class ChecksFinderContext(CommonContext): 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%/ChecksFinder") + else: + # not windows. game is an exe so let's see if wine might be around to run it + if "WINEPREFIX" in os.environ: + wineprefix = os.environ["WINEPREFIX"] + elif shutil.which("wine") or shutil.which("wine-stable"): + wineprefix = os.path.expanduser("~/.wine") # default root of wine system data, deep in which is app data + else: + msg = "ChecksFinderClient couldn't detect system type. Unable to infer required game_communication_path" + logger.error("Error: " + msg) + Utils.messagebox("Error", msg, error=True) + sys.exit(1) + self.game_communication_path = os.path.join( + wineprefix, + "drive_c", + os.path.expandvars("users/$USER/Local Settings/Application Data/ChecksFinder")) async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: @@ -41,8 +61,7 @@ class ChecksFinderContext(CommonContext): async def connection_closed(self): await super(ChecksFinderContext, self).connection_closed() - path = os.path.expandvars(r"%localappdata%/ChecksFinder") - for root, dirs, files in os.walk(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) @@ -56,26 +75,25 @@ class ChecksFinderContext(CommonContext): async def shutdown(self): await super(ChecksFinderContext, self).shutdown() - path = os.path.expandvars(r"%localappdata%/ChecksFinder") - for root, dirs, files in os.walk(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) def on_package(self, cmd: str, args: dict): if cmd in {"Connected"}: - if not os.path.exists(os.path.expandvars(r"%localappdata%/ChecksFinder")): - os.mkdir(os.path.expandvars(r"%localappdata%/ChecksFinder")) + 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.expandvars(r"%localappdata%/ChecksFinder/" + filename), 'w') as f: + with open(os.path.join(self.game_communication_path, filename), 'w') as f: f.close() if cmd in {"ReceivedItems"}: start_index = args["index"] if start_index != len(self.items_received): for item in args['items']: filename = f"AP_{str(NetworkItem(*item).location)}PLR{str(NetworkItem(*item).player)}.item" - with open(os.path.expandvars(r"%localappdata%/ChecksFinder/" + filename), 'w') as f: + with open(os.path.join(self.game_communication_path, filename), 'w') as f: f.write(str(NetworkItem(*item).item)) f.close() @@ -83,7 +101,7 @@ class ChecksFinderContext(CommonContext): if "checked_locations" in args: for ss in self.checked_locations: filename = f"send{ss}" - with open(os.path.expandvars(r"%localappdata%/ChecksFinder/" + filename), 'w') as f: + with open(os.path.join(self.game_communication_path, filename), 'w') as f: f.close() def run_gui(self): @@ -109,10 +127,9 @@ async def game_watcher(ctx: ChecksFinderContext): sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)}) await ctx.send_msgs(sync_msg) ctx.syncing = False - path = os.path.expandvars(r"%localappdata%/ChecksFinder") sending = [] victory = False - for root, dirs, files in os.walk(path): + 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] diff --git a/CommonClient.py b/CommonClient.py index 76623ff3f2..f830035425 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -493,7 +493,8 @@ async def server_loop(ctx: CommonContext, address=None): logger.info(f'Connecting to Archipelago server at {address}') try: socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None) - ctx.ui.update_address_bar(server_url.netloc) + if ctx.ui is not None: + ctx.ui.update_address_bar(server_url.netloc) ctx.server = Endpoint(socket) logger.info('Connected') ctx.server_address = address @@ -562,18 +563,21 @@ async def process_server_cmd(ctx: CommonContext, args: dict): f" for each location checked. Use !hint for more information.") ctx.hint_cost = int(args['hint_cost']) ctx.check_points = int(args['location_check_points']) - players = args.get("players", []) - if len(players) < 1: - logger.info('No player connected') - else: - players.sort() - current_team = -1 - logger.info('Connected Players:') - for network_player in players: - if network_player.team != current_team: - logger.info(f' Team #{network_player.team + 1}') - current_team = network_player.team - logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot)) + + if "players" in args: # TODO remove when servers sending this are outdated + players = args.get("players", []) + if len(players) < 1: + logger.info('No player connected') + else: + players.sort() + current_team = -1 + logger.info('Connected Players:') + for network_player in players: + if network_player.team != current_team: + logger.info(f' Team #{network_player.team + 1}') + current_team = network_player.team + logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot)) + # update datapackage await ctx.prepare_datapackage(set(args["games"]), args["datapackage_versions"]) @@ -723,7 +727,7 @@ if __name__ == '__main__': class TextContext(CommonContext): tags = {"AP", "IgnoreGame", "TextOnly"} game = "" # empty matches any game since 0.3.2 - items_handling = 0 # don't receive any NetworkItems + items_handling = 0b111 # receive all items for /received async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: diff --git a/FactorioClient.py b/FactorioClient.py index b7bf324311..6797578a3a 100644 --- a/FactorioClient.py +++ b/FactorioClient.py @@ -20,8 +20,7 @@ import Utils if __name__ == "__main__": Utils.init_logging("FactorioClient", exception_logger="Client") -from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger, gui_enabled, \ - get_base_parser +from CommonClient import CommonContext, server_loop, ClientCommandProcessor, logger, gui_enabled, get_base_parser from MultiServer import mark_raw from NetUtils import NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePart @@ -400,6 +399,7 @@ if __name__ == '__main__': "Refer to Factorio --help for those.") parser.add_argument('--rcon-port', default='24242', type=int, help='Port to use to communicate with Factorio') parser.add_argument('--rcon-password', help='Password to authenticate with RCON.') + parser.add_argument('--server-settings', help='Factorio server settings configuration file.') args, rest = parser.parse_known_args() colorama.init() @@ -410,6 +410,9 @@ if __name__ == '__main__': factorio_server_logger = logging.getLogger("FactorioServer") options = Utils.get_options() executable = options["factorio_options"]["executable"] + server_settings = args.server_settings if args.server_settings else options["factorio_options"].get("server_settings", None) + if server_settings: + server_settings = os.path.abspath(server_settings) if not os.path.exists(os.path.dirname(executable)): raise FileNotFoundError(f"Path {os.path.dirname(executable)} does not exist or could not be accessed.") @@ -421,7 +424,10 @@ if __name__ == '__main__': else: raise FileNotFoundError(f"Path {executable} is not an executable file.") - server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, *rest) + if server_settings and os.path.isfile(server_settings): + server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, "--server-settings", server_settings, *rest) + else: + server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, *rest) asyncio.run(main(args)) colorama.deinit() diff --git a/Fill.py b/Fill.py index a5cf155ec1..e44c80e720 100644 --- a/Fill.py +++ b/Fill.py @@ -220,8 +220,8 @@ def distribute_items_restrictive(world: MultiWorld) -> None: world.push_item(defaultlocations.pop(i), item_to_place, False) break else: - logging.warning( - f"Could not place non_local_item {item_to_place} among {defaultlocations}, tossing.") + raise Exception(f"Could not place non_local_item {item_to_place} among {defaultlocations}. " + f"Too many non-local items for too few remaining locations.") world.random.shuffle(defaultlocations) diff --git a/Generate.py b/Generate.py index b46c730c9a..1cad836345 100644 --- a/Generate.py +++ b/Generate.py @@ -7,7 +7,7 @@ import urllib.request import urllib.parse from typing import Set, Dict, Tuple, Callable, Any, Union import os -from collections import Counter +from collections import Counter, ChainMap import string import enum @@ -61,6 +61,11 @@ class PlandoSettings(enum.IntFlag): else: return base | part + def __str__(self) -> str: + if self.value: + return ", ".join((flag.name for flag in PlandoSettings if self.value & flag.value)) + return "Off" + def mystery_argparse(): options = get_options() @@ -128,12 +133,14 @@ def main(args=None, callback=ERmain): if args.meta_file_path and os.path.exists(args.meta_file_path): try: - weights_cache[args.meta_file_path] = read_weights_yamls(args.meta_file_path) + meta_weights = read_weights_yamls(args.meta_file_path)[-1] except Exception as e: raise ValueError(f"File {args.meta_file_path} is destroyed. Please fix your yaml.") from e - meta_weights = weights_cache[args.meta_file_path][-1] print(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights)}") - del(meta_weights["meta_description"]) + try: # meta description allows us to verify that the file named meta.yaml is intentionally a meta file + del(meta_weights["meta_description"]) + except Exception as e: + raise ValueError("No meta description found for meta.yaml. Unable to verify.") from e if args.samesettings: raise Exception("Cannot mix --samesettings with --meta") else: @@ -159,7 +166,7 @@ def main(args=None, callback=ERmain): player_files[player_id] = filename player_id += 1 - args.multi = max(player_id-1, args.multi) + args.multi = max(player_id - 1, args.multi) print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed} with plando: " f"{args.plando}") @@ -181,26 +188,28 @@ def main(args=None, callback=ERmain): erargs.enemizercli = args.enemizercli settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \ - {fname: ( tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None) - for fname, yamls in weights_cache.items()} - player_path_cache = {} - for player in range(1, args.multi + 1): - player_path_cache[player] = player_files.get(player, args.weights_file_path) + {fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None) + for fname, yamls in weights_cache.items()} if meta_weights: for category_name, category_dict in meta_weights.items(): for key in category_dict: - option = get_choice(key, category_dict) + option = roll_meta_option(key, category_name, category_dict) if option is not None: - for player, path in player_path_cache.items(): + for path in weights_cache: for yaml in weights_cache[path]: if category_name is None: - yaml[key] = option + for category in yaml: + if category in AutoWorldRegister.world_types and key in Options.common_options: + yaml[category][key] = option elif category_name not in yaml: logging.warning(f"Meta: Category {category_name} is not present in {path}.") else: - yaml[category_name][key] = option + yaml[category_name][key] = option + player_path_cache = {} + for player in range(1, args.multi + 1): + player_path_cache[player] = player_files.get(player, args.weights_file_path) name_counter = Counter() erargs.player_settings = {} @@ -382,6 +391,28 @@ def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> di return weights +def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any: + if not game: + return get_choice(option_key, category_dict) + if game in AutoWorldRegister.world_types: + game_world = AutoWorldRegister.world_types[game] + options = ChainMap(game_world.option_definitions, Options.per_game_common_options) + if option_key in options: + if options[option_key].supports_weighting: + return get_choice(option_key, category_dict) + return options[option_key] + if game == "A Link to the Past": # TODO wow i hate this + if option_key in {"glitches_required", "dark_room_logic", "entrance_shuffle", "goals", "triforce_pieces_mode", + "triforce_pieces_percentage", "triforce_pieces_available", "triforce_pieces_extra", + "triforce_pieces_required", "shop_shuffle", "mode", "item_pool", "item_functionality", + "boss_shuffle", "enemy_damage", "enemy_health", "timer", "countdown_start_time", + "red_clock_time", "blue_clock_time", "green_clock_time", "dungeon_counters", "shuffle_prizes", + "misery_mire_medallion", "turtle_rock_medallion", "sprite_pool", "sprite", + "random_sprite_on_event"}: + return get_choice(option_key, category_dict) + raise Exception(f"Error generating meta option {option_key} for {game}.") + + def roll_linked_options(weights: dict) -> dict: weights = copy.deepcopy(weights) # make sure we don't write back to other weights sets in same_settings for option_set in weights["linked_options"]: @@ -526,7 +557,7 @@ def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings setattr(ret, option_key, option.from_any(get_choice(option_key, weights, option.default))) if ret.game in AutoWorldRegister.world_types: - for option_key, option in world_type.options.items(): + for option_key, option in world_type.option_definitions.items(): handle_option(ret, game_weights, option_key, option) for option_key, option in Options.per_game_common_options.items(): # skip setting this option if already set from common_options, defaulting to root option @@ -583,9 +614,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): ret.goal = goals[goal] - # TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when - # fast ganon + ganon at hole - ret.open_pyramid = get_choice_legacy('open_pyramid', weights, 'goal') extra_pieces = get_choice_legacy('triforce_pieces_mode', weights, 'available') diff --git a/Launcher.py b/Launcher.py index 809a8937d7..53032ea251 100644 --- a/Launcher.py +++ b/Launcher.py @@ -126,7 +126,7 @@ components: Iterable[Component] = ( Component('Text Client', 'CommonClient', 'ArchipelagoTextClient'), # SNI Component('SNI Client', 'SNIClient', - file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3')), + file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3')), Component('LttP Adjuster', 'LttPAdjuster'), # Factorio Component('Factorio Client', 'FactorioClient'), diff --git a/LttPAdjuster.py b/LttPAdjuster.py index 3368620e6c..3de6e3b13a 100644 --- a/LttPAdjuster.py +++ b/LttPAdjuster.py @@ -289,7 +289,7 @@ def run_sprite_update(): else: top.withdraw() task = BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set()) - while not done.isSet(): + while not done.is_set(): task.do_events() logging.info("Done updating sprites") @@ -300,6 +300,7 @@ def update_sprites(task, on_finish=None): sprite_dir = user_path("data", "sprites", "alttpr") os.makedirs(sprite_dir, exist_ok=True) ctx = get_cert_none_ssl_context() + def finished(): task.close_window() if on_finish: diff --git a/Main.py b/Main.py index acbb4ad5cf..48095e06bd 100644 --- a/Main.py +++ b/Main.py @@ -47,7 +47,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No world.item_functionality = args.item_functionality.copy() world.timer = args.timer.copy() world.goal = args.goal.copy() - world.open_pyramid = args.open_pyramid.copy() world.boss_shuffle = args.shufflebosses.copy() world.enemy_health = args.enemy_health.copy() world.enemy_damage = args.enemy_damage.copy() @@ -218,9 +217,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No logger.info("Running Item Plando") - for item in world.itempool: - item.world = world - distribute_planned(world) logger.info('Running Pre Main Fill.') @@ -364,7 +360,8 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No for location in world.get_filled_locations(): if type(location.address) == int: assert location.item.code is not None, "item code None should be event, " \ - "location.address should then also be None" + "location.address should then also be None. Location: " \ + f" {location}" locations_data[location.player][location.address] = \ location.item.code, location.item.player, location.item.flags if location.name in world.start_location_hints[location.player]: @@ -426,7 +423,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase)) zipfilename = output_path(f"AP_{world.seed_name}.zip") - logger.info(f'Creating final archive at {zipfilename}.') + logger.info(f"Creating final archive at {zipfilename}") with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf: for file in os.scandir(temp_dir): diff --git a/MultiServer.py b/MultiServer.py index 06f9a9f9cd..8a1844bf92 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -30,13 +30,8 @@ except ImportError: OperationalError = ConnectionError import NetUtils -from worlds.AutoWorld import AutoWorldRegister - -proxy_worlds = {name: world(None, 0) for name, world in AutoWorldRegister.world_types.items()} -from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_location_id_to_name import Utils -from Utils import get_item_name_from_id, get_location_name_from_id, \ - version_tuple, restricted_loads, Version +from Utils import version_tuple, restricted_loads, Version from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \ SlotType @@ -126,6 +121,11 @@ class Context: stored_data: typing.Dict[str, object] stored_data_notification_clients: typing.Dict[str, typing.Set[Client]] + item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})') + location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})') + all_item_and_group_names: typing.Dict[str, typing.Set[str]] + forced_auto_forfeits: typing.Dict[str, bool] + def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int, hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", collect_mode="disabled", remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2, @@ -190,8 +190,43 @@ class Context: self.stored_data = {} self.stored_data_notification_clients = collections.defaultdict(weakref.WeakSet) - # General networking + # init empty to satisfy linter, I suppose + self.gamespackage = {} + self.item_name_groups = {} + self.all_item_and_group_names = {} + self.forced_auto_forfeits = collections.defaultdict(lambda: False) + self.non_hintable_names = {} + self._load_game_data() + self._init_game_data() + + # Datapackage retrieval + def _load_game_data(self): + import worlds + self.gamespackage = worlds.network_data_package["games"] + + self.item_name_groups = {world_name: world.item_name_groups for world_name, world in + worlds.AutoWorldRegister.world_types.items()} + for world_name, world in worlds.AutoWorldRegister.world_types.items(): + self.forced_auto_forfeits[world_name] = world.forced_auto_forfeit + self.non_hintable_names[world_name] = world.hint_blacklist + + def _init_game_data(self): + for game_name, game_package in self.gamespackage.items(): + for item_name, item_id in game_package["item_name_to_id"].items(): + self.item_names[item_id] = item_name + for location_name, location_id in game_package["location_name_to_id"].items(): + self.location_names[location_id] = location_name + self.all_item_and_group_names[game_name] = \ + set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name]) + + def item_names_for_game(self, game: str) -> typing.Dict[str, int]: + return self.gamespackage[game]["item_name_to_id"] + + def location_names_for_game(self, game: str) -> typing.Dict[str, int]: + return self.gamespackage[game]["location_name_to_id"] + + # General networking async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool: if not endpoint.socket or not endpoint.socket.open: return False @@ -544,12 +579,12 @@ class Context: finished_msg = f'{self.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1})' \ f' has completed their goal.' self.notify_all(finished_msg) - if "auto" in self.forfeit_mode: - forfeit_player(self, client.team, client.slot) - elif proxy_worlds[self.games[client.slot]].forced_auto_forfeit: - forfeit_player(self, client.team, client.slot) if "auto" in self.collect_mode: collect_player(self, client.team, client.slot) + if "auto" in self.forfeit_mode: + forfeit_player(self, client.team, client.slot) + elif self.forced_auto_forfeits[self.games[client.slot]]: + forfeit_player(self, client.team, client.slot) def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False): @@ -642,9 +677,10 @@ async def on_client_connected(ctx: Context, client: Client): 'permissions': get_permissions(ctx), 'hint_cost': ctx.hint_cost, 'location_check_points': ctx.location_check_points, - 'datapackage_version': network_data_package["version"], + 'datapackage_version': sum(game_data["version"] for game_data in ctx.gamespackage.values()) + if all(game_data["version"] for game_data in ctx.gamespackage.values()) else 0, 'datapackage_versions': {game: game_data["version"] for game, game_data - in network_data_package["games"].items()}, + in ctx.gamespackage.items()}, 'seed_name': ctx.seed_name, 'time': time.time(), }]) @@ -720,16 +756,16 @@ def get_players_string(ctx: Context): return f'{len(auth_clients)} players of {total} connected ' + text[:-1] -def get_status_string(ctx: Context, team: int): - text = "Player Status on your team:" +def get_status_string(ctx: Context, team: int, tag: str): + text = f"Player Status on team {team}:" for slot in ctx.locations: connected = len(ctx.clients[team][slot]) - death_link = len([client for client in ctx.clients[team][slot] if "DeathLink" in client.tags]) + tagged = len([client for client in ctx.clients[team][slot] if tag in client.tags]) completion_text = f"({len(ctx.location_checks[team, slot])}/{len(ctx.locations[slot])})" - death_text = f" {death_link} of which are death link" if connected else "" + tag_text = f" {tagged} of which are tagged {tag}" if connected and tag else "" goal_text = " and has finished." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_GOAL else "." text += f"\n{ctx.get_aliased_name(team, slot)} has {connected} connection{'' if connected == 1 else 's'}" \ - f"{death_text}{goal_text} {completion_text}" + f"{tag_text}{goal_text} {completion_text}" return text @@ -822,8 +858,8 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi send_items_to(ctx, team, target_player, new_item) logging.info('(Team #%d) %s sent %s to %s (%s)' % ( - team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(item_id), - ctx.player_names[(team, target_player)], get_location_name_from_id(location))) + team + 1, ctx.player_names[(team, slot)], ctx.item_names[item_id], + ctx.player_names[(team, target_player)], ctx.location_names[location])) info_text = json_format_send_event(new_item, target_player) ctx.broadcast_team(team, [info_text]) @@ -838,13 +874,14 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi ctx.save() -def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[NetUtils.Hint]: +def collect_hints(ctx: Context, team: int, slot: int, item_name: str) -> typing.List[NetUtils.Hint]: hints = [] slots: typing.Set[int] = {slot} for group_id, group in ctx.groups.items(): if slot in group: slots.add(group_id) - seeked_item_id = proxy_worlds[ctx.games[slot]].item_name_to_id[item] + + seeked_item_id = ctx.item_names_for_game(ctx.games[slot])[item_name] for finding_player, check_data in ctx.locations.items(): for location_id, (item_id, receiving_player, item_flags) in check_data.items(): if receiving_player in slots and item_id == seeked_item_id: @@ -857,7 +894,7 @@ def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[ def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]: - seeked_location: int = proxy_worlds[ctx.games[slot]].location_name_to_id[location] + seeked_location: int = ctx.location_names_for_game(ctx.games[slot])[location] return collect_hint_location_id(ctx, team, slot, seeked_location) @@ -874,8 +911,8 @@ def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str: text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \ - f"{lookup_any_item_id_to_name[hint.item]} is " \ - f"at {get_location_name_from_id(hint.location)} " \ + f"{ctx.item_names[hint.item]} is " \ + f"at {ctx.location_names[hint.location]} " \ f"in {ctx.player_names[team, hint.finding_player]}'s World" if hint.entrance: @@ -1113,9 +1150,11 @@ class ClientMessageProcessor(CommonCommandProcessor): self.output(get_players_string(self.ctx)) return True - def _cmd_status(self) -> bool: - """Get status information about your team.""" - self.output(get_status_string(self.ctx, self.client.team)) + def _cmd_status(self, tag:str="") -> bool: + """Get status information about your team. + Optionally mention a Tag name and get information on who has that Tag. + For example: DeathLink or EnergyLink.""" + self.output(get_status_string(self.ctx, self.client.team, tag)) return True def _cmd_release(self) -> bool: @@ -1131,8 +1170,8 @@ class ClientMessageProcessor(CommonCommandProcessor): forfeit_player(self.ctx, self.client.team, self.client.slot) return True elif "disabled" in self.ctx.forfeit_mode: - self.output( - "Sorry, client item releasing has been disabled on this server. You can ask the server admin for a /release") + self.output("Sorry, client item releasing has been disabled on this server. " + "You can ask the server admin for a /release") return False else: # is auto or goal if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL: @@ -1168,7 +1207,7 @@ class ClientMessageProcessor(CommonCommandProcessor): if self.ctx.remaining_mode == "enabled": remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) if remaining_item_ids: - self.output("Remaining items: " + ", ".join(lookup_any_item_id_to_name.get(item_id, "unknown item") + self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id] for item_id in remaining_item_ids)) else: self.output("No remaining items found.") @@ -1181,7 +1220,7 @@ class ClientMessageProcessor(CommonCommandProcessor): if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL: remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) if remaining_item_ids: - self.output("Remaining items: " + ", ".join(lookup_any_item_id_to_name.get(item_id, "unknown item") + self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id] for item_id in remaining_item_ids)) else: self.output("No remaining items found.") @@ -1197,7 +1236,7 @@ class ClientMessageProcessor(CommonCommandProcessor): locations = get_missing_checks(self.ctx, self.client.team, self.client.slot) if locations: - texts = [f'Missing: {get_location_name_from_id(location)}' for location in locations] + texts = [f'Missing: {self.ctx.location_names[location]}' for location in locations] texts.append(f"Found {len(locations)} missing location checks") self.ctx.notify_client_multiple(self.client, texts) else: @@ -1210,7 +1249,7 @@ class ClientMessageProcessor(CommonCommandProcessor): locations = get_checked_checks(self.ctx, self.client.team, self.client.slot) if locations: - texts = [f'Checked: {get_location_name_from_id(location)}' for location in locations] + texts = [f'Checked: {self.ctx.location_names[location]}' for location in locations] texts.append(f"Found {len(locations)} done location checks") self.ctx.notify_client_multiple(self.client, texts) else: @@ -1239,11 +1278,13 @@ class ClientMessageProcessor(CommonCommandProcessor): def _cmd_getitem(self, item_name: str) -> bool: """Cheat in an item, if it is enabled on this server""" if self.ctx.item_cheat: - world = proxy_worlds[self.ctx.games[self.client.slot]] - item_name, usable, response = get_intended_text(item_name, - world.item_names) + names = self.ctx.item_names_for_game(self.ctx.games[self.client.slot]) + item_name, usable, response = get_intended_text( + item_name, + names + ) if usable: - new_item = NetworkItem(world.create_item(item_name).code, -1, self.client.slot) + new_item = NetworkItem(names[item_name], -1, self.client.slot) get_received_items(self.ctx, self.client.team, self.client.slot, False).append(new_item) get_received_items(self.ctx, self.client.team, self.client.slot, True).append(new_item) self.ctx.notify_all( @@ -1269,20 +1310,22 @@ class ClientMessageProcessor(CommonCommandProcessor): f"You have {points_available} points.") return True else: - world = proxy_worlds[self.ctx.games[self.client.slot]] - names = world.location_names if for_location else world.all_item_and_group_names + game = self.ctx.games[self.client.slot] + names = self.ctx.location_names_for_game(game) \ + if for_location else \ + self.ctx.all_item_and_group_names[game] hint_name, usable, response = get_intended_text(input_text, names) if usable: - if hint_name in world.hint_blacklist: + if hint_name in self.ctx.non_hintable_names[game]: self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.") hints = [] - elif not for_location and hint_name in world.item_name_groups: # item group name + elif not for_location and hint_name in self.ctx.item_name_groups[game]: # item group name hints = [] - for item in world.item_name_groups[hint_name]: - if item in world.item_name_to_id: # ensure item has an ID - hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item)) - elif not for_location and hint_name in world.item_names: # item name + for item_name in self.ctx.item_name_groups[game][hint_name]: + if item_name in self.ctx.item_names_for_game(game): # ensure item has an ID + hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name)) + elif not for_location and hint_name in self.ctx.item_names_for_game(game): # item name hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name) else: # location name hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name) @@ -1306,6 +1349,8 @@ class ClientMessageProcessor(CommonCommandProcessor): can_pay = 1000 self.ctx.random.shuffle(not_found_hints) + # By popular vote, make hints prefer non-local placements + not_found_hints.sort(key=lambda hint: int(hint.receiving_player != hint.finding_player)) hints = found_hints while can_pay > 0: @@ -1342,12 +1387,12 @@ class ClientMessageProcessor(CommonCommandProcessor): return False @mark_raw - def _cmd_hint(self, item: str = "") -> bool: + def _cmd_hint(self, item_name: str = "") -> bool: """Use !hint {item_name}, for example !hint Lamp to get a spoiler peek for that item. If hint costs are on, this will only give you one new result, you can rerun the command to get more in that case.""" - return self.get_hints(item) + return self.get_hints(item_name) @mark_raw def _cmd_hint_location(self, location: str = "") -> bool: @@ -1473,23 +1518,23 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): elif cmd == "GetDataPackage": exclusions = args.get("exclusions", []) if "games" in args: - games = {name: game_data for name, game_data in network_data_package["games"].items() + games = {name: game_data for name, game_data in ctx.gamespackage.items() if name in set(args.get("games", []))} await ctx.send_msgs(client, [{"cmd": "DataPackage", "data": {"games": games}}]) # TODO: remove exclusions behaviour around 0.5.0 elif exclusions: exclusions = set(exclusions) - games = {name: game_data for name, game_data in network_data_package["games"].items() + games = {name: game_data for name, game_data in ctx.gamespackage.items() if name not in exclusions} - package = network_data_package.copy() - package["games"] = games + + package = {"games": games} await ctx.send_msgs(client, [{"cmd": "DataPackage", "data": package}]) else: await ctx.send_msgs(client, [{"cmd": "DataPackage", - "data": network_data_package}]) + "data": {"games": ctx.gamespackage}}]) elif client.auth: if cmd == "ConnectUpdate": @@ -1545,7 +1590,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): create_as_hint: int = int(args.get("create_as_hint", 0)) hints = [] for location in args["locations"]: - if type(location) is not int or location not in lookup_any_location_id_to_name: + if type(location) is not int: await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts', "original_cmd": cmd}]) @@ -1657,6 +1702,14 @@ class ServerCommandProcessor(CommonCommandProcessor): self.output(get_players_string(self.ctx)) return True + def _cmd_status(self, tag: str = "") -> bool: + """Get status information about teams. + Optionally mention a Tag name and get information on who has that Tag. + For example: DeathLink or EnergyLink.""" + for team in self.ctx.clients: + self.output(get_status_string(self.ctx, team, tag)) + return True + def _cmd_exit(self) -> bool: """Shutdown the server""" asyncio.create_task(self.ctx.server.ws_server._close()) @@ -1751,18 +1804,18 @@ class ServerCommandProcessor(CommonCommandProcessor): seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values()) if usable: team, slot = self.ctx.player_name_lookup[seeked_player] - item = " ".join(item_name) - world = proxy_worlds[self.ctx.games[slot]] - item, usable, response = get_intended_text(item, world.item_names) + item_name = " ".join(item_name) + names = self.ctx.item_names_for_game(self.ctx.games[slot]) + item_name, usable, response = get_intended_text(item_name, names) if usable: amount: int = int(amount) - new_items = [NetworkItem(world.item_name_to_id[item], -1, 0) for i in range(int(amount))] + new_items = [NetworkItem(names[item_name], -1, 0) for _ in range(int(amount))] send_items_to(self.ctx, team, slot, *new_items) send_new_items(self.ctx) self.ctx.notify_all( 'Cheat console: sending ' + ('' if amount == 1 else f'{amount} of ') + - f'"{item}" to {self.ctx.get_aliased_name(team, slot)}') + f'"{item_name}" to {self.ctx.get_aliased_name(team, slot)}') return True else: self.output(response) @@ -1775,22 +1828,22 @@ class ServerCommandProcessor(CommonCommandProcessor): """Sends an item to the specified player""" return self._cmd_send_multiple(1, player_name, *item_name) - def _cmd_hint(self, player_name: str, *item: str) -> bool: + def _cmd_hint(self, player_name: str, *item_name: str) -> bool: """Send out a hint for a player's item to their team""" seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values()) if usable: team, slot = self.ctx.player_name_lookup[seeked_player] - item = " ".join(item) - world = proxy_worlds[self.ctx.games[slot]] - item, usable, response = get_intended_text(item, world.all_item_and_group_names) + item_name = " ".join(item_name) + game = self.ctx.games[slot] + item_name, usable, response = get_intended_text(item_name, self.ctx.all_item_and_group_names[game]) if usable: - if item in world.item_name_groups: + if item_name in self.ctx.item_name_groups[game]: hints = [] - for item in world.item_name_groups[item]: - if item in world.item_name_to_id: # ensure item has an ID - hints.extend(collect_hints(self.ctx, team, slot, item)) + for item_name_from_group in self.ctx.item_name_groups[game][item_name]: + if item_name_from_group in self.ctx.item_names_for_game(game): # ensure item has an ID + hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group)) else: # item name - hints = collect_hints(self.ctx, team, slot, item) + hints = collect_hints(self.ctx, team, slot, item_name) if hints: notify_hints(self.ctx, team, hints) @@ -1806,16 +1859,16 @@ class ServerCommandProcessor(CommonCommandProcessor): self.output(response) return False - def _cmd_hint_location(self, player_name: str, *location: str) -> bool: + def _cmd_hint_location(self, player_name: str, *location_name: str) -> bool: """Send out a hint for a player's location to their team""" seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values()) if usable: team, slot = self.ctx.player_name_lookup[seeked_player] - item = " ".join(location) - world = proxy_worlds[self.ctx.games[slot]] - item, usable, response = get_intended_text(item, world.location_names) + location_name = " ".join(location_name) + location_name, usable, response = get_intended_text(location_name, + self.ctx.location_names_for_game(self.ctx.games[slot])) if usable: - hints = collect_hint_location_name(self.ctx, team, slot, item) + hints = collect_hint_location_name(self.ctx, team, slot, location_name) if hints: notify_hints(self.ctx, team, hints) else: diff --git a/NetUtils.py b/NetUtils.py index 6cf3ab6d41..1e7d66d824 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -270,7 +270,7 @@ class RawJSONtoTextParser(JSONtoTextParser): color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43, - 'blue_bg': 44, 'purple_bg': 45, 'cyan_bg': 46, 'white_bg': 47} + 'blue_bg': 44, 'magenta_bg': 45, 'cyan_bg': 46, 'white_bg': 47} def color_code(*args): diff --git a/OoTClient.py b/OoTClient.py index e455efdcd3..fbe2b35d1a 100644 --- a/OoTClient.py +++ b/OoTClient.py @@ -48,7 +48,7 @@ deathlink_sent_this_death: we interacted with the multiworld on this death, wait oot_loc_name_to_id = network_data_package["games"]["Ocarina of Time"]["location_name_to_id"] -script_version: int = 1 +script_version: int = 2 def get_item_value(ap_id): return ap_id - 66000 @@ -186,7 +186,7 @@ async def n64_sync_task(ctx: OoTContext): data = await asyncio.wait_for(reader.readline(), timeout=10) data_decoded = json.loads(data.decode()) reported_version = data_decoded.get('scriptVersion', 0) - if reported_version == script_version: + if reported_version >= script_version: if ctx.game is not None and 'locations' in data_decoded: # Not just a keep alive ping, parse asyncio.create_task(parse_payload(data_decoded, ctx, False)) diff --git a/Patch.py b/Patch.py index a2f29fdabc..f90e376656 100644 --- a/Patch.py +++ b/Patch.py @@ -166,13 +166,15 @@ GAME_ALTTP = "A Link to the Past" GAME_SM = "Super Metroid" GAME_SOE = "Secret of Evermore" GAME_SMZ3 = "SMZ3" -supported_games = {"A Link to the Past", "Super Metroid", "Secret of Evermore", "SMZ3"} +GAME_DKC3 = "Donkey Kong Country 3" +supported_games = {"A Link to the Past", "Super Metroid", "Secret of Evermore", "SMZ3", "Donkey Kong Country 3"} preferred_endings = { GAME_ALTTP: "apbp", GAME_SM: "apm3", GAME_SOE: "apsoe", - GAME_SMZ3: "apsmz" + GAME_SMZ3: "apsmz", + GAME_DKC3: "apdkc3" } @@ -187,6 +189,8 @@ def generate_yaml(patch: bytes, metadata: Optional[dict] = None, game: str = GAM from worlds.alttp.Rom import LTTPJPN10HASH as ALTTPHASH from worlds.sm.Rom import SMJUHASH as SMHASH HASH = ALTTPHASH + SMHASH + elif game == GAME_DKC3: + from worlds.dkc3.Rom import USHASH as HASH else: raise RuntimeError(f"Selected game {game} for base rom not found.") @@ -216,7 +220,10 @@ def create_patch_file(rom_file_to_patch: str, server: str = "", destination: str meta, game) target = destination if destination else os.path.splitext(rom_file_to_patch)[0] + ( - ".apbp" if game == GAME_ALTTP else ".apsmz" if game == GAME_SMZ3 else ".apm3") + ".apbp" if game == GAME_ALTTP + else ".apsmz" if game == GAME_SMZ3 + else ".apdkc3" if game == GAME_DKC3 + else ".apm3") write_lzma(bytes, target) return target @@ -245,6 +252,8 @@ def get_base_rom_data(game: str): get_base_rom_bytes = lambda: bytes(read_rom(open(get_base_rom_path(), "rb"))) elif game == GAME_SMZ3: from worlds.smz3.Rom import get_base_rom_bytes + elif game == GAME_DKC3: + from worlds.dkc3.Rom import get_base_rom_bytes else: raise RuntimeError("Selected game for base rom not found.") return get_base_rom_bytes() @@ -389,6 +398,13 @@ if __name__ == "__main__": if 'server' in data: Utils.persistent_store("servers", data['hash'], data['server']) print(f"Host is {data['server']}") + elif rom.endswith(".apdkc3"): + print(f"Applying patch {rom}") + data, target = create_rom_file(rom) + print(f"Created rom {target}.") + if 'server' in data: + Utils.persistent_store("servers", data['hash'], data['server']) + print(f"Host is {data['server']}") elif rom.endswith(".zip"): print(f"Updating host in patch files contained in {rom}") @@ -396,7 +412,9 @@ if __name__ == "__main__": def _handle_zip_file_entry(zfinfo: zipfile.ZipInfo, server: str): data = zfr.read(zfinfo) - if zfinfo.filename.endswith(".apbp") or zfinfo.filename.endswith(".apm3"): + if zfinfo.filename.endswith(".apbp") or \ + zfinfo.filename.endswith(".apm3") or \ + zfinfo.filename.endswith(".apdkc3"): data = update_patch_data(data, server) with ziplock: zfw.writestr(zfinfo, data) diff --git a/README.md b/README.md index 2b9cde4093..9403159c74 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ Currently, the following games are supported: * The Witness * Sonic Adventure 2: Battle * Starcraft 2: Wings of Liberty +* Donkey Kong Country 3 +* Dark Souls 3 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 @@ -49,7 +51,7 @@ Archipelago was directly forked from bonta0's `multiworld_31` branch of ALttPEnt ## Running Archipelago For most people all you need to do is head over to the [releases](https://github.com/ArchipelagoMW/Archipelago/releases) page then download and run the appropriate installer. The installers function on Windows only. -If you are running Archipelago from a non-Windows system then the likely scenario is that you are comfortable running source code directly. Please see our wiki page on [running Archipelago from source](https://github.com/ArchipelagoMW/Archipelago/wiki/Running-from-source). +If you are running Archipelago from a non-Windows system then the likely scenario is that you are comfortable running source code directly. Please see our doc on [running Archipelago from source](docs/running%20from%20source.md). ## Related Repositories This project makes use of multiple other projects. We wouldn't be here without these other repositories and the contributions of their developers, past and present. @@ -66,7 +68,7 @@ Contributions are welcome. We have a few asks of any new contributors. Otherwise, we tend to judge code on a case to case basis. It is a generally good idea to stick to PEP-8 guidelines to ensure consistency with existing code. (And to make the linter happy.) -For adding a new game to Archipelago and other documentation on how Archipelago functions, please see the docs folder for the relevant information and feel free to ask any questions in the #archipelago-dev channel in our discord. +For adding a new game to Archipelago and other documentation on how Archipelago functions, please see [the docs folder](docs/) for the relevant information and feel free to ask any questions in the #archipelago-dev channel in our discord. ## FAQ For frequently asked questions see the website's [FAQ Page](https://archipelago.gg/faq/en/) diff --git a/SNIClient.py b/SNIClient.py index e313feff00..aad231691b 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -33,7 +33,7 @@ from worlds.sm.Rom import ROM_PLAYER_LIMIT as SM_ROM_PLAYER_LIMIT from worlds.smz3.Rom import ROM_PLAYER_LIMIT as SMZ3_ROM_PLAYER_LIMIT import Utils from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser -from Patch import GAME_ALTTP, GAME_SM, GAME_SMZ3 +from Patch import GAME_ALTTP, GAME_SM, GAME_SMZ3, GAME_DKC3 snes_logger = logging.getLogger("SNES") @@ -62,7 +62,7 @@ class SNIClientCommandProcessor(ClientCommandProcessor): def _cmd_snes(self, snes_options: str = "") -> bool: """Connect to a snes. Optionally include network address of a snes to connect to, otherwise show available devices; and a SNES device number if more than one SNES is detected. - Examples: "/snes", "/snes 1", "/snes localhost:8080 1" """ + Examples: "/snes", "/snes 1", "/snes localhost:23074 1" """ snes_address = self.ctx.snes_address snes_device_number = -1 @@ -188,7 +188,10 @@ class Context(CommonContext): async def shutdown(self): await super(Context, self).shutdown() if self.snes_connect_task: - await self.snes_connect_task + try: + await asyncio.wait_for(self.snes_connect_task, 1) + except asyncio.TimeoutError: + self.snes_connect_task.cancel() def on_package(self, cmd: str, args: dict): if cmd in {"Connected", "RoomUpdate"}: @@ -251,6 +254,9 @@ async def deathlink_kill_player(ctx: Context): if not gamemode or gamemode[0] in SM_DEATH_MODES or ( ctx.death_link_allow_survive and health is not None and health > 0): ctx.death_state = DeathState.dead + elif ctx.game == GAME_DKC3: + from worlds.dkc3.Client import deathlink_kill_player as dkc3_deathlink_kill_player + await dkc3_deathlink_kill_player(ctx) ctx.last_death_link = time.time() @@ -595,7 +601,7 @@ class SNESState(enum.IntEnum): SNES_ATTACHED = 3 -def launch_sni(ctx: Context): +def launch_sni(): sni_path = Utils.get_options()["lttp_options"]["sni"] if not os.path.isdir(sni_path): @@ -633,11 +639,9 @@ async def _snes_connect(ctx: Context, address: str): address = f"ws://{address}" if "://" not in address else address snes_logger.info("Connecting to SNI at %s ..." % address) seen_problems = set() - succesful = False - while not succesful: + while 1: try: snes_socket = await websockets.connect(address, ping_timeout=None, ping_interval=None) - succesful = True except Exception as e: problem = "%s" % e # only tell the user about new problems, otherwise silently lay in wait for a working connection @@ -647,7 +651,7 @@ async def _snes_connect(ctx: Context, address: str): if len(seen_problems) == 1: # this is the first problem. Let's try launching SNI if it isn't already running - launch_sni(ctx) + launch_sni() await asyncio.sleep(1) else: @@ -1034,44 +1038,48 @@ async def game_watcher(ctx: Context): if not ctx.rom: ctx.finished_game = False ctx.death_link_allow_survive = False - game_name = await snes_read(ctx, SM_ROMNAME_START, 5) - if game_name is None: - continue - elif game_name[:2] == b"SM": - ctx.game = GAME_SM - # versions lower than 0.3.0 dont have item handling flag nor remote item support - romVersion = int(game_name[2:5].decode('UTF-8')) - if romVersion < 30: - ctx.items_handling = 0b001 # full local - else: - item_handling = await snes_read(ctx, SM_REMOTE_ITEM_FLAG_ADDR, 1) - ctx.items_handling = 0b001 if item_handling is None else item_handling[0] - else: - game_name = await snes_read(ctx, SMZ3_ROMNAME_START, 3) - if game_name == b"ZSM": - ctx.game = GAME_SMZ3 - ctx.items_handling = 0b101 # local items and remote start inventory - else: - ctx.game = GAME_ALTTP - ctx.items_handling = 0b001 # full local - rom = await snes_read(ctx, SM_ROMNAME_START if ctx.game == GAME_SM else SMZ3_ROMNAME_START if ctx.game == GAME_SMZ3 else ROMNAME_START, ROMNAME_SIZE) - if rom is None or rom == bytes([0] * ROMNAME_SIZE): - continue + from worlds.dkc3.Client import dkc3_rom_init + init_handled = await dkc3_rom_init(ctx) + if not init_handled: + game_name = await snes_read(ctx, SM_ROMNAME_START, 5) + if game_name is None: + continue + elif game_name[:2] == b"SM": + ctx.game = GAME_SM + # versions lower than 0.3.0 dont have item handling flag nor remote item support + romVersion = int(game_name[2:5].decode('UTF-8')) + if romVersion < 30: + ctx.items_handling = 0b001 # full local + else: + item_handling = await snes_read(ctx, SM_REMOTE_ITEM_FLAG_ADDR, 1) + ctx.items_handling = 0b001 if item_handling is None else item_handling[0] + else: + game_name = await snes_read(ctx, SMZ3_ROMNAME_START, 3) + if game_name == b"ZSM": + ctx.game = GAME_SMZ3 + ctx.items_handling = 0b101 # local items and remote start inventory + else: + ctx.game = GAME_ALTTP + ctx.items_handling = 0b001 # full local - ctx.rom = rom - if ctx.game != GAME_SMZ3: - death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR if ctx.game == GAME_ALTTP else - SM_DEATH_LINK_ACTIVE_ADDR, 1) - if death_link: - ctx.allow_collect = bool(death_link[0] & 0b100) - ctx.death_link_allow_survive = bool(death_link[0] & 0b10) - await ctx.update_death_link(bool(death_link[0] & 0b1)) - if not ctx.prev_rom or ctx.prev_rom != ctx.rom: - ctx.locations_checked = set() - ctx.locations_scouted = set() - ctx.locations_info = {} - ctx.prev_rom = ctx.rom + rom = await snes_read(ctx, SM_ROMNAME_START if ctx.game == GAME_SM else SMZ3_ROMNAME_START if ctx.game == GAME_SMZ3 else ROMNAME_START, ROMNAME_SIZE) + if rom is None or rom == bytes([0] * ROMNAME_SIZE): + continue + + ctx.rom = rom + if ctx.game != GAME_SMZ3: + death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR if ctx.game == GAME_ALTTP else + SM_DEATH_LINK_ACTIVE_ADDR, 1) + if death_link: + ctx.allow_collect = bool(death_link[0] & 0b100) + ctx.death_link_allow_survive = bool(death_link[0] & 0b10) + await ctx.update_death_link(bool(death_link[0] & 0b1)) + if not ctx.prev_rom or ctx.prev_rom != ctx.rom: + ctx.locations_checked = set() + ctx.locations_scouted = set() + ctx.locations_info = {} + ctx.prev_rom = ctx.rom if ctx.awaiting_rom: await ctx.server_auth(False) @@ -1279,6 +1287,9 @@ async def game_watcher(ctx: Context): color(ctx.item_names[item.item], 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), ctx.location_names[item.location], itemOutPtr, len(ctx.items_received))) await snes_flush_writes(ctx) + elif ctx.game == GAME_DKC3: + from worlds.dkc3.Client import dkc3_game_watcher + await dkc3_game_watcher(ctx) async def run_game(romfile): @@ -1296,7 +1307,7 @@ async def main(): parser = get_base_parser() parser.add_argument('diff_file', default="", type=str, nargs="?", help='Path to a Archipelago Binary Patch file') - parser.add_argument('--snes', default='localhost:8080', help='Address of the SNI server.') + parser.add_argument('--snes', default='localhost:23074', help='Address of the SNI server.') parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical']) args = parser.parse_args() diff --git a/Starcraft2Client.py b/Starcraft2Client.py index f9b6b43fe3..dc63e9a456 100644 --- a/Starcraft2Client.py +++ b/Starcraft2Client.py @@ -19,7 +19,13 @@ from worlds.sc2wol.Items import lookup_id_to_name, item_table from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET from worlds.sc2wol import SC2WoLWorld -from Utils import init_logging +from pathlib import Path +import re +from MultiServer import mark_raw +import ctypes +import sys + +from Utils import init_logging, is_windows if __name__ == "__main__": init_logging("SC2Client", exception_logger="Client") @@ -38,11 +44,38 @@ nest_asyncio.apply() class StarcraftClientProcessor(ClientCommandProcessor): ctx: SC2Context + def _cmd_difficulty(self, difficulty: str = "") -> bool: + """Overrides the current difficulty set for the seed. Takes the argument casual, normal, hard, or brutal""" + options = difficulty.split() + num_options = len(options) + difficulty_choice = options[0].lower() + + if num_options > 0: + if difficulty_choice == "casual": + self.ctx.difficulty_override = 0 + elif difficulty_choice == "normal": + self.ctx.difficulty_override = 1 + elif difficulty_choice == "hard": + self.ctx.difficulty_override = 2 + elif difficulty_choice == "brutal": + self.ctx.difficulty_override = 3 + else: + self.output("Unable to parse difficulty '" + options[0] + "'") + return False + + self.output("Difficulty set to " + options[0]) + return True + + else: + self.output("Difficulty needs to be specified in the command.") + return False + def _cmd_disable_mission_check(self) -> bool: """Disables the check to see if a mission is available to play. Meant for co-op runs where one player can play the next mission in a chain the other player is doing.""" self.ctx.missions_unlocked = True sc2_logger.info("Mission check has been disabled") + return True def _cmd_play(self, mission_id: str = "") -> bool: """Start a Starcraft 2 mission""" @@ -58,6 +91,7 @@ class StarcraftClientProcessor(ClientCommandProcessor): else: sc2_logger.info( "Mission ID needs to be specified. Use /unfinished or /available to view ids for available missions.") + return False return True @@ -73,6 +107,17 @@ class StarcraftClientProcessor(ClientCommandProcessor): request_unfinished_missions(self.ctx.checked_locations, self.ctx.mission_req_table, self.ctx.ui, self.ctx) return True + @mark_raw + def _cmd_set_path(self, path: str = '') -> bool: + """Manually set the SC2 install directory (if the automatic detection fails).""" + if path: + os.environ["SC2PATH"] = path + check_mod_install() + return True + else: + sc2_logger.warning("When using set_path, you must type the path to your SC2 install directory.") + return False + class SC2Context(CommonContext): command_processor = StarcraftClientProcessor @@ -91,6 +136,7 @@ class SC2Context(CommonContext): missions_unlocked = False current_tooltip = None last_loc_list = None + difficulty_override = -1 async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: @@ -111,6 +157,11 @@ class SC2Context(CommonContext): for mission in slot_req_table: self.mission_req_table[mission] = MissionInfo(**slot_req_table[mission]) + # Look for and set SC2PATH. + # check_game_install_path() returns True if and only if it finds + sets SC2PATH. + if "SC2PATH" not in os.environ and check_game_install_path(): + check_mod_install() + if cmd in {"PrintJSON"}: if "receiving" in args: if self.slot_concerns_self(args["receiving"]): @@ -415,8 +466,9 @@ async def starcraft_launch(ctx: SC2Context, mission_id): sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id]}. If game does not launch check log file for errors.") - run_game(sc2.maps.get(maps_table[mission_id - 1]), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id), - name="Archipelago", fullscreen=True)], realtime=True) + with DllDirectory(None): + run_game(sc2.maps.get(maps_table[mission_id - 1]), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id), + name="Archipelago", fullscreen=True)], realtime=True) class ArchipelagoBot(sc2.bot_ai.BotAI): @@ -447,7 +499,10 @@ class ArchipelagoBot(sc2.bot_ai.BotAI): game_state = 0 if iteration == 0: start_items = calculate_items(self.ctx.items_received) - difficulty = calc_difficulty(self.ctx.difficulty) + if self.ctx.difficulty_override >= 0: + difficulty = calc_difficulty(self.ctx.difficulty_override) + else: + difficulty = calc_difficulty(self.ctx.difficulty) await self.chat_send("ArchipelagoLoad {} {} {} {} {} {} {} {} {} {} {} {} {}".format( difficulty, start_items[0], start_items[1], start_items[2], start_items[3], start_items[4], @@ -796,6 +851,101 @@ def initialize_blank_mission_dict(location_table): return unlocks +def check_game_install_path() -> bool: + # First thing: go to the default location for ExecuteInfo. + # An exception for Windows is included because it's very difficult to find ~\Documents if the user moved it. + if is_windows: + # The next five lines of utterly inscrutable code are brought to you by copy-paste from Stack Overflow. + # https://stackoverflow.com/questions/6227590/finding-the-users-my-documents-path/30924555# + import ctypes.wintypes + CSIDL_PERSONAL = 5 # My Documents + SHGFP_TYPE_CURRENT = 0 # Get current, not default value + + buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH) + ctypes.windll.shell32.SHGetFolderPathW(None, CSIDL_PERSONAL, None, SHGFP_TYPE_CURRENT, buf) + documentspath = buf.value + einfo = str(documentspath / Path("StarCraft II\\ExecuteInfo.txt")) + else: + einfo = str(sc2.paths.get_home() / Path(sc2.paths.USERPATH[sc2.paths.PF])) + + # Check if the file exists. + if os.path.isfile(einfo): + + # Open the file and read it, picking out the latest executable's path. + with open(einfo) as f: + content = f.read() + if content: + base = re.search(r" = (.*)Versions", content).group(1) + if os.path.exists(base): + executable = sc2.paths.latest_executeble(Path(base).expanduser() / "Versions") + + # Finally, check the path for an actual executable. + # If we find one, great. Set up the SC2PATH. + if os.path.isfile(executable): + sc2_logger.info(f"Found an SC2 install at {base}!") + sc2_logger.debug(f"Latest executable at {executable}.") + os.environ["SC2PATH"] = base + sc2_logger.debug(f"SC2PATH set to {base}.") + return True + else: + sc2_logger.warning(f"We may have found an SC2 install at {base}, but couldn't find {executable}.") + else: + sc2_logger.warning(f"{einfo} pointed to {base}, but we could not find an SC2 install there.") + else: + sc2_logger.warning(f"Couldn't find {einfo}. Please run /set_path with your SC2 install directory.") + return False + + +def check_mod_install() -> bool: + # Pull up the SC2PATH if set. If not, encourage the user to manually run /set_path. + try: + # Check inside the Mods folder for Archipelago.SC2Mod. If found, tell user. If not, tell user. + if os.path.isfile(modfile := (os.environ["SC2PATH"] / Path("Mods") / Path("Archipelago.SC2Mod"))): + sc2_logger.info(f"Archipelago mod found at {modfile}.") + return True + else: + sc2_logger.warning(f"Archipelago mod could not be found at {modfile}. Please install the mod file there.") + except KeyError: + sc2_logger.warning(f"SC2PATH isn't set. Please run /set_path with the path to your SC2 install.") + return False + + +class DllDirectory: + # Credit to Black Sliver for this code. + # More info: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setdlldirectoryw + _old: typing.Optional[str] = None + _new: typing.Optional[str] = None + + def __init__(self, new: typing.Optional[str]): + self._new = new + + def __enter__(self): + old = self.get() + if self.set(self._new): + self._old = old + + def __exit__(self, *args): + if self._old is not None: + self.set(self._old) + + @staticmethod + def get() -> str: + if sys.platform == "win32": + n = ctypes.windll.kernel32.GetDllDirectoryW(0, None) + buf = ctypes.create_unicode_buffer(n) + ctypes.windll.kernel32.GetDllDirectoryW(n, buf) + return buf.value + # NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific + return None + + @staticmethod + def set(s: typing.Optional[str]) -> bool: + if sys.platform == "win32": + return ctypes.windll.kernel32.SetDllDirectoryW(s) != 0 + # NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific + return False + + if __name__ == '__main__': colorama.init() asyncio.run(main()) diff --git a/Utils.py b/Utils.py index e13a5fe773..c621e31c9a 100644 --- a/Utils.py +++ b/Utils.py @@ -1,6 +1,5 @@ from __future__ import annotations -import shutil import typing import builtins import os @@ -12,12 +11,18 @@ import io import collections import importlib import logging -import decimal +from yaml import load, load_all, dump, SafeLoader + +try: + from yaml import CLoader as UnsafeLoader + from yaml import CDumper as Dumper +except ImportError: + from yaml import Loader as UnsafeLoader + from yaml import Dumper if typing.TYPE_CHECKING: - from tkinter import Tk -else: - Tk = typing.Any + import tkinter + import pathlib def tuplize_version(version: str) -> Version: @@ -30,21 +35,13 @@ class Version(typing.NamedTuple): build: int -__version__ = "0.3.3" +__version__ = "0.3.4" version_tuple = tuplize_version(__version__) -is_linux = sys.platform.startswith('linux') -is_macos = sys.platform == 'darwin' +is_linux = sys.platform.startswith("linux") +is_macos = sys.platform == "darwin" is_windows = sys.platform in ("win32", "cygwin", "msys") -import jellyfish -from yaml import load, load_all, dump, SafeLoader - -try: - from yaml import CLoader as Loader -except ImportError: - from yaml import Loader - def int16_as_bytes(value: int) -> typing.List[int]: value = value & 0xFFFF @@ -125,17 +122,18 @@ def home_path(*path: str) -> str: def user_path(*path: str) -> str: """Returns either local_path or home_path based on write permissions.""" - if hasattr(user_path, 'cached_path'): + if hasattr(user_path, "cached_path"): pass elif os.access(local_path(), os.W_OK): user_path.cached_path = local_path() else: user_path.cached_path = home_path() # populate home from local - TODO: upgrade feature - if user_path.cached_path != local_path() and not os.path.exists(user_path('host.yaml')): - for dn in ('Players', 'data/sprites'): + if user_path.cached_path != local_path() and not os.path.exists(user_path("host.yaml")): + import shutil + for dn in ("Players", "data/sprites"): shutil.copytree(local_path(dn), user_path(dn), dirs_exist_ok=True) - for fn in ('manifest.json', 'host.yaml'): + for fn in ("manifest.json", "host.yaml"): shutil.copy2(local_path(fn), user_path(fn)) return os.path.join(user_path.cached_path, *path) @@ -150,11 +148,12 @@ def output_path(*path: str): return path -def open_file(filename): - if sys.platform == 'win32': +def open_file(filename: typing.Union[str, "pathlib.Path"]) -> None: + if is_windows: os.startfile(filename) else: - open_command = 'open' if sys.platform == 'darwin' else 'xdg-open' + from shutil import which + open_command = which("open") if is_macos else (which("xdg-open") or which("gnome-open") or which("kde-open")) subprocess.call([open_command, filename]) @@ -173,7 +172,9 @@ class UniqueKeyLoader(SafeLoader): parse_yaml = functools.partial(load, Loader=UniqueKeyLoader) parse_yamls = functools.partial(load_all, Loader=UniqueKeyLoader) -unsafe_parse_yaml = functools.partial(load, Loader=Loader) +unsafe_parse_yaml = functools.partial(load, Loader=UnsafeLoader) + +del load, load_all # should not be used. don't leak their names def get_cert_none_ssl_context(): @@ -191,11 +192,12 @@ def get_public_ipv4() -> str: ip = socket.gethostbyname(socket.gethostname()) ctx = get_cert_none_ssl_context() try: - ip = urllib.request.urlopen('https://checkip.amazonaws.com/', context=ctx).read().decode('utf8').strip() + ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx).read().decode("utf8").strip() except Exception as e: + # noinspection PyBroadException try: - ip = urllib.request.urlopen('https://v4.ident.me', context=ctx).read().decode('utf8').strip() - except: + ip = urllib.request.urlopen("https://v4.ident.me", context=ctx).read().decode("utf8").strip() + except Exception: logging.exception(e) pass # we could be offline, in a local game, so no point in erroring out return ip @@ -208,7 +210,7 @@ def get_public_ipv6() -> str: ip = socket.gethostbyname(socket.gethostname()) ctx = get_cert_none_ssl_context() try: - ip = urllib.request.urlopen('https://v6.ident.me', context=ctx).read().decode('utf8').strip() + ip = urllib.request.urlopen("https://v6.ident.me", context=ctx).read().decode("utf8").strip() except Exception as e: logging.exception(e) pass # we could be offline, in a local game, or ipv6 may not be available @@ -277,7 +279,12 @@ def get_default_options() -> dict: }, "oot_options": { "rom_file": "The Legend of Zelda - Ocarina of Time.z64", - } + }, + "dkc3_options": { + "rom_file": "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc", + "sni": "SNI", + "rom_start": True, + }, } return options @@ -304,33 +311,19 @@ def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict: @cache_argsless def get_options() -> dict: - if not hasattr(get_options, "options"): - filenames = ("options.yaml", "host.yaml") - locations = [] - if os.path.join(os.getcwd()) != local_path(): - locations += filenames # use files from cwd only if it's not the local_path - locations += [user_path(filename) for filename in filenames] + filenames = ("options.yaml", "host.yaml") + locations = [] + if os.path.join(os.getcwd()) != local_path(): + locations += filenames # use files from cwd only if it's not the local_path + locations += [user_path(filename) for filename in filenames] - for location in locations: - if os.path.exists(location): - with open(location) as f: - options = parse_yaml(f.read()) + for location in locations: + if os.path.exists(location): + with open(location) as f: + options = parse_yaml(f.read()) + return update_options(get_default_options(), options, location, list()) - get_options.options = update_options(get_default_options(), options, location, list()) - break - else: - raise FileNotFoundError(f"Could not find {filenames[1]} to load options.") - return get_options.options - - -def get_item_name_from_id(code: int) -> str: - from worlds import lookup_any_item_id_to_name - return lookup_any_item_id_to_name.get(code, f'Unknown item (ID:{code})') - - -def get_location_name_from_id(code: int) -> str: - from worlds import lookup_any_location_id_to_name - return lookup_any_location_id_to_name.get(code, f'Unknown location (ID:{code})') + raise FileNotFoundError(f"Could not find {filenames[1]} to load options.") def persistent_store(category: str, key: typing.Any, value: typing.Any): @@ -339,10 +332,10 @@ def persistent_store(category: str, key: typing.Any, value: typing.Any): category = storage.setdefault(category, {}) category[key] = value with open(path, "wt") as f: - f.write(dump(storage)) + f.write(dump(storage, Dumper=Dumper)) -def persistent_load() -> typing.Dict[dict]: +def persistent_load() -> typing.Dict[str, dict]: storage = getattr(persistent_load, "storage", None) if storage: return storage @@ -360,8 +353,8 @@ def persistent_load() -> typing.Dict[dict]: return storage -def get_adjuster_settings(gameName: str): - adjuster_settings = persistent_load().get("adjuster", {}).get(gameName, {}) +def get_adjuster_settings(game_name: str): + adjuster_settings = persistent_load().get("adjuster", {}).get(game_name, {}) return adjuster_settings @@ -377,10 +370,10 @@ def get_unique_identifier(): return uuid -safe_builtins = { +safe_builtins = frozenset(( 'set', 'frozenset', -} +)) class RestrictedUnpickler(pickle.Unpickler): @@ -408,8 +401,7 @@ class RestrictedUnpickler(pickle.Unpickler): if issubclass(obj, self.options_module.Option): return obj # Forbid everything else. - raise pickle.UnpicklingError("global '%s.%s' is forbidden" % - (module, name)) + raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden") def restricted_loads(s): @@ -418,6 +410,9 @@ def restricted_loads(s): class KeyedDefaultDict(collections.defaultdict): + """defaultdict variant that uses the missing key as argument to default_factory""" + default_factory: typing.Callable[[typing.Any], typing.Any] + def __missing__(self, key): self[key] = value = self.default_factory(key) return value @@ -427,6 +422,10 @@ def get_text_between(text: str, start: str, end: str) -> str: return text[text.index(start) + len(start): text.rindex(end)] +def get_text_after(text: str, start: str) -> str: + return text[text.index(start) + len(start):] + + loglevel_mapping = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG} @@ -474,9 +473,13 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri def stream_input(stream, queue): def queuer(): while 1: - text = stream.readline().strip() - if text: - queue.put_nowait(text) + try: + text = stream.readline().strip() + except UnicodeDecodeError as e: + logging.exception(e) + else: + if text: + queue.put_nowait(text) from threading import Thread thread = Thread(target=queuer, name=f"Stream handler for {stream.name}", daemon=True) @@ -484,11 +487,11 @@ def stream_input(stream, queue): return thread -def tkinter_center_window(window: Tk): +def tkinter_center_window(window: "tkinter.Tk") -> None: window.update() - xPos = int(window.winfo_screenwidth() / 2 - window.winfo_reqwidth() / 2) - yPos = int(window.winfo_screenheight() / 2 - window.winfo_reqheight() / 2) - window.geometry("+{}+{}".format(xPos, yPos)) + x = int(window.winfo_screenwidth() / 2 - window.winfo_reqwidth() / 2) + y = int(window.winfo_screenheight() / 2 - window.winfo_reqheight() / 2) + window.geometry(f"+{x}+{y}") class VersionException(Exception): @@ -505,24 +508,27 @@ def chaining_prefix(index: int, labels: typing.Tuple[str]) -> str: # noinspection PyPep8Naming -def format_SI_prefix(value, power=1000, power_labels=('', 'k', 'M', 'G', 'T', "P", "E", "Z", "Y")) -> str: +def format_SI_prefix(value, power=1000, power_labels=("", "k", "M", "G", "T", "P", "E", "Z", "Y")) -> str: """Formats a value into a value + metric/si prefix. More info at https://en.wikipedia.org/wiki/Metric_prefix""" + import decimal n = 0 value = decimal.Decimal(value) - while value >= power: + limit = power - decimal.Decimal("0.005") + while value >= limit: value /= power n += 1 return f"{value.quantize(decimal.Decimal('1.00'))} {chaining_prefix(n, power_labels)}" -def get_fuzzy_ratio(word1: str, word2: str) -> float: - return (1 - jellyfish.damerau_levenshtein_distance(word1.lower(), word2.lower()) - / max(len(word1), len(word2))) - - def get_fuzzy_results(input_word: str, wordlist: typing.Sequence[str], limit: typing.Optional[int] = None) \ -> typing.List[typing.Tuple[str, int]]: + import jellyfish + + def get_fuzzy_ratio(word1: str, word2: str) -> float: + return (1 - jellyfish.damerau_levenshtein_distance(word1.lower(), word2.lower()) + / max(len(word1), len(word2))) + limit: int = limit if limit else len(wordlist) return list( map( @@ -540,18 +546,19 @@ def get_fuzzy_results(input_word: str, wordlist: typing.Sequence[str], limit: ty def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typing.Sequence[str]]]) \ -> typing.Optional[str]: def run(*args: str): - return subprocess.run(args, capture_output=True, text=True).stdout.split('\n', 1)[0] or None + return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None if is_linux: # prefer native dialog - kdialog = shutil.which('kdialog') + from shutil import which + kdialog = which("kdialog") if kdialog: k_filters = '|'.join((f'{text} (*{" *".join(ext)})' for (text, ext) in filetypes)) - return run(kdialog, f'--title={title}', '--getopenfilename', '.', k_filters) - zenity = shutil.which('zenity') + return run(kdialog, f"--title={title}", "--getopenfilename", ".", k_filters) + zenity = which("zenity") if zenity: z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes) - return run(zenity, f'--title={title}', '--file-selection', *z_filters) + return run(zenity, f"--title={title}", "--file-selection", *z_filters) # fall back to tk try: @@ -569,10 +576,10 @@ def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typin def messagebox(title: str, text: str, error: bool = False) -> None: def run(*args: str): - return subprocess.run(args, capture_output=True, text=True).stdout.split('\n', 1)[0] or None + return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None def is_kivy_running(): - if 'kivy' in sys.modules: + if "kivy" in sys.modules: from kivy.app import App return App.get_running_app() is not None return False @@ -582,14 +589,15 @@ def messagebox(title: str, text: str, error: bool = False) -> None: MessageBox(title, text, error).open() return - if is_linux and not 'tkinter' in sys.modules: + if is_linux and "tkinter" not in sys.modules: # prefer native dialog - kdialog = shutil.which('kdialog') + from shutil import which + kdialog = which("kdialog") if kdialog: - return run(kdialog, f'--title={title}', '--error' if error else '--msgbox', text) - zenity = shutil.which('zenity') + return run(kdialog, f"--title={title}", "--error" if error else "--msgbox", text) + zenity = which("zenity") if zenity: - return run(zenity, f'--title={title}', f'--text={text}', '--error' if error else '--info') + return run(zenity, f"--title={title}", f"--text={text}", "--error" if error else "--info") # fall back to tk try: @@ -604,3 +612,14 @@ def messagebox(title: str, text: str, error: bool = False) -> None: root.withdraw() showerror(title, text) if error else showinfo(title, text) root.update() + + +def title_sorted(data: typing.Sequence, key=None, ignore: typing.Set = frozenset(("a", "the"))): + """Sorts a sequence of text ignoring typical articles like "a" or "the" in the beginning.""" + def sorter(element: str) -> str: + parts = element.split(maxsplit=1) + if parts[0].lower() in ignore: + return parts[1] + else: + return element + return sorted(data, key=lambda i: sorter(key(i)) if key else sorter(i)) diff --git a/WebHost.py b/WebHost.py index eb575df3e9..db802193a6 100644 --- a/WebHost.py +++ b/WebHost.py @@ -14,7 +14,7 @@ import Utils Utils.local_path.cached_path = os.path.dirname(__file__) -from WebHostLib import app as raw_app +from WebHostLib import register, app as raw_app from waitress import serve from WebHostLib.models import db @@ -22,14 +22,13 @@ from WebHostLib.autolauncher import autohost, autogen from WebHostLib.lttpsprites import update_sprites_lttp from WebHostLib.options import create as create_options_files -from worlds.AutoWorld import AutoWorldRegister - configpath = os.path.abspath("config.yaml") if not os.path.exists(configpath): # fall back to config.yaml in home configpath = os.path.abspath(Utils.user_path('config.yaml')) def get_app(): + register() app = raw_app if os.path.exists(configpath): import yaml @@ -43,19 +42,39 @@ def get_app(): def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]: import json import shutil + import zipfile + + zfile: zipfile.ZipInfo + + from worlds.AutoWorld import AutoWorldRegister worlds = {} data = [] for game, world in AutoWorldRegister.world_types.items(): if hasattr(world.web, 'tutorials') and (not world.hidden or game == 'Archipelago'): worlds[game] = world + + base_target_path = Utils.local_path("WebHostLib", "static", "generated", "docs") for game, world in worlds.items(): # copy files from world's docs folder to the generated folder - source_path = Utils.local_path(os.path.dirname(sys.modules[world.__module__].__file__), 'docs') - target_path = Utils.local_path("WebHostLib", "static", "generated", "docs", game) - files = os.listdir(source_path) - for file in files: - os.makedirs(os.path.dirname(Utils.local_path(target_path, file)), exist_ok=True) - shutil.copyfile(Utils.local_path(source_path, file), Utils.local_path(target_path, file)) + target_path = os.path.join(base_target_path, game) + os.makedirs(target_path, exist_ok=True) + + if world.zip_path: + zipfile_path = world.zip_path + + assert os.path.isfile(zipfile_path), f"{zipfile_path} is not a valid file(path)." + assert zipfile.is_zipfile(zipfile_path), f"{zipfile_path} is not a valid zipfile." + + with zipfile.ZipFile(zipfile_path) as zf: + for zfile in zf.infolist(): + if not zfile.is_dir() and "/docs/" in zfile.filename: + zf.extract(zfile, target_path) + else: + source_path = Utils.local_path(os.path.dirname(world.__file__), "docs") + files = os.listdir(source_path) + for file in files: + shutil.copyfile(Utils.local_path(source_path, file), Utils.local_path(target_path, file)) + # build a json tutorial dict per game game_data = {'gameTitle': game, 'tutorials': []} for tutorial in world.web.tutorials: @@ -85,7 +104,7 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]] for games in data: if 'Archipelago' in games['gameTitle']: generic_data = data.pop(data.index(games)) - sorted_data = [generic_data] + sorted(data, key=lambda entry: entry["gameTitle"].lower()) + sorted_data = [generic_data] + Utils.title_sorted(data, key=lambda entry: entry["gameTitle"].lower()) json.dump(sorted_data, json_target, indent=2, ensure_ascii=False) return sorted_data diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index c0179b2238..b7bf4e38d1 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -3,13 +3,13 @@ import uuid import base64 import socket -import jinja2.exceptions from pony.flask import Pony -from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory +from flask import Flask from flask_caching import Cache from flask_compress import Compress -from worlds.AutoWorld import AutoWorldRegister +from werkzeug.routing import BaseConverter +from Utils import title_sorted from .models import * UPLOAD_FOLDER = os.path.relpath('uploads') @@ -53,8 +53,6 @@ app.config["PATCH_TARGET"] = "archipelago.gg" cache = Cache(app) Compress(app) -from werkzeug.routing import BaseConverter - class B64UUIDConverter(BaseConverter): @@ -68,170 +66,18 @@ class B64UUIDConverter(BaseConverter): # short UUID app.url_map.converters["suuid"] = B64UUIDConverter app.jinja_env.filters['suuid'] = lambda value: base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii') +app.jinja_env.filters["title_sorted"] = title_sorted -def get_world_theme(game_name: str): - if game_name in AutoWorldRegister.world_types: - return AutoWorldRegister.world_types[game_name].web.theme - return 'grass' +def register(): + """Import submodules, triggering their registering on flask routing. + Note: initializes worlds subsystem.""" + # has automatic patch integration + import Patch + app.jinja_env.filters['supports_apdeltapatch'] = lambda game_name: game_name in Patch.AutoPatchRegister.patch_types + from WebHostLib.customserver import run_server_process + # to trigger app routing picking up on it + from . import tracker, upload, landing, check, generate, downloads, api, stats, misc -@app.before_request -def register_session(): - session.permanent = True # technically 31 days after the last visit - if not session.get("_id", None): - session["_id"] = uuid4() # uniquely identify each session without needing a login - - -@app.errorhandler(404) -@app.errorhandler(jinja2.exceptions.TemplateNotFound) -def page_not_found(err): - return render_template('404.html'), 404 - - -# Start Playing Page -@app.route('/start-playing') -def start_playing(): - return render_template(f"startPlaying.html") - - -@app.route('/weighted-settings') -def weighted_settings(): - return render_template(f"weighted-settings.html") - - -# Player settings pages -@app.route('/games//player-settings') -def player_settings(game): - return render_template(f"player-settings.html", game=game, theme=get_world_theme(game)) - - -# Game Info Pages -@app.route('/games//info/') -def game_info(game, lang): - return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game)) - - -# List of supported games -@app.route('/games') -def games(): - worlds = {} - for game, world in AutoWorldRegister.world_types.items(): - if not world.hidden: - worlds[game] = world - return render_template("supportedGames.html", worlds=worlds) - - -@app.route('/tutorial///') -def tutorial(game, file, lang): - return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game)) - - -@app.route('/tutorial/') -def tutorial_landing(): - worlds = {} - for game, world in AutoWorldRegister.world_types.items(): - if not world.hidden: - worlds[game] = world - return render_template("tutorialLanding.html") - - -@app.route('/faq//') -def faq(lang): - return render_template("faq.html", lang=lang) - - -@app.route('/glossary//') -def terms(lang): - return render_template("glossary.html", lang=lang) - - -@app.route('/seed/') -def view_seed(seed: UUID): - seed = Seed.get(id=seed) - if not seed: - abort(404) - return render_template("viewSeed.html", seed=seed, slot_count=count(seed.slots)) - - -@app.route('/new_room/') -def new_room(seed: UUID): - seed = Seed.get(id=seed) - if not seed: - abort(404) - room = Room(seed=seed, owner=session["_id"], tracker=uuid4()) - commit() - return redirect(url_for("host_room", room=room.id)) - - -def _read_log(path: str): - if os.path.exists(path): - with open(path, encoding="utf-8-sig") as log: - yield from log - else: - yield f"Logfile {path} does not exist. " \ - f"Likely a crash during spinup of multiworld instance or it is still spinning up." - - -@app.route('/log/') -def display_log(room: UUID): - room = Room.get(id=room) - if room is None: - return abort(404) - if room.owner == session["_id"]: - return Response(_read_log(os.path.join("logs", str(room.id) + ".txt")), mimetype="text/plain;charset=UTF-8") - return "Access Denied", 403 - - -@app.route('/room/', methods=['GET', 'POST']) -def host_room(room: UUID): - room = Room.get(id=room) - if room is None: - return abort(404) - if request.method == "POST": - if room.owner == session["_id"]: - cmd = request.form["cmd"] - if cmd: - Command(room=room, commandtext=cmd) - commit() - - with db_session: - room.last_activity = datetime.utcnow() # will trigger a spinup, if it's not already running - - return render_template("hostRoom.html", room=room) - - -@app.route('/favicon.ico') -def favicon(): - return send_from_directory(os.path.join(app.root_path, 'static/static'), - 'favicon.ico', mimetype='image/vnd.microsoft.icon') - - -@app.route('/discord') -def discord(): - return redirect("https://discord.gg/archipelago") - - -@app.route('/datapackage') -@cache.cached() -def get_datapackge(): - """A pretty print version of /api/datapackage""" - from worlds import network_data_package - import json - return Response(json.dumps(network_data_package, indent=4), mimetype="text/plain") - - -@app.route('/index') -@app.route('/sitemap') -def get_sitemap(): - available_games = [] - for game, world in AutoWorldRegister.world_types.items(): - if not world.hidden: - available_games.append(game) - return render_template("siteMap.html", games=available_games) - - -from WebHostLib.customserver import run_server_process -from . import tracker, upload, landing, check, generate, downloads, api, stats # to trigger app routing picking up on it - -app.register_blueprint(api.api_endpoints) + app.register_blueprint(api.api_endpoints) diff --git a/WebHostLib/api/__init__.py b/WebHostLib/api/__init__.py index c2f9b3840f..80c60a093a 100644 --- a/WebHostLib/api/__init__.py +++ b/WebHostLib/api/__init__.py @@ -32,14 +32,14 @@ def room_info(room: UUID): @api_endpoints.route('/datapackage') @cache.cached() -def get_datapackge(): +def get_datapackage(): from worlds import network_data_package return network_data_package @api_endpoints.route('/datapackage_version') @cache.cached() -def get_datapackge_versions(): +def get_datapackage_versions(): from worlds import network_data_package, AutoWorldRegister version_package = {game: world.data_version for game, world in AutoWorldRegister.world_types.items()} version_package["version"] = network_data_package["version"] diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py index 9d7b7f4959..77445eadea 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -154,8 +154,10 @@ def autogen(config: dict): while 1: time.sleep(0.1) with db_session: + # for update locks the database row(s) during transaction, preventing writes from elsewhere to_start = select( - generation for generation in Generation if generation.state == STATE_QUEUED) + generation for generation in Generation + if generation.state == STATE_QUEUED).for_update() for generation in to_start: launch_generator(generator_pool, generation) except AlreadyRunningException: @@ -182,7 +184,7 @@ class MultiworldInstance(): logging.info(f"Spinning up {self.room_id}") process = multiprocessing.Process(group=None, target=run_server_process, - args=(self.room_id, self.ponyconfig), + args=(self.room_id, self.ponyconfig, get_static_server_data()), name="MultiHost") process.start() # bind after start to prevent thread sync issues with guardian. @@ -236,5 +238,5 @@ def run_guardian(): from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed -from .customserver import run_server_process +from .customserver import run_server_process, get_static_server_data from .generate import gen_game diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index f78a8eb24d..01f1fd25e5 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -9,12 +9,13 @@ import time import random import pickle import logging +import datetime import Utils -from .models import * +from .models import db_session, Room, select, commit, Command, db from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor -from Utils import get_public_ipv4, get_public_ipv6, restricted_loads +from Utils import get_public_ipv4, get_public_ipv6, restricted_loads, cache_argsless class CustomClientMessageProcessor(ClientMessageProcessor): @@ -39,7 +40,7 @@ class CustomClientMessageProcessor(ClientMessageProcessor): import MultiServer MultiServer.client_message_processor = CustomClientMessageProcessor -del (MultiServer) +del MultiServer class DBCommandProcessor(ServerCommandProcessor): @@ -48,12 +49,20 @@ class DBCommandProcessor(ServerCommandProcessor): class WebHostContext(Context): - def __init__(self): + def __init__(self, static_server_data: dict): + # static server data is used during _load_game_data to load required data, + # without needing to import worlds system, which takes quite a bit of memory + self.static_server_data = static_server_data super(WebHostContext, self).__init__("", 0, "", "", 1, 40, True, "enabled", "enabled", "enabled", 0, 2) + del self.static_server_data self.main_loop = asyncio.get_running_loop() self.video = {} self.tags = ["AP", "WebHost"] + def _load_game_data(self): + for key, value in self.static_server_data.items(): + setattr(self, key, value) + def listen_to_db_commands(self): cmdprocessor = DBCommandProcessor(self) @@ -107,14 +116,32 @@ def get_random_port(): return random.randint(49152, 65535) -def run_server_process(room_id, ponyconfig: dict): +@cache_argsless +def get_static_server_data() -> dict: + import worlds + data = { + "forced_auto_forfeits": {}, + "non_hintable_names": {}, + "gamespackage": worlds.network_data_package["games"], + "item_name_groups": {world_name: world.item_name_groups for world_name, world in + worlds.AutoWorldRegister.world_types.items()}, + } + + for world_name, world in worlds.AutoWorldRegister.world_types.items(): + data["forced_auto_forfeits"][world_name] = world.forced_auto_forfeit + data["non_hintable_names"][world_name] = world.hint_blacklist + + return data + + +def run_server_process(room_id, ponyconfig: dict, static_server_data: dict): # establish DB connection for multidata and multisave db.bind(**ponyconfig) db.generate_mapping(check_tables=False) async def main(): Utils.init_logging(str(room_id), write_mode="a") - ctx = WebHostContext() + ctx = WebHostContext(static_server_data) ctx.load(room_id) ctx.init_save() diff --git a/WebHostLib/downloads.py b/WebHostLib/downloads.py index 9b93b82c54..528cbe5ec0 100644 --- a/WebHostLib/downloads.py +++ b/WebHostLib/downloads.py @@ -36,14 +36,14 @@ def download_patch(room_id, patch_id): fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}" \ f"{AutoPatchRegister.patch_types[patch.game].patch_file_ending}" new_file.seek(0) - return send_file(new_file, as_attachment=True, attachment_filename=fname) + return send_file(new_file, as_attachment=True, download_name=fname) else: patch_data = update_patch_data(patch.data, server=f"{app.config['PATCH_TARGET']}:{last_port}") patch_data = BytesIO(patch_data) fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}." \ f"{preferred_endings[patch.game]}" - return send_file(patch_data, as_attachment=True, attachment_filename=fname) + return send_file(patch_data, as_attachment=True, download_name=fname) @app.route("/dl_spoiler/") @@ -66,7 +66,7 @@ def download_slot_file(room_id, player_id: int): from worlds.minecraft import mc_update_output fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmc" data = mc_update_output(slot_data.data, server=app.config['PATCH_TARGET'], port=room.last_port) - return send_file(io.BytesIO(data), as_attachment=True, attachment_filename=fname) + return send_file(io.BytesIO(data), as_attachment=True, download_name=fname) elif slot_data.game == "Factorio": with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf: for name in zf.namelist(): @@ -78,9 +78,11 @@ def download_slot_file(room_id, player_id: int): fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apv6" elif slot_data.game == "Super Mario 64": fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apsm64ex" + elif slot_data.game == "Dark Souls III": + fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}.json" else: return "Game download not supported." - return send_file(io.BytesIO(slot_data.data), as_attachment=True, attachment_filename=fname) + return send_file(io.BytesIO(slot_data.data), as_attachment=True, download_name=fname) @app.route("/templates") diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index c33d2648a7..15067e131b 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -4,7 +4,7 @@ import random import json import zipfile from collections import Counter -from typing import Dict, Optional as TypeOptional +from typing import Dict, Optional, Any from Utils import __version__ from flask import request, flash, redirect, url_for, session, render_template @@ -15,7 +15,7 @@ from BaseClasses import seeddigits, get_seed from Generate import handle_name, PlandoSettings import pickle -from .models import * +from .models import Generation, STATE_ERROR, STATE_QUEUED, commit, db_session, Seed, UUID from WebHostLib import app from .check import get_yaml_data, roll_options from .upload import upload_zip_to_db @@ -30,16 +30,15 @@ def get_meta(options_source: dict) -> dict: } plando_options -= {""} - meta = { + server_options = { "hint_cost": int(options_source.get("hint_cost", 10)), "forfeit_mode": options_source.get("forfeit_mode", "goal"), "remaining_mode": options_source.get("remaining_mode", "disabled"), "collect_mode": options_source.get("collect_mode", "disabled"), "item_cheat": bool(int(options_source.get("item_cheat", 1))), "server_password": options_source.get("server_password", None), - "plando_options": list(plando_options) } - return meta + return {"server_options": server_options, "plando_options": list(plando_options)} @app.route('/generate', methods=['GET', 'POST']) @@ -60,13 +59,13 @@ def generate(race=False): results, gen_options = roll_options(options, meta["plando_options"]) if race: - meta["item_cheat"] = False - meta["remaining_mode"] = "disabled" + meta["server_options"]["item_cheat"] = False + meta["server_options"]["remaining_mode"] = "disabled" if any(type(result) == str for result in results.values()): return render_template("checkResult.html", results=results) elif len(gen_options) > app.config["MAX_ROLL"]: - flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players for now. " + flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. " f"If you have a larger group, please generate it yourself and upload it.") elif len(gen_options) >= app.config["JOB_THRESHOLD"]: gen = Generation( @@ -92,23 +91,22 @@ def generate(race=False): return render_template("generate.html", race=race, version=__version__) -def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=None, sid=None): +def gen_game(gen_options, meta: Optional[Dict[str, Any]] = None, owner=None, sid=None): if not meta: - meta: Dict[str, object] = {} + meta: Dict[str, Any] = {} + + meta.setdefault("server_options", {}).setdefault("hint_cost", 10) + race = meta.setdefault("race", False) - meta.setdefault("hint_cost", 10) - race = meta.get("race", False) - del (meta["race"]) - plando_options = meta.get("plando", {"bosses", "items", "connections", "texts"}) - del (meta["plando_options"]) try: target = tempfile.TemporaryDirectory() playercount = len(gen_options) seed = get_seed() - random.seed(seed) if race: - random.seed() # reset to time-based random source + random.seed() # use time-based random source + else: + random.seed(seed) seedname = "W" + (f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits)) @@ -120,7 +118,8 @@ def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=No erargs.outputname = seedname erargs.outputpath = target.name erargs.teams = 1 - erargs.plando_options = PlandoSettings.from_set(plando_options) + erargs.plando_options = PlandoSettings.from_set(meta.setdefault("plando_options", + {"bosses", "items", "connections", "texts"})) name_counter = Counter() for player, (playerfile, settings) in enumerate(gen_options.items(), 1): @@ -136,7 +135,7 @@ def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=No erargs.name[player] = handle_name(erargs.name[player], player, name_counter) if len(set(erargs.name.values())) != len(erargs.name): raise Exception(f"Names have to be unique. Names: {Counter(erargs.name.values())}") - ERmain(erargs, seed, baked_server_options=meta) + ERmain(erargs, seed, baked_server_options=meta["server_options"]) return upload_to_db(target.name, sid, owner, race) except BaseException as e: @@ -148,7 +147,6 @@ def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=No meta = json.loads(gen.meta) meta["error"] = (e.__class__.__name__ + ": " + str(e)) gen.meta = json.dumps(meta) - commit() raise diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py new file mode 100644 index 0000000000..03cd03b624 --- /dev/null +++ b/WebHostLib/misc.py @@ -0,0 +1,173 @@ +import datetime +import os + +import jinja2.exceptions +from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory + +from .models import count, Seed, commit, Room, db_session, Command, UUID, uuid4 +from worlds.AutoWorld import AutoWorldRegister +from . import app, cache + + +def get_world_theme(game_name: str): + if game_name in AutoWorldRegister.world_types: + return AutoWorldRegister.world_types[game_name].web.theme + return 'grass' + + +@app.before_request +def register_session(): + session.permanent = True # technically 31 days after the last visit + if not session.get("_id", None): + session["_id"] = uuid4() # uniquely identify each session without needing a login + + +@app.errorhandler(404) +@app.errorhandler(jinja2.exceptions.TemplateNotFound) +def page_not_found(err): + return render_template('404.html'), 404 + + +# Start Playing Page +@app.route('/start-playing') +def start_playing(): + return render_template(f"startPlaying.html") + + +@app.route('/weighted-settings') +def weighted_settings(): + return render_template(f"weighted-settings.html") + + +# Player settings pages +@app.route('/games//player-settings') +def player_settings(game): + return render_template(f"player-settings.html", game=game, theme=get_world_theme(game)) + + +# Game Info Pages +@app.route('/games//info/') +def game_info(game, lang): + return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game)) + + +# List of supported games +@app.route('/games') +def games(): + worlds = {} + for game, world in AutoWorldRegister.world_types.items(): + if not world.hidden: + worlds[game] = world + return render_template("supportedGames.html", worlds=worlds) + + +@app.route('/tutorial///') +def tutorial(game, file, lang): + return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game)) + + +@app.route('/tutorial/') +def tutorial_landing(): + worlds = {} + for game, world in AutoWorldRegister.world_types.items(): + if not world.hidden: + worlds[game] = world + return render_template("tutorialLanding.html") + + +@app.route('/faq//') +def faq(lang): + return render_template("faq.html", lang=lang) + + +@app.route('/glossary//') +def terms(lang): + return render_template("glossary.html", lang=lang) + + +@app.route('/seed/') +def view_seed(seed: UUID): + seed = Seed.get(id=seed) + if not seed: + abort(404) + return render_template("viewSeed.html", seed=seed, slot_count=count(seed.slots)) + + +@app.route('/new_room/') +def new_room(seed: UUID): + seed = Seed.get(id=seed) + if not seed: + abort(404) + room = Room(seed=seed, owner=session["_id"], tracker=uuid4()) + commit() + return redirect(url_for("host_room", room=room.id)) + + +def _read_log(path: str): + if os.path.exists(path): + with open(path, encoding="utf-8-sig") as log: + yield from log + else: + yield f"Logfile {path} does not exist. " \ + f"Likely a crash during spinup of multiworld instance or it is still spinning up." + + +@app.route('/log/') +def display_log(room: UUID): + room = Room.get(id=room) + if room is None: + return abort(404) + if room.owner == session["_id"]: + return Response(_read_log(os.path.join("logs", str(room.id) + ".txt")), mimetype="text/plain;charset=UTF-8") + return "Access Denied", 403 + + +@app.route('/room/', methods=['GET', 'POST']) +def host_room(room: UUID): + room: Room = Room.get(id=room) + if room is None: + return abort(404) + if request.method == "POST": + if room.owner == session["_id"]: + cmd = request.form["cmd"] + if cmd: + Command(room=room, commandtext=cmd) + commit() + + now = datetime.datetime.utcnow() + # indicate that the page should reload to get the assigned port + should_refresh = not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3) + with db_session: + room.last_activity = now # will trigger a spinup, if it's not already running + + return render_template("hostRoom.html", room=room, should_refresh=should_refresh) + + +@app.route('/favicon.ico') +def favicon(): + return send_from_directory(os.path.join(app.root_path, 'static/static'), + 'favicon.ico', mimetype='image/vnd.microsoft.icon') + + +@app.route('/discord') +def discord(): + return redirect("https://discord.gg/archipelago") + + +@app.route('/datapackage') +@cache.cached() +def get_datapackage(): + """A pretty print version of /api/datapackage""" + from worlds import network_data_package + import json + return Response(json.dumps(network_data_package, indent=4), mimetype="text/plain") + + +@app.route('/index') +@app.route('/sitemap') +def get_sitemap(): + available_games = [] + for game, world in AutoWorldRegister.world_types.items(): + if not world.hidden: + available_games.append(game) + return render_template("siteMap.html", games=available_games) diff --git a/WebHostLib/models.py b/WebHostLib/models.py index 3d6de6812c..70f0318f85 100644 --- a/WebHostLib/models.py +++ b/WebHostLib/models.py @@ -27,7 +27,7 @@ class Room(db.Entity): seed = Required('Seed', index=True) multisave = Optional(buffer, lazy=True) show_spoiler = Required(int, default=0) # 0 -> never, 1 -> after completion, -> 2 always - timeout = Required(int, default=lambda: 6 * 60 * 60) # seconds since last activity to shutdown + timeout = Required(int, default=lambda: 2 * 60 * 60) # seconds since last activity to shutdown tracker = Optional(UUID, index=True) last_port = Optional(int, default=lambda: 0) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 203f223561..3c481be62b 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -60,7 +60,7 @@ def create(): for game_name, world in AutoWorldRegister.world_types.items(): - all_options = {**world.options, **Options.per_game_common_options} + all_options = {**Options.per_game_common_options, **world.option_definitions} res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render( options=all_options, __version__=__version__, game=game_name, yaml_dump=yaml.dump, @@ -110,7 +110,7 @@ def create(): if option.default == "random": this_option["defaultValue"] = "random" - elif hasattr(option, "range_start") and hasattr(option, "range_end"): + elif issubclass(option, Options.Range): game_options[option_name] = { "type": "range", "displayName": option.display_name if hasattr(option, "display_name") else option_name, @@ -121,7 +121,7 @@ def create(): "max": option.range_end, } - if hasattr(option, "special_range_names"): + if issubclass(option, Options.SpecialRange): game_options[option_name]["type"] = 'special_range' game_options[option_name]["value_names"] = {} for key, val in option.special_range_names.items(): @@ -141,7 +141,7 @@ def create(): "description": option.__doc__ if option.__doc__ else "Please document me!", } - elif hasattr(option, "valid_keys"): + elif issubclass(option, Options.OptionList) or issubclass(option, Options.OptionSet): if option.valid_keys: game_options[option_name] = { "type": "custom-list", diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index 83132b5625..52d0316b2a 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -1,7 +1,7 @@ -flask>=2.1.2 +flask>=2.1.3 pony>=0.7.16 waitress>=2.1.1 -flask-caching>=1.11.1 +Flask-Caching>=2.0.1 Flask-Compress>=1.12 -Flask-Limiter>=2.4.6 -bokeh>=2.4.3 \ No newline at end of file +Flask-Limiter>=2.5.0 +bokeh>=2.4.3 diff --git a/WebHostLib/static/assets/tutorialLanding.js b/WebHostLib/static/assets/tutorialLanding.js index d00eefa307..b820cc3465 100644 --- a/WebHostLib/static/assets/tutorialLanding.js +++ b/WebHostLib/static/assets/tutorialLanding.js @@ -23,6 +23,7 @@ window.addEventListener('load', () => { games.forEach((game) => { const gameTitle = document.createElement('h2'); gameTitle.innerText = game.gameTitle; + gameTitle.id = `${encodeURIComponent(game.gameTitle)}`; tutorialDiv.appendChild(gameTitle); game.tutorials.forEach((tutorial) => { @@ -65,6 +66,15 @@ window.addEventListener('load', () => { showError(); console.error(error); } + + // Check if we are on an anchor when coming in, and scroll to it. + const hash = window.location.hash; + if (hash) { + const offset = 128; // To account for navbar banner at top of page. + window.scrollTo(0, 0); + const rect = document.getElementById(hash.slice(1)).getBoundingClientRect(); + window.scrollTo(rect.left, rect.top - offset); + } }; ajax.open('GET', `${window.location.origin}/static/generated/tutorials.json`, true); ajax.send(); diff --git a/WebHostLib/stats.py b/WebHostLib/stats.py index 9a164d02cb..54f5e598d1 100644 --- a/WebHostLib/stats.py +++ b/WebHostLib/stats.py @@ -1,54 +1,104 @@ from collections import Counter, defaultdict -from itertools import cycle +from colorsys import hsv_to_rgb from datetime import datetime, timedelta, date from math import tau +import typing from bokeh.embed import components -from bokeh.palettes import Dark2_8 as palette +from bokeh.models import HoverTool from bokeh.plotting import figure, ColumnDataSource from bokeh.resources import INLINE +from bokeh.colors import RGB from flask import render_template from pony.orm import select from . import app, cache from .models import Room +PLOT_WIDTH = 600 -def get_db_data(): + +def get_db_data(known_games: str) -> typing.Tuple[typing.Dict[str, int], typing.Dict[datetime.date, typing.Dict[str, int]]]: games_played = defaultdict(Counter) total_games = Counter() - cutoff = date.today()-timedelta(days=30000) + cutoff = date.today()-timedelta(days=30) room: Room for room in select(room for room in Room if room.creation_time >= cutoff): for slot in room.seed.slots: - total_games[slot.game] += 1 - games_played[room.creation_time.date()][slot.game] += 1 + if slot.game in known_games: + total_games[slot.game] += 1 + games_played[room.creation_time.date()][slot.game] += 1 return total_games, games_played -@app.route('/stats') -@cache.memoize(timeout=60*60) # regen once per hour should be plenty -def stats(): - plot = figure(title="Games Played Per Day", x_axis_type='datetime', x_axis_label="Date", - y_axis_label="Games Played", sizing_mode="scale_both", width=500, height=500) +def get_color_palette(colors_needed: int) -> typing.List[RGB]: + colors = [] + # colors_needed +1 to prevent first and last color being too close to each other + colors_needed += 1 - total_games, games_played = get_db_data() + for x in range(0, 361, 360 // colors_needed): + # a bit of noise on value to add some luminosity difference + colors.append(RGB(*(val * 255 for val in hsv_to_rgb(x / 360, 0.8, 0.8 + (x / 1800))))) + + # splice colors for maximum hue contrast. + colors = colors[::2] + colors[1::2] + + return colors + + +def create_game_played_figure(all_games_data: typing.Dict[datetime.date, typing.Dict[str, int]], + game: str, color: RGB) -> figure: + occurences = [] + days = [day for day, game_data in all_games_data.items() if game_data[game]] + for day in days: + occurences.append(all_games_data[day][game]) + data = { + "days": [datetime.combine(day, datetime.min.time()) for day in days], + "played": occurences + } + + plot = figure( + title=f"{game} Played Per Day", x_axis_type='datetime', x_axis_label="Date", + y_axis_label="Games Played", sizing_mode="scale_both", width=PLOT_WIDTH, height=500, + toolbar_location=None, tools="", + # setting legend to False seems broken in bokeh currently? + # legend=False + ) + + hover = HoverTool(tooltips=[("Date:", "@days{%F}"), ("Played:", "@played")], formatters={"@days": "datetime"}) + plot.add_tools(hover) + plot.vbar(x="days", top="played", legend_label=game, color=color, source=ColumnDataSource(data=data), width=1) + return plot + + +@app.route('/stats') +@cache.memoize(timeout=60 * 60) # regen once per hour should be plenty +def stats(): + from worlds import network_data_package + known_games = set(network_data_package["games"]) + plot = figure(title="Games Played Per Day", x_axis_type='datetime', x_axis_label="Date", + y_axis_label="Games Played", sizing_mode="scale_both", width=PLOT_WIDTH, height=500) + + total_games, games_played = get_db_data(known_games) days = sorted(games_played) - cyc_palette = cycle(palette) + color_palette = get_color_palette(len(total_games)) + game_to_color: typing.Dict[str, RGB] = {game: color for game, color in zip(total_games, color_palette)} for game in sorted(total_games): occurences = [] for day in days: occurences.append(games_played[day][game]) plot.line([datetime.combine(day, datetime.min.time()) for day in days], - occurences, legend_label=game, line_width=2, color=next(cyc_palette)) + occurences, legend_label=game, line_width=2, color=game_to_color[game]) total = sum(total_games.values()) pie = figure(plot_height=350, title=f"Games Played in the Last 30 Days (Total: {total})", toolbar_location=None, tools="hover", tooltips=[("Game:", "@games"), ("Played:", "@count")], - sizing_mode="scale_both", width=500, height=500) + sizing_mode="scale_both", width=PLOT_WIDTH, height=500, x_range=(-0.5, 1.2)) pie.axis.visible = False + pie.xgrid.visible = False + pie.ygrid.visible = False data = { "games": [], @@ -65,12 +115,15 @@ def stats(): current_angle += angle data["end_angles"].append(current_angle) - data["colors"] = [element[1] for element in sorted((game, color) for game, color in - zip(data["games"], cycle(palette)))] - pie.wedge(x=0.5, y=0.5, radius=0.5, + data["colors"] = [game_to_color[game] for game in data["games"]] + + pie.wedge(x=0, y=0, radius=0.5, start_angle="start_angles", end_angle="end_angles", fill_color="colors", source=ColumnDataSource(data=data), legend_field="games") - script, charts = components((plot, pie)) + per_game_charts = [create_game_played_figure(games_played, game, game_to_color[game]) for game in total_games + if total_games[game] > 1] + + script, charts = components((plot, pie, *per_game_charts)) return render_template("stats.html", js_resources=INLINE.render_js(), css_resources=INLINE.render_css(), chart_data=script, charts=charts) diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html index b5ec01f256..8981de9b7a 100644 --- a/WebHostLib/templates/hostRoom.html +++ b/WebHostLib/templates/hostRoom.html @@ -2,6 +2,7 @@ {% import "macros.html" as macros %} {% block head %} Multiworld {{ room.id|suuid }} + {% if should_refresh %}{% endif %} {% endblock %} @@ -16,9 +17,9 @@ This room has a Multiworld Tracker enabled.
{% endif %} - This room will be closed after {{ room.timeout//60//60 }} hours of inactivity. Should you wish to continue - later, - you can simply refresh this page and the server will be started again.
+ The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity. + Should you wish to continue later, + anyone can simply refresh this page and the server will resume.
{% if room.last_port %} You can connect to this room by using diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html index 70b41fad9e..9a92edbbf1 100644 --- a/WebHostLib/templates/macros.html +++ b/WebHostLib/templates/macros.html @@ -40,9 +40,12 @@ {% elif patch.game == "Super Mario 64" and room.seed.slots|length == 1 %} Download APSM64EX File... - {% elif patch.game in ["A Link to the Past", "Secret of Evermore", "Super Metroid", "SMZ3"] %} + {% elif patch.game | supports_apdeltapatch %} Download Patch File... + {% elif patch.game == "Dark Souls III" %} + + Download JSON File... {% else %} No file to download for this game. {% endif %} diff --git a/WebHostLib/templates/supportedGames.html b/WebHostLib/templates/supportedGames.html index 00f49d6d18..fe81463a46 100644 --- a/WebHostLib/templates/supportedGames.html +++ b/WebHostLib/templates/supportedGames.html @@ -10,15 +10,21 @@ {% include 'header/oceanHeader.html' %}

Currently Supported Games

- {% for game_name, world in worlds.items() | sort(attribute=0) %} + {% for game_name in worlds | title_sorted %} + {% set world = worlds[game_name] %}

{{ game_name }}

{{ world.__doc__ | default("No description provided.", true) }}
Game Page + {% if world.web.tutorials %} | + Setup Guides + {% endif %} {% if world.web.settings_page is string %} + | Settings Page {% elif world.web.settings_page %} + | Settings Page {% endif %} {% if world.web.bug_report_page %} diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 5e249c19ea..fb5df81c9a 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -11,7 +11,7 @@ from worlds.alttp import Items from WebHostLib import app, cache, Room from Utils import restricted_loads from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name -from MultiServer import get_item_name_from_id, Context +from MultiServer import Context from NetUtils import SlotType alttp_icons = { @@ -987,10 +987,10 @@ def getTracker(tracker: UUID): if game_state == 30: inventory[team][player][106] = 1 # Triforce - player_big_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1) if playernumber not in groups} - player_small_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1) if playernumber not in groups} + player_big_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)} + player_small_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)} for loc_data in locations.values(): - for values in loc_data.values(): + for values in loc_data.values(): item_id, item_player, flags = values if item_id in ids_big_key: @@ -1021,7 +1021,7 @@ def getTracker(tracker: UUID): for (team, player), data in multisave.get("video", []): video[(team, player)] = data - return render_template("tracker.html", inventory=inventory, get_item_name_from_id=get_item_name_from_id, + return render_template("tracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name, lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names, tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=alttp_icons, multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas, diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py index e6b2c7de95..00825df47b 100644 --- a/WebHostLib/upload.py +++ b/WebHostLib/upload.py @@ -80,6 +80,11 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name, player_id=int(slot_id[1:]), game="Ocarina of Time")) + elif file.filename.endswith(".json"): + _, seed_name, slot_id, slot_name = file.filename.split('.')[0].split('-', 3) + slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name, + player_id=int(slot_id[1:]), game="Dark Souls III")) + elif file.filename.endswith(".txt"): spoiler = zfile.open(file, "r").read().decode("utf-8-sig") diff --git a/data/lua/OOT/oot_connector.lua b/data/lua/OOT/oot_connector.lua index 96eee4f78f..a82bcdcb83 100644 --- a/data/lua/OOT/oot_connector.lua +++ b/data/lua/OOT/oot_connector.lua @@ -2,8 +2,8 @@ local socket = require("socket") local json = require('json') local math = require('math') -local last_modified_date = '2022-05-25' -- Should be the last modified date -local script_version = 1 +local last_modified_date = '2022-07-24' -- Should be the last modified date +local script_version = 2 -------------------------------------------------- -- Heavily modified form of RiptideSage's tracker @@ -1723,6 +1723,11 @@ function get_death_state() end function kill_link() + -- market entrance: 27/28/29 + -- outside ToT: 35/36/37. + -- if killed on these scenes the game crashes, so we wait until not on this screen. + local scene = global_context:rawget('cur_scene'):rawget() + if scene == 27 or scene == 28 or scene == 29 or scene == 35 or scene == 36 or scene == 37 then return end mainmemory.write_u16_be(0x11A600, 0) end @@ -1824,13 +1829,15 @@ function main() elseif (curstate == STATE_UNINITIALIZED) then if (frame % 60 == 0) then server:settimeout(2) - print("Attempting to connect") local client, timeout = server:accept() if timeout == nil then print('Initial Connection Made') curstate = STATE_INITIAL_CONNECTION_MADE ootSocket = client ootSocket:settimeout(0) + else + print('Connection failed, ensure OoTClient is running and rerun oot_connector.lua') + return end end end diff --git a/docs/apworld specification.md b/docs/apworld specification.md new file mode 100644 index 0000000000..2dcc3f0bef --- /dev/null +++ b/docs/apworld specification.md @@ -0,0 +1,25 @@ +# apworld Specification + +Archipelago depends on worlds to provide game-specific details like items, locations and output generation. +Those are located in the `worlds/` folder (source) or `/lib/worlds/` (when installed). +See [world api.md](world api.md) for details. + +apworld provides a way to package and ship a world that is not part of the main distribution by placing a `*.apworld` +file into the worlds folder. + + +## File Format + +apworld files are zip archives with the case-sensitive file ending `.apworld`. +The zip has to contain a folder with the same name as the zip, case-sensitive, that contains what would normally be in +the world's folder in `worlds/`. I.e. `worlds/ror2.apworld` containing `ror2/__init__.py`. + + +## Metadata + +No metadata is specified yet. + + +## Extra Data + +The zip can contain arbitrary files in addition what was specified above. diff --git a/docs/network diagram.jpg b/docs/network diagram.jpg deleted file mode 100644 index e738778d26..0000000000 Binary files a/docs/network diagram.jpg and /dev/null differ diff --git a/docs/network diagram.svg b/docs/network diagram.svg deleted file mode 100644 index f8bc7ef46d..0000000000 --- a/docs/network diagram.svg +++ /dev/null @@ -1 +0,0 @@ -
Factorio
Secret of Evermore
WebHost (archipelago.gg)
.NET
Java
Native
SMZ3
Super Metroid
Ocarina of Time
Final Fantasy 1
A Link to the Past
ChecksFinder
Starcraft 2
FNA/XNA
Unity
Minecraft
Secret of Evermore
WebSockets
WebSockets
Integrated
Integrated
Various, depending on SNES device
LuaSockets
Integrated
LuaSockets
Integrated
Integrated
WebSockets
Various, depending on SNES device
Various, depending on SNES device
The Witness Randomizer
Various, depending on SNES device
WebSockets
WebSockets
Mod the Spire
TCP
Forge Mod Loader
WebSockets
TsRandomizer
RogueLegacyRandomizer
BepInEx
QModLoader (BepInEx)
HK Modding API
WebSockets
SQL
Subprocesses
SQL
Deposit Generated Worlds
Provide Generation Instructions
Subprocesses
Subprocesses
RCON
UDP
Integrated
Factorio Server
FactorioClient
Factorio Games
Factorio Mod Generated by AP
Factorio Modding API
SNES
Configurable (waitress, gunicorn, flask)
AutoHoster
PonyORM DB
WebHost
Flask WebContent
AutoGenerator
Mod with Archipelago.MultiClient.Net
Risk of Rain 2
Subnautica
Hollow Knight
Raft
Timespinner
Rogue Legacy
Mod with Archipelago.MultiClient.Java
Slay the Spire
Minecraft Forge Server
Any Java Minecraft Clients
Game using apclientpp Client Library
Game using Apcpp Client Library
Super Mario 64 Ex
VVVVVV
Meritous
The Witness
Sonic Adventure 2: Battle
ap-soeclient
SNES
SNES
OoTClient
Lua Connector
BizHawk with Ocarina of Time Loaded
FF1Client
Lua Connector
BizHawk with Final Fantasy Loaded
SNES
ChecksFinderClient
ChecksFinder
Starcraft 2 Game Client
Starcraft2Client.py
apsc2 Python Package
Archipelago Server
CommonClient.py
Super Nintendo Interface (SNI)
SNIClient
\ No newline at end of file diff --git a/docs/network diagram/network diagram.jpg b/docs/network diagram/network diagram.jpg new file mode 100644 index 0000000000..15495e2724 Binary files /dev/null and b/docs/network diagram/network diagram.jpg differ diff --git a/docs/network diagram/network diagram.svg b/docs/network diagram/network diagram.svg new file mode 100644 index 0000000000..38d3cc0713 --- /dev/null +++ b/docs/network diagram/network diagram.svg @@ -0,0 +1 @@ +
Factorio
Secret of Evermore
WebHost (archipelago.gg)
.NET
Java
Native
Donkey Kong Country 3
Super Metroid/A Link to the Past Combo Randomizer
Super Metroid
Ocarina of Time
Final Fantasy 1
A Link to the Past
ChecksFinder
Starcraft 2
FNA/XNA
Unity
Minecraft
Secret of Evermore
WebSockets
WebSockets
Integrated
Integrated
Various, depending on SNES device
LuaSockets
Integrated
LuaSockets
Integrated
Integrated
WebSockets
Various, depending on SNES device
Various, depending on SNES device
Various, depending on SNES device
The Witness Randomizer
Various, depending on SNES device
WebSockets
WebSockets
Mod the Spire
TCP
Forge Mod Loader
WebSockets
TsRandomizer
RogueLegacyRandomizer
BepInEx
QModLoader (BepInEx)
HK Modding API
WebSockets
SQL
Subprocesses
SQL
Deposit Generated Worlds
Provide Generation Instructions
Subprocesses
Subprocesses
RCON
UDP
Integrated
Factorio Server
FactorioClient
Factorio Games
Factorio Mod Generated by AP
Factorio Modding API
SNES
Configurable (waitress, gunicorn, flask)
AutoHoster
PonyORM DB
WebHost
Flask WebContent
AutoGenerator
Mod with Archipelago.MultiClient.Net
Risk of Rain 2
Subnautica
Hollow Knight
Raft
Timespinner
Rogue Legacy
Mod with Archipelago.MultiClient.Java
Slay the Spire
Minecraft Forge Server
Any Java Minecraft Clients
Game using apclientpp Client Library
Game using Apcpp Client Library
Super Mario 64 Ex
VVVVVV
Meritous
The Witness
Sonic Adventure 2: Battle
Dark Souls 3
ap-soeclient
SNES
SNES
SNES
OoTClient
Lua Connector
BizHawk with Ocarina of Time Loaded
FF1Client
Lua Connector
BizHawk with Final Fantasy Loaded
SNES
ChecksFinderClient
ChecksFinder
Starcraft 2 Game Client
Starcraft2Client.py
apsc2 Python Package
Archipelago Server
CommonClient.py
Super Nintendo Interface (SNI)
SNIClient
\ No newline at end of file diff --git a/docs/running from source.md b/docs/running from source.md new file mode 100644 index 0000000000..4360b28c16 --- /dev/null +++ b/docs/running from source.md @@ -0,0 +1,58 @@ +# Running From Source + +If you just want to play and there is a compiled version available on the +[Archipelago releases page](https://github.com/ArchipelagoMW/Archipelago/releases), +use that version. These steps are for developers or platforms without compiled releases available. + +## General + +What you'll need: + * Python 3.8.7 or newer + * pip (Depending on platform may come included) + * A C compiler + * possibly optional, read OS-specific sections + +Then run any of the starting point scripts, like Generate.py, and the included ModuleUpdater should prompt to install or update the +required modules and after pressing enter proceed to install everything automatically. +After this, you should be able to run the programs. + + +## Windows + +Recommended steps + * Download and install a "Windows installer (64-bit)" from the [Python download page](https://www.python.org/downloads) + * Download and install full Visual Studio from + [Visual Studio Downloads](https://visualstudio.microsoft.com/downloads/) + or an older "Build Tools for Visual Studio" from + [Visual Studio Older Downloads](https://visualstudio.microsoft.com/vs/older-downloads/). + + * Refer to [Windows Compilers on the python wiki](https://wiki.python.org/moin/WindowsCompilers) for details + * This step is optional. Pre-compiled modules are pinned on + [Discord in #archipelago-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808) + + * It is recommended to use [PyCharm IDE](https://www.jetbrains.com/pycharm/) + * Run Generate.py which will prompt installation of missing modules, press enter to confirm + + +## macOS + +Refer to [Guide to Run Archipelago from Source Code on macOS](../worlds/generic/docs/mac_en.md). + + +## Optional: A Link to the Past Enemizer + +Only required to generate seeds that include A Link to the Past with certain options enabled. You will receive an +error if it is required. + +You can get the latest Enemizer release at [Enemizer Github releases](https://github.com/Ijwu/Enemizer/releases). +It should be dropped as "EnemizerCLI" into the root folder of the project. Alternatively, you can point the Enemizer +setting in host.yaml at your Enemizer executable. + + +## Optional: SNI + +SNI is required to use SNIClient. If not integrated into the project, it has to be started manually. + +You can get the latest SNI release at [SNI Github releases](https://github.com/alttpo/sni/releases). +It should be dropped as "SNI" into the root folder of the project. Alternatively, you can point the sni setting in +host.yaml at your SNI folder. diff --git a/docs/sphinx/NetworkProtocol.md b/docs/sphinx/NetworkProtocol.md index ee6a2cc4b8..e0ae39790c 100644 --- a/docs/sphinx/NetworkProtocol.md +++ b/docs/sphinx/NetworkProtocol.md @@ -1,52 +1,35 @@ -# Archipelago Network Protocol +# Archipelago General Client ## Archipelago Connection Handshake These steps should be followed in order to establish a gameplay connection with an Archipelago session. 1. Client establishes WebSocket connection to Archipelago server. -2. Server accepts connection and responds with a [RoomInfo](#roominfo) packet. -3. Client may send a [GetDataPackage](#getdatapackage) packet. -4. Server sends a [DataPackage](#datapackage) packet in return. (If the client sent GetDataPackage.) -5. Client sends [Connect](#connect) packet in order to authenticate with the server. -6. Server validates the client's packet and responds with [Connected](#connected) or -[ConnectionRefused](#connectionrefused). -7. Server may send [ReceivedItems](#receiveditems) to the client, in the case that the client is missing items that -are queued up for it. -8. Server sends [Print](#print) to all players to notify them of the new client connection. +2. Server accepts connection and responds with a [RoomInfo](#RoomInfo) packet. +3. Client may send a [GetDataPackage](#GetDataPackage) packet. +4. Server sends a [DataPackage](#DataPackage) packet in return. (If the client sent GetDataPackage.) +5. Client sends [Connect](#Connect) packet in order to authenticate with the server. +6. Server validates the client's packet and responds with [Connected](#Connected) or [ConnectionRefused](#ConnectionRefused). +7. Server may send [ReceivedItems](#ReceivedItems) to the client, in the case that the client is missing items that are queued up for it. +8. Server sends [Print](#Print) to all players to notify them of the new client connection. -In the case that the client does not authenticate properly and receives a [ConnectionRefused](#connectionrefused) then -the server will maintain the connection and allow for follow-up [Connect](#connect) packet. +In the case that the client does not authenticate properly and receives a [ConnectionRefused](#ConnectionRefused) then the server will maintain the connection and allow for follow-up [Connect](#Connect) packet. -There are libraries available that implement this network protocol in -[Python](https://github.com/ArchipelagoMW/Archipelago/blob/main/CommonClient.py), -[Java](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Java), -[.NET](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Net) and -[C++](https://github.com/black-sliver/apclientpp) +There are libraries available that implement this network protocol in [Python](https://github.com/ArchipelagoMW/Archipelago/blob/main/CommonClient.py), [Java](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Java), [.NET](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Net) and [C++](https://github.com/black-sliver/apclientpp) -For Super Nintendo games there are clients available in -[Python](https://github.com/ArchipelagoMW/Archipelago/blob/main/SNIClient.py). There is also a game specific client for -[Final Fantasy 1](https://github.com/ArchipelagoMW/Archipelago/blob/main/FF1Client.py) +For Super Nintendo games there are clients available in either [Node.js](https://github.com/ArchipelagoMW/SuperNintendoClient) or [Python](https://github.com/ArchipelagoMW/Archipelago/blob/main/SNIClient.py), There are also game specific clients available for [The Legend of Zelda: Ocarina of Time](https://github.com/ArchipelagoMW/Z5Client) or [Final Fantasy 1](https://github.com/ArchipelagoMW/Archipelago/blob/main/FF1Client.py) ## Synchronizing Items -When the client receives a [ReceivedItems](#receiveditems) packet, if the `index` argument does not match the next index -that the client expects then it is expected that the client will re-sync items with the server. This can be accomplished -by sending the server a [Sync](#sync) packet and then a [LocationChecks](#locationchecks) packet. +When the client receives a [ReceivedItems](#ReceivedItems) packet, if the `index` argument does not match the next index that the client expects then it is expected that the client will re-sync items with the server. This can be accomplished by sending the server a [Sync](#Sync) packet and then a [LocationChecks](#LocationChecks) packet. -Even if the client detects a desync, it can still accept the items provided in this packet to prevent gameplay -interruption. +Even if the client detects a desync, it can still accept the items provided in this packet to prevent gameplay interruption. -When the client receives a [ReceivedItems](#receiveditems) packet and the `index` arg is `0` (zero) then the client -should accept the provided `items` list as its full inventory. (Abandon previous inventory.) +When the client receives a [ReceivedItems](#ReceivedItems) packet and the `index` arg is `0` (zero) then the client should accept the provided `items` list as its full inventory. (Abandon previous inventory.) -## Archipelago Protocol Packets -Packets are sent between the multiworld server and client in order to sync information between them. -Below is a directory of each packet. +# Archipelago Protocol Packets +Packets are sent between the multiworld server and client in order to sync information between them. Below is a directory of each packet. -Packets are simple JSON lists in which any number of ordered network commands can be sent, which are objects. -Each command has a "cmd" key, indicating its purpose. All packet argument types documented here refer to JSON types, -unless otherwise specified. +Packets are simple JSON lists in which any number of ordered network commands can be sent, which are objects. Each command has a "cmd" key, indicating its purpose. All packet argument types documented here refer to JSON types, unless otherwise specified. -An object can contain the "class" key, which will tell the content data type, such as "Version" in the following -example. +An object can contain the "class" key, which will tell the content data type, such as "Version" in the following example. Example: ```javascript @@ -55,40 +38,39 @@ Example: ## (Server -> Client) These packets are are sent from the multiworld server to the client. They are not messages which the server accepts. -* [RoomInfo](#roominfo) -* [ConnectionRefused](#connectionrefused) -* [Connected](#connected) -* [ReceivedItems](#receiveditems) -* [LocationInfo](#locationinfo) -* [RoomUpdate](#roomupdate) -* [Print](#print) -* [PrintJSON](#printjson) -* [DataPackage](#datapackage) -* [Bounced](#bounced) -* [InvalidPacket](#invalidpacket) -* [Retrieved](#retrieved) -* [SetReply](#setreply) +* [RoomInfo](#RoomInfo) +* [ConnectionRefused](#ConnectionRefused) +* [Connected](#Connected) +* [ReceivedItems](#ReceivedItems) +* [LocationInfo](#LocationInfo) +* [RoomUpdate](#RoomUpdate) +* [Print](#Print) +* [PrintJSON](#PrintJSON) +* [DataPackage](#DataPackage) +* [Bounced](#Bounced) +* [InvalidPacket](#InvalidPacket) +* [Retrieved](#Retrieved) +* [SetReply](#SetReply) ### RoomInfo Sent to clients when they connect to an Archipelago server. #### Arguments | Name | Type | Notes | | ---- | ---- | ----- | -| version | [NetworkVersion](#networkversion) | Object denoting the version of Archipelago which the server is running. | +| version | [NetworkVersion](#NetworkVersion) | Object denoting the version of Archipelago which the server is running. | | tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. Example: `WebHost` | | password | bool | Denoted whether a password is required to join this room.| -| permissions | dict\[str, [Permission](#permission)\[int\]\] | Mapping of permission name to [Permission](#permission), keys are: "forfeit", "collect" and "remaining". | +| permissions | dict\[str, [Permission](#Permission)\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "forfeit", "collect" and "remaining". | | hint_cost | int | The amount of points it costs to receive a hint from the server. | | location_check_points | int | The amount of hint points you receive per item/location check completed. || | games | list\[str\] | List of games present in this multiworld. | | datapackage_version | int | Sum of individual games' datapackage version. Deprecated. Use `datapackage_versions` instead. | -| datapackage_versions | dict\[str, int\] | Data versions of the individual games' data packages the server will send. Used to decide which games' caches are outdated. See [Data Package Contents](#data-package-contents). | +| datapackage_versions | dict\[str, int\] | Data versions of the individual games' data packages the server will send. Used to decide which games' caches are outdated. See [Data Package Contents](#Data-Package-Contents). | | seed_name | str | uniquely identifying name of this generation | | time | float | Unix time stamp of "now". Send for time synchronization if wanted for things like the DeathLink Bounce. | #### forfeit -Dictates what is allowed when it comes to a player forfeiting their run. A forfeit is an action which distributes the -rest of the items in a player's run to those other players awaiting them. +Dictates what is allowed when it comes to a player forfeiting their run. A forfeit is an action which distributes the rest of the items in a player's run to those other players awaiting them. * `auto`: Distributes a player's items to other players when they complete their goal. * `enabled`: Denotes that players may forfeit at any time in the game. @@ -97,8 +79,7 @@ rest of the items in a player's run to those other players awaiting them. * `goal`: Allows for manual use of forfeit command once a player completes their goal. (Disabled until goal completion) #### collect -Dictates what is allowed when it comes to a player collecting their run. A collect is an action which sends the rest of -the items in a player's run. +Dictates what is allowed when it comes to a player collecting their run. A collect is an action which sends the rest of the items in a player's run. * `auto`: Automatically when they complete their goal. * `enabled`: Denotes that players may !collect at any time in the game. @@ -132,13 +113,13 @@ Sent to clients when the connection handshake is successfully completed. #### Arguments | Name | Type | Notes | | ---- | ---- | ----- | -| team | int | Your team number. See [NetworkPlayer](#networkplayer) for more info on team number. | -| slot | int | Your slot number on your team. See [NetworkPlayer](#networkplayer) for more info on the slot number. | -| players | list\[[NetworkPlayer](#networkplayer)\] | List denoting other players in the multiworld, whether connected or not. | +| team | int | Your team number. See [NetworkPlayer](#NetworkPlayer) for more info on team number. | +| slot | int | Your slot number on your team. See [NetworkPlayer](#NetworkPlayer) for more info on the slot number. | +| players | list\[[NetworkPlayer](#NetworkPlayer)\] | List denoting other players in the multiworld, whether connected or not. | | missing_locations | list\[int\] | Contains ids of remaining locations that need to be checked. Useful for trackers, among other things. | | checked_locations | list\[int\] | Contains ids of all locations that have been checked. Useful for trackers, among other things. Location ids are in the range of ± 253-1. | | slot_data | dict | Contains a json object for slot related data, differs per game. Empty if not required. | -| slot_info | dict\[int, [NetworkSlot](#networkslot)\] | maps each slot to a [NetworkSlot](#networkslot) information | +| slot_info | dict\[int, [NetworkSlot](#NetworkSlot)\] | maps each slot to a [NetworkSlot](#NetworkSlot) information | ### ReceivedItems Sent to clients when they receive an item. @@ -146,25 +127,25 @@ Sent to clients when they receive an item. | Name | Type | Notes | | ---- | ---- | ----- | | index | int | The next empty slot in the list of items for the receiving client. | -| items | list\[[NetworkItem](#networkitem)\] | The items which the client is receiving. | +| items | list\[[NetworkItem](#NetworkItem)\] | The items which the client is receiving. | ### LocationInfo -Sent to clients to acknowledge a received [LocationScouts](#locationscouts) packet and responds with the item in the location(s) being scouted. +Sent to clients to acknowledge a received [LocationScouts](#LocationScouts) packet and responds with the item in the location(s) being scouted. #### Arguments | Name | Type | Notes | | ---- | ---- | ----- | -| locations | list\[[NetworkItem](#networkitem)\] | Contains list of item(s) in the location(s) scouted. | +| locations | list\[[NetworkItem](#NetworkItem)\] | Contains list of item(s) in the location(s) scouted. | ### RoomUpdate Sent when there is a need to update information about the present game session. Generally useful for async games. Once authenticated (received Connected), this may also contain data from Connected. #### Arguments -The arguments for RoomUpdate are identical to [RoomInfo](#roominfo) barring: +The arguments for RoomUpdate are identical to [RoomInfo](#RoomInfo) barring: | Name | Type | Notes | | ---- | ---- | ----- | | hint_points | int | New argument. The client's current hint points. | -| players | list\[[NetworkPlayer](#networkplayer)\] | Send in the event of an alias rename. Always sends all players, whether connected or not. | +| players | list\[[NetworkPlayer](#NetworkPlayer)\] | Send in the event of an alias rename. Always sends all players, whether connected or not. | | checked_locations | list\[int\] | May be a partial update, containing new locations that were checked, especially from a coop partner in the same slot. | | missing_locations | list\[int\] | Should never be sent as an update, if needed is the inverse of checked_locations. | @@ -178,56 +159,64 @@ Sent to clients purely to display a message to the player. | text | str | Message to display to player. | ### PrintJSON -Sent to clients purely to display a message to the player. This packet differs from [Print](#print) in that the data -being sent with this packet allows for more configurable or specific messaging. +Sent to clients purely to display a message to the player. This packet differs from [Print](#Print) in that the data being sent with this packet allows for more configurable or specific messaging. #### Arguments | Name | Type | Notes | | ---- | ---- | ----- | -| data | list\[[JSONMessagePart](#jsonmessagepart)\] | Type of this part of the message. | +| data | list\[[JSONMessagePart](#JSONMessagePart)\] | Type of this part of the message. | | type | str | May be present to indicate the nature of this message. Known types are Hint and ItemSend. | | receiving | int | Is present if type is Hint or ItemSend and marks the destination player's ID. | -| item | [NetworkItem](#networkitem) | Is present if type is Hint or ItemSend and marks the source player id, location id, item id and item flags. | +| item | [NetworkItem](#NetworkItem) | Is present if type is Hint or ItemSend and marks the source player id, location id, item id and item flags. | | found | bool | Is present if type is Hint, denotes whether the location hinted for was checked. | ### DataPackage -Sent to clients to provide what is known as a 'data package' which contains information to enable a client to most -easily communicate with the Archipelago server. Contents include things like location id to name mappings, -among others; see [Data Package Contents](#data-package-contents) for more info. +Sent to clients to provide what is known as a 'data package' which contains information to enable a client to most easily communicate with the Archipelago server. Contents include things like location id to name mappings, among others; see [Data Package Contents](#Data-Package-Contents) for more info. #### Arguments | Name | Type | Notes | | ---- | ---- | ----- | -| data | [DataPackageObject](#data-package-contents) | The data package as a JSON object. | +| data | [DataPackageObject](#Data-Package-Contents) | The data package as a JSON object. | ### Bounced -Sent to clients after a client requested this message be sent to them, more info in the [Bounce](#bounce) package. +Sent to clients after a client requested this message be sent to them, more info in the [Bounce](#Bounce) package. #### Arguments | Name | Type | Notes | | ---- | ---- | ----- | | games | list\[str\] | Optional. Game names this message is targeting | | slots | list\[int\] | Optional. Player slot IDs that this message is targeting | -| tags | list\[str\] | Optional. Client [Tags](#tags) this message is targeting | -| data | dict | The data in the [Bounce](#bounce) package copied | +| tags | list\[str\] | Optional. Client [Tags](#Tags) this message is targeting | +| data | dict | The data in the [Bounce](#Bounce) package copied | ### InvalidPacket -Sent to clients if the server caught a problem with a packet. This only occurs for errors that are explicitly checked -for. +Sent to clients if the server caught a problem with a packet. This only occurs for errors that are explicitly checked for. -### Retrieved -Sent to clients as a response the a [Get](#get) package #### Arguments | Name | Type | Notes | | ---- | ---- | ----- | -| keys | dict\[str\, any] | A key-value collection containing all the values for the keys requested in the [Get](#get) package. | +| type | str | The [PacketProblemType](#PacketProblemType) that was detected in the packet. | +| original_cmd | Optional[str] | The `cmd` argument of the faulty packet, will be `None` if the `cmd` failed to be parsed. | +| text | str | A descriptive message of the problem at hand. | -Additional arguments added to the [Get](#get) package that triggered this [Retrieved](#retrieved) will also be passed -along. +##### PacketProblemType +`PacketProblemType` indicates the type of problem that was detected in the faulty packet, the known problem types are below but others may be added in the future. + +| Type | Notes | +| ---- | ----- | +| cmd | `cmd` argument of the faulty packet that could not be parsed correctly. | +| arguments | Arguments of the faulty packet which were not correct. | + +### Retrieved +Sent to clients as a response the a [Get](#Get) package. +#### Arguments +| Name | Type | Notes | +| ---- | ---- | ----- | +| keys | dict\[str\, any] | A key-value collection containing all the values for the keys requested in the [Get](#Get) package. | + +Additional arguments added to the [Get](#Get) package that triggered this [Retrieved](#Retrieved) will also be passed along. ### SetReply -Sent to clients in response to a [Set](#set) package if want_reply was set to true, or if the client has registered to -receive updates for a certain key using the [SetNotify](#setnotify) package. SetReply packages are sent even if a -[Set](#set) package did not alter the value for the key. +Sent to clients in response to a [Set](#Set) package if want_reply was set to true, or if the client has registered to receive updates for a certain key using the [SetNotify](#SetNotify) package. SetReply packages are sent even if a [Set](#Set) package did not alter the value for the key. #### Arguments | Name | Type | Notes | | ---- | ---- | ----- | @@ -235,23 +224,22 @@ receive updates for a certain key using the [SetNotify](#setnotify) package. Set | value | any | The new value for the key. | | original_value | any | The value the key had before it was updated. | -Additional arguments added to the [Set](#set) package that triggered this [SetReply](#setreply) will also be passed -along. +Additional arguments added to the [Set](#Set) package that triggered this [SetReply](#SetReply) will also be passed along. ## (Client -> Server) These packets are sent purely from client to server. They are not accepted by clients. -* [Connect](#connect) -* [Sync](#sync) -* [LocationChecks](#locationchecks) -* [LocationScouts](#locationscouts) -* [StatusUpdate](#statusupdate) -* [Say](#say) -* [GetDataPackage](#getdatapackage) -* [Bounce](#bounce) -* [Get](#get) -* [Set](#set) -* [SetNotify](#setnotify) +* [Connect](#Connect) +* [Sync](#Sync) +* [LocationChecks](#LocationChecks) +* [LocationScouts](#LocationScouts) +* [StatusUpdate](#StatusUpdate) +* [Say](#Say) +* [GetDataPackage](#GetDataPackage) +* [Bounce](#Bounce) +* [Get](#Get) +* [Set](#Set) +* [SetNotify](#SetNotify) ### Connect Sent by the client to initiate a connection to an Archipelago game session. @@ -263,9 +251,9 @@ Sent by the client to initiate a connection to an Archipelago game session. | game | str | The name of the game the client is playing. Example: `A Link to the Past` | | name | str | The player name for this client. | | uuid | str | Unique identifier for player client. | -| version | [NetworkVersion](#networkversion) | An object representing the Archipelago version this client supports. | +| version | [NetworkVersion](#NetworkVersion) | An object representing the Archipelago version this client supports. | | items_handling | int | Flags configuring which items should be sent by the server. Read below for individual flags. | -| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#tags) | +| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#Tags) | #### items_handling flags | Value | Meaning | @@ -277,8 +265,7 @@ Sent by the client to initiate a connection to an Archipelago game session. | null | Null or undefined loads settings from world definition for backwards compatibility. This is deprecated. | #### Authentication -Many, if not all, other packets require a successfully authenticated client. This is described in more detail in -[Archipelago Connection Handshake](#archipelago-connection-handshake). +Many, if not all, other packets require a successfully authenticated client. This is described in more detail in [Archipelago Connection Handshake](#Archipelago-Connection-Handshake). ### ConnectUpdate Update arguments from the Connect package, currently only updating tags and items_handling is supported. @@ -287,16 +274,15 @@ Update arguments from the Connect package, currently only updating tags and item | Name | Type | Notes | | ---- | ---- | ----- | | items_handling | int | Flags configuring which items should be sent by the server. | -| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#tags) | +| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#Tags) | ### Sync -Sent to server to request a [ReceivedItems](#receiveditems) packet to synchronize items. +Sent to server to request a [ReceivedItems](#ReceivedItems) packet to synchronize items. #### Arguments No arguments necessary. ### LocationChecks -Sent to server to inform it of locations that the client has checked. Used to inform the server of new checks that are -made, as well as to sync state. +Sent to server to inform it of locations that the client has checked. Used to inform the server of new checks that are made, as well as to sync state. #### Arguments | Name | Type | Notes | @@ -304,15 +290,13 @@ made, as well as to sync state. | locations | list\[int\] | The ids of the locations checked by the client. May contain any number of checks, even ones sent before; duplicates do not cause issues with the Archipelago server. | ### LocationScouts -Sent to the server to inform it of locations the client has seen, but not checked. Useful in cases in which the item may -appear in the game world, such as 'ledge items' in A Link to the Past. The server will always respond with a -[LocationInfo](#locationinfo) packet with the items located in the scouted location. +Sent to the server to inform it of locations the client has seen, but not checked. Useful in cases in which the item may appear in the game world, such as 'ledge items' in A Link to the Past. The server will always respond with a [LocationInfo](#LocationInfo) packet with the items located in the scouted location. #### Arguments | Name | Type | Notes | | ---- | ---- | ----- | | locations | list\[int\] | The ids of the locations seen by the client. May contain any number of locations, even ones sent before; duplicates do not cause issues with the Archipelago server. | -| create_as_hint | int | If non-zero, the scouted locations get created and broadcast as a player-visible hint.
If 2 only new hints are broadcast, however this does not remove them from the LocationInfo reply. | +| create_as_hint | int | If non-zero, the scouted locations get created and broadcasted as a player-visible hint.
If 2 only new hints are broadcast, however this does not remove them from the LocationInfo reply. | ### StatusUpdate Sent to the server to update on the sender's status. Examples include readiness or goal completion. (Example: defeated Ganon in A Link to the Past) @@ -320,7 +304,7 @@ Sent to the server to update on the sender's status. Examples include readiness #### Arguments | Name | Type | Notes | | ---- | ---- | ----- | -| status | ClientStatus\[int\] | One of [Client States](#client-states). Send as int. Follow the link for more information. | +| status | ClientStatus\[int\] | One of [Client States](#Client-States). Send as int. Follow the link for more information. | ### Say Basic chat command which sends text to the server to be distributed to other clients. @@ -351,36 +335,29 @@ the server will forward the message to all those targets to which any one requir | data | dict | Any data you want to send | ### Get -Used to request a single or multiple values from the server's data storage, see the [Set](#set) package for how to write -values to the data storage. A Get package will be answered with a [Retrieved](#retrieved) package. +Used to request a single or multiple values from the server's data storage, see the [Set](#Set) package for how to write values to the data storage. A Get package will be answered with a [Retrieved](#Retrieved) package. #### Arguments | Name | Type | Notes | | ------ | ----- | ------ | | keys | list\[str\] | Keys to retrieve the values for. | -Additional arguments sent in this package will also be added to the [Retrieved](#retrieved) package it triggers. +Additional arguments sent in this package will also be added to the [Retrieved](#Retrieved) package it triggers. ### Set -Used to write data to the server's data storage, that data can then be shared across worlds or just saved for later. -Values for keys in the data storage can be retrieved with a [Get](#get) package, or monitored with a -[SetNotify](#setnotify) package. +Used to write data to the server's data storage, that data can then be shared across worlds or just saved for later. Values for keys in the data storage can be retrieved with a [Get](#Get) package, or monitored with a [SetNotify](#SetNotify) package. #### Arguments | Name | Type | Notes | | ------ | ----- | ------ | | key | str | The key to manipulate. | | default | any | The default value to use in case the key has no value on the server. | -| want_reply | bool | If set, the server will send a [SetReply](#setreply) response back to the client. | -| operations | list\[[DataStorageOperation](#datastorageoperation)\] | Operations to apply to the value, multiple operations can be present and they will be executed in order of appearance. | +| want_reply | bool | If set, the server will send a [SetReply](#SetReply) response back to the client. | +| operations | list\[[DataStorageOperation](#DataStorageOperation)\] | Operations to apply to the value, multiple operations can be present and they will be executed in order of appearance. | -Additional arguments sent in this package will also be added to the [SetReply](#setreply) package it triggers. +Additional arguments sent in this package will also be added to the [SetReply](#SetReply) package it triggers. #### DataStorageOperation -A DataStorageOperation manipulates or alters the value of a key in the data storage. If the operation transforms the -value from one state to another then the current value of the key is used as the starting point otherwise the -[Set](#set)'s package `default` is used if the key does not exist on the server already. - -DataStorageOperations consist of an object containing both the operation to be applied, provided in the form of a -string, as well as the value to be used for that operation, Example: +A DataStorageOperation manipulates or alters the value of a key in the data storage. If the operation transforms the value from one state to another then the current value of the key is used as the starting point otherwise the [Set](#Set)'s package `default` is used if the key does not exist on the server already. +DataStorageOperations consist of an object containing both the operation to be applied, provided in the form of a string, as well as the value to be used for that operation, Example: ```json {"operation": "add", "value": 12} ``` @@ -389,7 +366,7 @@ The following operations can be applied to a datastorage key | Operation | Effect | | ------ | ----- | | replace | Sets the current value of the key to `value`. | -| default | If the key has no value yet, sets the current value of the key to `default` of the [Set](#set)'s package (`value` is ignored). | +| default | If the key has no value yet, sets the current value of the key to `default` of the [Set](#Set)'s package (`value` is ignored). | | add | Adds `value` to the current value of the key, if both the current value and `value` are arrays then `value` will be appended to the current value. | | mul | Multiplies the current value of the key by `value`. | | pow | Multiplies the current value of the key to the power of `value`. | @@ -403,35 +380,28 @@ The following operations can be applied to a datastorage key | right_shift | Applies a bitwise right-shift to the current value of the key by `value`. | ### SetNotify -Used to register your current session for receiving all [SetReply](#setreply) packages of certain keys to allow your client to keep track of changes. +Used to register your current session for receiving all [SetReply](#SetReply) packages of certain keys to allow your client to keep track of changes. #### Arguments | Name | Type | Notes | | ------ | ----- | ------ | -| keys | list\[str\] | Keys to receive all [SetReply](#setreply) packages for. | +| keys | list\[str\] | Keys to receive all [SetReply](#SetReply) packages for. | ## Appendix ### Coop -Coop in Archipelago is automatically facilitated by the server, however some default behaviour may not be what you -desire. +Coop in Archipelago is automatically facilitated by the server, however some of the default behaviour may not be what you desire. If the game in question is a remote-items game (attribute on AutoWorld), then all items will always be sent and received. -If the game in question is not a remote-items game, then any items that are placed within the same world will not be -sent by the server. +If the game in question is not a remote-items game, then any items that are placed within the same world will not be sent by the server. -To manually react to others in the same player slot doing checks, listen to [RoomUpdate](#roomupdate) -> -checked_locations. +To manually react to others in the same player slot doing checks, listen to [RoomUpdate](#RoomUpdate) -> checked_locations. ### NetworkPlayer -A list of objects. Each object denotes one player. Each object has four fields about the player, in this order: `team`, -`slot`, `alias`, and `name`. `team` and `slot` are ints, `alias` and `name` are strings. +A list of objects. Each object denotes one player. Each object has four fields about the player, in this order: `team`, `slot`, `alias`, and `name`. `team` and `slot` are ints, `alias` and `name` are stringss. -Each player belongs to a `team` and has a `slot`. Team numbers start at `0`. Slot numbers are unique per team and start -at `1`. Slot number `0` refers to the Archipelago server; this may appear in instances where the server grants the -player an item. +Each player belongs to a `team` and has a `slot`. Team numbers start at `0`. Slot numbers are unique per team and start at `1`. Slot number `0` refers to the Archipelago server; this may appear in instances where the server grants the player an item. -`alias` represents the player's name in current time. `name` is the original name used when the session was generated. -This is typically distinct in games which require baking names into ROMs or for async games. +`alias` represents the player's name in current time. `name` is the original name used when the session was generated. This is typically distinct in games which require baking names into ROMs or for async games. ```python from typing import NamedTuple @@ -474,8 +444,7 @@ In JSON this may look like: `location` is the location id of the item inside the world. Location ids are in the range of ± 253-1. -`player` is the player slot of the world the item is located in, except when inside an [LocationInfo](#locationinfo) -Packet then it will be the slot of the player to receive the item. +`player` is the player slot of the world the item is located in, except when inside an [LocationInfo](#LocationInfo) Packet then it will be the slot of the player to receive the item `flags` are bit flags: | Flag | Meaning | @@ -486,8 +455,7 @@ Packet then it will be the slot of the player to receive the item. | 0b100 | If set, indicates the item is a trap | ### JSONMessagePart -Message nodes sent along with [PrintJSON](#printjson) packet to be reconstructed into a legible message. -The nodes are intended to be read in the order they are listed in the packet. +Message nodes sent along with [PrintJSON](#PrintJSON) packet to be reconstructed into a legible message. The nodes are intended to be read in the order they are listed in the packet. ```python from typing import TypedDict, Optional @@ -499,10 +467,7 @@ class JSONMessagePart(TypedDict): player: Optional[int] # only available if type is either item or location ``` -`type` is used to denote the intent of the message part. This can be used to indicate special information which may be -rendered differently depending on client. How these types are displayed in Archipelago's ALttP client is not the end-all -be-all. Other clients may choose to interpret and display these messages differently. - +`type` is used to denote the intent of the message part. This can be used to indicate special information which may be rendered differently depending on client. How these types are displayed in Archipelago's ALttP client is not the end-all be-all. Other clients may choose to interpret and display these messages differently. Possible values for `type` include: | Name | Notes | @@ -518,10 +483,7 @@ Possible values for `type` include: | color | Regular text that should be colored. Only `type` that will contain `color` data. | -`color` is used to denote a console color to display the message part with and is only send if the `type` is `color`. -This is limited to console colors due to backwards compatibility needs with games such as ALttP. -Although background colors as well as foreground colors are listed, only one may be applied to a -[JSONMessagePart](#jsonmessagepart) at a time. +`color` is used to denote a console color to display the message part with and is only send if the `type` is `color`. This is limited to console colors due to backwards compatibility needs with games such as ALttP. Although background colors as well as foreground colors are listed, only one may be applied to a [JSONMessagePart](#JSONMessagePart) at a time. Color options: * bold @@ -539,17 +501,16 @@ Color options: * green_bg * yellow_bg * blue_bg -* purple_bg +* magenta_bg * cyan_bg * white_bg `text` is the content of the message part to be displayed. `player` marks owning player id for location/item, -`flags` contains the [NetworkItem](#networkitem) flags that belong to the item +`flags` contains the [NetworkItem](#NetworkItem) flags that belong to the item ### Client States -An enumeration containing the possible client states that may be used to inform the server in -[StatusUpdate](#statusupdate). +An enumeration containing the possible client states that may be used to inform the server in [StatusUpdate](#StatusUpdate). ```python import enum @@ -561,8 +522,7 @@ class ClientStatus(enum.IntEnum): ``` ### NetworkVersion -An object representing software versioning. Used in the [Connect](#connect) packet to allow the client to inform the -server of the Archipelago version it supports. +An object representing software versioning. Used in the [Connect](#Connect) packet to allow the client to inform the server of the Archipelago version it supports. ```python from typing import NamedTuple class Version(NamedTuple): @@ -608,14 +568,9 @@ class Permission(enum.IntEnum): ``` ### Data Package Contents -A data package is a JSON object which may contain arbitrary metadata to enable a client to interact with the Archipelago -server most easily. Currently, this package is used to send ID to name mappings so that clients need not maintain their -own mappings. +A data package is a JSON object which may contain arbitrary metadata to enable a client to interact with the Archipelago server most easily. Currently, this package is used to send ID to name mappings so that clients need not maintain their own mappings. -We encourage clients to cache the data package they receive on disk, or otherwise not tied to a session. -You will know when your cache is outdated if the [RoomInfo](#roominfo) packet or the datapackage itself denote a -different version. A special case is datapackage version 0, where it is expected the package is custom and should not be -cached. +We encourage clients to cache the data package they receive on disk, or otherwise not tied to a session. You will know when your cache is outdated if the [RoomInfo](#RoomInfo) packet or the datapackage itself denote a different version. A special case is datapackage version 0, where it is expected the package is custom and should not be cached. Note: * Any ID is unique to its type across AP: Item 56 only exists once and Location 56 only exists once. @@ -643,7 +598,7 @@ Tags are represented as a list of strings, the common Client tags follow: | Name | Notes | |------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | AP | Signifies that this client is a reference client, its usefulness is mostly in debugging to compare client behaviours more easily. | -| IgnoreGame | Deprecated. See Tracker and TextOnly. Tells the server to ignore the "game" attribute in the [Connect](#connect) packet. | +| IgnoreGame | Deprecated. See Tracker and TextOnly. Tells the server to ignore the "game" attribute in the [Connect](#Connect) packet. | | DeathLink | Client participates in the DeathLink mechanic, therefore will send and receive DeathLink bounce packets | | Tracker | Tells the server that this client will not send locations and is actually a Tracker. When specified and used with empty or null `game` in [Connect](#connect), game and game's version validation will be skipped. | | TextOnly | Tells the server that this client will not send locations and is intended for chat. When specified and used with empty or null `game` in [Connect](#connect), game and game's version validation will be skipped. | diff --git a/docs/sphinx/WorldAPI.md b/docs/sphinx/WorldAPI.md index eb025ff483..9cb6d04c3b 100644 --- a/docs/sphinx/WorldAPI.md +++ b/docs/sphinx/WorldAPI.md @@ -79,7 +79,7 @@ inside a World object. Players provide customized settings for their World in the form of yamls. Those are accessible through `self.world.[self.player]`. A dict -of valid options has to be provided in `self.options`. Options are automatically +of valid options has to be provided in `self.option_definitions`. Options are automatically added to the `World` object for easy access. ### World Options @@ -229,7 +229,7 @@ class MyGameLocation(Location): game: str = "My Game" # override constructor to automatically mark event locations as such - def __init__(self, player: int, name = '', code = None, parent = None): + def __init__(self, player: int, name = "", code = None, parent = None): super(MyGameLocation, self).__init__(player, name, code, parent) self.event = code is None ``` @@ -245,7 +245,7 @@ to describe it and a `display_name` property for display on the website and in spoiler logs. The actual name as used in the yaml is defined in a `dict[str, Option]`, that is -assigned to the world under `self.options`. +assigned to the world under `self.option_definitions`. Common option types are `Toggle`, `DefaultOnToggle`, `Choice`, `Range`. For more see `Options.py` in AP's base directory. @@ -321,7 +321,7 @@ from .Options import mygame_options # import the options dict class MyGameWorld(World): #... - options = mygame_options # assign the options dict to the world + option_definitions = mygame_options # assign the options dict to the world #... ``` @@ -358,7 +358,7 @@ class MyGameLocation(Location): # or from Locations import MyGameLocation class MyGameWorld(World): """Insert description of the world/game here.""" game: str = "My Game" # name of the game/world - options = mygame_options # options the player can set + option_definitions = mygame_options # options the player can set topology_present: bool = True # show path to required location checks in spoiler remote_items: bool = False # True if all items come from the server remote_start_inventory: bool = False # True if start inventory comes from the server @@ -480,14 +480,14 @@ def create_items(self) -> None: for item in map(self.create_item, mygame_items): if item in exclude: exclude.remove(item) # this is destructive. create unique list above - self.world.itempool.append(self.create_item('nothing')) + self.world.itempool.append(self.create_item("nothing")) else: self.world.itempool.append(item) # itempool and number of locations should match up. # If this is not the case we want to fill the itempool with junk. junk = 0 # calculate this based on player settings - self.world.itempool += [self.create_item('nothing') for _ in range(junk)] + self.world.itempool += [self.create_item("nothing") for _ in range(junk)] ``` #### create_regions @@ -621,7 +621,7 @@ class MyGameLogic(LogicMixin): def _mygame_has_key(self, world: MultiWorld, player: int): # Arguments above are free to choose # it may make sense to use World as argument instead of MultiWorld - return self.has('key', player) # or whatever + return self.has("key", player) # or whatever ``` ```python # __init__.py diff --git a/docs/style.md b/docs/style.md new file mode 100644 index 0000000000..a9f55caa7c --- /dev/null +++ b/docs/style.md @@ -0,0 +1,49 @@ +# Style Guide + +## Generic + +* This guide can be ignored for data files that are not to be viewed in an editor. +* 120 character per line for all source files. +* Avoid white space errors like trailing spaces. + + +## Python Code + +* We mostly follow [PEP8](https://peps.python.org/pep-0008/). Read below to see the differences. +* 120 characters per line. PyCharm does this automatically, other editors can be configured for it. +* Strings in core code will be `"strings"`. In other words: double quote your strings. +* Strings in worlds should use double quotes as well, but imported code may differ. +* Prefer [format string literals](https://peps.python.org/pep-0498/) over string concatenation, + use single quotes inside them: `f"Like {dct['key']}"` +* Use type annotation where possible. + + +## Markdown + +* We almost follow [Google's styleguide](https://google.github.io/styleguide/docguide/style.html). + Read below for differences. +* For existing documents, try to follow its style or ask to completely reformat it. +* 120 characters per line. +* One space between bullet/number and text. +* No lazy numbering. + + +## HTML + +* Indent with 2 spaces for new code. +* kebab-case for ids and classes. + + +## CSS + +* Indent with 2 spaces for new code. +* `{` on the same line as the selector. +* No space between selector and `{`. + + +## JS + +* Indent with 2 spaces. +* Indent `case` inside `switch ` with 2 spaces. +* Use single quotes. +* Semicolons are required after every statement. diff --git a/host.yaml b/host.yaml index af16a8258e..901e6cd727 100644 --- a/host.yaml +++ b/host.yaml @@ -101,7 +101,9 @@ sm_options: # Alternatively, a path to a program to open the .sfc file with rom_start: true factorio_options: - executable: "factorio\\bin\\x64\\factorio" + executable: "factorio/bin/x64/factorio" + # by default, no settings are loaded if this file does not exist. If this file does exist, then it will be used. + # server_settings: "factorio\\data\\server-settings.json" minecraft_options: forge_directory: "Minecraft Forge server" max_heap_size: "2G" @@ -127,3 +129,12 @@ smz3_options: # True for operating system default program # Alternatively, a path to a program to open the .sfc file with rom_start: true +dkc3_options: + # File name of the DKC3 US rom + rom_file: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc" + # Set this to your SNI folder location if you want the MultiClient to attempt an auto start, does nothing if not found + sni: "SNI" + # Set this to false to never autostart a rom (such as after patching) + # True for operating system default program + # Alternatively, a path to a program to open the .sfc file with + rom_start: true diff --git a/inno_setup.iss b/inno_setup.iss index 1dee01af18..ff2da1211a 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -54,6 +54,7 @@ Name: "custom"; Description: "Custom installation"; Flags: iscustom Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed Name: "generator"; Description: "Generator"; Types: full hosting Name: "generator/sm"; Description: "Super Metroid ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning +Name: "generator/dkc3"; Description: "Donkey Kong Country 3 ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680 Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296; Flags: disablenouninstallwarning @@ -62,6 +63,7 @@ Name: "client"; Description: "Clients"; Types: full playing Name: "client/sni"; Description: "SNI Client"; Types: full playing Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Setup"; Types: full playing; Flags: disablenouninstallwarning Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing; Flags: disablenouninstallwarning +Name: "client/sni/dkc3"; Description: "SNI Client - Donkey Kong Country 3 Patch Setup"; Types: full playing; Flags: disablenouninstallwarning Name: "client/factorio"; Description: "Factorio"; Types: full playing Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278 Name: "client/oot"; Description: "Ocarina of Time"; Types: full playing @@ -76,6 +78,7 @@ NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-mod [Files] Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external; Components: client/sni/lttp or generator/lttp Source: "{code:GetSMROMPath}"; DestDir: "{app}"; DestName: "Super Metroid (JU).sfc"; Flags: external; Components: client/sni/sm or generator/sm +Source: "{code:GetDKC3ROMPath}"; DestDir: "{app}"; DestName: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"; Flags: external; Components: client/sni/dkc3 or generator/dkc3 Source: "{code:GetSoEROMPath}"; DestDir: "{app}"; DestName: "Secret of Evermore (USA).sfc"; Flags: external; Components: generator/soe Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: client/oot or generator/oot Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs @@ -129,6 +132,7 @@ Type: dirifempty; Name: "{app}" [InstallDelete] Type: files; Name: "{app}\ArchipelagoLttPClient.exe" +Type: filesandordirs; Name: "{app}\lib\worlds\rogue-legacy*" [Registry] @@ -142,6 +146,11 @@ Root: HKCR; Subkey: "{#MyAppName}smpatch"; ValueData: "Archi Root: HKCR; Subkey: "{#MyAppName}smpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni Root: HKCR; Subkey: "{#MyAppName}smpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: ".apdkc3"; ValueData: "{#MyAppName}dkc3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}dkc3patch"; ValueData: "Archipelago Donkey Kong Country 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}dkc3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}dkc3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni + Root: HKCR; Subkey: ".apsmz3"; ValueData: "{#MyAppName}smz3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni Root: HKCR; Subkey: "{#MyAppName}smz3patch"; ValueData: "Archipelago SMZ3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni Root: HKCR; Subkey: "{#MyAppName}smz3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni @@ -205,6 +214,9 @@ var LttPROMFilePage: TInputFileWizardPage; var smrom: string; var SMRomFilePage: TInputFileWizardPage; +var dkc3rom: string; +var DKC3RomFilePage: TInputFileWizardPage; + var soerom: string; var SoERomFilePage: TInputFileWizardPage; @@ -294,6 +306,8 @@ begin Result := not (LttPROMFilePage.Values[0] = '') else if (assigned(SMROMFilePage)) and (CurPageID = SMROMFilePage.ID) then Result := not (SMROMFilePage.Values[0] = '') + else if (assigned(DKC3ROMFilePage)) and (CurPageID = DKC3ROMFilePage.ID) then + Result := not (DKC3ROMFilePage.Values[0] = '') else if (assigned(SoEROMFilePage)) and (CurPageID = SoEROMFilePage.ID) then Result := not (SoEROMFilePage.Values[0] = '') else if (assigned(OoTROMFilePage)) and (CurPageID = OoTROMFilePage.ID) then @@ -334,6 +348,22 @@ begin Result := ''; end; +function GetDKC3ROMPath(Param: string): string; +begin + if Length(dkc3rom) > 0 then + Result := dkc3rom + else if Assigned(DKC3RomFilePage) then + begin + R := CompareStr(GetSNESMD5OfFile(DKC3ROMFilePage.Values[0]), '120abf304f0c40fe059f6a192ed4f947') + if R <> 0 then + MsgBox('Donkey Kong Country 3 ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); + + Result := DKC3ROMFilePage.Values[0] + end + else + Result := ''; + end; + function GetSoEROMPath(Param: string): string; begin if Length(soerom) > 0 then @@ -378,6 +408,10 @@ begin if Length(smrom) = 0 then SMRomFilePage:= AddRomPage('Super Metroid (JU).sfc'); + dkc3rom := CheckRom('Donkey Kong Country 3 - Dixie Kong''s Double Trouble! (USA) (En,Fr).sfc', '120abf304f0c40fe059f6a192ed4f947'); + if Length(dkc3rom) = 0 then + DKC3RomFilePage:= AddRomPage('Donkey Kong Country 3 - Dixie Kong''s Double Trouble! (USA) (En,Fr).sfc'); + soerom := CheckRom('Secret of Evermore (USA).sfc', '6e9c94511d04fac6e0a1e582c170be3a'); if Length(soerom) = 0 then SoEROMFilePage:= AddRomPage('Secret of Evermore (USA).sfc'); @@ -391,6 +425,8 @@ begin Result := not (WizardIsComponentSelected('client/sni/lttp') or WizardIsComponentSelected('generator/lttp')); if (assigned(SMROMFilePage)) and (PageID = SMROMFilePage.ID) then Result := not (WizardIsComponentSelected('client/sni/sm') or WizardIsComponentSelected('generator/sm')); + if (assigned(DKC3ROMFilePage)) and (PageID = DKC3ROMFilePage.ID) then + Result := not (WizardIsComponentSelected('client/sni/dkc3') or WizardIsComponentSelected('generator/dkc3')); if (assigned(SoEROMFilePage)) and (PageID = SoEROMFilePage.ID) then Result := not (WizardIsComponentSelected('generator/soe')); if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then diff --git a/playerSettings.yaml b/playerSettings.yaml index ff3596a77a..4ebae9e6d7 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -175,12 +175,15 @@ A Link to the Past: retro_caves: on: 0 # Zelda-1 like mode. There are randomly placed take-any caves that contain one Sword and choices of Heart Container/Blue Potion. off: 50 - hints: # Vendors: King Zora and Bottle Merchant say what they're selling. - # On/Full: Put item and entrance placement hints on telepathic tiles and some NPCs, Full removes joke hints. + hints: # On/Full: Put item and entrance placement hints on telepathic tiles and some NPCs, Full removes joke hints. 'on': 50 - vendors: 0 'off': 0 full: 0 + scams: # If on, these Merchants will no longer tell you what they're selling. + 'off': 50 + 'king_zora': 0 + 'bottle_merchant': 0 + 'all': 0 swordless: on: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change off: 1 @@ -273,6 +276,7 @@ A Link to the Past: p: 0 # Randomize the prices of the items in shop inventories u: 0 # Shuffle capacity upgrades into the item pool (and allow them to traverse the multiworld) w: 0 # Consider witch's hut like any other shop and shuffle/randomize it too + P: 0 # Prices of the items in shop inventories cost hearts, arrow, or bombs instead of rupees ip: 0 # Shuffle inventories and randomize prices fpu: 0 # Generate new inventories, randomize prices and shuffle capacity upgrades into item pool uip: 0 # Shuffle inventories, randomize prices and shuffle capacity upgrades into the item pool diff --git a/requirements.txt b/requirements.txt index 0067d461d7..661209e072 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -colorama>=0.4.4 +colorama>=0.4.5 websockets>=10.3 PyYAML>=6.0 jellyfish>=0.9.0 diff --git a/test/dungeons/TestDungeon.py b/test/dungeons/TestDungeon.py index c44c090f6f..0568e799f2 100644 --- a/test/dungeons/TestDungeon.py +++ b/test/dungeons/TestDungeon.py @@ -16,7 +16,7 @@ class TestDungeon(unittest.TestCase): def setUp(self): self.world = MultiWorld(1) args = Namespace() - for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): + for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items(): setattr(args, name, {1: option.from_any(option.default)}) self.world.set_options(args) self.world.set_default_common_options() diff --git a/test/general/TestFill.py b/test/general/TestFill.py index 5cec895221..189aafafb2 100644 --- a/test/general/TestFill.py +++ b/test/general/TestFill.py @@ -49,8 +49,7 @@ class PlayerDefinition(object): region_name = "player" + str(self.id) + region_tag region = Region("player" + str(self.id) + region_tag, RegionType.Generic, "Region Hint", self.id, self.world) - self.locations += generate_locations(size, - self.id, None, region, region_tag) + self.locations += generate_locations(size, self.id, None, region, region_tag) entrance = Entrance(self.id, region_name + "_entrance", parent) parent.exits.append(entrance) diff --git a/test/general/__init__.py b/test/general/__init__.py index 8b966c0e34..479f4af520 100644 --- a/test/general/__init__.py +++ b/test/general/__init__.py @@ -12,7 +12,7 @@ def setup_default_world(world_type) -> MultiWorld: world.player_name = {1: "Tester"} world.set_seed() args = Namespace() - for name, option in world_type.options.items(): + for name, option in world_type.option_definitions.items(): setattr(args, name, {1: option.from_any(option.default)}) world.set_options(args) world.set_default_common_options() diff --git a/test/inverted/TestInverted.py b/test/inverted/TestInverted.py index 586eb57907..0c96f0b26d 100644 --- a/test/inverted/TestInverted.py +++ b/test/inverted/TestInverted.py @@ -16,7 +16,7 @@ class TestInverted(TestBase): def setUp(self): self.world = MultiWorld(1) args = Namespace() - for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): + for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items(): setattr(args, name, {1: option.from_any(option.default)}) self.world.set_options(args) self.world.set_default_common_options() diff --git a/test/inverted/TestInvertedBombRules.py b/test/inverted/TestInvertedBombRules.py index cca252e3e1..f6afa9d0dc 100644 --- a/test/inverted/TestInvertedBombRules.py +++ b/test/inverted/TestInvertedBombRules.py @@ -17,7 +17,7 @@ class TestInvertedBombRules(unittest.TestCase): self.world = MultiWorld(1) self.world.mode[1] = "inverted" args = Namespace - for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): + for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items(): setattr(args, name, {1: option.from_any(option.default)}) self.world.set_options(args) self.world.set_default_common_options() diff --git a/test/inverted_minor_glitches/TestInvertedMinor.py b/test/inverted_minor_glitches/TestInvertedMinor.py index d737f21a07..42e7c942d6 100644 --- a/test/inverted_minor_glitches/TestInvertedMinor.py +++ b/test/inverted_minor_glitches/TestInvertedMinor.py @@ -17,7 +17,7 @@ class TestInvertedMinor(TestBase): def setUp(self): self.world = MultiWorld(1) args = Namespace() - for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): + for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items(): setattr(args, name, {1: option.from_any(option.default)}) self.world.set_options(args) self.world.set_default_common_options() diff --git a/test/inverted_owg/TestInvertedOWG.py b/test/inverted_owg/TestInvertedOWG.py index 7192fcb08b..064dd9e083 100644 --- a/test/inverted_owg/TestInvertedOWG.py +++ b/test/inverted_owg/TestInvertedOWG.py @@ -18,7 +18,7 @@ class TestInvertedOWG(TestBase): def setUp(self): self.world = MultiWorld(1) args = Namespace() - for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): + for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items(): setattr(args, name, {1: option.from_any(option.default)}) self.world.set_options(args) self.world.set_default_common_options() diff --git a/test/minor_glitches/TestMinor.py b/test/minor_glitches/TestMinor.py index db77ee919c..81c09cfb27 100644 --- a/test/minor_glitches/TestMinor.py +++ b/test/minor_glitches/TestMinor.py @@ -17,7 +17,7 @@ class TestMinor(TestBase): def setUp(self): self.world = MultiWorld(1) args = Namespace() - for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): + for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items(): setattr(args, name, {1: option.from_any(option.default)}) self.world.set_options(args) self.world.set_default_common_options() diff --git a/test/owg/TestVanillaOWG.py b/test/owg/TestVanillaOWG.py index 68b10732bb..e5489117a7 100644 --- a/test/owg/TestVanillaOWG.py +++ b/test/owg/TestVanillaOWG.py @@ -18,7 +18,7 @@ class TestVanillaOWG(TestBase): def setUp(self): self.world = MultiWorld(1) args = Namespace() - for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): + for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items(): setattr(args, name, {1: option.from_any(option.default)}) self.world.set_options(args) self.world.set_default_common_options() diff --git a/test/utils/TestSIPrefix.py b/test/utils/TestSIPrefix.py new file mode 100644 index 0000000000..81c7e0daf0 --- /dev/null +++ b/test/utils/TestSIPrefix.py @@ -0,0 +1,44 @@ +# Tests for SI prefix in Utils.py + +import unittest +from decimal import Decimal +from Utils import format_SI_prefix + + +class TestGenerateMain(unittest.TestCase): + """This tests SI prefix formatting in Utils.py""" + def assertEqual(self, first, second, msg=None): + # we strip spaces everywhere because that is an undefined implementation detail + super().assertEqual(first.replace(" ", ""), second.replace(" ", ""), msg) + + def test_rounding(self): + # we don't care if float(999.995) would fail due to error in precision + self.assertEqual(format_SI_prefix(999.999), "1.00k") + self.assertEqual(format_SI_prefix(1000.001), "1.00k") + self.assertEqual(format_SI_prefix(Decimal("999.995")), "1.00k") + self.assertEqual(format_SI_prefix(Decimal("1000.004")), "1.00k") + + def test_letters(self): + self.assertEqual(format_SI_prefix(0e0), "0.00") + self.assertEqual(format_SI_prefix(1e3), "1.00k") + self.assertEqual(format_SI_prefix(2e6), "2.00M") + self.assertEqual(format_SI_prefix(3e9), "3.00G") + self.assertEqual(format_SI_prefix(4e12), "4.00T") + self.assertEqual(format_SI_prefix(5e15), "5.00P") + self.assertEqual(format_SI_prefix(6e18), "6.00E") + self.assertEqual(format_SI_prefix(7e21), "7.00Z") + self.assertEqual(format_SI_prefix(8e24), "8.00Y") + + def test_multiple_letters(self): + self.assertEqual(format_SI_prefix(9e27), "9.00kY") + + def test_custom_power(self): + self.assertEqual(format_SI_prefix(1023.99, 1024), "1023.99") + self.assertEqual(format_SI_prefix(1034.24, 1024), "1.01k") + + def test_custom_labels(self): + labels = ("E", "da", "h", "k") + self.assertEqual(format_SI_prefix(1, 10, labels), "1.00E") + self.assertEqual(format_SI_prefix(10, 10, labels), "1.00da") + self.assertEqual(format_SI_prefix(100, 10, labels), "1.00h") + self.assertEqual(format_SI_prefix(1000, 10, labels), "1.00k") diff --git a/test/utils/__init__.py b/test/utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/vanilla/TestVanilla.py b/test/vanilla/TestVanilla.py index 4ffddc0747..e5ee73406a 100644 --- a/test/vanilla/TestVanilla.py +++ b/test/vanilla/TestVanilla.py @@ -16,7 +16,7 @@ class TestVanilla(TestBase): def setUp(self): self.world = MultiWorld(1) args = Namespace() - for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): + for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items(): setattr(args, name, {1: option.from_any(option.default)}) self.world.set_options(args) self.world.set_default_common_options() diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 4b2da5d588..e0dea8c277 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -2,10 +2,14 @@ from __future__ import annotations import logging import sys -from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Union, NamedTuple +import pathlib +from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Union, TYPE_CHECKING -from BaseClasses import MultiWorld, Item, CollectionState, Location, Tutorial from Options import Option +from BaseClasses import CollectionState + +if TYPE_CHECKING: + from BaseClasses import MultiWorld, Item, Location, Tutorial class AutoWorldRegister(type): @@ -41,14 +45,18 @@ class AutoWorldRegister(type): # construct class new_class = super().__new__(mcs, name, bases, dct) if "game" in dct: + if dct["game"] in AutoWorldRegister.world_types: + raise RuntimeError(f"""Game {dct["game"]} already registered.""") AutoWorldRegister.world_types[dct["game"]] = new_class new_class.__file__ = sys.modules[new_class.__module__].__file__ + if ".apworld" in new_class.__file__: + new_class.zip_path = pathlib.Path(new_class.__file__).parents[1] return new_class class AutoLogicRegister(type): - def __new__(cls, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoLogicRegister: - new_class = super().__new__(cls, name, bases, dct) + def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoLogicRegister: + new_class = super().__new__(mcs, name, bases, dct) function: Callable[..., Any] for item_name, function in dct.items(): if item_name == "copy_mixin": @@ -62,12 +70,12 @@ class AutoLogicRegister(type): return new_class -def call_single(world: MultiWorld, method_name: str, player: int, *args: Any) -> Any: +def call_single(world: "MultiWorld", method_name: str, player: int, *args: Any) -> Any: method = getattr(world.worlds[player], method_name) return method(*args) -def call_all(world: MultiWorld, method_name: str, *args: Any) -> None: +def call_all(world: "MultiWorld", method_name: str, *args: Any) -> None: world_types: Set[AutoWorldRegister] = set() for player in world.player_ids: world_types.add(world.worlds[player].__class__) @@ -79,7 +87,7 @@ def call_all(world: MultiWorld, method_name: str, *args: Any) -> None: stage_callable(world, *args) -def call_stage(world: MultiWorld, method_name: str, *args: Any) -> None: +def call_stage(world: "MultiWorld", method_name: str, *args: Any) -> None: world_types = {world.worlds[player].__class__ for player in world.player_ids} for world_type in world_types: stage_callable = getattr(world_type, f"stage_{method_name}", None) @@ -97,7 +105,7 @@ class WebWorld: # docs folder will also be scanned for tutorial guides given the relevant information in this list. Each Tutorial # class is to be used for one guide. - tutorials: List[Tutorial] + tutorials: List["Tutorial"] # Choose a theme for your /game/* pages # Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, stone @@ -111,11 +119,11 @@ class World(metaclass=AutoWorldRegister): """A World object encompasses a game's Items, Locations, Rules and additional data or functionality required. A Game should have its own subclass of World in which it defines the required data structures.""" - options: Dict[str, Option[Any]] = {} + option_definitions: Dict[str, Option[Any]] = {} """ link your Options mapping """ game: str - """ name the game """ + """ name of the game the world is for """ topology_present: bool = False """ indicate if world type has any meaningful layout/pathing """ @@ -172,7 +180,10 @@ class World(metaclass=AutoWorldRegister): hidden: bool = False """ Hide World Type from various views. Does not remove functionality. """ - world: MultiWorld + # see WebWorld for options + web: WebWorld = WebWorld() + + world: "MultiWorld" """ autoset on creation """ player: int @@ -190,9 +201,10 @@ class World(metaclass=AutoWorldRegister): location_names: Set[str] """ set of all potential location names """ - web: WebWorld = WebWorld() + zip_path: Optional[pathlib.Path] = None # If loaded from a .apworld, this is the Path to it. + __file__: str # path it was loaded from - def __init__(self, world: MultiWorld, player: int): + def __init__(self, world: "MultiWorld", player: int): self.world = world self.player = player @@ -227,12 +239,12 @@ class World(metaclass=AutoWorldRegister): @classmethod def fill_hook(cls, - progitempool: List[Item], - nonexcludeditempool: List[Item], - localrestitempool: Dict[int, List[Item]], - nonlocalrestitempool: Dict[int, List[Item]], - restitempool: List[Item], - fill_locations: List[Location]) -> None: + progitempool: List["Item"], + nonexcludeditempool: List["Item"], + localrestitempool: Dict[int, List["Item"]], + nonlocalrestitempool: Dict[int, List["Item"]], + restitempool: List["Item"], + fill_locations: List["Location"]) -> None: """Special method that gets called as part of distribute_items_restrictive (main fill). This gets called once per present world type.""" pass @@ -270,7 +282,7 @@ class World(metaclass=AutoWorldRegister): # end of ordered Main.py calls - def create_item(self, name: str) -> Item: + def create_item(self, name: str) -> "Item": """Create an item for this world type and player. Warning: this may be called with self.world = None, for example by MultiServer""" raise NotImplementedError @@ -281,7 +293,7 @@ class World(metaclass=AutoWorldRegister): return self.world.random.choice(tuple(self.item_name_to_id.keys())) # decent place to implement progressive items, in most cases can stay as-is - def collect_item(self, state: CollectionState, item: Item, remove: bool = False) -> Optional[str]: + def collect_item(self, state: "CollectionState", item: "Item", remove: bool = False) -> Optional[str]: """Collect an item name into state. For speed reasons items that aren't logically useful get skipped. Collect None to skip item. :param state: CollectionState to collect into @@ -291,19 +303,19 @@ class World(metaclass=AutoWorldRegister): return item.name return None - def get_pre_fill_items(self) -> List[Item]: + def get_pre_fill_items(self) -> List["Item"]: """ called to create all_state, return Items that are created during pre_fill """ return [] # following methods should not need to be overridden. - def collect(self, state: CollectionState, item: Item) -> bool: + def collect(self, state: "CollectionState", item: "Item") -> bool: name = self.collect_item(state, item) if name: state.prog_items[name, self.player] += 1 return True return False - def remove(self, state: CollectionState, item: Item) -> bool: + def remove(self, state: "CollectionState", item: "Item") -> bool: name = self.collect_item(state, item, True) if name: state.prog_items[name, self.player] -= 1 @@ -312,7 +324,7 @@ class World(metaclass=AutoWorldRegister): return True return False - def create_filler(self) -> Item: + def create_filler(self) -> "Item": return self.create_item(self.get_filler_item_name()) diff --git a/worlds/__init__.py b/worlds/__init__.py index e5bd4170d6..a331257166 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -1,29 +1,56 @@ import importlib +import zipimport import os +import typing -__all__ = ["lookup_any_item_id_to_name", - "lookup_any_location_id_to_name", - "network_data_package", - "AutoWorldRegister"] +folder = os.path.dirname(__file__) + +__all__ = [ + "lookup_any_item_id_to_name", + "lookup_any_location_id_to_name", + "network_data_package", + "AutoWorldRegister", + "world_sources", + "folder", +] + +if typing.TYPE_CHECKING: + from .AutoWorld import World + + +class WorldSource(typing.NamedTuple): + path: str # typically relative path from this module + is_zip: bool = False + + +# find potential world containers, currently folders and zip-importable .apworld's +world_sources: typing.List[WorldSource] = [] +file: os.DirEntry # for me (Berserker) at least, PyCharm doesn't seem to infer the type correctly +for file in os.scandir(folder): + if not file.name.startswith("_"): # prevent explicitly loading __pycache__ and allow _* names for non-world folders + if file.is_dir(): + world_sources.append(WorldSource(file.name)) + elif file.is_file() and file.name.endswith(".apworld"): + world_sources.append(WorldSource(file.name, is_zip=True)) # import all submodules to trigger AutoWorldRegister -world_folders = [] -for file in os.scandir(os.path.dirname(__file__)): - if file.is_dir(): - world_folders.append(file.name) -world_folders.sort() -for world in world_folders: - if not world.startswith("_"): # prevent explicitly loading __pycache__ and allow _* names for non-world folders - importlib.import_module(f".{world}", "worlds") +world_sources.sort() +for world_source in world_sources: + if world_source.is_zip: + importer = zipimport.zipimporter(os.path.join(folder, world_source.path)) + importer.load_module(world_source.path.split(".", 1)[0]) + else: + importlib.import_module(f".{world_source.path}", "worlds") -from .AutoWorld import AutoWorldRegister lookup_any_item_id_to_name = {} lookup_any_location_id_to_name = {} games = {} +from .AutoWorld import AutoWorldRegister + for world_name, world in AutoWorldRegister.world_types.items(): games[world_name] = { - "item_name_to_id" : world.item_name_to_id, + "item_name_to_id": world.item_name_to_id, "location_name_to_id": world.location_name_to_id, "version": world.data_version, # seems clients don't actually want this. Keeping it here in case someone changes their mind. @@ -41,5 +68,6 @@ network_data_package = { if any(not world.data_version for world in AutoWorldRegister.world_types.values()): network_data_package["version"] = 0 import logging + logging.warning(f"Datapackage is in custom mode. Custom Worlds: " f"{[world for world in AutoWorldRegister.world_types.values() if not world.data_version]}") diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py index 861dce9ac0..8850786be6 100644 --- a/worlds/alttp/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -15,7 +15,6 @@ def create_dungeons(world, player): dungeon_items, player) for item in dungeon.all_items: item.dungeon = dungeon - item.world = world dungeon.boss = BossFactory(default_boss, player) if default_boss else None for region in dungeon.regions: world.get_region(region, player).dungeon = dungeon diff --git a/worlds/alttp/Items.py b/worlds/alttp/Items.py index d1d372bf4b..3663db5cf4 100644 --- a/worlds/alttp/Items.py +++ b/worlds/alttp/Items.py @@ -51,6 +51,11 @@ class ItemData(typing.NamedTuple): flute_boy_credit: typing.Optional[str] hint_text: typing.Optional[str] + def as_init_dict(self) -> typing.Dict[str, typing.Any]: + return {key: getattr(self, key) for key in + ('classification', 'type', 'item_code', 'pedestal_hint', 'hint_text')} + + # Format: Name: (Advancement, Type, ItemCode, Pedestal Hint Text, Pedestal Credit Text, Sick Kid Credit Text, Zora Credit Text, Witch Credit Text, Flute Boy Credit Text, Hint Text) item_table = {'Bow': ItemData(IC.progression, None, 0x0B, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the Bow'), 'Progressive Bow': ItemData(IC.progression, None, 0x64, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'), @@ -218,7 +223,7 @@ item_table = {'Bow': ItemData(IC.progression, None, 0x0B, 'You have\nchosen the\ 'Open Floodgate': ItemData(IC.progression, 'Event', None, None, None, None, None, None, None, None), } -as_dict_item_table = {name: data._asdict() for name, data in item_table.items()} +item_init_table = {name: data.as_init_dict() for name, data in item_table.items()} progression_mapping = { "Golden Sword": ("Progressive Sword", 4), diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index d7f9becbfd..183f3eda91 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -1,5 +1,6 @@ import typing +from BaseClasses import MultiWorld from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink @@ -27,6 +28,35 @@ class Goal(Choice): option_hand_in = 2 +class OpenPyramid(Choice): + """Determines whether the hole at the top of pyramid is open. + Goal will open the pyramid if the goal requires you to kill Ganon, without needing to kill Agahnim 2. + Auto is the same as goal except if Ganon's dropdown is in another location, the hole will be closed.""" + display_name = "Open Pyramid Hole" + option_closed = 0 + option_open = 1 + option_goal = 2 + option_auto = 3 + default = option_goal + + alias_true = option_open + alias_false = option_closed + alias_yes = option_open + alias_no = option_closed + + def to_bool(self, world: MultiWorld, player: int) -> bool: + if self.value == self.option_goal: + return world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} + elif self.value == self.option_auto: + return world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} \ + and (world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'} or not + world.shuffle_ganon) + elif self.value == self.option_open: + return True + else: + return False + + class DungeonItem(Choice): value: int option_original_dungeon = 0 @@ -185,9 +215,11 @@ class Scams(Choice): option_all = 3 alias_false = 0 + @property def gives_king_zora_hint(self): return self.value in {0, 2} + @property def gives_bottle_merchant_hint(self): return self.value in {0, 1} @@ -331,6 +363,7 @@ class AllowCollect(Toggle): alttp_options: typing.Dict[str, type(Option)] = { "crystals_needed_for_gt": CrystalsTower, "crystals_needed_for_ganon": CrystalsGanon, + "open_pyramid": OpenPyramid, "bigkey_shuffle": bigkey_shuffle, "smallkey_shuffle": smallkey_shuffle, "compass_shuffle": compass_shuffle, diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 72cd1ceac5..dd5cc8c4dc 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -1247,7 +1247,7 @@ def patch_rom(world, rom, player, enemized): rom.write_bytes(0x50563, [0x3F, 0x14]) # disable below ganon chest rom.write_byte(0x50599, 0x00) # disable below ganon chest rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest - rom.write_byte(0x18008B, 0x01 if world.open_pyramid[player] else 0x00) # pre-open Pyramid Hole + rom.write_byte(0x18008B, 0x01 if world.open_pyramid[player].to_bool(world, player) else 0x00) # pre-open Pyramid Hole rom.write_byte(0x18008C, 0x01 if world.crystals_needed_for_gt[ player] == 0 else 0x00) # GT pre-opened if crystal requirement is 0 rom.write_byte(0xF5D73, 0xF0) # bees are catchable @@ -2091,7 +2091,9 @@ def write_string_to_rom(rom, target, string): def write_strings(rom, world, player): + from . import ALTTPWorld local_random = world.slot_seeds[player] + w: ALTTPWorld = world.worlds[player] tt = TextTable() tt.removeUnwantedText() @@ -2420,7 +2422,8 @@ def write_strings(rom, world, player): pedestal_text = 'Some Hot Air' if pedestalitem is None else hint_text(pedestalitem, True) if pedestalitem.pedestal_hint_text is not None else 'Unknown Item' tt['mastersword_pedestal_translated'] = pedestal_text - pedestal_credit_text = 'and the Hot Air' if pedestalitem is None else pedestalitem.pedestal_credit_text if pedestalitem.pedestal_credit_text is not None else 'and the Unknown Item' + pedestal_credit_text = 'and the Hot Air' if pedestalitem is None else \ + w.pedestal_credit_texts.get(pedestalitem.code, 'and the Unknown Item') etheritem = world.get_location('Ether Tablet', player).item ether_text = 'Some Hot Air' if etheritem is None else hint_text(etheritem, @@ -2448,20 +2451,24 @@ def write_strings(rom, world, player): credits = Credits() sickkiditem = world.get_location('Sick Kid', player).item - sickkiditem_text = local_random.choice( - SickKid_texts) if sickkiditem is None or sickkiditem.sickkid_credit_text is None else sickkiditem.sickkid_credit_text + sickkiditem_text = local_random.choice(SickKid_texts) \ + if sickkiditem is None or sickkiditem.code not in w.sickkid_credit_texts \ + else w.sickkid_credit_texts[sickkiditem.code] zoraitem = world.get_location('King Zora', player).item - zoraitem_text = local_random.choice( - Zora_texts) if zoraitem is None or zoraitem.zora_credit_text is None else zoraitem.zora_credit_text + zoraitem_text = local_random.choice(Zora_texts) \ + if zoraitem is None or zoraitem.code not in w.zora_credit_texts \ + else w.zora_credit_texts[zoraitem.code] magicshopitem = world.get_location('Potion Shop', player).item - magicshopitem_text = local_random.choice( - MagicShop_texts) if magicshopitem is None or magicshopitem.magicshop_credit_text is None else magicshopitem.magicshop_credit_text + magicshopitem_text = local_random.choice(MagicShop_texts) \ + if magicshopitem is None or magicshopitem.code not in w.magicshop_credit_texts \ + else w.magicshop_credit_texts[magicshopitem.code] fluteboyitem = world.get_location('Flute Spot', player).item - fluteboyitem_text = local_random.choice( - FluteBoy_texts) if fluteboyitem is None or fluteboyitem.fluteboy_credit_text is None else fluteboyitem.fluteboy_credit_text + fluteboyitem_text = local_random.choice(FluteBoy_texts) \ + if fluteboyitem is None or fluteboyitem.code not in w.fluteboy_credit_texts \ + else w.fluteboy_credit_texts[fluteboyitem.code] credits.update_credits_line('castle', 0, local_random.choice(KingsReturn_texts)) credits.update_credits_line('sanctuary', 0, local_random.choice(Sanctuary_texts)) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 7e16d6e566..36e16cf57c 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -935,7 +935,6 @@ def set_trock_key_rules(world, player): else: # A key is required in the Big Key Chest to prevent a possible softlock. Place an extra key to ensure 100% locations still works item = ItemFactory('Small Key (Turtle Rock)', player) - item.world = world location = world.get_location('Turtle Rock - Big Key Chest', player) location.place_locked_item(item) location.event = True diff --git a/worlds/alttp/Shops.py b/worlds/alttp/Shops.py index 77eec9dd0f..f6233286f0 100644 --- a/worlds/alttp/Shops.py +++ b/worlds/alttp/Shops.py @@ -207,10 +207,10 @@ def ShopSlotFill(world): shops_per_sphere.append(current_shops_slots) candidates_per_sphere.append(current_candidates) for location in sphere: - if location.shop_slot is not None: + if isinstance(location, ALttPLocation) and location.shop_slot is not None: if not location.shop_slot_disabled: current_shops_slots.append(location) - elif not location.locked and not location.item.name in blacklist_words: + elif not location.locked and location.item.name not in blacklist_words: current_candidates.append(location) if cumu_weights: x = cumu_weights[-1] @@ -335,7 +335,6 @@ def create_shops(world, player: int): else: loc.item = ItemFactory(GetBeemizerItem(world, player, 'Nothing'), player) loc.shop_slot_disabled = True - loc.item.world = world shop.region.locations.append(loc) world.clear_location_cache() @@ -460,10 +459,11 @@ def shuffle_shops(world, items, player: int): f"Not all upgrades put into Player{player}' item pool. Putting remaining items in Capacity Upgrade shop instead.") bombupgrades = sum(1 for item in new_items if 'Bomb Upgrade' in item) arrowupgrades = sum(1 for item in new_items if 'Arrow Upgrade' in item) + slots = iter(range(2)) if bombupgrades: - capacityshop.add_inventory(1, 'Bomb Upgrade (+5)', 100, bombupgrades) + capacityshop.add_inventory(next(slots), 'Bomb Upgrade (+5)', 100, bombupgrades) if arrowupgrades: - capacityshop.add_inventory(1, 'Arrow Upgrade (+5)', 100, arrowupgrades) + capacityshop.add_inventory(next(slots), 'Arrow Upgrade (+5)', 100, arrowupgrades) else: for item in new_items: world.push_precollected(ItemFactory(item, player)) diff --git a/worlds/alttp/SubClasses.py b/worlds/alttp/SubClasses.py index 87f10f48e0..f54ab16e92 100644 --- a/worlds/alttp/SubClasses.py +++ b/worlds/alttp/SubClasses.py @@ -6,31 +6,33 @@ from BaseClasses import Location, Item, ItemClassification class ALttPLocation(Location): game: str = "A Link to the Past" + crystal: bool + player_address: Optional[int] + _hint_text: Optional[str] + shop_slot: Optional[int] = None + """If given as integer, shop_slot is the shop's inventory index.""" + shop_slot_disabled: bool = False - def __init__(self, player: int, name: str = '', address=None, crystal: bool = False, - hint_text: Optional[str] = None, parent=None, - player_address=None): + def __init__(self, player: int, name: str, address: Optional[int] = None, crystal: bool = False, + hint_text: Optional[str] = None, parent=None, player_address: Optional[int] = None): super(ALttPLocation, self).__init__(player, name, address, parent) self.crystal = crystal self.player_address = player_address - self._hint_text: str = hint_text + self._hint_text = hint_text class ALttPItem(Item): game: str = "A Link to the Past" + type: Optional[str] + _pedestal_hint_text: Optional[str] + _hint_text: Optional[str] dungeon = None - def __init__(self, name, player, classification=ItemClassification.filler, type=None, item_code=None, pedestal_hint=None, - pedestal_credit=None, sick_kid_credit=None, zora_credit=None, witch_credit=None, - flute_boy_credit=None, hint_text=None): + def __init__(self, name, player, classification=ItemClassification.filler, type=None, item_code=None, + pedestal_hint=None, hint_text=None): super(ALttPItem, self).__init__(name, classification, item_code, player) self.type = type self._pedestal_hint_text = pedestal_hint - self.pedestal_credit_text = pedestal_credit - self.sickkid_credit_text = sick_kid_credit - self.zora_credit_text = zora_credit - self.magicshop_credit_text = witch_credit - self.fluteboy_credit_text = flute_boy_credit self._hint_text = hint_text @property diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 8e4ec1c143..e7f111c3b7 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -1,26 +1,23 @@ -import random import logging import os +import random import threading import typing from BaseClasses import Item, CollectionState, Tutorial +from .Dungeons import create_dungeons +from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect +from .InvertedRegions import create_inverted_regions, mark_dark_world_regions +from .ItemPool import generate_itempool, difficulties +from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem +from .Options import alttp_options, smallkey_shuffle +from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions +from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \ + get_hash_string, get_base_rom_path, LttPDeltaPatch +from .Rules import set_rules +from .Shops import create_shops, ShopSlotFill from .SubClasses import ALttPItem from ..AutoWorld import World, WebWorld, LogicMixin -from .Options import alttp_options, smallkey_shuffle -from .Items import as_dict_item_table, item_name_groups, item_table, GetBeemizerItem -from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions -from .Rules import set_rules -from .ItemPool import generate_itempool, difficulties -from .Shops import create_shops, ShopSlotFill -from .Dungeons import create_dungeons -from .Rom import LocalRom, patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, get_hash_string, \ - get_base_rom_path, LttPDeltaPatch -import Patch -from itertools import chain - -from .InvertedRegions import create_inverted_regions, mark_dark_world_regions -from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect lttp_logger = logging.getLogger("A Link to the Past") @@ -110,7 +107,7 @@ class ALTTPWorld(World): Ganon! """ game: str = "A Link to the Past" - options = alttp_options + option_definitions = alttp_options topology_present = True item_name_groups = item_name_groups hint_blacklist = {"Triforce"} @@ -124,6 +121,17 @@ class ALTTPWorld(World): required_client_version = (0, 3, 2) web = ALTTPWeb() + pedestal_credit_texts: typing.Dict[int, str] = \ + {data.item_code: data.pedestal_credit for data in item_table.values() if data.pedestal_credit} + sickkid_credit_texts: typing.Dict[int, str] = \ + {data.item_code: data.sick_kid_credit for data in item_table.values() if data.sick_kid_credit} + zora_credit_texts: typing.Dict[int, str] = \ + {data.item_code: data.zora_credit for data in item_table.values() if data.zora_credit} + magicshop_credit_texts: typing.Dict[int, str] = \ + {data.item_code: data.witch_credit for data in item_table.values() if data.witch_credit} + fluteboy_credit_texts: typing.Dict[int, str] = \ + {data.item_code: data.flute_boy_credit for data in item_table.values() if data.flute_boy_credit} + set_rules = set_rules create_items = generate_itempool @@ -145,6 +153,9 @@ class ALTTPWorld(World): player = self.player world = self.world + if self.use_enemizer(): + check_enemizer(world.enemizer) + # system for sharing ER layouts self.er_seed = str(world.random.randint(0, 2 ** 64)) @@ -176,17 +187,6 @@ class ALTTPWorld(World): def create_regions(self): player = self.player world = self.world - if world.open_pyramid[player] == 'goal': - world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', - 'localganontriforcehunt', 'ganonpedestal'} - elif world.open_pyramid[player] == 'auto': - world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', - 'localganontriforcehunt', 'ganonpedestal'} and \ - (world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull', - 'dungeonscrossed'} or not world.shuffle_ganon) - else: - world.open_pyramid[player] = {'on': True, 'off': False, 'yes': True, 'no': False}.get( - world.open_pyramid[player], 'auto') world.triforce_pieces_available[player] = max(world.triforce_pieces_available[player], world.triforce_pieces_required[player]) @@ -341,14 +341,19 @@ class ALTTPWorld(World): def stage_post_fill(cls, world): ShopSlotFill(world) + def use_enemizer(self): + world = self.world + player = self.player + return (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] + or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default' + or world.pot_shuffle[player] or world.bush_shuffle[player] + or world.killable_thieves[player]) + def generate_output(self, output_directory: str): world = self.world player = self.player try: - use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] - or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default' - or world.pot_shuffle[player] or world.bush_shuffle[player] - or world.killable_thieves[player]) + use_enemizer = self.use_enemizer() rom = LocalRom(get_base_rom_path()) @@ -411,7 +416,7 @@ class ALTTPWorld(World): multidata["connect_names"][new_name] = multidata["connect_names"][self.world.player_name[self.player]] def create_item(self, name: str) -> Item: - return ALttPItem(name, self.player, **as_dict_item_table[name]) + return ALttPItem(name, self.player, **item_init_table[name]) @classmethod def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, diff --git a/worlds/archipidle/Items.py b/worlds/archipidle/Items.py index 3100330d1b..945d3aae60 100644 --- a/worlds/archipidle/Items.py +++ b/worlds/archipidle/Items.py @@ -299,4 +299,5 @@ item_table = ( 'A Shrubbery', 'Roomba with a Knife', 'Wet Cat', + 'The missing moderator, Frostwares', ) diff --git a/worlds/archipidle/__init__.py b/worlds/archipidle/__init__.py index 6afcf4aa30..8b1061b5d1 100644 --- a/worlds/archipidle/__init__.py +++ b/worlds/archipidle/__init__.py @@ -25,7 +25,7 @@ class ArchipIDLEWorld(World): """ game = "ArchipIDLE" topology_present = False - data_version = 3 + data_version = 4 hidden = (datetime.now().month != 4) # ArchipIDLE is only visible during April web = ArchipIDLEWebWorld() @@ -47,13 +47,12 @@ class ArchipIDLEWorld(World): item_pool = [] for i in range(100): - item = Item( + item = ArchipIDLEItem( item_table_copy[i], ItemClassification.progression if i < 20 else ItemClassification.filler, self.item_name_to_id[item_table_copy[i]], self.player ) - item.game = 'ArchipIDLE' item_pool.append(item) self.world.itempool += item_pool @@ -93,6 +92,10 @@ def create_region(world: MultiWorld, player: int, name: str, locations=None, exi return region +class ArchipIDLEItem(Item): + game = "ArchipIDLE" + + class ArchipIDLELocation(Location): game: str = "ArchipIDLE" diff --git a/worlds/checksfinder/__init__.py b/worlds/checksfinder/__init__.py index d4a6f2aef3..ec9091c3d2 100644 --- a/worlds/checksfinder/__init__.py +++ b/worlds/checksfinder/__init__.py @@ -27,7 +27,7 @@ class ChecksFinderWorld(World): with the mines! You win when you get all your items and beat the board! """ game: str = "ChecksFinder" - options = checksfinder_options + option_definitions = checksfinder_options topology_present = True web = ChecksFinderWeb() diff --git a/worlds/dark_souls_3/Options.py b/worlds/dark_souls_3/Options.py new file mode 100644 index 0000000000..6b52cf53b6 --- /dev/null +++ b/worlds/dark_souls_3/Options.py @@ -0,0 +1,41 @@ +import typing +from Options import Toggle, Option + + +class AutoEquipOption(Toggle): + """Automatically equips any received armor or left/right weapons.""" + display_name = "Auto-equip" + + +class LockEquipOption(Toggle): + """Lock the equipment slots so you cannot change your armor or your left/right weapons. Works great with the + Auto-equip option.""" + display_name = "Lock Equipement Slots" + + +class NoWeaponRequirementsOption(Toggle): + """Disable the weapon requirements by removing any movement or damage penalties. + Permitting you to use any weapon early""" + display_name = "No Weapon Requirements" + + +class RandomizeWeaponsLevelOption(Toggle): + """Enable this option to upgrade 33% ( based on the probability chance ) of the pool of weapons to a random value + between +1 and +5/+10""" + display_name = "Randomize weapons level" + + +class LateBasinOfVowsOption(Toggle): + """Force the Basin of Vows to be located as a reward of defeating Pontiff Sulyvahn. It permits to ease the + progression by preventing having to kill the Dancer of the Boreal Valley as the first boss""" + display_name = "Late Basin of Vows" + + +dark_souls_options: typing.Dict[str, type(Option)] = { + "auto_equip": AutoEquipOption, + "lock_equip": LockEquipOption, + "no_weapon_requirements": NoWeaponRequirementsOption, + "randomize_weapons_level": RandomizeWeaponsLevelOption, + "late_basin_of_vows": LateBasinOfVowsOption, +} + diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py new file mode 100644 index 0000000000..1ded4203c5 --- /dev/null +++ b/worlds/dark_souls_3/__init__.py @@ -0,0 +1,299 @@ +# world/dark_souls_3/__init__.py +import json +import os + +from .Options import dark_souls_options +from .data.items_data import weapons_upgrade_5_table, weapons_upgrade_10_table, item_dictionary_table, key_items_list +from .data.locations_data import location_dictionary_table, cemetery_of_ash_table, fire_link_shrine_table, \ + high_wall_of_lothric, \ + undead_settlement_table, road_of_sacrifice_table, consumed_king_garden_table, cathedral_of_the_deep_table, \ + farron_keep_table, catacombs_of_carthus_table, smouldering_lake_table, irithyll_of_the_boreal_valley_table, \ + irithyll_dungeon_table, profaned_capital_table, anor_londo_table, lothric_castle_table, grand_archives_table, \ + untended_graves_table, archdragon_peak_table, firelink_shrine_bell_tower_table +from ..AutoWorld import World, WebWorld +from BaseClasses import MultiWorld, Location, Region, Item, RegionType, Entrance, Tutorial, ItemClassification +from ..generic.Rules import set_rule + + +class DarkSouls3Web(WebWorld): + bug_report_page = "https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/issues" + setup_en = Tutorial( + "Multiworld Setup Tutorial", + "A guide to setting up the Archipelago Dark Souls III randomizer on your computer.", + "English", + "setup_en.md", + "setup/en", + ["Marech"] + ) + + setup_fr = Tutorial( + setup_en.tutorial_name, + setup_en.description, + "Français", + "setup_fr.md", + "setup/fr", + ["Marech"] + ) + + tutorials = [setup_en, setup_fr] + + +class DarkSouls3World(World): + """ + Dark souls III is an Action role-playing game and is part of the Souls series developed by FromSoftware. + Played in a third-person perspective, players have access to various weapons, armour, magic, and consumables that + they can use to fight their enemies. + """ + + game: str = "Dark Souls III" + option_definitions = dark_souls_options + topology_present: bool = True + remote_items: bool = False + remote_start_inventory: bool = False + web = DarkSouls3Web() + data_version = 2 + base_id = 100000 + item_name_to_id = {name: id for id, name in enumerate(item_dictionary_table, base_id)} + location_name_to_id = {name: id for id, name in enumerate(location_dictionary_table, base_id)} + + def __init__(self, world: MultiWorld, player: int): + super().__init__(world, player) + self.locked_items = [] + self.locked_locations = [] + self.main_path_locations = [] + + def create_item(self, name: str) -> Item: + data = self.item_name_to_id[name] + + if name in key_items_list: + item_classification = ItemClassification.progression + elif name in weapons_upgrade_5_table or name in weapons_upgrade_10_table: + item_classification = ItemClassification.useful + else: + item_classification = ItemClassification.filler + + return DarkSouls3Item(name, item_classification, data, self.player) + + def create_regions(self): + menu_region = Region("Menu", RegionType.Generic, "Menu", self.player) + self.world.regions.append(menu_region) + + # Create all Vanilla regions of Dark Souls III + cemetery_of_ash_region = self.create_region("Cemetery Of Ash", cemetery_of_ash_table) + firelink_shrine_region = self.create_region("Firelink Shrine", fire_link_shrine_table) + firelink_shrine_bell_tower_region = self.create_region("Firelink Shrine Bell Tower", + firelink_shrine_bell_tower_table) + high_wall_of_lothric_region = self.create_region("High Wall of Lothric", high_wall_of_lothric) + undead_settlement_region = self.create_region("Undead Settlement", undead_settlement_table) + road_of_sacrifices_region = self.create_region("Road of Sacrifices", road_of_sacrifice_table) + consumed_king_garden_region = self.create_region("Consumed King's Garden", consumed_king_garden_table) + cathedral_of_the_deep_region = self.create_region("Cathedral of the Deep", cathedral_of_the_deep_table) + farron_keep_region = self.create_region("Farron Keep", farron_keep_table) + catacombs_of_carthus_region = self.create_region("Catacombs of Carthus", catacombs_of_carthus_table) + smouldering_lake_region = self.create_region("Smouldering Lake", smouldering_lake_table) + irithyll_of_the_boreal_valley_region = self.create_region("Irithyll of the Boreal Valley", + irithyll_of_the_boreal_valley_table) + irithyll_dungeon_region = self.create_region("Irithyll Dungeon", irithyll_dungeon_table) + profaned_capital_region = self.create_region("Profaned Capital", profaned_capital_table) + anor_londo_region = self.create_region("Anor Londo", anor_londo_table) + lothric_castle_region = self.create_region("Lothric Castle", lothric_castle_table) + grand_archives_region = self.create_region("Grand Archives", grand_archives_table) + untended_graves_region = self.create_region("Untended Graves", untended_graves_table) + archdragon_peak_region = self.create_region("Archdragon Peak", archdragon_peak_table) + kiln_of_the_first_flame_region = self.create_region("Kiln Of The First Flame", None) + + # Create the entrance to connect those regions + menu_region.exits.append(Entrance(self.player, "New Game", menu_region)) + self.world.get_entrance("New Game", self.player).connect(cemetery_of_ash_region) + cemetery_of_ash_region.exits.append(Entrance(self.player, "Goto Firelink Shrine", cemetery_of_ash_region)) + self.world.get_entrance("Goto Firelink Shrine", self.player).connect(firelink_shrine_region) + firelink_shrine_region.exits.append(Entrance(self.player, "Goto High Wall of Lothric", + firelink_shrine_region)) + firelink_shrine_region.exits.append(Entrance(self.player, "Goto Kiln Of The First Flame", + firelink_shrine_region)) + firelink_shrine_region.exits.append(Entrance(self.player, "Goto Bell Tower", + firelink_shrine_region)) + self.world.get_entrance("Goto High Wall of Lothric", self.player).connect(high_wall_of_lothric_region) + self.world.get_entrance("Goto Kiln Of The First Flame", self.player).connect(kiln_of_the_first_flame_region) + self.world.get_entrance("Goto Bell Tower", self.player).connect(firelink_shrine_bell_tower_region) + high_wall_of_lothric_region.exits.append(Entrance(self.player, "Goto Undead Settlement", + high_wall_of_lothric_region)) + high_wall_of_lothric_region.exits.append(Entrance(self.player, "Goto Lothric Castle", + high_wall_of_lothric_region)) + self.world.get_entrance("Goto Undead Settlement", self.player).connect(undead_settlement_region) + self.world.get_entrance("Goto Lothric Castle", self.player).connect(lothric_castle_region) + undead_settlement_region.exits.append(Entrance(self.player, "Goto Road Of Sacrifices", + undead_settlement_region)) + self.world.get_entrance("Goto Road Of Sacrifices", self.player).connect(road_of_sacrifices_region) + road_of_sacrifices_region.exits.append(Entrance(self.player, "Goto Cathedral", road_of_sacrifices_region)) + road_of_sacrifices_region.exits.append(Entrance(self.player, "Goto Farron keep", road_of_sacrifices_region)) + self.world.get_entrance("Goto Cathedral", self.player).connect(cathedral_of_the_deep_region) + self.world.get_entrance("Goto Farron keep", self.player).connect(farron_keep_region) + farron_keep_region.exits.append(Entrance(self.player, "Goto Carthus catacombs", farron_keep_region)) + self.world.get_entrance("Goto Carthus catacombs", self.player).connect(catacombs_of_carthus_region) + catacombs_of_carthus_region.exits.append(Entrance(self.player, "Goto Irithyll of the boreal", + catacombs_of_carthus_region)) + catacombs_of_carthus_region.exits.append(Entrance(self.player, "Goto Smouldering Lake", + catacombs_of_carthus_region)) + self.world.get_entrance("Goto Irithyll of the boreal", self.player).\ + connect(irithyll_of_the_boreal_valley_region) + self.world.get_entrance("Goto Smouldering Lake", self.player).connect(smouldering_lake_region) + irithyll_of_the_boreal_valley_region.exits.append(Entrance(self.player, "Goto Irithyll dungeon", + irithyll_of_the_boreal_valley_region)) + irithyll_of_the_boreal_valley_region.exits.append(Entrance(self.player, "Goto Anor Londo", + irithyll_of_the_boreal_valley_region)) + self.world.get_entrance("Goto Irithyll dungeon", self.player).connect(irithyll_dungeon_region) + self.world.get_entrance("Goto Anor Londo", self.player).connect(anor_londo_region) + irithyll_dungeon_region.exits.append(Entrance(self.player, "Goto Archdragon peak", irithyll_dungeon_region)) + irithyll_dungeon_region.exits.append(Entrance(self.player, "Goto Profaned capital", irithyll_dungeon_region)) + self.world.get_entrance("Goto Archdragon peak", self.player).connect(archdragon_peak_region) + self.world.get_entrance("Goto Profaned capital", self.player).connect(profaned_capital_region) + lothric_castle_region.exits.append(Entrance(self.player, "Goto Consumed King Garden", lothric_castle_region)) + lothric_castle_region.exits.append(Entrance(self.player, "Goto Grand Archives", lothric_castle_region)) + self.world.get_entrance("Goto Consumed King Garden", self.player).connect(consumed_king_garden_region) + self.world.get_entrance("Goto Grand Archives", self.player).connect(grand_archives_region) + consumed_king_garden_region.exits.append(Entrance(self.player, "Goto Untended Graves", + consumed_king_garden_region)) + self.world.get_entrance("Goto Untended Graves", self.player).connect(untended_graves_region) + + # For each region, add the associated locations retrieved from the corresponding location_table + def create_region(self, region_name, location_table) -> Region: + new_region = Region(region_name, RegionType.Generic, region_name, self.player, self.world) + if location_table: + for name, address in location_table.items(): + location = DarkSouls3Location(self.player, name, self.location_name_to_id[name], new_region) + new_region.locations.append(location) + self.world.regions.append(new_region) + return new_region + + def create_items(self): + for name, address in self.item_name_to_id.items(): + # Specific items will be included in the item pool under certain conditions. See generate_basic + if name != "Basin of Vows": + self.world.itempool += [self.create_item(name)] + + def generate_early(self): + pass + + def set_rules(self) -> None: + + # Define the access rules to the entrances + set_rule(self.world.get_entrance("Goto Bell Tower", self.player), + lambda state: state.has("Tower Key", self.player)) + set_rule(self.world.get_entrance("Goto Undead Settlement", self.player), + lambda state: state.has("Small Lothric Banner", self.player)) + set_rule(self.world.get_entrance("Goto Lothric Castle", self.player), + lambda state: state.has("Basin of Vows", self.player)) + set_rule(self.world.get_entrance("Goto Irithyll of the boreal", self.player), + lambda state: state.has("Small Doll", self.player)) + set_rule(self.world.get_entrance("Goto Archdragon peak", self.player), + lambda state: state.can_reach("CKG: Soul of Consumed Oceiros", "Location", self.player)) + set_rule(self.world.get_entrance("Goto Profaned capital", self.player), + lambda state: state.has("Storm Ruler", self.player)) + set_rule(self.world.get_entrance("Goto Grand Archives", self.player), + lambda state: state.has("Grand Archives Key", self.player)) + set_rule(self.world.get_entrance("Goto Kiln Of The First Flame", self.player), + lambda state: state.has("Cinders of a Lord - Abyss Watcher", self.player) and + state.has("Cinders of a Lord - Yhorm the Giant", self.player) and + state.has("Cinders of a Lord - Aldrich", self.player) and + state.has("Cinders of a Lord - Lothric Prince", self.player)) + + # Define the access rules to some specific locations + set_rule(self.world.get_location("HWL: Soul of the Dancer", self.player), + lambda state: state.has("Basin of Vows", self.player)) + set_rule(self.world.get_location("HWL: Greirat's Ashes", self.player), + lambda state: state.has("Cell Key", self.player)) + set_rule(self.world.get_location("ID: Bellowing Dragoncrest Ring", self.player), + lambda state: state.has("Jailbreaker's Key", self.player)) + set_rule(self.world.get_location("ID: Prisoner Chief's Ashes", self.player), + lambda state: state.has("Jailer's Key Ring", self.player)) + set_rule(self.world.get_location("ID: Covetous Gold Serpent Ring", self.player), + lambda state: state.has("Old Cell Key", self.player)) + black_hand_gotthard_corpse_rule = lambda state: \ + (state.can_reach("AL: Cinders of a Lord - Aldrich", "Location", self.player) and + state.can_reach("PC: Cinders of a Lord - Yhorm the Giant", "Location", self.player)) + set_rule(self.world.get_location("LC: Grand Archives Key", self.player), black_hand_gotthard_corpse_rule) + set_rule(self.world.get_location("LC: Gotthard Twinswords", self.player), black_hand_gotthard_corpse_rule) + + self.world.completion_condition[self.player] = lambda state: \ + state.has("Cinders of a Lord - Abyss Watcher", self.player) and \ + state.has("Cinders of a Lord - Yhorm the Giant", self.player) and \ + state.has("Cinders of a Lord - Aldrich", self.player) and \ + state.has("Cinders of a Lord - Lothric Prince", self.player) + + def generate_basic(self): + # Depending on the specified option, add the Basin of Vows to a specific location or to the item pool + item = self.create_item("Basin of Vows") + if self.world.late_basin_of_vows[self.player]: + self.world.get_location("IBV: Soul of Pontiff Sulyvahn", self.player).place_locked_item(item) + else: + self.world.itempool += [item] + + # Fill item pool with additional items + item_pool_len = self.item_name_to_id.__len__() + total_required_locations = self.location_name_to_id.__len__() + for i in range(item_pool_len, total_required_locations): + self.world.itempool += [self.create_item("Soul of an Intrepid Hero")] + + def generate_output(self, output_directory: str): + # Depending on the specified option, modify items hexadecimal value to add an upgrade level + item_dictionary = item_dictionary_table.copy() + if self.world.randomize_weapons_level[self.player]: + # Randomize some weapons upgrades + for name in weapons_upgrade_5_table.keys(): + if self.world.random.randint(0, 100) < 33: + value = self.world.random.randint(1, 5) + item_dictionary[name] += value + + for name in weapons_upgrade_10_table.keys(): + if self.world.random.randint(0, 100) < 33: + value = self.world.random.randint(1, 10) + item_dictionary[name] += value + + # Create the mandatory lists to generate the player's output file + items_id = [] + items_address = [] + locations_id = [] + locations_address = [] + locations_target = [] + for location in self.world.get_filled_locations(): + if location.item.player == self.player: + items_id.append(location.item.code) + items_address.append(item_dictionary[location.item.name]) + + if location.player == self.player: + locations_address.append(location_dictionary_table[location.name]) + locations_id.append(location.address) + if location.item.player == self.player: + locations_target.append(item_dictionary[location.item.name]) + else: + locations_target.append(0) + + data = { + "options": { + "auto_equip": self.world.auto_equip[self.player].value, + "lock_equip": self.world.lock_equip[self.player].value, + "no_weapon_requirements": self.world.no_weapon_requirements[self.player].value, + }, + "seed": self.world.seed_name, # to verify the server's multiworld + "slot": self.world.player_name[self.player], # to connect to server + "base_id": self.base_id, # to merge location and items lists + "locationsId": locations_id, + "locationsAddress": locations_address, + "locationsTarget": locations_target, + "itemsId": items_id, + "itemsAddress": items_address + } + + # generate the file + filename = f"AP-{self.world.seed_name}-P{self.player}-{self.world.player_name[self.player]}.json" + with open(os.path.join(output_directory, filename), 'w') as outfile: + json.dump(data, outfile) + + +class DarkSouls3Location(Location): + game: str = "Dark Souls III" + + +class DarkSouls3Item(Item): + game: str = "Dark Souls III" diff --git a/worlds/dark_souls_3/data/items_data.py b/worlds/dark_souls_3/data/items_data.py new file mode 100644 index 0000000000..9add18820b --- /dev/null +++ b/worlds/dark_souls_3/data/items_data.py @@ -0,0 +1,383 @@ +""" +Tools used to create this list : +List of all items https://docs.google.com/spreadsheets/d/1nK2g7g6XJ-qphFAk1tjP3jZtlXWDQY-ItKLa_sniawo/edit#gid=1551945791 +Regular expression parser https://regex101.com/r/XdtiLR/2 +List of locations https://darksouls3.wiki.fextralife.com/Locations +""" + +weapons_upgrade_5_table = { + "Irithyll Straight Sword": 0x0020A760, + "Chaos Blade": 0x004C9960, + "Dragonrider Bow": 0x00D6B0F0, + "White Hair Talisman": 0x00CAF120, + "Izalith Staff": 0x00C96A80, + "Fume Ultra Greatsword": 0x0060E4B0, + "Black Knight Sword": 0x005F5E10, + + "Yorshka's Spear": 0x008C3A70, + "Smough's Great Hammer": 0x007E30B0, + "Dragonslayer Greatbow": 0x00CF8500, + "Golden Ritual Spear": 0x00C83200, + "Eleonora": 0x006CCB90, + "Witch's Locks": 0x00B7B740, + "Crystal Chime": 0x00CA2DD0, + "Black Knight Glaive": 0x009AE070, + "Dragonslayer Spear": 0x008CAFA0, + "Caitha's Chime": 0x00CA06C0, + "Sunlight Straight Sword": 0x00203230, + + "Firelink Greatsword": 0x0060BDA0, + "Hollowslayer Greatsword": 0x00604870, + "Arstor's Spear": 0x008BEC50, + "Vordt's Great Hammer": 0x007CD120, + "Crystal Sage's Rapier": 0x002E6300, + "Farron Greatsword": 0x005E9AC0, + "Wolf Knight's Greatsword": 0x00602160, + "Dancer's Enchanted Swords": 0x00F4C040, + "Wolnir's Holy Sword": 0x005FFA50, + "Demon's Greataxe": 0x006CA480, + "Demon's Fist": 0x00A84DF0, + + "Old King's Great Hammer": 0x007CF830, + "Greatsword of Judgment": 0x005E2590, + "Profaned Greatsword": 0x005E4CA0, + "Yhorm's Great Machete": 0x005F0FF0, + "Cleric's Candlestick": 0x0020F580, + "Dragonslayer Greataxe": 0x006C7D70, + "Moonlight Greatsword": 0x00606F80, + "Gundyr's Halberd": 0x009A1D20, + "Lothric's Holy Sword": 0x005FD340, + "Lorian's Greatsword": 0x005F8520, + "Twin Princes' Greatsword": 0x005FAC30, + "Storm Curved Sword": 0x003E4180, + "Dragonslayer Swordspear": 0x008BC540, + "Sage's Crystal Staff": 0x00C8CE40, +} + +weapons_upgrade_10_table = { + "Broken Straight Sword": 0x001EF9B0, + "Deep Battle Axe": 0x0006AFA54, + "Club": 0x007A1200, + "Claymore": 0x005BDBA0, + "Longbow": 0x00D689E0, + "Mail Breaker": 0x002DEDD0, + "Broadsword": 0x001ED2A0, + "Astora's Straight Sword": 0x002191C0, + "Rapier": 0x002E14E0, + "Lucerne": 0x0098BD90, + "Whip": 0x00B71B00, + "Reinforced Club": 0x007A8730, + "Caestus": 0x00A7FFD0, + "Partizan": 0x0089C970, + "Red Hilted Halberd": 0x009AB960, + "Saint's Talisman": 0x00CACA10, + "Large Club": 0x007AFC60, + + "Brigand Twindaggers": 0x00F50E60, + "Butcher Knife": 0x006BE130, + "Brigand Axe": 0x006B1DE0, + "Heretic's Staff": 0x00C8F550, + "Great Club": 0x007B4A80, + "Exile Greatsword": 0x005DD770, + "Sellsword Twinblades": 0x00F42400, + "Notched Whip": 0x00B7DE50, + "Astora Greatsword": 0x005C9EF0, + "Executioner's Greatsword": 0x0021DFE0, + "Saint-tree Bellvine": 0x00C9DFB0, + "Saint Bident": 0x008C1360, + "Drang Hammers": 0x00F61FD0, + "Arbalest": 0x00D662D0, + "Sunlight Talisman": 0x00CA54E0, + "Greatsword": 0x005C50D0, + "Black Bow of Pharis": 0x00D7E970, + "Great Axe": 0x006B9310, + "Black Blade": 0x004CC070, + "Blacksmith Hammer": 0x007E57C0, + "Witchtree Branch": 0x00C94370, + "Painting Guardian's Curved Sword": 0x003E6890, + "Pickaxe": 0x007DE290, + "Court Sorcerer's Staff": 0x00C91C60, + "Avelyn": 0x00D6FF10, + "Onikiri and Ubadachi": 0x00F58390, + "Ricard's Rapier": 0x002E3BF0, + "Drakeblood Greatsword": 0x00609690, + "Greatlance": 0x008A8CC0, + "Sniper Crossbow": 0x00D83790, + + "Claw": 0x00A7D8C0, + "Drang Twinspears": 0x00F5AAA0, +} + +shields_table = { + "East-West Shield": 0x0142B930, + "Silver Eagle Kite Shield": 0x014418C0, + "Small Leather Shield": 0x01315410, + "Blue Wooden Shield": 0x0143F1B0, + "Plank Shield": 0x01346150, + "Caduceus Round Shield": 0x01341330, + "Wargod Wooden Shield": 0x0144DC10, + "Grass Crest Shield": 0x01437C80, + "Golden Falcon Shield": 0x01354BB0, + "Twin Dragon Greatshield": 0x01513820, + "Spider Shield": 0x01435570, + "Crest Shield": 0x01430750, + "Curse Ward Greatshield": 0x01518640, + "Stone Parma": 0x01443FD0, + "Dragon Crest Shield": 0x01432E60, + "Shield of Want": 0x0144B500, + "Black Iron Greatshield": 0x0150EA00, + "Greatshield of Glory": 0x01515F30, + "Sacred Bloom Shield": 0x013572C0, + "Golden Wing Crest Shield": 0x0143CAA0, + "Ancient Dragon Greatshield": 0x013599D0, + "Spirit Tree Crest Shield": 0x014466E0, + +} + +goods_table = { + "Soul of an Intrepid Hero": 0x4000019D, + "Soul of the Nameless King": 0x400002D2, + "Soul of Champion Gundyr": 0x400002C8, + "Soul of the Twin Princes": 0x400002DB, + "Soul of Consumed Oceiros": 0x400002CE, + "Soul of Aldrich": 0x400002D5, + "Soul of Yhorm the Giant": 0x400002DC, + "Soul of Pontiff Sulyvahn": 0x400002D4, + "Soul of the Old Demon King": 0x400002D0, + "Soul of High Lord Wolnir": 0x400002D6, + "Soul of the Blood of the Wolf": 0x400002CD, + "Soul of the Deacons of the Deep": 0x400002D9, + "Soul of a Crystal Sage": 0x400002CB, + "Soul of Boreal Valley Vordt": 0x400002CF, + "Soul of a Stray Demon": 0x400002E7, + "Soul of a Demon": 0x400002E3, +} + +armor_table = { + "Fire Keeper Robe": 0x140D9CE8, + "Fire Keeper Gloves": 0x140DA0D0, + "Fire Keeper Skirt": 0x140DA4B8, + "Deserter Trousers": 0x126265B8, + "Cleric Hat": 0x11D905C0, + "Cleric Blue Robe": 0x11D909A8, + "Cleric Gloves": 0x11D90D90, + "Cleric Trousers": 0x11D91178, + "Northern Helm": 0x116E3600, + "Northern Armor": 0x116E39E8, + "Northern Gloves": 0x116E3DD0, + "Northern Trousers": 0x116E41B8, + "Loincloth": 0x148F57D8, + + "Brigand Hood": 0x148009E0, + "Brigand Armor": 0x14800DC8, + "Brigand Gauntlets": 0x148011B0, + "Brigand Trousers": 0x14801598, + "Sorcerer Hood": 0x11C9C380, + "Sorcerer Robe": 0x11C9C768, + "Sorcerer Gloves": 0x11C9CB50, + "Sorcerer Trousers": 0x11C9CF38, + "Fallen Knight Helm": 0x1121EAC0, + "Fallen Knight Armor": 0x1121EEA8, + "Fallen Knight Gauntlets": 0x1121F290, + "Fallen Knight Trousers": 0x1121F678, + "Conjurator Hood": 0x149E8E60, + "Conjurator Robe": 0x149E9248, + "Conjurator Manchettes": 0x149E9630, + "Conjurator Boots": 0x149E9A18, + + "Sellsword Helm": 0x11481060, + "Sellsword Armor": 0x11481448, + "Sellsword Gauntlet": 0x11481830, + "Sellsword Trousers": 0x11481C18, + "Herald Helm": 0x114FB180, + "Herald Armor": 0x114FB568, + "Herald Gloves": 0x114FB950, + "Herald Trousers": 0x114FBD38, + + "Maiden Hood": 0x14BD12E0, + "Maiden Robe": 0x14BD16C8, + "Maiden Gloves": 0x14BD1AB0, + "Maiden Skirt": 0x14BD1E98, + "Drang Armor": 0x154E0C28, + "Drang Gauntlets": 0x154E1010, + "Drang Shoes": 0x154E13F8, + "Archdeacon White Crown": 0x13EF1480, + "Archdeacon Holy Garb": 0x13EF1868, + "Archdeacon Skirt": 0x13EF2038, + "Antiquated Dress": 0x15D76068, + "Antiquated Gloves": 0x15D76450, + "Antiquated Skirt": 0x15D76838, + "Ragged Mask": 0x148F4C20, + "Crown of Dusk": 0x15D75C80, + "Pharis's Hat": 0x1487AB00, + "Old Sage's Blindfold": 0x11945BA0, + + "Painting Guardian Hood": 0x156C8CC0, + "Painting Guardian Gown": 0x156C90A8, + "Painting Guardian Gloves": 0x156C9490, + "Painting Guardian Waistcloth": 0x156C9878, + "Brass Helm": 0x1501BD00, + "Brass Armor": 0x1501C0E8, + "Brass Gauntlets": 0x1501C4D0, + "Brass Leggings": 0x1501C8B8, + "Old Sorcerer Hat": 0x1496ED40, + "Old Sorcerer Coat": 0x1496F128, + "Old Sorcerer Gauntlets": 0x1496F510, + "Old Sorcerer Boots": 0x1496F8F8, + "Court Sorcerer Hood": 0x11BA8140, + "Court Sorcerer Robe": 0x11BA8528, + "Court Sorcerer Gloves": 0x11BA8910, + "Court Sorcerer Trousers": 0x11BA8CF8, + "Dragonslayer Helm": 0x158B1140, + "Dragonslayer Armor": 0x158B1528, + "Dragonslayer Gauntlets": 0x158B1910, + "Dragonslayer Leggings": 0x158B1CF8, + + "Hood of Prayer": 0x13AA6A60, + "Robe of Prayer": 0x13AA6E48, + "Skirt of Prayer": 0x13AA7618, + "Winged Knight Helm": 0x12EBAE40, + "Winged Knight Armor": 0x12EBB228, + "Winged Knight Gauntlets": 0x12EBB610, + "Winged Knight Leggings": 0x12EBB9F8, + "Shadow Mask": 0x14D3F640, + "Shadow Garb": 0x14D3FA28, + "Shadow Gauntlets": 0x14D3FE10, + "Shadow Leggings": 0x14D401F8, +} + +rings_table = { + "Estus Ring": 0x200050DC, + "Covetous Silver Serpent Ring": 0x20004FB0, + "Fire Clutch Ring": 0x2000501E, + "Flame Stoneplate Ring": 0x20004E52, + "Flynn's Ring": 0x2000503C, + "Chloranthy Ring": 0x20004E2A, + + "Morne's Ring": 0x20004F1A, + "Sage Ring": 0x20004F38, + "Aldrich's Sapphire": 0x20005096, + "Lloyd's Sword Ring": 0x200050B4, + "Poisonbite Ring": 0x20004E8E, + "Deep Ring": 0x20004F60, + "Lingering Dragoncrest Ring": 0x20004F2E, + "Carthus Milkring": 0x20004FE2, + "Witch's Ring": 0x20004F11, + "Carthus Bloodring": 0x200050FA, + + "Speckled Stoneplate Ring": 0x20004E7A, + "Magic Clutch Ring": 0x2000500A, + "Ring of the Sun's First Born": 0x20004F1B, + "Pontiff's Right Eye": 0x2000510E, "Leo Ring": 0x20004EE8, + "Dark Stoneplate Ring": 0x20004E70, + "Reversal Ring": 0x20005104, + "Ring of Favor": 0x20004E3E, + "Bellowing Dragoncrest Ring": 0x20004F07, + "Covetous Gold Serpent Ring": 0x20004FA6, + "Dusk Crown Ring": 0x20004F4C, + "Dark Clutch Ring": 0x20005028, + "Cursebite Ring": 0x20004E98, + "Sun Princess Ring": 0x20004FBA, + "Aldrich's Ruby": 0x2000508C, + "Scholar Ring": 0x20004EB6, + "Fleshbite Ring": 0x20004EA2, + "Hunter's Ring": 0x20004FF6, + "Ashen Estus Ring": 0x200050E6, + "Hornet Ring": 0x20004F9C, + "Lightning Clutch Ring": 0x20005014, + "Ring of Steel Protection": 0x20004E48, + "Calamity Ring": 0x20005078, + "Thunder Stoneplate Ring": 0x20004E5C, + "Knight's Ring": 0x20004FEC, + "Red Tearstone Ring": 0x20004ECA, + "Dragonscale Ring": 0x2000515E, + "Knight Slayer's Ring": 0x20005000, +} + +spells_table = { + "Seek Guidance": 0x40360420, + "Lightning Spear": 0x40362B30, + "Atonement": 0x4039ADA0, + "Great Magic Weapon": 0x40140118, + "Iron Flesh": 0x40251430, + "Lightning Stake": 0x40389C30, + "Toxic Mist": 0x4024F108, + "Sacred Flame": 0x40284880, + "Dorhys' Gnawing": 0x40363EB8, + "Great Heal": 0x40356FB0, + "Lightning Blade": 0x4036C770, + "Profaned Flame": 0x402575D8, + "Wrath of the Gods": 0x4035E0F8, + "Power Within": 0x40253B40, + "Soul Stream": 0x4018B820, + "Divine Pillars of Light": 0x4038C340, + "Great Magic Barrier": 0x40365628, + "Great Magic Shield": 0x40144F38, +} + +misc_items_table = { + "Tower Key": 0x400007DF, + "Grave Key": 0x400007D9, + "Cell Key": 0x400007DA, + "Small Lothric Banner": 0x40000836, + "Mortician's Ashes": 0x4000083B, + "Braille Divine Tome of Carim": 0x40000847, # Shop + "Great Swamp Pyromancy Tome": 0x4000084F, # Shop + "Farron Coal ": 0x40000837, # Shop + "Paladin's Ashes": 0x4000083D, #Shop + "Deep Braille Divine Tome": 0x40000860, # Shop + "Small Doll": 0x400007D5, + "Golden Scroll": 0x4000085C, + "Sage's Coal": 0x40000838, # Shop #Unique + "Sage's Scroll": 0x40000854, + "Dreamchaser's Ashes": 0x4000083C, # Shop #Unique + "Cinders of a Lord - Abyss Watcher": 0x4000084B, + "Cinders of a Lord - Yhorm the Giant": 0x4000084D, + "Cinders of a Lord - Aldrich": 0x4000084C, + "Grand Archives Key": 0x400007DE, + "Basin of Vows": 0x40000845, + "Cinders of a Lord - Lothric Prince": 0x4000084E, + "Carthus Pyromancy Tome": 0x40000850, + "Grave Warden's Ashes": 0x4000083E, + "Grave Warden Pyromancy Tome": 0x40000853, + "Quelana Pyromancy Tome": 0x40000852, + "Izalith Pyromancy Tome": 0x40000851, + "Greirat's Ashes": 0x4000083F, + "Excrement-covered Ashes": 0x40000862, + "Easterner's Ashes": 0x40000868, + "Prisoner Chief's Ashes": 0x40000863, + "Jailbreaker's Key": 0x400007D7, + "Dragon Torso Stone": 0x4000017A, + "Profaned Coal": 0x4000083A, + "Xanthous Ashes": 0x40000864, + "Old Cell Key": 0x400007DC, + "Jailer's Key Ring": 0x400007D8, + "Logan's Scroll": 0x40000855, + "Storm Ruler": 0x006132D0, + "Giant's Coal": 0x40000839, + "Coiled Sword Fragment": 0x4000015F, + "Dragon Chaser's Ashes": 0x40000867, + "Twinkling Dragon Torso Stone": 0x40000184, + "Braille Divine Tome of Lothric": 0x40000848, +} + +key_items_list = { + "Small Lothric Banner", + "Basin of Vows", + "Small Doll", + "Storm Ruler", + "Grand Archives Key", + "Cinders of a Lord - Abyss Watcher", + "Cinders of a Lord - Yhorm the Giant", + "Cinders of a Lord - Aldrich", + "Cinders of a Lord - Lothric Prince", + "Mortician's Ashes", + "Cell Key", + "Tower Key", + "Jailbreaker's Key", + "Prisoner Chief's Ashes", + "Old Cell Key", + "Jailer's Key Ring", +} + +item_dictionary_table = {**weapons_upgrade_5_table, **weapons_upgrade_10_table, **shields_table, **armor_table, **rings_table, **spells_table, **misc_items_table, **goods_table} diff --git a/worlds/dark_souls_3/data/locations_data.py b/worlds/dark_souls_3/data/locations_data.py new file mode 100644 index 0000000000..384da049ac --- /dev/null +++ b/worlds/dark_souls_3/data/locations_data.py @@ -0,0 +1,434 @@ +""" +Tools used to create this list : +List of all items https://docs.google.com/spreadsheets/d/1nK2g7g6XJ-qphFAk1tjP3jZtlXWDQY-ItKLa_sniawo/edit#gid=1551945791 +Regular expression parser https://regex101.com/r/XdtiLR/2 +List of locations https://darksouls3.wiki.fextralife.com/Locations +""" + +cemetery_of_ash_table = { +} + +fire_link_shrine_table = { + # "FS: Coiled Sword": 0x40000859, You can still light the Firelink Shrine fire whether you have it or not, useless + "FS: Broken Straight Sword": 0x001EF9B0, + "FS: East-West Shield": 0x0142B930, + "FS: Uchigatana": 0x004C4B40, + "FS: Master's Attire": 0x148F5008, + "FS: Master's Gloves": 0x148F53F0, +} + +firelink_shrine_bell_tower_table = { + "FSBT: Covetous Silver Serpent Ring": 0x20004FB0, + "FSBT: Fire Keeper Robe": 0x140D9CE8, + "FSBT: Fire Keeper Gloves": 0x140DA0D0, + "FSBT: Fire Keeper Skirt": 0x140DA4B8, + "FSBT: Estus Ring": 0x200050DC, + "FSBT: Fire Keeper Soul": 0x40000186 +} + +high_wall_of_lothric = { + "HWL: Deep Battle Axe": 0x0006AFA54, + "HWL: Club": 0x007A1200, + "HWL: Claymore": 0x005BDBA0, + "HWL: Binoculars": 0x40000173, + "HWL: Longbow": 0x00D689E0, + "HWL: Mail Breaker": 0x002DEDD0, + "HWL: Broadsword": 0x001ED2A0, + "HWL: Silver Eagle Kite Shield": 0x014418C0, + "HWL: Astora's Straight Sword": 0x002191C0, + "HWL: Cell Key": 0x400007DA, + "HWL: Rapier": 0x002E14E0, + "HWL: Lucerne": 0x0098BD90, + "HWL: Small Lothric Banner": 0x40000836, + "HWL: Basin of Vows": 0x40000845, + "HWL: Soul of Boreal Valley Vordt": 0x400002CF, + "HWL: Soul of the Dancer": 0x400002CA, + "HWL: Way of Blue Covenant": 0x2000274C, + "HWL: Greirat's Ashes": 0x4000083F, +} + +undead_settlement_table = { + "US: Small Leather Shield": 0x01315410, + "US: Whip": 0x00B71B00, + "US: Reinforced Club": 0x007A8730, + "US: Blue Wooden Shield": 0x0143F1B0, + + "US: Cleric Hat": 0x11D905C0, + "US: Cleric Blue Robe": 0x11D909A8, + "US: Cleric Gloves": 0x11D90D90, + "US: Cleric Trousers": 0x11D91178, + + "US: Mortician's Ashes": 0x4000083B, + "US: Caestus": 0x00A7FFD0, + "US: Plank Shield": 0x01346150, + "US: Flame Stoneplate Ring": 0x20004E52, + "US: Caduceus Round Shield": 0x01341330, + "US: Fire Clutch Ring": 0x2000501E, + "US: Partizan": 0x0089C970, + "US: Bloodbite Ring": 0x20004E84, + + "US: Red Hilted Halberd": 0x009AB960, + "US: Saint's Talisman": 0x00CACA10, + "US: Irithyll Straight Sword": 0x0020A760, + "US: Large Club": 0x007AFC60, + "US: Northern Helm": 0x116E3600, + "US: Northern Armor": 0x116E39E8, + "US: Northern Gloves": 0x116E3DD0, + "US: Northern Trousers": 0x116E41B8, + "US: Flynn's Ring": 0x2000503C, + + "US: Mirrah Vest": 0x15204568, + "US: Mirrah Gloves": 0x15204950, + "US: Mirrah Trousers": 0x15204D38, + + "US: Chloranthy Ring": 0x20004E2A, + "US: Loincloth": 0x148F57D8, + "US: Wargod Wooden Shield": 0x0144DC10, + + "US: Loretta's Bone": 0x40000846, + + "US: Hand Axe": 0x006ACFC0, + "US: Great Scythe": 0x00989680, + "US: Soul of the Rotted Greatwood": 0x400002D7, + "US: Hawk Ring": 0x20004F92, + "US: Warrior of Sunlight Covenant": 0x20002738, +} + +road_of_sacrifice_table = { + "RS: Brigand Twindaggers": 0x00F50E60, + + "RS: Brigand Hood": 0x148009E0, + "RS: Brigand Armor": 0x14800DC8, + "RS: Brigand Gauntlets": 0x148011B0, + "RS: Brigand Trousers": 0x14801598, + + "RS: Butcher Knife": 0x006BE130, + "RS: Brigand Axe": 0x006B1DE0, + "RS: Braille Divine Tome of Carim": 0x40000847, + "RS: Morne's Ring": 0x20004F1A, + "RS: Twin Dragon Greatshield": 0x01513820, + "RS: Heretic's Staff": 0x00C8F550, + + "RS: Sorcerer Hood": 0x11C9C380, + "RS: Sorcerer Robe": 0x11C9C768, + "RS: Sorcerer Gloves": 0x11C9CB50, + "RS: Sorcerer Trousers": 0x11C9CF38, + + "RS: Sage Ring": 0x20004F38, + + "RS: Fallen Knight Helm": 0x1121EAC0, + "RS: Fallen Knight Armor": 0x1121EEA8, + "RS: Fallen Knight Gauntlets": 0x1121F290, + "RS: Fallen Knight Trousers": 0x1121F678, + + "RS: Conjurator Hood": 0x149E8E60, + "RS: Conjurator Robe": 0x149E9248, + "RS: Conjurator Manchettes": 0x149E9630, + "RS: Conjurator Boots": 0x149E9A18, + + "RS: Great Swamp Pyromancy Tome": 0x4000084F, + + "RS: Great Club": 0x007B4A80, + "RS: Exile Greatsword": 0x005DD770, + + "RS: Farron Coal ": 0x40000837, + + "RS: Sellsword Twinblades": 0x00F42400, + "RS: Sellsword Helm": 0x11481060, + "RS: Sellsword Armor": 0x11481448, + "RS: Sellsword Gauntlet": 0x11481830, + "RS: Sellsword Trousers": 0x11481C18, + + "RS: Golden Falcon Shield": 0x01354BB0, + + "RS: Herald Helm": 0x114FB180, + "RS: Herald Armor": 0x114FB568, + "RS: Herald Gloves": 0x114FB950, + "RS: Herald Trousers": 0x114FBD38, + + "RS: Grass Crest Shield": 0x01437C80, + "RS: Soul of a Crystal Sage": 0x400002CB, + "RS: Great Swamp Ring": 0x20004F10, +} + +cathedral_of_the_deep_table = { + "CD: Paladin's Ashes": 0x4000083D, + "CD: Spider Shield": 0x01435570, + "CD: Crest Shield": 0x01430750, + "CD: Notched Whip": 0x00B7DE50, + "CD: Astora Greatsword": 0x005C9EF0, + "CD: Executioner's Greatsword": 0x0021DFE0, + "CD: Curse Ward Greatshield": 0x01518640, + "CD: Saint-tree Bellvine": 0x00C9DFB0, + "CD: Poisonbite Ring": 0x20004E8E, + + "CD: Lloyd's Sword Ring": 0x200050B4, + "CD: Seek Guidance": 0x40360420, + + "CD: Aldrich's Sapphire": 0x20005096, + "CD: Deep Braille Divine Tome": 0x40000860, + + "CD: Saint Bident": 0x008C1360, + "CD: Maiden Hood": 0x14BD12E0, + "CD: Maiden Robe": 0x14BD16C8, + "CD: Maiden Gloves": 0x14BD1AB0, + "CD: Maiden Skirt": 0x14BD1E98, + "CD: Drang Armor": 0x154E0C28, + "CD: Drang Gauntlets": 0x154E1010, + "CD: Drang Shoes": 0x154E13F8, + "CD: Drang Hammers": 0x00F61FD0, + "CD: Deep Ring": 0x20004F60, + + "CD: Archdeacon White Crown": 0x13EF1480, + "CD: Archdeacon Holy Garb": 0x13EF1868, + "CD: Archdeacon Skirt": 0x13EF2038, + + "CD: Arbalest": 0x00D662D0, + "CD: Small Doll": 0x400007D5, + "CD: Soul of the Deacons of the Deep": 0x400002D9, + "CD: Rosaria's Fingers Covenant": 0x20002760, +} + +farron_keep_table = { + "FK: Ragged Mask": 0x148F4C20, + "FK: Iron Flesh": 0x40251430, + "FK: Golden Scroll": 0x4000085C, + + "FK: Antiquated Dress": 0x15D76068, + "FK: Antiquated Gloves": 0x15D76450, + "FK: Antiquated Skirt": 0x15D76838, + + "FK: Nameless Knight Helm": 0x143B5FC0, + "FK: Nameless Knight Armor": 0x143B63A8, + "FK: Nameless Knight Gauntlets": 0x143B6790, + "FK: Nameless Knight Leggings": 0x143B6B78, + + "FK: Sunlight Talisman": 0x00CA54E0, + "FK: Wolf's Blood Swordgrass": 0x4000016E, + "FK: Greatsword": 0x005C50D0, + + "FK: Sage's Coal": 0x40000838, + "FK: Stone Parma": 0x01443FD0, + "FK: Sage's Scroll": 0x40000854, + "FK: Crown of Dusk": 0x15D75C80, + + "FK: Lingering Dragoncrest Ring": 0x20004F2E, + "FK: Pharis's Hat": 0x1487AB00, + "FK: Black Bow of Pharis": 0x00D7E970, + + "FK: Dreamchaser's Ashes": 0x4000083C, + "FK: Great Axe": 0x006B9310, + "FK: Dragon Crest Shield": 0x01432E60, + "FK: Lightning Spear": 0x40362B30, + "FK: Atonement": 0x4039ADA0, + "FK: Great Magic Weapon": 0x40140118, + "FK: Cinders of a Lord - Abyss Watcher": 0x4000084B, + "FK: Soul of the Blood of the Wolf": 0x400002CD, + "FK: Soul of a Stray Demon": 0x400002E7, + "FK: Watchdogs of Farron Covenant": 0x20002724, +} + +catacombs_of_carthus_table = { + "CC: Carthus Pyromancy Tome": 0x40000850, + "CC: Carthus Milkring": 0x20004FE2, + "CC: Grave Warden's Ashes": 0x4000083E, + "CC: Carthus Bloodring": 0x200050FA, + "CC: Grave Warden Pyromancy Tome": 0x40000853, + "CC: Old Sage's Blindfold": 0x11945BA0, + "CC: Witch's Ring": 0x20004F11, + "CC: Black Blade": 0x004CC070, + "CC: Soul of High Lord Wolnir": 0x400002D6, + "CC: Soul of a Demon": 0x400002E3, +} + +smouldering_lake_table = { + "SL: Shield of Want": 0x0144B500, + "SL: Speckled Stoneplate Ring": 0x20004E7A, + "SL: Dragonrider Bow": 0x00D6B0F0, + "SL: Lightning Stake": 0x40389C30, + "SL: Izalith Pyromancy Tome": 0x40000851, + "SL: Black Knight Sword": 0x005F5E10, + "SL: Quelana Pyromancy Tome": 0x40000852, + "SL: Toxic Mist": 0x4024F108, + "SL: White Hair Talisman": 0x00CAF120, + "SL: Izalith Staff": 0x00C96A80, + "SL: Sacred Flame": 0x40284880, + "SL: Fume Ultra Greatsword": 0x0060E4B0, + "SL: Black Iron Greatshield": 0x0150EA00, + "SL: Soul of the Old Demon King": 0x400002D0, + "SL: Knight Slayer's Ring": 0x20005000, +} + +irithyll_of_the_boreal_valley_table = { + "IBV: Dorhys' Gnawing": 0x40363EB8, + "IBV: Witchtree Branch": 0x00C94370, + "IBV: Magic Clutch Ring": 0x2000500A, + "IBV: Ring of the Sun's First Born": 0x20004F1B, + "IBV: Roster of Knights": 0x4000006C, + "IBV: Pontiff's Right Eye": 0x2000510E, + + "IBV: Yorshka's Spear": 0x008C3A70, + "IBV: Great Heal": 0x40356FB0, + + "IBV: Smough's Great Hammer": 0x007E30B0, + "IBV: Leo Ring": 0x20004EE8, + "IBV: Excrement-covered Ashes": 0x40000862, + + "IBV: Dark Stoneplate Ring": 0x20004E70, + "IBV: Easterner's Ashes": 0x40000868, + "IBV: Painting Guardian's Curved Sword": 0x003E6890, + "IBV: Painting Guardian Hood": 0x156C8CC0, + "IBV: Painting Guardian Gown": 0x156C90A8, + "IBV: Painting Guardian Gloves": 0x156C9490, + "IBV: Painting Guardian Waistcloth": 0x156C9878, + "IBV: Dragonslayer Greatbow": 0x00CF8500, + "IBV: Reversal Ring": 0x20005104, + "IBV: Brass Helm": 0x1501BD00, + "IBV: Brass Armor": 0x1501C0E8, + "IBV: Brass Gauntlets": 0x1501C4D0, + "IBV: Brass Leggings": 0x1501C8B8, + "IBV: Ring of Favor": 0x20004E3E, + "IBV: Golden Ritual Spear": 0x00C83200, + "IBV: Soul of Pontiff Sulyvahn": 0x400002D4, + "IBV: Aldrich Faithful Covenant": 0x2000272E, + "IBV: Drang Twinspears": 0x00F5AAA0, +} + +irithyll_dungeon_table = { + "ID: Bellowing Dragoncrest Ring": 0x20004F07, + "ID: Jailbreaker's Key": 0x400007D7, + "ID: Prisoner Chief's Ashes": 0x40000863, + "ID: Old Sorcerer Hat": 0x1496ED40, + "ID: Old Sorcerer Coat": 0x1496F128, + "ID: Old Sorcerer Gauntlets": 0x1496F510, + "ID: Old Sorcerer Boots": 0x1496F8F8, + "ID: Great Magic Shield": 0x40144F38, + + "ID: Dragon Torso Stone": 0x4000017A, + "ID: Lightning Blade": 0x4036C770, + "ID: Profaned Coal": 0x4000083A, + "ID: Xanthous Ashes": 0x40000864, + "ID: Old Cell Key": 0x400007DC, + "ID: Pickaxe": 0x007DE290, + "ID: Profaned Flame": 0x402575D8, + "ID: Covetous Gold Serpent Ring": 0x20004FA6, + "ID: Jailer's Key Ring": 0x400007D8, + "ID: Dusk Crown Ring": 0x20004F4C, + "ID: Dark Clutch Ring": 0x20005028, +} + +profaned_capital_table = { + "PC: Cursebite Ring": 0x20004E98, + "PC: Court Sorcerer Hood": 0x11BA8140, + "PC: Court Sorcerer Robe": 0x11BA8528, + "PC: Court Sorcerer Gloves": 0x11BA8910, + "PC: Court Sorcerer Trousers": 0x11BA8CF8, + "PC: Wrath of the Gods": 0x4035E0F8, + "PC: Logan's Scroll": 0x40000855, + "PC: Eleonora": 0x006CCB90, + "PC: Court Sorcerer's Staff": 0x00C91C60, + "PC: Greatshield of Glory": 0x01515F30, + "PC: Storm Ruler": 0x006132D0, + "PC: Cinders of a Lord - Yhorm the Giant": 0x4000084D, + "PC: Soul of Yhorm the Giant": 0x400002DC, +} + +anor_londo_table = { + "AL: Giant's Coal": 0x40000839, + "AL: Sun Princess Ring": 0x20004FBA, + "AL: Aldrich's Ruby": 0x2000508C, + "AL: Cinders of a Lord - Aldrich": 0x4000084C, + "AL: Soul of Aldrich": 0x400002D5, +} + +lothric_castle_table = { + "LC: Hood of Prayer": 0x13AA6A60, + "LC: Robe of Prayer": 0x13AA6E48, + "LC: Skirt of Prayer": 0x13AA7618, + + "LC: Sacred Bloom Shield": 0x013572C0, + "LC: Winged Knight Helm": 0x12EBAE40, + "LC: Winged Knight Armor": 0x12EBB228, + "LC: Winged Knight Gauntlets": 0x12EBB610, + "LC: Winged Knight Leggings": 0x12EBB9F8, + + "LC: Greatlance": 0x008A8CC0, + "LC: Sniper Crossbow": 0x00D83790, + "LC: Spirit Tree Crest Shield": 0x014466E0, + "LC: Red Tearstone Ring": 0x20004ECA, + "LC: Caitha's Chime": 0x00CA06C0, + "LC: Braille Divine Tome of Lothric": 0x40000848, + "LC: Knight's Ring": 0x20004FEC, + "LC: Sunlight Straight Sword": 0x00203230, + "LC: Soul of Dragonslayer Armour": 0x400002D1, + + # The Black Hand Gotthard corpse appears when you have defeated Yhorm and Aldrich and triggered the cutscene + "LC: Grand Archives Key": 0x400007DE, # On Black Hand Gotthard corpse + "LC: Gotthard Twinswords": 0x00F53570 # On Black Hand Gotthard corpse +} + +consumed_king_garden_table = { + "CKG: Dragonscale Ring": 0x2000515E, + "CKG: Shadow Mask": 0x14D3F640, + "CKG: Shadow Garb": 0x14D3FA28, + "CKG: Shadow Gauntlets": 0x14D3FE10, + "CKG: Shadow Leggings": 0x14D401F8, + "CKG: Claw": 0x00A7D8C0, + "CKG: Soul of Consumed Oceiros": 0x400002CE, + # "CKG: Path of the Dragon Gesture": 0x40002346, I can't technically randomize it as it is a gesture and not an item +} + +grand_archives_table = { + "GA: Avelyn": 0x00D6FF10, + "GA: Witch's Locks": 0x00B7B740, + "GA: Power Within": 0x40253B40, + "GA: Scholar Ring": 0x20004EB6, + "GA: Soul Stream": 0x4018B820, + "GA: Fleshbite Ring": 0x20004EA2, + "GA: Crystal Chime": 0x00CA2DD0, + "GA: Golden Wing Crest Shield": 0x0143CAA0, + "GA: Onikiri and Ubadachi": 0x00F58390, + "GA: Hunter's Ring": 0x20004FF6, + "GA: Divine Pillars of Light": 0x4038C340, + "GA: Cinders of a Lord - Lothric Prince": 0x4000084E, + "GA: Soul of the Twin Princes": 0x400002DB, + "GA: Sage's Crystal Staff": 0x00C8CE40, +} + +untended_graves_table = { + "UG: Ashen Estus Ring": 0x200050E6, + "UG: Black Knight Glaive": 0x009AE070, + "UG: Hornet Ring": 0x20004F9C, + "UG: Chaos Blade": 0x004C9960, + "UG: Blacksmith Hammer": 0x007E57C0, + "UG: Eyes of a Fire Keeper": 0x4000085A, + "UG: Coiled Sword Fragment": 0x4000015F, + "UG: Soul of Champion Gundyr": 0x400002C8, +} + +archdragon_peak_table = { + "AP: Lightning Clutch Ring": 0x20005014, + "AP: Ancient Dragon Greatshield": 0x013599D0, + "AP: Ring of Steel Protection": 0x20004E48, + "AP: Calamity Ring": 0x20005078, + "AP: Drakeblood Greatsword": 0x00609690, + "AP: Dragonslayer Spear": 0x008CAFA0, + + "AP: Thunder Stoneplate Ring": 0x20004E5C, + "AP: Great Magic Barrier": 0x40365628, + "AP: Dragon Chaser's Ashes": 0x40000867, + "AP: Twinkling Dragon Torso Stone": 0x40000184, + "AP: Dragonslayer Helm": 0x158B1140, + "AP: Dragonslayer Armor": 0x158B1528, + "AP: Dragonslayer Gauntlets": 0x158B1910, + "AP: Dragonslayer Leggings": 0x158B1CF8, + "AP: Ricard's Rapier": 0x002E3BF0, + "AP: Soul of the Nameless King": 0x400002D2, + "AP: Dragon Tooth": 0x007E09A0, + "AP: Havel's Greatshield": 0x013376F0, +} + +location_dictionary_table = {**cemetery_of_ash_table, **fire_link_shrine_table, **firelink_shrine_bell_tower_table, **high_wall_of_lothric, **undead_settlement_table, **road_of_sacrifice_table, + **cathedral_of_the_deep_table, **farron_keep_table, **catacombs_of_carthus_table, **smouldering_lake_table, **irithyll_of_the_boreal_valley_table, + **irithyll_dungeon_table, **profaned_capital_table, **anor_londo_table, **lothric_castle_table, **consumed_king_garden_table, + **grand_archives_table, **untended_graves_table, **archdragon_peak_table} diff --git a/worlds/dark_souls_3/docs/en_Dark Souls III.md b/worlds/dark_souls_3/docs/en_Dark Souls III.md new file mode 100644 index 0000000000..2effa5f124 --- /dev/null +++ b/worlds/dark_souls_3/docs/en_Dark Souls III.md @@ -0,0 +1,25 @@ +# Dark Souls III + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +In Dark Souls III, all unique items you can earn from a static corpse, a chest or the death of a Boss/NPC are randomized. +This exclude the upgrade materials such as the titanite shards, the estus shards and the consumables which remain at +the same location. I also added an option available from the settings page to randomize the level of the generated +weapons(from +0 to +10/+5) + +To beat the game you need to collect the 4 "Cinders of a Lord" randomized in the multiworld +and kill the final boss "Soul of Cinder" + +## What Dark Souls III items can appear in other players' worlds? + +Every unique items from Dark Souls III can appear in other player's worlds, such as a piece of armor, an upgraded weapon +or a key item. + +## What does another world's item look like in Dark Souls III? + +In Dark Souls III, items which need to be sent to other worlds appear as a Prism Stone. \ No newline at end of file diff --git a/worlds/dark_souls_3/docs/setup_en.md b/worlds/dark_souls_3/docs/setup_en.md new file mode 100644 index 0000000000..3d8606a5cf --- /dev/null +++ b/worlds/dark_souls_3/docs/setup_en.md @@ -0,0 +1,37 @@ +# Dark Souls III Randomizer Setup Guide + +## Required Software + +- [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/) +- [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases) + +## General Concept + +The Dark Souls III AP Client is a dinput8.dll triggered when launching Dark Souls III. This .dll file will launch a command +prompt where you can read information about your run and write any command to interact with the Archipelago server. + +The randomization is performed by the AP.json file, an output file generated by the Archipelago server. + +## Installation Procedures + + +**This mod can ban you permanently from the FromSoftware servers if used online.** + +This client has only been tested with the Official Steam version of the game (v1.15/1.35) not matter which DLCs are installed. + +Get the dinput8.dll from the [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases). +Then you need to add the two following files at the root folder of your game (e.g. "SteamLibrary\steamapps\common\DARK SOULS III\Game"): +- **dinput8.dll** +- **AP.json** : The .json file downloaded from the multiworld room or provided by the host, named AP-{ROOM_ID}.json, has to be renamed to AP.json. + +## Joining a MultiWorld Game + +1. Run DarkSoulsIII.exe or run the game through Steam +2. Type in "/connect {SERVER_IP}:{SERVER_PORT}" in the "Windows Command Prompt" that opened +3. Once connected, create a new game, choose a class and wait for the others before starting +4. You can quit and launch at anytime during a game + +## Where do I get a config file? + +The [Player Settings](/games/Dark%20Souls%20III/player-settings) page on the website allows you to +configure your personal settings and export them into a config file diff --git a/worlds/dark_souls_3/docs/setup_fr.md b/worlds/dark_souls_3/docs/setup_fr.md new file mode 100644 index 0000000000..f33b6951c5 --- /dev/null +++ b/worlds/dark_souls_3/docs/setup_fr.md @@ -0,0 +1,38 @@ +# Guide d'installation de Dark Souls III Randomizer + +## Logiciels requis + +- [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/) +- [Client AP de Dark Souls III](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases) + +## Concept général + +Le client Archipelago de Dark Souls III est un fichier dinput8.dll. Cette .dll va lancer une invite de commande Windows +permettant de lire des informations de la partie et écrire des commandes pour intéragir avec le serveur Archipelago. + +Le mélange des objets est réalisé par le fichier AP.json, un fichier généré par le serveur Archipelago. + +## Procédures d'installation + + +**Il y a des risques de bannissement permanent des serveurs FromSoftware si ce mod est utilisé en ligne.** + +Ce client a été testé sur la version Steam officielle du jeu (v1.15/1.35), peu importe les DLCs actuellement installés. + +Télécharger le fichier dinput8.dll disponible dans le [Client AP de Dark Souls III](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases). +Vous devez ensuite ajouter les deux fichiers suivants à la racine du jeu +(ex: "SteamLibrary\steamapps\common\DARK SOULS III\Game"): +- **dinput8.dll** +- **AP.json** : Le fichier .json téléchargé depuis la room ou donné par l'hôte de la partie, nommé AP-{ROOM_ID}.json, doit être renommé en AP.json. + +## Rejoindre une partie Multiworld + +1. Lancer DarkSoulsIII.exe ou lancer le jeu depuis Steam +2. Ecrire "/connect {SERVER_IP}:{SERVER_PORT}" dans l'invite de commande Windows ouverte au lancement du jeu +3. Une fois connecté, créez une nouvelle partie, choisissez une classe et attendez que les autres soient prêts avant de lancer +4. Vous pouvez quitter et lancer le jeu n'importe quand pendant une partie + +## Où trouver le fichier de configuration ? + +La [Page de configuration](/games/Dark%20Souls%20III/player-settings) sur le site vous permez de configurer vos +paramètres et de les exporter sous la forme d'un fichier. diff --git a/worlds/dkc3/Client.py b/worlds/dkc3/Client.py new file mode 100644 index 0000000000..7ab82187b0 --- /dev/null +++ b/worlds/dkc3/Client.py @@ -0,0 +1,239 @@ +import logging +import asyncio + +from NetUtils import ClientStatus, color +from SNIClient import Context, snes_buffered_write, snes_flush_writes, snes_read +from Patch import GAME_DKC3 + +snes_logger = logging.getLogger("SNES") + +# DKC3 - DKC3_TODO: Check these values +ROM_START = 0x000000 +WRAM_START = 0xF50000 +WRAM_SIZE = 0x20000 +SRAM_START = 0xE00000 + +SAVEDATA_START = WRAM_START + 0xF000 +SAVEDATA_SIZE = 0x500 + +DKC3_ROMNAME_START = 0x00FFC0 +DKC3_ROMHASH_START = 0x7FC0 +ROMNAME_SIZE = 0x15 +ROMHASH_SIZE = 0x15 + +DKC3_RECV_PROGRESS_ADDR = WRAM_START + 0x632 # DKC3_TODO: Find a permanent home for this +DKC3_FILE_NAME_ADDR = WRAM_START + 0x5D9 +DEATH_LINK_ACTIVE_ADDR = DKC3_ROMNAME_START + 0x15 # DKC3_TODO: Find a permanent home for this + + +async def deathlink_kill_player(ctx: Context): + pass + #if ctx.game == GAME_DKC3: + # DKC3_TODO: Handle Receiving Deathlink + + +async def dkc3_rom_init(ctx: Context): + if not ctx.rom: + ctx.finished_game = False + ctx.death_link_allow_survive = False + game_name = await snes_read(ctx, DKC3_ROMNAME_START, 0x15) + if game_name is None or game_name != b"DONKEY KONG COUNTRY 3": + return False + else: + ctx.game = GAME_DKC3 + ctx.items_handling = 0b111 # remote items + + rom = await snes_read(ctx, DKC3_ROMHASH_START, ROMHASH_SIZE) + if rom is None or rom == bytes([0] * ROMHASH_SIZE): + return False + + ctx.rom = rom + + #death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR, 1) + ## DKC3_TODO: Handle Deathlink + #if death_link: + # ctx.allow_collect = bool(death_link[0] & 0b100) + # await ctx.update_death_link(bool(death_link[0] & 0b1)) + return True + + +async def dkc3_game_watcher(ctx: Context): + if ctx.game == GAME_DKC3: + # DKC3_TODO: Handle Deathlink + save_file_name = await snes_read(ctx, DKC3_FILE_NAME_ADDR, 0x5) + if save_file_name is None or save_file_name[0] == 0x00: + # We haven't loaded a save file + return + + new_checks = [] + from worlds.dkc3.Rom import location_rom_data, item_rom_data, boss_location_ids, level_unlock_map + for loc_id, loc_data in location_rom_data.items(): + if loc_id not in ctx.locations_checked: + data = await snes_read(ctx, WRAM_START + loc_data[0], 1) + masked_data = data[0] & (1 << loc_data[1]) + bit_set = (masked_data != 0) + invert_bit = ((len(loc_data) >= 3) and loc_data[2]) + if bit_set != invert_bit: + # DKC3_TODO: Handle non-included checks + new_checks.append(loc_id) + + verify_save_file_name = await snes_read(ctx, DKC3_FILE_NAME_ADDR, 0x5) + if verify_save_file_name is None or verify_save_file_name[0] == 0x00 or verify_save_file_name != save_file_name: + # We have somehow exited the save file (or worse) + return + + rom = await snes_read(ctx, DKC3_ROMHASH_START, ROMHASH_SIZE) + if rom != ctx.rom: + ctx.rom = None + # We have somehow loaded a different ROM + return + + for new_check_id in new_checks: + ctx.locations_checked.add(new_check_id) + location = ctx.location_names[new_check_id] + snes_logger.info( + f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') + await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}]) + + # DKC3_TODO: Make this actually visually display new things received (ASM Hook required) + recv_count = await snes_read(ctx, DKC3_RECV_PROGRESS_ADDR, 1) + recv_index = recv_count[0] + + if recv_index < len(ctx.items_received): + item = ctx.items_received[recv_index] + recv_index += 1 + logging.info('Received %s from %s (%s) (%d/%d in list)' % ( + color(ctx.item_names[item.item], 'red', 'bold'), + color(ctx.player_names[item.player], 'yellow'), + ctx.location_names[item.location], recv_index, len(ctx.items_received))) + + snes_buffered_write(ctx, DKC3_RECV_PROGRESS_ADDR, bytes([recv_index])) + if item.item in item_rom_data: + item_count = await snes_read(ctx, WRAM_START + item_rom_data[item.item][0], 0x1) + new_item_count = item_count[0] + 1 + for address in item_rom_data[item.item]: + snes_buffered_write(ctx, WRAM_START + address, bytes([new_item_count])) + + # Handle Coin Displays + current_level = await snes_read(ctx, WRAM_START + 0x5E3, 0x5) + overworld_locked = ((await snes_read(ctx, WRAM_START + 0x5FC, 0x1))[0] == 0x01) + if item.item == 0xDC3002 and not overworld_locked and (current_level[0] == 0x0A and current_level[2] == 0x00 and current_level[4] == 0x03): + # Bazaar and Barter + item_count = await snes_read(ctx, WRAM_START + 0xB02, 0x1) + new_item_count = item_count[0] + 1 + snes_buffered_write(ctx, WRAM_START + 0xB02, bytes([new_item_count])) + elif item.item == 0xDC3002 and not overworld_locked and current_level[0] == 0x04: + # Swanky + item_count = await snes_read(ctx, WRAM_START + 0xA26, 0x1) + new_item_count = item_count[0] + 1 + snes_buffered_write(ctx, WRAM_START + 0xA26, bytes([new_item_count])) + elif item.item == 0xDC3003 and not overworld_locked and (current_level[0] == 0x0A and current_level[2] == 0x08 and current_level[4] == 0x01): + # Boomer + item_count = await snes_read(ctx, WRAM_START + 0xB02, 0x1) + new_item_count = item_count[0] + 1 + snes_buffered_write(ctx, WRAM_START + 0xB02, bytes([new_item_count])) + else: + # Handle Patch and Skis + if item.item == 0xDC3007: + num_upgrades = 1 + inventory = await snes_read(ctx, WRAM_START + 0x605, 0xF) + + if (inventory[0] & 0x02): + num_upgrades = 3 + elif (inventory[13] & 0x08) or (inventory[0] & 0x01): + num_upgrades = 2 + + if num_upgrades == 1: + snes_buffered_write(ctx, WRAM_START + 0x605, bytes([inventory[0] | 0x01])) + if inventory[4] == 0: + snes_buffered_write(ctx, WRAM_START + 0x609, bytes([0x01])) + elif inventory[6] == 0: + snes_buffered_write(ctx, WRAM_START + 0x60B, bytes([0x01])) + elif inventory[8] == 0: + snes_buffered_write(ctx, WRAM_START + 0x60D, bytes([0x01])) + elif inventory[10] == 0: + snes_buffered_write(ctx, WRAM_START + 0x60F, bytes([0x01])) + + cove_mekanos_progress = await snes_read(ctx, WRAM_START + 0x691, 0x2) + snes_buffered_write(ctx, WRAM_START + 0x691, bytes([cove_mekanos_progress[0] | 0x01])) + snes_buffered_write(ctx, WRAM_START + 0x692, bytes([cove_mekanos_progress[1] | 0x01])) + elif num_upgrades == 2: + snes_buffered_write(ctx, WRAM_START + 0x605, bytes([inventory[0] | 0x02])) + if inventory[4] == 0: + snes_buffered_write(ctx, WRAM_START + 0x609, bytes([0x02])) + elif inventory[6] == 0: + snes_buffered_write(ctx, WRAM_START + 0x60B, bytes([0x02])) + elif inventory[8] == 0: + snes_buffered_write(ctx, WRAM_START + 0x60D, bytes([0x02])) + elif inventory[10] == 0: + snes_buffered_write(ctx, WRAM_START + 0x60F, bytes([0x02])) + elif num_upgrades == 3: + snes_buffered_write(ctx, WRAM_START + 0x606, bytes([inventory[1] | 0x20])) + + k3_ridge_progress = await snes_read(ctx, WRAM_START + 0x693, 0x2) + snes_buffered_write(ctx, WRAM_START + 0x693, bytes([k3_ridge_progress[0] | 0x01])) + snes_buffered_write(ctx, WRAM_START + 0x694, bytes([k3_ridge_progress[1] | 0x01])) + elif item.item == 0xDC3000: + # Handle Victory + if not ctx.finished_game: + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + else: + print("Item Not Recognized: ", item.item) + pass + + await snes_flush_writes(ctx) + + # DKC3_TODO: This method of collect should work, however it does not unlock the next level correctly when previous is flagged + # Handle Collected Locations + for loc_id in ctx.checked_locations: + if loc_id not in ctx.locations_checked and loc_id not in boss_location_ids: + loc_data = location_rom_data[loc_id] + data = await snes_read(ctx, WRAM_START + loc_data[0], 1) + invert_bit = ((len(loc_data) >= 3) and loc_data[2]) + if not invert_bit: + masked_data = data[0] | (1 << loc_data[1]) + #print("Collected Location: ", hex(loc_data[0]), " | ", loc_data[1]) + snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data])) + + if (loc_data[1] == 1): + # Make the next levels accessible + level_id = loc_data[0] - 0x632 + levels_to_tiles = await snes_read(ctx, ROM_START + 0x3FF800, 0x60) + tiles_to_levels = await snes_read(ctx, ROM_START + 0x3FF860, 0x60) + tile_id = levels_to_tiles[level_id] if levels_to_tiles[level_id] != 0xFF else level_id + tile_id = tile_id + 0x632 + #print("Tile ID: ", hex(tile_id)) + if tile_id in level_unlock_map: + for next_level_address in level_unlock_map[tile_id]: + next_level_id = next_level_address - 0x632 + next_tile_id = tiles_to_levels[next_level_id] if tiles_to_levels[next_level_id] != 0xFF else next_level_id + next_tile_id = next_tile_id + 0x632 + #print("Next Level ID: ", hex(next_tile_id)) + next_data = await snes_read(ctx, WRAM_START + next_tile_id, 1) + snes_buffered_write(ctx, WRAM_START + next_tile_id, bytes([next_data[0] | 0x01])) + + await snes_flush_writes(ctx) + else: + masked_data = data[0] & ~(1 << loc_data[1]) + print("Collected Inverted Location: ", hex(loc_data[0]), " | ", loc_data[1]) + snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data])) + await snes_flush_writes(ctx) + ctx.locations_checked.add(loc_id) + + # Calculate Boomer Cost Text + boomer_cost_text = await snes_read(ctx, WRAM_START + 0xAAFD, 2) + if boomer_cost_text[0] == 0x31 and boomer_cost_text[1] == 0x35: + boomer_cost = await snes_read(ctx, ROM_START + 0x349857, 1) + boomer_cost_tens = int(boomer_cost[0]) // 10 + boomer_cost_ones = int(boomer_cost[0]) % 10 + snes_buffered_write(ctx, WRAM_START + 0xAAFD, bytes([0x30 + boomer_cost_tens, 0x30 + boomer_cost_ones])) + await snes_flush_writes(ctx) + + boomer_final_cost_text = await snes_read(ctx, WRAM_START + 0xAB9B, 2) + if boomer_final_cost_text[0] == 0x32 and boomer_final_cost_text[1] == 0x35: + boomer_cost = await snes_read(ctx, ROM_START + 0x349857, 1) + boomer_cost_tens = boomer_cost[0] // 10 + boomer_cost_ones = boomer_cost[0] % 10 + snes_buffered_write(ctx, WRAM_START + 0xAB9B, bytes([0x30 + boomer_cost_tens, 0x30 + boomer_cost_ones])) + await snes_flush_writes(ctx) diff --git a/worlds/dkc3/Items.py b/worlds/dkc3/Items.py new file mode 100644 index 0000000000..358873cd20 --- /dev/null +++ b/worlds/dkc3/Items.py @@ -0,0 +1,52 @@ +import typing + +from BaseClasses import Item, ItemClassification +from .Names import ItemName + + +class ItemData(typing.NamedTuple): + code: typing.Optional[int] + progression: bool + quantity: int = 1 + event: bool = False + + +class DKC3Item(Item): + game: str = "Donkey Kong Country 3" + + +# Separate tables for each type of item. +junk_table = { + ItemName.one_up_balloon: ItemData(0xDC3001, False), + ItemName.bear_coin: ItemData(0xDC3002, False), +} + +collectable_table = { + ItemName.bonus_coin: ItemData(0xDC3003, True), + ItemName.dk_coin: ItemData(0xDC3004, True), + ItemName.banana_bird: ItemData(0xDC3005, True), + ItemName.krematoa_cog: ItemData(0xDC3006, True), + ItemName.progressive_boat: ItemData(0xDC3007, True), +} + +inventory_table = { + ItemName.present: ItemData(0xDC3008, True), + ItemName.bowling_ball: ItemData(0xDC3009, True), + ItemName.shell: ItemData(0xDC300A, True), + ItemName.mirror: ItemData(0xDC300B, True), + ItemName.flower: ItemData(0xDC300C, True), + ItemName.wrench: ItemData(0xDC300D, True), +} + +event_table = { + ItemName.victory: ItemData(0xDC3000, True), +} + +# Complete item table. +item_table = { + **junk_table, + **collectable_table, + **event_table, +} + +lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code} diff --git a/worlds/dkc3/Levels.py b/worlds/dkc3/Levels.py new file mode 100644 index 0000000000..c3983ab817 --- /dev/null +++ b/worlds/dkc3/Levels.py @@ -0,0 +1,115 @@ + +from .Names import LocationName + +class DKC3Level(): + nameIDAddress: int + levelIDAddress: int + nameID: int + levelID: int + + def __init__(self, nameIDAddress: int, levelIDAddress: int, nameID: int, levelID: int): + self.nameIDAddress = nameIDAddress + self.levelIDAddress = levelIDAddress + self.nameID = nameID + self.levelID = levelID + + +level_dict = { + LocationName.lakeside_limbo_region: DKC3Level(0x34D19C, 0x34D19D, 0x01, 0x25), + LocationName.doorstop_dash_region: DKC3Level(0x34D1A7, 0x34D1A8, 0x02, 0x28), + LocationName.tidal_trouble_region: DKC3Level(0x34D1BD, 0x34D1BE, 0x04, 0x27), + LocationName.skiddas_row_region: DKC3Level(0x34D1C8, 0x34D1C9, 0x05, 0x2B), + LocationName.murky_mill_region: DKC3Level(0x34D1D3, 0x34D1D4, 0x0D, 0x2A), + + LocationName.barrel_shield_bust_up_region: DKC3Level(0x34D217, 0x34D218, 0x0B, 0x30), + LocationName.riverside_race_region: DKC3Level(0x34D22D, 0x34D22E, 0x0C, 0x32), + LocationName.squeals_on_wheels_region: DKC3Level(0x34D238, 0x34D239, 0x06, 0x29), + LocationName.springin_spiders_region: DKC3Level(0x34D24E, 0x34D24F, 0x0E, 0x2F), + LocationName.bobbing_barrel_brawl_region: DKC3Level(0x34D264, 0x34D265, 0x37, 0x34), + + LocationName.bazzas_blockade_region: DKC3Level(0x34D29D, 0x34D29E, 0x14, 0x35), + LocationName.rocket_barrel_ride_region: DKC3Level(0x34D2A8, 0x34D2A9, 0x15, 0x38), + LocationName.kreeping_klasps_region: DKC3Level(0x34D2BE, 0x34D2BF, 0x16, 0x26), + LocationName.tracker_barrel_trek_region: DKC3Level(0x34D2D4, 0x34D2D5, 0x17, 0x39), + LocationName.fish_food_frenzy_region: DKC3Level(0x34D2DF, 0x34D2E0, 0x18, 0x36), + + LocationName.fire_ball_frenzy_region: DKC3Level(0x34D30D, 0x34D30E, 0x1B, 0x3B), + LocationName.demolition_drain_pipe_region: DKC3Level(0x34D323, 0x34D324, 0x1D, 0x40), + LocationName.ripsaw_rage_region: DKC3Level(0x34D339, 0x34D33A, 0x1E, 0x2E), + LocationName.blazing_bazookas_region: DKC3Level(0x34D34F, 0x34D350, 0x1F, 0x3C), + LocationName.low_g_labyrinth_region: DKC3Level(0x34D35A, 0x34D35B, 0x20, 0x3E), + + LocationName.krevice_kreepers_region: DKC3Level(0x34D388, 0x34D389, 0x23, 0x41), + LocationName.tearaway_toboggan_region: DKC3Level(0x34D393, 0x34D394, 0x24, 0x2D), + LocationName.barrel_drop_bounce_region: DKC3Level(0x34D39E, 0x34D39F, 0x25, 0x3A), + LocationName.krack_shot_kroc_region: DKC3Level(0x34D3A9, 0x34D3AA, 0x26, 0x3D), + LocationName.lemguin_lunge_region: DKC3Level(0x34D3B4, 0x34D3B5, 0x27, 0x2C), + + LocationName.buzzer_barrage_region: DKC3Level(0x34D40E, 0x34D40F, 0x2B, 0x44), + LocationName.kong_fused_cliffs_region: DKC3Level(0x34D424, 0x34D425, 0x2D, 0x42), + LocationName.floodlit_fish_region: DKC3Level(0x34D42F, 0x34D430, 0x2E, 0x37), + LocationName.pothole_panic_region: DKC3Level(0x34D43A, 0x34D43B, 0x2F, 0x45), + LocationName.ropey_rumpus_region: DKC3Level(0x34D450, 0x34D451, 0x30, 0x43), + + LocationName.konveyor_rope_clash_region: DKC3Level(0x34D489, 0x34D48A, 0x38, 0x48), + LocationName.creepy_caverns_region: DKC3Level(0x34D49F, 0x34D4A0, 0x36, 0x46), + LocationName.lightning_lookout_region: DKC3Level(0x34D4AA, 0x34D4AB, 0x10, 0x33), + LocationName.koindozer_klamber_region: DKC3Level(0x34D4C0, 0x34D4C1, 0x34, 0x47), + LocationName.poisonous_pipeline_region: DKC3Level(0x34D4D6, 0x34D4D7, 0x39, 0x3F), + + LocationName.stampede_sprint_region: DKC3Level(0x34D51A, 0x34D51B, 0x3D, 0x49), + LocationName.criss_cross_cliffs_region: DKC3Level(0x34D525, 0x34D526, 0x3E, 0x4A), + LocationName.tyrant_twin_tussle_region: DKC3Level(0x34D530, 0x34D531, 0x3F, 0x4B), + LocationName.swoopy_salvo_region: DKC3Level(0x34D53B, 0x34D53C, 0x40, 0x31), + #LocationName.rocket_rush_region: DKC3Level(0x34D546, 0x34D547, 0x05, 0x4C), # Rocket Rush is not getting shuffled +} + +level_list = [ + LocationName.lakeside_limbo_region, + LocationName.doorstop_dash_region, + LocationName.tidal_trouble_region, + LocationName.skiddas_row_region, + LocationName.murky_mill_region, + + LocationName.barrel_shield_bust_up_region, + LocationName.riverside_race_region, + LocationName.squeals_on_wheels_region, + LocationName.springin_spiders_region, + LocationName.bobbing_barrel_brawl_region, + + LocationName.bazzas_blockade_region, + LocationName.rocket_barrel_ride_region, + LocationName.kreeping_klasps_region, + LocationName.tracker_barrel_trek_region, + LocationName.fish_food_frenzy_region, + + LocationName.fire_ball_frenzy_region, + LocationName.demolition_drain_pipe_region, + LocationName.ripsaw_rage_region, + LocationName.blazing_bazookas_region, + LocationName.low_g_labyrinth_region, + + LocationName.krevice_kreepers_region, + LocationName.tearaway_toboggan_region, + LocationName.barrel_drop_bounce_region, + LocationName.krack_shot_kroc_region, + LocationName.lemguin_lunge_region, + + LocationName.buzzer_barrage_region, + LocationName.kong_fused_cliffs_region, + LocationName.floodlit_fish_region, + LocationName.pothole_panic_region, + LocationName.ropey_rumpus_region, + + LocationName.konveyor_rope_clash_region, + LocationName.creepy_caverns_region, + LocationName.lightning_lookout_region, + LocationName.koindozer_klamber_region, + LocationName.poisonous_pipeline_region, + + LocationName.stampede_sprint_region, + LocationName.criss_cross_cliffs_region, + LocationName.tyrant_twin_tussle_region, + LocationName.swoopy_salvo_region, + #LocationName.rocket_rush_region, +] diff --git a/worlds/dkc3/Locations.py b/worlds/dkc3/Locations.py new file mode 100644 index 0000000000..e8d5409b15 --- /dev/null +++ b/worlds/dkc3/Locations.py @@ -0,0 +1,336 @@ +import typing + +from BaseClasses import Location +from .Names import LocationName + + +class DKC3Location(Location): + game: str = "Donkey Kong Country 3" + + progress_byte: int = 0x000000 + progress_bit: int = 0 + inverted_bit: bool = False + + def __init__(self, player: int, name: str = '', address: int = None, parent=None, prog_byte: int = None, prog_bit: int = None, invert: bool = False): + super().__init__(player, name, address, parent) + self.progress_byte = prog_byte + self.progress_bit = prog_bit + self.inverted_bit = invert + + +level_location_table = { + LocationName.lakeside_limbo_flag: 0xDC3000, + LocationName.lakeside_limbo_bonus_1: 0xDC3001, + LocationName.lakeside_limbo_bonus_2: 0xDC3002, + LocationName.lakeside_limbo_dk: 0xDC3003, + + LocationName.doorstop_dash_flag: 0xDC3004, + LocationName.doorstop_dash_bonus_1: 0xDC3005, + LocationName.doorstop_dash_bonus_2: 0xDC3006, + LocationName.doorstop_dash_dk: 0xDC3007, + + LocationName.tidal_trouble_flag: 0xDC3008, + LocationName.tidal_trouble_bonus_1: 0xDC3009, + LocationName.tidal_trouble_bonus_2: 0xDC300A, + LocationName.tidal_trouble_dk: 0xDC300B, + + LocationName.skiddas_row_flag: 0xDC300C, + LocationName.skiddas_row_bonus_1: 0xDC300D, + LocationName.skiddas_row_bonus_2: 0xDC300E, + LocationName.skiddas_row_dk: 0xDC300F, + + LocationName.murky_mill_flag: 0xDC3010, + LocationName.murky_mill_bonus_1: 0xDC3011, + LocationName.murky_mill_bonus_2: 0xDC3012, + LocationName.murky_mill_dk: 0xDC3013, + + LocationName.barrel_shield_bust_up_flag: 0xDC3014, + LocationName.barrel_shield_bust_up_bonus_1: 0xDC3015, + LocationName.barrel_shield_bust_up_bonus_2: 0xDC3016, + LocationName.barrel_shield_bust_up_dk: 0xDC3017, + + LocationName.riverside_race_flag: 0xDC3018, + LocationName.riverside_race_bonus_1: 0xDC3019, + LocationName.riverside_race_bonus_2: 0xDC301A, + LocationName.riverside_race_dk: 0xDC301B, + + LocationName.squeals_on_wheels_flag: 0xDC301C, + LocationName.squeals_on_wheels_bonus_1: 0xDC301D, + LocationName.squeals_on_wheels_bonus_2: 0xDC301E, + LocationName.squeals_on_wheels_dk: 0xDC301F, + + LocationName.springin_spiders_flag: 0xDC3020, + LocationName.springin_spiders_bonus_1: 0xDC3021, + LocationName.springin_spiders_bonus_2: 0xDC3022, + LocationName.springin_spiders_dk: 0xDC3023, + + LocationName.bobbing_barrel_brawl_flag: 0xDC3024, + LocationName.bobbing_barrel_brawl_bonus_1: 0xDC3025, + LocationName.bobbing_barrel_brawl_bonus_2: 0xDC3026, + LocationName.bobbing_barrel_brawl_dk: 0xDC3027, + + LocationName.bazzas_blockade_flag: 0xDC3028, + LocationName.bazzas_blockade_bonus_1: 0xDC3029, + LocationName.bazzas_blockade_bonus_2: 0xDC302A, + LocationName.bazzas_blockade_dk: 0xDC302B, + + LocationName.rocket_barrel_ride_flag: 0xDC302C, + LocationName.rocket_barrel_ride_bonus_1: 0xDC302D, + LocationName.rocket_barrel_ride_bonus_2: 0xDC302E, + LocationName.rocket_barrel_ride_dk: 0xDC302F, + + LocationName.kreeping_klasps_flag: 0xDC3030, + LocationName.kreeping_klasps_bonus_1: 0xDC3031, + LocationName.kreeping_klasps_bonus_2: 0xDC3032, + LocationName.kreeping_klasps_dk: 0xDC3033, + + LocationName.tracker_barrel_trek_flag: 0xDC3034, + LocationName.tracker_barrel_trek_bonus_1: 0xDC3035, + LocationName.tracker_barrel_trek_bonus_2: 0xDC3036, + LocationName.tracker_barrel_trek_dk: 0xDC3037, + + LocationName.fish_food_frenzy_flag: 0xDC3038, + LocationName.fish_food_frenzy_bonus_1: 0xDC3039, + LocationName.fish_food_frenzy_bonus_2: 0xDC303A, + LocationName.fish_food_frenzy_dk: 0xDC303B, + + LocationName.fire_ball_frenzy_flag: 0xDC303C, + LocationName.fire_ball_frenzy_bonus_1: 0xDC303D, + LocationName.fire_ball_frenzy_bonus_2: 0xDC303E, + LocationName.fire_ball_frenzy_dk: 0xDC303F, + + LocationName.demolition_drain_pipe_flag: 0xDC3040, + LocationName.demolition_drain_pipe_bonus_1: 0xDC3041, + LocationName.demolition_drain_pipe_bonus_2: 0xDC3042, + LocationName.demolition_drain_pipe_dk: 0xDC3043, + + LocationName.ripsaw_rage_flag: 0xDC3044, + LocationName.ripsaw_rage_bonus_1: 0xDC3045, + LocationName.ripsaw_rage_bonus_2: 0xDC3046, + LocationName.ripsaw_rage_dk: 0xDC3047, + + LocationName.blazing_bazookas_flag: 0xDC3048, + LocationName.blazing_bazookas_bonus_1: 0xDC3049, + LocationName.blazing_bazookas_bonus_2: 0xDC304A, + LocationName.blazing_bazookas_dk: 0xDC304B, + + LocationName.low_g_labyrinth_flag: 0xDC304C, + LocationName.low_g_labyrinth_bonus_1: 0xDC304D, + LocationName.low_g_labyrinth_bonus_2: 0xDC304E, + LocationName.low_g_labyrinth_dk: 0xDC304F, + + LocationName.krevice_kreepers_flag: 0xDC3050, + LocationName.krevice_kreepers_bonus_1: 0xDC3051, + LocationName.krevice_kreepers_bonus_2: 0xDC3052, + LocationName.krevice_kreepers_dk: 0xDC3053, + + LocationName.tearaway_toboggan_flag: 0xDC3054, + LocationName.tearaway_toboggan_bonus_1: 0xDC3055, + LocationName.tearaway_toboggan_bonus_2: 0xDC3056, + LocationName.tearaway_toboggan_dk: 0xDC3057, + + LocationName.barrel_drop_bounce_flag: 0xDC3058, + LocationName.barrel_drop_bounce_bonus_1: 0xDC3059, + LocationName.barrel_drop_bounce_bonus_2: 0xDC305A, + LocationName.barrel_drop_bounce_dk: 0xDC305B, + + LocationName.krack_shot_kroc_flag: 0xDC305C, + LocationName.krack_shot_kroc_bonus_1: 0xDC305D, + LocationName.krack_shot_kroc_bonus_2: 0xDC305E, + LocationName.krack_shot_kroc_dk: 0xDC305F, + + LocationName.lemguin_lunge_flag: 0xDC3060, + LocationName.lemguin_lunge_bonus_1: 0xDC3061, + LocationName.lemguin_lunge_bonus_2: 0xDC3062, + LocationName.lemguin_lunge_dk: 0xDC3063, + + LocationName.buzzer_barrage_flag: 0xDC3064, + LocationName.buzzer_barrage_bonus_1: 0xDC3065, + LocationName.buzzer_barrage_bonus_2: 0xDC3066, + LocationName.buzzer_barrage_dk: 0xDC3067, + + LocationName.kong_fused_cliffs_flag: 0xDC3068, + LocationName.kong_fused_cliffs_bonus_1: 0xDC3069, + LocationName.kong_fused_cliffs_bonus_2: 0xDC306A, + LocationName.kong_fused_cliffs_dk: 0xDC306B, + + LocationName.floodlit_fish_flag: 0xDC306C, + LocationName.floodlit_fish_bonus_1: 0xDC306D, + LocationName.floodlit_fish_bonus_2: 0xDC306E, + LocationName.floodlit_fish_dk: 0xDC306F, + + LocationName.pothole_panic_flag: 0xDC3070, + LocationName.pothole_panic_bonus_1: 0xDC3071, + LocationName.pothole_panic_bonus_2: 0xDC3072, + LocationName.pothole_panic_dk: 0xDC3073, + + LocationName.ropey_rumpus_flag: 0xDC3074, + LocationName.ropey_rumpus_bonus_1: 0xDC3075, + LocationName.ropey_rumpus_bonus_2: 0xDC3076, + LocationName.ropey_rumpus_dk: 0xDC3077, + + LocationName.konveyor_rope_clash_flag: 0xDC3078, + LocationName.konveyor_rope_clash_bonus_1: 0xDC3079, + LocationName.konveyor_rope_clash_bonus_2: 0xDC307A, + LocationName.konveyor_rope_clash_dk: 0xDC307B, + + LocationName.creepy_caverns_flag: 0xDC307C, + LocationName.creepy_caverns_bonus_1: 0xDC307D, + LocationName.creepy_caverns_bonus_2: 0xDC307E, + LocationName.creepy_caverns_dk: 0xDC307F, + + LocationName.lightning_lookout_flag: 0xDC3080, + LocationName.lightning_lookout_bonus_1: 0xDC3081, + LocationName.lightning_lookout_bonus_2: 0xDC3082, + LocationName.lightning_lookout_dk: 0xDC3083, + + LocationName.koindozer_klamber_flag: 0xDC3084, + LocationName.koindozer_klamber_bonus_1: 0xDC3085, + LocationName.koindozer_klamber_bonus_2: 0xDC3086, + LocationName.koindozer_klamber_dk: 0xDC3087, + + LocationName.poisonous_pipeline_flag: 0xDC3088, + LocationName.poisonous_pipeline_bonus_1: 0xDC3089, + LocationName.poisonous_pipeline_bonus_2: 0xDC308A, + LocationName.poisonous_pipeline_dk: 0xDC308B, + + LocationName.stampede_sprint_flag: 0xDC308C, + LocationName.stampede_sprint_bonus_1: 0xDC308D, + LocationName.stampede_sprint_bonus_2: 0xDC308E, + LocationName.stampede_sprint_bonus_3: 0xDC308F, + LocationName.stampede_sprint_dk: 0xDC3090, + + LocationName.criss_cross_cliffs_flag: 0xDC3091, + LocationName.criss_cross_cliffs_bonus_1: 0xDC3092, + LocationName.criss_cross_cliffs_bonus_2: 0xDC3093, + LocationName.criss_cross_cliffs_dk: 0xDC3094, + + LocationName.tyrant_twin_tussle_flag: 0xDC3095, + LocationName.tyrant_twin_tussle_bonus_1: 0xDC3096, + LocationName.tyrant_twin_tussle_bonus_2: 0xDC3097, + LocationName.tyrant_twin_tussle_bonus_3: 0xDC3098, + LocationName.tyrant_twin_tussle_dk: 0xDC3099, + + LocationName.swoopy_salvo_flag: 0xDC309A, + LocationName.swoopy_salvo_bonus_1: 0xDC309B, + LocationName.swoopy_salvo_bonus_2: 0xDC309C, + LocationName.swoopy_salvo_bonus_3: 0xDC309D, + LocationName.swoopy_salvo_dk: 0xDC309E, + + LocationName.rocket_rush_flag: 0xDC309F, + LocationName.rocket_rush_dk: 0xDC30A0, +} + +kong_location_table = { + LocationName.lakeside_limbo_kong: 0xDC3100, + LocationName.doorstop_dash_kong: 0xDC3104, + LocationName.tidal_trouble_kong: 0xDC3108, + LocationName.skiddas_row_kong: 0xDC310C, + LocationName.murky_mill_kong: 0xDC3110, + + LocationName.barrel_shield_bust_up_kong: 0xDC3114, + LocationName.riverside_race_kong: 0xDC3118, + LocationName.squeals_on_wheels_kong: 0xDC311C, + LocationName.springin_spiders_kong: 0xDC3120, + LocationName.bobbing_barrel_brawl_kong: 0xDC3124, + + LocationName.bazzas_blockade_kong: 0xDC3128, + LocationName.rocket_barrel_ride_kong: 0xDC312C, + LocationName.kreeping_klasps_kong: 0xDC3130, + LocationName.tracker_barrel_trek_kong: 0xDC3134, + LocationName.fish_food_frenzy_kong: 0xDC3138, + + LocationName.fire_ball_frenzy_kong: 0xDC313C, + LocationName.demolition_drain_pipe_kong: 0xDC3140, + LocationName.ripsaw_rage_kong: 0xDC3144, + LocationName.blazing_bazookas_kong: 0xDC3148, + LocationName.low_g_labyrinth_kong: 0xDC314C, + + LocationName.krevice_kreepers_kong: 0xDC3150, + LocationName.tearaway_toboggan_kong: 0xDC3154, + LocationName.barrel_drop_bounce_kong: 0xDC3158, + LocationName.krack_shot_kroc_kong: 0xDC315C, + LocationName.lemguin_lunge_kong: 0xDC3160, + + LocationName.buzzer_barrage_kong: 0xDC3164, + LocationName.kong_fused_cliffs_kong: 0xDC3168, + LocationName.floodlit_fish_kong: 0xDC316C, + LocationName.pothole_panic_kong: 0xDC3170, + LocationName.ropey_rumpus_kong: 0xDC3174, + + LocationName.konveyor_rope_clash_kong: 0xDC3178, + LocationName.creepy_caverns_kong: 0xDC317C, + LocationName.lightning_lookout_kong: 0xDC3180, + LocationName.koindozer_klamber_kong: 0xDC3184, + LocationName.poisonous_pipeline_kong: 0xDC3188, + + LocationName.stampede_sprint_kong: 0xDC318C, + LocationName.criss_cross_cliffs_kong: 0xDC3191, + LocationName.tyrant_twin_tussle_kong: 0xDC3195, + LocationName.swoopy_salvo_kong: 0xDC319A, +} + + +boss_location_table = { + LocationName.belchas_barn: 0xDC30A1, + LocationName.arichs_ambush: 0xDC30A2, + LocationName.squirts_showdown: 0xDC30A3, + LocationName.kaos_karnage: 0xDC30A4, + LocationName.bleaks_house: 0xDC30A5, + LocationName.barboss_barrier: 0xDC30A6, + LocationName.kastle_kaos: 0xDC30A7, + LocationName.knautilus: 0xDC30A8, +} + +secret_cave_location_table = { + LocationName.belchas_burrow: 0xDC30A9, + LocationName.kong_cave: 0xDC30AA, + LocationName.undercover_cove: 0xDC30AB, + LocationName.ks_cache: 0xDC30AC, + LocationName.hill_top_hoard: 0xDC30AD, + LocationName.bounty_beach: 0xDC30AE, + LocationName.smugglers_cove: 0xDC30AF, + LocationName.arichs_hoard: 0xDC30B0, + LocationName.bounty_bay: 0xDC30B1, + LocationName.sky_high_secret: 0xDC30B2, + LocationName.glacial_grotto: 0xDC30B3, + LocationName.cifftop_cache: 0xDC30B4, + LocationName.sewer_stockpile: 0xDC30B5, + LocationName.banana_bird_mother: 0xDC30B6, +} + +brothers_bear_location_table = { + LocationName.bazaars_general_store_1: 0xDC30B7, + LocationName.bazaars_general_store_2: 0xDC30B8, + LocationName.brambles_bungalow: 0xDC30B9, + LocationName.flower_spot: 0xDC30BA, + LocationName.barters_swap_shop: 0xDC30BB, + LocationName.barnacles_island: 0xDC30BC, + LocationName.blues_beach_hut: 0xDC30BD, + LocationName.blizzards_basecamp: 0xDC30BE, +} + +all_locations = { + **level_location_table, + **boss_location_table, + **secret_cave_location_table, + **brothers_bear_location_table, + **kong_location_table, +} + +location_table = {} + + +def setup_locations(world, player: int): + location_table = {**level_location_table, **boss_location_table, **secret_cave_location_table} + + if False:#world.include_trade_sequence[player].value: + location_table.update({**brothers_bear_location_table}) + + if world.kongsanity[player].value: + location_table.update({**kong_location_table}) + + return location_table + + +lookup_id_to_name: typing.Dict[int, str] = {id: name for name, _ in all_locations.items()} diff --git a/worlds/dkc3/Names/ItemName.py b/worlds/dkc3/Names/ItemName.py new file mode 100644 index 0000000000..d3395832f4 --- /dev/null +++ b/worlds/dkc3/Names/ItemName.py @@ -0,0 +1,21 @@ +# Junk Definitions +one_up_balloon = "1-Up Balloon" +bear_coin = "Bear Coin" + +# Collectable Definitions +bonus_coin = "Bonus Coin" +dk_coin = "DK Coin" +banana_bird = "Banana Bird" +krematoa_cog = "Krematoa Cog" + +# Inventory Definitions +progressive_boat = "Progressive Boat Upgrade" +present = "Present" +bowling_ball = "Bowling Ball" +shell = "Shell" +mirror = "Mirror" +flower = "Flupperius Petallus Pongus" +wrench = "No. 6 Wrench" + +# Other Definitions +victory = "Donkey Kong" diff --git a/worlds/dkc3/Names/LocationName.py b/worlds/dkc3/Names/LocationName.py new file mode 100644 index 0000000000..f79a25f143 --- /dev/null +++ b/worlds/dkc3/Names/LocationName.py @@ -0,0 +1,375 @@ +# Level Definitions +lakeside_limbo_flag = "Lakeside Limbo - Flag" +lakeside_limbo_kong = "Lakeside Limbo - KONG" +lakeside_limbo_bonus_1 = "Lakeside Limbo - Bonus 1" +lakeside_limbo_bonus_2 = "Lakeside Limbo - Bonus 2" +lakeside_limbo_dk = "Lakeside Limbo - DK Coin" + +doorstop_dash_flag = "Doorstop Dash - Flag" +doorstop_dash_kong = "Doorstop Dash - KONG" +doorstop_dash_bonus_1 = "Doorstop Dash - Bonus 1" +doorstop_dash_bonus_2 = "Doorstop Dash - Bonus 2" +doorstop_dash_dk = "Doorstop Dash - DK Coin" + +tidal_trouble_flag = "Tidal Trouble - Flag" +tidal_trouble_kong = "Tidal Trouble - KONG" +tidal_trouble_bonus_1 = "Tidal Trouble - Bonus 1" +tidal_trouble_bonus_2 = "Tidal Trouble - Bonus 2" +tidal_trouble_dk = "Tidal Trouble - DK Coin" + +skiddas_row_flag = "Skidda's Row - Flag" +skiddas_row_kong = "Skidda's Row - KONG" +skiddas_row_bonus_1 = "Skidda's Row - Bonus 1" +skiddas_row_bonus_2 = "Skidda's Row - Bonus 2" +skiddas_row_dk = "Skidda's Row - DK Coin" + +murky_mill_flag = "Murky Mill - Flag" +murky_mill_kong = "Murky Mill - KONG" +murky_mill_bonus_1 = "Murky Mill - Bonus 1" +murky_mill_bonus_2 = "Murky Mill - Bonus 2" +murky_mill_dk = "Murky Mill - DK Coin" + +barrel_shield_bust_up_flag = "Barrel Shield Bust-Up - Flag" +barrel_shield_bust_up_kong = "Barrel Shield Bust-Up - KONG" +barrel_shield_bust_up_bonus_1 = "Barrel Shield Bust-Up - Bonus 1" +barrel_shield_bust_up_bonus_2 = "Barrel Shield Bust-Up - Bonus 2" +barrel_shield_bust_up_dk = "Barrel Shield Bust-Up - DK Coin" + +riverside_race_flag = "Riverside Race - Flag" +riverside_race_kong = "Riverside Race - KONG" +riverside_race_bonus_1 = "Riverside Race - Bonus 1" +riverside_race_bonus_2 = "Riverside Race - Bonus 2" +riverside_race_dk = "Riverside Race - DK Coin" + +squeals_on_wheels_flag = "Squeals On Wheels - Flag" +squeals_on_wheels_kong = "Squeals On Wheels - KONG" +squeals_on_wheels_bonus_1 = "Squeals On Wheels - Bonus 1" +squeals_on_wheels_bonus_2 = "Squeals On Wheels - Bonus 2" +squeals_on_wheels_dk = "Squeals On Wheels - DK Coin" + +springin_spiders_flag = "Springin' Spiders - Flag" +springin_spiders_kong = "Springin' Spiders - KONG" +springin_spiders_bonus_1 = "Springin' Spiders - Bonus 1" +springin_spiders_bonus_2 = "Springin' Spiders - Bonus 2" +springin_spiders_dk = "Springin' Spiders - DK Coin" + +bobbing_barrel_brawl_flag = "Bobbing Barrel Brawl - Flag" +bobbing_barrel_brawl_kong = "Bobbing Barrel Brawl - KONG" +bobbing_barrel_brawl_bonus_1 = "Bobbing Barrel Brawl - Bonus 1" +bobbing_barrel_brawl_bonus_2 = "Bobbing Barrel Brawl - Bonus 2" +bobbing_barrel_brawl_dk = "Bobbing Barrel Brawl - DK Coin" + +bazzas_blockade_flag = "Bazza's Blockade - Flag" +bazzas_blockade_kong = "Bazza's Blockade - KONG" +bazzas_blockade_bonus_1 = "Bazza's Blockade - Bonus 1" +bazzas_blockade_bonus_2 = "Bazza's Blockade - Bonus 2" +bazzas_blockade_dk = "Bazza's Blockade - DK Coin" + +rocket_barrel_ride_flag = "Rocket Barrel Ride - Flag" +rocket_barrel_ride_kong = "Rocket Barrel Ride - KONG" +rocket_barrel_ride_bonus_1 = "Rocket Barrel Ride - Bonus 1" +rocket_barrel_ride_bonus_2 = "Rocket Barrel Ride - Bonus 2" +rocket_barrel_ride_dk = "Rocket Barrel Ride - DK Coin" + +kreeping_klasps_flag = "Kreeping Klasps - Flag" +kreeping_klasps_kong = "Kreeping Klasps - KONG" +kreeping_klasps_bonus_1 = "Kreeping Klasps - Bonus 1" +kreeping_klasps_bonus_2 = "Kreeping Klasps - Bonus 2" +kreeping_klasps_dk = "Kreeping Klasps - DK Coin" + +tracker_barrel_trek_flag = "Tracker Barrel Trek - Flag" +tracker_barrel_trek_kong = "Tracker Barrel Trek - KONG" +tracker_barrel_trek_bonus_1 = "Tracker Barrel Trek - Bonus 1" +tracker_barrel_trek_bonus_2 = "Tracker Barrel Trek - Bonus 2" +tracker_barrel_trek_dk = "Tracker Barrel Trek - DK Coin" + +fish_food_frenzy_flag = "Fish Food Frenzy - Flag" +fish_food_frenzy_kong = "Fish Food Frenzy - KONG" +fish_food_frenzy_bonus_1 = "Fish Food Frenzy - Bonus 1" +fish_food_frenzy_bonus_2 = "Fish Food Frenzy - Bonus 2" +fish_food_frenzy_dk = "Fish Food Frenzy - DK Coin" + +fire_ball_frenzy_flag = "Fire-Ball Frenzy - Flag" +fire_ball_frenzy_kong = "Fire-Ball Frenzy - KONG" +fire_ball_frenzy_bonus_1 = "Fire-Ball Frenzy - Bonus 1" +fire_ball_frenzy_bonus_2 = "Fire-Ball Frenzy - Bonus 2" +fire_ball_frenzy_dk = "Fire-Ball Frenzy - DK Coin" + +demolition_drain_pipe_flag = "Demolition Drain-Pipe - Flag" +demolition_drain_pipe_kong = "Demolition Drain-Pipe - KONG" +demolition_drain_pipe_bonus_1 = "Demolition Drain-Pipe - Bonus 1" +demolition_drain_pipe_bonus_2 = "Demolition Drain-Pipe - Bonus 2" +demolition_drain_pipe_dk = "Demolition Drain-Pipe - DK Coin" + +ripsaw_rage_flag = "Ripsaw Rage - Flag" +ripsaw_rage_kong = "Ripsaw Rage - KONG" +ripsaw_rage_bonus_1 = "Ripsaw Rage - Bonus 1" +ripsaw_rage_bonus_2 = "Ripsaw Rage - Bonus 2" +ripsaw_rage_dk = "Ripsaw Rage - DK Coin" + +blazing_bazookas_flag = "Blazing Bazukas - Flag" +blazing_bazookas_kong = "Blazing Bazukas - KONG" +blazing_bazookas_bonus_1 = "Blazing Bazukas - Bonus 1" +blazing_bazookas_bonus_2 = "Blazing Bazukas - Bonus 2" +blazing_bazookas_dk = "Blazing Bazukas - DK Coin" + +low_g_labyrinth_flag = "Low-G Labyrinth - Flag" +low_g_labyrinth_kong = "Low-G Labyrinth - KONG" +low_g_labyrinth_bonus_1 = "Low-G Labyrinth - Bonus 1" +low_g_labyrinth_bonus_2 = "Low-G Labyrinth - Bonus 2" +low_g_labyrinth_dk = "Low-G Labyrinth - DK Coin" + +krevice_kreepers_flag = "Krevice Kreepers - Flag" +krevice_kreepers_kong = "Krevice Kreepers - KONG" +krevice_kreepers_bonus_1 = "Krevice Kreepers - Bonus 1" +krevice_kreepers_bonus_2 = "Krevice Kreepers - Bonus 2" +krevice_kreepers_dk = "Krevice Kreepers - DK Coin" + +tearaway_toboggan_flag = "Tearaway Toboggan - Flag" +tearaway_toboggan_kong = "Tearaway Toboggan - KONG" +tearaway_toboggan_bonus_1 = "Tearaway Toboggan - Bonus 1" +tearaway_toboggan_bonus_2 = "Tearaway Toboggan - Bonus 2" +tearaway_toboggan_dk = "Tearaway Toboggan - DK Coin" + +barrel_drop_bounce_flag = "Barrel Drop Bounce - Flag" +barrel_drop_bounce_kong = "Barrel Drop Bounce - KONG" +barrel_drop_bounce_bonus_1 = "Barrel Drop Bounce - Bonus 1" +barrel_drop_bounce_bonus_2 = "Barrel Drop Bounce - Bonus 2" +barrel_drop_bounce_dk = "Barrel Drop Bounce - DK Coin" + +krack_shot_kroc_flag = "Krack-Shot Kroc - Flag" +krack_shot_kroc_kong = "Krack-Shot Kroc - KONG" +krack_shot_kroc_bonus_1 = "Krack-Shot Kroc - Bonus 1" +krack_shot_kroc_bonus_2 = "Krack-Shot Kroc - Bonus 2" +krack_shot_kroc_dk = "Krack-Shot Kroc - DK Coin" + +lemguin_lunge_flag = "Lemguin Lunge - Flag" +lemguin_lunge_kong = "Lemguin Lunge - KONG" +lemguin_lunge_bonus_1 = "Lemguin Lunge - Bonus 1" +lemguin_lunge_bonus_2 = "Lemguin Lunge - Bonus 2" +lemguin_lunge_dk = "Lemguin Lunge - DK Coin" + +buzzer_barrage_flag = "Buzzer Barrage - Flag" +buzzer_barrage_kong = "Buzzer Barrage - KONG" +buzzer_barrage_bonus_1 = "Buzzer Barrage - Bonus 1" +buzzer_barrage_bonus_2 = "Buzzer Barrage - Bonus 2" +buzzer_barrage_dk = "Buzzer Barrage - DK Coin" + +kong_fused_cliffs_flag = "Kong-Fused Cliffs - Flag" +kong_fused_cliffs_kong = "Kong-Fused Cliffs - KONG" +kong_fused_cliffs_bonus_1 = "Kong-Fused Cliffs - Bonus 1" +kong_fused_cliffs_bonus_2 = "Kong-Fused Cliffs - Bonus 2" +kong_fused_cliffs_dk = "Kong-Fused Cliffs - DK Coin" + +floodlit_fish_flag = "Floodlit Fish - Flag" +floodlit_fish_kong = "Floodlit Fish - KONG" +floodlit_fish_bonus_1 = "Floodlit Fish - Bonus 1" +floodlit_fish_bonus_2 = "Floodlit Fish - Bonus 2" +floodlit_fish_dk = "Floodlit Fish - DK Coin" + +pothole_panic_flag = "Pothole Panic - Flag" +pothole_panic_kong = "Pothole Panic - KONG" +pothole_panic_bonus_1 = "Pothole Panic - Bonus 1" +pothole_panic_bonus_2 = "Pothole Panic - Bonus 2" +pothole_panic_dk = "Pothole Panic - DK Coin" + +ropey_rumpus_flag = "Ropey Rumpus - Flag" +ropey_rumpus_kong = "Ropey Rumpus - KONG" +ropey_rumpus_bonus_1 = "Ropey Rumpus - Bonus 1" +ropey_rumpus_bonus_2 = "Ropey Rumpus - Bonus 2" +ropey_rumpus_dk = "Ropey Rumpus - DK Coin" + +konveyor_rope_clash_flag = "Konveyor Rope Klash - Flag" +konveyor_rope_clash_kong = "Konveyor Rope Klash - KONG" +konveyor_rope_clash_bonus_1 = "Konveyor Rope Klash - Bonus 1" +konveyor_rope_clash_bonus_2 = "Konveyor Rope Klash - Bonus 2" +konveyor_rope_clash_dk = "Konveyor Rope Klash - DK Coin" + +creepy_caverns_flag = "Creepy Caverns - Flag" +creepy_caverns_kong = "Creepy Caverns - KONG" +creepy_caverns_bonus_1 = "Creepy Caverns - Bonus 1" +creepy_caverns_bonus_2 = "Creepy Caverns - Bonus 2" +creepy_caverns_dk = "Creepy Caverns - DK Coin" + +lightning_lookout_flag = "Lightning Lookout - Flag" +lightning_lookout_kong = "Lightning Lookout - KONG" +lightning_lookout_bonus_1 = "Lightning Lookout - Bonus 1" +lightning_lookout_bonus_2 = "Lightning Lookout - Bonus 2" +lightning_lookout_dk = "Lightning Lookout - DK Coin" + +koindozer_klamber_flag = "Koindozer Klamber - Flag" +koindozer_klamber_kong = "Koindozer Klamber - KONG" +koindozer_klamber_bonus_1 = "Koindozer Klamber - Bonus 1" +koindozer_klamber_bonus_2 = "Koindozer Klamber - Bonus 2" +koindozer_klamber_dk = "Koindozer Klamber - DK Coin" + +poisonous_pipeline_flag = "Poisonous Pipeline - Flag" +poisonous_pipeline_kong = "Poisonous Pipeline - KONG" +poisonous_pipeline_bonus_1 = "Poisonous Pipeline - Bonus 1" +poisonous_pipeline_bonus_2 = "Poisonous Pipeline - Bonus 2" +poisonous_pipeline_dk = "Poisonous Pipeline - DK Coin" + +stampede_sprint_flag = "Stampede Sprint - Flag" +stampede_sprint_kong = "Stampede Sprint - KONG" +stampede_sprint_bonus_1 = "Stampede Sprint - Bonus 1" +stampede_sprint_bonus_2 = "Stampede Sprint - Bonus 2" +stampede_sprint_bonus_3 = "Stampede Sprint - Bonus 3" +stampede_sprint_dk = "Stampede Sprint - DK Coin" + +criss_cross_cliffs_flag = "Criss Kross Cliffs - Flag" +criss_cross_cliffs_kong = "Criss Kross Cliffs - KONG" +criss_cross_cliffs_bonus_1 = "Criss Kross Cliffs - Bonus 1" +criss_cross_cliffs_bonus_2 = "Criss Kross Cliffs - Bonus 2" +criss_cross_cliffs_dk = "Criss Kross Cliffs - DK Coin" + +tyrant_twin_tussle_flag = "Tyrant Twin Tussle - Flag" +tyrant_twin_tussle_kong = "Tyrant Twin Tussle - KONG" +tyrant_twin_tussle_bonus_1 = "Tyrant Twin Tussle - Bonus 1" +tyrant_twin_tussle_bonus_2 = "Tyrant Twin Tussle - Bonus 2" +tyrant_twin_tussle_bonus_3 = "Tyrant Twin Tussle - Bonus 3" +tyrant_twin_tussle_dk = "Tyrant Twin Tussle - DK Coin" + +swoopy_salvo_flag = "Swoopy Salvo - Flag" +swoopy_salvo_kong = "Swoopy Salvo - KONG" +swoopy_salvo_bonus_1 = "Swoopy Salvo - Bonus 1" +swoopy_salvo_bonus_2 = "Swoopy Salvo - Bonus 2" +swoopy_salvo_bonus_3 = "Swoopy Salvo - Bonus 3" +swoopy_salvo_dk = "Swoopy Salvo - DK Coin" + +rocket_rush_flag = "Rocket Rush - Flag" +rocket_rush_dk = "Rocket Rush - DK Coin" + +# Boss Definitions +belchas_barn = "Belcha's Barn" +arichs_ambush = "Arich's Ambush" +squirts_showdown = "Squirt's Showdown" +kaos_karnage = "KAOS Karnage" +bleaks_house = "Bleak's House" +barboss_barrier = "Barbos's Barrier" +kastle_kaos = "Kastle KAOS" +knautilus = "Knautilus" + + +# Banana Bird Cave Definitions +belchas_burrow = "Belcha's Burrow" +kong_cave = "Kong Cave" +undercover_cove = "Undercover Cove" +ks_cache = "K's Cache" +hill_top_hoard = "Hill-Top Hoard" +bounty_beach = "Bounty Beach" +smugglers_cove = "Smuggler's Cove" +arichs_hoard = "Arich's Hoard" +bounty_bay = "Bounty Bay" +sky_high_secret = "Sky-High Secret" +glacial_grotto = "Glacial Grotto" +cifftop_cache = "Clifftop Cache" +sewer_stockpile = "Sewer Stockpile" + +banana_bird_mother = "Banana Bird Mother" + + +# Brothers Bear Definitions +bazaars_general_store_1 = "Bazaar's General Store - 1" +bazaars_general_store_2 = "Bazaar's General Store - 2" +brambles_bungalow = "Bramble's Bungalow" +flower_spot = "Flower Spot" +barters_swap_shop = "Barter's Swap Shop" +barnacles_island = "Barnacle's Island" +blues_beach_hut = "Blue's Beach Hut" +blizzards_basecamp = "Bizzard's Basecamp" + + +# Region Definitions +menu_region = "Menu" +overworld_1_region = "Overworld 1" +overworld_2_region = "Overworld 2" +overworld_3_region = "Overworld 3" +overworld_4_region = "Overworld 4" + +bazaar_region = "Bazaar's General Store Region" +bramble_region = "Bramble's Bungalow Region" +flower_spot_region = "Flower Spot Region" +barter_region = "Barter's Swap Shop Region" +barnacle_region = "Barnacle's Island Region" +blue_region = "Blue's Beach Hut Region" +blizzard_region = "Bizzard's Basecamp Region" + +lake_orangatanga_region = "Lake_Orangatanga" +kremwood_forest_region = "Kremwood Forest" +cotton_top_cove_region = "Cotton-Top Cove" +mekanos_region = "Mekanos" +k3_region = "K3" +razor_ridge_region = "Razor Ridge" +kaos_kore_region = "KAOS Kore" +krematoa_region = "Krematoa" + +belchas_barn_region = "Belcha's Barn Region" +arichs_ambush_region = "Arich's Ambush Region" +squirts_showdown_region = "Squirt's Showdown Region" +kaos_karnage_region = "KAOS Karnage Region" +bleaks_house_region = "Bleak's House Region" +barboss_barrier_region = "Barbos's Barrier Region" +kastle_kaos_region = "Kastle KAOS Region" +knautilus_region = "Knautilus Region" + +belchas_burrow_region = "Belcha's Burrow Region" +kong_cave_region = "Kong Cave Region" +undercover_cove_region = "Undercover Cove Region" +ks_cache_region = "K's Cache Region" +hill_top_hoard_region = "Hill-Top Hoard Region" +bounty_beach_region = "Bounty Beach Region" +smugglers_cove_region = "Smuggler's Cove Region" +arichs_hoard_region = "Arich's Hoard Region" +bounty_bay_region = "Bounty Bay Region" +sky_high_secret_region = "Sky-High Secret Region" +glacial_grotto_region = "Glacial Grotto Region" +cifftop_cache_region = "Clifftop Cache Region" +sewer_stockpile_region = "Sewer Stockpile Region" + +lakeside_limbo_region = "Lakeside Limbo" +doorstop_dash_region = "Doorstop Dash" +tidal_trouble_region = "Tidal Trouble" +skiddas_row_region = "Skidda's Row" +murky_mill_region = "Murky Mill" + +barrel_shield_bust_up_region = "Barrel Shield Bust-Up" +riverside_race_region = "Riverside Race" +squeals_on_wheels_region = "Squeals On Wheels" +springin_spiders_region = "Springin' Spiders" +bobbing_barrel_brawl_region = "Bobbing Barrel Brawl" + +bazzas_blockade_region = "Bazza's Blockade" +rocket_barrel_ride_region = "Rocket Barrel Ride" +kreeping_klasps_region = "Kreeping Klasps" +tracker_barrel_trek_region = "Tracker Barrel Trek" +fish_food_frenzy_region = "Fish Food Frenzy" + +fire_ball_frenzy_region = "Fire-Ball Frenzy" +demolition_drain_pipe_region = "Demolition Drain-Pipe" +ripsaw_rage_region = "Ripsaw Rage" +blazing_bazookas_region = "Blazing Bazukas" +low_g_labyrinth_region = "Low-G Labyrinth" + +krevice_kreepers_region = "Krevice Kreepers" +tearaway_toboggan_region = "Tearaway Toboggan" +barrel_drop_bounce_region = "Barrel Drop Bounce" +krack_shot_kroc_region = "Krack-Shot Kroc" +lemguin_lunge_region = "Lemguin Lunge" + +buzzer_barrage_region = "Buzzer Barrage" +kong_fused_cliffs_region = "Kong-Fused Cliffs" +floodlit_fish_region = "Floodlit Fish" +pothole_panic_region = "Pothole Panic" +ropey_rumpus_region = "Ropey Rumpus" + +konveyor_rope_clash_region = "Konveyor Rope Klash" +creepy_caverns_region = "Creepy Caverns" +lightning_lookout_region = "Lightning Lookout" +koindozer_klamber_region = "Koindozer Klamber" +poisonous_pipeline_region = "Poisonous Pipeline" + +stampede_sprint_region = "Stampede Sprint" +criss_cross_cliffs_region = "Criss Kross Cliffs" +tyrant_twin_tussle_region = "Tyrant Twin Tussle" +swoopy_salvo_region = "Swoopy Salvo" +rocket_rush_region = "Rocket Rush" diff --git a/worlds/dkc3/Options.py b/worlds/dkc3/Options.py new file mode 100644 index 0000000000..7c0f532cfc --- /dev/null +++ b/worlds/dkc3/Options.py @@ -0,0 +1,178 @@ +import typing + +from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, OptionList + + +class Goal(Choice): + """ + Determines the goal of the seed + Knautilus: Scuttle the Knautilus in Krematoa and defeat Baron K. Roolenstein + Banana Bird Hunt: Find a certain number of Banana Birds and rescue their mother + """ + display_name = "Goal" + option_knautilus = 0 + option_banana_bird_hunt = 1 + default = 0 + + +class IncludeTradeSequence(Toggle): + """ + Allows logic to place items at the various steps of the trade sequence + """ + display_name = "Include Trade Sequence" + + +class DKCoinsForGyrocopter(Range): + """ + How many DK Coins are needed to unlock the Gyrocopter + Note: Achieving this number before unlocking the Turbo Ski will cause the game to grant you a + one-time upgrade to the next non-unlocked boat, until you return to Funky. Logic does not assume + that you will use this. + """ + display_name = "DK Coins for Gyrocopter" + range_start = 10 + range_end = 41 + default = 30 + + +class KrematoaBonusCoinCost(Range): + """ + How many Bonus Coins are needed to unlock each level in Krematoa + """ + display_name = "Krematoa Bonus Coins Cost" + range_start = 1 + range_end = 17 + default = 15 + + +class PercentageOfExtraBonusCoins(Range): + """ + What Percentage of unneeded Bonus Coins are included in the item pool + """ + display_name = "Percentage of Extra Bonus Coins" + range_start = 0 + range_end = 100 + default = 100 + + +class NumberOfBananaBirds(Range): + """ + How many Banana Birds are put into the item pool + """ + display_name = "Number of Banana Birds" + range_start = 5 + range_end = 15 + default = 15 + + +class PercentageOfBananaBirds(Range): + """ + What Percentage of Banana Birds in the item pool are required for Banana Bird Hunt + """ + display_name = "Percentage of Banana Birds" + range_start = 20 + range_end = 100 + default = 100 + + +class KONGsanity(Toggle): + """ + Whether collecting all four KONG letters in each level grants a check + """ + display_name = "KONGsanity" + + +class LevelShuffle(Toggle): + """ + Whether levels are shuffled + """ + display_name = "Level Shuffle" + + +class Difficulty(Choice): + """ + Which Difficulty Level to use + NORML: The Normal Difficulty + HARDR: Many DK Barrels are removed + TUFST: Most DK Barrels and all Midway Barrels are removed + """ + display_name = "Difficulty" + option_norml = 0 + option_hardr = 1 + option_tufst = 2 + default = 0 + + @classmethod + def get_option_name(cls, value) -> str: + if cls.auto_display_name: + return cls.name_lookup[value].upper() + else: + return cls.name_lookup[value] + + +class Autosave(DefaultOnToggle): + """ + Whether the game should autosave after each level + """ + display_name = "Autosave" + + +class MERRY(Toggle): + """ + Whether the Bonus Barrels will be Christmas-themed + """ + display_name = "MERRY" + + +class MusicShuffle(Toggle): + """ + Whether music is shuffled + """ + display_name = "Music Shuffle" + + +class KongPaletteSwap(Choice): + """ + Which Palette to use for the Kongs + """ + display_name = "Kong Palette Swap" + option_default = 0 + option_purple = 1 + option_spooky = 2 + option_dark = 3 + option_chocolate = 4 + option_shadow = 5 + option_red_gold = 6 + option_gbc = 7 + option_halloween = 8 + default = 0 + + +class StartingLifeCount(Range): + """ + How many extra lives to start the game with + """ + display_name = "Starting Life Count" + range_start = 1 + range_end = 99 + default = 5 + + +dkc3_options: typing.Dict[str, type(Option)] = { + #"death_link": DeathLink, # Disabled + "goal": Goal, + #"include_trade_sequence": IncludeTradeSequence, # Disabled + "dk_coins_for_gyrocopter": DKCoinsForGyrocopter, + "krematoa_bonus_coin_cost": KrematoaBonusCoinCost, + "percentage_of_extra_bonus_coins": PercentageOfExtraBonusCoins, + "number_of_banana_birds": NumberOfBananaBirds, + "percentage_of_banana_birds": PercentageOfBananaBirds, + "kongsanity": KONGsanity, + "level_shuffle": LevelShuffle, + "difficulty": Difficulty, + "autosave": Autosave, + "merry": MERRY, + "music_shuffle": MusicShuffle, + "kong_palette_swap": KongPaletteSwap, + "starting_life_count": StartingLifeCount, +} diff --git a/worlds/dkc3/Regions.py b/worlds/dkc3/Regions.py new file mode 100644 index 0000000000..e33ff38c15 --- /dev/null +++ b/worlds/dkc3/Regions.py @@ -0,0 +1,952 @@ +import typing + +from BaseClasses import MultiWorld, Region, Entrance +from .Items import DKC3Item +from .Locations import DKC3Location +from .Names import LocationName, ItemName + + +def create_regions(world, player: int, active_locations): + menu_region = create_region(world, player, active_locations, 'Menu', None, None) + + overworld_1_region_locations = {} + if world.goal[player] != "knautilus": + overworld_1_region_locations.update({LocationName.banana_bird_mother: []}) + overworld_1_region = create_region(world, player, active_locations, LocationName.overworld_1_region, + overworld_1_region_locations, None) + + overworld_2_region_locations = {} + overworld_2_region = create_region(world, player, active_locations, LocationName.overworld_2_region, + overworld_2_region_locations, None) + + overworld_3_region_locations = {} + overworld_3_region = create_region(world, player, active_locations, LocationName.overworld_3_region, + overworld_3_region_locations, None) + + overworld_4_region_locations = {} + overworld_4_region = create_region(world, player, active_locations, LocationName.overworld_4_region, + overworld_4_region_locations, None) + + + lake_orangatanga_region = create_region(world, player, active_locations, LocationName.lake_orangatanga_region, None, None) + kremwood_forest_region = create_region(world, player, active_locations, LocationName.kremwood_forest_region, None, None) + cotton_top_cove_region = create_region(world, player, active_locations, LocationName.cotton_top_cove_region, None, None) + mekanos_region = create_region(world, player, active_locations, LocationName.mekanos_region, None, None) + k3_region = create_region(world, player, active_locations, LocationName.k3_region, None, None) + razor_ridge_region = create_region(world, player, active_locations, LocationName.razor_ridge_region, None, None) + kaos_kore_region = create_region(world, player, active_locations, LocationName.kaos_kore_region, None, None) + krematoa_region = create_region(world, player, active_locations, LocationName.krematoa_region, None, None) + + + lakeside_limbo_region_locations = { + LocationName.lakeside_limbo_flag : [0x657, 1], + LocationName.lakeside_limbo_bonus_1 : [0x657, 2], + LocationName.lakeside_limbo_bonus_2 : [0x657, 3], + LocationName.lakeside_limbo_dk : [0x657, 5], + } + if world.kongsanity[player]: + lakeside_limbo_region_locations[LocationName.lakeside_limbo_kong] = [] + lakeside_limbo_region = create_region(world, player, active_locations, LocationName.lakeside_limbo_region, + lakeside_limbo_region_locations, None) + + doorstop_dash_region_locations = { + LocationName.doorstop_dash_flag : [0x65A, 1], + LocationName.doorstop_dash_bonus_1 : [0x65A, 2], + LocationName.doorstop_dash_bonus_2 : [0x65A, 3], + LocationName.doorstop_dash_dk : [0x65A, 5], + } + if world.kongsanity[player]: + doorstop_dash_region_locations[LocationName.doorstop_dash_kong] = [] + doorstop_dash_region = create_region(world, player, active_locations, LocationName.doorstop_dash_region, + doorstop_dash_region_locations, None) + + tidal_trouble_region_locations = { + LocationName.tidal_trouble_flag : [0x659, 1], + LocationName.tidal_trouble_bonus_1 : [0x659, 2], + LocationName.tidal_trouble_bonus_2 : [0x659, 3], + LocationName.tidal_trouble_dk : [0x659, 5], + } + if world.kongsanity[player]: + tidal_trouble_region_locations[LocationName.tidal_trouble_kong] = [] + tidal_trouble_region = create_region(world, player, active_locations, LocationName.tidal_trouble_region, + tidal_trouble_region_locations, None) + + skiddas_row_region_locations = { + LocationName.skiddas_row_flag : [0x65D, 1], + LocationName.skiddas_row_bonus_1 : [0x65D, 2], + LocationName.skiddas_row_bonus_2 : [0x65D, 3], + LocationName.skiddas_row_dk : [0x65D, 5], + } + if world.kongsanity[player]: + skiddas_row_region_locations[LocationName.skiddas_row_kong] = [] + skiddas_row_region = create_region(world, player, active_locations, LocationName.skiddas_row_region, + skiddas_row_region_locations, None) + + murky_mill_region_locations = { + LocationName.murky_mill_flag : [0x65C, 1], + LocationName.murky_mill_bonus_1 : [0x65C, 2], + LocationName.murky_mill_bonus_2 : [0x65C, 3], + LocationName.murky_mill_dk : [0x65C, 5], + } + if world.kongsanity[player]: + murky_mill_region_locations[LocationName.murky_mill_kong] = [] + murky_mill_region = create_region(world, player, active_locations, LocationName.murky_mill_region, + murky_mill_region_locations, None) + + barrel_shield_bust_up_region_locations = { + LocationName.barrel_shield_bust_up_flag : [0x662, 1], + LocationName.barrel_shield_bust_up_bonus_1 : [0x662, 2], + LocationName.barrel_shield_bust_up_bonus_2 : [0x662, 3], + LocationName.barrel_shield_bust_up_dk : [0x662, 5], + } + if world.kongsanity[player]: + barrel_shield_bust_up_region_locations[LocationName.barrel_shield_bust_up_kong] = [] + barrel_shield_bust_up_region = create_region(world, player, active_locations, LocationName.barrel_shield_bust_up_region, + barrel_shield_bust_up_region_locations, None) + + riverside_race_region_locations = { + LocationName.riverside_race_flag : [0x664, 1], + LocationName.riverside_race_bonus_1 : [0x664, 2], + LocationName.riverside_race_bonus_2 : [0x664, 3], + LocationName.riverside_race_dk : [0x664, 5], + } + if world.kongsanity[player]: + riverside_race_region_locations[LocationName.riverside_race_kong] = [] + riverside_race_region = create_region(world, player, active_locations, LocationName.riverside_race_region, + riverside_race_region_locations, None) + + squeals_on_wheels_region_locations = { + LocationName.squeals_on_wheels_flag : [0x65B, 1], + LocationName.squeals_on_wheels_bonus_1 : [0x65B, 2], + LocationName.squeals_on_wheels_bonus_2 : [0x65B, 3], + LocationName.squeals_on_wheels_dk : [0x65B, 5], + } + if world.kongsanity[player]: + squeals_on_wheels_region_locations[LocationName.squeals_on_wheels_kong] = [] + squeals_on_wheels_region = create_region(world, player, active_locations, LocationName.squeals_on_wheels_region, + squeals_on_wheels_region_locations, None) + + springin_spiders_region_locations = { + LocationName.springin_spiders_flag : [0x661, 1], + LocationName.springin_spiders_bonus_1 : [0x661, 2], + LocationName.springin_spiders_bonus_2 : [0x661, 3], + LocationName.springin_spiders_dk : [0x661, 5], + } + if world.kongsanity[player]: + springin_spiders_region_locations[LocationName.springin_spiders_kong] = [] + springin_spiders_region = create_region(world, player, active_locations, LocationName.springin_spiders_region, + springin_spiders_region_locations, None) + + bobbing_barrel_brawl_region_locations = { + LocationName.bobbing_barrel_brawl_flag : [0x666, 1], + LocationName.bobbing_barrel_brawl_bonus_1 : [0x666, 2], + LocationName.bobbing_barrel_brawl_bonus_2 : [0x666, 3], + LocationName.bobbing_barrel_brawl_dk : [0x666, 5], + } + if world.kongsanity[player]: + bobbing_barrel_brawl_region_locations[LocationName.bobbing_barrel_brawl_kong] = [] + bobbing_barrel_brawl_region = create_region(world, player, active_locations, LocationName.bobbing_barrel_brawl_region, + bobbing_barrel_brawl_region_locations, None) + + bazzas_blockade_region_locations = { + LocationName.bazzas_blockade_flag : [0x667, 1], + LocationName.bazzas_blockade_bonus_1 : [0x667, 2], + LocationName.bazzas_blockade_bonus_2 : [0x667, 3], + LocationName.bazzas_blockade_dk : [0x667, 5], + } + if world.kongsanity[player]: + bazzas_blockade_region_locations[LocationName.bazzas_blockade_kong] = [] + bazzas_blockade_region = create_region(world, player, active_locations, LocationName.bazzas_blockade_region, + bazzas_blockade_region_locations, None) + + rocket_barrel_ride_region_locations = { + LocationName.rocket_barrel_ride_flag : [0x66A, 1], + LocationName.rocket_barrel_ride_bonus_1 : [0x66A, 2], + LocationName.rocket_barrel_ride_bonus_2 : [0x66A, 3], + LocationName.rocket_barrel_ride_dk : [0x66A, 5], + } + if world.kongsanity[player]: + rocket_barrel_ride_region_locations[LocationName.rocket_barrel_ride_kong] = [] + rocket_barrel_ride_region = create_region(world, player, active_locations, LocationName.rocket_barrel_ride_region, + rocket_barrel_ride_region_locations, None) + + kreeping_klasps_region_locations = { + LocationName.kreeping_klasps_flag : [0x658, 1], + LocationName.kreeping_klasps_bonus_1 : [0x658, 2], + LocationName.kreeping_klasps_bonus_2 : [0x658, 3], + LocationName.kreeping_klasps_dk : [0x658, 5], + } + if world.kongsanity[player]: + kreeping_klasps_region_locations[LocationName.kreeping_klasps_kong] = [] + kreeping_klasps_region = create_region(world, player, active_locations, LocationName.kreeping_klasps_region, + kreeping_klasps_region_locations, None) + + tracker_barrel_trek_region_locations = { + LocationName.tracker_barrel_trek_flag : [0x66B, 1], + LocationName.tracker_barrel_trek_bonus_1 : [0x66B, 2], + LocationName.tracker_barrel_trek_bonus_2 : [0x66B, 3], + LocationName.tracker_barrel_trek_dk : [0x66B, 5], + } + if world.kongsanity[player]: + tracker_barrel_trek_region_locations[LocationName.tracker_barrel_trek_kong] = [] + tracker_barrel_trek_region = create_region(world, player, active_locations, LocationName.tracker_barrel_trek_region, + tracker_barrel_trek_region_locations, None) + + fish_food_frenzy_region_locations = { + LocationName.fish_food_frenzy_flag : [0x668, 1], + LocationName.fish_food_frenzy_bonus_1 : [0x668, 2], + LocationName.fish_food_frenzy_bonus_2 : [0x668, 3], + LocationName.fish_food_frenzy_dk : [0x668, 5], + } + if world.kongsanity[player]: + fish_food_frenzy_region_locations[LocationName.fish_food_frenzy_kong] = [] + fish_food_frenzy_region = create_region(world, player, active_locations, LocationName.fish_food_frenzy_region, + fish_food_frenzy_region_locations, None) + + fire_ball_frenzy_region_locations = { + LocationName.fire_ball_frenzy_flag : [0x66D, 1], + LocationName.fire_ball_frenzy_bonus_1 : [0x66D, 2], + LocationName.fire_ball_frenzy_bonus_2 : [0x66D, 3], + LocationName.fire_ball_frenzy_dk : [0x66D, 5], + } + if world.kongsanity[player]: + fire_ball_frenzy_region_locations[LocationName.fire_ball_frenzy_kong] = [] + fire_ball_frenzy_region = create_region(world, player, active_locations, LocationName.fire_ball_frenzy_region, + fire_ball_frenzy_region_locations, None) + + demolition_drain_pipe_region_locations = { + LocationName.demolition_drain_pipe_flag : [0x672, 1], + LocationName.demolition_drain_pipe_bonus_1 : [0x672, 2], + LocationName.demolition_drain_pipe_bonus_2 : [0x672, 3], + LocationName.demolition_drain_pipe_dk : [0x672, 5], + } + if world.kongsanity[player]: + demolition_drain_pipe_region_locations[LocationName.demolition_drain_pipe_kong] = [] + demolition_drain_pipe_region = create_region(world, player, active_locations, LocationName.demolition_drain_pipe_region, + demolition_drain_pipe_region_locations, None) + + ripsaw_rage_region_locations = { + LocationName.ripsaw_rage_flag : [0x660, 1], + LocationName.ripsaw_rage_bonus_1 : [0x660, 2], + LocationName.ripsaw_rage_bonus_2 : [0x660, 3], + LocationName.ripsaw_rage_dk : [0x660, 5], + } + if world.kongsanity[player]: + ripsaw_rage_region_locations[LocationName.ripsaw_rage_kong] = [] + ripsaw_rage_region = create_region(world, player, active_locations, LocationName.ripsaw_rage_region, + ripsaw_rage_region_locations, None) + + blazing_bazookas_region_locations = { + LocationName.blazing_bazookas_flag : [0x66E, 1], + LocationName.blazing_bazookas_bonus_1 : [0x66E, 2], + LocationName.blazing_bazookas_bonus_2 : [0x66E, 3], + LocationName.blazing_bazookas_dk : [0x66E, 5], + } + if world.kongsanity[player]: + blazing_bazookas_region_locations[LocationName.blazing_bazookas_kong] = [] + blazing_bazookas_region = create_region(world, player, active_locations, LocationName.blazing_bazookas_region, + blazing_bazookas_region_locations, None) + + low_g_labyrinth_region_locations = { + LocationName.low_g_labyrinth_flag : [0x670, 1], + LocationName.low_g_labyrinth_bonus_1 : [0x670, 2], + LocationName.low_g_labyrinth_bonus_2 : [0x670, 3], + LocationName.low_g_labyrinth_dk : [0x670, 5], + } + if world.kongsanity[player]: + low_g_labyrinth_region_locations[LocationName.low_g_labyrinth_kong] = [] + low_g_labyrinth_region = create_region(world, player, active_locations, LocationName.low_g_labyrinth_region, + low_g_labyrinth_region_locations, None) + + krevice_kreepers_region_locations = { + LocationName.krevice_kreepers_flag : [0x673, 1], + LocationName.krevice_kreepers_bonus_1 : [0x673, 2], + LocationName.krevice_kreepers_bonus_2 : [0x673, 3], + LocationName.krevice_kreepers_dk : [0x673, 5], + } + if world.kongsanity[player]: + krevice_kreepers_region_locations[LocationName.krevice_kreepers_kong] = [] + krevice_kreepers_region = create_region(world, player, active_locations, LocationName.krevice_kreepers_region, + krevice_kreepers_region_locations, None) + + tearaway_toboggan_region_locations = { + LocationName.tearaway_toboggan_flag : [0x65F, 1], + LocationName.tearaway_toboggan_bonus_1 : [0x65F, 2], + LocationName.tearaway_toboggan_bonus_2 : [0x65F, 3], + LocationName.tearaway_toboggan_dk : [0x65F, 5], + } + if world.kongsanity[player]: + tearaway_toboggan_region_locations[LocationName.tearaway_toboggan_kong] = [] + tearaway_toboggan_region = create_region(world, player, active_locations, LocationName.tearaway_toboggan_region, + tearaway_toboggan_region_locations, None) + + barrel_drop_bounce_region_locations = { + LocationName.barrel_drop_bounce_flag : [0x66C, 1], + LocationName.barrel_drop_bounce_bonus_1 : [0x66C, 2], + LocationName.barrel_drop_bounce_bonus_2 : [0x66C, 3], + LocationName.barrel_drop_bounce_dk : [0x66C, 5], + } + if world.kongsanity[player]: + barrel_drop_bounce_region_locations[LocationName.barrel_drop_bounce_kong] = [] + barrel_drop_bounce_region = create_region(world, player, active_locations, LocationName.barrel_drop_bounce_region, + barrel_drop_bounce_region_locations, None) + + krack_shot_kroc_region_locations = { + LocationName.krack_shot_kroc_flag : [0x66F, 1], + LocationName.krack_shot_kroc_bonus_1 : [0x66F, 2], + LocationName.krack_shot_kroc_bonus_2 : [0x66F, 3], + LocationName.krack_shot_kroc_dk : [0x66F, 5], + } + if world.kongsanity[player]: + krack_shot_kroc_region_locations[LocationName.krack_shot_kroc_kong] = [] + krack_shot_kroc_region = create_region(world, player, active_locations, LocationName.krack_shot_kroc_region, + krack_shot_kroc_region_locations, None) + + lemguin_lunge_region_locations = { + LocationName.lemguin_lunge_flag : [0x65E, 1], + LocationName.lemguin_lunge_bonus_1 : [0x65E, 2], + LocationName.lemguin_lunge_bonus_2 : [0x65E, 3], + LocationName.lemguin_lunge_dk : [0x65E, 5], + } + if world.kongsanity[player]: + lemguin_lunge_region_locations[LocationName.lemguin_lunge_kong] = [] + lemguin_lunge_region = create_region(world, player, active_locations, LocationName.lemguin_lunge_region, + lemguin_lunge_region_locations, None) + + buzzer_barrage_region_locations = { + LocationName.buzzer_barrage_flag : [0x676, 1], + LocationName.buzzer_barrage_bonus_1 : [0x676, 2], + LocationName.buzzer_barrage_bonus_2 : [0x676, 3], + LocationName.buzzer_barrage_dk : [0x676, 5], + } + if world.kongsanity[player]: + buzzer_barrage_region_locations[LocationName.buzzer_barrage_kong] = [] + buzzer_barrage_region = create_region(world, player, active_locations, LocationName.buzzer_barrage_region, + buzzer_barrage_region_locations, None) + + kong_fused_cliffs_region_locations = { + LocationName.kong_fused_cliffs_flag : [0x674, 1], + LocationName.kong_fused_cliffs_bonus_1 : [0x674, 2], + LocationName.kong_fused_cliffs_bonus_2 : [0x674, 3], + LocationName.kong_fused_cliffs_dk : [0x674, 5], + } + if world.kongsanity[player]: + kong_fused_cliffs_region_locations[LocationName.kong_fused_cliffs_kong] = [] + kong_fused_cliffs_region = create_region(world, player, active_locations, LocationName.kong_fused_cliffs_region, + kong_fused_cliffs_region_locations, None) + + floodlit_fish_region_locations = { + LocationName.floodlit_fish_flag : [0x669, 1], + LocationName.floodlit_fish_bonus_1 : [0x669, 2], + LocationName.floodlit_fish_bonus_2 : [0x669, 3], + LocationName.floodlit_fish_dk : [0x669, 5], + } + if world.kongsanity[player]: + floodlit_fish_region_locations[LocationName.floodlit_fish_kong] = [] + floodlit_fish_region = create_region(world, player, active_locations, LocationName.floodlit_fish_region, + floodlit_fish_region_locations, None) + + pothole_panic_region_locations = { + LocationName.pothole_panic_flag : [0x677, 1], + LocationName.pothole_panic_bonus_1 : [0x677, 2], + LocationName.pothole_panic_bonus_2 : [0x677, 3], + LocationName.pothole_panic_dk : [0x677, 5], + } + if world.kongsanity[player]: + pothole_panic_region_locations[LocationName.pothole_panic_kong] = [] + pothole_panic_region = create_region(world, player, active_locations, LocationName.pothole_panic_region, + pothole_panic_region_locations, None) + + ropey_rumpus_region_locations = { + LocationName.ropey_rumpus_flag : [0x675, 1], + LocationName.ropey_rumpus_bonus_1 : [0x675, 2], + LocationName.ropey_rumpus_bonus_2 : [0x675, 3], + LocationName.ropey_rumpus_dk : [0x675, 5], + } + if world.kongsanity[player]: + ropey_rumpus_region_locations[LocationName.ropey_rumpus_kong] = [] + ropey_rumpus_region = create_region(world, player, active_locations, LocationName.ropey_rumpus_region, + ropey_rumpus_region_locations, None) + + konveyor_rope_clash_region_locations = { + LocationName.konveyor_rope_clash_flag : [0x657, 1], + LocationName.konveyor_rope_clash_bonus_1 : [0x657, 2], + LocationName.konveyor_rope_clash_bonus_2 : [0x657, 3], + LocationName.konveyor_rope_clash_dk : [0x657, 5], + } + if world.kongsanity[player]: + konveyor_rope_clash_region_locations[LocationName.konveyor_rope_clash_kong] = [] + konveyor_rope_clash_region = create_region(world, player, active_locations, LocationName.konveyor_rope_clash_region, + konveyor_rope_clash_region_locations, None) + + creepy_caverns_region_locations = { + LocationName.creepy_caverns_flag : [0x678, 1], + LocationName.creepy_caverns_bonus_1 : [0x678, 2], + LocationName.creepy_caverns_bonus_2 : [0x678, 3], + LocationName.creepy_caverns_dk : [0x678, 5], + } + if world.kongsanity[player]: + creepy_caverns_region_locations[LocationName.creepy_caverns_kong] = [] + creepy_caverns_region = create_region(world, player, active_locations, LocationName.creepy_caverns_region, + creepy_caverns_region_locations, None) + + lightning_lookout_region_locations = { + LocationName.lightning_lookout_flag : [0x665, 1], + LocationName.lightning_lookout_bonus_1 : [0x665, 2], + LocationName.lightning_lookout_bonus_2 : [0x665, 3], + LocationName.lightning_lookout_dk : [0x665, 5], + } + if world.kongsanity[player]: + lightning_lookout_region_locations[LocationName.lightning_lookout_kong] = [] + lightning_lookout_region = create_region(world, player, active_locations, LocationName.lightning_lookout_region, + lightning_lookout_region_locations, None) + + koindozer_klamber_region_locations = { + LocationName.koindozer_klamber_flag : [0x679, 1], + LocationName.koindozer_klamber_bonus_1 : [0x679, 2], + LocationName.koindozer_klamber_bonus_2 : [0x679, 3], + LocationName.koindozer_klamber_dk : [0x679, 5], + } + if world.kongsanity[player]: + koindozer_klamber_region_locations[LocationName.koindozer_klamber_kong] = [] + koindozer_klamber_region = create_region(world, player, active_locations, LocationName.koindozer_klamber_region, + koindozer_klamber_region_locations, None) + + poisonous_pipeline_region_locations = { + LocationName.poisonous_pipeline_flag : [0x671, 1], + LocationName.poisonous_pipeline_bonus_1 : [0x671, 2], + LocationName.poisonous_pipeline_bonus_2 : [0x671, 3], + LocationName.poisonous_pipeline_dk : [0x671, 5], + } + if world.kongsanity[player]: + poisonous_pipeline_region_locations[LocationName.poisonous_pipeline_kong] = [] + poisonous_pipeline_region = create_region(world, player, active_locations, LocationName.poisonous_pipeline_region, + poisonous_pipeline_region_locations, None) + + stampede_sprint_region_locations = { + LocationName.stampede_sprint_flag : [0x67B, 1], + LocationName.stampede_sprint_bonus_1 : [0x67B, 2], + LocationName.stampede_sprint_bonus_2 : [0x67B, 3], + LocationName.stampede_sprint_bonus_3 : [0x67B, 4], + LocationName.stampede_sprint_dk : [0x67B, 5], + } + if world.kongsanity[player]: + stampede_sprint_region_locations[LocationName.stampede_sprint_kong] = [] + stampede_sprint_region = create_region(world, player, active_locations, LocationName.stampede_sprint_region, + stampede_sprint_region_locations, None) + + criss_cross_cliffs_region_locations = { + LocationName.criss_cross_cliffs_flag : [0x67C, 1], + LocationName.criss_cross_cliffs_bonus_1 : [0x67C, 2], + LocationName.criss_cross_cliffs_bonus_2 : [0x67C, 3], + LocationName.criss_cross_cliffs_dk : [0x67C, 5], + } + if world.kongsanity[player]: + criss_cross_cliffs_region_locations[LocationName.criss_cross_cliffs_kong] = [] + criss_cross_cliffs_region = create_region(world, player, active_locations, LocationName.criss_cross_cliffs_region, + criss_cross_cliffs_region_locations, None) + + tyrant_twin_tussle_region_locations = { + LocationName.tyrant_twin_tussle_flag : [0x67D, 1], + LocationName.tyrant_twin_tussle_bonus_1 : [0x67D, 2], + LocationName.tyrant_twin_tussle_bonus_2 : [0x67D, 3], + LocationName.tyrant_twin_tussle_bonus_3 : [0x67D, 4], + LocationName.tyrant_twin_tussle_dk : [0x67D, 5], + } + if world.kongsanity[player]: + tyrant_twin_tussle_region_locations[LocationName.tyrant_twin_tussle_kong] = [] + tyrant_twin_tussle_region = create_region(world, player, active_locations, LocationName.tyrant_twin_tussle_region, + tyrant_twin_tussle_region_locations, None) + + swoopy_salvo_region_locations = { + LocationName.swoopy_salvo_flag : [0x663, 1], + LocationName.swoopy_salvo_bonus_1 : [0x663, 2], + LocationName.swoopy_salvo_bonus_2 : [0x663, 3], + LocationName.swoopy_salvo_bonus_3 : [0x663, 4], + LocationName.swoopy_salvo_dk : [0x663, 5], + } + if world.kongsanity[player]: + swoopy_salvo_region_locations[LocationName.swoopy_salvo_kong] = [] + swoopy_salvo_region = create_region(world, player, active_locations, LocationName.swoopy_salvo_region, + swoopy_salvo_region_locations, None) + + rocket_rush_region_locations = { + LocationName.rocket_rush_flag : [0x67E, 1], + LocationName.rocket_rush_dk : [0x67E, 5], + } + rocket_rush_region = create_region(world, player, active_locations, LocationName.rocket_rush_region, + rocket_rush_region_locations, None) + + belchas_barn_region_locations = { + LocationName.belchas_barn: [0x64F, 1], + } + belchas_barn_region = create_region(world, player, active_locations, LocationName.belchas_barn_region, + belchas_barn_region_locations, None) + + arichs_ambush_region_locations = { + LocationName.arichs_ambush: [0x650, 1], + } + arichs_ambush_region = create_region(world, player, active_locations, LocationName.arichs_ambush_region, + arichs_ambush_region_locations, None) + + squirts_showdown_region_locations = { + LocationName.squirts_showdown: [0x651, 1], + } + squirts_showdown_region = create_region(world, player, active_locations, LocationName.squirts_showdown_region, + squirts_showdown_region_locations, None) + + kaos_karnage_region_locations = { + LocationName.kaos_karnage: [0x652, 1], + } + kaos_karnage_region = create_region(world, player, active_locations, LocationName.kaos_karnage_region, + kaos_karnage_region_locations, None) + + bleaks_house_region_locations = { + LocationName.bleaks_house: [0x653, 1], + } + bleaks_house_region = create_region(world, player, active_locations, LocationName.bleaks_house_region, + bleaks_house_region_locations, None) + + barboss_barrier_region_locations = { + LocationName.barboss_barrier: [0x654, 1], + } + barboss_barrier_region = create_region(world, player, active_locations, LocationName.barboss_barrier_region, + barboss_barrier_region_locations, None) + + kastle_kaos_region_locations = { + LocationName.kastle_kaos: [0x655, 1], + } + kastle_kaos_region = create_region(world, player, active_locations, LocationName.kastle_kaos_region, + kastle_kaos_region_locations, None) + + knautilus_region_locations = { + LocationName.knautilus: [0x656, 1], + } + knautilus_region = create_region(world, player, active_locations, LocationName.knautilus_region, + knautilus_region_locations, None) + + belchas_burrow_region_locations = { + LocationName.belchas_burrow: [0x647, 1], + } + belchas_burrow_region = create_region(world, player, active_locations, LocationName.belchas_burrow_region, + belchas_burrow_region_locations, None) + + kong_cave_region_locations = { + LocationName.kong_cave: [0x645, 1], + } + kong_cave_region = create_region(world, player, active_locations, LocationName.kong_cave_region, + kong_cave_region_locations, None) + + undercover_cove_region_locations = { + LocationName.undercover_cove: [0x644, 1], + } + undercover_cove_region = create_region(world, player, active_locations, LocationName.undercover_cove_region, + undercover_cove_region_locations, None) + + ks_cache_region_locations = { + LocationName.ks_cache: [0x642, 1], + } + ks_cache_region = create_region(world, player, active_locations, LocationName.ks_cache_region, + ks_cache_region_locations, None) + + hill_top_hoard_region_locations = { + LocationName.hill_top_hoard: [0x643, 1], + } + hill_top_hoard_region = create_region(world, player, active_locations, LocationName.hill_top_hoard_region, + hill_top_hoard_region_locations, None) + + bounty_beach_region_locations = { + LocationName.bounty_beach: [0x646, 1], + } + bounty_beach_region = create_region(world, player, active_locations, LocationName.bounty_beach_region, + bounty_beach_region_locations, None) + + smugglers_cove_region_locations = { + LocationName.smugglers_cove: [0x648, 1], + } + smugglers_cove_region = create_region(world, player, active_locations, LocationName.smugglers_cove_region, + smugglers_cove_region_locations, None) + + arichs_hoard_region_locations = { + LocationName.arichs_hoard: [0x649, 1], + } + arichs_hoard_region = create_region(world, player, active_locations, LocationName.arichs_hoard_region, + arichs_hoard_region_locations, None) + + bounty_bay_region_locations = { + LocationName.bounty_bay: [0x64A, 1], + } + bounty_bay_region = create_region(world, player, active_locations, LocationName.bounty_bay_region, + bounty_bay_region_locations, None) + + sky_high_secret_region_locations = {} + if False:#world.include_trade_sequence[player]: + sky_high_secret_region_locations[LocationName.sky_high_secret] = [0x64B, 1] + sky_high_secret_region = create_region(world, player, active_locations, LocationName.sky_high_secret_region, + sky_high_secret_region_locations, None) + + glacial_grotto_region_locations = { + LocationName.glacial_grotto: [0x64C, 1], + } + glacial_grotto_region = create_region(world, player, active_locations, LocationName.glacial_grotto_region, + glacial_grotto_region_locations, None) + + cifftop_cache_region_locations = {} + if False:#world.include_trade_sequence[player]: + cifftop_cache_region_locations[LocationName.cifftop_cache] = [0x64D, 1] + cifftop_cache_region = create_region(world, player, active_locations, LocationName.cifftop_cache_region, + cifftop_cache_region_locations, None) + + sewer_stockpile_region_locations = { + LocationName.sewer_stockpile: [0x64E, 1], + } + sewer_stockpile_region = create_region(world, player, active_locations, LocationName.sewer_stockpile_region, + sewer_stockpile_region_locations, None) + + + # Set up the regions correctly. + world.regions += [ + menu_region, + overworld_1_region, + overworld_2_region, + overworld_3_region, + overworld_4_region, + lake_orangatanga_region, + kremwood_forest_region, + cotton_top_cove_region, + mekanos_region, + k3_region, + razor_ridge_region, + kaos_kore_region, + krematoa_region, + lakeside_limbo_region, + doorstop_dash_region, + tidal_trouble_region, + skiddas_row_region, + murky_mill_region, + barrel_shield_bust_up_region, + riverside_race_region, + squeals_on_wheels_region, + springin_spiders_region, + bobbing_barrel_brawl_region, + bazzas_blockade_region, + rocket_barrel_ride_region, + kreeping_klasps_region, + tracker_barrel_trek_region, + fish_food_frenzy_region, + fire_ball_frenzy_region, + demolition_drain_pipe_region, + ripsaw_rage_region, + blazing_bazookas_region, + low_g_labyrinth_region, + krevice_kreepers_region, + tearaway_toboggan_region, + barrel_drop_bounce_region, + krack_shot_kroc_region, + lemguin_lunge_region, + buzzer_barrage_region, + kong_fused_cliffs_region, + floodlit_fish_region, + pothole_panic_region, + ropey_rumpus_region, + konveyor_rope_clash_region, + creepy_caverns_region, + lightning_lookout_region, + koindozer_klamber_region, + poisonous_pipeline_region, + stampede_sprint_region, + criss_cross_cliffs_region, + tyrant_twin_tussle_region, + swoopy_salvo_region, + rocket_rush_region, + belchas_barn_region, + arichs_ambush_region, + squirts_showdown_region, + kaos_karnage_region, + bleaks_house_region, + barboss_barrier_region, + kastle_kaos_region, + knautilus_region, + belchas_burrow_region, + kong_cave_region, + undercover_cove_region, + ks_cache_region, + hill_top_hoard_region, + bounty_beach_region, + smugglers_cove_region, + arichs_hoard_region, + bounty_bay_region, + sky_high_secret_region, + glacial_grotto_region, + cifftop_cache_region, + sewer_stockpile_region, + ] + + bazaar_region_locations = {} + bramble_region_locations = {} + flower_spot_region_locations = {} + barter_region_locations = {} + barnacle_region_locations = {} + blue_region_locations = {} + blizzard_region_locations = {} + + if False:#world.include_trade_sequence[player]: + bazaar_region_locations.update({ + LocationName.bazaars_general_store_1: [0x615, 2, True], + LocationName.bazaars_general_store_2: [0x615, 3, True], + }) + + bramble_region_locations[LocationName.brambles_bungalow] = [0x619, 2] + + #flower_spot_region_locations.update({ + # LocationName.flower_spot: [0x615, 3, True], + #}) + + barter_region_locations[LocationName.barters_swap_shop] = [0x61B, 3] + + barnacle_region_locations[LocationName.barnacles_island] = [0x61D, 2] + + blue_region_locations[LocationName.blues_beach_hut] = [0x621, 4] + + blizzard_region_locations[LocationName.blizzards_basecamp] = [0x625, 4, True] + + bazaar_region = create_region(world, player, active_locations, LocationName.bazaar_region, + bazaar_region_locations, None) + bramble_region = create_region(world, player, active_locations, LocationName.bramble_region, + bramble_region_locations, None) + flower_spot_region = create_region(world, player, active_locations, LocationName.flower_spot_region, + flower_spot_region_locations, None) + barter_region = create_region(world, player, active_locations, LocationName.barter_region, + barter_region_locations, None) + barnacle_region = create_region(world, player, active_locations, LocationName.barnacle_region, + barnacle_region_locations, None) + blue_region = create_region(world, player, active_locations, LocationName.blue_region, + blue_region_locations, None) + blizzard_region = create_region(world, player, active_locations, LocationName.blizzard_region, + blizzard_region_locations, None) + + world.regions += [ + bazaar_region, + bramble_region, + flower_spot_region, + barter_region, + barnacle_region, + blue_region, + blizzard_region, + ] + + +def connect_regions(world, player, level_list): + names: typing.Dict[str, int] = {} + + # Overworld + connect(world, player, names, 'Menu', LocationName.overworld_1_region) + connect(world, player, names, LocationName.overworld_1_region, LocationName.overworld_2_region, + lambda state: (state.has(ItemName.progressive_boat, player, 1))) + connect(world, player, names, LocationName.overworld_2_region, LocationName.overworld_3_region, + lambda state: (state.has(ItemName.progressive_boat, player, 3))) + connect(world, player, names, LocationName.overworld_1_region, LocationName.overworld_4_region, + lambda state: (state.has(ItemName.dk_coin, player, world.dk_coins_for_gyrocopter[player].value) and + state.has(ItemName.progressive_boat, player, 3))) + + # World Connections + connect(world, player, names, LocationName.overworld_1_region, LocationName.lake_orangatanga_region) + connect(world, player, names, LocationName.overworld_1_region, LocationName.kremwood_forest_region) + connect(world, player, names, LocationName.overworld_1_region, LocationName.bounty_beach_region) + connect(world, player, names, LocationName.overworld_1_region, LocationName.bazaar_region) + + connect(world, player, names, LocationName.overworld_2_region, LocationName.cotton_top_cove_region) + connect(world, player, names, LocationName.overworld_2_region, LocationName.mekanos_region) + connect(world, player, names, LocationName.overworld_2_region, LocationName.kong_cave_region) + connect(world, player, names, LocationName.overworld_2_region, LocationName.bramble_region) + + connect(world, player, names, LocationName.overworld_3_region, LocationName.k3_region) + connect(world, player, names, LocationName.overworld_3_region, LocationName.razor_ridge_region) + connect(world, player, names, LocationName.overworld_3_region, LocationName.kaos_kore_region) + connect(world, player, names, LocationName.overworld_3_region, LocationName.krematoa_region) + connect(world, player, names, LocationName.overworld_3_region, LocationName.undercover_cove_region) + connect(world, player, names, LocationName.overworld_3_region, LocationName.flower_spot_region) + connect(world, player, names, LocationName.overworld_3_region, LocationName.barter_region) + + connect(world, player, names, LocationName.overworld_4_region, LocationName.belchas_burrow_region) + connect(world, player, names, LocationName.overworld_4_region, LocationName.ks_cache_region) + connect(world, player, names, LocationName.overworld_4_region, LocationName.hill_top_hoard_region) + + + # Lake Orangatanga Connections + lake_orangatanga_levels = [ + level_list[0], + level_list[1], + level_list[2], + level_list[3], + level_list[4], + LocationName.belchas_barn_region, + LocationName.barnacle_region, + LocationName.smugglers_cove_region, + ] + + for i in range(0, len(lake_orangatanga_levels)): + connect(world, player, names, LocationName.lake_orangatanga_region, lake_orangatanga_levels[i]) + + # Kremwood Forest Connections + kremwood_forest_levels = [ + level_list[5], + level_list[6], + level_list[7], + level_list[8], + level_list[9], + LocationName.arichs_ambush_region, + LocationName.arichs_hoard_region, + ] + + for i in range(0, len(kremwood_forest_levels) - 1): + connect(world, player, names, LocationName.kremwood_forest_region, kremwood_forest_levels[i]) + + connect(world, player, names, LocationName.kremwood_forest_region, kremwood_forest_levels[-1], + lambda state: (state.can_reach(LocationName.riverside_race_flag, "Location", player))) + + # Cotton-Top Cove Connections + cotton_top_cove_levels = [ + LocationName.blue_region, + level_list[10], + level_list[11], + level_list[12], + level_list[13], + level_list[14], + LocationName.squirts_showdown_region, + LocationName.bounty_bay_region, + ] + + for i in range(0, len(cotton_top_cove_levels)): + connect(world, player, names, LocationName.cotton_top_cove_region, cotton_top_cove_levels[i]) + + # Mekanos Connections + mekanos_levels = [ + level_list[15], + level_list[16], + level_list[17], + level_list[18], + level_list[19], + LocationName.kaos_karnage_region, + ] + + for i in range(0, len(mekanos_levels)): + connect(world, player, names, LocationName.mekanos_region, mekanos_levels[i]) + + if False:#world.include_trade_sequence[player]: + connect(world, player, names, LocationName.mekanos_region, LocationName.sky_high_secret_region, + lambda state: (state.has(ItemName.bowling_ball, player, 1))) + else: + connect(world, player, names, LocationName.mekanos_region, LocationName.sky_high_secret_region, + lambda state: (state.can_reach(LocationName.bleaks_house, "Location", player))) + + # K3 Connections + k3_levels = [ + level_list[20], + level_list[21], + level_list[22], + level_list[23], + level_list[24], + LocationName.bleaks_house_region, + LocationName.blizzard_region, + LocationName.glacial_grotto_region, + ] + + for i in range(0, len(k3_levels)): + connect(world, player, names, LocationName.k3_region, k3_levels[i]) + + # Razor Ridge Connections + razor_ridge_levels = [ + level_list[25], + level_list[26], + level_list[27], + level_list[28], + level_list[29], + LocationName.barboss_barrier_region, + ] + + for i in range(0, len(razor_ridge_levels)): + connect(world, player, names, LocationName.razor_ridge_region, razor_ridge_levels[i]) + + if False:#world.include_trade_sequence[player]: + connect(world, player, names, LocationName.razor_ridge_region, LocationName.cifftop_cache_region, + lambda state: (state.has(ItemName.wrench, player, 1))) + else: + connect(world, player, names, LocationName.razor_ridge_region, LocationName.cifftop_cache_region) + + # KAOS Kore Connections + kaos_kore_levels = [ + level_list[30], + level_list[31], + level_list[32], + level_list[33], + level_list[34], + LocationName.sewer_stockpile_region, + ] + + for i in range(0, len(kaos_kore_levels)): + connect(world, player, names, LocationName.kaos_kore_region, kaos_kore_levels[i]) + + # Krematoa Connections + krematoa_levels = [ + level_list[35], + level_list[36], + level_list[37], + level_list[38], + LocationName.rocket_rush_region, + ] + + for i in range(0, len(krematoa_levels)): + connect(world, player, names, LocationName.krematoa_region, krematoa_levels[i], + lambda state, i=i: (state.has(ItemName.bonus_coin, player, world.krematoa_bonus_coin_cost[player].value * (i+1)))) + + if world.goal[player] == "knautilus": + connect(world, player, names, LocationName.kaos_kore_region, LocationName.knautilus_region) + connect(world, player, names, LocationName.krematoa_region, LocationName.kastle_kaos_region, + lambda state: (state.has(ItemName.krematoa_cog, player, 5))) + else: + connect(world, player, names, LocationName.kaos_kore_region, LocationName.kastle_kaos_region) + connect(world, player, names, LocationName.krematoa_region, LocationName.knautilus_region, + lambda state: (state.has(ItemName.krematoa_cog, player, 5))) + + +def create_region(world: MultiWorld, player: int, active_locations, name: str, locations=None, exits=None): + # Shamelessly stolen from the ROR2 definition + ret = Region(name, None, name, player) + ret.world = world + if locations: + for locationName, locationData in locations.items(): + loc_id = active_locations.get(locationName, 0) + if loc_id: + loc_byte = locationData[0] if (len(locationData) > 0) else 0 + loc_bit = locationData[1] if (len(locationData) > 1) else 0 + loc_invert = locationData[2] if (len(locationData) > 2) else False + + location = DKC3Location(player, locationName, loc_id, ret, loc_byte, loc_bit, loc_invert) + ret.locations.append(location) + if exits: + for exit in exits: + ret.exits.append(Entrance(player, exit, ret)) + + return ret + + +def connect(world: MultiWorld, player: int, used_names: typing.Dict[str, int], source: str, target: str, + rule: typing.Optional[typing.Callable] = None): + source_region = world.get_region(source, player) + target_region = world.get_region(target, player) + + if target not in used_names: + used_names[target] = 1 + name = target + else: + used_names[target] += 1 + name = target + (' ' * used_names[target]) + + connection = Entrance(player, name, source_region) + + if rule: + connection.access_rule = rule + + source_region.exits.append(connection) + connection.connect(target_region) diff --git a/worlds/dkc3/Rom.py b/worlds/dkc3/Rom.py new file mode 100644 index 0000000000..90c4507e44 --- /dev/null +++ b/worlds/dkc3/Rom.py @@ -0,0 +1,739 @@ +import Utils +from Patch import read_rom, APDeltaPatch +from .Locations import lookup_id_to_name, all_locations +from .Levels import level_list, level_dict + +USHASH = '120abf304f0c40fe059f6a192ed4f947' +ROM_PLAYER_LIMIT = 65535 + +import hashlib +import os +import math + + +level_unlock_map = { + 0x657: [0x65A], + 0x65A: [0x680, 0x639, 0x659], + 0x659: [0x65D], + 0x65D: [0x65C], + 0x65C: [0x688, 0x64F], + + 0x662: [0x681, 0x664], + 0x664: [0x65B], + 0x65B: [0x689, 0x661], + 0x661: [0x63A, 0x666], + 0x666: [0x650, 0x649], + + 0x667: [0x66A], + 0x66A: [0x682, 0x658], + 0x658: [0x68A, 0x66B], + 0x66B: [0x668], + 0x668: [0x651], + + 0x66D: [0x63C, 0x672], + 0x672: [0x68B, 0x660], + 0x660: [0x683, 0x66E], + 0x66E: [0x670], + 0x670: [0x652], + + 0x673: [0x684, 0x65F], + 0x65F: [0x66C], + 0x66C: [0x66F], + 0x66F: [0x65E], + 0x65E: [0x63D, 0x653, 0x68C, 0x64C], + + 0x676: [0x63E, 0x674, 0x685], + 0x674: [0x63F, 0x669], + 0x669: [0x677], + 0x677: [0x68D, 0x675], + 0x675: [0x654], + + 0x67A: [0x640, 0x678], + 0x678: [0x665], + 0x665: [0x686, 0x679], + 0x679: [0x68E, 0x671], + + 0x67B: [0x67C], + 0x67C: [0x67D], + 0x67D: [0x663], + 0x663: [0x67E], +} + +location_rom_data = { + 0xDC3000: [0x657, 1], # Lakeside Limbo + 0xDC3001: [0x657, 2], + 0xDC3002: [0x657, 3], + 0xDC3003: [0x657, 5], + 0xDC3100: [0x657, 7], + + 0xDC3004: [0x65A, 1], # Doorstop Dash + 0xDC3005: [0x65A, 2], + 0xDC3006: [0x65A, 3], + 0xDC3007: [0x65A, 5], + 0xDC3104: [0x65A, 7], + + 0xDC3008: [0x659, 1], # Tidal Trouble + 0xDC3009: [0x659, 2], + 0xDC300A: [0x659, 3], + 0xDC300B: [0x659, 5], + 0xDC3108: [0x659, 7], + + 0xDC300C: [0x65D, 1], # Skidda's Row + 0xDC300D: [0x65D, 2], + 0xDC300E: [0x65D, 3], + 0xDC300F: [0x65D, 5], + 0xDC310C: [0x65D, 7], + + 0xDC3010: [0x65C, 1], # Murky Mill + 0xDC3011: [0x65C, 2], + 0xDC3012: [0x65C, 3], + 0xDC3013: [0x65C, 5], + 0xDC3110: [0x65C, 7], + + + 0xDC3014: [0x662, 1], # Barrel Shield Bust-Up + 0xDC3015: [0x662, 2], + 0xDC3016: [0x662, 3], + 0xDC3017: [0x662, 5], + 0xDC3114: [0x662, 7], + + 0xDC3018: [0x664, 1], # Riverside Race + 0xDC3019: [0x664, 2], + 0xDC301A: [0x664, 3], + 0xDC301B: [0x664, 5], + 0xDC3118: [0x664, 7], + + 0xDC301C: [0x65B, 1], # Squeals on Wheels + 0xDC301D: [0x65B, 2], + 0xDC301E: [0x65B, 3], + 0xDC301F: [0x65B, 5], + 0xDC311C: [0x65B, 7], + + 0xDC3020: [0x661, 1], # Springin' Spiders + 0xDC3021: [0x661, 2], + 0xDC3022: [0x661, 3], + 0xDC3023: [0x661, 5], + 0xDC3120: [0x661, 7], + + 0xDC3024: [0x666, 1], # Bobbing Barrel Brawl + 0xDC3025: [0x666, 2], + 0xDC3026: [0x666, 3], + 0xDC3027: [0x666, 5], + 0xDC3124: [0x666, 7], + + + 0xDC3028: [0x667, 1], # Bazza's Blockade + 0xDC3029: [0x667, 2], + 0xDC302A: [0x667, 3], + 0xDC302B: [0x667, 5], + 0xDC3128: [0x667, 7], + + 0xDC302C: [0x66A, 1], # Rocket Barrel Ride + 0xDC302D: [0x66A, 2], + 0xDC302E: [0x66A, 3], + 0xDC302F: [0x66A, 5], + 0xDC312C: [0x66A, 7], + + 0xDC3030: [0x658, 1], # Kreeping Klasps + 0xDC3031: [0x658, 2], + 0xDC3032: [0x658, 3], + 0xDC3033: [0x658, 5], + 0xDC3130: [0x658, 7], + + 0xDC3034: [0x66B, 1], # Tracker Barrel Trek + 0xDC3035: [0x66B, 2], + 0xDC3036: [0x66B, 3], + 0xDC3037: [0x66B, 5], + 0xDC3134: [0x66B, 7], + + 0xDC3038: [0x668, 1], # Fish Food Frenzy + 0xDC3039: [0x668, 2], + 0xDC303A: [0x668, 3], + 0xDC303B: [0x668, 5], + 0xDC3138: [0x668, 7], + + + 0xDC303C: [0x66D, 1], # Fire-ball Frenzy + 0xDC303D: [0x66D, 2], + 0xDC303E: [0x66D, 3], + 0xDC303F: [0x66D, 5], + 0xDC313C: [0x66D, 7], + + 0xDC3040: [0x672, 1], # Demolition Drainpipe + 0xDC3041: [0x672, 2], + 0xDC3042: [0x672, 3], + 0xDC3043: [0x672, 5], + 0xDC3140: [0x672, 7], + + 0xDC3044: [0x660, 1], # Ripsaw Rage + 0xDC3045: [0x660, 2], + 0xDC3046: [0x660, 3], + 0xDC3047: [0x660, 5], + 0xDC3144: [0x660, 7], + + 0xDC3048: [0x66E, 1], # Blazing Bazukas + 0xDC3049: [0x66E, 2], + 0xDC304A: [0x66E, 3], + 0xDC304B: [0x66E, 5], + 0xDC3148: [0x66E, 7], + + 0xDC304C: [0x670, 1], # Low-G Labyrinth + 0xDC304D: [0x670, 2], + 0xDC304E: [0x670, 3], + 0xDC304F: [0x670, 5], + 0xDC314C: [0x670, 7], + + + 0xDC3050: [0x673, 1], # Krevice Kreepers + 0xDC3051: [0x673, 2], + 0xDC3052: [0x673, 3], + 0xDC3053: [0x673, 5], + 0xDC3150: [0x673, 7], + + 0xDC3054: [0x65F, 1], # Tearaway Toboggan + 0xDC3055: [0x65F, 2], + 0xDC3056: [0x65F, 3], + 0xDC3057: [0x65F, 5], + 0xDC3154: [0x65F, 7], + + 0xDC3058: [0x66C, 1], # Barrel Drop Bounce + 0xDC3059: [0x66C, 2], + 0xDC305A: [0x66C, 3], + 0xDC305B: [0x66C, 5], + 0xDC3158: [0x66C, 7], + + 0xDC305C: [0x66F, 1], # Krack-Shot Kroc + 0xDC305D: [0x66F, 2], + 0xDC305E: [0x66F, 3], + 0xDC305F: [0x66F, 5], + 0xDC315C: [0x66F, 7], + + 0xDC3060: [0x65E, 1], # Lemguin Lunge + 0xDC3061: [0x65E, 2], + 0xDC3062: [0x65E, 3], + 0xDC3063: [0x65E, 5], + 0xDC3160: [0x65E, 7], + + + 0xDC3064: [0x676, 1], # Buzzer Barrage + 0xDC3065: [0x676, 2], + 0xDC3066: [0x676, 3], + 0xDC3067: [0x676, 5], + 0xDC3164: [0x676, 7], + + 0xDC3068: [0x674, 1], # Kong-Fused Cliffs + 0xDC3069: [0x674, 2], + 0xDC306A: [0x674, 3], + 0xDC306B: [0x674, 5], + 0xDC3168: [0x674, 7], + + 0xDC306C: [0x669, 1], # Floodlit Fish + 0xDC306D: [0x669, 2], + 0xDC306E: [0x669, 3], + 0xDC306F: [0x669, 5], + 0xDC316C: [0x669, 7], + + 0xDC3070: [0x677, 1], # Pothole Panic + 0xDC3071: [0x677, 2], + 0xDC3072: [0x677, 3], + 0xDC3073: [0x677, 5], + 0xDC3170: [0x677, 7], + + 0xDC3074: [0x675, 1], # Ropey Rumpus + 0xDC3075: [0x675, 2], + 0xDC3076: [0x675, 3], + 0xDC3077: [0x675, 5], + 0xDC3174: [0x675, 7], + + + 0xDC3078: [0x67A, 1], # Konveyor Rope Klash + 0xDC3079: [0x67A, 2], + 0xDC307A: [0x67A, 3], + 0xDC307B: [0x67A, 5], + 0xDC3178: [0x67A, 7], + + 0xDC307C: [0x678, 1], # Creepy Caverns + 0xDC307D: [0x678, 2], + 0xDC307E: [0x678, 3], + 0xDC307F: [0x678, 5], + 0xDC317C: [0x678, 7], + + 0xDC3080: [0x665, 1], # Lightning Lookout + 0xDC3081: [0x665, 2], + 0xDC3082: [0x665, 3], + 0xDC3083: [0x665, 5], + 0xDC3180: [0x665, 7], + + 0xDC3084: [0x679, 1], # Koindozer Klamber + 0xDC3085: [0x679, 2], + 0xDC3086: [0x679, 3], + 0xDC3087: [0x679, 5], + 0xDC3184: [0x679, 7], + + 0xDC3088: [0x671, 1], # Poisonous Pipeline + 0xDC3089: [0x671, 2], + 0xDC308A: [0x671, 3], + 0xDC308B: [0x671, 5], + 0xDC3188: [0x671, 7], + + + 0xDC308C: [0x67B, 1], # Stampede Sprint + 0xDC308D: [0x67B, 2], + 0xDC308E: [0x67B, 3], + 0xDC308F: [0x67B, 4], + 0xDC3090: [0x67B, 5], + 0xDC318C: [0x67B, 7], + + 0xDC3091: [0x67C, 1], # Criss Kross Cliffs + 0xDC3092: [0x67C, 2], + 0xDC3093: [0x67C, 3], + 0xDC3094: [0x67C, 5], + 0xDC3191: [0x67C, 7], + + 0xDC3095: [0x67D, 1], # Tyrant Twin Tussle + 0xDC3096: [0x67D, 2], + 0xDC3097: [0x67D, 3], + 0xDC3098: [0x67D, 4], + 0xDC3099: [0x67D, 5], + 0xDC3195: [0x67D, 7], + + 0xDC309A: [0x663, 1], # Swoopy Salvo + 0xDC309B: [0x663, 2], + 0xDC309C: [0x663, 3], + 0xDC309D: [0x663, 4], + 0xDC309E: [0x663, 5], + 0xDC319A: [0x663, 7], + + 0xDC309F: [0x67E, 1], # Rocket Rush + 0xDC30A0: [0x67E, 5], + + 0xDC30A1: [0x64F, 1], # Bosses + 0xDC30A2: [0x650, 1], + 0xDC30A3: [0x651, 1], + 0xDC30A4: [0x652, 1], + 0xDC30A5: [0x653, 1], + 0xDC30A6: [0x654, 1], + 0xDC30A7: [0x655, 1], + 0xDC30A8: [0x656, 1], + + 0xDC30A9: [0x647, 1], # Banana Bird Caves + 0xDC30AA: [0x645, 1], + 0xDC30AB: [0x644, 1], + 0xDC30AC: [0x642, 1], + 0xDC30AD: [0x643, 1], + 0xDC30AE: [0x646, 1], + 0xDC30AF: [0x648, 1], + 0xDC30B0: [0x649, 1], + 0xDC30B1: [0x64A, 1], + #0xDC30B2: [0x64B, 1], # Disabled until Trade Sequence + 0xDC30B3: [0x64C, 1], + #0xDC30B4: [0x64D, 1], # Disabled until Trade Sequence + 0xDC30B5: [0x64E, 1], + + 0xDC30B6: [0x5FE, 4], # Banana Bird Mother + + # DKC3_TODO: Disabled until Trade Sequence + #0xDC30B7: [0x615, 2, True], + #0xDC30B8: [0x615, 3, True], + #0xDC30B9: [0x619, 2], + ##0xDC30BA: + #0xDC30BB: [0x61B, 3], + #0xDC30BC: [0x61D, 2], + #0xDC30BD: [0x621, 4], + #0xDC30BE: [0x625, 4, True], +} + +boss_location_ids = [ + 0xDC30A1, + 0xDC30A2, + 0xDC30A3, + 0xDC30A4, + 0xDC30A5, + 0xDC30A6, + 0xDC30A7, + 0xDC30A8, + 0xDC30B6, +] + + +item_rom_data = { + 0xDC3001: [0x5D5], # 1-Up Balloon + 0xDC3002: [0x5C9], # Bear Coin + 0xDC3003: [0x5CB], # Bonus Coin + 0xDC3004: [0x5CF], # DK Coin + 0xDC3005: [0x5CD], # Banana Bird + 0xDC3006: [0x5D1, 0x603], # Cog +} + +music_rom_data = [ + 0x3D06B1, + 0x3D0753, + 0x3D071D, + 0x3D07FA, + 0x3D07C4, + + 0x3D08FE, + 0x3D096C, + 0x3D078E, + 0x3D08CD, + 0x3D09DD, + + 0x3D0A0E, + 0x3D0AB3, + 0x3D06E7, + 0x3D0AE4, + 0x3D0A45, + + 0x3D0B46, + 0x3D0C40, + 0x3D0897, + 0x3D0B77, + 0x3D0BD9, + + 0x3D0C71, + 0x3D0866, + 0x3D0B15, + 0x3D0BA8, + 0x3D0830, + + 0x3D0D04, + 0x3D0CA2, + 0x3D0A7C, + 0x3D0D35, + 0x3D0CD3, + + 0x3D0DC8, + 0x3D0D66, + 0x3D09AC, + 0x3D0D97, + 0x3D0C0F, + + 0x3D0DF9, + 0x3D0E31, + 0x3D0E62, + 0x3D0934, + 0x3D0E9A, +] + +level_music_ids = [ + 0x06, + 0x07, + 0x08, + 0x0A, + 0x0B, + 0x0E, + 0x0F, + 0x10, + 0x17, + 0x19, + 0x1C, + 0x1D, + 0x1E, + 0x21, +] + +class LocalRom(object): + + def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None): + self.name = name + self.hash = hash + self.orig_buffer = None + + with open(file, 'rb') as stream: + self.buffer = read_rom(stream) + #if patch: + # self.patch_rom() + # self.orig_buffer = self.buffer.copy() + #if vanillaRom: + # with open(vanillaRom, 'rb') as vanillaStream: + # self.orig_buffer = read_rom(vanillaStream) + + def read_bit(self, address: int, bit_number: int) -> bool: + bitflag = (1 << bit_number) + return ((self.buffer[address] & bitflag) != 0) + + def read_byte(self, address: int) -> int: + return self.buffer[address] + + def read_bytes(self, startaddress: int, length: int) -> bytes: + return self.buffer[startaddress:startaddress + length] + + def write_byte(self, address: int, value: int): + self.buffer[address] = value + + def write_bytes(self, startaddress: int, values): + self.buffer[startaddress:startaddress + len(values)] = values + + def write_to_file(self, file): + with open(file, 'wb') as outfile: + outfile.write(self.buffer) + + def read_from_file(self, file): + with open(file, 'rb') as stream: + self.buffer = bytearray(stream.read()) + + + +def patch_rom(world, rom, player, active_level_list): + local_random = world.slot_seeds[player] + + # Boomer Costs + bonus_coin_cost = world.krematoa_bonus_coin_cost[player] + inverted_bonus_coin_cost = 0x100 - bonus_coin_cost + rom.write_byte(0x3498B9, inverted_bonus_coin_cost) + rom.write_byte(0x3498BA, inverted_bonus_coin_cost) + rom.write_byte(0x3498BB, inverted_bonus_coin_cost) + rom.write_byte(0x3498BC, inverted_bonus_coin_cost) + rom.write_byte(0x3498BD, inverted_bonus_coin_cost) + + rom.write_byte(0x349857, bonus_coin_cost) + rom.write_byte(0x349862, bonus_coin_cost) + + # Gyrocopter Costs + dk_coin_cost = world.dk_coins_for_gyrocopter[player] + rom.write_byte(0x3484A6, dk_coin_cost) + rom.write_byte(0x3484D5, dk_coin_cost) + rom.write_byte(0x3484D7, 0x90) + rom.write_byte(0x3484DC, 0xEA) + rom.write_byte(0x3484DD, 0xEA) + rom.write_byte(0x3484DE, 0xEA) + rom.write_byte(0x348528, 0x80) # Prevent Single-Ski Lock + + # Make Swanky free + rom.write_byte(0x348C48, 0x00) + + # Banana Bird Costs + if world.goal[player] == "banana_bird_hunt": + banana_bird_cost = math.floor(world.number_of_banana_birds[player] * world.percentage_of_banana_birds[player] / 100.0) + rom.write_byte(0x34AB85, banana_bird_cost) + rom.write_byte(0x329FD8, banana_bird_cost) + rom.write_byte(0x32A025, banana_bird_cost) + rom.write_byte(0x329FDA, 0xB0) + else: + # rom.write_byte(0x34AB84, 0x20) # These cause hangs at Wrinkly's + # rom.write_byte(0x329FD8, 0x20) + # rom.write_byte(0x32A025, 0x20) + rom.write_byte(0x329FDA, 0xB0) + + # Baffle Mirror Fix + rom.write_byte(0x9133, 0x08) + rom.write_byte(0x9135, 0x0C) + rom.write_byte(0x9136, 0x2B) + rom.write_byte(0x9137, 0x06) + + # Palette Swap + rom.write_byte(0x3B96A5, 0xD0) + if world.kong_palette_swap[player] == "default": + rom.write_byte(0x3B96A9, 0x00) + rom.write_byte(0x3B96A8, 0x00) + elif world.kong_palette_swap[player] == "purple": + rom.write_byte(0x3B96A9, 0x00) + rom.write_byte(0x3B96A8, 0x3C) + elif world.kong_palette_swap[player] == "spooky": + rom.write_byte(0x3B96A9, 0x00) + rom.write_byte(0x3B96A8, 0xA0) + elif world.kong_palette_swap[player] == "dark": + rom.write_byte(0x3B96A9, 0x05) + rom.write_byte(0x3B96A8, 0xA0) + elif world.kong_palette_swap[player] == "chocolate": + rom.write_byte(0x3B96A9, 0x1D) + rom.write_byte(0x3B96A8, 0xA0) + elif world.kong_palette_swap[player] == "shadow": + rom.write_byte(0x3B96A9, 0x45) + rom.write_byte(0x3B96A8, 0xA0) + elif world.kong_palette_swap[player] == "red_gold": + rom.write_byte(0x3B96A9, 0x5D) + rom.write_byte(0x3B96A8, 0xA0) + elif world.kong_palette_swap[player] == "gbc": + rom.write_byte(0x3B96A9, 0x20) + rom.write_byte(0x3B96A8, 0x3C) + elif world.kong_palette_swap[player] == "halloween": + rom.write_byte(0x3B96A9, 0x70) + rom.write_byte(0x3B96A8, 0x3C) + + if world.music_shuffle[player]: + for address in music_rom_data: + rand_song = local_random.choice(level_music_ids) + rom.write_byte(address, rand_song) + + # Starting Lives + rom.write_byte(0x9130, world.starting_life_count[player].value) + rom.write_byte(0x913B, world.starting_life_count[player].value) + + # Cheat options + cheat_bytes = [0x00, 0x00] + + if world.merry[player]: + cheat_bytes[0] |= 0x01 + + if world.autosave[player]: + cheat_bytes[0] |= 0x02 + + if world.difficulty[player] == "tufst": + cheat_bytes[0] |= 0x80 + cheat_bytes[1] |= 0x80 + elif world.difficulty[player] == "hardr": + cheat_bytes[0] |= 0x00 + cheat_bytes[1] |= 0x00 + elif world.difficulty[player] == "norml": + cheat_bytes[1] |= 0x40 + + rom.write_bytes(0x8303, bytearray(cheat_bytes)) + + # Handle Level Shuffle Here + if world.level_shuffle[player]: + for i in range(len(active_level_list)): + rom.write_byte(level_dict[level_list[i]].nameIDAddress, level_dict[active_level_list[i]].nameID) + rom.write_byte(level_dict[level_list[i]].levelIDAddress, level_dict[active_level_list[i]].levelID) + + rom.write_byte(0x3FF800 + level_dict[active_level_list[i]].levelID, level_dict[level_list[i]].levelID) + rom.write_byte(0x3FF860 + level_dict[level_list[i]].levelID, level_dict[active_level_list[i]].levelID) + + # First levels of each world + rom.write_byte(0x34BC3E, (0x32 + level_dict[active_level_list[0]].levelID)) + rom.write_byte(0x34BC47, (0x32 + level_dict[active_level_list[5]].levelID)) + rom.write_byte(0x34BC4A, (0x32 + level_dict[active_level_list[10]].levelID)) + rom.write_byte(0x34BC53, (0x32 + level_dict[active_level_list[15]].levelID)) + rom.write_byte(0x34BC59, (0x32 + level_dict[active_level_list[20]].levelID)) + rom.write_byte(0x34BC5C, (0x32 + level_dict[active_level_list[25]].levelID)) + rom.write_byte(0x34BC65, (0x32 + level_dict[active_level_list[30]].levelID)) + rom.write_byte(0x34BC6E, (0x32 + level_dict[active_level_list[35]].levelID)) + + # Cotton-Top Cove Boss Unlock + rom.write_byte(0x34C02A, (0x32 + level_dict[active_level_list[14]].levelID)) + + # Kong-Fused Cliffs Unlock + rom.write_byte(0x34C213, (0x32 + level_dict[active_level_list[25]].levelID)) + rom.write_byte(0x34C21B, (0x32 + level_dict[active_level_list[26]].levelID)) + + if world.goal[player] == "knautilus": + # Swap Kastle KAOS and Knautilus + rom.write_byte(0x34D4E1, 0xC2) + rom.write_byte(0x34D4E2, 0x24) + rom.write_byte(0x34D551, 0xBA) + rom.write_byte(0x34D552, 0x23) + + rom.write_byte(0x32F339, 0x55) + + # Handle KONGsanity Here + if world.kongsanity[player]: + # Arich's Hoard KONGsanity fix + rom.write_bytes(0x34BA8C, bytearray([0xEA, 0xEA])) + + # Don't hide the level flag if the 0x80 bit is set + rom.write_bytes(0x34CE92, bytearray([0x80])) + + # Use the `!` next to level name for indicating KONG letters + rom.write_bytes(0x34B8F0, bytearray([0x80])) + rom.write_bytes(0x34B8F3, bytearray([0x80])) + + # Hijack to code to set the 0x80 flag for the level when you complete KONG + rom.write_bytes(0x3BCD4B, bytearray([0x22, 0x80, 0xFA, 0XB8])) # JSL $B8FA80 + + rom.write_bytes(0x38FA80, bytearray([0xDA])) # PHX + rom.write_bytes(0x38FA81, bytearray([0x48])) # PHA + rom.write_bytes(0x38FA82, bytearray([0x08])) # PHP + rom.write_bytes(0x38FA83, bytearray([0xE2, 0x20])) # SEP #20 + rom.write_bytes(0x38FA85, bytearray([0x48])) # PHA + rom.write_bytes(0x38FA86, bytearray([0x18])) # CLC + rom.write_bytes(0x38FA87, bytearray([0x6D, 0xD3, 0x18])) # ADC $18D3 + rom.write_bytes(0x38FA8A, bytearray([0x8D, 0xD3, 0x18])) # STA $18D3 + rom.write_bytes(0x38FA8D, bytearray([0x68])) # PLA + rom.write_bytes(0x38FA8E, bytearray([0xC2, 0x20])) # REP 20 + rom.write_bytes(0x38FA90, bytearray([0X18])) # CLC + rom.write_bytes(0x38FA91, bytearray([0x6D, 0xD5, 0x05])) # ADC $05D5 + rom.write_bytes(0x38FA94, bytearray([0x8D, 0xD5, 0x05])) # STA $05D5 + rom.write_bytes(0x38FA97, bytearray([0xAE, 0xB9, 0x05])) # LDX $05B9 + rom.write_bytes(0x38FA9A, bytearray([0xBD, 0x32, 0x06])) # LDA $0632, X + rom.write_bytes(0x38FA9D, bytearray([0x09, 0x80, 0x00])) # ORA #8000 + rom.write_bytes(0x38FAA0, bytearray([0x9D, 0x32, 0x06])) # STA $0632, X + rom.write_bytes(0x38FAA3, bytearray([0xAD, 0xD5, 0x18])) # LDA $18D5 + rom.write_bytes(0x38FAA6, bytearray([0xD0, 0x03])) # BNE $80EA + rom.write_bytes(0x38FAA8, bytearray([0x9C, 0xD9, 0x18])) # STZ $18D9 + rom.write_bytes(0x38FAAB, bytearray([0xA9, 0x78, 0x00])) # LDA #0078 + rom.write_bytes(0x38FAAE, bytearray([0x8D, 0xD5, 0x18])) # STA $18D5 + rom.write_bytes(0x38FAB1, bytearray([0x28])) # PLP + rom.write_bytes(0x38FAB2, bytearray([0x68])) # PLA + rom.write_bytes(0x38FAB3, bytearray([0xFA])) # PLX + rom.write_bytes(0x38FAB4, bytearray([0x6B])) # RTL + # End Handle KONGsanity + + # Handle Credits + rom.write_bytes(0x32A5DF, bytearray([0x41, 0x52, 0x43, 0x48, 0x49, 0x50, 0x45, 0x4C, 0x41, 0x47, 0x4F, 0x20, 0x4D, 0x4F, 0xC4])) # "ARCHIPELAGO MOD" + rom.write_bytes(0x32A5EE, bytearray([0x00, 0x03, 0x50, 0x4F, 0x52, 0x59, 0x47, 0x4F, 0x4E, 0xC5])) # "PORYGONE" + + from Main import __version__ + rom.name = bytearray(f'D3{__version__.replace(".", "")[0:3]}_{player}_{world.seed:11}\0', 'utf8')[:21] + rom.name.extend([0] * (21 - len(rom.name))) + rom.write_bytes(0x7FC0, rom.name) + + # DKC3_TODO: This is a hack, reconsider + # Don't grant (DK, Bonus, Bear) Coins + rom.write_byte(0x3BD454, 0xEA) + rom.write_byte(0x3BD455, 0xEA) + + # Don't grant Cogs + rom.write_byte(0x3BD574, 0xEA) + rom.write_byte(0x3BD575, 0xEA) + rom.write_byte(0x3BD576, 0xEA) + + # Don't grant Banana Birds at their caves + rom.write_byte(0x32DD62, 0xEA) + rom.write_byte(0x32DD63, 0xEA) + rom.write_byte(0x32DD64, 0xEA) + + # Don't grant Banana Birds at Bears + rom.write_byte(0x3492DB, 0xEA) + rom.write_byte(0x3492DC, 0xEA) + rom.write_byte(0x3492DD, 0xEA) + rom.write_byte(0x3493F4, 0xEA) + rom.write_byte(0x3493F5, 0xEA) + rom.write_byte(0x3493F6, 0xEA) + + # Don't grant present at Blizzard + rom.write_byte(0x8454, 0x00) + + # Don't grant Patch and Skis from their bosses + rom.write_byte(0x3F3762, 0x00) + rom.write_byte(0x3F377B, 0x00) + rom.write_byte(0x3F3797, 0x00) + + # Always allow Start+Select + rom.write_byte(0x8BAB, 0x01) + + # Handle Alt Palettes in Krematoa + rom.write_byte(0x3B97E9, 0x80) + rom.write_byte(0x3B97EA, 0xEA) + + +class DKC3DeltaPatch(APDeltaPatch): + hash = USHASH + game = "Donkey Kong Country 3" + patch_file_ending = ".apdkc3" + + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_bytes() + + +def get_base_rom_bytes(file_name: str = "") -> bytes: + base_rom_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 = bytes(read_rom(open(file_name, "rb"))) + + basemd5 = hashlib.md5() + basemd5.update(base_rom_bytes) + if USHASH != basemd5.hexdigest(): + raise Exception('Supplied Base Rom does not match known MD5 for US(1.0) release. ' + 'Get the correct game and version, then dump it') + get_base_rom_bytes.base_rom_bytes = base_rom_bytes + return base_rom_bytes + +def get_base_rom_path(file_name: str = "") -> str: + options = Utils.get_options() + if not file_name: + file_name = options["dkc3_options"]["rom_file"] + if not os.path.exists(file_name): + file_name = Utils.local_path(file_name) + return file_name diff --git a/worlds/dkc3/Rules.py b/worlds/dkc3/Rules.py new file mode 100644 index 0000000000..dcfc912448 --- /dev/null +++ b/worlds/dkc3/Rules.py @@ -0,0 +1,32 @@ +import math + +from BaseClasses import MultiWorld +from .Names import LocationName, ItemName +from ..AutoWorld import LogicMixin +from ..generic.Rules import add_rule, set_rule + + +def set_rules(world: MultiWorld, player: int): + + if False:#world.include_trade_sequence[player]: + add_rule(world.get_location(LocationName.barnacles_island, player), + lambda state: state.has(ItemName.shell, player)) + + add_rule(world.get_location(LocationName.blues_beach_hut, player), + lambda state: state.has(ItemName.present, player)) + + add_rule(world.get_location(LocationName.brambles_bungalow, player), + lambda state: state.has(ItemName.flower, player)) + + add_rule(world.get_location(LocationName.barters_swap_shop, player), + lambda state: state.has(ItemName.mirror, player)) + + + if world.goal[player] != "knautilus": + required_banana_birds = math.floor( + world.number_of_banana_birds[player].value * (world.percentage_of_banana_birds[player].value / 100.0)) + + add_rule(world.get_location(LocationName.banana_bird_mother, player), + lambda state: state.has(ItemName.banana_bird, player, required_banana_birds)) + + world.completion_condition[player] = lambda state: state.has(ItemName.victory, player) diff --git a/worlds/dkc3/__init__.py b/worlds/dkc3/__init__.py new file mode 100644 index 0000000000..5c575b85b5 --- /dev/null +++ b/worlds/dkc3/__init__.py @@ -0,0 +1,215 @@ +import os +import typing +import math +import threading + +from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification +from .Items import DKC3Item, ItemData, item_table, inventory_table, junk_table +from .Locations import DKC3Location, all_locations, setup_locations +from .Options import dkc3_options +from .Regions import create_regions, connect_regions +from .Levels import level_list +from .Rules import set_rules +from .Names import ItemName, LocationName +from ..AutoWorld import WebWorld, World +from .Rom import LocalRom, patch_rom, get_base_rom_path, DKC3DeltaPatch +import Patch + + +class DKC3Web(WebWorld): + theme = "jungle" + + setup_en = Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the Donkey Kong Country 3 randomizer connected to an Archipelago Multiworld.", + "English", + "setup_en.md", + "setup/en", + ["PoryGone"] + ) + + tutorials = [setup_en] + + +class DKC3World(World): + """ + Donkey Kong Country 3 is an action platforming game. + Play as Dixie Kong and her baby cousin Kiddy as they try to solve the + mystery of why Donkey Kong and Diddy disappeared while on vacation. + """ + game: str = "Donkey Kong Country 3" + option_definitions = dkc3_options + topology_present = False + data_version = 2 + #hint_blacklist = {LocationName.rocket_rush_flag} + + item_name_to_id = {name: data.code for name, data in item_table.items()} + location_name_to_id = all_locations + + active_level_list: typing.List[str] + web = DKC3Web() + + def __init__(self, world: MultiWorld, player: int): + self.rom_name_available_event = threading.Event() + super().__init__(world, player) + + @classmethod + def stage_assert_generate(cls, world): + rom_file = get_base_rom_path() + if not os.path.exists(rom_file): + raise FileNotFoundError(rom_file) + + def _get_slot_data(self): + return { + #"death_link": self.world.death_link[self.player].value, + "active_levels": self.active_level_list, + } + + def _create_items(self, name: str): + data = item_table[name] + return [self.create_item(name)] * data.quantity + + def fill_slot_data(self) -> dict: + slot_data = self._get_slot_data() + for option_name in dkc3_options: + option = getattr(self.world, option_name)[self.player] + slot_data[option_name] = option.value + + return slot_data + + def generate_basic(self): + self.topology_present = self.world.level_shuffle[self.player].value + itempool: typing.List[DKC3Item] = [] + + # Levels + total_required_locations = 159 + + number_of_banana_birds = 0 + # Rocket Rush Cog + total_required_locations -= 1 + number_of_cogs = 4 + self.world.get_location(LocationName.rocket_rush_flag, self.player).place_locked_item(self.create_item(ItemName.krematoa_cog)) + number_of_bosses = 8 + if self.world.goal[self.player] == "knautilus": + self.world.get_location(LocationName.kastle_kaos, self.player).place_locked_item(self.create_item(ItemName.victory)) + number_of_bosses = 7 + else: + self.world.get_location(LocationName.banana_bird_mother, self.player).place_locked_item(self.create_item(ItemName.victory)) + number_of_banana_birds = self.world.number_of_banana_birds[self.player] + + # Bosses + total_required_locations += number_of_bosses + + # Secret Caves + total_required_locations += 13 + + if self.world.kongsanity[self.player]: + total_required_locations += 39 + + ## Brothers Bear + if False:#self.world.include_trade_sequence[self.player]: + total_required_locations += 10 + + number_of_bonus_coins = (self.world.krematoa_bonus_coin_cost[self.player] * 5) + number_of_bonus_coins += math.ceil((85 - number_of_bonus_coins) * self.world.percentage_of_extra_bonus_coins[self.player] / 100) + + itempool += [self.create_item(ItemName.bonus_coin)] * number_of_bonus_coins + itempool += [self.create_item(ItemName.dk_coin)] * 41 + itempool += [self.create_item(ItemName.banana_bird)] * number_of_banana_birds + itempool += [self.create_item(ItemName.krematoa_cog)] * number_of_cogs + itempool += [self.create_item(ItemName.progressive_boat)] * 3 + + total_junk_count = total_required_locations - len(itempool) + + junk_pool = [] + for item_name in self.world.random.choices(list(junk_table.keys()), k=total_junk_count): + junk_pool += [self.create_item(item_name)] + + itempool += junk_pool + + self.active_level_list = level_list.copy() + + if self.world.level_shuffle[self.player]: + self.world.random.shuffle(self.active_level_list) + + connect_regions(self.world, self.player, self.active_level_list) + + self.world.itempool += itempool + + def generate_output(self, output_directory: str): + try: + world = self.world + player = self.player + + rom = LocalRom(get_base_rom_path()) + patch_rom(self.world, rom, self.player, self.active_level_list) + + self.active_level_list.append(LocationName.rocket_rush_region) + + outfilepname = f'_P{player}' + outfilepname += f"_{world.player_name[player].replace(' ', '_')}" \ + if world.player_name[player] != 'Player%d' % player else '' + + rompath = os.path.join(output_directory, f'AP_{world.seed_name}{outfilepname}.sfc') + rom.write_to_file(rompath) + self.rom_name = rom.name + + patch = DKC3DeltaPatch(os.path.splitext(rompath)[0]+DKC3DeltaPatch.patch_file_ending, player=player, + player_name=world.player_name[player], patched_path=rompath) + patch.write() + except: + raise + finally: + if os.path.exists(rompath): + os.unlink(rompath) + self.rom_name_available_event.set() # make sure threading continues and errors are collected + + def modify_multidata(self, multidata: dict): + import base64 + # 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.world.player_name[self.player]] + + if self.topology_present: + world_names = [ + LocationName.lake_orangatanga_region, + LocationName.kremwood_forest_region, + LocationName.cotton_top_cove_region, + LocationName.mekanos_region, + LocationName.k3_region, + LocationName.razor_ridge_region, + LocationName.kaos_kore_region, + LocationName.krematoa_region, + ] + er_hint_data = {} + for world_index in range(len(world_names)): + for level_index in range(5): + level_region = self.world.get_region(self.active_level_list[world_index * 5 + level_index], self.player) + for location in level_region.locations: + er_hint_data[location.address] = world_names[world_index] + multidata['er_hint_data'][self.player] = er_hint_data + + def create_regions(self): + location_table = setup_locations(self.world, self.player) + create_regions(self.world, self.player, location_table) + + def create_item(self, name: str, force_non_progression=False) -> Item: + data = item_table[name] + + if force_non_progression: + classification = ItemClassification.filler + elif data.progression: + classification = ItemClassification.progression + else: + classification = ItemClassification.filler + + created_item = DKC3Item(name, classification, data.code, self.player) + + return created_item + + def set_rules(self): + set_rules(self.world, self.player) diff --git a/worlds/dkc3/docs/en_Donkey Kong Country 3.md b/worlds/dkc3/docs/en_Donkey Kong Country 3.md new file mode 100644 index 0000000000..2041f0a41b --- /dev/null +++ b/worlds/dkc3/docs/en_Donkey Kong Country 3.md @@ -0,0 +1,35 @@ +# Donkey Kong Country 3 + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file. + +## What does randomization do to this game? + +Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game is +always able to be completed, but because of the item shuffle the player may need to access certain areas before they +would in the vanilla game. + +## What is the goal of Donkey Kong Country 3 when randomized? + +There are two goals which can be chosen: +- `Knautilus`: Collect Bonus Coins and Krematoa Cogs to reach K. Rool's submarine in Krematoa +- `Banana Bird Hunt`: Collect Banana Birds to free the Banana Bird Mother + +## What items and locations get shuffled? + +All Bonus Coins, DK Coins, and Banana Birds (if on a `Banana Bird Hunt` goal) are randomized. Additionally, level clears award a location check. +The Patch and two Skis for upgrading the boat are included. Bear Coins are provided if additional items are needed for the item pool. +Four of the Five Krematoa Cogs are randomized, but the final one is always in its vanilla location at the Flag of Rocket Rush in Krematoa + +## Which items can be in another player's world? + +Any shuffled item can be in other players' worlds. + +## What does another world's item look like in Donkey Kong Country 3 + +Items pickups all retain their original appearance. You won't know if an item belongs to another player until you collect. + +## When the player receives an item, what happens? + +Currently, the items are silently added to the player's inventory, which can be seen when saving the game. diff --git a/worlds/dkc3/docs/setup_en.md b/worlds/dkc3/docs/setup_en.md new file mode 100644 index 0000000000..471248deb8 --- /dev/null +++ b/worlds/dkc3/docs/setup_en.md @@ -0,0 +1,165 @@ +# Donkey Kong Country 3 Randomizer Setup Guide + +## Required Software + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `SNI Client - Donkey Kong Country 3 Patch Setup` + + +- Hardware or software capable of loading and playing SNES ROM files + - An emulator capable of connecting to SNI such as: + - snes9x Multitroid + from: [snes9x Multitroid Download](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz), + - BizHawk from: [BizHawk Website](http://tasvideos.org/BizHawk.html) + - RetroArch 1.10.3 or newer from: [RetroArch Website](https://retroarch.com?page=platforms). Or, + - An SD2SNES, FXPak Pro ([FXPak Pro Store Page](https://krikzz.com/store/home/54-fxpak-pro.html)), or other + compatible hardware +- Your legally obtained Donkey Kong Country 3 ROM file, probably named `Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc` + +## Optional Software +- Donkey Kong Country 3 Tracker + - PopTracker from: [PopTracker Releases Page](https://github.com/black-sliver/PopTracker/releases/) + - Donkey Kong Country 3 Archipelago PopTracker pack from: [DKC3 AP Tracker Releases Page](https://github.com/PoryGone/DKC3_AP_Tracker/releases/) + +## Installation Procedures + +### Windows Setup + +1. During the installation of Archipelago, you will have been asked to install the SNI Client. If you did not do this, + or you are on an older version, you may run the installer again to install the SNI Client. +2. During setup, you will be asked to locate your base ROM file. This is your Donkey Kong Country 3 ROM file. +3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM + files. + 1. Extract your emulator's folder to your Desktop, or somewhere you will remember. + 2. Right-click on a ROM file and select **Open with...** + 3. Check the box next to **Always use this app to open .sfc files** + 4. Scroll to the bottom of the list and click the grey text **Look for another App on this PC** + 5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside the folder you + extracted in step one. + +## Create a Config (.yaml) File + +### What is a config file and why do I need one? + +See the guide on setting up a basic YAML at the Archipelago setup +guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) + +### Where do I get a config file? + +The Player Settings page on the website allows you to configure your personal settings and export a config file from +them. Player settings page: [Donkey Kong Country 3 Player Settings Page](/games/Donkey%20Kong%20Country%203/player-settings) + +### Verifying your config file + +If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML +validator page: [YAML Validation page](/mysterycheck) + +## Generating a Single-Player Game + +1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button. + - Player Settings page: [Donkey Kong Country 3 Player Settings Page](/games/Donkey%20Kong%20Country%203/player-settings) +2. You will be presented with a "Seed Info" page. +3. Click the "Create New Room" link. +4. You will be presented with a server page, from which you can download your patch file. +5. Double-click on your patch file, and the Donkey Kong Country 3 Client will launch automatically, create your ROM from the + patch file, and open your emulator for you. + +## Joining a MultiWorld Game + +### Obtain your patch file and create your ROM + +When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done, +the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch +files. Your patch file should have a `.apdkc3` extension. + +Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the +client, and will also create your ROM in the same place as your patch file. + +### Connect to the client + +#### With an emulator + +When the client launched automatically, SNI should have also automatically launched in the background. If this is its +first time launching, you may be prompted to allow it to communicate through the Windows Firewall. + +##### snes9x Multitroid + +1. Load your ROM file if it hasn't already been loaded. +2. Click on the File menu and hover on **Lua Scripting** +3. Click on **New Lua Script Window...** +4. In the new window, click **Browse...** +5. Select the connector lua file included with your client + - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the + emulator is 64-bit or 32-bit. +6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of +the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install. + +##### BizHawk + +1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following these + menu options: + `Config --> Cores --> SNES --> BSNES` + Once you have changed the loaded core, you must restart BizHawk. +2. Load your ROM file if it hasn't already been loaded. +3. Click on the Tools menu and click on **Lua Console** +4. Click the button to open a new Lua script. +5. Select the `Connector.lua` file included with your client + - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the + emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only. + +##### RetroArch 1.10.3 or newer + +You only have to do these steps once. Note, RetroArch 1.9.x will not work as it is older than 1.10.3. + +1. Enter the RetroArch main menu screen. +2. Go to Settings --> User Interface. Set "Show Advanced Settings" to ON. +3. Go to Settings --> Network. Set "Network Commands" to ON. (It is found below Request Device 16.) Leave the default + Network Command Port at 55355. + +![Screenshot of Network Commands setting](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png) +4. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and select "Nintendo - SNES / SFC (bsnes-mercury + Performance)". + +When loading a ROM, be sure to select a **bsnes-mercury** core. These are the only cores that allow external tools to +read ROM data. + +#### With hardware + +This guide assumes you have downloaded the correct firmware for your device. If you have not done so already, please do +this now. SD2SNES and FXPak Pro users may download the appropriate firmware on the SD2SNES releases page. SD2SNES +releases page: [SD2SNES Releases Page](https://github.com/RedGuyyyy/sd2snes/releases) + +Other hardware may find helpful information on the usb2snes platforms +page: [usb2snes Supported Platforms Page](http://usb2snes.com/#supported-platforms) + +1. Close your emulator, which may have auto-launched. +2. Power on your device and load the ROM. + +### Connect to the Archipelago Server + +The patch file which launched your client should have automatically connected you to the AP Server. There are a few +reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the +client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it +into the "Server" input field then press enter. + +The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected". + +### Play the game + +When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations on +successfully joining a multiworld game! + +## Hosting a MultiWorld game + +The recommended way to host a game is to use our hosting service. The process is relatively simple: + +1. Collect config files from your players. +2. Create a zip file containing your players' config files. +3. Upload that zip file to the Generate page above. + - Generate page: [WebHost Seed Generation Page](/generate) +4. Wait a moment while the seed is generated. +5. When the seed is generated, you will be redirected to a "Seed Info" page. +6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players, so + they may download their patch files from there. +7. Note that a link to a MultiWorld Tracker is at the top of the room page. The tracker shows the progress of all + players in the game. Any observers may also be given the link to this page. +8. Once all players have joined, you may begin playing. diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 8c7dcf669c..37c503b047 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -78,9 +78,14 @@ def generate_mod(world, output_directory: str): global data_final_template, locale_template, control_template, data_template, settings_template with template_load_lock: if not data_final_template: - mod_template_folder = os.path.join(os.path.dirname(__file__), "data", "mod_template") + def load_template(name: str): + import pkgutil + data = pkgutil.get_data(__name__, "data/mod_template/" + name).decode() + return data, name, lambda: False + template_env: Optional[jinja2.Environment] = \ - jinja2.Environment(loader=jinja2.FileSystemLoader([mod_template_folder])) + jinja2.Environment(loader=jinja2.FunctionLoader(load_template)) + data_template = template_env.get_template("data.lua") data_final_template = template_env.get_template("data-final-fixes.lua") locale_template = template_env.get_template(r"locale/en/locale.cfg") @@ -158,7 +163,21 @@ def generate_mod(world, output_directory: str): mod_dir = os.path.join(output_directory, mod_name + "_" + Utils.__version__) en_locale_dir = os.path.join(mod_dir, "locale", "en") os.makedirs(en_locale_dir, exist_ok=True) - shutil.copytree(os.path.join(os.path.dirname(__file__), "data", "mod"), mod_dir, dirs_exist_ok=True) + + if world.zip_path: + # Maybe investigate read from zip, write to zip, without temp file? + with zipfile.ZipFile(world.zip_path) as zf: + for file in zf.infolist(): + if not file.is_dir() and "/data/mod/" in file.filename: + path_part = Utils.get_text_after(file.filename, "/data/mod/") + target = os.path.join(mod_dir, path_part) + os.makedirs(os.path.split(target)[0], exist_ok=True) + + with open(target, "wb") as f: + f.write(zf.read(file)) + else: + shutil.copytree(os.path.join(os.path.dirname(__file__), "data", "mod"), mod_dir, dirs_exist_ok=True) + with open(os.path.join(mod_dir, "data.lua"), "wt") as f: f.write(data_template_code) with open(os.path.join(mod_dir, "data-final-fixes.lua"), "wt") as f: diff --git a/worlds/factorio/Shapes.py b/worlds/factorio/Shapes.py index eaf44ac1ba..f42da4d20c 100644 --- a/worlds/factorio/Shapes.py +++ b/worlds/factorio/Shapes.py @@ -1,7 +1,7 @@ from typing import Dict, List, Set from collections import deque -from worlds.factorio.Options import TechTreeLayout +from .Options import TechTreeLayout funnel_layers = {TechTreeLayout.option_small_funnels: 3, TechTreeLayout.option_medium_funnels: 4, diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index 0d32f51cbb..b88cc9b1ad 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -19,8 +19,8 @@ pool = ThreadPoolExecutor(1) def load_json_data(data_name: str) -> Union[List[str], Dict[str, Any]]: - with open(os.path.join(source_folder, f"{data_name}.json")) as f: - return json.load(f) + import pkgutil + return json.loads(pkgutil.get_data(__name__, "data/" + data_name + ".json").decode()) techs_future = pool.submit(load_json_data, "techs") @@ -501,6 +501,7 @@ def get_science_pack_pools() -> Dict[str, Set[str]]: item_stack_sizes: Dict[str, int] = items_future.result() non_stacking_items: Set[str] = {item for item, stack in item_stack_sizes.items() if stack == 1} stacking_items: Set[str] = set(item_stack_sizes) - non_stacking_items +valid_ingredients: Set[str] = stacking_items | fluids # cleanup async helpers pool.shutdown() diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 53c9897c17..a01abac748 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -1,14 +1,14 @@ import collections import typing -from ..AutoWorld import World, WebWorld +from worlds.AutoWorld import World, WebWorld from BaseClasses import Region, Entrance, Location, Item, RegionType, Tutorial, ItemClassification from .Technologies import base_tech_table, recipe_sources, base_technology_table, \ all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, \ progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \ get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies, \ - fluids, stacking_items + fluids, stacking_items, valid_ingredients from .Shapes import get_shapes from .Mod import generate_mod from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal @@ -193,7 +193,7 @@ class Factorio(World): return super(Factorio, self).collect_item(state, item, remove) - options = factorio_options + option_definitions = factorio_options @classmethod def stage_write_spoiler(cls, world, spoiler_handle): @@ -221,7 +221,7 @@ class Factorio(World): # Return the liquid to the pool and get a new ingredient. pool.append(new_ingredient) new_ingredient = pool.pop(0) - liquids_used += 1 + liquids_used += 1 if new_ingredient in fluids else 0 new_ingredients[new_ingredient] = 1 return Recipe(original.name, self.get_category(original.category, liquids_used), new_ingredients, original.products, original.energy) @@ -231,7 +231,7 @@ class Factorio(World): """Generate a recipe from pool with time and cost similar to original * factor""" new_ingredients = {} # have to first sort for determinism, while filtering out non-stacking items - pool: typing.List[str] = sorted(pool & stacking_items) + pool: typing.List[str] = sorted(pool & valid_ingredients) # then sort with random data to shuffle self.world.random.shuffle(pool) target_raw = int(sum((count for ingredient, count in original.base_cost.items())) * factor) @@ -329,10 +329,8 @@ class Factorio(World): def set_custom_recipes(self): original_rocket_part = recipes["rocket-part"] science_pack_pools = get_science_pack_pools() - valid_pool = sorted(science_pack_pools[self.world.max_science_pack[self.player].get_max_pack()] & stacking_items) + valid_pool = sorted(science_pack_pools[self.world.max_science_pack[self.player].get_max_pack()] & valid_ingredients) self.world.random.shuffle(valid_pool) - while any([valid_pool[x] in fluids for x in range(3)]): - self.world.random.shuffle(valid_pool) self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category, {valid_pool[x]: 10 for x in range(3)}, original_rocket_part.products, diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index e7774fef00..63473808c6 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -286,6 +286,12 @@ end) -- hook into researches done script.on_event(defines.events.on_research_finished, function(event) local technology = event.research + if string.find(technology.force.name, "EE_TESTFORCE") == 1 then + --Don't acknowledge AP research as an Editor Extensions test force + --Also no need for free samples in the Editor extensions testing surfaces, as these testing surfaces + --are worked on exclusively in editor mode. + return + end if technology.researched and string.find(technology.name, "ap%-") == 1 then -- check if it came from the server anyway, then we don't need to double send. dumpInfo(technology.force) --is sendable diff --git a/worlds/factorio/data/mod_template/data-final-fixes.lua b/worlds/factorio/data/mod_template/data-final-fixes.lua index 7da4f3a62d..cc813b2fff 100644 --- a/worlds/factorio/data/mod_template/data-final-fixes.lua +++ b/worlds/factorio/data/mod_template/data-final-fixes.lua @@ -1,6 +1,48 @@ {% from "macros.lua" import dict_to_recipe %} -- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template require('lib') +data.raw["rocket-silo"]["rocket-silo"].fluid_boxes = { + { + production_type = "input", + pipe_picture = assembler2pipepictures(), + pipe_covers = pipecoverspictures(), + base_area = 10, + base_level = -1, + pipe_connections = { + { type = "input", position = { 0, 5 } }, + { type = "input", position = { 0, -5 } }, + { type = "input", position = { 5, 0 } }, + { type = "input", position = { -5, 0 } } + } + }, + { + production_type = "input", + pipe_picture = assembler2pipepictures(), + pipe_covers = pipecoverspictures(), + base_area = 10, + base_level = -1, + pipe_connections = { + { type = "input", position = { -3, 5 } }, + { type = "input", position = { -3, -5 } }, + { type = "input", position = { 5, -3 } }, + { type = "input", position = { -5, -3 } } + } + }, + { + production_type = "input", + pipe_picture = assembler2pipepictures(), + pipe_covers = pipecoverspictures(), + base_area = 10, + base_level = -1, + pipe_connections = { + { type = "input", position = { 3, 5 } }, + { type = "input", position = { 3, -5 } }, + { type = "input", position = { 5, 3 } }, + { type = "input", position = { -5, 3 } } + } + }, + off_when_no_fluid_recipe = true +} {%- for recipe_name, recipe in custom_recipes.items() %} data.raw["recipe"]["{{recipe_name}}"].category = "{{recipe.category}}" @@ -169,8 +211,8 @@ copy_factorio_icon(new_tree_copy, "{{ progressive_technology_table[item_name][0] {%- endif -%} {#- connect Technology #} {%- if original_tech_name in tech_tree_layout_prerequisites %} -{%- for prerequesite in tech_tree_layout_prerequisites[original_tech_name] %} -table.insert(new_tree_copy.prerequisites, "ap-{{ tech_table[prerequesite] }}-") +{%- for prerequisite in tech_tree_layout_prerequisites[original_tech_name] %} +table.insert(new_tree_copy.prerequisites, "ap-{{ tech_table[prerequisite] }}-") {% endfor %} {% endif -%} {#- add new Technology to game #} diff --git a/worlds/factorio/requirements.txt b/worlds/factorio/requirements.txt index ce5a83049a..c45fb771da 100644 --- a/worlds/factorio/requirements.txt +++ b/worlds/factorio/requirements.txt @@ -1 +1 @@ -factorio-rcon-py>=1.2.1 +factorio-rcon-py>=2.0.1 diff --git a/worlds/ff1/__init__.py b/worlds/ff1/__init__.py index 761d9fbbe4..0d731ace4b 100644 --- a/worlds/ff1/__init__.py +++ b/worlds/ff1/__init__.py @@ -1,5 +1,5 @@ from typing import Dict -from BaseClasses import Item, Location, MultiWorld, Tutorial +from BaseClasses import Item, Location, MultiWorld, Tutorial, ItemClassification from .Items import ItemData, FF1Items, FF1_STARTER_ITEMS, FF1_PROGRESSION_LIST, FF1_BRIDGE from .Locations import EventId, FF1Locations, generate_rule, CHAOS_TERMINATED_EVENT from .Options import ff1_options @@ -27,7 +27,7 @@ class FF1World(World): Part puzzle and part speed-run, it breathes new life into one of the most influential games ever made. """ - options = ff1_options + option_definitions = ff1_options game = "Final Fantasy" topology_present = False remote_items = True @@ -54,8 +54,9 @@ class FF1World(World): locations = get_options(self.world, 'locations', self.player) rules = get_options(self.world, 'rules', self.player) menu_region = self.ff1_locations.create_menu_region(self.player, locations, rules) + menu_region.world = self.world terminated_event = Location(self.player, CHAOS_TERMINATED_EVENT, EventId, menu_region) - terminated_item = Item(CHAOS_TERMINATED_EVENT, True, EventId, self.player) + terminated_item = Item(CHAOS_TERMINATED_EVENT, ItemClassification.progression, EventId, self.player) terminated_event.place_locked_item(terminated_item) items = get_options(self.world, 'items', self.player) @@ -114,5 +115,6 @@ class FF1World(World): def get_filler_item_name(self) -> str: return self.world.random.choice(["Heal", "Pure", "Soft", "Tent", "Cabin", "House"]) + def get_options(world: MultiWorld, name: str, player: int): return getattr(world, name, None)[player].value diff --git a/worlds/generic/__init__.py b/worlds/generic/__init__.py index 3baba9a709..0d8a220d98 100644 --- a/worlds/generic/__init__.py +++ b/worlds/generic/__init__.py @@ -15,6 +15,8 @@ class GenericWeb(WebWorld): commands = Tutorial('Archipelago Server and Client Commands', 'A guide detailing the commands available to the user when participating in an Archipelago session.', 'English', 'commands_en.md', 'commands/en', ['jat2980', 'Ijwu']) + mac = Tutorial('Archipelago Setup Guide for Mac', 'A guide detailing how to run Archipelago clients on macOS.', + 'English', 'mac_en.md','mac/en', ['Bicoloursnake']) plando = Tutorial('Archipelago Plando Guide', 'A guide to understanding and using plando for your game.', 'English', 'plando_en.md', 'plando/en', ['alwaysintreble', 'Alchav']) setup = Tutorial('Multiworld Setup Tutorial', @@ -25,7 +27,7 @@ class GenericWeb(WebWorld): using_website = Tutorial('Archipelago Website User Guide', 'A guide to using the Archipelago website to generate multiworlds or host pre-generated multiworlds.', 'English', 'using_website_en.md', 'using_website/en', ['alwaysintreble']) - tutorials = [setup, using_website, commands, advanced_settings, triggers, plando] + tutorials = [setup, using_website, mac, commands, advanced_settings, triggers, plando] class GenericWorld(World): diff --git a/worlds/generic/docs/advanced_settings_en.md b/worlds/generic/docs/advanced_settings_en.md index a0808fb444..d19c9d5ee6 100644 --- a/worlds/generic/docs/advanced_settings_en.md +++ b/worlds/generic/docs/advanced_settings_en.md @@ -272,7 +272,7 @@ Super Mario 64: StrictCapRequirements: true StrictCannonRequirements: true StarsToFinish: 70 - ExtraStars: 30 + AmountOfStars: 70 DeathLink: true BuddyChecks: true AreaRandomizer: true diff --git a/worlds/generic/docs/mac_en.md b/worlds/generic/docs/mac_en.md new file mode 100644 index 0000000000..1e2d235c8c --- /dev/null +++ b/worlds/generic/docs/mac_en.md @@ -0,0 +1,32 @@ +# Guide to Run Archipelago from Source Code on macOS +Archipelago does not have a compiled release on macOS. However, it is possible to run from source code on macOS. This guide expects you to have some experience with running software from the terminal. +## Prerequisite Software +Here is a list of software to install and source code to download. +1. Python 3.8 or newer from the [macOS Python downloads page](https://www.python.org/downloads/macos/). +2. Xcode from the [macOS App Store](https://apps.apple.com/us/app/xcode/id497799835). +3. The source code from the [Archipelago releases page](https://github.com/ArchipelagoMW/Archipelago/releases). +4. The asset with darwin in the name from the [SNI Github releases page](https://github.com/alttpo/sni/releases). +5. If you would like to generate Enemized seeds for ALTTP locally (not on the website), you may need the EnemizerCLI from its [Github releases page](https://github.com/Ijwu/Enemizer/releases). +6. An Emulator of your choosing for games that need an emulator. For SNES games, I recommend RetroArch, entirely because it was the easiest for me to setup on macOS. It can be downloaded at the [RetroArch downloads page](https://www.retroarch.com/?page=platforms) +## Extracting the Archipelago Directory +1. Double click on the Archipelago source code zip file to extract the files to an Archipelago directory. +2. Move this Archipelago directory out of your downloads directory. +3. Open terminal and navigate to your Archipelago directory. +## Setting up a Virtual Environment +It is generally recommended that you use a virtual environment to run python based software to avoid contamination that can break some software. If Archipelago is the only piece of software you use that runs from python source code however, it is not necessary to use a virtual environment. +1. Open terminal and navigate to the Archipelago directory. +2. Run the command `python3 -m venv venv` to create a virtual environment. Running this command will create a new directory at the specified path, so make sure that path is clear for a new directory to be created. +3. Run the command `source venv/bin/activate` to activate the virtual environment. +4. If you want to exit the virtual environment, run the command `deactivate`. +## Steps to Run the Clients +1. If your game doesn't have a patch file, run the command `python3 SNIClient.py`, changing the filename with the file of the client you want to run. +2. If your game does have a patch file, move the base rom to the Archipelago directory and run the command `python3 SNIClient.py 'patchfile'` with the filename extension for the patch file (apsm, aplttp, apsmz3, etc.) included and changing the filename with the file of the client you want to run. +3. Your client should now be running and rom created (where applicable). +## Additional Steps for SNES Games +1. If using RetroArch, the instructions to set up your emulator [here in the Link to the Past setup guide](https://archipelago.gg/tutorial/A%20Link%20to%20the%20Past/multiworld/en) also work on the macOS version of RetroArch. +2. Double click on the SNI tar.gz download to extract the files to an SNI directory. If it isn't already, rename this directory to SNI to make some steps easier. +3. Move the SNI directory out of the downloads directory, preferably into the Archipelago directory created earlier. +4. If the SNI directory is correctly named and moved into the Archipelago directory, it should auto run with the SNI client. If it doesn't automatically run, open up the SNI directory and run the SNI executable file manually. +5. If using EnemizerCLI, extract that downloaded directory and rename it to EnemizerCLI. +6. Move the EnemizerCLI directory into the Archipelago directory so that Generate.py can take advantage of it. +7. Now that SNI, the client, and the emulator are all running, you should be good to go. diff --git a/worlds/generic/docs/plando_en.md b/worlds/generic/docs/plando_en.md index c73fa8d398..fa3edd1fa1 100644 --- a/worlds/generic/docs/plando_en.md +++ b/worlds/generic/docs/plando_en.md @@ -7,8 +7,7 @@ changes it up by allowing you to plan out certain aspects of the game by placing certain bosses in certain rooms, edit text for certain NPCs/signs, or even force certain region connections. Each of these options are going to be detailed separately as `item plando`, `boss plando`, `text plando`, and `connection plando`. Every game in archipelago supports item plando but the other plando options are only supported -by certain games. Currently, Minecraft and LTTP both support connection plando, but only LTTP supports text and boss -plando. +by certain games. Currently, only LTTP supports text and boss plando. Support for connection plando may vary. ### Enabling Plando diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py index 6c3a9bf548..fd8d036175 100644 --- a/worlds/hk/Options.py +++ b/worlds/hk/Options.py @@ -50,7 +50,7 @@ option_docstrings = { "pool and open their locations for randomization.", "RandomizeGrubs": "Randomize Grubs into the item pool and open their locations for randomization.", "RandomizeMimics": "Randomize Mimic Grubs into the item pool and open their locations for randomization." - "Mimic Grubs are always placed in your own game.", + "Mimic Grubs are always placed in your own game.", "RandomizeMaps": "Randomize Maps into the item pool. This causes Cornifer to give you a message allowing you to see" " and buy an item that is randomized into that location as well.", "RandomizeStags": "Randomize Stag Stations unlocks into the item pool as well as placing randomized items " @@ -222,6 +222,7 @@ class MinimumCharmPrice(Range): class MaximumCharmPrice(MinimumCharmPrice): """The maximum charm price in the range of prices that an item should cost for Salubra's shop item which also carry a charm cost.""" + display_name = "Maximum Charm Requirement" default = 20 @@ -235,7 +236,7 @@ class MinimumGeoPrice(Range): class MaximumGeoPrice(Range): """The maximum geo price for items in geo shops.""" - display_name = "Minimum Geo Price" + display_name = "Maximum Geo Price" range_start = 1 range_end = 2000 default = 400 diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index af0e54e237..1667ab81f7 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -121,6 +121,7 @@ shop_cost_types: typing.Dict[str, typing.Tuple[str, ...]] = { "Leg_Eater": ("GEO",), } + class HKWeb(WebWorld): tutorials = [Tutorial( "Mod Setup and Use Guide", @@ -131,6 +132,8 @@ class HKWeb(WebWorld): ["Ijwu"] )] + bug_report_page = "https://github.com/Ijwu/Archipelago.HollowKnight/issues/new?assignees=&labels=bug%2C+needs+investigation&template=bug_report.md&title=" + class HKWorld(World): """Beneath the fading town of Dirtmouth sleeps a vast, ancient kingdom. Many are drawn beneath the surface, @@ -139,7 +142,7 @@ class HKWorld(World): As the enigmatic Knight, you’ll traverse the depths, unravel its mysteries and conquer its evils. """ # from https://www.hollowknight.com game: str = "Hollow Knight" - options = hollow_knight_options + option_definitions = hollow_knight_options web = HKWeb() @@ -254,6 +257,9 @@ class HKWorld(World): if location_name == "Start": if item_name in randomized_starting_items: + if item_name == "Focus": + self.create_location("Focus") + unfilled_locations += 1 pool.append(item) else: self.world.push_precollected(item) @@ -429,7 +435,7 @@ class HKWorld(World): slot_data = {} options = slot_data["options"] = {} - for option_name in self.options: + for option_name in self.option_definitions: option = getattr(self.world, option_name)[self.player] try: optionvalue = int(option.value) @@ -502,6 +508,7 @@ class HKWorld(World): location.place_locked_item(item) if costs: location.costs = costs.pop() + return location def collect(self, state, item: HKItem) -> bool: change = super(HKWorld, self).collect(state, item) @@ -625,8 +632,9 @@ class HKLocation(Location): class HKItem(Item): game = "Hollow Knight" + type: str - def __init__(self, name, advancement, code, type, player: int = None): + def __init__(self, name, advancement, code, type: str, player: int = None): if name == "Mimic_Grub": classification = ItemClassification.trap elif type in ("Grub", "DreamWarrior", "Root", "Egg"): diff --git a/worlds/meritous/Items.py b/worlds/meritous/Items.py index 1b5228e5ce..9f28c5d178 100644 --- a/worlds/meritous/Items.py +++ b/worlds/meritous/Items.py @@ -6,14 +6,9 @@ import typing from BaseClasses import Item, ItemClassification +from worlds.alttp import ALTTPWorld -# pedestal_credit_text: str = "and the Unknown Item" -# sickkid_credit_text: Optional[str] = None -# magicshop_credit_text: Optional[str] = None -# zora_credit_text: Optional[str] = None -# fluteboy_credit_text: Optional[str] = None - class MeritousLttPText(typing.NamedTuple): pedestal: typing.Optional[str] sickkid: typing.Optional[str] @@ -143,6 +138,7 @@ LttPCreditsText = { class MeritousItem(Item): game: str = "Meritous" + type: str def __init__(self, name, advancement, code, player): super(MeritousItem, self).__init__(name, @@ -171,14 +167,6 @@ class MeritousItem(Item): self.type = "Artifact" self.classification = ItemClassification.useful - if name in LttPCreditsText: - lttp = LttPCreditsText[name] - self.pedestal_credit_text = f"and the {lttp.pedestal}" - self.sickkid_credit_text = lttp.sickkid - self.magicshop_credit_text = lttp.magicshop - self.zora_credit_text = lttp.zora - self.fluteboy_credit_text = lttp.fluteboy - offset = 593_000 @@ -217,3 +205,10 @@ item_groups = { "Important Artifacts": ["Shield Boost", "Circuit Booster", "Metabolism", "Dodge Enhancer"], "Crystals": ["Crystals x500", "Crystals x1000", "Crystals x2000"] } + +ALTTPWorld.pedestal_credit_texts.update({item_table[name]: f"and the {texts.pedestal}" + for name, texts in LttPCreditsText.items()}) +ALTTPWorld.sickkid_credit_texts.update({item_table[name]: texts.sickkid for name, texts in LttPCreditsText.items()}) +ALTTPWorld.magicshop_credit_texts.update({item_table[name]: texts.magicshop for name, texts in LttPCreditsText.items()}) +ALTTPWorld.zora_credit_texts.update({item_table[name]: texts.zora for name, texts in LttPCreditsText.items()}) +ALTTPWorld.fluteboy_credit_texts.update({item_table[name]: texts.fluteboy for name, texts in LttPCreditsText.items()}) diff --git a/worlds/meritous/__init__.py b/worlds/meritous/__init__.py index 3c29032aa5..d0d076da40 100644 --- a/worlds/meritous/__init__.py +++ b/worlds/meritous/__init__.py @@ -45,12 +45,11 @@ class MeritousWorld(World): item_name_groups = item_groups data_version = 2 - forced_auto_forfeit = False # NOTE: Remember to change this before this game goes live required_client_version = (0, 2, 4) - options = meritous_options + option_definitions = meritous_options def __init__(self, world: MultiWorld, player: int): super(MeritousWorld, self).__init__(world, player) diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index e5dbe0b0cd..6e7addb2d0 100644 --- a/worlds/minecraft/__init__.py +++ b/worlds/minecraft/__init__.py @@ -58,7 +58,7 @@ class MinecraftWorld(World): victory! """ game: str = "Minecraft" - options = minecraft_options + option_definitions = minecraft_options topology_present = True web = MinecraftWebWorld() diff --git a/worlds/minecraft/docs/minecraft_en.md b/worlds/minecraft/docs/minecraft_en.md index b33710f73c..e8b1a3642e 100644 --- a/worlds/minecraft/docs/minecraft_en.md +++ b/worlds/minecraft/docs/minecraft_en.md @@ -33,12 +33,12 @@ leave this window open as this is your server console. ### Connect to the MultiServer -Using Minecraft 1.18.2 connect to the server `localhost`. +Open Minecraft, go to `Multiplayer > Direct Connection`, and join the `localhost` server address. If you are using the website to host the game then it should auto-connect to the AP server without the need to `/connect` otherwise once you are in game type `/connect (Port) (Password)` where `` is the address of the -Archipelago server. `(Port)` is only required if the Archipelago server is not using the default port of 38281. +Archipelago server. `(Port)` is only required if the Archipelago server is not using the default port of 38281. Note that there is no colon between `` and `(Port)`. `(Password)` is only required if the Archipelago server you are using has a password set. ### Play the game diff --git a/worlds/minecraft/requirements.txt b/worlds/minecraft/requirements.txt index a5590930fd..ddedb7c332 100644 --- a/worlds/minecraft/requirements.txt +++ b/worlds/minecraft/requirements.txt @@ -1 +1 @@ -requests >= 2.27.1 # used by client \ No newline at end of file +requests >= 2.28.1 # used by client \ No newline at end of file diff --git a/worlds/oot/HintList.py b/worlds/oot/HintList.py index 06af1a9be3..7fc298b0d7 100644 --- a/worlds/oot/HintList.py +++ b/worlds/oot/HintList.py @@ -1,6 +1,7 @@ import random from BaseClasses import LocationProgressType +from .Items import OOTItem # Abbreviations # DMC Death Mountain Crater @@ -1260,7 +1261,7 @@ def hintExclusions(world, clear_cache=False): world.hint_exclusions = [] for location in world.get_locations(): - if (location.locked and (location.item.type != 'Song' or world.shuffle_song_items != 'song')) or location.progress_type == LocationProgressType.EXCLUDED: + if (location.locked and ((isinstance(location.item, OOTItem) and location.item.type != 'Song') or world.shuffle_song_items != 'song')) or location.progress_type == LocationProgressType.EXCLUDED: world.hint_exclusions.append(location.name) world_location_names = [ diff --git a/worlds/oot/Hints.py b/worlds/oot/Hints.py index 4250c5590e..b8ae7dfafc 100644 --- a/worlds/oot/Hints.py +++ b/worlds/oot/Hints.py @@ -10,6 +10,7 @@ from urllib.error import URLError, HTTPError import json from enum import Enum +from .Items import OOTItem from .HintList import getHint, getHintGroup, Hint, hintExclusions from .Messages import COLOR_MAP, update_message_by_id from .TextBox import line_wrap @@ -480,7 +481,7 @@ def get_specific_item_hint(world, checked): def get_random_location_hint(world, checked): locations = list(filter(lambda location: is_not_checked(location, checked) - and location.item.type not in ('Drop', 'Event', 'Shop', 'DungeonReward') + and not (isinstance(location.item, OOTItem) and location.item.type in ('Drop', 'Event', 'Shop', 'DungeonReward')) # and not (location.parent_region.dungeon and isRestrictedDungeonItem(location.parent_region.dungeon, location.item)) # AP already locks dungeon items and not location.locked and location.name not in world.hint_exclusions diff --git a/worlds/oot/ItemPool.py b/worlds/oot/ItemPool.py index 24dda8e24f..301c502a7e 100644 --- a/worlds/oot/ItemPool.py +++ b/worlds/oot/ItemPool.py @@ -1088,10 +1088,10 @@ def get_pool_core(world): placed_items['Hideout Jail Guard (4 Torches)'] = 'Recovery Heart' skip_in_spoiler_locations.extend(['Hideout Jail Guard (2 Torches)', 'Hideout Jail Guard (3 Torches)', 'Hideout Jail Guard (4 Torches)']) else: - placed_items['Hideout Jail Guard (1 Torch)'] = 'Small Key (Gerudo Fortress)' - placed_items['Hideout Jail Guard (2 Torches)'] = 'Small Key (Gerudo Fortress)' - placed_items['Hideout Jail Guard (3 Torches)'] = 'Small Key (Gerudo Fortress)' - placed_items['Hideout Jail Guard (4 Torches)'] = 'Small Key (Gerudo Fortress)' + placed_items['Hideout Jail Guard (1 Torch)'] = 'Small Key (Thieves Hideout)' + placed_items['Hideout Jail Guard (2 Torches)'] = 'Small Key (Thieves Hideout)' + placed_items['Hideout Jail Guard (3 Torches)'] = 'Small Key (Thieves Hideout)' + placed_items['Hideout Jail Guard (4 Torches)'] = 'Small Key (Thieves Hideout)' if world.shuffle_gerudo_card and world.gerudo_fortress != 'open': pool.append('Gerudo Membership Card') diff --git a/worlds/oot/Items.py b/worlds/oot/Items.py index f07d91a85e..b4c0719700 100644 --- a/worlds/oot/Items.py +++ b/worlds/oot/Items.py @@ -24,6 +24,7 @@ def ap_id_to_oot_data(ap_id): class OOTItem(Item): game: str = "Ocarina of Time" + type: str def __init__(self, name, player, data, event, force_not_advancement): (type, advancement, index, special) = data @@ -38,7 +39,6 @@ class OOTItem(Item): classification = ItemClassification.progression else: classification = ItemClassification.filler - adv = bool(advancement) and not force_not_advancement super(OOTItem, self).__init__(name, classification, oot_data_to_ap_id(data, event), player) self.type = type self.index = index @@ -46,25 +46,12 @@ class OOTItem(Item): self.looks_like_item = None self.price = special.get('price', None) if special else None self.internal = False - - # The playthrough calculation calls a function that uses "sweep_for_events(key_only=True)" - # This checks if the item it's looking for is a small key, using the small key property. - # Because of overlapping item fields, this means that OoT small keys are technically counted, unless we do this. - # This causes them to be double-collected during playthrough and generation. - @property - def smallkey(self) -> bool: - return False - - @property - def bigkey(self) -> bool: - return False @property def dungeonitem(self) -> bool: return self.type in ['SmallKey', 'HideoutSmallKey', 'BossKey', 'GanonBossKey', 'Map', 'Compass'] - # Progressive: True -> Advancement # False -> Priority # None -> Normal diff --git a/worlds/oot/LogicTricks.py b/worlds/oot/LogicTricks.py index 6950bc2124..548b7b969f 100644 --- a/worlds/oot/LogicTricks.py +++ b/worlds/oot/LogicTricks.py @@ -51,20 +51,20 @@ known_logic_tricks = { Can be reached by side-hopping off the watchtower. '''}, - 'Dodongo\'s Cavern Staircase with Bow': { + "Dodongo's Cavern Staircase with Bow": { 'name' : 'logic_dc_staircase', 'tags' : ("Dodongo's Cavern",), 'tooltip' : '''\ The Bow can be used to knock down the stairs with two well-timed shots. '''}, - 'Dodongo\'s Cavern Spike Trap Room Jump without Hover Boots': { + "Dodongo's Cavern Spike Trap Room Jump without Hover Boots": { 'name' : 'logic_dc_jump', 'tags' : ("Dodongo's Cavern",), 'tooltip' : '''\ Jump is adult only. '''}, - 'Dodongo\'s Cavern Vines GS from Below with Longshot': { + "Dodongo's Cavern Vines GS from Below with Longshot": { 'name' : 'logic_dc_vines_gs', 'tags' : ("Dodongo's Cavern", "Skulltulas",), 'tooltip' : '''\ @@ -73,7 +73,7 @@ known_logic_tricks = { from below, by shooting it through the vines, bypassing the need to lower the staircase. '''}, - 'Thieves\' Hideout "Kitchen" with No Additional Items': { + '''Thieves' Hideout "Kitchen" with No Additional Items''': { 'name' : 'logic_gerudo_kitchen', 'tags' : ("Thieves' Hideout", "Gerudo's Fortress"), 'tooltip' : '''\ @@ -157,7 +157,7 @@ known_logic_tricks = { Can jump up to the spinning platform from below as adult. '''}, - 'Crater\'s Bean PoH with Hover Boots': { + "Crater's Bean PoH with Hover Boots": { 'name' : 'logic_crater_bean_poh_with_hovers', 'tags' : ("Death Mountain Crater",), 'tooltip' : '''\ @@ -165,7 +165,7 @@ known_logic_tricks = { near Goron City and walk up the very steep slope. '''}, - 'Zora\'s Domain Entry with Cucco': { + "Zora's Domain Entry with Cucco": { 'name' : 'logic_zora_with_cucco', 'tags' : ("Zora's River",), 'tooltip' : '''\ @@ -404,7 +404,7 @@ known_logic_tricks = { Longshot can be shot through the ceiling to obtain the token with two fewer small keys than normal. '''}, - 'Zora\'s River Lower Freestanding PoH as Adult with Nothing': { + "Zora's River Lower Freestanding PoH as Adult with Nothing": { 'name' : 'logic_zora_river_lower', 'tags' : ("Zora's River",), 'tooltip' : '''\ @@ -502,7 +502,7 @@ known_logic_tricks = { you can get enough of a break to take some time to aim more carefully. '''}, - 'Dodongo\'s Cavern Scarecrow GS with Armos Statue': { + "Dodongo's Cavern Scarecrow GS with Armos Statue": { 'name' : 'logic_dc_scarecrow_gs', 'tags' : ("Dodongo's Cavern", "Skulltulas",), 'tooltip' : '''\ @@ -541,7 +541,7 @@ known_logic_tricks = { 'name' : 'logic_spirit_mq_lower_adult', 'tags' : ("Spirit Temple",), 'tooltip' : '''\ - It can be done with Din\'s Fire and Bow. + It can be done with Din's Fire and Bow. Whenever an arrow passes through a lit torch, it resets the timer. It's finicky but it's also possible to stand on the pillar next to the center @@ -704,13 +704,13 @@ known_logic_tricks = { in the same jump in order to destroy it before you fall into the lava. '''}, - 'Zora\'s Domain Entry with Hover Boots': { + "Zora's Domain Entry with Hover Boots": { 'name' : 'logic_zora_with_hovers', 'tags' : ("Zora's River",), 'tooltip' : '''\ Can hover behind the waterfall as adult. '''}, - 'Zora\'s Domain GS with No Additional Items': { + "Zora's Domain GS with No Additional Items": { 'name' : 'logic_domain_gs', 'tags' : ("Zora's Domain", "Skulltulas",), 'tooltip' : '''\ @@ -736,7 +736,7 @@ known_logic_tricks = { needing a Bow. Applies in both vanilla and MQ Shadow. '''}, - 'Stop Link the Goron with Din\'s Fire': { + "Stop Link the Goron with Din's Fire": { 'name' : 'logic_link_goron_dins', 'tags' : ("Goron City",), 'tooltip' : '''\ @@ -825,7 +825,7 @@ known_logic_tricks = { Link will not be expected to do anything at Gerudo's Fortress. '''}, - 'Zora\'s River Upper Freestanding PoH as Adult with Nothing': { + "Zora's River Upper Freestanding PoH as Adult with Nothing": { 'name' : 'logic_zora_river_upper', 'tags' : ("Zora's River",), 'tooltip' : '''\ @@ -971,7 +971,7 @@ known_logic_tricks = { in the Water Temple are not going to be relevant unless this trick is first enabled. '''}, - 'Water Temple Central Pillar GS with Farore\'s Wind': { + "Water Temple Central Pillar GS with Farore's Wind": { 'name' : 'logic_water_central_gs_fw', 'tags' : ("Water Temple", "Skulltulas",), 'tooltip' : '''\ @@ -1104,7 +1104,7 @@ known_logic_tricks = { this allows you to obtain the GS on the door frame as adult without Hookshot or Song of Time. '''}, - 'Dodongo\'s Cavern MQ Early Bomb Bag Area as Child': { + "Dodongo's Cavern MQ Early Bomb Bag Area as Child": { 'name' : 'logic_dc_mq_child_bombs', 'tags' : ("Dodongo's Cavern",), 'tooltip' : '''\ @@ -1113,7 +1113,7 @@ known_logic_tricks = { without needing a Slingshot. You will take fall damage. '''}, - 'Dodongo\'s Cavern Two Scrub Room with Strength': { + "Dodongo's Cavern Two Scrub Room with Strength": { 'name' : 'logic_dc_scrub_room', 'tags' : ("Dodongo's Cavern",), 'tooltip' : '''\ @@ -1122,7 +1122,7 @@ known_logic_tricks = { destroy the mud wall blocking the room with two Deku Scrubs. '''}, - 'Dodongo\'s Cavern Child Slingshot Skips': { + "Dodongo's Cavern Child Slingshot Skips": { 'name' : 'logic_dc_slingshot_skip', 'tags' : ("Dodongo's Cavern",), 'tooltip' : '''\ @@ -1132,7 +1132,7 @@ known_logic_tricks = { you also enable the Adult variant: "Dodongo's Cavern Spike Trap Room Jump without Hover Boots". '''}, - 'Dodongo\'s Cavern MQ Light the Eyes with Strength': { + "Dodongo's Cavern MQ Light the Eyes with Strength": { 'name' : 'logic_dc_mq_eyes', 'tags' : ("Dodongo's Cavern",), 'tooltip' : '''\ @@ -1145,7 +1145,7 @@ known_logic_tricks = { Also, the bombable floor before King Dodongo can be destroyed with Hammer if hit in the very center. '''}, - 'Dodongo\'s Cavern MQ Back Areas as Child without Explosives': { + "Dodongo's Cavern MQ Back Areas as Child without Explosives": { 'name' : 'logic_dc_mq_child_back', 'tags' : ("Dodongo's Cavern",), 'tooltip' : '''\ @@ -1232,7 +1232,7 @@ known_logic_tricks = { It can also be done as child, using the Slingshot instead of the Bow. '''}, - 'Fire Temple East Tower without Scarecrow\'s Song': { + "Fire Temple East Tower without Scarecrow's Song": { 'name' : 'logic_fire_scarecrow', 'tags' : ("Fire Temple",), 'tooltip' : '''\ @@ -1277,14 +1277,14 @@ known_logic_tricks = { Removes the requirements for the Lens of Truth in Bottom of the Well. '''}, - 'Ganon\'s Castle MQ without Lens of Truth': { + "Ganon's Castle MQ without Lens of Truth": { 'name' : 'logic_lens_castle_mq', 'tags' : ("Lens of Truth","Ganon's Castle",), 'tooltip' : '''\ Removes the requirements for the Lens of Truth in Ganon's Castle MQ. '''}, - 'Ganon\'s Castle without Lens of Truth': { + "Ganon's Castle without Lens of Truth": { 'name' : 'logic_lens_castle', 'tags' : ("Lens of Truth","Ganon's Castle",), 'tooltip' : '''\ diff --git a/worlds/oot/Patches.py b/worlds/oot/Patches.py index 177a4c6165..7bf31c4f7a 100644 --- a/worlds/oot/Patches.py +++ b/worlds/oot/Patches.py @@ -5,6 +5,7 @@ import zlib from collections import defaultdict from functools import partial +from .Items import OOTItem from .LocationList import business_scrubs from .Hints import writeGossipStoneHints, buildAltarHints, \ buildGanonText, getSimpleHintNoPrefix @@ -1881,9 +1882,9 @@ def get_override_entry(player_id, location): type = 2 elif location.type == 'GS Token': type = 3 - elif location.type == 'Shop' and location.item.type != 'Shop': + elif location.type == 'Shop' and not (isinstance(location.item, OOTItem) and location.item.type == 'Shop'): type = 0 - elif location.type == 'GrottoNPC' and location.item.type != 'Shop': + elif location.type == 'GrottoNPC' and not (isinstance(location.item, OOTItem) and location.item.type == 'Shop'): type = 4 elif location.type in ['Song', 'Cutscene']: type = 5 @@ -2103,7 +2104,7 @@ def place_shop_items(rom, world, shop_items, messages, locations, init_shop_id=F shop_objs = { 0x0148 } # "Sold Out" object for location in locations: - if location.item.type == 'Shop': + if isinstance(location.item, OOTItem) and location.item.type == 'Shop': shop_objs.add(location.item.special['object']) rom.write_int16(location.address1, location.item.index) else: diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index b640578c16..fb90b04e77 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -95,7 +95,7 @@ class OOTWorld(World): to rescue the Seven Sages, and then confront Ganondorf to save Hyrule! """ game: str = "Ocarina of Time" - options: dict = oot_options + option_definitions: dict = oot_options topology_present: bool = True item_name_to_id = {item_name: oot_data_to_ap_id(data, False) for item_name, data in item_table.items() if data[2] is not None} diff --git a/worlds/oribf/__init__.py b/worlds/oribf/__init__.py index 33f8d4b07e..05d237659c 100644 --- a/worlds/oribf/__init__.py +++ b/worlds/oribf/__init__.py @@ -17,7 +17,7 @@ class OriBlindForest(World): item_name_to_id = item_table location_name_to_id = lookup_name_to_id - options = options + option_definitions = options hidden = True diff --git a/worlds/raft/__init__.py b/worlds/raft/__init__.py index cf4b7975e5..da4b58f24f 100644 --- a/worlds/raft/__init__.py +++ b/worlds/raft/__init__.py @@ -37,7 +37,7 @@ class RaftWorld(World): lastItemId = max(filter(lambda val: val is not None, item_name_to_id.values())) location_name_to_id = locations_lookup_name_to_id - options = raft_options + option_definitions = raft_options data_version = 2 required_client_version = (0, 3, 4) diff --git a/worlds/rogue_legacy/__init__.py b/worlds/rogue_legacy/__init__.py index af8fa9a791..ba58e133c1 100644 --- a/worlds/rogue_legacy/__init__.py +++ b/worlds/rogue_legacy/__init__.py @@ -30,7 +30,7 @@ class LegacyWorld(World): But that's OK, because no one is perfect, and you don't have to be to succeed. """ game: str = "Rogue Legacy" - options = legacy_options + option_definitions = legacy_options topology_present = False data_version = 3 required_client_version = (0, 2, 3) diff --git a/worlds/ror2/Options.py b/worlds/ror2/Options.py index d747f3801c..727d01ffaa 100644 --- a/worlds/ror2/Options.py +++ b/worlds/ror2/Options.py @@ -6,7 +6,7 @@ class TotalLocations(Range): """Number of location checks which are added to the Risk of Rain playthrough.""" display_name = "Total Locations" range_start = 10 - range_end = 500 + range_end = 250 default = 20 @@ -36,10 +36,14 @@ class AllowLunarItems(DefaultOnToggle): class StartWithRevive(DefaultOnToggle): """Start the game with a `Dio's Best Friend` item.""" display_name = "Start with a Revive" + +class FinalStageDeath(DefaultOnToggle): + """Death on the final boss stage counts as a win.""" + display_name = "Final Stage Death is Win" class GreenScrap(Range): - """Weight of Green Scraps in the item pool.""" + """Weight of Green Scraps in the item pool. (Ignored unless Item Weight Presets is 'No')""" display_name = "Green Scraps" range_start = 0 range_end = 100 @@ -47,7 +51,7 @@ class GreenScrap(Range): class RedScrap(Range): - """Weight of Red Scraps in the item pool.""" + """Weight of Red Scraps in the item pool. (Ignored unless Item Weight Presets is 'No')""" display_name = "Red Scraps" range_start = 0 range_end = 100 @@ -55,7 +59,7 @@ class RedScrap(Range): class YellowScrap(Range): - """Weight of yellow scraps in the item pool.""" + """Weight of yellow scraps in the item pool. (Ignored unless Item Weight Presets is 'No')""" display_name = "Yellow Scraps" range_start = 0 range_end = 100 @@ -63,7 +67,7 @@ class YellowScrap(Range): class WhiteScrap(Range): - """Weight of white scraps in the item pool.""" + """Weight of white scraps in the item pool. (Ignored unless Item Weight Presets is 'No')""" display_name = "White Scraps" range_start = 0 range_end = 100 @@ -71,7 +75,7 @@ class WhiteScrap(Range): class CommonItem(Range): - """Weight of common items in the item pool.""" + """Weight of common items in the item pool. (Ignored unless Item Weight Presets is 'No')""" display_name = "Common Items" range_start = 0 range_end = 100 @@ -79,7 +83,7 @@ class CommonItem(Range): class UncommonItem(Range): - """Weight of uncommon items in the item pool.""" + """Weight of uncommon items in the item pool. (Ignored unless Item Weight Presets is 'No')""" display_name = "Uncommon Items" range_start = 0 range_end = 100 @@ -87,7 +91,7 @@ class UncommonItem(Range): class LegendaryItem(Range): - """Weight of legendary items in the item pool.""" + """Weight of legendary items in the item pool. (Ignored unless Item Weight Presets is 'No')""" display_name = "Legendary Items" range_start = 0 range_end = 100 @@ -95,7 +99,7 @@ class LegendaryItem(Range): class BossItem(Range): - """Weight of boss items in the item pool.""" + """Weight of boss items in the item pool. (Ignored unless Item Weight Presets is 'No')""" display_name = "Boss Items" range_start = 0 range_end = 100 @@ -103,7 +107,7 @@ class BossItem(Range): class LunarItem(Range): - """Weight of lunar items in the item pool.""" + """Weight of lunar items in the item pool. (Ignored unless Item Weight Presets is 'No')""" display_name = "Lunar Items" range_start = 0 range_end = 100 @@ -111,7 +115,7 @@ class LunarItem(Range): class Equipment(Range): - """Weight of equipment items in the item pool.""" + """Weight of equipment items in the item pool. (Ignored unless Item Weight Presets is 'No')""" display_name = "Equipment" range_start = 0 range_end = 100 @@ -122,15 +126,16 @@ class ItemPoolPresetToggle(DefaultOnToggle): """Will use the item weight presets when set to true, otherwise will use the custom set item pool weights.""" display_name = "Item Weight Presets" + class ItemWeights(Choice): - """Preset choices for determining the weights of the item pool.
- New is a test for a potential adjustment to the default weights.
- Uncommon puts a large number of uncommon items in the pool.
- Legendary puts a large number of legendary items in the pool.
- Lunartic makes everything a lunar item.
- Chaos generates the pool completely at random with rarer items having a slight cap to prevent this option being too easy.
- No Scraps removes all scrap items from the item pool.
- Even generates the item pool with every item having an even weight.
+ """Preset choices for determining the weights of the item pool. + New is a test for a potential adjustment to the default weights. + Uncommon puts a large number of uncommon items in the pool. + Legendary puts a large number of legendary items in the pool. + Lunartic makes everything a lunar item. + Chaos generates the pool completely at random with rarer items having a slight cap to prevent this option being too easy. + No Scraps removes all scrap items from the item pool. + Even generates the item pool with every item having an even weight. Scraps Only will be only scrap items in the item pool.""" display_name = "Item Weights" option_default = 0 @@ -143,7 +148,8 @@ class ItemWeights(Choice): option_even = 7 option_scraps_only = 8 -#define a dictionary for the weights of the generated item pool. + +# define a dictionary for the weights of the generated item pool. ror2_weights: typing.Dict[str, type(Option)] = { "green_scrap": GreenScrap, "red_scrap": RedScrap, @@ -161,6 +167,7 @@ ror2_options: typing.Dict[str, type(Option)] = { "total_locations": TotalLocations, "total_revivals": TotalRevivals, "start_with_revive": StartWithRevive, + "final_stage_death": FinalStageDeath, "item_pickup_step": ItemPickupStep, "enable_lunar": AllowLunarItems, "item_weights": ItemWeights, diff --git a/worlds/ror2/Rules.py b/worlds/ror2/Rules.py index 05c08c8803..64d741f99f 100644 --- a/worlds/ror2/Rules.py +++ b/worlds/ror2/Rules.py @@ -1,5 +1,5 @@ from BaseClasses import MultiWorld -from ..generic.Rules import set_rule, add_rule +from worlds.generic.Rules import set_rule, add_rule def set_rules(world: MultiWorld, player: int): diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index 06df244b97..9d0d693b61 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -5,7 +5,7 @@ from .Rules import set_rules from BaseClasses import Region, RegionType, Entrance, Item, ItemClassification, MultiWorld, Tutorial from .Options import ror2_options -from ..AutoWorld import World, WebWorld +from worlds.AutoWorld import World, WebWorld client_version = 1 @@ -28,7 +28,7 @@ class RiskOfRainWorld(World): first crash landing. """ game: str = "Risk of Rain 2" - options = ror2_options + option_definitions = ror2_options topology_present = False item_name_to_id = item_table @@ -110,15 +110,16 @@ class RiskOfRainWorld(World): "seed": "".join(self.world.slot_seeds[self.player].choice(string.digits) for i in range(16)), "totalLocations": self.world.total_locations[self.player].value, "totalRevivals": self.world.total_revivals[self.player].value, - "startWithDio": self.world.start_with_revive[self.player].value + "startWithDio": self.world.start_with_revive[self.player].value, + "FinalStageDeath": self.world.final_stage_death[self.player].value } def create_item(self, name: str) -> Item: item_id = item_table[name] item = RiskOfRainItem(name, ItemClassification.filler, item_id, self.player) - if name == 'Dio\'s Best Friend': + if name == "Dio's Best Friend": item.classification = ItemClassification.progression - elif name == 'Equipment': + elif name in {"Equipment", "Legendary Item"}: item.classification = ItemClassification.useful return item diff --git a/worlds/ror2/docs/en_Risk of Rain 2.md b/worlds/ror2/docs/en_Risk of Rain 2.md index 92232116cc..a58269a35b 100644 --- a/worlds/ror2/docs/en_Risk of Rain 2.md +++ b/worlds/ror2/docs/en_Risk of Rain 2.md @@ -12,6 +12,32 @@ functionality in which certain chests (made clear via a location check progress multiworld. The items that _would have been_ in those chests will be returned to the Risk of Rain player via grants by other players in other worlds. +## What is the goal of Risk of Rain 2 in Archipelago? + +Just like in the original game, any way to "beat the game or obliterate" counts as a win. By default, if you die while +on a final boss stage, that also counts as a win. (You can turn this off in your player settings.) **You do not need to +complete all the location checks** to win; any item you don't collect is automatically sent out to the multiworld when +you meet your goal. + +If you die before you accomplish your goal, you can start a new run. You will start the run with any items that you +received from other players. Any items that you picked up the "normal" way will be lost. + +Note, you can play Simulacrum mode as part of an Archipelago, but you can't achieve any of the victory conditions in +Simulacrum. So you could, for example, collect most of your items through a Simulacrum run, then finish a normal mode +run while keeping the items you received via the multiworld. + +## Can you play multiplayer? + +Yes! You can have a single multiplayer instance as one world in the multiworld. All the players involved need to have +the Archipelago mod, but only the host needs to configure the Archipelago settings. When someone finds an item for your +world, all the connected players will receive a copy of the item, and the location check bar will increase whenever any +player finds an item in Risk of Rain. + +You cannot have players with different player slots in the same co-op game instance. Only the host's Archipelago +settings apply, so each Risk of Rain 2 player slot in the multiworld needs to be a separate game instance. You could, +for example, have two players trade off hosting and making progress on each other's player slot, but a single co-op +instance can't make progress towards multiple player slots in the multiworld. + ## What Risk of Rain items can appear in other players' worlds? The Risk of Rain items are: @@ -31,13 +57,34 @@ in-game item of that tier will appear in the Risk of Rain player's inventory. If the player already has an equipment item equipped then the _item that was equipped_ will be dropped on the ground and _ the new equipment_ will take it's place. (If you want the old one back, pick it up.) +### How many items are there? + +Since a Risk of Rain 2 run can go on indefinitely, you have to configure how many collectible items (also known as +"checks") the game has for purposes of Archipelago when you set up a multiworld. You can configure anywhere from **10 +to 250** items. The number of items will be randomized between all players, so you may want to adjust the number and +item pickup step based on how many items the other players in the multiworld have. (Around 100 seems to be a good +ballpark if you want to have a similar number of items to most other games.) + +After you have completed the specified number of checks, you won't send anything else to the multiworld. You can +receive up to the specified number of randomized items from the multiworld as the players find them. In either case, +you can continue to collect items as normal in Risk of Rain 2 if you've already found all your location checks. + ## What does another world's item look like in Risk of Rain? When the Risk of Rain player fills up their location check bar then the next spawned item will become an item grant for -another player's world. The item in Risk of Rain will disappear in a poof of smoke and the grant will automatically go -out to the multiworld. +another player's world (or possibly get sent back to yourself). The item in Risk of Rain will disappear in a poof of +smoke and the grant will automatically go out to the multiworld. ## What is the item pickup step? The item pickup step is a YAML setting which allows you to set how many items you need to spawn before the _next_ item that is spawned disappears (in a poof of smoke) and goes out to the multiworld. + +## Is Archipelago compatible with other Risk of Rain 2 mods? + +Mostly, yes. Not every mod will work; in particular, anything that causes items to go directly into your inventory +rather than spawning onto the map will interfere with the way the Archipelago mod works. However, many common mods work +just fine with Archipelago. + +For competitive play, of course, you should only use mods that are agreed-upon by the competitors so that you don't +have an unfair advantage. diff --git a/worlds/sa2b/Items.py b/worlds/sa2b/Items.py index bebfa44cd4..d11178f575 100644 --- a/worlds/sa2b/Items.py +++ b/worlds/sa2b/Items.py @@ -2,6 +2,7 @@ import typing from BaseClasses import Item, ItemClassification from .Names import ItemName +from worlds.alttp import ALTTPWorld class ItemData(typing.NamedTuple): @@ -18,9 +19,6 @@ class SA2BItem(Item): def __init__(self, name, classification: ItemClassification, code: int = None, player: int = None): super(SA2BItem, self).__init__(name, classification, code, player) - if self.name == ItemName.sonic_light_shoes or self.name == ItemName.shadow_air_shoes: - self.pedestal_credit_text = "and the Soap Shoes" - # Separate tables for each type of item. emblems_table = { @@ -94,3 +92,6 @@ item_table = { } lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code} + +ALTTPWorld.pedestal_credit_texts[item_table[ItemName.sonic_light_shoes].code] = "and the Soap Shoes" +ALTTPWorld.pedestal_credit_texts[item_table[ItemName.shadow_air_shoes].code] = "and the Soap Shoes" diff --git a/worlds/sa2b/__init__.py b/worlds/sa2b/__init__.py index ffff2a93ea..84a38f221c 100644 --- a/worlds/sa2b/__init__.py +++ b/worlds/sa2b/__init__.py @@ -49,7 +49,7 @@ class SA2BWorld(World): Sonic Adventure 2 Battle is an action platforming game. Play as Sonic, Tails, Knuckles, Shadow, Rogue, and Eggman across 31 stages and prevent the destruction of the earth. """ game: str = "Sonic Adventure 2 Battle" - options = sa2b_options + option_definitions = sa2b_options topology_present = False data_version = 2 diff --git a/worlds/sc2wol/Items.py b/worlds/sc2wol/Items.py index 4ecff7e15f..59b59bc137 100644 --- a/worlds/sc2wol/Items.py +++ b/worlds/sc2wol/Items.py @@ -49,27 +49,27 @@ item_table = { "Projectile Accelerator (Bunker)": ItemData(200 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 0), "Neosteel Bunker (Bunker)": ItemData(201 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 1), - "Titanium Housing (Missile Turret)": ItemData(202 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 2), + "Titanium Housing (Missile Turret)": ItemData(202 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 2, classification=ItemClassification.filler), "Hellstorm Batteries (Missile Turret)": ItemData(203 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 3), "Advanced Construction (SCV)": ItemData(204 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 4), "Dual-Fusion Welders (SCV)": ItemData(205 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 5), - "Fire-Suppression System (Building)": ItemData(206 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 6), + "Fire-Suppression System (Building)": ItemData(206 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 6, classification=ItemClassification.filler), "Orbital Command (Building)": ItemData(207 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 7), "Stimpack (Marine)": ItemData(208 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 8), - "Combat Shield (Marine)": ItemData(209 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 9), - "Advanced Medic Facilities (Medic)": ItemData(210 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 10), - "Stabilizer Medpacks (Medic)": ItemData(211 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 11), + "Combat Shield (Marine)": ItemData(209 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 9, classification=ItemClassification.progression), + "Advanced Medic Facilities (Medic)": ItemData(210 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 10, classification=ItemClassification.progression), + "Stabilizer Medpacks (Medic)": ItemData(211 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 11, classification=ItemClassification.progression), "Incinerator Gauntlets (Firebat)": ItemData(212 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 12, classification=ItemClassification.filler), "Juggernaut Plating (Firebat)": ItemData(213 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 13), "Concussive Shells (Marauder)": ItemData(214 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 14), "Kinetic Foam (Marauder)": ItemData(215 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 15), "U-238 Rounds (Reaper)": ItemData(216 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 16), - "G-4 Clusterbomb (Reaper)": ItemData(217 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 17, classification=ItemClassification.filler), + "G-4 Clusterbomb (Reaper)": ItemData(217 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 17, classification=ItemClassification.progression), "Twin-Linked Flamethrower (Hellion)": ItemData(300 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 0, classification=ItemClassification.filler), "Thermite Filaments (Hellion)": ItemData(301 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 1), - "Cerberus Mine (Vulture)": ItemData(302 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 2), - "Replenishable Magazine (Vulture)": ItemData(303 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 3), + "Cerberus Mine (Vulture)": ItemData(302 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 2, classification=ItemClassification.filler), + "Replenishable Magazine (Vulture)": ItemData(303 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 3, classification=ItemClassification.filler), "Multi-Lock Weapons System (Goliath)": ItemData(304 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 4), "Ares-Class Targeting System (Goliath)": ItemData(305 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 5), "Tri-Lithium Power Cell (Diamondback)": ItemData(306 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 6, classification=ItemClassification.filler), @@ -77,9 +77,9 @@ item_table = { "Maelstrom Rounds (Siege Tank)": ItemData(308 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 8), "Shaped Blast (Siege Tank)": ItemData(309 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 9), "Rapid Deployment Tube (Medivac)": ItemData(310 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 10, classification=ItemClassification.filler), - "Advanced Healing AI (Medivac)": ItemData(311 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 11), + "Advanced Healing AI (Medivac)": ItemData(311 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 11, classification=ItemClassification.filler), "Tomahawk Power Cells (Wraith)": ItemData(312 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 12, classification=ItemClassification.filler), - "Displacement Field (Wraith)": ItemData(313 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 13), + "Displacement Field (Wraith)": ItemData(313 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 13, classification=ItemClassification.filler), "Ripwave Missiles (Viking)": ItemData(314 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 14), "Phobos-Class Weapons System (Viking)": ItemData(315 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 15), "Cross-Spectrum Dampeners (Banshee)": ItemData(316 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 16, classification=ItemClassification.filler), @@ -88,7 +88,7 @@ item_table = { "Defensive Matrix (Battlecruiser)": ItemData(319 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 19, classification=ItemClassification.filler), "Ocular Implants (Ghost)": ItemData(320 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 20), "Crius Suit (Ghost)": ItemData(321 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 21), - "Psionic Lash (Spectre)": ItemData(322 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 22), + "Psionic Lash (Spectre)": ItemData(322 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 22, classification=ItemClassification.progression), "Nyx-Class Cloaking Module (Spectre)": ItemData(323 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 23), "330mm Barrage Cannon (Thor)": ItemData(324 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 24, classification=ItemClassification.filler), "Immortality Protocol (Thor)": ItemData(325 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 25, classification=ItemClassification.filler), @@ -97,12 +97,12 @@ item_table = { "Missile Turret": ItemData(401 + SC2WOL_ITEM_ID_OFFSET, "Building", 1, classification=ItemClassification.progression), "Sensor Tower": ItemData(402 + SC2WOL_ITEM_ID_OFFSET, "Building", 2), - "War Pigs": ItemData(500 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 0), + "War Pigs": ItemData(500 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 0, classification=ItemClassification.progression), "Devil Dogs": ItemData(501 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 1, classification=ItemClassification.filler), "Hammer Securities": ItemData(502 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 2), - "Spartan Company": ItemData(503 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 3), + "Spartan Company": ItemData(503 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 3, classification=ItemClassification.progression), "Siege Breakers": ItemData(504 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 4), - "Hel's Angel": ItemData(505 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 5), + "Hel's Angel": ItemData(505 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 5, classification=ItemClassification.progression), "Dusk Wings": ItemData(506 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 6), "Jackson's Revenge": ItemData(507 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 7), diff --git a/worlds/sc2wol/Locations.py b/worlds/sc2wol/Locations.py index dc2ec74a4b..3425dc7199 100644 --- a/worlds/sc2wol/Locations.py +++ b/worlds/sc2wol/Locations.py @@ -44,7 +44,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("Zero Hour", "Beat Zero Hour", None, lambda state: state._sc2wol_has_common_unit(world, player)), LocationData("Evacuation", "Evacuation: Victory", SC2WOL_LOC_ID_OFFSET + 400, - lambda state: state._sc2wol_has_mobile_anti_air(world, player)), + lambda state: state._sc2wol_has_anti_air(world, player)), LocationData("Evacuation", "Evacuation: First Chysalis", SC2WOL_LOC_ID_OFFSET + 401), LocationData("Evacuation", "Evacuation: Second Chysalis", SC2WOL_LOC_ID_OFFSET + 402, lambda state: state._sc2wol_has_common_unit(world, player)), @@ -52,7 +52,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L lambda state: state._sc2wol_has_common_unit(world, player)), LocationData("Evacuation", "Beat Evacuation", None, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Outbreak", "Outbreak: Victory", SC2WOL_LOC_ID_OFFSET + 500, lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), LocationData("Outbreak", "Outbreak: Left Infestor", SC2WOL_LOC_ID_OFFSET + 501, @@ -63,19 +63,37 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), LocationData("Safe Haven", "Safe Haven: Victory", SC2WOL_LOC_ID_OFFSET + 600, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), + LocationData("Safe Haven", "Safe Haven: North Nexus", SC2WOL_LOC_ID_OFFSET + 601, + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_competent_anti_air(world, player)), + LocationData("Safe Haven", "Safe Haven: East Nexus", SC2WOL_LOC_ID_OFFSET + 602, + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_competent_anti_air(world, player)), + LocationData("Safe Haven", "Safe Haven: South Nexus", SC2WOL_LOC_ID_OFFSET + 603, + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Safe Haven", "Beat Safe Haven", None, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Haven's Fall", "Haven's Fall: Victory", SC2WOL_LOC_ID_OFFSET + 700, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), + LocationData("Haven's Fall", "Haven's Fall: North Hive", SC2WOL_LOC_ID_OFFSET + 701, + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_competent_anti_air(world, player)), + LocationData("Haven's Fall", "Haven's Fall: East Hive", SC2WOL_LOC_ID_OFFSET + 702, + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_competent_anti_air(world, player)), + LocationData("Haven's Fall", "Haven's Fall: South Hive", SC2WOL_LOC_ID_OFFSET + 703, + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Haven's Fall", "Beat Haven's Fall", None, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Smash and Grab", "Smash and Grab: Victory", SC2WOL_LOC_ID_OFFSET + 800, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Smash and Grab", "Smash and Grab: First Relic", SC2WOL_LOC_ID_OFFSET + 801), LocationData("Smash and Grab", "Smash and Grab: Second Relic", SC2WOL_LOC_ID_OFFSET + 802), LocationData("Smash and Grab", "Smash and Grab: Third Relic", SC2WOL_LOC_ID_OFFSET + 803, @@ -101,53 +119,62 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L state._sc2wol_has_anti_air(world, player) and state._sc2wol_has_heavy_defense(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000, - lambda state: state._sc2wol_has_air(world, player)), - LocationData("The Moebius Factor", "The Moebius Factor: 1st Data Core ", SC2WOL_LOC_ID_OFFSET + 1001, - lambda state: state._sc2wol_has_air(world, player) or True), - LocationData("The Moebius Factor", "The Moebius Factor: 2nd Data Core", SC2WOL_LOC_ID_OFFSET + 1002, - lambda state: state._sc2wol_has_air(world, player)), + lambda state: state._sc2wol_has_air(world, player) and state._sc2wol_has_anti_air(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003, - lambda state: state._sc2wol_able_to_rescue(world, player) or True), + lambda state: state._sc2wol_able_to_rescue(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004, - lambda state: state._sc2wol_able_to_rescue(world, player) or True), + lambda state: state._sc2wol_able_to_rescue(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: Mid Rescue", SC2WOL_LOC_ID_OFFSET + 1005, - lambda state: state._sc2wol_able_to_rescue(world, player) or True), + lambda state: state._sc2wol_able_to_rescue(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: Nydus Roof Rescue", SC2WOL_LOC_ID_OFFSET + 1006, - lambda state: state._sc2wol_able_to_rescue(world, player) or True), + lambda state: state._sc2wol_able_to_rescue(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: Alive Inside Rescue", SC2WOL_LOC_ID_OFFSET + 1007, - lambda state: state._sc2wol_able_to_rescue(world, player) or True), + lambda state: state._sc2wol_able_to_rescue(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008, lambda state: state._sc2wol_has_air(world, player)), LocationData("The Moebius Factor", "Beat The Moebius Factor", None, lambda state: state._sc2wol_has_air(world, player)), LocationData("Supernova", "Supernova: Victory", SC2WOL_LOC_ID_OFFSET + 1100, - lambda state: state._sc2wol_has_common_unit(world, player)), + lambda state: state._sc2wol_beats_protoss_deathball(world, player)), LocationData("Supernova", "Supernova: West Relic", SC2WOL_LOC_ID_OFFSET + 1101), - LocationData("Supernova", "Supernova: North Relic", SC2WOL_LOC_ID_OFFSET + 1102, - lambda state: state._sc2wol_has_common_unit(world, player)), + LocationData("Supernova", "Supernova: North Relic", SC2WOL_LOC_ID_OFFSET + 1102), LocationData("Supernova", "Supernova: South Relic", SC2WOL_LOC_ID_OFFSET + 1103, - lambda state: state._sc2wol_has_common_unit(world, player)), + lambda state: state._sc2wol_beats_protoss_deathball(world, player)), LocationData("Supernova", "Supernova: East Relic", SC2WOL_LOC_ID_OFFSET + 1104, - lambda state: state._sc2wol_has_common_unit(world, player)), + lambda state: state._sc2wol_beats_protoss_deathball(world, player)), LocationData("Supernova", "Beat Supernova", None, - lambda state: state._sc2wol_has_common_unit(world, player)), + lambda state: state._sc2wol_beats_protoss_deathball(world, player)), LocationData("Maw of the Void", "Maw of the Void: Victory", SC2WOL_LOC_ID_OFFSET + 1200, - lambda state: state.has('Battlecruiser', player) or state.has('Science Vessel', player) and - state._sc2wol_has_air(world, player)), + lambda state: state.has('Battlecruiser', player) or + state._sc2wol_has_air(world, player) and + state._sc2wol_has_competent_anti_air(world, player) and + state.has('Science Vessel', player)), LocationData("Maw of the Void", "Maw of the Void: Landing Zone Cleared", SC2WOL_LOC_ID_OFFSET + 1201), - LocationData("Maw of the Void", "Maw of the Void: Expansion Prisoners", SC2WOL_LOC_ID_OFFSET + 1202), + LocationData("Maw of the Void", "Maw of the Void: Expansion Prisoners", SC2WOL_LOC_ID_OFFSET + 1202, + lambda state: state.has('Battlecruiser', player) or + state._sc2wol_has_air(world, player) and + state._sc2wol_has_competent_anti_air(world, player) and + state.has('Science Vessel', player)), LocationData("Maw of the Void", "Maw of the Void: South Close Prisoners", SC2WOL_LOC_ID_OFFSET + 1203, - lambda state: state.has('Battlecruiser', player) or state.has('Science Vessel', player) and - state._sc2wol_has_air(world, player)), + lambda state: state.has('Battlecruiser', player) or + state._sc2wol_has_air(world, player) and + state._sc2wol_has_competent_anti_air(world, player) and + state.has('Science Vessel', player)), LocationData("Maw of the Void", "Maw of the Void: South Far Prisoners", SC2WOL_LOC_ID_OFFSET + 1204, - lambda state: state.has('Battlecruiser', player) or state.has('Science Vessel', player) and - state._sc2wol_has_air(world, player)), + lambda state: state.has('Battlecruiser', player) or + state._sc2wol_has_air(world, player) and + state._sc2wol_has_competent_anti_air(world, player) and + state.has('Science Vessel', player)), LocationData("Maw of the Void", "Maw of the Void: North Prisoners", SC2WOL_LOC_ID_OFFSET + 1205, - lambda state: state.has('Battlecruiser', player) or state.has('Science Vessel', player) and - state._sc2wol_has_air(world, player)), + lambda state: state.has('Battlecruiser', player) or + state._sc2wol_has_air(world, player) and + state._sc2wol_has_competent_anti_air(world, player) and + state.has('Science Vessel', player)), LocationData("Maw of the Void", "Beat Maw of the Void", None, - lambda state: state.has('Battlecruiser', player) or state.has('Science Vessel', player) and - state._sc2wol_has_air(world, player)), + lambda state: state.has('Battlecruiser', player) or + state._sc2wol_has_air(world, player) and + state._sc2wol_has_competent_anti_air(world, player) and + state.has('Science Vessel', player)), LocationData("Devil's Playground", "Devil's Playground: Victory", SC2WOL_LOC_ID_OFFSET + 1300, lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), LocationData("Devil's Playground", "Devil's Playground: Tosh's Miners", SC2WOL_LOC_ID_OFFSET + 1301), @@ -157,17 +184,17 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), LocationData("Welcome to the Jungle", "Welcome to the Jungle: Victory", SC2WOL_LOC_ID_OFFSET + 1400, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Welcome to the Jungle", "Welcome to the Jungle: Close Relic", SC2WOL_LOC_ID_OFFSET + 1401), LocationData("Welcome to the Jungle", "Welcome to the Jungle: West Relic", SC2WOL_LOC_ID_OFFSET + 1402, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Welcome to the Jungle", "Welcome to the Jungle: North-East Relic", SC2WOL_LOC_ID_OFFSET + 1403, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Welcome to the Jungle", "Beat Welcome to the Jungle", None, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Breakout", "Breakout: Victory", SC2WOL_LOC_ID_OFFSET + 1500), LocationData("Breakout", "Breakout: Diamondback Prison", SC2WOL_LOC_ID_OFFSET + 1501), LocationData("Breakout", "Breakout: Siegetank Prison", SC2WOL_LOC_ID_OFFSET + 1502), @@ -180,7 +207,8 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("Ghost of a Chance", "Ghost of a Chance: Third Island Spectres", SC2WOL_LOC_ID_OFFSET + 1605), LocationData("Ghost of a Chance", "Beat Ghost of a Chance", None), LocationData("The Great Train Robbery", "The Great Train Robbery: Victory", SC2WOL_LOC_ID_OFFSET + 1700, - lambda state: state._sc2wol_has_train_killers(world, player)), + lambda state: state._sc2wol_has_train_killers(world, player) and + state._sc2wol_has_anti_air(world, player)), LocationData("The Great Train Robbery", "The Great Train Robbery: North Defiler", SC2WOL_LOC_ID_OFFSET + 1701), LocationData("The Great Train Robbery", "The Great Train Robbery: Mid Defiler", SC2WOL_LOC_ID_OFFSET + 1702), LocationData("The Great Train Robbery", "The Great Train Robbery: South Defiler", SC2WOL_LOC_ID_OFFSET + 1703), @@ -198,20 +226,20 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("Cutthroat", "Beat Cutthroat", None, lambda state: state._sc2wol_has_common_unit(world, player)), LocationData("Engine of Destruction", "Engine of Destruction: Victory", SC2WOL_LOC_ID_OFFSET + 1900, - lambda state: state._sc2wol_has_mobile_anti_air(world, player)), + lambda state: state._sc2wol_has_competent_anti_air(world, player)), LocationData("Engine of Destruction", "Engine of Destruction: Odin", SC2WOL_LOC_ID_OFFSET + 1901), LocationData("Engine of Destruction", "Engine of Destruction: Loki", SC2WOL_LOC_ID_OFFSET + 1902, - lambda state: state._sc2wol_has_mobile_anti_air(world, player) and + lambda state: state._sc2wol_has_competent_anti_air(world, player) and state._sc2wol_has_common_unit(world, player) or state.has('Wraith', player)), LocationData("Engine of Destruction", "Engine of Destruction: Lab Devourer", SC2WOL_LOC_ID_OFFSET + 1903), LocationData("Engine of Destruction", "Engine of Destruction: North Devourer", SC2WOL_LOC_ID_OFFSET + 1904, - lambda state: state._sc2wol_has_mobile_anti_air(world, player) and + lambda state: state._sc2wol_has_competent_anti_air(world, player) and state._sc2wol_has_common_unit(world, player) or state.has('Wraith', player)), LocationData("Engine of Destruction", "Engine of Destruction: Southeast Devourer", SC2WOL_LOC_ID_OFFSET + 1905, - lambda state: state._sc2wol_has_mobile_anti_air(world, player) and + lambda state: state._sc2wol_has_competent_anti_air(world, player) and state._sc2wol_has_common_unit(world, player) or state.has('Wraith', player)), LocationData("Engine of Destruction", "Beat Engine of Destruction", None, - lambda state: state._sc2wol_has_mobile_anti_air(world, player) and + lambda state: state._sc2wol_has_competent_anti_air(world, player) and state._sc2wol_has_common_unit(world, player) or state.has('Wraith', player)), LocationData("Media Blitz", "Media Blitz: Victory", SC2WOL_LOC_ID_OFFSET + 2000, lambda state: state._sc2wol_has_competent_comp(world, player)), @@ -224,13 +252,19 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("Media Blitz", "Media Blitz: Science Facility", SC2WOL_LOC_ID_OFFSET + 2004), LocationData("Media Blitz", "Beat Media Blitz", None, lambda state: state._sc2wol_has_competent_comp(world, player)), - LocationData("Piercing the Shroud", "Piercing the Shroud: Victory", SC2WOL_LOC_ID_OFFSET + 2100), + LocationData("Piercing the Shroud", "Piercing the Shroud: Victory", SC2WOL_LOC_ID_OFFSET + 2100, + lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)), LocationData("Piercing the Shroud", "Piercing the Shroud: Holding Cell Relic", SC2WOL_LOC_ID_OFFSET + 2101), - LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk Relic", SC2WOL_LOC_ID_OFFSET + 2102), - LocationData("Piercing the Shroud", "Piercing the Shroud: First Escape Relic", SC2WOL_LOC_ID_OFFSET + 2103), - LocationData("Piercing the Shroud", "Piercing the Shroud: Second Escape Relic", SC2WOL_LOC_ID_OFFSET + 2104), - LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk ", SC2WOL_LOC_ID_OFFSET + 2105), - LocationData("Piercing the Shroud", "Beat Piercing the Shroud", None), + LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk Relic", SC2WOL_LOC_ID_OFFSET + 2102, + lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)), + LocationData("Piercing the Shroud", "Piercing the Shroud: First Escape Relic", SC2WOL_LOC_ID_OFFSET + 2103, + lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)), + LocationData("Piercing the Shroud", "Piercing the Shroud: Second Escape Relic", SC2WOL_LOC_ID_OFFSET + 2104, + lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)), + LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk ", SC2WOL_LOC_ID_OFFSET + 2105, + lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)), + LocationData("Piercing the Shroud", "Beat Piercing the Shroud", None, + lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)), LocationData("Whispers of Doom", "Whispers of Doom: Victory", SC2WOL_LOC_ID_OFFSET + 2200), LocationData("Whispers of Doom", "Whispers of Doom: First Hatchery", SC2WOL_LOC_ID_OFFSET + 2201), LocationData("Whispers of Doom", "Whispers of Doom: Second Hatchery", SC2WOL_LOC_ID_OFFSET + 2202), @@ -251,13 +285,12 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L lambda state: state._sc2wol_has_protoss_common_units(world, player)), LocationData("Echoes of the Future", "Beat Echoes of the Future", None, lambda state: state._sc2wol_has_protoss_medium_units(world, player)), - LocationData("In Utter Darkness", "In Utter Darkness: Defeat", SC2WOL_LOC_ID_OFFSET + 2500, - lambda state: state._sc2wol_has_protoss_medium_units(world, player)), + LocationData("In Utter Darkness", "In Utter Darkness: Defeat", SC2WOL_LOC_ID_OFFSET + 2500), LocationData("In Utter Darkness", "In Utter Darkness: Protoss Archive", SC2WOL_LOC_ID_OFFSET + 2501, lambda state: state._sc2wol_has_protoss_medium_units(world, player)), - LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2502), - LocationData("In Utter Darkness", "Beat In Utter Darkness", None, - lambda state: state._sc2wol_has_protoss_medium_units(world, player)), + LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2502, + lambda state: state._sc2wol_has_protoss_common_units(world, player)), + LocationData("In Utter Darkness", "Beat In Utter Darkness", None), LocationData("Gates of Hell", "Gates of Hell: Victory", SC2WOL_LOC_ID_OFFSET + 2600, lambda state: state._sc2wol_has_competent_comp(world, player)), LocationData("Gates of Hell", "Gates of Hell: Large Army", SC2WOL_LOC_ID_OFFSET + 2601, diff --git a/worlds/sc2wol/LogicMixin.py b/worlds/sc2wol/LogicMixin.py index 7e2fc2f0e8..7a08142672 100644 --- a/worlds/sc2wol/LogicMixin.py +++ b/worlds/sc2wol/LogicMixin.py @@ -10,16 +10,17 @@ class SC2WoLLogic(LogicMixin): return self.has_any({'Marine', 'Marauder'}, player) def _sc2wol_has_air(self, world: MultiWorld, player: int) -> bool: - return self.has_any({'Viking', 'Wraith', 'Medivac', 'Banshee', 'Hercules'}, player) + return self.has_any({'Viking', 'Wraith', 'Banshee'}, player) or \ + self.has_any({'Hercules', 'Medivac'}, player) and self._sc2wol_has_common_unit(world, player) def _sc2wol_has_air_anti_air(self, world: MultiWorld, player: int) -> bool: return self.has_any({'Viking', 'Wraith'}, player) - def _sc2wol_has_mobile_anti_air(self, world: MultiWorld, player: int) -> bool: + def _sc2wol_has_competent_anti_air(self, world: MultiWorld, player: int) -> bool: return self.has_any({'Marine', 'Goliath'}, player) or self._sc2wol_has_air_anti_air(world, player) def _sc2wol_has_anti_air(self, world: MultiWorld, player: int) -> bool: - return self.has('Missile Turret', player) or self._sc2wol_has_mobile_anti_air(world, player) + return self.has_any({'Missile Turret', 'Thor', 'War Pigs', 'Spartan Company', "Hel's Angel", 'Battlecruiser'}, player) or self._sc2wol_has_competent_anti_air(world, player) def _sc2wol_has_heavy_defense(self, world: MultiWorld, player: int) -> bool: return (self.has_any({'Siege Tank', 'Vulture'}, player) or @@ -28,16 +29,18 @@ class SC2WoLLogic(LogicMixin): def _sc2wol_has_competent_comp(self, world: MultiWorld, player: int) -> bool: return (self.has('Marine', player) or self.has('Marauder', player) and - self._sc2wol_has_mobile_anti_air(world, player)) and self.has_any({'Medivac', 'Medic'}, player) or \ - self.has('Thor', player) or self.has("Banshee", player) and self._sc2wol_has_mobile_anti_air(world, player) or \ - self.has('Battlecruiser', player) and self._sc2wol_has_common_unit(world, player) + self._sc2wol_has_competent_anti_air(world, player)) and self.has_any({'Medivac', 'Medic'}, player) or \ + self.has('Thor', player) or self.has("Banshee", player) and self._sc2wol_has_competent_anti_air(world, player) or \ + self.has('Battlecruiser', player) and self._sc2wol_has_common_unit(world, player) or \ + self.has('Siege Tank', player) and self._sc2wol_has_competent_anti_air(world, player) def _sc2wol_has_train_killers(self, world: MultiWorld, player: int) -> bool: return (self.has_any({'Siege Tank', 'Diamondback'}, player) or - self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player)) + self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player) + or self.has('Marauders', player)) def _sc2wol_able_to_rescue(self, world: MultiWorld, player: int) -> bool: - return self.has_any({'Medivac', 'Hercules', 'Raven', 'Orbital Strike'}, player) + return self.has_any({'Medivac', 'Hercules', 'Raven', 'Viking'}, player) def _sc2wol_has_protoss_common_units(self, world: MultiWorld, player: int) -> bool: return self.has_any({'Zealot', 'Immortal', 'Stalker', 'Dark Templar'}, player) @@ -46,6 +49,10 @@ class SC2WoLLogic(LogicMixin): return self._sc2wol_has_protoss_common_units(world, player) and \ self.has_any({'Stalker', 'Void Ray', 'Phoenix', 'Carrier'}, player) + def _sc2wol_beats_protoss_deathball(self, world: MultiWorld, player: int) -> bool: + return self.has_any({'Banshee', 'Battlecruiser'}, player) and self._sc2wol_has_competent_anti_air or \ + self._sc2wol_has_competent_comp(world, player) and self._sc2wol_has_air_anti_air(world, player) + def _sc2wol_cleared_missions(self, world: MultiWorld, player: int, mission_count: int) -> bool: return self.has_group("Missions", player, mission_count) diff --git a/worlds/sc2wol/Regions.py b/worlds/sc2wol/Regions.py index 3a00b60401..4e20752982 100644 --- a/worlds/sc2wol/Regions.py +++ b/worlds/sc2wol/Regions.py @@ -200,7 +200,10 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData connect(world, player, names, "Menu", missions[i]) else: connect(world, player, names, missions[connection], missions[i], - (lambda name: (lambda state: state.has(f"Beat {name}", player)))(missions[connection])) + (lambda name, missions_req: (lambda state: state.has(f"Beat {name}", player) and + state._sc2wol_cleared_missions(world, player, + missions_req))) + (missions[connection], vanilla_shuffle_order[i].number)) connections.append(connection + 1) mission_req_table.update({missions[i]: MissionInfo( diff --git a/worlds/sc2wol/__init__.py b/worlds/sc2wol/__init__.py index 5d48c9c0f4..cf3175bd6e 100644 --- a/worlds/sc2wol/__init__.py +++ b/worlds/sc2wol/__init__.py @@ -33,11 +33,11 @@ class SC2WoLWorld(World): game = "Starcraft 2 Wings of Liberty" web = Starcraft2WoLWebWorld() - data_version = 2 + data_version = 3 item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {location.name: location.code for location in get_locations(None, None)} - options = sc2wol_options + option_definitions = sc2wol_options item_name_groups = item_name_groups locked_locations: typing.List[str] diff --git a/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md b/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md index aba882c7d6..8fa20c86f9 100644 --- a/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md +++ b/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md @@ -1,37 +1,34 @@ # Starcraft 2 Wings of Liberty -## Where is the settings page? - -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a -config file. - ## What does randomization do to this game? -Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game is -always able to be completed. Options exist to also shuffle around the mission order of the campaign. +The following unlocks are randomized as items: +1. Your ability to build any non-worker unit (including Marines!). +2. Your ability to upgrade infantry weapons, infantry armor, vehicle weapons, etc. +3. All armory upgrades +4. All laboratory upgrades +5. All mercenaries +6. Small boosts to your starting mineral and vespene gas totals on each mission -## What is the goal of Starcraft 2 when randomized? +You find items by making progress in bonus objectives (like by rescuing allies in 'Zero Hour') and by completing +missions. When you receive items, they will immediately become available, even during a mission, and you will be +notified via a text box in the top-right corner of the game screen. (The text client for StarCraft 2 also records all +items in all worlds.) -The goal remains unchanged. Beat the final mission All In. +Missions are launched only through the text client. The Hyperion is never visited. Aditionally, credits are not used. -## What items and locations get shuffled? +## What is the goal of this game when randomized? -Unit unlocks, upgrade unlocks, armory upgrades, laboratory researches, and mercenary unlocks can be shuffled, and all -bonus objectives, side missions, mission completions are now locations that can contain these items. +The goal is to beat the final mission: 'All In'. The config file determines which variant you must complete. -## What has been changed from vanilla Starcraft 2? +## What non-randomized changes are there from vanilla Starcraft 2? -Some missions have been given more vespene gas available to mine to allow for a wider variety of unit compositions on -those missions. Starports no longer require Factories in order to be built. In 'A Sinister Turn' and 'Echoes -of the Future', you can research protoss air armor and weapon upgrades. +1. Some missions have more vespene geysers available to allow a wider variety of units. +2. Starports no longer require Factories in order to be built. +3. In 'A Sinister Turn' and 'Echoes of the Future', you can research Protoss air weapon/armor upgrades. -## 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, they will receive a message through their text client and in game if currently playing - a mission. They will immediately be able to use that unlock/upgrade. +## Which of my items can be in another player's world? +By default, any of StarCraft 2's items (specified above) can be in another player's world. See the +[Advanced YAML Guide](https://archipelago.gg/tutorial/Archipelago/advanced_settings/en) +for more information on how to change this. \ No newline at end of file diff --git a/worlds/sc2wol/docs/setup_en.md b/worlds/sc2wol/docs/setup_en.md index 6697277d9f..1539a21291 100644 --- a/worlds/sc2wol/docs/setup_en.md +++ b/worlds/sc2wol/docs/setup_en.md @@ -1,62 +1,69 @@ -# Starcraft 2 Wings of Liberty Randomizer Setup Guide +# StarCraft 2 Wings of Liberty Randomizer Setup Guide + +This guide contains instructions on how to install and troubleshoot the StarCraft 2 Archipelago client, as well as where +to obtain a config file for StarCraft 2. ## Required Software -- [Starcraft 2](https://starcraft2.com/en-us/) -- [Starcraft 2 AP Client](https://github.com/ArchipelagoMW/Archipelago) -- [Starcraft 2 AP Maps and Data](https://github.com/TheCondor07/Starcraft2ArchipelagoData) +- [StarCraft 2](https://starcraft2.com/en-us/) +- [The most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases) +- [StarCraft 2 AP Maps and Data](https://github.com/TheCondor07/Starcraft2ArchipelagoData) -## General Concept +## How do I install this randomizer? -Starcraft 2 AP Client launches a custom version of Starcraft 2 running modified Wings of Liberty campaign maps - to allow for randomization of the items +1. Install StarCraft 2 and Archipelago using the first two links above. (The StarCraft 2 client for Archipelago is + included by default.) +2. Click the third link above and follow the instructions there. +3. Linux users should also follow the instructions found at the bottom of this page + (["Running in Linux"](#running-in-linux)). -## Installation Procedures +## Where do I get a config file (aka "YAML") for this game? -Follow the installation directions at the -[Starcraft 2 AP Maps and Data](https://github.com/TheCondor07/Starcraft2ArchipelagoData) page you can find the .zip -files on the releases page. After it is installed, just run ArchipelagoStarcraft2Client.exe to start the client and connect -to a Multiworld Game. +The [Player Settings](https://archipelago.gg/games/Starcraft%202%20Wings%20of%20Liberty/player-settings) page on this +website allows you to choose your personal settings for the randomizer and download them into a config file. Remember +the name you type in the `Player Name` box; that's the "slot name" the client will ask you for when you attempt to +connect! -## Joining a MultiWorld Game +### And why do I need a config file? + +Config files tell Archipelago how you'd like your game to be randomized, even if you're only using default settings. +When you're setting up a multiworld, every world needs its own config file. +Check out [Creating a YAML](https://archipelago.gg/tutorial/Archipelago/setup/en#creating-a-yaml) for more information. + +## How do I join a MultiWorld game? 1. Run ArchipelagoStarcraft2Client.exe. -2. Type in `/connect [server ip]`. -3. Insert slot name and password as prompted. -4. Once connected, use `/unfinished` to find what missions you can play and `/play [mission id]` to launch a mission. - For new games under default settings the first mission available will always be Liberation Day[1] playable using the - command `/play 1`. +2. Type `/connect [server ip]`. +3. Type your slot name and the server's password when prompted. +4. Once connected, switch to the 'StarCraft 2 Launcher' tab in the client. There, you can see every mission. By default, + only 'Liberation Day' will be available at the beginning. Just click on a mission to start it! -## Where do I get a config file? +## The game isn't launching when I try to start a mission. -The [Player Settings](/games/Starcraft%202%20Wings%20of%20Liberty/player-settings) page on the website allows you to -configure your personal settings and export them into a config file. +First, check the log file for issues (stored at `[Archipelago Directory]/logs/SC2Client.txt`). If the below fix doesn't +work for you, and you can't figure out the log file, visit our [Discord's](https://discord.com/invite/8Z65BR2) +tech-support channel for help. Please include a specific description of what's going wrong and attach your log file to +your message. -## Game isn't launching when I type /play +### Check your installation -First check the log file for issues (stored at [Archipelago Directory]/logs/SC2Client.txt. There is sometimes an issue -where the client can not find Starcraft 2. Usually 'Documents/StarCraft II/ExecuteInfo.txt' is checked to find where -Starcraft 2 is installed. On some computers particularly if you have OneDrive running this may fail. The following -directions may help you in this case if you are on Windows. - -1. Navigate to '%userprofile%'. Easiest way to do this is to hit Windows key+R type in `%userprofile%` and hit run or -type in `%userprofile%` in the navigation bar of your file explorer. -2. If it does not exist create a folder in her named 'Documents'. -3. Locate your 'My Documents' folder on your PC. If you navigate to 'My PC' on the sidebar of file explorer should be a -link to this folder there labeled 'Documents'. -4. Find a folder labeled 'StarCraft II' and copy it. -5. Paste this 'StarCraft II' folder into the folder created or found in step 2. - -These steps have been shown to work for some people for some people having issues with launching the game. If you are -still having issues check out our [Discord](https://discord.com/invite/8Z65BR2) for help. +Make sure you've followed the installation instructions completely. Specifically, make sure that you've placed the Maps +and Mods folders directly inside the StarCraft II installation folder. They should be in the same location as the +SC2Data, Support, Support64, and Versions folders. ## Running in Linux -To run StarCraft 2 through Archipelago in Linux, you will need to install the game using Wine then run the Linux build of the Archipelago client. +To run StarCraft 2 through Archipelago in Linux, you will need to install the game using Wine, then run the Linux build +of the Archipelago client. -Make sure you have StarCraft 2 installed using Wine and you have followed the [Installation Procedures](#installation-procedures) to add the Archipelago maps to the correct location. You will not need to copy the .dll files. If you're having trouble installing or running StarCraft 2 on Linux, I recommend using the Lutris installer. +Make sure you have StarCraft 2 installed using Wine, and that you have followed the +[installation procedures](#how-do-i-install-this-randomizer?) to add the Archipelago maps to the correct location. You will not +need to copy the .dll files. If you're having trouble installing or running StarCraft 2 on Linux, I recommend using the +Lutris installer. -Copy the following into a .sh file, replacing the values of **WINE** and **SC2PATH** variables to the relevant locations, as well as setting **PATH_TO_ARCHIPELAGO** to the directory containing the AppImage if it is not in the same folder as the script. +Copy the following into a .sh file, replacing the values of **WINE** and **SC2PATH** variables with the relevant +locations, as well as setting **PATH_TO_ARCHIPELAGO** to the directory containing the AppImage if it is not in the same +folder as the script. ```sh # Let the client know we're running SC2 in Wine @@ -81,8 +88,11 @@ ARCHIPELAGO="$(ls ${PATH_TO_ARCHIPELAGO:-$(dirname $0)}/Archipelago_*.AppImage | $ARCHIPELAGO Starcraft2Client ``` -For Lutris installs, you can run `lutris -l` to get the numerical ID of your StarCraft II install, then run the command below, replacing **${ID}** with the numerical ID. +For Lutris installs, you can run `lutris -l` to get the numerical ID of your StarCraft II install, then run the command +below, replacing **${ID}** with the numerical ID. lutris lutris:rungameid/${ID} --output-script sc2.sh -This will get all of the relevant environment variables Lutris sets to run StarCraft 2 in a script, including the path to the Wine binary that Lutris uses. You can then remove the line that runs the Battle.Net launcher and copy the code above into the existing script. +This will get all of the relevant environment variables Lutris sets to run StarCraft 2 in a script, including the path +to the Wine binary that Lutris uses. You can then remove the line that runs the Battle.Net launcher and copy the code +above into the existing script. diff --git a/worlds/sm/Options.py b/worlds/sm/Options.py index 07f79f17bd..814b19f4d4 100644 --- a/worlds/sm/Options.py +++ b/worlds/sm/Options.py @@ -143,15 +143,15 @@ class BossRandomization(Toggle): display_name = "Boss Randomization" class FunCombat(Toggle): - """Forces removal of Plasma Beam and Screw Attack if the preset and settings allow it. In addition, can randomly remove Spazer and Wave Beam from the Combat set. If used, might force 'items' accessibility.""" + """Forces removal of Plasma Beam and Screw Attack if the preset and settings allow it. In addition, can randomly remove Spazer and Wave Beam from the Combat set. If used, might force 'minimal' accessibility.""" display_name = "Fun Combat" class FunMovement(Toggle): - """Forces removal of Space Jump if the preset allows it. In addition, can randomly remove High Jump, Grappling Beam, Spring Ball, Speed Booster, and Bombs from the Movement set. If used, might force 'items' accessibility.""" + """Forces removal of Space Jump if the preset allows it. In addition, can randomly remove High Jump, Grappling Beam, Spring Ball, Speed Booster, and Bombs from the Movement set. If used, might force 'minimal' accessibility.""" display_name = "Fun Movement" class FunSuits(Toggle): - """If the preset and seed layout allow it, will force removal of at least one of Varia Suit and/or Gravity Suit. If used, might force 'items' accessibility.""" + """If the preset and seed layout allow it, will force removal of at least one of Varia Suit and/or Gravity Suit. If used, might force 'minimal' accessibility.""" display_name = "Fun Suits" class LayoutPatches(DefaultOnToggle): @@ -182,6 +182,10 @@ class SpinJumpRestart(Toggle): """Allows Samus to start spinning in mid air after jumping or falling.""" display_name = "Spin Jump Restart" +class SpeedKeep(Toggle): + """Let Samus keeps her momentum when landing from a fall or from jumping.""" + display_name = "Momentum conservation (a.k.a. Speedkeep)" + class InfiniteSpaceJump(Toggle): """Space jumps can be done quicker and at any time in air, water or lava, even after falling long distances.""" display_name = "Infinite Space Jump" @@ -266,7 +270,7 @@ sm_options: typing.Dict[str, type(Option)] = { #"item_sounds": "on", "elevators_doors_speed": ElevatorsDoorsSpeed, "spin_jump_restart": SpinJumpRestart, - #"rando_speed": "off", + "rando_speed": SpeedKeep, "infinite_space_jump": InfiniteSpaceJump, "refill_before_save": RefillBeforeSave, "hud": Hud, diff --git a/worlds/sm/Rom.py b/worlds/sm/Rom.py index a01fcbe3a8..e2957fe00f 100644 --- a/worlds/sm/Rom.py +++ b/worlds/sm/Rom.py @@ -1,11 +1,12 @@ import hashlib import os +import json import Utils from Patch import read_rom, APDeltaPatch SMJUHASH = '21f3e98df4780ee1c667b84e57d88675' -ROM_PLAYER_LIMIT = 65535 +ROM_PLAYER_LIMIT = 65535 # max archipelago player ID. note, SM ROM itself will only store 201 names+ids max class SMDeltaPatch(APDeltaPatch): @@ -17,7 +18,6 @@ class SMDeltaPatch(APDeltaPatch): def get_source_data(cls) -> bytes: return get_base_rom_bytes() - def get_base_rom_bytes(file_name: str = "") -> bytes: base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None) if not base_rom_bytes: @@ -40,3 +40,48 @@ def get_base_rom_path(file_name: str = "") -> str: if not os.path.exists(file_name): file_name = Utils.user_path(file_name) return file_name + +def get_sm_symbols(sym_json_path) -> dict: + with open(sym_json_path, "r") as stream: + symbols = json.load(stream) + symboltable = {} + for name, sixdigitaddr in symbols.items(): + (bank, addr_within_bank) = sixdigitaddr.split(":") + bank = int(bank, 16) + addr_within_bank = int(addr_within_bank, 16) + # categorize addresses using snes lorom mapping: + # (reference: https://en.wikibooks.org/wiki/Super_NES_Programming/SNES_memory_map) + if (bank >= 0x70 and bank <= 0x7d): + offset_within_rom_file = None + # SRAM is not continuous, but callers may want it in continuous terms + # SRAM @ data bank $70-$7D, addr_within_bank $0000-$7FFF + # + # symbol aka snes offestwithincontinuousSRAM + # --------------- -------------------------- + # $70:0000-7FFF -> 0x0000- 7FFF + # $71:0000-7FFF -> 0x8000- FFFF + # $72:0000-7FFF -> 0x10000-17FFF + # etc... + offset_within_continuous_sram = (bank - 0x70) * 0x8000 + addr_within_bank + offset_within_wram = None + elif bank == 0x7e or bank == 0x7f or (bank == 0x00 and addr_within_bank <= 0x1fff): + offset_within_rom_file = None + offset_within_continuous_sram = None + offset_within_wram = addr_within_bank + if bank == 0x7f: + offset_within_wram += 0x10000 + elif bank >= 0x80: + offset_within_rom_file = ((bank - 0x80) * 0x8000) + (addr_within_bank % 0x8000) + offset_within_continuous_sram = None + offset_within_wram = None + else: + offset_within_rom_file = None + offset_within_continuous_sram = None + offset_within_wram = None + symboltable[name] = {"bank": bank, + "addr_within_bank": addr_within_bank, + "offset_within_rom_file": offset_within_rom_file, + "offset_within_continuous_sram": offset_within_continuous_sram, + "offset_within_wram": offset_within_wram + } + return symboltable diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 29d428abc2..5da1c40f75 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -5,7 +5,9 @@ import copy import os import threading import base64 -from typing import Set, List, TextIO +from typing import Set, TextIO + +from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils logger = logging.getLogger("Super Metroid") @@ -14,7 +16,7 @@ from .Items import lookup_name_to_id as items_lookup_name_to_id from .Regions import create_regions from .Rules import set_rules, add_entrance_rule from .Options import sm_options -from .Rom import get_base_rom_path, ROM_PLAYER_LIMIT, SMDeltaPatch +from .Rom import get_base_rom_path, ROM_PLAYER_LIMIT, SMDeltaPatch, get_sm_symbols import Utils from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassification, RegionType, CollectionState, Tutorial @@ -77,7 +79,7 @@ class SMWorld(World): game: str = "Super Metroid" topology_present = True data_version = 1 - options = sm_options + option_definitions = sm_options item_names: Set[str] = frozenset(items_lookup_name_to_id) location_names: Set[str] = frozenset(locations_lookup_name_to_id) item_name_to_id = items_lookup_name_to_id @@ -123,8 +125,8 @@ class SMWorld(World): self.remote_items = self.world.remote_items[self.player] if (len(self.variaRando.randoExec.setup.restrictedLocs) > 0): - self.world.accessibility[self.player] = self.world.accessibility[self.player].from_text("items") - logger.warning(f"accessibility forced to 'items' for player {self.world.get_player_name(self.player)} because of 'fun' settings") + self.world.accessibility[self.player] = self.world.accessibility[self.player].from_text("minimal") + logger.warning(f"accessibility forced to 'minimal' for player {self.world.get_player_name(self.player)} because of 'fun' settings") def generate_basic(self): itemPool = self.variaRando.container.itemPool @@ -199,10 +201,7 @@ class SMWorld(World): create_locations(self, self.player) create_regions(self, self.world, self.player) - def getWord(self, w): - return (w & 0x00FF, (w & 0xFF00) >> 8) - - def getWordArray(self, w): + def getWordArray(self, w): # little-endian convert a 16-bit number to an array of numbers <= 255 each return [w & 0x00FF, (w & 0xFF00) >> 8] # used for remote location Credits Spoiler of local items @@ -267,109 +266,177 @@ class SMWorld(World): itemName = "___" + itemName + "___" for char in itemName: - (w0, w1) = self.getWord(charMap.get(char, 0x3C4E)) + [w0, w1] = self.getWordArray(charMap.get(char, 0x3C4E)) data.append(w0) data.append(w1) return data - def APPatchRom(self, romPatcher): - multiWorldLocations = {} - multiWorldItems = {} + def APPrePatchRom(self, romPatcher): + # first apply the sm multiworld code patch named 'basepatch' (also has empty tables that we'll overwrite), + # + apply some patches from varia that we want to be always-on. + # basepatch and variapatches are both generated from https://github.com/lordlou/SMBasepatch + romPatcher.applyIPSPatch(os.path.join(os.path.dirname(__file__), + "data", "SMBasepatch_prebuilt", "multiworld-basepatch.ips")) + romPatcher.applyIPSPatch(os.path.join(os.path.dirname(__file__), + "data", "SMBasepatch_prebuilt", "variapatches.ips")) + + def APPostPatchRom(self, romPatcher): + symbols = get_sm_symbols(os.path.join(os.path.dirname(__file__), + "data", "SMBasepatch_prebuilt", "sm-basepatch-symbols.json")) + multiWorldLocations = [] + multiWorldItems = [] idx = 0 self.playerIDMap = {} - playerIDCount = 0 # 0 is for "Archipelago" server + playerIDCount = 0 # 0 is for "Archipelago" server; highest possible = 200 (201 entries) + vanillaItemTypesCount = 21 for itemLoc in self.world.get_locations(): - romPlayerID = itemLoc.item.player if itemLoc.item.player <= ROM_PLAYER_LIMIT else 0 if itemLoc.player == self.player and locationsDict[itemLoc.name].Id != None: - if itemLoc.item.type in ItemManager.Items: - itemId = ItemManager.Items[itemLoc.item.type].Id + # this SM world can find this item: write full item data to tables and assign player data for writing + romPlayerID = itemLoc.item.player if itemLoc.item.player <= ROM_PLAYER_LIMIT else 0 + if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items: + itemId = ItemManager.Items[itemLoc.item.type].Id else: itemId = ItemManager.Items['ArchipelagoItem'].Id + idx - multiWorldItems[0x029EA3 + idx*64] = self.convertToROMItemName(itemLoc.item.name) + multiWorldItems.append({"sym": symbols["message_item_names"], + "offset": (vanillaItemTypesCount + idx)*64, + "values": self.convertToROMItemName(itemLoc.item.name)}) idx += 1 - + if (romPlayerID > 0 and romPlayerID not in self.playerIDMap.keys()): playerIDCount += 1 self.playerIDMap[romPlayerID] = playerIDCount - (w0, w1) = self.getWord(0 if itemLoc.item.player == self.player else 1) - (w2, w3) = self.getWord(itemId) - (w4, w5) = self.getWord(romPlayerID) - (w6, w7) = self.getWord(0 if itemLoc.item.advancement else 1) - multiWorldLocations[0x1C6000 + locationsDict[itemLoc.name].Id*8] = [w0, w1, w2, w3, w4, w5, w6, w7] + [w0, w1] = self.getWordArray(0 if itemLoc.item.player == self.player else 1) + [w2, w3] = self.getWordArray(itemId) + [w4, w5] = self.getWordArray(romPlayerID) + [w6, w7] = self.getWordArray(0 if itemLoc.item.advancement else 1) + multiWorldLocations.append({"sym": symbols["rando_item_table"], + "offset": locationsDict[itemLoc.name].Id*8, + "values": [w0, w1, w2, w3, w4, w5, w6, w7]}) - if itemLoc.item.player == self.player: + elif itemLoc.item.player == self.player: + # this SM world owns the item: so in case the sending player might not have anything placed in this + # world to receive from it, assign them space in playerIDMap so that the ROM can display their name + # (SM item name not needed, as SM item type id will be in the message they send to this world live) + romPlayerID = itemLoc.player if itemLoc.player <= ROM_PLAYER_LIMIT else 0 if (romPlayerID > 0 and romPlayerID not in self.playerIDMap.keys()): playerIDCount += 1 self.playerIDMap[romPlayerID] = playerIDCount - itemSprites = ["off_world_prog_item.bin", "off_world_item.bin"] + itemSprites = [{"fileName": "off_world_prog_item.bin", + "paletteSymbolName": "prog_item_eight_palette_indices", + "dataSymbolName": "offworld_graphics_data_progression_item"}, + + {"fileName": "off_world_item.bin", + "paletteSymbolName": "nonprog_item_eight_palette_indices", + "dataSymbolName": "offworld_graphics_data_item"}] idx = 0 - offworldSprites = {} - for fileName in itemSprites: - with open(Utils.local_path("lib", "worlds", "sm", "data", "custom_sprite", fileName) if Utils.is_frozen() else Utils.local_path("worlds", "sm", "data", "custom_sprite", fileName), 'rb') as stream: + offworldSprites = [] + for itemSprite in itemSprites: + with open(os.path.join(os.path.dirname(__file__), "data", "custom_sprite", itemSprite["fileName"]), 'rb') as stream: buffer = bytearray(stream.read()) - offworldSprites[0x027882 + 10*(21 + idx) + 2] = buffer[0:8] - offworldSprites[0x049100 + idx*256] = buffer[8:264] + offworldSprites.append({"sym": symbols[itemSprite["paletteSymbolName"]], + "offset": 0, + "values": buffer[0:8]}) + offworldSprites.append({"sym": symbols[itemSprite["dataSymbolName"]], + "offset": 0, + "values": buffer[8:264]}) idx += 1 - - openTourianGreyDoors = {0x07C823 + 5: [0x0C], 0x07C831 + 5: [0x0C]} - deathLink = {0x277f04: [self.world.death_link[self.player].value]} - remoteItem = {0x277f06: self.getWordArray(0b001 + (0b010 if self.remote_items else 0b000))} + deathLink = [{"sym": symbols["config_deathlink"], + "offset": 0, + "values": [self.world.death_link[self.player].value]}] + remoteItem = [{"sym": symbols["config_remote_items"], + "offset": 0, + "values": self.getWordArray(0b001 + (0b010 if self.remote_items else 0b000))}] + ownPlayerId = [{"sym": symbols["config_player_id"], + "offset": 0, + "values": self.getWordArray(self.player)}] - playerNames = {} - playerNameIDMap = {} - playerNames[0x1C5000] = "Archipelago".upper().center(16).encode() - playerNameIDMap[0x1C5800] = self.getWordArray(0) + playerNames = [] + playerNameIDMap = [] + playerNames.append({"sym": symbols["rando_player_table"], + "offset": 0, + "values": "Archipelago".upper().center(16).encode()}) + playerNameIDMap.append({"sym": symbols["rando_player_id_table"], + "offset": 0, + "values": self.getWordArray(0)}) for key,value in self.playerIDMap.items(): - playerNames[0x1C5000 + value * 16] = self.world.player_name[key][:16].upper().center(16).encode() - playerNameIDMap[0x1C5800 + value * 2] = self.getWordArray(key) + playerNames.append({"sym": symbols["rando_player_table"], + "offset": value * 16, + "values": self.world.player_name[key][:16].upper().center(16).encode()}) + playerNameIDMap.append({"sym": symbols["rando_player_id_table"], + "offset": value * 2, + "values": self.getWordArray(key)}) patchDict = { 'MultiWorldLocations': multiWorldLocations, 'MultiWorldItems': multiWorldItems, 'offworldSprites': offworldSprites, - 'openTourianGreyDoors': openTourianGreyDoors, 'deathLink': deathLink, 'remoteItem': remoteItem, + 'ownPlayerId': ownPlayerId, 'PlayerName': playerNames, 'PlayerNameIDMap': playerNameIDMap} + + # convert an array of symbolic byte_edit dicts like {"sym": symobj, "offset": 0, "values": [1, 0]} + # to a single rom patch dict like {0x438c: [1, 0], 0xa4a5: [0, 0, 0]} which varia will understand and apply + def resolve_symbols_to_file_offset_based_dict(byte_edits_arr) -> dict: + this_patch_as_dict = {} + for byte_edit in byte_edits_arr: + offset_within_rom_file = byte_edit["sym"]["offset_within_rom_file"] + byte_edit["offset"] + this_patch_as_dict[offset_within_rom_file] = byte_edit["values"] + return this_patch_as_dict + + for patchname, byte_edits_arr in patchDict.items(): + patchDict[patchname] = resolve_symbols_to_file_offset_based_dict(byte_edits_arr) + romPatcher.applyIPSPatchDict(patchDict) + openTourianGreyDoors = {0x07C823 + 5: [0x0C], 0x07C831 + 5: [0x0C]} + romPatcher.applyIPSPatchDict({'openTourianGreyDoors': openTourianGreyDoors}) + + # set rom name # 21 bytes from Main import __version__ - self.romName = bytearray(f'SM{__version__.replace(".", "")[0:3]}_{self.player}_{self.world.seed:11}\0', 'utf8')[:21] + self.romName = bytearray(f'SM{__version__.replace(".", "")[0:3]}_{self.player}_{self.world.seed:11}', 'utf8')[:21] self.romName.extend([0] * (21 - len(self.romName))) # clients should read from 0x7FC0, the location of the rom title in the SNES header. # duplicative ROM name at 0x1C4F00 is still written here for now, since people with archipelago pre-0.3.0 client installed will still be depending on this location for connecting to SM romPatcher.applyIPSPatch('ROMName', { 'ROMName': {0x1C4F00 : self.romName, 0x007FC0 : self.romName} }) - startItemROMAddressBase = 0x2FD8B9 + startItemROMAddressBase = symbols["start_item_data_major"]["offset_within_rom_file"] - # current, base value or bitmask, max, base value or bitmask - startItemROMDict = {'ETank': [0x8, 0x64, 0xA, 0x64], - 'Missile': [0xC, 0x5, 0xE, 0x5], - 'Super': [0x10, 0x5, 0x12, 0x5], - 'PowerBomb': [0x14, 0x5, 0x16, 0x5], - 'Reserve': [0x1A, 0x64, 0x18, 0x64], - 'Morph': [0x2, 0x4, 0x0, 0x4], - 'Bomb': [0x3, 0x10, 0x1, 0x10], - 'SpringBall': [0x2, 0x2, 0x0, 0x2], - 'HiJump': [0x3, 0x1, 0x1, 0x1], - 'Varia': [0x2, 0x1, 0x0, 0x1], - 'Gravity': [0x2, 0x20, 0x0, 0x20], - 'SpeedBooster': [0x3, 0x20, 0x1, 0x20], - 'SpaceJump': [0x3, 0x2, 0x1, 0x2], - 'ScrewAttack': [0x2, 0x8, 0x0, 0x8], - 'Charge': [0x7, 0x10, 0x5, 0x10], - 'Ice': [0x6, 0x2, 0x4, 0x2], - 'Wave': [0x6, 0x1, 0x4, 0x1], - 'Spazer': [0x6, 0x4, 0x4, 0x4], - 'Plasma': [0x6, 0x8, 0x4, 0x8], - 'Grapple': [0x3, 0x40, 0x1, 0x40], - 'XRayScope': [0x3, 0x80, 0x1, 0x80] + # array for each item: + # offset within ROM table "start_item_data_major" of this item"s info (starting status) + # item bitmask or amount per pickup (BVOB = base value or bitmask), + # offset within ROM table "start_item_data_major" of this item"s info (starting maximum/starting collected items) + # current BVOB max + # ------- ---- --- + startItemROMDict = {"ETank": [ 0x8, 0x64, 0xA], + "Missile": [ 0xC, 0x5, 0xE], + "Super": [0x10, 0x5, 0x12], + "PowerBomb": [0x14, 0x5, 0x16], + "Reserve": [0x1A, 0x64, 0x18], + "Morph": [ 0x2, 0x4, 0x0], + "Bomb": [ 0x3, 0x10, 0x1], + "SpringBall": [ 0x2, 0x2, 0x0], + "HiJump": [ 0x3, 0x1, 0x1], + "Varia": [ 0x2, 0x1, 0x0], + "Gravity": [ 0x2, 0x20, 0x0], + "SpeedBooster": [ 0x3, 0x20, 0x1], + "SpaceJump": [ 0x3, 0x2, 0x1], + "ScrewAttack": [ 0x2, 0x8, 0x0], + "Charge": [ 0x7, 0x10, 0x5], + "Ice": [ 0x6, 0x2, 0x4], + "Wave": [ 0x6, 0x1, 0x4], + "Spazer": [ 0x6, 0x4, 0x4], + "Plasma": [ 0x6, 0x8, 0x4], + "Grapple": [ 0x3, 0x40, 0x1], + "XRayScope": [ 0x3, 0x80, 0x1] + + # BVOB = base value or bitmask } mergedData = {} hasETank = False @@ -377,48 +444,58 @@ class SMWorld(World): hasPlasma = False for startItem in self.startItems: item = startItem.Type - if item == 'ETank': hasETank = True - if item == 'Spazer': hasSpazer = True - if item == 'Plasma': hasPlasma = True - if (item in ['ETank', 'Missile', 'Super', 'PowerBomb', 'Reserve']): - (currentValue, currentBase, maxValue, maxBase) = startItemROMDict[item] + if item == "ETank": hasETank = True + if item == "Spazer": hasSpazer = True + if item == "Plasma": hasPlasma = True + if (item in ["ETank", "Missile", "Super", "PowerBomb", "Reserve"]): + (currentValue, amountPerItem, maxValue) = startItemROMDict[item] if (startItemROMAddressBase + currentValue) in mergedData: - mergedData[startItemROMAddressBase + currentValue] += currentBase - mergedData[startItemROMAddressBase + maxValue] += maxBase + mergedData[startItemROMAddressBase + currentValue] += amountPerItem + mergedData[startItemROMAddressBase + maxValue] += amountPerItem else: - mergedData[startItemROMAddressBase + currentValue] = currentBase - mergedData[startItemROMAddressBase + maxValue] = maxBase + mergedData[startItemROMAddressBase + currentValue] = amountPerItem + mergedData[startItemROMAddressBase + maxValue] = amountPerItem else: - (collected, currentBitmask, equipped, maxBitmask) = startItemROMDict[item] + (collected, bitmask, equipped) = startItemROMDict[item] if (startItemROMAddressBase + collected) in mergedData: - mergedData[startItemROMAddressBase + collected] |= currentBitmask - mergedData[startItemROMAddressBase + equipped] |= maxBitmask + mergedData[startItemROMAddressBase + collected] |= bitmask + mergedData[startItemROMAddressBase + equipped] |= bitmask else: - mergedData[startItemROMAddressBase + collected] = currentBitmask - mergedData[startItemROMAddressBase + equipped] = maxBitmask + mergedData[startItemROMAddressBase + collected] = bitmask + mergedData[startItemROMAddressBase + equipped] = bitmask if hasETank: + # we are overwriting the starting energy, so add up the E from 99 (normal starting energy) rather than from 0 mergedData[startItemROMAddressBase + 0x8] += 99 mergedData[startItemROMAddressBase + 0xA] += 99 if hasSpazer and hasPlasma: + # de-equip spazer. + # otherwise, firing the unintended spazer+plasma combo would cause massive game glitches and crashes mergedData[startItemROMAddressBase + 0x4] &= ~0x4 for key, value in mergedData.items(): if (key - startItemROMAddressBase > 7): - (w0, w1) = self.getWord(value) + [w0, w1] = self.getWordArray(value) mergedData[key] = [w0, w1] else: mergedData[key] = [value] - - startItemPatch = { 'startItemPatch': mergedData } - romPatcher.applyIPSPatch('startItemPatch', startItemPatch) + startItemPatch = { "startItemPatch": mergedData } + romPatcher.applyIPSPatch("startItemPatch", startItemPatch) + + # commit all the changes we've made here to the ROM romPatcher.commitIPS() - itemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type if itemLoc.item.type in ItemManager.Items else 'ArchipelagoItem'], locationsDict[itemLoc.name], True) for itemLoc in self.world.get_locations() if itemLoc.player == self.player] - romPatcher.writeItemsLocs(itemLocs) + itemLocs = [ + ItemLocation(ItemManager.Items[itemLoc.item.type + if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items else + 'ArchipelagoItem'], + locationsDict[itemLoc.name], True) + for itemLoc in self.world.get_locations() if itemLoc.player == self.player + ] + romPatcher.writeItemsLocs(itemLocs) itemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type], locationsDict[itemLoc.name] if itemLoc.name in locationsDict and itemLoc.player == self.player else self.DummyLocation(self.world.get_player_name(itemLoc.player) + " " + itemLoc.name), True) for itemLoc in self.world.get_locations() if itemLoc.item.player == self.player] progItemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type], locationsDict[itemLoc.name] if itemLoc.name in locationsDict and itemLoc.player == self.player else self.DummyLocation(self.world.get_player_name(itemLoc.player) + " " + itemLoc.name), True) for itemLoc in self.world.get_locations() if itemLoc.item.player == self.player and itemLoc.item.advancement == True] @@ -435,7 +512,7 @@ class SMWorld(World): outputFilename = os.path.join(output_directory, f'{outfilebase}{outfilepname}.sfc') try: - self.variaRando.PatchRom(outputFilename, self.APPatchRom) + self.variaRando.PatchRom(outputFilename, self.APPrePatchRom, self.APPostPatchRom) self.write_crc(outputFilename) self.rom_name = self.romName except: @@ -490,7 +567,7 @@ class SMWorld(World): def fill_slot_data(self): slot_data = {} if not self.world.is_race: - for option_name in self.options: + for option_name in self.option_definitions: option = getattr(self.world, option_name)[self.player] slot_data[option_name] = option.value @@ -527,7 +604,7 @@ class SMWorld(World): def create_item(self, name: str) -> Item: item = next(x for x in ItemManager.Items.values() if x.Name == name) - return SMItem(item.Name, ItemClassification.progression, item.Type, self.item_name_to_id[item.Name], + return SMItem(item.Name, ItemClassification.progression if item.Class != 'Minor' else ItemClassification.filler, item.Type, self.item_name_to_id[item.Name], player=self.player) def get_filler_item_name(self) -> str: @@ -654,18 +731,18 @@ class SMLocation(Location): def can_comeback(self, state: CollectionState, item: Item): randoExec = state.world.worlds[self.player].variaRando.randoExec for key in locationsDict[self.name].AccessFrom.keys(): - if (randoExec.areaGraph.canAccess( state.smbm[self.player], - key, - randoExec.graphSettings.startAP, - state.smbm[self.player].maxDiff, - None)): + if (randoExec.areaGraph.canAccessList( state.smbm[self.player], + key, + [randoExec.graphSettings.startAP, 'Landing Site'] if not GraphUtils.isStandardStart(randoExec.graphSettings.startAP) else ['Landing Site'], + state.smbm[self.player].maxDiff)): return True return False class SMItem(Item): game = "Super Metroid" + type: str - def __init__(self, name, classification, type, code, player: int = None): + def __init__(self, name, classification, type: str, code, player: int): super(SMItem, self).__init__(name, classification, code, player) self.type = type diff --git a/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips b/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips new file mode 100644 index 0000000000..d7fd17613e Binary files /dev/null and b/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips differ diff --git a/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym b/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym new file mode 100644 index 0000000000..751f470f53 --- /dev/null +++ b/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym @@ -0,0 +1,689 @@ +; wla symbolic information file +; generated by asar + +[labels] +B8:8026 :neg_1_1 +85:B9B4 :neg_1_2 +85:B9E6 :neg_1_3 +B8:C81F :neg_1_4 +B8:C831 :neg_1_5 +B8:C843 :neg_1_6 +B8:800C :pos_1_0 +B8:81DE :pos_1_1 +84:FA6B :pos_1_2 +84:FA75 :pos_1_3 +B8:C862 :pos_1_4 +B8:C86F :pos_1_5 +B8:C87C :pos_1_6 +85:FF00 CLIPCHECK +85:9900 CLIPLEN +85:990F CLIPLEN_end +85:990C CLIPLEN_no_multi +85:FF1D CLIPSET +B8:80EF COLLECTTANK +85:FF45 MISCFX +84:8BF2 NORMAL +85:FF4E SETFX +85:FF30 SOUNDFX +84:F9E0 SOUNDFX_84 +85:FF3C SPECIALFX +84:F896 ammo_loop_table +84:F874 archipelago_chozo_item_plm +84:F878 archipelago_hidden_item_plm +84:F870 archipelago_visible_item_plm +84:F892 c_item +CE:FF04 config_deathlink +CE:FF00 config_flags +CE:FF00 config_multiworld +CE:FF08 config_player_id +CE:FF06 config_remote_items +CE:FF02 config_sprite +84:F894 h_item +84:F8AD i_chozo_item +84:F8B4 i_hidden_item +84:FA5A i_hidden_item_setup +B8:885C i_item_setup_shared +B8:8878 i_item_setup_shared_all_items +B8:8883 i_item_setup_shared_alwaysloaded +84:FA79 i_live_pickup +B8:817F i_live_pickup_multiworld +B8:81C4 i_live_pickup_multiworld_end +B8:819B i_live_pickup_multiworld_local_item_or_offworld +B8:81B0 i_live_pickup_multiworld_own_item +B8:81BC i_live_pickup_multiworld_own_item1 +84:FA1E i_load_custom_graphics +84:FA39 i_load_custom_graphics_all_items +84:FA49 i_load_custom_graphics_alwaysloaded +84:FA61 i_load_rando_item +84:FA78 i_load_rando_item_end +84:F9F1 i_start_draw_loop +84:FA0A i_start_draw_loop_all_items +84:F9EC i_start_draw_loop_hidden +84:FA1C i_start_draw_loop_non_ammo_item +84:F9E5 i_start_draw_loop_visible_or_chozo +84:F8A6 i_visible_item +84:FA53 i_visible_item_setup +85:BA8A message_PlaceholderBig +85:BA0A message_char_table +85:BABC message_hook_tilemap_calc +85:BADC message_hook_tilemap_calc_msgbox_mwrecv +85:BACE message_hook_tilemap_calc_msgbox_mwsend +85:824C message_hook_tilemap_calc_normal +85:BAC9 message_hook_tilemap_calc_vanilla +85:9963 message_item_names +85:B8A3 message_item_received +85:B9A3 message_item_received_end +85:B7A3 message_item_sent +85:B8A3 message_item_sent_end +85:BA95 message_multiworld_init_new_messagebox_if_needed +85:BAB1 message_multiworld_init_new_messagebox_if_needed_msgbox_mwrecv +85:BAB1 message_multiworld_init_new_messagebox_if_needed_msgbox_mwsend +85:BAA9 message_multiworld_init_new_messagebox_if_needed_vanilla +85:B9A3 message_write_placeholders +85:B9A5 message_write_placeholders_adjust +85:BA04 message_write_placeholders_end +85:B9CA message_write_placeholders_loop +85:B9DC message_write_placeholders_notfound +85:B9DF message_write_placeholders_value_ok +B8:8092 mw_display_item_sent +B8:80FF mw_handle_queue +B8:8178 mw_handle_queue_end +B8:8101 mw_handle_queue_loop +B8:8151 mw_handle_queue_new_remote_item +B8:816D mw_handle_queue_next +B8:8163 mw_handle_queue_perform_receive +B8:81C8 mw_hook_main_game +B8:8011 mw_init +B8:8044 mw_init_end +B8:8000 mw_init_memory +B8:8083 mw_load_sram +B8:80B0 mw_receive_item +B8:80E8 mw_receive_item_end +B8:8070 mw_save_sram +B8:8049 mw_write_message +84:F888 nonprog_item_eight_palette_indices +89:9200 offworld_graphics_data_item +89:9100 offworld_graphics_data_progression_item +84:F972 p_chozo_item +84:F9A0 p_chozo_item_end +84:F98D p_chozo_item_loop +84:F999 p_chozo_item_trigger +84:F8FB p_etank_hloop +84:F8BB p_etank_loop +84:F9A6 p_hidden_item +84:F9D8 p_hidden_item_end +84:F9BD p_hidden_item_loop +84:F9A8 p_hidden_item_loop2 +84:F9D1 p_hidden_item_trigger +84:F90F p_missile_hloop +84:F8CB p_missile_loop +84:F937 p_pb_hloop +84:F8EB p_pb_loop +84:F923 p_super_hloop +84:F8DB p_super_loop +84:F94B p_visible_item +84:F96E p_visible_item_end +84:F95B p_visible_item_loop +84:F967 p_visible_item_trigger +B8:81DF patch_load_multiworld +84:FA7E perform_item_pickup +84:F886 plm_graphics_entry_offworld_item +84:F87C plm_graphics_entry_offworld_progression_item +84:FA90 plm_sequence_generic_item_0_bitmask +84:F87E prog_item_eight_palette_indices +B8:E000 rando_item_table +B8:DC90 rando_player_id_table +B8:DE22 rando_player_id_table_end +B8:D000 rando_player_table +B8:CF00 rando_seed_data +B8:8800 sm_item_graphics +B8:882E sm_item_plm_pickup_sequence_pointers +B8:C81C start_item +B8:C800 start_item_data_major +B8:C808 start_item_data_minor +B8:C818 start_item_data_reserve +B8:C856 update_graphic +84:F890 v_item + +[source files] +0000 e25029c5 main.asm +0001 06780555 ../common/nofanfare.asm +0002 e76d1f83 ../common/multiworld.asm +0003 613d24e1 ../common/itemextras.asm +0004 d6616c0c ../common/items.asm +0005 440b54fe ../common/startitem.asm + +[rom checksum] +09b134c5 + +[addr-to-line mapping] +ff:ffff 0000:00000001 +85:ff00 0001:0000010b +85:ff03 0001:0000010c +85:ff06 0001:0000010d +85:ff08 0001:0000010e +85:ff0b 0001:0000010f +85:ff0f 0001:00000110 +85:ff12 0001:00000111 +85:ff16 0001:00000112 +85:ff19 0001:00000113 +85:ff1c 0001:00000114 +85:ff1d 0001:00000117 +85:ff20 0001:00000118 +85:ff24 0001:00000119 +85:ff28 0001:0000011a +85:ff2b 0001:0000011c +85:ff2f 0001:0000011d +85:ff30 0001:00000120 +85:ff34 0001:00000121 +85:ff37 0001:00000122 +85:ff3b 0001:00000123 +85:ff3c 0001:00000126 +85:ff40 0001:00000127 +85:ff44 0001:00000128 +85:ff45 0001:0000012b +85:ff49 0001:0000012c +85:ff4d 0001:0000012d +85:ff4e 0001:00000131 +85:ff51 0001:00000132 +85:ff54 0001:00000134 +85:ff57 0001:00000135 +85:ff58 0001:00000136 +85:8490 0001:0000013a +85:9900 0001:0000013e +85:9901 0001:0000013f +85:9905 0001:00000140 +85:9907 0001:00000141 +85:990a 0001:00000142 +85:990c 0001:00000144 +85:990f 0001:00000146 +85:9910 0001:00000147 +82:e126 0001:0000014a +82:e12a 0001:0000014b +85:8089 0001:0000014e +84:8bf2 0001:00000152 +84:8bf6 0001:00000153 +84:8bf7 0001:00000153 +b8:8000 0002:00000019 +b8:8002 0002:0000001a +b8:8006 0002:0000001b +b8:8008 0002:0000001c +b8:800c 0002:00000020 +b8:800e 0002:00000021 +b8:8010 0002:00000022 +b8:8011 0002:00000025 +b8:8012 0002:00000025 +b8:8013 0002:00000025 +b8:8014 0002:00000025 +b8:8015 0000:00000013 +b8:8017 0002:00000029 +b8:801b 0002:0000002a +b8:801e 0002:0000002b +b8:8020 0002:0000002d +b8:8023 0002:0000002e +b8:8026 0002:00000031 +b8:802a 0002:00000032 +b8:802e 0002:00000033 +b8:8032 0002:00000034 +b8:8036 0002:00000035 +b8:8037 0002:00000035 +b8:8038 0002:00000036 +b8:803b 0002:00000037 +b8:803d 0002:00000039 +b8:8040 0002:0000003a +b8:8044 0002:0000003d +b8:8045 0002:0000003d +b8:8046 0002:0000003d +b8:8047 0002:0000003d +b8:8048 0002:0000003e +b8:8049 0002:00000043 +b8:804a 0002:00000043 +b8:804b 0002:00000044 +b8:804c 0002:00000044 +b8:804d 0002:00000045 +b8:8051 0002:00000046 +b8:8054 0002:00000046 +b8:8055 0002:00000047 +b8:8056 0002:00000048 +b8:805a 0002:00000049 +b8:805b 0002:0000004a +b8:805f 0002:0000004b +b8:8060 0002:0000004c +b8:8064 0002:0000004e +b8:8068 0002:0000004f +b8:8069 0002:00000050 +b8:806d 0002:00000051 +b8:806e 0002:00000051 +b8:806f 0002:00000052 +b8:8070 0002:00000055 +b8:8071 0002:00000055 +b8:8072 0000:00000013 +b8:8074 0002:00000057 +b8:8078 0002:00000058 +b8:807c 0002:00000059 +b8:807d 0002:00000059 +b8:807e 0002:0000005b +b8:807f 0002:0000005c +b8:8082 0002:0000005d +b8:8083 0002:00000060 +b8:8084 0002:00000060 +b8:8085 0000:00000013 +b8:8087 0002:00000062 +b8:808b 0002:00000063 +b8:808f 0002:00000064 +b8:8090 0002:00000064 +b8:8091 0002:00000065 +b8:8092 0002:0000006a +b8:8094 0002:0000006b +b8:8096 0002:0000006e +b8:8099 0002:0000006f +b8:809b 0002:00000070 +b8:809e 0002:00000071 +b8:80a0 0002:00000072 +b8:80a3 0002:00000073 +b8:80a7 0002:00000074 +b8:80a9 0002:00000075 +b8:80ab 0002:00000076 +b8:80ad 0002:00000077 +b8:80af 0002:00000078 +b8:80b0 0002:0000007c +b8:80b1 0002:0000007c +b8:80b2 0002:0000007d +b8:80b5 0002:0000007e +b8:80b7 0002:0000007f +b8:80ba 0002:00000080 +b8:80bc 0002:00000081 +b8:80bd 0002:00000082 +b8:80be 0002:00000084 +b8:80c1 0002:00000085 +b8:80c3 0002:00000086 +b8:80c6 0002:00000087 +b8:80c7 0002:00000088 +b8:80ca 0002:00000089 +b8:80cb 0002:00000089 +b8:80cc 0002:0000008a +b8:80d0 0002:0000008b +b8:80d1 0002:0000008c +b8:80d4 0002:0000008d +b8:80d8 0002:0000008e +b8:80da 0002:00000090 +b8:80dd 0002:00000091 +b8:80df 0002:00000092 +b8:80e2 0002:00000093 +b8:80e4 0002:00000095 +b8:80e8 0002:00000097 +b8:80ea 0002:00000098 +b8:80ec 0002:00000099 +b8:80ed 0002:00000099 +b8:80ee 0002:0000009a +b8:80ef 0002:000000a5 +b8:80f0 0002:000000a6 +b8:80f4 0002:000000a7 +b8:80f5 0002:000000a8 +b8:80f9 0002:000000a9 +b8:80fa 0002:000000ab +b8:80fe 0002:000000ac +b8:80ff 0002:000000de +b8:8100 0002:000000de +b8:8101 0002:000000e1 +b8:8105 0002:000000e2 +b8:8109 0002:000000e3 +b8:810b 0002:000000e5 +b8:810d 0002:000000e5 +b8:810e 0002:000000e8 +b8:8112 0002:000000e9 +b8:8114 0002:000000ea +b8:8118 0002:000000eb +b8:811a 0002:000000ec +b8:811e 0002:000000ed +b8:8121 0002:000000ee +b8:8123 0002:000000ef +b8:8125 0002:000000f0 +b8:8129 0002:000000f1 +b8:812b 0002:000000f2 +b8:812d 0002:000000f3 +b8:8130 0002:000000f4 +b8:8133 0002:000000f5 +b8:8135 0002:000000f6 +b8:813d 0002:000000fa +b8:813e 0002:000000fb +b8:813f 0002:000000fc +b8:8143 0002:000000ff +b8:8147 0002:00000100 +b8:814b 0002:00000101 +b8:814d 0002:00000103 +b8:814e 0002:00000104 +b8:814f 0002:00000105 +b8:8151 0002:0000010a +b8:8152 0002:0000010b +b8:8156 0002:0000010e +b8:815a 0002:0000010f +b8:815e 0002:00000110 +b8:8162 0002:00000111 +b8:8163 0002:00000115 +b8:8165 0002:00000116 +b8:8168 0002:00000117 +b8:816a 0002:00000118 +b8:816d 0002:0000011b +b8:8171 0002:0000011c +b8:8172 0002:0000011d +b8:8176 0002:0000011f +b8:8178 0002:00000122 +b8:817a 0002:00000123 +b8:817c 0002:00000124 +b8:817d 0002:00000124 +b8:817e 0002:00000125 +b8:817f 0002:00000129 +b8:8180 0002:00000129 +b8:8181 0002:00000129 +b8:8182 0002:0000012a +b8:8186 0002:0000012b +b8:8189 0002:0000012b +b8:818a 0002:0000012d +b8:818e 0002:0000012e +b8:818f 0002:0000012f +b8:8193 0002:00000130 +b8:8196 0002:00000131 +b8:8198 0002:00000133 +b8:819b 0002:00000136 +b8:819f 0002:00000137 +b8:81a3 0002:00000138 +b8:81a5 0002:0000013a +b8:81a9 0002:0000013b +b8:81aa 0002:0000013d +b8:81ae 0002:0000013e +b8:81b0 0002:00000141 +b8:81b4 0002:00000142 +b8:81b7 0002:00000143 +b8:81b9 0002:00000144 +b8:81bc 0002:00000147 +b8:81bd 0002:00000148 +b8:81be 0002:00000149 +b8:81c2 0002:0000014a +b8:81c4 0002:0000014d +b8:81c5 0002:0000014d +b8:81c6 0002:0000014d +b8:81c7 0002:0000014e +b8:81c8 0002:00000152 +b8:81cc 0002:00000153 +b8:81d0 0002:00000154 +b8:81d2 0002:00000155 +b8:81d6 0002:00000156 +b8:81d9 0002:00000157 +b8:81db 0002:00000158 +b8:81de 0002:0000015a +b8:81df 0002:0000015d +b8:81e3 0002:0000015e +b8:81e4 0002:0000015f +b8:81e7 0002:00000160 +b8:81eb 0002:00000162 +b8:81ec 0002:00000163 +b8:81ed 0002:00000164 +b8:81ee 0002:00000165 +b8:81ef 0002:00000166 +8b:914a 0002:0000016b +81:80f7 0002:0000016e +81:8027 0002:00000171 +82:8bb3 0002:00000174 +85:b9a3 0002:0000020e +85:b9a4 0002:0000020e +85:b9a5 0002:00000211 +85:b9a7 0002:00000212 +85:b9ad 0002:00000212 +85:b9ae 0002:00000213 +85:b9b1 0002:00000214 +85:b9b2 0002:00000215 +85:b9b3 0002:00000215 +85:b9b4 0002:00000219 +85:b9b7 0002:0000021a +85:b9bb 0002:0000021b +85:b9bd 0002:0000021b +85:b9bf 0002:0000021c +85:b9c2 0002:0000021d +85:b9c4 0002:0000021f +85:b9c5 0002:00000220 +85:b9c7 0002:00000224 +85:b9ca 0002:00000226 +85:b9cd 0002:00000227 +85:b9cf 0002:00000228 +85:b9d1 0002:00000229 +85:b9d5 0002:0000022a +85:b9d7 0002:0000022b +85:b9d9 0002:0000022c +85:b9da 0002:0000022d +85:b9dc 0002:0000022f +85:b9df 0002:00000231 +85:b9e2 0002:00000231 +85:b9e3 0002:00000232 +85:b9e6 0002:00000234 +85:b9ea 0002:00000235 +85:b9ed 0002:00000236 +85:b9ee 0002:00000237 +85:b9ef 0002:00000237 +85:b9f0 0002:00000238 +85:b9f4 0002:00000239 +85:b9f5 0002:0000023a +85:b9f9 0002:0000023b +85:b9fb 0002:0000023c +85:b9fc 0002:0000023d +85:b9fd 0002:0000023e +85:ba00 0002:0000023f +85:ba02 0002:00000240 +85:ba04 0002:00000243 +85:ba05 0002:00000243 +85:ba06 0002:00000244 +85:ba09 0002:00000245 +85:ba8a 0002:00000253 +85:ba8c 0002:00000254 +85:ba8f 0002:00000255 +85:ba92 0002:00000256 +85:ba95 0002:0000025e +85:ba96 0002:0000025f +85:ba98 0002:00000260 +85:ba9b 0002:00000261 +85:ba9d 0002:00000262 +85:ba9f 0002:00000263 +85:baa2 0002:00000264 +85:baa4 0002:00000265 +85:baa7 0002:00000266 +85:baa9 0002:00000269 +85:baaa 0002:0000026a +85:baab 0002:0000026b +85:baac 0002:0000026c +85:baae 0002:0000026d +85:baaf 0002:0000026e +85:bab0 0002:0000026f +85:bab1 0002:00000274 +85:bab4 0002:00000275 +85:bab5 0002:00000276 +85:bab8 0002:00000277 +85:bab9 0002:00000278 +85:baba 0002:00000279 +85:babb 0002:0000027a +85:babc 0002:00000285 +85:babd 0002:00000286 +85:babf 0002:00000287 +85:bac2 0002:00000288 +85:bac4 0002:00000289 +85:bac7 0002:0000028a +85:bac9 0002:0000028d +85:baca 0002:0000028e +85:bacb 0002:0000028f +85:bacd 0002:00000290 +85:bace 0002:00000292 +85:bacf 0002:00000293 +85:bad1 0002:00000294 +85:bad4 0002:00000295 +85:bad6 0002:00000296 +85:bad9 0002:00000297 +85:badb 0002:00000298 +85:badc 0002:0000029a +85:badd 0002:0000029b +85:badf 0002:0000029c +85:bae2 0002:0000029d +85:bae4 0002:0000029e +85:bae7 0002:0000029f +85:bae9 0002:000002a0 +85:8246 0002:000002a5 +85:8249 0002:000002a6 +85:824b 0002:000002a7 +85:82f9 0002:000002ab +b8:885c 0003:00000045 +b8:885d 0003:00000045 +b8:885e 0003:00000046 +b8:885f 0003:00000047 +b8:8863 0003:00000048 +b8:8866 0003:00000049 +b8:8867 0003:0000004a +b8:886b 0003:0000004b +b8:886e 0003:0000004c +b8:8870 0003:0000004e +b8:8873 0003:0000004f +b8:8874 0003:0000004f +b8:8878 0003:00000051 +b8:8879 0003:00000052 +b8:887a 0003:00000053 +b8:887e 0003:00000054 +b8:8880 0003:00000056 +b8:8881 0003:00000056 +b8:8882 0003:00000057 +b8:8883 0003:0000005a +b8:8884 0003:0000005a +b8:8885 0003:0000005b +b8:8886 0003:0000005c +b8:888a 0003:0000005d +84:f8a6 0004:00000051 +84:f8a9 0004:00000052 +84:f8ac 0004:00000053 +84:f8ad 0004:00000056 +84:f8b0 0004:00000057 +84:f8b3 0004:00000058 +84:f8b4 0004:0000005b +84:f8b7 0004:0000005c +84:f8ba 0004:0000005d +84:f9e0 0004:000000d4 +84:f9e4 0004:000000d5 +84:f9e5 0004:000000d8 +84:f9e8 0004:000000d9 +84:f9ea 0004:000000da +84:f9ec 0004:000000dd +84:f9ef 0004:000000de +84:f9f1 0004:000000e5 +84:f9f2 0004:000000e6 +84:f9f5 0004:000000e7 +84:f9f8 0004:000000e7 +84:f9f9 0004:000000e8 +84:f9fd 0004:000000e9 +84:fa00 0004:000000ea +84:fa02 0004:000000ec +84:fa05 0004:000000ed +84:fa06 0004:000000ee +84:fa0a 0004:000000f1 +84:fa0d 0004:000000f2 +84:fa0f 0004:000000f4 +84:fa11 0004:000000f5 +84:fa12 0004:000000f6 +84:fa14 0004:000000f7 +84:fa15 0004:000000f8 +84:fa19 0004:000000f9 +84:fa1a 0004:000000fa +84:fa1b 0004:000000fb +84:fa1c 0004:000000fe +84:fa1d 0004:000000ff +84:fa1e 0004:00000103 +84:fa1f 0004:00000103 +84:fa20 0004:00000103 +84:fa21 0004:00000104 +84:fa24 0004:00000105 +84:fa27 0004:00000106 +84:fa28 0004:00000107 +84:fa2c 0004:00000108 +84:fa2f 0004:00000109 +84:fa31 0004:0000010b +84:fa34 0004:0000010c +84:fa35 0004:0000010c +84:fa39 0004:0000010e +84:fa3a 0004:00000110 +84:fa3b 0004:00000111 +84:fa3c 0004:00000112 +84:fa40 0004:00000113 +84:fa42 0004:00000114 +84:fa43 0004:00000115 +84:fa44 0004:00000116 +84:fa47 0004:00000117 +84:fa48 0004:00000118 +84:fa49 0004:0000011b +84:fa4a 0004:0000011c +84:fa4c 0004:0000011d +84:fa4d 0004:0000011e +84:fa51 0004:0000011f +84:fa52 0004:00000120 +84:fa53 0004:00000123 +84:fa57 0004:00000124 +84:fa5a 0004:00000127 +84:fa5e 0004:00000128 +84:fa61 0004:0000012c +84:fa64 0004:0000012c +84:fa66 0004:0000012d +84:fa69 0004:0000012e +84:fa6b 0004:0000012f +84:fa6e 0004:0000012f +84:fa70 0004:00000130 +84:fa73 0004:00000131 +84:fa75 0004:00000132 +84:fa78 0004:00000135 +84:fa79 0004:00000139 +84:fa7d 0004:0000013a +84:fa7e 0004:0000013f +84:fa7f 0004:00000140 +84:fa80 0004:00000141 +84:fa81 0004:00000141 +84:fa82 0004:00000145 +84:fa86 0004:00000146 +84:fa87 0004:00000147 +84:fa88 0004:00000148 +84:fa89 0004:00000148 +84:fa8a 0004:00000149 +84:fa8d 0004:0000014a +84:fa8e 0004:0000014b +84:fa8f 0004:0000014c +81:b303 0005:00000003 +81:b307 0005:00000004 +81:b308 0005:00000005 +b8:c81c 0005:00000016 +b8:c81f 0005:00000018 +b8:c823 0005:00000019 +b8:c827 0005:0000001a +b8:c828 0005:0000001b +b8:c829 0005:0000001c +b8:c82c 0005:0000001d +b8:c82e 0005:0000001e +b8:c831 0005:00000020 +b8:c835 0005:00000021 +b8:c839 0005:00000022 +b8:c83a 0005:00000023 +b8:c83b 0005:00000024 +b8:c83e 0005:00000025 +b8:c840 0005:00000026 +b8:c843 0005:00000028 +b8:c847 0005:00000029 +b8:c84b 0005:0000002a +b8:c84c 0005:0000002b +b8:c84d 0005:0000002c +b8:c850 0005:0000002d +b8:c852 0005:0000002e +b8:c855 0005:00000031 +b8:c856 0005:00000034 +b8:c859 0005:00000035 +b8:c85b 0005:00000036 +b8:c85e 0005:00000037 +b8:c862 0005:00000039 +b8:c866 0005:0000003a +b8:c869 0005:0000003b +b8:c86b 0005:0000003c +b8:c86f 0005:0000003e +b8:c873 0005:0000003f +b8:c876 0005:00000040 +b8:c878 0005:00000041 +b8:c87c 0005:00000043 +b8:c880 0005:00000044 diff --git a/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json b/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json new file mode 100644 index 0000000000..63198cde72 --- /dev/null +++ b/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json @@ -0,0 +1,141 @@ +{ + "CLIPCHECK": "85:FF00", + "CLIPLEN": "85:9900", + "CLIPLEN_end": "85:990F", + "CLIPLEN_no_multi": "85:990C", + "CLIPSET": "85:FF1D", + "COLLECTTANK": "B8:80EF", + "MISCFX": "85:FF45", + "NORMAL": "84:8BF2", + "SETFX": "85:FF4E", + "SOUNDFX": "85:FF30", + "SOUNDFX_84": "84:F9E0", + "SPECIALFX": "85:FF3C", + "ammo_loop_table": "84:F896", + "archipelago_chozo_item_plm": "84:F874", + "archipelago_hidden_item_plm": "84:F878", + "archipelago_visible_item_plm": "84:F870", + "c_item": "84:F892", + "config_deathlink": "CE:FF04", + "config_flags": "CE:FF00", + "config_multiworld": "CE:FF00", + "config_player_id": "CE:FF08", + "config_remote_items": "CE:FF06", + "config_sprite": "CE:FF02", + "h_item": "84:F894", + "i_chozo_item": "84:F8AD", + "i_hidden_item": "84:F8B4", + "i_hidden_item_setup": "84:FA5A", + "i_item_setup_shared": "B8:885C", + "i_item_setup_shared_all_items": "B8:8878", + "i_item_setup_shared_alwaysloaded": "B8:8883", + "i_live_pickup": "84:FA79", + "i_live_pickup_multiworld": "B8:817F", + "i_live_pickup_multiworld_end": "B8:81C4", + "i_live_pickup_multiworld_local_item_or_offworld": "B8:819B", + "i_live_pickup_multiworld_own_item": "B8:81B0", + "i_live_pickup_multiworld_own_item1": "B8:81BC", + "i_load_custom_graphics": "84:FA1E", + "i_load_custom_graphics_all_items": "84:FA39", + "i_load_custom_graphics_alwaysloaded": "84:FA49", + "i_load_rando_item": "84:FA61", + "i_load_rando_item_end": "84:FA78", + "i_start_draw_loop": "84:F9F1", + "i_start_draw_loop_all_items": "84:FA0A", + "i_start_draw_loop_hidden": "84:F9EC", + "i_start_draw_loop_non_ammo_item": "84:FA1C", + "i_start_draw_loop_visible_or_chozo": "84:F9E5", + "i_visible_item": "84:F8A6", + "i_visible_item_setup": "84:FA53", + "message_PlaceholderBig": "85:BA8A", + "message_char_table": "85:BA0A", + "message_hook_tilemap_calc": "85:BABC", + "message_hook_tilemap_calc_msgbox_mwrecv": "85:BADC", + "message_hook_tilemap_calc_msgbox_mwsend": "85:BACE", + "message_hook_tilemap_calc_normal": "85:824C", + "message_hook_tilemap_calc_vanilla": "85:BAC9", + "message_item_names": "85:9963", + "message_item_received": "85:B8A3", + "message_item_received_end": "85:B9A3", + "message_item_sent": "85:B7A3", + "message_item_sent_end": "85:B8A3", + "message_multiworld_init_new_messagebox_if_needed": "85:BA95", + "message_multiworld_init_new_messagebox_if_needed_msgbox_mwrecv": "85:BAB1", + "message_multiworld_init_new_messagebox_if_needed_msgbox_mwsend": "85:BAB1", + "message_multiworld_init_new_messagebox_if_needed_vanilla": "85:BAA9", + "message_write_placeholders": "85:B9A3", + "message_write_placeholders_adjust": "85:B9A5", + "message_write_placeholders_end": "85:BA04", + "message_write_placeholders_loop": "85:B9CA", + "message_write_placeholders_notfound": "85:B9DC", + "message_write_placeholders_value_ok": "85:B9DF", + "mw_display_item_sent": "B8:8092", + "mw_handle_queue": "B8:80FF", + "mw_handle_queue_end": "B8:8178", + "mw_handle_queue_loop": "B8:8101", + "mw_handle_queue_new_remote_item": "B8:8151", + "mw_handle_queue_next": "B8:816D", + "mw_handle_queue_perform_receive": "B8:8163", + "mw_hook_main_game": "B8:81C8", + "mw_init": "B8:8011", + "mw_init_end": "B8:8044", + "mw_init_memory": "B8:8000", + "mw_load_sram": "B8:8083", + "mw_receive_item": "B8:80B0", + "mw_receive_item_end": "B8:80E8", + "mw_save_sram": "B8:8070", + "mw_write_message": "B8:8049", + "nonprog_item_eight_palette_indices": "84:F888", + "offworld_graphics_data_item": "89:9200", + "offworld_graphics_data_progression_item": "89:9100", + "p_chozo_item": "84:F972", + "p_chozo_item_end": "84:F9A0", + "p_chozo_item_loop": "84:F98D", + "p_chozo_item_trigger": "84:F999", + "p_etank_hloop": "84:F8FB", + "p_etank_loop": "84:F8BB", + "p_hidden_item": "84:F9A6", + "p_hidden_item_end": "84:F9D8", + "p_hidden_item_loop": "84:F9BD", + "p_hidden_item_loop2": "84:F9A8", + "p_hidden_item_trigger": "84:F9D1", + "p_missile_hloop": "84:F90F", + "p_missile_loop": "84:F8CB", + "p_pb_hloop": "84:F937", + "p_pb_loop": "84:F8EB", + "p_super_hloop": "84:F923", + "p_super_loop": "84:F8DB", + "p_visible_item": "84:F94B", + "p_visible_item_end": "84:F96E", + "p_visible_item_loop": "84:F95B", + "p_visible_item_trigger": "84:F967", + "patch_load_multiworld": "B8:81DF", + "perform_item_pickup": "84:FA7E", + "plm_graphics_entry_offworld_item": "84:F886", + "plm_graphics_entry_offworld_progression_item": "84:F87C", + "plm_sequence_generic_item_0_bitmask": "84:FA90", + "prog_item_eight_palette_indices": "84:F87E", + "rando_item_table": "B8:E000", + "rando_player_id_table": "B8:DC90", + "rando_player_id_table_end": "B8:DE22", + "rando_player_table": "B8:D000", + "rando_seed_data": "B8:CF00", + "sm_item_graphics": "B8:8800", + "sm_item_plm_pickup_sequence_pointers": "B8:882E", + "start_item": "B8:C81C", + "start_item_data_major": "B8:C800", + "start_item_data_minor": "B8:C808", + "start_item_data_reserve": "B8:C818", + "update_graphic": "B8:C856", + "v_item": "84:F890", + "ITEM_RAM": "7E:09A2", + "SRAM_MW_ITEMS_RECV": "70:2000", + "SRAM_MW_ITEMS_RECV_RPTR": "70:2600", + "SRAM_MW_ITEMS_RECV_WPTR": "70:2602", + "SRAM_MW_ITEMS_RECV_SPTR": "70:2604", + "SRAM_MW_ITEMS_SENT_RPTR": "70:2680", + "SRAM_MW_ITEMS_SENT_WPTR": "70:2682", + "SRAM_MW_ITEMS_SENT": "70:2700", + "SRAM_MW_INITIALIZED": "70:26fe", + "CollectedItems": "7E:D86E" +} \ No newline at end of file diff --git a/worlds/sm/variaRandomizer/patches/common/ips/basepatch.ips b/worlds/sm/data/SMBasepatch_prebuilt/variapatches.ips similarity index 54% rename from worlds/sm/variaRandomizer/patches/common/ips/basepatch.ips rename to worlds/sm/data/SMBasepatch_prebuilt/variapatches.ips index b0b34963e8..c1285e1653 100644 Binary files a/worlds/sm/variaRandomizer/patches/common/ips/basepatch.ips and b/worlds/sm/data/SMBasepatch_prebuilt/variapatches.ips differ diff --git a/worlds/sm/data/sourceinfo.txt b/worlds/sm/data/sourceinfo.txt new file mode 100644 index 0000000000..8facb6b249 --- /dev/null +++ b/worlds/sm/data/sourceinfo.txt @@ -0,0 +1,3 @@ +SMBasepatch_prebuilt: +- comes exactly from build/vanilla/ directory of https://github.com/lordlou/SMBasepatch +- keep it in sync with the basepatch repo; do not modify the contents in this repo alone! diff --git a/worlds/sm/docs/en_Super Metroid.md b/worlds/sm/docs/en_Super Metroid.md index 44a292f582..941cbf48cf 100644 --- a/worlds/sm/docs/en_Super Metroid.md +++ b/worlds/sm/docs/en_Super Metroid.md @@ -23,7 +23,8 @@ certain items to your own world. ## What does another world's item look like in Super Metroid? -A unique item sprite has been added to the game to represent items belonging to another world. +Two unique item sprites have been added to the game to represent items belonging to another world. Progression items have +a small up arrow on the sprite and non-progression don't. ## When the player receives an item, what happens? diff --git a/worlds/sm/variaRandomizer/graph/graph.py b/worlds/sm/variaRandomizer/graph/graph.py index bcbf138123..6ca7465a7e 100644 --- a/worlds/sm/variaRandomizer/graph/graph.py +++ b/worlds/sm/variaRandomizer/graph/graph.py @@ -367,6 +367,22 @@ class AccessGraph(object): #print("canAccess: {}".format(can)) return can + # test access from an access point to a list of others, given an optional item + def canAccessList(self, smbm, srcAccessPointName, destAccessPointNameList, maxDiff, item=None): + if item is not None: + smbm.addItem(item) + #print("canAccess: item: {}, src: {}, dest: {}".format(item, srcAccessPointName, destAccessPointName)) + destAccessPointList = [self.accessPoints[destAccessPointName] for destAccessPointName in destAccessPointNameList] + srcAccessPoint = self.accessPoints[srcAccessPointName] + availAccessPoints = self.getAvailableAccessPoints(srcAccessPoint, smbm, maxDiff, item) + can = any(ap in availAccessPoints for ap in destAccessPointList) + # if not can: + # self.log.debug("canAccess KO: avail = {}".format([ap.Name for ap in availAccessPoints.keys()])) + if item is not None: + smbm.removeItem(item) + #print("canAccess: {}".format(can)) + return can + # returns a list of AccessPoint instances from srcAccessPointName to destAccessPointName # (not including source ap) # or None if no possible path diff --git a/worlds/sm/variaRandomizer/patches/common/ips/beam_doors_gfx.ips b/worlds/sm/variaRandomizer/patches/common/ips/beam_doors_gfx.ips index 00239095e2..509bbbf8e5 100644 Binary files a/worlds/sm/variaRandomizer/patches/common/ips/beam_doors_gfx.ips and b/worlds/sm/variaRandomizer/patches/common/ips/beam_doors_gfx.ips differ diff --git a/worlds/sm/variaRandomizer/randomizer.py b/worlds/sm/variaRandomizer/randomizer.py index 0da2b2d042..3f6a9cc5fa 100644 --- a/worlds/sm/variaRandomizer/randomizer.py +++ b/worlds/sm/variaRandomizer/randomizer.py @@ -341,6 +341,8 @@ class VariaRandomizer: if preset == 'custom': PresetLoader.factory(world.custom_preset[player].value).load(self.player) elif preset == 'varia_custom': + if len(world.varia_custom_preset[player].value) == 0: + raise Exception("varia_custom was chosen but varia_custom_preset is missing.") url = 'https://randommetroidsolver.pythonanywhere.com/presetWebService' preset_name = next(iter(world.varia_custom_preset[player].value)) payload = '{{"preset": "{}"}}'.format(preset_name) @@ -695,7 +697,7 @@ class VariaRandomizer: #if args.patchOnly == False: # randoExec.postProcessItemLocs(itemLocs, args.hideItems) - def PatchRom(self, outputFilename, customPatchApply = None): + def PatchRom(self, outputFilename, customPrePatchApply = None, customPostPatchApply = None): args = self.args optErrMsgs = self.optErrMsgs @@ -747,6 +749,9 @@ class VariaRandomizer: else: romPatcher = RomPatcher(magic=args.raceMagic) + if customPrePatchApply != None: + customPrePatchApply(romPatcher) + if args.hud == True or args.majorsSplit == "FullWithHUD": args.patches.append("varia_hud.ips") if args.patchOnly == False: @@ -765,8 +770,8 @@ class VariaRandomizer: # don't color randomize custom ships args.shift_ship_palette = False - if customPatchApply != None: - customPatchApply(romPatcher) + if customPostPatchApply != None: + customPostPatchApply(romPatcher) # we have to write ips to ROM before doing our direct modifications which will rewrite some parts (like in credits), # but in web mode we only want to generate a global ips at the end diff --git a/worlds/sm/variaRandomizer/rom/rompatcher.py b/worlds/sm/variaRandomizer/rom/rompatcher.py index 22b83ceb5d..9dae1cea87 100644 --- a/worlds/sm/variaRandomizer/rom/rompatcher.py +++ b/worlds/sm/variaRandomizer/rom/rompatcher.py @@ -24,8 +24,6 @@ class RomPatcher: 'Removes_Gravity_Suit_heat_protection', # door ASM to skip G4 cutscene when all 4 bosses are dead 'g4_skip.ips', - # basepatch is generated from https://github.com/lordlou/SMBasepatch - 'basepatch.ips' ], # VARIA tweaks 'VariaTweaks' : ['WS_Etank', 'LN_Chozo_SpaceJump_Check_Disable', 'ln_chozo_platform.ips', 'bomb_torizo.ips'], diff --git a/worlds/sm64ex/Locations.py b/worlds/sm64ex/Locations.py index 5aee0189bf..f33c4306a1 100644 --- a/worlds/sm64ex/Locations.py +++ b/worlds/sm64ex/Locations.py @@ -182,26 +182,50 @@ loc100Coin_table = { "RR: 100 Coins": 3626104 } +locPSS_table = { + "The Princess's Secret Slide Block": 3626126, + "The Princess's Secret Slide Fast": 3626127, +} + +locSA_table = { + "The Secret Aquarium": 3626161 +} + locBitDW_table = { "Bowser in the Dark World Red Coins": 3626105, "Bowser in the Dark World Key": 3626178 } +locTotWC_table = { + "Tower of the Wing Cap Switch": 3626181, + "Tower of the Wing Cap Red Coins": 3626140 +} + +locCotMC_table = { + "Cavern of the Metal Cap Switch": 3626182, + "Cavern of the Metal Cap Red Coins": 3626133 +} + +locVCutM_table = { + "Vanish Cap Under the Moat Switch": 3626183, + "Vanish Cap Under the Moat Red Coins": 3626147 +} + locBitFS_table = { "Bowser in the Fire Sea Red Coins": 3626112, "Bowser in the Fire Sea Key": 3626179 } -#Secret Stars and Stages +locWMotR_table = { + "Wing Mario Over the Rainbow": 3626154 +} + +locBitS_table = { + "Bowser in the Sky Red Coins": 3626119 +} + +#Secret Stars found inside the Castle locSS_table = { - "Bowser in the Sky Red Coins": 3626119, - "The Princess's Secret Slide Block": 3626126, - "The Princess's Secret Slide Fast": 3626127, - "Cavern of the Metal Cap Red Coins": 3626133, - "Tower of the Wing Cap Red Coins": 3626140, - "Vanish Cap Under the Moat Red Coins": 3626147, - "Wing Mario Over the Rainbow": 3626154, - "The Secret Aquarium": 3626161, "Toad (Basement)": 3626168, "Toad (Second Floor)": 3626169, "Toad (Third Floor)": 3626170, @@ -209,15 +233,10 @@ locSS_table = { "MIPS 2": 3626172 } -#Caps -locCap_table = { - "Tower of the Wing Cap Switch": 3626181, - "Cavern of the Metal Cap Switch": 3626182, - "Vanish Cap Under the Moat Switch": 3626183 -} - # Correspond to 3626000 + course index * 7 + star index, then secret stars, then keys, then 100 Coin Stars location_table = {**locBoB_table,**locWhomp_table,**locJRB_table,**locCCM_table,**locBBH_table, \ **locHMC_table,**locLLL_table,**locSSL_table,**locDDD_table,**locSL_table, \ **locWDW_table,**locTTM_table,**locTHI_table,**locTTC_table,**locRR_table, \ - **loc100Coin_table,**locBitDW_table,**locBitFS_table,**locSS_table,**locCap_table} \ No newline at end of file + **loc100Coin_table,**locPSS_table,**locSA_table,**locBitDW_table,**locTotWC_table, \ + **locCotMC_table, **locVCutM_table, **locBitFS_table, **locWMotR_table, **locBitS_table, \ + **locSS_table} \ No newline at end of file diff --git a/worlds/sm64ex/Options.py b/worlds/sm64ex/Options.py index 99b3b3ee0b..594b0561c0 100644 --- a/worlds/sm64ex/Options.py +++ b/worlds/sm64ex/Options.py @@ -1,5 +1,5 @@ import typing -from Options import Option, DefaultOnToggle, Range, Toggle, DeathLink +from Options import Option, DefaultOnToggle, Range, Toggle, DeathLink, Choice class EnableCoinStars(DefaultOnToggle): """Disable to Ignore 100 Coin Stars. You can still collect them, but they don't do anything""" @@ -13,21 +13,56 @@ class StrictCannonRequirements(DefaultOnToggle): """If disabled, Stars that expect cannons may have to be acquired without them. Only makes a difference if Buddy Checks are enabled""" display_name = "Strict Cannon Requirements" +class FirstBowserStarDoorCost(Range): + """How many stars are required at the Star Door to Bowser in the Dark World""" + range_start = 0 + range_end = 50 + default = 8 + +class BasementStarDoorCost(Range): + """How many stars are required at the Star Door in the Basement""" + range_start = 0 + range_end = 70 + default = 30 + +class SecondFloorStarDoorCost(Range): + """How many stars are required to access the third floor""" + range_start = 0 + range_end = 90 + default = 50 + +class MIPS1Cost(Range): + """How many stars are required to spawn MIPS the first time""" + range_start = 0 + range_end = 40 + default = 15 + +class MIPS2Cost(Range): + """How many stars are required to spawn MIPS the secound time. Must be bigger or equal MIPS1Cost""" + range_start = 0 + range_end = 80 + default = 50 + class StarsToFinish(Range): """How many stars are required at the infinite stairs""" - range_start = 50 + display_name = "Endless Stairs Stars" + range_start = 0 range_end = 100 default = 70 -class ExtraStars(Range): - """How many stars exist beyond those set for StarsToFinish""" - range_start = 0 - range_end = 50 - default = 50 +class AmountOfStars(Range): + """How many stars exist. Disabling 100 Coin Stars removes 15 from the Pool. At least max of any Cost set""" + range_start = 35 + range_end = 120 + default = 120 -class AreaRandomizer(Toggle): - """Randomize Entrances to Courses""" - display_name = "Course Randomizer" +class AreaRandomizer(Choice): + """Randomize Entrances""" + display_name = "Entrance Randomizer" + alias_false = 0 + option_Off = 0 + option_Courses_Only = 1 + option_Courses_and_Secrets = 2 class BuddyChecks(Toggle): """Bob-omb Buddies are checks, Cannon Unlocks are items""" @@ -41,10 +76,15 @@ sm64_options: typing.Dict[str,type(Option)] = { "AreaRandomizer": AreaRandomizer, "ProgressiveKeys": ProgressiveKeys, "EnableCoinStars": EnableCoinStars, + "AmountOfStars": AmountOfStars, "StrictCapRequirements": StrictCapRequirements, "StrictCannonRequirements": StrictCannonRequirements, + "FirstBowserStarDoorCost": FirstBowserStarDoorCost, + "BasementStarDoorCost": BasementStarDoorCost, + "SecondFloorStarDoorCost": SecondFloorStarDoorCost, + "MIPS1Cost": MIPS1Cost, + "MIPS2Cost": MIPS2Cost, "StarsToFinish": StarsToFinish, - "ExtraStars": ExtraStars, "death_link": DeathLink, "BuddyChecks": BuddyChecks, } \ No newline at end of file diff --git a/worlds/sm64ex/Regions.py b/worlds/sm64ex/Regions.py index 85c08933f5..f8e856a9f6 100644 --- a/worlds/sm64ex/Regions.py +++ b/worlds/sm64ex/Regions.py @@ -4,148 +4,168 @@ from .Locations import SM64Location, location_table, locBoB_table, locWhomp_tabl locBBH_table, \ locHMC_table, locLLL_table, locSSL_table, locDDD_table, locSL_table, \ locWDW_table, locTTM_table, locTHI_table, locTTC_table, locRR_table, \ - locBitDW_table, locBitFS_table, locSS_table, locCap_table + locPSS_table, locSA_table, locBitDW_table, locTotWC_table, locCotMC_table, \ + locVCutM_table, locBitFS_table, locWMotR_table, locBitS_table, locSS_table +# List of all courses, including secrets, without BitS as that one is static sm64courses = ["Bob-omb Battlefield", "Whomp's Fortress", "Jolly Roger Bay", "Cool, Cool Mountain", "Big Boo's Haunt", "Hazy Maze Cave", "Lethal Lava Land", "Shifting Sand Land", "Dire, Dire Docks", "Snowman's Land", - "Wet-Dry World", - "Tall, Tall Mountain", "Tiny-Huge Island", "Tick Tock Clock", "Rainbow Ride"] + "Wet-Dry World", "Tall, Tall Mountain", "Tiny-Huge Island", "Tick Tock Clock", "Rainbow Ride", + "The Princess's Secret Slide", "The Secret Aquarium", "Bowser in the Dark World", "Tower of the Wing Cap", + "Cavern of the Metal Cap", "Vanish Cap under the Moat", "Bowser in the Fire Sea", "Wing Mario over the Rainbow"] +# sm64paintings is list of entrances, format LEVEL | AREA. String Reference below +sm64paintings = [91,241,121,51,41,71,221,81,231,101,111,361,132,131,141,151] +sm64paintings_s = ["BOB", "WF", "JRB", "CCM", "BBH", "HMC", "LLL", "SSL", "DDD", "SL", "WDW", "TTM", "THI Tiny", "THI Huge", "TTC", "RR"] +# sm64secrets is list of secret areas +sm64secrets = [271, 201, 171, 291, 281, 181, 191, 311] +sm64secrets_s = ["PSS", "SA", "BitDW", "TOTWC", "COTMC", "VCUTM", "BitFS", "WMOTR"] + +sm64entrances = sm64paintings + sm64secrets +sm64entrances_s = sm64paintings_s + sm64secrets_s +sm64_internalloc_to_string = dict(zip(sm64paintings+sm64secrets, sm64entrances_s)) +sm64_internalloc_to_regionid = dict(zip(sm64paintings+sm64secrets, list(range(13)) + [12,13,14] + list(range(15,15+len(sm64secrets))))) def create_regions(world: MultiWorld, player: int): regSS = Region("Menu", RegionType.Generic, "Castle Area", player, world) - locSS_names = [name for name, id in locSS_table.items()] - locSS_names += [name for name, id in locCap_table.items()] - regSS.locations += [SM64Location(player, loc_name, location_table[loc_name], regSS) for loc_name in locSS_names] + create_default_locs(regSS, locSS_table, player) world.regions.append(regSS) - regBoB = Region("Bob-omb Battlefield", RegionType.Generic, "Bob-omb Battlefield", player, world) - locBoB_names = [name for name, id in locBoB_table.items()] - regBoB.locations += [SM64Location(player, loc_name, location_table[loc_name], regBoB) for loc_name in locBoB_names] + regBoB = create_region("Bob-omb Battlefield", player, world) + create_default_locs(regBoB, locBoB_table, player) if (world.EnableCoinStars[player].value): regBoB.locations.append(SM64Location(player, "BoB: 100 Coins", location_table["BoB: 100 Coins"], regBoB)) world.regions.append(regBoB) - regWhomp = Region("Whomp's Fortress", RegionType.Generic, "Whomp's Fortress", player, world) - locWhomp_names = [name for name, id in locWhomp_table.items()] - regWhomp.locations += [SM64Location(player, loc_name, location_table[loc_name], regWhomp) for loc_name in - locWhomp_names] + regWhomp = create_region("Whomp's Fortress", player, world) + create_default_locs(regWhomp, locWhomp_table, player) if (world.EnableCoinStars[player].value): regWhomp.locations.append(SM64Location(player, "WF: 100 Coins", location_table["WF: 100 Coins"], regWhomp)) world.regions.append(regWhomp) - regJRB = Region("Jolly Roger Bay", RegionType.Generic, "Jolly Roger Bay", player, world) - locJRB_names = [name for name, id in locJRB_table.items()] - regJRB.locations += [SM64Location(player, loc_name, location_table[loc_name], regJRB) for loc_name in locJRB_names] + regJRB = create_region("Jolly Roger Bay", player, world) + create_default_locs(regJRB, locJRB_table, player) if (world.EnableCoinStars[player].value): regJRB.locations.append(SM64Location(player, "JRB: 100 Coins", location_table["JRB: 100 Coins"], regJRB)) world.regions.append(regJRB) - regCCM = Region("Cool, Cool Mountain", RegionType.Generic, "Cool, Cool Mountain", player, world) - locCCM_names = [name for name, id in locCCM_table.items()] - regCCM.locations += [SM64Location(player, loc_name, location_table[loc_name], regCCM) for loc_name in locCCM_names] + regCCM = create_region("Cool, Cool Mountain", player, world) + create_default_locs(regCCM, locCCM_table, player) if (world.EnableCoinStars[player].value): regCCM.locations.append(SM64Location(player, "CCM: 100 Coins", location_table["CCM: 100 Coins"], regCCM)) world.regions.append(regCCM) - regBBH = Region("Big Boo's Haunt", RegionType.Generic, "Big Boo's Haunt", player, world) - locBBH_names = [name for name, id in locBBH_table.items()] - regBBH.locations += [SM64Location(player, loc_name, location_table[loc_name], regBBH) for loc_name in locBBH_names] + regBBH = create_region("Big Boo's Haunt", player, world) + create_default_locs(regBBH, locBBH_table, player) if (world.EnableCoinStars[player].value): regBBH.locations.append(SM64Location(player, "BBH: 100 Coins", location_table["BBH: 100 Coins"], regBBH)) world.regions.append(regBBH) - regBitDW = Region("Bowser in the Dark World", RegionType.Generic, "Bowser in the Dark World", player, world) - locBitDW_names = [name for name, id in locBitDW_table.items()] - regBitDW.locations += [SM64Location(player, loc_name, location_table[loc_name], regBitDW) for loc_name in - locBitDW_names] + regPSS = create_region("The Princess's Secret Slide", player, world) + create_default_locs(regPSS, locPSS_table, player) + world.regions.append(regPSS) + + regSA = create_region("The Secret Aquarium", player, world) + create_default_locs(regSA, locSA_table, player) + world.regions.append(regSA) + + regTotWC = create_region("Tower of the Wing Cap", player, world) + create_default_locs(regTotWC, locTotWC_table, player) + world.regions.append(regTotWC) + + regBitDW = create_region("Bowser in the Dark World", player, world) + create_default_locs(regBitDW, locBitDW_table, player) world.regions.append(regBitDW) - regBasement = Region("Basement", RegionType.Generic, "Basement", player, world) + regBasement = create_region("Basement", player, world) world.regions.append(regBasement) - regHMC = Region("Hazy Maze Cave", RegionType.Generic, "Hazy Maze Cave", player, world) - locHMC_names = [name for name, id in locHMC_table.items()] - regHMC.locations += [SM64Location(player, loc_name, location_table[loc_name], regHMC) for loc_name in locHMC_names] + regHMC = create_region("Hazy Maze Cave", player, world) + create_default_locs(regHMC, locHMC_table, player) if (world.EnableCoinStars[player].value): regHMC.locations.append(SM64Location(player, "HMC: 100 Coins", location_table["HMC: 100 Coins"], regHMC)) world.regions.append(regHMC) - regLLL = Region("Lethal Lava Land", RegionType.Generic, "Lethal Lava Land", player, world) - locLLL_names = [name for name, id in locLLL_table.items()] - regLLL.locations += [SM64Location(player, loc_name, location_table[loc_name], regLLL) for loc_name in locLLL_names] + regLLL = create_region("Lethal Lava Land", player, world) + create_default_locs(regLLL, locLLL_table, player) if (world.EnableCoinStars[player].value): regLLL.locations.append(SM64Location(player, "LLL: 100 Coins", location_table["LLL: 100 Coins"], regLLL)) world.regions.append(regLLL) - regSSL = Region("Shifting Sand Land", RegionType.Generic, "Shifting Sand Land", player, world) - locSSL_names = [name for name, id in locSSL_table.items()] - regSSL.locations += [SM64Location(player, loc_name, location_table[loc_name], regSSL) for loc_name in locSSL_names] + regSSL = create_region("Shifting Sand Land", player, world) + create_default_locs(regSSL, locSSL_table, player) if (world.EnableCoinStars[player].value): regSSL.locations.append(SM64Location(player, "SSL: 100 Coins", location_table["SSL: 100 Coins"], regSSL)) world.regions.append(regSSL) - regDDD = Region("Dire, Dire Docks", RegionType.Generic, "Dire, Dire Docks", player, world) - locDDD_names = [name for name, id in locDDD_table.items()] - regDDD.locations += [SM64Location(player, loc_name, location_table[loc_name], regDDD) for loc_name in locDDD_names] + regDDD = create_region("Dire, Dire Docks", player, world) + create_default_locs(regDDD, locDDD_table, player) if (world.EnableCoinStars[player].value): regDDD.locations.append(SM64Location(player, "DDD: 100 Coins", location_table["DDD: 100 Coins"], regDDD)) world.regions.append(regDDD) - regBitFS = Region("Bowser in the Fire Sea", RegionType.Generic, "Bowser in the Fire Sea", player, world) - locBitFS_names = [name for name, id in locBitFS_table.items()] - regBitFS.locations += [SM64Location(player, loc_name, location_table[loc_name], regBitFS) for loc_name in - locBitFS_names] + regCotMC = create_region("Cavern of the Metal Cap", player, world) + create_default_locs(regCotMC, locCotMC_table, player) + world.regions.append(regCotMC) + + regVCutM = create_region("Vanish Cap under the Moat", player, world) + create_default_locs(regVCutM, locVCutM_table, player) + world.regions.append(regVCutM) + + regBitFS = create_region("Bowser in the Fire Sea", player, world) + create_default_locs(regBitFS, locBitFS_table, player) world.regions.append(regBitFS) - regFloor2 = Region("Second Floor", RegionType.Generic, "Second Floor", player, world) + regFloor2 = create_region("Second Floor", player, world) world.regions.append(regFloor2) - regSL = Region("Snowman's Land", RegionType.Generic, "Snowman's Land", player, world) - locSL_names = [name for name, id in locSL_table.items()] - regSL.locations += [SM64Location(player, loc_name, location_table[loc_name], regSL) for loc_name in locSL_names] + regSL = create_region("Snowman's Land", player, world) + create_default_locs(regSL, locSL_table, player) if (world.EnableCoinStars[player].value): regSL.locations.append(SM64Location(player, "SL: 100 Coins", location_table["SL: 100 Coins"], regSL)) world.regions.append(regSL) - regWDW = Region("Wet-Dry World", RegionType.Generic, "Wet-Dry World", player, world) - locWDW_names = [name for name, id in locWDW_table.items()] - regWDW.locations += [SM64Location(player, loc_name, location_table[loc_name], regWDW) for loc_name in locWDW_names] + regWDW = create_region("Wet-Dry World", player, world) + create_default_locs(regWDW, locWDW_table, player) if (world.EnableCoinStars[player].value): regWDW.locations.append(SM64Location(player, "WDW: 100 Coins", location_table["WDW: 100 Coins"], regWDW)) world.regions.append(regWDW) - regTTM = Region("Tall, Tall Mountain", RegionType.Generic, "Tall, Tall Mountain", player, world) - locTTM_names = [name for name, id in locTTM_table.items()] - regTTM.locations += [SM64Location(player, loc_name, location_table[loc_name], regTTM) for loc_name in locTTM_names] + regTTM = create_region("Tall, Tall Mountain", player, world) + create_default_locs(regTTM, locTTM_table, player) if (world.EnableCoinStars[player].value): regTTM.locations.append(SM64Location(player, "TTM: 100 Coins", location_table["TTM: 100 Coins"], regTTM)) world.regions.append(regTTM) - regTHI = Region("Tiny-Huge Island", RegionType.Generic, "Tiny-Huge Island", player, world) - locTHI_names = [name for name, id in locTHI_table.items()] - regTHI.locations += [SM64Location(player, loc_name, location_table[loc_name], regTHI) for loc_name in locTHI_names] + regTHI = create_region("Tiny-Huge Island", player, world) + create_default_locs(regTHI, locTHI_table, player) if (world.EnableCoinStars[player].value): regTHI.locations.append(SM64Location(player, "THI: 100 Coins", location_table["THI: 100 Coins"], regTHI)) world.regions.append(regTHI) - regFloor3 = Region("Third Floor", RegionType.Generic, "Third Floor", player, world) + regFloor3 = create_region("Third Floor", player, world) world.regions.append(regFloor3) - regTTC = Region("Tick Tock Clock", RegionType.Generic, "Tick Tock Clock", player, world) - locTTC_names = [name for name, id in locTTC_table.items()] - regTTC.locations += [SM64Location(player, loc_name, location_table[loc_name], regTTC) for loc_name in locTTC_names] + regTTC = create_region("Tick Tock Clock", player, world) + create_default_locs(regTTC, locTTC_table, player) if (world.EnableCoinStars[player].value): regTTC.locations.append(SM64Location(player, "TTC: 100 Coins", location_table["TTC: 100 Coins"], regTTC)) world.regions.append(regTTC) - regRR = Region("Rainbow Ride", RegionType.Generic, "Rainbow Ride", player, world) - locRR_names = [name for name, id in locRR_table.items()] - regRR.locations += [SM64Location(player, loc_name, location_table[loc_name], regRR) for loc_name in locRR_names] + regRR = create_region("Rainbow Ride", player, world) + create_default_locs(regRR, locRR_table, player) if (world.EnableCoinStars[player].value): regRR.locations.append(SM64Location(player, "RR: 100 Coins", location_table["RR: 100 Coins"], regRR)) world.regions.append(regRR) + regWMotR = create_region("Wing Mario over the Rainbow", player, world) + create_default_locs(regWMotR, locWMotR_table, player) + world.regions.append(regWMotR) + + regBitS = create_region("Bowser in the Sky", player, world) + create_default_locs(regBitS, locBitS_table, player) + world.regions.append(regBitS) + def connect_regions(world: MultiWorld, player: int, source: str, target: str, rule=None): sourceRegion = world.get_region(source, player) @@ -157,3 +177,10 @@ def connect_regions(world: MultiWorld, player: int, source: str, target: str, ru sourceRegion.exits.append(connection) connection.connect(targetRegion) + +def create_region(name: str, player: int, world: MultiWorld) -> Region: + return Region(name, RegionType.Generic, name, player, world) + +def create_default_locs(reg: Region, locs, player): + reg_names = [name for name, id in locs.items()] + reg.locations += [SM64Location(player, loc_name, location_table[loc_name], reg) for loc_name in locs] diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index fcd5619323..a4a82b2737 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -1,46 +1,76 @@ from ..generic.Rules import add_rule -from .Regions import connect_regions, sm64courses +from .Regions import connect_regions, sm64courses, sm64paintings, sm64secrets, sm64entrances +def fix_reg(entrance_ids, reg, invalidspot, swaplist, world): + if entrance_ids.index(reg) == invalidspot: # Unlucky :C + swaplist.remove(invalidspot) + rand = world.random.choice(swaplist) + entrance_ids[invalidspot], entrance_ids[rand] = entrance_ids[rand], entrance_ids[invalidspot] + swaplist.append(invalidspot) + swaplist.remove(rand) def set_rules(world, player: int, area_connections): - courseshuffle = list(range(len(sm64courses))) - if world.AreaRandomizer[player]: - world.random.shuffle(courseshuffle) - area_connections.update({index: value for index, value in enumerate(courseshuffle)}) + destination_regions = list(range(13)) + [12,13,14] + list(range(15,15+len(sm64secrets))) # Two instances of Destination Course THI. Past normal course idx are secret regions + if world.AreaRandomizer[player].value == 0: + entrance_ids = list(range(len(sm64paintings + sm64secrets))) + if world.AreaRandomizer[player].value >= 1: # Some randomization is happening, randomize Courses + entrance_ids = list(range(len(sm64paintings))) + world.random.shuffle(entrance_ids) + entrance_ids = entrance_ids + list(range(len(sm64paintings), len(sm64paintings) + len(sm64secrets))) + if world.AreaRandomizer[player].value == 2: # Secret Regions as well + world.random.shuffle(entrance_ids) + # Guarantee first entrance is a course + swaplist = list(range(len(entrance_ids))) + if entrance_ids.index(0) > 15: # Unlucky :C + rand = world.random.randint(0,15) + entrance_ids[entrance_ids.index(0)], entrance_ids[rand] = entrance_ids[rand], entrance_ids[entrance_ids.index(0)] + swaplist.remove(entrance_ids.index(0)) + # Guarantee COTMC is not mapped to HMC, cuz thats impossible + fix_reg(entrance_ids, 20, 5, swaplist, world) + # Guarantee BITFS is not mapped to DDD + fix_reg(entrance_ids, 22, 8, swaplist, world) + temp_assign = dict(zip(entrance_ids,destination_regions)) # Used for Rules only - connect_regions(world, player, "Menu", sm64courses[area_connections[0]]) - connect_regions(world, player, "Menu", sm64courses[area_connections[1]], lambda state: state.has("Power Star", player, 1)) - connect_regions(world, player, "Menu", sm64courses[area_connections[2]], lambda state: state.has("Power Star", player, 3)) - connect_regions(world, player, "Menu", sm64courses[area_connections[3]], lambda state: state.has("Power Star", player, 3)) - connect_regions(world, player, "Menu", "Bowser in the Dark World", lambda state: state.has("Power Star", player, 8)) - connect_regions(world, player, "Menu", sm64courses[area_connections[4]], lambda state: state.has("Power Star", player, 12)) + # Destination Format: LVL | AREA with LVL = LEVEL_x, AREA = Area as used in sm64 code + area_connections.update({sm64entrances[entrance]: destination for entrance, destination in zip(entrance_ids,sm64entrances)}) + + connect_regions(world, player, "Menu", sm64courses[temp_assign[0]]) # BOB + connect_regions(world, player, "Menu", sm64courses[temp_assign[1]], lambda state: state.has("Power Star", player, 1)) # WF + connect_regions(world, player, "Menu", sm64courses[temp_assign[2]], lambda state: state.has("Power Star", player, 3)) # JRB + connect_regions(world, player, "Menu", sm64courses[temp_assign[3]], lambda state: state.has("Power Star", player, 3)) # CCM + connect_regions(world, player, "Menu", sm64courses[temp_assign[4]], lambda state: state.has("Power Star", player, 12)) # BBH + connect_regions(world, player, "Menu", sm64courses[temp_assign[16]], lambda state: state.has("Power Star", player, 1)) # PSS + connect_regions(world, player, "Menu", sm64courses[temp_assign[17]], lambda state: state.has("Power Star", player, 3)) # SA + connect_regions(world, player, "Menu", sm64courses[temp_assign[19]], lambda state: state.has("Power Star", player, 10)) # TOTWC + connect_regions(world, player, "Menu", sm64courses[temp_assign[18]], lambda state: state.has("Power Star", player, world.FirstBowserStarDoorCost[player].value)) # BITDW connect_regions(world, player, "Menu", "Basement", lambda state: state.has("Basement Key", player) or state.has("Progressive Key", player, 1)) - connect_regions(world, player, "Basement", sm64courses[area_connections[5]]) - connect_regions(world, player, "Basement", sm64courses[area_connections[6]]) - connect_regions(world, player, "Basement", sm64courses[area_connections[7]]) - connect_regions(world, player, "Basement", sm64courses[area_connections[8]], lambda state: state.has("Power Star", player, 30)) - connect_regions(world, player, "Basement", "Bowser in the Fire Sea", lambda state: state.has("Power Star", player, 30) and - state.can_reach("DDD: Board Bowser's Sub", 'Location', player)) + connect_regions(world, player, "Basement", sm64courses[temp_assign[5]]) # HMC + connect_regions(world, player, "Basement", sm64courses[temp_assign[6]]) # LLL + connect_regions(world, player, "Basement", sm64courses[temp_assign[7]]) # SSL + connect_regions(world, player, "Basement", sm64courses[temp_assign[8]], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value)) # DDD + connect_regions(world, player, "Hazy Maze Cave", sm64courses[temp_assign[20]]) # COTMC + connect_regions(world, player, "Basement", sm64courses[temp_assign[21]]) # VCUTM + connect_regions(world, player, "Basement", sm64courses[temp_assign[22]], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value) and + state.can_reach("DDD: Board Bowser's Sub", 'Location', player)) # BITFS connect_regions(world, player, "Menu", "Second Floor", lambda state: state.has("Second Floor Key", player) or state.has("Progressive Key", player, 2)) - connect_regions(world, player, "Second Floor", sm64courses[area_connections[9]]) - connect_regions(world, player, "Second Floor", sm64courses[area_connections[10]]) - connect_regions(world, player, "Second Floor", sm64courses[area_connections[11]]) - connect_regions(world, player, "Second Floor", sm64courses[area_connections[12]]) + connect_regions(world, player, "Second Floor", sm64courses[temp_assign[9]]) # SL + connect_regions(world, player, "Second Floor", sm64courses[temp_assign[10]]) # WDW + connect_regions(world, player, "Second Floor", sm64courses[temp_assign[11]]) # TTM + connect_regions(world, player, "Second Floor", sm64courses[temp_assign[12]]) # THI Tiny + connect_regions(world, player, "Second Floor", sm64courses[temp_assign[13]]) # THI Huge - connect_regions(world, player, "Second Floor", "Third Floor", lambda state: state.has("Power Star", player, 50)) + connect_regions(world, player, "Second Floor", "Third Floor", lambda state: state.has("Power Star", player, world.SecondFloorStarDoorCost[player].value)) - connect_regions(world, player, "Third Floor", sm64courses[area_connections[13]]) - connect_regions(world, player, "Third Floor", sm64courses[area_connections[14]]) + connect_regions(world, player, "Third Floor", sm64courses[temp_assign[14]]) # TTC + connect_regions(world, player, "Third Floor", sm64courses[temp_assign[15]]) # RR + connect_regions(world, player, "Third Floor", sm64courses[temp_assign[23]]) # WMOTR + connect_regions(world, player, "Third Floor", "Bowser in the Sky", lambda state: state.has("Power Star", player, world.StarsToFinish[player].value)) # BITS #Special Rules for some Locations - add_rule(world.get_location("Tower of the Wing Cap Switch", player), lambda state: state.has("Power Star", player, 10)) - add_rule(world.get_location("Cavern of the Metal Cap Switch", player), lambda state: state.can_reach("Hazy Maze Cave", 'Region', player)) - add_rule(world.get_location("Vanish Cap Under the Moat Switch", player), lambda state: state.can_reach("Basement", 'Region', player)) - add_rule(world.get_location("BoB: Mario Wings to the Sky", player), lambda state: state.has("Cannon Unlock BoB", player)) add_rule(world.get_location("BBH: Eye to Eye in the Secret Room", player), lambda state: state.has("Vanish Cap", player)) add_rule(world.get_location("DDD: Collect the Caps...", player), lambda state: state.has("Vanish Cap", player)) @@ -79,18 +109,14 @@ def set_rules(world, player: int, area_connections): add_rule(world.get_location("BoB: 100 Coins", player), lambda state: state.has("Cannon Unlock BoB", player) or state.has("Wing Cap", player)) #Rules for Secret Stars - add_rule(world.get_location("Bowser in the Sky Red Coins", player), lambda state: state.can_reach("Third Floor", 'Region',player) and state.has("Power Star", player, world.StarsToFinish[player].value)) - add_rule(world.get_location("The Princess's Secret Slide Block", player), lambda state: state.has("Power Star", player, 1)) - add_rule(world.get_location("The Princess's Secret Slide Fast", player), lambda state: state.has("Power Star", player, 1)) - add_rule(world.get_location("Cavern of the Metal Cap Red Coins", player), lambda state: state.can_reach("Cavern of the Metal Cap Switch", 'Location', player)) - add_rule(world.get_location("Tower of the Wing Cap Red Coins", player), lambda state: state.can_reach("Tower of the Wing Cap Switch", 'Location', player)) - add_rule(world.get_location("Vanish Cap Under the Moat Red Coins", player), lambda state: state.can_reach("Vanish Cap Under the Moat Switch", 'Location', player)) - add_rule(world.get_location("Wing Mario Over the Rainbow", player), lambda state: state.can_reach("Third Floor", 'Region', player) and state.has("Wing Cap", player)) - add_rule(world.get_location("The Secret Aquarium", player), lambda state: state.has("Power Star", player, 3)) + add_rule(world.get_location("Wing Mario Over the Rainbow", player), lambda state: state.has("Wing Cap", player)) add_rule(world.get_location("Toad (Basement)", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, 12)) add_rule(world.get_location("Toad (Second Floor)", player), lambda state: state.can_reach("Second Floor", 'Region', player) and state.has("Power Star", player, 25)) add_rule(world.get_location("Toad (Third Floor)", player), lambda state: state.can_reach("Third Floor", 'Region', player) and state.has("Power Star", player, 35)) - add_rule(world.get_location("MIPS 1", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, 15)) - add_rule(world.get_location("MIPS 2", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, 50)) - world.completion_condition[player] = lambda state: state.can_reach("Third Floor", 'Region', player) and state.has("Power Star", player, world.StarsToFinish[player].value) + if world.MIPS1Cost[player].value > world.MIPS2Cost[player].value: + world.MIPS2Cost[player].value = world.MIPS1Cost[player].value + add_rule(world.get_location("MIPS 1", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, world.MIPS1Cost[player].value)) + add_rule(world.get_location("MIPS 2", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, world.MIPS2Cost[player].value)) + + world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Sky", 'Region', player) diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index f19f8ee79e..e0f911fbd9 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -5,13 +5,10 @@ from .Items import item_table, cannon_item_table, SM64Item from .Locations import location_table, SM64Location from .Options import sm64_options from .Rules import set_rules -from .Regions import create_regions, sm64courses +from .Regions import create_regions, sm64courses, sm64entrances_s, sm64_internalloc_to_string, sm64_internalloc_to_regionid from BaseClasses import Item, Tutorial, ItemClassification from ..AutoWorld import World, WebWorld -client_version = 1 - - class SM64Web(WebWorld): tutorials = [Tutorial( "Multiworld Setup Guide", @@ -38,11 +35,11 @@ class SM64World(World): location_name_to_id = location_table data_version = 6 - forced_auto_forfeit = False + required_client_version = (0, 3, 0) area_connections: typing.Dict[int, int] - options = sm64_options + option_definitions = sm64_options def generate_early(self): self.topology_present = self.world.AreaRandomizer[self.player].value @@ -55,10 +52,10 @@ class SM64World(World): set_rules(self.world, self.player, self.area_connections) if self.topology_present: # Write area_connections to spoiler log - for painting_id, course_id in self.area_connections.items(): + for entrance, destination in self.area_connections.items(): self.world.spoiler.set_entrance( - sm64courses[painting_id] + " Painting", - sm64courses[course_id], + sm64_internalloc_to_string[entrance] + " Entrance", + sm64_internalloc_to_string[destination], 'entrance', self.player) def create_item(self, name: str) -> Item: @@ -75,9 +72,13 @@ class SM64World(World): def generate_basic(self): staritem = self.create_item("Power Star") - starcount = min(self.world.StarsToFinish[self.player].value + self.world.ExtraStars[self.player].value,120) + starcount = self.world.AmountOfStars[self.player].value if (not self.world.EnableCoinStars[self.player].value): - starcount = max(starcount - 15,self.world.StarsToFinish[self.player].value) + starcount = max(35,self.world.AmountOfStars[self.player].value-15) + starcount = max(starcount, self.world.FirstBowserStarDoorCost[self.player].value, + self.world.BasementStarDoorCost[self.player].value, self.world.SecondFloorStarDoorCost[self.player].value, + self.world.MIPS1Cost[self.player].value, self.world.MIPS2Cost[self.player].value, + self.world.StarsToFinish[self.player].value) self.world.itempool += [staritem for i in range(0,starcount)] mushroomitem = self.create_item("1Up Mushroom") self.world.itempool += [mushroomitem for i in range(starcount,120 - (15 if not self.world.EnableCoinStars[self.player].value else 0))] @@ -115,6 +116,11 @@ class SM64World(World): def fill_slot_data(self): return { "AreaRando": self.area_connections, + "FirstBowserDoorCost": self.world.FirstBowserStarDoorCost[self.player].value, + "BasementDoorCost": self.world.BasementStarDoorCost[self.player].value, + "SecondFloorDoorCost": self.world.SecondFloorStarDoorCost[self.player].value, + "MIPS1Cost": self.world.MIPS1Cost[self.player].value, + "MIPS2Cost": self.world.MIPS2Cost[self.player].value, "StarsToFinish": self.world.StarsToFinish[self.player].value, "DeathLink": self.world.death_link[self.player].value, } @@ -143,8 +149,9 @@ class SM64World(World): def modify_multidata(self, multidata): if self.topology_present: er_hint_data = {} - for painting_id, course_id in self.area_connections.items(): - region = self.world.get_region(sm64courses[course_id], self.player) + for entrance, destination in self.area_connections.items(): + regionid = sm64_internalloc_to_regionid[destination] + region = self.world.get_region(sm64courses[regionid], self.player) for location in region.locations: - er_hint_data[location.address] = sm64courses[painting_id] + er_hint_data[location.address] = sm64_internalloc_to_string[entrance] multidata['er_hint_data'][self.player] = er_hint_data diff --git a/worlds/smz3/Options.py b/worlds/smz3/Options.py index ad59bc6d42..2bbddf7ab3 100644 --- a/worlds/smz3/Options.py +++ b/worlds/smz3/Options.py @@ -1,5 +1,5 @@ import typing -from Options import Choice, Option +from Options import Choice, Option, Toggle, DefaultOnToggle, Range class SMLogic(Choice): """This option selects what kind of logic to use for item placement inside @@ -45,6 +45,22 @@ class MorphLocation(Choice): option_Original = 2 default = 0 + +class Goal(Choice): + """This option decides what goal is required to finish the randomizer. + Defeat Ganon and Mother Brain - Find the required crystals and boss tokens kill both bosses. + Fast Ganon and Defeat Mother Brain - The hole to ganon is open without having to defeat Agahnim in + Ganon's Tower and Ganon can be defeat as soon you have the required + crystals to make Ganon vulnerable. For keysanity, this mode also removes + the Crateria Boss Key requirement from Tourian to allow faster access. + All Dungeons and Defeat Mother Brain - Similar to "Defeat Ganon and Mother Brain", but also requires all dungeons + to be beaten including Castle Tower and Agahnim.""" + display_name = "Goal" + option_DefeatBoth = 0 + option_FastGanonDefeatMotherBrain = 1 + option_AllDungeonsDefeatMotherBrain = 2 + default = 0 + class KeyShuffle(Choice): """This option decides how dungeon items such as keys are shuffled. None - A Link to the Past dungeon items can only be placed inside the @@ -55,9 +71,75 @@ class KeyShuffle(Choice): option_Keysanity = 1 default = 0 +class OpenTower(Range): + """The amount of crystals required to be able to enter Ganon's Tower. + If this is set to Random, the amount can be found in-game on a sign next to Ganon's Tower.""" + display_name = "Open Tower" + range_start = 0 + range_end = 7 + default = 7 + +class GanonVulnerable(Range): + """The amount of crystals required to be able to harm Ganon. The amount can be found + in-game on a sign near the top of the Pyramid.""" + display_name = "Ganon Vulnerable" + range_start = 0 + range_end = 7 + default = 7 + +class OpenTourian(Range): + """The amount of boss tokens required to enter Tourian. The amount can be found in-game + on a sign above the door leading to the Tourian entrance.""" + display_name = "Open Tourian" + range_start = 0 + range_end = 4 + default = 4 + +class SpinJumpsAnimation(Toggle): + """Enable separate space/screw jump animations""" + display_name = "Spin Jumps Animation" + +class HeartBeepSpeed(Choice): + """Sets the speed of the heart beep sound in A Link to the Past.""" + display_name = "Heart Beep Speed" + option_Off = 0 + option_Quarter = 1 + option_Half = 2 + option_Normal = 3 + option_Double = 4 + alias_false = 0 + default = 3 + +class HeartColor(Choice): + """Changes the color of the hearts in the HUD for A Link to the Past.""" + display_name = "Heart Color" + option_Red = 0 + option_Green = 1 + option_Blue = 2 + option_Yellow = 3 + default = 0 + +class QuickSwap(Toggle): + """When enabled, lets you switch items in ALTTP with L/R""" + display_name = "Quick Swap" + +class EnergyBeep(DefaultOnToggle): + """Toggles the low health energy beep in Super Metroid.""" + display_name = "Energy Beep" + + smz3_options: typing.Dict[str, type(Option)] = { "sm_logic": SMLogic, "sword_location": SwordLocation, "morph_location": MorphLocation, - "key_shuffle": KeyShuffle + "goal": Goal, + "key_shuffle": KeyShuffle, + "open_tower": OpenTower, + "ganon_vulnerable": GanonVulnerable, + "open_tourian": OpenTourian, + "spin_jumps_animation": SpinJumpsAnimation, + "heart_beep_speed": HeartBeepSpeed, + "heart_color": HeartColor, + "quick_swap": QuickSwap, + "energy_beep": EnergyBeep } diff --git a/worlds/smz3/TotalSMZ3/Config.py b/worlds/smz3/TotalSMZ3/Config.py index bfcd541b98..23dde1c88e 100644 --- a/worlds/smz3/TotalSMZ3/Config.py +++ b/worlds/smz3/TotalSMZ3/Config.py @@ -26,16 +26,42 @@ class MorphLocation(Enum): class Goal(Enum): DefeatBoth = 0 + FastGanonDefeatMotherBrain = 1 + AllDungeonsDefeatMotherBrain = 2 class KeyShuffle(Enum): Null = 0 Keysanity = 1 -class GanonInvincible(Enum): - Never = 0 - BeforeCrystals = 1 - BeforeAllDungeons = 2 - Always = 3 +class OpenTower(Enum): + Random = -1 + NoCrystals = 0 + OneCrystal = 1 + TwoCrystals = 2 + ThreeCrystals = 3 + FourCrystals = 4 + FiveCrystals = 5 + SixCrystals = 6 + SevenCrystals = 7 + +class GanonVulnerable(Enum): + Random = -1 + NoCrystals = 0 + OneCrystal = 1 + TwoCrystals = 2 + ThreeCrystals = 3 + FourCrystals = 4 + FiveCrystals = 5 + SixCrystals = 6 + SevenCrystals = 7 + +class OpenTourian(Enum): + Random = -1 + NoBosses = 0 + OneBoss = 1 + TwoBosses = 2 + ThreeBosses = 3 + FourBosses = 4 class Config: GameMode: GameMode = GameMode.Multiworld @@ -45,63 +71,20 @@ class Config: MorphLocation: MorphLocation = MorphLocation.Randomized Goal: Goal = Goal.DefeatBoth KeyShuffle: KeyShuffle = KeyShuffle.Null - Keysanity: bool = KeyShuffle != KeyShuffle.Null Race: bool = False - GanonInvincible: GanonInvincible = GanonInvincible.BeforeCrystals - def __init__(self, options: Dict[str, str]): - self.GameMode = self.ParseOption(options, GameMode.Multiworld) - self.Z3Logic = self.ParseOption(options, Z3Logic.Normal) - self.SMLogic = self.ParseOption(options, SMLogic.Normal) - self.SwordLocation = self.ParseOption(options, SwordLocation.Randomized) - self.MorphLocation = self.ParseOption(options, MorphLocation.Randomized) - self.Goal = self.ParseOption(options, Goal.DefeatBoth) - self.GanonInvincible = self.ParseOption(options, GanonInvincible.BeforeCrystals) - self.KeyShuffle = self.ParseOption(options, KeyShuffle.Null) - self.Keysanity = self.KeyShuffle != KeyShuffle.Null - self.Race = self.ParseOptionWith(options, "Race", False) + OpenTower: OpenTower = OpenTower.SevenCrystals + GanonVulnerable: GanonVulnerable = GanonVulnerable.SevenCrystals + OpenTourian: OpenTourian = OpenTourian.FourBosses - def ParseOption(self, options:Dict[str, str], defaultValue:Enum): - enumKey = defaultValue.__class__.__name__.lower() - if (enumKey in options): - return defaultValue.__class__[options[enumKey]] - return defaultValue + @property + def SingleWorld(self) -> bool: + return self.GameMode == GameMode.Normal + + @property + def Multiworld(self) -> bool: + return self.GameMode == GameMode.Multiworld - def ParseOptionWith(self, options:Dict[str, str], option:str, defaultValue:bool): - if (option.lower() in options): - return options[option.lower()] - return defaultValue - - """ public static RandomizerOption GetRandomizerOption(string description, string defaultOption = "") where T : Enum { - var enumType = typeof(T); - var values = Enum.GetValues(enumType).Cast(); - - return new RandomizerOption { - Key = enumType.Name.ToLower(), - Description = description, - Type = RandomizerOptionType.Dropdown, - Default = string.IsNullOrEmpty(defaultOption) ? GetDefaultValue().ToLString() : defaultOption, - Values = values.ToDictionary(k => k.ToLString(), v => v.GetDescription()) - }; - } - - public static RandomizerOption GetRandomizerOption(string name, string description, bool defaultOption = false) { - return new RandomizerOption { - Key = name.ToLower(), - Description = description, - Type = RandomizerOptionType.Checkbox, - Default = defaultOption.ToString().ToLower(), - Values = new Dictionary() - }; - } - - public static TEnum GetDefaultValue() where TEnum : Enum { - Type t = typeof(TEnum); - var attributes = (DefaultValueAttribute[])t.GetCustomAttributes(typeof(DefaultValueAttribute), false); - if ((attributes?.Length ?? 0) > 0) { - return (TEnum)attributes.First().Value; - } - else { - return default; - } - } """ + @property + def Keysanity(self) -> bool: + return self.KeyShuffle != KeyShuffle.Null \ No newline at end of file diff --git a/worlds/smz3/TotalSMZ3/Item.py b/worlds/smz3/TotalSMZ3/Item.py index bad16ce9d0..2aced8bfac 100644 --- a/worlds/smz3/TotalSMZ3/Item.py +++ b/worlds/smz3/TotalSMZ3/Item.py @@ -130,6 +130,11 @@ class ItemType(Enum): CardLowerNorfairL1 = 0xDE CardLowerNorfairBoss = 0xDF + SmMapBrinstar = 0xCA + SmMapWreckedShip = 0xCB + SmMapMaridia = 0xCC + SmMapLowerNorfair = 0xCD + Missile = 0xC2 Super = 0xC3 PowerBomb = 0xC4 @@ -174,6 +179,7 @@ class Item: map = re.compile("^Map") compass = re.compile("^Compass") keycard = re.compile("^Card") + smMap = re.compile("^SmMap") def IsDungeonItem(self): return self.dungeon.match(self.Type.name) def IsBigKey(self): return self.bigKey.match(self.Type.name) @@ -181,6 +187,7 @@ class Item: def IsMap(self): return self.map.match(self.Type.name) def IsCompass(self): return self.compass.match(self.Type.name) def IsKeycard(self): return self.keycard.match(self.Type.name) + def IsSmMap(self): return self.smMap.match(self.Type.name) def Is(self, type: ItemType, world): return self.Type == type and self.World == world @@ -313,7 +320,7 @@ class Item: Item.AddRange(itemPool, 4, Item(ItemType.BombUpgrade5)) Item.AddRange(itemPool, 2, Item(ItemType.OneRupee)) Item.AddRange(itemPool, 4, Item(ItemType.FiveRupees)) - Item.AddRange(itemPool, 25 if world.Config.Keysanity else 28, Item(ItemType.TwentyRupees)) + Item.AddRange(itemPool, 21 if world.Config.Keysanity else 28, Item(ItemType.TwentyRupees)) Item.AddRange(itemPool, 7, Item(ItemType.FiftyRupees)) Item.AddRange(itemPool, 5, Item(ItemType.ThreeHundredRupees)) @@ -421,6 +428,21 @@ class Item: return itemPool + @staticmethod + def CreateSmMaps(world): + itemPool = [ + Item(ItemType.SmMapBrinstar, world), + Item(ItemType.SmMapWreckedShip, world), + Item(ItemType.SmMapMaridia, world), + Item(ItemType.SmMapLowerNorfair, world) + ] + + for item in itemPool: + item.Progression = True + item.World = world + + return itemPool + @staticmethod def Get(items, itemType:ItemType): item = next((i for i in items if i.Type == itemType), None) @@ -725,7 +747,7 @@ class Progression: def CanAccessMiseryMirePortal(self, config: Config): if (config.SMLogic == SMLogic.Normal): - return (self.CardNorfairL2 or (self.SpeedBooster and self.Wave)) and self.Varia and self.Super and (self.Gravity and self.SpaceJump) and self.CanUsePowerBombs() + return (self.CardNorfairL2 or (self.SpeedBooster and self.Wave)) and self.Varia and self.Super and self.Gravity and self.SpaceJump and self.CanUsePowerBombs() else: return (self.CardNorfairL2 or self.SpeedBooster) and self.Varia and self.Super and \ (self.CanFly() or self.HiJump or self.SpeedBooster or self.CanSpringBallJump() or self.Ice) \ @@ -769,11 +791,11 @@ class Progression: if (world.Config.SMLogic == SMLogic.Normal): return self.MoonPearl and self.Flippers and \ self.Gravity and self.Morph and \ - (world.CanAquire(self, worlds.smz3.TotalSMZ3.Region.RewardType.Agahnim) or self.Hammer and self.CanLiftLight() or self.CanLiftHeavy()) + (world.CanAcquire(self, worlds.smz3.TotalSMZ3.Region.RewardType.Agahnim) or self.Hammer and self.CanLiftLight() or self.CanLiftHeavy()) else: return self.MoonPearl and self.Flippers and \ (self.CanSpringBallJump() or self.HiJump or self.Gravity) and self.Morph and \ - (world.CanAquire(self, worlds.smz3.TotalSMZ3.Region.RewardType.Agahnim) or self.Hammer and self.CanLiftLight() or self.CanLiftHeavy()) + (world.CanAcquire(self, worlds.smz3.TotalSMZ3.Region.RewardType.Agahnim) or self.Hammer and self.CanLiftLight() or self.CanLiftHeavy()) # Start of AP integration items_start_id = 84000 diff --git a/worlds/smz3/TotalSMZ3/Patch.py b/worlds/smz3/TotalSMZ3/Patch.py index 54395714ba..2b8d278d49 100644 --- a/worlds/smz3/TotalSMZ3/Patch.py +++ b/worlds/smz3/TotalSMZ3/Patch.py @@ -6,7 +6,7 @@ import typing from BaseClasses import Location from worlds.smz3.TotalSMZ3.Item import Item, ItemType from worlds.smz3.TotalSMZ3.Location import LocationType -from worlds.smz3.TotalSMZ3.Region import IMedallionAccess, IReward, RewardType, SMRegion, Z3Region +from worlds.smz3.TotalSMZ3.Region import IReward, RewardType, SMRegion, Z3Region from worlds.smz3.TotalSMZ3.Regions.Zelda.EasternPalace import EasternPalace from worlds.smz3.TotalSMZ3.Regions.Zelda.DesertPalace import DesertPalace from worlds.smz3.TotalSMZ3.Regions.Zelda.TowerOfHera import TowerOfHera @@ -18,10 +18,14 @@ from worlds.smz3.TotalSMZ3.Regions.Zelda.IcePalace import IcePalace from worlds.smz3.TotalSMZ3.Regions.Zelda.MiseryMire import MiseryMire from worlds.smz3.TotalSMZ3.Regions.Zelda.TurtleRock import TurtleRock from worlds.smz3.TotalSMZ3.Regions.Zelda.GanonsTower import GanonsTower +from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.Brinstar.Kraid import Kraid +from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.WreckedShip import WreckedShip +from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.Maridia.Inner import Inner +from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.NorfairLower.East import East from worlds.smz3.TotalSMZ3.Text.StringTable import StringTable from worlds.smz3.TotalSMZ3.World import World -from worlds.smz3.TotalSMZ3.Config import Config, GameMode, GanonInvincible +from worlds.smz3.TotalSMZ3.Config import Config, OpenTourian, Goal from worlds.smz3.TotalSMZ3.Text.Texts import Texts from worlds.smz3.TotalSMZ3.Text.Dialog import Dialog @@ -30,6 +34,11 @@ class KeycardPlaque: Level2 = 0xe1 Boss = 0xe2 Null = 0x00 + Zero = 0xe3 + One = 0xe4 + Two = 0xe5 + Three = 0xe6 + Four = 0xe7 class KeycardDoors: Left = 0xd414 @@ -73,8 +82,8 @@ class DropPrize(Enum): Fairy = 0xE3 class Patch: - Major = 0 - Minor = 1 + Major = 11 + Minor = 3 allWorlds: List[World] myWorld: World seedGuid: str @@ -105,13 +114,16 @@ class Patch: self.WriteDiggingGameRng() - self.WritePrizeShuffle() + self.WritePrizeShuffle(self.myWorld.WorldState.DropPrizes) self.WriteRemoveEquipmentFromUncle( self.myWorld.GetLocation("Link's Uncle").APLocation.item.item if self.myWorld.GetLocation("Link's Uncle").APLocation.item.game == "SMZ3" else Item(ItemType.Something)) - self.WriteGanonInvicible(config.GanonInvincible) + self.WriteGanonInvicible(config.Goal) + self.WritePreOpenPyramid(config.Goal) + self.WriteCrystalsNeeded(self.myWorld.TowerCrystals, self.myWorld.GanonCrystals) + self.WriteBossesNeeded(self.myWorld.TourianBossTokens) self.WriteRngBlock() self.WriteSaveAndQuitFromBossRoom() @@ -135,26 +147,27 @@ class Patch: return {patch[0]:patch[1] for patch in self.patches} def WriteMedallions(self): + from worlds.smz3.TotalSMZ3.WorldState import Medallion turtleRock = next(region for region in self.myWorld.Regions if isinstance(region, TurtleRock)) miseryMire = next(region for region in self.myWorld.Regions if isinstance(region, MiseryMire)) turtleRockAddresses = [0x308023, 0xD020, 0xD0FF, 0xD1DE ] miseryMireAddresses = [ 0x308022, 0xCFF2, 0xD0D1, 0xD1B0 ] - if turtleRock.Medallion == ItemType.Bombos: + if turtleRock.Medallion == Medallion.Bombos: turtleRockValues = [0x00, 0x51, 0x10, 0x00] - elif turtleRock.Medallion == ItemType.Ether: + elif turtleRock.Medallion == Medallion.Ether: turtleRockValues = [0x01, 0x51, 0x18, 0x00] - elif turtleRock.Medallion == ItemType.Quake: + elif turtleRock.Medallion == Medallion.Quake: turtleRockValues = [0x02, 0x14, 0xEF, 0xC4] else: raise exception(f"Tried using {turtleRock.Medallion} in place of Turtle Rock medallion") - if miseryMire.Medallion == ItemType.Bombos: + if miseryMire.Medallion == Medallion.Bombos: miseryMireValues = [0x00, 0x51, 0x00, 0x00] - elif miseryMire.Medallion == ItemType.Ether: + elif miseryMire.Medallion == Medallion.Ether: miseryMireValues = [0x01, 0x13, 0x9F, 0xF1] - elif miseryMire.Medallion == ItemType.Quake: + elif miseryMire.Medallion == Medallion.Quake: miseryMireValues = [0x02, 0x51, 0x08, 0x00] else: raise exception(f"Tried using {miseryMire.Medallion} in place of Misery Mire medallion") @@ -174,12 +187,19 @@ class Patch: self.rnd.shuffle(pendantsBlueRed) pendantRewards = pendantsGreen + pendantsBlueRed + bossTokens = [ 1, 2, 3, 4 ] + regions = [region for region in self.myWorld.Regions if isinstance(region, IReward)] crystalRegions = [region for region in regions if region.Reward == RewardType.CrystalBlue] + [region for region in regions if region.Reward == RewardType.CrystalRed] pendantRegions = [region for region in regions if region.Reward == RewardType.PendantGreen] + [region for region in regions if region.Reward == RewardType.PendantNonGreen] + bossRegions = [region for region in regions if region.Reward == RewardType.BossTokenKraid] + \ + [region for region in regions if region.Reward == RewardType.BossTokenPhantoon] + \ + [region for region in regions if region.Reward == RewardType.BossTokenDraygon] + \ + [region for region in regions if region.Reward == RewardType.BossTokenRidley] self.patches += self.RewardPatches(crystalRegions, crystalRewards, self.CrystalValues) self.patches += self.RewardPatches(pendantRegions, pendantRewards, self.PendantValues) + self.patches += self.RewardPatches(bossRegions, bossTokens, self.BossTokenValues) def RewardPatches(self, regions: List[IReward], rewards: List[int], rewardValues: Callable): addresses = [self.RewardAddresses(region) for region in regions] @@ -189,17 +209,22 @@ class Patch: def RewardAddresses(self, region: IReward): regionType = { - EasternPalace : [ 0x2A09D, 0xABEF8, 0xABEF9, 0x308052, 0x30807C, 0x1C6FE ], - DesertPalace : [ 0x2A09E, 0xABF1C, 0xABF1D, 0x308053, 0x308078, 0x1C6FF ], - TowerOfHera : [ 0x2A0A5, 0xABF0A, 0xABF0B, 0x30805A, 0x30807A, 0x1C706 ], - PalaceOfDarkness : [ 0x2A0A1, 0xABF00, 0xABF01, 0x308056, 0x30807D, 0x1C702 ], - SwampPalace : [ 0x2A0A0, 0xABF6C, 0xABF6D, 0x308055, 0x308071, 0x1C701 ], - SkullWoods : [ 0x2A0A3, 0xABF12, 0xABF13, 0x308058, 0x30807B, 0x1C704 ], - ThievesTown : [ 0x2A0A6, 0xABF36, 0xABF37, 0x30805B, 0x308077, 0x1C707 ], - IcePalace : [ 0x2A0A4, 0xABF5A, 0xABF5B, 0x308059, 0x308073, 0x1C705 ], - MiseryMire : [ 0x2A0A2, 0xABF48, 0xABF49, 0x308057, 0x308075, 0x1C703 ], - TurtleRock : [ 0x2A0A7, 0xABF24, 0xABF25, 0x30805C, 0x308079, 0x1C708 ] + EasternPalace : [ 0x2A09D, 0xABEF8, 0xABEF9, 0x308052, 0x30807C, 0x1C6FE, 0x30D100], + DesertPalace : [ 0x2A09E, 0xABF1C, 0xABF1D, 0x308053, 0x308078, 0x1C6FF, 0x30D101 ], + TowerOfHera : [ 0x2A0A5, 0xABF0A, 0xABF0B, 0x30805A, 0x30807A, 0x1C706, 0x30D102 ], + PalaceOfDarkness : [ 0x2A0A1, 0xABF00, 0xABF01, 0x308056, 0x30807D, 0x1C702, 0x30D103 ], + SwampPalace : [ 0x2A0A0, 0xABF6C, 0xABF6D, 0x308055, 0x308071, 0x1C701, 0x30D104 ], + SkullWoods : [ 0x2A0A3, 0xABF12, 0xABF13, 0x308058, 0x30807B, 0x1C704, 0x30D105 ], + ThievesTown : [ 0x2A0A6, 0xABF36, 0xABF37, 0x30805B, 0x308077, 0x1C707, 0x30D106 ], + IcePalace : [ 0x2A0A4, 0xABF5A, 0xABF5B, 0x308059, 0x308073, 0x1C705, 0x30D107 ], + MiseryMire : [ 0x2A0A2, 0xABF48, 0xABF49, 0x308057, 0x308075, 0x1C703, 0x30D108 ], + TurtleRock : [ 0x2A0A7, 0xABF24, 0xABF25, 0x30805C, 0x308079, 0x1C708, 0x30D109 ], + Kraid : [ 0xF26002, 0xF26004, 0xF26005, 0xF26000, 0xF26006, 0xF26007, 0x82FD36 ], + WreckedShip : [ 0xF2600A, 0xF2600C, 0xF2600D, 0xF26008, 0xF2600E, 0xF2600F, 0x82FE26 ], + Inner : [ 0xF26012, 0xF26014, 0xF26015, 0xF26010, 0xF26016, 0xF26017, 0x82FE76 ], + East : [ 0xF2601A, 0xF2601C, 0xF2601D, 0xF26018, 0xF2601E, 0xF2601F, 0x82FDD6 ] } + result = regionType.get(type(region), None) if result is None: raise exception(f"Region {region} should not be a dungeon reward region") @@ -208,13 +233,13 @@ class Patch: def CrystalValues(self, crystal: int): crystalMap = { - 1 : [ 0x02, 0x34, 0x64, 0x40, 0x7F, 0x06 ], - 2 : [ 0x10, 0x34, 0x64, 0x40, 0x79, 0x06 ], - 3 : [ 0x40, 0x34, 0x64, 0x40, 0x6C, 0x06 ], - 4 : [ 0x20, 0x34, 0x64, 0x40, 0x6D, 0x06 ], - 5 : [ 0x04, 0x32, 0x64, 0x40, 0x6E, 0x06 ], - 6 : [ 0x01, 0x32, 0x64, 0x40, 0x6F, 0x06 ], - 7 : [ 0x08, 0x34, 0x64, 0x40, 0x7C, 0x06 ], + 1 : [ 0x02, 0x34, 0x64, 0x40, 0x7F, 0x06, 0x10 ], + 2 : [ 0x10, 0x34, 0x64, 0x40, 0x79, 0x06, 0x10 ], + 3 : [ 0x40, 0x34, 0x64, 0x40, 0x6C, 0x06, 0x10 ], + 4 : [ 0x20, 0x34, 0x64, 0x40, 0x6D, 0x06, 0x10 ], + 5 : [ 0x04, 0x32, 0x64, 0x40, 0x6E, 0x06, 0x11 ], + 6 : [ 0x01, 0x32, 0x64, 0x40, 0x6F, 0x06, 0x11 ], + 7 : [ 0x08, 0x34, 0x64, 0x40, 0x7C, 0x06, 0x10 ], } result = crystalMap.get(crystal, None) if result is None: @@ -224,15 +249,28 @@ class Patch: def PendantValues(self, pendant: int): pendantMap = { - 1 : [ 0x04, 0x38, 0x62, 0x00, 0x69, 0x01 ], - 2 : [ 0x01, 0x32, 0x60, 0x00, 0x69, 0x03 ], - 3 : [ 0x02, 0x34, 0x60, 0x00, 0x69, 0x02 ], + 1 : [ 0x04, 0x38, 0x62, 0x00, 0x69, 0x01, 0x12 ], + 2 : [ 0x01, 0x32, 0x60, 0x00, 0x69, 0x03, 0x14 ], + 3 : [ 0x02, 0x34, 0x60, 0x00, 0x69, 0x02, 0x13 ] } result = pendantMap.get(pendant, None) if result is None: raise exception(f"Tried using {pendant} as a pendant number") else: return result + + def BossTokenValues(self, token: int): + tokenMap = { + 1 : [ 0x01, 0x38, 0x40, 0x80, 0x69, 0x80, 0x15 ], + 2 : [ 0x02, 0x34, 0x42, 0x80, 0x69, 0x81, 0x16 ], + 3 : [ 0x04, 0x34, 0x44, 0x80, 0x69, 0x82, 0x17 ], + 4 : [ 0x08, 0x32, 0x46, 0x80, 0x69, 0x83, 0x18 ] + } + result = tokenMap.get(token, None) + if result is None: + raise exception(f"Tried using {token} as a boss token number") + else: + return result def WriteSMLocations(self, locations: List[Location]): def GetSMItemPLM(location:Location): @@ -259,7 +297,7 @@ class Patch: ItemType.SpaceJump : 0xEF1B, ItemType.ScrewAttack : 0xEF1F } - plmId = 0xEFE0 if self.myWorld.Config.GameMode == GameMode.Multiworld else \ + plmId = 0xEFE0 if self.myWorld.Config.Multiworld else \ itemMap.get(location.APLocation.item.item.Type, 0xEFE0) if (plmId == 0xEFE0): plmId += 4 if location.Type == LocationType.Chozo else 8 if location.Type == LocationType.Hidden else 0 @@ -268,7 +306,7 @@ class Patch: return plmId for location in locations: - if (self.myWorld.Config.GameMode == GameMode.Multiworld): + if (self.myWorld.Config.Multiworld): self.patches.append((Snes(location.Address), getWordArray(GetSMItemPLM(location)))) self.patches.append(self.ItemTablePatch(location, self.GetZ3ItemId(location))) else: @@ -283,18 +321,14 @@ class Patch: self.patches.append((Snes(0x9E3BB), [0xE4] if location.APLocation.item.game == "SMZ3" and location.APLocation.item.item.Type == ItemType.KeyTH else [0xEB])) elif (location.Type in [LocationType.Pedestal, LocationType.Ether, LocationType.Bombos]): text = Texts.ItemTextbox(location.APLocation.item.item if location.APLocation.item.game == "SMZ3" else Item(ItemType.Something)) - dialog = Dialog.Simple(text) if (location.Type == LocationType.Pedestal): self.stringTable.SetPedestalText(text) - self.patches.append((Snes(0x308300), dialog)) elif (location.Type == LocationType.Ether): self.stringTable.SetEtherText(text) - self.patches.append((Snes(0x308F00), dialog)) elif (location.Type == LocationType.Bombos): self.stringTable.SetBombosText(text) - self.patches.append((Snes(0x309000), dialog)) - if (self.myWorld.Config.GameMode == GameMode.Multiworld): + if (self.myWorld.Config.Multiworld): self.patches.append((Snes(location.Address), [(location.Id - 256)])) self.patches.append(self.ItemTablePatch(location, self.GetZ3ItemId(location))) else: @@ -305,11 +339,11 @@ class Patch: item = location.APLocation.item.item itemDungeon = None if item.IsKey(): - itemDungeon = ItemType.Key if (not item.World.Config.Keysanity or item.Type != ItemType.KeyHC) else ItemType.KeyHC + itemDungeon = ItemType.Key elif item.IsBigKey(): itemDungeon = ItemType.BigKey elif item.IsMap(): - itemDungeon = ItemType.Map if (not item.World.Config.Keysanity or item.Type != ItemType.MapHC) else ItemType.MapHC + itemDungeon = ItemType.Map elif item.IsCompass(): itemDungeon = ItemType.Compass @@ -327,15 +361,11 @@ class Patch: def WriteDungeonMusic(self, keysanity: bool): if (not keysanity): - regions = [region for region in self.myWorld.Regions if isinstance(region, IReward)] - music = [] + regions = [region for region in self.myWorld.Regions if isinstance(region, Z3Region) and isinstance(region, IReward) and + region.Reward != None and region.Reward != RewardType.Agahnim] pendantRegions = [region for region in regions if region.Reward in [RewardType.PendantGreen, RewardType.PendantNonGreen]] crystalRegions = [region for region in regions if region.Reward in [RewardType.CrystalBlue, RewardType.CrystalRed]] - regions = pendantRegions + crystalRegions - music = [ - 0x11, 0x11, 0x11, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, - ] + music = [0x11 if (region.Reward == RewardType.PendantGreen or region.Reward == RewardType.PendantNonGreen) else 0x16 for region in regions] self.patches += self.MusicPatches(regions, music) #IEnumerable RandomDungeonMusic() { @@ -366,51 +396,13 @@ class Patch: else: return result - def WritePrizeShuffle(self): - prizePackItems = 56 - treePullItems = 3 - - bytes = [] - drop = 0 - final = 0 - - pool = [ - DropPrize.Heart, DropPrize.Heart, DropPrize.Heart, DropPrize.Heart, DropPrize.Green, DropPrize.Heart, DropPrize.Heart, DropPrize.Green, #// pack 1 - DropPrize.Blue, DropPrize.Green, DropPrize.Blue, DropPrize.Red, DropPrize.Blue, DropPrize.Green, DropPrize.Blue, DropPrize.Blue, #// pack 2 - DropPrize.FullMagic, DropPrize.Magic, DropPrize.Magic, DropPrize.Blue, DropPrize.FullMagic, DropPrize.Magic, DropPrize.Heart, DropPrize.Magic, #// pack 3 - DropPrize.Bomb1, DropPrize.Bomb1, DropPrize.Bomb1, DropPrize.Bomb4, DropPrize.Bomb1, DropPrize.Bomb1, DropPrize.Bomb8, DropPrize.Bomb1, #// pack 4 - DropPrize.Arrow5, DropPrize.Heart, DropPrize.Arrow5, DropPrize.Arrow10, DropPrize.Arrow5, DropPrize.Heart, DropPrize.Arrow5, DropPrize.Arrow10,#// pack 5 - DropPrize.Magic, DropPrize.Green, DropPrize.Heart, DropPrize.Arrow5, DropPrize.Magic, DropPrize.Bomb1, DropPrize.Green, DropPrize.Heart, #// pack 6 - DropPrize.Heart, DropPrize.Fairy, DropPrize.FullMagic, DropPrize.Red, DropPrize.Bomb8, DropPrize.Heart, DropPrize.Red, DropPrize.Arrow10, #// pack 7 - DropPrize.Green, DropPrize.Blue, DropPrize.Red,#// from pull trees - DropPrize.Green, DropPrize.Red,#// from prize crab - DropPrize.Green, #// stunned prize - DropPrize.Red,#// saved fish prize - ] - - prizes = pool - self.rnd.shuffle(prizes) - - #/* prize pack drop order */ - (bytes, prizes) = SplitOff(prizes, prizePackItems) - self.patches.append((Snes(0x6FA78), [byte.value for byte in bytes])) - - #/* tree pull prizes */ - (bytes, prizes) = SplitOff(prizes, treePullItems) - self.patches.append((Snes(0x1DFBD4), [byte.value for byte in bytes])) - - #/* crab prizes */ - (drop, final, prizes) = (prizes[0], prizes[1], prizes[2:]) - self.patches.append((Snes(0x6A9C8), [ drop.value ])) - self.patches.append((Snes(0x6A9C4), [ final.value ])) - - #/* stun prize */ - (drop, prizes) = (prizes[0], prizes[1:]) - self.patches.append((Snes(0x6F993), [ drop.value ])) - - #/* fish prize */ - drop = prizes[0] - self.patches.append((Snes(0x1D82CC), [ drop.value ])) + def WritePrizeShuffle(self, dropPrizes): + self.patches.append((Snes(0x6FA78), [e.value for e in dropPrizes.Packs])) + self.patches.append((Snes(0x1DFBD4), [e.value for e in dropPrizes.TreePulls])) + self.patches.append((Snes(0x6A9C8), [dropPrizes.CrabContinous.value])) + self.patches.append((Snes(0x6A9C4), [dropPrizes.CrabFinal.value])) + self.patches.append((Snes(0x6F993), [dropPrizes.Stun.value])) + self.patches.append((Snes(0x1D82CC), [dropPrizes.Fish.value])) self.patches += self.EnemyPrizePackDistribution() @@ -524,46 +516,29 @@ class Patch: redCrystalDungeons = [region for region in regions if region.Reward == RewardType.CrystalRed] sahasrahla = Texts.SahasrahlaReveal(greenPendantDungeon) - self.patches.append((Snes(0x308A00), Dialog.Simple(sahasrahla))) self.stringTable.SetSahasrahlaRevealText(sahasrahla) bombShop = Texts.BombShopReveal(redCrystalDungeons) - self.patches.append((Snes(0x308E00), Dialog.Simple(bombShop))) self.stringTable.SetBombShopRevealText(bombShop) blind = Texts.Blind(self.rnd) - self.patches.append((Snes(0x308800), Dialog.Simple(blind))) self.stringTable.SetBlindText(blind) tavernMan = Texts.TavernMan(self.rnd) - self.patches.append((Snes(0x308C00), Dialog.Simple(tavernMan))) self.stringTable.SetTavernManText(tavernMan) ganon = Texts.GanonFirstPhase(self.rnd) - self.patches.append((Snes(0x308600), Dialog.Simple(ganon))) self.stringTable.SetGanonFirstPhaseText(ganon) - #// Todo: Verify these two are correct if ganon invincible patch is ever added - #// ganon_fall_in_alt in v30 - ganonFirstPhaseInvincible = "You think you\nare ready to\nface me?\n\nI will not die\n\nunless you\ncomplete your\ngoals. Dingus!" - self.patches.append((Snes(0x309100), Dialog.Simple(ganonFirstPhaseInvincible))) - - #// ganon_phase_3_alt in v30 - ganonThirdPhaseInvincible = "Got wax in\nyour ears?\nI cannot die!" - self.patches.append((Snes(0x309200), Dialog.Simple(ganonThirdPhaseInvincible))) - #// --- - silversLocation = [loc for world in self.allWorlds for loc in world.Locations if loc.ItemIs(ItemType.SilverArrows, self.myWorld)] if len(silversLocation) == 0: silvers = Texts.GanonThirdPhaseMulti(None, self.myWorld, self.silversWorldID, self.playerIDToNames[self.silversWorldID]) else: - silvers = Texts.GanonThirdPhaseMulti(silversLocation[0].Region, self.myWorld) if config.GameMode == GameMode.Multiworld else \ + silvers = Texts.GanonThirdPhaseMulti(silversLocation[0].Region, self.myWorld) if config.Multiworld else \ Texts.GanonThirdPhaseSingle(silversLocation[0].Region) - self.patches.append((Snes(0x308700), Dialog.Simple(silvers))) self.stringTable.SetGanonThirdPhaseText(silvers) triforceRoom = Texts.TriforceRoom(self.rnd) - self.patches.append((Snes(0x308400), Dialog.Simple(triforceRoom))) self.stringTable.SetTriforceRoomText(triforceRoom) def WriteStringTable(self): @@ -579,26 +554,32 @@ class Patch: return bytearray(name, 'utf8') def WriteSeedData(self): - configField = \ + configField1 = \ ((1 if self.myWorld.Config.Race else 0) << 15) | \ ((1 if self.myWorld.Config.Keysanity else 0) << 13) | \ - ((1 if self.myWorld.Config.GameMode == Config.GameMode.Multiworld else 0) << 12) | \ + ((1 if self.myWorld.Config.Multiworld else 0) << 12) | \ (self.myWorld.Config.Z3Logic.value << 10) | \ (self.myWorld.Config.SMLogic.value << 8) | \ (Patch.Major << 4) | \ (Patch.Minor << 0) + configField2 = \ + ((1 if self.myWorld.Config.SwordLocation else 0) << 14) | \ + ((1 if self.myWorld.Config.MorphLocation else 0) << 12) | \ + ((1 if self.myWorld.Config.Goal else 0) << 8) + self.patches.append((Snes(0x80FF50), getWordArray(self.myWorld.Id))) - self.patches.append((Snes(0x80FF52), getWordArray(configField))) + self.patches.append((Snes(0x80FF52), getWordArray(configField1))) self.patches.append((Snes(0x80FF54), getDoubleWordArray(self.seed))) + self.patches.append((Snes(0x80FF58), getWordArray(configField2))) #/* Reserve the rest of the space for future use */ - self.patches.append((Snes(0x80FF58), [0x00] * 8)) + self.patches.append((Snes(0x80FF5A), [0x00] * 6)) self.patches.append((Snes(0x80FF60), bytearray(self.seedGuid, 'utf8'))) self.patches.append((Snes(0x80FF80), bytearray(self.myWorld.Guid, 'utf8'))) def WriteCommonFlags(self): #/* Common Combo Configuration flags at [asm]/config.asm */ - if (self.myWorld.Config.GameMode == GameMode.Multiworld): + if (self.myWorld.Config.Multiworld): self.patches.append((Snes(0xF47000), getWordArray(0x0001))) if (self.myWorld.Config.Keysanity): self.patches.append((Snes(0xF47006), getWordArray(0x0001))) @@ -619,96 +600,104 @@ class Patch: if (self.myWorld.Config.Keysanity): self.patches.append((Snes(0x40003B), [ 1 ])) #// MapMode #$00 = Always On (default) - #$01 = Require Map Item self.patches.append((Snes(0x400045), [ 0x0f ])) #// display ----dcba a: Small Keys, b: Big Key, c: Map, d: Compass + self.patches.append((Snes(0x40016A), [ 0x01 ])) #// FreeItemText: db #$01 ; #00 = Off (default) - #$01 = On def WriteSMKeyCardDoors(self): - if (not self.myWorld.Config.Keysanity): - return - - plaquePLm = 0xd410 - - doorList = [ - #// RoomId Door Facing yyxx Keycard Event Type Plaque type yyxx, Address (if 0 a dynamic PLM is created) - #// Crateria - [ 0x91F8, KeycardDoors.Right, 0x2601, KeycardEvents.CrateriaLevel1, KeycardPlaque.Level1, 0x2400, 0x0000 ], #// Crateria - Landing Site - Door to gauntlet - [ 0x91F8, KeycardDoors.Left, 0x168E, KeycardEvents.CrateriaLevel1, KeycardPlaque.Level1, 0x148F, 0x801E ], #// Crateria - Landing Site - Door to landing site PB - [ 0x948C, KeycardDoors.Left, 0x062E, KeycardEvents.CrateriaLevel2, KeycardPlaque.Level2, 0x042F, 0x8222 ], #// Crateria - Before Moat - Door to moat (overwrite PB door) - [ 0x99BD, KeycardDoors.Left, 0x660E, KeycardEvents.CrateriaBoss, KeycardPlaque.Boss, 0x640F, 0x8470 ], #// Crateria - Before G4 - Door to G4 - [ 0x9879, KeycardDoors.Left, 0x062E, KeycardEvents.CrateriaBoss, KeycardPlaque.Boss, 0x042F, 0x8420 ], #// Crateria - Before BT - Door to Bomb Torizo - - #// Brinstar - [ 0x9F11, KeycardDoors.Left, 0x060E, KeycardEvents.BrinstarLevel1, KeycardPlaque.Level1, 0x040F, 0x8784 ], #// Brinstar - Blue Brinstar - Door to ceiling e-tank room - - [ 0x9AD9, KeycardDoors.Right, 0xA601, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0xA400, 0x0000 ], #// Brinstar - Green Brinstar - Door to etecoon area - [ 0x9D9C, KeycardDoors.Down, 0x0336, KeycardEvents.BrinstarBoss, KeycardPlaque.Boss, 0x0234, 0x863A ], #// Brinstar - Pink Brinstar - Door to spore spawn - [ 0xA130, KeycardDoors.Left, 0x161E, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0x141F, 0x881C ], #// Brinstar - Pink Brinstar - Door to wave gate e-tank - [ 0xA0A4, KeycardDoors.Left, 0x062E, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0x042F, 0x0000 ], #// Brinstar - Pink Brinstar - Door to spore spawn super - - [ 0xA56B, KeycardDoors.Left, 0x161E, KeycardEvents.BrinstarBoss, KeycardPlaque.Boss, 0x141F, 0x8A1A ], #// Brinstar - Before Kraid - Door to Kraid - - #// Upper Norfair - [ 0xA7DE, KeycardDoors.Right, 0x3601, KeycardEvents.NorfairLevel1, KeycardPlaque.Level1, 0x3400, 0x8B00 ], #// Norfair - Business Centre - Door towards Ice - [ 0xA923, KeycardDoors.Right, 0x0601, KeycardEvents.NorfairLevel1, KeycardPlaque.Level1, 0x0400, 0x0000 ], #// Norfair - Pre-Crocomire - Door towards Ice - - [ 0xA788, KeycardDoors.Left, 0x162E, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x142F, 0x8AEA ], #// Norfair - Lava Missile Room - Door towards Bubble Mountain - [ 0xAF72, KeycardDoors.Left, 0x061E, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x041F, 0x0000 ], #// Norfair - After frog speedway - Door to Bubble Mountain - [ 0xAEDF, KeycardDoors.Down, 0x0206, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x0204, 0x0000 ], #// Norfair - Below bubble mountain - Door to Bubble Mountain - [ 0xAD5E, KeycardDoors.Right, 0x0601, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x0400, 0x0000 ], #// Norfair - LN Escape - Door to Bubble Mountain - - [ 0xA923, KeycardDoors.Up, 0x2DC6, KeycardEvents.NorfairBoss, KeycardPlaque.Boss, 0x2EC4, 0x8B96 ], #// Norfair - Pre-Crocomire - Door to Crocomire - - #// Lower Norfair - [ 0xB4AD, KeycardDoors.Left, 0x160E, KeycardEvents.LowerNorfairLevel1, KeycardPlaque.Level1, 0x140F, 0x0000 ], #// Lower Norfair - WRITG - Door to Amphitheatre - [ 0xAD5E, KeycardDoors.Left, 0x065E, KeycardEvents.LowerNorfairLevel1, KeycardPlaque.Level1, 0x045F, 0x0000 ], #// Lower Norfair - Exit - Door to "Reverse LN Entry" - [ 0xB37A, KeycardDoors.Right, 0x0601, KeycardEvents.LowerNorfairBoss, KeycardPlaque.Boss, 0x0400, 0x8EA6 ], #// Lower Norfair - Pre-Ridley - Door to Ridley - - #// Maridia - [ 0xD0B9, KeycardDoors.Left, 0x065E, KeycardEvents.MaridiaLevel1, KeycardPlaque.Level1, 0x045F, 0x0000 ], #// Maridia - Mt. Everest - Door to Pink Maridia - [ 0xD5A7, KeycardDoors.Right, 0x1601, KeycardEvents.MaridiaLevel1, KeycardPlaque.Level1, 0x1400, 0x0000 ], #// Maridia - Aqueduct - Door towards Beach - - [ 0xD617, KeycardDoors.Left, 0x063E, KeycardEvents.MaridiaLevel2, KeycardPlaque.Level2, 0x043F, 0x0000 ], #// Maridia - Pre-Botwoon - Door to Botwoon - [ 0xD913, KeycardDoors.Right, 0x2601, KeycardEvents.MaridiaLevel2, KeycardPlaque.Level2, 0x2400, 0x0000 ], #// Maridia - Pre-Colloseum - Door to post-botwoon - - [ 0xD78F, KeycardDoors.Right, 0x2601, KeycardEvents.MaridiaBoss, KeycardPlaque.Boss, 0x2400, 0xC73B ], #// Maridia - Precious Room - Door to Draygon - - [ 0xDA2B, KeycardDoors.BossLeft, 0x164E, 0x00f0, KeycardPlaque.Null, 0x144F, 0x0000 ], #// Maridia - Change Cac Alley Door to Boss Door (prevents key breaking) - - #// Wrecked Ship - [ 0x93FE, KeycardDoors.Left, 0x167E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x147F, 0x0000 ], #// Wrecked Ship - Outside Wrecked Ship West - Door to Reserve Tank Check - [ 0x968F, KeycardDoors.Left, 0x060E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x040F, 0x0000 ], #// Wrecked Ship - Outside Wrecked Ship West - Door to Bowling Alley - [ 0xCE40, KeycardDoors.Left, 0x060E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x040F, 0x0000 ], #// Wrecked Ship - Gravity Suit - Door to Bowling Alley - - [ 0xCC6F, KeycardDoors.Left, 0x064E, KeycardEvents.WreckedShipBoss, KeycardPlaque.Boss, 0x044F, 0xC29D ], #// Wrecked Ship - Pre-Phantoon - Door to Phantoon - ] - - doorId = 0x0000 + plaquePlm = 0xd410 plmTablePos = 0xf800 - for door in doorList: - doorArgs = doorId | door[3] if door[4] != KeycardPlaque.Null else door[3] - if (door[6] == 0): - #// Write dynamic door - doorData = [] - for x in door[0:3]: - doorData += getWordArray(x) - doorData += getWordArray(doorArgs) - self.patches.append((Snes(0x8f0000 + plmTablePos), doorData)) - plmTablePos += 0x08 - else: - #// Overwrite existing door - doorData = [] - for x in door[1:3]: - doorData += getWordArray(x) - doorData += getWordArray(doorArgs) - self.patches.append((Snes(0x8f0000 + door[6]), doorData)) - if((door[3] == KeycardEvents.BrinstarBoss and door[0] != 0x9D9C) or door[3] == KeycardEvents.LowerNorfairBoss or door[3] == KeycardEvents.MaridiaBoss or door[3] == KeycardEvents.WreckedShipBoss): - #// Overwrite the extra parts of the Gadora with a PLM that just deletes itself - self.patches.append((Snes(0x8f0000 + door[6] + 0x06), [ 0x2F, 0xB6, 0x00, 0x00, 0x00, 0x00, 0x2F, 0xB6, 0x00, 0x00, 0x00, 0x00 ])) - #// Plaque data - if (door[4] != KeycardPlaque.Null): - plaqueData = getWordArray(door[0]) + getWordArray(plaquePLm) + getWordArray(door[5]) + getWordArray(door[4]) - self.patches.append((Snes(0x8f0000 + plmTablePos), plaqueData)) - plmTablePos += 0x08 - doorId += 1 + if ( self.myWorld.Config.Keysanity): + doorList = [ + #// RoomId Door Facing yyxx Keycard Event Type Plaque type yyxx, Address (if 0 a dynamic PLM is created) + #// Crateria + [ 0x91F8, KeycardDoors.Right, 0x2601, KeycardEvents.CrateriaLevel1, KeycardPlaque.Level1, 0x2400, 0x0000 ], #// Crateria - Landing Site - Door to gauntlet + [ 0x91F8, KeycardDoors.Left, 0x168E, KeycardEvents.CrateriaLevel1, KeycardPlaque.Level1, 0x148F, 0x801E ], #// Crateria - Landing Site - Door to landing site PB + [ 0x948C, KeycardDoors.Left, 0x062E, KeycardEvents.CrateriaLevel2, KeycardPlaque.Level2, 0x042F, 0x8222 ], #// Crateria - Before Moat - Door to moat (overwrite PB door) + [ 0x99BD, KeycardDoors.Left, 0x660E, KeycardEvents.CrateriaBoss, KeycardPlaque.Boss, 0x640F, 0x8470 ], #// Crateria - Before G4 - Door to G4 + [ 0x9879, KeycardDoors.Left, 0x062E, KeycardEvents.CrateriaBoss, KeycardPlaque.Boss, 0x042F, 0x8420 ], #// Crateria - Before BT - Door to Bomb Torizo + + #// Brinstar + [ 0x9F11, KeycardDoors.Left, 0x060E, KeycardEvents.BrinstarLevel1, KeycardPlaque.Level1, 0x040F, 0x8784 ], #// Brinstar - Blue Brinstar - Door to ceiling e-tank room + + [ 0x9AD9, KeycardDoors.Right, 0xA601, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0xA400, 0x0000 ], #// Brinstar - Green Brinstar - Door to etecoon area + [ 0x9D9C, KeycardDoors.Down, 0x0336, KeycardEvents.BrinstarBoss, KeycardPlaque.Boss, 0x0234, 0x863A ], #// Brinstar - Pink Brinstar - Door to spore spawn + [ 0xA130, KeycardDoors.Left, 0x161E, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0x141F, 0x881C ], #// Brinstar - Pink Brinstar - Door to wave gate e-tank + [ 0xA0A4, KeycardDoors.Left, 0x062E, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0x042F, 0x0000 ], #// Brinstar - Pink Brinstar - Door to spore spawn super + + [ 0xA56B, KeycardDoors.Left, 0x161E, KeycardEvents.BrinstarBoss, KeycardPlaque.Boss, 0x141F, 0x8A1A ], #// Brinstar - Before Kraid - Door to Kraid + + #// Upper Norfair + [ 0xA7DE, KeycardDoors.Right, 0x3601, KeycardEvents.NorfairLevel1, KeycardPlaque.Level1, 0x3400, 0x8B00 ], #// Norfair - Business Centre - Door towards Ice + [ 0xA923, KeycardDoors.Right, 0x0601, KeycardEvents.NorfairLevel1, KeycardPlaque.Level1, 0x0400, 0x0000 ], #// Norfair - Pre-Crocomire - Door towards Ice + + [ 0xA788, KeycardDoors.Left, 0x162E, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x142F, 0x8AEA ], #// Norfair - Lava Missile Room - Door towards Bubble Mountain + [ 0xAF72, KeycardDoors.Left, 0x061E, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x041F, 0x0000 ], #// Norfair - After frog speedway - Door to Bubble Mountain + [ 0xAEDF, KeycardDoors.Down, 0x0206, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x0204, 0x0000 ], #// Norfair - Below bubble mountain - Door to Bubble Mountain + [ 0xAD5E, KeycardDoors.Right, 0x0601, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x0400, 0x0000 ], #// Norfair - LN Escape - Door to Bubble Mountain + + [ 0xA923, KeycardDoors.Up, 0x2DC6, KeycardEvents.NorfairBoss, KeycardPlaque.Boss, 0x2EC4, 0x8B96 ], #// Norfair - Pre-Crocomire - Door to Crocomire + + #// Lower Norfair + [ 0xB4AD, KeycardDoors.Left, 0x160E, KeycardEvents.LowerNorfairLevel1, KeycardPlaque.Level1, 0x140F, 0x0000 ], #// Lower Norfair - WRITG - Door to Amphitheatre + [ 0xAD5E, KeycardDoors.Left, 0x065E, KeycardEvents.LowerNorfairLevel1, KeycardPlaque.Level1, 0x045F, 0x0000 ], #// Lower Norfair - Exit - Door to "Reverse LN Entry" + [ 0xB37A, KeycardDoors.Right, 0x0601, KeycardEvents.LowerNorfairBoss, KeycardPlaque.Boss, 0x0400, 0x8EA6 ], #// Lower Norfair - Pre-Ridley - Door to Ridley + + #// Maridia + [ 0xD0B9, KeycardDoors.Left, 0x065E, KeycardEvents.MaridiaLevel1, KeycardPlaque.Level1, 0x045F, 0x0000 ], #// Maridia - Mt. Everest - Door to Pink Maridia + [ 0xD5A7, KeycardDoors.Right, 0x1601, KeycardEvents.MaridiaLevel1, KeycardPlaque.Level1, 0x1400, 0x0000 ], #// Maridia - Aqueduct - Door towards Beach + + [ 0xD617, KeycardDoors.Left, 0x063E, KeycardEvents.MaridiaLevel2, KeycardPlaque.Level2, 0x043F, 0x0000 ], #// Maridia - Pre-Botwoon - Door to Botwoon + [ 0xD913, KeycardDoors.Right, 0x2601, KeycardEvents.MaridiaLevel2, KeycardPlaque.Level2, 0x2400, 0x0000 ], #// Maridia - Pre-Colloseum - Door to post-botwoon + + [ 0xD78F, KeycardDoors.Right, 0x2601, KeycardEvents.MaridiaBoss, KeycardPlaque.Boss, 0x2400, 0xC73B ], #// Maridia - Precious Room - Door to Draygon + + [ 0xDA2B, KeycardDoors.BossLeft, 0x164E, 0x00f0, KeycardPlaque.Null, 0x144F, 0x0000 ], #// Maridia - Change Cac Alley Door to Boss Door (prevents key breaking) + + #// Wrecked Ship + [ 0x93FE, KeycardDoors.Left, 0x167E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x147F, 0x0000 ], #// Wrecked Ship - Outside Wrecked Ship West - Door to Reserve Tank Check + [ 0x968F, KeycardDoors.Left, 0x060E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x040F, 0x0000 ], #// Wrecked Ship - Outside Wrecked Ship West - Door to Bowling Alley + [ 0xCE40, KeycardDoors.Left, 0x060E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x040F, 0x0000 ], #// Wrecked Ship - Gravity Suit - Door to Bowling Alley + + [ 0xCC6F, KeycardDoors.Left, 0x064E, KeycardEvents.WreckedShipBoss, KeycardPlaque.Boss, 0x044F, 0xC29D ], #// Wrecked Ship - Pre-Phantoon - Door to Phantoon + ] + + doorId = 0x0000 + for door in doorList: + #/* When "Fast Ganon" is set, don't place the G4 Boss key door to enable faster games */ + if (door[0] == 0x99BD and self.myWorld.Config.Goal == Goal.FastGanonDefeatMotherBrain): + continue + doorArgs = doorId | door[3] if door[4] != KeycardPlaque.Null else door[3] + if (door[6] == 0): + #// Write dynamic door + doorData = [] + for x in door[0:3]: + doorData += getWordArray(x) + doorData += getWordArray(doorArgs) + self.patches.append((Snes(0x8f0000 + plmTablePos), doorData)) + plmTablePos += 0x08 + else: + #// Overwrite existing door + doorData = [] + for x in door[1:3]: + doorData += getWordArray(x) + doorData += getWordArray(doorArgs) + self.patches.append((Snes(0x8f0000 + door[6]), doorData)) + if((door[3] == KeycardEvents.BrinstarBoss and door[0] != 0x9D9C) or door[3] == KeycardEvents.LowerNorfairBoss or door[3] == KeycardEvents.MaridiaBoss or door[3] == KeycardEvents.WreckedShipBoss): + #// Overwrite the extra parts of the Gadora with a PLM that just deletes itself + self.patches.append((Snes(0x8f0000 + door[6] + 0x06), [ 0x2F, 0xB6, 0x00, 0x00, 0x00, 0x00, 0x2F, 0xB6, 0x00, 0x00, 0x00, 0x00 ])) + + #// Plaque data + if (door[4] != KeycardPlaque.Null): + plaqueData = getWordArray(door[0]) + getWordArray(plaquePlm) + getWordArray(door[5]) + getWordArray(door[4]) + self.patches.append((Snes(0x8f0000 + plmTablePos), plaqueData)) + plmTablePos += 0x08 + doorId += 1 + + #/* Write plaque showing SM bosses that needs to be killed */ + if (self.myWorld.Config.OpenTourian != OpenTourian.FourBosses): + plaqueData = getWordArray(0xA5ED) + getWordArray(plaquePlm) + getWordArray(0x044F) + getWordArray(KeycardPlaque.Zero + self.myWorld.TourianBossTokens) + self.patches.append((Snes(0x8f0000 + plmTablePos), plaqueData)) + plmTablePos += 0x08 self.patches.append((Snes(0x8f0000 + plmTablePos), [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ])) @@ -744,20 +733,32 @@ class Patch: (Snes(0xDD313), [ 0x00, 0x00, 0xE4, 0xFF, 0x08, 0x0E ]), ] - def WriteGanonInvicible(self, invincible: GanonInvincible): + def WritePreOpenPyramid(self, goal: Goal): + if (goal == Goal.FastGanonDefeatMotherBrain): + self.patches.append((Snes(0x30808B), [0x01])) + + def WriteGanonInvicible(self, goal: Goal): #/* Defaults to $00 (never) at [asm]/z3/randomizer/tables.asm */ - invincibleMap = { - GanonInvincible.Never : 0x00, - GanonInvincible.Always : 0x01, - GanonInvincible.BeforeAllDungeons : 0x02, - GanonInvincible.BeforeCrystals : 0x03, - } - value = invincibleMap.get(invincible, None) + valueMap = { + Goal.DefeatBoth : 0x03, + Goal.FastGanonDefeatMotherBrain : 0x04, + Goal.AllDungeonsDefeatMotherBrain : 0x02 + } + value = valueMap.get(goal, None) if (value is None): - raise exception(f"Unknown Ganon invincible value {invincible}") + raise exception(f"Unknown Ganon invincible value {goal}") else: self.patches.append((Snes(0x30803E), [value])) + def WriteBossesNeeded(self, tourianBossTokens): + self.patches.append((Snes(0xF47200), getWordArray(tourianBossTokens))) + + def WriteCrystalsNeeded(self, towerCrystals, ganonCrystals): + self.patches.append((Snes(0x30805E), [towerCrystals])) + self.patches.append((Snes(0x30805F), [ganonCrystals])) + + self.stringTable.SetTowerRequirementText(f"You need {towerCrystals} crystals to enter Ganon's Tower.") + self.stringTable.SetGanonRequirementText(f"You need {ganonCrystals} crystals to defeat Ganon.") def WriteRngBlock(self): #/* Repoint RNG Block */ diff --git a/worlds/smz3/TotalSMZ3/Region.py b/worlds/smz3/TotalSMZ3/Region.py index f352247c80..00e209ce45 100644 --- a/worlds/smz3/TotalSMZ3/Region.py +++ b/worlds/smz3/TotalSMZ3/Region.py @@ -5,12 +5,19 @@ from worlds.smz3.TotalSMZ3.Item import Item, ItemType class RewardType(Enum): Null = 0 - Agahnim = 1 - PendantGreen = 2 - PendantNonGreen = 3 - CrystalBlue = 4 - CrystalRed = 5 - GoldenFourBoss = 6 + Agahnim = 1 << 0 + PendantGreen = 1 << 1 + PendantNonGreen = 1 << 2 + CrystalBlue = 1 << 3 + CrystalRed = 1 << 4 + BossTokenKraid = 1 << 5 + BossTokenPhantoon = 1 << 6 + BossTokenDraygon = 1 << 7 + BossTokenRidley = 1 << 8 + + AnyPendant = PendantGreen | PendantNonGreen + AnyCrystal = CrystalBlue | CrystalRed + AnyBossToken = BossTokenKraid | BossTokenPhantoon | BossTokenDraygon | BossTokenRidley class IReward: Reward: RewardType @@ -18,7 +25,7 @@ class IReward: pass class IMedallionAccess: - Medallion: object + Medallion = None class Region: import worlds.smz3.TotalSMZ3.Location as Location diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Kraid.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Kraid.py index fe3f804da9..2b99081dd3 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Kraid.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Kraid.py @@ -7,7 +7,7 @@ class Kraid(SMRegion, IReward): Name = "Brinstar Kraid" Area = "Brinstar" - Reward = RewardType.GoldenFourBoss + Reward = RewardType.Null def __init__(self, world, config: Config): super().__init__(world, config) diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Pink.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Pink.py index 465f885b11..bb1036fb81 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Pink.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Pink.py @@ -40,5 +40,5 @@ class Pink(SMRegion): else: return items.CanOpenRedDoors() and (items.CanDestroyBombWalls() or items.SpeedBooster) or \ items.CanUsePowerBombs() or \ - items.CanAccessNorfairUpperPortal() and items.Morph and (items.CanOpenRedDoors() or items.Wave) and \ + items.CanAccessNorfairUpperPortal() and items.Morph and (items.Missile or items.Super or items.Wave ) and \ (items.Ice or items.HiJump or items.CanSpringBallJump() or items.CanFly()) diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Crateria/East.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Crateria/East.py index d223fd82a2..72d10a4496 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Crateria/East.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Crateria/East.py @@ -17,9 +17,9 @@ class East(SMRegion): self.world.CanEnter("Wrecked Ship", items)) if self.Logic == SMLogic.Normal else \ lambda items: items.Morph), Location(self, 2, 0x8F81EE, LocationType.Hidden, "Missile (outside Wrecked Ship top)", - lambda items: self.world.CanEnter("Wrecked Ship", items) and (not self.Config.Keysanity or items.CardWreckedShipBoss) and items.CanPassBombPassages()), + lambda items: self.world.CanEnter("Wrecked Ship", items) and items.CardWreckedShipBoss and items.CanPassBombPassages()), Location(self, 3, 0x8F81F4, LocationType.Visible, "Missile (outside Wrecked Ship middle)", - lambda items: self.world.CanEnter("Wrecked Ship", items) and (not self.Config.Keysanity or items.CardWreckedShipBoss) and items.CanPassBombPassages()), + lambda items: self.world.CanEnter("Wrecked Ship", items) and items.CardWreckedShipBoss and items.CanPassBombPassages()), Location(self, 4, 0x8F8248, LocationType.Visible, "Missile (Crateria moat)", lambda items: True) ] diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Maridia/Inner.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Maridia/Inner.py index 280f7e5b28..7de0798bae 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Maridia/Inner.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Maridia/Inner.py @@ -9,20 +9,17 @@ class Inner(SMRegion, IReward): def __init__(self, world, config: Config): super().__init__(world, config) - self.Reward = RewardType.GoldenFourBoss + self.Reward = RewardType.Null self.Locations = [ Location(self, 140, 0x8FC4AF, LocationType.Visible, "Super Missile (yellow Maridia)", - lambda items: items.CardMaridiaL1 and items.CanPassBombPassages() if self.Logic == SMLogic.Normal else \ - lambda items: items.CardMaridiaL1 and items.CanPassBombPassages() and - (items.Gravity or items.Ice or items.HiJump and items.CanSpringBallJump())), + lambda items: items.CardMaridiaL1 and items.CanPassBombPassages() and self.CanReachAqueduct(items) and + (items.Gravity or items.Ice or items.HiJump and items.SpringBall)), Location(self, 141, 0x8FC4B5, LocationType.Visible, "Missile (yellow Maridia super missile)", - lambda items: items.CardMaridiaL1 and items.CanPassBombPassages() if self.Logic == SMLogic.Normal else \ lambda items: items.CardMaridiaL1 and items.CanPassBombPassages() and - (items.Gravity or items.Ice or items.HiJump and items.CanSpringBallJump())), + (items.Gravity or items.Ice or items.HiJump and items.SpringBall)), Location(self, 142, 0x8FC533, LocationType.Visible, "Missile (yellow Maridia false wall)", - lambda items: items.CardMaridiaL1 and items.CanPassBombPassages() if self.Logic == SMLogic.Normal else \ lambda items: items.CardMaridiaL1 and items.CanPassBombPassages() and - (items.Gravity or items.Ice or items.HiJump and items.CanSpringBallJump())), + (items.Gravity or items.Ice or items.HiJump and items.SpringBall)), Location(self, 143, 0x8FC559, LocationType.Chozo, "Plasma Beam", lambda items: self.CanDefeatDraygon(items) and (items.ScrewAttack or items.Plasma) and (items.HiJump or items.CanFly()) if self.Logic == SMLogic.Normal else \ lambda items: self.CanDefeatDraygon(items) and @@ -31,17 +28,17 @@ class Inner(SMRegion, IReward): Location(self, 144, 0x8FC5DD, LocationType.Visible, "Missile (left Maridia sand pit room)", lambda items: self.CanReachAqueduct(items) and items.Super and items.CanPassBombPassages() if self.Logic == SMLogic.Normal else \ lambda items: self.CanReachAqueduct(items) and items.Super and - (items.HiJump and (items.SpaceJump or items.CanSpringBallJump()) or items.Gravity)), + (items.Gravity or items.HiJump and (items.SpaceJump or items.CanSpringBallJump()))), Location(self, 145, 0x8FC5E3, LocationType.Chozo, "Reserve Tank, Maridia", lambda items: self.CanReachAqueduct(items) and items.Super and items.CanPassBombPassages() if self.Logic == SMLogic.Normal else \ lambda items: self.CanReachAqueduct(items) and items.Super and - (items.HiJump and (items.SpaceJump or items.CanSpringBallJump()) or items.Gravity)), + (items.Gravity or items.HiJump and (items.SpaceJump or items.CanSpringBallJump()))), Location(self, 146, 0x8FC5EB, LocationType.Visible, "Missile (right Maridia sand pit room)", lambda items: self.CanReachAqueduct(items) and items.Super) if self.Logic == SMLogic.Normal else \ lambda items: self.CanReachAqueduct(items) and items.Super and (items.HiJump or items.Gravity), Location(self, 147, 0x8FC5F1, LocationType.Visible, "Power Bomb (right Maridia sand pit room)", lambda items: self.CanReachAqueduct(items) and items.Super) if self.Logic == SMLogic.Normal else \ - lambda items: self.CanReachAqueduct(items) and items.Super and (items.HiJump and items.CanSpringBallJump() or items.Gravity), + lambda items: self.CanReachAqueduct(items) and items.Super and (items.Gravity or items.HiJump and items.CanSpringBallJump()), Location(self, 148, 0x8FC603, LocationType.Visible, "Missile (pink Maridia)", lambda items: self.CanReachAqueduct(items) and items.SpeedBooster if self.Logic == SMLogic.Normal else \ lambda items: self.CanReachAqueduct(items) and items.Gravity), diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/East.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/East.py index 0cd577f78d..f1a325a12b 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/East.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/East.py @@ -9,7 +9,7 @@ class East(SMRegion, IReward): def __init__(self, world, config: Config): super().__init__(world, config) - self.Reward = RewardType.GoldenFourBoss + self.Reward = RewardType.Null self.Locations = [ Location(self, 74, 0x8F8FCA, LocationType.Visible, "Missile (lower Norfair above fire flea room)", lambda items: self.CanExit(items)), @@ -30,11 +30,11 @@ class East(SMRegion, IReward): def CanExit(self, items:Progression): if self.Logic == SMLogic.Normal: # /*Bubble Mountain*/ - return items.CardNorfairL2 or ( + return items.Morph and (items.CardNorfairL2 or ( # /* Volcano Room and Blue Gate */ items.Gravity) and items.Wave and ( # /*Spikey Acid Snakes and Croc Escape*/ - items.Grapple or items.SpaceJump) + items.Grapple or items.SpaceJump)) else: # /*Vanilla LN Escape*/ return (items.Morph and (items.CardNorfairL2 or # /*Bubble Mountain*/ diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/West.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/West.py index 8740c545e3..4e44d28ca5 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/West.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/West.py @@ -17,13 +17,13 @@ class West(SMRegion): items.CanAccessNorfairLowerPortal() and (items.CanFly() or items.CanSpringBallJump() or items.SpeedBooster) and items.Super)), Location(self, 71, 0x8F8E74, LocationType.Hidden, "Super Missile (Gold Torizo)", lambda items: items.CanDestroyBombWalls() and (items.Super or items.Charge) and - (items.CanAccessNorfairLowerPortal() or items.SpaceJump and items.CanUsePowerBombs()) if self.Logic == SMLogic.Normal else \ + (items.CanAccessNorfairLowerPortal() or items.CanUsePowerBombs() and items.SpaceJump) if self.Logic == SMLogic.Normal else \ lambda items: items.CanDestroyBombWalls() and items.Varia and (items.Super or items.Charge)), Location(self, 79, 0x8F9110, LocationType.Chozo, "Screw Attack", - lambda items: items.CanDestroyBombWalls() and (items.SpaceJump and items.CanUsePowerBombs() or items.CanAccessNorfairLowerPortal()) if self.Logic == SMLogic.Normal else \ - lambda items: items.CanDestroyBombWalls() and (items.Varia or items.CanAccessNorfairLowerPortal())), + lambda items: items.CanDestroyBombWalls() and (items.CanAccessNorfairLowerPortal() or items.CanUsePowerBombs() and items.SpaceJump) if self.Logic == SMLogic.Normal else \ + lambda items: items.CanDestroyBombWalls() and (items.CanAccessNorfairLowerPortal() or items.Varia)), Location(self, 73, 0x8F8F30, LocationType.Visible, "Missile (Mickey Mouse room)", - lambda items: items.CanFly() and items.Morph and items.Super and ( + lambda items: items.Morph and items.Super and items.CanFly() and items.CanUsePowerBombs() and ( # /*Exit to Upper Norfair*/ (items.CardLowerNorfairL1 or # /*Vanilla or Reverse Lava Dive*/ @@ -33,17 +33,20 @@ class West(SMRegion): # /* Volcano Room and Blue Gate */ items.Gravity and items.Wave and # /*Spikey Acid Snakes and Croc Escape*/ - (items.Grapple or items.SpaceJump) or + (items.Grapple or items.SpaceJump) or # /*Exit via GT fight and Portal*/ - (items.CanUsePowerBombs() and items.SpaceJump and (items.Super or items.Charge))) if self.Logic == SMLogic.Normal else \ + items.CanUsePowerBombs() and items.SpaceJump and (items.Super or items.Charge)) if self.Logic == SMLogic.Normal else \ lambda items: - items.Morph and items.Varia and items.Super and ((items.CanFly() or items.CanSpringBallJump() or - items.SpeedBooster and (items.HiJump and items.CanUsePowerBombs() or items.Charge and items.Ice)) and - # /*Exit to Upper Norfair*/ - (items.CardNorfairL2 or (items.SpeedBooster or items.CanFly() or items.Grapple or items.HiJump and - (items.CanSpringBallJump() or items.Ice))) or - # /*Return to Portal*/ - items.CanUsePowerBombs())) + items.Varia and items.Morph and items.Super and + #/* Climb worst room (from LN East CanEnter) */ + (items.CanFly() or items.HiJump or items.CanSpringBallJump() or items.Ice and items.Charge) and + (items.CanPassBombPassages() or items.ScrewAttack and items.SpaceJump) and ( + #/* Exit to Upper Norfair */ + items.CardNorfairL2 or items.SpeedBooster or items.CanFly() or items.Grapple or + items.HiJump and (items.CanSpringBallJump() or items.Ice) or + #/* Portal (with GGG) */ + items.CanUsePowerBombs() + )) ] # // Todo: account for Croc Speedway once Norfair Upper East also do so, otherwise it would be inconsistent to do so here diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairUpper/Crocomire.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairUpper/Crocomire.py index 914d07c3be..b38bbe70c6 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairUpper/Crocomire.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairUpper/Crocomire.py @@ -45,11 +45,10 @@ class Crocomire(SMRegion): # /* Cathedral -> through the floor or Vulcano */ items.CanOpenRedDoors() and (items.CardNorfairL2 if self.Config.Keysanity else items.Super) and (items.CanFly() or items.HiJump or items.SpeedBooster) and - (items.CanPassBombPassages() or items.Gravity and items.Morph) and items.Wave - or + (items.CanPassBombPassages() or items.Gravity and items.Morph) and items.Wave) or ( # /* Reverse Lava Dive */ - items.CanAccessNorfairLowerPortal() and items.ScrewAttack and items.SpaceJump and items.Super and - items.Gravity and items.Wave and (items.CardNorfairL2 or items.Morph)) + items.Varia) and items.CanAccessNorfairLowerPortal() and items.ScrewAttack and items.SpaceJump and items.Super and ( + items.Gravity) and items.Wave and (items.CardNorfairL2 or items.Morph) else: return ((items.CanDestroyBombWalls() or items.SpeedBooster) and items.Super and items.Morph or items.CanAccessNorfairUpperPortal()) and ( # /* Ice Beam -> Croc Speedway */ @@ -65,5 +64,5 @@ class Crocomire(SMRegion): (items.Missile or items.Super or items.Wave) # /* Blue Gate */ ) or ( # /* Reverse Lava Dive */ - items.CanAccessNorfairLowerPortal()) and items.ScrewAttack and items.SpaceJump and items.Varia and items.Super and ( + items.Varia and items.CanAccessNorfairLowerPortal()) and items.ScrewAttack and items.SpaceJump and items.Super and ( items.HasEnergyReserves(2)) and (items.CardNorfairL2 or items.Morph) diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/WreckedShip.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/WreckedShip.py index 053de3d1a6..e83c6f539c 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/WreckedShip.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/WreckedShip.py @@ -9,12 +9,13 @@ class WreckedShip(SMRegion, IReward): def __init__(self, world, config: Config): super().__init__(world, config) - self.Reward = RewardType.GoldenFourBoss + self.Weight = 4 + self.Reward = RewardType.Null self.Locations = [ Location(self, 128, 0x8FC265, LocationType.Visible, "Missile (Wrecked Ship middle)", lambda items: items.CanPassBombPassages()), Location(self, 129, 0x8FC2E9, LocationType.Chozo, "Reserve Tank, Wrecked Ship", - lambda items: self.CanUnlockShip(items) and items.CardWreckedShipL1 and items.SpeedBooster and items.CanUsePowerBombs() and + lambda items: self.CanUnlockShip(items) and items.CardWreckedShipL1 and items.CanUsePowerBombs() and items.SpeedBooster and (items.Grapple or items.SpaceJump or items.Varia and items.HasEnergyReserves(2) or items.HasEnergyReserves(3)) if self.Logic == SMLogic.Normal else \ lambda items: self.CanUnlockShip(items) and items.CardWreckedShipL1 and items.CanUsePowerBombs() and items.SpeedBooster and (items.Varia or items.HasEnergyReserves(2))), @@ -27,7 +28,7 @@ class WreckedShip(SMRegion, IReward): Location(self, 132, 0x8FC337, LocationType.Visible, "Energy Tank, Wrecked Ship", lambda items: self.CanUnlockShip(items) and (items.HiJump or items.SpaceJump or items.SpeedBooster or items.Gravity) if self.Logic == SMLogic.Normal else \ - lambda items: self.CanUnlockShip(items) and (items.Bombs or items.PowerBomb or items.CanSpringBallJump() or + lambda items: self.CanUnlockShip(items) and (items.Morph and (items.Bombs or items.PowerBomb) or items.CanSpringBallJump() or items.HiJump or items.SpaceJump or items.SpeedBooster or items.Gravity)), Location(self, 133, 0x8FC357, LocationType.Visible, "Super Missile (Wrecked Ship left)", lambda items: self.CanUnlockShip(items)), diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthEast.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthEast.py index 5bc581c6d4..7ca34cb031 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthEast.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthEast.py @@ -14,15 +14,15 @@ class NorthEast(Z3Region): lambda items: items.MoonPearl and items.CanLiftLight()), Location(self, 256+79, 0x308147, LocationType.Regular, "Pyramid"), Location(self, 256+80, 0x1E980, LocationType.Regular, "Pyramid Fairy - Left", - lambda items: self.world.CanAquireAll(items, RewardType.CrystalRed) and items.MoonPearl and self.world.CanEnter("Dark World South", items) and - (items.Hammer or items.Mirror and self.world.CanAquire(items, RewardType.Agahnim))), + lambda items: self.world.CanAcquireAll(items, RewardType.CrystalRed) and items.MoonPearl and self.world.CanEnter("Dark World South", items) and + (items.Hammer or items.Mirror and self.world.CanAcquire(items, RewardType.Agahnim))), Location(self, 256+81, 0x1E983, LocationType.Regular, "Pyramid Fairy - Right", - lambda items: self.world.CanAquireAll(items, RewardType.CrystalRed) and items.MoonPearl and self.world.CanEnter("Dark World South", items) and - (items.Hammer or items.Mirror and self.world.CanAquire(items, RewardType.Agahnim))) + lambda items: self.world.CanAcquireAll(items, RewardType.CrystalRed) and items.MoonPearl and self.world.CanEnter("Dark World South", items) and + (items.Hammer or items.Mirror and self.world.CanAcquire(items, RewardType.Agahnim))) ] def CanEnter(self, items: Progression): - return self.world.CanAquire(items, RewardType.Agahnim) or items.MoonPearl and ( + return self.world.CanAcquire(items, RewardType.Agahnim) or items.MoonPearl and ( items.Hammer and items.CanLiftLight() or items.CanLiftHeavy() and items.Flippers or items.CanAccessDarkWorldPortal(self.Config) and items.Flippers) diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthWest.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthWest.py index 57b5ece194..28a318e80d 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthWest.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthWest.py @@ -25,7 +25,7 @@ class NorthWest(Z3Region): def CanEnter(self, items: Progression): return items.MoonPearl and (( - self.world.CanAquire(items, RewardType.Agahnim) or + self.world.CanAcquire(items, RewardType.Agahnim) or items.CanAccessDarkWorldPortal(self.Config) and items.Flippers ) and items.Hookshot and (items.Flippers or items.CanLiftLight() or items.Hammer) or items.Hammer and items.CanLiftLight() or diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/South.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/South.py index f43cb8886b..14f4515c6d 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/South.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/South.py @@ -21,7 +21,7 @@ class South(Z3Region): def CanEnter(self, items: Progression): return items.MoonPearl and (( - self.world.CanAquire(items, RewardType.Agahnim) or + self.world.CanAcquire(items, RewardType.Agahnim) or items.CanAccessDarkWorldPortal(self.Config) and items.Flippers ) and (items.Hammer or items.Hookshot and (items.Flippers or items.CanLiftLight())) or items.Hammer and items.CanLiftLight() or diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/GanonsTower.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/GanonsTower.py index 68fbd3b8db..0f21d7e284 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/GanonsTower.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/GanonsTower.py @@ -34,33 +34,33 @@ class GanonsTower(Z3Region): self.GetLocation("Ganon's Tower - Randomizer Room - Bottom Left"), self.GetLocation("Ganon's Tower - Randomizer Room - Bottom Right") ]) or self.GetLocation("Ganon's Tower - Firesnake Room").ItemIs(ItemType.KeyGT, self.world) else 3)), - Location(self, 256+196, 0x1EAC4, LocationType.Regular, "Ganon's Tower - Randomizer Room - Top Left", + Location(self, 256+230, 0x1EAC4, LocationType.Regular, "Ganon's Tower - Randomizer Room - Top Left", lambda items: self.LeftSide(items, [ self.GetLocation("Ganon's Tower - Randomizer Room - Top Right"), self.GetLocation("Ganon's Tower - Randomizer Room - Bottom Left"), self.GetLocation("Ganon's Tower - Randomizer Room - Bottom Right") ])), - Location(self, 256+197, 0x1EAC7, LocationType.Regular, "Ganon's Tower - Randomizer Room - Top Right", + Location(self, 256+231, 0x1EAC7, LocationType.Regular, "Ganon's Tower - Randomizer Room - Top Right", lambda items: self.LeftSide(items, [ self.GetLocation("Ganon's Tower - Randomizer Room - Top Left"), self.GetLocation("Ganon's Tower - Randomizer Room - Bottom Left"), self.GetLocation("Ganon's Tower - Randomizer Room - Bottom Right") ])), - Location(self, 256+198, 0x1EACA, LocationType.Regular, "Ganon's Tower - Randomizer Room - Bottom Left", + Location(self, 256+232, 0x1EACA, LocationType.Regular, "Ganon's Tower - Randomizer Room - Bottom Left", lambda items: self.LeftSide(items, [ self.GetLocation("Ganon's Tower - Randomizer Room - Top Right"), self.GetLocation("Ganon's Tower - Randomizer Room - Top Left"), self.GetLocation("Ganon's Tower - Randomizer Room - Bottom Right") ])), - Location(self, 256+199, 0x1EACD, LocationType.Regular, "Ganon's Tower - Randomizer Room - Bottom Right", + Location(self, 256+233, 0x1EACD, LocationType.Regular, "Ganon's Tower - Randomizer Room - Bottom Right", lambda items: self.LeftSide(items, [ self.GetLocation("Ganon's Tower - Randomizer Room - Top Right"), self.GetLocation("Ganon's Tower - Randomizer Room - Top Left"), self.GetLocation("Ganon's Tower - Randomizer Room - Bottom Left") ])), - Location(self, 256+200, 0x1EAD9, LocationType.Regular, "Ganon's Tower - Hope Room - Left"), - Location(self, 256+201, 0x1EADC, LocationType.Regular, "Ganon's Tower - Hope Room - Right"), - Location(self, 256+202, 0x1EAE2, LocationType.Regular, "Ganon's Tower - Tile Room", + Location(self, 256+234, 0x1EAD9, LocationType.Regular, "Ganon's Tower - Hope Room - Left"), + Location(self, 256+235, 0x1EADC, LocationType.Regular, "Ganon's Tower - Hope Room - Right"), + Location(self, 256+236, 0x1EAE2, LocationType.Regular, "Ganon's Tower - Tile Room", lambda items: items.Somaria), Location(self, 256+203, 0x1EAE5, LocationType.Regular, "Ganon's Tower - Compass Room - Top Left", lambda items: self.RightSide(items, [ @@ -118,8 +118,9 @@ class GanonsTower(Z3Region): return items.Somaria and items.Firerod and items.KeyGT >= (3 if any(l.ItemIs(ItemType.BigKeyGT, self.world) for l in locations) else 4) def BigKeyRoom(self, items: Progression): - return items.KeyGT >= 3 and self.CanBeatArmos(items) \ - and (items.Hammer and items.Hookshot or items.Firerod and items.Somaria) + return items.KeyGT >= 3 and \ + (items.Hammer and items.Hookshot or items.Firerod and items.Somaria) \ + and self.CanBeatArmos(items) def TowerAscend(self, items: Progression): return items.BigKeyGT and items.KeyGT >= 3 and items.Bow and items.CanLightTorches() @@ -134,13 +135,14 @@ class GanonsTower(Z3Region): def CanEnter(self, items: Progression): return items.MoonPearl and self.world.CanEnter("Dark World Death Mountain East", items) and \ - self.world.CanAquireAll(items, RewardType.CrystalBlue, RewardType.CrystalRed, RewardType.GoldenFourBoss) + self.world.CanAcquireAtLeast(self.world.TowerCrystals, items, RewardType.AnyCrystal) and \ + self.world.CanAcquireAtLeast(self.world.TourianBossTokens * (self.world.TowerCrystals / 7), items, RewardType.AnyBossToken) def CanFill(self, item: Item): - if (self.Config.GameMode == GameMode.Multiworld): + if (self.Config.Multiworld): if (item.World != self.world or item.Progression): return False - if (self.Config.KeyShuffle == KeyShuffle.Keysanity and not ((item.Type == ItemType.BigKeyGT or item.Type == ItemType.KeyGT) and item.World == self.world) and (item.IsKey() or item.IsBigKey() or item.IsKeycard())): + if (self.Config.Keysanity and not ((item.Type == ItemType.BigKeyGT or item.Type == ItemType.KeyGT) and item.World == self.world) and (item.IsKey() or item.IsBigKey() or item.IsKeycard())): return False return super().CanFill(item) diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/IcePalace.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/IcePalace.py index 6543017c6f..9b16a08b4d 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/IcePalace.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/IcePalace.py @@ -10,6 +10,7 @@ class IcePalace(Z3Region, IReward): def __init__(self, world, config: Config): super().__init__(world, config) + self.Weight = 4 self.RegionItems = [ ItemType.KeyIP, ItemType.BigKeyIP, ItemType.MapIP, ItemType.CompassIP] self.Reward = RewardType.Null self.Locations = [ @@ -43,7 +44,7 @@ class IcePalace(Z3Region, IReward): ] def CanNotWasteKeysBeforeAccessible(self, items: Progression, locations: List[Location]): - return not items.BigKeyIP or any(l.ItemIs(ItemType.BigKeyIP, self.world) for l in locations) + return self.world.ForwardSearch or not items.BigKeyIP or any(l.ItemIs(ItemType.BigKeyIP, self.world) for l in locations) def CanEnter(self, items: Progression): return items.MoonPearl and items.Flippers and items.CanLiftHeavy() and items.CanMeltFreezors() diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthEast.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthEast.py index 0edf93f302..c111b07dfd 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthEast.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthEast.py @@ -24,5 +24,5 @@ class NorthEast(Z3Region): Location(self, 256+42, 0x1EA85, LocationType.Regular, "Sahasrahla's Hut - Middle").Weighted(sphereOne), Location(self, 256+43, 0x1EA88, LocationType.Regular, "Sahasrahla's Hut - Right").Weighted(sphereOne), Location(self, 256+44, 0x5F1FC, LocationType.Regular, "Sahasrahla", - lambda items: self.world.CanAquire(items, RewardType.PendantGreen)) + lambda items: self.world.CanAcquire(items, RewardType.PendantGreen)) ] diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthWest.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthWest.py index f35cbad33e..46f830dc8b 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthWest.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthWest.py @@ -11,11 +11,11 @@ class NorthWest(Z3Region): sphereOne = -14 self.Locations = [ Location(self, 256+14, 0x589B0, LocationType.Pedestal, "Master Sword Pedestal", - lambda items: self.world.CanAquireAll(items, RewardType.PendantGreen, RewardType.PendantNonGreen)), + lambda items: self.world.CanAcquireAll(items, RewardType.AnyPendant)), Location(self, 256+15, 0x308013, LocationType.Regular, "Mushroom").Weighted(sphereOne), Location(self, 256+16, 0x308000, LocationType.Regular, "Lost Woods Hideout").Weighted(sphereOne), Location(self, 256+17, 0x308001, LocationType.Regular, "Lumberjack Tree", - lambda items: self.world.CanAquire(items, RewardType.Agahnim) and items.Boots), + lambda items: self.world.CanAcquire(items, RewardType.Agahnim) and items.Boots), Location(self, 256+18, 0x1EB3F, LocationType.Regular, "Pegasus Rocks", lambda items: items.Boots), Location(self, 256+19, 0x308004, LocationType.Regular, "Graveyard Ledge", diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/MiseryMire.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/MiseryMire.py index 855c326f23..b1746184d3 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/MiseryMire.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/MiseryMire.py @@ -10,9 +10,10 @@ class MiseryMire(Z3Region, IReward, IMedallionAccess): def __init__(self, world, config: Config): super().__init__(world, config) + self.Weight = 2 self.RegionItems = [ ItemType.KeyMM, ItemType.BigKeyMM, ItemType.MapMM, ItemType.CompassMM] self.Reward = RewardType.Null - self.Medallion = ItemType.Nothing + self.Medallion = None self.Locations = [ Location(self, 256+169, 0x1EA5E, LocationType.Regular, "Misery Mire - Main Lobby", lambda items: items.BigKeyMM or items.KeyMM >= 1), @@ -34,8 +35,9 @@ class MiseryMire(Z3Region, IReward, IMedallionAccess): # // Need "CanKillManyEnemies" if implementing swordless def CanEnter(self, items: Progression): - return (items.Bombos if self.Medallion == ItemType.Bombos else ( - items.Ether if self.Medallion == ItemType.Ether else items.Quake)) and items.Sword and \ + from worlds.smz3.TotalSMZ3.WorldState import Medallion + return (items.Bombos if self.Medallion == Medallion.Bombos else ( + items.Ether if self.Medallion == Medallion.Ether else items.Quake)) and items.Sword and \ items.MoonPearl and (items.Boots or items.Hookshot) and \ self.world.CanEnter("Dark World Mire", items) diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py index 1805e74dca..27b5a1db43 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py @@ -10,6 +10,7 @@ class SwampPalace(Z3Region, IReward): def __init__(self, world, config: Config): super().__init__(world, config) + self.Weight = 3 self.RegionItems = [ ItemType.KeySP, ItemType.BigKeySP, ItemType.MapSP, ItemType.CompassSP] self.Reward = RewardType.Null self.Locations = [ diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/TurtleRock.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/TurtleRock.py index c4d19bcda1..45546e9e99 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/TurtleRock.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/TurtleRock.py @@ -10,9 +10,10 @@ class TurtleRock(Z3Region, IReward, IMedallionAccess): def __init__(self, world, config: Config): super().__init__(world, config) + self.Weight = 6 self.RegionItems = [ ItemType.KeyTR, ItemType.BigKeyTR, ItemType.MapTR, ItemType.CompassTR] self.Reward = RewardType.Null - self.Medallion = ItemType.Nothing + self.Medallion = None self.Locations = [ Location(self, 256+177, 0x1EA22, LocationType.Regular, "Turtle Rock - Compass Chest"), Location(self, 256+178, 0x1EA1C, LocationType.Regular, "Turtle Rock - Roller Room - Left", @@ -46,8 +47,9 @@ class TurtleRock(Z3Region, IReward, IMedallionAccess): return items.Firerod and items.Icerod def CanEnter(self, items: Progression): - return (items.Bombos if self.Medallion == ItemType.Bombos else ( - items.Ether if self.Medallion == ItemType.Ether else items.Quake)) and items.Sword and \ + from worlds.smz3.TotalSMZ3.WorldState import Medallion + return (items.Bombos if self.Medallion == Medallion.Bombos else ( + items.Ether if self.Medallion == Medallion.Ether else items.Quake)) and items.Sword and \ items.MoonPearl and items.CanLiftHeavy() and items.Hammer and items.Somaria and \ self.world.CanEnter("Light World Death Mountain East", items) diff --git a/worlds/smz3/TotalSMZ3/Text/Dialog.py b/worlds/smz3/TotalSMZ3/Text/Dialog.py index d465e1e201..92e034af67 100644 --- a/worlds/smz3/TotalSMZ3/Text/Dialog.py +++ b/worlds/smz3/TotalSMZ3/Text/Dialog.py @@ -4,9 +4,7 @@ class Dialog: command = re.compile(r"^\{[^}]*\}") invalid = re.compile(r"(?[0-9])|(?P[A-Z])|(?P[a-z])") @staticmethod def Simple(text: str): @@ -29,19 +27,16 @@ class Dialog: lineIndex += 1 - if (lineIndex % 3 == 0 and lineIndex < len(lines)): - bytes.append(0x7E) - if (lineIndex >= 3 and lineIndex < len(lines)): - bytes.append(0x73) + if (lineIndex < len(lines)): + if (lineIndex % 3 == 0): + bytes.append(0x7E) # pause for input + if (lineIndex >= 3): + bytes.append(0x73) # scroll - bytes.append(0x7F) - if (len(bytes) > maxBytes): - return bytes[:maxBytes - 1].append(0x7F) - - return bytes + return bytes[:maxBytes - 1].append(0x7F) @staticmethod - def Compiled(text: str, pause = True): + def Compiled(text: str): maxBytes = 2046 wrap = 19 @@ -49,6 +44,7 @@ class Dialog: raise Exception("Dialog commands must be placed on separate lines", text) padOut = False + pause = True bytes = [ 0xFB ] lines = Dialog.Wordwrap(text, wrap) @@ -61,33 +57,11 @@ class Dialog: return [ 0xFB, 0xFE, 0x6E, 0x00, 0xFE, 0x6B, 0x04 ] if (match.string == "{INTRO}"): padOut = True + if (match.string == "{NOPAUSE}"): + pause = False + continue - bytesMap = { - "{SPEED0}" : [ 0xFC, 0x00 ], - "{SPEED2}" : [ 0xFC, 0x02 ], - "{SPEED6}" : [ 0xFC, 0x06 ], - "{PAUSE1}" : [ 0xFE, 0x78, 0x01 ], - "{PAUSE3}" : [ 0xFE, 0x78, 0x03 ], - "{PAUSE5}" : [ 0xFE, 0x78, 0x05 ], - "{PAUSE7}" : [ 0xFE, 0x78, 0x07 ], - "{PAUSE9}" : [ 0xFE, 0x78, 0x09 ], - "{INPUT}" : [ 0xFA ], - "{CHOICE}" : [ 0xFE, 0x68 ], - "{ITEMSELECT}" : [ 0xFE, 0x69 ], - "{CHOICE2}" : [ 0xFE, 0x71 ], - "{CHOICE3}" : [ 0xFE, 0x72 ], - "{C:GREEN}" : [ 0xFE, 0x77, 0x07 ], - "{C:YELLOW}" : [ 0xFE, 0x77, 0x02 ], - "{HARP}" : [ 0xFE, 0x79, 0x2D ], - "{MENU}" : [ 0xFE, 0x6D, 0x00 ], - "{BOTTOM}" : [ 0xFE, 0x6D, 0x01 ], - "{NOBORDER}" : [ 0xFE, 0x6B, 0x02 ], - "{CHANGEPIC}" : [ 0xFE, 0x67, 0xFE, 0x67 ], - "{CHANGEMUSIC}" : [ 0xFE, 0x67 ], - "{INTRO}" : [ 0xFE, 0x6E, 0x00, 0xFE, 0x77, 0x07, 0xFC, 0x03, 0xFE, 0x6B, 0x02, 0xFE, 0x67 ], - "{IBOX}" : [ 0xFE, 0x6B, 0x02, 0xFE, 0x77, 0x07, 0xFC, 0x03, 0xF7 ], - } - result = bytesMap.get(match.string, None) + result = Dialog.CommandBytesFor(match.string) if (result is None): raise Exception(f"Dialog text contained unknown command {match.string}", text) else: @@ -98,12 +72,10 @@ class Dialog: continue - if (lineIndex == 1): - bytes.append(0xF8); #// row 2 - elif (lineIndex >= 3 and lineIndex < lineCount): - bytes.append(0xF6); #// scroll - elif (lineIndex >= 2): - bytes.append(0xF9); #// row 3 + if (lineIndex > 0): + bytes.append(0xF8 if lineIndex == 1 else #// row 2 + 0xF9 if lineIndex == 2 else #// row 3 + 0xF6) #// scroll #// The first box needs to fill the full width with spaces as the palette is loaded weird. letters = line + (" " * wrap) if padOut and lineIndex < 3 else line @@ -113,10 +85,39 @@ class Dialog: lineIndex += 1 if (pause and lineIndex % 3 == 0 and lineIndex < lineCount): - bytes.append(0xFA) #// wait for input + bytes.append(0xFA) #// pause for input return bytes[:maxBytes] + @staticmethod + def CommandBytesFor(text: str): + bytesMap = { + "{SPEED0}" : [ 0xFC, 0x00 ], + "{SPEED2}" : [ 0xFC, 0x02 ], + "{SPEED6}" : [ 0xFC, 0x06 ], + "{PAUSE1}" : [ 0xFE, 0x78, 0x01 ], + "{PAUSE3}" : [ 0xFE, 0x78, 0x03 ], + "{PAUSE5}" : [ 0xFE, 0x78, 0x05 ], + "{PAUSE7}" : [ 0xFE, 0x78, 0x07 ], + "{PAUSE9}" : [ 0xFE, 0x78, 0x09 ], + "{INPUT}" : [ 0xFA ], + "{CHOICE}" : [ 0xFE, 0x68 ], + "{ITEMSELECT}" : [ 0xFE, 0x69 ], + "{CHOICE2}" : [ 0xFE, 0x71 ], + "{CHOICE3}" : [ 0xFE, 0x72 ], + "{C:GREEN}" : [ 0xFE, 0x77, 0x07 ], + "{C:YELLOW}" : [ 0xFE, 0x77, 0x02 ], + "{HARP}" : [ 0xFE, 0x79, 0x2D ], + "{MENU}" : [ 0xFE, 0x6D, 0x00 ], + "{BOTTOM}" : [ 0xFE, 0x6D, 0x01 ], + "{NOBORDER}" : [ 0xFE, 0x6B, 0x02 ], + "{CHANGEPIC}" : [ 0xFE, 0x67, 0xFE, 0x67 ], + "{CHANGEMUSIC}" : [ 0xFE, 0x67 ], + "{INTRO}" : [ 0xFE, 0x6E, 0x00, 0xFE, 0x77, 0x07, 0xFC, 0x03, 0xFE, 0x6B, 0x02, 0xFE, 0x67 ], + "{IBOX}" : [ 0xFE, 0x6B, 0x02, 0xFE, 0x77, 0x07, 0xFC, 0x03, 0xF7 ], + } + return bytesMap.get(text, None) + @staticmethod def Wordwrap(text: str, width: int): result = [] @@ -146,9 +147,13 @@ class Dialog: @staticmethod def LetterToBytes(c: str): - if Dialog.digit.match(c): return [(ord(c) - ord('0') + 0xA0) ] - elif Dialog.uppercaseLetter.match(c): return [ (ord(c) - ord('A') + 0xAA) ] - elif Dialog.lowercaseLetter.match(c): return [ (ord(c) - ord('a') + 0x30) ] + match = Dialog.character.match(c) + if match is None: + value = Dialog.letters.get(c, None) + return value if value else [ 0xFF ] + elif match.group("digit") != None: return [(ord(c) - ord('0') + 0xA0) ] + elif match.group("upper") != None: return [ (ord(c) - ord('A') + 0xAA) ] + elif match.group("lower") != None: return [ (ord(c) - ord('a') + 0x30) ] else: value = Dialog.letters.get(c, None) return value if value else [ 0xFF ] diff --git a/worlds/smz3/TotalSMZ3/Text/Scripts/General.yaml b/worlds/smz3/TotalSMZ3/Text/Scripts/General.yaml index c0eae5bbbe..065e7a9e93 100644 --- a/worlds/smz3/TotalSMZ3/Text/Scripts/General.yaml +++ b/worlds/smz3/TotalSMZ3/Text/Scripts/General.yaml @@ -377,9 +377,80 @@ Items: THE GREEN BOOMERANG IS THE FASTEST! - Keycard: |- - A key from - the future? + + CardCrateriaL1: |- + An Alien Key! + It says On top + of the world! + CardCrateriaL2: |- + An Alien Key! + It says Lower + the drawbridge + CardCrateriaBoss: |- + An Alien Key! + It says The First + and The Last + CardBrinstarL1: |- + An Alien Key! + It says But wait + there's more! + CardBrinstarL2: |- + An Alien Key! + It says + Green Monkeys + CardBrinstarBoss: |- + An Alien Key! + It says + Metroid DLC + CardNorfairL1: |- + An Alien Key! + It says ice? + In this heat? + CardNorfairL2: |- + An Alien Key! + It says + THE BUBBLES! + CardNorfairBoss: |- + An Alien Key! + It says + Place your bets + CardMaridiaL1: |- + An Alien Key! + It says A + Day at the Beach + CardMaridiaL2: |- + An Alien Key! + It says + That's a Moray + CardMaridiaBoss: |- + An Alien Key! + It says Shrimp + for dinner? + CardWreckedShipL1: |- + An Alien Key! + It says + Gutter Ball + CardWreckedShipBoss: |- + An Alien Key! + It says The + Ghost of Arrghus + CardLowerNorfairL1: |- + An Alien Key! + It says Worst + Key in the Game + CardLowerNorfairBoss: |- + An Alien Key! + It says + This guy again? + + SmMap: |- + You can now + find your way + to the stars! + + Something: |- + A small victory! + default: |- Don't waste your time! diff --git a/worlds/smz3/TotalSMZ3/Text/Scripts/StringTable.yaml b/worlds/smz3/TotalSMZ3/Text/Scripts/StringTable.yaml index d1e5b9c364..918ef3f63d 100644 --- a/worlds/smz3/TotalSMZ3/Text/Scripts/StringTable.yaml +++ b/worlds/smz3/TotalSMZ3/Text/Scripts/StringTable.yaml @@ -4,8 +4,8 @@ # The order of the dialog entries is significant - set_cursor: [0xFB, 0xFC, 0x00, 0xF9, 0xFF, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xE4, 0xFE, 0x68] - set_cursor2: [0xFB, 0xFC, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, 0xF9, 0xFF, 0xFF, 0xE4, 0xFE, 0x68] -- game_over_menu: { NoPause: "{SPEED0}\nSave and Continue\nSave and Quit\nContinue" } -- var_test: { NoPause: "0= ᚋ, 1= ᚌ\n2= ᚍ, 3= ᚎ" } +- game_over_menu: "{NOPAUSE}\n{SPEED0}\nSave and Continue\nSave and Quit\nContinue" +- var_test: "{NOPAUSE}\n0= ᚋ, 1= ᚌ\n2= ᚍ, 3= ᚎ" - follower_no_enter: "Can't you take me some place nice." - choice_1_3: [0xFB, 0xFC, 0x00, 0xF7, 0xE4, 0xF8, 0xFF, 0xF9, 0xFF, 0xFE, 0x71] - choice_2_3: [0xFB, 0xFC, 0x00, 0xF7, 0xFF, 0xF8, 0xE4, 0xF9, 0xFF, 0xFE, 0x71] @@ -290,10 +290,10 @@ # $110 - magic_bat_wake: "You bum! I was sleeping! Where's my magic bolts?" - magic_bat_give_half_magic: "How you like me now?" -- intro_main: { NoPause: "{INTRO}\n Episode III\n{PAUSE3}\n A Link to\n the Past\n{PAUSE3}\n Randomizer\n{PAUSE3}\nAfter mostly disregarding what happened in the first two games.\n{PAUSE3}\nLink awakens to his uncle leaving the house.\n{PAUSE3}\nHe just runs out the door,\n{PAUSE3}\ninto the rainy night.\n{PAUSE3}\n{CHANGEPIC}\nGanon has moved around all the items in Hyrule.\n{PAUSE7}\nYou will have to find all the items necessary to beat Ganon.\n{PAUSE7}\nThis is your chance to be a hero.\n{PAUSE3}\n{CHANGEPIC}\nYou must get the 7 crystals to beat Ganon.\n{PAUSE9}\n{CHANGEPIC}" } -- intro_throne_room: { NoPause: "{IBOX}\nLook at this Stalfos on the throne." } -- intro_zelda_cell: { NoPause: "{IBOX}\nIt is your time to shine!" } -- intro_agahnim: { NoPause: "{IBOX}\nAlso, you need to defeat this guy!" } +- intro_main: "{NOPAUSE}\n{INTRO}\n Episode III\n{PAUSE3}\n A Link to\n the Past\n{PAUSE3}\n Randomizer\n{PAUSE3}\nAfter mostly disregarding what happened in the first two games.\n{PAUSE3}\nLink awakens to his uncle leaving the house.\n{PAUSE3}\nHe just runs out the door,\n{PAUSE3}\ninto the rainy night.\n{PAUSE3}\n{CHANGEPIC}\nGanon has moved around all the items in Hyrule.\n{PAUSE7}\nYou will have to find all the items necessary to beat Ganon.\n{PAUSE7}\nThis is your chance to be a hero.\n{PAUSE3}\n{CHANGEPIC}\nYou must get the 7 crystals to beat Ganon.\n{PAUSE9}\n{CHANGEPIC}" +- intro_throne_room: "{NOPAUSE}\n{IBOX}\nLook at this Stalfos on the throne." +- intro_zelda_cell: "{NOPAUSE}\n{IBOX}\nIt is your time to shine!" +- intro_agahnim: "{NOPAUSE}\n{IBOX}\nAlso, you need to defeat this guy!" - pickup_purple_chest: "A curious box. Let's take it with us!" - bomb_shop: "30 bombs for 100 rupees. Good deals all day!" - bomb_shop_big_bomb: "30 bombs for 100 rupees, 100 rupees 1 BIG bomb. Good deals all day!" @@ -341,7 +341,6 @@ # $140 - agahnim_defeated: "Arrrgggghhh. Well you're coming with me!" - agahnim_final_meeting: "You have done well to come this far. Now, die!" -# $142 - zora_meeting: "What do you want?\n ≥ Flippers\n _Nothin'\n{CHOICE}" - zora_tells_cost: "Fine! But they aren't cheap. You got 500 rupees?\n ≥ Duh\n _Oh carp\n{CHOICE}" - zora_get_flippers: "Here's some Flippers for you! Swim little fish, swim." @@ -396,14 +395,12 @@ - lost_woods_thief: "Have you seen Andy?\n\nHe was out looking for our prized Ether medallion.\nI wonder when he will be back?" - blinds_hut_dude: "I'm just some dude. This is Blind's hut." - end_triforce: "{SPEED2}\n{MENU}\n{NOBORDER}\n G G" -# $174 - toppi_fallen: "Ouch!\n\nYou Jerk!" - kakariko_tavern_fisherman: "Don't argue\nwith a frozen\nDeadrock.\nHe'll never\nchange his\nposition!" - thief_money: "It's a secret to everyone." - thief_desert_rupee_cave: "So you, like, busted down my door, and are being a jerk by talking to me? Normally I would be angry and make you pay for it, but I bet you're just going to break all my pots and steal my 50 rupees." - thief_ice_rupee_cave: "I'm a rupee pot farmer. One day I will take over the world with my skillz. Have you met my brother in the desert? He's way richer than I am." - telepathic_tile_south_east_darkworld_cave: "~~ Dev cave ~~\n No farming\n required" -# $17A - cukeman: "Did you hear that Veetorp beat ajneb174 in a 1 on 1 race at AGDQ?" - cukeman_2: "You found Shabadoo, huh?\nNiiiiice." - potion_shop_no_cash: "Yo! I'm not running a charity here." @@ -415,19 +412,25 @@ - game_chest_lost_woods: "Pay 100 rupees, open 1 chest. Are you lucky?\nSo, Play game?\n ≥ Play\n Never!\n{CHOICE}" - kakariko_flophouse_man_no_flippers: "I sure do have a lot of beds.\n\nZora is a cheapskate and will try to sell you his trash for 500 rupees…" - kakariko_flophouse_man: "I sure do have a lot of beds.\n\nDid you know if you played the flute in the center of town things could happen?" -- menu_start_2: { NoPause: "{MENU}\n{SPEED0}\n≥£'s House\n_Sanctuary\n{CHOICE3}" } -- menu_start_3: { NoPause: "{MENU}\n{SPEED0}\n≥£'s House\n_Sanctuary\n_Mountain Cave\n{CHOICE2}" } -- menu_pause: { NoPause: "{SPEED0}\n≥Continue Game\n_Save and Quit\n{CHOICE3}" } +- menu_start_2: "{NOPAUSE}\n{MENU}\n{SPEED0}\n≥£'s House\n_Sanctuary\n{CHOICE3}" +- menu_start_3: "{NOPAUSE}\n{MENU}\n{SPEED0}\n≥£'s House\n_Sanctuary\n_Mountain Cave\n{CHOICE2}" +- menu_pause: "{NOPAUSE}\n{SPEED0}\n≥Continue Game\n_Save and Quit\n{CHOICE3}" - game_digging_choice: "Have 80 Rupees? Want to play digging game?\n ≥Yes\n _No\n{CHOICE}" - game_digging_start: "Okay, use the shovel with Y!" - game_digging_no_cash: "Shovel rental is 80 rupees.\nI have all day" - game_digging_end_time: "Time's up!\nTime for you to go." - game_digging_come_back_later: "Come back later, I have to bury things." - game_digging_no_follower: "Something is following you. I don't like." -- menu_start_4: { NoPause: "{MENU}\n{SPEED0}\n≥£'s House\n_Mountain Cave\n{CHOICE3}" } +- menu_start_4: "{NOPAUSE}\n{MENU}\n{SPEED0}\n≥£'s House\n_Mountain Cave\n{CHOICE3}" - ganon_fall_in_alt: "You think you\nare ready to\nface me?\n\nI will not die\n\nunless you\ncomplete your\ngoals. Dingus!" - ganon_phase_3_alt: "Got wax in your ears? I cannot die!" # $190 - sign_east_death_mountain_bridge: "How did you get up here?" - fish_money: "It's a secret to everyone." +- sign_ganons_tower: "You need all 7 crystals to enter." +- sign_ganon: "You need all 7 crystals to beat Ganon." +- ganon_phase_3_no_bow: "You have no bow. Dingus!" +- ganon_phase_3_no_silvers_alt: "You can't best me without silver arrows!" +- ganon_phase_3_no_silvers: "You can't best me without silver arrows!" +- ganon_phase_3_silvers: "Oh no! Silver! My one true weakness!" - end_pad_data: "" diff --git a/worlds/smz3/TotalSMZ3/Text/StringTable.py b/worlds/smz3/TotalSMZ3/Text/StringTable.py index 4c1986993a..13f3f5edb5 100644 --- a/worlds/smz3/TotalSMZ3/Text/StringTable.py +++ b/worlds/smz3/TotalSMZ3/Text/StringTable.py @@ -3,7 +3,7 @@ from typing import Any, List import copy from worlds.smz3.TotalSMZ3.Text.Dialog import Dialog from worlds.smz3.TotalSMZ3.Text.Texts import text_folder -from yaml import load, Loader +from Utils import unsafe_parse_yaml class StringTable: @@ -11,7 +11,7 @@ class StringTable: def ParseEntries(resource: str): with open(resource, 'rb') as f: yaml = str(f.read(), "utf-8") - content = load(yaml, Loader) + content = unsafe_parse_yaml(yaml) result = [] for entryValue in content: @@ -20,8 +20,6 @@ class StringTable: result.append((key, value)) elif isinstance(value, str): result.append((key, Dialog.Compiled(value))) - elif isinstance(value, dict): - result.append((key, Dialog.Compiled(value["NoPause"], False))) else: raise Exception(f"Did not expect an object of type {type(value)}") return result @@ -47,9 +45,11 @@ class StringTable: def SetGanonThirdPhaseText(self, text: str): self.SetText("ganon_phase_3", text) + self.SetText("ganon_phase_3_no_silvers", text) + self.SetText("ganon_phase_3_no_silvers_alt", text) def SetTriforceRoomText(self, text: str): - self.SetText("end_triforce", "{NOBORDER}\n" + text) + self.SetText("end_triforce", f"{{NOBORDER}}\n{text}") def SetPedestalText(self, text: str): self.SetText("mastersword_pedestal_translated", text) @@ -60,6 +60,12 @@ class StringTable: def SetBombosText(self, text: str): self.SetText("tablet_bombos_book", text) + def SetTowerRequirementText(self, text: str): + self.SetText("sign_ganons_tower", text) + + def SetGanonRequirementText(self, text: str): + self.SetText("sign_ganon", text) + def SetText(self, name: str, text: str): count = 0 for key, value in self.entries: diff --git a/worlds/smz3/TotalSMZ3/Text/Texts.py b/worlds/smz3/TotalSMZ3/Text/Texts.py index 247ff92b1a..dfaeee06da 100644 --- a/worlds/smz3/TotalSMZ3/Text/Texts.py +++ b/worlds/smz3/TotalSMZ3/Text/Texts.py @@ -2,7 +2,7 @@ from worlds.smz3.TotalSMZ3.Region import Region from worlds.smz3.TotalSMZ3.Regions.Zelda.GanonsTower import GanonsTower from worlds.smz3.TotalSMZ3.Item import Item, ItemType -from yaml import load, Loader +from Utils import unsafe_parse_yaml import random import os @@ -13,7 +13,7 @@ class Texts: def ParseYamlScripts(resource: str): with open(resource, 'rb') as f: yaml = str(f.read(), "utf-8") - return load(yaml, Loader) + return unsafe_parse_yaml(yaml) @staticmethod def ParseTextScript(resource: str): @@ -75,7 +75,7 @@ class Texts: } if item.IsMap(): name = "Map" elif item.IsCompass(): name = "Compass" - elif item.IsKeycard(): name = "Keycard" + elif item.IsSmMap(): name = "SmMap" else: name = nameMap[item.Type] items = Texts.scripts["Items"] diff --git a/worlds/smz3/TotalSMZ3/World.py b/worlds/smz3/TotalSMZ3/World.py index 14d685167f..722d5858e6 100644 --- a/worlds/smz3/TotalSMZ3/World.py +++ b/worlds/smz3/TotalSMZ3/World.py @@ -54,10 +54,26 @@ class World: Player: str Guid: str Id: int + WorldState = None + + @property + def TowerCrystals(self): + return 7 if self.WorldState is None else self.WorldState.TowerCrystals + + @property + def GanonCrystals(self): + return 7 if self.WorldState is None else self.WorldState.GanonCrystals + + @property + def TourianBossTokens(self): + return 4 if self.WorldState is None else self.WorldState.TourianBossTokens def Items(self): return [l.Item for l in self.Locations if l.Item != None] + ForwardSearch: bool = False + + rewardLookup: Dict[int, List[Region.IReward]] locationLookup: Dict[str, Location.Location] regionLookup: Dict[str, Region.Region] @@ -95,22 +111,22 @@ class World: DarkWorldNorthEast(self, self.Config), DarkWorldSouth(self, self.Config), DarkWorldMire(self, self.Config), - Central(self, self.Config), CrateriaWest(self, self.Config), + Central(self, self.Config), CrateriaEast(self, self.Config), Blue(self, self.Config), Green(self, self.Config), - Kraid(self, self.Config), Pink(self, self.Config), Red(self, self.Config), + Kraid(self, self.Config), + WreckedShip(self, self.Config), Outer(self, self.Config), Inner(self, self.Config), NorfairUpperWest(self, self.Config), NorfairUpperEast(self, self.Config), Crocomire(self, self.Config), NorfairLowerWest(self, self.Config), - NorfairLowerEast(self, self.Config), - WreckedShip(self, self.Config) + NorfairLowerEast(self, self.Config) ] self.Locations = [] @@ -130,37 +146,32 @@ class World: raise Exception(f"World.CanEnter: Invalid region name {regionName}", f'{regionName=}'.partition('=')[0]) return region.CanEnter(items) - def CanAquire(self, items: Item.Progression, reward: Region.RewardType): + def CanAcquire(self, items: Item.Progression, reward: Region.RewardType): return next(iter([region for region in self.Regions if isinstance(region, Region.IReward) and region.Reward == reward])).CanComplete(items) - def CanAquireAll(self, items: Item.Progression, *rewards: Region.RewardType): - for region in self.Regions: - if issubclass(type(region), Region.IReward): - if (region.Reward in rewards): - if not region.CanComplete(items): - return False - return True + def CanAcquireAll(self, items: Item.Progression, rewardsMask: Region.RewardType): + return all(region.CanComplete(items) for region in self.rewardLookup[rewardsMask.value]) - # return all(region.CanComplete(items) for region in self.Regions if (isinstance(region, Region.IReward) and region.Reward in rewards)) + def CanAcquireAtLeast(self, amount, items: Item.Progression, rewardsMask: Region.RewardType): + return len([region for region in self.rewardLookup[rewardsMask.value] if region.CanComplete(items)]) >= amount - def Setup(self, rnd: random): - self.SetMedallions(rnd) - self.SetRewards(rnd) + def Setup(self, state): + self.WorldState = state + self.SetMedallions(state.Medallions) + self.SetRewards(state.Rewards) + self.SetRewardLookup() - def SetMedallions(self, rnd: random): - medallionMap = {0: Item.ItemType.Bombos, 1: Item.ItemType.Ether, 2: Item.ItemType.Quake} - regionList = [region for region in self.Regions if isinstance(region, Region.IMedallionAccess)] - for region in regionList: - region.Medallion = medallionMap[rnd.randint(0, 2)] + def SetRewards(self, rewards: List): + regions = [region for region in self.Regions if isinstance(region, Region.IReward) and region.Reward == Region.RewardType.Null] + for (region, reward) in zip(regions, rewards): + region.Reward = reward - def SetRewards(self, rnd: random): - rewards = [ - Region.RewardType.PendantGreen, Region.RewardType.PendantNonGreen, Region.RewardType.PendantNonGreen, Region.RewardType.CrystalRed, Region.RewardType.CrystalRed, - Region.RewardType.CrystalBlue, Region.RewardType.CrystalBlue, Region.RewardType.CrystalBlue, Region.RewardType.CrystalBlue, Region.RewardType.CrystalBlue - ] - rnd.shuffle(rewards) - regionList = [region for region in self.Regions if isinstance(region, Region.IReward) and region.Reward == Region.RewardType.Null] - for region in regionList: - region.Reward = rewards[0] - rewards.remove(region.Reward) + def SetMedallions(self, medallions: List): + self.GetRegion("Misery Mire").Medallion = medallions[0] + self.GetRegion("Turtle Rock").Medallion = medallions[1] + def SetRewardLookup(self): + #/* Generate a lookup of all possible regions for any given reward combination for faster lookup later */ + self.rewardLookup: Dict[int, Region.IReward] = {} + for i in range(0, 512): + self.rewardLookup[i] = [region for region in self.Regions if isinstance(region, Region.IReward) and (region.Reward.value & i) != 0] diff --git a/worlds/smz3/TotalSMZ3/WorldState.py b/worlds/smz3/TotalSMZ3/WorldState.py new file mode 100644 index 0000000000..c857b539d1 --- /dev/null +++ b/worlds/smz3/TotalSMZ3/WorldState.py @@ -0,0 +1,170 @@ +from enum import Enum +from typing import List +from copy import copy + +from worlds.smz3.TotalSMZ3.Patch import DropPrize +from worlds.smz3.TotalSMZ3.Region import RewardType +from worlds.smz3.TotalSMZ3.Config import OpenTower, GanonVulnerable, OpenTourian + +class Medallion(Enum): + Bombos = 0 + Ether = 1 + Quake = 2 + +class DropPrizeRecord: + Packs: List[DropPrize] + TreePulls: List[DropPrize] + CrabContinous: DropPrize + CrabFinal: DropPrize + Stun: DropPrize + Fish: DropPrize + + def __init__(self, Packs, TreePulls, CrabContinous, CrabFinal, Stun, Fish): + self.Packs = Packs + self.TreePulls = TreePulls + self.CrabContinous = CrabContinous + self.CrabFinal = CrabFinal + self.Stun = Stun + self.Fish = Fish + +class WorldState: + Rewards: List[RewardType] + Medallions: List[Medallion] + + TowerCrystals: int + GanonCrystals: int + TourianBossTokens: int + + DropPrizes: DropPrizeRecord + + def __init__(self, config, rnd): + self.Rewards = self.DistributeRewards(rnd) + self.Medallions = self.GenerateMedallions(rnd) + self.TowerCrystals = rnd.randint(0, 7) if config.OpenTower == OpenTower.Random else config.OpenTower.value + self.GanonCrystals = rnd.randint(0, 7) if config.GanonVulnerable == GanonVulnerable.Random else config.GanonVulnerable.value + self.TourianBossTokens = rnd.randint(0, 4) if config.OpenTourian == OpenTourian.Random else config.OpenTourian.value + self.DropPrizes = self.ShuffleDropPrizes(rnd) + + @staticmethod + def Generate(config, rnd): + return WorldState(config, rnd) + + BaseRewards = [ + RewardType.PendantGreen, RewardType.PendantNonGreen, RewardType.PendantNonGreen, RewardType.CrystalRed, RewardType.CrystalRed, + RewardType.CrystalBlue, RewardType.CrystalBlue, RewardType.CrystalBlue, RewardType.CrystalBlue, RewardType.CrystalBlue, + RewardType.AnyBossToken, RewardType.AnyBossToken, RewardType.AnyBossToken, RewardType.AnyBossToken, + ] + + BossTokens = [ + RewardType.BossTokenKraid, RewardType.BossTokenPhantoon, RewardType.BossTokenDraygon, RewardType.BossTokenRidley + ] + + @staticmethod + def DistributeRewards(rnd): + #// Assign four rewards for SM using a "loot table", randomized result + gen = WorldState.Distribution().Generate(lambda dist: dist.Hit(rnd.randrange(dist.Sum))) + smRewards = [next(gen) for x in range(4)] + + #// Exclude the SM rewards to get the Z3 lineup + z3Rewards = WorldState.BaseRewards[:] + for reward in smRewards: + z3Rewards.remove(reward) + + rnd.shuffle(z3Rewards) + #// Replace "any token" with random specific tokens + rewards = z3Rewards + smRewards + tokens = WorldState.BossTokens[:] + rnd.shuffle(tokens) + rewards = [tokens.pop() if reward == RewardType.AnyBossToken else reward for reward in rewards] + + return rewards + + + class Distribution: + factor = 3 + + def __init__(self, distribution = None, boss = None, blue = None, red = None, pend = None, green = None): + self.Boss = 4 * self.factor + self.Blue = 5 * self.factor + self.Red = 2 * self.factor + self.Pend = 2 + self.Green = 1 + + if (distribution is not None): + self = copy(distribution) + if (boss is not None): + self.Boss = boss + if (blue is not None): + self.Blue = blue + if (red is not None): + self.Red = red + if (pend is not None): + self.Pend = pend + if (green is not None): + self.Green = green + + @property + def Sum(self): + return self.Boss + self.Blue + self.Red + self.Pend + self.Green + + def Hit(self, p): + p -= self.Boss + if (p < 0): return (RewardType.AnyBossToken, WorldState.Distribution(self, boss = self.Boss - WorldState.Distribution.factor)) + p -= self.Blue + if (p - self.Blue < 0): return (RewardType.CrystalBlue, WorldState.Distribution(self, blue = self.Blue - WorldState.Distribution.factor)) + p -= self.Red + if (p - self.Red < 0): return (RewardType.CrystalRed, WorldState.Distribution(self, red = self.Red - WorldState.Distribution.factor)) + p -= self.Pend + if (p - self.Pend < 0): return (RewardType.PendantNonGreen, WorldState.Distribution(self, pend = self.Pend - 1)) + return (RewardType.PendantGreen, WorldState.Distribution(self, green = self.Green - 1)) + + def Generate(self, func): + result = None + while (True): + (result, newSelf) = func(self) + self.Boss = newSelf.Boss + self.Blue = newSelf.Blue + self.Red = newSelf.Red + self.Pend = newSelf.Pend + self.Green = newSelf.Green + yield result + + @staticmethod + def GenerateMedallions(rnd): + return [ + Medallion(rnd.randrange(3)), + Medallion(rnd.randrange(3)), + ] + + BaseDropPrizes = [ + DropPrize.Heart, DropPrize.Heart, DropPrize.Heart, DropPrize.Heart, DropPrize.Green, DropPrize.Heart, DropPrize.Heart, DropPrize.Green, #// pack 1 + DropPrize.Blue, DropPrize.Green, DropPrize.Blue, DropPrize.Red, DropPrize.Blue, DropPrize.Green, DropPrize.Blue, DropPrize.Blue, #// pack 2 + DropPrize.FullMagic, DropPrize.Magic, DropPrize.Magic, DropPrize.Blue, DropPrize.FullMagic, DropPrize.Magic, DropPrize.Heart, DropPrize.Magic, #// pack 3 + DropPrize.Bomb1, DropPrize.Bomb1, DropPrize.Bomb1, DropPrize.Bomb4, DropPrize.Bomb1, DropPrize.Bomb1, DropPrize.Bomb8, DropPrize.Bomb1, #// pack 4 + DropPrize.Arrow5, DropPrize.Heart, DropPrize.Arrow5, DropPrize.Arrow10, DropPrize.Arrow5, DropPrize.Heart, DropPrize.Arrow5, DropPrize.Arrow10,#// pack 5 + DropPrize.Magic, DropPrize.Green, DropPrize.Heart, DropPrize.Arrow5, DropPrize.Magic, DropPrize.Bomb1, DropPrize.Green, DropPrize.Heart, #// pack 6 + DropPrize.Heart, DropPrize.Fairy, DropPrize.FullMagic, DropPrize.Red, DropPrize.Bomb8, DropPrize.Heart, DropPrize.Red, DropPrize.Arrow10, #// pack 7 + DropPrize.Green, DropPrize.Blue, DropPrize.Red,#// from pull trees + DropPrize.Green, DropPrize.Red,#// from prize crab + DropPrize.Green, #// stunned prize + DropPrize.Red,#// saved fish prize + ] + + @staticmethod + def ShuffleDropPrizes(rnd): + nrPackDrops = 8 * 7 + nrTreePullDrops = 3 + + prizes = WorldState.BaseDropPrizes[:] + rnd.shuffle(prizes) + + (packs, prizes) = (prizes[:nrPackDrops], prizes[nrPackDrops:]) + (treePulls, prizes) = (prizes[:nrTreePullDrops], prizes[nrTreePullDrops:]) + (crabContinous, crabFinalDrop, prizes) = (prizes[0], prizes[1], prizes[2:]) + (stun, prizes) = (prizes[0], prizes[1:]) + fish = prizes[0] + return DropPrizeRecord(packs, treePulls, crabContinous, crabFinalDrop, stun, fish) + + @staticmethod + def SplitOff(source, count): + return (source[:count], source[count:]) \ No newline at end of file diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 7f05e0dfd5..15ac85c7c3 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -8,15 +8,19 @@ from typing import Dict, Set, TextIO from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassification, RegionType, CollectionState, \ Tutorial from worlds.generic.Rules import set_rule +from worlds.smz3.TotalSMZ3.Item import ItemType import worlds.smz3.TotalSMZ3.Item as TotalSMZ3Item from worlds.smz3.TotalSMZ3.World import World as TotalSMZ3World -from worlds.smz3.TotalSMZ3.Config import Config, GameMode, GanonInvincible, Goal, KeyShuffle, MorphLocation, SMLogic, SwordLocation, Z3Logic +from worlds.smz3.TotalSMZ3.Regions.Zelda.GanonsTower import GanonsTower +from worlds.smz3.TotalSMZ3.Config import Config, GameMode, Goal, KeyShuffle, MorphLocation, SMLogic, SwordLocation, Z3Logic, OpenTower, GanonVulnerable, OpenTourian from worlds.smz3.TotalSMZ3.Location import LocationType, locations_start_id, Location as TotalSMZ3Location from worlds.smz3.TotalSMZ3.Patch import Patch as TotalSMZ3Patch, getWord, getWordArray +from worlds.smz3.TotalSMZ3.WorldState import WorldState from ..AutoWorld import World, AutoLogicRegister, WebWorld from .Rom import get_base_rom_bytes, SMZ3DeltaPatch from .ips import IPS_Patch from .Options import smz3_options +from Options import Accessibility world_folder = os.path.dirname(__file__) logger = logging.getLogger("SMZ3") @@ -27,14 +31,18 @@ class SMZ3CollectionState(metaclass=AutoLogicRegister): # for unit tests where MultiWorld is instantiated before worlds if hasattr(parent, "state"): self.smz3state = {player: TotalSMZ3Item.Progression([]) for player in parent.get_game_players("SMZ3")} + for player, group in parent.groups.items(): + if (group["game"] == "SMZ3"): + self.smz3state[player] = TotalSMZ3Item.Progression([]) + if player not in parent.state.smz3state: + parent.state.smz3state[player] = TotalSMZ3Item.Progression([]) else: self.smz3state = {} def copy_mixin(self, ret) -> CollectionState: - ret.smz3state = {player: copy.deepcopy(self.smz3state[player]) for player in self.world.get_game_players("SMZ3")} + ret.smz3state = {player: copy.deepcopy(self.smz3state[player]) for player in self.smz3state} return ret - class SMZ3Web(WebWorld): tutorials = [Tutorial( "Multiworld Setup Guide", @@ -53,62 +61,160 @@ class SMZ3World(World): """ game: str = "SMZ3" topology_present = False - data_version = 1 - options = smz3_options + data_version = 2 + option_definitions = smz3_options item_names: Set[str] = frozenset(TotalSMZ3Item.lookup_name_to_id) location_names: Set[str] item_name_to_id = TotalSMZ3Item.lookup_name_to_id - location_name_to_id: Dict[str, int] = {key : locations_start_id + value.Id for key, value in TotalSMZ3World(Config({}), "", 0, "").locationLookup.items()} + location_name_to_id: Dict[str, int] = {key : locations_start_id + value.Id for key, value in TotalSMZ3World(Config(), "", 0, "").locationLookup.items()} web = SMZ3Web() remote_items: bool = False remote_start_inventory: bool = False + locationNamesGT: Set[str] = {loc.Name for loc in GanonsTower(None, None).Locations} + # first added for 0.2.6 required_client_version = (0, 2, 6) def __init__(self, world: MultiWorld, player: int): self.rom_name_available_event = threading.Event() - self.locations = {} + self.locations: Dict[str, Location] = {} self.unreachable = [] super().__init__(world, player) + @classmethod + def isProgression(cls, itemType): + progressionTypes = { + ItemType.ProgressiveShield, + ItemType.ProgressiveSword, + ItemType.Bow, + ItemType.Hookshot, + ItemType.Mushroom, + ItemType.Powder, + ItemType.Firerod, + ItemType.Icerod, + ItemType.Bombos, + ItemType.Ether, + ItemType.Quake, + ItemType.Lamp, + ItemType.Hammer, + ItemType.Shovel, + ItemType.Flute, + ItemType.Bugnet, + ItemType.Book, + ItemType.Bottle, + ItemType.Somaria, + ItemType.Byrna, + ItemType.Cape, + ItemType.Mirror, + ItemType.Boots, + ItemType.ProgressiveGlove, + ItemType.Flippers, + ItemType.MoonPearl, + ItemType.HalfMagic, + + ItemType.Grapple, + ItemType.Charge, + ItemType.Ice, + ItemType.Wave, + ItemType.Plasma, + ItemType.Varia, + ItemType.Gravity, + ItemType.Morph, + ItemType.Bombs, + ItemType.SpringBall, + ItemType.ScrewAttack, + ItemType.HiJump, + ItemType.SpaceJump, + ItemType.SpeedBooster, + + ItemType.ETank, + ItemType.ReserveTank, + + ItemType.BigKeyGT, + ItemType.KeyGT, + ItemType.BigKeyEP, + ItemType.BigKeyDP, + ItemType.BigKeyTH, + ItemType.BigKeyPD, + ItemType.BigKeySP, + ItemType.BigKeySW, + ItemType.BigKeyTT, + ItemType.BigKeyIP, + ItemType.BigKeyMM, + ItemType.BigKeyTR, + + ItemType.KeyHC, + ItemType.KeyCT, + ItemType.KeyDP, + ItemType.KeyTH, + ItemType.KeyPD, + ItemType.KeySP, + ItemType.KeySW, + ItemType.KeyTT, + ItemType.KeyIP, + ItemType.KeyMM, + ItemType.KeyTR, + + ItemType.CardCrateriaL1, + ItemType.CardCrateriaL2, + ItemType.CardCrateriaBoss, + ItemType.CardBrinstarL1, + ItemType.CardBrinstarL2, + ItemType.CardBrinstarBoss, + ItemType.CardNorfairL1, + ItemType.CardNorfairL2, + ItemType.CardNorfairBoss, + ItemType.CardMaridiaL1, + ItemType.CardMaridiaL2, + ItemType.CardMaridiaBoss, + ItemType.CardWreckedShipL1, + ItemType.CardWreckedShipBoss, + ItemType.CardLowerNorfairL1, + ItemType.CardLowerNorfairBoss, + } + return itemType in progressionTypes + @classmethod def stage_assert_generate(cls, world): base_combined_rom = get_base_rom_bytes() def generate_early(self): - config = Config({}) - config.GameMode = GameMode.Multiworld - config.Z3Logic = Z3Logic.Normal - config.SMLogic = SMLogic(self.world.sm_logic[self.player].value) - config.SwordLocation = SwordLocation(self.world.sword_location[self.player].value) - config.MorphLocation = MorphLocation(self.world.morph_location[self.player].value) - config.Goal = Goal.DefeatBoth - config.KeyShuffle = KeyShuffle(self.world.key_shuffle[self.player].value) - config.Keysanity = config.KeyShuffle != KeyShuffle.Null - config.GanonInvincible = GanonInvincible.BeforeCrystals + self.config = Config() + self.config.GameMode = GameMode.Multiworld + self.config.Z3Logic = Z3Logic.Normal + self.config.SMLogic = SMLogic(self.world.sm_logic[self.player].value) + self.config.SwordLocation = SwordLocation(self.world.sword_location[self.player].value) + self.config.MorphLocation = MorphLocation(self.world.morph_location[self.player].value) + self.config.Goal = Goal(self.world.goal[self.player].value) + self.config.KeyShuffle = KeyShuffle(self.world.key_shuffle[self.player].value) + self.config.OpenTower = OpenTower(self.world.open_tower[self.player].value) + self.config.GanonVulnerable = GanonVulnerable(self.world.ganon_vulnerable[self.player].value) + self.config.OpenTourian = OpenTourian(self.world.open_tourian[self.player].value) self.local_random = random.Random(self.world.random.randint(0, 1000)) - self.smz3World = TotalSMZ3World(config, self.world.get_player_name(self.player), self.player, self.world.seed_name) + self.smz3World = TotalSMZ3World(self.config, self.world.get_player_name(self.player), self.player, self.world.seed_name) self.smz3DungeonItems = [] SMZ3World.location_names = frozenset(self.smz3World.locationLookup.keys()) self.world.state.smz3state[self.player] = TotalSMZ3Item.Progression([]) def generate_basic(self): - self.smz3World.Setup(self.world.random) + self.smz3World.Setup(WorldState.Generate(self.config, self.world.random)) self.dungeon = TotalSMZ3Item.Item.CreateDungeonPool(self.smz3World) self.dungeon.reverse() self.progression = TotalSMZ3Item.Item.CreateProgressionPool(self.smz3World) self.keyCardsItems = TotalSMZ3Item.Item.CreateKeycards(self.smz3World) + self.SmMapsItems = TotalSMZ3Item.Item.CreateSmMaps(self.smz3World) niceItems = TotalSMZ3Item.Item.CreateNicePool(self.smz3World) junkItems = TotalSMZ3Item.Item.CreateJunkPool(self.smz3World) allJunkItems = niceItems + junkItems + self.junkItemsNames = [item.Type.name for item in junkItems] if (self.smz3World.Config.Keysanity): - progressionItems = self.progression + self.dungeon + self.keyCardsItems + progressionItems = self.progression + self.dungeon + self.keyCardsItems + self.SmMapsItems else: progressionItems = self.progression for item in self.keyCardsItems: @@ -249,6 +355,49 @@ class SMZ3World(World): return patch + def SnesCustomization(self, addr: int): + addr = (0x400000 if addr < 0x800000 else 0)| (addr & 0x3FFFFF) + return addr + + def apply_customization(self): + patch = {} + + # smSpinjumps + if (self.world.spin_jumps_animation[self.player].value == 1): + patch[self.SnesCustomization(0x9B93FE)] = bytearray([0x01]) + + # z3HeartBeep + values = [ 0x00, 0x80, 0x40, 0x20, 0x10] + index = self.world.heart_beep_speed[self.player].value + patch[0x400033] = bytearray([values[index if index < len(values) else 2]]) + + # z3HeartColor + values = [ + [0x24, [0x18, 0x00]], + [0x3C, [0x04, 0x17]], + [0x2C, [0xC9, 0x69]], + [0x28, [0xBC, 0x02]] + ] + index = self.world.heart_color[self.player].value + (hud, fileSelect) = values[index if index < len(values) else 0] + for i in range(0, 20, 2): + patch[self.SnesCustomization(0xDFA1E + i)] = bytearray([hud]) + patch[self.SnesCustomization(0x1BD6AA)] = bytearray(fileSelect) + + # z3QuickSwap + patch[0x40004B] = bytearray([0x01 if self.world.quick_swap[self.player].value else 0x00]) + + # smEnergyBeepOff + if (self.world.energy_beep[self.player].value == 0): + for ([addr, value]) in [ + [0x90EA9B, 0x80], + [0x90F337, 0x80], + [0x91E6D5, 0x80] + ]: + patch[self.SnesCustomization(addr)] = bytearray([value]) + + return patch + def generate_output(self, output_directory: str): try: base_combined_rom = get_base_rom_bytes() @@ -256,15 +405,16 @@ class SMZ3World(World): base_combined_rom = basepatch.apply(base_combined_rom) patcher = TotalSMZ3Patch(self.smz3World, - [world.smz3World for key, world in self.world.worlds.items() if isinstance(world, SMZ3World)], + [world.smz3World for key, world in self.world.worlds.items() if isinstance(world, SMZ3World) and hasattr(world, "smz3World")], self.world.seed_name, self.world.seed, self.local_random, - self.world.world_name_lookup, + {v: k for k, v in self.world.player_name.items()}, next(iter(loc.player for loc in self.world.get_locations() if (loc.item.name == "SilverArrows" and loc.item.player == self.player)))) patches = patcher.Create(self.smz3World.Config) patches.update(self.apply_sm_custom_sprite()) patches.update(self.apply_item_names()) + patches.update(self.apply_customization()) for addr, bytes in patches.items(): offset = 0 for byte in bytes: @@ -312,7 +462,7 @@ class SMZ3World(World): return slot_data def collect(self, state: CollectionState, item: Item) -> bool: - state.smz3state[item.player].Add([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World)]) + state.smz3state[self.player].Add([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World if hasattr(self, "smz3World") else None)]) if item.advancement: state.prog_items[item.name, item.player] += 1 return True # indicate that a logical state change has occured @@ -321,7 +471,7 @@ class SMZ3World(World): def remove(self, state: CollectionState, item: Item) -> bool: name = self.collect_item(state, item, True) if name: - state.smz3state[item.player].Remove([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World)]) + state.smz3state[item.player].Remove([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World if hasattr(self, "smz3World") else None)]) state.prog_items[name, item.player] -= 1 if state.prog_items[name, item.player] < 1: del (state.prog_items[name, item.player]) @@ -329,8 +479,11 @@ class SMZ3World(World): return False def create_item(self, name: str) -> Item: - return SMZ3Item(name, ItemClassification.progression, - TotalSMZ3Item.ItemType[name], self.item_name_to_id[name], player = self.player) + return SMZ3Item(name, + ItemClassification.progression if SMZ3World.isProgression(TotalSMZ3Item.ItemType[name]) else ItemClassification.filler, + TotalSMZ3Item.ItemType[name], self.item_name_to_id[name], + self.player, + TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[name], self)) def pre_fill(self): from Fill import fill_restrictive @@ -357,6 +510,7 @@ class SMZ3World(World): item.item.Progression = False item.location.event = False self.unreachable.append(item.location) + self.JunkFillGT(0.5) def get_pre_fill_items(self): if (not self.smz3World.Config.Keysanity): @@ -364,9 +518,40 @@ class SMZ3World(World): else: return [] + def get_filler_item_name(self) -> str: + return self.world.random.choice(self.junkItemsNames) + def write_spoiler(self, spoiler_handle: TextIO): self.world.spoiler.unreachables.update(self.unreachable) + def JunkFillGT(self, factor): + poolLength = len(self.world.itempool) + junkPoolIdx = [i for i in range(0, poolLength) + if self.world.itempool[i].classification in (ItemClassification.filler, ItemClassification.trap) and + self.world.itempool[i].player == self.player] + toRemove = [] + for loc in self.locations.values(): + # commenting this for now since doing a partial GT pre fill would allow for non SMZ3 progression in GT + # which isnt desirable (SMZ3 logic only filters for SMZ3 items). Having progression in GT can only happen in Single Player. + # if len(toRemove) >= int(len(self.locationNamesGT) * factor * self.smz3World.TowerCrystals / 7): + # break + if loc.name in self.locationNamesGT and loc.item is None: + poolLength = len(junkPoolIdx) + # start looking at a random starting index and loop at start if no match found + start = self.world.random.randint(0, poolLength) + for off in range(0, poolLength): + i = (start + off) % poolLength + candidate = self.world.itempool[junkPoolIdx[i]] + if junkPoolIdx[i] not in toRemove and loc.can_fill(self.world.state, candidate, False): + itemFromPool = candidate + toRemove.append(junkPoolIdx[i]) + break + self.world.push_item(loc, itemFromPool, False) + loc.event = False + toRemove.sort(reverse = True) + for i in toRemove: + self.world.itempool.pop(i) + def FillItemAtLocation(self, itemPool, itemType, location): itemToPlace = TotalSMZ3Item.Item.Get(itemPool, itemType, self.smz3World) if (itemToPlace == None): @@ -391,7 +576,7 @@ class SMZ3World(World): raise Exception(f"Tried to front fill {item.Name} in, but no location was available") location.Item = item - itemFromPool = next((i for i in self.world.itempool if i.player == self.player and i.name == item.Type.name), None) + itemFromPool = next((i for i in self.world.itempool if i.player == self.player and i.name == item.Type.name and i.advancement == item.Progression), None) if itemFromPool is not None: self.world.get_location(location.Name, self.player).place_locked_item(itemFromPool) self.world.itempool.remove(itemFromPool) @@ -399,12 +584,12 @@ class SMZ3World(World): def InitialFillInOwnWorld(self): self.FillItemAtLocation(self.dungeon, TotalSMZ3Item.ItemType.KeySW, self.smz3World.GetLocation("Skull Woods - Pinball Room")) + if (not self.smz3World.Config.Keysanity): + self.FillItemAtLocation(self.dungeon, TotalSMZ3Item.ItemType.KeySP, self.smz3World.GetLocation("Swamp Palace - Entrance")) # /* Check Swords option and place as needed */ if self.smz3World.Config.SwordLocation == SwordLocation.Uncle: self.FillItemAtLocation(self.progression, TotalSMZ3Item.ItemType.ProgressiveSword, self.smz3World.GetLocation("Link's Uncle")) - elif self.smz3World.Config.SwordLocation == SwordLocation.Early: - self.FrontFillItemInOwnWorld(self.progression, TotalSMZ3Item.ItemType.ProgressiveSword) # /* Check Morph option and place as needed */ if self.smz3World.Config.MorphLocation == MorphLocation.Original: @@ -412,6 +597,10 @@ class SMZ3World(World): elif self.smz3World.Config.MorphLocation == MorphLocation.Early: self.FrontFillItemInOwnWorld(self.progression, TotalSMZ3Item.ItemType.Morph) + # We do early Sword placement after Morph in case its Original location + if self.smz3World.Config.SwordLocation == SwordLocation.Early: + self.FrontFillItemInOwnWorld(self.progression, TotalSMZ3Item.ItemType.ProgressiveSword) + # /* We place a PB and Super in Sphere 1 to make sure the filler # * doesn't start locking items behind this when there are a # * high chance of the trash fill actually making them available */ @@ -447,8 +636,10 @@ class SMZ3Location(Location): class SMZ3Item(Item): game = "SMZ3" + type: ItemType + item: Item - def __init__(self, name, classification, type, code, player: int = None, item=None): + def __init__(self, name, classification, type: ItemType, code, player: int, item: Item): + super(SMZ3Item, self).__init__(name, classification, code, player) self.type = type self.item = item - super(SMZ3Item, self).__init__(name, classification, code, player) diff --git a/worlds/smz3/data/zsm.ips b/worlds/smz3/data/zsm.ips index faf7443a57..2d6027d5e5 100644 Binary files a/worlds/smz3/data/zsm.ips and b/worlds/smz3/data/zsm.ips differ diff --git a/worlds/smz3/docs/en_SMZ3.md b/worlds/smz3/docs/en_SMZ3.md index 91dace7b4d..f0302d12f3 100644 --- a/worlds/smz3/docs/en_SMZ3.md +++ b/worlds/smz3/docs/en_SMZ3.md @@ -23,7 +23,8 @@ certain items to your own world. ## What does another world's item look like in Super Metroid? -A unique item sprite has been added to the game to represent items belonging to another world. +Two unique item sprites have been added to the game to represent items belonging to another world. Progression items have +a small up arrow on the sprite and non-progression don't. ## What does another world's item look like in LttP? diff --git a/worlds/soe/Logic.py b/worlds/soe/Logic.py index f25f2ada1b..d08e6a3e96 100644 --- a/worlds/soe/Logic.py +++ b/worlds/soe/Logic.py @@ -1,5 +1,6 @@ from BaseClasses import MultiWorld from ..AutoWorld import LogicMixin +from .Options import EnergyCore from typing import Set # TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early? @@ -8,9 +9,9 @@ from . import pyevermizer # TODO: resolve/flatten/expand rules to get rid of recursion below where possible # Logic.rules are all rules including locations, excluding those with no progress (i.e. locations that only drop items) rules = [rule for rule in pyevermizer.get_logic() if len(rule.provides) > 0] -# Logic.items are all items excluding non-progression items and duplicates +# Logic.items are all items and extra items excluding non-progression items and duplicates item_names: Set[str] = set() -items = [item for item in filter(lambda item: item.progression, pyevermizer.get_items()) +items = [item for item in filter(lambda item: item.progression, pyevermizer.get_items() + pyevermizer.get_extra_items()) if item.name not in item_names and not item_names.add(item.name)] @@ -47,4 +48,9 @@ class SecretOfEvermoreLogic(LogicMixin): """ Returns True if count of one of evermizer's progress steps is reached based on collected items. i.e. 2 * P_DE """ + if progress == pyevermizer.P_ENERGY_CORE: # logic is shared between worlds, so we override in the call + w = world.worlds[player] + if w.energy_core == EnergyCore.option_fragments: + progress = pyevermizer.P_CORE_FRAGMENT + count = w.required_fragments return self._soe_count(progress, world, player, count) >= count diff --git a/worlds/soe/Options.py b/worlds/soe/Options.py index 0c24399ddd..4ec0ce2bcc 100644 --- a/worlds/soe/Options.py +++ b/worlds/soe/Options.py @@ -37,6 +37,32 @@ class Difficulty(EvermizerFlags, Choice): flags = ['e', 'n', 'h', 'x'] +class EnergyCore(EvermizerFlags, Choice): + """How to obtain the Energy Core""" + display_name = "Energy Core" + option_vanilla = 0 + option_shuffle = 1 + option_fragments = 2 + default = 1 + flags = ['z', '', 'Z'] + + +class RequiredFragments(Range): + """Required fragment count for Energy Core = Fragments""" + display_name = "Required Fragments" + range_start = 1 + range_end = 99 + default = 10 + + +class AvailableFragments(Range): + """Placed fragment count for Energy Core = Fragments""" + display_name = "Available Fragments" + range_start = 1 + range_end = 99 + default = 11 + + class MoneyModifier(Range): """Money multiplier in %""" display_name = "Money Modifier" @@ -186,10 +212,15 @@ class TrapChanceOHKO(TrapChance): class SoEProgressionBalancing(ProgressionBalancing): default = 30 + __doc__ = ProgressionBalancing.__doc__.replace(f"default {ProgressionBalancing.default}", f"default {default}") + special_range_names = {**ProgressionBalancing.special_range_names, "normal": default} soe_options: typing.Dict[str, type(Option)] = { "difficulty": Difficulty, + "energy_core": EnergyCore, + "required_fragments": RequiredFragments, + "available_fragments": AvailableFragments, "money_modifier": MoneyModifier, "exp_modifier": ExpModifier, "fix_sequence": FixSequence, diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index f00ebbe143..f86fc48e93 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -16,7 +16,7 @@ except ImportError: from . import pyevermizer # as part of the source tree from . import Logic # load logic mixin -from .Options import soe_options +from .Options import soe_options, EnergyCore, RequiredFragments, AvailableFragments from .Patch import SoEDeltaPatch, get_base_rom_path """ @@ -52,7 +52,6 @@ Item grouping currently supports * Ingredients - Matches all ingredient drops * Alchemy - Matches all alchemy formulas * Weapons - Matches all weapons but Bazooka, Bone Crusher, Neutron Blade -* Bazooka - Matches all bazookas (currently only one) * Traps - Matches all traps """ @@ -63,12 +62,14 @@ _id_offset: typing.Dict[int, int] = { pyevermizer.CHECK_GOURD: _id_base + 100, # gourds 64100..64399 pyevermizer.CHECK_NPC: _id_base + 400, # npc 64400..64499 # TODO: sniff 64500..64799 - pyevermizer.CHECK_TRAP: _id_base + 900, # npc 64900..64999 + pyevermizer.CHECK_EXTRA: _id_base + 800, # extra items 64800..64899 + pyevermizer.CHECK_TRAP: _id_base + 900, # trap 64900..64999 } # cache native evermizer items and locations _items = pyevermizer.get_items() _traps = pyevermizer.get_traps() +_extras = pyevermizer.get_extra_items() # items that are not placed by default _locations = pyevermizer.get_locations() # fix up texts for AP for _loc in _locations: @@ -104,7 +105,7 @@ def _get_location_mapping() -> typing.Tuple[typing.Dict[str, int], typing.Dict[i def _get_item_mapping() -> typing.Tuple[typing.Dict[str, int], typing.Dict[int, pyevermizer.Item]]: name_to_id = {} id_to_raw = {} - for item in itertools.chain(_items, _traps): + for item in itertools.chain(_items, _extras, _traps): if item.name in name_to_id: continue ap_id = _id_offset[item.type] + item.index @@ -127,7 +128,6 @@ def _get_item_grouping() -> typing.Dict[str, typing.Set[str]]: groups['Alchemy'] = set(item.name for item in _items if item.type == pyevermizer.CHECK_ALCHEMY) groups['Weapons'] = {'Spider Claw', 'Horn Spear', 'Gladiator Sword', 'Bronze Axe', 'Bronze Spear', 'Crusader Sword', 'Lance (Weapon)', 'Knight Basher', 'Atom Smasher', 'Laser Lance'} - groups['Bazooka'] = {'Bazooka+Shells / Shining Armor / 5k Gold'} groups['Traps'] = {trap.name for trap in _traps} return groups @@ -136,7 +136,8 @@ class SoEWebWorld(WebWorld): theme = 'jungle' tutorials = [Tutorial( "Multiworld Setup Guide", - "A guide to playing Secret of Evermore randomizer. This guide covers single-player, multiworld and related software.", + "A guide to playing Secret of Evermore randomizer. This guide covers single-player, multiworld and related" + " software.", "English", "multiworld_en.md", "multiworld/en", @@ -150,21 +151,24 @@ class SoEWorld(World): space station where the final boss must be defeated. """ game: str = "Secret of Evermore" - options = soe_options + option_definitions = soe_options topology_present = False remote_items = False - data_version = 2 + data_version = 3 web = SoEWebWorld() - required_client_version = (0, 2, 6) + required_client_version = (0, 3, 3) item_name_to_id, item_id_to_raw = _get_item_mapping() location_name_to_id, location_id_to_raw = _get_location_mapping() item_name_groups = _get_item_grouping() - trap_types = [name[12:] for name in options if name.startswith('trap_chance_')] + trap_types = [name[12:] for name in option_definitions if name.startswith('trap_chance_')] evermizer_seed: int connect_name: str + energy_core: int + available_fragments: int + required_fragments: int _halls_ne_chest_names: typing.List[str] = [loc.name for loc in _locations if 'Halls NE' in loc.name] @@ -172,6 +176,14 @@ class SoEWorld(World): self.connect_name_available_event = threading.Event() super(SoEWorld, self).__init__(*args, **kwargs) + def generate_early(self) -> None: + # store option values that change logic + self.energy_core = self.world.energy_core[self.player].value + self.required_fragments = self.world.required_fragments[self.player].value + if self.required_fragments > self.world.available_fragments[self.player].value: + self.world.available_fragments[self.player].value = self.required_fragments + self.available_fragments = self.world.available_fragments[self.player].value + def create_event(self, event: str) -> Item: return SoEItem(event, ItemClassification.progression, None, self.player) @@ -182,6 +194,8 @@ class SoEWorld(World): classification = ItemClassification.trap elif item.progression: classification = ItemClassification.progression + elif item.useful: + classification = ItemClassification.useful else: classification = ItemClassification.filler @@ -208,9 +222,33 @@ class SoEWorld(World): self.world.get_entrance('New Game', self.player).connect(self.world.get_region('Ingame', self.player)) def create_items(self): - # add items to the pool - items = list(map(lambda item: self.create_item(item), _items)) + # add regular items to the pool + exclusions: typing.List[str] = [] + if self.energy_core != EnergyCore.option_shuffle: + exclusions.append("Energy Core") # will be placed in generate_basic or replaced by a fragment below + items = list(map(lambda item: self.create_item(item), (item for item in _items if item.name not in exclusions))) + # remove one pair of wings that will be placed in generate_basic + items.remove(self.create_item("Wings")) + + def is_ingredient(item): + for ingredient in _ingredients: + if _match_item_name(item, ingredient): + return True + return False + + # add energy core fragments to the pool + ingredients = [n for n, item in enumerate(items) if is_ingredient(item)] + if self.energy_core == EnergyCore.option_fragments: + items.append(self.create_item("Energy Core Fragment")) # replaces the vanilla energy core + for _ in range(self.available_fragments - 1): + if len(ingredients) < 1: + break # out of ingredients to replace + r = self.world.random.choice(ingredients) + ingredients.remove(r) + items[r] = self.create_item("Energy Core Fragment") + + # add traps to the pool trap_count = self.world.trap_count[self.player].value trap_chances = {} trap_names = {} @@ -232,13 +270,12 @@ class SoEWorld(World): return self.create_item(trap_names[t]) v -= c - while trap_count > 0: - r = self.world.random.randrange(len(items)) - for ingredient in _ingredients: - if _match_item_name(items[r], ingredient): - items[r] = create_trap() - trap_count -= 1 - break + for _ in range(trap_count): + if len(ingredients) < 1: + break # out of ingredients to replace + r = self.world.random.choice(ingredients) + ingredients.remove(r) + items[r] = create_trap() self.world.itempool += items @@ -271,7 +308,10 @@ class SoEWorld(World): wings_location = self.world.random.choice(self._halls_ne_chest_names) wings_item = self.create_item('Wings') self.world.get_location(wings_location, self.player).place_locked_item(wings_item) - self.world.itempool.remove(wings_item) + # place energy core at vanilla location for vanilla mode + if self.energy_core == EnergyCore.option_vanilla: + energy_core = self.create_item('Energy Core') + self.world.get_location('Energy Core #285', self.player).place_locked_item(energy_core) # generate stuff for later self.evermizer_seed = self.world.random.randint(0, 2 ** 16 - 1) # TODO: make this an option for "full" plando? @@ -286,9 +326,12 @@ class SoEWorld(World): try: money = self.world.money_modifier[self.player].value exp = self.world.exp_modifier[self.player].value - switches = [] + switches: typing.List[str] = [] if self.world.death_link[self.player].value: switches.append("--death-link") + if self.energy_core == EnergyCore.option_fragments: + switches.extend(('--available-fragments', str(self.available_fragments), + '--required-fragments', str(self.required_fragments))) rom_file = get_base_rom_path() out_base = output_path(output_directory, f'AP_{self.world.seed_name}_P{self.player}_' f'{self.world.get_file_safe_player_name(self.player)}') @@ -296,7 +339,7 @@ class SoEWorld(World): placement_file = out_base + '.txt' patch_file = out_base + '.apsoe' flags = 'l' # spoiler log - for option_name in self.options: + for option_name in self.option_definitions: option = getattr(self.world, option_name)[self.player] if hasattr(option, 'to_flag'): flags += option.to_flag() diff --git a/worlds/soe/requirements.txt b/worlds/soe/requirements.txt index 8dfdc0de9f..7f6a11e490 100644 --- a/worlds/soe/requirements.txt +++ b/worlds/soe/requirements.txt @@ -1,14 +1,14 @@ -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp38-cp38-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp39-cp39-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp310-cp310-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp38-cp38-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.8' -#https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp39-cp39-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp39-cp39-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2-cp310-cp310-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.10' -#https://github.com/black-sliver/pyevermizer/releases/download/v0.41.2/pyevermizer-0.41.2.tar.gz#egg=pyevermizer; python_version == '3.11' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp38-cp38-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp310-cp310-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp38-cp38-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.8' +#https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp310-cp310-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.10' +#https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3.tar.gz#egg=pyevermizer; python_version == '3.11' diff --git a/worlds/spire/__init__.py b/worlds/spire/__init__.py index 594605cd34..476afad8d9 100644 --- a/worlds/spire/__init__.py +++ b/worlds/spire/__init__.py @@ -22,7 +22,12 @@ class SpireWeb(WebWorld): class SpireWorld(World): - options = spire_options + """ + A deck-building roguelike where you must craft a unique deck, encounter bizarre creatures, discover relics of + immense power, and Slay the Spire! + """ + + option_definitions = spire_options game = "Slay the Spire" topology_present = False data_version = 1 diff --git a/worlds/subnautica/Creatures.py b/worlds/subnautica/Creatures.py new file mode 100644 index 0000000000..56e2a7efa1 --- /dev/null +++ b/worlds/subnautica/Creatures.py @@ -0,0 +1,82 @@ +from typing import Dict, Set, List + +# EN Locale Creature Name to rough depth in meters found at +all_creatures: Dict[str, int] = { + "Gasopod": 0, + "Bladderfish": 0, + "Ancient Floater": 0, + "Skyray": 0, + "Garryfish": 0, + "Peeper": 0, + "Shuttlebug": 0, + "Rabbit Ray": 0, + "Stalker": 0, + "Floater": 0, + "Holefish": 0, + "Cave Crawler": 0, + "Hoopfish": 0, + "Crashfish": 0, + "Hoverfish": 0, + "Spadefish": 0, + "Reefback Leviathan": 0, + "Reaper Leviathan": 0, + "Warper": 0, + "Boomerang": 0, + "Biter": 200, + "Sand Shark": 200, + "Bleeder": 200, + "Crabsnake": 300, + "Jellyray": 300, + "Oculus": 300, + "Mesmer": 300, + "Eyeye": 300, + "Reginald": 400, + "Sea Treader Leviathan": 400, + "Crabsquid": 400, + "Ampeel": 400, + "Boneshark": 400, + "Rockgrub": 400, + "Ghost Leviathan": 500, + "Ghost Leviathan Juvenile": 500, + "Spinefish": 600, + "Blighter": 600, + "Blood Crawler": 600, + "Ghostray": 1000, + "Amoeboid": 1000, + "River Prowler": 1000, + "Red Eyeye": 1300, + "Magmarang": 1300, + "Crimson Ray": 1300, + "Lava Larva": 1300, + "Lava Lizard": 1300, + "Sea Dragon Leviathan": 1300, + "Sea Emperor Leviathan": 1700, + "Sea Emperor Juvenile": 1700, + + # "Cuddlefish": 300, # maybe at some point, needs hatching in containment chamber (20 real-life minutes) +} + +# be nice and make these require Stasis Rifle +aggressive: Set[str] = { + "Cave Crawler", # is very easy without Stasis Rifle, but included for consistency + "Crashfish", + "Bleeder", + "Mesmer", + "Reaper Leviathan", + "Crabsquid", + "Warper", + "Crabsnake", + "Ampeel", + "Boneshark", + "Lava Lizard", + "Sea Dragon Leviathan", + "River Prowler", +} + +suffix: str = " Scan" + +creature_locations: Dict[str, int] = { + creature+suffix: creature_id for creature_id, creature in enumerate(all_creatures, start=34000) +} + +all_creatures_presorted: List[str] = sorted(all_creatures) diff --git a/worlds/subnautica/Items.py b/worlds/subnautica/Items.py index b9377b7ae6..0f05d5e31a 100644 --- a/worlds/subnautica/Items.py +++ b/worlds/subnautica/Items.py @@ -1,23 +1,353 @@ -import json -import os +from BaseClasses import ItemClassification +from typing import TypedDict, Dict, Set -with open(os.path.join(os.path.dirname(__file__), 'items.json'), 'r') as file: - item_table = json.loads(file.read()) -lookup_id_to_name = {} -lookup_name_to_item = {} -advancement_item_names = set() -non_advancement_item_names = set() +class ItemDict(TypedDict): + classification: ItemClassification + count: int + name: str + tech_type: str -for item in item_table: - item_name = item["name"] - lookup_id_to_name[item["id"]] = item_name - lookup_name_to_item[item_name] = item - if item["progression"]: + +item_table: Dict[int, ItemDict] = { + 35000: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'Compass', + 'tech_type': 'Compass'}, + 35001: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Lightweight High Capacity Tank', + 'tech_type': 'PlasteelTank'}, + 35002: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Vehicle Upgrade Console', + 'tech_type': 'BaseUpgradeConsole'}, + 35003: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ultra Glide Fins', + 'tech_type': 'UltraGlideFins'}, + 35004: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'Cyclops Sonar Upgrade', + 'tech_type': 'CyclopsSonarModule'}, + 35005: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'Reinforced Dive Suit', + 'tech_type': 'ReinforcedDiveSuit'}, + 35006: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'Cyclops Thermal Reactor Module', + 'tech_type': 'CyclopsThermalReactorModule'}, + 35007: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Stillsuit', + 'tech_type': 'Stillsuit'}, + 35008: {'classification': ItemClassification.filler, + 'count': 2, + 'name': 'Alien Containment Fragment', + 'tech_type': 'BaseWaterParkFragment'}, + 35009: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'Creature Decoy', + 'tech_type': 'CyclopsDecoy'}, + 35010: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'Cyclops Fire Suppression System', + 'tech_type': 'CyclopsFireSuppressionModule'}, + 35011: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'Swim Charge Fins', + 'tech_type': 'SwimChargeFins'}, + 35012: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'Repulsion Cannon', + 'tech_type': 'RepulsionCannon'}, + 35013: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'Cyclops Decoy Tube Upgrade', + 'tech_type': 'CyclopsDecoyModule'}, + 35014: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Cyclops Shield Generator', + 'tech_type': 'CyclopsShieldModule'}, + 35015: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Cyclops Depth Module MK1', + 'tech_type': 'CyclopsHullModule1'}, + 35016: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'Cyclops Docking Bay Repair Module', + 'tech_type': 'CyclopsSeamothRepairModule'}, + 35017: {'classification': ItemClassification.useful, + 'count': 2, + 'name': 'Battery Charger fragment', + 'tech_type': 'BatteryChargerFragment'}, + 35018: {'classification': ItemClassification.filler, + 'count': 2, + 'name': 'Beacon Fragment', + 'tech_type': 'BeaconFragment'}, + 35019: {'classification': ItemClassification.useful, + 'count': 2, + 'name': 'Bioreactor Fragment', + 'tech_type': 'BaseBioReactorFragment'}, + 35020: {'classification': ItemClassification.progression, + 'count': 3, + 'name': 'Cyclops Bridge Fragment', + 'tech_type': 'CyclopsBridgeFragment'}, + 35021: {'classification': ItemClassification.progression, + 'count': 3, + 'name': 'Cyclops Engine Fragment', + 'tech_type': 'CyclopsEngineFragment'}, + 35022: {'classification': ItemClassification.progression, + 'count': 3, + 'name': 'Cyclops Hull Fragment', + 'tech_type': 'CyclopsHullFragment'}, + 35023: {'classification': ItemClassification.filler, + 'count': 2, + 'name': 'Grav Trap Fragment', + 'tech_type': 'GravSphereFragment'}, + 35024: {'classification': ItemClassification.progression, + 'count': 3, + 'name': 'Laser Cutter Fragment', + 'tech_type': 'LaserCutterFragment'}, + 35025: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Light Stick Fragment', + 'tech_type': 'TechlightFragment'}, + 35026: {'classification': ItemClassification.progression, + 'count': 4, + 'name': 'Mobile Vehicle Bay Fragment', + 'tech_type': 'ConstructorFragment'}, + 35027: {'classification': ItemClassification.progression, + 'count': 3, + 'name': 'Modification Station Fragment', + 'tech_type': 'WorkbenchFragment'}, + 35028: {'classification': ItemClassification.progression, + 'count': 2, + 'name': 'Moonpool Fragment', + 'tech_type': 'MoonpoolFragment'}, + 35029: {'classification': ItemClassification.useful, + 'count': 3, + 'name': 'Nuclear Reactor Fragment', + 'tech_type': 'BaseNuclearReactorFragment'}, + 35030: {'classification': ItemClassification.useful, + 'count': 2, + 'name': 'Power Cell Charger Fragment', + 'tech_type': 'PowerCellChargerFragment'}, + 35031: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Power Transmitter Fragment', + 'tech_type': 'PowerTransmitterFragment'}, + 35032: {'classification': ItemClassification.progression, + 'count': 4, + 'name': 'Prawn Suit Fragment', + 'tech_type': 'ExosuitFragment'}, + 35033: {'classification': ItemClassification.useful, + 'count': 2, + 'name': 'Prawn Suit Drill Arm Fragment', + 'tech_type': 'ExosuitDrillArmFragment'}, + 35034: {'classification': ItemClassification.useful, + 'count': 2, + 'name': 'Prawn Suit Grappling Arm Fragment', + 'tech_type': 'ExosuitGrapplingArmFragment'}, + 35035: {'classification': ItemClassification.useful, + 'count': 2, + 'name': 'Prawn Suit Propulsion Cannon Fragment', + 'tech_type': 'ExosuitPropulsionArmFragment'}, + 35036: {'classification': ItemClassification.useful, + 'count': 2, + 'name': 'Prawn Suit Torpedo Arm Fragment', + 'tech_type': 'ExosuitTorpedoArmFragment'}, + 35037: {'classification': ItemClassification.useful, + 'count': 3, + 'name': 'Scanner Room Fragment', + 'tech_type': 'BaseMapRoomFragment'}, + 35038: {'classification': ItemClassification.progression, + 'count': 5, + 'name': 'Seamoth Fragment', + 'tech_type': 'SeamothFragment'}, + 35039: {'classification': ItemClassification.progression, + 'count': 2, + 'name': 'Stasis Rifle Fragment', + 'tech_type': 'StasisRifleFragment'}, + 35040: {'classification': ItemClassification.useful, + 'count': 2, + 'name': 'Thermal Plant Fragment', + 'tech_type': 'ThermalPlantFragment'}, + 35041: {'classification': ItemClassification.progression, + 'count': 2, + 'name': 'Seaglide Fragment', + 'tech_type': 'SeaglideFragment'}, + 35042: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Radiation Suit', + 'tech_type': 'RadiationSuit'}, + 35043: {'classification': ItemClassification.progression, + 'count': 2, + 'name': 'Propulsion Cannon Fragment', + 'tech_type': 'PropulsionCannonFragment'}, + 35044: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Neptune Launch Platform', + 'tech_type': 'RocketBase'}, + 35045: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ion Power Cell', + 'tech_type': 'PrecursorIonPowerCell'}, + 35046: {'classification': ItemClassification.filler, + 'count': 2, + 'name': 'Exterior Growbed Fragment', + 'tech_type': 'FarmingTrayFragment'}, + 35047: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Picture Frame', + 'tech_type': 'PictureFrameFragment'}, + 35048: {'classification': ItemClassification.filler, + 'count': 2, + 'name': 'Bench Fragment', + 'tech_type': 'BenchFragment'}, + 35049: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Basic Plant Pot', + 'tech_type': 'PlanterPotFragment'}, + 35050: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Interior Growbed', + 'tech_type': 'PlanterBoxFragment'}, + 35051: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Plant Shelf', + 'tech_type': 'PlanterShelfFragment'}, + 35052: {'classification': ItemClassification.filler, + 'count': 2, + 'name': 'Observatory Fragment', + 'tech_type': 'BaseObservatoryFragment'}, + 35053: {'classification': ItemClassification.useful, + 'count': 2, + 'name': 'Multipurpose Room', + 'tech_type': 'BaseRoom'}, + 35054: {'classification': ItemClassification.useful, + 'count': 2, + 'name': 'Bulkhead Fragment', + 'tech_type': 'BaseBulkheadFragment'}, + 35055: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Spotlight', + 'tech_type': 'Spotlight'}, + 35056: {'classification': ItemClassification.filler, + 'count': 2, + 'name': 'Desk', + 'tech_type': 'StarshipDesk'}, + 35057: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Swivel Chair', + 'tech_type': 'StarshipChair'}, + 35058: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Office Chair', + 'tech_type': 'StarshipChair2'}, + 35059: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Command Chair', + 'tech_type': 'StarshipChair3'}, + 35060: {'classification': ItemClassification.filler, + 'count': 2, + 'name': 'Counter', + 'tech_type': 'LabCounter'}, + 35061: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Single Bed', + 'tech_type': 'NarrowBed'}, + 35062: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Basic Double Bed', + 'tech_type': 'Bed1'}, + 35063: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Quilted Double Bed', + 'tech_type': 'Bed2'}, + 35064: {'classification': ItemClassification.filler, + 'count': 2, + 'name': 'Coffee Vending Machine', + 'tech_type': 'CoffeeVendingMachine'}, + 35065: {'classification': ItemClassification.filler, + 'count': 2, + 'name': 'Trash Can', + 'tech_type': 'Trashcans'}, + 35066: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Floodlight', + 'tech_type': 'Techlight'}, + 35067: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Bar Table', + 'tech_type': 'BarTable'}, + 35068: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Vending Machine', + 'tech_type': 'VendingMachine'}, + 35069: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Single Wall Shelf', + 'tech_type': 'SingleWallShelf'}, + 35070: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Wall Shelves', + 'tech_type': 'WallShelves'}, + 35071: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Round Plant Pot', + 'tech_type': 'PlanterPot2'}, + 35072: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Chic Plant Pot', + 'tech_type': 'PlanterPot3'}, + 35073: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Nuclear Waste Disposal', + 'tech_type': 'LabTrashcan'}, + 35074: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Wall Planter', + 'tech_type': 'BasePlanter'}, + 35075: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ion Battery', + 'tech_type': 'PrecursorIonBattery'}, + 35076: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Neptune Gantry', + 'tech_type': 'RocketBaseLadder'}, + 35077: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Neptune Boosters', + 'tech_type': 'RocketStage1'}, + 35078: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Neptune Fuel Reserve', + 'tech_type': 'RocketStage2'}, + 35079: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Neptune Cockpit', + 'tech_type': 'RocketStage3'}, + 35080: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Water Filtration Machine', + 'tech_type': 'BaseFiltrationMachine'}} + +advancement_item_names: Set[str] = set() +non_advancement_item_names: Set[str] = set() + +for item_id, item_data in item_table.items(): + item_name = item_data["name"] + if ItemClassification.progression in item_data["classification"]: advancement_item_names.add(item_name) else: non_advancement_item_names.add(item_name) -lookup_id_to_name[None] = "Victory" +if False: # turn to True to export for Subnautica mod + payload = {item_id: item_data["tech_type"] for item_id, item_data in item_table.items()} + import json -lookup_name_to_id = {name: id for id, name in lookup_id_to_name.items()} \ No newline at end of file + with open("items.json", "w") as f: + json.dump(payload, f) diff --git a/worlds/subnautica/Locations.py b/worlds/subnautica/Locations.py index 361a712ba8..3effd1eac3 100644 --- a/worlds/subnautica/Locations.py +++ b/worlds/subnautica/Locations.py @@ -1,12 +1,574 @@ -import json -import os +from typing import Dict, TypedDict, List -with open(os.path.join(os.path.dirname(__file__), 'locations.json'), 'r') as file: - location_table = json.loads(file.read()) -lookup_id_to_name = {} -for item in location_table: - lookup_id_to_name[item["id"]] = item["name"] +class Vector(TypedDict): + x: float + y: float + z: float -lookup_id_to_name[None] = "Neptune Launch" -lookup_name_to_id = {name: id for id, name in lookup_id_to_name.items()} + +class LocationDict(TypedDict, total=False): + name: str + can_slip_through: bool + need_laser_cutter: bool + position: Vector + need_propulsion_cannon: bool + + +events: List[str] = ["Neptune Launch", "Disable Quarantine", "Full Infection", "Repair Aurora Drive"] + +location_table: Dict[int, LocationDict] = { + 33000: {'can_slip_through': False, + 'name': 'Blood Kelp Trench Wreck - Outside Databox', + 'need_laser_cutter': False, + 'position': {'x': -1234.3, 'y': -349.7, 'z': -396.0}}, + 33001: {'can_slip_through': False, + 'name': 'Blood Kelp Trench Wreck - Inside Databox', + 'need_laser_cutter': False, + 'position': {'x': -1208.0, 'y': -349.6, 'z': -383.0}}, + 33002: {'can_slip_through': False, + 'name': 'Blood Kelp Trench Wreck - PDA', + 'need_laser_cutter': False, + 'position': {'x': -1210.6, 'y': -340.7, 'z': -393.4}}, + 33003: {'can_slip_through': False, + 'name': 'Bulb Zone West Wreck - Outside Databox', + 'need_laser_cutter': False, + 'position': {'x': 903.8, 'y': -220.3, 'z': 590.9}}, + 33004: {'can_slip_through': False, + 'name': 'Bulb Zone West Wreck - Under Databox', + 'need_laser_cutter': False, + 'position': {'x': 910.9, 'y': -201.8, 'z': 623.5}}, + 33005: {'can_slip_through': False, + 'name': 'Bulb Zone West Wreck - Inside Databox', + 'need_laser_cutter': True, + 'position': {'x': 914.9, 'y': -202.1, 'z': 611.8}}, + 33006: {'can_slip_through': False, + 'name': 'Bulb Zone West Wreck - PDA', + 'need_laser_cutter': True, + 'position': {'x': 912.6, 'y': -202.0, 'z': 609.5}}, + 33007: {'can_slip_through': False, + 'name': 'Bulb Zone East Wreck - Databox', + 'need_laser_cutter': False, + 'position': {'x': 1327.1, 'y': -234.9, 'z': 575.8}}, + 33008: {'can_slip_through': False, + 'name': 'Dunes North Wreck - Outside Databox', + 'need_laser_cutter': False, + 'position': {'x': -1407.7, 'y': -344.2, 'z': 721.5}}, + 33009: {'can_slip_through': False, + 'name': 'Dunes North Wreck - Office Databox', + 'need_laser_cutter': False, + 'position': {'x': -1393.9, 'y': -329.7, 'z': 733.5}}, + 33010: {'can_slip_through': False, + 'name': 'Dunes North Wreck - PDA', + 'need_laser_cutter': False, + 'position': {'x': -1396.3, 'y': -330.8, 'z': 730.0}}, + 33011: {'can_slip_through': False, + 'name': 'Dunes North Wreck - Cargo Databox', + 'need_laser_cutter': True, + 'position': {'x': -1409.8, 'y': -332.4, 'z': 706.9}}, + 33012: {'can_slip_through': False, + 'name': 'Dunes West Wreck - Databox', + 'need_laser_cutter': False, + 'position': {'x': -1626.2, 'y': -357.5, 'z': 99.5}}, + 33013: {'can_slip_through': False, + 'name': 'Dunes East Wreck - Outside Databox', + 'need_laser_cutter': False, + 'position': {'x': -1196.3, 'y': -223.0, 'z': 12.5}}, + 33014: {'can_slip_through': False, + 'name': 'Dunes East Wreck - Inside Databox', + 'need_laser_cutter': False, + 'position': {'x': -1206.4, 'y': -225.6, 'z': 4.0}}, + 33015: {'can_slip_through': False, + 'name': 'Grand Reef North Wreck - Outside Databox', + 'need_laser_cutter': False, + 'position': {'x': -269.7, 'y': -262.8, 'z': -764.3}}, + 33016: {'can_slip_through': False, + 'name': 'Grand Reef North Wreck - Elevator Databox', + 'need_laser_cutter': True, + 'position': {'x': -285.8, 'y': -240.2, 'z': -786.5}}, + 33017: {'can_slip_through': False, + 'name': 'Grand Reef North Wreck - Bottom Databox', + 'need_laser_cutter': False, + 'position': {'x': -285.2, 'y': -262.4, 'z': -788.4}}, + 33018: {'can_slip_through': False, + 'name': 'Grand Reef North Wreck - Hangar PDA', + 'need_laser_cutter': False, + 'position': {'x': -272.5, 'y': -254.7, 'z': -788.5}}, + 33019: {'can_slip_through': False, + 'name': 'Grand Reef South Wreck - Trench Databox', + 'need_laser_cutter': False, + 'position': {'x': -850.9, 'y': -473.2, 'z': -1414.6}}, + 33020: {'can_slip_through': False, + 'name': 'Grand Reef South Wreck - Comms Databox', + 'need_laser_cutter': True, + 'position': {'x': -889.4, 'y': -433.8, 'z': -1424.8}}, + 33021: {'can_slip_through': False, + 'name': 'Grand Reef South Wreck - Outside Databox', + 'need_laser_cutter': False, + 'position': {'x': -862.4, 'y': -437.5, 'z': -1444.1}}, + 33022: {'can_slip_through': False, + 'name': 'Grand Reef South Wreck - PDA', + 'need_laser_cutter': False, + 'position': {'x': -887.9, 'y': -446.0, 'z': -1422.7}}, + 33023: {'can_slip_through': False, + 'name': 'Grassy Plateaus South Wreck - Databox', + 'need_laser_cutter': False, + 'position': {'x': -23.3, 'y': -105.8, 'z': -604.2}}, + 33024: {'can_slip_through': False, + 'name': 'Grassy Plateaus South Wreck - PDA', + 'need_laser_cutter': False, + 'position': {'x': -27.3, 'y': -106.8, 'z': -607.2}}, + 33025: {'can_slip_through': True, + 'name': 'Grassy Plateaus East Wreck - Breach Databox', + 'need_laser_cutter': True, + 'position': {'x': 313.9, 'y': -91.8, 'z': 432.6}}, + 33026: {'can_slip_through': True, + 'name': 'Grassy Plateaus East Wreck - Hangar Databox', + 'need_laser_cutter': True, + 'position': {'x': 319.4, 'y': -104.3, 'z': 441.5}}, + 33027: {'can_slip_through': False, + 'name': 'Grassy Plateaus West Wreck - Locker PDA', + 'need_laser_cutter': False, + 'position': {'x': -632.3, 'y': -75.0, 'z': -8.9}}, + 33028: {'can_slip_through': False, + 'name': 'Grassy Plateaus West Wreck - Data Terminal', + 'need_laser_cutter': False, + 'position': {'x': -664.4, 'y': -97.8, 'z': -8.0}}, + 33029: {'can_slip_through': False, + 'name': 'Grassy Plateaus West Wreck - Databox', + 'need_laser_cutter': True, + 'position': {'x': -421.4, 'y': -107.8, 'z': -266.5}}, + 33030: {'can_slip_through': False, + 'name': 'Safe Shallows Wreck - PDA', + 'need_laser_cutter': False, + 'position': {'x': -44.0, 'y': -29.1, 'z': -403.6}}, + 33031: {'can_slip_through': False, + 'name': 'Kelp Forest Wreck - Databox', + 'need_laser_cutter': False, + 'position': {'x': -317.6, 'y': -78.8, 'z': 247.4}}, + 33032: {'can_slip_through': False, + 'name': 'Kelp Forest Wreck - PDA', + 'need_laser_cutter': False, + 'position': {'x': 63.2, 'y': -38.5, 'z': 382.9}}, + 33033: {'can_slip_through': False, + 'name': 'Mountains West Wreck - Outside Databox', + 'need_laser_cutter': False, + 'position': {'x': 740.3, 'y': -389.2, 'z': 1179.8}}, + 33034: {'can_slip_through': False, + 'name': 'Mountains West Wreck - Data Terminal', + 'need_laser_cutter': True, + 'position': {'x': 703.7, 'y': -365.9, 'z': 1199.3}}, + 33035: {'can_slip_through': False, + 'name': 'Mountains West Wreck - Hangar Databox', + 'need_laser_cutter': True, + 'position': {'x': 698.2, 'y': -350.8, 'z': 1186.9}}, + 33036: {'can_slip_through': False, + 'name': 'Mountains West Wreck - Office Databox', + 'need_laser_cutter': False, + 'position': {'x': 676.3, 'y': -343.6, 'z': 1204.6}}, + 33037: {'can_slip_through': False, + 'name': 'Mountains East Wreck - Comms Databox', + 'need_laser_cutter': False, + 'position': {'x': 1068.5, 'y': -283.4, 'z': 1345.3}}, + 33038: {'can_slip_through': False, + 'name': 'Mountains East Wreck - Outside Databox', + 'need_laser_cutter': False, + 'position': {'x': 1075.7, 'y': -288.9, 'z': 1321.8}}, + 33039: {'can_slip_through': False, + 'name': 'Northwestern Mushroom Forest Wreck - Cargo Databox', + 'need_laser_cutter': True, + 'position': {'x': -655.1, 'y': -109.6, 'z': 791.0}}, + 33040: {'can_slip_through': False, + 'name': 'Northwestern Mushroom Forest Wreck - Office Databox', + 'need_laser_cutter': False, + 'position': {'x': -663.4, 'y': -111.9, 'z': 777.9}}, + 33041: {'can_slip_through': False, + 'name': 'Northwestern Mushroom Forest Wreck - PDA', + 'need_laser_cutter': False, + 'position': {'x': -662.2, 'y': -113.4, 'z': 777.7}}, + 33042: {'can_slip_through': False, + 'name': "Sea Treader's Path Wreck - Outside Databox", + 'need_laser_cutter': False, + 'position': {'x': -1161.1, 'y': -191.7, 'z': -758.3}}, + 33043: {'can_slip_through': False, + 'name': "Sea Treader's Path Wreck - Hangar Databox", + 'need_laser_cutter': True, + 'position': {'x': -1129.5, 'y': -155.2, 'z': -729.3}}, + 33044: {'can_slip_through': False, + 'name': "Sea Treader's Path Wreck - Lobby Databox", + 'need_laser_cutter': False, + 'position': {'x': -1115.9, 'y': -175.3, 'z': -724.5}}, + 33045: {'can_slip_through': False, + 'name': "Sea Treader's Path Wreck - PDA", + 'need_laser_cutter': False, + 'position': {'x': -1136.8, 'y': -157.0, 'z': -734.6}}, + 33046: {'can_slip_through': False, + 'name': 'Sparse Reef Wreck - Locker Databox', + 'need_laser_cutter': True, + 'position': {'x': -789.8, 'y': -216.1, 'z': -711.0}}, + 33047: {'can_slip_through': False, + 'name': 'Sparse Reef Wreck - Outside Databox', + 'need_laser_cutter': False, + 'position': {'x': -810.7, 'y': -209.3, 'z': -685.5}}, + 33048: {'can_slip_through': False, + 'name': 'Sparse Reef Wreck - Lab Databox', + 'need_laser_cutter': True, + 'position': {'x': -795.5, 'y': -204.1, 'z': -774.7}}, + 33049: {'can_slip_through': False, + 'name': 'Underwater Islands Wreck - Outside Databox', + 'need_laser_cutter': False, + 'position': {'x': -170.8, 'y': -187.6, 'z': 880.7}}, + 33050: {'can_slip_through': False, + 'name': 'Underwater Islands Wreck - Hangar Databox', + 'need_laser_cutter': True, + 'position': {'x': -138.4, 'y': -193.6, 'z': 888.7}}, + 33051: {'can_slip_through': False, + 'name': 'Underwater Islands Wreck - Data Terminal', + 'need_laser_cutter': True, + 'position': {'x': -130.7, 'y': -193.2, 'z': 883.3}}, + 33052: {'can_slip_through': False, + 'name': 'Underwater Islands Wreck - Cable Databox', + 'need_laser_cutter': False, + 'position': {'x': -137.8, 'y': -193.4, 'z': 879.4}}, + 33053: {'can_slip_through': False, + 'name': 'Underwater Islands Wreck - Pipes Databox 1', + 'need_laser_cutter': False, + 'need_propulsion_cannon': True, + 'position': {'x': -124.4, 'y': -200.7, 'z': 853.0}}, + 33054: {'can_slip_through': False, + 'name': 'Underwater Islands Wreck - Pipes Databox 2', + 'need_laser_cutter': False, + 'need_propulsion_cannon': True, + 'position': {'x': -126.8, 'y': -201.1, 'z': 852.1}}, + 33055: {'can_slip_through': False, + 'name': 'Degasi Seabase - Deep Grand Reef - Bedroom Databox', + 'need_laser_cutter': False, + 'position': {'x': -643.8, 'y': -509.9, 'z': -941.9}}, + 33056: {'can_slip_through': False, + 'name': 'Degasi Seabase - Deep Grand Reef - Observatory Databox', + 'need_laser_cutter': False, + 'position': {'x': -635.1, 'y': -502.7, 'z': -951.4}}, + 33057: {'can_slip_through': False, + 'name': 'Degasi Seabase - Deep Grand Reef - Bedroom PDA', + 'need_laser_cutter': False, + 'position': {'x': -645.8, 'y': -508.7, 'z': -943.0}}, + 33058: {'can_slip_through': False, + 'name': 'Degasi Seabase - Deep Grand Reef - Outside PDA', + 'need_laser_cutter': False, + 'position': {'x': -630.5, 'y': -511.1, 'z': -936.1}}, + 33059: {'can_slip_through': False, + 'name': 'Degasi Seabase - Deep Grand Reef - Observatory PDA', + 'need_laser_cutter': False, + 'position': {'x': -647.7, 'y': -502.6, 'z': -935.8}}, + 33060: {'can_slip_through': False, + 'name': 'Degasi Seabase - Deep Grand Reef - Lab PDA', + 'need_laser_cutter': False, + 'position': {'x': -639.6, 'y': -505.9, 'z': -946.6}}, + 33061: {'can_slip_through': False, + 'name': 'Floating Island - Lake PDA', + 'need_laser_cutter': False, + 'position': {'x': -707.2, 'y': 0.5, 'z': -1096.7}}, + 33062: {'can_slip_through': False, + 'name': 'Degasi Seabase - Floating Island - Databox', + 'need_laser_cutter': False, + 'position': {'x': -765.7, 'y': 17.6, 'z': -1116.4}}, + 33063: {'can_slip_through': False, + 'name': 'Degasi Seabase - Floating Island - Room PDA', + 'need_laser_cutter': False, + 'position': {'x': -754.9, 'y': 14.6, 'z': -1108.9}}, + 33064: {'can_slip_through': False, + 'name': 'Degasi Seabase - Floating Island - Green Wall PDA', + 'need_laser_cutter': False, + 'position': {'x': -765.3, 'y': 14.1, 'z': -1115.0}}, + 33065: {'can_slip_through': False, + 'name': 'Degasi Seabase - Floating Island - Corridor PDA', + 'need_laser_cutter': False, + 'position': {'x': -758.6, 'y': 14.1, 'z': -1111.3}}, + 33066: {'can_slip_through': False, + 'name': 'Degasi Seabase - Floating Island - North Observatory PDA', + 'need_laser_cutter': False, + 'position': {'x': -805.4, 'y': 76.9, 'z': -1055.7}}, + 33067: {'can_slip_through': False, + 'name': 'Degasi Seabase - Floating Island - South Observatory PDA', + 'need_laser_cutter': False, + 'position': {'x': -715.9, 'y': 75.4, 'z': -1168.8}}, + 33068: {'can_slip_through': False, + 'name': 'Jellyshroom Cave - PDA', + 'need_laser_cutter': False, + 'position': {'x': -540.5, 'y': -250.8, 'z': -83.4}}, + 33069: {'can_slip_through': False, + 'name': 'Degasi Seabase - Jellyshroom Cave - Bedroom Databox', + 'need_laser_cutter': False, + 'position': {'x': 110.6, 'y': -264.9, 'z': -369.0}}, + 33070: {'can_slip_through': False, + 'name': 'Degasi Seabase - Jellyshroom Cave - Detached PDA', + 'need_laser_cutter': False, + 'position': {'x': 80.6, 'y': -268.6, 'z': -358.3}}, + 33071: {'can_slip_through': False, + 'name': 'Degasi Seabase - Jellyshroom Cave - Office PDA', + 'need_laser_cutter': False, + 'position': {'x': 78.2, 'y': -265.0, 'z': -373.4}}, + 33072: {'can_slip_through': False, + 'name': 'Degasi Seabase - Jellyshroom Cave - Locker PDA', + 'need_laser_cutter': False, + 'position': {'x': 85.1, 'y': -264.1, 'z': -372.8}}, + 33073: {'can_slip_through': False, + 'name': 'Degasi Seabase - Jellyshroom Cave - Bedroom PDA', + 'need_laser_cutter': False, + 'position': {'x': 112.3, 'y': -264.9, 'z': -369.3}}, + 33074: {'can_slip_through': False, + 'name': 'Degasi Seabase - Jellyshroom Cave - Observatory PDA', + 'need_laser_cutter': False, + 'position': {'x': 95.5, 'y': -258.9, 'z': -366.5}}, + 33075: {'can_slip_through': False, + 'name': 'Lifepod 2 - Databox', + 'need_laser_cutter': False, + 'position': {'x': -483.6, 'y': -504.7, 'z': 1326.6}}, + 33076: {'can_slip_through': False, + 'name': 'Lifepod 2 - PDA', + 'need_laser_cutter': False, + 'position': {'x': -481.4, 'y': -503.6, 'z': 1324.1}}, + 33077: {'can_slip_through': False, + 'name': 'Lifepod 3 - Databox', + 'need_laser_cutter': False, + 'position': {'x': -34.2, 'y': -22.4, 'z': 410.5}}, + 33078: {'can_slip_through': False, + 'name': 'Lifepod 3 - PDA', + 'need_laser_cutter': False, + 'position': {'x': -33.8, 'y': -22.5, 'z': 408.8}}, + 33079: {'can_slip_through': False, + 'name': 'Lifepod 4 - Databox', + 'need_laser_cutter': False, + 'position': {'x': 712.4, 'y': -3.4, 'z': 160.8}}, + 33080: {'can_slip_through': False, + 'name': 'Lifepod 4 - PDA', + 'need_laser_cutter': False, + 'position': {'x': 712.0, 'y': -3.5, 'z': 161.5}}, + 33081: {'can_slip_through': False, + 'name': 'Lifepod 6 - Databox', + 'need_laser_cutter': False, + 'position': {'x': 358.7, 'y': -117.1, 'z': 306.8}}, + 33082: {'can_slip_through': False, + 'name': 'Lifepod 6 - Inside PDA', + 'need_laser_cutter': False, + 'position': {'x': 361.8, 'y': -116.2, 'z': 309.5}}, + 33083: {'can_slip_through': False, + 'name': 'Lifepod 6 - Outside PDA', + 'need_laser_cutter': False, + 'position': {'x': 359.9, 'y': -117.0, 'z': 312.1}}, + 33084: {'can_slip_through': False, + 'name': 'Lifepod 7 - PDA', + 'need_laser_cutter': False, + 'position': {'x': -56.0, 'y': -182.0, 'z': -1039.0}}, + 33085: {'can_slip_through': False, + 'name': 'Lifepod 12 - Databox', + 'need_laser_cutter': False, + 'position': {'x': 1119.5, 'y': -271.7, 'z': 561.7}}, + 33086: {'can_slip_through': False, + 'name': 'Lifepod 12 - PDA', + 'need_laser_cutter': False, + 'position': {'x': 1116.1, 'y': -271.3, 'z': 566.9}}, + 33087: {'can_slip_through': False, + 'name': 'Lifepod 13 - Databox', + 'need_laser_cutter': False, + 'position': {'x': -926.4, 'y': -185.2, 'z': 501.8}}, + 33088: {'can_slip_through': False, + 'name': 'Lifepod 13 - PDA', + 'need_laser_cutter': False, + 'position': {'x': -926.8, 'y': -184.4, 'z': 506.6}}, + 33089: {'can_slip_through': False, + 'name': 'Lifepod 17 - PDA', + 'need_laser_cutter': False, + 'position': {'x': -514.5, 'y': -98.1, 'z': -56.5}}, + 33090: {'can_slip_through': False, + 'name': 'Lifepod 19 - Databox', + 'need_laser_cutter': False, + 'position': {'x': -809.8, 'y': -302.2, 'z': -876.9}}, + 33091: {'can_slip_through': False, + 'name': 'Lifepod 19 - Outside PDA', + 'need_laser_cutter': False, + 'position': {'x': -806.1, 'y': -294.1, 'z': -866.0}}, + 33092: {'can_slip_through': False, + 'name': 'Lifepod 19 - Inside PDA', + 'need_laser_cutter': False, + 'position': {'x': -810.5, 'y': -299.4, 'z': -873.1}}, + 33093: {'can_slip_through': False, + 'name': 'Aurora Seamoth Bay - Upgrade Console', + 'need_laser_cutter': False, + 'need_propulsion_cannon': True, + 'position': {'x': 903.5, 'y': -0.2, 'z': 16.1}}, + 33094: {'can_slip_through': False, + 'name': 'Aurora Drive Room - Upgrade Console', + 'need_laser_cutter': False, + 'need_propulsion_cannon': True, + 'position': {'x': 872.5, 'y': 2.7, 'z': -0.7}}, + 33095: {'can_slip_through': False, + 'name': 'Aurora Prawn Suit Bay - Upgrade Console', + 'need_laser_cutter': True, + 'need_propulsion_cannon': True, + 'position': {'x': 991.6, 'y': 3.2, 'z': -31.0}}, + 33096: {'can_slip_through': False, + 'name': 'Aurora - Office PDA', + 'need_laser_cutter': False, + 'position': {'x': 952.1, 'y': 41.2, 'z': 113.9}}, + 33097: {'can_slip_through': False, + 'name': 'Aurora - Corridor PDA', + 'need_laser_cutter': False, + 'position': {'x': 977.2, 'y': 39.1, 'z': 83.0}}, + 33098: {'can_slip_through': False, + 'name': 'Aurora - Cargo Bay PDA', + 'need_laser_cutter': False, + 'need_propulsion_cannon': True, + 'position': {'x': 954.9, 'y': 11.2, 'z': 3.4}}, + 33099: {'can_slip_through': False, + 'name': 'Aurora - Seamoth Bay PDA', + 'need_laser_cutter': False, + 'need_propulsion_cannon': True, + 'position': {'x': 907.1, 'y': -1.5, 'z': 15.3}}, + 33100: {'can_slip_through': False, + 'name': 'Aurora - Medkit Locker PDA', + 'need_laser_cutter': True, + 'need_propulsion_cannon': True, + 'position': {'x': 951.8, 'y': -2.3, 'z': -34.7}}, + 33101: {'can_slip_through': False, + 'name': 'Aurora - Locker PDA', + 'need_laser_cutter': True, + 'need_propulsion_cannon': True, + 'position': {'x': 952.0, 'y': -3.7, 'z': -23.4}}, + 33102: {'can_slip_through': False, + 'name': 'Aurora - Canteen PDA', + 'need_laser_cutter': True, + 'need_propulsion_cannon': True, + 'position': {'x': 986.5, 'y': 9.6, 'z': -48.6}}, + 33103: {'can_slip_through': False, + 'name': 'Aurora - Cabin 4 PDA', + 'need_laser_cutter': True, + 'need_propulsion_cannon': True, + 'position': {'x': 951.3, 'y': 11.2, 'z': -51.0}}, + 33104: {'can_slip_through': False, + 'name': 'Aurora - Cabin 7 PDA', + 'need_laser_cutter': True, + 'need_propulsion_cannon': True, + 'position': {'x': 967.1, 'y': 10.4, 'z': -47.4}}, + 33105: {'can_slip_through': False, + 'name': 'Aurora - Cabin 1 PDA', + 'need_laser_cutter': True, + 'need_propulsion_cannon': True, + 'position': {'x': 964.1, 'y': 11.1, 'z': -61.9}}, + 33106: {'can_slip_through': False, + 'name': 'Aurora - Captain PDA', + 'need_laser_cutter': True, + 'need_propulsion_cannon': True, + 'position': {'x': 971.2, 'y': 10.8, 'z': -70.4}}, + 33107: {'can_slip_through': False, + 'name': 'Aurora - Ring PDA', + 'need_laser_cutter': False, + 'need_propulsion_cannon': True, + 'position': {'x': 1033.6, 'y': -8.5, 'z': 16.2}}, + 33108: {'can_slip_through': False, + 'name': 'Aurora - Lab PDA', + 'need_laser_cutter': False, + 'need_propulsion_cannon': True, + 'position': {'x': 1032.5, 'y': -7.8, 'z': 32.4}}, + 33109: {'can_slip_through': False, + 'name': 'Aurora - Office Data Terminal', + 'need_laser_cutter': False, + 'position': {'x': 945.8, 'y': 40.8, 'z': 115.1}}, + 33110: {'can_slip_through': False, + 'name': 'Aurora - Captain Data Terminal', + 'need_laser_cutter': True, + 'need_propulsion_cannon': True, + 'position': {'x': 974.8, 'y': 10.0, 'z': -77.0}}, + 33111: {'can_slip_through': False, + 'name': 'Aurora - Battery Room Data Terminal', + 'need_laser_cutter': True, + 'need_propulsion_cannon': True, + 'position': {'x': 1040.8, 'y': -11.4, 'z': -3.4}}, + 33112: {'can_slip_through': False, + 'name': 'Aurora - Lab Data Terminal', + 'need_laser_cutter': False, + 'need_propulsion_cannon': True, + 'position': {'x': 1029.5, 'y': -8.7, 'z': 35.9}}, + 33113: {'can_slip_through': False, + 'name': "Quarantine Enforcement Platform's - Upper Alien Data " + 'Terminal', + 'need_laser_cutter': False, + 'position': {'x': 432.2, 'y': 3.0, 'z': 1193.2}}, + 33114: {'can_slip_through': False, + 'name': "Quarantine Enforcement Platform's - Mid Alien Data Terminal", + 'need_laser_cutter': False, + 'position': {'x': 474.4, 'y': -4.5, 'z': 1224.4}}, + 33115: {'can_slip_through': False, + 'name': 'Dunes Sanctuary - Alien Data Terminal', + 'need_laser_cutter': False, + 'position': {'x': -1224.2, 'y': -400.4, 'z': 1057.9}}, + 33116: {'can_slip_through': False, + 'name': 'Deep Sparse Reef Sanctuary - Alien Data Terminal', + 'need_laser_cutter': False, + 'position': {'x': -895.5, 'y': -311.6, 'z': -838.1}}, + 33117: {'can_slip_through': False, + 'name': 'Northern Blood Kelp Zone Sanctuary - Alien Data Terminal', + 'need_laser_cutter': False, + 'position': {'x': -642.9, 'y': -563.5, 'z': 1485.5}}, + 33118: {'can_slip_through': False, + 'name': 'Lost River Laboratory Cache - Alien Data Terminal', + 'need_laser_cutter': False, + 'position': {'x': -1112.3, 'y': -687.3, 'z': -695.5}}, + 33119: {'can_slip_through': False, + 'name': 'Disease Research Facility - Upper Alien Data Terminal', + 'need_laser_cutter': False, + 'position': {'x': -280.2, 'y': -804.3, 'z': 305.1}}, + 33120: {'can_slip_through': False, + 'name': 'Disease Research Facility - Mid Alien Data Terminal', + 'need_laser_cutter': False, + 'position': {'x': -267.9, 'y': -806.6, 'z': 250.0}}, + 33121: {'can_slip_through': False, + 'name': 'Disease Research Facility - Lower Alien Data Terminal', + 'need_laser_cutter': False, + 'position': {'x': -286.2, 'y': -815.6, 'z': 297.8}}, + 33122: {'can_slip_through': False, + 'name': 'Alien Thermal Plant - Entrance Alien Data Terminal', + 'need_laser_cutter': False, + 'position': {'x': -71.3, 'y': -1227.2, 'z': 104.8}}, + 33123: {'can_slip_through': False, + 'name': 'Alien Thermal Plant - Green Alien Data Terminal', + 'need_laser_cutter': False, + 'position': {'x': -38.7, 'y': -1226.6, 'z': 111.8}}, + 33124: {'can_slip_through': False, + 'name': 'Alien Thermal Plant - Yellow Alien Data Terminal', + 'need_laser_cutter': False, + 'position': {'x': -30.4, 'y': -1220.3, 'z': 111.8}}, + 33125: {'can_slip_through': False, + 'name': "Primary Containment Facility's Antechamber - Alien Data " + 'Terminal', + 'need_laser_cutter': False, + 'position': {'x': 245.8, 'y': -1430.6, 'z': -311.5}}, + 33126: {'can_slip_through': False, + 'name': "Primary Containment Facility's Egg Laboratory - Alien Data " + 'Terminal', + 'need_laser_cutter': False, + 'position': {'x': 165.5, 'y': -1442.4, 'z': -385.8}}, + 33127: {'can_slip_through': False, + 'name': "Primary Containment Facility's Pipe Room - Alien Data " + 'Terminal', + 'need_laser_cutter': False, + 'position': {'x': 348.7, 'y': -1443.5, 'z': -291.9}}, + 33128: {'can_slip_through': False, + 'name': 'Grassy Plateaus West Wreck - Beam PDA', + 'need_laser_cutter': False, + 'position': {'x': -641.8, 'y': -111.3, 'z': -19.7}}, + 33129: {'can_slip_through': False, + 'name': 'Floating Island - Cave Entrance PDA', + 'need_laser_cutter': False, + 'position': {'x': -748.9, 'y': 14.4, 'z': -1179.5}}, + 33130: {'can_slip_through': False, + 'name': 'Degasi Seabase - Jellyshroom Cave - Outside PDA', + 'need_laser_cutter': False, + 'position': {'x': 83.2, 'y': -276.4, 'z': -345.5}}, +} +if False: # turn to True to export for Subnautica mod + payload = {location_id: location_data["position"] for location_id, location_data in location_table.items()} + import json + + with open("locations.json", "w") as f: + json.dump(payload, f) diff --git a/worlds/subnautica/Options.py b/worlds/subnautica/Options.py index 0695a08950..f9f3f56756 100644 --- a/worlds/subnautica/Options.py +++ b/worlds/subnautica/Options.py @@ -1,13 +1,52 @@ -from Options import Choice +from Options import Choice, Range, DeathLink +from .Creatures import all_creatures class ItemPool(Choice): - """Valuable item pool moves all not progression relevant items to starting inventory and - creates random duplicates of important items in their place.""" + """Valuable item pool leaves all filler items in their vanilla locations and + creates random duplicates of important items into freed spots.""" + display_name = "Item Pool" option_standard = 0 option_valuable = 1 +class Goal(Choice): + """Goal to complete. + Launch: Leave the planet. + Free: Disable quarantine. + Infected: Reach maximum infection level. + Drive: Repair the Aurora's Drive Core""" + auto_display_name = True + display_name = "Goal" + option_launch = 0 + option_free = 1 + option_infected = 2 + option_drive = 3 + + def get_event_name(self) -> str: + return { + self.option_launch: "Neptune Launch", + self.option_infected: "Full Infection", + self.option_free: "Disable Quarantine", + self.option_drive: "Repair Aurora Drive" + }[self.value] + + +class CreatureScans(Range): + """Place items on specific creature scans. + Warning: Includes aggressive Leviathans.""" + display_name = "Creature Scans" + range_end = len(all_creatures) + + +class SubnauticaDeathLink(DeathLink): + """When you die, everyone dies. Of course the reverse is true too. + Note: can be toggled via in-game console command "deathlink".""" + + options = { - "item_pool": ItemPool + "item_pool": ItemPool, + "goal": Goal, + "creature_scans": CreatureScans, + "death_link": SubnauticaDeathLink, } diff --git a/worlds/subnautica/Regions.py b/worlds/subnautica/Regions.py deleted file mode 100644 index 1eb0e12f61..0000000000 --- a/worlds/subnautica/Regions.py +++ /dev/null @@ -1,8 +0,0 @@ -def create_regions(world, player: int): - from . import create_region - from .Locations import lookup_name_to_id as location_lookup_name_to_id - - world.regions += [ - create_region(world, player, 'Menu', None, ['Lifepod 5']), - create_region(world, player, 'Planet 4546B', [location for location in location_lookup_name_to_id]) - ] diff --git a/worlds/subnautica/Rules.py b/worlds/subnautica/Rules.py index 8d5bcae457..b8f8f1a7b4 100644 --- a/worlds/subnautica/Rules.py +++ b/worlds/subnautica/Rules.py @@ -1,113 +1,122 @@ -from ..generic.Rules import set_rule -from .Locations import location_table -import logging +from typing import TYPE_CHECKING + +from worlds.generic.Rules import set_rule +from .Locations import location_table, LocationDict +from .Creatures import all_creatures, aggressive, suffix import math +if TYPE_CHECKING: + from . import SubnauticaWorld -def has_seaglide(state, player): + +def has_seaglide(state, player: int): return state.has("Seaglide Fragment", player, 2) -def has_modification_station(state, player): +def has_modification_station(state, player: int): return state.has("Modification Station Fragment", player, 3) -def has_mobile_vehicle_bay(state, player): +def has_mobile_vehicle_bay(state, player: int): return state.has("Mobile Vehicle Bay Fragment", player, 3) -def has_moonpool(state, player): +def has_moonpool(state, player: int): return state.has("Moonpool Fragment", player, 2) -def has_vehicle_upgrade_console(state, player): +def has_vehicle_upgrade_console(state, player: int): return state.has("Vehicle Upgrade Console", player) and \ has_moonpool(state, player) -def has_seamoth(state, player): +def has_seamoth(state, player: int): return state.has("Seamoth Fragment", player, 3) and \ has_mobile_vehicle_bay(state, player) -def has_seamoth_depth_module_mk1(state, player): +def has_seamoth_depth_module_mk1(state, player: int): return has_vehicle_upgrade_console(state, player) -def has_seamoth_depth_module_mk2(state, player): +def has_seamoth_depth_module_mk2(state, player: int): return has_seamoth_depth_module_mk1(state, player) and \ has_modification_station(state, player) -def has_seamoth_depth_module_mk3(state, player): +def has_seamoth_depth_module_mk3(state, player: int): return has_seamoth_depth_module_mk2(state, player) and \ has_modification_station(state, player) -def has_cyclops_bridge(state, player): +def has_cyclops_bridge(state, player: int): return state.has("Cyclops Bridge Fragment", player, 3) -def has_cyclops_engine(state, player): +def has_cyclops_engine(state, player: int): return state.has("Cyclops Engine Fragment", player, 3) -def has_cyclops_hull(state, player): +def has_cyclops_hull(state, player: int): return state.has("Cyclops Hull Fragment", player, 3) -def has_cyclops(state, player): +def has_cyclops(state, player: int): return has_cyclops_bridge(state, player) and \ has_cyclops_engine(state, player) and \ has_cyclops_hull(state, player) and \ has_mobile_vehicle_bay(state, player) -def has_cyclops_depth_module_mk1(state, player): +def has_cyclops_depth_module_mk1(state, player: int): return state.has("Cyclops Depth Module MK1", player) and \ has_modification_station(state, player) -def has_cyclops_depth_module_mk2(state, player): +def has_cyclops_depth_module_mk2(state, player: int): return has_cyclops_depth_module_mk1(state, player) and \ has_modification_station(state, player) -def has_cyclops_depth_module_mk3(state, player): +def has_cyclops_depth_module_mk3(state, player: int): return has_cyclops_depth_module_mk2(state, player) and \ has_modification_station(state, player) -def has_prawn(state, player): +def has_prawn(state, player: int): return state.has("Prawn Suit Fragment", player, 4) and \ has_mobile_vehicle_bay(state, player) -def has_praw_propulsion_arm(state, player): +def has_praw_propulsion_arm(state, player: int): return state.has("Prawn Suit Propulsion Cannon Fragment", player, 2) and \ has_vehicle_upgrade_console(state, player) -def has_prawn_depth_module_mk1(state, player): +def has_prawn_depth_module_mk1(state, player: int): return has_vehicle_upgrade_console(state, player) -def has_prawn_depth_module_mk2(state, player): +def has_prawn_depth_module_mk2(state, player: int): return has_prawn_depth_module_mk1(state, player) and \ has_modification_station(state, player) -def has_laser_cutter(state, player): +def has_laser_cutter(state, player: int): return state.has("Laser Cutter Fragment", player, 3) +def has_stasis_rile(state, player: int): + return state.has("Stasis Rifle Fragment", player, 2) + + # Either we have propulsion cannon, or prawn + propulsion cannon arm -def has_propulsion_cannon(state, player): +def has_propulsion_cannon(state, player: int): return state.has("Propulsion Cannon Fragment", player, 2) or \ (has_prawn(state, player) and has_praw_propulsion_arm(state, player)) -def has_cyclops_shield(state, player): +def has_cyclops_shield(state, player: int): return has_cyclops(state, player) and \ state.has("Cyclops Shield Generator", player) @@ -120,7 +129,7 @@ def has_cyclops_shield(state, player): # negligeable with from high capacity tank. 430m -> 460m # Fins are not used when using seaglide # -def get_max_swim_depth(state, player): +def get_max_swim_depth(state, player: int): # TODO, Make this a difficulty setting. # Only go up to 200m without any submarines for now. return 200 @@ -131,7 +140,7 @@ def get_max_swim_depth(state, player): # has_ultra_glide_fins = state.has("Ultra Glide Fins", player) # max_depth = 400 # More like 430m. Give some room - # if has_seaglide(state, player): + # if has_seaglide(state, player: int): # if has_ultra_high_capacity_tank: # max_depth = 750 # It's about 50m more. Give some room # else: @@ -147,7 +156,7 @@ def get_max_swim_depth(state, player): # return max_depth -def get_seamoth_max_depth(state, player): +def get_seamoth_max_depth(state, player: int): if has_seamoth(state, player): if has_seamoth_depth_module_mk3(state, player): return 900 @@ -187,7 +196,7 @@ def get_prawn_max_depth(state, player): return 0 -def get_max_depth(state, player): +def get_max_depth(state, player: int): # TODO, Difficulty option, we can add vehicle depth + swim depth # But at this point, we have to consider traver distance in caves, not # just depth @@ -197,54 +206,82 @@ def get_max_depth(state, player): get_prawn_max_depth(state, player)) -def can_access_location(state, player, loc): - pos_x = loc.get("position").get("x") - pos_y = loc.get("position").get("y") - pos_z = loc.get("position").get("z") - depth = -pos_y # y-up - map_center_dist = math.sqrt(pos_x ** 2 + pos_z ** 2) - aurora_dist = math.sqrt((pos_x - 1038.0) ** 2 + (pos_y - -3.4) ** 2 + (pos_z - -163.1) ** 2) - - need_radiation_suit = aurora_dist < 950 +def can_access_location(state, player: int, loc: LocationDict) -> bool: need_laser_cutter = loc.get("need_laser_cutter", False) - need_propulsion_cannon = loc.get("need_propulsion_cannon", False) - if need_laser_cutter and not has_laser_cutter(state, player): return False - if need_radiation_suit and not state.has("Radiation Suit", player): + need_propulsion_cannon = loc.get("need_propulsion_cannon", False) + if need_propulsion_cannon and not has_propulsion_cannon(state, player): return False - if need_propulsion_cannon and not has_propulsion_cannon(state, player): + pos = loc["position"] + pos_x = pos["x"] + pos_y = pos["y"] + pos_z = pos["z"] + + aurora_dist = math.sqrt((pos_x - 1038.0) ** 2 + (pos_y - -3.4) ** 2 + (pos_z - -163.1) ** 2) + need_radiation_suit = aurora_dist < 950 + if need_radiation_suit and not state.has("Radiation Suit", player): return False # Seaglide doesn't unlock anything specific, but just allows for faster movement. # Otherwise the game is painfully slow. + map_center_dist = math.sqrt(pos_x ** 2 + pos_z ** 2) if (map_center_dist > 800 or pos_y < -200) and not has_seaglide(state, player): return False + depth = -pos_y # y-up return get_max_depth(state, player) >= depth -def set_location_rule(world, player, loc): +def set_location_rule(world, player: int, loc: LocationDict): set_rule(world.get_location(loc["name"], player), lambda state: can_access_location(state, player, loc)) -def set_rules(world, player): - for loc in location_table: +def can_scan_creature(state, player: int, creature: str) -> bool: + if not has_seaglide(state, player): + return False + if creature in aggressive and not has_stasis_rile(state, player): + return False + return get_max_depth(state, player) >= all_creatures[creature] + + +def set_creature_rule(world, player, creature_name: str): + set_rule(world.get_location(creature_name + suffix, player), + lambda state: can_scan_creature(state, player, creature_name)) + + +def set_rules(subnautica_world: "SubnauticaWorld"): + player = subnautica_world.player + world = subnautica_world.world + + for loc in location_table.values(): set_location_rule(world, player, loc) - # Victory location - set_rule(world.get_location("Neptune Launch", player), lambda state: \ - get_max_depth(state, player) >= 1444 and \ - has_mobile_vehicle_bay(state, player) and \ - state.has('Neptune Launch Platform', player) and \ - state.has('Neptune Gantry', player) and \ - state.has('Neptune Boosters', player) and \ - state.has('Neptune Fuel Reserve', player) and \ - state.has('Neptune Cockpit', player) and \ - state.has('Ion Power Cell', player) and \ - state.has('Ion Battery', player) and \ + for creature_name in subnautica_world.creatures_to_scan: + set_creature_rule(world, player, creature_name) + + # Victory locations + set_rule(world.get_location("Neptune Launch", player), lambda state: + get_max_depth(state, player) >= 1444 and + has_mobile_vehicle_bay(state, player) and + state.has("Neptune Launch Platform", player) and + state.has("Neptune Gantry", player) and + state.has("Neptune Boosters", player) and + state.has("Neptune Fuel Reserve", player) and + state.has("Neptune Cockpit", player) and + state.has("Ion Power Cell", player) and + state.has("Ion Battery", player) and has_cyclops_shield(state, player)) - world.completion_condition[player] = lambda state: state.has('Victory', player) + set_rule(world.get_location("Disable Quarantine", player), lambda state: + get_max_depth(state, player) >= 1444) + + set_rule(world.get_location("Full Infection", player), lambda state: + get_max_depth(state, player) >= 900) + + room = world.get_location("Aurora Drive Room - Upgrade Console", player) + set_rule(world.get_location("Repair Aurora Drive", player), lambda state: room.can_reach(state)) + + world.completion_condition[player] = lambda state: state.has("Victory", player) diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index ae92331809..6fa064d53a 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -1,18 +1,17 @@ import logging +from typing import List, Dict, Any + +from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, RegionType +from worlds.AutoWorld import World, WebWorld +from . import Items +from . import Locations +from . import Creatures +from . import Options +from .Items import item_table +from .Rules import set_rules logger = logging.getLogger("Subnautica") -from .Locations import lookup_name_to_id as locations_lookup_name_to_id -from .Items import item_table, lookup_name_to_item, advancement_item_names -from .Items import lookup_name_to_id as items_lookup_name_to_id - -from .Regions import create_regions -from .Rules import set_rules -from .Options import options - -from BaseClasses import Region, Entrance, Location, MultiWorld, Item, Tutorial, ItemClassification, RegionType -from ..AutoWorld import World, WebWorld - class SubnaticaWeb(WebWorld): tutorials = [Tutorial( @@ -25,6 +24,10 @@ class SubnaticaWeb(WebWorld): )] +all_locations = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()} +all_locations.update(Creatures.creature_locations) + + class SubnauticaWorld(World): """ Subnautica is an undersea exploration game. Stranded on an alien world, you become infected by @@ -34,34 +37,56 @@ class SubnauticaWorld(World): game: str = "Subnautica" web = SubnaticaWeb() - item_name_to_id = items_lookup_name_to_id - location_name_to_id = locations_lookup_name_to_id - options = options + item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()} + location_name_to_id = all_locations + option_definitions = Options.options - data_version = 2 - required_client_version = (0, 1, 9) + data_version = 5 + required_client_version = (0, 3, 4) + + prefill_items: List[Item] + creatures_to_scan: List[str] + + def generate_early(self) -> None: + self.prefill_items = [ + self.create_item("Seaglide Fragment"), + self.create_item("Seaglide Fragment") + ] + self.creatures_to_scan = self.world.random.sample(Creatures.all_creatures_presorted, + self.world.creature_scans[self.player].value) + + def create_regions(self): + self.world.regions += [ + self.create_region("Menu", None, ["Lifepod 5"]), + self.create_region("Planet 4546B", + Locations.events + + [location["name"] for location in Locations.location_table.values()] + + [creature+Creatures.suffix for creature in self.creatures_to_scan]) + ] + + # refer to Rules.py + set_rules = set_rules def generate_basic(self): # Link regions - self.world.get_entrance('Lifepod 5', self.player).connect(self.world.get_region('Planet 4546B', self.player)) + self.world.get_entrance("Lifepod 5", self.player).connect(self.world.get_region("Planet 4546B", self.player)) # Generate item pool pool = [] neptune_launch_platform = None - extras = 0 - valuable = self.world.item_pool[self.player] == "valuable" - for item in item_table: + extras = self.world.creature_scans[self.player].value + valuable = self.world.item_pool[self.player] == Options.ItemPool.option_valuable + for item in item_table.values(): for i in range(item["count"]): subnautica_item = self.create_item(item["name"]) if item["name"] == "Neptune Launch Platform": neptune_launch_platform = subnautica_item - elif valuable and not item["progression"]: - self.world.push_precollected(subnautica_item) + elif valuable and ItemClassification.filler == item["classification"]: extras += 1 else: pool.append(subnautica_item) - for item_name in self.world.random.choices(sorted(advancement_item_names - {"Neptune Launch Platform"}), + for item_name in self.world.random.choices(sorted(Items.advancement_item_names - {"Neptune Launch Platform"}), k=extras): item = self.create_item(item_name) item.classification = ItemClassification.filler # as it's an extra, just fast-fill it somewhere @@ -72,39 +97,59 @@ class SubnauticaWorld(World): # Victory item self.world.get_location("Aurora - Captain Data Terminal", self.player).place_locked_item( neptune_launch_platform) - self.world.get_location("Neptune Launch", self.player).place_locked_item( - SubnauticaItem("Victory", ItemClassification.progression, None, player=self.player)) + for event in Locations.events: + self.world.get_location(event, self.player).place_locked_item( + SubnauticaItem(event, ItemClassification.progression, None, player=self.player)) + # make the goal event the victory "item" + self.world.get_location(self.world.goal[self.player].get_event_name(), self.player).item.name = "Victory" - def set_rules(self): - set_rules(self.world, self.player) + def fill_slot_data(self) -> Dict[str, Any]: + goal: Options.Goal = self.world.goal[self.player] + item_pool: Options.ItemPool = self.world.item_pool[self.player] + vanilla_tech: List[str] = [] + if item_pool == Options.ItemPool.option_valuable: + for item in Items.item_table.values(): + if item["classification"] == ItemClassification.filler: + vanilla_tech.append(item["tech_type"]) - def create_regions(self): - create_regions(self.world, self.player) + slot_data: Dict[str, Any] = { + "goal": goal.current_key, + "vanilla_tech": vanilla_tech, + "creatures_to_scan": self.creatures_to_scan, + "death_link": self.world.death_link[self.player].value, + } - def fill_slot_data(self): - slot_data = {} return slot_data def create_item(self, name: str) -> Item: - item = lookup_name_to_item[name] + item_id: int = self.item_name_to_id[name] + return SubnauticaItem(name, - ItemClassification.progression if item["progression"] else ItemClassification.filler, - item["id"], player=self.player) + item_table[item_id]["classification"], + item_id, player=self.player) + def create_region(self, name: str, locations=None, exits=None): + ret = Region(name, RegionType.Generic, name, self.player) + ret.world = self.world + if locations: + for location in locations: + loc_id = self.location_name_to_id.get(location, None) + location = SubnauticaLocation(self.player, location, loc_id, ret) + ret.locations.append(location) + if exits: + for region_exit in exits: + ret.exits.append(Entrance(self.player, region_exit, ret)) + return ret -def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): - ret = Region(name, RegionType.Generic, name, player) - ret.world = world - if locations: - for location in locations: - loc_id = locations_lookup_name_to_id.get(location, 0) - location = SubnauticaLocation(player, location, loc_id, ret) - ret.locations.append(location) - if exits: - for exit in exits: - ret.exits.append(Entrance(player, exit, ret)) + def get_pre_fill_items(self) -> List[Item]: + return self.prefill_items - return ret + def pre_fill(self) -> None: + reachable = self.world.get_reachable_locations(player=self.player) + self.world.random.shuffle(reachable) + items = self.prefill_items.copy() + for item in items: + reachable.pop().place_locked_item(item) class SubnauticaLocation(Location): @@ -112,4 +157,4 @@ class SubnauticaLocation(Location): class SubnauticaItem(Item): - game = "Subnautica" + game: str = "Subnautica" diff --git a/worlds/subnautica/docs/en_Subnautica.md b/worlds/subnautica/docs/en_Subnautica.md index f71e14b7fe..9a112aa596 100644 --- a/worlds/subnautica/docs/en_Subnautica.md +++ b/worlds/subnautica/docs/en_Subnautica.md @@ -16,8 +16,12 @@ The goal remains unchanged. Cure the plague, build the Neptune Escape Rocket, an ## What items and locations get shuffled? -Most of the technologies the player will need throughout the game will be shuffled. Location checks in Subnautica are -data pads and technology lockers. +Most of the technologies the player will need throughout the game will be shuffled. +Location checks in Subnautica are data pads and technology lockers. + +Optionally up to 50 Creatures to scan can be included as well, for each one added a random duplicate item is created. + +As playing without Seaglide can be daunting, 2 of your Fragments of it can always be found in these locations: Grassy Plateaus South Wreck - Databox, Grassy Plateaus South Wreck - PDA, Grassy Plateaus West Wreck - Locker PDA, Grassy Plateaus West Wreck - Data Terminal, Safe Shallows Wreck - PDA, Kelp Forest Wreck - Databox, Kelp Forest Wreck - PDA, Lifepod 3 - Databox, Lifepod 3 - PDA, Lifepod 17 - PDA, Grassy Plateaus West Wreck - Beam PDA. ## Which items can be in another player's world? diff --git a/worlds/subnautica/docs/setup_en.md b/worlds/subnautica/docs/setup_en.md index 39d292938d..665cb8b336 100644 --- a/worlds/subnautica/docs/setup_en.md +++ b/worlds/subnautica/docs/setup_en.md @@ -18,20 +18,26 @@ 4. Start Subnautica. You should see a Connect Menu in the topleft of your main Menu. +## Connecting + +Using the Connect Menu in Subnautica's Main Menu you enter your connection info to connect to an Archipelago Multiworld. +Menu points: + - Host: the full url that you're trying to connect to, such as `archipelago.gg:38281`. + - PlayerName: your name in the multiworld. Can also be called Slot Name and is the name you entered when creating your settings. + - Password: optional password, leave blank if no password was set. + +After the connection is made, start a new game. You should start to see Archipelago chat messages to appear, such as a message announcing that you joined the multiworld. + +## Resuming + +When loading a savegame it will automatically attempt to resume the connection that was active when the savegame was made. +If that connection information is no longer valid, such as if the server's IP and/or port has changed, the Connect Menu will reappear after loading. Use the Connect Menu before or after loading the savegame to connect to the new instance. + +Warning: Currently it is not checked if this is the correct multiworld belonging to that savegame, please ensure that yourself beforehand. + ## Troubleshooting -If you don't see the connect window check that you see a qmodmanager_log-Subnautica.txt in Subnautica, if not +If you don't see the Connect Menu within the Main Menu, check that you see a file named `qmodmanager_log-Subnautica.txt` in the Subnautica game directory. If not, QModManager4 is not correctly installed, otherwise open it and look -for `[Info : BepInEx] Loading [Archipelago 1.0.0.0]`, version number doesn't matter. If it doesn't show this, then +for `[Info : BepInEx] Loading [Archipelago`. If it doesn't show this, then QModManager4 didn't find the Archipelago mod, so check your paths. - -## Joining a MultiWorld Game - -1. In Host, enter the address of the server, such as archipelago.gg:38281, your server host should be able to tell you - this. - -2. In Password enter the server password if one exists, otherwise leave blank. - -3. In PlayerName enter your "name" field from the yaml, or website config. - -4. Hit Connect. If it says successfully authenticated you can now create a new savegame or resume the correct savegame. \ No newline at end of file diff --git a/worlds/subnautica/items.json b/worlds/subnautica/items.json deleted file mode 100644 index 2dc4b22575..0000000000 --- a/worlds/subnautica/items.json +++ /dev/null @@ -1,83 +0,0 @@ -[ - { "id": 35000, "count": 1, "progression": false, "tech_type": "Compass", "name": "Compass" }, - { "id": 35001, "count": 1, "progression": true, "tech_type": "PlasteelTank", "name": "Lightweight High Capacity Tank" }, - { "id": 35002, "count": 1, "progression": true, "tech_type": "BaseUpgradeConsole", "name": "Vehicle Upgrade Console" }, - { "id": 35003, "count": 1, "progression": true, "tech_type": "UltraGlideFins", "name": "Ultra Glide Fins" }, - { "id": 35004, "count": 1, "progression": false, "tech_type": "CyclopsSonarModule", "name": "Cyclops Sonar Upgrade" }, - { "id": 35005, "count": 1, "progression": false, "tech_type": "ReinforcedDiveSuit", "name": "Reinforced Dive Suit" }, - { "id": 35006, "count": 1, "progression": false, "tech_type": "CyclopsThermalReactorModule", "name": "Cyclops Thermal Reactor Module" }, - { "id": 35007, "count": 1, "progression": false, "tech_type": "Stillsuit", "name": "Stillsuit" }, - { "id": 35008, "count": 2, "progression": false, "tech_type": "BaseWaterParkFragment", "name": "Alien Containment Fragment" }, - { "id": 35009, "count": 1, "progression": false, "tech_type": "CyclopsDecoy", "name": "Creature Decoy" }, - { "id": 35010, "count": 1, "progression": false, "tech_type": "CyclopsFireSuppressionModule", "name": "Cyclops Fire Suppression System" }, - { "id": 35011, "count": 1, "progression": false, "tech_type": "SwimChargeFins", "name": "Swim Charge Fins" }, - { "id": 35012, "count": 1, "progression": false, "tech_type": "RepulsionCannon", "name": "Repulsion Cannon" }, - { "id": 35013, "count": 1, "progression": false, "tech_type": "CyclopsDecoyModule", "name": "Cyclops Decoy Tube Upgrade" }, - { "id": 35014, "count": 1, "progression": true, "tech_type": "CyclopsShieldModule", "name": "Cyclops Shield Generator" }, - { "id": 35015, "count": 1, "progression": true, "tech_type": "CyclopsHullModule1", "name": "Cyclops Depth Module MK1" }, - { "id": 35016, "count": 1, "progression": false, "tech_type": "CyclopsSeamothRepairModule", "name": "Cyclops Docking Bay Repair Module" }, - { "id": 35017, "count": 2, "progression": false, "tech_type": "BatteryChargerFragment", "name": "Battery Charger fragment" }, - { "id": 35018, "count": 2, "progression": false, "tech_type": "BeaconFragment", "name": "Beacon Fragment" }, - { "id": 35019, "count": 2, "progression": false, "tech_type": "BaseBioReactorFragment", "name": "Bioreactor Fragment" }, - { "id": 35020, "count": 3, "progression": true, "tech_type": "CyclopsBridgeFragment", "name": "Cyclops Bridge Fragment" }, - { "id": 35021, "count": 3, "progression": true, "tech_type": "CyclopsEngineFragment", "name": "Cyclops Engine Fragment" }, - { "id": 35022, "count": 3, "progression": true, "tech_type": "CyclopsHullFragment", "name": "Cyclops Hull Fragment" }, - { "id": 35023, "count": 2, "progression": false, "tech_type": "GravSphereFragment", "name": "Grav Trap Fragment" }, - { "id": 35024, "count": 3, "progression": true, "tech_type": "LaserCutterFragment", "name": "Laser Cutter Fragment" }, - { "id": 35025, "count": 1, "progression": false, "tech_type": "TechlightFragment", "name": "Light Stick Fragment" }, - { "id": 35026, "count": 3, "progression": true, "tech_type": "ConstructorFragment", "name": "Mobile Vehicle Bay Fragment" }, - { "id": 35027, "count": 3, "progression": true, "tech_type": "WorkbenchFragment", "name": "Modification Station Fragment" }, - { "id": 35028, "count": 2, "progression": true, "tech_type": "MoonpoolFragment", "name": "Moonpool Fragment" }, - { "id": 35029, "count": 3, "progression": false, "tech_type": "BaseNuclearReactorFragment", "name": "Nuclear Reactor Fragment" }, - { "id": 35030, "count": 2, "progression": false, "tech_type": "PowerCellChargerFragment", "name": "Power Cell Charger Fragment" }, - { "id": 35031, "count": 1, "progression": false, "tech_type": "PowerTransmitterFragment", "name": "Power Transmitter Fragment" }, - { "id": 35032, "count": 4, "progression": true, "tech_type": "ExosuitFragment", "name": "Prawn Suit Fragment" }, - { "id": 35033, "count": 2, "progression": false, "tech_type": "ExosuitDrillArmFragment", "name": "Prawn Suit Drill Arm Fragment" }, - { "id": 35034, "count": 2, "progression": false, "tech_type": "ExosuitGrapplingArmFragment", "name": "Prawn Suit Grappling Arm Fragment" }, - { "id": 35035, "count": 2, "progression": false, "tech_type": "ExosuitPropulsionArmFragment", "name": "Prawn Suit Propulsion Cannon Fragment" }, - { "id": 35036, "count": 2, "progression": false, "tech_type": "ExosuitTorpedoArmFragment", "name": "Prawn Suit Torpedo Arm Fragment" }, - { "id": 35037, "count": 3, "progression": false, "tech_type": "BaseMapRoomFragment", "name": "Scanner Room Fragment" }, - { "id": 35038, "count": 5, "progression": true, "tech_type": "SeamothFragment", "name": "Seamoth Fragment" }, - { "id": 35039, "count": 2, "progression": false, "tech_type": "StasisRifleFragment", "name": "Stasis Rifle Fragment" }, - { "id": 35040, "count": 2, "progression": false, "tech_type": "ThermalPlantFragment", "name": "Thermal Plant Fragment" }, - { "id": 35041, "count": 4, "progression": true, "tech_type": "SeaglideFragment", "name": "Seaglide Fragment" }, - { "id": 35042, "count": 1, "progression": true, "tech_type": "RadiationSuit", "name": "Radiation Suit" }, - { "id": 35043, "count": 2, "progression": true, "tech_type": "PropulsionCannonFragment", "name": "Propulsion Cannon Fragment" }, - { "id": 35044, "count": 1, "progression": true, "tech_type": "RocketBase", "name": "Neptune Launch Platform" }, - { "id": 35045, "count": 1, "progression": true, "tech_type": "PrecursorIonPowerCell", "name": "Ion Power Cell" }, - { "id": 35046, "count": 2, "progression": false, "tech_type": "FarmingTrayFragment", "name": "Exterior Growbed Fragment" }, - { "id": 35047, "count": 1, "progression": false, "tech_type": "PictureFrameFragment", "name": "Picture Frame" }, - { "id": 35048, "count": 2, "progression": false, "tech_type": "BenchFragment", "name": "Bench Fragment" }, - { "id": 35049, "count": 1, "progression": false, "tech_type": "PlanterPotFragment", "name": "Basic Plant Pot" }, - { "id": 35050, "count": 1, "progression": false, "tech_type": "PlanterBoxFragment", "name": "Interior Growbed" }, - { "id": 35051, "count": 1, "progression": false, "tech_type": "PlanterShelfFragment", "name": "Plant Shelf" }, - { "id": 35052, "count": 2, "progression": false, "tech_type": "BaseObservatoryFragment", "name": "Observatory Fragment" }, - { "id": 35053, "count": 2, "progression": false, "tech_type": "BaseRoomFragment", "name": "Multipurpose Room Fragment" }, - { "id": 35054, "count": 2, "progression": false, "tech_type": "BaseBulkheadFragment", "name": "Bulkhead Fragment" }, - { "id": 35055, "count": 1, "progression": false, "tech_type": "Spotlight", "name": "Spotlight" }, - { "id": 35056, "count": 2, "progression": false, "tech_type": "StarshipDesk", "name": "Desk" }, - { "id": 35057, "count": 1, "progression": false, "tech_type": "StarshipChair", "name": "Swivel Chair" }, - { "id": 35058, "count": 1, "progression": false, "tech_type": "StarshipChair2", "name": "Office Chair" }, - { "id": 35059, "count": 1, "progression": false, "tech_type": "StarshipChair3", "name": "Command Chair" }, - { "id": 35060, "count": 2, "progression": false, "tech_type": "LabCounter", "name": "Counter" }, - { "id": 35061, "count": 1, "progression": false, "tech_type": "NarrowBed", "name": "Single Bed" }, - { "id": 35062, "count": 1, "progression": false, "tech_type": "Bed1", "name": "Basic Double Bed" }, - { "id": 35063, "count": 1, "progression": false, "tech_type": "Bed2", "name": "Quilted Double Bed" }, - { "id": 35064, "count": 2, "progression": false, "tech_type": "CoffeeVendingMachine", "name": "Coffee Vending Machine" }, - { "id": 35065, "count": 2, "progression": false, "tech_type": "Trashcans", "name": "Trash Can" }, - { "id": 35066, "count": 1, "progression": false, "tech_type": "Techlight", "name": "Floodlight" }, - { "id": 35067, "count": 1, "progression": false, "tech_type": "BarTable", "name": "Bar Table" }, - { "id": 35068, "count": 1, "progression": false, "tech_type": "VendingMachine", "name": "Vending Machine" }, - { "id": 35069, "count": 1, "progression": false, "tech_type": "SingleWallShelf", "name": "Single Wall Shelf" }, - { "id": 35070, "count": 1, "progression": false, "tech_type": "WallShelves", "name": "Wall Shelves" }, - { "id": 35071, "count": 1, "progression": false, "tech_type": "PlanterPot2", "name": "Round Plant Pot" }, - { "id": 35072, "count": 1, "progression": false, "tech_type": "PlanterPot3", "name": "Chic Plant Pot" }, - { "id": 35073, "count": 1, "progression": false, "tech_type": "LabTrashcan", "name": "Nuclear Waste Disposal" }, - { "id": 35074, "count": 1, "progression": false, "tech_type": "BasePlanter", "name": "Wall Planter" }, - { "id": 35075, "count": 1, "progression": true, "tech_type": "PrecursorIonBattery", "name": "Ion Battery" }, - { "id": 35076, "count": 1, "progression": true, "tech_type": "RocketBaseLadder", "name": "Neptune Gantry" }, - { "id": 35077, "count": 1, "progression": true, "tech_type": "RocketStage1", "name": "Neptune Boosters" }, - { "id": 35078, "count": 1, "progression": true, "tech_type": "RocketStage2", "name": "Neptune Fuel Reserve" }, - { "id": 35079, "count": 1, "progression": true, "tech_type": "RocketStage3", "name": "Neptune Cockpit" }, - { "id": 35080, "count": 1, "progression": false, "tech_type": "BaseFiltrationMachine", "name": "Water Filtration Machine" } -] diff --git a/worlds/subnautica/locations.json b/worlds/subnautica/locations.json deleted file mode 100644 index f39e453e67..0000000000 --- a/worlds/subnautica/locations.json +++ /dev/null @@ -1,521 +0,0 @@ -[ - { "id": 33000, "position": { "x": -1234.3, "y": -349.7, "z": -396.0}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Blood Kelp Trench Wreck - Outside Databox" }, - - { "id": 33001, "position": { "x": -1208.0, "y": -349.6, "z": -383.0}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Blood Kelp Trench Wreck - Inside Databox" }, - - { "id": 33002, "position": { "x": -1210.6, "y": -340.7, "z": -393.4}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Blood Kelp Trench Wreck - PDA" }, - - { "id": 33003, "position": { "x": 903.8, "y": -220.3, "z": 590.9}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Bulb Zone West Wreck - Outside Databox" }, - - { "id": 33004, "position": { "x": 910.9, "y": -201.8, "z": 623.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Bulb Zone West Wreck - Under Databox" }, - - { "id": 33005, "position": { "x": 914.9, "y": -202.1, "z": 611.8}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Bulb Zone West Wreck - Inside Databox" }, - - { "id": 33006, "position": { "x": 912.6, "y": -202.0, "z": 609.5}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Bulb Zone West Wreck - PDA" }, - - { "id": 33007, "position": { "x": 1327.1, "y": -234.9, "z": 575.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Bulb Zone East Wreck - Databox" }, - - { "id": 33008, "position": { "x": -1407.7, "y": -344.2, "z": 721.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Dunes North Wreck - Outside Databox" }, - - { "id": 33009, "position": { "x": -1393.9, "y": -329.7, "z": 733.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Dunes North Wreck - Office Databox" }, - - { "id": 33010, "position": { "x": -1396.3, "y": -330.8, "z": 730.0}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Dunes North Wreck - PDA" }, - - { "id": 33011, "position": { "x": -1409.8, "y": -332.4, "z": 706.9}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Dunes North Wreck - Cargo Databox" }, - - { "id": 33012, "position": { "x": -1626.2, "y": -357.5, "z": 99.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Dunes West Wreck - Databox" }, - - { "id": 33013, "position": { "x": -1196.3, "y": -223.0, "z": 12.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Dunes East Wreck - Outside Databox" }, - - { "id": 33014, "position": { "x": -1206.4, "y": -225.6, "z": 4.0}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Dunes East Wreck - Inside Databox" }, - - { "id": 33015, "position": { "x": -269.7, "y": -262.8, "z": -764.3}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Grand Reef North Wreck - Outside Databox" }, - - { "id": 33016, "position": { "x": -285.8, "y": -240.2, "z": -786.5}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Grand Reef North Wreck - Elevator Databox" }, - - { "id": 33017, "position": { "x": -285.2, "y": -262.4, "z": -788.4}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Grand Reef North Wreck - Bottom Databox" }, - - { "id": 33018, "position": { "x": -272.5, "y": -254.7, "z": -788.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Grand Reef North Wreck - Hangar PDA" }, - - { "id": 33019, "position": { "x": -850.9, "y": -473.2, "z": -1414.6}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Grand Reef South Wreck - Trench Databox" }, - - { "id": 33020, "position": { "x": -889.4, "y": -433.8, "z": -1424.8}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Grand Reef South Wreck - Comms Databox" }, - - { "id": 33021, "position": { "x": -862.4, "y": -437.5, "z": -1444.1}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Grand Reef South Wreck - Outside Databox" }, - - { "id": 33022, "position": { "x": -887.9, "y": -446.0, "z": -1422.7}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Grand Reef South Wreck - PDA" }, - - { "id": 33023, "position": { "x": -23.3, "y": -105.8, "z": -604.2}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Grassy Plateaus South Wreck - Databox" }, - - { "id": 33024, "position": { "x": -27.3, "y": -106.8, "z": -607.2}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Grassy Plateaus South Wreck - PDA" }, - - { "id": 33025, "position": { "x": 313.9, "y": -91.8, "z": 432.6}, - "need_laser_cutter": true, "can_slip_through": true, - "name": "Grassy Plateaus East Wreck - Breach Databox" }, - - { "id": 33026, "position": { "x": 319.4, "y": -104.3, "z": 441.5}, - "need_laser_cutter": true, "can_slip_through": true, - "name": "Grassy Plateaus East Wreck - Hangar Databox" }, - - { "id": 33027, "position": { "x": -632.3, "y": -75.0, "z": -8.9}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Grassy Plateaus West Wreck - Locker PDA" }, - - { "id": 33028, "position": { "x": -664.4, "y": -97.8, "z": -8.0}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Grassy Plateaus West Wreck - Data Terminal" }, - - { "id": 33029, "position": { "x": -421.4, "y": -107.8, "z": -266.5}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Grassy Plateaus West Wreck - Databox" }, - - { "id": 33030, "position": { "x": -44.0, "y": -29.1, "z": -403.6}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Safe Shallows Wreck - PDA" }, - - { "id": 33031, "position": { "x": -317.1, "y": -79.0, "z": 248.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Kelp Forest Wreck - Databox" }, - - { "id": 33032, "position": { "x": 63.2, "y": -38.5, "z": 382.9}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Kelp Forest Wreck - PDA" }, - - { "id": 33033, "position": { "x": 740.3, "y": -389.2, "z": 1179.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Mountains West Wreck - Outside Databox" }, - - { "id": 33034, "position": { "x": 703.7, "y": -365.9, "z": 1199.3}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Mountains West Wreck - Data Terminal" }, - - { "id": 33035, "position": { "x": 698.2, "y": -350.8, "z": 1186.9}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Mountains West Wreck - Hangar Databox" }, - - { "id": 33036, "position": { "x": 676.3, "y": -343.6, "z": 1204.6}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Mountains West Wreck - Office Databox" }, - - { "id": 33037, "position": { "x": 1068.5, "y": -283.4, "z": 1345.3}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Mountains East Wreck - Comms Databox" }, - - { "id": 33038, "position": { "x": 1075.7, "y": -288.9, "z": 1321.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Mountains East Wreck - Outside Databox" }, - - { "id": 33039, "position": { "x": -655.1, "y": -109.6, "z": 791.0}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Northwestern Mushroom Forest Wreck - Cargo Databox" }, - - { "id": 33040, "position": { "x": -663.4, "y": -111.9, "z": 777.9}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Northwestern Mushroom Forest Wreck - Office Databox" }, - - { "id": 33041, "position": { "x": -662.2, "y": -113.4, "z": 777.7}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Northwestern Mushroom Forest Wreck - PDA" }, - - { "id": 33042, "position": { "x": -1161.1, "y": -191.7, "z": -758.3}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Sea Treader's Path Wreck - Outside Databox" }, - - { "id": 33043, "position": { "x": -1129.5, "y": -155.2, "z": -729.3}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Sea Treader's Path Wreck - Hangar Databox" }, - - { "id": 33044, "position": { "x": -1115.9, "y": -175.3, "z": -724.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Sea Treader's Path Wreck - Lobby Databox" }, - - { "id": 33045, "position": { "x": -1136.8, "y": -157.0, "z": -734.6}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Sea Treader's Path Wreck - PDA" }, - - { "id": 33046, "position": { "x": -789.8, "y": -216.1, "z": -711.0}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Sparse Reef Wreck - Locker Databox" }, - - { "id": 33047, "position": { "x": -810.7, "y": -209.3, "z": -685.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Sparse Reef Wreck - Outside Databox" }, - - { "id": 33048, "position": { "x": -795.5, "y": -204.1, "z": -774.7}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Sparse Reef Wreck - Lab Databox" }, - - { "id": 33049, "position": { "x": -170.8, "y": -187.6, "z": 880.7}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Underwater Islands Wreck - Outside Databox" }, - - { "id": 33050, "position": { "x": -138.4, "y": -193.6, "z": 888.7}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Underwater Islands Wreck - Hangar Databox" }, - - { "id": 33051, "position": { "x": -130.7, "y": -193.2, "z": 883.3}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Underwater Islands Wreck - Data Terminal" }, - - { "id": 33052, "position": { "x": -137.8, "y": -193.4, "z": 879.4}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Underwater Islands Wreck - Cable Databox" }, - - { "id": 33053, "position": { "x": -124.4, "y": -200.7, "z": 853.0}, - "need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Underwater Islands Wreck - Pipes Databox 1" }, - - { "id": 33054, "position": { "x": -126.8, "y": -201.1, "z": 852.1}, - "need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Underwater Islands Wreck - Pipes Databox 2" }, - - { "id": 33055, "position": { "x": -643.8, "y": -509.9, "z": -941.9}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Deep Grand Reef - Bedroom Databox" }, - - { "id": 33056, "position": { "x": -635.1, "y": -502.7, "z": -951.4}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Deep Grand Reef - Observatory Databox" }, - - { "id": 33057, "position": { "x": -645.8, "y": -508.7, "z": -943.0}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Deep Grand Reef - Bedroom PDA" }, - - { "id": 33058, "position": { "x": -630.5, "y": -511.1, "z": -936.1}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Deep Grand Reef - Outside PDA" }, - - { "id": 33059, "position": { "x": -647.7, "y": -502.6, "z": -935.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Deep Grand Reef - Observatory PDA" }, - - { "id": 33060, "position": { "x": -639.6, "y": -505.9, "z": -946.6}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Deep Grand Reef - Lab PDA" }, - - { "id": 33061, "position": { "x": -707.2, "y": 0.5, "z": -1096.7}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Floating Island - Lake PDA" }, - - { "id": 33062, "position": { "x": -765.7, "y": 17.6, "z": -1116.4}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Floating Island - Databox" }, - - { "id": 33063, "position": { "x": -754.9, "y": 14.6, "z": -1108.9}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Floating Island - Room PDA" }, - - { "id": 33064, "position": { "x": -765.3, "y": 14.1, "z": -1115.0}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Floating Island - Green Wall PDA" }, - - { "id": 33065, "position": { "x": -758.6, "y": 14.1, "z": -1111.3}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Floating Island - Corridor PDA" }, - - { "id": 33066, "position": { "x": -805.4, "y": 76.9, "z": -1055.7}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Floating Island - North Observatory PDA" }, - - { "id": 33067, "position": { "x": -715.9, "y": 75.4, "z": -1168.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Floating Island - South Observatory PDA" }, - - { "id": 33068, "position": { "x": -540.5, "y": -250.8, "z": -83.4}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Jellyshroom Cave - PDA" }, - - { "id": 33069, "position": { "x": 110.6, "y": -264.9, "z": -369.0}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Jellyshroom Cave - Bedroom Databox" }, - - { "id": 33070, "position": { "x": 80.6, "y": -268.6, "z": -358.3}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Jellyshroom Cave - Detached PDA" }, - - { "id": 33071, "position": { "x": 78.2, "y": -265.0, "z": -373.4}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Jellyshroom Cave - Office PDA" }, - - { "id": 33072, "position": { "x": 85.1, "y": -264.1, "z": -372.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Jellyshroom Cave - Locker PDA" }, - - { "id": 33073, "position": { "x": 112.3, "y": -264.9, "z": -369.3}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Jellyshroom Cave - Bedroom PDA" }, - - { "id": 33074, "position": { "x": 95.5, "y": -258.9, "z": -366.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Degasi Seabase - Jellyshroom Cave - Observatory PDA" }, - - { "id": 33075, "position": { "x": -483.6, "y": -504.7, "z": 1326.6}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 2 - Databox" }, - - { "id": 33076, "position": { "x": -481.4, "y": -503.6, "z": 1324.1}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 2 - PDA" }, - - { "id": 33077, "position": { "x": -34.2, "y": -22.4, "z": 410.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 3 - Databox" }, - - { "id": 33078, "position": { "x": -33.8, "y": -22.5, "z": 408.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 3 - PDA" }, - - { "id": 33079, "position": { "x": 712.4, "y": -3.4, "z": 160.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 4 - Databox" }, - - { "id": 33080, "position": { "x": 712.0, "y": -3.5, "z": 161.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 4 - PDA" }, - - { "id": 33081, "position": { "x": 358.7, "y": -117.1, "z": 306.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 6 - Databox" }, - - { "id": 33082, "position": { "x": 361.8, "y": -116.2, "z": 309.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 6 - Inside PDA" }, - - { "id": 33083, "position": { "x": 359.9, "y": -117.0, "z": 312.1}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 6 - Outside PDA" }, - - { "id": 33084, "position": { "x": -56.0, "y": -182.0, "z": -1039.0}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 7 - PDA" }, - - { "id": 33085, "position": { "x": 1119.5, "y": -271.7, "z": 561.7}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 12 - Databox" }, - - { "id": 33086, "position": { "x": 1116.1, "y": -271.3, "z": 566.9}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 12 - PDA" }, - - { "id": 33087, "position": { "x": -926.4, "y": -185.2, "z": 501.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 13 - Databox" }, - - { "id": 33088, "position": { "x": -926.8, "y": -184.4, "z": 506.6}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 13 - PDA" }, - - { "id": 33089, "position": { "x": -514.5, "y": -98.1, "z": -56.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 17 - PDA" }, - - { "id": 33090, "position": { "x": -809.8, "y": -302.2, "z": -876.9}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 19 - Databox" }, - - { "id": 33091, "position": { "x": -806.1, "y": -294.1, "z": -866.0}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 19 - Outside PDA" }, - - { "id": 33092, "position": { "x": -810.5, "y": -299.4, "z": -873.1}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lifepod 19 - Inside PDA" }, - - { "id": 33093, "position": { "x": 903.5, "y": -0.2, "z": 16.1}, - "need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora Seamoth Bay - Upgrade Console" }, - - { "id": 33094, "position": { "x": 872.5, "y": 2.7, "z": -0.7}, - "need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora Drive Room - Upgrade Console" }, - - { "id": 33095, "position": { "x": 991.6, "y": 3.2, "z": -31.0}, - "need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora Prawn Suit Bay - Upgrade Console" }, - - { "id": 33096, "position": { "x": 952.1, "y": 41.2, "z": 113.9}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Aurora - Office PDA" }, - - { "id": 33097, "position": { "x": 977.2, "y": 39.1, "z": 83.0}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Aurora - Corridor PDA" }, - - { "id": 33098, "position": { "x": 954.9, "y": 11.2, "z": 3.4}, - "need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Cargo Bay PDA" }, - - { "id": 33099, "position": { "x": 907.1, "y": -1.5, "z": 15.3}, - "need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Seamoth Bay PDA" }, - - { "id": 33100, "position": { "x": 951.8, "y": -2.3, "z": -34.7}, - "need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Medkit Locker PDA" }, - - { "id": 33101, "position": { "x": 952.0, "y": -3.7, "z": -23.4}, - "need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Locker PDA" }, - - { "id": 33102, "position": { "x": 986.5, "y": 9.6, "z": -48.6}, - "need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Canteen PDA" }, - - { "id": 33103, "position": { "x": 951.3, "y": 11.2, "z": -51.0}, - "need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Cabin 4 PDA" }, - - { "id": 33104, "position": { "x": 967.1, "y": 10.4, "z": -47.4}, - "need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Cabin 7 PDA" }, - - { "id": 33105, "position": { "x": 964.1, "y": 11.1, "z": -61.9}, - "need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Cabin 1 PDA" }, - - { "id": 33106, "position": { "x": 971.2, "y": 10.8, "z": -70.4}, - "need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Captain PDA" }, - - { "id": 33107, "position": { "x": 1033.6, "y": -8.5, "z": 16.2}, - "need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Ring PDA" }, - - { "id": 33108, "position": { "x": 1032.5, "y": -7.8, "z": 32.4}, - "need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Lab PDA" }, - - { "id": 33109, "position": { "x": 945.8, "y": 40.8, "z": 115.1}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Aurora - Office Data Terminal" }, - - { "id": 33110, "position": { "x": 974.8, "y": 10.0, "z": -77.0}, - "need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Captain Data Terminal" }, - - { "id": 33111, "position": { "x": 1040.8, "y": -11.4, "z": -3.4}, - "need_laser_cutter": true, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Battery Room Data Terminal" }, - - { "id": 33112, "position": { "x": 1029.5, "y": -8.7, "z": 35.9}, - "need_laser_cutter": false, "can_slip_through": false, "need_propulsion_cannon": true, - "name": "Aurora - Lab Data Terminal" }, - - { "id": 33113, "position": { "x": 432.2, "y": 3.0, "z": 1193.2}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Quarantine Enforcement Platform's - Upper Alien Data Terminal" }, - - { "id": 33114, "position": { "x": 474.4, "y": -4.5, "z": 1224.4}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Quarantine Enforcement Platform's - Mid Alien Data Terminal" }, - - { "id": 33115, "position": { "x": -1224.2, "y": -400.4, "z": 1057.9}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Dunes Sanctuary - Alien Data Terminal" }, - - { "id": 33116, "position": { "x": -895.5, "y": -311.6, "z": -838.1}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Deep Sparse Reef Sanctuary - Alien Data Terminal" }, - - { "id": 33117, "position": { "x": -642.9, "y": -563.5, "z": 1485.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Northern Blood Kelp Zone Sanctuary - Alien Data Terminal" }, - - { "id": 33118, "position": { "x": -1112.3, "y": -687.3, "z": -695.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Lost River Laboratory Cache - Alien Data Terminal" }, - - { "id": 33119, "position": { "x": -280.2, "y": -804.3, "z": 305.1}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Disease Research Facility - Upper Alien Data Terminal" }, - - { "id": 33120, "position": { "x": -267.9, "y": -806.6, "z": 250.0}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Disease Research Facility - Mid Alien Data Terminal" }, - - { "id": 33121, "position": { "x": -286.2, "y": -815.6, "z": 297.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Disease Research Facility - Lower Alien Data Terminal" }, - - { "id": 33122, "position": { "x": -71.3, "y": -1227.2, "z": 104.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Alien Thermal Plant - Entrance Alien Data Terminal" }, - - { "id": 33123, "position": { "x": -38.7, "y": -1226.6, "z": 111.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Alien Thermal Plant - Green Alien Data Terminal" }, - - { "id": 33124, "position": { "x": -30.4, "y": -1220.3, "z": 111.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Alien Thermal Plant - Yellow Alien Data Terminal" }, - - { "id": 33125, "position": { "x": 245.8, "y": -1430.6, "z": -311.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Primary Containment Facility's Antechamber - Alien Data Terminal" }, - - { "id": 33126, "position": { "x": 165.5, "y": -1442.4, "z": -385.8}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Primary Containment Facility's Egg Laboratory - Alien Data Terminal" }, - - { "id": 33127, "position": { "x": 348.7, "y": -1443.5, "z": -291.9}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Primary Containment Facility's Pipe Room - Alien Data Terminal" }, - - { "id": 33128, "position": { "x": -641.8, "y": -111.3, "z": -19.7}, - "need_laser_cutter": true, "can_slip_through": false, - "name": "Grassy Plateaus West Wreck - Beam PDA" }, - - { "id": 33129, "position": { "x": -748.9, "y": 14.4, "z": -1179.5}, - "need_laser_cutter": false, "can_slip_through": false, - "name": "Floating Island - Cave Entrance PDA" } -] diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index d789e9ddef..c8b94a2763 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -40,7 +40,7 @@ class TimespinnerWorld(World): Travel back in time to change fate itself. Join timekeeper Lunais on her quest for revenge against the empire that killed her family. """ - options = timespinner_options + option_definitions = timespinner_options game = "Timespinner" topology_present = True remote_items = False diff --git a/worlds/v6/__init__.py b/worlds/v6/__init__.py index 04947716d3..38690e5a00 100644 --- a/worlds/v6/__init__.py +++ b/worlds/v6/__init__.py @@ -35,14 +35,13 @@ class V6World(World): location_name_to_id = location_table data_version = 1 - forced_auto_forfeit = False area_connections: typing.Dict[int, int] area_cost_map: typing.Dict[int,int] music_map: typing.Dict[int,int] - options = v6_options + option_definitions = v6_options def create_regions(self): create_regions(self.world,self.player) diff --git a/worlds/witness/Door_Shuffle.txt b/worlds/witness/Door_Shuffle.txt deleted file mode 100644 index 7d48064cc8..0000000000 --- a/worlds/witness/Door_Shuffle.txt +++ /dev/null @@ -1,30 +0,0 @@ -100 - 0x01A54 - None - Glass Factory Entry Door -105 - 0x000B0 - 0x0343A - Door to Symmetry Island Lower -107 - 0x1C349 - 0x00076 - Door to Symmetry Island Upper -110 - 0x0C339 - 0x09F94 - Door to Desert Flood Light Room -111 - 0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B - None - Desert Flood Room Flood Controls -120 - 0x03678 - None - Quarry Mill Ramp Control -122 - 0x03679 - 0x014E8 - Quarry Mill Elevator Control -125 - 0x03852 - 0x034D4,0x021D5 - Quarry Boathouse Ramp Height Control -127 - 0x03858 - 0x021AE - Quarry Boathouse Ramp Horizontal Control -131 - 0x334DB,0x334DC - None - Shadows Door Timer -150 - 0x00B10 - None - Monastery Entry Door Left -151 - 0x00C92 - None - Monastery Entry Door Right -162 - 0x28998 - None - Town Door to RGB House -163 - 0x28A0D - 0x28998 - Town Door to Church -166 - 0x28A79 - None - Town Maze Panel (Drop-Down Staircase) -169 - 0x17F5F - None - Windmill Door -200 - 0x0288C - None - Treehouse First & Second Door -202 - 0x0A182 - None - Treehouse Third Door -205 - 0x2700B - None - Treehouse Laser House Door Timer -208 - 0x17CBC - None - Treehouse Shortcut Drop-Down Bridge -175 - 0x17CAB - 0x002C7 - Jungle Popup Wall -180 - 0x17C2E - None - Bunker Entry Door -183 - 0x0A099 - 0x09DAF - Inside Bunker Door to Bunker Proper -186 - 0x0A079 - None - Bunker Elevator Control -190 - 0x0056E - None - Swamp Entry Door -192 - 0x00609,0x18488 - 0x181A9 - Swamp Sliding Bridge -195 - 0x181F5 - None - Swamp Rotating Bridge -197 - 0x17C0A - None - Swamp Maze Control -300 - 0x0042D - None - Mountaintop River Shape Panel (Shortcut to Secret Area) -310 - 0x17CDF,0x17CC8,0x17CA6,0x09DB8,0x17C95,0x0A054 - None - Boat diff --git a/worlds/witness/Early_UTM.txt b/worlds/witness/Early_UTM.txt deleted file mode 100644 index 57da491e35..0000000000 --- a/worlds/witness/Early_UTM.txt +++ /dev/null @@ -1,5 +0,0 @@ -Event Items: -Shortcut to Secret Area Opens - 0x0042D - -Region Changes: -Inside Mountain Secret Area (Inside Mountain Secret Area) - Inside Mountain Path to Secret Area - 0x00FF8 - Main Island - 0x021D7 | 0x0042D - Main Island - 0x17CF2 \ No newline at end of file diff --git a/worlds/witness/Options.py b/worlds/witness/Options.py index e8b7e576da..631a5bc076 100644 --- a/worlds/witness/Options.py +++ b/worlds/witness/Options.py @@ -7,37 +7,49 @@ from Options import Toggle, DefaultOnToggle, Option, Range, Choice # "Play the randomizer in hardmode" # display_name = "Hard Mode" -# class UnlockSymbols(DefaultOnToggle): -# "All Puzzle symbols of a specific panel need to be unlocked before the panel can be used" -# display_name = "Unlock Symbols" - class DisableNonRandomizedPuzzles(DefaultOnToggle): - """Disable puzzles that cannot be randomized. - Non randomized puzzles are Shadows, Monastery, and Greenhouse. + """Disables puzzles that cannot be randomized. + This includes many puzzles that heavily involve the environment, such as Shadows, Monastery or Orchard. The lasers for those areas will be activated as you solve optional puzzles throughout the island.""" display_name = "Disable non randomized puzzles" class EarlySecretArea(Toggle): - """The Mountainside shortcut to the Mountain Secret Area is open from the start. + """Opens the Mountainside shortcut to the Mountain Secret Area from the start. (Otherwise known as "UTM", "Caves" or the "Challenge Area")""" display_name = "Early Secret Area" class ShuffleSymbols(DefaultOnToggle): - """You will need to unlock puzzle symbols as items to be able to solve the panels that contain those symbols.""" + """You will need to unlock puzzle symbols as items to be able to solve the panels that contain those symbols. + If you turn this off, there will be no progression items in the game unless you turn on door shuffle.""" display_name = "Shuffle Symbols" -class ShuffleDoors(Toggle): - """Many doors around the island will have their panels turned off initially. - You will need to find the items that power the panels to open those doors.""" +class ShuffleLasers(Toggle): + """If on, the 11 lasers are turned into items and will activate on their own upon receiving them. + Note: There is a visual bug that can occur with the Desert Laser. It does not affect gameplay - The Laser can still + be redirected as normal, for both applications of redirection.""" + display_name = "Shuffle Lasers" + + +class ShuffleDoors(Choice): + """If on, opening doors will require their respective "keys". + If set to "panels", those keys will unlock the panels on doors. + In "doors_simple" and "doors_complex", the doors will magically open by themselves upon receiving the key. + The last option, "max", is a combination of "doors_complex" and "panels".""" display_name = "Shuffle Doors" + option_none = 0 + option_panels = 1 + option_doors_simple = 2 + option_doors_complex = 3 + option_max = 4 class ShuffleDiscardedPanels(Toggle): - """Discarded Panels will have items on them. - Solving certain Discarded Panels may still be necessary even if off!""" + """Add Discarded Panels into the location pool. + Solving certain Discarded Panels may still be necessary to beat the game, even if this is off.""" + display_name = "Shuffle Discarded Panels" @@ -52,9 +64,10 @@ class ShuffleUncommonLocations(Toggle): display_name = "Shuffle Uncommon Locations" -class ShuffleHardLocations(Toggle): - """Adds some harder locations into the game, e.g. Mountain Secret Area panels""" - display_name = "Shuffle Hard Locations" +class ShufflePostgame(Toggle): + """Adds locations into the pool that are guaranteed to be locked behind your goal. Use this if you don't play with + forfeit on victory.""" + display_name = "Shuffle Postgame" class VictoryCondition(Choice): @@ -103,18 +116,19 @@ class PuzzleSkipAmount(Range): the_witness_options: Dict[str, type] = { # "hard_mode": HardMode, + "shuffle_symbols": ShuffleSymbols, + "shuffle_doors": ShuffleDoors, + "shuffle_lasers": ShuffleLasers, "disable_non_randomized_puzzles": DisableNonRandomizedPuzzles, "shuffle_discarded_panels": ShuffleDiscardedPanels, "shuffle_vault_boxes": ShuffleVaultBoxes, "shuffle_uncommon": ShuffleUncommonLocations, - "shuffle_hard": ShuffleHardLocations, + "shuffle_postgame": ShufflePostgame, "victory_condition": VictoryCondition, - "trap_percentage": TrapPercentage, - "early_secret_area": EarlySecretArea, - # "shuffle_symbols": ShuffleSymbols, - # "shuffle_doors": ShuffleDoors, "mountain_lasers": MountainLasers, "challenge_lasers": ChallengeLasers, + "early_secret_area": EarlySecretArea, + "trap_percentage": TrapPercentage, "puzzle_skip_amount": PuzzleSkipAmount, } diff --git a/worlds/witness/WitnessItems.txt b/worlds/witness/WitnessItems.txt index 9d8831bb10..4449602529 100644 --- a/worlds/witness/WitnessItems.txt +++ b/worlds/witness/WitnessItems.txt @@ -1,6 +1,8 @@ Progression: 0 - Dots 1 - Colored Dots +2 - Full Dots +3 - Invisible Dots 5 - Sound Dots 10 - Symmetry 20 - Triangles @@ -12,6 +14,7 @@ Progression: 61 - Stars + Same Colored Symbol 71 - Black/White Squares 72 - Colored Squares +80 - Arrows Usefuls: 101 - Functioning Brain - False @@ -23,3 +26,173 @@ Boosts: Traps: 600 - Slowness 610 - Power Surge + +Doors: +1100 - Glass Factory Entry Door (Panel) - 0x01A54 +1105 - Door to Symmetry Island Lower (Panel) - 0x000B0 +1107 - Door to Symmetry Island Upper (Panel) - 0x1C349 +1110 - Door to Desert Flood Light Room (Panel) - 0x0C339 +1111 - Desert Flood Room Flood Controls (Panel) - 0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B +1119 - Quarry Door to Mill (Panel) - 0x01E5A,0x01E59 +1120 - Quarry Mill Ramp Controls (Panel) - 0x03678,0x03676 +1122 - Quarry Mill Elevator Controls (Panel) - 0x03679,0x03675 +1125 - Quarry Boathouse Ramp Height Control (Panel) - 0x03852 +1127 - Quarry Boathouse Ramp Horizontal Control (Panel) - 0x03858 +1131 - Shadows Door Timer (Panel) - 0x334DB,0x334DC +1150 - Monastery Entry Door Left (Panel) - 0x00B10 +1151 - Monastery Entry Door Right (Panel) - 0x00C92 +1162 - Town Door to RGB House (Panel) - 0x28998 +1163 - Town Door to Church (Panel) - 0x28A0D +1166 - Town Maze Panel (Drop-Down Staircase) (Panel) - 0x28A79 +1169 - Windmill Door (Panel) - 0x17F5F +1200 - Treehouse First & Second Doors (Panel) - 0x0288C,0x02886 +1202 - Treehouse Third Door (Panel) - 0x0A182 +1205 - Treehouse Laser House Door Timer (Panel) - 0x2700B,0x334DC +1208 - Treehouse Shortcut Drop-Down Bridge (Panel) - 0x17CBC +1175 - Jungle Popup Wall (Panel) - 0x17CAB +1180 - Bunker Entry Door (Panel) - 0x17C2E +1183 - Inside Bunker Door to Bunker Proper (Panel) - 0x0A099 +1186 - Bunker Elevator Control (Panel) - 0x0A079 +1190 - Swamp Entry Door (Panel) - 0x0056E +1192 - Swamp Sliding Bridge (Panel) - 0x00609,0x18488 +1195 - Swamp Rotating Bridge (Panel) - 0x181F5 +1197 - Swamp Maze Control (Panel) - 0x17C0A +1310 - Boat - 0x17CDF,0x17CC8,0x17CA6,0x09DB8,0x17C95,0x0A054 + +1400 - Caves Mountain Shortcut - 0x2D73F + +1500 - Symmetry Laser - 0x00509 +1501 - Desert Laser - 0x012FB,0x01317 +1502 - Quarry Laser - 0x01539 +1503 - Shadows Laser - 0x181B3 +1504 - Keep Laser - 0x014BB +1505 - Monastery Laser - 0x17C65 +1506 - Town Laser - 0x032F9 +1507 - Jungle Laser - 0x00274 +1508 - Bunker Laser - 0x0C2B2 +1509 - Swamp Laser - 0x00BF6 +1510 - Treehouse Laser - 0x028A4 + +1600 - Outside Tutorial Optional Door - 0x03BA2 +1603 - Outside Tutorial Outpost Entry Door - 0x0A170 +1606 - Outside Tutorial Outpost Exit Door - 0x04CA3 +1609 - Glass Factory Entry Door - 0x01A29 +1612 - Glass Factory Back Wall - 0x0D7ED +1615 - Symmetry Island Lower Door - 0x17F3E +1618 - Symmetry Island Upper Door - 0x18269 +1619 - Orchard Middle Gate - 0x03307 +1620 - Orchard Final Gate - 0x03313 +1621 - Desert Door to Flood Light Room - 0x09FEE +1624 - Desert Door to Pond Room - 0x0C2C3 +1627 - Desert Door to Water Levels Room - 0x0A24B +1630 - Desert Door to Elevator Room - 0x0C316 +1633 - Quarry Main Entry 1 - 0x09D6F +1636 - Quarry Main Entry 2 - 0x17C07 +1639 - Quarry Door to Mill - 0x02010 +1642 - Quarry Mill Side Door - 0x275FF +1645 - Quarry Mill Rooftop Shortcut - 0x17CE8 +1648 - Quarry Mill Stairs - 0x0368A +1651 - Quarry Boathouse Boat Staircase - 0x2769B,0x27163 +1653 - Quarry Boathouse First Barrier - 0x17C50 +1654 - Quarry Boathouse Shortcut - 0x3865F +1656 - Shadows Timed Door - 0x19B24 +1657 - Shadows Laser Room Right Door - 0x194B2 +1660 - Shadows Laser Room Left Door - 0x19665 +1663 - Shadows Barrier to Quarry - 0x19865,0x0A2DF +1666 - Shadows Barrier to Ledge - 0x1855B,0x19ADE +1669 - Keep Hedge Maze 1 Exit Door - 0x01954 +1672 - Keep Pressure Plates 1 Exit Door - 0x01BEC +1675 - Keep Hedge Maze 2 Shortcut - 0x018CE +1678 - Keep Hedge Maze 2 Exit Door - 0x019D8 +1681 - Keep Hedge Maze 3 Shortcut - 0x019B5 +1684 - Keep Hedge Maze 3 Exit Door - 0x019E6 +1687 - Keep Hedge Maze 4 Shortcut - 0x0199A +1690 - Keep Hedge Maze 4 Exit Door - 0x01A0E +1693 - Keep Pressure Plates 2 Exit Door - 0x01BEA +1696 - Keep Pressure Plates 3 Exit Door - 0x01CD5 +1699 - Keep Pressure Plates 4 Exit Door - 0x01D40 +1702 - Keep Shortcut to Shadows - 0x09E3D +1705 - Keep Tower Shortcut - 0x04F8F +1708 - Monastery Shortcut - 0x0364E +1711 - Monastery Inner Door - 0x0C128 +1714 - Monastery Outer Door - 0x0C153 +1717 - Monastery Door to Garden - 0x03750 +1718 - Town Cargo Box Door - 0x0A0C9 +1720 - Town Wooden Roof Staircase - 0x034F5 +1723 - Town Tinted Door to RGB House - 0x28A61 +1726 - Town Door to Church - 0x03BB0 +1729 - Town Maze Staircase - 0x28AA2 +1732 - Town Windmill Door - 0x1845B +1735 - Town RGB House Staircase - 0x2897B +1738 - Town Tower Blue Panels Door - 0x27798 +1741 - Town Tower Lattice Door - 0x27799 +1744 - Town Tower Environmental Set Door - 0x2779A +1747 - Town Tower Wooden Roof Set Door - 0x2779C +1750 - Theater Entry Door - 0x17F88 +1753 - Theater Exit Door Left - 0x0A16D +1756 - Theater Exit Door Right - 0x3CCDF +1759 - Jungle Bamboo Shortcut to River - 0x3873B +1760 - Jungle Popup Wall - 0x1475B +1762 - River Shortcut to Monastery Garden - 0x0CF2A +1765 - Bunker Bunker Entry Door - 0x0C2A4 +1768 - Bunker Tinted Glass Door - 0x17C79 +1771 - Bunker Door to Ultraviolet Room - 0x0C2A3 +1774 - Bunker Door to Elevator - 0x0A08D +1777 - Swamp Entry Door - 0x00C1C +1780 - Swamp Door to Broken Shapers - 0x184B7 +1783 - Swamp Platform Shortcut Door - 0x38AE6 +1786 - Swamp Cyan Water Pump - 0x04B7F +1789 - Swamp Door to Rotated Shapers - 0x18507 +1792 - Swamp Red Water Pump - 0x183F2 +1795 - Swamp Red Underwater Exit - 0x305D5 +1798 - Swamp Blue Water Pump - 0x18482 +1801 - Swamp Purple Water Pump - 0x0A1D6 +1804 - Swamp Near Laser Shortcut - 0x2D880 +1807 - Treehouse First Door - 0x0C309 +1810 - Treehouse Second Door - 0x0C310 +1813 - Treehouse Beyond Yellow Bridge Door - 0x0A181 +1816 - Treehouse Drawbridge - 0x0C32D +1819 - Treehouse Timed Door to Laser House - 0x0C323 +1822 - Inside Mountain First Layer Exit Door - 0x09E54 +1825 - Inside Mountain Second Layer Staircase Near - 0x09FFB +1828 - Inside Mountain Second Layer Exit Door - 0x09EDD +1831 - Inside Mountain Second Layer Staircase Far - 0x09E07 +1834 - Inside Mountain Giant Puzzle Exit Door - 0x09F89 +1840 - Inside Mountain Door to Final Room - 0x0C141 +1843 - Inside Mountain Bottom Layer Rock - 0x17F33 +1846 - Inside Mountain Door to Secret Area - 0x2D77D +1849 - Caves Pillar Door - 0x019A5 +1855 - Caves Swamp Shortcut - 0x2D859 +1858 - Challenge Entry Door - 0x0A19A +1861 - Challenge Door to Theater Walkway - 0x0348A +1864 - Theater Walkway Door to Windmill Interior - 0x27739 +1867 - Theater Walkway Door to Desert Elevator Room - 0x27263 +1870 - Theater Walkway Door to Town - 0x09E87 + +1903 - Outside Tutorial Outpost Doors - 0x03BA2,0x0A170,0x04CA3 +1906 - Symmetry Island Doors - 0x17F3E,0x18269 +1909 - Orchard Gates - 0x03313,0x03307 +1912 - Desert Doors - 0x09FEE,0x0C2C3,0x0A24B,0x0C316 +1915 - Quarry Main Entry - 0x09D6F,0x17C07 +1918 - Quarry Mill Shortcuts - 0x17CE8,0x0368A,0x275FF +1921 - Quarry Boathouse Barriers - 0x17C50,0x3865F +1924 - Shadows Laser Room Door - 0x194B2,0x19665 +1927 - Shadows Barriers - 0x19865,0x0A2DF,0x1855B,0x19ADE +1930 - Keep Hedge Maze Doors - 0x01954,0x018CE,0x019D8,0x019B5,0x019E6,0x0199A,0x01A0E +1933 - Keep Pressure Plates Doors - 0x01BEC,0x01BEA,0x01CD5,0x01D40 +1936 - Keep Shortcuts - 0x09E3D,0x04F8F +1939 - Monastery Entry Door - 0x0C128,0x0C153 +1942 - Monastery Shortcuts - 0x0364E,0x03750 +1945 - Town Doors - 0x0A0C9,0x034F5,0x28A61,0x03BB0,0x28AA2,0x1845B,0x2897B +1948 - Town Tower Doors - 0x27798,0x27799,0x2779A,0x2779C +1951 - Theater Exit Door - 0x0A16D,0x3CCDF +1954 - Jungle & River Shortcuts - 0x3873B,0x0CF2A +1957 - Bunker Doors - 0x0C2A4,0x17C79,0x0C2A3,0x0A08D +1960 - Swamp Doors - 0x00C1C,0x184B7,0x38AE6,0x18507 +1963 - Swamp Water Pumps - 0x04B7F,0x183F2,0x305D5,0x18482,0x0A1D6 +1966 - Treehouse Entry Doors - 0x0C309,0x0C310,0x0A181 +1975 - Inside Mountain Second Layer Stairs & Doors - 0x09FFB,0x09EDD,0x09E07 +1978 - Inside Mountain Bottom Layer Doors to Caves - 0x17F33,0x2D77D +1981 - Caves Doors to Challenge - 0x019A5,0x0A19A +1984 - Caves Exits to Main Island - 0x2D859,0x2D73F +1987 - Theater Walkway Doors - 0x27739,0x27263,0x09E87 \ No newline at end of file diff --git a/worlds/witness/WitnessLogic.txt b/worlds/witness/WitnessLogic.txt index e4e63dc434..c98257fb73 100644 --- a/worlds/witness/WitnessLogic.txt +++ b/worlds/witness/WitnessLogic.txt @@ -1,712 +1,930 @@ -First Hallway (First Hallway) - Entry - True: -0x00064 (Straight) - True - True -0x00182 (Bend) - 0x00064 - True +First Hallway (First Hallway) - Entry - True - Tutorial - 0x00182: +158000 - 0x00064 (Straight) - True - True +158001 - 0x00182 (Bend) - 0x00064 - True -Tutorial (Tutorial) - First Hallway - 0x00182: -0x00293 (Front Center) - True - True -0x00295 (Center Left) - 0x00293 - True -0x002C2 (Front Left) - 0x00295 - True -0x0A3B5 (Back Left) - True - True -0x0A3B2 (Back Right) - True - True -0x03629 (Gate Open) - 0x002C2 & 0x0A3B5 & 0x0A3B2 - True -0x03505 (Gate Close) - 0x2FAF6 - True -0x0C335 (Pillar) - True - Triangles - True -0x0C373 (Patio Floor) - 0x0C335 - Dots +Tutorial (Tutorial) - Outside Tutorial - 0x03629: +158002 - 0x00293 (Front Center) - True - True +158003 - 0x00295 (Center Left) - 0x00293 - True +158004 - 0x002C2 (Front Left) - 0x00295 - True +158005 - 0x0A3B5 (Back Left) - True - True +158006 - 0x0A3B2 (Back Right) - True - True +158007 - 0x03629 (Gate Open) - 0x002C2 & 0x0A3B5 & 0x0A3B2 - True +158008 - 0x03505 (Gate Close) - 0x2FAF6 - True +158009 - 0x0C335 (Pillar) - True - Triangles - True +158010 - 0x0C373 (Patio Floor) - 0x0C335 - Dots -Outside Tutorial (Outside Tutorial) - Tutorial - 0x03629: -0x033D4 (Vault) - True - Dots & Squares & Black/White Squares -0x03481 (Vault Box) - 0x033D4 - True -0x0A171 (Optional Door 1) - 0x0A3B5 - Dots -0x17CFB (Discard) - 0x0A171 - Triangles -0x04CA4 (Optional Door 2) - 0x0A171 - Dots & Squares & Black/White Squares -0x0005D (Dots Introduction 1) - True - Dots -0x0005E (Dots Introduction 2) - 0x0005D - Dots -0x0005F (Dots Introduction 3) - 0x0005E - Dots -0x00060 (Dots Introduction 4) - 0x0005F - Dots -0x00061 (Dots Introduction 5) - 0x00060 - Dots -0x018AF (Squares Introduction 1) - True - Squares & Black/White Squares -0x0001B (Squares Introduction 2) - 0x018AF - Squares & Black/White Squares -0x012C9 (Squares Introduction 3) - 0x0001B - Squares & Black/White Squares -0x0001C (Squares Introduction 4) - 0x012C9 - Squares & Black/White Squares -0x0001D (Squares Introduction 5) - 0x0001C - Squares & Black/White Squares -0x0001E (Squares Introduction 6) - 0x0001D - Squares & Black/White Squares -0x0001F (Squares Introduction 7) - 0x0001E - Squares & Black/White Squares -0x00020 (Squares Introduction 8) - 0x0001F - Squares & Black/White Squares -0x00021 (Squares Introduction 9) - 0x00020 - Squares & Black/White Squares +Outside Tutorial (Outside Tutorial) - Outside Tutorial Path To Outpost - 0x03BA2: +158650 - 0x033D4 (Vault) - True - Dots & Squares & Black/White Squares +158651 - 0x03481 (Vault Box) - 0x033D4 - True +158013 - 0x0005D (Dots Introduction 1) - True - Dots +158014 - 0x0005E (Dots Introduction 2) - 0x0005D - Dots +158015 - 0x0005F (Dots Introduction 3) - 0x0005E - Dots +158016 - 0x00060 (Dots Introduction 4) - 0x0005F - Dots +158017 - 0x00061 (Dots Introduction 5) - 0x00060 - Dots +158018 - 0x018AF (Squares Introduction 1) - True - Squares & Black/White Squares +158019 - 0x0001B (Squares Introduction 2) - 0x018AF - Squares & Black/White Squares +158020 - 0x012C9 (Squares Introduction 3) - 0x0001B - Squares & Black/White Squares +158021 - 0x0001C (Squares Introduction 4) - 0x012C9 - Squares & Black/White Squares +158022 - 0x0001D (Squares Introduction 5) - 0x0001C - Squares & Black/White Squares +158023 - 0x0001E (Squares Introduction 6) - 0x0001D - Squares & Black/White Squares +158024 - 0x0001F (Squares Introduction 7) - 0x0001E - Squares & Black/White Squares +158025 - 0x00020 (Squares Introduction 8) - 0x0001F - Squares & Black/White Squares +158026 - 0x00021 (Squares Introduction 9) - 0x00020 - Squares & Black/White Squares +Door - 0x03BA2 (Optional Door 1) - 0x0A3B5 + +Outside Tutorial Path To Outpost (Outside Tutorial) - Outside Tutorial Outpost - 0x0A170: +158011 - 0x0A171 (Door to Outpost Panel) - True - Dots +Door - 0x0A170 (Door to Outpost) - 0x0A171 + +Outside Tutorial Outpost (Outside Tutorial) - Outside Tutorial - 0x04CA3: +158012 - 0x04CA4 (Exit Door from Outpost Panel) - True - Dots & Squares & Black/White Squares +Door - 0x04CA3 (Exit Door from Outpost) - 0x04CA4 +158600 - 0x17CFB (Discard) - True - Triangles Main Island () - Outside Tutorial - True: -Outside Glass Factory (Glass Factory) - Main Island - True: -0x01A54 (Entry Door) - True - Symmetry -0x3C12B (Discard) - True - Triangles +Outside Glass Factory (Glass Factory) - Main Island - True - Inside Glass Factory - 0x01A29: +158027 - 0x01A54 (Entry Door Panel) - True - Symmetry +Door - 0x01A29 (Entry Door) - 0x01A54 +158601 - 0x3C12B (Discard) - True - Triangles -Inside Glass Factory (Glass Factory) - Outside Glass Factory - 0x01A54: -0x00086 (Vertical Symmetry 1) - True - Symmetry -0x00087 (Vertical Symmetry 2) - 0x00086 - Symmetry -0x00059 (Vertical Symmetry 3) - 0x00087 - Symmetry -0x00062 (Vertical Symmetry 4) - 0x00059 - Symmetry -0x0005C (Vertical Symmetry 5) - 0x00062 - Symmetry -0x0008D (Rotational Symmetry 1) - 0x0005C - Symmetry -0x00081 (Rotational Symmetry 2) - 0x0008D - Symmetry -0x00083 (Rotational Symmetry 3) - 0x00081 - Symmetry -0x00084 (Melting 1) - 0x00083 - Symmetry -0x00082 (Melting 2) - 0x00084 - Symmetry -0x0343A (Melting 3) - 0x00082 - Symmetry -0x17CC8 (Boat Spawn) - 0x0005C - Boat +Inside Glass Factory (Glass Factory) - Inside Glass Factory Behind Back Wall - 0x0D7ED: +158028 - 0x00086 (Vertical Symmetry 1) - True - Symmetry +158029 - 0x00087 (Vertical Symmetry 2) - 0x00086 - Symmetry +158030 - 0x00059 (Vertical Symmetry 3) - 0x00087 - Symmetry +158031 - 0x00062 (Vertical Symmetry 4) - 0x00059 - Symmetry +158032 - 0x0005C (Vertical Symmetry 5) - 0x00062 - Symmetry +158033 - 0x0008D (Rotational Symmetry 1) - 0x0005C - Symmetry +158034 - 0x00081 (Rotational Symmetry 2) - 0x0008D - Symmetry +158035 - 0x00083 (Rotational Symmetry 3) - 0x00081 - Symmetry +158036 - 0x00084 (Melting 1) - 0x00083 - Symmetry +158037 - 0x00082 (Melting 2) - 0x00084 - Symmetry +158038 - 0x0343A (Melting 3) - 0x00082 - Symmetry +Door - 0x0D7ED (Back Wall) - 0x0005C -Outside Symmetry Island (Symmetry Island) - Main Island - True: -0x000B0 (Door to Symmetry Island Lower) - 0x0343A - Dots +Inside Glass Factory Behind Back Wall (Glass Factory) - Boat - 0x17CC8: +158039 - 0x17CC8 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat -Symmetry Island Lower (Symmetry Island) - Outside Symmetry Island - 0x000B0: -0x00022 (Black Dots 1) - True - Symmetry & Dots -0x00023 (Black Dots 2) - 0x00022 - Symmetry & Dots -0x00024 (Black Dots 3) - 0x00023 - Symmetry & Dots -0x00025 (Black Dots 4) - 0x00024 - Symmetry & Dots -0x00026 (Black Dots 5) - 0x00025 - Symmetry & Dots -0x0007C (Colored Dots 1) - 0x00026 - Symmetry & Colored Dots -0x0007E (Colored Dots 2) - 0x0007C - Symmetry & Colored Dots -0x00075 (Colored Dots 3) - 0x0007E - Symmetry & Colored Dots -0x00073 (Colored Dots 4) - 0x00075 - Symmetry & Colored Dots -0x00077 (Colored Dots 5) - 0x00073 - Symmetry & Colored Dots -0x00079 (Colored Dots 6) - 0x00077 - Symmetry & Colored Dots -0x00065 (Fading Lines 1) - 0x00079 - Symmetry & Colored Dots -0x0006D (Fading Lines 2) - 0x00065 - Symmetry & Colored Dots -0x00072 (Fading Lines 3) - 0x0006D - Symmetry & Colored Dots -0x0006F (Fading Lines 4) - 0x00072 - Symmetry & Colored Dots -0x00070 (Fading Lines 5) - 0x0006F - Symmetry & Colored Dots -0x00071 (Fading Lines 6) - 0x00070 - Symmetry & Colored Dots -0x00076 (Fading Lines 7) - 0x00071 - Symmetry & Colored Dots -0x009B8 (Scenery Outlines 1) - True - Symmetry & Environment -0x003E8 (Scenery Outlines 2) - 0x009B8 - Symmetry & Environment -0x00A15 (Scenery Outlines 3) - 0x003E8 - Symmetry & Environment -0x00B53 (Scenery Outlines 4) - 0x00A15 - Symmetry & Environment -0x00B8D (Scenery Outlines 5) - 0x00B53 - Symmetry & Environment -0x1C349 (Door to Symmetry Island Upper) - 0x00076 - Symmetry & Dots +Outside Symmetry Island (Symmetry Island) - Main Island - True - Symmetry Island Lower - 0x17F3E: +158040 - 0x000B0 (Door to Symmetry Island Lower Panel) - 0x0343A - Dots +Door - 0x17F3E (Door to Symmetry Island Lower) - 0x000B0 -Symmetry Island Upper (Symmetry Island) - Symmetry Island Lower - 0x1C349: -0x00A52 (Yellow 1) - True - Symmetry & Colored Dots -0x00A57 (Yellow 2) - 0x00A52 - Symmetry & Colored Dots -0x00A5B (Yellow 3) - 0x00A57 - Symmetry & Colored Dots -0x00A61 (Blue 1) - 0x00A52 - Symmetry & Colored Dots -0x00A64 (Blue 2) - 0x00A61 & 0x00A52 - Symmetry & Colored Dots -0x00A68 (Blue 3) - 0x00A64 & 0x00A57 - Symmetry & Colored Dots -0x0360D (Laser) - 0x00A68 - True +Symmetry Island Lower (Symmetry Island) - Symmetry Island Upper - 0x18269: +158041 - 0x00022 (Black Dots 1) - True - Symmetry & Dots +158042 - 0x00023 (Black Dots 2) - 0x00022 - Symmetry & Dots +158043 - 0x00024 (Black Dots 3) - 0x00023 - Symmetry & Dots +158044 - 0x00025 (Black Dots 4) - 0x00024 - Symmetry & Dots +158045 - 0x00026 (Black Dots 5) - 0x00025 - Symmetry & Dots +158046 - 0x0007C (Colored Dots 1) - 0x00026 - Symmetry & Colored Dots +158047 - 0x0007E (Colored Dots 2) - 0x0007C - Symmetry & Colored Dots +158048 - 0x00075 (Colored Dots 3) - 0x0007E - Symmetry & Colored Dots +158049 - 0x00073 (Colored Dots 4) - 0x00075 - Symmetry & Colored Dots +158050 - 0x00077 (Colored Dots 5) - 0x00073 - Symmetry & Colored Dots +158051 - 0x00079 (Colored Dots 6) - 0x00077 - Symmetry & Colored Dots +158052 - 0x00065 (Fading Lines 1) - 0x00079 - Symmetry & Colored Dots +158053 - 0x0006D (Fading Lines 2) - 0x00065 - Symmetry & Colored Dots +158054 - 0x00072 (Fading Lines 3) - 0x0006D - Symmetry & Colored Dots +158055 - 0x0006F (Fading Lines 4) - 0x00072 - Symmetry & Colored Dots +158056 - 0x00070 (Fading Lines 5) - 0x0006F - Symmetry & Colored Dots +158057 - 0x00071 (Fading Lines 6) - 0x00070 - Symmetry & Colored Dots +158058 - 0x00076 (Fading Lines 7) - 0x00071 - Symmetry & Colored Dots +158059 - 0x009B8 (Scenery Outlines 1) - True - Symmetry & Environment +158060 - 0x003E8 (Scenery Outlines 2) - 0x009B8 - Symmetry & Environment +158061 - 0x00A15 (Scenery Outlines 3) - 0x003E8 - Symmetry & Environment +158062 - 0x00B53 (Scenery Outlines 4) - 0x00A15 - Symmetry & Environment +158063 - 0x00B8D (Scenery Outlines 5) - 0x00B53 - Symmetry & Environment +158064 - 0x1C349 (Door to Symmetry Island Upper Panel) - 0x00076 - Symmetry & Dots +Door - 0x18269 (Door to Symmetry Island Upper) - 0x1C349 -Orchard (Orchard) - Main Island - True: -0x00143 (Apple Tree 1) - True - Environment -0x0003B (Apple Tree 2) - 0x00143 - Environment -0x00055 (Apple Tree 3) - 0x0003B - Environment -0x032F7 (Apple Tree 4) - 0x00055 - Environment -0x032FF (Apple Tree 5) - 0x032F7 - Environment +Symmetry Island Upper (Symmetry Island): +158065 - 0x00A52 (Yellow 1) - True - Symmetry & Colored Dots +158066 - 0x00A57 (Yellow 2) - 0x00A52 - Symmetry & Colored Dots +158067 - 0x00A5B (Yellow 3) - 0x00A57 - Symmetry & Colored Dots +158068 - 0x00A61 (Blue 1) - 0x00A52 - Symmetry & Colored Dots +158069 - 0x00A64 (Blue 2) - 0x00A61 & 0x00A52 - Symmetry & Colored Dots +158070 - 0x00A68 (Blue 3) - 0x00A64 & 0x00A57 - Symmetry & Colored Dots +158700 - 0x0360D (Laser Panel) - 0x00A68 - True +Laser - 0x00509 (Laser) - 0x0360D - True -Desert Outside (Desert) - Main Island - True: -0x0CC7B (Vault) - True - Dots & Shapers & Rotated Shapers & Negative Shapers -0x0339E (Vault Box) - 0x0CC7B - True -0x17CE7 (Discard) - True - Triangles -0x00698 (Sun Reflection 1) - True - Reflection -0x0048F (Sun Reflection 2) - 0x00698 - Reflection -0x09F92 (Sun Reflection 3) - 0x0048F & 0x09FA0 - Reflection -0x09FA0 (Reflection 3 Control) - 0x0048F - True -0x0A036 (Sun Reflection 4) - 0x09F92 - Reflection -0x09DA6 (Sun Reflection 5) - 0x09F92 - Reflection -0x0A049 (Sun Reflection 6) - 0x09F92 - Reflection -0x0A053 (Sun Reflection 7) - 0x0A036 & 0x09DA6 & 0x0A049 - Reflection -0x09F94 (Sun Reflection 8) - 0x0A053 & 0x09F86 - Reflection -0x09F86 (Reflection 8 Control) - 0x0A053 - True -0x0C339 (Door to Desert Flood Light Room) - 0x09F94 - True +Orchard (Orchard) - Main Island - True - Orchard Beyond First Gate - 0x03307: +158071 - 0x00143 (Apple Tree 1) - True - Environment +158072 - 0x0003B (Apple Tree 2) - 0x00143 - Environment +158073 - 0x00055 (Apple Tree 3) - 0x0003B - Environment +Door - 0x03307 (Mid Gate) - 0x00055 -Desert Floodlight Room (Desert) - Desert Outside - 0x0C339: -0x09FAA (Light Control) - True - True -0x00422 (Artificial Light Reflection 1) - 0x09FAA - Reflection -0x006E3 (Artificial Light Reflection 2) - 0x09FAA - Reflection -0x0A02D (Artificial Light Reflection 3) - 0x09FAA & 0x00422 & 0x006E3 - Reflection +Orchard Beyond First Gate (Orchard) - Orchard End - 0x03313: +158074 - 0x032F7 (Apple Tree 4) - 0x00055 - Environment +158075 - 0x032FF (Apple Tree 5) - 0x032F7 - Environment +Door - 0x03313 (Final Gate) - 0x032FF -Desert Pond Room (Desert) - Desert Floodlight Room - 0x0A02D: -0x00C72 (Pond Reflection 1) - True - Reflection -0x0129D (Pond Reflection 2) - 0x00C72 - Reflection -0x008BB (Pond Reflection 3) - 0x0129D - Reflection -0x0078D (Pond Reflection 4) - 0x008BB - Reflection -0x18313 (Pond Reflection 5) - 0x0078D - Reflection -0x0A249 (Door to Desert Water Levels Room) - 0x18313 - Reflection +Orchard End (Orchard): -Desert Water Levels Room (Desert) - Desert Pond Room - 0x0A249: -0x1C2DF (Reduce Water Level Far Left) - True - True -0x1831E (Reduce Water Level Far Right) - True - True -0x1C260 (Reduce Water Level Near Left) - True - True -0x1831C (Reduce Water Level Near Right) - True - True -0x1C2F3 (Raise Water Level Far Left) - True - True -0x1831D (Raise Water Level Far Right) - True - True -0x1C2B1 (Raise Water Level Near Left) - True - True -0x1831B (Raise Water Level Near Right) - True - True -0x04D18 (Flood Reflection 1) - 0x1C260 & 0x1831C - Reflection -0x01205 (Flood Reflection 2) - 0x04D18 & 0x1C260 & 0x1831C - Reflection -0x181AB (Flood Reflection 3) - 0x01205 & 0x1C260 & 0x1831C - Reflection -0x0117A (Flood Reflection 4) - 0x181AB & 0x1C260 & 0x1831C - Reflection -0x17ECA (Flood Reflection 5) - 0x0117A & 0x1C260 & 0x1831C - Reflection -0x18076 (Flood Reflection 6) - 0x17ECA & 0x1C260 & 0x1831C - Reflection +Desert Outside (Desert) - Main Island - True - Desert Floodlight Room - 0x09FEE: +158652 - 0x0CC7B (Vault) - True - Dots & Shapers & Rotated Shapers & Negative Shapers +158653 - 0x0339E (Vault Box) - 0x0CC7B - True +158602 - 0x17CE7 (Discard) - True - Triangles +158076 - 0x00698 (Sun Reflection 1) - True - Reflection +158077 - 0x0048F (Sun Reflection 2) - 0x00698 - Reflection +158078 - 0x09F92 (Sun Reflection 3) - 0x0048F & 0x09FA0 - Reflection +158079 - 0x09FA0 (Reflection 3 Control) - 0x0048F - True +158080 - 0x0A036 (Sun Reflection 4) - 0x09F92 - Reflection +158081 - 0x09DA6 (Sun Reflection 5) - 0x09F92 - Reflection +158082 - 0x0A049 (Sun Reflection 6) - 0x09F92 - Reflection +158083 - 0x0A053 (Sun Reflection 7) - 0x0A036 & 0x09DA6 & 0x0A049 - Reflection +158084 - 0x09F94 (Sun Reflection 8) - 0x0A053 & 0x09F86 - Reflection +158085 - 0x09F86 (Reflection 8 Control) - 0x0A053 - True +158086 - 0x0C339 (Door to Desert Flood Light Room Panel) - 0x09F94 - True +Door - 0x09FEE (Door to Desert Flood Light Room) - 0x0C339 - True -Desert Elevator Room (Desert) - Desert Water Levels Room - 0x18076: -0x17C31 (Final Transparent Reflection) - True - Reflection -0x012D7 (Final Reflection) - 0x17C31 & 0x0A015 - Reflection -0x0A015 (Final Reflection Control) - 0x17C31 - True -0x0A15C (Final Bent Reflection 1) - True - Reflection -0x09FFF (Final Bent Reflection 2) - 0x0A15C - Reflection -0x0A15F (Final Bent Reflection 3) - 0x09FFF - Reflection -0x03608 (Laser) - 0x012D7 & 0x0A15F - True +Desert Floodlight Room (Desert) - Desert Pond Room - 0x0C2C3: +158087 - 0x09FAA (Light Control) - True - True +158088 - 0x00422 (Artificial Light Reflection 1) - 0x09FAA - Reflection +158089 - 0x006E3 (Artificial Light Reflection 2) - 0x09FAA - Reflection +158090 - 0x0A02D (Artificial Light Reflection 3) - 0x09FAA & 0x00422 & 0x006E3 - Reflection +Door - 0x0C2C3 (Door to Pond Room) - 0x0A02D -Outside Quarry (Quarry) - Main Island - True: -0x09E57 (Door to Quarry 1) - True - Squares & Black/White Squares -0x17C09 (Door to Quarry 2) - 0x09E57 - Shapers -0x17CC4 (Elevator Control) - 0x0367C - Dots & Eraser +Desert Pond Room (Desert) - Desert Water Levels Room - 0x0A24B: +158091 - 0x00C72 (Pond Reflection 1) - True - Reflection +158092 - 0x0129D (Pond Reflection 2) - 0x00C72 - Reflection +158093 - 0x008BB (Pond Reflection 3) - 0x0129D - Reflection +158094 - 0x0078D (Pond Reflection 4) - 0x008BB - Reflection +158095 - 0x18313 (Pond Reflection 5) - 0x0078D - Reflection +158096 - 0x0A249 (Door to Water Levels Room Panel) - 0x18313 - Reflection +Door - 0x0A24B (Door to Water Levels Room) - 0x0A249 -Quarry (Quarry) - Outside Quarry - 0x17C09 - Quarry Mill - 0x275ED - Quarry Mill - 0x17CAC - Shadows Ledge - 0x198BF: -0x01E5A (Door to Mill Left) - True - Squares & Black/White Squares -0x01E59 (Door to Mill Right) - True - Dots -0x17CF0 (Discard) - True - Triangles -0x03612 (Laser) - 0x0A3D0 & 0x0367C - Eraser & Shapers +Desert Water Levels Room (Desert) - Desert Elevator Room - 0x0C316: +158097 - 0x1C2DF (Reduce Water Level Far Left) - True - True +158098 - 0x1831E (Reduce Water Level Far Right) - True - True +158099 - 0x1C260 (Reduce Water Level Near Left) - True - True +158100 - 0x1831C (Reduce Water Level Near Right) - True - True +158101 - 0x1C2F3 (Raise Water Level Far Left) - True - True +158102 - 0x1831D (Raise Water Level Far Right) - True - True +158103 - 0x1C2B1 (Raise Water Level Near Left) - True - True +158104 - 0x1831B (Raise Water Level Near Right) - True - True +158105 - 0x04D18 (Flood Reflection 1) - 0x1C260 & 0x1831C - Reflection +158106 - 0x01205 (Flood Reflection 2) - 0x04D18 & 0x1C260 & 0x1831C - Reflection +158107 - 0x181AB (Flood Reflection 3) - 0x01205 & 0x1C260 & 0x1831C - Reflection +158108 - 0x0117A (Flood Reflection 4) - 0x181AB & 0x1C260 & 0x1831C - Reflection +158109 - 0x17ECA (Flood Reflection 5) - 0x0117A & 0x1C260 & 0x1831C - Reflection +158110 - 0x18076 (Flood Reflection 6) - 0x17ECA & 0x1C260 & 0x1831C - Reflection +Door - 0x0C316 (Door to Elevator Room) - 0x18076 -Quarry Mill (Quarry Mill) - Quarry - 0x01E59 & 0x01E5A: -0x275ED (Ground Floor Shortcut Door) - True - True -0x03678 (Lower Ramp Control) - True - Dots & Eraser -0x00E0C (Eraser and Dots 1) - 0x03678 - Dots & Eraser -0x01489 (Eraser and Dots 2) - 0x00E0C - Dots & Eraser -0x0148A (Eraser and Dots 3) - 0x01489 - Dots & Eraser -0x014D9 (Eraser and Dots 4) - 0x0148A - Dots & Eraser -0x014E7 (Eraser and Dots 5) - 0x014D9 - Dots & Eraser -0x014E8 (Eraser and Dots 6) - 0x014E7 - Dots & Eraser -0x03679 (Lower Lift Control) - 0x014E8 - Dots & Eraser -0x03675 (Upper Ramp Control) - 0x03679 - Dots & Eraser -0x03676 (Upper Lift Control) - 0x03679 - Dots & Eraser -0x00557 (Eraser and Squares 1) - 0x03679 - Squares & Colored Squares & Eraser -0x005F1 (Eraser and Squares 2) - 0x00557 - Squares & Colored Squares & Eraser -0x00620 (Eraser and Squares 3) - 0x005F1 - Squares & Colored Squares & Eraser -0x009F5 (Eraser and Squares 4) - 0x00620 - Squares & Colored Squares & Eraser -0x0146C (Eraser and Squares 5) - 0x009F5 - Squares & Colored Squares & Eraser -0x3C12D (Eraser and Squares 6) - 0x0146C - Squares & Colored Squares & Eraser -0x03686 (Eraser and Squares 7) - 0x3C12D - Squares & Colored Squares & Eraser -0x014E9 (Eraser and Squares 8) - 0x03686 - Squares & Colored Squares & Eraser -0x03677 (Stair Control) - 0x014E8 - Squares & Colored Squares & Eraser -0x3C125 (Big Squares & Dots & Eraser) - 0x0367C - Squares & Black/White Squares & Dots & Eraser -0x0367C (Small Squares & Dots & Eraser) - 0x014E9 - Squares & Colored Squares & Dots & Eraser -0x17CAC (Door to Outside Quarry Stairs) - True - True +Desert Elevator Room (Desert) - Desert Lowest Level Inbetween Shortcuts - 0x012FB: +158111 - 0x17C31 (Final Transparent Reflection) - True - Reflection +158113 - 0x012D7 (Final Reflection) - 0x17C31 & 0x0A015 - Reflection +158114 - 0x0A015 (Final Reflection Control) - 0x17C31 - True +158115 - 0x0A15C (Final Bent Reflection 1) - True - Reflection +158116 - 0x09FFF (Final Bent Reflection 2) - 0x0A15C - Reflection +158117 - 0x0A15F (Final Bent Reflection 3) - 0x09FFF - Reflection +158701 - 0x03608 (Laser Panel) - 0x012D7 & 0x0A15F - True +Laser - 0x012FB (Laser) - 0x03608 -Quarry Boathouse (Quarry Boathouse) - Quarry - True: -0x034D4 (Intro Stars) - True - Stars -0x021D5 (Intro Shapers) - True - Shapers & Rotated Shapers -0x03852 (Ramp Height Control) - 0x034D4 & 0x021D5 - Rotated Shapers -0x021B3 (Eraser and Shapers 1) - 0x03852 - Shapers & Eraser -0x021B4 (Eraser and Shapers 2) - 0x021B3 - Shapers & Eraser -0x021B0 (Eraser and Shapers 3) - 0x021B4 - Shapers & Eraser -0x021AF (Eraser and Shapers 4) - 0x021B0 - Shapers & Eraser -0x021AE (Eraser and Shapers 5) - 0x021AF - Shapers & Eraser & Broken Shapers -0x03858 (Ramp Horizontal Control) - 0x021AE - Shapers & Eraser -0x38663 (Shortcut Door) - 0x03858 - True -0x021B5 (Stars and Colored Eraser 1) - 0x03858 - Stars & Stars + Same Colored Symbol & Eraser -0x021B6 (Stars and Colored Eraser 2) - 0x021B5 - Stars & Stars + Same Colored Symbol & Eraser -0x021B7 (Stars and Colored Eraser 3) - 0x021B6 - Stars & Stars + Same Colored Symbol & Eraser -0x021BB (Stars and Colored Eraser 4) - 0x021B7 - Stars & Stars + Same Colored Symbol & Eraser -0x09DB5 (Stars and Colored Eraser 5) - 0x021BB - Stars & Stars + Same Colored Symbol & Eraser -0x09DB1 (Stars and Colored Eraser 6) - 0x09DB5 - Stars & Stars + Same Colored Symbol & Eraser -0x3C124 (Stars and Colored Eraser 7) - 0x09DB1 - Stars & Stars + Same Colored Symbol & Eraser -0x09DB3 (Stars & Eraser & Shapers 1) - 0x3C124 - Stars & Eraser & Shapers -0x09DB4 (Stars & Eraser & Shapers 2) - 0x09DB3 - Stars & Eraser & Shapers -0x275FA (Hook Control) - 0x03858 - Shapers & Eraser -0x17CA6 (Boat Spawn) - True - Boat -0x0A3CB (Stars & Eraser & Shapers 3) - 0x09DB4 - Stars & Eraser & Shapers -0x0A3CC (Stars & Eraser & Shapers 4) - 0x0A3CB - Stars & Eraser & Shapers -0x0A3D0 (Stars & Eraser & Shapers 5) - 0x0A3CC - Stars & Eraser & Shapers +Desert Lowest Level Inbetween Shortcuts (Desert): -Shadows (Shadows) - Main Island - True - Keep Glass Plates - 0x09E49: -0x334DB (Door Timer Outside) - True - True -0x0AC74 (Lower Avoid 6) - 0x0A8DC - Shadows Avoid -0x0AC7A (Lower Avoid 7) - 0x0AC74 - Shadows Avoid -0x0A8E0 (Lower Avoid 8) - 0x0AC7A - Shadows Avoid -0x386FA (Environmental Avoid 1) - 0x0A8E0 - Shadows Avoid & Environment -0x1C33F (Environmental Avoid 2) - 0x386FA - Shadows Avoid & Environment -0x196E2 (Environmental Avoid 3) - 0x1C33F - Shadows Avoid & Environment -0x1972A (Environmental Avoid 4) - 0x196E2 - Shadows Avoid & Environment -0x19809 (Environmental Avoid 5) - 0x1972A - Shadows Avoid & Environment -0x19806 (Environmental Avoid 6) - 0x19809 - Shadows Avoid & Environment -0x196F8 (Environmental Avoid 7) - 0x19806 - Shadows Avoid & Environment -0x1972F (Environmental Avoid 8) - 0x196F8 - Shadows Avoid & Environment -0x19797 (Follow 1) - 0x0A8E0 - Shadows Follow -0x1979A (Follow 2) - 0x19797 - Shadows Follow -0x197E0 (Follow 3) - 0x1979A - Shadows Follow -0x197E8 (Follow 4) - 0x197E0 - Shadows Follow -0x197E5 (Follow 5) - 0x197E8 - Shadows Follow -0x19650 (Laser) - 0x197E5 & 0x196F8 - Shadows Avoid & Shadows Follow +Outside Quarry (Quarry) - Main Island - True - Quarry Between Entry Doors - 0x09D6F: +158118 - 0x09E57 (Door to Quarry 1 Panel) - True - Squares & Black/White Squares +158120 - 0x17CC4 (Elevator Control) - 0x0367C - Dots & Eraser +158603 - 0x17CF0 (Discard) - True - Triangles +158702 - 0x03612 (Laser Panel) - 0x0A3D0 & 0x0367C - Eraser & Shapers +Laser - 0x01539 (Laser) - 0x03612 +Door - 0x09D6F (Door to Quarry 1) - 0x09E57 -Shadows Ledge (Shadows) - Shadows - 0x334DB | 0x334DC | 0x0A8DC: -0x334DC (Door Timer Inside) - True - True -0x198B5 (Lower Avoid 1) - True - Shadows Avoid -0x198BD (Lower Avoid 2) - 0x198B5 - Shadows Avoid -0x198BF (Lower Avoid 3) - 0x198BD & 0x334DC - Shadows Avoid -0x19771 (Lower Avoid 4) - 0x198BF - Shadows Avoid -0x0A8DC (Lower Avoid 5) - 0x19771 - Shadows Avoid +Quarry Between Entry Doors (Quarry) - Quarry - 0x17C07: +158119 - 0x17C09 (Door to Quarry 2 Panel) - True - Shapers +Door - 0x17C07 (Door to Quarry 2) - 0x17C09 -Keep (Keep) - Main Island - True: +Quarry (Quarry) - Quarry Mill Ground Floor - 0x02010: +158121 - 0x01E5A (Door to Mill Left) - True - Squares & Black/White Squares +158122 - 0x01E59 (Door to Mill Right) - True - Dots +Door - 0x02010 (Door to Mill) - 0x01E59 & 0x01E5A -Keep Hedges (Keep) - Keep - True: -0x00139 (Hedge Maze 1) - True - Environment -0x019DC (Hedge Maze 2) - 0x00139 - Environment -0x019E7 (Hedge Maze 3) - 0x019DC - Environment & Sound -0x01A0F (Hedge Maze 4) - 0x019E7 - Environment +Quarry Mill Ground Floor (Quarry Mill) - Quarry - 0x275FF - Quarry Mill Middle Floor - 0x03678 - Outside Quarry - 0x17CE8: +158123 - 0x275ED (Ground Floor Shortcut Door Panel) - True - True +Door - 0x275FF (Ground Floor Shortcut Door) - 0x275ED +158124 - 0x03678 (Lower Ramp Control) - True - Dots & Eraser +158145 - 0x17CAC (Door to Outside Quarry Stairs Panel) - True - True +Door - 0x17CE8 (Door to Outside Quarry Stairs) - 0x17CAC -Keep Glass Plates (Keep) - Keep - True - Keep Tower - 0x0361B: -0x0A3A8 (Reset Pressure Plates 1) - True - True -0x033EA (Pressure Plates 1) - 0x0A3A8 - Pressure Plates & Dots -0x0A3B9 (Reset Pressure Plates 2) - 0x033EA - True -0x01BE9 (Pressure Plates 2) - 0x033EA & 0x0A3B9 - Pressure Plates & Stars & Stars + Same Colored Symbol & Squares & Black/White Squares -0x0A3BB (Reset Pressure Plates 3) - 0x0A3A8 - True -0x01CD3 (Pressure Plates 3) - 0x0A3A8 & 0x0A3BB - Pressure Plates & Shapers & Squares & Black/White Squares & Colored Squares -0x0A3AD (Reset Pressure Plates 4) - 0x01CD3 - True -0x01D3F (Pressure Plates 4) - 0x01CD3 & 0x0A3AD - Pressure Plates & Shapers & Dots & Symmetry -0x17D27 (Discard) - 0x01CD3 - Triangles -0x09E49 (Shortcut to Shadows) - 0x01CD3 - True +Quarry Mill Middle Floor (Quarry Mill) - Quarry Mill Ground Floor - 0x03675 - Quarry Mill Upper Floor - 0x03679: +158125 - 0x00E0C (Eraser and Dots 1) - True - Dots & Eraser +158126 - 0x01489 (Eraser and Dots 2) - 0x00E0C - Dots & Eraser +158127 - 0x0148A (Eraser and Dots 3) - 0x01489 - Dots & Eraser +158128 - 0x014D9 (Eraser and Dots 4) - 0x0148A - Dots & Eraser +158129 - 0x014E7 (Eraser and Dots 5) - 0x014D9 - Dots & Eraser +158130 - 0x014E8 (Eraser and Dots 6) - 0x014E7 - Dots & Eraser +158131 - 0x03679 (Lower Lift Control) - 0x014E8 - Dots & Eraser -Shipwreck (Shipwreck) - Keep Glass Plates - 0x033EA: -0x00AFB (Vault) - True - Symmetry & Sound & Sound Dots & Colored Dots -0x03535 (Vault Box) - 0x00AFB - True -0x17D28 (Discard) - True - Triangles +Quarry Mill Upper Floor (Quarry Mill) - Quarry Mill Middle Floor - 0x03676 & 0x03679 - Quarry Mill Ground Floor - 0x0368A: +158132 - 0x03676 (Upper Ramp Control) - True - Dots & Eraser +158133 - 0x03675 (Upper Lift Control) - True - Dots & Eraser +158134 - 0x00557 (Eraser and Squares 1) - True - Squares & Colored Squares & Eraser +158135 - 0x005F1 (Eraser and Squares 2) - 0x00557 - Squares & Colored Squares & Eraser +158136 - 0x00620 (Eraser and Squares 3) - 0x005F1 - Squares & Colored Squares & Eraser +158137 - 0x009F5 (Eraser and Squares 4) - 0x00620 - Squares & Colored Squares & Eraser +158138 - 0x0146C (Eraser and Squares 5) - 0x009F5 - Squares & Colored Squares & Eraser +158139 - 0x3C12D (Eraser and Squares 6) - 0x0146C - Squares & Colored Squares & Eraser +158140 - 0x03686 (Eraser and Squares 7) - 0x3C12D - Squares & Colored Squares & Eraser +158141 - 0x014E9 (Eraser and Squares 8) - 0x03686 - Squares & Colored Squares & Eraser +158142 - 0x03677 (Stair Control) - True - Squares & Colored Squares & Eraser +Door - 0x0368A (Stairs) - 0x03677 +158143 - 0x3C125 (Big Squares & Dots & Eraser) - 0x0367C - Squares & Black/White Squares & Dots & Eraser +158144 - 0x0367C (Small Squares & Dots & Eraser) - 0x014E9 - Squares & Colored Squares & Dots & Eraser -Keep Tower (Keep) - Keep Hedges - 0x01A0F - Keep Glass Plates - 0x01D3F: -0x0361B (Shortcut to Keep Glass Plates) - True - True -0x0360E (Laser Hedges) - 0x01A0F - Environment & Sound -0x03317 (Laser Pressure Plates) - 0x01D3F - Shapers & Squares & Black/White Squares & Colored Squares & Stars & Stars + Same Colored Symbol & Dots +Quarry Boathouse (Quarry Boathouse) - Quarry - True - Quarry Boathouse Upper Front - 0x03852 - Quarry Boathouse Behind Staircase - 0x2769B: +158146 - 0x034D4 (Intro Stars) - True - Stars +158147 - 0x021D5 (Intro Shapers) - True - Shapers & Rotated Shapers +158148 - 0x03852 (Ramp Height Control) - 0x034D4 & 0x021D5 - Rotated Shapers +158166 - 0x17CA6 (Boat Spawn) - True - Boat +Door - 0x2769B (Boat Staircase) - 0x17CA6 +Door - 0x27163 (Boat Staircase Invis Barrier) - 0x17CA6 -Outside Monastery (Monastery) - Main Island - True: -0x03713 (Shortcut) - True - True -0x00B10 (Door Open Left) - True - True -0x00C92 (Door Open Right) - True - True -0x00290 (Rhombic Avoid 1) - 0x09D9B - Environment -0x00038 (Rhombic Avoid 2) - 0x09D9B & 0x00290 - Environment -0x00037 (Rhombic Avoid 3) - 0x09D9B & 0x00038 - Environment -0x17CA4 (Laser) - 0x193A6 - True +Quarry Boathouse Behind Staircase (Quarry Boathouse) - Boat - 0x17CA6: -Inside Monastery (Monastery) - Outside Monastery - 0x00B10 & 0x00C92: -0x09D9B (Overhead Door Control) - True - Dots -0x193A7 (Branch Avoid 1) - 0x00037 - Environment -0x193AA (Branch Avoid 2) - 0x193A7 - Environment -0x193AB (Branch Follow 1) - 0x193AA - Environment -0x193A6 (Branch Follow 2) - 0x193AB - Environment +Quarry Boathouse Upper Front (Quarry Boathouse) - Quarry Boathouse Upper Middle - 0x17C50: +158149 - 0x021B3 (Eraser and Shapers 1) - True - Shapers & Eraser +158150 - 0x021B4 (Eraser and Shapers 2) - 0x021B3 - Shapers & Eraser +158151 - 0x021B0 (Eraser and Shapers 3) - 0x021B4 - Shapers & Eraser +158152 - 0x021AF (Eraser and Shapers 4) - 0x021B0 - Shapers & Eraser +158153 - 0x021AE (Eraser and Shapers 5) - 0x021AF - Shapers & Eraser & Broken Shapers +Door - 0x17C50 (Boathouse Barrier 1) - 0x021AE -Monastery Garden (Monastery) - Outside Monastery - 0x00037 - Outside Jungle River - 0x17CAA: +Quarry Boathouse Upper Middle (Quarry Boathouse) - Quarry Boathouse Upper Back - 0x03858: +158154 - 0x03858 (Ramp Horizontal Control) - True - Shapers & Eraser -Town (Town) - Main Island - True - Theater - 0x0A168 | 0x33AB2: -0x0A054 (Boat Summon) - True - Boat -0x0A0C8 (Cargo Box) - True - Squares & Black/White Squares & Shapers -0x17D01 (Cargo Box Discard) - 0x0A0C8 - Triangles -0x09F98 (Desert Laser Redirect) - True - True -0x18590 (Tree Outlines) - True - Symmetry & Environment -0x28AE3 (Vines Shadows Follow) - 0x18590 - Shadows Follow & Environment -0x28938 (Four-way Apple Tree) - 0x28AE3 - Environment -0x079DF (Triple Environmental Puzzle) - 0x28938 - Shadows Avoid & Environment & Reflection -0x28B39 (Hexagonal Reflection) - 0x079DF & 0x2896A - Reflection -0x28998 (Tinted Door to RGB House) - True - Stars & Rotated Shapers -0x28A0D (Door to Church) - 0x28998 - Stars & RGB & Environment -0x28A69 (Square Avoid) - 0x28A0D - Environment -0x28A79 (Maze Stair Control) - True - Environment -0x2896A (Maze Rooftop Bridge Control) - 0x28A79 - Shapers -0x17C71 (Rooftop Discard) - 0x2896A - Triangles -0x28AC7 (Symmetry Squares 1) - 0x2896A - Symmetry & Squares & Black/White Squares -0x28AC8 (Symmetry Squares 2) - 0x28AC7 - Symmetry & Squares & Black/White Squares -0x28ACA (Symmetry Squares 3 + Dots) - 0x28AC8 - Symmetry & Squares & Black/White Squares & Dots -0x28ACB (Symmetry Squares 4 + Dots) - 0x28ACA - Symmetry & Squares & Black/White Squares & Dots -0x28ACC (Symmetry Squares 5 + Dots) - 0x28ACB - Symmetry & Squares & Black/White Squares & Dots -0x2899C (Full Dot Grid Shapers 1) - True - Rotated Shapers & Dots -0x28A33 (Full Dot Grid Shapers 2) - 0x2899C - Shapers & Dots -0x28ABF (Full Dot Grid Shapers 3) - 0x28A33 - Shapers & Rotated Shapers & Dots -0x28AC0 (Full Dot Grid Shapers 4) - 0x28ABF - Rotated Shapers & Dots -0x28AC1 (Full Dot Grid Shapers 5) - 0x28AC0 - Rotated Shapers & Dots -0x28AD9 (Shapers & Dots & Eraser) - 0x28AC1 - Rotated Shapers & Dots & Eraser -0x17F5F (Windmill Door) - True - Dots +Quarry Boathouse Upper Back (Quarry Boathouse) - Quarry Boathouse Upper Middle - 0x3865F: +158155 - 0x38663 (Shortcut Door Panel) - True - True +Door - 0x3865F (Shortcut Door) - 0x38663 +158156 - 0x021B5 (Stars and Colored Eraser 1) - True - Stars & Stars + Same Colored Symbol & Eraser +158157 - 0x021B6 (Stars and Colored Eraser 2) - 0x021B5 - Stars & Stars + Same Colored Symbol & Eraser +158158 - 0x021B7 (Stars and Colored Eraser 3) - 0x021B6 - Stars & Stars + Same Colored Symbol & Eraser +158159 - 0x021BB (Stars and Colored Eraser 4) - 0x021B7 - Stars & Stars + Same Colored Symbol & Eraser +158160 - 0x09DB5 (Stars and Colored Eraser 5) - 0x021BB - Stars & Stars + Same Colored Symbol & Eraser +158161 - 0x09DB1 (Stars and Colored Eraser 6) - 0x09DB5 - Stars & Stars + Same Colored Symbol & Eraser +158162 - 0x3C124 (Stars and Colored Eraser 7) - 0x09DB1 - Stars & Stars + Same Colored Symbol & Eraser +158163 - 0x09DB3 (Stars & Eraser & Shapers 1) - 0x3C124 - Stars & Eraser & Shapers +158164 - 0x09DB4 (Stars & Eraser & Shapers 2) - 0x09DB3 - Stars & Eraser & Shapers +158165 - 0x275FA (Hook Control) - True - Shapers & Eraser +158167 - 0x0A3CB (Stars & Eraser & Shapers 3) - 0x09DB4 - Stars & Eraser & Shapers +158168 - 0x0A3CC (Stars & Eraser & Shapers 4) - 0x0A3CB - Stars & Eraser & Shapers +158169 - 0x0A3D0 (Stars & Eraser & Shapers 5) - 0x0A3CC - Stars & Eraser & Shapers -RGB House (Town) - Town - 0x28998: -0x034E4 (Sound Room Left) - True - Sound & Sound Waves -0x034E3 (Sound Room Right) - True - Sound & Sound Dots -0x334D8 (RGB Control) - 0x034E4 & 0x034E3 - Rotated Shapers & RGB & Squares & Colored Squares -0x03C0C (RGB Squares) - 0x334D8 - RGB & Squares & Colored Squares & Black/White Squares -0x03C08 (RGB Stars) - 0x334D8 - RGB & Stars +Shadows (Shadows) - Main Island - True - Shadows Ledge - 0x19B24 - Shadows Laser Room - 0x194B2 & 0x19665: +158170 - 0x334DB (Door Timer Outside) - True - True +Door - 0x19B24 (Timed Door) - 0x334DB +158171 - 0x0AC74 (Lower Avoid 6) - 0x0A8DC - Shadows Avoid +158172 - 0x0AC7A (Lower Avoid 7) - 0x0AC74 - Shadows Avoid +158173 - 0x0A8E0 (Lower Avoid 8) - 0x0AC7A - Shadows Avoid +158174 - 0x386FA (Environmental Avoid 1) - 0x0A8E0 - Shadows Avoid & Environment +158175 - 0x1C33F (Environmental Avoid 2) - 0x386FA - Shadows Avoid & Environment +158176 - 0x196E2 (Environmental Avoid 3) - 0x1C33F - Shadows Avoid & Environment +158177 - 0x1972A (Environmental Avoid 4) - 0x196E2 - Shadows Avoid & Environment +158178 - 0x19809 (Environmental Avoid 5) - 0x1972A - Shadows Avoid & Environment +158179 - 0x19806 (Environmental Avoid 6) - 0x19809 - Shadows Avoid & Environment +158180 - 0x196F8 (Environmental Avoid 7) - 0x19806 - Shadows Avoid & Environment +158181 - 0x1972F (Environmental Avoid 8) - 0x196F8 - Shadows Avoid & Environment +Door - 0x194B2 (Laser Room Right Door) - 0x1972F +158182 - 0x19797 (Follow 1) - 0x0A8E0 - Shadows Follow +158183 - 0x1979A (Follow 2) - 0x19797 - Shadows Follow +158184 - 0x197E0 (Follow 3) - 0x1979A - Shadows Follow +158185 - 0x197E8 (Follow 4) - 0x197E0 - Shadows Follow +158186 - 0x197E5 (Follow 5) - 0x197E8 - Shadows Follow +Door - 0x19665 (Laser Room Left Door) - 0x197E5 -Town Tower Top (Town) - Town - 0x28A69 & 0x28B39 & 0x28ACC & 0x28AD9: -0x032F5 (Laser) - True - True +Shadows Ledge (Shadows) - Shadows - 0x1855B - Quarry - 0x19865 & 0x0A2DF: +158187 - 0x334DC (Door Timer Inside) - True - True +158188 - 0x198B5 (Lower Avoid 1) - True - Shadows Avoid +158189 - 0x198BD (Lower Avoid 2) - 0x198B5 - Shadows Avoid +158190 - 0x198BF (Lower Avoid 3) - 0x198BD & 0x334DC & 0x19B24 - Shadows Avoid +Door - 0x19865 (Barrier to Quarry) - 0x198BF +Door - 0x0A2DF (Barrier to Quarry 2) - 0x198BF +158191 - 0x19771 (Lower Avoid 4) - 0x198BF - Shadows Avoid +158192 - 0x0A8DC (Lower Avoid 5) - 0x19771 - Shadows Avoid +Door - 0x1855B (Barrier to Shadows) - 0x0A8DC +Door - 0x19ADE (Barrier to Shadows 2) - 0x0A8DC -Windmill Interior (Windmill) - Town - 0x17F5F: -0x17D02 (Turn Control) - True - Dots -0x17F89 (Door to Front of Theater) - True - Squares & Black/White Squares +Shadows Laser Room (Shadows): +158703 - 0x19650 (Laser Panel) - True - Shadows Avoid & Shadows Follow +Laser - 0x181B3 (Laser) - 0x19650 -Theater (Theater) - Windmill Interior - 0x17F89: -0x00815 (Video Input) - True - True -0x03553 (Tutorial Video) - 0x00815 & 0x03481 - True -0x03552 (Desert Video) - 0x00815 & 0x0339E - True -0x0354E (Jungle Video) - 0x00815 & 0x03702 - True -0x03549 (Challenge Video) - 0x00815 & 0x2FAF6 - True -0x0354F (Shipwreck Video) - 0x00815 & 0x03535 - True -0x03545 (Mountain Video) - 0x00815 & 0x03542 - True -0x0A168 (Door to Cargo Box Left) - True - Squares & Black/White Squares & Eraser -0x33AB2 (Door to Cargo Box Right) - True - Squares & Black/White Squares & Shapers -0x17CF7 (Discard) - True - Triangles +Keep (Keep) - Main Island - True - Keep 2nd Maze - 0x01954 - Keep 2nd Pressure Plate - 0x01BEC: +158193 - 0x00139 (Hedge Maze 1) - True - Environment +158197 - 0x0A3A8 (Reset Pressure Plates 1) - True - True +158198 - 0x033EA (Pressure Plates 1) - 0x0A3A8 - Pressure Plates & Dots +Door - 0x01954 (Hedge Maze 1 Exit Door) - 0x00139 +Door - 0x01BEC (Pressure Plates 1 Exit Door) - 0x033EA -Jungle (Jungle) - Main Island - True: -0x17CDF (Shore Boat Spawn) - True - Boat -0x17F9B (Discard) - True - Triangles -0x002C4 (Waves 1) - True - Sound & Sound Waves -0x00767 (Waves 2) - 0x002C4 - Sound & Sound Waves -0x002C6 (Waves 3) - 0x00767 - Sound & Sound Waves -0x0070E (Waves 4) - 0x002C6 - Sound & Sound Waves -0x0070F (Waves 5) - 0x0070E - Sound & Sound Waves -0x0087D (Waves 6) - 0x0070F - Sound & Sound Waves -0x002C7 (Waves 7) - 0x0087D - Sound & Sound Waves -0x17CAB (Popup Wall Control) - 0x002C7 - True -0x0026D (Popup Wall 1) - 0x17CAB - Sound & Sound Dots -0x0026E (Popup Wall 2) - 0x0026D - Sound & Sound Dots -0x0026F (Popup Wall 3) - 0x0026E - Sound & Sound Dots -0x00C3F (Popup Wall 4) - 0x0026F - Sound & Sound Dots -0x00C41 (Popup Wall 5) - 0x00C3F - Sound & Sound Dots -0x014B2 (Popup Wall 6) - 0x00C41 - Sound & Sound Dots -0x03616 (Laser) - 0x014B2 - True -0x337FA (Shortcut to River) - True - True +Keep 2nd Maze (Keep) - Keep - 0x018CE - Keep 3rd Maze - 0x019D8: +Door - 0x018CE (Hedge Maze 2 Shortcut) - 0x00139 +158194 - 0x019DC (Hedge Maze 2) - True - Environment +Door - 0x019D8 (Hedge Maze 2 Exit Door) - 0x019DC -Outside Jungle River (River) - Main Island - True - Jungle - 0x337FA: -0x17CAA (Rhombic Avoid to Monastery Garden) - True - Environment -0x15ADD (Vault) - True - Environment & Black/White Squares & Dots -0x03702 (Vault Box) - 0x15ADD - True +Keep 3rd Maze (Keep) - Keep - 0x019B5 - Keep 4th Maze - 0x019E6: +Door - 0x019B5 (Hedge Maze 3 Shortcut) - 0x019DC +158195 - 0x019E7 (Hedge Maze 3) - True - Environment & Sound +Door - 0x019E6 (Hedge Maze 3 Exit Door) - 0x019E7 -Outside Bunker (Bunker) - Main Island - True - Inside Bunker - 0x0A079: -0x17C2E (Door to Bunker) - True - Squares & Black/White Squares -0x09DE0 (Laser) - 0x0A079 - True +Keep 4th Maze (Keep) - Keep - 0x0199A - Keep Tower - 0x01A0E: +Door - 0x0199A (Hedge Maze 4 Shortcut) - 0x019E7 +158196 - 0x01A0F (Hedge Maze 4) - True - Environment +Door - 0x01A0E (Hedge Maze 4 Exit Door) - 0x01A0F -Inside Bunker (Bunker) - Outside Bunker - 0x17C2E: -0x09F7D (Drawn Squares 1) - True - Squares & Colored Squares -0x09FDC (Drawn Squares 2) - 0x09F7D - Squares & Colored Squares & Black/White Squares -0x09FF7 (Drawn Squares 3) - 0x09FDC - Squares & Colored Squares & Black/White Squares -0x09F82 (Drawn Squares 4) - 0x09FF7 - Squares & Colored Squares & Black/White Squares -0x09FF8 (Drawn Squares 5) - 0x09F82 - Squares & Colored Squares & Black/White Squares -0x09D9F (Drawn Squares 6) - 0x09FF8 - Squares & Colored Squares & Black/White Squares -0x09DA1 (Drawn Squares 7) - 0x09D9F - Squares & Colored Squares -0x09DA2 (Drawn Squares 8) - 0x09DA1 - Squares & Colored Squares -0x09DAF (Drawn Squares 9) - 0x09DA2 - Squares & Colored Squares -0x0A099 (Door to Bunker Proper) - 0x09DAF - True -0x0A010 (Drawn Squares through Tinted Glass 1) - 0x0A099 - Squares & Colored Squares & RGB & Environment -0x0A01B (Drawn Squares through Tinted Glass 2) - 0x0A010 - Squares & Colored Squares & Black/White Squares & RGB & Environment -0x0A01F (Drawn Squares through Tinted Glass 3) - 0x0A01B - Squares & Colored Squares & Black/White Squares & RGB & Environment -0x34BC5 (Drop-Down Door Open) - 0x0A01F - True -0x34BC6 (Drop-Down Door Close) - 0x34BC5 - True -0x17E63 (Drop-Down Door Squares 1) - 0x0A01F & 0x34BC5 - Squares & Colored Squares & RGB & Environment -0x17E67 (Drop-Down Door Squares 2) - 0x17E63 & 0x34BC6 - Squares & Colored Squares & Black/White Squares & RGB -0x0A079 (Elevator Control) - 0x17E67 - Squares & Colored Squares & Black/White Squares & RGB +Keep 2nd Pressure Plate (Keep) - Keep 3rd Pressure Plate - 0x01BEA: +158199 - 0x0A3B9 (Reset Pressure Plates 2) - True - True +158200 - 0x01BE9 (Pressure Plates 2) - 0x0A3B9 - Pressure Plates & Stars & Stars + Same Colored Symbol & Squares & Black/White Squares +Door - 0x01BEA (Pressure Plates 2 Exit Door) - 0x01BE9 -Outside Swamp (Swamp) - Main Island - True: -0x0056E (Entry Door) - True - Shapers +Keep 3rd Pressure Plate (Keep) - Keep 4th Pressure Plate - 0x01CD5: +158201 - 0x0A3BB (Reset Pressure Plates 3) - True - True +158202 - 0x01CD3 (Pressure Plates 3) - 0x0A3BB - Pressure Plates & Shapers & Squares & Black/White Squares & Colored Squares +Door - 0x01CD5 (Pressure Plates 3 Exit Door) - 0x01CD3 -Swamp Entry Area (Swamp) - Outside Swamp - 0x0056E: -0x00469 (Seperatable Shapers 1) - True - Shapers -0x00472 (Seperatable Shapers 2) - 0x00469 - Shapers -0x00262 (Seperatable Shapers 3) - 0x00472 - Shapers -0x00474 (Seperatable Shapers 4) - 0x00262 - Shapers -0x00553 (Seperatable Shapers 5) - 0x00474 - Shapers -0x0056F (Seperatable Shapers 6) - 0x00553 - Shapers -0x00390 (Combinable Shapers 1) - 0x0056F - Shapers -0x010CA (Combinable Shapers 2) - 0x00390 - Shapers -0x00983 (Combinable Shapers 3) - 0x010CA - Shapers -0x00984 (Combinable Shapers 4) - 0x00983 - Shapers -0x00986 (Combinable Shapers 5) - 0x00984 - Shapers -0x00985 (Combinable Shapers 6) - 0x00986 - Shapers -0x00987 (Combinable Shapers 7) - 0x00985 - Shapers -0x181A9 (Combinable Shapers 8) - 0x00987 - Shapers -0x00609 (Sliding Bridge) - 0x181A9 - Shapers +Keep 4th Pressure Plate (Keep) - Keep - 0x09E3D - Keep Tower - 0x01D40: +158203 - 0x0A3AD (Reset Pressure Plates 4) - True - True +158204 - 0x01D3F (Pressure Plates 4) - 0x0A3AD - Pressure Plates & Shapers & Dots & Symmetry +Door - 0x01D40 (Pressure Plates 4 Exit Door) - 0x01D3F +158604 - 0x17D27 (Discard) - True - Triangles +158205 - 0x09E49 (Shortcut to Shadows Panel) - True - True +Door - 0x09E3D (Shortcut to Shadows) - 0x09E49 -Swamp Near Platform (Swamp) - Swamp Entry Area - 0x00609 | 0x18488: -0x00999 (Broken Shapers 1) - 0x00990 - Broken Shapers -0x0099D (Broken Shapers 2) - 0x00999 - Broken Shapers -0x009A0 (Broken Shapers 3) - 0x0099D - Broken Shapers -0x009A1 (Broken Shapers 4) - 0x009A0 - Broken Shapers -0x00002 (Cyan Underwater Negative Shapers 1) - 0x00006 - Shapers & Negative Shapers -0x00004 (Cyan Underwater Negative Shapers 2) - 0x00002 - Shapers & Negative Shapers -0x00005 (Cyan Underwater Negative Shapers 3) - 0x00004 - Shapers & Negative Shapers -0x013E6 (Cyan Underwater Negative Shapers 4) - 0x00005 - Shapers & Negative Shapers -0x00596 (Cyan Underwater Negative Shapers 5) - 0x013E6 - Shapers & Negative Shapers -0x18488 (Cyan Underwater Sliding Bridge Control) - 0x00006 - Shapers +Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True: +158654 - 0x00AFB (Vault) - True - Symmetry & Sound & Sound Dots & Colored Dots +158655 - 0x03535 (Vault Box) - 0x00AFB - True +158605 - 0x17D28 (Discard) - True - Triangles -Swamp Platform (Swamp) - Swamp Near Platform - True: -0x00982 (Platform Shapers 1) - True - Shapers -0x0097F (Platform Shapers 2) - 0x00982 - Shapers -0x0098F (Platform Shapers 3) - 0x0097F - Shapers -0x00990 (Platform Shapers 4) - 0x0098F - Shapers -0x17C0D (Platform Shortcut Door Left) - True - Shapers -0x17C0E (Platform Shortcut Door Right) - True - Shapers +Keep Tower (Keep) - Keep - 0x04F8F: +158206 - 0x0361B (Tower Shortcut to Keep Panel) - True - True +Door - 0x04F8F (Tower Shortcut to Keep) - 0x0361B +158704 - 0x0360E (Laser Panel Hedges) - 0x01A0F & 0x019E7 & 0x019DC & 0x00139 - Environment & Sound +158705 - 0x03317 (Laser Panel Pressure Plates) - 0x01D3F - Shapers & Squares & Black/White Squares & Colored Squares & Stars & Stars + Same Colored Symbol & Dots +Laser - 0x014BB (Laser) - 0x0360E | 0x03317 -Swamp Rotating Bridge Near Side (Swamp) - Swamp Near Platform - 0x009A1: -0x00007 (Rotated Shapers 1) - 0x009A1 - Rotated Shapers -0x00008 (Rotated Shapers 2) - 0x00007 - Rotated Shapers & Shapers -0x00009 (Rotated Shapers 3) - 0x00008 - Rotated Shapers -0x0000A (Rotated Shapers 4) - 0x00009 - Rotated Shapers -0x00001 (Red Underwater Negative Shapers 1) - 0x00596 - Shapers & Negative Shapers -0x014D2 (Red Underwater Negative Shapers 2) - 0x00596 - Shapers & Negative Shapers -0x014D4 (Red Underwater Negative Shapers 3) - 0x00596 - Shapers & Negative Shapers -0x014D1 (Red Underwater Negative Shapers 4) - 0x00596 - Shapers & Negative Shapers +Outside Monastery (Monastery) - Main Island - True - Main Island - 0x0364E - Inside Monastery - 0x0C128 & 0x0C153 - Monastery Garden - 0x03750: +158207 - 0x03713 (Shortcut Door Panel) - True - True +Door - 0x0364E (Shortcut) - 0x03713 +158208 - 0x00B10 (Door Open Left) - True - True +158209 - 0x00C92 (Door Open Right) - True - True +Door - 0x0C128 (Left Door) - 0x00B10 +Door - 0x0C153 (Right Door) - 0x00C92 +158210 - 0x00290 (Rhombic Avoid 1) - 0x09D9B - Environment +158211 - 0x00038 (Rhombic Avoid 2) - 0x09D9B & 0x00290 - Environment +158212 - 0x00037 (Rhombic Avoid 3) - 0x09D9B & 0x00038 - Environment +Door - 0x03750 (Door to Garden) - 0x00037 +158706 - 0x17CA4 (Laser Panel) - 0x193A6 - True +Laser - 0x17C65 (Laser) - 0x17CA4 -Swamp Near Boat (Swamp) - Swamp Rotating Bridge Near Side - 0x0000A - Swamp Platform - 0x17C0D & 0x17C0E: -0x181F5 (Rotating Bridge) - True - Rotated Shapers & Shapers -0x09DB8 (Boat Spawn) - True - Boat -0x003B2 (More Rotated Shapers 1) - 0x0000A - Rotated Shapers -0x00A1E (More Rotated Shapers 2) - 0x003B2 - Rotated Shapers -0x00C2E (More Rotated Shapers 3) - 0x00A1E - Rotated Shapers -0x00E3A (More Rotated Shapers 4) - 0x00C2E - Rotated Shapers -0x009A6 (Underwater Back Optional) - 0x00E3A & 0x181F5 - Shapers -0x009AB (Blue Underwater Negative Shapers 1) - 0x00E3A - Shapers & Negative Shapers -0x009AD (Blue Underwater Negative Shapers 2) - 0x009AB - Shapers & Negative Shapers -0x009AE (Blue Underwater Negetive Shapers 3) - 0x009AD - Shapers & Negative Shapers -0x009AF (Blue Underwater Negative Shapers 4) - 0x009AE - Shapers & Negative Shapers -0x00006 (Blue Underwater Negative Shapers 5) - 0x009AF - Shapers & Negative Shapers & Broken Negative Shapers -0x17E2B (Long Bridge Control) - True - Rotated Shapers & Shapers +Inside Monastery (Monastery): +158213 - 0x09D9B (Overhead Door Control) - True - Dots +158214 - 0x193A7 (Branch Avoid 1) - 0x00037 - Environment +158215 - 0x193AA (Branch Avoid 2) - 0x193A7 - Environment +158216 - 0x193AB (Branch Follow 1) - 0x193AA - Environment +158217 - 0x193A6 (Branch Follow 2) - 0x193AB - Environment -Swamp Maze (Swamp) - Swamp Rotating Bridge Near Side - 0x00001 & 0x014D2 & 0x014D4 & 0x014D1 - Outside Swamp - 0x17C05 & 0x17C02: -0x17C0A (Maze Control) - True - Shapers & Negative Shapers & Rotated Shapers & Environment -0x17E07 (Maze Control Other Side) - True - Shapers & Negative Shapers & Rotated Shapers & Environment -0x03615 (Laser) - 0x17C0A & 0x17E07 - True -0x17C05 (Near Laser Shortcut Door Left) - True - Rotated Shapers -0x17C02 (Near Laser Shortcut Door Right) - 0x17C05 - Shapers & Negative Shapers & Rotated Shapers +Monastery Garden (Monastery): -Treehouse Entry Area (Treehouse): -0x17C95 (Boat Spawn) - True - Boat -0x0288C (First Door) - True - Stars -0x02886 (Second Door) - 0x0288C - Stars -0x17D72 (Yellow Bridge 1) - 0x02886 - Stars -0x17D8F (Yellow Bridge 2) - 0x17D72 - Stars -0x17D74 (Yellow Bridge 3) - 0x17D8F - Stars -0x17DAC (Yellow Bridge 4) - 0x17D74 - Stars -0x17D9E (Yellow Bridge 5) - 0x17DAC - Stars -0x17DB9 (Yellow Bridge 6) - 0x17D9E - Stars -0x17D9C (Yellow Bridge 7) - 0x17DB9 - Stars -0x17DC2 (Yellow Bridge 8) - 0x17D9C - Stars -0x17DC4 (Yellow Bridge 9) - 0x17DC2 - Stars -0x0A182 (Beyond Yellow Bridge Door) - 0x17DC4 - Stars +Town (Town) - Main Island - True - Boat - 0x0A054 - Town Maze Rooftop - 0x28AA2 - Town Church - True - Town Wooden Rooftop - 0x034F5 - RGB House - 0x28A61 - Windmill Interior - 0x1845B - Town Inside Cargo Box - 0x0A0C9: +158218 - 0x0A054 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat +158219 - 0x0A0C8 (Cargo Box Panel) - True - Squares & Black/White Squares & Shapers +Door - 0x0A0C9 (Cargo Box Door) - 0x0A0C8 +158707 - 0x09F98 (Desert Laser Redirect) - True - True +158220 - 0x18590 (Tree Outlines) - True - Symmetry & Environment +158221 - 0x28AE3 (Vines Shadows Follow) - 0x18590 - Shadows Follow & Environment +158222 - 0x28938 (Four-way Apple Tree) - 0x28AE3 - Environment +158223 - 0x079DF (Triple Environmental Puzzle) - 0x28938 - Shadows Avoid & Environment & Reflection +158235 - 0x2899C (Full Dot Grid Shapers 1) - True - Rotated Shapers & Dots +158236 - 0x28A33 (Full Dot Grid Shapers 2) - 0x2899C - Shapers & Dots +158237 - 0x28ABF (Full Dot Grid Shapers 3) - 0x28A33 - Shapers & Rotated Shapers & Dots +158238 - 0x28AC0 (Full Dot Grid Shapers 4) - 0x28ABF - Rotated Shapers & Dots +158239 - 0x28AC1 (Full Dot Grid Shapers 5) - 0x28AC0 - Rotated Shapers & Dots +Door - 0x034F5 (Wooden Roof Staircase) - 0x28AC1 +158225 - 0x28998 (Tinted Door Panel) - True - Stars & Rotated Shapers +Door - 0x28A61 (Tinted Door to RGB House) - 0x28998 +158226 - 0x28A0D (Door to Church Stars Panel) - 0x28998 - Stars & RGB & Environment +Door - 0x03BB0 (Door to Church) - 0x28A0D +158228 - 0x28A79 (Maze Stair Control) - True - Environment +Door - 0x28AA2 (Maze Staircase) - 0x28A79 +158241 - 0x17F5F (Windmill Door Panel) - True - Dots +Door - 0x1845B (Windmill Door) - 0x17F5F -Treehouse Beyond Yellow Bridge (Treehouse) - Treehouse Entry Area - 0x0A182: -0x2700B (Laser House Door Timer Outside Control) - True - True -0x17DC8 (First Purple Bridge 1) - True - Stars & Dots -0x17DC7 (First Purple Bridge 2) - 0x17DC8 - Stars & Dots -0x17CE4 (First Purple Bridge 3) - 0x17DC7 - Stars & Dots -0x17D2D (First Purple Bridge 4) - 0x17CE4 - Stars & Dots -0x17D6C (First Purple Bridge 5) - 0x17D2D - Stars & Dots -0x17D9B (Second Purple Bridge 1) - 0x17D6C - Stars & Squares & Black/White Squares -0x17D99 (Second Purple Bridge 2) - 0x17D9B - Stars & Squares & Black/White Squares -0x17DAA (Second Purple Bridge 3) - 0x17D99 - Stars & Squares & Black/White Squares -0x17D97 (Second Purple Bridge 4) - 0x17DAA - Stars & Squares & Black/White Squares & Colored Squares -0x17BDF (Second Purple Bridge 5) - 0x17D97 - Stars & Squares & Colored Squares -0x17D91 (Second Purple Bridge 6) - 0x17BDF - Stars & Squares & Colored Squares -0x17DC6 (Second Purple Bridge 7) - 0x17D91 - Stars & Squares & Colored Squares -0x17E3C (Green Bridge 1) - True - Stars & Shapers -0x17E4D (Green Bridge 2) - 0x17E3C - Stars & Shapers -0x17E4F (Green Bridge 3) - 0x17E4D - Stars & Shapers & Rotated Shapers -0x17E52 (Green Bridge 4 & Directional) - 0x17E4F - Stars & Rotated Shapers & Environment -0x17E5B (Green Bridge 5) - 0x17E52 - Stars & Shapers & Colored Shapers & Stars + Same Colored Symbol -0x17E5F (Green Bridge 6) - 0x17E5B - Stars & Shapers & Colored Shapers & Negative Shapers & Colored Negative Shapers & Stars + Same Colored Symbol -0x17E61 (Green Bridge 7) - 0x17E5F - Stars & Shapers & Rotated Shapers -0x17FA9 (Green Bridge Discard) - 0x17E61 - Triangles -0x17DB3 (Left Orange Bridge 1) - 0x17DC6 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -0x17DB5 (Left Orange Bridge 2) - 0x17DB3 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -0x17DB6 (Left Orange Bridge 3) - 0x17DB5 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -0x17DC0 (Left Orange Bridge 4) - 0x17DB6 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -0x17DD7 (Left Orange Bridge 5) - 0x17DC0 - Stars & Squares & Black/White Squares & Colored Squares & Stars + Same Colored Symbol -0x17DD9 (Left Orange Bridge 6) - 0x17DD7 - Stars & Squares & Black/White Squares & Colored Squares & Stars + Same Colored Symbol -0x17DB8 (Left Orange Bridge 7) - 0x17DD9 - Stars & Squares & Black/White Squares & Colored Squares & Stars + Same Colored Symbol -0x17DDC (Left Orange Bridge 8) - 0x17DB8 - Stars & Squares & Colored Squares & Stars + Same Colored Symbol -0x17DD1 (Left Orange Bridge 9 & Directional) - 0x17DDC - Stars & Squares & Colored Squares & Stars + Same Colored Symbol & Environment -0x17DDE (Left Orange Bridge 10) - 0x17DD1 - Stars & Squares & Colored Squares & Stars + Same Colored Symbol -0x17DE3 (Left Orange Bridge 11) - 0x17DDE - Stars & Squares & Colored Squares & Stars + Same Colored Symbol -0x17DEC (Left Orange Bridge 12) - 0x17DE3 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -0x17DAE (Left Orange Bridge 13) - 0x17DEC - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -0x17DB0 (Left Orange Bridge 14) - 0x17DAE - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -0x17DDB (Left Orange Bridge 15) - 0x17DB0 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -0x17FA0 (Burned House Discard) - 0x17DDB - Triangles -0x17D88 (Right Orange Bridge 1) - True - Stars -0x17DB4 (Right Orange Bridge 2) - 0x17D88 - Stars -0x17D8C (Right Orange Bridge 3) - 0x17DB4 - Stars -0x17CE3 (Right Orange Bridge 4 & Directional) - 0x17D8C - Stars & Environment -0x17DCD (Right Orange Bridge 5) - 0x17CE3 - Stars -0x17DB2 (Right Orange Bridge 6) - 0x17DCD - Stars -0x17DCC (Right Orange Bridge 7) - 0x17DB2 - Stars -0x17DCA (Right Orange Bridge 8) - 0x17DCC - Stars -0x17D8E (Right Orange Bridge 9) - 0x17DCA - Stars -0x17DB7 (Right Orange Bridge 10 & Directional) - 0x17D8E - Stars -0x17DB1 (Right Orange Bridge 11) - 0x17DB7 - Stars -0x17DA2 (Right Orange Bridge 12) - 0x17DB1 - Stars +Town Inside Cargo Box (Town): +158606 - 0x17D01 (Cargo Box Discard) - True - Triangles -Treehouse Laser Room (Treehouse) - Treehouse Beyond Yellow Bridge - 0x2700B & 0x17DA2 & 0x17DDB: -0x03613 (Laser) - True - True -0x17CBC (Laser House Door Timer Inside Control) - True - True +Town Maze Rooftop (Town) - Town Red Rooftop - 0x2896A: +158229 - 0x2896A (Maze Rooftop Bridge Control) - True - Shapers -Treehouse Bridge Platform (Treehouse) - Treehouse Beyond Yellow Bridge - 0x17DA2 - Main Island - 0x037FF: -0x037FF (Bridge Control) - True - Stars +Town Red Rooftop (Town): +158607 - 0x17C71 (Rooftop Discard) - True - Triangles +158230 - 0x28AC7 (Symmetry Squares 1) - True - Symmetry & Squares & Black/White Squares +158231 - 0x28AC8 (Symmetry Squares 2) - 0x28AC7 - Symmetry & Squares & Black/White Squares +158232 - 0x28ACA (Symmetry Squares 3 + Dots) - 0x28AC8 - Symmetry & Squares & Black/White Squares & Dots +158233 - 0x28ACB (Symmetry Squares 4 + Dots) - 0x28ACA - Symmetry & Squares & Black/White Squares & Dots +158234 - 0x28ACC (Symmetry Squares 5 + Dots) - 0x28ACB - Symmetry & Squares & Black/White Squares & Dots +158224 - 0x28B39 (Hexagonal Reflection) - 0x079DF - Reflection -Mountaintop (Mountaintop) - Main Island - True: -0x0042D (River Shape) - True - True -0x09F7F (Box Short) - 7 Lasers - True -0xFFF00 (Box Long) - 11 Lasers & 0x17C34 - True -0x17C34 (Trap Door Triple Exit) - 0x09F7F - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -0x17C42 (Discard) - True - Triangles -0x002A6 (Vault) - True - Symmetry & Colored Dots & Squares & Black/White Squares & Dots -0x03542 (Vault Box) - 0x002A6 - True +Town Wooden Rooftop (Town): +158240 - 0x28AD9 (Shapers & Dots & Eraser) - 0x28AC1 - Rotated Shapers & Dots & Eraser -Inside Mountain Top Layer (Inside Mountain) - Mountaintop - 0x17C34: -0x09E39 (Light Bridge Controller) - True - Squares & Black/White Squares & Colored Squares & Eraser & Colored Eraser +Town Church (Town): +158227 - 0x28A69 (Church Lattice) - 0x03BB0 - Environment -Inside Mountain Top Layer Bridge (Inside Mountain) - Inside Mountain Top Layer - 0x09E39: -0x09E7A (Obscured Vision 1) - True - Obscured & Squares & Black/White Squares & Dots -0x09E71 (Obscured Vision 2) - 0x09E7A - Obscured & Squares & Black/White Squares & Dots -0x09E72 (Obscured Vision 3) - 0x09E71 - Obscured & Squares & Black/White Squares & Shapers & Dots -0x09E69 (Obscured Vision 4) - 0x09E72 - Obscured & Squares & Black/White Squares & Dots -0x09E7B (Obscured Vision 5) - 0x09E69 - Obscured & Squares & Black/White Squares & Dots -0x09E73 (Moving Background 1) - True - Moving & Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -0x09E75 (Moving Background 2) - 0x09E73 - Moving & Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -0x09E78 (Moving Background 3) - 0x09E75 - Moving & Shapers -0x09E79 (Moving Background 4) - 0x09E78 - Moving & Shapers & Rotated Shapers -0x09E6C (Moving Background 5) - 0x09E79 - Moving & Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -0x09E6F (Moving Background 6) - 0x09E6C - Moving & Stars & Rotated Shapers & Shapers -0x09E6B (Moving Background 7) - 0x09E6F - Moving & Stars & Dots -0x33AF5 (Physically Obstructed 1) - True - Squares & Black/White Squares & Environment & Symmetry -0x33AF7 (Physically Obstructed 2) - 0x33AF5 - Squares & Black/White Squares & Stars & Environment -0x09F6E (Physically Obstructed 3) - 0x33AF7 - Symmetry & Dots & Environment -0x09EAD (Angled Inside Trash 1) - True - Squares & Black/White Squares & Shapers & Angled -0x09EAF (Angled Inside Trash 2) - 0x09EAD - Squares & Black/White Squares & Shapers & Angled +RGB House (Town) - RGB Room - 0x2897B: +158242 - 0x034E4 (Sound Room Left) - True - Sound & Sound Waves +158243 - 0x034E3 (Sound Room Right) - True - Sound & Sound Dots +Door - 0x2897B (RGB House Staircase) - 0x034E4 & 0x034E3 -Inside Mountain Second Layer (Inside Mountain) - Inside Mountain Top Layer Bridge - 0x09EAF & 0x09F6E & 0x09E6B & 0x09E7B: -0x09FD3 (Color Cycle 1) - True - Color Cycle & RGB & Stars & Squares & Colored Squares & Stars + Same Colored Symbol -0x09FD4 (Color Cycle 2) - 0x09FD3 - Color Cycle & RGB & Stars & Squares & Colored Squares & Stars + Same Colored Symbol -0x09FD6 (Color Cycle 3) - 0x09FD4 - Color Cycle & RGB & Stars & Squares & Colored Squares & Stars + Same Colored Symbol -0x09FD7 (Color Cycle 4) - 0x09FD6 - Color Cycle & RGB & Stars & Squares & Colored Squares & Stars + Same Colored Symbol & Shapers & Colored Shapers -0x09FD8 (Color Cycle 5) - 0x09FD7 - Color Cycle & RGB & Squares & Colored Squares & Symmetry & Colored Dots -0x09E86 (Light Bridge Controller 2) - 0x09FD8 - Stars & Stars + Same Colored Symbol & Colored Rotated Shapers & Rotated Shapers & Eraser & Two Lines +RGB Room (Town): +158244 - 0x334D8 (RGB Control) - True - Rotated Shapers & RGB & Squares & Colored Squares +158245 - 0x03C0C (RGB Squares) - 0x334D8 - RGB & Squares & Colored Squares & Black/White Squares +158246 - 0x03C08 (RGB Stars) - 0x334D8 - RGB & Stars -Inside Mountain Second Layer Beyond Bridge (Inside Mountain) - Inside Mountain Second Layer - 0x09E86: -0x09FCC (Same Solution 1) - True - Dots & Same Solution -0x09FCE (Same Solution 2) - 0x09FCC - Squares & Black/White Squares & Same Solution -0x09FCF (Same Solution 3) - 0x09FCE - Stars & Same Solution -0x09FD0 (Same Solution 4) - 0x09FCF - Rotated Shapers & Same Solution -0x09FD1 (Same Solution 5) - 0x09FD0 - Stars & Squares & Colored Squares & Stars + Same Colored Symbol & Same Solution -0x09FD2 (Same Solution 6) - 0x09FD1 - Shapers & Same Solution -0x09ED8 (Light Bridge Controller 3) - 0x09FD2 - Stars & Stars + Same Colored Symbol & Colored Rotated Shapers & Rotated Shapers & Eraser & Two Lines +Town Tower (Town Tower) - Town - True - Town Tower Top - 0x27798 & 0x27799 & 0x2779A & 0x2779C: +Door - 0x27798 (Blue Panels Door) - 0x28ACC +Door - 0x27799 (Church Lattice Door) - 0x28A69 +Door - 0x2779A (Environmental Set Door) - 0x28B39 +Door - 0x2779C (Eraser Set Door) - 0x28AD9 -Inside Mountain Second Layer Elevator (Inside Mountain) - Inside Mountain Second Layer - 0x09ED8 & 0x09E86: -0x09EEB (Elevator Control Panel) - True - Dots -0x17F93 (Elevator Discard) - True - Triangles +Town Tower Top (Town): +158708 - 0x032F5 (Laser Panel) - True - True +Laser - 0x032F9 (Laser) - 0x032F5 -Inside Mountain Third Layer (Inside Mountain) - Inside Mountain Second Layer Elevator - 0x09EEB: -0x09FC1 (Giant Puzzle Bottom Left) - True - Shapers & Eraser -0x09F8E (Giant Puzzle Bottom Right) - True - Shapers & Eraser -0x09F01 (Giant Puzzle Top Right) - True - Rotated Shapers -0x09EFF (Giant Puzzle Top Left) - True - Shapers & Eraser -0x09FDA (Giant Puzzle) - 0x09FC1 & 0x09F8E & 0x09F01 & 0x09EFF - Shapers & Symmetry +Windmill Interior (Windmill) - Theater - 0x17F88: +158247 - 0x17D02 (Turn Control) - True - Dots +158248 - 0x17F89 (Door to Front of Theater Panel) - True - Squares & Black/White Squares +Door - 0x17F88 (Door to Front of Theater) - 0x17F89 -Inside Mountain Bottom Layer (Inside Mountain) - Inside Mountain Third Layer - 0x09FDA - Inside Mountain Path to Secret Area - 0x334E1: -0x17FA2 (Bottom Layer Discard) - 0xFFF00 - Triangles & Environment -0x01983 (Door to Final Room Left) - True - Shapers & Stars -0x01987 (Door to Final Room Right) - True - Squares & Colored Squares & Dots +Theater (Theater) - Town - 0x0A16D | 0x3CCDF: +158656 - 0x00815 (Video Input) - True - True +158657 - 0x03553 (Tutorial Video) - 0x00815 & 0x03481 - True +158658 - 0x03552 (Desert Video) - 0x00815 & 0x0339E - True +158659 - 0x0354E (Jungle Video) - 0x00815 & 0x03702 - True +158660 - 0x03549 (Challenge Video) - 0x00815 & 0x0356B - True +158661 - 0x0354F (Shipwreck Video) - 0x00815 & 0x03535 - True +158662 - 0x03545 (Mountain Video) - 0x00815 & 0x03542 - True +158249 - 0x0A168 (Door to Cargo Box Left Panel) - True - Squares & Black/White Squares & Eraser +158250 - 0x33AB2 (Door to Cargo Box Right Panel) - True - Squares & Black/White Squares & Shapers +Door - 0x0A16D (Door to Cargo Box Left) - 0x0A168 +Door - 0x3CCDF (Door to Cargo Box Right) - 0x33AB2 +158608 - 0x17CF7 (Discard) - True - Triangles -Inside Mountain Path to Secret Area (Inside Mountain) - Inside Mountain Bottom Layer - 0x17FA2: -0x00FF8 (Door to Secret Area) - True - Triangles & Black/White Squares & Squares -0x334E1 (Rock Control) - True - True +Jungle (Jungle) - Main Island - True - Outside Jungle River - 0x3873B - Boat - 0x17CDF: +158251 - 0x17CDF (Shore Boat Spawn) - True - Boat +158609 - 0x17F9B (Discard) - True - Triangles +158252 - 0x002C4 (Waves 1) - True - Sound & Sound Waves +158253 - 0x00767 (Waves 2) - 0x002C4 - Sound & Sound Waves +158254 - 0x002C6 (Waves 3) - 0x00767 - Sound & Sound Waves +158255 - 0x0070E (Waves 4) - 0x002C6 - Sound & Sound Waves +158256 - 0x0070F (Waves 5) - 0x0070E - Sound & Sound Waves +158257 - 0x0087D (Waves 6) - 0x0070F - Sound & Sound Waves +158258 - 0x002C7 (Waves 7) - 0x0087D - Sound & Sound Waves +158259 - 0x17CAB (Popup Wall Control) - 0x002C7 - True +Door - 0x1475B (Popup Wall) - 0x17CAB +158260 - 0x0026D (Popup Wall 1) - 0x1475B - Sound & Sound Dots +158261 - 0x0026E (Popup Wall 2) - 0x0026D - Sound & Sound Dots +158262 - 0x0026F (Popup Wall 3) - 0x0026E - Sound & Sound Dots +158263 - 0x00C3F (Popup Wall 4) - 0x0026F - Sound & Sound Dots +158264 - 0x00C41 (Popup Wall 5) - 0x00C3F - Sound & Sound Dots +158265 - 0x014B2 (Popup Wall 6) - 0x00C41 - Sound & Sound Dots +158709 - 0x03616 (Laser Panel) - 0x014B2 - True +Laser - 0x00274 (Laser) - 0x03616 +158266 - 0x337FA (Shortcut to River Panel) - True - True +Door - 0x3873B (Shortcut to River) - 0x337FA -Inside Mountain Secret Area (Inside Mountain Secret Area) - Inside Mountain Path to Secret Area - 0x00FF8 - Main Island - 0x021D7 - Main Island - 0x17CF2: -0x021D7 (Shortcut to Mountain) - True - Triangles & Stars & Stars + Same Colored Symbol & Colored Triangles -0x17CF2 (Shortcut to Swamp) - True - Triangles -0x335AB (Elevator Inside Control) - True - Dots & Squares & Black/White Squares -0x335AC (Elevator Upper Outside Control) - 0x335AB - Squares & Black/White Squares -0x3369D (Elevator Lower Outside Control) - 0x335AB - Squares & Black/White Squares & Dots -0x00190 (Dot Grid Triangles 1) - True - Dots & Triangles -0x00558 (Dot Grid Triangles 2) - 0x00190 - Dots & Triangles -0x00567 (Dot Grid Triangles 3) - 0x00558 - Dots & Triangles -0x006FE (Dot Grid Triangles 4) - 0x00567 - Dots & Triangles -0x01A0D (Symmetry Triangles) - True - Symmetry & Triangles -0x008B8 (Squares and Triangles) - True - Squares & Black/White Squares & Triangles -0x00973 (Stars and Triangles) - 0x008B8 - Stars & Triangles -0x0097B (Stars and Triangles of same color) - 0x00973 - Stars & Triangles & Stars and Triangles of same color & Stars + Same Colored Symbol -0x0097D (Stars & Squares and Triangles) - 0x0097B - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol & Triangles -0x0097E (Stars & Squares and Triangles 2) - 0x0097D - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol & Stars and Triangles of same color -0x00994 (Rotated Shapers and Triangles 1) - True - Rotated Shapers & Triangles -0x334D5 (Rotated Shapers and Triangles 2) - 0x00994 - Rotated Shapers & Triangles -0x00995 (Rotated Shapers and Triangles 3) - 0x334D5 - Rotated Shapers & Triangles -0x00996 (Shapers and Triangles 1) - 0x00995 - Shapers & Triangles -0x00998 (Shapers and Triangles 2) - 0x00996 - Shapers & Triangles -0x009A4 (Broken Shapers) - True - Shapers & Broken Shapers -0x018A0 (Symmetry Shapers) - True - Shapers & Symmetry -0x00A72 (Broken and Negative Shapers) - True - Shapers & Broken Shapers & Negative Shapers -0x32962 (Rotated Broken Shapers) - True - Rotated Shapers & Broken Rotated Shapers -0x32966 (Stars and Squares) - True - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -0x01A31 (Rainbow Squares) - True - Color Cycle & RGB & Squares & Colored Squares -0x00B71 (Squares & Stars and Colored Eraser) - True - Colored Eraser & Squares & Colored Squares & Stars & Stars + Same Colored Symbol & Eraser -0x09DD5 (Lone Pillar) - True - Pillar & Triangles -0x0A16E (Door to Challenge) - 0x09DD5 - Stars & Shapers & Colored Shapers & Stars + Same Colored Symbol -0x288EA (Wooden Beam Shapers) - True - Environment & Shapers -0x288FC (Wooden Beam Squares and Shapers) - True - Environment & Squares & Black/White Squares & Shapers & Rotated Shapers -0x289E7 (Wooden Beam Stars and Squares) - True - Environment & Stars & Squares & Black/White Squares -0x288AA (Wooden Beam Shapers and Stars) - True - Environment & Stars & Shapers -0x17FB9 (Upstairs Dot Grid Negative Shapers) - True - Shapers & Dots & Negative Shapers -0x0A16B (Upstairs Dot Grid Gap Dots) - True - Dots -0x0A2CE (Upstairs Dot Grid Stars) - 0x0A16B - Stars & Dots -0x0A2D7 (Upstairs Dot Grid Stars & Squares) - 0x0A2CE - Dots & Black/White Squares & Stars + Same Colored Symbol & Stars -0x0A2DD (Upstairs Dot Grid Shapers) - 0x0A2D7 - Shapers & Dots -0x0A2EA (Upstairs Dot Grid Rotated Shapers) - 0x0A2DD - Rotated Shapers & Dots -0x0008F (Upstairs Invisible Dots 1) - True - Dots & Invisible Dots -0x0006B (Upstairs Invisible Dots 2) - 0x0008F - Dots & Invisible Dots -0x0008B (Upstairs Invisible Dots 3) - 0x0006B - Dots & Invisible Dots -0x0008C (Upstairs Invisible Dots 4) - 0x0008B - Dots & Invisible Dots -0x0008A (Upstairs Invisible Dots 5) - 0x0008C - Dots & Invisible Dots -0x00089 (Upstairs Invisible Dots 6) - 0x0008A - Dots & Invisible Dots -0x0006A (Upstairs Invisible Dots 7) - 0x00089 - Dots & Invisible Dots -0x0006C (Upstairs Invisible Dots 8) - 0x0006A - Dots & Invisible Dots -0x00027 (Upstairs Invisible Dot Symmetry 1) - True - Dots & Invisible Dots & Symmetry -0x00028 (Upstairs Invisible Dot Symmetry 2) - 0x00027 - Dots & Invisible Dots & Symmetry -0x00029 (Upstairs Invisible Dot Symmetry 3) - 0x00028 - Dots & Invisible Dots & Symmetry +Outside Jungle River (River) - Main Island - True - Monastery Garden - 0x0CF2A: +158267 - 0x17CAA (Rhombic Avoid to Monastery Garden) - True - Environment +Door - 0x0CF2A (Shortcut to Monastery Garden) - 0x17CAA +158663 - 0x15ADD (Vault) - True - Environment & Black/White Squares & Dots +158664 - 0x03702 (Vault Box) - 0x15ADD - True -Challenge (Challenge) - Inside Mountain Secret Area - 0x0A16E: -0x0A332 (Start Timer) - 11 Lasers - True -0x0088E (Small Basic) - 0x0A332 - True -0x00BAF (Big Basic) - 0x0088E - True -0x00BF3 (Square) - 0x00BAF - Squares & Black/White Squares -0x00C09 (Maze Map) - 0x00BF3 - Dots -0x00CDB (Stars and Dots) - 0x00C09 - Stars & Dots -0x0051F (Symmetry) - 0x00CDB - Symmetry & Colored Dots & Dots -0x00524 (Stars and Shapers) - 0x0051F - Stars & Shapers -0x00CD4 (Big Basic 2) - 0x00524 - True -0x00CB9 (Choice Squares Right) - 0x00CD4 - Squares & Black/White Squares -0x00CA1 (Choice Squares Middle) - 0x00CD4 - Squares & Black/White Squares -0x00C80 (Choice Squares Left) - 0x00CD4 - Squares & Black/White Squares -0x00C68 (Choice Squares 2 Right) - 0x00CB9 | 0x00CA1 | 0x00C80 - Squares & Black/White Squares & Colored Squares -0x00C59 (Choice Squares 2 Middle) - 0x00CB9 | 0x00CA1 | 0x00C80 - Squares & Black/White Squares & Colored Squares -0x00C22 (Choice Squares 2 Left) - 0x00CB9 | 0x00CA1 | 0x00C80 - Squares & Black/White Squares & Colored Squares -0x034F4 (Maze Hidden 1) - 0x00C68 | 0x00C59 | 0x00C22 - Triangles -0x034EC (Maze Hidden 2) - 0x00C68 | 0x00C59 | 0x00C22 - Triangles -0x1C31A (Dots Pillar) - 0x034F4 & 0x034EC - Dots & Symmetry & Pillar -0x1C319 (Squares Pillar) - 0x034F4 & 0x034EC - Squares & Black/White Squares & Symmetry & Pillar -0x0356B (Vault Box) - 0x1C31A & 0x1C319 - True -0x039B4 (Door to Theater Walkway) - True - Triangles +Outside Bunker (Bunker) - Main Island - True - Inside Bunker - 0x0C2A4: +158268 - 0x17C2E (Bunker Entry Panel) - True - Squares & Black/White Squares & Colored Squares +Door - 0x0C2A4 (Bunker Entry Door) - 0x17C2E -Theater Walkway (Theater Walkway) - Challenge - 0x039B4 - Theater - 0x27732 - Desert Elevator Room - 0x2773D & 0x03608 - Town - 0x09E85: -0x2FAF6 (Vault Box) - True - True -0x27732 (Door to Back of Theater) - True - True -0x2773D (Door to Desert Elevator Room) - True - True -0x09E85 (Door to Town) - True - Triangles +Inside Bunker (Bunker) - Inside Bunker Glass Room - 0x17C79: +158269 - 0x09F7D (Drawn Squares 1) - True - Squares & Colored Squares +158270 - 0x09FDC (Drawn Squares 2) - 0x09F7D - Squares & Colored Squares & Black/White Squares +158271 - 0x09FF7 (Drawn Squares 3) - 0x09FDC - Squares & Colored Squares & Black/White Squares +158272 - 0x09F82 (Drawn Squares 4) - 0x09FF7 - Squares & Colored Squares & Black/White Squares +158273 - 0x09FF8 (Drawn Squares 5) - 0x09F82 - Squares & Colored Squares & Black/White Squares +158274 - 0x09D9F (Drawn Squares 6) - 0x09FF8 - Squares & Colored Squares & Black/White Squares +158275 - 0x09DA1 (Drawn Squares 7) - 0x09D9F - Squares & Colored Squares +158276 - 0x09DA2 (Drawn Squares 8) - 0x09DA1 - Squares & Colored Squares +158277 - 0x09DAF (Drawn Squares 9) - 0x09DA2 - Squares & Colored Squares +158278 - 0x0A099 (Door to Bunker Proper Panel) - 0x09DAF - True +Door - 0x17C79 (Door to Bunker Proper) - 0x0A099 -Final Room (Inside Mountain Final Room) - Inside Mountain Bottom Layer - 0x01983 & 0x01987: -0x0383A (Stars Pillar) - True - Stars & Pillar -0x09E56 (Stars and Dots Pillar) - 0x0383A - Stars & Dots & Pillar -0x09E5A (Dot Grid Pillar) - 0x09E56 - Dots & Pillar -0x33961 (Sparse Dots Pillar) - 0x09E5A - Dots & Symmetry & Pillar -0x0383D (Dot Maze Pillar) - True - Dots & Pillar -0x0383F (Squares Pillar) - 0x0383D - Squares & Black/White Squares & Pillar -0x03859 (Shapers Pillar) - 0x0383F - Shapers & Pillar -0x339BB (Squares and Stars) - 0x03859 - Squares & Black/White Squares & Stars & Symmetry & Pillar +Inside Bunker Glass Room (Bunker) - Inside Bunker Ultraviolet Room - 0x0C2A3: +158279 - 0x0A010 (Drawn Squares through Tinted Glass 1) - True - Squares & Colored Squares & RGB & Environment +158280 - 0x0A01B (Drawn Squares through Tinted Glass 2) - 0x0A010 - Squares & Colored Squares & Black/White Squares & RGB & Environment +158281 - 0x0A01F (Drawn Squares through Tinted Glass 3) - 0x0A01B - Squares & Colored Squares & Black/White Squares & RGB & Environment +Door - 0x0C2A3 (Door to Ultraviolet Room) - 0x0A01F -Elevator (Inside Mountain Final Room) - Final Room - 0x339BB & 0x33961: -0x3D9A6 (Elevator Door Closer Left) - True - True -0x3D9A7 (Elevator Door Close Right) - True - True -0x3C113 (Elevator Door Open Left) - 0x3D9A6 | 0x3D9A7 - True -0x3C114 (Elevator Door Open Right) - 0x3D9A6 | 0x3D9A7 - True -0x3D9AA (Back Wall Left) - 0x3D9A6 | 0x3D9A7 - True -0x3D9A8 (Back Wall Right) - 0x3D9A6 | 0x3D9A7 - True -0x3D9A9 (Elevator Start) - 0x3D9AA | 0x3D9A8 - True +Inside Bunker Ultraviolet Room (Bunker) - Inside Bunker Elevator Section - 0x0A08D: +158282 - 0x34BC5 (Drop-Down Door Open) - True - True +158283 - 0x34BC6 (Drop-Down Door Close) - 0x34BC5 - True +158284 - 0x17E63 (Drop-Down Door Squares 1) - 0x34BC5 - Squares & Colored Squares & RGB & Environment +158285 - 0x17E67 (Drop-Down Door Squares 2) - 0x17E63 & 0x34BC6 - Squares & Colored Squares & Black/White Squares & RGB +Door - 0x0A08D (Door to Elevator) - 0x17E67 -Boat (Boat) - Main Island - 0x17CDF | 0x17CC8 & 0x0005C | 0x17CA6 | 0x09DB8 | 0x17C95 | 0x0A054 - Inside Glass Factory - 0x17CDF & 0x0005C | 0x17CC8 & 0x0005C | 0x17CA6 & 0x0005C | 0x09DB8 & 0x0005C | 0x17C95 & 0x0005C | 0x0A054 & 0x0005C - Quarry Boathouse - 0x17CA6 - Swamp Near Boat - 0x17CDF | 0x17CC8 & 0x0005C | 0x17CA6 | 0x09DB8 | 0x17C95 | 0x0A054 - Treehouse Entry Area - 0x17CDF | 0x17CC8 & 0x0005C | 0x17CA6 | 0x09DB8 | 0x17C95 | 0x0A054: +Inside Bunker Elevator Section (Bunker) - Bunker Laser Platform - 0x0A079: +158286 - 0x0A079 (Elevator Control) - True - Squares & Colored Squares & Black/White Squares & RGB + +Bunker Laser Platform (Bunker): +158710 - 0x09DE0 (Laser Panel) - True - True +Laser - 0x0C2B2 (Laser) - 0x09DE0 + +Outside Swamp (Swamp) - Swamp Entry Area - 0x00C1C - Main Island - True: +158287 - 0x0056E (Entry Panel) - True - Shapers +Door - 0x00C1C (Entry Door) - 0x0056E + +Swamp Entry Area (Swamp) - Swamp Sliding Bridge - TrueOneWay: +158288 - 0x00469 (Seperatable Shapers 1) - True - Shapers +158289 - 0x00472 (Seperatable Shapers 2) - 0x00469 - Shapers +158290 - 0x00262 (Seperatable Shapers 3) - 0x00472 - Shapers +158291 - 0x00474 (Seperatable Shapers 4) - 0x00262 - Shapers +158292 - 0x00553 (Seperatable Shapers 5) - 0x00474 - Shapers +158293 - 0x0056F (Seperatable Shapers 6) - 0x00553 - Shapers +158294 - 0x00390 (Combinable Shapers 1) - 0x0056F - Shapers +158295 - 0x010CA (Combinable Shapers 2) - 0x00390 - Shapers +158296 - 0x00983 (Combinable Shapers 3) - 0x010CA - Shapers +158297 - 0x00984 (Combinable Shapers 4) - 0x00983 - Shapers +158298 - 0x00986 (Combinable Shapers 5) - 0x00984 - Shapers +158299 - 0x00985 (Combinable Shapers 6) - 0x00986 - Shapers +158300 - 0x00987 (Combinable Shapers 7) - 0x00985 - Shapers +158301 - 0x181A9 (Combinable Shapers 8) - 0x00987 - Shapers + +Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Near Platform - 0x00609: +158302 - 0x00609 (Sliding Bridge) - True - Shapers + +Swamp Near Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat - 0x38AE6 - Swamp Broken Shapers - 0x184B7 - Swamp Sliding Bridge - TrueOneWay: +158313 - 0x00982 (Platform Shapers 1) - True - Shapers +158314 - 0x0097F (Platform Shapers 2) - 0x00982 - Shapers +158315 - 0x0098F (Platform Shapers 3) - 0x0097F - Shapers +158316 - 0x00990 (Platform Shapers 4) - 0x0098F - Shapers +Door - 0x184B7 (Door to Broken Shapers) - 0x00990 +158317 - 0x17C0D (Platform Shortcut Left Panel) - True - Shapers +158318 - 0x17C0E (Platform Shortcut Right Panel) - True - Shapers +Door - 0x38AE6 (Platform Shortcut Door) - 0x17C0E +Door - 0x04B7F (Cyan Water Pump) - 0x00006 + +Swamp Cyan Underwater (Swamp): +158307 - 0x00002 (Cyan Underwater Negative Shapers 1) - True - Shapers & Negative Shapers +158308 - 0x00004 (Cyan Underwater Negative Shapers 2) - 0x00002 - Shapers & Negative Shapers +158309 - 0x00005 (Cyan Underwater Negative Shapers 3) - 0x00004 - Shapers & Negative Shapers +158310 - 0x013E6 (Cyan Underwater Negative Shapers 4) - 0x00005 - Shapers & Negative Shapers +158311 - 0x00596 (Cyan Underwater Negative Shapers 5) - 0x013E6 - Shapers & Negative Shapers +158312 - 0x18488 (Cyan Underwater Sliding Bridge Control) - True - Shapers + +Swamp Broken Shapers (Swamp) - Swamp Rotated Shapers - 0x18507: +158303 - 0x00999 (Broken Shapers 1) - 0x00990 - Shapers & Broken Shapers +158304 - 0x0099D (Broken Shapers 2) - 0x00999 - Shapers & Broken Shapers +158305 - 0x009A0 (Broken Shapers 3) - 0x0099D - Shapers & Broken Shapers +158306 - 0x009A1 (Broken Shapers 4) - 0x009A0 - Shapers & Broken Shapers +Door - 0x18507 (Door to Rotated Shapers) - 0x009A1 + +Swamp Rotated Shapers (Swamp) - Swamp Red Underwater - 0x183F2 - Swamp Rotating Bridge - TrueOneWay: +158319 - 0x00007 (Rotated Shapers 1) - 0x009A1 - Rotated Shapers +158320 - 0x00008 (Rotated Shapers 2) - 0x00007 - Rotated Shapers & Shapers +158321 - 0x00009 (Rotated Shapers 3) - 0x00008 - Rotated Shapers +158322 - 0x0000A (Rotated Shapers 4) - 0x00009 - Rotated Shapers +Door - 0x183F2 (Red Water Pump) - 0x00596 + +Swamp Red Underwater (Swamp) - Swamp Maze - 0x014D1: +158323 - 0x00001 (Red Underwater Negative Shapers 1) - True - Shapers & Negative Shapers +158324 - 0x014D2 (Red Underwater Negative Shapers 2) - True - Shapers & Negative Shapers +158325 - 0x014D4 (Red Underwater Negative Shapers 3) - True - Shapers & Negative Shapers +158326 - 0x014D1 (Red Underwater Negative Shapers 4) - True - Shapers & Negative Shapers +Door - 0x305D5 (Red Underwater Exit) - 0x014D1 + +Swamp Rotating Bridge (Swamp) - Swamp Rotated Shapers - 0x181F5 - Swamp Near Boat - 0x181F5 - Swamp Purple Area - 0x181F5: +158327 - 0x181F5 (Rotating Bridge) - True - Rotated Shapers & Shapers + +Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482: +158328 - 0x09DB8 (Boat Spawn) - True - Boat +158329 - 0x003B2 (More Rotated Shapers 1) - 0x0000A - Rotated Shapers +158330 - 0x00A1E (More Rotated Shapers 2) - 0x003B2 - Rotated Shapers +158331 - 0x00C2E (More Rotated Shapers 3) - 0x00A1E - Rotated Shapers +158332 - 0x00E3A (More Rotated Shapers 4) - 0x00C2E - Rotated Shapers +158339 - 0x17E2B (Long Bridge Control) - True - Rotated Shapers & Shapers +Door - 0x18482 (Blue Water Pump) - 0x00E3A + +Swamp Purple Area (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Purple Underwater - 0x0A1D6: +Door - 0x0A1D6 (Purple Water Pump) - 0x00E3A + +Swamp Purple Underwater (Swamp): +158333 - 0x009A6 (Underwater Back Optional) - True - Shapers + +Swamp Blue Underwater (Swamp): +158334 - 0x009AB (Blue Underwater Negative Shapers 1) - True - Shapers & Negative Shapers +158335 - 0x009AD (Blue Underwater Negative Shapers 2) - 0x009AB - Shapers & Negative Shapers +158336 - 0x009AE (Blue Underwater Negetive Shapers 3) - 0x009AD - Shapers & Negative Shapers +158337 - 0x009AF (Blue Underwater Negative Shapers 4) - 0x009AE - Shapers & Negative Shapers +158338 - 0x00006 (Blue Underwater Negative Shapers 5) - 0x009AF - Shapers & Negative Shapers & Broken Negative Shapers + +Swamp Maze (Swamp) - Swamp Laser Area - 0x17C0A & 0x17E07: +158340 - 0x17C0A (Maze Control) - True - Shapers & Negative Shapers & Rotated Shapers & Environment +158112 - 0x17E07 (Maze Control Other Side) - True - Shapers & Negative Shapers & Rotated Shapers & Environment + +Swamp Laser Area (Swamp) - Outside Swamp - 0x2D880: +158711 - 0x03615 (Laser Panel) - True - True +Laser - 0x00BF6 (Laser) - 0x03615 +158341 - 0x17C05 (Near Laser Shortcut Left Panel) - True - Rotated Shapers +158342 - 0x17C02 (Near Laser Shortcut Right Panel) - 0x17C05 - Shapers & Negative Shapers & Rotated Shapers +Door - 0x2D880 (Near Laser Shortcut) - 0x17C02 + +Treehouse Entry Area (Treehouse) - Treehouse Between Doors - 0x0C309: +158343 - 0x17C95 (Boat Spawn) - True - Boat +158344 - 0x0288C (First Door Panel) - True - Stars +Door - 0x0C309 (First Door) - 0x0288C + +Treehouse Between Doors (Treehouse) - Treehouse Yellow Bridge - 0x0C310: +158345 - 0x02886 (Second Door Panel) - True - Stars +Door - 0x0C310 (Second Door) - 0x02886 + +Treehouse Yellow Bridge (Treehouse) - Treehouse After Yellow Bridge - 0x17DC4: +158346 - 0x17D72 (Yellow Bridge 1) - True - Stars +158347 - 0x17D8F (Yellow Bridge 2) - 0x17D72 - Stars +158348 - 0x17D74 (Yellow Bridge 3) - 0x17D8F - Stars +158349 - 0x17DAC (Yellow Bridge 4) - 0x17D74 - Stars +158350 - 0x17D9E (Yellow Bridge 5) - 0x17DAC - Stars +158351 - 0x17DB9 (Yellow Bridge 6) - 0x17D9E - Stars +158352 - 0x17D9C (Yellow Bridge 7) - 0x17DB9 - Stars +158353 - 0x17DC2 (Yellow Bridge 8) - 0x17D9C - Stars +158354 - 0x17DC4 (Yellow Bridge 9) - 0x17DC2 - Stars + +Treehouse After Yellow Bridge (Treehouse) - Treehouse Junction - 0x0A181: +158355 - 0x0A182 (Beyond Yellow Bridge Door Panel) - True - Stars +Door - 0x0A181 (Beyond Yellow Bridge Door) - 0x0A182 + +Treehouse Junction (Treehouse) - Treehouse Right Orange Bridge - True - Treehouse First Purple Bridge - True - Treehouse Green Bridge - True: +158356 - 0x2700B (Laser House Door Timer Outside Control) - True - True + +Treehouse First Purple Bridge (Treehouse) - Treehouse Second Purple Bridge - 0x17D6C: +158357 - 0x17DC8 (First Purple Bridge 1) - True - Stars & Dots +158358 - 0x17DC7 (First Purple Bridge 2) - 0x17DC8 - Stars & Dots +158359 - 0x17CE4 (First Purple Bridge 3) - 0x17DC7 - Stars & Dots +158360 - 0x17D2D (First Purple Bridge 4) - 0x17CE4 - Stars & Dots +158361 - 0x17D6C (First Purple Bridge 5) - 0x17D2D - Stars & Dots + +Treehouse Right Orange Bridge (Treehouse) - Treehouse Bridge Platform - 0x17DA2: +158391 - 0x17D88 (Right Orange Bridge 1) - True - Stars +158392 - 0x17DB4 (Right Orange Bridge 2) - 0x17D88 - Stars +158393 - 0x17D8C (Right Orange Bridge 3) - 0x17DB4 - Stars +158394 - 0x17CE3 (Right Orange Bridge 4 & Directional) - 0x17D8C - Stars & Environment +158395 - 0x17DCD (Right Orange Bridge 5) - 0x17CE3 - Stars +158396 - 0x17DB2 (Right Orange Bridge 6) - 0x17DCD - Stars +158397 - 0x17DCC (Right Orange Bridge 7) - 0x17DB2 - Stars +158398 - 0x17DCA (Right Orange Bridge 8) - 0x17DCC - Stars +158399 - 0x17D8E (Right Orange Bridge 9) - 0x17DCA - Stars +158400 - 0x17DB7 (Right Orange Bridge 10 & Directional) - 0x17D8E - Stars +158401 - 0x17DB1 (Right Orange Bridge 11) - 0x17DB7 - Stars +158402 - 0x17DA2 (Right Orange Bridge 12) - 0x17DB1 - Stars + +Treehouse Bridge Platform (Treehouse) - Main Island - 0x0C32D: +158404 - 0x037FF (Bridge Control) - True - Stars +Door - 0x0C32D (Drawbridge) - 0x037FF + +Treehouse Second Purple Bridge (Treehouse) - Treehouse Left Orange Bridge - 0x17DC6: +158362 - 0x17D9B (Second Purple Bridge 1) - True - Stars & Squares & Black/White Squares +158363 - 0x17D99 (Second Purple Bridge 2) - 0x17D9B - Stars & Squares & Black/White Squares +158364 - 0x17DAA (Second Purple Bridge 3) - 0x17D99 - Stars & Squares & Black/White Squares +158365 - 0x17D97 (Second Purple Bridge 4) - 0x17DAA - Stars & Squares & Black/White Squares & Colored Squares +158366 - 0x17BDF (Second Purple Bridge 5) - 0x17D97 - Stars & Squares & Colored Squares +158367 - 0x17D91 (Second Purple Bridge 6) - 0x17BDF - Stars & Squares & Colored Squares +158368 - 0x17DC6 (Second Purple Bridge 7) - 0x17D91 - Stars & Squares & Colored Squares + +Treehouse Left Orange Bridge (Treehouse) - Treehouse Laser Room Front Platform - 0x17DDB - Treehouse Laser Room Back Platform - 0x17DDB: +158376 - 0x17DB3 (Left Orange Bridge 1) - True - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol +158377 - 0x17DB5 (Left Orange Bridge 2) - 0x17DB3 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol +158378 - 0x17DB6 (Left Orange Bridge 3) - 0x17DB5 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol +158379 - 0x17DC0 (Left Orange Bridge 4) - 0x17DB6 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol +158380 - 0x17DD7 (Left Orange Bridge 5) - 0x17DC0 - Stars & Squares & Black/White Squares & Colored Squares & Stars + Same Colored Symbol +158381 - 0x17DD9 (Left Orange Bridge 6) - 0x17DD7 - Stars & Squares & Black/White Squares & Colored Squares & Stars + Same Colored Symbol +158382 - 0x17DB8 (Left Orange Bridge 7) - 0x17DD9 - Stars & Squares & Black/White Squares & Colored Squares & Stars + Same Colored Symbol +158383 - 0x17DDC (Left Orange Bridge 8) - 0x17DB8 - Stars & Squares & Colored Squares & Stars + Same Colored Symbol +158384 - 0x17DD1 (Left Orange Bridge 9 & Directional) - 0x17DDC - Stars & Squares & Colored Squares & Stars + Same Colored Symbol & Environment +158385 - 0x17DDE (Left Orange Bridge 10) - 0x17DD1 - Stars & Squares & Colored Squares & Stars + Same Colored Symbol +158386 - 0x17DE3 (Left Orange Bridge 11) - 0x17DDE - Stars & Squares & Colored Squares & Stars + Same Colored Symbol +158387 - 0x17DEC (Left Orange Bridge 12) - 0x17DE3 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol +158388 - 0x17DAE (Left Orange Bridge 13) - 0x17DEC - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol +158389 - 0x17DB0 (Left Orange Bridge 14) - 0x17DAE - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol +158390 - 0x17DDB (Left Orange Bridge 15) - 0x17DB0 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol + +Treehouse Green Bridge (Treehouse): +158369 - 0x17E3C (Green Bridge 1) - True - Stars & Shapers +158370 - 0x17E4D (Green Bridge 2) - 0x17E3C - Stars & Shapers +158371 - 0x17E4F (Green Bridge 3) - 0x17E4D - Stars & Shapers & Rotated Shapers +158372 - 0x17E52 (Green Bridge 4 & Directional) - 0x17E4F - Stars & Rotated Shapers & Environment +158373 - 0x17E5B (Green Bridge 5) - 0x17E52 - Stars & Shapers & Colored Shapers & Stars + Same Colored Symbol +158374 - 0x17E5F (Green Bridge 6) - 0x17E5B - Stars & Shapers & Colored Shapers & Negative Shapers & Colored Negative Shapers & Stars + Same Colored Symbol +158375 - 0x17E61 (Green Bridge 7) - 0x17E5F - Stars & Shapers & Rotated Shapers +158610 - 0x17FA9 (Green Bridge Discard) - 0x17E61 - Triangles + +Treehouse Laser Room Front Platform (Treehouse) - Treehouse Laser Room - 0x0C323: +Door - 0x0C323 (Door to Laser House) - 0x17DA2 & 0x2700B & 0x17DDB + +Treehouse Laser Room Back Platform (Treehouse): +158611 - 0x17FA0 (Burnt House Discard) - True - Triangles + +Treehouse Laser Room (Treehouse): +158712 - 0x03613 (Laser Panel) - True - True +158403 - 0x17CBC (Laser House Door Timer Inside Control) - True - True +Laser - 0x028A4 (Laser) - 0x03613 + +Mountaintop (Mountaintop) - Main Island - True - Inside Mountain Top Layer - 0x17C34: +158405 - 0x0042D (River Shape) - True - True +158406 - 0x09F7F (Box Short) - 7 Lasers - True +158407 - 0x17C34 (Trap Door Triple Exit) - 0x09F7F - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol +158612 - 0x17C42 (Discard) - True - Triangles +158665 - 0x002A6 (Vault) - True - Symmetry & Colored Dots & Squares & Black/White Squares & Dots +158666 - 0x03542 (Vault Box) - 0x002A6 - True +158800 - 0xFFF00 (Box Long) - 7 Lasers & 11 Lasers & 0x17C34 - True + +Inside Mountain Top Layer (Inside Mountain) - Inside Mountain Top Layer Bridge - 0x09E39: +158408 - 0x09E39 (Light Bridge Controller) - True - Squares & Black/White Squares & Colored Squares & Eraser & Colored Eraser + +Inside Mountain Top Layer Bridge (Inside Mountain) - Inside Mountain Second Layer - 0x09E54: +158409 - 0x09E7A (Obscured Vision 1) - True - Obscured & Squares & Black/White Squares & Dots +158410 - 0x09E71 (Obscured Vision 2) - 0x09E7A - Obscured & Squares & Black/White Squares & Dots +158411 - 0x09E72 (Obscured Vision 3) - 0x09E71 - Obscured & Squares & Black/White Squares & Shapers & Dots +158412 - 0x09E69 (Obscured Vision 4) - 0x09E72 - Obscured & Squares & Black/White Squares & Dots +158413 - 0x09E7B (Obscured Vision 5) - 0x09E69 - Obscured & Squares & Black/White Squares & Dots +158414 - 0x09E73 (Moving Background 1) - True - Moving & Stars & Squares & Black/White Squares & Stars + Same Colored Symbol +158415 - 0x09E75 (Moving Background 2) - 0x09E73 - Moving & Stars & Squares & Black/White Squares & Stars + Same Colored Symbol +158416 - 0x09E78 (Moving Background 3) - 0x09E75 - Moving & Shapers +158417 - 0x09E79 (Moving Background 4) - 0x09E78 - Moving & Shapers & Rotated Shapers +158418 - 0x09E6C (Moving Background 5) - 0x09E79 - Moving & Stars & Squares & Black/White Squares & Stars + Same Colored Symbol +158419 - 0x09E6F (Moving Background 6) - 0x09E6C - Moving & Stars & Rotated Shapers & Shapers +158420 - 0x09E6B (Moving Background 7) - 0x09E6F - Moving & Stars & Dots +158421 - 0x33AF5 (Physically Obstructed 1) - True - Squares & Black/White Squares & Environment & Symmetry +158422 - 0x33AF7 (Physically Obstructed 2) - 0x33AF5 - Squares & Black/White Squares & Stars & Environment +158423 - 0x09F6E (Physically Obstructed 3) - 0x33AF7 - Symmetry & Dots & Environment +158424 - 0x09EAD (Angled Inside Trash 1) - True - Squares & Black/White Squares & Shapers & Angled +158425 - 0x09EAF (Angled Inside Trash 2) - 0x09EAD - Squares & Black/White Squares & Shapers & Angled +Door - 0x09E54 (Door to Second Layer) - 0x09EAF & 0x09F6E & 0x09E6B & 0x09E7B + +Inside Mountain Second Layer (Inside Mountain) - Inside Mountain Second Layer Light Bridge Room Near - 0x09FFB - Inside Mountain Second Layer Blue Bridge - 0x09E86: +158426 - 0x09FD3 (Color Cycle 1) - True - Color Cycle & RGB & Stars & Squares & Colored Squares & Stars + Same Colored Symbol +158427 - 0x09FD4 (Color Cycle 2) - 0x09FD3 - Color Cycle & RGB & Stars & Squares & Colored Squares & Stars + Same Colored Symbol +158428 - 0x09FD6 (Color Cycle 3) - 0x09FD4 - Color Cycle & RGB & Stars & Squares & Colored Squares & Stars + Same Colored Symbol +158429 - 0x09FD7 (Color Cycle 4) - 0x09FD6 - Color Cycle & RGB & Stars & Squares & Colored Squares & Stars + Same Colored Symbol & Shapers & Colored Shapers +158430 - 0x09FD8 (Color Cycle 5) - 0x09FD7 - Color Cycle & RGB & Squares & Colored Squares & Symmetry & Colored Dots +Door - 0x09FFB (Staircase Near) - 0x09FD8 + +Inside Mountain Second Layer Blue Bridge (Inside Mountain) - Inside Mountain Second Layer Beyond Bridge - TrueOneWay - Inside Mountain Second Layer At Door - TrueOneWay: + +Inside Mountain Second Layer At Door (Inside Mountain) - Inside Mountain Second Layer Elevator Room - 0x09EDD: +Door - 0x09EDD (Door to Elevator) - 0x09ED8 & 0x09E86 + +Inside Mountain Second Layer Light Bridge Room Near (Inside Mountain): +158431 - 0x09E86 (Light Bridge Controller 2) - True - Stars & Stars + Same Colored Symbol & Colored Rotated Shapers & Rotated Shapers & Eraser & Two Lines + +Inside Mountain Second Layer Beyond Bridge (Inside Mountain) - Inside Mountain Second Layer Light Bridge Room Far - 0x09E07: +158432 - 0x09FCC (Same Solution 1) - True - Dots & Same Solution +158433 - 0x09FCE (Same Solution 2) - 0x09FCC - Squares & Black/White Squares & Same Solution +158434 - 0x09FCF (Same Solution 3) - 0x09FCE - Stars & Same Solution +158435 - 0x09FD0 (Same Solution 4) - 0x09FCF - Rotated Shapers & Same Solution +158436 - 0x09FD1 (Same Solution 5) - 0x09FD0 - Stars & Squares & Colored Squares & Stars + Same Colored Symbol & Same Solution +158437 - 0x09FD2 (Same Solution 6) - 0x09FD1 - Shapers & Same Solution +Door - 0x09E07 (Staircase Far) - 0x09FD2 + +Inside Mountain Second Layer Light Bridge Room Far (Inside Mountain): +158438 - 0x09ED8 (Light Bridge Controller 3) - True - Stars & Stars + Same Colored Symbol & Colored Rotated Shapers & Rotated Shapers & Eraser & Two Lines + +Inside Mountain Second Layer Elevator Room (Inside Mountain) - Inside Mountain Second Layer Elevator - TrueOneWay: +158613 - 0x17F93 (Elevator Discard) - True - Triangles + +Inside Mountain Second Layer Elevator (Inside Mountain) - Inside Mountain Second Layer Elevator Room - 0x09EEB - Inside Mountain Third Layer - 0x09EEB: +158439 - 0x09EEB (Elevator Control Panel) - True - Dots + +Inside Mountain Third Layer (Inside Mountain) - Inside Mountain Second Layer Elevator - TrueOneWay - Inside Mountain Bottom Layer - 0x09F89: +158440 - 0x09FC1 (Giant Puzzle Bottom Left) - True - Shapers & Eraser +158441 - 0x09F8E (Giant Puzzle Bottom Right) - True - Shapers & Eraser +158442 - 0x09F01 (Giant Puzzle Top Right) - True - Rotated Shapers +158443 - 0x09EFF (Giant Puzzle Top Left) - True - Shapers & Eraser +158444 - 0x09FDA (Giant Puzzle) - 0x09FC1 & 0x09F8E & 0x09F01 & 0x09EFF - Shapers & Symmetry +Door - 0x09F89 (Glass Door) - 0x09FDA + +Inside Mountain Bottom Layer (Inside Mountain) - Inside Mountain Bottom Layer Rock - 0x17FA2 - Final Room - 0x0C141: +158614 - 0x17FA2 (Bottom Layer Discard) - 0xFFF00 - Triangles & Environment +158445 - 0x01983 (Door to Final Room Left) - True - Shapers & Stars +158446 - 0x01987 (Door to Final Room Right) - True - Squares & Colored Squares & Dots +Door - 0x0C141 (Door to Final Room) - 0x01983 & 0x01987 + +Inside Mountain Bottom Layer Rock (Inside Mountain) - Inside Mountain Bottom Layer - 0x17F33 - Inside Mountain Path to Secret Area - 0x17F33: +Door - 0x17F33 (Bottom Layer Rock Open) - True + +Inside Mountain Path to Secret Area (Inside Mountain) - Inside Mountain Bottom Layer Rock - 0x334E1 - Inside Mountain Caves - 0x2D77D: +158447 - 0x00FF8 (Secret Area Entry Panel) - True - Triangles & Black/White Squares & Squares +Door - 0x2D77D (Door to Secret Area) - 0x00FF8 +158448 - 0x334E1 (Rock Control) - True - True + +Inside Mountain Caves (Inside Mountain Caves) - Main Island - 0x2D73F - Main Island - 0x2D859 - Path to Challenge - 0x019A5: +158451 - 0x335AB (Elevator Inside Control) - True - Dots & Squares & Black/White Squares +158452 - 0x335AC (Elevator Upper Outside Control) - 0x335AB - Squares & Black/White Squares +158453 - 0x3369D (Elevator Lower Outside Control) - 0x335AB - Squares & Black/White Squares & Dots +158454 - 0x00190 (Dot Grid Triangles 1) - True - Dots & Triangles +158455 - 0x00558 (Dot Grid Triangles 2) - 0x00190 - Dots & Triangles +158456 - 0x00567 (Dot Grid Triangles 3) - 0x00558 - Dots & Triangles +158457 - 0x006FE (Dot Grid Triangles 4) - 0x00567 - Dots & Triangles +158458 - 0x01A0D (Symmetry Triangles) - True - Symmetry & Triangles +158459 - 0x008B8 (Squares and Triangles) - True - Squares & Black/White Squares & Triangles +158460 - 0x00973 (Stars and Triangles) - 0x008B8 - Stars & Triangles +158461 - 0x0097B (Stars and Triangles of same color) - 0x00973 - Stars & Triangles & Stars and Triangles of same color & Stars + Same Colored Symbol +158462 - 0x0097D (Stars & Squares and Triangles) - 0x0097B - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol & Triangles +158463 - 0x0097E (Stars & Squares and Triangles 2) - 0x0097D - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol & Stars and Triangles of same color +158464 - 0x00994 (Rotated Shapers and Triangles 1) - True - Rotated Shapers & Triangles +158465 - 0x334D5 (Rotated Shapers and Triangles 2) - 0x00994 - Rotated Shapers & Triangles +158466 - 0x00995 (Rotated Shapers and Triangles 3) - 0x334D5 - Rotated Shapers & Triangles +158467 - 0x00996 (Shapers and Triangles 1) - 0x00995 - Shapers & Triangles +158468 - 0x00998 (Shapers and Triangles 2) - 0x00996 - Shapers & Triangles +158469 - 0x009A4 (Broken Shapers) - True - Shapers & Broken Shapers +158470 - 0x018A0 (Symmetry Shapers) - True - Shapers & Symmetry +158471 - 0x00A72 (Broken and Negative Shapers) - True - Shapers & Broken Shapers & Negative Shapers +158472 - 0x32962 (Rotated Broken Shapers) - True - Rotated Shapers & Broken Rotated Shapers +158473 - 0x32966 (Stars and Squares) - True - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol +158474 - 0x01A31 (Rainbow Squares) - True - Color Cycle & RGB & Squares & Colored Squares +158475 - 0x00B71 (Squares & Stars and Colored Eraser) - True - Colored Eraser & Squares & Colored Squares & Stars & Stars + Same Colored Symbol & Eraser +158478 - 0x288EA (Wooden Beam Shapers) - True - Environment & Shapers +158479 - 0x288FC (Wooden Beam Squares and Shapers) - True - Environment & Squares & Black/White Squares & Shapers & Rotated Shapers +158480 - 0x289E7 (Wooden Beam Stars and Squares) - True - Environment & Stars & Squares & Black/White Squares +158481 - 0x288AA (Wooden Beam Shapers and Stars) - True - Environment & Stars & Shapers +158482 - 0x17FB9 (Upstairs Dot Grid Negative Shapers) - True - Shapers & Dots & Negative Shapers +158483 - 0x0A16B (Upstairs Dot Grid Gap Dots) - True - Dots +158484 - 0x0A2CE (Upstairs Dot Grid Stars) - 0x0A16B - Stars & Dots +158485 - 0x0A2D7 (Upstairs Dot Grid Stars & Squares) - 0x0A2CE - Dots & Black/White Squares & Stars + Same Colored Symbol & Stars +158486 - 0x0A2DD (Upstairs Dot Grid Shapers) - 0x0A2D7 - Shapers & Dots +158487 - 0x0A2EA (Upstairs Dot Grid Rotated Shapers) - 0x0A2DD - Rotated Shapers & Dots +158488 - 0x0008F (Upstairs Invisible Dots 1) - True - Dots & Invisible Dots +158489 - 0x0006B (Upstairs Invisible Dots 2) - 0x0008F - Dots & Invisible Dots +158490 - 0x0008B (Upstairs Invisible Dots 3) - 0x0006B - Dots & Invisible Dots +158491 - 0x0008C (Upstairs Invisible Dots 4) - 0x0008B - Dots & Invisible Dots +158492 - 0x0008A (Upstairs Invisible Dots 5) - 0x0008C - Dots & Invisible Dots +158493 - 0x00089 (Upstairs Invisible Dots 6) - 0x0008A - Dots & Invisible Dots +158494 - 0x0006A (Upstairs Invisible Dots 7) - 0x00089 - Dots & Invisible Dots +158495 - 0x0006C (Upstairs Invisible Dots 8) - 0x0006A - Dots & Invisible Dots +158496 - 0x00027 (Upstairs Invisible Dot Symmetry 1) - True - Dots & Invisible Dots & Symmetry +158497 - 0x00028 (Upstairs Invisible Dot Symmetry 2) - 0x00027 - Dots & Invisible Dots & Symmetry +158498 - 0x00029 (Upstairs Invisible Dot Symmetry 3) - 0x00028 - Dots & Invisible Dots & Symmetry +158476 - 0x09DD5 (Lone Pillar) - True - Pillar & Triangles +Door - 0x019A5 (Secret Black Door to Challenge) - 0x09DD5 +158449 - 0x021D7 (Shortcut to Mountain Panel) - True - Triangles & Stars & Stars + Same Colored Symbol & Colored Triangles +Door - 0x2D73F (Shortcut to Mountain Door) - 0x021D7 +158450 - 0x17CF2 (Shortcut to Swamp Panel) - True - Triangles +Door - 0x2D859 (Shortcut to Swamp Door) - 0x17CF2 + +Path to Challenge (Inside Mountain Caves) - Challenge - 0x0A19A: +158477 - 0x0A16E (Challenge Entry Panel) - True - Stars & Shapers & Colored Shapers & Stars + Same Colored Symbol +Door - 0x0A19A (Challenge Entry Door) - 0x0A16E + +Challenge (Challenge) - Theater Walkway - 0x0348A: +158499 - 0x0A332 (Start Timer) - 11 Lasers - True +158500 - 0x0088E (Small Basic) - 0x0A332 - True +158501 - 0x00BAF (Big Basic) - 0x0088E - True +158502 - 0x00BF3 (Square) - 0x00BAF - Squares & Black/White Squares +158503 - 0x00C09 (Maze Map) - 0x00BF3 - Dots +158504 - 0x00CDB (Stars and Dots) - 0x00C09 - Stars & Dots +158505 - 0x0051F (Symmetry) - 0x00CDB - Symmetry & Colored Dots & Dots +158506 - 0x00524 (Stars and Shapers) - 0x0051F - Stars & Shapers +158507 - 0x00CD4 (Big Basic 2) - 0x00524 - True +158508 - 0x00CB9 (Choice Squares Right) - 0x00CD4 - Squares & Black/White Squares +158509 - 0x00CA1 (Choice Squares Middle) - 0x00CD4 - Squares & Black/White Squares +158510 - 0x00C80 (Choice Squares Left) - 0x00CD4 - Squares & Black/White Squares +158511 - 0x00C68 (Choice Squares 2 Right) - 0x00CB9 | 0x00CA1 | 0x00C80 - Squares & Black/White Squares & Colored Squares +158512 - 0x00C59 (Choice Squares 2 Middle) - 0x00CB9 | 0x00CA1 | 0x00C80 - Squares & Black/White Squares & Colored Squares +158513 - 0x00C22 (Choice Squares 2 Left) - 0x00CB9 | 0x00CA1 | 0x00C80 - Squares & Black/White Squares & Colored Squares +158514 - 0x034F4 (Maze Hidden 1) - 0x00C68 | 0x00C59 | 0x00C22 - Triangles +158515 - 0x034EC (Maze Hidden 2) - 0x00C68 | 0x00C59 | 0x00C22 - Triangles +158516 - 0x1C31A (Dots Pillar) - 0x034F4 & 0x034EC - Dots & Symmetry & Pillar +158517 - 0x1C319 (Squares Pillar) - 0x034F4 & 0x034EC - Squares & Black/White Squares & Symmetry & Pillar +158667 - 0x0356B (Vault Box) - 0x1C31A & 0x1C319 - True +158518 - 0x039B4 (Door to Theater Walkway Panel) - True - Triangles +Door - 0x0348A (Door to Theater Walkway) - 0x039B4 + +Theater Walkway (Theater Walkway) - Windmill Interior - 0x27739 - Desert Lowest Level Inbetween Shortcuts - 0x27263 - Town - 0x09E87: +158668 - 0x2FAF6 (Vault Box) - True - True +158519 - 0x27732 (Theater Shortcut Panel) - True - True +Door - 0x27739 (Door to Windmill Interior) - 0x27732 +158520 - 0x2773D (Desert Shortcut Panel) - True - True +Door - 0x27263 (Door to Desert Elevator Room) - 0x2773D +158521 - 0x09E85 (Town Shortcut Panel) - True - Triangles +Door - 0x09E87 (Door to Town) - 0x09E85 + +Final Room (Inside Mountain Final Room) - Elevator - 0x339BB & 0x33961: +158522 - 0x0383A (Right Pillar 1) - True - Stars & Pillar +158523 - 0x09E56 (Right Pillar 2) - 0x0383A - Stars & Dots & Pillar +158524 - 0x09E5A (Right Pillar 3) - 0x09E56 - Dots & Pillar +158525 - 0x33961 (Right Pillar 4) - 0x09E5A - Dots & Symmetry & Pillar +158526 - 0x0383D (Left Pillar 1) - True - Dots & Pillar +158527 - 0x0383F (Left Pillar 2) - 0x0383D - Squares & Black/White Squares & Pillar +158528 - 0x03859 (Left Pillar 3) - 0x0383F - Shapers & Pillar +158529 - 0x339BB (Left Pillar 4) - 0x03859 - Squares & Black/White Squares & Stars & Symmetry & Pillar + +Elevator (Inside Mountain Final Room): +158530 - 0x3D9A6 (Elevator Door Closer Left) - True - True +158531 - 0x3D9A7 (Elevator Door Close Right) - True - True +158532 - 0x3C113 (Elevator Door Open Left) - 0x3D9A6 | 0x3D9A7 - True +158533 - 0x3C114 (Elevator Door Open Right) - 0x3D9A6 | 0x3D9A7 - True +158534 - 0x3D9AA (Back Wall Left) - 0x3D9A6 | 0x3D9A7 - True +158535 - 0x3D9A8 (Back Wall Right) - 0x3D9A6 | 0x3D9A7 - True +158536 - 0x3D9A9 (Elevator Start) - 0x3D9AA | 0x3D9A8 - True + +Boat (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Treehouse Entry Area - TrueOneWay - Quarry Boathouse Behind Staircase - TrueOneWay - Inside Glass Factory Behind Back Wall - TrueOneWay: diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 0857ef6b42..19c9b97240 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -35,29 +35,37 @@ class WitnessWorld(World): """ game = "The Witness" topology_present = False - data_version = 2 + data_version = 5 static_logic = StaticWitnessLogic() static_locat = StaticWitnessLocations() static_items = StaticWitnessItems() web = WitnessWebWorld() - options = the_witness_options + option_definitions = the_witness_options item_name_to_id = { name: data.code for name, data in static_items.ALL_ITEM_TABLE.items() } location_name_to_id = StaticWitnessLocations.ALL_LOCATIONS_TO_ID + item_name_groups = StaticWitnessItems.ITEM_NAME_GROUPS def _get_slot_data(self): return { 'seed': self.world.random.randint(0, 1000000), 'victory_location': int(self.player_logic.VICTORY_LOCATION, 16), 'panelhex_to_id': self.locat.CHECK_PANELHEX_TO_ID, - 'doorhex_to_id': self.player_logic.DOOR_DICT_FOR_CLIENT, - 'door_connections_to_sever': self.player_logic.DOOR_CONNECTIONS_TO_SEVER + 'item_id_to_door_hexes': self.items.ITEM_ID_TO_DOOR_HEX, + 'door_hexes': self.items.DOORS, + 'symbols_not_in_the_game': self.items.SYMBOLS_NOT_IN_THE_GAME } def generate_early(self): + if not (is_option_enabled(self.world, self.player, "shuffle_symbols") + or get_option_value(self.world, self.player, "shuffle_doors") + or is_option_enabled(self.world, self.player, "shuffle_lasers")): + raise Exception("This Witness world doesn't have any progression items. Please turn on Symbol Shuffle, Door" + " Shuffle or Laser Shuffle") + self.player_logic = WitnessPlayerLogic(self.world, self.player) self.locat = WitnessPlayerLocations(self.world, self.player, self.player_logic) self.items = WitnessPlayerItems(self.locat, self.world, self.player, self.player_logic) @@ -78,11 +86,11 @@ class WitnessWorld(World): less_junk = 0 # Put good item on first check if symbol shuffle is on - # symbols = is_option_enabled(self.world, self.player, "shuffle_symbols") - symbols = True + symbols = is_option_enabled(self.world, self.player, "shuffle_symbols") if symbols: random_good_item = self.world.random.choice(self.items.GOOD_ITEMS) + first_check = self.world.get_location( "Tutorial Gate Open", self.player ) @@ -91,6 +99,10 @@ class WitnessWorld(World): less_junk = 1 + for item in self.player_logic.STARTING_INVENTORY: + self.world.push_precollected(items_by_name[item]) + pool.remove(items_by_name[item]) + for item in self.items.EXTRA_AMOUNTS: witness_item = self.create_item(item) for i in range(0, self.items.EXTRA_AMOUNTS[item]): diff --git a/worlds/witness/items.py b/worlds/witness/items.py index 0d7530988e..9ffd5a1173 100644 --- a/worlds/witness/items.py +++ b/worlds/witness/items.py @@ -2,7 +2,7 @@ Defines progression, junk and event items for The Witness """ import copy -from typing import Dict, NamedTuple, Optional +from typing import Dict, NamedTuple, Optional, Set from BaseClasses import Item, MultiWorld from . import StaticWitnessLogic, WitnessPlayerLocations, WitnessPlayerLogic @@ -35,6 +35,8 @@ class StaticWitnessItems: ALL_ITEM_TABLE: Dict[str, ItemData] = {} + ITEM_NAME_GROUPS: Dict[str, Set[str]] = dict() + # These should always add up to 1!!! BONUS_WEIGHTS = { "Speed Boost": Fraction(1, 1), @@ -51,12 +53,23 @@ class StaticWitnessItems: def __init__(self): item_tab = dict() - for item in StaticWitnessLogic.ALL_SYMBOL_ITEMS.union(StaticWitnessLogic.ALL_DOOR_ITEMS): + for item in StaticWitnessLogic.ALL_SYMBOL_ITEMS: if item[0] == "11 Lasers" or item == "7 Lasers": continue item_tab[item[0]] = ItemData(158000 + item[1], True, False) + self.ITEM_NAME_GROUPS.setdefault("Symbols", set()).add(item[0]) + + for item in StaticWitnessLogic.ALL_DOOR_ITEMS: + item_tab[item[0]] = ItemData(158000 + item[1], True, False) + + # 1500 - 1510 are the laser items, which are handled like doors but should be their own separate group. + if item[1] in range(1500, 1511): + self.ITEM_NAME_GROUPS.setdefault("Lasers", set()).add(item[0]) + else: + self.ITEM_NAME_GROUPS.setdefault("Doors", set()).add(item[0]) + for item in StaticWitnessLogic.ALL_TRAPS: item_tab[item[0]] = ItemData( 158000 + item[1], False, False, True @@ -89,23 +102,39 @@ class WitnessPlayerItems: self.ITEM_TABLE = copy.copy(StaticWitnessItems.ALL_ITEM_TABLE) self.PROGRESSION_TABLE = dict() + self.ITEM_ID_TO_DOOR_HEX = dict() + self.DOORS = set() + + self.SYMBOLS_NOT_IN_THE_GAME = set() + self.EXTRA_AMOUNTS = { "Functioning Brain": 1, "Puzzle Skip": get_option_value(world, player, "puzzle_skip_amount") } for item in StaticWitnessLogic.ALL_SYMBOL_ITEMS.union(StaticWitnessLogic.ALL_DOOR_ITEMS): - if item not in player_logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME: + if item[0] not in player_logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME: del self.ITEM_TABLE[item[0]] + if item in StaticWitnessLogic.ALL_SYMBOL_ITEMS: + self.SYMBOLS_NOT_IN_THE_GAME.add(StaticWitnessItems.ALL_ITEM_TABLE[item[0]].code) else: self.PROGRESSION_TABLE[item[0]] = self.ITEM_TABLE[item[0]] + for entity_hex, items in player_logic.DOOR_ITEMS_BY_ID.items(): + entity_hex_int = int(entity_hex, 16) + + self.DOORS.add(entity_hex_int) + + for item in items: + item_id = StaticWitnessItems.ALL_ITEM_TABLE[item].code + self.ITEM_ID_TO_DOOR_HEX.setdefault(item_id, set()).add(entity_hex_int) + symbols = is_option_enabled(world, player, "shuffle_symbols") if "shuffle_symbols" not in the_witness_options.keys(): symbols = True - doors = is_option_enabled(world, player, "shuffle_doors") + doors = get_option_value(world, player, "shuffle_doors") if doors and symbols: self.GOOD_ITEMS = [ @@ -117,10 +146,10 @@ class WitnessPlayerItems: "Shapers", "Symmetry" ] - if is_option_enabled(world, player, "shuffle_discarded_panels"): - self.GOOD_ITEMS.append("Triangles") - if not is_option_enabled(world, player, "disable_non_randomized_puzzles"): - self.GOOD_ITEMS.append("Colored Squares") + if is_option_enabled(world, player, "shuffle_discarded_panels"): + self.GOOD_ITEMS.append("Triangles") + if not is_option_enabled(world, player, "disable_non_randomized_puzzles"): + self.GOOD_ITEMS.append("Colored Squares") for event_location in locat.EVENT_LOCATION_TABLE: location = player_logic.EVENT_ITEM_PAIRS[event_location] diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py index 380c64c069..f6fcad70ce 100644 --- a/worlds/witness/locations.py +++ b/worlds/witness/locations.py @@ -2,7 +2,7 @@ Defines constants for different types of locations in the game """ -from .Options import is_option_enabled +from .Options import is_option_enabled, get_option_value from .player_logic import StaticWitnessLogic, WitnessPlayerLogic @@ -42,7 +42,7 @@ class StaticWitnessLocations: "Symmetry Island Colored Dots 6", "Symmetry Island Fading Lines 7", "Symmetry Island Scenery Outlines 5", - "Symmetry Island Laser", + "Symmetry Island Laser Panel", "Orchard Apple Tree 5", @@ -52,7 +52,7 @@ class StaticWitnessLocations: "Desert Artificial Light Reflection 3", "Desert Pond Reflection 5", "Desert Flood Reflection 6", - "Desert Laser", + "Desert Laser Panel", "Quarry Mill Eraser and Dots 6", "Quarry Mill Eraser and Squares 8", @@ -63,34 +63,34 @@ class StaticWitnessLocations: "Quarry Boathouse Stars & Eraser & Shapers 2", "Quarry Boathouse Stars & Eraser & Shapers 5", "Quarry Discard", - "Quarry Laser", + "Quarry Laser Panel", "Shadows Lower Avoid 8", "Shadows Environmental Avoid 8", "Shadows Follow 5", - "Shadows Laser", + "Shadows Laser Panel", "Keep Hedge Maze 4", "Keep Pressure Plates 4", "Keep Discard", - "Keep Laser Hedges", - "Keep Laser Pressure Plates", + "Keep Laser Panel Hedges", + "Keep Laser Panel Pressure Plates", "Shipwreck Vault Box", "Shipwreck Discard", "Monastery Rhombic Avoid 3", "Monastery Branch Follow 2", - "Monastery Laser", + "Monastery Laser Panel", "Town Cargo Box Discard", "Town Hexagonal Reflection", - "Town Square Avoid", + "Town Church Lattice", "Town Rooftop Discard", "Town Symmetry Squares 5 + Dots", "Town Full Dot Grid Shapers 5", "Town Shapers & Dots & Eraser", - "Town Laser", + "Town Laser Panel", "Theater Discard", @@ -98,7 +98,7 @@ class StaticWitnessLocations: "Jungle Waves 3", "Jungle Waves 7", "Jungle Popup Wall 6", - "Jungle Laser", + "Jungle Laser Panel", "River Vault Box", @@ -106,7 +106,7 @@ class StaticWitnessLocations: "Bunker Drawn Squares 9", "Bunker Drawn Squares through Tinted Glass 3", "Bunker Drop-Down Door Squares 2", - "Bunker Laser", + "Bunker Laser Panel", "Swamp Seperatable Shapers 6", "Swamp Combinable Shapers 8", @@ -117,7 +117,7 @@ class StaticWitnessLocations: "Swamp Red Underwater Negative Shapers 4", "Swamp More Rotated Shapers 4", "Swamp Blue Underwater Negative Shapers 5", - "Swamp Laser", + "Swamp Laser Panel", "Treehouse Yellow Bridge 9", "Treehouse First Purple Bridge 5", @@ -125,21 +125,12 @@ class StaticWitnessLocations: "Treehouse Green Bridge 7", "Treehouse Green Bridge Discard", "Treehouse Left Orange Bridge 15", - "Treehouse Burned House Discard", + "Treehouse Burnt House Discard", "Treehouse Right Orange Bridge 12", - "Treehouse Laser", + "Treehouse Laser Panel", "Mountaintop Discard", "Mountaintop Vault Box", - - "Inside Mountain Obscured Vision 5", - "Inside Mountain Moving Background 7", - "Inside Mountain Physically Obstructed 3", - "Inside Mountain Angled Inside Trash 2", - "Inside Mountain Color Cycle 5", - "Inside Mountain Same Solution 6", - "Inside Mountain Elevator Discard", - "Inside Mountain Giant Puzzle", } UNCOMMON_LOCATIONS = { @@ -156,35 +147,53 @@ class StaticWitnessLocations: "Swamp Underwater Back Optional", } - HARD_LOCATIONS = { - "Inside Mountain Secret Area Dot Grid Triangles 4", - "Inside Mountain Secret Area Symmetry Triangles", - "Inside Mountain Secret Area Stars & Squares and Triangles 2", - "Inside Mountain Secret Area Shapers and Triangles 2", - "Inside Mountain Secret Area Symmetry Shapers", - "Inside Mountain Secret Area Broken and Negative Shapers", - "Inside Mountain Secret Area Broken Shapers", + CAVES_LOCATIONS = { + "Inside Mountain Caves Dot Grid Triangles 4", + "Inside Mountain Caves Symmetry Triangles", + "Inside Mountain Caves Stars & Squares and Triangles 2", + "Inside Mountain Caves Shapers and Triangles 2", + "Inside Mountain Caves Symmetry Shapers", + "Inside Mountain Caves Broken and Negative Shapers", + "Inside Mountain Caves Broken Shapers", - "Inside Mountain Secret Area Rainbow Squares", - "Inside Mountain Secret Area Squares & Stars and Colored Eraser", - "Inside Mountain Secret Area Rotated Broken Shapers", - "Inside Mountain Secret Area Stars and Squares", - "Inside Mountain Secret Area Lone Pillar", - "Inside Mountain Secret Area Wooden Beam Shapers", - "Inside Mountain Secret Area Wooden Beam Squares and Shapers", - "Inside Mountain Secret Area Wooden Beam Stars and Squares", - "Inside Mountain Secret Area Wooden Beam Shapers and Stars", - "Inside Mountain Secret Area Upstairs Invisible Dots 8", - "Inside Mountain Secret Area Upstairs Invisible Dot Symmetry 3", - "Inside Mountain Secret Area Upstairs Dot Grid Negative Shapers", - "Inside Mountain Secret Area Upstairs Dot Grid Rotated Shapers", + "Inside Mountain Caves Rainbow Squares", + "Inside Mountain Caves Squares & Stars and Colored Eraser", + "Inside Mountain Caves Rotated Broken Shapers", + "Inside Mountain Caves Stars and Squares", + "Inside Mountain Caves Lone Pillar", + "Inside Mountain Caves Wooden Beam Shapers", + "Inside Mountain Caves Wooden Beam Squares and Shapers", + "Inside Mountain Caves Wooden Beam Stars and Squares", + "Inside Mountain Caves Wooden Beam Shapers and Stars", + "Inside Mountain Caves Upstairs Invisible Dots 8", + "Inside Mountain Caves Upstairs Invisible Dot Symmetry 3", + "Inside Mountain Caves Upstairs Dot Grid Negative Shapers", + "Inside Mountain Caves Upstairs Dot Grid Rotated Shapers", - "Challenge Vault Box", "Theater Walkway Vault Box", "Inside Mountain Bottom Layer Discard", "Theater Challenge Video", } + MOUNTAIN_UNREACHABLE_FROM_BEHIND = { + "Mountaintop Trap Door Triple Exit", + + "Inside Mountain Obscured Vision 5", + "Inside Mountain Moving Background 7", + "Inside Mountain Physically Obstructed 3", + "Inside Mountain Angled Inside Trash 2", + "Inside Mountain Color Cycle 5", + "Inside Mountain Same Solution 6", + } + + MOUNTAIN_REACHABLE_FROM_BEHIND = { + "Inside Mountain Elevator Discard", + "Inside Mountain Giant Puzzle", + + "Inside Mountain Final Room Left Pillar 4", + "Inside Mountain Final Room Right Pillar 4", + } + ALL_LOCATIONS_TO_ID = dict() @staticmethod @@ -193,12 +202,7 @@ class StaticWitnessLocations: Calculates the location ID for any given location """ - panel_offset = StaticWitnessLogic.CHECKS_BY_HEX[chex]["idOffset"] - type_offset = StaticWitnessLocations.TYPE_OFFSETS[ - StaticWitnessLogic.CHECKS_BY_HEX[chex]["panelType"] - ] - - return StaticWitnessLocations.ID_START + panel_offset + type_offset + return StaticWitnessLogic.CHECKS_BY_HEX[chex]["id"] @staticmethod def get_event_name(panel_hex): @@ -213,6 +217,7 @@ class StaticWitnessLocations: all_loc_to_id = { panel_obj["checkName"]: self.get_id(chex) for chex, panel_obj in StaticWitnessLogic.CHECKS_BY_HEX.items() + if panel_obj["id"] } all_loc_to_id = dict( @@ -229,12 +234,34 @@ class WitnessPlayerLocations: """ def __init__(self, world, player, player_logic: WitnessPlayerLogic): + """Defines locations AFTER logic changes due to options""" + self.PANEL_TYPES_TO_SHUFFLE = {"General", "Laser"} self.CHECK_LOCATIONS = ( StaticWitnessLocations.GENERAL_LOCATIONS ) - """Defines locations AFTER logic changes due to options""" + doors = get_option_value(world, player, "shuffle_doors") + earlyutm = is_option_enabled(world, player, "early_secret_area") + victory = get_option_value(world, player, "victory_condition") + lasers = get_option_value(world, player, "challenge_lasers") + laser_shuffle = get_option_value(world, player, "shuffle_lasers") + + postgame = set() + postgame = postgame | StaticWitnessLocations.CAVES_LOCATIONS + postgame = postgame | StaticWitnessLocations.MOUNTAIN_REACHABLE_FROM_BEHIND + postgame = postgame | StaticWitnessLocations.MOUNTAIN_UNREACHABLE_FROM_BEHIND + + self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | postgame + + if earlyutm or doors >= 2 or (victory == 1 and (lasers <= 11 or laser_shuffle)): + postgame -= StaticWitnessLocations.CAVES_LOCATIONS + + if doors >= 2: + postgame -= StaticWitnessLocations.MOUNTAIN_REACHABLE_FROM_BEHIND + + if victory != 2: + postgame -= StaticWitnessLocations.MOUNTAIN_UNREACHABLE_FROM_BEHIND if is_option_enabled(world, player, "shuffle_discarded_panels"): self.PANEL_TYPES_TO_SHUFFLE.add("Discard") @@ -245,18 +272,11 @@ class WitnessPlayerLocations: if is_option_enabled(world, player, "shuffle_uncommon"): self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | StaticWitnessLocations.UNCOMMON_LOCATIONS - if is_option_enabled(world, player, "shuffle_hard"): - self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | StaticWitnessLocations.HARD_LOCATIONS - - if is_option_enabled(world, player, "shuffle_symbols") and is_option_enabled(world, player, "shuffle_doors"): - if is_option_enabled(world, player, "disable_non_randomized_puzzles"): - # This particular combination of logic settings leads to logic so restrictive that generation can fail - # Hence, we add some extra sphere 0 locations - - self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | StaticWitnessLocations.EXTRA_LOCATIONS - self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | player_logic.ADDED_CHECKS + if not is_option_enabled(world, player, "shuffle_postgame"): + self.CHECK_LOCATIONS -= postgame + self.CHECK_LOCATIONS = self.CHECK_LOCATIONS - { StaticWitnessLogic.CHECKS_BY_HEX[check_hex]["checkName"] for check_hex in player_logic.COMPLETELY_DISABLED_CHECKS @@ -272,7 +292,7 @@ class WitnessPlayerLocations: ) event_locations = { - p for p in player_logic.NECESSARY_EVENT_PANELS + p for p in player_logic.EVENT_PANELS } self.EVENT_LOCATION_TABLE = { diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index 689403dc22..efbb177f00 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -18,22 +18,15 @@ When the world has parsed its options, a second function is called to finalize t import copy from BaseClasses import MultiWorld from .static_logic import StaticWitnessLogic -from .utils import define_new_region, get_disable_unrandomized_list, parse_lambda, get_early_utm_list +from .utils import define_new_region, get_disable_unrandomized_list, parse_lambda, get_early_utm_list, \ + get_symbol_shuffle_list, get_door_panel_shuffle_list, get_doors_complex_list, get_doors_max_list, \ + get_doors_simple_list, get_laser_shuffle from .Options import is_option_enabled, get_option_value, the_witness_options class WitnessPlayerLogic: """WITNESS LOGIC CLASS""" - def update_door_dict(self, panel_hex): - item_id = StaticWitnessLogic.ALL_DOOR_ITEM_IDS_BY_HEX.get(panel_hex) - - if item_id is None: - return - - self.DOOR_DICT_FOR_CLIENT[panel_hex] = item_id - self.DOOR_CONNECTIONS_TO_SEVER.update(StaticWitnessLogic.CONNECTIONS_TO_SEVER_BY_DOOR_HEX[panel_hex]) - def reduce_req_within_region(self, panel_hex): """ Panels in this game often only turn on when other panels are solved. @@ -43,35 +36,42 @@ class WitnessPlayerLogic: Panels outside of the same region will still be checked manually. """ - these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["items"] + check_obj = StaticWitnessLogic.CHECKS_BY_HEX[panel_hex] - real_items = {item[0] for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME} + these_items = frozenset({frozenset()}) + + if check_obj["id"]: + these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["items"] these_items = frozenset({ - subset.intersection(real_items) + subset.intersection(self.PROG_ITEMS_ACTUALLY_IN_THE_GAME) for subset in these_items }) + if panel_hex in self.DOOR_ITEMS_BY_ID: + door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[panel_hex]}) + + all_options = set() + + for items_option in these_items: + for dependentItem in door_items: + all_options.add(items_option.union(dependentItem)) + + return frozenset(all_options) + these_panels = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["panels"] - if StaticWitnessLogic.DOOR_NAMES_BY_HEX.get(panel_hex) in real_items: - self.update_door_dict(panel_hex) - - these_panels = frozenset({frozenset()}) - if these_panels == frozenset({frozenset()}): return these_items all_options = set() - check_obj = StaticWitnessLogic.CHECKS_BY_HEX[panel_hex] - for option in these_panels: dependent_items_for_option = frozenset({frozenset()}) for option_panel in option: - new_items = set() dep_obj = StaticWitnessLogic.CHECKS_BY_HEX.get(option_panel) + if option_panel in {"7 Lasers", "11 Lasers"}: new_items = frozenset({frozenset([option_panel])}) # If a panel turns on when a panel in a different region turns on, @@ -101,8 +101,34 @@ class WitnessPlayerLogic: return frozenset(all_options) def make_single_adjustment(self, adj_type, line): + from . import StaticWitnessItems """Makes a single logic adjustment based on additional logic file""" + if adj_type == "Items": + if line not in StaticWitnessItems.ALL_ITEM_TABLE: + raise RuntimeError("Item \"" + line + "\" does not exit.") + + self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(line) + + if line in StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT: + panel_hexes = StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT[line][2] + for panel_hex in panel_hexes: + self.DOOR_ITEMS_BY_ID.setdefault(panel_hex, set()).add(line) + + return + + if adj_type == "Remove Items": + self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.discard(line) + + if line in StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT: + panel_hexes = StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT[line][2] + for panel_hex in panel_hexes: + if panel_hex in self.DOOR_ITEMS_BY_ID: + self.DOOR_ITEMS_BY_ID[panel_hex].discard(line) + + if adj_type == "Starting Inventory": + self.STARTING_INVENTORY.add(line) + if adj_type == "Event Items": line_split = line.split(" - ") hex_set = line_split[1].split(",") @@ -130,18 +156,20 @@ class WitnessPlayerLogic: if adj_type == "Requirement Changes": line_split = line.split(" - ") - required_items = parse_lambda(line_split[2]) - items_actually_in_the_game = {item[0] for item in StaticWitnessLogic.ALL_SYMBOL_ITEMS} - required_items = frozenset( - subset.intersection(items_actually_in_the_game) - for subset in required_items - ) - requirement = { "panels": parse_lambda(line_split[1]), - "items": required_items } + if len(line_split) > 2: + required_items = parse_lambda(line_split[2]) + items_actually_in_the_game = {item[0] for item in StaticWitnessLogic.ALL_SYMBOL_ITEMS} + required_items = frozenset( + subset.intersection(items_actually_in_the_game) + for subset in required_items + ) + + requirement["items"] = required_items + self.DEPENDENT_REQUIREMENTS_BY_HEX[line_split[0]] = requirement return @@ -151,11 +179,6 @@ class WitnessPlayerLogic: self.COMPLETELY_DISABLED_CHECKS.add(panel_hex) - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME = { - item for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME - if item[0] != StaticWitnessLogic.DOOR_NAMES_BY_HEX.get(panel_hex) - } - return if adj_type == "Region Changes": @@ -189,18 +212,25 @@ class WitnessPlayerLogic: adjustment_linesets_in_order.append(get_disable_unrandomized_list()) if is_option_enabled(world, player, "shuffle_symbols") or "shuffle_symbols" not in the_witness_options.keys(): - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.update(StaticWitnessLogic.ALL_SYMBOL_ITEMS) + adjustment_linesets_in_order.append(get_symbol_shuffle_list()) - if is_option_enabled(world, player, "shuffle_doors"): - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.update(StaticWitnessLogic.ALL_DOOR_ITEMS) + if get_option_value(world, player, "shuffle_doors") == 1: + adjustment_linesets_in_order.append(get_door_panel_shuffle_list()) + + if get_option_value(world, player, "shuffle_doors") == 2: + adjustment_linesets_in_order.append(get_doors_simple_list()) + + if get_option_value(world, player, "shuffle_doors") == 3: + adjustment_linesets_in_order.append(get_doors_complex_list()) + + if get_option_value(world, player, "shuffle_doors") == 4: + adjustment_linesets_in_order.append(get_doors_max_list()) if is_option_enabled(world, player, "early_secret_area"): adjustment_linesets_in_order.append(get_early_utm_list()) - else: - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME = { - item for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME - if item[0] != "Mountaintop River Shape Power On" - } + + if is_option_enabled(world, player, "shuffle_lasers"): + adjustment_linesets_in_order.append(get_laser_shuffle()) for adjustment_lineset in adjustment_linesets_in_order: current_adjustment_type = None @@ -233,62 +263,32 @@ class WitnessPlayerLogic: pair = (name, self.EVENT_ITEM_NAMES[panel]) return pair - def _regions_are_adjacent(self, region1, region2): - for connection in self.CONNECTIONS_BY_REGION_NAME[region1]: - if connection[0] == region2: - return True - - for connection in self.CONNECTIONS_BY_REGION_NAME[region2]: - if connection[0] == region1: - return True - - return False - def make_event_panel_lists(self): """ Special event panel data structures """ - for region_conn in self.CONNECTIONS_BY_REGION_NAME.values(): - for region_and_option in region_conn: - for panelset in region_and_option[1]: - for panel in panelset: - self.EVENT_PANELS_FROM_REGIONS.add(panel) - self.ALWAYS_EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory" - self.ORIGINAL_EVENT_PANELS.update(self.EVENT_PANELS_FROM_PANELS) - self.ORIGINAL_EVENT_PANELS.update(self.EVENT_PANELS_FROM_REGIONS) + for region_name, connections in self.CONNECTIONS_BY_REGION_NAME.items(): + for connection in connections: + for panel_req in connection[1]: + for panel in panel_req: + if panel == "TrueOneWay": + continue - for panel in self.EVENT_PANELS_FROM_REGIONS: - for region_name, region in StaticWitnessLogic.ALL_REGIONS_BY_NAME.items(): - for connection in self.CONNECTIONS_BY_REGION_NAME[region_name]: - connected_r = connection[0] - if connected_r not in StaticWitnessLogic.ALL_REGIONS_BY_NAME: - continue - if region_name == "Boat" or connected_r == "Boat": - continue - connected_r = StaticWitnessLogic.ALL_REGIONS_BY_NAME[connected_r] - if not any([panel in option for option in connection[1]]): - continue - if panel not in region["panels"] | connected_r["panels"]: - self.NECESSARY_EVENT_PANELS.add(panel) + if StaticWitnessLogic.CHECKS_BY_HEX[panel]["region"]["name"] != region_name: + self.EVENT_PANELS_FROM_REGIONS.add(panel) - for event_panel in self.EVENT_PANELS_FROM_PANELS: - for panel, panel_req in self.REQUIREMENTS_BY_HEX.items(): - if any([event_panel in item_set for item_set in panel_req]): - region1 = StaticWitnessLogic.CHECKS_BY_HEX[panel]["region"]["name"] - region2 = StaticWitnessLogic.CHECKS_BY_HEX[event_panel]["region"]["name"] - - if not self._regions_are_adjacent(region1, region2): - self.NECESSARY_EVENT_PANELS.add(event_panel) + self.EVENT_PANELS.update(self.EVENT_PANELS_FROM_PANELS) + self.EVENT_PANELS.update(self.EVENT_PANELS_FROM_REGIONS) for always_hex, always_item in self.ALWAYS_EVENT_NAMES_BY_HEX.items(): self.ALWAYS_EVENT_HEX_CODES.add(always_hex) - self.NECESSARY_EVENT_PANELS.add(always_hex) + self.EVENT_PANELS.add(always_hex) self.EVENT_ITEM_NAMES[always_hex] = always_item - for panel in self.NECESSARY_EVENT_PANELS: + for panel in self.EVENT_PANELS: pair = self.make_event_item_pair(panel) self.EVENT_ITEM_PAIRS[pair[0]] = pair[1] @@ -297,8 +297,8 @@ class WitnessPlayerLogic: self.EVENT_PANELS_FROM_REGIONS = set() self.PROG_ITEMS_ACTUALLY_IN_THE_GAME = set() - self.DOOR_DICT_FOR_CLIENT = dict() - self.DOOR_CONNECTIONS_TO_SEVER = set() + self.DOOR_ITEMS_BY_ID = dict() + self.STARTING_INVENTORY = set() self.CONNECTIONS_BY_REGION_NAME = copy.copy(StaticWitnessLogic.STATIC_CONNECTIONS_BY_REGION_NAME) self.DEPENDENT_REQUIREMENTS_BY_HEX = copy.copy(StaticWitnessLogic.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX) @@ -306,8 +306,7 @@ class WitnessPlayerLogic: # Determining which panels need to be events is a difficult process. # At the end, we will have EVENT_ITEM_PAIRS for all the necessary ones. - self.ORIGINAL_EVENT_PANELS = set() - self.NECESSARY_EVENT_PANELS = set() + self.EVENT_PANELS = set() self.EVENT_ITEM_PAIRS = dict() self.ALWAYS_EVENT_HEX_CODES = set() self.COMPLETELY_DISABLED_CHECKS = set() @@ -320,42 +319,64 @@ class WitnessPlayerLogic: "0x00037": "Monastery Branch Panels Activate", "0x0A079": "Access to Bunker Laser", "0x0A3B5": "Door to Tutorial Discard Opens", + "0x00139": "Keep Hedges 1 Knowledge", + "0x019DC": "Keep Hedges 2 Knowledge", + "0x019E7": "Keep Hedges 3 Knowledge", "0x01D3F": "Keep Laser Panel (Pressure Plates) Activates", "0x09F7F": "Mountain Access", "0x0367C": "Quarry Laser Mill Requirement Met", - "0x009A1": "Swamp Rotating Bridge Near Side", + "0x009A1": "Swamp Rotated Shapers 1 Activates", "0x00006": "Swamp Cyan Water Drains", "0x00990": "Swamp Broken Shapers 1 Activates", "0x0A8DC": "Lower Avoid 6 Activates", "0x0000A": "Swamp More Rotated Shapers 1 Access", - "0x09ED8": "Inside Mountain Second Layer Both Light Bridges Solved", + "0x09E86": "Inside Mountain Second Layer Blue Bridge Access", + "0x09ED8": "Inside Mountain Second Layer Yellow Bridge Access", "0x0A3D0": "Quarry Laser Boathouse Requirement Met", "0x00596": "Swamp Red Water Drains", - "0x28B39": "Town Tower 4th Door Opens", + "0x00E3A": "Swamp Purple Water Drains", "0x0343A": "Door to Symmetry Island Powers On", - "0xFFF00": "Inside Mountain Bottom Layer Discard Turns On" + "0xFFF00": "Inside Mountain Bottom Layer Discard Turns On", + "0x17CA6": "All Boat Panels Turn On", + "0x17CDF": "All Boat Panels Turn On", + "0x09DB8": "All Boat Panels Turn On", + "0x17C95": "All Boat Panels Turn On", + "0x03BB0": "Town Church Lattice Vision From Outside", + "0x28AC1": "Town Shapers & Dots & Eraser Turns On", + "0x28A69": "Town Tower 1st Door Opens", + "0x28ACC": "Town Tower 2nd Door Opens", + "0x28AD9": "Town Tower 3rd Door Opens", + "0x28B39": "Town Tower 4th Door Opens", + "0x03675": "Quarry Mill Ramp Activation From Above", + "0x03679": "Quarry Mill Lift Lowering While Standing On It", + "0x2FAF6": "Tutorial Gate Secret Solution Knowledge", + "0x079DF": "Town Hexagonal Reflection Turns On", + "0x17DA2": "Right Orange Bridge Fully Extended", + "0x19B24": "Shadows Lower Avoid Patterns Visible", + "0x2700B": "Open Door to Treehouse Laser House", + "0x00055": "Orchard Apple Trees 4 Turns On", + "0x17DDB": "Left Orange Bridge Fully Extended", } self.ALWAYS_EVENT_NAMES_BY_HEX = { - "0x0360D": "Symmetry Laser Activation", - "0x03608": "Desert Laser Activation", + "0x00509": "Symmetry Laser Activation", + "0x012FB": "Desert Laser Activation", "0x09F98": "Desert Laser Redirection", - "0x03612": "Quarry Laser Activation", - "0x19650": "Shadows Laser Activation", - "0x0360E": "Keep Laser Activation", - "0x03317": "Keep Laser Activation", - "0x17CA4": "Monastery Laser Activation", - "0x032F5": "Town Laser Activation", - "0x03616": "Jungle Laser Activation", - "0x09DE0": "Bunker Laser Activation", - "0x03615": "Swamp Laser Activation", - "0x03613": "Treehouse Laser Activation", + "0x01539": "Quarry Laser Activation", + "0x181B3": "Shadows Laser Activation", + "0x014BB": "Keep Laser Activation", + "0x17C65": "Monastery Laser Activation", + "0x032F9": "Town Laser Activation", + "0x00274": "Jungle Laser Activation", + "0x0C2B2": "Bunker Laser Activation", + "0x00BF6": "Swamp Laser Activation", + "0x028A4": "Treehouse Laser Activation", "0x03535": "Shipwreck Video Pattern Knowledge", "0x03542": "Mountain Video Pattern Knowledge", "0x0339E": "Desert Video Pattern Knowledge", "0x03481": "Tutorial Video Pattern Knowledge", "0x03702": "Jungle Video Pattern Knowledge", - "0x2FAF6": "Theater Walkway Video Pattern Knowledge", + "0x0356B": "Challenge Video Pattern Knowledge", "0x09F7F": "Mountaintop Trap Door Turns On", "0x17C34": "Mountain Access", } diff --git a/worlds/witness/regions.py b/worlds/witness/regions.py index a7d549e704..b5ee31b8ca 100644 --- a/worlds/witness/regions.py +++ b/worlds/witness/regions.py @@ -33,6 +33,10 @@ class WitnessRegions: source_region = world.get_region(source, player) target_region = world.get_region(target, player) + #print(source_region) + #print(target_region) + #print("---") + connection = Entrance( player, source + " to " + target + " via " + str(panel_hex_to_solve_set), @@ -76,10 +80,17 @@ class WitnessRegions: for connection in player_logic.CONNECTIONS_BY_REGION_NAME[region_name]: if connection[0] == "Entry": continue - self.connect(world, player, region_name, - connection[0], player_logic, connection[1]) - self.connect(world, player, connection[0], - region_name, player_logic, connection[1]) + + if connection[1] == frozenset({frozenset(["TrueOneWay"])}): + self.connect(world, player, region_name, connection[0], player_logic, frozenset({frozenset()})) + continue + + for subset in connection[1]: + if all({panel in player_logic.DOOR_ITEMS_BY_ID for panel in subset}): + if all({StaticWitnessLogic.CHECKS_BY_HEX[panel]["id"] is None for panel in subset}): + self.connect(world, player, connection[0], region_name, player_logic, frozenset({subset})) + + self.connect(world, player, region_name, connection[0], player_logic, connection[1]) world.get_entrance("The Splashscreen?", player).connect( world.get_region('First Hallway', player) diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index 1f13074a88..2b9888b361 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -22,6 +22,21 @@ class WitnessLogic(LogicMixin): def _witness_has_lasers(self, world, player: int, amount: int) -> bool: lasers = 0 + if is_option_enabled(world, player, "shuffle_lasers"): + lasers += int(self.has("Symmetry Laser", player)) + lasers += int(self.has("Desert Laser", player) + and self.has("Desert Laser Redirection", player)) + lasers += int(self.has("Town Laser", player)) + lasers += int(self.has("Monastery Laser", player)) + lasers += int(self.has("Keep Laser", player)) + lasers += int(self.has("Quarry Laser", player)) + lasers += int(self.has("Treehouse Laser", player)) + lasers += int(self.has("Jungle Laser", player)) + lasers += int(self.has("Bunker Laser", player)) + lasers += int(self.has("Swamp Laser", player)) + lasers += int(self.has("Shadows Laser", player)) + return lasers >= amount + lasers += int(self.has("Symmetry Laser Activation", player)) lasers += int(self.has("Desert Laser Activation", player) and self.has("Desert Laser Redirection", player)) @@ -48,11 +63,8 @@ class WitnessLogic(LogicMixin): if (check_name + " Solved" in locat.EVENT_LOCATION_TABLE and not self.has(player_logic.EVENT_ITEM_PAIRS[check_name + " Solved"], player)): return False - if panel not in player_logic.ORIGINAL_EVENT_PANELS and not self.can_reach(check_name, "Location", player): - return False - if (panel in player_logic.ORIGINAL_EVENT_PANELS - and check_name + " Solved" not in locat.EVENT_LOCATION_TABLE - and not self._witness_safe_manual_panel_check(panel, world, player, player_logic, locat)): + if (check_name + " Solved" not in locat.EVENT_LOCATION_TABLE + and not self._witness_meets_item_requirements(panel, world, player, player_logic, locat)): return False return True @@ -79,8 +91,10 @@ class WitnessLogic(LogicMixin): if not self._witness_has_lasers(world, player, get_option_value(world, player, "challenge_lasers")): valid_option = False break - elif item in player_logic.ORIGINAL_EVENT_PANELS: - valid_option = self._witness_can_solve_panel(item, world, player, player_logic, locat) + elif item in player_logic.EVENT_PANELS: + if not self._witness_can_solve_panel(item, world, player, player_logic, locat): + valid_option = False + break elif not self.has(item, player): valid_option = False break @@ -90,24 +104,6 @@ class WitnessLogic(LogicMixin): return False - def _witness_safe_manual_panel_check(self, panel, world, player, player_logic: WitnessPlayerLogic, locat): - """ - nested can_reach can cause problems, but only if the region being - checked is neither of the two original regions from the first - can_reach. - A nested can_reach is okay here because the only panels this - function is called on are panels that exist on either side of all - connections they are required for. - The spoiler log looks so much nicer this way, - it gets rid of a bunch of event items, only leaving a couple. :) - """ - region = StaticWitnessLogic.CHECKS_BY_HEX[panel]["region"]["name"] - - return ( - self._witness_meets_item_requirements(panel, world, player, player_logic, locat) - and self.can_reach(region, "Region", player) - ) - def _witness_can_solve_panels(self, panel_hex_to_solve_set, world, player, player_logic: WitnessPlayerLogic, locat): """ Checks whether a set of panels can be solved. diff --git a/worlds/witness/Disable_Unrandomized.txt b/worlds/witness/settings/Disable_Unrandomized.txt similarity index 93% rename from worlds/witness/Disable_Unrandomized.txt rename to worlds/witness/settings/Disable_Unrandomized.txt index cad3804f34..43c2596405 100644 --- a/worlds/witness/Disable_Unrandomized.txt +++ b/worlds/witness/settings/Disable_Unrandomized.txt @@ -1,18 +1,16 @@ Event Items: -Shadows Laser Activation - 0x00021,0x17D28,0x17C71 -Keep Laser Activation - 0x03317 -Bunker Laser Activation - 0x00061,0x17D01,0x17C42 -Monastery Laser Activation - 0x00A5B,0x17CE7,0x17FA9,0x17CA4 Town Tower 4th Door Opens - 0x17CFB,0x3C12B,0x00B8D,0x17CF7 +Monastery Laser Activation - 0x00A5B,0x17CE7,0x17FA9,0x17CA4 +Bunker Laser Activation - 0x00061,0x17D01,0x17C42 +Shadows Laser Activation - 0x00021,0x17D28,0x17C71 Requirement Changes: -0x17CA4 - True - True -0x28B39 - 0x2896A - Reflection +0x17C65 - 0x00A5B | 0x17CE7 | 0x17FA9 | 0x17CA4 +0x0C2B2 - 0x00061 | 0x17D01 | 0x17C42 +0x181B3 - 0x00021 | 0x17D28 | 0x17C71 +0x28B39 - True - Reflection 0x17CAB - True - True -Region Changes: -Quarry (Quarry) - Outside Quarry - 0x17C09 - Quarry Mill - 0x275ED - Quarry Mill - 0x17CAC - Disabled Locations: 0x03505 (Tutorial Gate Close) 0x0C335 (Tutorial Pillar) @@ -84,7 +82,8 @@ Disabled Locations: 0x002C7 (Waves 7) 0x15ADD (River Rhombic Avoid Vault) 0x03702 (River Vault Box) -0x17C2E (Door to Bunker) - True - Squares & Black/White Squares +0x17CAA (Rhombic Avoid to Monastery Garden) +0x17C2E (Door to Bunker) 0x09F7D (Bunker Drawn Squares 1) 0x09FDC (Bunker Drawn Squares 2) 0x09FF7 (Bunker Drawn Squares 3) diff --git a/worlds/witness/settings/Door_Panel_Shuffle.txt b/worlds/witness/settings/Door_Panel_Shuffle.txt new file mode 100644 index 0000000000..d6982f52e3 --- /dev/null +++ b/worlds/witness/settings/Door_Panel_Shuffle.txt @@ -0,0 +1,31 @@ +Items: +Glass Factory Entry Door (Panel) +Door to Symmetry Island Lower (Panel) +Door to Symmetry Island Upper (Panel) +Door to Desert Flood Light Room (Panel) +Desert Flood Room Flood Controls (Panel) +Quarry Door to Mill (Panel) +Quarry Mill Ramp Controls (Panel) +Quarry Mill Elevator Controls (Panel) +Quarry Boathouse Ramp Height Control (Panel) +Quarry Boathouse Ramp Horizontal Control (Panel) +Shadows Door Timer (Panel) +Monastery Entry Door Left (Panel) +Monastery Entry Door Right (Panel) +Town Door to RGB House (Panel) +Town Door to Church (Panel) +Town Maze Panel (Drop-Down Staircase) (Panel) +Windmill Door (Panel) +Treehouse First & Second Doors (Panel) +Treehouse Third Door (Panel) +Treehouse Laser House Door Timer (Panel) +Treehouse Shortcut Drop-Down Bridge (Panel) +Jungle Popup Wall (Panel) +Bunker Entry Door (Panel) +Inside Bunker Door to Bunker Proper (Panel) +Bunker Elevator Control (Panel) +Swamp Entry Door (Panel) +Swamp Sliding Bridge (Panel) +Swamp Rotating Bridge (Panel) +Swamp Maze Control (Panel) +Boat diff --git a/worlds/witness/settings/Doors_Complex.txt b/worlds/witness/settings/Doors_Complex.txt new file mode 100644 index 0000000000..c62562e32a --- /dev/null +++ b/worlds/witness/settings/Doors_Complex.txt @@ -0,0 +1,201 @@ +Items: +Outside Tutorial Optional Door +Outside Tutorial Outpost Entry Door +Outside Tutorial Outpost Exit Door +Glass Factory Entry Door +Glass Factory Back Wall +Symmetry Island Lower Door +Symmetry Island Upper Door +Orchard Middle Gate +Orchard Final Gate +Desert Door to Flood Light Room +Desert Door to Pond Room +Desert Door to Water Levels Room +Desert Door to Elevator Room +Quarry Main Entry 1 +Quarry Main Entry 2 +Quarry Door to Mill +Quarry Mill Side Door +Quarry Mill Rooftop Shortcut +Quarry Mill Stairs +Quarry Boathouse Boat Staircase +Quarry Boathouse First Barrier +Quarry Boathouse Shortcut +Shadows Timed Door +Shadows Laser Room Right Door +Shadows Laser Room Left Door +Shadows Barrier to Quarry +Shadows Barrier to Ledge +Keep Hedge Maze 1 Exit Door +Keep Pressure Plates 1 Exit Door +Keep Hedge Maze 2 Shortcut +Keep Hedge Maze 2 Exit Door +Keep Hedge Maze 3 Shortcut +Keep Hedge Maze 3 Exit Door +Keep Hedge Maze 4 Shortcut +Keep Hedge Maze 4 Exit Door +Keep Pressure Plates 2 Exit Door +Keep Pressure Plates 3 Exit Door +Keep Pressure Plates 4 Exit Door +Keep Shortcut to Shadows +Keep Tower Shortcut +Monastery Shortcut +Monastery Inner Door +Monastery Outer Door +Monastery Door to Garden +Town Cargo Box Door +Town Wooden Roof Staircase +Town Tinted Door to RGB House +Town Door to Church +Town Maze Staircase +Town Windmill Door +Town RGB House Staircase +Town Tower Blue Panels Door +Town Tower Lattice Door +Town Tower Environmental Set Door +Town Tower Wooden Roof Set Door +Theater Entry Door +Theater Exit Door Left +Theater Exit Door Right +Jungle Bamboo Shortcut to River +Jungle Popup Wall +River Shortcut to Monastery Garden +Bunker Bunker Entry Door +Bunker Tinted Glass Door +Bunker Door to Ultraviolet Room +Bunker Door to Elevator +Swamp Entry Door +Swamp Door to Broken Shapers +Swamp Platform Shortcut Door +Swamp Cyan Water Pump +Swamp Door to Rotated Shapers +Swamp Red Water Pump +Swamp Red Underwater Exit +Swamp Blue Water Pump +Swamp Purple Water Pump +Swamp Near Laser Shortcut +Treehouse First Door +Treehouse Second Door +Treehouse Beyond Yellow Bridge Door +Treehouse Drawbridge +Treehouse Timed Door to Laser House +Inside Mountain First Layer Exit Door +Inside Mountain Second Layer Staircase Near +Inside Mountain Second Layer Exit Door +Inside Mountain Second Layer Staircase Far +Inside Mountain Giant Puzzle Exit Door +Inside Mountain Door to Final Room +Inside Mountain Bottom Layer Rock +Inside Mountain Door to Secret Area +Caves Pillar Door +Caves Mountain Shortcut +Caves Swamp Shortcut +Challenge Entry Door +Challenge Door to Theater Walkway +Theater Walkway Door to Windmill Interior +Theater Walkway Door to Desert Elevator Room +Theater Walkway Door to Town + +Added Locations: +Outside Tutorial Door to Outpost Panel +Outside Tutorial Exit Door from Outpost Panel +Glass Factory Entry Door Panel +Glass Factory Vertical Symmetry 5 +Symmetry Island Door to Symmetry Island Lower Panel +Symmetry Island Door to Symmetry Island Upper Panel +Orchard Apple Tree 3 +Orchard Apple Tree 5 +Desert Door to Desert Flood Light Room Panel +Desert Artificial Light Reflection 3 +Desert Door to Water Levels Room Panel +Desert Flood Reflection 6 +Quarry Door to Quarry 1 Panel +Quarry Door to Quarry 2 Panel +Quarry Door to Mill Right +Quarry Door to Mill Left +Quarry Mill Ground Floor Shortcut Door Panel +Quarry Mill Door to Outside Quarry Stairs Panel +Quarry Mill Stair Control +Quarry Boathouse Shortcut Door Panel +Shadows Door Timer Inside +Shadows Door Timer Outside +Shadows Environmental Avoid 8 +Shadows Follow 5 +Shadows Lower Avoid 3 +Shadows Lower Avoid 5 +Keep Hedge Maze 1 +Keep Pressure Plates 1 +Keep Hedge Maze 2 +Keep Hedge Maze 3 +Keep Hedge Maze 4 +Keep Pressure Plates 2 +Keep Pressure Plates 3 +Keep Pressure Plates 4 +Keep Shortcut to Shadows Panel +Keep Tower Shortcut to Keep Panel +Monastery Shortcut Door Panel +Monastery Door Open Left +Monastery Door Open Right +Monastery Rhombic Avoid 3 +Town Cargo Box Panel +Town Full Dot Grid Shapers 5 +Town Tinted Door Panel +Town Door to Church Stars Panel +Town Maze Stair Control +Town Windmill Door Panel +Town Sound Room Left +Town Sound Room Right +Town Symmetry Squares 5 + Dots +Town Church Lattice +Town Hexagonal Reflection +Town Shapers & Dots & Eraser +Windmill Door to Front of Theater Panel +Theater Door to Cargo Box Left Panel +Theater Door to Cargo Box Right Panel +Jungle Shortcut to River Panel +Jungle Popup Wall Control +River Rhombic Avoid to Monastery Garden +Bunker Bunker Entry Panel +Bunker Door to Bunker Proper Panel +Bunker Drawn Squares through Tinted Glass 3 +Bunker Drop-Down Door Squares 2 +Swamp Entry Panel +Swamp Platform Shapers 4 +Swamp Platform Shortcut Right Panel +Swamp Blue Underwater Negative Shapers 5 +Swamp Broken Shapers 4 +Swamp Cyan Underwater Negative Shapers 5 +Swamp Red Underwater Negative Shapers 4 +Swamp More Rotated Shapers 4 +Swamp More Rotated Shapers 4 +Swamp Near Laser Shortcut Right Panel +Treehouse First Door Panel +Treehouse Second Door Panel +Treehouse Beyond Yellow Bridge Door Panel +Treehouse Bridge Control +Treehouse Left Orange Bridge 15 +Treehouse Right Orange Bridge 12 +Treehouse Laser House Door Timer Outside Control +Treehouse Laser House Door Timer Inside Control +Inside Mountain Moving Background 7 +Inside Mountain Obscured Vision 5 +Inside Mountain Physically Obstructed 3 +Inside Mountain Angled Inside Trash 2 +Inside Mountain Color Cycle 5 +Inside Mountain Light Bridge Controller 2 +Inside Mountain Light Bridge Controller 3 +Inside Mountain Same Solution 6 +Inside Mountain Giant Puzzle +Inside Mountain Door to Final Room Left +Inside Mountain Door to Final Room Right +Inside Mountain Bottom Layer Discard +Inside Mountain Rock Control +Inside Mountain Secret Area Entry Panel +Inside Mountain Caves Lone Pillar +Inside Mountain Caves Shortcut to Mountain Panel +Inside Mountain Caves Shortcut to Swamp Panel +Inside Mountain Caves Challenge Entry Panel +Challenge Door to Theater Walkway Panel +Theater Walkway Theater Shortcut Panel +Theater Walkway Desert Shortcut Panel +Theater Walkway Town Shortcut Panel \ No newline at end of file diff --git a/worlds/witness/settings/Doors_Max.txt b/worlds/witness/settings/Doors_Max.txt new file mode 100644 index 0000000000..ec0a56a597 --- /dev/null +++ b/worlds/witness/settings/Doors_Max.txt @@ -0,0 +1,212 @@ +Items: +Outside Tutorial Optional Door +Outside Tutorial Outpost Entry Door +Outside Tutorial Outpost Exit Door +Glass Factory Entry Door +Glass Factory Back Wall +Symmetry Island Lower Door +Symmetry Island Upper Door +Orchard Middle Gate +Orchard Final Gate +Desert Door to Flood Light Room +Desert Door to Pond Room +Desert Door to Water Levels Room +Desert Door to Elevator Room +Quarry Main Entry 1 +Quarry Main Entry 2 +Quarry Door to Mill +Quarry Mill Side Door +Quarry Mill Rooftop Shortcut +Quarry Mill Stairs +Quarry Boathouse Boat Staircase +Quarry Boathouse First Barrier +Quarry Boathouse Shortcut +Shadows Timed Door +Shadows Laser Room Right Door +Shadows Laser Room Left Door +Shadows Barrier to Quarry +Shadows Barrier to Ledge +Keep Hedge Maze 1 Exit Door +Keep Pressure Plates 1 Exit Door +Keep Hedge Maze 2 Shortcut +Keep Hedge Maze 2 Exit Door +Keep Hedge Maze 3 Shortcut +Keep Hedge Maze 3 Exit Door +Keep Hedge Maze 4 Shortcut +Keep Hedge Maze 4 Exit Door +Keep Pressure Plates 2 Exit Door +Keep Pressure Plates 3 Exit Door +Keep Pressure Plates 4 Exit Door +Keep Shortcut to Shadows +Keep Tower Shortcut +Monastery Shortcut +Monastery Inner Door +Monastery Outer Door +Monastery Door to Garden +Town Cargo Box Door +Town Wooden Roof Staircase +Town Tinted Door to RGB House +Town Door to Church +Town Maze Staircase +Town Windmill Door +Town RGB House Staircase +Town Tower Blue Panels Door +Town Tower Lattice Door +Town Tower Environmental Set Door +Town Tower Wooden Roof Set Door +Theater Entry Door +Theater Exit Door Left +Theater Exit Door Right +Jungle Bamboo Shortcut to River +Jungle Popup Wall +River Shortcut to Monastery Garden +Bunker Bunker Entry Door +Bunker Tinted Glass Door +Bunker Door to Ultraviolet Room +Bunker Door to Elevator +Swamp Entry Door +Swamp Door to Broken Shapers +Swamp Platform Shortcut Door +Swamp Cyan Water Pump +Swamp Door to Rotated Shapers +Swamp Red Water Pump +Swamp Red Underwater Exit +Swamp Blue Water Pump +Swamp Purple Water Pump +Swamp Near Laser Shortcut +Treehouse First Door +Treehouse Second Door +Treehouse Beyond Yellow Bridge Door +Treehouse Drawbridge +Treehouse Timed Door to Laser House +Inside Mountain First Layer Exit Door +Inside Mountain Second Layer Staircase Near +Inside Mountain Second Layer Exit Door +Inside Mountain Second Layer Staircase Far +Inside Mountain Giant Puzzle Exit Door +Inside Mountain Door to Final Room +Inside Mountain Bottom Layer Rock +Inside Mountain Door to Secret Area +Caves Pillar Door +Caves Mountain Shortcut +Caves Swamp Shortcut +Challenge Entry Door +Challenge Door to Theater Walkway +Theater Walkway Door to Windmill Interior +Theater Walkway Door to Desert Elevator Room +Theater Walkway Door to Town + +Desert Flood Room Flood Controls (Panel) +Quarry Mill Ramp Controls (Panel) +Quarry Mill Elevator Controls (Panel) +Quarry Boathouse Ramp Height Control (Panel) +Quarry Boathouse Ramp Horizontal Control (Panel) +Bunker Elevator Control (Panel) +Swamp Sliding Bridge (Panel) +Swamp Rotating Bridge (Panel) +Swamp Maze Control (Panel) +Boat + +Added Locations: +Outside Tutorial Door to Outpost Panel +Outside Tutorial Exit Door from Outpost Panel +Glass Factory Entry Door Panel +Glass Factory Vertical Symmetry 5 +Symmetry Island Door to Symmetry Island Lower Panel +Symmetry Island Door to Symmetry Island Upper Panel +Orchard Apple Tree 3 +Orchard Apple Tree 5 +Desert Door to Desert Flood Light Room Panel +Desert Artificial Light Reflection 3 +Desert Door to Water Levels Room Panel +Desert Flood Reflection 6 +Quarry Door to Quarry 1 Panel +Quarry Door to Quarry 2 Panel +Quarry Door to Mill Right +Quarry Door to Mill Left +Quarry Mill Ground Floor Shortcut Door Panel +Quarry Mill Door to Outside Quarry Stairs Panel +Quarry Mill Stair Control +Quarry Boathouse Shortcut Door Panel +Shadows Door Timer Inside +Shadows Door Timer Outside +Shadows Environmental Avoid 8 +Shadows Follow 5 +Shadows Lower Avoid 3 +Shadows Lower Avoid 5 +Keep Hedge Maze 1 +Keep Pressure Plates 1 +Keep Hedge Maze 2 +Keep Hedge Maze 3 +Keep Hedge Maze 4 +Keep Pressure Plates 2 +Keep Pressure Plates 3 +Keep Pressure Plates 4 +Keep Shortcut to Shadows Panel +Keep Tower Shortcut to Keep Panel +Monastery Shortcut Door Panel +Monastery Door Open Left +Monastery Door Open Right +Monastery Rhombic Avoid 3 +Town Cargo Box Panel +Town Full Dot Grid Shapers 5 +Town Tinted Door Panel +Town Door to Church Stars Panel +Town Maze Stair Control +Town Windmill Door Panel +Town Sound Room Left +Town Sound Room Right +Town Symmetry Squares 5 + Dots +Town Church Lattice +Town Hexagonal Reflection +Town Shapers & Dots & Eraser +Windmill Door to Front of Theater Panel +Theater Door to Cargo Box Left Panel +Theater Door to Cargo Box Right Panel +Jungle Shortcut to River Panel +Jungle Popup Wall Control +River Rhombic Avoid to Monastery Garden +Bunker Bunker Entry Panel +Bunker Door to Bunker Proper Panel +Bunker Drawn Squares through Tinted Glass 3 +Bunker Drop-Down Door Squares 2 +Swamp Entry Panel +Swamp Platform Shapers 4 +Swamp Platform Shortcut Right Panel +Swamp Blue Underwater Negative Shapers 5 +Swamp Broken Shapers 4 +Swamp Cyan Underwater Negative Shapers 5 +Swamp Red Underwater Negative Shapers 4 +Swamp More Rotated Shapers 4 +Swamp More Rotated Shapers 4 +Swamp Near Laser Shortcut Right Panel +Treehouse First Door Panel +Treehouse Second Door Panel +Treehouse Beyond Yellow Bridge Door Panel +Treehouse Bridge Control +Treehouse Left Orange Bridge 15 +Treehouse Right Orange Bridge 12 +Treehouse Laser House Door Timer Outside Control +Treehouse Laser House Door Timer Inside Control +Inside Mountain Moving Background 7 +Inside Mountain Obscured Vision 5 +Inside Mountain Physically Obstructed 3 +Inside Mountain Angled Inside Trash 2 +Inside Mountain Color Cycle 5 +Inside Mountain Light Bridge Controller 2 +Inside Mountain Light Bridge Controller 3 +Inside Mountain Same Solution 6 +Inside Mountain Giant Puzzle +Inside Mountain Door to Final Room Left +Inside Mountain Door to Final Room Right +Inside Mountain Bottom Layer Discard +Inside Mountain Rock Control +Inside Mountain Secret Area Entry Panel +Inside Mountain Caves Lone Pillar +Inside Mountain Caves Shortcut to Mountain Panel +Inside Mountain Caves Shortcut to Swamp Panel +Inside Mountain Caves Challenge Entry Panel +Challenge Door to Theater Walkway Panel +Theater Walkway Theater Shortcut Panel +Theater Walkway Desert Shortcut Panel +Theater Walkway Town Shortcut Panel \ No newline at end of file diff --git a/worlds/witness/settings/Doors_Simple.txt b/worlds/witness/settings/Doors_Simple.txt new file mode 100644 index 0000000000..1335456d95 --- /dev/null +++ b/worlds/witness/settings/Doors_Simple.txt @@ -0,0 +1,146 @@ +Items: +Glass Factory Back Wall +Quarry Boathouse Boat Staircase +Outside Tutorial Outpost Doors +Glass Factory Entry Door +Symmetry Island Doors +Orchard Gates +Desert Doors +Quarry Main Entry +Quarry Door to Mill +Quarry Mill Shortcuts +Quarry Boathouse Barriers +Shadows Timed Door +Shadows Laser Room Door +Shadows Barriers +Keep Hedge Maze Doors +Keep Pressure Plates Doors +Keep Shortcuts +Monastery Entry Door +Monastery Shortcuts +Town Doors +Town Tower Doors +Theater Entry Door +Theater Exit Door +Jungle & River Shortcuts +Jungle Popup Wall +Bunker Doors +Swamp Doors +Swamp Near Laser Shortcut +Swamp Water Pumps +Treehouse Entry Doors +Treehouse Drawbridge +Treehouse Timed Door to Laser House +Inside Mountain First Layer Exit Door +Inside Mountain Second Layer Stairs & Doors +Inside Mountain Giant Puzzle Exit Door +Inside Mountain Door to Final Room +Inside Mountain Bottom Layer Doors to Caves +Caves Doors to Challenge +Caves Exits to Main Island +Challenge Door to Theater Walkway +Theater Walkway Doors + +Added Locations: +Outside Tutorial Door to Outpost Panel +Outside Tutorial Exit Door from Outpost Panel +Glass Factory Entry Door Panel +Glass Factory Vertical Symmetry 5 +Symmetry Island Door to Symmetry Island Lower Panel +Symmetry Island Door to Symmetry Island Upper Panel +Orchard Apple Tree 3 +Orchard Apple Tree 5 +Desert Door to Desert Flood Light Room Panel +Desert Artificial Light Reflection 3 +Desert Door to Water Levels Room Panel +Desert Flood Reflection 6 +Quarry Door to Quarry 1 Panel +Quarry Door to Quarry 2 Panel +Quarry Door to Mill Right +Quarry Door to Mill Left +Quarry Mill Ground Floor Shortcut Door Panel +Quarry Mill Door to Outside Quarry Stairs Panel +Quarry Mill Stair Control +Quarry Boathouse Shortcut Door Panel +Shadows Door Timer Inside +Shadows Door Timer Outside +Shadows Environmental Avoid 8 +Shadows Follow 5 +Shadows Lower Avoid 3 +Shadows Lower Avoid 5 +Keep Hedge Maze 1 +Keep Pressure Plates 1 +Keep Hedge Maze 2 +Keep Hedge Maze 3 +Keep Hedge Maze 4 +Keep Pressure Plates 2 +Keep Pressure Plates 3 +Keep Pressure Plates 4 +Keep Shortcut to Shadows Panel +Keep Tower Shortcut to Keep Panel +Monastery Shortcut Door Panel +Monastery Door Open Left +Monastery Door Open Right +Monastery Rhombic Avoid 3 +Town Cargo Box Panel +Town Full Dot Grid Shapers 5 +Town Tinted Door Panel +Town Door to Church Stars Panel +Town Maze Stair Control +Town Windmill Door Panel +Town Sound Room Left +Town Sound Room Right +Town Symmetry Squares 5 + Dots +Town Church Lattice +Town Hexagonal Reflection +Town Shapers & Dots & Eraser +Windmill Door to Front of Theater Panel +Theater Door to Cargo Box Left Panel +Theater Door to Cargo Box Right Panel +Jungle Shortcut to River Panel +Jungle Popup Wall Control +River Rhombic Avoid to Monastery Garden +Bunker Bunker Entry Panel +Bunker Door to Bunker Proper Panel +Bunker Drawn Squares through Tinted Glass 3 +Bunker Drop-Down Door Squares 2 +Swamp Entry Panel +Swamp Platform Shapers 4 +Swamp Platform Shortcut Right Panel +Swamp Blue Underwater Negative Shapers 5 +Swamp Broken Shapers 4 +Swamp Cyan Underwater Negative Shapers 5 +Swamp Red Underwater Negative Shapers 4 +Swamp More Rotated Shapers 4 +Swamp More Rotated Shapers 4 +Swamp Near Laser Shortcut Right Panel +Treehouse First Door Panel +Treehouse Second Door Panel +Treehouse Beyond Yellow Bridge Door Panel +Treehouse Bridge Control +Treehouse Left Orange Bridge 15 +Treehouse Right Orange Bridge 12 +Treehouse Laser House Door Timer Outside Control +Treehouse Laser House Door Timer Inside Control +Inside Mountain Moving Background 7 +Inside Mountain Obscured Vision 5 +Inside Mountain Physically Obstructed 3 +Inside Mountain Angled Inside Trash 2 +Inside Mountain Color Cycle 5 +Inside Mountain Light Bridge Controller 2 +Inside Mountain Light Bridge Controller 3 +Inside Mountain Same Solution 6 +Inside Mountain Giant Puzzle +Inside Mountain Door to Final Room Left +Inside Mountain Door to Final Room Right +Inside Mountain Bottom Layer Discard +Inside Mountain Rock Control +Inside Mountain Secret Area Entry Panel +Inside Mountain Caves Lone Pillar +Inside Mountain Caves Shortcut to Mountain Panel +Inside Mountain Caves Shortcut to Swamp Panel +Inside Mountain Caves Challenge Entry Panel +Challenge Door to Theater Walkway Panel +Theater Walkway Theater Shortcut Panel +Theater Walkway Desert Shortcut Panel +Theater Walkway Town Shortcut Panel \ No newline at end of file diff --git a/worlds/witness/settings/Early_UTM.txt b/worlds/witness/settings/Early_UTM.txt new file mode 100644 index 0000000000..893f29d8bb --- /dev/null +++ b/worlds/witness/settings/Early_UTM.txt @@ -0,0 +1,9 @@ +Items: +Caves Exits to Main Island + +Starting Inventory: +Caves Exits to Main Island + +Remove Items: +Caves Mountain Shortcut +Caves Swamp Shortcut \ No newline at end of file diff --git a/worlds/witness/settings/Laser_Shuffle.txt b/worlds/witness/settings/Laser_Shuffle.txt new file mode 100644 index 0000000000..668a13f94a --- /dev/null +++ b/worlds/witness/settings/Laser_Shuffle.txt @@ -0,0 +1,12 @@ +Items: +Symmetry Laser +Desert Laser +Keep Laser +Shadows Laser +Quarry Laser +Town Laser +Swamp Laser +Jungle Laser +Bunker Laser +Monastery Laser +Treehouse Laser \ No newline at end of file diff --git a/worlds/witness/settings/Symbol_Shuffle.txt b/worlds/witness/settings/Symbol_Shuffle.txt new file mode 100644 index 0000000000..d03391f5c5 --- /dev/null +++ b/worlds/witness/settings/Symbol_Shuffle.txt @@ -0,0 +1,14 @@ +Items: +Dots +Colored Dots +Sound Dots +Symmetry +Triangles +Eraser +Shapers +Rotated Shapers +Negative Shapers +Stars +Stars + Same Colored Symbol +Black/White Squares +Colored Squares \ No newline at end of file diff --git a/worlds/witness/static_logic.py b/worlds/witness/static_logic.py index 5f1d77d314..646957c462 100644 --- a/worlds/witness/static_logic.py +++ b/worlds/witness/static_logic.py @@ -5,12 +5,11 @@ from .utils import define_new_region, parse_lambda class StaticWitnessLogic: ALL_SYMBOL_ITEMS = set() + ALL_DOOR_ITEMS = set() + ALL_DOOR_ITEMS_AS_DICT = dict() ALL_USEFULS = set() ALL_TRAPS = set() ALL_BOOSTS = set() - ALL_DOOR_ITEM_IDS_BY_HEX = dict() - DOOR_NAMES_BY_HEX = dict() - ALL_DOOR_ITEMS = set() CONNECTIONS_TO_SEVER_BY_DOOR_HEX = dict() EVENT_PANELS_FROM_REGIONS = set() @@ -47,35 +46,23 @@ class StaticWitnessLogic: if line == "Usefuls:": current_set = self.ALL_USEFULS continue + if line == "Doors:": + current_set = self.ALL_DOOR_ITEMS + continue if line == "": continue line_split = line.split(" - ") - if current_set is not self.ALL_USEFULS: - current_set.add((line_split[1], int(line_split[0]))) - else: + if current_set is self.ALL_USEFULS: current_set.add((line_split[1], int(line_split[0]), line_split[2] == "True")) + elif current_set is self.ALL_DOOR_ITEMS: + new_door = (line_split[1], int(line_split[0]), frozenset(line_split[2].split(","))) + current_set.add(new_door) + self.ALL_DOOR_ITEMS_AS_DICT[line_split[1]] = new_door + else: + current_set.add((line_split[1], int(line_split[0]))) - path = os.path.join(os.path.dirname(__file__), "Door_Shuffle.txt") - with open(path, "r", encoding="utf-8") as file: - for line in file.readlines(): - line = line.strip() - - line_split = line.split(" - ") - - hex_set_split = line_split[1].split(",") - - sever_list = line_split[2].split(",") - sever_set = {sever_panel for sever_panel in sever_list if sever_panel != "None"} - - for door_hex in hex_set_split: - self.ALL_DOOR_ITEM_IDS_BY_HEX[door_hex] = int(line_split[0]) - self.CONNECTIONS_TO_SEVER_BY_DOOR_HEX[door_hex] = sever_set - - if len(line_split) > 3: - self.DOOR_NAMES_BY_HEX[door_hex] = line_split[3] - def read_logic_file(self): """ Reads the logic file and does the initial population of data structures @@ -84,10 +71,7 @@ class StaticWitnessLogic: with open(path, "r", encoding="utf-8") as file: current_region = dict() - discard_ids = 0 - normal_panel_ids = 0 - vault_ids = 0 - laser_ids = 0 + counter = 0 for line in file.readlines(): line = line.strip() @@ -95,7 +79,7 @@ class StaticWitnessLogic: if line == "": continue - if line[0] != "0": + if line[-1] == ":": new_region_and_connections = define_new_region(line) current_region = new_region_and_connections[0] region_name = current_region["name"] @@ -105,12 +89,33 @@ class StaticWitnessLogic: line_split = line.split(" - ") + location_id = line_split.pop(0) + check_name_full = line_split.pop(0) check_hex = check_name_full[0:7] check_name = check_name_full[9:-1] required_panel_lambda = line_split.pop(0) + + if location_id == "Door" or location_id == "Laser": + self.CHECKS_BY_HEX[check_hex] = { + "checkName": current_region["shortName"] + " " + check_name, + "checkHex": check_hex, + "region": current_region, + "id": None, + "panelType": location_id + } + + self.CHECKS_BY_NAME[self.CHECKS_BY_HEX[check_hex]["checkName"]] = self.CHECKS_BY_HEX[check_hex] + + self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[check_hex] = { + "panels": parse_lambda(required_panel_lambda) + } + + current_region["panels"].add(check_hex) + continue + required_item_lambda = line_split.pop(0) laser_names = { @@ -123,53 +128,14 @@ class StaticWitnessLogic: if "Discard" in check_name: location_type = "Discard" - location_id = discard_ids - discard_ids += 1 elif is_vault_or_video or check_name == "Tutorial Gate Close": location_type = "Vault" - location_id = vault_ids - vault_ids += 1 elif check_name in laser_names: location_type = "Laser" - location_id = laser_ids - laser_ids += 1 else: location_type = "General" - if check_hex == "0x012D7": # Compatibility - normal_panel_ids += 1 - - if check_hex == "0x17E07": # Compatibility - location_id = 112 - - elif check_hex == "0xFFF00": - location_id = 800 - - else: - location_id = normal_panel_ids - normal_panel_ids += 1 - required_items = parse_lambda(required_item_lambda) - items_actually_in_the_game = {item[0] for item in self.ALL_SYMBOL_ITEMS} - required_items = set( - subset.intersection(items_actually_in_the_game) - for subset in required_items - ) - - doors_in_the_game = self.ALL_DOOR_ITEM_IDS_BY_HEX.keys() - if check_hex in doors_in_the_game: - door_name = current_region["shortName"] + " " + check_name + " Power On" - if check_hex in self.DOOR_NAMES_BY_HEX.keys(): - door_name = self.DOOR_NAMES_BY_HEX[check_hex] - - required_items = set( - subset.union(frozenset({door_name})) - for subset in required_items - ) - - self.ALL_DOOR_ITEMS.add( - (door_name, self.ALL_DOOR_ITEM_IDS_BY_HEX[check_hex]) - ) required_items = frozenset(required_items) @@ -182,7 +148,7 @@ class StaticWitnessLogic: "checkName": current_region["shortName"] + " " + check_name, "checkHex": check_hex, "region": current_region, - "idOffset": location_id, + "id": int(location_id), "panelType": location_type } diff --git a/worlds/witness/utils.py b/worlds/witness/utils.py index df4b43717e..809b2b1c3d 100644 --- a/worlds/witness/utils.py +++ b/worlds/witness/utils.py @@ -98,9 +98,39 @@ def get_adjustment_file(adjustment_file): @cache_argsless def get_disable_unrandomized_list(): - return get_adjustment_file("Disable_Unrandomized.txt") + return get_adjustment_file("settings/Disable_Unrandomized.txt") @cache_argsless def get_early_utm_list(): - return get_adjustment_file("Early_UTM.txt") \ No newline at end of file + return get_adjustment_file("settings/Early_UTM.txt") + + +@cache_argsless +def get_symbol_shuffle_list(): + return get_adjustment_file("settings/Symbol_Shuffle.txt") + + +@cache_argsless +def get_door_panel_shuffle_list(): + return get_adjustment_file("settings/Door_Panel_Shuffle.txt") + + +@cache_argsless +def get_doors_simple_list(): + return get_adjustment_file("settings/Doors_Simple.txt") + + +@cache_argsless +def get_doors_complex_list(): + return get_adjustment_file("settings/Doors_Complex.txt") + + +@cache_argsless +def get_doors_max_list(): + return get_adjustment_file("settings/Doors_Max.txt") + + +@cache_argsless +def get_laser_shuffle(): + return get_adjustment_file("settings/Laser_Shuffle.txt")