diff --git a/.gitignore b/.gitignore index 8a7246210f..0caf00a978 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,10 @@ *.z64 *.n64 *.nes +*.sms +*.gb +*.gbc +*.gba *.wixobj *.lck *.db3 @@ -125,7 +129,7 @@ ipython_config.py # Environments .env -.venv +.venv* env/ venv/ ENV/ diff --git a/BaseClasses.py b/BaseClasses.py index df8ac02071..ce2fc9e3c5 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1,4 +1,5 @@ from __future__ import annotations +from argparse import Namespace import copy from enum import unique, IntEnum, IntFlag @@ -40,6 +41,7 @@ class MultiWorld(): plando_connections: List worlds: Dict[int, auto_world] groups: Dict[int, Group] + regions: List[Region] itempool: List[Item] is_race: bool = False precollected_items: Dict[int, List[Item]] @@ -50,6 +52,10 @@ class MultiWorld(): non_local_items: Dict[int, Options.NonLocalItems] progression_balancing: Dict[int, Options.ProgressionBalancing] completion_condition: Dict[int, Callable[[CollectionState], bool]] + indirect_connections: Dict[Region, Set[Entrance]] + exclude_locations: Dict[int, Options.ExcludeLocations] + + game: Dict[int, str] class AttributeProxy(): def __init__(self, rule): @@ -87,6 +93,7 @@ class MultiWorld(): self.customitemarray = [] self.shuffle_ganon = True self.spoiler = Spoiler(self) + self.indirect_connections = {} self.fix_trock_doors = self.AttributeProxy( lambda player: self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted') self.fix_skullwoods_exit = self.AttributeProxy( @@ -195,7 +202,7 @@ class MultiWorld(): self.slot_seeds = {player: random.Random(self.random.getrandbits(64)) for player in range(1, self.players + 1)} - def set_options(self, args): + def set_options(self, args: Namespace) -> None: for option_key in Options.common_options: setattr(self, option_key, getattr(args, option_key, {})) for option_key in Options.per_game_common_options: @@ -295,6 +302,13 @@ class MultiWorld(): def get_file_safe_player_name(self, player: int) -> str: return ''.join(c for c in self.get_player_name(player) if c not in '<>:"/\\|?*') + def get_out_file_name_base(self, player: int) -> str: + """ the base name (without file extension) for each player's output file for a seed """ + return f"AP_{self.seed_name}_P{player}" \ + + (f"_{self.get_file_safe_player_name(player).replace(' ', '_')}" + if (self.player_name[player] != f"Player{player}") + else '') + def initialize_regions(self, regions=None): for region in regions if regions else self.regions: region.world = self @@ -404,6 +418,11 @@ class MultiWorld(): def clear_entrance_cache(self): self._cached_entrances = None + def register_indirect_condition(self, region: Region, entrance: Entrance): + """Report that access to this Region can result in unlocking this Entrance, + state.can_reach(Region) in the Entrance's traversal condition, as opposed to pure transition logic.""" + self.indirect_connections.setdefault(region, set()).add(entrance) + def get_locations(self) -> List[Location]: if self._cached_locations is None: self._cached_locations = [location for region in self.regions for location in region.locations] @@ -529,7 +548,7 @@ class MultiWorld(): beatable_fulfilled = False - def location_conditition(location: Location): + def location_condition(location: Location): """Determine if this location has to be accessible, location is already filtered by location_relevant""" if location.player in players["minimal"]: return False @@ -546,7 +565,7 @@ class MultiWorld(): def all_done(): """Check if all access rules are fulfilled""" if beatable_fulfilled: - if any(location_conditition(location) for location in locations): + if any(location_condition(location) for location in locations): return False # still locations required to be collected return True @@ -608,7 +627,6 @@ class CollectionState(): self.collect(item, True) def update_reachable_regions(self, player: int): - from worlds.alttp.EntranceShuffle import indirect_connections self.stale[player] = False rrp = self.reachable_regions[player] bc = self.blocked_connections[player] @@ -616,7 +634,7 @@ class CollectionState(): start = self.world.get_region('Menu', player) # init on first call - this can't be done on construction since the regions don't exist yet - if not start in rrp: + if start not in rrp: rrp.add(start) bc.update(start.exits) queue.extend(start.exits) @@ -636,8 +654,7 @@ class CollectionState(): self.path[new_region] = (new_region.name, self.path.get(connection, None)) # Retry connections if the new region can unblock them - if new_region.name in indirect_connections: - new_entrance = self.world.get_entrance(indirect_connections[new_region.name], player) + for new_entrance in self.world.indirect_connections.get(new_region, set()): if new_entrance in bc and new_entrance not in queue: queue.append(new_entrance) @@ -674,14 +691,14 @@ class CollectionState(): def sweep_for_events(self, key_only: bool = False, locations: Optional[Iterable[Location]] = None) -> None: if locations is None: locations = self.world.get_filled_locations() - new_locations = True + reachable_events = True # since the loop has a good chance to run more than once, only filter the events once - locations = {location for location in locations if location.event and + locations = {location for location in locations if location.event and location not in self.events and not key_only or getattr(location.item, "locked_dungeon_item", False)} - while new_locations: + while reachable_events: reachable_events = {location for location in locations if location.can_reach(self)} - new_locations = reachable_events - self.events - for event in new_locations: + locations -= reachable_events + for event in reachable_events: self.events.add(event) assert isinstance(event.item, Item), "tried to collect Event with no Item" self.collect(event.item, True, event) @@ -993,7 +1010,7 @@ class Entrance: return False - def connect(self, region: Region, addresses=None, target=None): + def connect(self, region: Region, addresses: Any = None, target: Any = None) -> None: self.connected_region = region self.target = target self.addresses = addresses @@ -1081,7 +1098,7 @@ class Location: show_in_spoiler: bool = True progress_type: LocationProgressType = LocationProgressType.DEFAULT always_allow = staticmethod(lambda item, state: False) - access_rule = staticmethod(lambda state: True) + access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True) item_rule = staticmethod(lambda item: True) item: Optional[Item] = None diff --git a/CommonClient.py b/CommonClient.py index 94d4359dd1..b17709eecf 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -132,12 +132,12 @@ class CommonContext: # defaults starting_reconnect_delay: int = 5 current_reconnect_delay: int = starting_reconnect_delay - command_processor: type(CommandProcessor) = ClientCommandProcessor + command_processor: typing.Type[CommandProcessor] = ClientCommandProcessor ui = None - ui_task: typing.Optional[asyncio.Task] = None - input_task: typing.Optional[asyncio.Task] = None - keep_alive_task: typing.Optional[asyncio.Task] = None - server_task: typing.Optional[asyncio.Task] = None + ui_task: typing.Optional["asyncio.Task[None]"] = None + input_task: typing.Optional["asyncio.Task[None]"] = None + keep_alive_task: typing.Optional["asyncio.Task[None]"] = None + server_task: typing.Optional["asyncio.Task[None]"] = None server: typing.Optional[Endpoint] = None server_version: Version = Version(0, 0, 0) current_energy_link_value: int = 0 # to display in UI, gets set by server @@ -146,14 +146,20 @@ class CommonContext: # remaining type info slot_info: typing.Dict[int, NetworkSlot] - server_address: str + server_address: typing.Optional[str] password: typing.Optional[str] hint_cost: typing.Optional[int] player_names: typing.Dict[int, str] + finished_game: bool + ready: bool + auth: typing.Optional[str] + seed_name: typing.Optional[str] + # locations locations_checked: typing.Set[int] # local state locations_scouted: typing.Set[int] + items_received: typing.List[NetworkItem] missing_locations: typing.Set[int] # server state checked_locations: typing.Set[int] # server state server_locations: typing.Set[int] # all locations the server knows of, missing_location | checked_locations @@ -163,7 +169,7 @@ class CommonContext: # current message box through kvui _messagebox = None - def __init__(self, server_address, password): + def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str]) -> None: # server state self.server_address = server_address self.username = None @@ -243,7 +249,8 @@ class CommonContext: if self.server_task is not None: await self.server_task - async def send_msgs(self, msgs): + async def send_msgs(self, msgs: typing.List[typing.Any]) -> None: + """ `msgs` JSON serializable """ if not self.server or not self.server.socket.open or self.server.socket.closed: return await self.server.socket.send(encode(msgs)) @@ -271,7 +278,7 @@ class CommonContext: logger.info('Enter slot name:') self.auth = await self.console_input() - async def send_connect(self, **kwargs): + async def send_connect(self, **kwargs: typing.Any) -> None: payload = { 'cmd': 'Connect', 'password': self.password, 'name': self.auth, 'version': Utils.version_tuple, @@ -282,11 +289,11 @@ class CommonContext: payload.update(kwargs) await self.send_msgs([payload]) - async def console_input(self): + async def console_input(self) -> str: self.input_requests += 1 return await self.input_queue.get() - async def connect(self, address=None): + async def connect(self, address: typing.Optional[str] = None) -> None: await self.disconnect() self.server_task = asyncio.create_task(server_loop(self, address), name="server loop") @@ -390,7 +397,7 @@ class CommonContext: # DeathLink hooks - def on_deathlink(self, data: dict): + def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None: """Gets dispatched when a new DeathLink is triggered by another linked player.""" self.last_death_link = max(data["time"], self.last_death_link) text = data.get("cause", "") @@ -477,7 +484,7 @@ async def keep_alive(ctx: CommonContext, seconds_between_checks=100): seconds_elapsed = 0 -async def server_loop(ctx: CommonContext, address=None): +async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None) -> None: if ctx.server and ctx.server.socket: logger.error('Already connected') return @@ -722,7 +729,7 @@ async def console_loop(ctx: CommonContext): logger.exception(e) -def get_base_parser(description=None): +def get_base_parser(description: typing.Optional[str] = None): import argparse parser = argparse.ArgumentParser(description=description) parser.add_argument('--connect', default=None, help='Address of the multiworld host.') diff --git a/FactorioClient.py b/FactorioClient.py index 6797578a3a..1efca05d3c 100644 --- a/FactorioClient.py +++ b/FactorioClient.py @@ -211,6 +211,8 @@ async def game_watcher(ctx: FactorioContext): def stream_factorio_output(pipe, queue, process): + pipe.reconfigure(errors="replace") + def queuer(): while process.poll() is None: text = pipe.readline().strip() diff --git a/Fill.py b/Fill.py index c62eaabde8..cb9844b442 100644 --- a/Fill.py +++ b/Fill.py @@ -4,9 +4,10 @@ import collections import itertools from collections import Counter, deque -from BaseClasses import CollectionState, Location, LocationProgressType, MultiWorld, Item +from BaseClasses import CollectionState, Location, LocationProgressType, MultiWorld, Item, ItemClassification from worlds.AutoWorld import call_all +from worlds.generic.Rules import add_item_rule class FillError(RuntimeError): @@ -22,7 +23,8 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: typing.List[Location], - itempool: typing.List[Item], single_player_placement: bool = False, lock: bool = False) -> None: + itempool: typing.List[Item], single_player_placement: bool = False, lock: bool = False, + swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None) -> None: unplaced_items: typing.List[Item] = [] placements: typing.List[Location] = [] @@ -69,60 +71,66 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: else: # we filled all reachable spots. - # try swapping this item with previously placed items - for (i, location) in enumerate(placements): - placed_item = location.item - # Unplaceable items can sometimes be swapped infinitely. Limit the - # number of times we will swap an individual item to prevent this - swap_count = swapped_items[placed_item.player, - placed_item.name] - if swap_count > 1: + if swap: + # try swapping this item with previously placed items + for (i, location) in enumerate(placements): + placed_item = location.item + # Unplaceable items can sometimes be swapped infinitely. Limit the + # number of times we will swap an individual item to prevent this + swap_count = swapped_items[placed_item.player, + placed_item.name] + if swap_count > 1: + continue + + location.item = None + placed_item.location = None + swap_state = sweep_from_pool(base_state, [placed_item]) + # swap_state assumes we can collect placed item before item_to_place + if (not single_player_placement or location.player == item_to_place.player) \ + and location.can_fill(swap_state, item_to_place, perform_access_check): + + # Verify that placing this item won't reduce available locations, which could happen with rules + # that want to not have both items. Left in until removal is proven useful. + prev_state = swap_state.copy() + prev_loc_count = len( + world.get_reachable_locations(prev_state)) + + swap_state.collect(item_to_place, True) + new_loc_count = len( + world.get_reachable_locations(swap_state)) + + if new_loc_count >= prev_loc_count: + # Add this item to the existing placement, and + # add the old item to the back of the queue + spot_to_fill = placements.pop(i) + + swap_count += 1 + swapped_items[placed_item.player, + placed_item.name] = swap_count + + reachable_items[placed_item.player].appendleft( + placed_item) + itempool.append(placed_item) + + break + + # Item can't be placed here, restore original item + location.item = placed_item + placed_item.location = location + + if spot_to_fill is None: + # Can't place this item, move on to the next + unplaced_items.append(item_to_place) continue - - location.item = None - placed_item.location = None - swap_state = sweep_from_pool(base_state) - if (not single_player_placement or location.player == item_to_place.player) \ - and location.can_fill(swap_state, item_to_place, perform_access_check): - - # Verify that placing this item won't reduce available locations - prev_state = swap_state.copy() - prev_state.collect(placed_item) - prev_loc_count = len( - world.get_reachable_locations(prev_state)) - - swap_state.collect(item_to_place, True) - new_loc_count = len( - world.get_reachable_locations(swap_state)) - - if new_loc_count >= prev_loc_count: - # Add this item to the existing placement, and - # add the old item to the back of the queue - spot_to_fill = placements.pop(i) - - swap_count += 1 - swapped_items[placed_item.player, - placed_item.name] = swap_count - - reachable_items[placed_item.player].appendleft( - placed_item) - itempool.append(placed_item) - - break - - # Item can't be placed here, restore original item - location.item = placed_item - placed_item.location = location - - if spot_to_fill is None: - # Can't place this item, move on to the next + else: unplaced_items.append(item_to_place) continue - world.push_item(spot_to_fill, item_to_place, False) spot_to_fill.locked = lock placements.append(spot_to_fill) spot_to_fill.event = item_to_place.advancement + if on_place: + on_place(spot_to_fill) if len(unplaced_items) > 0 and len(locations) > 0: # There are leftover unplaceable items and locations that won't accept them @@ -209,6 +217,37 @@ def fast_fill(world: MultiWorld, return item_pool[placing:], fill_locations[placing:] +def accessibility_corrections(world: MultiWorld, state: CollectionState, locations, pool=[]): + maximum_exploration_state = sweep_from_pool(state, pool) + minimal_players = {player for player in world.player_ids if world.accessibility[player] == "minimal"} + unreachable_locations = [location for location in world.get_locations() if location.player in minimal_players and + not location.can_reach(maximum_exploration_state)] + for location in unreachable_locations: + if (location.item is not None and location.item.advancement and location.address is not None and not + location.locked and location.item.player not in minimal_players): + pool.append(location.item) + state.remove(location.item) + location.item = None + location.event = False + if location in state.events: + state.events.remove(location) + locations.append(location) + if pool and locations: + locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY) + fill_restrictive(world, state, locations, pool) + + +def inaccessible_location_rules(world: MultiWorld, state: CollectionState, locations): + maximum_exploration_state = sweep_from_pool(state) + unreachable_locations = [location for location in locations if not location.can_reach(maximum_exploration_state)] + if unreachable_locations: + def forbid_important_item_rule(item: Item): + return not ((item.classification & 0b0011) and world.accessibility[item.player] != 'minimal') + + for location in unreachable_locations: + add_item_rule(location, forbid_important_item_rule) + + def distribute_items_restrictive(world: MultiWorld) -> None: fill_locations = sorted(world.get_unfilled_locations()) world.random.shuffle(fill_locations) @@ -239,15 +278,33 @@ def distribute_items_restrictive(world: MultiWorld) -> None: defaultlocations = locations[LocationProgressType.DEFAULT] excludedlocations = locations[LocationProgressType.EXCLUDED] - fill_restrictive(world, world.state, prioritylocations, progitempool, lock=True) + # can't lock due to accessibility corrections touching things, so we remember which ones got placed and lock later + lock_later = [] + + def mark_for_locking(location: Location): + nonlocal lock_later + lock_later.append(location) + if prioritylocations: + # "priority fill" + fill_restrictive(world, world.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking) + accessibility_corrections(world, world.state, prioritylocations, progitempool) defaultlocations = prioritylocations + defaultlocations if progitempool: + # "progression fill" fill_restrictive(world, world.state, defaultlocations, progitempool) if progitempool: raise FillError( f'Not enough locations for progress items. There are {len(progitempool)} more items than locations') + accessibility_corrections(world, world.state, defaultlocations) + + for location in lock_later: + if location.item: + location.locked = True + del mark_for_locking, lock_later + + inaccessible_location_rules(world, world.state, defaultlocations) remaining_fill(world, excludedlocations, filleritempool) if excludedlocations: diff --git a/Generate.py b/Generate.py index f048e54383..f19e23700c 100644 --- a/Generate.py +++ b/Generate.py @@ -154,11 +154,12 @@ def main(args=None, callback=ERmain): # sort dict for consistent results across platforms: weights_cache = {key: value for key, value in sorted(weights_cache.items())} for filename, yaml_data in weights_cache.items(): - for yaml in yaml_data: - print(f"P{player_id} Weights: {filename} >> " - f"{get_choice('description', yaml, 'No description specified')}") - player_files[player_id] = filename - player_id += 1 + if filename not in {args.meta_file_path, args.weights_file_path}: + for yaml in yaml_data: + print(f"P{player_id} Weights: {filename} >> " + f"{get_choice('description', yaml, 'No description specified')}") + player_files[player_id] = filename + player_id += 1 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: " @@ -377,7 +378,7 @@ def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any: if option_key in options: if options[option_key].supports_weighting: return get_choice(option_key, category_dict) - return options[option_key] + return category_dict[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", diff --git a/Launcher.py b/Launcher.py index 8a3d53f866..7d5b2f7316 100644 --- a/Launcher.py +++ b/Launcher.py @@ -132,7 +132,7 @@ components: Iterable[Component] = ( Component('Text Client', 'CommonClient', 'ArchipelagoTextClient'), # SNI Component('SNI Client', 'SNIClient', - file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3')), + file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3', '.apsmw')), Component('LttP Adjuster', 'LttPAdjuster'), # Factorio Component('Factorio Client', 'FactorioClient'), @@ -145,10 +145,15 @@ components: Iterable[Component] = ( Component('OoT Adjuster', 'OoTAdjuster'), # FF1 Component('FF1 Client', 'FF1Client'), + # Pokémon + Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')), # ChecksFinder Component('ChecksFinder Client', 'ChecksFinderClient'), # Starcraft 2 Component('Starcraft 2 Client', 'Starcraft2Client'), + # Zillion + Component('Zillion Client', 'ZillionClient', + file_identifier=SuffixIdentifier('.apzl')), # Functions Component('Open host.yaml', func=open_host_yaml), Component('Open Patch', func=open_patch), diff --git a/LttPAdjuster.py b/LttPAdjuster.py index 469e8920b3..9fab226c67 100644 --- a/LttPAdjuster.py +++ b/LttPAdjuster.py @@ -139,7 +139,7 @@ def adjust(args): vanillaRom = args.baserom if not os.path.exists(vanillaRom) and not os.path.isabs(vanillaRom): vanillaRom = local_path(vanillaRom) - if os.path.splitext(args.rom)[-1].lower() in {'.apbp', '.aplttp'}: + if os.path.splitext(args.rom)[-1].lower() == '.aplttp': import Patch meta, args.rom = Patch.create_rom_file(args.rom) @@ -195,7 +195,7 @@ def adjustGUI(): romEntry2 = Entry(romDialogFrame, textvariable=romVar2) def RomSelect2(): - rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc", ".apbp")), ("All Files", "*")]) + rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc", ".aplttp")), ("All Files", "*")]) romVar2.set(rom) romSelectButton2 = Button(romDialogFrame, text='Select Rom', command=RomSelect2) @@ -725,7 +725,7 @@ def get_rom_options_frame(parent=None): vars.auto_apply = StringVar(value=adjuster_settings.auto_apply) autoApplyFrame = Frame(romOptionsFrame) autoApplyFrame.grid(row=9, column=0, columnspan=2, sticky=W) - filler = Label(autoApplyFrame, text="Automatically apply last used settings on opening .apbp files") + filler = Label(autoApplyFrame, text="Automatically apply last used settings on opening .aplttp files") filler.pack(side=TOP, expand=True, fill=X) askRadio = Radiobutton(autoApplyFrame, text='Ask', variable=vars.auto_apply, value='ask') askRadio.pack(side=LEFT, padx=5, pady=5) diff --git a/Main.py b/Main.py index bbd0c805df..63f5b8a818 100644 --- a/Main.py +++ b/Main.py @@ -16,7 +16,7 @@ from worlds.alttp.Regions import is_main_entrance from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned from worlds.alttp.Shops import SHOP_ID_START, total_shop_slots, FillDisabledShopSlots from Utils import output_path, get_options, __version__, version_tuple -from worlds.generic.Rules import locality_rules, exclusion_rules, group_locality_rules +from worlds.generic.Rules import locality_rules, exclusion_rules from worlds import AutoWorld ordered_areas = ( @@ -82,7 +82,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types) numlength = 8 for name, cls in AutoWorld.AutoWorldRegister.world_types.items(): - if not cls.hidden: + if not cls.hidden and len(cls.item_names) > 0: logger.info(f" {name:{longest_name}}: {len(cls.item_names):3} " f"Items (IDs: {min(cls.item_id_to_name):{numlength}} - " f"{max(cls.item_id_to_name):{numlength}}) | " @@ -107,7 +107,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]: world.local_items[player].value.add('Triforce Piece') - # Not possible to place pendants/crystals out side of boss prizes yet. + # Not possible to place pendants/crystals outside boss prizes yet. world.non_local_items[player].value -= item_name_groups['Pendants'] world.non_local_items[player].value -= item_name_groups['Crystals'] @@ -122,9 +122,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No logger.info('Calculating Access Rules.') if world.players > 1: - for player in world.player_ids: - locality_rules(world, player) - group_locality_rules(world) + locality_rules(world) else: world.non_local_items[1].value = set() world.local_items[1].value = set() diff --git a/ModuleUpdate.py b/ModuleUpdate.py index 17eb0906b1..2b6aed1f39 100644 --- a/ModuleUpdate.py +++ b/ModuleUpdate.py @@ -13,10 +13,12 @@ update_ran = getattr(sys, "frozen", False) # don't run update if environment is if not update_ran: for entry in os.scandir(os.path.join(local_dir, "worlds")): - if entry.is_dir(): - req_file = os.path.join(entry.path, "requirements.txt") - if os.path.exists(req_file): - requirements_files.add(req_file) + # skip .* (hidden / disabled) folders + if not entry.name.startswith("."): + if entry.is_dir(): + req_file = os.path.join(entry.path, "requirements.txt") + if os.path.exists(req_file): + requirements_files.add(req_file) def update_command(): @@ -37,11 +39,25 @@ def update(yes=False, force=False): path = os.path.join(os.path.dirname(__file__), req_file) with open(path) as requirementsfile: for line in requirementsfile: - if line.startswith('https://'): - # extract name and version from url - wheel = line.split('/')[-1] - name, version, _ = wheel.split('-', 2) - line = f'{name}=={version}' + if line.startswith(("https://", "git+https://")): + # extract name and version for url + rest = line.split('/')[-1] + line = "" + if "#egg=" in rest: + # from egg info + rest, egg = rest.split("#egg=", 1) + egg = egg.split(";", 1)[0] + if any(compare in egg for compare in ("==", ">=", ">", "<", "<=", "!=")): + line = egg + else: + egg = "" + if "@" in rest and not line: + raise ValueError("Can't deduce version from requirement") + elif not line: + # from filename + rest = rest.replace(".zip", "-").replace(".tar.gz", "-") + name, version, _ = rest.split("-", 2) + line = f'{egg or name}=={version}' requirements = pkg_resources.parse_requirements(line) for requirement in requirements: requirement = str(requirement) diff --git a/NetUtils.py b/NetUtils.py index 1e7d66d824..513ab074fc 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -100,7 +100,7 @@ _encode = JSONEncoder( ).encode -def encode(obj): +def encode(obj: typing.Any) -> str: return _encode(_scan_for_TypedTuples(obj)) diff --git a/OoTClient.py b/OoTClient.py index fbe2b35d1a..b3c58612f3 100644 --- a/OoTClient.py +++ b/OoTClient.py @@ -5,7 +5,8 @@ import multiprocessing import subprocess from asyncio import StreamReader, StreamWriter -from CommonClient import CommonContext, server_loop, gui_enabled, console_loop, \ +# CommonClient import first to trigger ModuleUpdater +from CommonClient import CommonContext, server_loop, gui_enabled, \ ClientCommandProcessor, logger, get_base_parser import Utils from worlds import network_data_package diff --git a/Options.py b/Options.py index 49f044d8cd..c2007c1c41 100644 --- a/Options.py +++ b/Options.py @@ -78,6 +78,9 @@ class AssembleOptions(abc.ABCMeta): return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs) + @abc.abstractclassmethod + def from_any(cls, value: typing.Any) -> "Option[typing.Any]": ... + T = typing.TypeVar('T') @@ -165,6 +168,7 @@ class FreeText(Option): class NumericOption(Option[int], numbers.Integral): + default = 0 # note: some of the `typing.Any`` here is a result of unresolved issue in python standards # `int` is not a `numbers.Integral` according to the official typestubs # (even though isinstance(5, numbers.Integral) == True) @@ -426,7 +430,6 @@ class TextChoice(Choice): assert isinstance(value, str) or isinstance(value, int), \ f"{value} is not a valid option for {self.__class__.__name__}" self.value = value - super(TextChoice, self).__init__() @property def current_key(self) -> str: @@ -466,6 +469,124 @@ class TextChoice(Choice): raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}") +class BossMeta(AssembleOptions): + def __new__(mcs, name, bases, attrs): + if name != "PlandoBosses": + assert "bosses" in attrs, f"Please define valid bosses for {name}" + attrs["bosses"] = frozenset((boss.lower() for boss in attrs["bosses"])) + assert "locations" in attrs, f"Please define valid locations for {name}" + attrs["locations"] = frozenset((location.lower() for location in attrs["locations"])) + cls = super().__new__(mcs, name, bases, attrs) + assert not cls.duplicate_bosses or "singularity" in cls.options, f"Please define option_singularity for {name}" + return cls + + +class PlandoBosses(TextChoice, metaclass=BossMeta): + """Generic boss shuffle option that supports plando. Format expected is + 'location1-boss1;location2-boss2;shuffle_mode'. + If shuffle_mode is not provided in the string, this will be the default shuffle mode. Must override can_place_boss, + which passes a plando boss and location. Check if the placement is valid for your game here.""" + bosses: typing.ClassVar[typing.Union[typing.Set[str], typing.FrozenSet[str]]] + locations: typing.ClassVar[typing.Union[typing.Set[str], typing.FrozenSet[str]]] + + duplicate_bosses: bool = False + + @classmethod + def from_text(cls, text: str): + # set all of our text to lower case for name checking + text = text.lower() + if text == "random": + return cls(random.choice(list(cls.options.values()))) + for option_name, value in cls.options.items(): + if option_name == text: + return cls(value) + options = text.split(";") + + # since plando exists in the option verify the plando values given are valid + cls.validate_plando_bosses(options) + return cls.get_shuffle_mode(options) + + @classmethod + def get_shuffle_mode(cls, option_list: typing.List[str]): + # find out what mode of boss shuffle we should use for placing bosses after plando + # and add as a string to look nice in the spoiler + if "random" in option_list: + shuffle = random.choice(list(cls.options)) + option_list.remove("random") + options = ";".join(option_list) + f";{shuffle}" + boss_class = cls(options) + else: + for option in option_list: + if option in cls.options: + options = ";".join(option_list) + break + else: + if cls.duplicate_bosses and len(option_list) == 1: + if cls.valid_boss_name(option_list[0]): + # this doesn't exist in this class but it's a forced option for classes where this is called + options = option_list[0] + ";singularity" + else: + options = option_list[0] + f";{cls.name_lookup[cls.default]}" + else: + options = ";".join(option_list) + f";{cls.name_lookup[cls.default]}" + boss_class = cls(options) + return boss_class + + @classmethod + def validate_plando_bosses(cls, options: typing.List[str]) -> None: + used_locations = [] + used_bosses = [] + for option in options: + # check if a shuffle mode was provided in the incorrect location + if option == "random" or option in cls.options: + if option != options[-1]: + raise ValueError(f"{option} option must be at the end of the boss_shuffle options!") + elif "-" in option: + location, boss = option.split("-") + if location in used_locations: + raise ValueError(f"Duplicate Boss Location {location} not allowed.") + if not cls.duplicate_bosses and boss in used_bosses: + raise ValueError(f"Duplicate Boss {boss} not allowed.") + used_locations.append(location) + used_bosses.append(boss) + if not cls.valid_boss_name(boss): + raise ValueError(f"{boss.title()} is not a valid boss name.") + if not cls.valid_location_name(location): + raise ValueError(f"{location.title()} is not a valid boss location name.") + if not cls.can_place_boss(boss, location): + raise ValueError(f"{location.title()} is not a valid location for {boss.title()} to be placed.") + else: + if cls.duplicate_bosses: + if not cls.valid_boss_name(option): + raise ValueError(f"{option} is not a valid boss name.") + else: + raise ValueError(f"{option.title()} is not formatted correctly.") + + @classmethod + def can_place_boss(cls, boss: str, location: str) -> bool: + raise NotImplementedError + + @classmethod + def valid_boss_name(cls, value: str) -> bool: + return value in cls.bosses + + @classmethod + def valid_location_name(cls, value: str) -> bool: + return value in cls.locations + + def verify(self, world, player_name: str, plando_options) -> None: + if isinstance(self.value, int): + return + from Generate import PlandoSettings + if not(PlandoSettings.bosses & plando_options): + import logging + # plando is disabled but plando options were given so pull the option and change it to an int + option = self.value.split(";")[-1] + self.value = self.options[option] + logging.warning(f"The plando bosses module is turned off, so {self.name_lookup[self.value].title()} " + f"boss shuffle will be used for player {player_name}.") + + class Range(NumericOption): range_start = 0 range_end = 1 @@ -628,7 +749,7 @@ class VerifyKeys: class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys): - default = {} + default: typing.Dict[str, typing.Any] = {} supports_weighting = False def __init__(self, value: typing.Dict[str, typing.Any]): @@ -659,7 +780,7 @@ class ItemDict(OptionDict): class OptionList(Option[typing.List[typing.Any]], VerifyKeys): - default = [] + default: typing.List[typing.Any] = [] supports_weighting = False def __init__(self, value: typing.List[typing.Any]): diff --git a/Patch.py b/Patch.py index aaa4fc2404..4ff0e9602a 100644 --- a/Patch.py +++ b/Patch.py @@ -1,266 +1,33 @@ from __future__ import annotations -import shutil -import json -import bsdiff4 -import yaml import os -import lzma -import threading -import concurrent.futures -import zipfile import sys -from typing import Tuple, Optional, Dict, Any, Union, BinaryIO +from typing import Tuple, Optional, TypedDict -import ModuleUpdate -ModuleUpdate.update() +if __name__ == "__main__": + import ModuleUpdate + ModuleUpdate.update() -import Utils - -current_patch_version = 5 +from worlds.Files import AutoPatchRegister, APDeltaPatch -class AutoPatchRegister(type): - patch_types: Dict[str, APDeltaPatch] = {} - file_endings: Dict[str, APDeltaPatch] = {} - - def __new__(cls, name: str, bases, dct: Dict[str, Any]): - # construct class - new_class = super().__new__(cls, name, bases, dct) - if "game" in dct: - AutoPatchRegister.patch_types[dct["game"]] = new_class - if not dct["patch_file_ending"]: - raise Exception(f"Need an expected file ending for {name}") - AutoPatchRegister.file_endings[dct["patch_file_ending"]] = new_class - return new_class - - @staticmethod - def get_handler(file: str) -> Optional[type(APDeltaPatch)]: - for file_ending, handler in AutoPatchRegister.file_endings.items(): - if file.endswith(file_ending): - return handler - - -class APContainer: - """A zipfile containing at least archipelago.json""" - version: int = current_patch_version - compression_level: int = 9 - compression_method: int = zipfile.ZIP_DEFLATED - game: Optional[str] = None - - # instance attributes: - path: Optional[str] - player: Optional[int] - player_name: str - server: str - - def __init__(self, path: Optional[str] = None, player: Optional[int] = None, - player_name: str = "", server: str = ""): - self.path = path - self.player = player - self.player_name = player_name - self.server = server - - def write(self, file: Optional[Union[str, BinaryIO]] = None): - if not self.path and not file: - raise FileNotFoundError(f"Cannot write {self.__class__.__name__} due to no path provided.") - with zipfile.ZipFile(file if file else self.path, "w", self.compression_method, True, self.compression_level) \ - as zf: - if file: - self.path = zf.filename - self.write_contents(zf) - - def write_contents(self, opened_zipfile: zipfile.ZipFile): - manifest = self.get_manifest() - try: - manifest = json.dumps(manifest) - except Exception as e: - raise Exception(f"Manifest {manifest} did not convert to json.") from e - else: - opened_zipfile.writestr("archipelago.json", manifest) - - def read(self, file: Optional[Union[str, BinaryIO]] = None): - """Read data into patch object. file can be file-like, such as an outer zip file's stream.""" - if not self.path and not file: - raise FileNotFoundError(f"Cannot read {self.__class__.__name__} due to no path provided.") - with zipfile.ZipFile(file if file else self.path, "r") as zf: - if file: - self.path = zf.filename - self.read_contents(zf) - - def read_contents(self, opened_zipfile: zipfile.ZipFile): - with opened_zipfile.open("archipelago.json", "r") as f: - manifest = json.load(f) - if manifest["compatible_version"] > self.version: - raise Exception(f"File (version: {manifest['compatible_version']}) too new " - f"for this handler (version: {self.version})") - self.player = manifest["player"] - self.server = manifest["server"] - self.player_name = manifest["player_name"] - - def get_manifest(self) -> dict: - return { - "server": self.server, # allow immediate connection to server in multiworld. Empty string otherwise - "player": self.player, - "player_name": self.player_name, - "game": self.game, - # minimum version of patch system expected for patching to be successful - "compatible_version": 4, - "version": current_patch_version, - } - - -class APDeltaPatch(APContainer, metaclass=AutoPatchRegister): - """An APContainer that additionally has delta.bsdiff4 - containing a delta patch to get the desired file, often a rom.""" - - hash = Optional[str] # base checksum of source file - patch_file_ending: str = "" - delta: Optional[bytes] = None - result_file_ending: str = ".sfc" - source_data: bytes - - def __init__(self, *args, patched_path: str = "", **kwargs): - self.patched_path = patched_path - super(APDeltaPatch, self).__init__(*args, **kwargs) - - def get_manifest(self) -> dict: - manifest = super(APDeltaPatch, self).get_manifest() - manifest["base_checksum"] = self.hash - manifest["result_file_ending"] = self.result_file_ending - manifest["patch_file_ending"] = self.patch_file_ending - return manifest - - @classmethod - def get_source_data(cls) -> bytes: - """Get Base data""" - raise NotImplementedError() - - @classmethod - def get_source_data_with_cache(cls) -> bytes: - if not hasattr(cls, "source_data"): - cls.source_data = cls.get_source_data() - return cls.source_data - - def write_contents(self, opened_zipfile: zipfile.ZipFile): - super(APDeltaPatch, self).write_contents(opened_zipfile) - # write Delta - opened_zipfile.writestr("delta.bsdiff4", - bsdiff4.diff(self.get_source_data_with_cache(), open(self.patched_path, "rb").read()), - compress_type=zipfile.ZIP_STORED) # bsdiff4 is a format with integrated compression - - def read_contents(self, opened_zipfile: zipfile.ZipFile): - super(APDeltaPatch, self).read_contents(opened_zipfile) - self.delta = opened_zipfile.read("delta.bsdiff4") - - def patch(self, target: str): - """Base + Delta -> Patched""" - if not self.delta: - self.read() - result = bsdiff4.patch(self.get_source_data_with_cache(), self.delta) - with open(target, "wb") as f: - f.write(result) - - -# legacy patch handling follows: GAME_ALTTP = "A Link to the Past" GAME_SM = "Super Metroid" GAME_SOE = "Secret of Evermore" GAME_SMZ3 = "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_DKC3: "apdkc3" -} +GAME_SMW = "Super Mario World" -def generate_yaml(patch: bytes, metadata: Optional[dict] = None, game: str = GAME_ALTTP) -> bytes: - if game == GAME_ALTTP: - from worlds.alttp.Rom import LTTPJPN10HASH as HASH - elif game == GAME_SM: - from worlds.sm.Rom import SMJUHASH as HASH - elif game == GAME_SOE: - from worlds.soe.Patch import USHASH as HASH - elif game == GAME_SMZ3: - 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.") - patch = yaml.dump({"meta": metadata, - "patch": patch, - "game": game, - # minimum version of patch system expected for patching to be successful - "compatible_version": 3, - "version": current_patch_version, - "base_checksum": HASH}) - return patch.encode(encoding="utf-8-sig") +class RomMeta(TypedDict): + server: str + player: Optional[int] + player_name: str -def generate_patch(rom: bytes, metadata: Optional[dict] = None, game: str = GAME_ALTTP) -> bytes: - if metadata is None: - metadata = {} - patch = bsdiff4.diff(get_base_rom_data(game), rom) - return generate_yaml(patch, metadata, game) - - -def create_patch_file(rom_file_to_patch: str, server: str = "", destination: str = None, - player: int = 0, player_name: str = "", game: str = GAME_ALTTP) -> str: - meta = {"server": server, # allow immediate connection to server in multiworld. Empty string otherwise - "player_id": player, - "player_name": player_name} - bytes = generate_patch(load_bytes(rom_file_to_patch), - 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 ".apdkc3" if game == GAME_DKC3 - else ".apm3") - write_lzma(bytes, target) - return target - - -def create_rom_bytes(patch_file: str, ignore_version: bool = False) -> Tuple[dict, str, bytearray]: - data = Utils.parse_yaml(lzma.decompress(load_bytes(patch_file)).decode("utf-8-sig")) - game_name = data["game"] - if not ignore_version and data["compatible_version"] > current_patch_version: - raise RuntimeError("Patch file is incompatible with this patcher, likely an update is required.") - patched_data = bsdiff4.patch(get_base_rom_data(game_name), data["patch"]) - rom_hash = patched_data[int(0x7FC0):int(0x7FD5)] - data["meta"]["hash"] = "".join(chr(x) for x in rom_hash) - target = os.path.splitext(patch_file)[0] + ".sfc" - return data["meta"], target, patched_data - - -def get_base_rom_data(game: str): - if game == GAME_ALTTP: - from worlds.alttp.Rom import get_base_rom_bytes - elif game == "alttp": # old version for A Link to the Past - from worlds.alttp.Rom import get_base_rom_bytes - elif game == GAME_SM: - from worlds.sm.Rom import get_base_rom_bytes - elif game == GAME_SOE: - from worlds.soe.Patch import get_base_rom_path - 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() - - -def create_rom_file(patch_file: str) -> Tuple[dict, str]: +def create_rom_file(patch_file: str) -> Tuple[RomMeta, str]: auto_handler = AutoPatchRegister.get_handler(patch_file) if auto_handler: handler: APDeltaPatch = auto_handler(patch_file) @@ -269,171 +36,10 @@ def create_rom_file(patch_file: str) -> Tuple[dict, str]: return {"server": handler.server, "player": handler.player, "player_name": handler.player_name}, target - else: - data, target, patched_data = create_rom_bytes(patch_file) - with open(target, "wb") as f: - f.write(patched_data) - return data, target - - -def update_patch_data(patch_data: bytes, server: str = "") -> bytes: - data = Utils.parse_yaml(lzma.decompress(patch_data).decode("utf-8-sig")) - data["meta"]["server"] = server - bytes = generate_yaml(data["patch"], data["meta"], data["game"]) - return lzma.compress(bytes) - - -def load_bytes(path: str) -> bytes: - with open(path, "rb") as f: - return f.read() - - -def write_lzma(data: bytes, path: str): - with lzma.LZMAFile(path, 'wb') as f: - f.write(data) - - -def read_rom(stream, strip_header=True) -> bytearray: - """Reads rom into bytearray and optionally strips off any smc header""" - buffer = bytearray(stream.read()) - if strip_header and len(buffer) % 0x400 == 0x200: - return buffer[0x200:] - return buffer + raise NotImplementedError(f"No Handler for {patch_file} found.") if __name__ == "__main__": - host = Utils.get_public_ipv4() - options = Utils.get_options()['server_options'] - if options['host']: - host = options['host'] - - address = f"{host}:{options['port']}" - ziplock = threading.Lock() - print(f"Host for patches to be created is {address}") - with concurrent.futures.ThreadPoolExecutor() as pool: - for rom in sys.argv: - try: - if rom.endswith(".sfc"): - print(f"Creating patch for {rom}") - result = pool.submit(create_patch_file, rom, address) - result.add_done_callback(lambda task: print(f"Created patch {task.result()}")) - - elif rom.endswith(".apbp"): - print(f"Applying patch {rom}") - data, target = create_rom_file(rom) - #romfile, adjusted = Utils.get_adjuster_settings(target) - adjuster_settings = Utils.get_adjuster_settings(GAME_ALTTP) - adjusted = False - if adjuster_settings: - import pprint - from worlds.alttp.Rom import get_base_rom_path - adjuster_settings.rom = target - adjuster_settings.baserom = get_base_rom_path() - adjuster_settings.world = None - whitelist = {"music", "menuspeed", "heartbeep", "heartcolor", "ow_palettes", "quickswap", - "uw_palettes", "sprite", "sword_palettes", "shield_palettes", "hud_palettes", - "reduceflashing", "deathlink"} - printed_options = {name: value for name, value in vars(adjuster_settings).items() if name in whitelist} - if hasattr(adjuster_settings, "sprite_pool"): - sprite_pool = {} - for sprite in getattr(adjuster_settings, "sprite_pool"): - if sprite in sprite_pool: - sprite_pool[sprite] += 1 - else: - sprite_pool[sprite] = 1 - if sprite_pool: - printed_options["sprite_pool"] = sprite_pool - - adjust_wanted = str('no') - if not hasattr(adjuster_settings, 'auto_apply') or 'ask' in adjuster_settings.auto_apply: - adjust_wanted = input(f"Last used adjuster settings were found. Would you like to apply these? \n" - f"{pprint.pformat(printed_options)}\n" - f"Enter yes, no, always or never: ") - if adjuster_settings.auto_apply == 'never': # never adjust, per user request - adjust_wanted = 'no' - elif adjuster_settings.auto_apply == 'always': - adjust_wanted = 'yes' - - if adjust_wanted and "never" in adjust_wanted: - adjuster_settings.auto_apply = 'never' - Utils.persistent_store("adjuster", GAME_ALTTP, adjuster_settings) - - elif adjust_wanted and "always" in adjust_wanted: - adjuster_settings.auto_apply = 'always' - Utils.persistent_store("adjuster", GAME_ALTTP, adjuster_settings) - - if adjust_wanted and adjust_wanted.startswith("y"): - if hasattr(adjuster_settings, "sprite_pool"): - from LttPAdjuster import AdjusterWorld - adjuster_settings.world = AdjusterWorld(getattr(adjuster_settings, "sprite_pool")) - - adjusted = True - import LttPAdjuster - _, romfile = LttPAdjuster.adjust(adjuster_settings) - - if hasattr(adjuster_settings, "world"): - delattr(adjuster_settings, "world") - else: - adjusted = False - if adjusted: - try: - shutil.move(romfile, target) - romfile = target - except Exception as e: - print(e) - print(f"Created rom {romfile if adjusted else target}.") - if 'server' in data: - Utils.persistent_store("servers", data['hash'], data['server']) - print(f"Host is {data['server']}") - elif rom.endswith(".apm3"): - 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(".apsmz"): - 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(".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}") - - - def _handle_zip_file_entry(zfinfo: zipfile.ZipInfo, server: str): - data = zfr.read(zfinfo) - 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) - return zfinfo.filename - - - futures = [] - with zipfile.ZipFile(rom, "r") as zfr: - updated_zip = os.path.splitext(rom)[0] + "_updated.zip" - with zipfile.ZipFile(updated_zip, "w", compression=zipfile.ZIP_DEFLATED, - compresslevel=9) as zfw: - for zfname in zfr.namelist(): - futures.append(pool.submit(_handle_zip_file_entry, zfr.getinfo(zfname), address)) - for future in futures: - print(f"File {future.result()} added to {os.path.split(updated_zip)[1]}") - - except: - import traceback - - traceback.print_exc() - input("Press enter to close.") + for file in sys.argv[1:]: + meta_data, result_file = create_rom_file(file) + print(f"Patch with meta-data {meta_data} was written to {result_file}") diff --git a/PokemonClient.py b/PokemonClient.py new file mode 100644 index 0000000000..2328243de5 --- /dev/null +++ b/PokemonClient.py @@ -0,0 +1,319 @@ +import asyncio +import json +import time +import os +import bsdiff4 +import subprocess +import zipfile +import hashlib +from asyncio import StreamReader, StreamWriter +from typing import List + + +import Utils +from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \ + get_base_parser + +from worlds.pokemon_rb.locations import location_data + +location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}} +location_bytes_bits = {} +for location in location_data: + if location.ram_address is not None: + if type(location.ram_address) == list: + location_map[type(location.ram_address).__name__][(location.ram_address[0].flag, location.ram_address[1].flag)] = location.address + location_bytes_bits[location.address] = [{'byte': location.ram_address[0].byte, 'bit': location.ram_address[0].bit}, + {'byte': location.ram_address[1].byte, 'bit': location.ram_address[1].bit}] + else: + location_map[type(location.ram_address).__name__][location.ram_address.flag] = location.address + location_bytes_bits[location.address] = {'byte': location.ram_address.byte, 'bit': location.ram_address.bit} + +SYSTEM_MESSAGE_ID = 0 + +CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart pkmn_rb.lua" +CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure pkmn_rb.lua is running" +CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart pkmn_rb.lua" +CONNECTION_TENTATIVE_STATUS = "Initial Connection Made" +CONNECTION_CONNECTED_STATUS = "Connected" +CONNECTION_INITIAL_STATUS = "Connection has not been initiated" + +DISPLAY_MSGS = True + + +class GBCommandProcessor(ClientCommandProcessor): + def __init__(self, ctx: CommonContext): + super().__init__(ctx) + + def _cmd_gb(self): + """Check Gameboy Connection State""" + if isinstance(self.ctx, GBContext): + logger.info(f"Gameboy Status: {self.ctx.gb_status}") + + +class GBContext(CommonContext): + command_processor = GBCommandProcessor + game = 'Pokemon Red and Blue' + items_handling = 0b101 + + def __init__(self, server_address, password): + super().__init__(server_address, password) + self.gb_streams: (StreamReader, StreamWriter) = None + self.gb_sync_task = None + self.messages = {} + self.locations_array = None + self.gb_status = CONNECTION_INITIAL_STATUS + self.awaiting_rom = False + self.display_msgs = True + + async def server_auth(self, password_requested: bool = False): + if password_requested and not self.password: + await super(GBContext, self).server_auth(password_requested) + if not self.auth: + self.awaiting_rom = True + logger.info('Awaiting connection to Bizhawk to get Player information') + return + + await self.send_connect() + + def _set_message(self, msg: str, msg_id: int): + if DISPLAY_MSGS: + self.messages[(time.time(), msg_id)] = msg + + def on_package(self, cmd: str, args: dict): + if cmd == 'Connected': + self.locations_array = None + elif cmd == "RoomInfo": + self.seed_name = args['seed_name'] + elif cmd == 'Print': + msg = args['text'] + if ': !' not in msg: + self._set_message(msg, SYSTEM_MESSAGE_ID) + elif cmd == "ReceivedItems": + msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}" + self._set_message(msg, SYSTEM_MESSAGE_ID) + + def run_gui(self): + from kvui import GameManager + + class GBManager(GameManager): + logging_pairs = [ + ("Client", "Archipelago") + ] + base_title = "Archipelago Pokémon Client" + + self.ui = GBManager(self) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + + +def get_payload(ctx: GBContext): + current_time = time.time() + return json.dumps( + { + "items": [item.item for item in ctx.items_received], + "messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items() + if key[0] > current_time - 10} + } + ) + + +async def parse_locations(data: List, ctx: GBContext): + locations = [] + flags = {"EventFlag": data[:0x140], "Missable": data[0x140:0x140 + 0x20], + "Hidden": data[0x140 + 0x20: 0x140 + 0x20 + 0x0E], "Rod": data[0x140 + 0x20 + 0x0E:]} + + # Check for clear problems + if len(flags['Rod']) > 1: + return + if flags["EventFlag"][1] + flags["EventFlag"][8] + flags["EventFlag"][9] + flags["EventFlag"][12] \ + + flags["EventFlag"][61] + flags["EventFlag"][62] + flags["EventFlag"][63] + flags["EventFlag"][64] \ + + flags["EventFlag"][65] + flags["EventFlag"][66] + flags["EventFlag"][67] + flags["EventFlag"][68] \ + + flags["EventFlag"][69] + flags["EventFlag"][70] != 0: + return + + for flag_type, loc_map in location_map.items(): + for flag, loc_id in loc_map.items(): + if flag_type == "list": + if (flags["EventFlag"][location_bytes_bits[loc_id][0]['byte']] & 1 << location_bytes_bits[loc_id][0]['bit'] + and flags["Missable"][location_bytes_bits[loc_id][1]['byte']] & 1 << location_bytes_bits[loc_id][1]['bit']): + locations.append(loc_id) + elif flags[flag_type][location_bytes_bits[loc_id]['byte']] & 1 << location_bytes_bits[loc_id]['bit']: + locations.append(loc_id) + if flags["EventFlag"][280] & 1 and not ctx.finished_game: + await ctx.send_msgs([ + {"cmd": "StatusUpdate", + "status": 30} + ]) + ctx.finished_game = True + if locations == ctx.locations_array: + return + ctx.locations_array = locations + if locations is not None: + await ctx.send_msgs([{"cmd": "LocationChecks", "locations": locations}]) + + +async def gb_sync_task(ctx: GBContext): + logger.info("Starting GB connector. Use /gb for status information") + while not ctx.exit_event.is_set(): + error_status = None + if ctx.gb_streams: + (reader, writer) = ctx.gb_streams + msg = get_payload(ctx).encode() + writer.write(msg) + writer.write(b'\n') + try: + await asyncio.wait_for(writer.drain(), timeout=1.5) + try: + # Data will return a dict with up to two fields: + # 1. A keepalive response of the Players Name (always) + # 2. An array representing the memory values of the locations area (if in game) + data = await asyncio.wait_for(reader.readline(), timeout=5) + data_decoded = json.loads(data.decode()) + #print(data_decoded) + + if ctx.seed_name and ctx.seed_name != bytes(data_decoded['seedName']).decode(): + msg = "The server is running a different multiworld than your client is. (invalid seed_name)" + logger.info(msg, extra={'compact_gui': True}) + ctx.gui_error('Error', msg) + error_status = CONNECTION_RESET_STATUS + ctx.seed_name = bytes(data_decoded['seedName']).decode() + if not ctx.auth: + ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0]) + if ctx.auth == '': + logger.info("Invalid ROM detected. No player name built into the ROM.") + if ctx.awaiting_rom: + await ctx.server_auth(False) + if 'locations' in data_decoded and ctx.game and ctx.gb_status == CONNECTION_CONNECTED_STATUS \ + and not error_status and ctx.auth: + # Not just a keep alive ping, parse + asyncio.create_task(parse_locations(data_decoded['locations'], ctx)) + except asyncio.TimeoutError: + logger.debug("Read Timed Out, Reconnecting") + error_status = CONNECTION_TIMING_OUT_STATUS + writer.close() + ctx.gb_streams = None + except ConnectionResetError as e: + logger.debug("Read failed due to Connection Lost, Reconnecting") + error_status = CONNECTION_RESET_STATUS + writer.close() + ctx.gb_streams = None + except TimeoutError: + logger.debug("Connection Timed Out, Reconnecting") + error_status = CONNECTION_TIMING_OUT_STATUS + writer.close() + ctx.gb_streams = None + except ConnectionResetError: + logger.debug("Connection Lost, Reconnecting") + error_status = CONNECTION_RESET_STATUS + writer.close() + ctx.gb_streams = None + if ctx.gb_status == CONNECTION_TENTATIVE_STATUS: + if not error_status: + logger.info("Successfully Connected to Gameboy") + ctx.gb_status = CONNECTION_CONNECTED_STATUS + else: + ctx.gb_status = f"Was tentatively connected but error occured: {error_status}" + elif error_status: + ctx.gb_status = error_status + logger.info("Lost connection to Gameboy and attempting to reconnect. Use /gb for status updates") + else: + try: + logger.debug("Attempting to connect to Gameboy") + ctx.gb_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 17242), timeout=10) + ctx.gb_status = CONNECTION_TENTATIVE_STATUS + except TimeoutError: + logger.debug("Connection Timed Out, Trying Again") + ctx.gb_status = CONNECTION_TIMING_OUT_STATUS + continue + except ConnectionRefusedError: + logger.debug("Connection Refused, Trying Again") + ctx.gb_status = CONNECTION_REFUSED_STATUS + continue + + +async def run_game(romfile): + auto_start = Utils.get_options()["pokemon_rb_options"].get("rom_start", True) + if auto_start is True: + import webbrowser + webbrowser.open(romfile) + elif os.path.isfile(auto_start): + subprocess.Popen([auto_start, romfile], + stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + +async def patch_and_run_game(game_version, patch_file, ctx): + base_name = os.path.splitext(patch_file)[0] + comp_path = base_name + '.gb' + with open(Utils.local_path(Utils.get_options()["pokemon_rb_options"][f"{game_version}_rom_file"]), "rb") as stream: + base_rom = bytes(stream.read()) + try: + with open(Utils.local_path('lib', 'worlds', 'pokemon_rb', f'basepatch_{game_version}.bsdiff4'), 'rb') as stream: + base_patch = bytes(stream.read()) + except FileNotFoundError: + with open(Utils.local_path('worlds', 'pokemon_rb', f'basepatch_{game_version}.bsdiff4'), 'rb') as stream: + base_patch = bytes(stream.read()) + base_patched_rom_data = bsdiff4.patch(base_rom, base_patch) + basemd5 = hashlib.md5() + basemd5.update(base_patched_rom_data) + + with zipfile.ZipFile(patch_file, 'r') as patch_archive: + with patch_archive.open('delta.bsdiff4', 'r') as stream: + patch = stream.read() + patched_rom_data = bsdiff4.patch(base_patched_rom_data, patch) + + written_hash = patched_rom_data[0xFFCC:0xFFDC] + if written_hash == basemd5.digest(): + with open(comp_path, "wb") as patched_rom_file: + patched_rom_file.write(patched_rom_data) + + asyncio.create_task(run_game(comp_path)) + else: + msg = "Patch supplied was not generated with the same base patch version as this client. Patching failed." + logger.warning(msg) + ctx.gui_error('Error', msg) + + +if __name__ == '__main__': + + Utils.init_logging("PokemonClient") + + options = Utils.get_options() + + async def main(): + parser = get_base_parser() + parser.add_argument('patch_file', default="", type=str, nargs="?", + help='Path to an APRED or APBLUE patch file') + args = parser.parse_args() + + ctx = GBContext(args.connect, args.password) + ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + ctx.gb_sync_task = asyncio.create_task(gb_sync_task(ctx), name="GB Sync") + + if args.patch_file: + ext = args.patch_file.split(".")[len(args.patch_file.split(".")) - 1].lower() + if ext == "apred": + logger.info("APRED file supplied, beginning patching process...") + asyncio.create_task(patch_and_run_game("red", args.patch_file, ctx)) + elif ext == "apblue": + logger.info("APBLUE file supplied, beginning patching process...") + asyncio.create_task(patch_and_run_game("blue", args.patch_file, ctx)) + else: + logger.warning(f"Unknown patch file extension {ext}") + + await ctx.exit_event.wait() + ctx.server_address = None + + await ctx.shutdown() + + if ctx.gb_sync_task: + await ctx.gb_sync_task + + + import colorama + + colorama.init() + + asyncio.run(main()) + colorama.deinit() diff --git a/README.md b/README.md index c8362dddd0..a8f269f557 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,11 @@ Currently, the following games are supported: * Starcraft 2: Wings of Liberty * Donkey Kong Country 3 * Dark Souls 3 +* Super Mario World +* Pokémon Red and Blue +* Hylics 2 +* Overcooked! 2 +* Zillion 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 diff --git a/SNIClient.py b/SNIClient.py index 477cde86a2..188822bce7 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -15,10 +15,13 @@ import typing from json import loads, dumps -from Utils import init_logging, messagebox +# CommonClient import first to trigger ModuleUpdater +from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser + +import Utils if __name__ == "__main__": - init_logging("SNIClient", exception_logger="Client") + Utils.init_logging("SNIClient", exception_logger="Client") import colorama import websockets @@ -28,9 +31,8 @@ from worlds.alttp import Regions, Shops from worlds.alttp.Rom import ROM_PLAYER_LIMIT 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, GAME_DKC3 +from Patch import GAME_ALTTP, GAME_SM, GAME_SMZ3, GAME_DKC3, GAME_SMW + snes_logger = logging.getLogger("SNES") @@ -236,6 +238,10 @@ async def deathlink_kill_player(ctx: Context): snes_buffered_write(ctx, WRAM_START + 0x0A50, bytes([255])) # deal 255 of damage at next opportunity if not ctx.death_link_allow_survive: snes_buffered_write(ctx, WRAM_START + 0x09D6, bytes([0, 0])) # set current reserve to 0 + elif ctx.game == GAME_SMW: + from worlds.smw.Client import deathlink_kill_player as smw_deathlink_kill_player + await smw_deathlink_kill_player(ctx) + await snes_flush_writes(ctx) await asyncio.sleep(1) @@ -1041,6 +1047,9 @@ async def game_watcher(ctx: Context): from worlds.dkc3.Client import dkc3_rom_init init_handled = await dkc3_rom_init(ctx) + if not init_handled: + from worlds.smw.Client import smw_rom_init + init_handled = await smw_rom_init(ctx) if not init_handled: game_name = await snes_read(ctx, SM_ROMNAME_START, 5) if game_name is None: @@ -1299,6 +1308,9 @@ async def game_watcher(ctx: Context): elif ctx.game == GAME_DKC3: from worlds.dkc3.Client import dkc3_game_watcher await dkc3_game_watcher(ctx) + elif ctx.game == GAME_SMW: + from worlds.smw.Client import smw_game_watcher + await smw_game_watcher(ctx) async def run_game(romfile): @@ -1326,20 +1338,18 @@ async def main(): try: meta, romfile = Patch.create_rom_file(args.diff_file) except Exception as e: - messagebox('Error', str(e), True) + Utils.messagebox('Error', str(e), True) raise - if "server" in meta: - args.connect = meta["server"] + args.connect = meta["server"] logging.info(f"Wrote rom file to {romfile}") if args.diff_file.endswith(".apsoe"): import webbrowser - webbrowser.open("http://www.evermizer.com/apclient/" + - (f"#server={meta['server']}" if "server" in meta else "")) + webbrowser.open(f"http://www.evermizer.com/apclient/#server={meta['server']}") logging.info("Starting Evermizer Client in your Browser...") import time time.sleep(3) sys.exit() - elif args.diff_file.endswith((".apbp", ".apz3", ".aplttp")): + elif args.diff_file.endswith(".aplttp"): adjustedromfile, adjusted = get_alttp_settings(romfile) asyncio.create_task(run_game(adjustedromfile if adjusted else romfile)) else: diff --git a/Starcraft2Client.py b/Starcraft2Client.py index d91adffb08..de0a90411e 100644 --- a/Starcraft2Client.py +++ b/Starcraft2Client.py @@ -12,21 +12,9 @@ import typing import queue from pathlib import Path -import nest_asyncio -import sc2 -from sc2.bot_ai import BotAI -from sc2.data import Race -from sc2.main import run_game -from sc2.player import Bot - -import NetUtils -from MultiServer import mark_raw +# CommonClient import first to trigger ModuleUpdater +from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser from Utils import init_logging, is_windows -from worlds.sc2wol import SC2WoLWorld -from worlds.sc2wol.Items import lookup_id_to_name, item_table, ItemData, type_flaggroups -from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET -from worlds.sc2wol.MissionTables import lookup_id_to_mission -from worlds.sc2wol.Regions import MissionInfo if __name__ == "__main__": init_logging("SC2Client", exception_logger="Client") @@ -34,10 +22,21 @@ if __name__ == "__main__": logger = logging.getLogger("Client") sc2_logger = logging.getLogger("Starcraft2") -import colorama +import nest_asyncio +import sc2 +from sc2.bot_ai import BotAI +from sc2.data import Race +from sc2.main import run_game +from sc2.player import Bot +from worlds.sc2wol import SC2WoLWorld +from worlds.sc2wol.Items import lookup_id_to_name, item_table, ItemData, type_flaggroups +from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET +from worlds.sc2wol.MissionTables import lookup_id_to_mission +from worlds.sc2wol.Regions import MissionInfo -from NetUtils import ClientStatus, RawJSONtoTextParser -from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser +import colorama +from NetUtils import ClientStatus, NetworkItem, RawJSONtoTextParser +from MultiServer import mark_raw nest_asyncio.apply() max_bonus: int = 8 @@ -115,12 +114,40 @@ class StarcraftClientProcessor(ClientCommandProcessor): """Manually set the SC2 install directory (if the automatic detection fails).""" if path: os.environ["SC2PATH"] = path - check_mod_install() + is_mod_installed_correctly() return True else: sc2_logger.warning("When using set_path, you must type the path to your SC2 install directory.") return False + def _cmd_download_data(self, force: bool = False) -> bool: + """Download the most recent release of the necessary files for playing SC2 with + Archipelago. force should be True or False. force=True will overwrite your files.""" + if "SC2PATH" not in os.environ: + check_game_install_path() + + if os.path.exists(os.environ["SC2PATH"]+"ArchipelagoSC2Version.txt"): + with open(os.environ["SC2PATH"]+"ArchipelagoSC2Version.txt", "r") as f: + current_ver = f.read() + else: + current_ver = None + + tempzip, version = download_latest_release_zip('TheCondor07', 'Starcraft2ArchipelagoData', current_version=current_ver, force_download=force) + + if tempzip != '': + try: + import zipfile + zipfile.ZipFile(tempzip).extractall(path=os.environ["SC2PATH"]) + sc2_logger.info(f"Download complete. Version {version} installed.") + with open(os.environ["SC2PATH"]+"ArchipelagoSC2Version.txt", "w") as f: + f.write(version) + finally: + os.remove(tempzip) + else: + sc2_logger.warning("Download aborted/failed. Read the log for more information.") + return False + return True + class SC2Context(CommonContext): command_processor = StarcraftClientProcessor @@ -159,10 +186,13 @@ class SC2Context(CommonContext): self.build_location_to_mission_mapping() - # 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() + # Looks for the required maps and mods for SC2. Runs check_game_install_path. + is_mod_installed_correctly() + if os.path.exists(os.environ["SC2PATH"] + "ArchipelagoSC2Version.txt"): + with open(os.environ["SC2PATH"] + "ArchipelagoSC2Version.txt", "r") as f: + current_ver = f.read() + if is_mod_update_available("TheCondor07", "Starcraft2ArchipelagoData", current_ver): + sc2_logger.info("NOTICE: Update for required files found. Run /download_data to install.") def on_print_json(self, args: dict): # goes to this world @@ -357,8 +387,9 @@ class SC2Context(CommonContext): self.ui = SC2Manager(self) self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") - - Builder.load_file(Utils.local_path(os.path.dirname(SC2WoLWorld.__file__), "Starcraft2.kv")) + import pkgutil + data = pkgutil.get_data(SC2WoLWorld.__module__, "Starcraft2.kv").decode() + Builder.load_string(data) async def shutdown(self): await super(SC2Context, self).shutdown() @@ -440,8 +471,8 @@ wol_default_categories = [ ] -def calculate_items(items: typing.List[NetUtils.NetworkItem]) -> typing.List[int]: - network_item: NetUtils.NetworkItem +def calculate_items(items: typing.List[NetworkItem]) -> typing.List[int]: + network_item: NetworkItem accumulators: typing.List[int] = [0 for _ in type_flaggroups] for network_item in items: @@ -820,18 +851,53 @@ def check_game_install_path() -> bool: 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 +def is_mod_installed_correctly() -> bool: + """Searches for all required files.""" + if "SC2PATH" not in os.environ: + check_game_install_path() + + mapdir = os.environ['SC2PATH'] / Path('Maps/ArchipelagoCampaign') + modfile = os.environ["SC2PATH"] / Path("Mods/Archipelago.SC2Mod") + wol_required_maps = [ + "ap_thanson01.SC2Map", "ap_thanson02.SC2Map", "ap_thanson03a.SC2Map", "ap_thanson03b.SC2Map", + "ap_thorner01.SC2Map", "ap_thorner02.SC2Map", "ap_thorner03.SC2Map", "ap_thorner04.SC2Map", "ap_thorner05s.SC2Map", + "ap_traynor01.SC2Map", "ap_traynor02.SC2Map", "ap_traynor03.SC2Map", + "ap_ttosh01.SC2Map", "ap_ttosh02.SC2Map", "ap_ttosh03a.SC2Map", "ap_ttosh03b.SC2Map", + "ap_ttychus01.SC2Map", "ap_ttychus02.SC2Map", "ap_ttychus03.SC2Map", "ap_ttychus04.SC2Map", "ap_ttychus05.SC2Map", + "ap_tvalerian01.SC2Map", "ap_tvalerian02a.SC2Map", "ap_tvalerian02b.SC2Map", "ap_tvalerian03.SC2Map", + "ap_tzeratul01.SC2Map", "ap_tzeratul02.SC2Map", "ap_tzeratul03.SC2Map", "ap_tzeratul04.SC2Map" + ] + needs_files = False + + # Check for maps. + missing_maps = [] + for mapfile in wol_required_maps: + if not os.path.isfile(mapdir / mapfile): + missing_maps.append(mapfile) + if len(missing_maps) >= 19: + sc2_logger.warning(f"All map files missing from {mapdir}.") + needs_files = True + elif len(missing_maps) > 0: + for map in missing_maps: + sc2_logger.debug(f"Missing {map} from {mapdir}.") + sc2_logger.warning(f"Missing {len(missing_maps)} map files.") + needs_files = True + else: # Must be no maps missing + sc2_logger.info(f"All maps found in {mapdir}.") + + # Check for mods. + if os.path.isfile(modfile): + sc2_logger.info(f"Archipelago mod found at {modfile}.") + else: + sc2_logger.warning(f"Archipelago mod could not be found at {modfile}.") + needs_files = True + + # Final verdict. + if needs_files: + sc2_logger.warning(f"Required files are missing. Run /download_data to acquire them.") + return False + else: + return True class DllDirectory: @@ -870,6 +936,64 @@ class DllDirectory: return False +def download_latest_release_zip(owner: str, repo: str, current_version: str = None, force_download=False) -> (str, str): + """Downloads the latest release of a GitHub repo to the current directory as a .zip file.""" + import requests + + headers = {"Accept": 'application/vnd.github.v3+json'} + url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest" + + r1 = requests.get(url, headers=headers) + if r1.status_code == 200: + latest_version = r1.json()["tag_name"] + sc2_logger.info(f"Latest version: {latest_version}.") + else: + sc2_logger.warning(f"Status code: {r1.status_code}") + sc2_logger.warning(f"Failed to reach GitHub. Could not find download link.") + sc2_logger.warning(f"text: {r1.text}") + return "", current_version + + if (force_download is False) and (current_version == latest_version): + sc2_logger.info("Latest version already installed.") + return "", current_version + + sc2_logger.info(f"Attempting to download version {latest_version} of {repo}.") + download_url = r1.json()["assets"][0]["browser_download_url"] + + r2 = requests.get(download_url, headers=headers) + if r2.status_code == 200: + with open(f"{repo}.zip", "wb") as fh: + fh.write(r2.content) + sc2_logger.info(f"Successfully downloaded {repo}.zip.") + return f"{repo}.zip", latest_version + else: + sc2_logger.warning(f"Status code: {r2.status_code}") + sc2_logger.warning("Download failed.") + sc2_logger.warning(f"text: {r2.text}") + return "", current_version + + +def is_mod_update_available(owner: str, repo: str, current_version: str) -> bool: + import requests + + headers = {"Accept": 'application/vnd.github.v3+json'} + url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest" + + r1 = requests.get(url, headers=headers) + if r1.status_code == 200: + latest_version = r1.json()["tag_name"] + if current_version != latest_version: + return True + else: + return False + + else: + sc2_logger.warning(f"Failed to reach GitHub while checking for updates.") + sc2_logger.warning(f"Status code: {r1.status_code}") + sc2_logger.warning(f"text: {r1.text}") + return False + + if __name__ == '__main__': colorama.init() asyncio.run(main()) diff --git a/Utils.py b/Utils.py index c362131d75..d28834b698 100644 --- a/Utils.py +++ b/Utils.py @@ -11,6 +11,8 @@ import io import collections import importlib import logging +from typing import BinaryIO + from yaml import load, load_all, dump, SafeLoader try: @@ -217,8 +219,11 @@ def get_public_ipv6() -> str: return ip +OptionsType = typing.Dict[str, typing.Dict[str, typing.Any]] + + @cache_argsless -def get_default_options() -> dict: +def get_default_options() -> OptionsType: # Refer to host.yaml for comments as to what all these options mean. options = { "general_options": { @@ -285,12 +290,28 @@ def get_default_options() -> dict: "sni": "SNI", "rom_start": True, }, + "smw_options": { + "rom_file": "Super Mario World (USA).sfc", + "sni": "SNI", + "rom_start": True, + }, + "zillion_options": { + "rom_file": "Zillion (UE) [!].sms", + # RetroArch doesn't make it easy to launch a game from the command line. + # You have to know the path to the emulator core library on the user's computer. + "rom_start": "retroarch", + }, + "pokemon_rb_options": { + "red_rom_file": "Pokemon Red (UE) [S][!].gb", + "blue_rom_file": "Pokemon Blue (UE) [S][!].gb", + "rom_start": True + } } return options -def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict: +def update_options(src: dict, dest: dict, filename: str, keys: list) -> OptionsType: for key, value in src.items(): new_keys = keys.copy() new_keys.append(key) @@ -310,9 +331,9 @@ def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict: @cache_argsless -def get_options() -> dict: +def get_options() -> OptionsType: filenames = ("options.yaml", "host.yaml") - locations = [] + locations: typing.List[str] = [] 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] @@ -353,7 +374,7 @@ def persistent_load() -> typing.Dict[str, dict]: return storage -def get_adjuster_settings(game_name: str): +def get_adjuster_settings(game_name: str) -> typing.Dict[str, typing.Any]: adjuster_settings = persistent_load().get("adjuster", {}).get(game_name, {}) return adjuster_settings @@ -392,7 +413,8 @@ class RestrictedUnpickler(pickle.Unpickler): # Options and Plando are unpickled by WebHost -> Generate if module == "worlds.generic" and name in {"PlandoItem", "PlandoConnection"}: return getattr(self.generic_properties_module, name) - if module.endswith("Options"): + # pep 8 specifies that modules should have "all-lowercase names" (options, not Options) + if module.lower().endswith("options"): if module == "Options": mod = self.options_module else: @@ -623,3 +645,11 @@ def title_sorted(data: typing.Sequence, key=None, ignore: typing.Set = frozenset else: return element.lower() return sorted(data, key=lambda i: sorter(key(i)) if key else sorter(i)) + + +def read_snes_rom(stream: BinaryIO, strip_header: bool = True) -> bytearray: + """Reads rom into bytearray and optionally strips off any smc header""" + buffer = bytearray(stream.read()) + if strip_header and len(buffer) % 0x400 == 0x200: + return buffer[0x200:] + return buffer diff --git a/WebHost.py b/WebHost.py index 4c07e8b185..ce8443dbd9 100644 --- a/WebHost.py +++ b/WebHost.py @@ -1,5 +1,4 @@ import os -import sys import multiprocessing import logging import typing diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index b7bf4e38d1..c979b40089 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -1,16 +1,15 @@ -import os -import uuid import base64 +import os import socket +import uuid -from pony.flask import Pony from flask import Flask from flask_caching import Cache from flask_compress import Compress +from pony.flask import Pony from werkzeug.routing import BaseConverter from Utils import title_sorted -from .models import * UPLOAD_FOLDER = os.path.relpath('uploads') LOGS_FOLDER = os.path.relpath('logs') @@ -73,8 +72,10 @@ 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 + import worlds.AutoWorld + import worlds.Files + app.jinja_env.filters['supports_apdeltapatch'] = lambda game_name: \ + game_name in worlds.Files.AutoPatchRegister.patch_types from WebHostLib.customserver import run_server_process # to trigger app routing picking up on it diff --git a/WebHostLib/api/__init__.py b/WebHostLib/api/__init__.py index 80c60a093a..bac25255c2 100644 --- a/WebHostLib/api/__init__.py +++ b/WebHostLib/api/__init__.py @@ -1,11 +1,11 @@ """API endpoints package.""" -from uuid import UUID from typing import List, Tuple +from uuid import UUID from flask import Blueprint, abort -from ..models import Room, Seed from .. import cache +from ..models import Room, Seed api_endpoints = Blueprint('api', __name__, url_prefix="/api") diff --git a/WebHostLib/api/generate.py b/WebHostLib/api/generate.py index faad50e1c6..0f090039fb 100644 --- a/WebHostLib/api/generate.py +++ b/WebHostLib/api/generate.py @@ -1,15 +1,15 @@ import json import pickle - from uuid import UUID -from . import api_endpoints from flask import request, session, url_for from pony.orm import commit -from WebHostLib import app, Generation, STATE_QUEUED, Seed, STATE_ERROR +from WebHostLib import app from WebHostLib.check import get_yaml_data, roll_options from WebHostLib.generate import get_meta +from WebHostLib.models import Generation, STATE_QUEUED, Seed, STATE_ERROR +from . import api_endpoints @api_endpoints.route('/generate', methods=['POST']) diff --git a/WebHostLib/api/user.py b/WebHostLib/api/user.py index 5c563dafac..116d3afa22 100644 --- a/WebHostLib/api/user.py +++ b/WebHostLib/api/user.py @@ -1,6 +1,7 @@ from flask import session, jsonify +from pony.orm import select -from WebHostLib.models import * +from WebHostLib.models import Room, Seed from . import api_endpoints, get_players diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py index 77445eadea..8de73ba11b 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -1,14 +1,14 @@ from __future__ import annotations -import logging -import json -import multiprocessing -import threading -from datetime import timedelta, datetime -import sys -import typing -import time +import json +import logging +import multiprocessing import os +import sys +import threading +import time +import typing +from datetime import timedelta, datetime from pony.orm import db_session, select, commit diff --git a/WebHostLib/check.py b/WebHostLib/check.py index a4e2f518db..cd45dff440 100644 --- a/WebHostLib/check.py +++ b/WebHostLib/check.py @@ -1,7 +1,7 @@ import zipfile from typing import * -from flask import request, flash, redirect, url_for, session, render_template +from flask import request, flash, redirect, url_for, render_template from WebHostLib import app diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 6272633f4e..59eb1d3de2 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -10,13 +10,14 @@ import random import socket import threading import time + import websockets +from pony.orm import db_session, commit, select import Utils -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, cache_argsless +from .models import Room, Command, db class CustomClientMessageProcessor(ClientMessageProcessor): diff --git a/WebHostLib/downloads.py b/WebHostLib/downloads.py index c3a373c2e9..053fa35ce4 100644 --- a/WebHostLib/downloads.py +++ b/WebHostLib/downloads.py @@ -1,12 +1,13 @@ -import zipfile import json +import zipfile from io import BytesIO from flask import send_file, Response, render_template from pony.orm import select -from Patch import update_patch_data, preferred_endings, AutoPatchRegister -from WebHostLib import app, Slot, Room, Seed, cache +from worlds.Files import AutoPatchRegister +from . import app, cache +from .models import Slot, Room, Seed @app.route("/dl_patch//") @@ -41,12 +42,7 @@ def download_patch(room_id, patch_id): new_file.seek(0) 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, download_name=fname) + return "Old Patch file, no longer compatible." @app.route("/dl_spoiler/") @@ -79,6 +75,8 @@ def download_slot_file(room_id, player_id: int): fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apz5" elif slot_data.game == "VVVVVV": fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apv6" + elif slot_data.game == "Zillion": + fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apzl" 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": diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index 15067e131b..a9b1cb7685 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -1,23 +1,23 @@ -import os -import tempfile -import random import json +import os +import pickle +import random +import tempfile import zipfile from collections import Counter from typing import Dict, Optional, Any -from Utils import __version__ from flask import request, flash, redirect, url_for, session, render_template +from pony.orm import commit, db_session -from worlds.alttp.EntranceRandomizer import parse_arguments -from Main import main as ERmain from BaseClasses import seeddigits, get_seed from Generate import handle_name, PlandoSettings -import pickle - -from .models import Generation, STATE_ERROR, STATE_QUEUED, commit, db_session, Seed, UUID +from Main import main as ERmain +from Utils import __version__ from WebHostLib import app +from worlds.alttp.EntranceRandomizer import parse_arguments from .check import get_yaml_data, roll_options +from .models import Generation, STATE_ERROR, STATE_QUEUED, Seed, UUID from .upload import upload_zip_to_db diff --git a/WebHostLib/landing.py b/WebHostLib/landing.py index 377758ed70..14e90cc28d 100644 --- a/WebHostLib/landing.py +++ b/WebHostLib/landing.py @@ -1,7 +1,11 @@ +from datetime import timedelta, datetime + from flask import render_template +from pony.orm import count + from WebHostLib import app, cache -from .models import * -from datetime import timedelta +from .models import Room, Seed + @app.route('/', methods=['GET', 'POST']) @cache.cached(timeout=300) # cache has to appear under app route for caching to work diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py index 03cd03b624..f78ec3926d 100644 --- a/WebHostLib/misc.py +++ b/WebHostLib/misc.py @@ -3,10 +3,11 @@ import os import jinja2.exceptions from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory +from pony.orm import count, commit, db_session -from .models import count, Seed, commit, Room, db_session, Command, UUID, uuid4 from worlds.AutoWorld import AutoWorldRegister from . import app, cache +from .models import Seed, Room, Command, UUID, uuid4 def get_world_theme(game_name: str): @@ -151,7 +152,7 @@ def favicon(): @app.route('/discord') def discord(): - return redirect("https://discord.gg/archipelago") + return redirect("https://discord.gg/8Z65BR2") @app.route('/datapackage') diff --git a/WebHostLib/models.py b/WebHostLib/models.py index 70f0318f85..0dc67a5196 100644 --- a/WebHostLib/models.py +++ b/WebHostLib/models.py @@ -1,6 +1,6 @@ from datetime import datetime from uuid import UUID, uuid4 -from pony.orm import * +from pony.orm import Database, PrimaryKey, Required, Set, Optional, buffer, LongStr db = Database() diff --git a/WebHostLib/options.py b/WebHostLib/options.py index daa742d90e..fc13e2c29e 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -1,13 +1,14 @@ +import json import logging import os -from Utils import __version__, local_path -from jinja2 import Template -import yaml -import json import typing -from worlds.AutoWorld import AutoWorldRegister +import yaml +from jinja2 import Template + import Options +from Utils import __version__, local_path +from worlds.AutoWorld import AutoWorldRegister handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints", "exclude_locations"} @@ -15,7 +16,13 @@ handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hin def create(): target_folder = local_path("WebHostLib", "static", "generated") - os.makedirs(os.path.join(target_folder, "configs"), exist_ok=True) + yaml_folder = os.path.join(target_folder, "configs") + os.makedirs(yaml_folder, exist_ok=True) + + for file in os.listdir(yaml_folder): + full_path: str = os.path.join(yaml_folder, file) + if os.path.isfile(full_path): + os.unlink(full_path) def dictify_range(option: typing.Union[Options.Range, Options.SpecialRange]): data = {} @@ -25,9 +32,12 @@ def create(): data.update({ option.range_start: 0, option.range_end: 0, - "random": 0, "random-low": 0, "random-high": 0, option.default: 50 }) + for sub_option in {"random", "random-low", "random-high"}: + if sub_option != option.default: + data[sub_option] = 0 + notes = { special: "minimum value without special meaning", option.range_start: "minimum value", @@ -43,11 +53,6 @@ def create(): return data, notes - def default_converter(default_value): - if isinstance(default_value, (set, frozenset)): - return list(default_value) - return default_value - def get_html_doc(option_type: type(Options.Option)) -> str: if not option_type.__doc__: return "Please document me!" @@ -64,13 +69,16 @@ def create(): for game_name, world in AutoWorldRegister.world_types.items(): - all_options = {**Options.per_game_common_options, **world.option_definitions} + all_options: typing.Dict[str, Options.AssembleOptions] = { + **Options.per_game_common_options, + **world.option_definitions + } with open(local_path("WebHostLib", "templates", "options.yaml")) as f: file_data = f.read() res = Template(file_data).render( options=all_options, __version__=__version__, game=game_name, yaml_dump=yaml.dump, - dictify_range=dictify_range, default_converter=default_converter, + dictify_range=dictify_range, ) del file_data diff --git a/WebHostLib/static/assets/faq.js b/WebHostLib/static/assets/faq.js index 35f46e1628..1bf5e5a659 100644 --- a/WebHostLib/static/assets/faq.js +++ b/WebHostLib/static/assets/faq.js @@ -26,24 +26,22 @@ window.addEventListener('load', () => { adjustHeaderWidth(); // Reset the id of all header divs to something nicer - const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6')); - const scrollTargetIndex = window.location.href.search(/#[A-z0-9-_]*$/); - for (let i=0; i < headers.length; i++){ - const headerId = headers[i].innerText.replace(/[ ]/g,'-').toLowerCase() - headers[i].setAttribute('id', headerId); - headers[i].addEventListener('click', () => - window.location.href = window.location.href.substring(0, scrollTargetIndex) + `#${headerId}`); + for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) { + const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase(); + header.setAttribute('id', headerId); + header.addEventListener('click', () => { + window.location.hash = `#${headerId}`; + header.scrollIntoView(); + }); } // Manually scroll the user to the appropriate header if anchor navigation is used - if (scrollTargetIndex > -1) { - try{ - const scrollTarget = window.location.href.substring(scrollTargetIndex + 1); - document.getElementById(scrollTarget).scrollIntoView({ behavior: "smooth" }); - } catch(error) { - console.error(error); + document.fonts.ready.finally(() => { + if (window.location.hash) { + const scrollTarget = document.getElementById(window.location.hash.substring(1)); + scrollTarget?.scrollIntoView(); } - } + }); }).catch((error) => { console.error(error); tutorialWrapper.innerHTML = diff --git a/WebHostLib/static/assets/faq/faq_en.md b/WebHostLib/static/assets/faq/faq_en.md index cd144d7eff..6ad50a50f6 100644 --- a/WebHostLib/static/assets/faq/faq_en.md +++ b/WebHostLib/static/assets/faq/faq_en.md @@ -46,7 +46,7 @@ the website is not required to generate them. ## How do I get started? If you are ready to start randomizing games, or want to start playing your favorite randomizer with others, please join -our discord server at the [Archipelago Discord](https://discord.gg/archipelago). There are always people ready to answer +our discord server at the [Archipelago Discord](https://discord.gg/8Z65BR2). There are always people ready to answer any questions you might have. ## What are some common terms I should know? diff --git a/WebHostLib/static/assets/gameInfo.js b/WebHostLib/static/assets/gameInfo.js index 8a9c8b3f22..b8c56905a5 100644 --- a/WebHostLib/static/assets/gameInfo.js +++ b/WebHostLib/static/assets/gameInfo.js @@ -26,24 +26,22 @@ window.addEventListener('load', () => { adjustHeaderWidth(); // Reset the id of all header divs to something nicer - const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6')); - const scrollTargetIndex = window.location.href.search(/#[A-z0-9-_]*$/); - for (let i=0; i < headers.length; i++){ - const headerId = headers[i].innerText.replace(/[ ]/g,'-').toLowerCase() - headers[i].setAttribute('id', headerId); - headers[i].addEventListener('click', () => - window.location.href = window.location.href.substring(0, scrollTargetIndex) + `#${headerId}`); + for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) { + const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase(); + header.setAttribute('id', headerId); + header.addEventListener('click', () => { + window.location.hash = `#${headerId}`; + header.scrollIntoView(); + }); } // Manually scroll the user to the appropriate header if anchor navigation is used - if (scrollTargetIndex > -1) { - try{ - const scrollTarget = window.location.href.substring(scrollTargetIndex + 1); - document.getElementById(scrollTarget).scrollIntoView({ behavior: "smooth" }); - } catch(error) { - console.error(error); + document.fonts.ready.finally(() => { + if (window.location.hash) { + const scrollTarget = document.getElementById(window.location.hash.substring(1)); + scrollTarget?.scrollIntoView(); } - } + }); }).catch((error) => { console.error(error); gameInfo.innerHTML = diff --git a/WebHostLib/static/assets/glossary.js b/WebHostLib/static/assets/glossary.js index 44012d699f..04a2920086 100644 --- a/WebHostLib/static/assets/glossary.js +++ b/WebHostLib/static/assets/glossary.js @@ -26,24 +26,22 @@ window.addEventListener('load', () => { adjustHeaderWidth(); // Reset the id of all header divs to something nicer - const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6')); - const scrollTargetIndex = window.location.href.search(/#[A-z0-9-_]*$/); - for (let i=0; i < headers.length; i++){ - const headerId = headers[i].innerText.replace(/[ ]/g,'-').toLowerCase() - headers[i].setAttribute('id', headerId); - headers[i].addEventListener('click', () => - window.location.href = window.location.href.substring(0, scrollTargetIndex) + `#${headerId}`); + for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) { + const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase(); + header.setAttribute('id', headerId); + header.addEventListener('click', () => { + window.location.hash = `#${headerId}`; + header.scrollIntoView(); + }); } // Manually scroll the user to the appropriate header if anchor navigation is used - if (scrollTargetIndex > -1) { - try{ - const scrollTarget = window.location.href.substring(scrollTargetIndex + 1); - document.getElementById(scrollTarget).scrollIntoView({ behavior: "smooth" }); - } catch(error) { - console.error(error); + document.fonts.ready.finally(() => { + if (window.location.hash) { + const scrollTarget = document.getElementById(window.location.hash.substring(1)); + scrollTarget?.scrollIntoView(); } - } + }); }).catch((error) => { console.error(error); tutorialWrapper.innerHTML = diff --git a/WebHostLib/static/assets/tutorial.js b/WebHostLib/static/assets/tutorial.js index 23d2f076fc..1db08d85b3 100644 --- a/WebHostLib/static/assets/tutorial.js +++ b/WebHostLib/static/assets/tutorial.js @@ -27,25 +27,28 @@ window.addEventListener('load', () => { tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results); adjustHeaderWidth(); + const title = document.querySelector('h1') + if (title) { + document.title = title.textContent; + } + // Reset the id of all header divs to something nicer - const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6')); - const scrollTargetIndex = window.location.href.search(/#[A-z0-9-_]*$/); - for (let i=0; i < headers.length; i++){ - const headerId = headers[i].innerText.replace(/[ ]/g,'-').toLowerCase() - headers[i].setAttribute('id', headerId); - headers[i].addEventListener('click', () => - window.location.href = window.location.href.substring(0, scrollTargetIndex) + `#${headerId}`); + for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) { + const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase(); + header.setAttribute('id', headerId); + header.addEventListener('click', () => { + window.location.hash = `#${headerId}`; + header.scrollIntoView(); + }); } // Manually scroll the user to the appropriate header if anchor navigation is used - if (scrollTargetIndex > -1) { - try{ - const scrollTarget = window.location.href.substring(scrollTargetIndex + 1); - document.getElementById(scrollTarget).scrollIntoView({ behavior: "smooth" }); - } catch(error) { - console.error(error); + document.fonts.ready.finally(() => { + if (window.location.hash) { + const scrollTarget = document.getElementById(window.location.hash.substring(1)); + scrollTarget?.scrollIntoView(); } - } + }); }).catch((error) => { console.error(error); tutorialWrapper.innerHTML = diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index a438d0c64a..da4d60fcad 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -78,13 +78,16 @@ const createDefaultSettings = (settingData) => { break; case 'range': case 'special_range': - for (let i = setting.min; i <= setting.max; ++i){ - newSettings[game][gameSetting][i] = - (setting.hasOwnProperty('defaultValue') && setting.defaultValue === i) ? 25 : 0; - } + newSettings[game][gameSetting][setting.min] = 0; + newSettings[game][gameSetting][setting.max] = 0; newSettings[game][gameSetting]['random'] = 0; newSettings[game][gameSetting]['random-low'] = 0; newSettings[game][gameSetting]['random-high'] = 0; + if (setting.hasOwnProperty('defaultValue')) { + newSettings[game][gameSetting][setting.defaultValue] = 25; + } else { + newSettings[game][gameSetting][setting.min] = 25; + } break; case 'items-list': @@ -401,11 +404,17 @@ const buildWeightedSettingsDiv = (game, settings) => { tr.appendChild(tdDelete); rangeTbody.appendChild(tr); + + // Save new option to settings + range.dispatchEvent(new Event('change')); }); Object.keys(currentSettings[game][settingName]).forEach((option) => { - if (currentSettings[game][settingName][option] > 0) { - const tr = document.createElement('tr'); + // These options are statically generated below, and should always appear even if they are deleted + // from localStorage + if (['random-low', 'random', 'random-high'].includes(option)) { return; } + + const tr = document.createElement('tr'); const tdLeft = document.createElement('td'); tdLeft.classList.add('td-left'); tdLeft.innerText = option; @@ -439,14 +448,15 @@ const buildWeightedSettingsDiv = (game, settings) => { deleteButton.innerText = '❌'; deleteButton.addEventListener('click', () => { range.value = 0; - range.dispatchEvent(new Event('change')); + const changeEvent = new Event('change'); + changeEvent.action = 'rangeDelete'; + range.dispatchEvent(changeEvent); rangeTbody.removeChild(tr); }); tdDelete.appendChild(deleteButton); tr.appendChild(tdDelete); rangeTbody.appendChild(tr); - } }); } @@ -904,8 +914,12 @@ const updateGameSetting = (evt) => { const setting = evt.target.getAttribute('data-setting'); const option = evt.target.getAttribute('data-option'); document.getElementById(`${game}-${setting}-${option}`).innerText = evt.target.value; - options[game][setting][option] = isNaN(evt.target.value) ? - evt.target.value : parseInt(evt.target.value, 10); + console.log(event); + if (evt.action && evt.action === 'rangeDelete') { + delete options[game][setting][option]; + } else { + options[game][setting][option] = parseInt(evt.target.value, 10); + } localStorage.setItem('weighted-settings', JSON.stringify(options)); }; diff --git a/WebHostLib/static/styles/hostRoom.css b/WebHostLib/static/styles/hostRoom.css index 94ec24bfa6..827f74c04d 100644 --- a/WebHostLib/static/styles/hostRoom.css +++ b/WebHostLib/static/styles/hostRoom.css @@ -55,4 +55,6 @@ border: 1px solid #2a6c2f; border-radius: 6px; color: #000000; + overflow-y: auto; + max-height: 400px; } diff --git a/WebHostLib/static/styles/themes/base.css b/WebHostLib/static/styles/themes/base.css index 0dbdf5f6ea..fca65a51c1 100644 --- a/WebHostLib/static/styles/themes/base.css +++ b/WebHostLib/static/styles/themes/base.css @@ -1,5 +1,7 @@ html{ padding-top: 110px; + scroll-padding-top: 100px; + scroll-behavior: smooth; } #base-header{ diff --git a/WebHostLib/stats.py b/WebHostLib/stats.py index 54f5e598d1..e30ac82007 100644 --- a/WebHostLib/stats.py +++ b/WebHostLib/stats.py @@ -1,14 +1,14 @@ +import typing from collections import Counter, defaultdict from colorsys import hsv_to_rgb from datetime import datetime, timedelta, date from math import tau -import typing +from bokeh.colors import RGB from bokeh.embed import components 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 diff --git a/WebHostLib/templates/check.html b/WebHostLib/templates/check.html index 64f19b0f9c..04b51340b5 100644 --- a/WebHostLib/templates/check.html +++ b/WebHostLib/templates/check.html @@ -1,7 +1,6 @@ {% extends 'pageWrapper.html' %} {% block head %} - {{ super() }} Mystery Check Result diff --git a/WebHostLib/templates/generate.html b/WebHostLib/templates/generate.html index aa16a47d35..eff42700a7 100644 --- a/WebHostLib/templates/generate.html +++ b/WebHostLib/templates/generate.html @@ -1,7 +1,6 @@ {% extends 'pageWrapper.html' %} {% block head %} - {{ super() }} Generate Game diff --git a/WebHostLib/templates/hostGame.html b/WebHostLib/templates/hostGame.html index 55d155c74a..2bcb993af5 100644 --- a/WebHostLib/templates/hostGame.html +++ b/WebHostLib/templates/hostGame.html @@ -1,7 +1,6 @@ {% extends 'pageWrapper.html' %} {% block head %} - {{ super() }} Upload Multidata diff --git a/WebHostLib/templates/options.yaml b/WebHostLib/templates/options.yaml index 11009106b8..3c21ecfb1d 100644 --- a/WebHostLib/templates/options.yaml +++ b/WebHostLib/templates/options.yaml @@ -43,14 +43,18 @@ requires: {%- if option.range_start is defined and option.range_start is number %} {{- range_option(option) -}} {%- elif option.options -%} - {%- for suboption_option_id, sub_option_name in option.name_lookup.items() %} + {%- for suboption_option_id, sub_option_name in option.name_lookup.items() %} {{ sub_option_name }}: {% if suboption_option_id == option.default %}50{% else %}0{% endif %} - {%- endfor -%} - {% if option.default == "random" %} - random: 50 - {%- endif -%} + {%- endfor -%} + {% if option.name_lookup[option.default] not in option.options %} + {{ option.default }}: 50 + {%- endif -%} + {%- elif option.default is string %} + {{ option.default }}: 50 + {%- elif option.default is iterable and option.default is not mapping %} + {{ option.default | list }} {%- else %} - {{ yaml_dump(default_converter(option.default)) | indent(4, first=False) }} - {%- endif -%} + {{ yaml_dump(option.default) | indent(4, first=false) }} + {%- endif -%} {%- endfor %} {% if not options %}{}{% endif %} diff --git a/WebHostLib/templates/startPlaying.html b/WebHostLib/templates/startPlaying.html index 157d6de243..436af3df07 100644 --- a/WebHostLib/templates/startPlaying.html +++ b/WebHostLib/templates/startPlaying.html @@ -1,7 +1,6 @@ {% extends 'pageWrapper.html' %} {% block head %} - {{ super() }} Start Playing {% endblock %} diff --git a/WebHostLib/templates/timespinnerTracker.html b/WebHostLib/templates/timespinnerTracker.html index bd589e1068..82565316ab 100644 --- a/WebHostLib/templates/timespinnerTracker.html +++ b/WebHostLib/templates/timespinnerTracker.html @@ -41,7 +41,7 @@ {% endif %} - {% if 'FacebookMode' in options %} + {% if 'EyeSpy' in options %} {% else %} diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index fb5df81c9a..c28fb84ee8 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -1,18 +1,19 @@ import collections +import datetime import typing from typing import Counter, Optional, Dict, Any, Tuple +from uuid import UUID from flask import render_template from werkzeug.exceptions import abort -import datetime -from uuid import UUID -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 Context from NetUtils import SlotType +from Utils import restricted_loads +from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name +from worlds.alttp import Items +from . import app, cache +from .models import Room alttp_icons = { "Blue Shield": r"https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png", diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py index 00825df47b..22e1353fbe 100644 --- a/WebHostLib/upload.py +++ b/WebHostLib/upload.py @@ -1,19 +1,19 @@ -import typing -import zipfile -import lzma -import json import base64 -import MultiServer +import json +import typing import uuid +import zipfile from io import BytesIO from flask import request, flash, redirect, url_for, session, render_template from pony.orm import flush, select -from WebHostLib import app, Seed, Room, Slot -from Utils import parse_yaml, VersionException, __version__ -from Patch import preferred_endings, AutoPatchRegister +import MultiServer from NetUtils import NetworkSlot, SlotType +from Utils import VersionException, __version__ +from worlds.Files import AutoPatchRegister +from . import app +from .models import Seed, Room, Slot banned_zip_contents = (".sfc",) @@ -22,7 +22,7 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s if not owner: owner = session["_id"] infolist = zfile.infolist() - slots = set() + slots: typing.Set[Slot] = set() spoiler = "" multidata = None for file in infolist: @@ -38,17 +38,6 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s player_name=patch.player_name, player_id=patch.player, game=patch.game)) - elif file.filename.endswith(tuple(preferred_endings.values())): - data = zfile.open(file, "r").read() - yaml_data = parse_yaml(lzma.decompress(data).decode("utf-8-sig")) - if yaml_data["version"] < 2: - return "Old format cannot be uploaded (outdated .apbp)" - metadata = yaml_data["meta"] - - slots.add(Slot(data=data, - player_name=metadata["player_name"], - player_id=metadata["player_id"], - game=yaml_data["game"])) elif file.filename.endswith(".apmc"): data = zfile.open(file, "r").read() diff --git a/ZillionClient.py b/ZillionClient.py new file mode 100644 index 0000000000..dee5c2b756 --- /dev/null +++ b/ZillionClient.py @@ -0,0 +1,358 @@ +import asyncio +import base64 +import platform +from typing import Any, Coroutine, Dict, Optional, Type, cast + +# CommonClient import first to trigger ModuleUpdater +from CommonClient import CommonContext, server_loop, gui_enabled, \ + ClientCommandProcessor, logger, get_base_parser +from NetUtils import ClientStatus +import Utils + +import colorama # type: ignore + +from zilliandomizer.zri.memory import Memory +from zilliandomizer.zri import events +from zilliandomizer.utils.loc_name_maps import id_to_loc +from zilliandomizer.options import Chars +from zilliandomizer.patch import RescueInfo + +from worlds.zillion.id_maps import make_id_to_others +from worlds.zillion.config import base_id + + +class ZillionCommandProcessor(ClientCommandProcessor): + ctx: "ZillionContext" + + def _cmd_sms(self) -> None: + """ Tell the client that Zillion is running in RetroArch. """ + logger.info("ready to look for game") + self.ctx.look_for_retroarch.set() + + +class ZillionContext(CommonContext): + game = "Zillion" + command_processor: Type[ClientCommandProcessor] = ZillionCommandProcessor + items_handling = 1 # receive items from other players + + from_game: "asyncio.Queue[events.EventFromGame]" + to_game: "asyncio.Queue[events.EventToGame]" + ap_local_count: int + """ local checks watched by server """ + next_item: int + """ index in `items_received` """ + ap_id_to_name: Dict[int, str] + ap_id_to_zz_id: Dict[int, int] + start_char: Chars = "JJ" + rescues: Dict[int, RescueInfo] = {} + loc_mem_to_id: Dict[int, int] = {} + got_slot_data: asyncio.Event + """ serves as a flag for whether I am logged in to the server """ + + look_for_retroarch: asyncio.Event + """ + There is a bug in Python in Windows + https://github.com/python/cpython/issues/91227 + that makes it so if I look for RetroArch before it's ready, + it breaks the asyncio udp transport system. + + As a workaround, we don't look for RetroArch until this event is set. + """ + + def __init__(self, + server_address: str, + password: str) -> None: + super().__init__(server_address, password) + self.from_game = asyncio.Queue() + self.to_game = asyncio.Queue() + self.got_slot_data = asyncio.Event() + + self.look_for_retroarch = asyncio.Event() + if platform.system() != "Windows": + # asyncio udp bug is only on Windows + self.look_for_retroarch.set() + + self.reset_game_state() + + def reset_game_state(self) -> None: + for _ in range(self.from_game.qsize()): + self.from_game.get_nowait() + for _ in range(self.to_game.qsize()): + self.to_game.get_nowait() + self.got_slot_data.clear() + + self.ap_local_count = 0 + self.next_item = 0 + self.ap_id_to_name = {} + self.ap_id_to_zz_id = {} + self.rescues = {} + self.loc_mem_to_id = {} + + self.locations_checked.clear() + self.missing_locations.clear() + self.checked_locations.clear() + self.finished_game = False + self.items_received.clear() + + # override + def on_deathlink(self, data: Dict[str, Any]) -> None: + self.to_game.put_nowait(events.DeathEventToGame()) + return super().on_deathlink(data) + + # override + async def server_auth(self, password_requested: bool = False) -> None: + if password_requested and not self.password: + await super().server_auth(password_requested) + if not self.auth: + logger.info('waiting for connection to game...') + return + logger.info("logging in to server...") + await self.send_connect() + + # override + def run_gui(self) -> None: + from kvui import GameManager + + class ZillionManager(GameManager): + logging_pairs = [ + ("Client", "Archipelago") + ] + base_title = "Archipelago Zillion Client" + + self.ui = ZillionManager(self) + run_co: Coroutine[Any, Any, None] = self.ui.async_run() # type: ignore + # kivy types missing + self.ui_task = asyncio.create_task(run_co, name="UI") + + def on_package(self, cmd: str, args: Dict[str, Any]) -> None: + if cmd == "Connected": + logger.info("logged in to Archipelago server") + if "slot_data" not in args: + logger.warn("`Connected` packet missing `slot_data`") + return + slot_data = args["slot_data"] + + if "start_char" not in slot_data: + logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `start_char`") + return + self.start_char = slot_data['start_char'] + if self.start_char not in {"Apple", "Champ", "JJ"}: + logger.warn("invalid Zillion `Connected` packet, " + f"`slot_data` `start_char` has invalid value: {self.start_char}") + + if "rescues" not in slot_data: + logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `rescues`") + return + rescues = slot_data["rescues"] + self.rescues = {} + for rescue_id, json_info in rescues.items(): + assert rescue_id in ("0", "1"), f"invalid rescue_id in Zillion slot_data: {rescue_id}" + # TODO: just take start_char out of the RescueInfo so there's no opportunity for a mismatch? + assert json_info["start_char"] == self.start_char, \ + f'mismatch in Zillion slot data: {json_info["start_char"]} {self.start_char}' + ri = RescueInfo(json_info["start_char"], + json_info["room_code"], + json_info["mask"]) + self.rescues[0 if rescue_id == "0" else 1] = ri + + if "loc_mem_to_id" not in slot_data: + logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `loc_mem_to_id`") + return + loc_mem_to_id = slot_data["loc_mem_to_id"] + self.loc_mem_to_id = {} + for mem_str, id_str in loc_mem_to_id.items(): + mem = int(mem_str) + id_ = int(id_str) + room_i = mem // 256 + assert 0 <= room_i < 74 + assert id_ in id_to_loc + self.loc_mem_to_id[mem] = id_ + + self.got_slot_data.set() + + payload = { + "cmd": "Get", + "keys": [f"zillion-{self.auth}-doors"] + } + asyncio.create_task(self.send_msgs([payload])) + elif cmd == "Retrieved": + if "keys" not in args: + logger.warning(f"invalid Retrieved packet to ZillionClient: {args}") + return + keys = cast(Dict[str, Optional[str]], args["keys"]) + doors_b64 = keys[f"zillion-{self.auth}-doors"] + if doors_b64: + logger.info("received door data from server") + doors = base64.b64decode(doors_b64) + self.to_game.put_nowait(events.DoorEventToGame(doors)) + + def process_from_game_queue(self) -> None: + if self.from_game.qsize(): + event_from_game = self.from_game.get_nowait() + if isinstance(event_from_game, events.AcquireLocationEventFromGame): + server_id = event_from_game.id + base_id + loc_name = id_to_loc[event_from_game.id] + self.locations_checked.add(server_id) + if server_id in self.missing_locations: + self.ap_local_count += 1 + n_locations = len(self.missing_locations) + len(self.checked_locations) - 1 # -1 to ignore win + logger.info(f'New Check: {loc_name} ({self.ap_local_count}/{n_locations})') + asyncio.create_task(self.send_msgs([ + {"cmd": 'LocationChecks', "locations": [server_id]} + ])) + else: + # This will happen a lot in Zillion, + # because all the key words are local and unwatched by the server. + logger.debug(f"DEBUG: {loc_name} not in missing") + elif isinstance(event_from_game, events.DeathEventFromGame): + asyncio.create_task(self.send_death()) + elif isinstance(event_from_game, events.WinEventFromGame): + if not self.finished_game: + asyncio.create_task(self.send_msgs([ + {"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL} + ])) + self.finished_game = True + elif isinstance(event_from_game, events.DoorEventFromGame): + if self.auth: + doors_b64 = base64.b64encode(event_from_game.doors).decode() + payload = { + "cmd": "Set", + "key": f"zillion-{self.auth}-doors", + "operations": [{"operation": "replace", "value": doors_b64}] + } + asyncio.create_task(self.send_msgs([payload])) + else: + logger.warning(f"WARNING: unhandled event from game {event_from_game}") + + def process_items_received(self) -> None: + if len(self.items_received) > self.next_item: + zz_item_ids = [self.ap_id_to_zz_id[item.item] for item in self.items_received] + for index in range(self.next_item, len(self.items_received)): + ap_id = self.items_received[index].item + from_name = self.player_names[self.items_received[index].player] + # TODO: colors in this text, like sni client? + logger.info(f'received {self.ap_id_to_name[ap_id]} from {from_name}') + self.to_game.put_nowait( + events.ItemEventToGame(zz_item_ids) + ) + self.next_item = len(self.items_received) + + +async def zillion_sync_task(ctx: ZillionContext) -> None: + logger.info("started zillion sync task") + + # to work around the Python bug where we can't check for RetroArch + if not ctx.look_for_retroarch.is_set(): + logger.info("Start Zillion in RetroArch, then use the /sms command to connect to it.") + await asyncio.wait(( + asyncio.create_task(ctx.look_for_retroarch.wait()), + asyncio.create_task(ctx.exit_event.wait()) + ), return_when=asyncio.FIRST_COMPLETED) + + last_log = "" + + def log_no_spam(msg: str) -> None: + nonlocal last_log + if msg != last_log: + last_log = msg + logger.info(msg) + + # to only show this message once per client run + help_message_shown = False + + with Memory(ctx.from_game, ctx.to_game) as memory: + while not ctx.exit_event.is_set(): + ram = await memory.read() + name = memory.get_player_name(ram).decode() + if len(name): + if name == ctx.auth: + # this is the name we know + if ctx.server and ctx.server.socket: # type: ignore + if memory.have_generation_info(): + log_no_spam("everything connected") + await memory.process_ram(ram) + ctx.process_from_game_queue() + ctx.process_items_received() + else: # no generation info + if ctx.got_slot_data.is_set(): + memory.set_generation_info(ctx.rescues, ctx.loc_mem_to_id) + ctx.ap_id_to_name, ctx.ap_id_to_zz_id, _ap_id_to_zz_item = \ + make_id_to_others(ctx.start_char) + ctx.next_item = 0 + ctx.ap_local_count = len(ctx.checked_locations) + else: # no slot data yet + asyncio.create_task(ctx.send_connect()) + log_no_spam("logging in to server...") + await asyncio.wait(( + ctx.got_slot_data.wait(), + ctx.exit_event.wait(), + asyncio.sleep(6) + ), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets + else: # server not connected + log_no_spam("waiting for server connection...") + else: # new game + log_no_spam("connected to new game") + await ctx.disconnect() + ctx.reset_server_state() + ctx.reset_game_state() + memory.reset_game_state() + + ctx.auth = name + asyncio.create_task(ctx.connect()) + await asyncio.wait(( + ctx.got_slot_data.wait(), + ctx.exit_event.wait(), + asyncio.sleep(6) + ), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets + else: # no name found in game + if not help_message_shown: + logger.info('In RetroArch, make sure "Settings > Network > Network Commands" is on.') + help_message_shown = True + log_no_spam("looking for connection to game...") + await asyncio.sleep(0.3) + + await asyncio.sleep(0.09375) + logger.info("zillion sync task ending") + + +async def main() -> None: + parser = get_base_parser() + parser.add_argument('diff_file', default="", type=str, nargs="?", + help='Path to a .apzl Archipelago Binary Patch file') + # SNI parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical']) + args = parser.parse_args() + print(args) + + if args.diff_file: + import Patch + logger.info("patch file was supplied - creating sms rom...") + meta, rom_file = Patch.create_rom_file(args.diff_file) + if "server" in meta: + args.connect = meta["server"] + logger.info(f"wrote rom file to {rom_file}") + + ctx = ZillionContext(args.connect, args.password) + if ctx.server_task is None: + ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") + + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + + sync_task = asyncio.create_task(zillion_sync_task(ctx)) + + await ctx.exit_event.wait() + + ctx.server_address = None + logger.debug("waiting for sync task to end") + await sync_task + logger.debug("sync task ended") + await ctx.shutdown() + + +if __name__ == "__main__": + Utils.init_logging("ZillionClient", exception_logger="Client") + + colorama.init() + asyncio.run(main()) + colorama.deinit() diff --git a/data/basepatch.apbp b/data/basepatch.apbp deleted file mode 100644 index 2a30d9f8c2..0000000000 Binary files a/data/basepatch.apbp and /dev/null differ diff --git a/data/basepatch.bsdiff4 b/data/basepatch.bsdiff4 new file mode 100644 index 0000000000..a578b248f5 Binary files /dev/null and b/data/basepatch.bsdiff4 differ diff --git a/data/lua/PKMN_RB/core.dll b/data/lua/PKMN_RB/core.dll new file mode 100644 index 0000000000..3e9569571a Binary files /dev/null and b/data/lua/PKMN_RB/core.dll differ diff --git a/data/lua/PKMN_RB/json.lua b/data/lua/PKMN_RB/json.lua new file mode 100644 index 0000000000..a1f6e4ede2 --- /dev/null +++ b/data/lua/PKMN_RB/json.lua @@ -0,0 +1,389 @@ +-- +-- json.lua +-- +-- Copyright (c) 2015 rxi +-- +-- This library is free software; you can redistribute it and/or modify it +-- under the terms of the MIT license. See LICENSE for details. +-- + +local json = { _version = "0.1.0" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +function error(err) + print(err) +end + +local escape_char_map = { + [ "\\" ] = "\\\\", + [ "\"" ] = "\\\"", + [ "\b" ] = "\\b", + [ "\f" ] = "\\f", + [ "\n" ] = "\\n", + [ "\r" ] = "\\r", + [ "\t" ] = "\\t", +} + +local escape_char_map_inv = { [ "\\/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return escape_char_map[c] or string.format("\\u%04x", c:byte()) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if val[1] ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + print("invalid table: sparse array") + print(n) + print("VAL:") + print(val) + print("STACK:") + print(stack) + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + --local line_count = 1 + --local col_count = 1 + --for i = 1, idx - 1 do + -- col_count = col_count + 1 + -- if str:sub(i, i) == "\n" then + -- line_count = line_count + 1 + -- col_count = 1 + -- end + -- end + -- emu.message( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(3, 6), 16 ) + local n2 = tonumber( s:sub(9, 12), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local has_unicode_escape = false + local has_surrogate_escape = false + local has_escape = false + local last + for j = i + 1, #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + end + + if last == 92 then -- "\\" (escape char) + if x == 117 then -- "u" (unicode escape sequence) + local hex = str:sub(j + 1, j + 5) + if not hex:find("%x%x%x%x") then + decode_error(str, j, "invalid unicode escape in string") + end + if hex:find("^[dD][89aAbB]") then + has_surrogate_escape = true + else + has_unicode_escape = true + end + else + local c = string.char(x) + if not escape_chars[c] then + decode_error(str, j, "invalid escape char '" .. c .. "' in string") + end + has_escape = true + end + last = nil + + elseif x == 34 then -- '"' (end of string) + local s = str:sub(i + 1, j - 1) + if has_surrogate_escape then + s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape) + end + if has_unicode_escape then + s = s:gsub("\\u....", parse_unicode_escape) + end + if has_escape then + s = s:gsub("\\.", escape_char_map_inv) + end + return s, j + 1 + + else + last = x + end + end + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + return ( parse(str, next_char(str, 1, space_chars, true)) ) +end + + +return json \ No newline at end of file diff --git a/data/lua/PKMN_RB/pkmn_rb.lua b/data/lua/PKMN_RB/pkmn_rb.lua new file mode 100644 index 0000000000..7518a5f12b --- /dev/null +++ b/data/lua/PKMN_RB/pkmn_rb.lua @@ -0,0 +1,238 @@ +local socket = require("socket") +local json = require('json') +local math = require('math') + +local STATE_OK = "Ok" +local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected" +local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made" +local STATE_UNINITIALIZED = "Uninitialized" + +local APIndex = 0x1A6E +local APItemAddress = 0x00FF +local EventFlagAddress = 0x1735 +local MissableAddress = 0x161A +local HiddenItemsAddress = 0x16DE +local RodAddress = 0x1716 +local InGame = 0x1A71 + +local ItemsReceived = nil +local playerName = nil +local seedName = nil + +local prevstate = "" +local curstate = STATE_UNINITIALIZED +local gbSocket = nil +local frame = 0 + +local u8 = nil +local wU8 = nil +local u16 + +--Sets correct memory access functions based on whether NesHawk or QuickNES is loaded +local function defineMemoryFunctions() + local memDomain = {} + local domains = memory.getmemorydomainlist() + --if domains[1] == "System Bus" then + -- --NesHawk + -- isNesHawk = true + -- memDomain["systembus"] = function() memory.usememorydomain("System Bus") end + -- memDomain["saveram"] = function() memory.usememorydomain("Battery RAM") end + -- memDomain["rom"] = function() memory.usememorydomain("PRG ROM") end + --elseif domains[1] == "WRAM" then + -- --QuickNES + -- memDomain["systembus"] = function() memory.usememorydomain("System Bus") end + -- memDomain["saveram"] = function() memory.usememorydomain("WRAM") end + -- memDomain["rom"] = function() memory.usememorydomain("PRG ROM") end + --end + memDomain["rom"] = function() memory.usememorydomain("ROM") end + memDomain["wram"] = function() memory.usememorydomain("WRAM") end + return memDomain +end + +local memDomain = defineMemoryFunctions() +u8 = memory.read_u8 +wU8 = memory.write_u8 +u16 = memory.read_u16_le +function uRange(address, bytes) + data = memory.readbyterange(address - 1, bytes + 1) + data[0] = nil + return data +end + + +function table.empty (self) + for _, _ in pairs(self) do + return false + end + return true +end + +function slice (tbl, s, e) + local pos, new = 1, {} + for i = s + 1, e do + new[pos] = tbl[i] + pos = pos + 1 + end + return new +end + +function processBlock(block) + if block == nil then + return + end + local itemsBlock = block["items"] + memDomain.wram() + if itemsBlock ~= nil then-- and u8(0x116B) ~= 0x00 then + -- print(itemsBlock) + ItemsReceived = itemsBlock + + end +end + +function difference(a, b) + local aa = {} + for k,v in pairs(a) do aa[v]=true end + for k,v in pairs(b) do aa[v]=nil end + local ret = {} + local n = 0 + for k,v in pairs(a) do + if aa[v] then n=n+1 ret[n]=v end + end + return ret +end + +function generateLocationsChecked() + memDomain.wram() + events = uRange(EventFlagAddress, 0x140) + missables = uRange(MissableAddress, 0x20) + hiddenitems = uRange(HiddenItemsAddress, 0x0E) + rod = u8(RodAddress) + + data = {} + + table.foreach(events, function(k, v) table.insert(data, v) end) + table.foreach(missables, function(k, v) table.insert(data, v) end) + table.foreach(hiddenitems, function(k, v) table.insert(data, v) end) + table.insert(data, rod) + + return data +end +function generateSerialData() + memDomain.wram() + status = u8(0x1A73) + if status == 0 then + return nil + end + return uRange(0x1A76, u8(0x1A74)) +end +local function arrayEqual(a1, a2) + if #a1 ~= #a2 then + return false + end + + for i, v in ipairs(a1) do + if v ~= a2[i] then + return false + end + end + + return true +end + +function receive() + l, e = gbSocket:receive() + if e == 'closed' then + if curstate == STATE_OK then + print("Connection closed") + end + curstate = STATE_UNINITIALIZED + return + elseif e == 'timeout' then + --print("timeout") -- this keeps happening for some reason? just hide it + return + elseif e ~= nil then + print(e) + curstate = STATE_UNINITIALIZED + return + end + if l ~= nil then + processBlock(json.decode(l)) + end + -- Determine Message to send back + memDomain.rom() + newPlayerName = uRange(0xFFF0, 0x10) + newSeedName = uRange(0xFFDC, 20) + if (playerName ~= nil and not arrayEqual(playerName, newPlayerName)) or (seedName ~= nil and not arrayEqual(seedName, newSeedName)) then + print("ROM changed, quitting") + curstate = STATE_UNINITIALIZED + return + end + playerName = newPlayerName + seedName = newSeedName + local retTable = {} + retTable["playerName"] = playerName + retTable["seedName"] = seedName + memDomain.wram() + if u8(InGame) == 0xAC then + retTable["locations"] = generateLocationsChecked() + serialData = generateSerialData() + if serialData ~= nil then + retTable["serial"] = serialData + end + end + msg = json.encode(retTable).."\n" + local ret, error = gbSocket:send(msg) + if ret == nil then + print(error) + elseif curstate == STATE_INITIAL_CONNECTION_MADE then + curstate = STATE_TENTATIVELY_CONNECTED + elseif curstate == STATE_TENTATIVELY_CONNECTED then + print("Connected!") + curstate = STATE_OK + end +end + +function main() + if (is23Or24Or25 or is26To28) == false then + print("Must use a version of bizhawk 2.3.1 or higher") + return + end + server, error = socket.bind('localhost', 17242) + + while true do + if not (curstate == prevstate) then + print("Current state: "..curstate) + prevstate = curstate + end + if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then + if (frame % 60 == 0) then + receive() + if u8(InGame) == 0xAC then + ItemIndex = u16(APIndex) + if ItemsReceived[ItemIndex + 1] ~= nil then + wU8(APItemAddress, ItemsReceived[ItemIndex + 1] - 172000000) + end + end + end + elseif (curstate == STATE_UNINITIALIZED) then + if (frame % 60 == 0) then + + print("Waiting for client.") + + emu.frameadvance() + 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 + gbSocket = client + gbSocket:settimeout(0) + end + end + end + emu.frameadvance() + end +end + +main() diff --git a/data/lua/PKMN_RB/socket.lua b/data/lua/PKMN_RB/socket.lua new file mode 100644 index 0000000000..a98e952115 --- /dev/null +++ b/data/lua/PKMN_RB/socket.lua @@ -0,0 +1,132 @@ +----------------------------------------------------------------------------- +-- LuaSocket helper module +-- Author: Diego Nehab +-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $ +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module and import dependencies +----------------------------------------------------------------------------- +local base = _G +local string = require("string") +local math = require("math") +local socket = require("socket.core") +module("socket") + +----------------------------------------------------------------------------- +-- Exported auxiliar functions +----------------------------------------------------------------------------- +function connect(address, port, laddress, lport) + local sock, err = socket.tcp() + if not sock then return nil, err end + if laddress then + local res, err = sock:bind(laddress, lport, -1) + if not res then return nil, err end + end + local res, err = sock:connect(address, port) + if not res then return nil, err end + return sock +end + +function bind(host, port, backlog) + local sock, err = socket.tcp() + if not sock then return nil, err end + sock:setoption("reuseaddr", true) + local res, err = sock:bind(host, port) + if not res then return nil, err end + res, err = sock:listen(backlog) + if not res then return nil, err end + return sock +end + +try = newtry() + +function choose(table) + return function(name, opt1, opt2) + if base.type(name) ~= "string" then + name, opt1, opt2 = "default", name, opt1 + end + local f = table[name or "nil"] + if not f then base.error("unknown key (".. base.tostring(name) ..")", 3) + else return f(opt1, opt2) end + end +end + +----------------------------------------------------------------------------- +-- Socket sources and sinks, conforming to LTN12 +----------------------------------------------------------------------------- +-- create namespaces inside LuaSocket namespace +sourcet = {} +sinkt = {} + +BLOCKSIZE = 2048 + +sinkt["close-when-done"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if not chunk then + sock:close() + return 1 + else return sock:send(chunk) end + end + }) +end + +sinkt["keep-open"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if chunk then return sock:send(chunk) + else return 1 end + end + }) +end + +sinkt["default"] = sinkt["keep-open"] + +sink = choose(sinkt) + +sourcet["by-length"] = function(sock, length) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + if length <= 0 then return nil end + local size = math.min(socket.BLOCKSIZE, length) + local chunk, err = sock:receive(size) + if err then return nil, err end + length = length - string.len(chunk) + return chunk + end + }) +end + +sourcet["until-closed"] = function(sock) + local done + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + if done then return nil end + local chunk, err, partial = sock:receive(socket.BLOCKSIZE) + if not err then return chunk + elseif err == "closed" then + sock:close() + done = 1 + return partial + else return nil, err end + end + }) +end + + +sourcet["default"] = sourcet["until-closed"] + +source = choose(sourcet) diff --git a/docs/adding games.md b/docs/adding games.md index 69f2bfb01f..e6b6195ec8 100644 --- a/docs/adding games.md +++ b/docs/adding games.md @@ -221,7 +221,7 @@ Starting with version 4 of the APBP format, this is a ZIP file containing metada files required by the game / patching process. For ROM-based games the ZIP will include a `delta.bsdiff4` which is the bsdiff between the original and the randomized ROM. -To make using APBP easy, they can be generated by inheriting from `Patch.APDeltaPatch`. +To make using APBP easy, they can be generated by inheriting from `worlds.Files.APDeltaPatch`. ### Mod files Games which support modding will usually just let you drag and drop the mod’s files into a folder somewhere. @@ -230,7 +230,7 @@ They can either be generic and modify the game using a seed or `slot_data` from generated per seed. If the mod is generated by AP and is installed from a ZIP file, it may be possible to include APBP metadata for easy -integration into the Webhost by inheriting from `Patch.APContainer`. +integration into the Webhost by inheriting from `worlds.Files.APContainer`. ## Archipelago Integration diff --git a/docs/network protocol.md b/docs/network protocol.md index 3315ddec2d..84587ab237 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -21,7 +21,7 @@ There are also a number of community-supported libraries available that implemen | | [Archipelago SNIClient](https://github.com/ArchipelagoMW/Archipelago/blob/main/SNIClient.py) | For Super Nintendo Game Support; Utilizes [SNI](https://github.com/alttpo/sni). | | JVM (Java / Kotlin) | [Archipelago.MultiClient.Java](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Java) | | | .NET (C# / C++ / F# / VB.NET) | [Archipelago.MultiClient.Net](https://www.nuget.org/packages/Archipelago.MultiClient.Net) | | -| C++ | [apclientpp](https://github.com/black-sliver/apclientpp) | almost-header-only | +| C++ | [apclientpp](https://github.com/black-sliver/apclientpp) | header-only | | | [APCpp](https://github.com/N00byKing/APCpp) | CMake | | JavaScript / TypeScript | [archipelago.js](https://www.npmjs.com/package/archipelago.js) | Browser and Node.js Supported | | Haxe | [hxArchipelago](https://lib.haxe.org/p/hxArchipelago) | | @@ -234,6 +234,8 @@ Sent to clients as a response the a [Get](#Get) package. | ---- | ---- | ----- | | keys | dict\[str\, any] | A key-value collection containing all the values for the keys requested in the [Get](#Get) package. | +If a requested key was not present in the server's data, the associated value will be `null`. + Additional arguments added to the [Get](#Get) package that triggered this [Retrieved](#Retrieved) will also be passed along. ### SetReply @@ -371,7 +373,7 @@ Used to write data to the server's data storage, that data can then be shared ac | ------ | ----- | ------ | | 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. | +| want_reply | bool | If true, 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. diff --git a/docs/running from source.md b/docs/running from source.md index 39addd0a28..2bda62ec1a 100644 --- a/docs/running from source.md +++ b/docs/running from source.md @@ -16,6 +16,14 @@ Then run any of the starting point scripts, like Generate.py, and the included M required modules and after pressing enter proceed to install everything automatically. After this, you should be able to run the programs. + * With yaml(s) in the `Players` folder, `Generate.py` will generate the multiworld archive. + * `MultiServer.py`, with the filename of the generated archive as a command line parameter, will host the multiworld locally. + * `--log_network` is a command line parameter useful for debugging. + * `WebHost.py` will host the website on your computer. + * You can copy `docs/webhost configuration sample.yaml` to `config.yaml` + to change WebHost options (like the web hosting port number). + * As a side effect, `WebHost.py` creates the template yamls for all the games in `WebHostLib/static/generated`. + ## Windows diff --git a/host.yaml b/host.yaml index 901e6cd727..2bb0e5ef5d 100644 --- a/host.yaml +++ b/host.yaml @@ -138,3 +138,29 @@ dkc3_options: # True for operating system default program # Alternatively, a path to a program to open the .sfc file with rom_start: true +pokemon_rb_options: + # File names of the Pokemon Red and Blue roms + red_rom_file: "Pokemon Red (UE) [S][!].gb" + blue_rom_file: "Pokemon Blue (UE) [S][!].gb" + # 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 .gb file with + rom_start: true +smw_options: + # File name of the SMW US rom + rom_file: "Super Mario World (USA).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 +zillion_options: + # File name of the Zillion US rom + rom_file: "Zillion (UE) [!].sms" + # 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 + # RetroArch doesn't make it easy to launch a game from the command line. + # You have to know the path to the emulator core library on the user's computer. + rom_start: "retroarch" diff --git a/inno_setup.iss b/inno_setup.iss index cfdfec7ba8..5e7414d181 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -55,21 +55,30 @@ Name: "core"; Description: "Core Files"; Types: full hosting playing 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/smw"; Description: "Super Mario World 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 +Name: "generator/zl"; Description: "Zillion ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 150000; Flags: disablenouninstallwarning +Name: "generator/pkmn_r"; Description: "Pokemon Red ROM Setup"; Types: full hosting +Name: "generator/pkmn_b"; Description: "Pokemon Blue ROM Setup"; Types: full hosting Name: "server"; Description: "Server"; Types: full hosting 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/sni/smw"; Description: "SNI Client - Super Mario World 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 Name: "client/ff1"; Description: "Final Fantasy 1"; Types: full playing +Name: "client/pkmn"; Description: "Pokemon Client" +Name: "client/pkmn/red"; Description: "Pokemon Client - Pokemon Red Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576 +Name: "client/pkmn/blue"; Description: "Pokemon Client - Pokemon Blue Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576 Name: "client/cf"; Description: "ChecksFinder"; Types: full playing Name: "client/sc2"; Description: "Starcraft 2"; Types: full playing +Name: "client/zl"; Description: "Zillion"; Types: full playing Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing [Dirs] @@ -79,8 +88,12 @@ NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-mod 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:GetSMWROMPath}"; DestDir: "{app}"; DestName: "Super Mario World (USA).sfc"; Flags: external; Components: client/sni/smw or generator/smw 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: "{code:GetZlROMPath}"; DestDir: "{app}"; DestName: "Zillion (UE) [!].sms"; Flags: external; Components: client/zl or generator/zl +Source: "{code:GetRedROMPath}"; DestDir: "{app}"; DestName: "Pokemon Red (UE) [S][!].gb"; Flags: external; Components: client/pkmn/red or generator/pkmn_r +Source: "{code:GetBlueROMPath}"; DestDir: "{app}"; DestName: "Pokemon Blue (UE) [S][!].gb"; Flags: external; Components: client/pkmn/blue or generator/pkmn_b Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp @@ -94,7 +107,9 @@ Source: "{#source_path}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: i Source: "{#source_path}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft Source: "{#source_path}\ArchipelagoOoTClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot Source: "{#source_path}\ArchipelagoOoTAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot +Source: "{#source_path}\ArchipelagoZillionClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/zl Source: "{#source_path}\ArchipelagoFF1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ff1 +Source: "{#source_path}\ArchipelagoPokemonClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/pkmn Source: "{#source_path}\ArchipelagoChecksFinderClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/cf Source: "{#source_path}\ArchipelagoStarcraft2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sc2 Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall @@ -107,7 +122,9 @@ Name: "{group}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.e Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft Name: "{group}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Components: client/oot +Name: "{group}\{#MyAppName} Zillion Client"; Filename: "{app}\ArchipelagoZillionClient.exe"; Components: client/zl Name: "{group}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Components: client/ff1 +Name: "{group}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Components: client/pkmn Name: "{group}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Components: client/cf Name: "{group}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Components: client/sc2 @@ -117,7 +134,9 @@ Name: "{commondesktop}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNI Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Tasks: desktopicon; Components: client/factorio Name: "{commondesktop}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Tasks: desktopicon; Components: client/minecraft Name: "{commondesktop}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Tasks: desktopicon; Components: client/oot +Name: "{commondesktop}\{#MyAppName} Zillion Client"; Filename: "{app}\ArchipelagoZillionClient.exe"; Tasks: desktopicon; Components: client/zl Name: "{commondesktop}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Tasks: desktopicon; Components: client/ff1 +Name: "{commondesktop}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Tasks: desktopicon; Components: client/pkmn Name: "{commondesktop}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Tasks: desktopicon; Components: client/cf Name: "{commondesktop}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Tasks: desktopicon; Components: client/sc2 @@ -151,6 +170,16 @@ Root: HKCR; Subkey: "{#MyAppName}dkc3patch"; ValueData: "Arc 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: ".apsmw"; ValueData: "{#MyAppName}smwpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}smwpatch"; ValueData: "Archipelago Super Mario World Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}smwpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}smwpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni + +Root: HKCR; Subkey: ".apzl"; ValueData: "{#MyAppName}zlpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/zl +Root: HKCR; Subkey: "{#MyAppName}zlpatch"; ValueData: "Archipelago Zillion Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/zl +Root: HKCR; Subkey: "{#MyAppName}zlpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoZillionClient.exe,0"; ValueType: string; ValueName: ""; Components: client/zl +Root: HKCR; Subkey: "{#MyAppName}zlpatch\shell\open\command"; ValueData: """{app}\ArchipelagoZillionClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/zl + 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 @@ -171,6 +200,16 @@ Root: HKCR; Subkey: "{#MyAppName}n64zpf"; ValueData: "Archip Root: HKCR; Subkey: "{#MyAppName}n64zpf\DefaultIcon"; ValueData: "{app}\ArchipelagoOoTClient.exe,0"; ValueType: string; ValueName: ""; Components: client/oot Root: HKCR; Subkey: "{#MyAppName}n64zpf\shell\open\command"; ValueData: """{app}\ArchipelagoOoTClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/oot +Root: HKCR; Subkey: ".apred"; ValueData: "{#MyAppName}pkmnrpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/pkmn/red +Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch"; ValueData: "Archipelago Pokemon Red Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/pkmn/red +Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; Components: client/pkmn/red +Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/pkmn/red + +Root: HKCR; Subkey: ".apblue"; ValueData: "{#MyAppName}pkmnbpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/pkmn/blue +Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch"; ValueData: "Archipelago Pokemon Blue Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/pkmn/blue +Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; Components: client/pkmn/blue +Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/pkmn/blue + Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: server Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: server Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""; Components: server @@ -217,12 +256,24 @@ var SMRomFilePage: TInputFileWizardPage; var dkc3rom: string; var DKC3RomFilePage: TInputFileWizardPage; +var smwrom: string; +var SMWRomFilePage: TInputFileWizardPage; + var soerom: string; var SoERomFilePage: TInputFileWizardPage; var ootrom: string; var OoTROMFilePage: TInputFileWizardPage; +var zlrom: string; +var ZlROMFilePage: TInputFileWizardPage; + +var redrom: string; +var RedROMFilePage: TInputFileWizardPage; + +var bluerom: string; +var BlueROMFilePage: TInputFileWizardPage; + function GetSNESMD5OfFile(const rom: string): string; var data: AnsiString; begin @@ -236,6 +287,15 @@ begin end; end; +function GetSMSMD5OfFile(const rom: string): string; +var data: AnsiString; +begin + if LoadStringFromFile(rom, data) then + begin + Result := GetMD5OfString(data); + end; +end; + function CheckRom(name: string; hash: string): string; var rom: string; begin @@ -255,6 +315,25 @@ begin end; end; +function CheckSMSRom(name: string; hash: string): string; +var rom: string; +begin + log('Handling ' + name) + rom := FileSearch(name, WizardDirValue()); + if Length(rom) > 0 then + begin + log('existing ROM found'); + log(IntToStr(CompareStr(GetSMSMD5OfFile(rom), hash))); + if CompareStr(GetSMSMD5OfFile(rom), hash) = 0 then + begin + log('existing ROM verified'); + Result := rom; + exit; + end; + log('existing ROM failed verification'); + end; +end; + function AddRomPage(name: string): TInputFileWizardPage; begin Result := @@ -270,6 +349,37 @@ begin '.sfc'); end; + +function AddGBRomPage(name: string): TInputFileWizardPage; +begin + Result := + CreateInputFilePage( + wpSelectComponents, + 'Select ROM File', + 'Where is your ' + name + ' located?', + 'Select the file, then click Next.'); + + Result.Add( + 'Location of ROM file:', + 'GB ROM files|*.gb;*.gbc|All files|*.*', + '.gb'); +end; + +function AddSMSRomPage(name: string): TInputFileWizardPage; +begin + Result := + CreateInputFilePage( + wpSelectComponents, + 'Select ROM File', + 'Where is your ' + name + ' located?', + 'Select the file, then click Next.'); + + Result.Add( + 'Location of ROM file:', + 'SMS ROM files|*.sms|All files|*.*', + '.sms'); +end; + procedure AddOoTRomPage(); begin ootrom := FileSearch('The Legend of Zelda - Ocarina of Time.z64', WizardDirValue()); @@ -308,10 +418,14 @@ begin Result := not (SMROMFilePage.Values[0] = '') else if (assigned(DKC3ROMFilePage)) and (CurPageID = DKC3ROMFilePage.ID) then Result := not (DKC3ROMFilePage.Values[0] = '') + else if (assigned(SMWROMFilePage)) and (CurPageID = SMWROMFilePage.ID) then + Result := not (SMWROMFilePage.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 Result := not (OoTROMFilePage.Values[0] = '') + else if (assigned(ZlROMFilePage)) and (CurPageID = ZlROMFilePage.ID) then + Result := not (ZlROMFilePage.Values[0] = '') else Result := True; end; @@ -364,6 +478,22 @@ begin Result := ''; end; +function GetSMWROMPath(Param: string): string; +begin + if Length(smwrom) > 0 then + Result := smwrom + else if Assigned(SMWRomFilePage) then + begin + R := CompareStr(GetSNESMD5OfFile(SMWROMFilePage.Values[0]), 'cdd3c8c37322978ca8669b34bc89c804') + if R <> 0 then + MsgBox('Super Mario World ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); + + Result := SMWROMFilePage.Values[0] + end + else + Result := ''; + end; + function GetSoEROMPath(Param: string): string; begin if Length(soerom) > 0 then @@ -396,6 +526,54 @@ begin Result := ''; end; +function GetZlROMPath(Param: string): string; +begin + if Length(zlrom) > 0 then + Result := zlrom + else if Assigned(ZlROMFilePage) then + begin + R := CompareStr(GetMD5OfFile(ZlROMFilePage.Values[0]), 'd4bf9e7bcf9a48da53785d2ae7bc4270'); + if R <> 0 then + MsgBox('Zillion ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); + + Result := ZlROMFilePage.Values[0] + end + else + Result := ''; +end; + +function GetRedROMPath(Param: string): string; +begin + if Length(redrom) > 0 then + Result := redrom + else if Assigned(RedRomFilePage) then + begin + R := CompareStr(GetMD5OfFile(RedROMFilePage.Values[0]), '3d45c1ee9abd5738df46d2bdda8b57dc') + if R <> 0 then + MsgBox('Pokemon Red ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); + + Result := RedROMFilePage.Values[0] + end + else + Result := ''; + end; + +function GetBlueROMPath(Param: string): string; +begin + if Length(bluerom) > 0 then + Result := bluerom + else if Assigned(BlueRomFilePage) then + begin + R := CompareStr(GetMD5OfFile(BlueROMFilePage.Values[0]), '50927e843568814f7ed45ec4f944bd8b') + if R <> 0 then + MsgBox('Pokemon Blue ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); + + Result := BlueROMFilePage.Values[0] + end + else + Result := ''; + end; + procedure InitializeWizard(); begin AddOoTRomPage(); @@ -412,9 +590,25 @@ begin if Length(dkc3rom) = 0 then DKC3RomFilePage:= AddRomPage('Donkey Kong Country 3 - Dixie Kong''s Double Trouble! (USA) (En,Fr).sfc'); + smwrom := CheckRom('Super Mario World (USA).sfc', 'cdd3c8c37322978ca8669b34bc89c804'); + if Length(smwrom) = 0 then + SMWRomFilePage:= AddRomPage('Super Mario World (USA).sfc'); + soerom := CheckRom('Secret of Evermore (USA).sfc', '6e9c94511d04fac6e0a1e582c170be3a'); if Length(soerom) = 0 then SoEROMFilePage:= AddRomPage('Secret of Evermore (USA).sfc'); + + zlrom := CheckSMSRom('Zillion (UE) [!].sms', 'd4bf9e7bcf9a48da53785d2ae7bc4270'); + if Length(zlrom) = 0 then + ZlROMFilePage:= AddSMSRomPage('Zillion (UE) [!].sms'); + + redrom := CheckRom('Pokemon Red (UE) [S][!].gb','3d45c1ee9abd5738df46d2bdda8b57dc'); + if Length(redrom) = 0 then + RedROMFilePage:= AddGBRomPage('Pokemon Red (UE) [S][!].gb'); + + bluerom := CheckRom('Pokemon Blue (UE) [S][!].gb','50927e843568814f7ed45ec4f944bd8b'); + if Length(bluerom) = 0 then + BlueROMFilePage:= AddGBRomPage('Pokemon Blue (UE) [S][!].gb'); end; @@ -427,8 +621,16 @@ begin 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(SMWROMFilePage)) and (PageID = SMWROMFilePage.ID) then + Result := not (WizardIsComponentSelected('client/sni/smw') or WizardIsComponentSelected('generator/smw')); if (assigned(SoEROMFilePage)) and (PageID = SoEROMFilePage.ID) then Result := not (WizardIsComponentSelected('generator/soe')); if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then Result := not (WizardIsComponentSelected('generator/oot') or WizardIsComponentSelected('client/oot')); -end; \ No newline at end of file + if (assigned(ZlROMFilePage)) and (PageID = ZlROMFilePage.ID) then + Result := not (WizardIsComponentSelected('generator/zl') or WizardIsComponentSelected('client/zl')); + if (assigned(RedROMFilePage)) and (PageID = RedROMFilePage.ID) then + Result := not (WizardIsComponentSelected('generator/pkmn_r') or WizardIsComponentSelected('client/pkmn/red')); + if (assigned(BlueROMFilePage)) and (PageID = BlueROMFilePage.ID) then + Result := not (WizardIsComponentSelected('generator/pkmn_b') or WizardIsComponentSelected('client/pkmn/blue')); +end; diff --git a/setup.py b/setup.py index 11c993774c..19d042189b 100644 --- a/setup.py +++ b/setup.py @@ -289,6 +289,7 @@ tmp="${{exe#*/}}" if [ ! "${{#tmp}}" -lt "${{#exe}}" ]; then exe="{default_exe.parent}/$exe" fi +export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$APPDIR/{default_exe.parent}/lib" $APPDIR/$exe "$@" """) launcher_filename.chmod(0o755) diff --git a/test/general/TestFill.py b/test/general/TestFill.py index 8ce5b3b281..86d86a223a 100644 --- a/test/general/TestFill.py +++ b/test/general/TestFill.py @@ -4,7 +4,7 @@ from worlds.AutoWorld import World from Fill import FillError, balance_multiworld_progression, fill_restrictive, distribute_items_restrictive from BaseClasses import Entrance, LocationProgressType, MultiWorld, Region, RegionType, Item, Location, \ ItemClassification -from worlds.generic.Rules import CollectionRule, locality_rules, set_rule +from worlds.generic.Rules import CollectionRule, add_item_rule, locality_rules, set_rule def generate_multi_world(players: int = 1) -> MultiWorld: @@ -359,6 +359,46 @@ class TestFillRestrictive(unittest.TestCase): fill_restrictive(multi_world, multi_world.state, locations, player1.prog_items) + def test_swap_to_earlier_location_with_item_rule(self): + # test for PR#1109 + multi_world = generate_multi_world(1) + player1 = generate_player_data(multi_world, 1, 4, 4) + locations = player1.locations[:] # copy required + items = player1.prog_items[:] # copy required + # for the test to work, item and location order is relevant: Sphere 1 last, allowed_item not last + for location in locations[:-1]: # Sphere 2 + # any one provides access to Sphere 2 + set_rule(location, lambda state: any(state.has(item.name, player1.id) for item in items)) + # forbid all but 1 item in Sphere 1 + sphere1_loc = locations[-1] + allowed_item = items[1] + add_item_rule(sphere1_loc, lambda item_to_place: item_to_place == allowed_item) + # test our rules + self.assertTrue(location.can_fill(None, allowed_item, False), "Test is flawed") + self.assertTrue(location.can_fill(None, items[2], False), "Test is flawed") + self.assertTrue(sphere1_loc.can_fill(None, allowed_item, False), "Test is flawed") + self.assertFalse(sphere1_loc.can_fill(None, items[2], False), "Test is flawed") + # fill has to place items[1] in locations[0] which will result in a swap because of placement order + fill_restrictive(multi_world, multi_world.state, player1.locations, player1.prog_items) + # assert swap happened + self.assertTrue(sphere1_loc.item, "Did not swap required item into Sphere 1") + self.assertEqual(sphere1_loc.item, allowed_item, "Wrong item in Sphere 1") + + def test_double_sweep(self): + # test for PR1114 + multi_world = generate_multi_world(1) + player1 = generate_player_data(multi_world, 1, 1, 1) + location = player1.locations[0] + location.address = None + location.event = True + item = player1.prog_items[0] + item.code = None + location.place_locked_item(item) + multi_world.state.sweep_for_events() + multi_world.state.sweep_for_events() + self.assertTrue(multi_world.state.prog_items[item.name, item.player], "Sweep did not collect - Test flawed") + self.assertEqual(multi_world.state.prog_items[item.name, item.player], 1, "Sweep collected multiple times") + class TestDistributeItemsRestrictive(unittest.TestCase): def test_basic_distribute(self): @@ -575,8 +615,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase): multi_world.local_items[player1.id].value = set(names(player1.basic_items)) multi_world.local_items[player2.id].value = set(names(player2.basic_items)) - locality_rules(multi_world, player1.id) - locality_rules(multi_world, player2.id) + locality_rules(multi_world) distribute_items_restrictive(multi_world) diff --git a/test/general/TestImplemented.py b/test/general/TestImplemented.py index b2e696ab0d..15e099ff09 100644 --- a/test/general/TestImplemented.py +++ b/test/general/TestImplemented.py @@ -8,7 +8,7 @@ class TestImplemented(unittest.TestCase): def testCompletionCondition(self): """Ensure a completion condition is set that has requirements.""" for gamename, world_type in AutoWorldRegister.world_types.items(): - if not world_type.hidden and gamename not in {"ArchipIDLE", "Final Fantasy"}: + if not world_type.hidden and gamename not in {"ArchipIDLE", "Final Fantasy", "Sudoku"}: with self.subTest(gamename): world = setup_default_world(world_type) self.assertFalse(world.completion_condition[1](world.state)) diff --git a/test/general/TestReachability.py b/test/general/TestReachability.py index 2cadf9d29a..13d56d740a 100644 --- a/test/general/TestReachability.py +++ b/test/general/TestReachability.py @@ -20,7 +20,7 @@ class TestBase(unittest.TestCase): for location in world.get_locations(): if location.name not in excluded: with self.subTest("Location should be reached", location=location): - self.assertTrue(location.can_reach(state)) + self.assertTrue(location.can_reach(state), f"{location.name} unreachable") with self.subTest("Completion Condition"): self.assertTrue(world.can_beat_game(state)) @@ -28,7 +28,7 @@ class TestBase(unittest.TestCase): def testEmptyStateCanReachSomething(self): for game_name, world_type in AutoWorldRegister.world_types.items(): # Final Fantasy logic is controlled by finalfantasyrandomizer.com - if game_name != "Archipelago" and game_name != "Final Fantasy": + if game_name not in {"Archipelago", "Final Fantasy", "Sudoku"}: with self.subTest("Game", game=game_name): world = setup_default_world(world_type) state = CollectionState(world) diff --git a/test/minor_glitches/TestMinor.py b/test/minor_glitches/TestMinor.py index 81c09cfb27..41cb13161a 100644 --- a/test/minor_glitches/TestMinor.py +++ b/test/minor_glitches/TestMinor.py @@ -23,10 +23,8 @@ class TestMinor(TestBase): self.world.set_default_common_options() self.world.logic[1] = "minorglitches" self.world.difficulty_requirements[1] = difficulties['normal'] - create_regions(self.world, 1) - create_dungeons(self.world, 1) - create_shops(self.world, 1) - link_entrances(self.world, 1) + self.world.worlds[1].er_seed = 0 + self.world.worlds[1].create_regions() self.world.worlds[1].create_items() self.world.required_medallions[1] = ['Ether', 'Quake'] self.world.itempool.extend(get_dungeon_item_pool(self.world)) diff --git a/test/options/TestPlandoBosses.py b/test/options/TestPlandoBosses.py new file mode 100644 index 0000000000..3c218b69aa --- /dev/null +++ b/test/options/TestPlandoBosses.py @@ -0,0 +1,136 @@ +import unittest +import Generate +from Options import PlandoBosses + + +class SingleBosses(PlandoBosses): + bosses = {"B1", "B2"} + locations = {"L1", "L2"} + + option_vanilla = 0 + option_shuffle = 1 + + @staticmethod + def can_place_boss(boss: str, location: str) -> bool: + if boss == "b1" and location == "l1": + return False + return True + + +class MultiBosses(SingleBosses): + bosses = SingleBosses.bosses # explicit copy required + locations = SingleBosses.locations + duplicate_bosses = True + + option_singularity = 2 # required when duplicate_bosses = True + + +class TestPlandoBosses(unittest.TestCase): + def testCI(self): + """Bosses, locations and modes are supposed to be case-insensitive""" + self.assertEqual(MultiBosses.from_any("L1-B2").value, "l1-b2;vanilla") + self.assertEqual(MultiBosses.from_any("ShUfFlE").value, SingleBosses.option_shuffle) + + def testRandom(self): + """Validate random is random""" + import random + random.seed(0) + value1 = MultiBosses.from_any("random") + random.seed(0) + value2 = MultiBosses.from_text("random") + self.assertEqual(value1, value2) + for n in range(0, 100): + if MultiBosses.from_text("random") != value1: + break + else: + raise Exception("random is not random") + + def testShuffleMode(self): + """Test that simple modes (no Plando) work""" + self.assertEqual(MultiBosses.from_any("shuffle"), MultiBosses.option_shuffle) + self.assertNotEqual(MultiBosses.from_any("vanilla"), MultiBosses.option_shuffle) + + def testPlacement(self): + """Test that valid placements work and invalid placements fail""" + with self.assertRaises(ValueError): + MultiBosses.from_any("l1-b1") + MultiBosses.from_any("l1-b2;l2-b1") + + def testMixed(self): + """Test that shuffle is applied for remaining locations""" + self.assertIn("shuffle", MultiBosses.from_any("l1-b2;l2-b1;shuffle").value) + self.assertIn("vanilla", MultiBosses.from_any("l1-b2;l2-b1").value) + + def testUnknown(self): + """Test that unknown values throw exceptions""" + # unknown boss + with self.assertRaises(ValueError): + MultiBosses.from_any("l1-b0") + # unknown location + with self.assertRaises(ValueError): + MultiBosses.from_any("l0-b1") + # swapped boss-location + with self.assertRaises(ValueError): + MultiBosses.from_any("b2-b2") + # boss name in place of mode (no singularity) + with self.assertRaises(ValueError): + SingleBosses.from_any("b1") + with self.assertRaises(ValueError): + SingleBosses.from_any("l2-b2;b1") + # location name in place of mode + with self.assertRaises(ValueError): + MultiBosses.from_any("l1") + with self.assertRaises(ValueError): + MultiBosses.from_any("l2-b2;l1") + # mode name in place of location + with self.assertRaises(ValueError): + MultiBosses.from_any("shuffle-b2;vanilla") + with self.assertRaises(ValueError): + MultiBosses.from_any("shuffle-b2;l2-b2") + # mode name in place of boss + with self.assertRaises(ValueError): + MultiBosses.from_any("l2-shuffle;vanilla") + with self.assertRaises(ValueError): + MultiBosses.from_any("l1-shuffle;l2-b2") + + def testOrder(self): + """Can't use mode in random places""" + with self.assertRaises(ValueError): + MultiBosses.from_any("shuffle;l2-b2") + + def testDuplicateBoss(self): + """Can place the same boss twice""" + MultiBosses.from_any("l1-b2;l2-b2") + with self.assertRaises(ValueError): + SingleBosses.from_any("l1-b2;l2-b2") + + def testDuplicateLocation(self): + """Can't use the same location twice""" + with self.assertRaises(ValueError): + MultiBosses.from_any("l1-b2;l1-b2") + + def testSingularity(self): + """Test automatic singularity mode""" + self.assertIn(";singularity", MultiBosses.from_any("b2").value) + + def testPlandoSettings(self): + """Test that plando settings verification works""" + plandoed_string = "l1-b2;l2-b1" + mixed_string = "l1-b2;shuffle" + regular_string = "shuffle" + plandoed = MultiBosses.from_any(plandoed_string) + mixed = MultiBosses.from_any(mixed_string) + regular = MultiBosses.from_any(regular_string) + + # plando should work with boss plando + plandoed.verify(None, "Player", Generate.PlandoSettings.bosses) + self.assertTrue(plandoed.value.startswith(plandoed_string)) + # plando should fall back to default without boss plando + plandoed.verify(None, "Player", Generate.PlandoSettings.items) + self.assertEqual(plandoed, MultiBosses.option_vanilla) + # mixed should fall back to mode + mixed.verify(None, "Player", Generate.PlandoSettings.items) # should produce a warning and still work + self.assertEqual(mixed, MultiBosses.option_shuffle) + # mode stuff should just work + regular.verify(None, "Player", Generate.PlandoSettings.items) + self.assertEqual(regular, MultiBosses.option_shuffle) diff --git a/test/options/__init__.py b/test/options/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/overcooked2/TestOvercooked2.py b/test/overcooked2/TestOvercooked2.py new file mode 100644 index 0000000000..ec9efd1f67 --- /dev/null +++ b/test/overcooked2/TestOvercooked2.py @@ -0,0 +1,142 @@ +import unittest +import json + +from random import Random + +from worlds.overcooked2.Items import * +from worlds.overcooked2.Overcooked2Levels import Overcooked2Level, level_id_to_shortname +from worlds.overcooked2.Logic import level_logic, level_shuffle_factory +from worlds.overcooked2.Locations import oc2_location_name_to_id + + +class Overcooked2Test(unittest.TestCase): + def testItems(self): + self.assertEqual(len(item_name_to_id), len(item_id_to_name)) + self.assertEqual(len(item_name_to_id), len(item_table)) + + previous_item = None + for item_name in item_table.keys(): + item: Item = item_table[item_name] + self.assertGreaterEqual(item.code, oc2_base_id, "Overcooked Item ID out of range") + self.assertLessEqual(item.code, item_table["Calmer Unbread"].code, "Overcooked Item ID out of range") + + if previous_item is not None: + self.assertEqual(item.code, previous_item + 1, + f"Overcooked Item ID noncontinguous: {item.code-oc2_base_id}") + previous_item = item.code + + self.assertEqual(item_table["Ok Emote"].code - item_table["Cooking Emote"].code, + 5, "Overcooked Emotes noncontigious") + + for item_name in item_frequencies: + self.assertIn(item_name, item_table.keys(), "Unexpected Overcooked Item in item_frequencies") + + for item_name in item_name_to_config_name.keys(): + self.assertIn(item_name, item_table.keys(), "Unexpected Overcooked Item in config mapping") + + for config_name in item_name_to_config_name.values(): + self.assertIn(config_name, vanilla_values.keys(), "Unexpected Overcooked Item in default config mapping") + + for config_name in vanilla_values.keys(): + self.assertIn(config_name, item_name_to_config_name.values(), + "Unexpected Overcooked Item in default config mapping") + + events = [ + ("Kevin-2", {"action": "UNLOCK_LEVEL", "payload": "38"}), + ("Curse Emote", {"action": "UNLOCK_EMOTE", "payload": "1"}), + ("Larger Tip Jar", {"action": "INC_TIP_COMBO", "payload": ""}), + ("Order Lookahead", {"action": "INC_ORDERS_ON_SCREEN", "payload": ""}), + ("Control Stick Batteries", {"action": "SET_VALUE", "payload": "DisableControlStick=False"}), + ] + for (item_name, expected_event) in events: + expected_event["message"] = f"{item_name} Acquired!" + event = item_to_unlock_event(item_name) + self.assertEqual(event, expected_event) + + self.assertFalse(is_progression("Preparing Emote")) + + for item_name in item_table: + item_to_unlock_event(item_name) + + def testOvercooked2Levels(self): + level_count = 0 + for _ in Overcooked2Level(): + level_count += 1 + self.assertEqual(level_count, 44) + + def testOvercooked2ShuffleFactory(self): + previous_runs = set() + for seed in range(0, 5): + levels = level_shuffle_factory(Random(seed), True, False) + self.assertEqual(len(levels), 44) + previous_level_id = None + for level_id in levels.keys(): + if previous_level_id is not None: + self.assertEqual(previous_level_id+1, level_id) + previous_level_id = level_id + + self.assertNotIn(levels[15], previous_runs) + previous_runs.add(levels[15]) + + levels = level_shuffle_factory(Random(123), False, True) + self.assertEqual(len(levels), 44) + + def testLevelNameRepresentation(self): + shortnames = [level.as_generic_level.shortname for level in Overcooked2Level()] + + for shortname in shortnames: + self.assertIn(shortname, level_logic.keys()) + + self.assertEqual(len(level_logic), len(level_id_to_shortname)) + + for level_name in level_logic.keys(): + if level_name != "*": + self.assertIn(level_name, level_id_to_shortname.values()) + + for level_name in level_id_to_shortname.values(): + if level_name != "Tutorial": + self.assertIn(level_name, level_logic.keys()) + + region_names = [level.level_name for level in Overcooked2Level()] + for location_name in oc2_location_name_to_id.keys(): + level_name = location_name.split(" ")[0] + self.assertIn(level_name, region_names) + + def testLogic(self): + for level_name in level_logic.keys(): + logic = level_logic[level_name] + self.assertEqual(len(logic), 3, "Levels must provide logic for 1, 2, and 3 stars") + + for l in logic: + self.assertEqual(len(l), 2) + (exclusive, additive) = l + + for req in exclusive: + self.assertEqual(type(req), str) + self.assertIn(req, item_table.keys()) + + if len(additive) != 0: + self.assertGreater(len(additive), 1) + total_weight = 0.0 + for req in additive: + self.assertEqual(len(req), 2) + (item_name, weight) = req + self.assertEqual(type(item_name), str) + self.assertEqual(type(weight), float) + total_weight += weight + self.assertIn(item_name, item_table.keys()) + + self.assertGreaterEqual(total_weight, 0.99, "Additive requirements must add to 1.0 or greater to have any effect") + + def testItemLocationMapping(self): + number_of_items = 0 + for item_name in item_frequencies: + freq = item_frequencies[item_name] + self.assertGreaterEqual(freq, 0) + number_of_items += freq + + for item_name in item_table: + if item_name not in item_frequencies.keys(): + number_of_items += 1 + + self.assertLessEqual(number_of_items, len(oc2_location_name_to_id), "Too many items (before fillers placed)") diff --git a/test/overcooked2/__init__.py b/test/overcooked2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/owg/TestVanillaOWG.py b/test/owg/TestVanillaOWG.py index e5489117a7..a4367fb55e 100644 --- a/test/owg/TestVanillaOWG.py +++ b/test/owg/TestVanillaOWG.py @@ -24,10 +24,8 @@ class TestVanillaOWG(TestBase): self.world.set_default_common_options() self.world.difficulty_requirements[1] = difficulties['normal'] self.world.logic[1] = "owglitches" - create_regions(self.world, 1) - create_dungeons(self.world, 1) - create_shops(self.world, 1) - link_entrances(self.world, 1) + self.world.worlds[1].er_seed = 0 + self.world.worlds[1].create_regions() self.world.worlds[1].create_items() self.world.required_medallions[1] = ['Ether', 'Quake'] self.world.itempool.extend(get_dungeon_item_pool(self.world)) diff --git a/test/vanilla/TestVanilla.py b/test/vanilla/TestVanilla.py index e5ee73406a..c9fa3f763b 100644 --- a/test/vanilla/TestVanilla.py +++ b/test/vanilla/TestVanilla.py @@ -22,10 +22,8 @@ class TestVanilla(TestBase): self.world.set_default_common_options() self.world.logic[1] = "noglitches" self.world.difficulty_requirements[1] = difficulties['normal'] - create_regions(self.world, 1) - create_dungeons(self.world, 1) - create_shops(self.world, 1) - link_entrances(self.world, 1) + self.world.worlds[1].er_seed = 0 + self.world.worlds[1].create_regions() self.world.worlds[1].create_items() self.world.required_medallions[1] = ['Ether', 'Quake'] self.world.itempool.extend(get_dungeon_item_pool(self.world)) diff --git a/test/worlds/__init__.py b/test/worlds/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/worlds/soe/TestAccess.py b/test/worlds/soe/TestAccess.py new file mode 100644 index 0000000000..c7da7b8896 --- /dev/null +++ b/test/worlds/soe/TestAccess.py @@ -0,0 +1,22 @@ +import typing +from . import SoETestBase + + +class AccessTest(SoETestBase): + @staticmethod + def _resolveGourds(gourds: typing.Dict[str, typing.Iterable[int]]): + return [f"{name} #{number}" for name, numbers in gourds.items() for number in numbers] + + def testBronzeAxe(self): + gourds = { + "Pyramid bottom": (118, 121, 122, 123, 124, 125), + "Pyramid top": (140,) + } + locations = ["Rimsala"] + self._resolveGourds(gourds) + items = [["Bronze Axe"]] + self.assertAccessDependency(locations, items) + + def testBronzeSpearPlus(self): + locations = ["Megataur"] + items = [["Bronze Spear"], ["Lance (Weapon)"], ["Laser Lance"]] + self.assertAccessDependency(locations, items) diff --git a/test/worlds/soe/TestGoal.py b/test/worlds/soe/TestGoal.py new file mode 100644 index 0000000000..d127d38998 --- /dev/null +++ b/test/worlds/soe/TestGoal.py @@ -0,0 +1,53 @@ +from . import SoETestBase + + +class TestFragmentGoal(SoETestBase): + options = { + "energy_core": "fragments", + "available_fragments": 21, + "required_fragments": 20, + } + + def testFragments(self): + self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]) + self.assertBeatable(False) # 0 fragments + fragments = self.get_items_by_name("Energy Core Fragment") + victory = self.get_item_by_name("Victory") + self.collect(fragments[:-2]) # 1 too few + self.assertEqual(self.count("Energy Core Fragment"), 19) + self.assertBeatable(False) + self.collect(fragments[-2:-1]) # exact + self.assertEqual(self.count("Energy Core Fragment"), 20) + self.assertBeatable(True) + self.remove([victory]) # reset + self.collect(fragments[-1:]) # 1 extra + self.assertEqual(self.count("Energy Core Fragment"), 21) + self.assertBeatable(True) + + def testNoWeapon(self): + self.collect_by_name(["Diamond Eye", "Wheel", "Gauge", "Energy Core Fragment"]) + self.assertBeatable(False) + + def testNoRocket(self): + self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Energy Core Fragment"]) + self.assertBeatable(False) + + +class TestShuffleGoal(SoETestBase): + options = { + "energy_core": "shuffle", + } + + def testCore(self): + self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]) + self.assertBeatable(False) + self.collect_by_name(["Energy Core"]) + self.assertBeatable(True) + + def testNoWeapon(self): + self.collect_by_name(["Diamond Eye", "Wheel", "Gauge", "Energy Core"]) + self.assertBeatable(False) + + def testNoRocket(self): + self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Energy Core"]) + self.assertBeatable(False) diff --git a/test/worlds/soe/__init__.py b/test/worlds/soe/__init__.py new file mode 100644 index 0000000000..c79544e0de --- /dev/null +++ b/test/worlds/soe/__init__.py @@ -0,0 +1,5 @@ +from test.worlds.test_base import WorldTestBase + + +class SoETestBase(WorldTestBase): + game = "Secret of Evermore" diff --git a/test/worlds/test_base.py b/test/worlds/test_base.py new file mode 100644 index 0000000000..1aa6ff2317 --- /dev/null +++ b/test/worlds/test_base.py @@ -0,0 +1,98 @@ +import typing +import unittest +from argparse import Namespace +from test.general import gen_steps +from BaseClasses import MultiWorld, Item +from worlds import AutoWorld +from worlds.AutoWorld import call_all + + +class WorldTestBase(unittest.TestCase): + options: typing.Dict[str, typing.Any] = {} + world: MultiWorld + + game: typing.ClassVar[str] # define game name in subclass, example "Secret of Evermore" + auto_construct: typing.ClassVar[bool] = True + """ automatically set up a world for each test in this class """ + + def setUp(self) -> None: + if self.auto_construct: + self.world_setup() + + def world_setup(self) -> None: + if not hasattr(self, "game"): + raise NotImplementedError("didn't define game name") + self.world = MultiWorld(1) + self.world.game[1] = self.game + self.world.player_name = {1: "Tester"} + self.world.set_seed() + args = Namespace() + for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].option_definitions.items(): + setattr(args, name, { + 1: option.from_any(self.options.get(name, getattr(option, "default"))) + }) + self.world.set_options(args) + self.world.set_default_common_options() + for step in gen_steps: + call_all(self.world, step) + + def collect_all_but(self, item_names: typing.Union[str, typing.Iterable[str]]) -> None: + if isinstance(item_names, str): + item_names = (item_names,) + for item in self.world.get_items(): + if item.name not in item_names: + self.world.state.collect(item) + + def get_item_by_name(self, item_name: str) -> Item: + for item in self.world.get_items(): + if item.name == item_name: + return item + raise ValueError("No such item") + + def get_items_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]: + if isinstance(item_names, str): + item_names = (item_names,) + return [item for item in self.world.itempool if item.name in item_names] + + def collect_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]: + """ collect all of the items in the item pool that have the given names """ + items = self.get_items_by_name(item_names) + self.collect(items) + return items + + def collect(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None: + if isinstance(items, Item): + items = (items,) + for item in items: + self.world.state.collect(item) + + def remove(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None: + if isinstance(items, Item): + items = (items,) + for item in items: + if item.location and item.location.event and item.location in self.world.state.events: + self.world.state.events.remove(item.location) + self.world.state.remove(item) + + def can_reach_location(self, location: str) -> bool: + return self.world.state.can_reach(location, "Location", 1) + + def count(self, item_name: str) -> int: + return self.world.state.count(item_name, 1) + + def assertAccessDependency(self, + locations: typing.List[str], + possible_items: typing.Iterable[typing.Iterable[str]]) -> None: + all_items = [item_name for item_names in possible_items for item_name in item_names] + + self.collect_all_but(all_items) + for location in self.world.get_locations(): + self.assertEqual(self.world.state.can_reach(location), location.name not in locations) + for item_names in possible_items: + items = self.collect_by_name(item_names) + for location in locations: + self.assertTrue(self.can_reach_location(location)) + self.remove(items) + + def assertBeatable(self, beatable: bool): + self.assertEqual(self.world.can_beat_game(self.world.state), beatable) diff --git a/test/worlds/zillion/TestGoal.py b/test/worlds/zillion/TestGoal.py new file mode 100644 index 0000000000..96701e8352 --- /dev/null +++ b/test/worlds/zillion/TestGoal.py @@ -0,0 +1,144 @@ +from . import ZillionTestBase + + +class TestGoalVanilla(ZillionTestBase): + options = { + "start_char": "JJ", + "jump_levels": "vanilla", + "gun_levels": "vanilla", + "floppy_disk_count": 7, + "floppy_req": 6, + } + + def test_floppies(self): + self.collect_by_name(["Apple", "Champ", "Red ID Card"]) + self.assertBeatable(False) # 0 floppies + floppies = self.get_items_by_name("Floppy Disk") + win = self.get_item_by_name("Win") + self.collect(floppies[:-2]) # 1 too few + self.assertEqual(self.count("Floppy Disk"), 5) + self.assertBeatable(False) + self.collect(floppies[-2:-1]) # exact + self.assertEqual(self.count("Floppy Disk"), 6) + self.assertBeatable(True) + self.remove([win]) # reset + self.collect(floppies[-1:]) # 1 extra + self.assertEqual(self.count("Floppy Disk"), 7) + self.assertBeatable(True) + + def test_with_everything(self): + self.collect_by_name(["Apple", "Champ", "Red ID Card", "Floppy Disk"]) + self.assertBeatable(True) + + def test_no_jump(self): + self.collect_by_name(["Champ", "Red ID Card", "Floppy Disk"]) + self.assertBeatable(False) + + def test_no_gun(self): + self.collect_by_name(["Apple", "Red ID Card", "Floppy Disk"]) + self.assertBeatable(False) + + def test_no_red(self): + self.collect_by_name(["Apple", "Champ", "Floppy Disk"]) + self.assertBeatable(False) + + +class TestGoalBalanced(ZillionTestBase): + options = { + "start_char": "JJ", + "jump_levels": "balanced", + "gun_levels": "balanced", + } + + def test_jump(self): + self.collect_by_name(["Red ID Card", "Floppy Disk", "Zillion"]) + self.assertBeatable(False) # not enough jump + opas = self.get_items_by_name("Opa-Opa") + self.collect(opas[:1]) # too few + self.assertEqual(self.count("Opa-Opa"), 1) + self.assertBeatable(False) + self.collect(opas[1:]) + self.assertBeatable(True) + + def test_guns(self): + self.collect_by_name(["Red ID Card", "Floppy Disk", "Opa-Opa"]) + self.assertBeatable(False) # not enough gun + guns = self.get_items_by_name("Zillion") + self.collect(guns[:1]) # too few + self.assertEqual(self.count("Zillion"), 1) + self.assertBeatable(False) + self.collect(guns[1:]) + self.assertBeatable(True) + + +class TestGoalRestrictive(ZillionTestBase): + options = { + "start_char": "JJ", + "jump_levels": "restrictive", + "gun_levels": "restrictive", + } + + def test_jump(self): + self.collect_by_name(["Champ", "Red ID Card", "Floppy Disk", "Zillion"]) + self.assertBeatable(False) # not enough jump + self.collect_by_name("Opa-Opa") + self.assertBeatable(False) # with all opas, jj champ can't jump + self.collect_by_name("Apple") + self.assertBeatable(True) + + def test_guns(self): + self.collect_by_name(["Apple", "Red ID Card", "Floppy Disk", "Opa-Opa"]) + self.assertBeatable(False) # not enough gun + self.collect_by_name("Zillion") + self.assertBeatable(False) # with all guns, jj apple can't gun + self.collect_by_name("Champ") + self.assertBeatable(True) + + +class TestGoalAppleStart(ZillionTestBase): + """ creation of character rescue items has some special interactions with logic """ + options = { + "start_char": "Apple", + "jump_levels": "balanced", + "gun_levels": "low", + "zillion_count": 5 + } + + def test_guns_jj_first(self): + """ with low gun levels, 5 Zillion is enough to get JJ to gun 3 """ + self.collect_by_name(["JJ", "Red ID Card", "Floppy Disk", "Opa-Opa"]) + self.assertBeatable(False) # not enough gun + self.collect_by_name("Zillion") + self.assertBeatable(True) + + def test_guns_zillions_first(self): + """ with low gun levels, 5 Zillion is enough to get JJ to gun 3 """ + self.collect_by_name(["Zillion", "Red ID Card", "Floppy Disk", "Opa-Opa"]) + self.assertBeatable(False) # not enough gun + self.collect_by_name("JJ") + self.assertBeatable(True) + + +class TestGoalChampStart(ZillionTestBase): + """ creation of character rescue items has some special interactions with logic """ + options = { + "start_char": "Champ", + "jump_levels": "low", + "gun_levels": "balanced", + "opa_opa_count": 5, + "opas_per_level": 1 + } + + def test_jump_jj_first(self): + """ with low jump levels, 5 level-ups is enough to get JJ to jump 3 """ + self.collect_by_name(["JJ", "Red ID Card", "Floppy Disk", "Zillion"]) + self.assertBeatable(False) # not enough jump + self.collect_by_name("Opa-Opa") + self.assertBeatable(True) + + def test_jump_opa_first(self): + """ with low jump levels, 5 level-ups is enough to get JJ to jump 3 """ + self.collect_by_name(["Opa-Opa", "Red ID Card", "Floppy Disk", "Zillion"]) + self.assertBeatable(False) # not enough jump + self.collect_by_name("JJ") + self.assertBeatable(True) diff --git a/test/worlds/zillion/TestOptions.py b/test/worlds/zillion/TestOptions.py new file mode 100644 index 0000000000..20397a4ec8 --- /dev/null +++ b/test/worlds/zillion/TestOptions.py @@ -0,0 +1,26 @@ +from test.worlds.zillion import ZillionTestBase + +from worlds.zillion.options import ZillionJumpLevels, ZillionGunLevels, validate +from zilliandomizer.options import VBLR_CHOICES + + +class OptionsTest(ZillionTestBase): + auto_construct = False + + def test_validate_default(self) -> None: + self.world_setup() + validate(self.world, 1) + + def test_vblr_ap_to_zz(self) -> None: + """ all of the valid values for the AP options map to valid values for ZZ options """ + for option_name, vblr_class in ( + ("jump_levels", ZillionJumpLevels), + ("gun_levels", ZillionGunLevels) + ): + for value in vblr_class.name_lookup.values(): + self.options = {option_name: value} + self.world_setup() + zz_options, _item_counts = validate(self.world, 1) + assert getattr(zz_options, option_name) in VBLR_CHOICES + + # TODO: test validate with invalid combinations of options diff --git a/test/worlds/zillion/__init__.py b/test/worlds/zillion/__init__.py new file mode 100644 index 0000000000..43100d3a8c --- /dev/null +++ b/test/worlds/zillion/__init__.py @@ -0,0 +1,13 @@ +from test.worlds.test_base import WorldTestBase +from worlds.zillion.region import ZillionLocation + + +class ZillionTestBase(WorldTestBase): + game = "Zillion" + + def world_setup(self) -> None: + super().world_setup() + # make sure game requires gun 3 for tests + for location in self.world.get_locations(): + if isinstance(location, ZillionLocation) and location.name.startswith("O-7"): + location.zz_loc.req.gun = 3 diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index db72ca6a95..8d3fab64cf 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -3,9 +3,9 @@ from __future__ import annotations import logging import sys import pathlib -from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Union, TYPE_CHECKING +from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Type, Union, TYPE_CHECKING -from Options import Option +from Options import AssembleOptions from BaseClasses import CollectionState if TYPE_CHECKING: @@ -13,7 +13,7 @@ if TYPE_CHECKING: class AutoWorldRegister(type): - world_types: Dict[str, type(World)] = {} + world_types: Dict[str, Type[World]] = {} def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoWorldRegister: if "web" in dct: @@ -79,8 +79,16 @@ def call_single(world: "MultiWorld", method_name: str, player: int, *args: Any) def call_all(world: "MultiWorld", method_name: str, *args: Any) -> None: world_types: Set[AutoWorldRegister] = set() for player in world.player_ids: + prev_item_count = len(world.itempool) world_types.add(world.worlds[player].__class__) call_single(world, method_name, player, *args) + if __debug__: + new_items = world.itempool[prev_item_count:] + for i, item in enumerate(new_items): + for other in new_items[i+1:]: + assert item is not other, ( + f"Duplicate item reference of \"{item.name}\" in \"{world.worlds[player].game}\" " + f"of player \"{world.player_name[player]}\". Please make a copy instead.") for world_type in world_types: stage_callable = getattr(world_type, f"stage_{method_name}", None) @@ -120,7 +128,7 @@ 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.""" - option_definitions: Dict[str, Option[Any]] = {} # link your Options mapping + option_definitions: Dict[str, AssembleOptions] = {} # link your Options mapping game: str # name the game topology_present: bool = False # indicate if world type has any meaningful layout/pathing @@ -229,7 +237,8 @@ class World(metaclass=AutoWorldRegister): pass def post_fill(self) -> None: - """Optional Method that is called after regular fill. Can be used to do adjustments before output generation.""" + """Optional Method that is called after regular fill. Can be used to do adjustments before output generation. + This happens before progression balancing, so the items may not be in their final locations yet.""" def generate_output(self, output_directory: str) -> None: """This method gets called from a threadpool, do not use world.random here. @@ -237,7 +246,9 @@ class World(metaclass=AutoWorldRegister): pass def fill_slot_data(self) -> Dict[str, Any]: # json of WebHostLib.models.Slot - """Fill in the slot_data field in the Connected network package.""" + """Fill in the `slot_data` field in the `Connected` network package. + This is a way the generator can give custom data to the client. + The client will receive this as JSON in the `Connected` response.""" return {} def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]): diff --git a/worlds/Files.py b/worlds/Files.py new file mode 100644 index 0000000000..ac1acbf322 --- /dev/null +++ b/worlds/Files.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +import json +import zipfile + +from typing import ClassVar, Dict, Tuple, Any, Optional, Union, BinaryIO + +import bsdiff4 + + +class AutoPatchRegister(type): + patch_types: ClassVar[Dict[str, AutoPatchRegister]] = {} + file_endings: ClassVar[Dict[str, AutoPatchRegister]] = {} + + def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoPatchRegister: + # construct class + new_class = super().__new__(mcs, name, bases, dct) + if "game" in dct: + AutoPatchRegister.patch_types[dct["game"]] = new_class + if not dct["patch_file_ending"]: + raise Exception(f"Need an expected file ending for {name}") + AutoPatchRegister.file_endings[dct["patch_file_ending"]] = new_class + return new_class + + @staticmethod + def get_handler(file: str) -> Optional[AutoPatchRegister]: + for file_ending, handler in AutoPatchRegister.file_endings.items(): + if file.endswith(file_ending): + return handler + return None + + +current_patch_version: int = 5 + + +class APContainer: + """A zipfile containing at least archipelago.json""" + version: int = current_patch_version + compression_level: int = 9 + compression_method: int = zipfile.ZIP_DEFLATED + game: Optional[str] = None + + # instance attributes: + path: Optional[str] + player: Optional[int] + player_name: str + server: str + + def __init__(self, path: Optional[str] = None, player: Optional[int] = None, + player_name: str = "", server: str = ""): + self.path = path + self.player = player + self.player_name = player_name + self.server = server + + def write(self, file: Optional[Union[str, BinaryIO]] = None) -> None: + zip_file = file if file else self.path + if not zip_file: + raise FileNotFoundError(f"Cannot write {self.__class__.__name__} due to no path provided.") + with zipfile.ZipFile(zip_file, "w", self.compression_method, True, self.compression_level) \ + as zf: + if file: + self.path = zf.filename + self.write_contents(zf) + + def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None: + manifest = self.get_manifest() + try: + manifest_str = json.dumps(manifest) + except Exception as e: + raise Exception(f"Manifest {manifest} did not convert to json.") from e + else: + opened_zipfile.writestr("archipelago.json", manifest_str) + + def read(self, file: Optional[Union[str, BinaryIO]] = None) -> None: + """Read data into patch object. file can be file-like, such as an outer zip file's stream.""" + zip_file = file if file else self.path + if not zip_file: + raise FileNotFoundError(f"Cannot read {self.__class__.__name__} due to no path provided.") + with zipfile.ZipFile(zip_file, "r") as zf: + if file: + self.path = zf.filename + self.read_contents(zf) + + def read_contents(self, opened_zipfile: zipfile.ZipFile) -> None: + with opened_zipfile.open("archipelago.json", "r") as f: + manifest = json.load(f) + if manifest["compatible_version"] > self.version: + raise Exception(f"File (version: {manifest['compatible_version']}) too new " + f"for this handler (version: {self.version})") + self.player = manifest["player"] + self.server = manifest["server"] + self.player_name = manifest["player_name"] + + def get_manifest(self) -> Dict[str, Any]: + return { + "server": self.server, # allow immediate connection to server in multiworld. Empty string otherwise + "player": self.player, + "player_name": self.player_name, + "game": self.game, + # minimum version of patch system expected for patching to be successful + "compatible_version": 5, + "version": current_patch_version, + } + + +class APDeltaPatch(APContainer, metaclass=AutoPatchRegister): + """An APContainer that additionally has delta.bsdiff4 + containing a delta patch to get the desired file, often a rom.""" + + hash: Optional[str] # base checksum of source file + patch_file_ending: str = "" + delta: Optional[bytes] = None + result_file_ending: str = ".sfc" + source_data: bytes + + def __init__(self, *args: Any, patched_path: str = "", **kwargs: Any) -> None: + self.patched_path = patched_path + super(APDeltaPatch, self).__init__(*args, **kwargs) + + def get_manifest(self) -> Dict[str, Any]: + manifest = super(APDeltaPatch, self).get_manifest() + manifest["base_checksum"] = self.hash + manifest["result_file_ending"] = self.result_file_ending + manifest["patch_file_ending"] = self.patch_file_ending + return manifest + + @classmethod + def get_source_data(cls) -> bytes: + """Get Base data""" + raise NotImplementedError() + + @classmethod + def get_source_data_with_cache(cls) -> bytes: + if not hasattr(cls, "source_data"): + cls.source_data = cls.get_source_data() + return cls.source_data + + def write_contents(self, opened_zipfile: zipfile.ZipFile): + super(APDeltaPatch, self).write_contents(opened_zipfile) + # write Delta + opened_zipfile.writestr("delta.bsdiff4", + bsdiff4.diff(self.get_source_data_with_cache(), open(self.patched_path, "rb").read()), + compress_type=zipfile.ZIP_STORED) # bsdiff4 is a format with integrated compression + + def read_contents(self, opened_zipfile: zipfile.ZipFile): + super(APDeltaPatch, self).read_contents(opened_zipfile) + self.delta = opened_zipfile.read("delta.bsdiff4") + + def patch(self, target: str): + """Base + Delta -> Patched""" + if not self.delta: + self.read() + result = bsdiff4.patch(self.get_source_data_with_cache(), self.delta) + with open(target, "wb") as f: + f.write(result) diff --git a/worlds/__init__.py b/worlds/__init__.py index e36eb275a3..039f12eb93 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -1,7 +1,9 @@ import importlib -import zipimport import os +import sys import typing +import warnings +import zipimport folder = os.path.dirname(__file__) @@ -39,7 +41,14 @@ 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]) + spec = importer.find_spec(world_source.path.split(".", 1)[0]) + mod = importlib.util.module_from_spec(spec) + mod.__package__ = f"worlds.{mod.__package__}" + mod.__name__ = f"worlds.{mod.__name__}" + sys.modules[mod.__name__] = mod + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message="__package__ != __spec__.parent") + importer.exec_module(mod) else: importlib.import_module(f".{world_source.path}", "worlds") diff --git a/worlds/alttp/Bosses.py b/worlds/alttp/Bosses.py index 1c381b9a1c..870b3c7c2f 100644 --- a/worlds/alttp/Bosses.py +++ b/worlds/alttp/Bosses.py @@ -3,7 +3,7 @@ from typing import Optional, Union, List, Tuple, Callable, Dict from BaseClasses import Boss from Fill import FillError -from .Options import Bosses +from .Options import LTTPBosses as Bosses def BossFactory(boss: str, player: int) -> Optional[Boss]: diff --git a/worlds/alttp/EntranceShuffle.py b/worlds/alttp/EntranceShuffle.py index c2b56d6fd4..e10f4d5445 100644 --- a/worlds/alttp/EntranceShuffle.py +++ b/worlds/alttp/EntranceShuffle.py @@ -3834,14 +3834,21 @@ inverted_default_dungeon_connections = [('Desert Palace Entrance (South)', 'Dese # Regions that can be required to access entrances through rules, not paths indirect_connections = { - 'Turtle Rock (Top)': 'Turtle Rock', - 'East Dark World': 'Pyramid Fairy', - 'Big Bomb Shop': 'Pyramid Fairy', - 'Dark Desert': 'Pyramid Fairy', - 'West Dark World': 'Pyramid Fairy', - 'South Dark World': 'Pyramid Fairy', - 'Light World': 'Pyramid Fairy', - 'Old Man Cave': 'Old Man S&Q' + "Turtle Rock (Top)": "Turtle Rock", + "East Dark World": "Pyramid Fairy", + "Dark Desert": "Pyramid Fairy", + "West Dark World": "Pyramid Fairy", + "South Dark World": "Pyramid Fairy", + "Light World": "Pyramid Fairy", + "Old Man Cave": "Old Man S&Q" +} + +indirect_connections_inverted = { + "Inverted Big Bomb Shop": "Pyramid Fairy", +} + +indirect_connections_not_inverted = { + "Big Bomb Shop": "Pyramid Fairy", } # format: diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index b13d99f1e7..de6c479bd3 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -1,7 +1,7 @@ import typing from BaseClasses import MultiWorld -from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, TextChoice +from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, TextChoice, PlandoBosses class Logic(Choice): @@ -138,7 +138,7 @@ class WorldState(Choice): option_inverted = 2 -class Bosses(TextChoice): +class LTTPBosses(PlandoBosses): """Shuffles bosses around to different locations. Basic will shuffle all bosses except Ganon and Agahnim anywhere they can be placed. Full chooses 3 bosses at random to be placed twice instead of Lanmolas, Moldorm, and Helmasaur. @@ -152,7 +152,9 @@ class Bosses(TextChoice): option_chaos = 3 option_singularity = 4 - bosses: set = { + duplicate_bosses = True + + bosses = { "Armos Knights", "Lanmolas", "Moldorm", @@ -165,7 +167,7 @@ class Bosses(TextChoice): "Trinexx", } - locations: set = { + locations = { "Ganons Tower Top", "Tower of Hera", "Skull Woods", @@ -181,99 +183,16 @@ class Bosses(TextChoice): "Ganons Tower Bottom" } - def __init__(self, value: typing.Union[str, int]): - assert isinstance(value, str) or isinstance(value, int), \ - f"{value} is not a valid option for {self.__class__.__name__}" - self.value = value - @classmethod - def from_text(cls, text: str): - import random - # set all of our text to lower case for name checking - text = text.lower() - cls.bosses = {boss_name.lower() for boss_name in cls.bosses} - cls.locations = {boss_location.lower() for boss_location in cls.locations} - if text == "random": - return cls(random.choice(list(cls.options.values()))) - for option_name, value in cls.options.items(): - if option_name == text: - return cls(value) - options = text.split(";") - - # since plando exists in the option verify the plando values given are valid - cls.validate_plando_bosses(options) - - # find out what type of boss shuffle we should use for placing bosses after plando - # and add as a string to look nice in the spoiler - if "random" in options: - shuffle = random.choice(list(cls.options)) - options.remove("random") - options = ";".join(options) + ";" + shuffle - boss_class = cls(options) - else: - for option in options: - if option in cls.options: - boss_class = cls(";".join(options)) - break - else: - if len(options) == 1: - if cls.valid_boss_name(options[0]): - options = options[0] + ";singularity" - boss_class = cls(options) - else: - options = options[0] + ";none" - boss_class = cls(options) - else: - options = ";".join(options) + ";none" - boss_class = cls(options) - return boss_class - - @classmethod - def validate_plando_bosses(cls, options: typing.List[str]) -> None: - from .Bosses import can_place_boss, format_boss_location - for option in options: - if option == "random" or option in cls.options: - if option != options[-1]: - raise ValueError(f"{option} option must be at the end of the boss_shuffle options!") - continue - if "-" in option: - location, boss = option.split("-") - level = '' - if not cls.valid_boss_name(boss): - raise ValueError(f"{boss} is not a valid boss name for location {location}.") - if not cls.valid_location_name(location): - raise ValueError(f"{location} is not a valid boss location name.") - if location.split(" ")[-1] in ("top", "middle", "bottom"): - location = location.split(" ") - level = location[-1] - location = " ".join(location[:-1]) - location = location.title().replace("Of", "of") - if not can_place_boss(boss.title(), location, level): - raise ValueError(f"{format_boss_location(location, level)} " - f"is not a valid location for {boss.title()}.") - else: - if not cls.valid_boss_name(option): - raise ValueError(f"{option} is not a valid boss name.") - - @classmethod - def valid_boss_name(cls, value: str) -> bool: - return value.lower() in cls.bosses - - @classmethod - def valid_location_name(cls, value: str) -> bool: - return value in cls.locations - - def verify(self, world, player_name: str, plando_options) -> None: - if isinstance(self.value, int): - return - from Generate import PlandoSettings - if not(PlandoSettings.bosses & plando_options): - import logging - # plando is disabled but plando options were given so pull the option and change it to an int - option = self.value.split(";")[-1] - self.value = self.options[option] - logging.warning(f"The plando bosses module is turned off, so {self.name_lookup[self.value].title()} " - f"boss shuffle will be used for player {player_name}.") + def can_place_boss(cls, boss: str, location: str) -> bool: + from worlds.alttp.Bosses import can_place_boss + level = '' + words = location.split(" ") + if words[-1] in ("top", "middle", "bottom"): + level = words[-1] + location = " ".join(words[:-1]) + location = location.title().replace("Of", "of") + return can_place_boss(boss.title(), location, level) class Enemies(Choice): @@ -497,7 +416,7 @@ alttp_options: typing.Dict[str, type(Option)] = { "hints": Hints, "scams": Scams, "restrict_dungeon_item_on_boss": RestrictBossItem, - "boss_shuffle": Bosses, + "boss_shuffle": LTTPBosses, "pot_shuffle": PotShuffle, "enemy_shuffle": EnemyShuffle, "killable_thieves": KillableThieves, diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 9ca3a355e1..24a1588c52 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -1,11 +1,12 @@ from __future__ import annotations import Utils -from Patch import read_rom +import worlds.AutoWorld +import worlds.Files -LTTPJPN10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '9952c2a3ec1b421e408df0d20c8f0c7f' -ROM_PLAYER_LIMIT = 255 +LTTPJPN10HASH: str = "03a63945398191337e896e5771f77173" +RANDOMIZERBASEHASH: str = "9952c2a3ec1b421e408df0d20c8f0c7f" +ROM_PLAYER_LIMIT: int = 255 import io import json @@ -34,7 +35,7 @@ from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts DeathMountain_texts, \ LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, \ SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names -from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml +from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml, read_snes_rom from worlds.alttp.Items import ItemFactory, item_table, item_name_groups, progression_items from worlds.alttp.EntranceShuffle import door_addresses from worlds.alttp.Options import smallkey_shuffle @@ -57,13 +58,13 @@ class LocalRom(object): self.orig_buffer = None with open(file, 'rb') as stream: - self.buffer = read_rom(stream) + self.buffer = read_snes_rom(stream) if patch: self.patch_base_rom() self.orig_buffer = self.buffer.copy() if vanillaRom: with open(vanillaRom, 'rb') as vanillaStream: - self.orig_buffer = read_rom(vanillaStream) + self.orig_buffer = read_snes_rom(vanillaStream) def read_byte(self, address: int) -> int: return self.buffer[address] @@ -123,29 +124,24 @@ class LocalRom(object): return expected == buffermd5.hexdigest() def patch_base_rom(self): - if os.path.isfile(local_path('basepatch.sfc')): - with open(local_path('basepatch.sfc'), 'rb') as stream: + if os.path.isfile(user_path('basepatch.sfc')): + with open(user_path('basepatch.sfc'), 'rb') as stream: buffer = bytearray(stream.read()) if self.verify(buffer): self.buffer = buffer - if not os.path.exists(local_path('data', 'basepatch.apbp')): - Patch.create_patch_file(local_path('basepatch.sfc')) return - if not os.path.isfile(local_path('data', 'basepatch.apbp')): - raise RuntimeError('Base patch unverified. Unable to continue.') + with open(local_path("data", "basepatch.bsdiff4"), "rb") as f: + delta = f.read() - if os.path.isfile(local_path('data', 'basepatch.apbp')): - _, target, buffer = Patch.create_rom_bytes(local_path('data', 'basepatch.apbp'), ignore_version=True) - if self.verify(buffer): - self.buffer = bytearray(buffer) - with open(user_path('basepatch.sfc'), 'wb') as stream: - stream.write(buffer) - return - raise RuntimeError('Base patch unverified. Unable to continue.') - - raise RuntimeError('Could not find Base Patch. Unable to continue.') + buffer = bsdiff4.patch(get_base_rom_bytes(), delta) + if self.verify(buffer): + self.buffer = bytearray(buffer) + with open(user_path('basepatch.sfc'), 'wb') as stream: + stream.write(buffer) + return + raise RuntimeError('Base patch unverified. Unable to continue.') def write_crc(self): crc = (sum(self.buffer[:0x7FDC] + self.buffer[0x7FE0:]) + 0x01FE) & 0xFFFF @@ -544,7 +540,7 @@ class Sprite(): def get_vanilla_sprite_data(self): file_name = get_base_rom_path() - base_rom_bytes = bytes(read_rom(open(file_name, "rb"))) + base_rom_bytes = bytes(read_snes_rom(open(file_name, "rb"))) Sprite.sprite = base_rom_bytes[0x80000:0x87000] Sprite.palette = base_rom_bytes[0xDD308:0xDD380] Sprite.glove_palette = base_rom_bytes[0xDEDF5:0xDEDF9] @@ -2906,7 +2902,7 @@ hash_alphabet = [ ] -class LttPDeltaPatch(Patch.APDeltaPatch): +class LttPDeltaPatch(worlds.Files.APDeltaPatch): hash = LTTPJPN10HASH game = "A Link to the Past" patch_file_ending = ".aplttp" @@ -2920,7 +2916,7 @@ 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"))) + base_rom_bytes = bytes(read_snes_rom(open(file_name, "rb"))) basemd5 = hashlib.md5() basemd5.update(base_rom_bytes) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index bbdd941127..ce53154e92 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -7,7 +7,8 @@ import typing import Utils from BaseClasses import Item, CollectionState, Tutorial from .Dungeons import create_dungeons -from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect +from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect, \ + indirect_connections, indirect_connections_inverted, indirect_connections_not_inverted 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 @@ -19,7 +20,7 @@ from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enem from .Rules import set_rules from .Shops import create_shops, ShopSlotFill from .SubClasses import ALttPItem -from ..AutoWorld import World, WebWorld, LogicMixin +from worlds.AutoWorld import World, WebWorld, LogicMixin lttp_logger = logging.getLogger("A Link to the Past") @@ -216,13 +217,24 @@ class ALTTPWorld(World): if world.mode[player] != 'inverted': link_entrances(world, player) mark_light_world_regions(world, player) + for region_name, entrance_name in indirect_connections_not_inverted.items(): + world.register_indirect_condition(self.world.get_region(region_name, player), + self.world.get_entrance(entrance_name, player)) else: link_inverted_entrances(world, player) mark_dark_world_regions(world, player) + for region_name, entrance_name in indirect_connections_inverted.items(): + world.register_indirect_condition(self.world.get_region(region_name, player), + self.world.get_entrance(entrance_name, player)) world.random = old_random plando_connect(world, player) + for region_name, entrance_name in indirect_connections.items(): + world.register_indirect_condition(self.world.get_region(region_name, player), + self.world.get_entrance(entrance_name, player)) + + def collect_item(self, state: CollectionState, item: Item, remove=False): item_name = item.name if item_name.startswith('Progressive '): @@ -396,11 +408,7 @@ class ALTTPWorld(World): deathlink=world.death_link[player], allowcollect=world.allow_collect[player]) - outfilepname = f'_P{player}' - outfilepname += f"_{world.get_file_safe_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') + rompath = os.path.join(output_directory, f"{self.world.get_out_file_name_base(self.player)}.sfc") rom.write_to_file(rompath) patch = LttPDeltaPatch(os.path.splitext(rompath)[0]+LttPDeltaPatch.patch_file_ending, player=player, player_name=world.player_name[player], patched_path=rompath) @@ -490,11 +498,15 @@ class ALTTPWorld(World): while gtower_locations and filleritempool and trash_count > 0: spot_to_fill = gtower_locations.pop() - item_to_place = filleritempool.pop() - if spot_to_fill.item_rule(item_to_place): - world.push_item(spot_to_fill, item_to_place, False) - fill_locations.remove(spot_to_fill) # very slow, unfortunately - trash_count -= 1 + for index, item in enumerate(filleritempool): + if spot_to_fill.item_rule(item): + filleritempool.pop(index) # remove from outer fill + world.push_item(spot_to_fill, item, False) + fill_locations.remove(spot_to_fill) # very slow, unfortunately + trash_count -= 1 + break + else: + logging.warning(f"Could not trash fill Ganon's Tower for player {player}.") def get_filler_item_name(self) -> str: if self.world.goal[self.player] == "icerodhunt": diff --git a/worlds/bk_sudoku/__init__.py b/worlds/bk_sudoku/__init__.py new file mode 100644 index 0000000000..7183b0ec18 --- /dev/null +++ b/worlds/bk_sudoku/__init__.py @@ -0,0 +1,33 @@ +from BaseClasses import Tutorial +from ..AutoWorld import World, WebWorld +from typing import Dict + + +class Bk_SudokuWebWorld(WebWorld): + settings_page = "games/Sudoku/info/en" + theme = 'partyTime' + tutorials = [ + Tutorial( + tutorial_name='Setup Guide', + description='A guide to playing BK Sudoku', + language='English', + file_name='setup_en.md', + link='guide/en', + authors=['Jarno'] + ) + ] + + +class Bk_SudokuWorld(World): + """ + Play a little Sudoku while you're in BK mode to maybe get some useful hints + """ + game = "Sudoku" + web = Bk_SudokuWebWorld() + + item_name_to_id: Dict[str, int] = {} + location_name_to_id: Dict[str, int] = {} + + @classmethod + def stage_assert_generate(cls, world): + raise Exception("BK Sudoku cannot be used for generating worlds, the client can instead connect to any other world") diff --git a/worlds/bk_sudoku/docs/en_Sudoku.md b/worlds/bk_sudoku/docs/en_Sudoku.md new file mode 100644 index 0000000000..072e43a980 --- /dev/null +++ b/worlds/bk_sudoku/docs/en_Sudoku.md @@ -0,0 +1,13 @@ +# Bk Sudoku + +## What is this game? + +BK Sudoku is not a typical Archipelago game; instead, it is a generic Sudoku client that can connect to any existing multiworld. When connected, you can play Sudoku to unlock random hints for your game. While slow, it will give you something to do when you can't reach the checks in your game. + +## What hints are unlocked? + +After completing a Sudoku puzzle, the game will unlock 1 random hint for an unchecked location in the slot you are connected to. It is possible to hint the same location repeatedly if that location is still unchecked. + +## Where is the settings page? + +There is no settings page; this game cannot be used in your .yamls. Instead, the client can connect to any slot in a multiworld. diff --git a/worlds/bk_sudoku/docs/setup_en.md b/worlds/bk_sudoku/docs/setup_en.md new file mode 100644 index 0000000000..5e93dce873 --- /dev/null +++ b/worlds/bk_sudoku/docs/setup_en.md @@ -0,0 +1,24 @@ +# BK Sudoku Setup Guide + +## Required Software +- [Bk Sudoku](https://github.com/Jarno458/sudoku) +- [.Net 6](https://docs.microsoft.com/en-us/dotnet/core/install/windows?tabs=net60) + +## General Concept + +This is a client that can connect to any multiworld slot, and lets you play Sudoku to unlock random hints for that slot's locations. + +Due to the fact that the Sudoku client may connect to any slot, it is not necessary to generate a YAML for this game as it does not generate any new slots in the multiworld session. + +## Installation Procedures + +Go to the latest release on [BK Sudoku Releases](https://github.com/Jarno458/sudoku/releases). Download and extract the `Bk_Sudoku.zip` file. + +## Joining a MultiWorld Game + +1. Run Bk_Sudoku.exe +2. Enter the name of the slot you wish to connect to +3. Enter the server url & port number +4. Press connect +5. Choose difficulty +6. Try to solve the Sudoku diff --git a/worlds/dkc3/Names/__init__.py b/worlds/dkc3/Names/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/worlds/dkc3/Rom.py b/worlds/dkc3/Rom.py index 2bb5221a60..6308e95860 100644 --- a/worlds/dkc3/Rom.py +++ b/worlds/dkc3/Rom.py @@ -1,5 +1,6 @@ import Utils -from Patch import read_rom, APDeltaPatch +from Utils import read_snes_rom +from worlds.Files import APDeltaPatch from .Locations import lookup_id_to_name, all_locations from .Levels import level_list, level_dict @@ -440,13 +441,13 @@ class LocalRom(object): self.orig_buffer = None with open(file, 'rb') as stream: - self.buffer = read_rom(stream) + self.buffer = read_snes_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) + # self.orig_buffer = read_snes_rom(vanillaStream) def read_bit(self, address: int, bit_number: int) -> bool: bitflag = (1 << bit_number) @@ -724,7 +725,7 @@ 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"))) + base_rom_bytes = bytes(read_snes_rom(open(file_name, "rb"))) basemd5 = hashlib.md5() basemd5.update(base_rom_bytes) diff --git a/worlds/dkc3/__init__.py b/worlds/dkc3/__init__.py index 5c575b85b5..d45de8f85a 100644 --- a/worlds/dkc3/__init__.py +++ b/worlds/dkc3/__init__.py @@ -65,10 +65,6 @@ class DKC3World(World): "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: @@ -113,17 +109,17 @@ class DKC3World(World): 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 + itempool += [self.create_item(ItemName.bonus_coin) for _ in range(number_of_bonus_coins)] + itempool += [self.create_item(ItemName.dk_coin) for _ in range(41)] + itempool += [self.create_item(ItemName.banana_bird) for _ in range(number_of_banana_birds)] + itempool += [self.create_item(ItemName.krematoa_cog) for _ in range(number_of_cogs)] + itempool += [self.create_item(ItemName.progressive_boat) for _ in range(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)] + junk_pool.append(self.create_item(item_name)) itempool += junk_pool @@ -146,11 +142,7 @@ class DKC3World(World): 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') + rompath = os.path.join(output_directory, f"{self.world.get_out_file_name_base(self.player)}.sfc") rom.write_to_file(rompath) self.rom_name = rom.name diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 89666ffbdd..018816d90a 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -11,6 +11,8 @@ import shutil import Utils import Patch +import worlds.AutoWorld +import worlds.Files from . import Options from .Technologies import tech_table, recipes, free_sample_exclusions, progressive_technology_table, \ @@ -57,7 +59,7 @@ recipe_time_ranges = { } -class FactorioModFile(Patch.APContainer): +class FactorioModFile(worlds.Files.APContainer): game = "Factorio" compression_method = zipfile.ZIP_DEFLATED # Factorio can't load LZMA archives diff --git a/worlds/factorio/docs/setup_en.md b/worlds/factorio/docs/setup_en.md index 7ee91fb4f1..560a37d1e3 100644 --- a/worlds/factorio/docs/setup_en.md +++ b/worlds/factorio/docs/setup_en.md @@ -118,7 +118,7 @@ This allows you to host your own Factorio game. 3. Install the mod into your Factorio Client by copying the zip file into the `mods` folder, which is likely located at `C:\Users\YourName\AppData\Roaming\Factorio\mods`. 4. Obtain the Archipelago Server address from the website's host room, or from the server host. -5. Run your Archipelago Client, which is named `ArchilepagoFactorioClient.exe`. This was installed along with +5. Run your Archipelago Client, which is named `ArchipelagoFactorioClient.exe`. This was installed along with Archipelago if you chose to include it during the installation process. 6. Enter `/connect [server-address]` into the input box at the bottom of the Archipelago Client and press "Enter" diff --git a/worlds/generic/Rules.py b/worlds/generic/Rules.py index f53c417e1c..e69a34c504 100644 --- a/worlds/generic/Rules.py +++ b/worlds/generic/Rules.py @@ -1,6 +1,7 @@ +import collections import typing -from BaseClasses import LocationProgressType +from BaseClasses import LocationProgressType, MultiWorld if typing.TYPE_CHECKING: import BaseClasses @@ -12,39 +13,82 @@ else: ItemRule = typing.Callable[[object], bool] -def group_locality_rules(world): +def locality_needed(world: MultiWorld) -> bool: + for player in world.player_ids: + if world.local_items[player].value: + return True + if world.non_local_items[player].value: + return True + + # Group for group_id, group in world.groups.items(): if set(world.player_ids) == set(group["players"]): continue if group["local_items"]: - for location in world.get_locations(): - if location.player not in group["players"]: - forbid_items_for_player(location, group["local_items"], group_id) + return True if group["non_local_items"]: - for location in world.get_locations(): - if location.player in group["players"]: - forbid_items_for_player(location, group["non_local_items"], group_id) + return True -def locality_rules(world, player: int): - if world.local_items[player].value: +def locality_rules(world: MultiWorld): + if locality_needed(world): + + forbid_data: typing.Dict[int, typing.Dict[int, typing.Set[str]]] = \ + collections.defaultdict(lambda: collections.defaultdict(set)) + + def forbid(sender: int, receiver: int, items: typing.Set[str]): + forbid_data[sender][receiver].update(items) + + for receiving_player in world.player_ids: + local_items: typing.Set[str] = world.local_items[receiving_player].value + if local_items: + for sending_player in world.player_ids: + if receiving_player != sending_player: + forbid(sending_player, receiving_player, local_items) + non_local_items: typing.Set[str] = world.non_local_items[receiving_player].value + if non_local_items: + forbid(receiving_player, receiving_player, non_local_items) + + # Group + for receiving_group_id, receiving_group in world.groups.items(): + if set(world.player_ids) == set(receiving_group["players"]): + continue + if receiving_group["local_items"]: + for sending_player in world.player_ids: + if sending_player not in receiving_group["players"]: + forbid(sending_player, receiving_group_id, receiving_group["local_items"]) + if receiving_group["non_local_items"]: + for sending_player in world.player_ids: + if sending_player in receiving_group["players"]: + forbid(sending_player, receiving_group_id, receiving_group["non_local_items"]) + + # create fewer lambda's to save memory and cache misses + func_cache = {} for location in world.get_locations(): - if location.player != player: - forbid_items_for_player(location, world.local_items[player].value, player) - if world.non_local_items[player].value: - for location in world.get_locations(): - if location.player == player: - forbid_items_for_player(location, world.non_local_items[player].value, player) + if (location.player, location.item_rule) in func_cache: + location.item_rule = func_cache[location.player, location.item_rule] + # empty rule that just returns True, overwrite + elif location.item_rule is location.__class__.item_rule: + func_cache[location.player, location.item_rule] = location.item_rule = \ + lambda i, sending_blockers = forbid_data[location.player], \ + old_rule = location.item_rule: \ + i.name not in sending_blockers[i.player] + # special rule, needs to also be fulfilled. + else: + func_cache[location.player, location.item_rule] = location.item_rule = \ + lambda i, sending_blockers = forbid_data[location.player], \ + old_rule = location.item_rule: \ + i.name not in sending_blockers[i.player] and old_rule(i) -def exclusion_rules(world, player: int, exclude_locations: typing.Set[str]): +def exclusion_rules(world: MultiWorld, player: int, exclude_locations: typing.Set[str]) -> None: for loc_name in exclude_locations: try: location = world.get_location(loc_name, player) except KeyError as e: # failed to find the given location. Check if it's a legitimate location if loc_name not in world.worlds[player].location_name_to_id: raise Exception(f"Unable to exclude location {loc_name} in player {player}'s world.") from e - else: + else: add_item_rule(location, lambda i: not (i.advancement or i.useful)) location.progress_type = LocationProgressType.EXCLUDED @@ -53,17 +97,25 @@ def set_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], spot.access_rule = rule -def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule, combine='and'): +def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule, combine="and"): old_rule = spot.access_rule - if combine == 'or': - spot.access_rule = lambda state: rule(state) or old_rule(state) + # empty rule, replace instead of add + if old_rule is spot.__class__.access_rule: + spot.access_rule = rule if combine == "and" else old_rule else: - spot.access_rule = lambda state: rule(state) and old_rule(state) + if combine == "and": + spot.access_rule = lambda state: rule(state) and old_rule(state) + else: + spot.access_rule = lambda state: rule(state) or old_rule(state) def forbid_item(location: "BaseClasses.Location", item: str, player: int): old_rule = location.item_rule - location.item_rule = lambda i: (i.name != item or i.player != player) and old_rule(i) + # empty rule + if old_rule is location.__class__.item_rule: + location.item_rule = lambda i: i.name != item or i.player != player + else: + location.item_rule = lambda i: (i.name != item or i.player != player) and old_rule(i) def forbid_items_for_player(location: "BaseClasses.Location", items: typing.Set[str], player: int): @@ -77,9 +129,16 @@ def forbid_items(location: "BaseClasses.Location", items: typing.Set[str]): location.item_rule = lambda i: i.name not in items and old_rule(i) -def add_item_rule(location: "BaseClasses.Location", rule: ItemRule): +def add_item_rule(location: "BaseClasses.Location", rule: ItemRule, combine: str = "and"): old_rule = location.item_rule - location.item_rule = lambda item: rule(item) and old_rule(item) + # empty rule, replace instead of add + if old_rule is location.__class__.item_rule: + location.item_rule = rule if combine == "and" else old_rule + else: + if combine == "and": + location.item_rule = lambda item: rule(item) and old_rule(item) + else: + location.item_rule = lambda item: rule(item) or old_rule(item) def item_in_locations(state: "BaseClasses.CollectionState", item: str, player: int, diff --git a/worlds/generic/docs/plando_en.md b/worlds/generic/docs/plando_en.md index fa3edd1fa1..c9f70fcb90 100644 --- a/worlds/generic/docs/plando_en.md +++ b/worlds/generic/docs/plando_en.md @@ -2,23 +2,24 @@ ## What is Plando? -The purposes of randomizers is to randomize the items in a game to give a new experience. Plando takes this concept and +The purpose of randomizers is to randomize the items in a game to give a new experience. Plando takes this concept and changes it up by allowing you to plan out certain aspects of the game by placing certain items in certain locations, 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, only LTTP supports text and boss plando. Support for connection plando may vary. +and `connection plando`. Every game in Archipelago supports item plando but the other plando options are only supported +by certain games. Currently, only A Link to the Past supports text and boss plando. Support for connection plando may +vary. ### Enabling Plando -On the website plando will already be enabled. If you will be generating the game locally plando features must be +On the website, plando will already be enabled. If you will be generating the game locally, plando features must be enabled (opt-in). -* To opt-in go to the archipelago installation (default: `C:\ProgramData\Archipelago`), open the host.yaml with a text +* To opt-in go to the Archipelago installation (default: `C:\ProgramData\Archipelago`), open `host.yaml` with a text editor and find the `plando_options` key. The available plando modules can be enabled by adding them after this such as `plando_options: bosses, items, texts, connections`. -* You can add the necessary plando modules for your settings to the `requires` section of your yaml. Doing so will throw an error if the options that you need to generate properly are not enabled to ensure you will get the results you desire. Only enter in the plando modules that you are using here but it should look like: +* You can add the necessary plando modules for your settings to the `requires` section of your YAML. Doing so will throw an error if the options that you need to generate properly are not enabled to ensure you will get the results you desire. Only enter in the plando modules that you are using here but it should look like: ```yaml requires: @@ -27,45 +28,45 @@ enabled (opt-in). ``` ## Item Plando -Item plando allows a player to place an item in a specific location or specific locations, place multiple items into a +Item plando allows a player to place an item in a specific location or specific locations, or place multiple items into a list of specific locations both in their own game or in another player's game. -* The options for item plando are `from_pool`, `world`, `percentage`, `force`, `count`, and either item and location, or items - and locations. +* The options for item plando are `from_pool`, `world`, `percentage`, `force`, `count`, and either `item` and + `location`, or `items` and `locations`. * `from_pool` determines if the item should be taken *from* the item pool or *added* to it. This can be true or false and defaults to true if omitted. * `world` is the target world to place the item in. * It gets ignored if only one world is generated. * Can be a number, name, true, false, null, or a list. False is the default. - * If a number is used it targets that slot or player number in the multiworld. - * If a name is used it will target the world with that player name. - * If set to true it will be any player's world besides your own. - * If set to false it will target your own world. - * If set to null it will target a random world in the multiworld. + * If a number is used, it targets that slot or player number in the multiworld. + * If a name is used, it will target the world with that player name. + * If set to true, it will be any player's world besides your own. + * If set to false, it will target your own world. + * If set to null, it will target a random world in the multiworld. * If a list of names is used, it will target the games with the player names specified. - * `force` determines whether the generator will fail if the item can't be placed in the location can be true, false, + * `force` determines whether the generator will fail if the item can't be placed in the location. Can be true, false, or silent. Silent is the default. - * If set to true the item must be placed and the generator will throw an error if it is unable to do so. - * If set to false the generator will log a warning if the placement can't be done but will still generate. - * If set to silent and the placement fails it will be ignored entirely. + * If set to true, the item must be placed and the generator will throw an error if it is unable to do so. + * If set to false, the generator will log a warning if the placement can't be done but will still generate. + * If set to silent and the placement fails, it will be ignored entirely. * `percentage` is the percentage chance for the relevant block to trigger. This can be any value from 0 to 100 and if omitted will default to 100. * Single Placement is when you use a plando block to place a single item at a single location. * `item` is the item you would like to place and `location` is the location to place it. * Multi Placement uses a plando block to place multiple items in multiple locations until either list is exhausted. - * `items` defines the items to use and a number letting you place multiple of it. You can use true instead of a number to have it use however many of that item are in your item pool. + * `items` defines the items to use, each with a number for the amount. Using `true` instead of a number uses however many of that item are in your item pool. * `locations` is a list of possible locations those items can be placed in. * Using the multi placement method, placements are picked randomly. - * Instead of a number, you can use true + * `count` can be used to set the maximum number of items placed from the block. The default is 1 if using `item` and False if using `items` - * If a number is used it will try to place this number of items. - * If set to false it will try to place as many items from the block as it can. - * If `min` and `max` are defined, it will try to place a number of items between these two numbers at random + * If a number is used, it will try to place this number of items. + * If set to false, it will try to place as many items from the block as it can. + * If `min` and `max` are defined, it will try to place a number of items between these two numbers at random. ### Available Items and Locations -A list of all available items and locations can be found in the [website's datapackage](/datapackage). The items and locations will be in the `"item_name_to_id"` and `"location_name_to_id"` sections of the relevant game. You do not need the quotes but the name must be entered in the same as it appears on that page and is caps-sensitive. +A list of all available items and locations can be found in the [website's datapackage](/datapackage). The items and locations will be in the `"item_name_to_id"` and `"location_name_to_id"` sections of the relevant game. You do not need the quotes but the name must be entered in the same as it appears on that page and is case-sensitive. ### Examples @@ -142,43 +143,43 @@ plando_items: min: 1 max: 4 ``` -1. This block has a 50% chance to occur, and if it does will place either the Empire Orb or Radiant Orb on another player's -Starter Chest 1 and removes the chosen item from the item pool. +1. This block has a 50% chance to occur, and if it does, it will place either the Empire Orb or Radiant Orb on another +player's Starter Chest 1 and removes the chosen item from the item pool. 2. This block will always trigger and will place the player's swords, bow, magic meter, strength upgrades, and hookshots in their own dungeon major item chests. 3. This block will always trigger and will lock boss relics on the bosses. -4. This block has an 80% chance of occurring and when it does will place all but 1 of the items randomly among the four -locations chosen here. +4. This block has an 80% chance of occurring, and when it does, it will place all but 1 of the items randomly among the +four locations chosen here. 5. This block will always trigger and will attempt to place a random 2 of Levitate, Revealer and Energize into other players' Master Sword Pedestals or Boss Relic 1 locations. 6. This block will always trigger and will attempt to place a random number, between 1 and 4, of progressive swords -into any locations within the game slots named BobsSlaytheSpire and BobsRogueLegacy +into any locations within the game slots named BobsSlaytheSpire and BobsRogueLegacy. ## Boss Plando -As this is currently only supported by A Link to the Past instead of explaining here please refer to the +As this is currently only supported by A Link to the Past, instead of finding an explanation here, please refer to the relevant guide: [A Link to the Past Plando Guide](/tutorial/A%20Link%20to%20the%20Past/plando/en) ## Text Plando -As this is currently only supported by A Link to the Past instead of explaining here please refer to the +As this is currently only supported by A Link to the Past, instead of finding an explanation here, please refer to the relevant guide: [A Link to the Past Plando Guide](/tutorial/A%20Link%20to%20the%20Past/plando/en) ## Connections Plando This is currently only supported by Minecraft and A Link to the Past. As the way that these games interact with their -connections is different I will only explain the basics here while more specifics for Link to the Past connection plando -can be found in its plando guide. +connections is different, I will only explain the basics here, while more specifics for A Link to the Past connection +plando can be found in its plando guide. -* The options for connections are `percentage`, `entrance`, `exit`, and `direction`. Each of these options support +* The options for connections are `percentage`, `entrance`, `exit`, and `direction`. Each of these options supports subweights. * `percentage` is the percentage chance for this connection from 0 to 100 and defaults to 100. * Every connection has an `entrance` and an `exit`. These can be unlinked like in A Link to the Past insanity entrance shuffle. * `direction` can be `both`, `entrance`, or `exit` and determines in which direction this connection will operate. -[Link to the Past connections](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/EntranceShuffle.py#L3852) +[A Link to the Past connections](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/EntranceShuffle.py#L3852) [Minecraft connections](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/minecraft/Regions.py#L62) @@ -186,7 +187,7 @@ can be found in its plando guide. ```yaml plando_connections: - # example block 1 - Link to the Past + # example block 1 - A Link to the Past - entrance: Cave Shop (Lake Hylia) exit: Cave 45 direction: entrance @@ -206,9 +207,9 @@ plando_connections: direction: both ``` -1. These connections are decoupled so going into the lake hylia cave shop will take you to the inside of cave 45 and - when you leave the interior you will exit to the cave 45 ledge. Going into the cave 45 entrance will then take you to - the lake hylia cave shop. Walking into the entrance for the old man cave and Agahnim's Tower entrance will both take - you to their locations as normal but leaving old man cave will exit at Agahnim's Tower. -2. This will force a nether fortress and a village to be the overworld structures for your game. Note that for the +1. These connections are decoupled, so going into the Lake Hylia Cave Shop will take you to the inside of Cave 45, and + when you leave the interior, you will exit to the Cave 45 ledge. Going into the Cave 45 entrance will then take you to + the Lake Hylia Cave Shop. Walking into the entrance for the Old Man Cave and Agahnim's Tower entrance will both take + you to their locations as normal, but leaving Old Man Cave will exit at Agahnim's Tower. +2. This will force a Nether fortress and a village to be the Overworld structures for your game. Note that for the Minecraft connection plando to work structure shuffle must be enabled. diff --git a/worlds/generic/docs/triggers_en.md b/worlds/generic/docs/triggers_en.md index 03ce9c65cb..a9ffebb466 100644 --- a/worlds/generic/docs/triggers_en.md +++ b/worlds/generic/docs/triggers_en.md @@ -8,36 +8,31 @@ about 5 minutes to read. Triggers allow you to customize your game settings by allowing you to define one or many options which only occur under specific conditions. These are essentially "if, then" statements for options in your game. A good example of what you -can do with triggers is the custom mercenary mode YAML that was created using entirely triggers and plando. +can do with triggers is the [custom mercenary mode YAML +](https://github.com/alwaysintreble/Archipelago-yaml-dump/blob/main/Snippets/Mercenary%20Mode%20Snippet.yaml) that was +created using entirely triggers and plando. -Mercenary mode -YAML: [Mercenary Mode YAML on GitHub](https://github.com/alwaysintreble/Archipelago-yaml-dump/blob/main/Snippets/Mercenary%20Mode%20Snippet.yaml) - -For more information on plando you can reference the general plando guide or the Link to the Past plando guide. - -General plando guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en) - -Link to the Past plando guide: [LttP Plando Guide](/tutorial/A%20Link%20to%20the%20Past/plando/en) +For more information on plando, you can reference the [general plando guide](/tutorial/Archipelago/plando/en) or the +[A Link to the Past plando guide](/tutorial/A%20Link%20to%20the%20Past/plando/en). ## Trigger use -Triggers may be defined in either the root or in the relevant game sections. Generally, The best place to do this is the -bottom of the yaml for clear organization. +Triggers may be defined in either the root or in the relevant game sections. Generally, the best place to do this is the +bottom of the YAML for clear organization. -- Triggers comprise the trigger section and then each trigger must have an `option_category`, `option_name`, and - `option_result` from which it will react to and then an `options` section for the definition of what will happen. -- `option_category` is the defining section from which the option is defined in. +Each trigger consists of four parts: +- `option_category` specifies the section which the triggering option is defined in. - Example: `A Link to the Past` - - This is the root category the option is located in. If the option you're triggering off of is in root then you + - This is the category the option is located in. If the option you're triggering off of is in root then you would use `null`, otherwise this is the game for which you want this option trigger to activate. -- `option_name` is the option setting from which the triggered choice is going to react to. +- `option_name` specifies the name of the triggering option. - Example: `shop_item_slots` - - This can be any option from any category defined in the yaml file in either root or a game section. -- `option_result` is the result of this option setting from which you would like to react. + - This can be any option from any category defined in the YAML file in either root or a game section. +- `option_result` specifies the value of the option that activates this trigger. - Example: `15` - Each trigger must be used for exactly one option result. If you would like the same thing to occur with multiple - results you would need multiple triggers for this. -- `options` is where you define what will happen when this is detected. This can be something as simple as ensuring + results, you would need multiple triggers for this. +- `options` is where you define what will happen when the trigger activates. This can be something as simple as ensuring another option also gets selected or placing an item in a certain location. It is possible to have multiple things happen in this section. - Example: @@ -47,10 +42,10 @@ bottom of the yaml for clear organization. Rupees (300): 2 ``` -This format must be: +The general format is: ```yaml - root option: + category: option to change: desired result ``` @@ -70,8 +65,8 @@ The above examples all together will end up looking like this: Rupees(300): 2 ``` -For this example if the generator happens to roll 15 shuffled in shop item slots for your game you'll be granted 600 -rupees at the beginning. These can also be used to change other options. +For this example, if the generator happens to roll 15 shuffled in shop item slots for your game, you'll be granted 600 +rupees at the beginning. Triggers can also be used to change other options. For example: @@ -85,9 +80,9 @@ For example: Inverted: true ``` -In this example if your world happens to roll SpecificKeycards then your game will also start in inverted. +In this example, if your world happens to roll SpecificKeycards, then your game will also start in inverted. -It is also possible to use imaginary names in options to trigger specific settings. You can use these made up names in +It is also possible to use imaginary values in options to trigger specific settings. You can use these made-up values in either your main options or to trigger from another trigger. Currently, this is the only way to trigger on "setting 1 AND setting 2". @@ -97,33 +92,33 @@ For example: triggers: - option_category: Secret of Evermore option_name: doggomizer - option_result: pupdunk - options: - Secret of Evermore: - difficulty: - normal: 50 - pupdunk_hard: 25 - pupdunk_mystery: 25 - exp_modifier: - 150: 50 - 200: 50 - - option_category: Secret of Evermore - option_name: difficulty - option_result: pupdunk_hard - options: - Secret of Evermore: - fix_wings_glitch: false - difficulty: hard - - option_category: Secret of Evermore - option_name: difficulty - option_result: pupdunk_mystery - options: - Secret of Evermore: - fix_wings_glitch: false - difficulty: mystery + option_result: pupdunk + options: + Secret of Evermore: + difficulty: + normal: 50 + pupdunk_hard: 25 + pupdunk_mystery: 25 + exp_modifier: + 150: 50 + 200: 50 + - option_category: Secret of Evermore + option_name: difficulty + option_result: pupdunk_hard + options: + Secret of Evermore: + fix_wings_glitch: false + difficulty: hard + - option_category: Secret of Evermore + option_name: difficulty + option_result: pupdunk_mystery + options: + Secret of Evermore: + fix_wings_glitch: false + difficulty: mystery ``` -In this example (thanks to @Black-Sliver) if the `pupdunk` option is rolled then the difficulty values will be rolled +In this example (thanks to @Black-Sliver), if the `pupdunk` option is rolled, then the difficulty values will be rolled again using the new options `normal`, `pupdunk_hard`, and `pupdunk_mystery`, and the exp modifier will be rerolled using new weights for 150 and 200. This allows for two more triggers that will only be used for the new `pupdunk_hard` and `pupdunk_mystery` options so that they will only be triggered on "pupdunk AND hard/mystery". \ No newline at end of file diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 1667ab81f7..9ed0c929bb 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -637,7 +637,7 @@ class HKItem(Item): 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"): + elif type in ("Grub", "DreamWarrior", "Root", "Egg", "Dreamer"): classification = ItemClassification.progression_skip_balancing elif type == "Charm" and name not in progression_charms: classification = ItemClassification.progression_skip_balancing diff --git a/worlds/hylics2/Exits.py b/worlds/hylics2/Exits.py new file mode 100644 index 0000000000..99ebeba277 --- /dev/null +++ b/worlds/hylics2/Exits.py @@ -0,0 +1,94 @@ +from typing import List, Dict + + +region_exit_table: Dict[int, List[str]] = { + 0: ["New Game"], + + 1: ["To Waynehouse", + "To New Muldul", + "To Viewax", + "To TV Island", + "To Shield Facility", + "To Worm Pod", + "To Foglast", + "To Sage Labyrinth", + "To Hylemxylem"], + + 2: ["To World", + "To Afterlife",], + + 3: ["To Airship", + "To Waynehouse", + "To New Muldul", + "To Drill Castle", + "To Viewax", + "To Arcade Island", + "To TV Island", + "To Juice Ranch", + "To Shield Facility", + "To Worm Pod", + "To Foglast", + "To Sage Airship", + "To Hylemxylem"], + + 4: ["To World", + "To Afterlife", + "To New Muldul Vault"], + + 5: ["To New Muldul"], + + 6: ["To World", + "To Afterlife"], + + 7: ["To World"], + + 8: ["To World"], + + 9: ["To World", + "To Afterlife"], + + 10: ["To World"], + + 11: ["To World", + "To Afterlife", + "To Worm Pod"], + + 12: ["To Shield Facility", + "To Afterlife"], + + 13: ["To World", + "To Afterlife"], + + 14: ["To World", + "To Sage Labyrinth"], + + 15: ["To Drill Castle", + "To Afterlife"], + + 16: ["To World"], + + 17: ["To World", + "To Afterlife"] +} + + +exit_lookup_table: Dict[str, int] = { + "New Game": 2, + "To Waynehouse": 2, + "To Afterlife": 1, + "To World": 3, + "To New Muldul": 4, + "To New Muldul Vault": 5, + "To Viewax": 6, + "To Airship": 7, + "To Arcade Island": 8, + "To TV Island": 9, + "To Juice Ranch": 10, + "To Shield Facility": 11, + "To Worm Pod": 12, + "To Foglast": 13, + "To Drill Castle": 14, + "To Sage Labyrinth": 15, + "To Sage Airship": 16, + "To Hylemxylem": 17 +} \ No newline at end of file diff --git a/worlds/hylics2/Items.py b/worlds/hylics2/Items.py new file mode 100644 index 0000000000..9e36b3c393 --- /dev/null +++ b/worlds/hylics2/Items.py @@ -0,0 +1,243 @@ +from BaseClasses import ItemClassification +from typing import TypedDict, Dict + + +class ItemDict(TypedDict): + classification: ItemClassification + count: int + name: str + + +item_table: Dict[int, ItemDict] = { + # Things + 200622: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'DUBIOUS BERRY'}, + 200623: {'classification': ItemClassification.filler, + 'count': 11, + 'name': 'BURRITO'}, + 200624: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'COFFEE'}, + 200625: {'classification': ItemClassification.filler, + 'count': 6, + 'name': 'SOUL SPONGE'}, + 200626: {'classification': ItemClassification.useful, + 'count': 6, + 'name': 'MUSCLE APPLIQUE'}, + 200627: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'POOLWINE'}, + 200628: {'classification': ItemClassification.filler, + 'count': 3, + 'name': 'CUPCAKE'}, + 200629: {'classification': ItemClassification.filler, + 'count': 3, + 'name': 'COOKIE'}, + 200630: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'HOUSE KEY'}, + 200631: {'classification': ItemClassification.filler, + 'count': 2, + 'name': 'MEAT'}, + 200632: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'PNEUMATOPHORE'}, + 200633: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'CAVE KEY'}, + 200634: {'classification': ItemClassification.filler, + 'count': 6, + 'name': 'JUICE'}, + 200635: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'DOCK KEY'}, + 200636: {'classification': ItemClassification.filler, + 'count': 14, + 'name': 'BANANA'}, + 200637: {'classification': ItemClassification.progression, + 'count': 3, + 'name': 'PAPER CUP'}, + 200638: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'JAIL KEY'}, + 200639: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'PADDLE'}, + 200640: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'WORM ROOM KEY'}, + 200641: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'BRIDGE KEY'}, + 200642: {'classification': ItemClassification.filler, + 'count': 2, + 'name': 'STEM CELL'}, + 200643: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'UPPER CHAMBER KEY'}, + 200644: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'VESSEL ROOM KEY'}, + 200645: {'classification': ItemClassification.filler, + 'count': 3, + 'name': 'CLOUD GERM'}, + 200646: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'SKULL BOMB'}, + 200647: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'TOWER KEY'}, + 200648: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'DEEP KEY'}, + 200649: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'MULTI-COFFEE'}, + 200650: {'classification': ItemClassification.filler, + 'count': 4, + 'name': 'MULTI-JUICE'}, + 200651: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'MULTI STEM CELL'}, + 200652: {'classification': ItemClassification.filler, + 'count': 6, + 'name': 'MULTI SOUL SPONGE'}, + #200653: {'classification': ItemClassification.filler, + # 'count': 1, + # 'name': 'ANTENNA'}, + 200654: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'UPPER HOUSE KEY'}, + 200655: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'BOTTOMLESS JUICE'}, + 200656: {'classification': ItemClassification.progression, + 'count': 3, + 'name': 'SAGE TOKEN'}, + 200657: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'CLICKER'}, + + # Garbs > Gloves + 200658: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'CURSED GLOVES'}, + 200659: {'classification': ItemClassification.useful, + 'count': 5, + 'name': 'LONG GLOVES'}, + 200660: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'BRAIN DIGITS'}, + 200661: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'MATERIEL MITTS'}, + 200662: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'PLEATHER GAGE'}, + 200663: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'PEPTIDE BODKINS'}, + 200664: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'TELESCOPIC SLEEVE'}, + 200665: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'TENDRIL HAND'}, + 200666: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'PSYCHIC KNUCKLE'}, + 200667: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'SINGLE GLOVE'}, + + # Garbs > Accessories + 200668: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'FADED PONCHO'}, + 200669: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'JUMPSUIT'}, + 200670: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'BOOTS'}, + 200671: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'CONVERTER WORM'}, + 200672: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'COFFEE CHIP'}, + 200673: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'RANCHER PONCHO'}, + 200674: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'ORGAN FORT'}, + 200675: {'classification': ItemClassification.useful, + 'count': 2, + 'name': 'LOOPED DOME'}, + 200676: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'DUCTILE HABIT'}, + 200677: {'classification': ItemClassification.useful, + 'count': 2, + 'name': 'TARP'}, + + # Bones + 200686: {'classification': ItemClassification.filler, + 'count': 1, + 'name': '100 Bones'}, + 200687: {'classification': ItemClassification.filler, + 'count': 1, + 'name': '50 Bones'} +} + + +gesture_item_table: Dict[int, ItemDict] = { + 200678: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'POROMER BLEB'}, + 200679: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'SOUL CRISPER'}, + 200680: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'TIME SIGIL'}, + 200681: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'CHARGE UP'}, + 200682: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'FATE SANDBOX'}, + 200683: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'TELEDENUDATE'}, + 200684: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'LINK MOLLUSC'}, + 200685: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'BOMBO - GENESIS'}, + 200688: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'NEMATODE INTERFACE'}, +} + + +party_item_table: Dict[int, ItemDict] = { + 200689: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Pongorma'}, + 200690: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Dedusmuln'}, + 200691: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Somsnosa'} +} + +medallion_item_table: Dict[int, ItemDict] = { + 200692: {'classification': ItemClassification.filler, + 'count': 30, + 'name': '10 Bones'} +} \ No newline at end of file diff --git a/worlds/hylics2/Locations.py b/worlds/hylics2/Locations.py new file mode 100644 index 0000000000..0f8bbb9b99 --- /dev/null +++ b/worlds/hylics2/Locations.py @@ -0,0 +1,383 @@ +from typing import Dict, TypedDict + + +class LocationDict(TypedDict, total=False): + name: str + region: int + + +location_table: Dict[int, LocationDict] = { + # Waynehouse + 200622: {'name': "Waynehouse: Toilet", + 'region': 2}, + 200623: {'name': "Waynehouse: Basement Pot 1", + 'region': 2}, + 200624: {'name': "Waynehouse: Basement Pot 2", + 'region': 2}, + 200625: {'name': "Waynehouse: Basement Pot 3", + 'region': 2}, + 200626: {'name': "Waynehouse: Sarcophagus", + 'region': 2}, + + # Afterlife + 200628: {'name': "Afterlife: Mangled Wayne", + 'region': 1}, + 200629: {'name': "Afterlife: Jar near Mangled Wayne", + 'region': 1}, + 200630: {'name': "Afterlife: Jar under Pool", + 'region': 1}, + + # New Muldul + 200632: {'name': "New Muldul: Shop Ceiling Pot 1", + 'region': 4}, + 200633: {'name': "New Muldul: Shop Ceiling Pot 2", + 'region': 4}, + 200634: {'name': "New Muldul: Flag Banana", + 'region': 4}, + 200635: {'name': "New Muldul: Pot near Vault", + 'region': 4}, + 200636: {'name': "New Muldul: Underground Pot", + 'region': 4}, + 200637: {'name': "New Muldul: Underground Chest", + 'region': 4}, + 200638: {'name': "New Muldul: Juice Trade", + 'region': 4}, + 200639: {'name': "New Muldul: Basement Suitcase", + 'region': 4}, + 200640: {'name': "New Muldul: Upper House Chest 1", + 'region': 4}, + 200641: {'name': "New Muldul: Upper House Chest 2", + 'region': 4}, + + # New Muldul Vault + 200643: {'name': "New Muldul: Talk to Pongorma", + 'region': 4}, + 200645: {'name': "New Muldul: Rescued Blerol 1", + 'region': 4}, + 200646: {'name': "New Muldul: Rescued Blerol 2", + 'region': 4}, + 200647: {'name': "New Muldul: Vault Left Chest", + 'region': 5}, + 200648: {'name': "New Muldul: Vault Right Chest", + 'region': 5}, + 200649: {'name': "New Muldul: Vault Bomb", + 'region': 5}, + + # Viewax's Edifice + 200650: {'name': "Viewax's Edifice: Fountain Banana", + 'region': 6}, + 200651: {'name': "Viewax's Edifice: Dedusmuln's Suitcase", + 'region': 6}, + 200652: {'name': "Viewax's Edifice: Dedusmuln's Campfire", + 'region': 6}, + 200653: {'name': "Viewax's Edifice: Talk to Dedusmuln", + 'region': 6}, + 200655: {'name': "Viewax's Edifice: Canopic Jar", + 'region': 6}, + 200656: {'name': "Viewax's Edifice: Cave Sarcophagus", + 'region': 6}, + 200657: {'name': "Viewax's Edifice: Shielded Key", + 'region': 6}, + 200658: {'name': "Viewax's Edifice: Tower Pot", + 'region': 6}, + 200659: {'name': "Viewax's Edifice: Tower Jar", + 'region': 6}, + 200660: {'name': "Viewax's Edifice: Tower Chest", + 'region': 6}, + 200661: {'name': "Viewax's Edifice: Sage Fridge", + 'region': 6}, + 200662: {'name': "Viewax's Edifice: Sage Item 1", + 'region': 6}, + 200663: {'name': "Viewax's Edifice: Sage Item 2", + 'region': 6}, + 200664: {'name': "Viewax's Edifice: Viewax Pot", + 'region': 6}, + 200665: {'name': "Viewax's Edifice: Defeat Viewax", + 'region': 6}, + + # Viewax Arcade Minigame + 200667: {'name': "Arcade 1: Key", + 'region': 6}, + 200668: {'name': "Arcade 1: Coin Dash", + 'region': 6}, + 200669: {'name': "Arcade 1: Burrito Alcove 1", + 'region': 6}, + 200670: {'name': "Arcade 1: Burrito Alcove 2", + 'region': 6}, + 200671: {'name': "Arcade 1: Behind Spikes Banana", + 'region': 6}, + 200672: {'name': "Arcade 1: Pyramid Banana", + 'region': 6}, + 200673: {'name': "Arcade 1: Moving Platforms Muscle Applique", + 'region': 6}, + 200674: {'name': "Arcade 1: Bed Banana", + 'region': 6}, + + # Airship + 200675: {'name': "Airship: Talk to Somsnosa", + 'region': 7}, + + # Arcade Island + 200676: {'name': "Arcade Island: Shielded Key", + 'region': 8}, + 200677: {'name': "Arcade 2: Flying Machine Banana", + 'region': 8}, + 200678: {'name': "Arcade 2: Paper Cup Detour", + 'region': 8}, + 200679: {'name': "Arcade 2: Peak Muscle Applique", + 'region': 8}, + 200680: {'name': "Arcade 2: Double Banana 1", + 'region': 8}, + 200681: {'name': "Arcade 2: Double Banana 2", + 'region': 8}, + 200682: {'name': "Arcade 2: Cave Burrito", + 'region': 8}, + + # Juice Ranch + 200684: {'name': "Juice Ranch: Juice 1", + 'region': 10}, + 200685: {'name': "Juice Ranch: Juice 2", + 'region': 10}, + 200686: {'name': "Juice Ranch: Juice 3", + 'region': 10}, + 200687: {'name': "Juice Ranch: Ledge Rancher", + 'region': 10}, + 200688: {'name': "Juice Ranch: Battle with Somsnosa", + 'region': 10}, + 200690: {'name': "Juice Ranch: Fridge", + 'region': 10}, + + # Worm Pod + 200692: {'name': "Worm Pod: Key", + 'region': 12}, + + # Foglast + 200693: {'name': "Foglast: West Sarcophagus", + 'region': 13}, + 200694: {'name': "Foglast: Underground Sarcophagus", + 'region': 13}, + 200695: {'name': "Foglast: Shielded Key", + 'region': 13}, + 200696: {'name': "Foglast: Buy Clicker", + 'region': 13}, + 200698: {'name': "Foglast: Shielded Chest", + 'region': 13}, + 200699: {'name': "Foglast: Cave Fridge", + 'region': 13}, + 200700: {'name': "Foglast: Roof Sarcophagus", + 'region': 13}, + 200701: {'name': "Foglast: Under Lair Sarcophagus 1", + 'region': 13}, + 200702: {'name': "Foglast: Under Lair Sarcophagus 2", + 'region': 13}, + 200703: {'name': "Foglast: Under Lair Sarcophagus 3", + 'region': 13}, + 200704: {'name': "Foglast: Sage Sarcophagus", + 'region': 13}, + 200705: {'name': "Foglast: Sage Item 1", + 'region': 13}, + 200706: {'name': "Foglast: Sage Item 2", + 'region': 13}, + + # Drill Castle + 200707: {'name': "Drill Castle: Ledge Banana", + 'region': 14}, + 200708: {'name': "Drill Castle: Island Banana", + 'region': 14}, + 200709: {'name': "Drill Castle: Island Pot", + 'region': 14}, + 200710: {'name': "Drill Castle: Cave Sarcophagus", + 'region': 14}, + 200711: {'name': "Drill Castle: Roof Banana", + 'region': 14}, + + # Sage Labyrinth + 200713: {'name': "Sage Labyrinth: 1F Chest Near Fountain", + 'region': 15}, + 200714: {'name': "Sage Labyrinth: 1F Hidden Sarcophagus", + 'region': 15}, + 200715: {'name': "Sage Labyrinth: 1F Four Statues Chest 1", + 'region': 15}, + 200716: {'name': "Sage Labyrinth: 1F Four Statues Chest 2", + 'region': 15}, + 200717: {'name': "Sage Labyrinth: B1 Double Chest 1", + 'region': 15}, + 200718: {'name': "Sage Labyrinth: B1 Double Chest 2", + 'region': 15}, + 200719: {'name': "Sage Labyrinth: B1 Single Chest", + 'region': 15}, + 200720: {'name': "Sage Labyrinth: B1 Enemy Chest", + 'region': 15}, + 200721: {'name': "Sage Labyrinth: B1 Hidden Sarcophagus", + 'region': 15}, + 200722: {'name': "Sage Labyrinth: B1 Hole Chest", + 'region': 15}, + 200723: {'name': "Sage Labyrinth: B2 Hidden Sarcophagus 1", + 'region': 15}, + 200724: {'name': "Sage Labyrinth: B2 Hidden Sarcophagus 2", + 'region': 15}, + 200754: {'name': "Sage Labyrinth: 2F Sarcophagus", + 'region': 15}, + 200725: {'name': "Sage Labyrinth: Motor Hunter Sarcophagus", + 'region': 15}, + 200726: {'name': "Sage Labyrinth: Sage Item 1", + 'region': 15}, + 200727: {'name': "Sage Labyrinth: Sage Item 2", + 'region': 15}, + 200728: {'name': "Sage Labyrinth: Sage Left Arm", + 'region': 15}, + 200729: {'name': "Sage Labyrinth: Sage Right Arm", + 'region': 15}, + 200730: {'name': "Sage Labyrinth: Sage Left Leg", + 'region': 15}, + 200731: {'name': "Sage Labyrinth: Sage Right Leg", + 'region': 15}, + + # Sage Airship + 200732: {'name': "Sage Airship: Bottom Level Pot", + 'region': 16}, + 200733: {'name': "Sage Airship: Flesh Pot", + 'region': 16}, + 200734: {'name': "Sage Airship: Top Jar", + 'region': 16}, + + # Hylemxylem + 200736: {'name': "Hylemxylem: Jar", + 'region': 17}, + 200737: {'name': "Hylemxylem: Lower Reservoir Key", + 'region': 17}, + 200738: {'name': "Hylemxylem: Fountain Banana", + 'region': 17}, + 200739: {'name': "Hylemxylem: East Island Banana", + 'region': 17}, + 200740: {'name': "Hylemxylem: East Island Chest", + 'region': 17}, + 200741: {'name': "Hylemxylem: Upper Chamber Banana", + 'region': 17}, + 200742: {'name': "Hylemxylem: Across Upper Reservoir Chest", + 'region': 17}, + 200743: {'name': "Hylemxylem: Drained Lower Reservoir Chest", + 'region': 17}, + 200744: {'name': "Hylemxylem: Drained Lower Reservoir Burrito 1", + 'region': 17}, + 200745: {'name': "Hylemxylem: Drained Lower Reservoir Burrito 2", + 'region': 17}, + 200746: {'name': "Hylemxylem: Lower Reservoir Hole Pot 1", + 'region': 17}, + 200747: {'name': "Hylemxylem: Lower Reservoir Hole Pot 2", + 'region': 17}, + 200748: {'name': "Hylemxylem: Lower Reservoir Hole Pot 3", + 'region': 17}, + 200749: {'name': "Hylemxylem: Lower Reservoir Hole Sarcophagus", + 'region': 17}, + 200750: {'name': "Hylemxylem: Drained Upper Reservoir Burrito 1", + 'region': 17}, + 200751: {'name': "Hylemxylem: Drained Upper Reservoir Burrito 2", + 'region': 17}, + 200752: {'name': "Hylemxylem: Drained Upper Reservoir Burrito 3", + 'region': 17}, + 200753: {'name': "Hylemxylem: Upper Reservoir Hole Key", + 'region': 17} +} + + +tv_location_table: Dict[int, LocationDict] = { + 200627: {'name': "Waynehouse: TV", + 'region': 2}, + 200631: {'name': "Afterlife: TV", + 'region': 1}, + 200642: {'name': "New Muldul: TV", + 'region': 4}, + 200666: {'name': "Viewax's Edifice: TV", + 'region': 6}, + 200683: {'name': "TV Island: TV", + 'region': 9}, + 200691: {'name': "Juice Ranch: TV", + 'region': 10}, + 200697: {'name': "Foglast: TV", + 'region': 13}, + 200712: {'name': "Drill Castle: TV", + 'region': 14}, + 200735: {'name': "Sage Airship: TV", + 'region': 16} +} + + +party_location_table: Dict[int, LocationDict] = { + 200644: {'name': "New Muldul: Pongorma Joins", + 'region': 4}, + 200654: {'name': "Viewax's Edifice: Dedusmuln Joins", + 'region': 6}, + 200689: {'name': "Juice Ranch: Somsnosa Joins", + 'region': 10} +} + + +medallion_location_table: Dict[int, LocationDict] = { + 200755: {'name': "New Muldul: Upper House Medallion", + 'region': 4}, + + 200756: {'name': "New Muldul: Vault Rear Left Medallion", + 'region': 5}, + 200757: {'name': "New Muldul: Vault Rear Right Medallion", + 'region': 5}, + 200758: {'name': "New Muldul: Vault Center Medallion", + 'region': 5}, + 200759: {'name': "New Muldul: Vault Front Left Medallion", + 'region': 5}, + 200760: {'name': "New Muldul: Vault Front Right Medallion", + 'region': 5}, + + 200761: {'name': "Viewax's Edifice: Fort Wall Medallion", + 'region': 6}, + 200762: {'name': "Viewax's Edifice: Jar Medallion", + 'region': 6}, + 200763: {'name': "Viewax's Edifice: Sage Chair Medallion", + 'region': 6}, + 200764: {'name': "Arcade 1: Lonely Medallion", + 'region': 6}, + 200765: {'name': "Arcade 1: Alcove Medallion", + 'region': 6}, + 200766: {'name': "Arcade 1: Lava Medallion", + 'region': 6}, + + 200767: {'name': "Arcade 2: Flying Machine Medallion", + 'region': 8}, + 200768: {'name': "Arcade 2: Guarded Medallion", + 'region': 8}, + 200769: {'name': "Arcade 2: Spinning Medallion", + 'region': 8}, + 200770: {'name': "Arcade 2: Hook Medallion", + 'region': 8}, + 200771: {'name': "Arcade 2: Flag Medallion", + 'region': 8}, + + 200772: {'name': "Foglast: Under Lair Medallion", + 'region': 13}, + 200773: {'name': "Foglast: Mid-Air Medallion", + 'region': 13}, + 200774: {'name': "Foglast: Top of Tower Medallion", + 'region': 13}, + + 200775: {'name': "Sage Airship: Walkway Medallion", + 'region': 16}, + 200776: {'name': "Sage Airship: Flesh Medallion", + 'region': 16}, + 200777: {'name': "Sage Airship: Top of Ship Medallion", + 'region': 16}, + 200778: {'name': "Sage Airship: Hidden Medallion 1", + 'region': 16}, + 200779: {'name': "Sage Airship: Hidden Medallion 2", + 'region': 16}, + 200780: {'name': "Sage Airship: Hidden Medallion 3", + 'region': 16}, + + 200781: {'name': "Hylemxylem: Lower Reservoir Medallion", + 'region': 17}, + 200782: {'name': "Hylemxylem: Lower Reservoir Hole Medallion", + 'region': 17}, + 200783: {'name': "Hylemxylem: Drain Switch Medallion", + 'region': 17}, + 200784: {'name': "Hylemxylem: Warpo Medallion", + 'region': 17} +} \ No newline at end of file diff --git a/worlds/hylics2/Options.py b/worlds/hylics2/Options.py new file mode 100644 index 0000000000..ac57e666a1 --- /dev/null +++ b/worlds/hylics2/Options.py @@ -0,0 +1,41 @@ +from Options import Choice, Toggle, DefaultOnToggle, DeathLink + +class PartyShuffle(Toggle): + """Shuffles party members into the pool. + Note that enabling this can potentially increase both the difficulty and length of a run.""" + display_name = "Shuffle Party Members" + +class GestureShuffle(Choice): + """Choose where gestures will appear in the item pool.""" + display_name = "Shuffle Gestures" + option_anywhere = 0 + option_tvs_only = 1 + option_default_locations = 2 + default = 0 + +class MedallionShuffle(Toggle): + """Shuffles red medallions into the pool.""" + display_name = "Shuffle Red Medallions" + +class RandomStart(Toggle): + """Start the randomizer in 1 of 4 positions. + (Waynehouse, Viewax's Edifice, TV Island, Shield Facility)""" + display_name = "Randomize Start Location" + +class ExtraLogic(DefaultOnToggle): + """Include some extra items in logic (CHARGE UP, 1x PAPER CUP) to prevent the game from becoming too difficult.""" + display_name = "Extra Items in Logic" + +class Hylics2DeathLink(DeathLink): + """When you die, everyone dies. The reverse is also true. + Note that this also includes death by using the PERISH gesture. + Can be toggled via in-game console command "/deathlink".""" + +hylics2_options = { + "party_shuffle": PartyShuffle, + "gesture_shuffle" : GestureShuffle, + "medallion_shuffle" : MedallionShuffle, + "random_start" : RandomStart, + "extra_items_in_logic": ExtraLogic, + "death_link": Hylics2DeathLink +} \ No newline at end of file diff --git a/worlds/hylics2/Rules.py b/worlds/hylics2/Rules.py new file mode 100644 index 0000000000..be38e102ea --- /dev/null +++ b/worlds/hylics2/Rules.py @@ -0,0 +1,433 @@ +from worlds.generic.Rules import add_rule +from ..AutoWorld import LogicMixin + + +class Hylics2Logic(LogicMixin): + + def _hylics2_can_air_dash(self, player): + return self.has("PNEUMATOPHORE", player) + + def _hylics2_has_airship(self, player): + return self.has("DOCK KEY", player) + + def _hylics2_has_jail_key(self, player): + return self.has("JAIL KEY", player) + + def _hylics2_has_paddle(self, player): + return self.has("PADDLE", player) + + def _hylics2_has_worm_room_key(self, player): + return self.has("WORM ROOM KEY", player) + + def _hylics2_has_bridge_key(self, player): + return self.has("BRIDGE KEY", player) + + def _hylics2_has_upper_chamber_key(self, player): + return self.has("UPPER CHAMBER KEY", player) + + def _hylics2_has_vessel_room_key(self, player): + return self.has("VESSEL ROOM KEY", player) + + def _hylics2_has_house_key(self, player): + return self.has("HOUSE KEY", player) + + def _hylics2_has_cave_key(self, player): + return self.has("CAVE KEY", player) + + def _hylics2_has_skull_bomb(self, player): + return self.has("SKULL BOMB", player) + + def _hylics2_has_tower_key(self, player): + return self.has("TOWER KEY", player) + + def _hylics2_has_deep_key(self, player): + return self.has("DEEP KEY", player) + + def _hylics2_has_upper_house_key(self, player): + return self.has("UPPER HOUSE KEY", player) + + def _hylics2_has_clicker(self, player): + return self.has("CLICKER", player) + + def _hylics2_has_tokens(self, player): + return self.has("SAGE TOKEN", player, 3) + + def _hylics2_has_charge_up(self, player): + return self.has("CHARGE UP", player) + + def _hylics2_has_cup(self, player): + return self.has("PAPER CUP", player, 1) + + def _hylics2_has_1_member(self, player): + return self.has("Pongorma", player) or self.has("Dedusmuln", player) or self.has("Somsnosa", player) + + def _hylics2_has_2_members(self, player): + return (self.has("Pongorma", player) and self.has("Dedusmuln", player)) or\ + (self.has("Pongorma", player) and self.has("Somsnosa", player)) or\ + (self.has("Dedusmuln", player) and self.has("Somsnosa", player)) + + def _hylics2_has_3_members(self, player): + return self.has("Pongorma", player) and self.has("Dedusmuln", player) and self.has("Somsnosa", player) + + def _hylics2_enter_arcade2(self, player): + return self._hylics2_can_air_dash(player) and self._hylics2_has_airship(player) + + def _hylics2_enter_wormpod(self, player): + return self._hylics2_has_airship(player) and self._hylics2_has_worm_room_key(player) and\ + self._hylics2_has_paddle(player) + + def _hylics2_enter_sageship(self, player): + return self._hylics2_has_skull_bomb(player) and self._hylics2_has_airship(player) and\ + self._hylics2_has_paddle(player) + + def _hylics2_enter_foglast(self, player): + return self._hylics2_enter_wormpod(player) + + def _hylics2_enter_hylemxylem(self, player): + return self._hylics2_can_air_dash(player) and self._hylics2_enter_wormpod(player) and\ + self._hylics2_has_bridge_key(player) + + +def set_rules(hylics2world): + world = hylics2world.world + player = hylics2world.player + + # Afterlife + add_rule(world.get_location("Afterlife: TV", player), + lambda state: state._hylics2_has_cave_key(player)) + + # New Muldul + add_rule(world.get_location("New Muldul: Underground Chest", player), + lambda state: state._hylics2_can_air_dash(player)) + add_rule(world.get_location("New Muldul: TV", player), + lambda state: state._hylics2_has_house_key(player)) + add_rule(world.get_location("New Muldul: Upper House Chest 1", player), + lambda state: state._hylics2_has_upper_house_key(player)) + add_rule(world.get_location("New Muldul: Upper House Chest 2", player), + lambda state: state._hylics2_has_upper_house_key(player)) + + # New Muldul Vault + add_rule(world.get_location("New Muldul: Rescued Blerol 1", player), + lambda state: (state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player)) and\ + ((state._hylics2_has_jail_key(player) and state._hylics2_has_paddle(player)) or\ + (state._hylics2_has_bridge_key(player) and state._hylics2_has_worm_room_key(player)))) + add_rule(world.get_location("New Muldul: Rescued Blerol 2", player), + lambda state: (state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player)) and\ + ((state._hylics2_has_jail_key(player) and state._hylics2_has_paddle(player)) or\ + (state._hylics2_has_bridge_key(player) and state._hylics2_has_worm_room_key(player)))) + add_rule(world.get_location("New Muldul: Vault Left Chest", player), + lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("New Muldul: Vault Right Chest", player), + lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("New Muldul: Vault Bomb", player), + lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player)) + + # Viewax's Edifice + add_rule(world.get_location("Viewax's Edifice: Canopic Jar", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Viewax's Edifice: Cave Sarcophagus", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Viewax's Edifice: Shielded Key", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Viewax's Edifice: Shielded Key", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Viewax's Edifice: Tower Pot", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Viewax's Edifice: Tower Jar", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Viewax's Edifice: Tower Chest", player), + lambda state: state._hylics2_has_paddle(player) and state._hylics2_has_tower_key(player)) + add_rule(world.get_location("Viewax's Edifice: Viewax Pot", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Viewax's Edifice: Defeat Viewax", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Viewax's Edifice: TV", player), + lambda state: state._hylics2_has_paddle(player) and state._hylics2_has_jail_key(player)) + add_rule(world.get_location("Viewax's Edifice: Sage Fridge", player), + lambda state: state._hylics2_can_air_dash(player)) + add_rule(world.get_location("Viewax's Edifice: Sage Item 1", player), + lambda state: state._hylics2_can_air_dash(player)) + add_rule(world.get_location("Viewax's Edifice: Sage Item 2", player), + lambda state: state._hylics2_can_air_dash(player)) + + # Arcade 1 + add_rule(world.get_location("Arcade 1: Key", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Arcade 1: Coin Dash", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Arcade 1: Burrito Alcove 1", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Arcade 1: Burrito Alcove 2", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Arcade 1: Behind Spikes Banana", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Arcade 1: Pyramid Banana", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Arcade 1: Moving Platforms Muscle Applique", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Arcade 1: Bed Banana", player), + lambda state: state._hylics2_has_paddle(player)) + + # Airship + add_rule(world.get_location("Airship: Talk to Somsnosa", player), + lambda state: state._hylics2_has_worm_room_key(player)) + + # Foglast + add_rule(world.get_location("Foglast: Underground Sarcophagus", player), + lambda state: state._hylics2_can_air_dash(player)) + add_rule(world.get_location("Foglast: Shielded Key", player), + lambda state: state._hylics2_can_air_dash(player)) + add_rule(world.get_location("Foglast: TV", player), + lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_clicker(player)) + add_rule(world.get_location("Foglast: Buy Clicker", player), + lambda state: state._hylics2_can_air_dash(player)) + add_rule(world.get_location("Foglast: Shielded Chest", player), + lambda state: state._hylics2_can_air_dash(player)) + add_rule(world.get_location("Foglast: Cave Fridge", player), + lambda state: state._hylics2_can_air_dash(player)) + add_rule(world.get_location("Foglast: Roof Sarcophagus", player), + lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("Foglast: Under Lair Sarcophagus 1", player), + lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("Foglast: Under Lair Sarcophagus 2", player), + lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("Foglast: Under Lair Sarcophagus 3", player), + lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("Foglast: Sage Sarcophagus", player), + lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("Foglast: Sage Item 1", player), + lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("Foglast: Sage Item 2", player), + lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player)) + + # Drill Castle + add_rule(world.get_location("Drill Castle: Island Banana", player), + lambda state: state._hylics2_can_air_dash(player)) + add_rule(world.get_location("Drill Castle: Island Pot", player), + lambda state: state._hylics2_can_air_dash(player)) + add_rule(world.get_location("Drill Castle: Cave Sarcophagus", player), + lambda state: state._hylics2_can_air_dash(player)) + add_rule(world.get_location("Drill Castle: TV", player), + lambda state: state._hylics2_can_air_dash(player)) + + # Sage Labyrinth + add_rule(world.get_location("Sage Labyrinth: Sage Item 1", player), + lambda state: state._hylics2_has_deep_key(player)) + add_rule(world.get_location("Sage Labyrinth: Sage Item 2", player), + lambda state: state._hylics2_has_deep_key(player)) + add_rule(world.get_location("Sage Labyrinth: Sage Left Arm", player), + lambda state: state._hylics2_has_deep_key(player)) + add_rule(world.get_location("Sage Labyrinth: Sage Right Arm", player), + lambda state: state._hylics2_has_deep_key(player)) + add_rule(world.get_location("Sage Labyrinth: Sage Left Leg", player), + lambda state: state._hylics2_has_deep_key(player)) + add_rule(world.get_location("Sage Labyrinth: Sage Right Leg", player), + lambda state: state._hylics2_has_deep_key(player)) + + # Sage Airship + add_rule(world.get_location("Sage Airship: TV", player), + lambda state: state._hylics2_has_tokens(player)) + + # Hylemxylem + add_rule(world.get_location("Hylemxylem: Upper Chamber Banana", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + add_rule(world.get_location("Hylemxylem: Across Upper Reservoir Chest", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + add_rule(world.get_location("Hylemxylem: Drained Lower Reservoir Chest", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + add_rule(world.get_location("Hylemxylem: Drained Lower Reservoir Burrito 1", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + add_rule(world.get_location("Hylemxylem: Drained Lower Reservoir Burrito 2", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Pot 1", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Pot 2", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Pot 3", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Sarcophagus", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + add_rule(world.get_location("Hylemxylem: Drained Upper Reservoir Burrito 1", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + add_rule(world.get_location("Hylemxylem: Drained Upper Reservoir Burrito 2", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + add_rule(world.get_location("Hylemxylem: Drained Upper Reservoir Burrito 3", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + add_rule(world.get_location("Hylemxylem: Upper Reservoir Hole Key", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + + # extra rules if Extra Items in Logic is enabled + if world.extra_items_in_logic[player]: + for i in world.get_region("Foglast", player).entrances: + add_rule(i, lambda state: state._hylics2_has_charge_up(player)) + for i in world.get_region("Sage Airship", player).entrances: + add_rule(i, lambda state: state._hylics2_has_charge_up(player) and state._hylics2_has_cup(player) and\ + state._hylics2_has_worm_room_key(player)) + for i in world.get_region("Hylemxylem", player).entrances: + add_rule(i, lambda state: state._hylics2_has_charge_up(player) and state._hylics2_has_cup(player)) + + add_rule(world.get_location("Sage Labyrinth: Motor Hunter Sarcophagus", player), + lambda state: state._hylics2_has_charge_up(player) and state._hylics2_has_cup(player)) + + # extra rules if Shuffle Party Members is enabled + if world.party_shuffle[player]: + for i in world.get_region("Arcade Island", player).entrances: + add_rule(i, lambda state: state._hylics2_has_3_members(player)) + for i in world.get_region("Foglast", player).entrances: + add_rule(i, lambda state: state._hylics2_has_3_members(player) or\ + (state._hylics2_has_2_members(player) and state._hylics2_has_jail_key(player))) + for i in world.get_region("Sage Airship", player).entrances: + add_rule(i, lambda state: state._hylics2_has_3_members(player)) + for i in world.get_region("Hylemxylem", player).entrances: + add_rule(i, lambda state: state._hylics2_has_3_members(player)) + + add_rule(world.get_location("Viewax's Edifice: Defeat Viewax", player), + lambda state: state._hylics2_has_2_members(player)) + add_rule(world.get_location("New Muldul: Rescued Blerol 1", player), + lambda state: state._hylics2_has_2_members(player)) + add_rule(world.get_location("New Muldul: Rescued Blerol 2", player), + lambda state: state._hylics2_has_2_members(player)) + add_rule(world.get_location("New Muldul: Vault Left Chest", player), + lambda state: state._hylics2_has_3_members(player)) + add_rule(world.get_location("New Muldul: Vault Right Chest", player), + lambda state: state._hylics2_has_3_members(player)) + add_rule(world.get_location("New Muldul: Vault Bomb", player), + lambda state: state._hylics2_has_2_members(player)) + add_rule(world.get_location("Juice Ranch: Battle with Somsnosa", player), + lambda state: state._hylics2_has_2_members(player)) + add_rule(world.get_location("Juice Ranch: Somsnosa Joins", player), + lambda state: state._hylics2_has_2_members(player)) + add_rule(world.get_location("Airship: Talk to Somsnosa", player), + lambda state: state._hylics2_has_3_members(player)) + add_rule(world.get_location("Sage Labyrinth: Motor Hunter Sarcophagus", player), + lambda state: state._hylics2_has_3_members(player)) + + # extra rules if Shuffle Red Medallions is enabled + if world.medallion_shuffle[player]: + add_rule(world.get_location("New Muldul: Upper House Medallion", player), + lambda state: state._hylics2_has_upper_house_key(player)) + add_rule(world.get_location("New Muldul: Vault Rear Left Medallion", player), + lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("New Muldul: Vault Rear Right Medallion", player), + lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("New Muldul: Vault Center Medallion", player), + lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("New Muldul: Vault Front Left Medallion", player), + lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("New Muldul: Vault Front Right Medallion", player), + lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("Viewax's Edifice: Fort Wall Medallion", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Viewax's Edifice: Jar Medallion", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Viewax's Edifice: Sage Chair Medallion", player), + lambda state: state._hylics2_can_air_dash(player)) + add_rule(world.get_location("Arcade 1: Lonely Medallion", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Arcade 1: Alcove Medallion", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Foglast: Under Lair Medallion", player), + lambda state: state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("Foglast: Mid-Air Medallion", player), + lambda state: state._hylics2_can_air_dash(player)) + add_rule(world.get_location("Foglast: Top of Tower Medallion", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Medallion", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + + # extra rules is Shuffle Red Medallions and Party Shuffle are enabled + if world.party_shuffle[player] and world.medallion_shuffle[player]: + add_rule(world.get_location("New Muldul: Vault Rear Left Medallion", player), + lambda state: state._hylics2_has_jail_key(player)) + add_rule(world.get_location("New Muldul: Vault Rear Right Medallion", player), + lambda state: state._hylics2_has_jail_key(player)) + add_rule(world.get_location("New Muldul: Vault Center Medallion", player), + lambda state: state._hylics2_has_jail_key(player)) + add_rule(world.get_location("New Muldul: Vault Front Left Medallion", player), + lambda state: state._hylics2_has_jail_key(player)) + add_rule(world.get_location("New Muldul: Vault Front Right Medallion", player), + lambda state: state._hylics2_has_jail_key(player)) + + # entrances + for i in world.get_region("Airship", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Arcade Island", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player) and state._hylics2_can_air_dash(player)) + for i in world.get_region("Worm Pod", player).entrances: + add_rule(i, lambda state: state._hylics2_enter_wormpod(player)) + for i in world.get_region("Foglast", player).entrances: + add_rule(i, lambda state: state._hylics2_enter_foglast(player)) + for i in world.get_region("Sage Labyrinth", player).entrances: + add_rule(i, lambda state: state._hylics2_has_skull_bomb(player)) + for i in world.get_region("Sage Airship", player).entrances: + add_rule(i, lambda state: state._hylics2_enter_sageship(player)) + for i in world.get_region("Hylemxylem", player).entrances: + add_rule(i, lambda state: state._hylics2_enter_hylemxylem(player)) + + # random start logic (default) + if ((not world.random_start[player]) or \ + (world.random_start[player] and hylics2world.start_location == "Waynehouse")): + # entrances + for i in world.get_region("Viewax", player).entrances: + add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player)) + for i in world.get_region("TV Island", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Shield Facility", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Juice Ranch", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + + # random start logic (Viewax's Edifice) + elif (world.random_start[player] and hylics2world.start_location == "Viewax's Edifice"): + for i in world.get_region("Waynehouse", player).entrances: + add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player)) + for i in world.get_region("New Muldul", player).entrances: + add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player)) + for i in world.get_region("New Muldul Vault", player).entrances: + add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player)) + for i in world.get_region("Drill Castle", player).entrances: + add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player)) + for i in world.get_region("TV Island", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Shield Facility", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Juice Ranch", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Sage Labyrinth", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + + # random start logic (TV Island) + elif (world.random_start[player] and hylics2world.start_location == "TV Island"): + for i in world.get_region("Waynehouse", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("New Muldul", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("New Muldul Vault", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Drill Castle", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Viewax", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Shield Facility", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Juice Ranch", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Sage Labyrinth", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + + # random start logic (Shield Facility) + elif (world.random_start[player] and hylics2world.start_location == "Shield Facility"): + for i in world.get_region("Waynehouse", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("New Muldul", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("New Muldul Vault", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Drill Castle", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Viewax", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("TV Island", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Sage Labyrinth", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) \ No newline at end of file diff --git a/worlds/hylics2/__init__.py b/worlds/hylics2/__init__.py new file mode 100644 index 0000000000..b429eb6a44 --- /dev/null +++ b/worlds/hylics2/__init__.py @@ -0,0 +1,246 @@ +import random +from typing import Dict, Any +from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, RegionType +from worlds.generic.Rules import set_rule +from ..AutoWorld import World, WebWorld +from . import Items, Locations, Options, Rules, Exits + + +class Hylics2Web(WebWorld): + theme = "ocean" + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to settings up the Hylics 2 randomizer connected to an Archipelago Multiworld", + "English", + "setup_en.md", + "setup/en", + ["TRPG"] + )] + + +class Hylics2World(World): + """ + Hylics 2 is a surreal and unusual RPG, with a bizarre yet unique visual style. Play as Wayne, + travel the world, and gather your allies to defeat the nefarious Gibby in his Hylemxylem! + """ + game: str = "Hylics 2" + web = Hylics2Web() + + all_items = {**Items.item_table, **Items.gesture_item_table, **Items.party_item_table, + **Items.medallion_item_table} + all_locations = {**Locations.location_table, **Locations.tv_location_table, **Locations.party_location_table, + **Locations.medallion_location_table} + + item_name_to_id = {data["name"]: item_id for item_id, data in all_items.items()} + location_name_to_id = {data["name"]: loc_id for loc_id, data in all_locations.items()} + option_definitions = Options.hylics2_options + + topology_present: bool = True + remote_items: bool = True + remote_start_inventory: bool = True + + data_version: 1 + + start_location = "Waynehouse" + + + def set_rules(self): + Rules.set_rules(self) + + + def create_item(self, name: str) -> "Hylics2Item": + item_id: int = self.item_name_to_id[name] + + return Hylics2Item(name, self.all_items[item_id]["classification"], item_id, player=self.player) + + + def add_item(self, name: str, classification: ItemClassification, code: int) -> "Item": + return Hylics2Item(name, classification, code, self.player) + + + def create_event(self, event: str): + return Hylics2Item(event, ItemClassification.progression_skip_balancing, None, self.player) + + + # set random starting location if option is enabled + def generate_early(self): + if self.world.random_start[self.player]: + i = self.world.random.randint(0, 3) + if i == 0: + self.start_location = "Waynehouse" + elif i == 1: + self.start_location = "Viewax's Edifice" + elif i == 2: + self.start_location = "TV Island" + elif i == 3: + self.start_location = "Shield Facility" + + def generate_basic(self): + # create location for beating the game and place Victory event there + loc = Location(self.player, "Defeat Gibby", None, self.world.get_region("Hylemxylem", self.player)) + loc.place_locked_item(self.create_event("Victory")) + set_rule(loc, lambda state: state._hylics2_has_upper_chamber_key(self.player) + and state._hylics2_has_vessel_room_key(self.player)) + self.world.get_region("Hylemxylem", self.player).locations.append(loc) + self.world.completion_condition[self.player] = lambda state: state.has("Victory", self.player) + + # create item pool + pool = [] + + # add regular items + for i, data in Items.item_table.items(): + if data["count"] > 0: + for j in range(data["count"]): + pool.append(self.add_item(data["name"], data["classification"], i)) + + # add party members if option is enabled + if self.world.party_shuffle[self.player]: + for i, data in Items.party_item_table.items(): + pool.append(self.add_item(data["name"], data["classification"], i)) + + # handle gesture shuffle options + if self.world.gesture_shuffle[self.player] == 2: # vanilla locations + gestures = Items.gesture_item_table + self.world.get_location("Waynehouse: TV", self.player)\ + .place_locked_item(self.add_item(gestures[200678]["name"], gestures[200678]["classification"], 200678)) + self.world.get_location("Afterlife: TV", self.player)\ + .place_locked_item(self.add_item(gestures[200683]["name"], gestures[200683]["classification"], 200683)) + self.world.get_location("New Muldul: TV", self.player)\ + .place_locked_item(self.add_item(gestures[200679]["name"], gestures[200679]["classification"], 200679)) + self.world.get_location("Viewax's Edifice: TV", self.player)\ + .place_locked_item(self.add_item(gestures[200680]["name"], gestures[200680]["classification"], 200680)) + self.world.get_location("TV Island: TV", self.player)\ + .place_locked_item(self.add_item(gestures[200681]["name"], gestures[200681]["classification"], 200681)) + self.world.get_location("Juice Ranch: TV", self.player)\ + .place_locked_item(self.add_item(gestures[200682]["name"], gestures[200682]["classification"], 200682)) + self.world.get_location("Foglast: TV", self.player)\ + .place_locked_item(self.add_item(gestures[200684]["name"], gestures[200684]["classification"], 200684)) + self.world.get_location("Drill Castle: TV", self.player)\ + .place_locked_item(self.add_item(gestures[200688]["name"], gestures[200688]["classification"], 200688)) + self.world.get_location("Sage Airship: TV", self.player)\ + .place_locked_item(self.add_item(gestures[200685]["name"], gestures[200685]["classification"], 200685)) + + elif self.world.gesture_shuffle[self.player] == 1: # TVs only + gestures = list(Items.gesture_item_table.items()) + tvs = list(Locations.tv_location_table.items()) + + # if Extra Items in Logic is enabled place CHARGE UP first and make sure it doesn't get + # placed at Sage Airship: TV + if self.world.extra_items_in_logic[self.player]: + tv = self.world.random.choice(tvs) + gest = gestures.index((200681, Items.gesture_item_table[200681])) + while tv[1]["name"] == "Sage Airship: TV": + tv = self.world.random.choice(tvs) + self.world.get_location(tv[1]["name"], self.player)\ + .place_locked_item(self.add_item(gestures[gest][1]["name"], gestures[gest][1]["classification"], + gestures[gest])) + gestures.remove(gestures[gest]) + tvs.remove(tv) + + for i in range(len(gestures)): + gest = self.world.random.choice(gestures) + tv = self.world.random.choice(tvs) + self.world.get_location(tv[1]["name"], self.player)\ + .place_locked_item(self.add_item(gest[1]["name"], gest[1]["classification"], gest[1])) + gestures.remove(gest) + tvs.remove(tv) + + else: # add gestures to pool like normal + for i, data in Items.gesture_item_table.items(): + pool.append(self.add_item(data["name"], data["classification"], i)) + + # add '10 Bones' items if medallion shuffle is enabled + if self.world.medallion_shuffle[self.player]: + for i, data in Items.medallion_item_table.items(): + for j in range(data["count"]): + pool.append(self.add_item(data["name"], data["classification"], i)) + + # add to world's pool + self.world.itempool += pool + + + def fill_slot_data(self) -> Dict[str, Any]: + slot_data: Dict[str, Any] = { + "party_shuffle": self.world.party_shuffle[self.player].value, + "medallion_shuffle": self.world.medallion_shuffle[self.player].value, + "random_start" : self.world.random_start[self.player].value, + "start_location" : self.start_location, + "death_link": self.world.death_link[self.player].value + } + return slot_data + + + def create_regions(self) -> None: + + region_table: Dict[int, Region] = { + 0: Region("Menu", RegionType.Generic, "Menu", self.player, self.world), + 1: Region("Afterlife", RegionType.Generic, "Afterlife", self.player, self.world), + 2: Region("Waynehouse", RegionType.Generic, "Waynehouse", self.player, self.world), + 3: Region("World", RegionType.Generic, "World", self.player, self.world), + 4: Region("New Muldul", RegionType.Generic, "New Muldul", self.player, self.world), + 5: Region("New Muldul Vault", RegionType.Generic, "New Muldul Vault", self.player, self.world), + 6: Region("Viewax", RegionType.Generic, "Viewax's Edifice", self.player, self.world), + 7: Region("Airship", RegionType.Generic, "Airship", self.player, self.world), + 8: Region("Arcade Island", RegionType.Generic, "Arcade Island", self.player, self.world), + 9: Region("TV Island", RegionType.Generic, "TV Island", self.player, self.world), + 10: Region("Juice Ranch", RegionType.Generic, "Juice Ranch", self.player, self.world), + 11: Region("Shield Facility", RegionType.Generic, "Shield Facility", self.player, self.world), + 12: Region("Worm Pod", RegionType.Generic, "Worm Pod", self.player, self.world), + 13: Region("Foglast", RegionType.Generic, "Foglast", self.player, self.world), + 14: Region("Drill Castle", RegionType.Generic, "Drill Castle", self.player, self.world), + 15: Region("Sage Labyrinth", RegionType.Generic, "Sage Labyrinth", self.player, self.world), + 16: Region("Sage Airship", RegionType.Generic, "Sage Airship", self.player, self.world), + 17: Region("Hylemxylem", RegionType.Generic, "Hylemxylem", self.player, self.world) + } + + # create regions from table + for i, reg in region_table.items(): + self.world.regions.append(reg) + # get all exits per region + for j, exits in Exits.region_exit_table.items(): + if j == i: + for k in exits: + # create entrance and connect it to parent and destination regions + ent = Entrance(self.player, k, reg) + reg.exits.append(ent) + if k == "New Game" and self.world.random_start[self.player]: + if self.start_location == "Waynehouse": + ent.connect(region_table[2]) + elif self.start_location == "Viewax's Edifice": + ent.connect(region_table[6]) + elif self.start_location == "TV Island": + ent.connect(region_table[9]) + elif self.start_location == "Shield Facility": + ent.connect(region_table[11]) + else: + for name, num in Exits.exit_lookup_table.items(): + if k == name: + ent.connect(region_table[num]) + + # add regular locations + for i, data in Locations.location_table.items(): + region_table[data["region"]].locations\ + .append(Hylics2Location(self.player, data["name"], i, region_table[data["region"]])) + for i, data in Locations.tv_location_table.items(): + region_table[data["region"]].locations\ + .append(Hylics2Location(self.player, data["name"], i, region_table[data["region"]])) + + # add party member locations if option is enabled + if self.world.party_shuffle[self.player]: + for i, data in Locations.party_location_table.items(): + region_table[data["region"]].locations\ + .append(Hylics2Location(self.player, data["name"], i, region_table[data["region"]])) + + # add medallion locations if option is enabled + if self.world.medallion_shuffle[self.player]: + for i, data in Locations.medallion_location_table.items(): + region_table[data["region"]].locations\ + .append(Hylics2Location(self.player, data["name"], i, region_table[data["region"]])) + + +class Hylics2Location(Location): + game: str = "Hylics 2" + + +class Hylics2Item(Item): + game: str = "Hylics 2" \ No newline at end of file diff --git a/worlds/hylics2/docs/en_Hylics 2.md b/worlds/hylics2/docs/en_Hylics 2.md new file mode 100644 index 0000000000..cb201a52bb --- /dev/null +++ b/worlds/hylics2/docs/en_Hylics 2.md @@ -0,0 +1,17 @@ +# Hylics 2 + +## 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 Hylics 2, all unique items, equipment and skills are randomized. This includes items in chests, items that are freely standing in the world, items recieved from talking to certain characters, gestures learned from TVs, and so on. Items recieved from completing battles are not randomized, with the exception of the Jail Key recieved from defeating Viewax. + +## What Hylics 2 items can appear in other players' worlds? + +Consumable items, key items, gloves, accessories, and gestures can appear in other players' worlds. + +## What does another world's item look like in Hylics 2? + +All items retain their original appearance. You won't know if an item belongs to another player until you collect it. \ No newline at end of file diff --git a/worlds/hylics2/docs/setup_en.md b/worlds/hylics2/docs/setup_en.md new file mode 100644 index 0000000000..1e1ac49790 --- /dev/null +++ b/worlds/hylics2/docs/setup_en.md @@ -0,0 +1,35 @@ +# Hylics 2 Randomizer Setup Guide + +## Required Software + +- Hylics 2 from: [Steam](https://store.steampowered.com/app/1286710/Hylics_2/) or [itch.io](https://mason-lindroth.itch.io/hylics-2) +- BepInEx from: [GitHub](https://github.com/BepInEx/BepInEx/releases) +- Archipelago Mod for Hylics 2 from: [GitHub](https://github.com/TRPG0/ArchipelagoHylics2) + +## Instructions (Windows) + +1. Download and install BepInEx 5 (32-bit, version 5.4.20 or newer) to your Hylics 2 root folder. *Do not use any pre-release versions of BepInEx 6.* + +2. Start Hylics 2 once so that BepInEx can create its required configuration files. + +3. Download the latest version of ArchipelagoHylics2 from the [Releases](https://github.com/TRPG0/ArchipelagoHylics2/releases) page and extract the contents of the zip file into `BepInEx\plugins`. + +4. Start Hylics 2 again. To verify that the mod is working, begin a new game or load a save file. + +## Connecting + +To connect to an Archipelago server, open the in-game console (default key: `/`) and use the command `/connect [address:port] [name] [password]`. The port and password are both optional - if no port is provided then the default port of 38281 is used. +**Make sure that you have connected to a server at least once before attempting to check any locations.** + +## Other Commands + +There are a few additional commands that can be used while playing Hylics 2 randomizer: + +- `/disconnect` - Disconnect from an Archipelago server. +- `/popups` - Enables or disables in-game messages when an item is found or recieved. +- `/airship` - Resummons the airship at the dock above New Muldul and teleports Wayne to it, in case the player gets stuck. Player must have the DOCK KEY to use this command. +- `/respawn` - Moves Wayne back to the spawn position of the current area in case you get stuck. `/respawn home` will teleport Wayne back to his original starting position. +- `/checked [region]` - States how many locations have been checked in a given region. If no region is given, then the player's location will be used. +- `/deathlink` - Enables or disables DeathLink. +- `/help [command]` - Lists a command, it's description, and it's required arguments (if any). If no command is given, all commands will be displayed. +- `![command]` - Entering any command with an `!` at the beginning allows for remotely sending commands to the server. \ No newline at end of file diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index 6e7addb2d0..fd5752bd40 100644 --- a/worlds/minecraft/__init__.py +++ b/worlds/minecraft/__init__.py @@ -150,7 +150,7 @@ class MinecraftWorld(World): def generate_output(self, output_directory: str): data = self._get_mc_data() - filename = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_file_safe_player_name(self.player)}.apmc" + filename = f"{self.world.get_out_file_name_base(self.player)}.apmc" with open(os.path.join(output_directory, filename), 'wb') as f: f.write(b64encode(bytes(json.dumps(data), 'utf-8'))) diff --git a/worlds/oot/EntranceShuffle.py b/worlds/oot/EntranceShuffle.py index 08d1e3ff79..bd06a3d81b 100644 --- a/worlds/oot/EntranceShuffle.py +++ b/worlds/oot/EntranceShuffle.py @@ -343,6 +343,27 @@ priority_entrance_table = { } +# These hint texts have more than one entrance, so they are OK for impa's house and potion shop +multi_interior_regions = { + 'Kokiri Forest', + 'Lake Hylia', + 'the Market', + 'Kakariko Village', + 'Lon Lon Ranch', +} + +interior_entrance_bias = { + 'Kakariko Village -> Kak Potion Shop Front': 4, + 'Kak Backyard -> Kak Potion Shop Back': 4, + 'Kakariko Village -> Kak Impas House': 3, + 'Kak Impas Ledge -> Kak Impas House Back': 3, + 'Goron City -> GC Shop': 2, + 'Zoras Domain -> ZD Shop': 2, + 'Market Entrance -> Market Guard House': 2, + 'ToT Entrance -> Temple of Time': 1, +} + + class EntranceShuffleError(Exception): pass @@ -500,7 +521,7 @@ def shuffle_random_entrances(ootworld): delete_target_entrance(remaining_target) for pool_type, entrance_pool in one_way_entrance_pools.items(): - shuffle_entrance_pool(ootworld, entrance_pool, one_way_target_entrance_pools[pool_type], locations_to_ensure_reachable, all_state, none_state, check_all=True, retry_count=5) + shuffle_entrance_pool(ootworld, pool_type, entrance_pool, one_way_target_entrance_pools[pool_type], locations_to_ensure_reachable, all_state, none_state, check_all=True, retry_count=5) replaced_entrances = [entrance.replaces for entrance in entrance_pool] for remaining_target in chain.from_iterable(one_way_target_entrance_pools.values()): if remaining_target.replaces in replaced_entrances: @@ -510,7 +531,7 @@ def shuffle_random_entrances(ootworld): # Shuffle all entrance pools, in order for pool_type, entrance_pool in entrance_pools.items(): - shuffle_entrance_pool(ootworld, entrance_pool, target_entrance_pools[pool_type], locations_to_ensure_reachable, all_state, none_state) + shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrance_pools[pool_type], locations_to_ensure_reachable, all_state, none_state, check_all=True) # Multiple checks after shuffling to ensure everything is OK # Check that all entrances hook up correctly @@ -596,7 +617,7 @@ def place_one_way_priority_entrance(ootworld, priority_name, allowed_regions, al raise EntranceShuffleError(f'Unable to place priority one-way entrance for {priority_name} in world {ootworld.player}') -def shuffle_entrance_pool(ootworld, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=20): +def shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=20): restrictive_entrances, soft_entrances = split_entrances_by_requirements(ootworld, entrance_pool, target_entrances) @@ -604,11 +625,11 @@ def shuffle_entrance_pool(ootworld, entrance_pool, target_entrances, locations_t retry_count -= 1 rollbacks = [] try: - shuffle_entrances(ootworld, restrictive_entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state) + shuffle_entrances(ootworld, pool_type+'Rest', restrictive_entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state) if check_all: - shuffle_entrances(ootworld, soft_entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state) + shuffle_entrances(ootworld, pool_type+'Soft', soft_entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state) else: - shuffle_entrances(ootworld, soft_entrances, target_entrances, rollbacks, set(), all_state, none_state) + shuffle_entrances(ootworld, pool_type+'Soft', soft_entrances, target_entrances, rollbacks, set(), all_state, none_state) validate_world(ootworld, None, locations_to_ensure_reachable, all_state, none_state) for entrance, target in rollbacks: @@ -621,12 +642,16 @@ def shuffle_entrance_pool(ootworld, entrance_pool, target_entrances, locations_t raise EntranceShuffleError(f'Entrance placement attempt count exceeded for world {ootworld.player}') -def shuffle_entrances(ootworld, entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state): +def shuffle_entrances(ootworld, pool_type, entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state): ootworld.world.random.shuffle(entrances) for entrance in entrances: if entrance.connected_region != None: continue ootworld.world.random.shuffle(target_entrances) + # Here we deliberately introduce bias by prioritizing certain interiors, i.e. the ones most likely to cause problems. + # success rate over randomization + if pool_type in {'InteriorSoft', 'MixedSoft'}: + target_entrances.sort(reverse=True, key=lambda entrance: interior_entrance_bias.get(entrance.replaces.name, 0)) for target in target_entrances: if target.connected_region == None: continue @@ -715,25 +740,33 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all # Check if all locations are reachable if not beatable-only or game is not yet complete if locations_to_ensure_reachable: - if world.accessibility[player].current_key != 'minimal' or not world.can_beat_game(all_state): - for loc in locations_to_ensure_reachable: - if not all_state.can_reach(loc, 'Location', player): - raise EntranceShuffleError(f'{loc} is unreachable') + for loc in locations_to_ensure_reachable: + if not all_state.can_reach(loc, 'Location', player): + raise EntranceShuffleError(f'{loc} is unreachable') if ootworld.shuffle_interior_entrances and (ootworld.misc_hints or ootworld.hints != 'none') and \ (entrance_placed == None or entrance_placed.type in ['Interior', 'SpecialInterior']): # Ensure Kak Potion Shop entrances are in the same hint area so there is no ambiguity as to which entrance is used for hints - potion_front_entrance = get_entrance_replacing(world.get_region('Kak Potion Shop Front', player), 'Kakariko Village -> Kak Potion Shop Front', player) - potion_back_entrance = get_entrance_replacing(world.get_region('Kak Potion Shop Back', player), 'Kak Backyard -> Kak Potion Shop Back', player) - if potion_front_entrance is not None and potion_back_entrance is not None and not same_hint_area(potion_front_entrance, potion_back_entrance): + potion_front = get_entrance_replacing(world.get_region('Kak Potion Shop Front', player), 'Kakariko Village -> Kak Potion Shop Front', player) + potion_back = get_entrance_replacing(world.get_region('Kak Potion Shop Back', player), 'Kak Backyard -> Kak Potion Shop Back', player) + if potion_front is not None and potion_back is not None and not same_hint_area(potion_front, potion_back): raise EntranceShuffleError('Kak Potion Shop entrances are not in the same hint area') + elif (potion_front and not potion_back) or (not potion_front and potion_back): + # Check the hint area and ensure it's one of the ones with more than one entrance + potion_placed_entrance = potion_front if potion_front else potion_back + if get_hint_area(potion_placed_entrance) not in multi_interior_regions: + raise EntranceShuffleError('Kak Potion Shop entrances can never be in the same hint area') # When cows are shuffled, ensure the same thing for Impa's House, since the cow is reachable from both sides if ootworld.shuffle_cows: - impas_front_entrance = get_entrance_replacing(world.get_region('Kak Impas House', player), 'Kakariko Village -> Kak Impas House', player) - impas_back_entrance = get_entrance_replacing(world.get_region('Kak Impas House Back', player), 'Kak Impas Ledge -> Kak Impas House Back', player) - if impas_front_entrance is not None and impas_back_entrance is not None and not same_hint_area(impas_front_entrance, impas_back_entrance): + impas_front = get_entrance_replacing(world.get_region('Kak Impas House', player), 'Kakariko Village -> Kak Impas House', player) + impas_back = get_entrance_replacing(world.get_region('Kak Impas House Back', player), 'Kak Impas Ledge -> Kak Impas House Back', player) + if impas_front is not None and impas_back is not None and not same_hint_area(impas_front, impas_back): raise EntranceShuffleError('Kak Impas House entrances are not in the same hint area') + elif (impas_front and not impas_back) or (not impas_front and impas_back): + impas_placed_entrance = impas_front if impas_front else impas_back + if get_hint_area(impas_placed_entrance) not in multi_interior_regions: + raise EntranceShuffleError('Kak Impas House entrances can never be in the same hint area') # Check basic refills, time passing, return to ToT if (ootworld.shuffle_special_interior_entrances or ootworld.shuffle_overworld_entrances or ootworld.spawn_positions) and \ @@ -845,3 +878,4 @@ def delete_target_entrance(target): if target.parent_region != None: target.parent_region.exits.remove(target) target.parent_region = None + del target diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index b65882c874..c985ea13f0 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -1,7 +1,7 @@ import logging import threading import copy -from collections import Counter +from collections import Counter, deque logger = logging.getLogger("Ocarina of Time") @@ -412,17 +412,6 @@ class OOTWorld(World): self.shop_prices[location.name] = int(self.world.random.betavariate(1.5, 2) * 60) * 5 def fill_bosses(self, bossCount=9): - rewardlist = ( - 'Kokiri Emerald', - 'Goron Ruby', - 'Zora Sapphire', - 'Forest Medallion', - 'Fire Medallion', - 'Water Medallion', - 'Spirit Medallion', - 'Shadow Medallion', - 'Light Medallion' - ) boss_location_names = ( 'Queen Gohma', 'King Dodongo', @@ -434,7 +423,7 @@ class OOTWorld(World): 'Twinrova', 'Links Pocket' ) - boss_rewards = [self.create_item(reward) for reward in rewardlist] + boss_rewards = [item for item in self.itempool if item.type == 'DungeonReward'] boss_locations = [self.world.get_location(loc, self.player) for loc in boss_location_names] placed_prizes = [loc.item.name for loc in boss_locations if loc.item is not None] @@ -447,9 +436,8 @@ class OOTWorld(World): self.world.random.shuffle(prize_locs) item = prizepool.pop() loc = prize_locs.pop() - self.world.push_item(loc, item, collect=False) - loc.locked = True - loc.event = True + loc.place_locked_item(item) + self.world.itempool.remove(item) def create_item(self, name: str): if name in item_table: @@ -496,6 +484,10 @@ class OOTWorld(World): # Generate itempool generate_itempool(self) add_dungeon_items(self) + # Add dungeon rewards + rewardlist = sorted(list(self.item_name_groups['rewards'])) + self.itempool += map(self.create_item, rewardlist) + junk_pool = get_junk_pool(self) removed_items = [] # Determine starting items @@ -621,61 +613,64 @@ class OOTWorld(World): "Gerudo Training Ground Maze Path Final Chest", "Gerudo Training Ground MQ Ice Arrows Chest", ] + def get_names(items): + for item in items: + yield item.name + # Place/set rules for dungeon items itempools = { - 'dungeon': [], - 'overworld': [], - 'any_dungeon': [], + 'dungeon': set(), + 'overworld': set(), + 'any_dungeon': set(), } any_dungeon_locations = [] for dungeon in self.dungeons: - itempools['dungeon'] = [] + itempools['dungeon'] = set() # Put the dungeon items into their appropriate pools. # Build in reverse order since we need to fill boss key first and pop() returns the last element if self.shuffle_mapcompass in itempools: - itempools[self.shuffle_mapcompass].extend(dungeon.dungeon_items) + itempools[self.shuffle_mapcompass].update(get_names(dungeon.dungeon_items)) if self.shuffle_smallkeys in itempools: - itempools[self.shuffle_smallkeys].extend(dungeon.small_keys) + itempools[self.shuffle_smallkeys].update(get_names(dungeon.small_keys)) shufflebk = self.shuffle_bosskeys if dungeon.name != 'Ganons Castle' else self.shuffle_ganon_bosskey if shufflebk in itempools: - itempools[shufflebk].extend(dungeon.boss_key) + itempools[shufflebk].update(get_names(dungeon.boss_key)) # We can't put a dungeon item on the end of a dungeon if a song is supposed to go there. Make sure not to include it. dungeon_locations = [loc for region in dungeon.regions for loc in region.locations if loc.item is None and ( self.shuffle_song_items != 'dungeon' or loc.name not in dungeon_song_locations)] if itempools['dungeon']: # only do this if there's anything to shuffle - for item in itempools['dungeon']: + dungeon_itempool = [item for item in self.world.itempool if item.player == self.player and item.name in itempools['dungeon']] + for item in dungeon_itempool: self.world.itempool.remove(item) self.world.random.shuffle(dungeon_locations) fill_restrictive(self.world, self.world.get_all_state(False), dungeon_locations, - itempools['dungeon'], True, True) + dungeon_itempool, True, True) any_dungeon_locations.extend(dungeon_locations) # adds only the unfilled locations # Now fill items that can go into any dungeon. Retrieve the Gerudo Fortress keys from the pool if necessary if self.shuffle_fortresskeys == 'any_dungeon': - fortresskeys = filter(lambda item: item.player == self.player and item.type == 'HideoutSmallKey', - self.world.itempool) - itempools['any_dungeon'].extend(fortresskeys) + itempools['any_dungeon'].add('Small Key (Thieves Hideout)') if itempools['any_dungeon']: - for item in itempools['any_dungeon']: + any_dungeon_itempool = [item for item in self.world.itempool if item.player == self.player and item.name in itempools['any_dungeon']] + for item in any_dungeon_itempool: self.world.itempool.remove(item) - itempools['any_dungeon'].sort(key=lambda item: - {'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'HideoutSmallKey': 1}.get(item.type, 0)) + any_dungeon_itempool.sort(key=lambda item: + {'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'HideoutSmallKey': 1}.get(item.type, 0)) self.world.random.shuffle(any_dungeon_locations) fill_restrictive(self.world, self.world.get_all_state(False), any_dungeon_locations, - itempools['any_dungeon'], True, True) + any_dungeon_itempool, True, True) # If anything is overworld-only, fill into local non-dungeon locations if self.shuffle_fortresskeys == 'overworld': - fortresskeys = filter(lambda item: item.player == self.player and item.type == 'HideoutSmallKey', - self.world.itempool) - itempools['overworld'].extend(fortresskeys) + itempools['overworld'].add('Small Key (Thieves Hideout)') if itempools['overworld']: - for item in itempools['overworld']: + overworld_itempool = [item for item in self.world.itempool if item.player == self.player and item.name in itempools['overworld']] + for item in overworld_itempool: self.world.itempool.remove(item) - itempools['overworld'].sort(key=lambda item: - {'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'HideoutSmallKey': 1}.get(item.type, 0)) + overworld_itempool.sort(key=lambda item: + {'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'HideoutSmallKey': 1}.get(item.type, 0)) non_dungeon_locations = [loc for loc in self.get_locations() if not loc.item and loc not in any_dungeon_locations and (loc.type != 'Shop' or loc.name in self.shop_prices) and @@ -683,7 +678,7 @@ class OOTWorld(World): (loc.name not in dungeon_song_locations or self.shuffle_song_items != 'dungeon')] self.world.random.shuffle(non_dungeon_locations) fill_restrictive(self.world, self.world.get_all_state(False), non_dungeon_locations, - itempools['overworld'], True, True) + overworld_itempool, True, True) # Place songs # 5 built-in retries because this section can fail sometimes @@ -805,6 +800,10 @@ class OOTWorld(World): or (self.skip_child_zelda and loc.name in ['HC Zeldas Letter', 'Song from Impa'])): loc.address = None + # Handle item-linked dungeon items and songs + def stage_pre_fill(cls): + pass + def generate_output(self, output_directory: str): if self.hints != 'none': self.hint_data_available.wait() @@ -819,7 +818,7 @@ class OOTWorld(World): # Seed hint RNG, used for ganon text lines also self.hint_rng = self.world.slot_seeds[self.player] - outfile_name = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_file_safe_player_name(self.player)}" + outfile_name = self.world.get_out_file_name_base(self.player) rom = Rom(file=get_options()['oot_options']['rom_file']) if self.hints != 'none': buildWorldGossipHints(self) @@ -831,18 +830,25 @@ class OOTWorld(World): # Write entrances to spoiler log all_entrances = self.get_shuffled_entrances() - all_entrances.sort(key=lambda x: x.name) - all_entrances.sort(key=lambda x: x.type) + all_entrances.sort(reverse=True, key=lambda x: x.name) + all_entrances.sort(reverse=True, key=lambda x: x.type) if not self.decouple_entrances: - for loadzone in all_entrances: - if loadzone.primary: - entrance = loadzone + while all_entrances: + loadzone = all_entrances.pop() + if loadzone.type != 'Overworld': + if loadzone.primary: + entrance = loadzone + else: + entrance = loadzone.reverse + if entrance.reverse is not None: + self.world.spoiler.set_entrance(entrance, entrance.replaces.reverse, 'both', self.player) + else: + self.world.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player) else: - entrance = loadzone.reverse - if entrance.reverse is not None: - self.world.spoiler.set_entrance(entrance, entrance.replaces.reverse, 'both', self.player) - else: - self.world.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player) + reverse = loadzone.replaces.reverse + if reverse in all_entrances: + all_entrances.remove(reverse) + self.world.spoiler.set_entrance(loadzone, reverse, 'both', self.player) else: for entrance in all_entrances: self.world.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player) @@ -1027,7 +1033,7 @@ class OOTWorld(World): all_state = self.world.get_all_state(use_cache=False) # Remove event progression items for item, player in all_state.prog_items: - if (item not in item_table or item_table[item][2] is None) and player == self.player: + if player == self.player and (item not in item_table or (item_table[item][2] is None and item_table[item][0] != 'DungeonReward')): all_state.prog_items[(item, player)] = 0 # Remove all events and checked locations all_state.locations_checked = {loc for loc in all_state.locations_checked if loc.player != self.player} diff --git a/worlds/oribf/__init__.py b/worlds/oribf/__init__.py index 05d237659c..45e666efeb 100644 --- a/worlds/oribf/__init__.py +++ b/worlds/oribf/__init__.py @@ -62,7 +62,7 @@ class OriBlindForest(World): def generate_basic(self): for item_name, count in default_pool.items(): - self.world.itempool.extend([self.create_item(item_name)] * count) + self.world.itempool.extend([self.create_item(item_name) for _ in range(count)]) def create_item(self, name: str) -> Item: return Item(name, diff --git a/worlds/overcooked2/Items.py b/worlds/overcooked2/Items.py new file mode 100644 index 0000000000..8cbe071140 --- /dev/null +++ b/worlds/overcooked2/Items.py @@ -0,0 +1,152 @@ +from BaseClasses import Item +from typing import NamedTuple, Dict + + +class ItemData(NamedTuple): + code: int + + +class Overcooked2Item(Item): + game: str = "Overcooked! 2" + + +oc2_base_id = 213700 + +item_table: Dict[str, ItemData] = { + "Wood" : ItemData(oc2_base_id + 1 ), + "Coal Bucket" : ItemData(oc2_base_id + 2 ), + "Spare Plate" : ItemData(oc2_base_id + 3 ), + "Fire Extinguisher" : ItemData(oc2_base_id + 4 ), + "Bellows" : ItemData(oc2_base_id + 5 ), + "Clean Dishes" : ItemData(oc2_base_id + 6 ), + "Larger Tip Jar" : ItemData(oc2_base_id + 7 ), + "Progressive Dash" : ItemData(oc2_base_id + 8 ), + "Progressive Throw/Catch" : ItemData(oc2_base_id + 9 ), + "Coin Purse" : ItemData(oc2_base_id + 10), + "Control Stick Batteries" : ItemData(oc2_base_id + 11), + "Wok Wheels" : ItemData(oc2_base_id + 12), + "Dish Scrubber" : ItemData(oc2_base_id + 13), + "Burn Leniency" : ItemData(oc2_base_id + 14), + "Sharp Knife" : ItemData(oc2_base_id + 15), + "Order Lookahead" : ItemData(oc2_base_id + 16), + "Lightweight Backpack" : ItemData(oc2_base_id + 17), + "Faster Respawn Time" : ItemData(oc2_base_id + 18), + "Faster Condiment/Drink Switch" : ItemData(oc2_base_id + 19), + "Guest Patience" : ItemData(oc2_base_id + 20), + "Kevin-1" : ItemData(oc2_base_id + 21), + "Kevin-2" : ItemData(oc2_base_id + 22), + "Kevin-3" : ItemData(oc2_base_id + 23), + "Kevin-4" : ItemData(oc2_base_id + 24), + "Kevin-5" : ItemData(oc2_base_id + 25), + "Kevin-6" : ItemData(oc2_base_id + 26), + "Kevin-7" : ItemData(oc2_base_id + 27), + "Kevin-8" : ItemData(oc2_base_id + 28), + "Cooking Emote" : ItemData(oc2_base_id + 29), + "Curse Emote" : ItemData(oc2_base_id + 30), + "Serving Emote" : ItemData(oc2_base_id + 31), + "Preparing Emote" : ItemData(oc2_base_id + 32), + "Washing Up Emote" : ItemData(oc2_base_id + 33), + "Ok Emote" : ItemData(oc2_base_id + 34), + "Ramp Button" : ItemData(oc2_base_id + 35), + "Bonus Star" : ItemData(oc2_base_id + 36), + "Calmer Unbread" : ItemData(oc2_base_id + 37), +} + +item_frequencies = { + "Progressive Throw/Catch": 2, + "Larger Tip Jar": 2, + "Order Lookahead": 2, + "Progressive Dash": 2, + "Bonus Star": 0, # Filler Item + # default: 1 +} + +item_name_to_config_name = { + "Wood" : "DisableWood" , + "Coal Bucket" : "DisableCoal" , + "Spare Plate" : "DisableOnePlate" , + "Fire Extinguisher" : "DisableFireExtinguisher" , + "Bellows" : "DisableBellows" , + "Clean Dishes" : "PlatesStartDirty" , + "Control Stick Batteries" : "DisableControlStick" , + "Wok Wheels" : "DisableWokDrag" , + "Dish Scrubber" : "WashTimeMultiplier" , + "Burn Leniency" : "BurnSpeedMultiplier" , + "Sharp Knife" : "ChoppingTimeScale" , + "Lightweight Backpack" : "BackpackMovementScale" , + "Faster Respawn Time" : "RespawnTime" , + "Faster Condiment/Drink Switch": "CarnivalDispenserRefactoryTime", + "Guest Patience" : "CustomOrderLifetime" , + "Ramp Button" : "DisableRampButton" , + "Calmer Unbread" : "AggressiveHorde" , + "Coin Purse" : "DisableEarnHordeMoney" , +} + +vanilla_values = { + "DisableWood": False, + "DisableCoal": False, + "DisableOnePlate": False, + "DisableFireExtinguisher": False, + "DisableBellows": False, + "PlatesStartDirty": False, + "DisableControlStick": False, + "DisableWokDrag": False, + "DisableRampButton": False, + "WashTimeMultiplier": 1.0, + "BurnSpeedMultiplier": 1.0, + "ChoppingTimeScale": 1.0, + "BackpackMovementScale": 1.0, + "RespawnTime": 5.0, + "CarnivalDispenserRefactoryTime": 0.0, + "CustomOrderLifetime": 100.0, + "AggressiveHorde": False, + "DisableEarnHordeMoney": False, +} + +item_id_to_name: Dict[int, str] = { + data.code: item_name for item_name, data in item_table.items() if data.code +} + +item_name_to_id: Dict[str, int] = { + item_name: data.code for item_name, data in item_table.items() if data.code +} + + +def is_progression(item_name: str) -> bool: + return not item_name.endswith("Emote") + + +def item_to_unlock_event(item_name: str) -> Dict[str, str]: + message = f"{item_name} Acquired!" + action = "" + payload = "" + if item_name.startswith("Kevin"): + kevin_num = int(item_name.split("-")[-1]) + action = "UNLOCK_LEVEL" + payload = str(kevin_num + 36) + elif "Emote" in item_name: + action = "UNLOCK_EMOTE" + payload = str(item_table[item_name].code - item_table["Cooking Emote"].code) + elif item_name == "Larger Tip Jar": + action = "INC_TIP_COMBO" + elif item_name == "Order Lookahead": + action = "INC_ORDERS_ON_SCREEN" + elif item_name == "Bonus Star": + action = "INC_STAR_COUNT" + payload = "1" + elif item_name == "Progressive Dash": + action = "INC_DASH" + elif item_name == "Progressive Throw/Catch": + action = "INC_THROW" + else: + config_name = item_name_to_config_name[item_name] + vanilla_value = vanilla_values[config_name] + + action = "SET_VALUE" + payload = f"{config_name}={vanilla_value}" + + return { + "message": message, + "action": action, + "payload": payload, + } diff --git a/worlds/overcooked2/Locations.py b/worlds/overcooked2/Locations.py new file mode 100644 index 0000000000..1b73b74e94 --- /dev/null +++ b/worlds/overcooked2/Locations.py @@ -0,0 +1,15 @@ +from BaseClasses import Location +from .Overcooked2Levels import Overcooked2Level + + +class Overcooked2Location(Location): + game: str = "Overcooked! 2" + + +oc2_location_name_to_id = dict() +oc2_location_id_to_name = dict() +for level in Overcooked2Level(): + if level.level_id == 36: + continue # level 6-6 does not have an item location + oc2_location_name_to_id[level.location_name_item] = level.level_id + oc2_location_id_to_name[level.level_id] = level.location_name_item diff --git a/worlds/overcooked2/Logic.py b/worlds/overcooked2/Logic.py new file mode 100644 index 0000000000..6fb1a50a41 --- /dev/null +++ b/worlds/overcooked2/Logic.py @@ -0,0 +1,3899 @@ +from BaseClasses import CollectionState +from .Overcooked2Levels import Overcooked2GenericLevel, Overcooked2Dlc, Overcooked2Level +from typing import Dict +from random import Random + + +def has_requirements_for_level_access(state: CollectionState, level_name: str, previous_level_completed_event_name: str, + required_star_count: int, player: int) -> bool: + # Check if the ramps in the overworld are set correctly + if level_name in ramp_logic: + if not state.has("Ramp Button", player): + return False # need the item to use ramps + + for req in ramp_logic[level_name]: + if not state.has(req + " Level Complete", player): + return False # This level needs another to be beaten first + + # Kevin Levels Need to have the corresponding items + if level_name.startswith("K"): + return state.has(level_name, player) + + # Must have enough stars to purchase level + star_count = state.item_count("Star", player) + state.item_count("Bonus Star", player) + if star_count < required_star_count: + return False + + # If this isn't the first level in a world, it needs the previous level to be unlocked first + if previous_level_completed_event_name is not None: + if not state.has(previous_level_completed_event_name, player): + return False + + # If we made it this far we have all requirements + return True + + +def has_requirements_for_level_star( + state: CollectionState, level: Overcooked2GenericLevel, stars: int, player: int) -> bool: + assert 0 <= stars <= 3 + + # First ensure that previous stars are obtainable + if stars > 1: + if not has_requirements_for_level_star(state, level, stars-1, player): + return False + + # Second, ensure that global requirements are met + if not meets_requirements(state, "*", stars, player): + return False + + # Finally, return success only if this level's requirements are met + return meets_requirements(state, level.shortname, stars, player) + + +def meets_requirements(state: CollectionState, name: str, stars: int, player: int): + # Get requirements for level + (exclusive_reqs, additive_reqs) = level_logic[name][stars-1] + + # print(f"{name} ({stars}-Stars): {exclusive_reqs}|{additive_reqs}") + + # Check if we meet exclusive requirements + if len(exclusive_reqs) > 0 and not state.has_all(exclusive_reqs, player): + return False + + # Check if we meet additive requirements + if len(additive_reqs) == 0: + return True + + total: float = 0.0 + for (item_name, weight) in additive_reqs: + for _ in range(0, state.item_count(item_name, player)): + total += weight + if total >= 0.99: # be nice to rounding errors :) + return True + + return False + + +def is_item_progression(item_name, level_mapping, include_kevin): + if item_name.endswith("Emote"): + return False + + if "Kevin" in item_name or item_name in ["Ramp Button"]: + return True # always progression + + def item_in_logic(shortname, _item_name): + for star in range(0, 3): + (exclusive, additive) = level_logic[shortname][star] + + if _item_name in exclusive: + return True + + for req in additive: + if req[0] == _item_name: + if req[1] > 0.3: # this bit smells of a deal with the devil, but it seems to be for the better + return True + break + + return False + + if item_in_logic("*", item_name): + return True + + for level in Overcooked2Level(): + if not include_kevin and level.level_id > 36: + break + + if level_mapping is None: + unmapped_level = Overcooked2GenericLevel(level.level_id) + else: + unmapped_level = level_mapping[level.level_id] + + if item_in_logic(unmapped_level.shortname, item_name): + return True + + return False + + +def is_useful(item_name): + return item_name in [ + "Faster Respawn Time", + "Fire Extinguisher", + "Clean Dishes", + "Larger Tip Jar", + "Dish Scrubber", + "Burn Leniency", + "Sharp Knife", + "Order Lookahead", + "Guest Patience", + "Bonus Star", + ] + + +def level_shuffle_factory( + rng: Random, + shuffle_prep_levels: bool, + shuffle_horde_levels: bool, +) -> Dict[int, Overcooked2GenericLevel]: # return + # Create a list of all valid levels for selection + # (excludes tutorial, throne, kevin and sometimes horde levels) + pool = list() + for dlc in Overcooked2Dlc: + for level_id in range(dlc.start_level_id(), dlc.end_level_id()): + if level_id in dlc.excluded_levels(): + continue + + if not shuffle_horde_levels and level_id in dlc.horde_levels(): + continue + + if not shuffle_prep_levels and level_id in dlc.prep_levels(): + continue + + pool.append( + Overcooked2GenericLevel(level_id, dlc) + ) + + # Sort the pool to eliminate risk + pool.sort(key=lambda x: int(x.dlc)*1000 + x.level_id) + + result: Dict[int, Overcooked2GenericLevel] = dict() + story = Overcooked2Dlc.STORY + + while len(result) == 0 or not meets_minimum_sphere_one_requirements(result): + result.clear() + + # Shuffle the pool, using the provided RNG + rng.shuffle(pool) + + # Return the first 44 levels and assign those to each level + for level_id in range(story.start_level_id(), story.end_level_id()): + if level_id not in story.excluded_levels(): + result[level_id] = pool[level_id-1] + else: + result[level_id] = Overcooked2GenericLevel(level_id) # This is just 6-6 right now + + return result + + +def meets_minimum_sphere_one_requirements( + levels: Dict[int, Overcooked2GenericLevel], +) -> bool: + + # 1-1, 2-1, and 4-1 are garunteed to be accessible on + # the overworld without requiring a ramp or additional stars + sphere_one = [1, 7, 19] + + # 1-2, 2-2, 3-1 and 5-1 are almost always the next thing unlocked + sphere_twoish = [2, 8, 13, 25] + + # Peek the logic for sphere one and see how many are possible + # with no items + sphere_one_count = 0 + for level_id in sphere_one: + if (is_completable_no_items(levels[level_id])): + sphere_one_count += 1 + + sphere_twoish_count = 0 + for level_id in sphere_twoish: + if (is_completable_no_items(levels[level_id])): + sphere_twoish_count += 1 + + return sphere_one_count >= 2 and \ + sphere_twoish_count >= 2 and \ + sphere_one_count + sphere_twoish_count >= 6 + + +def is_completable_no_items(level: Overcooked2GenericLevel) -> bool: + one_star_logic = level_logic[level.shortname][0] + (exclusive, additive) = one_star_logic + + # print(f"\n{level.shortname}: {exclusive} / {additive}") + + return len(exclusive) == 0 and len(additive) == 0 + + +# If key missing, doesn't require a ramp to access (or the logic is handled by a preceeding level) +# +# If empty, a ramp is required to access, but the ramp button is garunteed accessible +# +# If populated, a ramp is required to access and the button requires all levels in the +# list to be compelted before it can be pressed +# +ramp_logic = { + "1-5": [], + "2-2": [], + "3-1": [], + "5-2": [], + "6-1": [], + "6-2": ["5-1"], # 5-1 spawns blue button, blue button gets you to red button + "Kevin-1": [], + "Kevin-7": ["5-1"], # 5-1 spawns blue button, + # press blue button, + # climb blue ramp, + # jump the gap, + # climb wood ramps + "Kevin-8": ["5-1", "6-2"], # Same as above, but 6-2 spawns the ramp to K8 +} + +horde_logic = { # Additive + ("Coin Purse", 0.7), + ("Calmer Unbread", 0.35), + ("Progressive Dash", 0.2), + ("Progressive Throw/Catch", 0.15), + ("Sharp Knife", 0.15), + ("Dish Scrubber", 0.125), + ("Burn Leniency", 0.1), + ("Spare Plate", 0.075), + ("Clean Dishes", 0.025), +} + +# Level 1 - dict keyed by friendly level names +# Level 2 - tuple with 3 elements, one for each star requirement +# Level 3 - tuple with 2 elements, one for exclusive requirements and one for additive requirements +# Level 4 (exclusive) - set of item name strings of items which MUST be in the inventory to allow logical completion +# Level 4 (additive) - list of tuples containing item name and item weight where the sum of which are in the player's inventory +# must be 1.0+ to allow logical completion +# +# Each Star's logical requirements imply any previous requirements +# +level_logic = { + # "Tutorial": [], + "*": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + ("Progressive Throw/Catch", 0.4), + ("Progressive Dash", 0.35), + ("Sharp Knife", 0.3), + ("Dish Scrubber", 0.25), + ("Larger Tip Jar", 0.2), + ("Spare Plate", 0.2), + ("Burn Leniency", 0.15), + ("Order Lookahead", 0.15), + ("Clean Dishes", 0.1), + ("Guest Patience", 0.1), + }, + ), + ( # 3-star + [ # Exclusive + "Progressive Dash", + "Spare Plate", + "Larger Tip Jar", + "Progressive Throw/Catch", + ], + { # Additive + }, + ) + ), + "Story 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 1-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 1-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 1-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 1-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 1-6": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 2-1": ( + ( # 1-star + { # Exclusive + "Progressive Throw/Catch", + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 2-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 2-3": ( + ( # 1-star + { # Exclusive + "Progressive Throw/Catch" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 2-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + ("Progressive Throw/Catch", 1.0), + ("Progressive Dash", 1.0), + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Fire Extinguisher", + }, + [ # Additive + + ] + ) + ), + "Story 2-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 2-6": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 3-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 3-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + ("Progressive Throw/Catch", 1.0), + ("Progressive Dash", 0.5), + ("Sharp Knife", 0.5), + ("Larger Tip Jar", 0.25), + ("Dish Scrubber", 0.25), + }, + ), + ( # 2-star + { # Exclusive + "Progressive Throw/Catch", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 3-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 3-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Progressive Throw/Catch", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 3-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Progressive Throw/Catch", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 3-6": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 4-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 4-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Progressive Throw/Catch", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 4-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 4-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 4-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 4-6": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 5-1": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 5-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Fire Extinguisher", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 5-3": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 5-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 5-5": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 5-6": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 6-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Fire Extinguisher", + }, + { # Additive + + }, + ) + ), + "Story 6-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 6-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 6-4": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 6-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 6-6": ( + ( # 1-star + { # Exclusive + "Progressive Throw/Catch", + "Progressive Dash", + "Spare Plate", + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story K-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story K-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story K-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story K-4": ( + ( # 1-star + { # Exclusive + "Fire Extinguisher", + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story K-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story K-6": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story K-7": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story K-8": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 1-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 1-3": ( + ( # 1-star + { # Exclusive + "Progressive Throw/Catch", + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 1-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 2-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Bellows", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 2-2": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Bellows", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 2-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 2-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 3-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Bellows", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 3-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 3-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 3-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Bellows", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf K-1": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Progressive Throw/Catch", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire 1-1": ( + ( # 1-star + { # Exclusive + "Wood" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire 1-2": ( + ( # 1-star + { # Exclusive + "Wood" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire 1-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Lightweight Backpack" + }, + { # Additive + + }, + ) + ), + "Campfire 1-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire 2-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire 2-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Lightweight Backpack" + }, + { # Additive + + }, + ) + ), + "Campfire 2-3": ( + ( # 1-star + { # Exclusive + "Wood" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Lightweight Backpack" + }, + { # Additive + + }, + ) + ), + "Campfire 2-4": ( + ( # 1-star + { # Exclusive + "Wood" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire 3-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Lightweight Backpack" + }, + { # Additive + + }, + ) + ), + "Campfire 3-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire 3-3": ( + ( # 1-star + { # Exclusive + "Wood", + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Lightweight Backpack", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire 3-4": ( + ( # 1-star + { # Exclusive + "Wood", + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire K-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire K-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire K-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Carnival 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Carnival 1-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Carnival 1-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Carnival 1-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Faster Condiment/Drink Switch" + }, + { # Additive + + }, + ) + ), + "Carnival 2-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Carnival 2-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Faster Condiment/Drink Switch" + }, + { # Additive + + }, + ) + ), + "Carnival 2-3": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Carnival 2-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Faster Condiment/Drink Switch" + }, + { # Additive + + }, + ) + ), + "Carnival 3-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Carnival 3-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Faster Condiment/Drink Switch" + }, + { # Additive + + }, + ) + ), + "Carnival 3-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Faster Condiment/Drink Switch" + }, + { # Additive + + }, + ) + ), + "Carnival 3-4": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Faster Condiment/Drink Switch" + }, + { # Additive + + }, + ) + ), + "Carnival K-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Carnival K-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Carnival K-3": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + } + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde 1-2": ( + ( # 1-star + { # Exclusive + + }, + horde_logic + ), + ( # 2-star + { # Exclusive + "Coal Bucket", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde 1-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Coal Bucket", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde 2-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde 2-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde 2-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Coal Bucket", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde 3-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde 3-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde 3-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + ("Progressive Throw/Catch", 0.5), + ("Progressive Dash", 0.5), + ("Coal Bucket", 0.5), + }, + ), + ( # 2-star + { # Exclusive + "Progressive Throw/Catch", + "Coal Bucket", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde K-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde K-2": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde K-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde H-1": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde H-2": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde H-3": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde H-4": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde H-5": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde H-6": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde H-7": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde H-8": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Christmas 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Christmas 1-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Christmas 1-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Christmas 1-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Christmas 1-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Chinese 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Chinese 1-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Wok Wheels" + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Chinese 1-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Chinese 1-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Wok Wheels" + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Chinese 1-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Chinese 1-6": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Chinese 1-7": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Wok Wheels" + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Winter 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Winter H-2": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Winter 1-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Winter H-4": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Winter 1-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Spring 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Wok Wheels" + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Spring 1-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Spring 1-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Wok Wheels" + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Spring 1-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Spring 1-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "SOBO 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Faster Condiment/Drink Switch" + }, + { # Additive + + }, + ) + ), + "SOBO 1-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Faster Condiment/Drink Switch" + }, + { # Additive + + }, + ) + ), + "SOBO 1-3": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Fire Extinguisher", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "SOBO 1-4": ( + ( # 1-star + { # Exclusive + "Fire Extinguisher", + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Faster Condiment/Drink Switch" + }, + { # Additive + + }, + ) + ), + "SOBO 1-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Fire Extinguisher", + "Faster Condiment/Drink Switch", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + }, + { # Additive + + }, + ) + ), + "Moon 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Moon 1-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Moon 1-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Moon 1-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Moon 1-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), +} diff --git a/worlds/overcooked2/Options.py b/worlds/overcooked2/Options.py new file mode 100644 index 0000000000..78e0fd6e90 --- /dev/null +++ b/worlds/overcooked2/Options.py @@ -0,0 +1,110 @@ +from typing import TypedDict +from Options import DefaultOnToggle, Range, Choice + + +class OC2OnToggle(DefaultOnToggle): + @property + def result(self) -> bool: + return bool(self.value) + + +class AlwaysServeOldestOrder(OC2OnToggle): + """Modifies the game so that serving an expired order doesn't target the ticket with the highest tip. This helps players dig out of a broken tip combo faster.""" + display_name = "Always Serve Oldest Order" + + +class AlwaysPreserveCookingProgress(OC2OnToggle): + """Modifies the game to behave more like AYCE, where adding an item to an in-progress container doesn't reset the entire progress bar.""" + display_name = "Preserve Cooking/Mixing Progress" + + +class DisplayLeaderboardScores(OC2OnToggle): + """Modifies the Overworld map to fetch and display the current world records for each level. Press number keys 1-4 to view leaderboard scores for that number of players.""" + display_name = "Display Leaderboard Scores" + + +class ShuffleLevelOrder(OC2OnToggle): + """Shuffles the order of kitchens on the overworld map. Also draws from DLC maps.""" + display_name = "Shuffle Level Order" + + +class IncludeHordeLevels(OC2OnToggle): + """Includes "Horde Defence" levels in the pool of possible kitchens when Shuffle Level Order is enabled. Also adds two horde-specific items into the item pool.""" + display_name = "Include Horde Levels" + + +class KevinLevels(OC2OnToggle): + """Includes the 8 Kevin level locations on the map as unlockables. Turn off to make games shorter.""" + display_name = "Kevin Level Checks" + + +class FixBugs(OC2OnToggle): + """Fixes Bugs Present in the base game: + - Double Serving Exploit + - Sink Bug + - Control Stick Cancel/Throw Bug + - Can't Throw Near Empty Burner Bug""" + display_name = "Fix Bugs" + + +class ShorterLevelDuration(OC2OnToggle): + """Modifies level duration to be about 1/3rd shorter than in the original game, thus bringing the item discovery pace in line with other popular Archipelago games. + + Points required to earn stars are scaled accordingly. ("Boss Levels" which change scenery mid-game are not affected.)""" + display_name = "Shorter Level Duration" + + +class PrepLevels(Choice): + """Choose How "Prep Levels" are handled (levels where the timer does not start until the first order is served): + + - Original: Prep Levels may appear + + - Excluded: Prep Levels are excluded from the pool during level shuffling + + - All You Can Eat: Prep Levels may appear, but the timer automatically starts. The star score requirements are also adjusted to use the All You Can Eat World Record (if it exists)""" + auto_display_name = True + display_name = "Prep Level Behavior" + option_original = 0 + option_excluded = 1 + option_all_you_can_eat = 2 + default = 1 + + +class StarsToWin(Range): + """Number of stars required to unlock 6-6. + + Level purchase requirements between 1-1 and 6-6 will be spread between these two numbers. Using too high of a number may result in more frequent generation failures, especially when horde levels are enabled.""" + display_name = "Stars to Win" + range_start = 0 + range_end = 100 + default = 66 + + +class StarThresholdScale(Range): + """How difficult should the third star for each level be on a scale of 1-100%, where 100% is the current world record score and 45% is the average vanilla 4-star score.""" + display_name = "Star Difficulty %" + range_start = 1 + range_end = 100 + default = 45 + + +overcooked_options = { + # randomization options + "shuffle_level_order": ShuffleLevelOrder, + "include_horde_levels": IncludeHordeLevels, + "prep_levels": PrepLevels, + "kevin_levels": KevinLevels, + + # quality of life options + "fix_bugs": FixBugs, + "shorter_level_duration": ShorterLevelDuration, + "always_preserve_cooking_progress": AlwaysPreserveCookingProgress, + "always_serve_oldest_order": AlwaysServeOldestOrder, + "display_leaderboard_scores": DisplayLeaderboardScores, + + # difficulty settings + "stars_to_win": StarsToWin, + "star_threshold_scale": StarThresholdScale, +} + +OC2Options = TypedDict("OC2Options", {option.__name__: option for option in overcooked_options.values()}) diff --git a/worlds/overcooked2/Overcooked2Levels.py b/worlds/overcooked2/Overcooked2Levels.py new file mode 100644 index 0000000000..aac9ea0cbe --- /dev/null +++ b/worlds/overcooked2/Overcooked2Levels.py @@ -0,0 +1,349 @@ +from enum import Enum +from typing import List + + +class Overcooked2Dlc(Enum): + STORY = "Story" + SURF_N_TURF = "Surf 'n' Turf" + CAMPFIRE_COOK_OFF = "Campfire Cook Off" + NIGHT_OF_THE_HANGRY_HORDE = "Night of the Hangry Horde" + CARNIVAL_OF_CHAOS = "Carnival of Chaos" + SEASONAL = "Seasonal" + # CHRISTMAS = "Christmas" + # CHINESE_NEW_YEAR = "Chinese New Year" + # WINTER_WONDERLAND = "Winter Wonderland" + # MOON_HARVEST = "Moon Harvest" + # SPRING_FRESTIVAL = "Spring Festival" + # SUNS_OUT_BUNS_OUT = "Sun's Out Buns Out" + + def __int__(self) -> int: + if self == Overcooked2Dlc.STORY: + return 0 + if self == Overcooked2Dlc.SURF_N_TURF: + return 1 + if self == Overcooked2Dlc.CAMPFIRE_COOK_OFF: + return 2 + if self == Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE: + return 3 + if self == Overcooked2Dlc.CARNIVAL_OF_CHAOS: + return 4 + if self == Overcooked2Dlc.SEASONAL: + return 5 + assert False + + # inclusive + def start_level_id(self) -> int: + if self == Overcooked2Dlc.STORY: + return 1 + return 0 + + # exclusive + def end_level_id(self) -> int: + id = None + if self == Overcooked2Dlc.STORY: + id = 6*6 + 8 # world_count*level_count + kevin count + if self == Overcooked2Dlc.SURF_N_TURF: + id = 3*4 + 1 + if self == Overcooked2Dlc.CAMPFIRE_COOK_OFF: + id = 3*4 + 3 + if self == Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE: + id = 3*3 + 3 + 8 + if self == Overcooked2Dlc.CARNIVAL_OF_CHAOS: + id = 3*4 + 3 + if self == Overcooked2Dlc.SEASONAL: + id = 31 + + return self.start_level_id() + id + + # Tutorial + Horde Levels + Endgame + def excluded_levels(self) -> List[int]: + if self == Overcooked2Dlc.STORY: + return [0, 36] + + return [] + + def horde_levels(self) -> List[int]: + if self == Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE: + return [12, 13, 14, 15, 16, 17, 18, 19] + if self == Overcooked2Dlc.SEASONAL: + return [13, 15] + + return [] + + def prep_levels(self) -> List[int]: + if self == Overcooked2Dlc.STORY: + return [1, 2, 5, 10, 12, 13, 28, 31] + if self == Overcooked2Dlc.SURF_N_TURF: + return [0, 4] + if self == Overcooked2Dlc.CAMPFIRE_COOK_OFF: + return [0, 2, 4, 9] + if self == Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE: + return [0, 1, 4] + if self == Overcooked2Dlc.CARNIVAL_OF_CHAOS: + return [0, 1, 3, 4, 5] + if self == Overcooked2Dlc.SEASONAL: + # moon 1-1 is a prep level for 1P only, but we can't make that assumption here + return [0, 1, 5, 6, 12, 14, 16, 17, 18, 22, 23, 24, 27, 29] + + return [] + + +class Overcooked2GameWorld(Enum): + ONE = 1 + TWO = 2 + THREE = 3 + FOUR = 4 + FIVE = 5 + SIX = 6 + KEVIN = 7 + + @property + def as_str(self) -> str: + if self == Overcooked2GameWorld.KEVIN: + return "Kevin" + + return str(int(self.value)) + + @property + def sublevel_count(self) -> int: + if self == Overcooked2GameWorld.KEVIN: + return 8 + + return 6 + + @property + def base_id(self) -> int: + if self == Overcooked2GameWorld.ONE: + return 1 + + prev = Overcooked2GameWorld(self.value - 1) + return prev.base_id + prev.sublevel_count + + @property + def name(self) -> str: + if self == Overcooked2GameWorld.KEVIN: + return "Kevin" + + return "World " + self.as_str + + +class Overcooked2GenericLevel(): + dlc: Overcooked2Dlc + level_id: int + + def __init__(self, level_id: int, dlc: Overcooked2Dlc = Overcooked2Dlc("Story")): + self.dlc = dlc + self.level_id = level_id + + def __str__(self) -> str: + return f"{self.dlc.value}|{self.level_id}" + + def __repr__(self) -> str: + return f"{self}" + + @property + def shortname(self) -> str: + return level_id_to_shortname[(self.dlc, self.level_id)] + + @property + def is_horde(self) -> bool: + return self.level_id in self.dlc.horde_levels() + + +class Overcooked2Level: + """ + Abstraction for a playable levels in Overcooked 2. By default constructor + it can be used as an iterator for all locations in the Story map. + """ + world: Overcooked2GameWorld + sublevel: int + + def __init__(self): + self.world = Overcooked2GameWorld.ONE + self.sublevel = 0 + + def __iter__(self): + return self + + def __next__(self): + self.sublevel += 1 + if self.sublevel > self.world.sublevel_count: + if self.world == Overcooked2GameWorld.KEVIN: + raise StopIteration + self.world = Overcooked2GameWorld(self.world.value + 1) + self.sublevel = 1 + + return self + + @property + def level_id(self) -> int: + return self.world.base_id + (self.sublevel - 1) + + @property + def level_name(self) -> str: + return self.world.as_str + "-" + str(self.sublevel) + + @property + def location_name_item(self) -> str: + return self.level_name + " Completed" + + @property + def location_name_level_complete(self) -> str: + return self.level_name + " Level Completed" + + @property + def event_name_level_complete(self) -> str: + return self.level_name + " Level Complete" + + def location_name_star_event(self, stars: int) -> str: + return "%s (%d-Star)" % (self.level_name, stars) + + @property + def as_generic_level(self) -> Overcooked2GenericLevel: + return Overcooked2GenericLevel(self.level_id) + + +# Note that there are valid levels beyond what is listed here, but they are all +# Onion King Dialogs +level_id_to_shortname = { + (Overcooked2Dlc.STORY , 0 ): "Tutorial" , + (Overcooked2Dlc.STORY , 1 ): "Story 1-1" , + (Overcooked2Dlc.STORY , 2 ): "Story 1-2" , + (Overcooked2Dlc.STORY , 3 ): "Story 1-3" , + (Overcooked2Dlc.STORY , 4 ): "Story 1-4" , + (Overcooked2Dlc.STORY , 5 ): "Story 1-5" , + (Overcooked2Dlc.STORY , 6 ): "Story 1-6" , + (Overcooked2Dlc.STORY , 7 ): "Story 2-1" , + (Overcooked2Dlc.STORY , 8 ): "Story 2-2" , + (Overcooked2Dlc.STORY , 9 ): "Story 2-3" , + (Overcooked2Dlc.STORY , 10 ): "Story 2-4" , + (Overcooked2Dlc.STORY , 11 ): "Story 2-5" , + (Overcooked2Dlc.STORY , 12 ): "Story 2-6" , + (Overcooked2Dlc.STORY , 13 ): "Story 3-1" , + (Overcooked2Dlc.STORY , 14 ): "Story 3-2" , + (Overcooked2Dlc.STORY , 15 ): "Story 3-3" , + (Overcooked2Dlc.STORY , 16 ): "Story 3-4" , + (Overcooked2Dlc.STORY , 17 ): "Story 3-5" , + (Overcooked2Dlc.STORY , 18 ): "Story 3-6" , + (Overcooked2Dlc.STORY , 19 ): "Story 4-1" , + (Overcooked2Dlc.STORY , 20 ): "Story 4-2" , + (Overcooked2Dlc.STORY , 21 ): "Story 4-3" , + (Overcooked2Dlc.STORY , 22 ): "Story 4-4" , + (Overcooked2Dlc.STORY , 23 ): "Story 4-5" , + (Overcooked2Dlc.STORY , 24 ): "Story 4-6" , + (Overcooked2Dlc.STORY , 25 ): "Story 5-1" , + (Overcooked2Dlc.STORY , 26 ): "Story 5-2" , + (Overcooked2Dlc.STORY , 27 ): "Story 5-3" , + (Overcooked2Dlc.STORY , 28 ): "Story 5-4" , + (Overcooked2Dlc.STORY , 29 ): "Story 5-5" , + (Overcooked2Dlc.STORY , 30 ): "Story 5-6" , + (Overcooked2Dlc.STORY , 31 ): "Story 6-1" , + (Overcooked2Dlc.STORY , 32 ): "Story 6-2" , + (Overcooked2Dlc.STORY , 33 ): "Story 6-3" , + (Overcooked2Dlc.STORY , 34 ): "Story 6-4" , + (Overcooked2Dlc.STORY , 35 ): "Story 6-5" , + (Overcooked2Dlc.STORY , 36 ): "Story 6-6" , + (Overcooked2Dlc.STORY , 37 ): "Story K-1" , + (Overcooked2Dlc.STORY , 38 ): "Story K-2" , + (Overcooked2Dlc.STORY , 39 ): "Story K-3" , + (Overcooked2Dlc.STORY , 40 ): "Story K-4" , + (Overcooked2Dlc.STORY , 41 ): "Story K-5" , + (Overcooked2Dlc.STORY , 42 ): "Story K-6" , + (Overcooked2Dlc.STORY , 43 ): "Story K-7" , + (Overcooked2Dlc.STORY , 44 ): "Story K-8" , + (Overcooked2Dlc.SURF_N_TURF , 0 ): "Surf 1-1" , + (Overcooked2Dlc.SURF_N_TURF , 1 ): "Surf 1-2" , + (Overcooked2Dlc.SURF_N_TURF , 2 ): "Surf 1-3" , + (Overcooked2Dlc.SURF_N_TURF , 3 ): "Surf 1-4" , + (Overcooked2Dlc.SURF_N_TURF , 4 ): "Surf 2-1" , + (Overcooked2Dlc.SURF_N_TURF , 5 ): "Surf 2-2" , + (Overcooked2Dlc.SURF_N_TURF , 6 ): "Surf 2-3" , + (Overcooked2Dlc.SURF_N_TURF , 7 ): "Surf 2-4" , + (Overcooked2Dlc.SURF_N_TURF , 8 ): "Surf 3-1" , + (Overcooked2Dlc.SURF_N_TURF , 9 ): "Surf 3-2" , + (Overcooked2Dlc.SURF_N_TURF , 10 ): "Surf 3-3" , + (Overcooked2Dlc.SURF_N_TURF , 11 ): "Surf 3-4" , + (Overcooked2Dlc.SURF_N_TURF , 12 ): "Surf K-1" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 0 ): "Campfire 1-1" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 1 ): "Campfire 1-2" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 2 ): "Campfire 1-3" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 3 ): "Campfire 1-4" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 4 ): "Campfire 2-1" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 5 ): "Campfire 2-2" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 6 ): "Campfire 2-3" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 7 ): "Campfire 2-4" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 8 ): "Campfire 3-1" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 9 ): "Campfire 3-2" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 10 ): "Campfire 3-3" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 11 ): "Campfire 3-4" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 12 ): "Campfire K-1" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 13 ): "Campfire K-2" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 14 ): "Campfire K-3" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 0 ): "Carnival 1-1" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 1 ): "Carnival 1-2" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 2 ): "Carnival 1-3" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 3 ): "Carnival 1-4" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 4 ): "Carnival 2-1" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 5 ): "Carnival 2-2" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 6 ): "Carnival 2-3" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 7 ): "Carnival 2-4" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 8 ): "Carnival 3-1" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 9 ): "Carnival 3-2" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 10 ): "Carnival 3-3" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 11 ): "Carnival 3-4" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 12 ): "Carnival K-1" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 13 ): "Carnival K-2" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 14 ): "Carnival K-3" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 0 ): "Horde 1-1" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 1 ): "Horde 1-2" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 2 ): "Horde 1-3" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 3 ): "Horde 2-1" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 4 ): "Horde 2-2" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 5 ): "Horde 2-3" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 6 ): "Horde 3-1" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 7 ): "Horde 3-2" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 8 ): "Horde 3-3" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 9 ): "Horde K-1" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 10 ): "Horde K-2" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 11 ): "Horde K-3" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 12 ): "Horde H-1" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 13 ): "Horde H-2" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 14 ): "Horde H-3" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 15 ): "Horde H-4" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 16 ): "Horde H-5" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 17 ): "Horde H-6" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 18 ): "Horde H-7" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 19 ): "Horde H-8" , + (Overcooked2Dlc.SEASONAL , 0 ): "Christmas 1-1" , + (Overcooked2Dlc.SEASONAL , 1 ): "Christmas 1-2" , + (Overcooked2Dlc.SEASONAL , 2 ): "Christmas 1-3" , + (Overcooked2Dlc.SEASONAL , 3 ): "Christmas 1-4" , + (Overcooked2Dlc.SEASONAL , 4 ): "Christmas 1-5" , + (Overcooked2Dlc.SEASONAL , 5 ): "Chinese 1-1" , + (Overcooked2Dlc.SEASONAL , 6 ): "Chinese 1-2" , + (Overcooked2Dlc.SEASONAL , 7 ): "Chinese 1-3" , + (Overcooked2Dlc.SEASONAL , 8 ): "Chinese 1-4" , + (Overcooked2Dlc.SEASONAL , 9 ): "Chinese 1-5" , + (Overcooked2Dlc.SEASONAL , 10 ): "Chinese 1-6" , + (Overcooked2Dlc.SEASONAL , 11 ): "Chinese 1-7" , + (Overcooked2Dlc.SEASONAL , 12 ): "Winter 1-1" , + (Overcooked2Dlc.SEASONAL , 13 ): "Winter H-2" , + (Overcooked2Dlc.SEASONAL , 14 ): "Winter 1-3" , + (Overcooked2Dlc.SEASONAL , 15 ): "Winter H-4" , + (Overcooked2Dlc.SEASONAL , 16 ): "Winter 1-5" , + (Overcooked2Dlc.SEASONAL , 17 ): "Spring 1-1" , + (Overcooked2Dlc.SEASONAL , 18 ): "Spring 1-2" , + (Overcooked2Dlc.SEASONAL , 19 ): "Spring 1-3" , + (Overcooked2Dlc.SEASONAL , 20 ): "Spring 1-4" , + (Overcooked2Dlc.SEASONAL , 21 ): "Spring 1-5" , + (Overcooked2Dlc.SEASONAL , 22 ): "SOBO 1-1" , + (Overcooked2Dlc.SEASONAL , 23 ): "SOBO 1-2" , + (Overcooked2Dlc.SEASONAL , 24 ): "SOBO 1-3" , + (Overcooked2Dlc.SEASONAL , 25 ): "SOBO 1-4" , + (Overcooked2Dlc.SEASONAL , 26 ): "SOBO 1-5" , + (Overcooked2Dlc.SEASONAL , 27 ): "Moon 1-1" , + (Overcooked2Dlc.SEASONAL , 28 ): "Moon 1-2" , + (Overcooked2Dlc.SEASONAL , 29 ): "Moon 1-3" , + (Overcooked2Dlc.SEASONAL , 30 ): "Moon 1-4" , + (Overcooked2Dlc.SEASONAL , 31 ): "Moon 1-5" , +} diff --git a/worlds/overcooked2/__init__.py b/worlds/overcooked2/__init__.py new file mode 100644 index 0000000000..c47b755fbf --- /dev/null +++ b/worlds/overcooked2/__init__.py @@ -0,0 +1,510 @@ +from enum import Enum +from typing import Callable, Dict, Any, List, Optional + +from BaseClasses import ItemClassification, CollectionState, Region, Entrance, Location, RegionType, Tutorial +from worlds.AutoWorld import World, WebWorld + +from .Overcooked2Levels import Overcooked2Level, Overcooked2GenericLevel +from .Locations import Overcooked2Location, oc2_location_name_to_id, oc2_location_id_to_name +from .Options import overcooked_options, OC2Options, OC2OnToggle +from .Items import item_table, Overcooked2Item, item_name_to_id, item_id_to_name, item_to_unlock_event, item_frequencies +from .Logic import has_requirements_for_level_star, has_requirements_for_level_access, level_shuffle_factory, is_item_progression, is_useful + + +class Overcooked2Web(WebWorld): + theme = "partyTime" + + bug_report_page = "https://github.com/toasterparty/oc2-modding/issues" + setup_en = Tutorial( + "Multiworld Setup Tutorial", + "A guide to setting up the Overcooked! 2 randomizer on your computer.", + "English", + "setup_en.md", + "setup/en", + ["toasterparty"] + ) + + tutorials = [setup_en] + + +class PrepLevelMode(Enum): + original = 0 + excluded = 1 + ayce = 2 + + +class Overcooked2World(World): + """ + Overcooked! 2 is a franticly paced arcade cooking game where + players race against the clock to complete orders for points. Bring + peace to the Onion Kingdom once again by recovering lost items and abilities, + earning stars to unlock levels, and defeating the unbread horde. Levels are + randomized to increase gameplay variety. Play with up to 4 friends. + """ + + # Autoworld API + + game = "Overcooked! 2" + web = Overcooked2Web() + required_client_version = (0, 3, 4) + option_definitions = overcooked_options + topology_present: bool = False + remote_items: bool = True + remote_start_inventory: bool = False + data_version = 2 + + item_name_to_id = item_name_to_id + item_id_to_name = item_id_to_name + + location_id_to_name = oc2_location_id_to_name + location_name_to_id = oc2_location_name_to_id + + options: Dict[str, Any] + itempool: List[Overcooked2Item] + + + # Helper Functions + + def is_level_horde(self, level_id: int) -> bool: + return self.options["IncludeHordeLevels"] and \ + (self.level_mapping is not None) and \ + level_id in self.level_mapping.keys() and \ + self.level_mapping[level_id].is_horde + + def create_item(self, item: str, classification: ItemClassification = ItemClassification.progression) -> Overcooked2Item: + return Overcooked2Item(item, classification, self.item_name_to_id[item], self.player) + + def create_event(self, event: str, classification: ItemClassification) -> Overcooked2Item: + return Overcooked2Item(event, classification, None, self.player) + + def place_event(self, location_name: str, item_name: str, + classification: ItemClassification = ItemClassification.progression_skip_balancing): + location: Location = self.world.get_location(location_name, self.player) + location.place_locked_item(self.create_event(item_name, classification)) + + def add_region(self, region_name: str): + region = Region( + region_name, + RegionType.Generic, + region_name, + self.player, + self.world, + ) + self.world.regions.append(region) + + def connect_regions(self, source: str, target: str, rule: Optional[Callable[[CollectionState], bool]] = None): + sourceRegion = self.world.get_region(source, self.player) + targetRegion = self.world.get_region(target, self.player) + + connection = Entrance(self.player, '', sourceRegion) + if rule: + connection.access_rule = rule + + sourceRegion.exits.append(connection) + connection.connect(targetRegion) + + def add_level_location( + self, + region_name: str, + location_name: str, + level_id: int, + stars: int, + is_event: bool = False, + ) -> None: + + if is_event: + location_id = None + else: + location_id = level_id + + region = self.world.get_region(region_name, self.player) + location = Overcooked2Location( + self.player, + location_name, + location_id, + region, + ) + + location.event = is_event + + # if level_id is none, then it's the 6-6 edge case + if level_id is None: + level_id = 36 + if self.level_mapping is not None and level_id in self.level_mapping: + level = self.level_mapping[level_id] + else: + level = Overcooked2GenericLevel(level_id) + + completion_condition: Callable[[CollectionState], bool] = \ + lambda state, level=level, stars=stars: \ + has_requirements_for_level_star(state, level, stars, self.player) + location.access_rule = completion_condition + + region.locations.append( + location + ) + + def get_options(self) -> Dict[str, Any]: + return OC2Options({option.__name__: getattr(self.world, name)[self.player].result + if issubclass(option, OC2OnToggle) else getattr(self.world, name)[self.player].value + for name, option in overcooked_options.items()}) + + # Helper Data + + level_unlock_counts: Dict[int, int] # level_id, stars to purchase + level_mapping: Dict[int, Overcooked2GenericLevel] # level_id, level + + # Autoworld Hooks + + def generate_early(self): + self.options = self.get_options() + + # 0.0 to 1.0 where 1.0 is World Record + self.star_threshold_scale = self.options["StarThresholdScale"] / 100.0 + + # Generate level unlock requirements such that the levels get harder to unlock + # the further the game has progressed, and levels progress radially rather than linearly + self.level_unlock_counts = level_unlock_requirement_factory(self.options["StarsToWin"]) + + # Assign new kitchens to each spot on the overworld using pure random chance and nothing else + if self.options["ShuffleLevelOrder"]: + self.level_mapping = \ + level_shuffle_factory( + self.world.random, + self.options["PrepLevels"] != PrepLevelMode.excluded.value, + self.options["IncludeHordeLevels"], + ) + else: + self.level_mapping = None + + def create_regions(self) -> None: + # Menu -> Overworld + self.add_region("Menu") + self.add_region("Overworld") + self.connect_regions("Menu", "Overworld") + + for level in Overcooked2Level(): + if not self.options["KevinLevels"] and level.level_id > 36: + break + + # Create Region (e.g. "1-1") + self.add_region(level.level_name) + + # Add Location to house progression item (1-star) + if level.level_id == 36: + # 6-6 doesn't have progression, but it does have victory condition which is placed later + self.add_level_location( + level.level_name, + level.location_name_item, + None, + 1, + ) + else: + # Location to house progression item + self.add_level_location( + level.level_name, + level.location_name_item, + level.level_id, + 1, + ) + + # Location to house level completed event + self.add_level_location( + level.level_name, + level.location_name_level_complete, + level.level_id, + 1, + is_event=True, + ) + + # Add Locations to house star aquisition events, except for horde levels + if not self.is_level_horde(level.level_id): + for n in [1, 2, 3]: + self.add_level_location( + level.level_name, + level.location_name_star_event(n), + level.level_id, + n, + is_event=True, + ) + + # Overworld -> Level + required_star_count: int = self.level_unlock_counts[level.level_id] + if level.level_id % 6 != 1 and level.level_id <= 36: + previous_level_completed_event_name: str = Overcooked2GenericLevel( + level.level_id - 1).shortname.split(" ")[1] + " Level Complete" + else: + previous_level_completed_event_name = None + + level_access_rule: Callable[[CollectionState], bool] = \ + lambda state, level_name=level.level_name, previous_level_completed_event_name=previous_level_completed_event_name, required_star_count=required_star_count: \ + has_requirements_for_level_access(state, level_name, previous_level_completed_event_name, required_star_count, self.player) + self.connect_regions("Overworld", level.level_name, level_access_rule) + + # Level --> Overworld + self.connect_regions(level.level_name, "Overworld") + + completion_condition: Callable[[CollectionState], bool] = lambda state: \ + state.has("Victory", self.player) + self.world.completion_condition[self.player] = completion_condition + + def create_items(self): + self.itempool = [] + + # Make Items + # useful = list() + # filler = list() + # progression = list() + for item_name in item_table: + if not self.options["IncludeHordeLevels"] and item_name in ["Calmer Unbread", "Coin Purse"]: + # skip items which are irrelevant to the seed + continue + + if not self.options["KevinLevels"] and item_name.startswith("Kevin"): + continue + + if is_item_progression(item_name, self.level_mapping, self.options["KevinLevels"]): + # print(f"{item_name} is progression") + # progression.append(item_name) + classification = ItemClassification.progression + else: + # print(f"{item_name} is filler") + if (is_useful(item_name)): + # useful.append(item_name) + classification = ItemClassification.useful + else: + # filler.append(item_name) + classification = ItemClassification.filler + + if item_name in item_frequencies: + freq = item_frequencies[item_name] + + while freq > 0: + self.itempool.append(self.create_item(item_name, classification)) + classification = ItemClassification.useful # only the first progressive item can be progression + freq -= 1 + else: + self.itempool.append(self.create_item(item_name, classification)) + + # print(f"progression: {progression}") + # print(f"useful: {useful}") + # print(f"filler: {filler}") + + # Fill any free space with filler + pool_count = len(oc2_location_name_to_id) + if not self.options["KevinLevels"]: + pool_count -= 8 + + while len(self.itempool) < pool_count: + self.itempool.append(self.create_item("Bonus Star", ItemClassification.useful)) + + self.world.itempool += self.itempool + + def set_rules(self): + pass + + def generate_basic(self) -> None: + # Add Events (Star Acquisition) + for level in Overcooked2Level(): + if not self.options["KevinLevels"] and level.level_id > 36: + break + + if level.level_id != 36: + self.place_event(level.location_name_level_complete, level.event_name_level_complete) + + if self.is_level_horde(level.level_id): + continue # horde levels don't have star rewards + + for n in [1, 2, 3]: + self.place_event(level.location_name_star_event(n), "Star") + + # Add Victory Condition + self.place_event("6-6 Completed", "Victory") + + # Items get distributed to locations + + def fill_json_data(self) -> Dict[str, Any]: + mod_name = f"AP-{self.world.seed_name}-P{self.player}-{self.world.player_name[self.player]}" + + # Serialize Level Order + story_level_order = dict() + + if self.options["ShuffleLevelOrder"]: + for level_id in self.level_mapping: + level: Overcooked2GenericLevel = self.level_mapping[level_id] + story_level_order[str(level_id)] = { + "DLC": level.dlc.value, + "LevelID": level.level_id, + } + + custom_level_order = dict() + custom_level_order["Story"] = story_level_order + + # Serialize Unlock Requirements + level_purchase_requirements = dict() + for level_id in self.level_unlock_counts: + level_purchase_requirements[str(level_id)] = self.level_unlock_counts[level_id] + + # Override Vanilla Unlock Chain Behavior + # (all worlds accessible from the start and progressible in any order) + level_unlock_requirements = dict() + level_force_reveal = [ + 1, # 1-1 + 7, # 2-1 + 13, # 3-1 + 19, # 4-1 + 25, # 5-1 + 31, # 6-1 + ] + for level_id in range(1, 37): + if (level_id not in level_force_reveal): + level_unlock_requirements[str(level_id)] = level_id - 1 + + # Set Kevin Unlock Requirements + if self.options["KevinLevels"]: + def kevin_level_to_keyholder_level_id(level_id: int) -> Optional[int]: + location = self.world.find_item(f"Kevin-{level_id-36}", self.player) + if location.player != self.player: + return None # This kevin level will be unlocked by the server at runtime + level_id = oc2_location_name_to_id[location.name] + return level_id + + for level_id in range(37, 45): + keyholder_level_id = kevin_level_to_keyholder_level_id(level_id) + if keyholder_level_id is not None: + level_unlock_requirements[str(level_id)] = keyholder_level_id + + # Place Items at Level Completion Screens (local only) + on_level_completed: Dict[str, list[Dict[str, str]]] = dict() + regions = self.world.get_regions(self.player) + for region in regions: + for location in region.locations: + if location.item is None: + continue + if location.item.code is None: + continue # it's an event + if location.item.player != self.player: + continue # not for us + level_id = str(oc2_location_name_to_id[location.name]) + on_level_completed[level_id] = [item_to_unlock_event(location.item.name)] + + # Put it all together + star_threshold_scale = self.options["StarThresholdScale"] / 100 + + base_data = { + # Changes Inherent to rando + "DisableAllMods": False, + "UnlockAllChefs": True, + "UnlockAllDLC": True, + "DisplayFPS": True, + "SkipTutorial": True, + "SkipAllOnionKing": True, + "SkipTutorialPopups": True, + "RevealAllLevels": False, + "PurchaseAllLevels": False, + "CheatsEnabled": False, + "ImpossibleTutorial": True, + "ForbidDLC": True, + "ForceSingleSaveSlot": True, + "DisableNGP": True, + "LevelForceReveal": level_force_reveal, + "SaveFolderName": mod_name, + "CustomOrderTimeoutPenalty": 10, + "LevelForceHide": [37, 38, 39, 40, 41, 42, 43, 44], + + # Game Modifications + "LevelPurchaseRequirements": level_purchase_requirements, + "Custom66TimerScale": max(0.4, (1.0 - star_threshold_scale)), + + "CustomLevelOrder": custom_level_order, + + # Items (Starting Inventory) + "CustomOrderLifetime": 70.0, # 100 is original + "DisableWood": True, + "DisableCoal": True, + "DisableOnePlate": True, + "DisableFireExtinguisher": True, + "DisableBellows": True, + "PlatesStartDirty": True, + "MaxTipCombo": 2, + "DisableDash": True, + "WeakDash": True, + "DisableThrow": True, + "DisableCatch": True, + "DisableControlStick": True, + "DisableWokDrag": True, + "DisableRampButton": True, + "WashTimeMultiplier": 1.4, + "BurnSpeedMultiplier": 1.43, + "MaxOrdersOnScreenOffset": -2, + "ChoppingTimeScale": 1.4, + "BackpackMovementScale": 0.75, + "RespawnTime": 10.0, + "CarnivalDispenserRefactoryTime": 4.0, + "LevelUnlockRequirements": level_unlock_requirements, + "LockedEmotes": [0, 1, 2, 3, 4, 5], + "StarOffset": 0, + "AggressiveHorde": True, + "DisableEarnHordeMoney": True, + + # Item Unlocking + "OnLevelCompleted": on_level_completed, + } + + # Set remaining data in the options dict + bugs = ["FixDoubleServing", "FixSinkBug", "FixControlStickThrowBug", "FixEmptyBurnerThrow"] + for bug in bugs: + self.options[bug] = self.options["FixBugs"] + self.options["PreserveCookingProgress"] = self.options["AlwaysPreserveCookingProgress"] + self.options["TimerAlwaysStarts"] = self.options["PrepLevels"] == PrepLevelMode.ayce.value + self.options["LevelTimerScale"] = 0.666 if self.options["ShorterLevelDuration"] else 1.0 + self.options["LeaderboardScoreScale"] = { + "FourStars": 1.0, + "ThreeStars": star_threshold_scale, + "TwoStars": star_threshold_scale * 0.75, + "OneStar": star_threshold_scale * 0.35, + } + + base_data.update(self.options) + return base_data + + def fill_slot_data(self) -> Dict[str, Any]: + return self.fill_json_data() + + +def level_unlock_requirement_factory(stars_to_win: int) -> Dict[int, int]: + level_unlock_counts = dict() + level = 1 + sublevel = 1 + for n in range(1, 37): + progress: float = float(n)/36.0 + progress *= progress # x^2 curve + + star_count = int(progress*float(stars_to_win)) + min = (n-1)*3 + if (star_count > min): + star_count = min + + level_id = (level-1)*6 + sublevel + + # print("%d-%d (%d) = %d" % (level, sublevel, level_id, star_count)) + + level_unlock_counts[level_id] = star_count + + level += 1 + if level > 6: + level = 1 + sublevel += 1 + + # force sphere 1 to 0 stars to help keep our promises to the item fill algo + level_unlock_counts[1] = 0 # 1-1 + level_unlock_counts[7] = 0 # 2-1 + level_unlock_counts[19] = 0 # 4-1 + + # Force 5-1 into sphere 1 to help things out + level_unlock_counts[25] = 1 # 5-1 + + for n in range(37, 46): + level_unlock_counts[n] = 0 + + return level_unlock_counts diff --git a/worlds/overcooked2/docs/en_Overcooked! 2.md b/worlds/overcooked2/docs/en_Overcooked! 2.md new file mode 100644 index 0000000000..d6de25f3e9 --- /dev/null +++ b/worlds/overcooked2/docs/en_Overcooked! 2.md @@ -0,0 +1,86 @@ +# Overcooked! 2 + +## Quick Links +- [Setup Guide](../../../../tutorial/Overcooked!%202/setup/en) +- [Settings Page](../../../../games/Overcooked!%202/player-settings) +- [OC2-Modding GitHub](https://github.com/toasterparty/oc2-modding) + +## How Does Randomizer Work in the Kitchen? + +The *Overcooked! 2* Randomizer completely transforms the game into a metroidvania with items and item locations. Many of the Chefs' inherent abilities have been temporarily removed such that your scoring potential is limited at the start of the game. The more your inventory grows, the easier it will be to earn 2 and 3 Stars on each level. + +The game takes place entirely in the "Story" campaign on a fresh save file. The ultimate goal is to reach and complete level 6-6. In order to do this you must regain enough of your abilities to complete all levels in World 6 and obtain enough stars to purchase 6-6*. + +Randomizer can be played alone (one player switches between controlling two chefs) or up to 4 local/online friends. Player count can be changed at any time during the Archipelago game. + +**Note: 6-6 is excluded from "Shuffle Level Order", so it will always be the standard final boss stage.* + +## Items + +The first time a level is completed, a random item is given to the chef(s). If playing in a MultiWorld, completing a level may instead give another Archipelago user their item. The item found is displayed as text at the top of the results screen. + +Once all items have been obtained, the game will play like the original experience. + +The following items were invented for Randomizer: + +### Player Abilities +- Dash/Dash Cooldown +- Throw/Catch +- Sharp Knife +- Dish Scrubber +- Control Stick Batteries +- Lightweight Backpack +- Faster Respawn Time +- Emote (x6) + +### Objects +- Spare Plate +- Clean Dishes +- Wood +- Coal Bucket +- Bellows +- Fire Extinguisher + +### Kitchen/Environment +- Larger Tip Jar +- Guest Patience +- Burn Leniency +- Faster Condiment & Drink Switch +- Wok Wheels +- Coin Purse +- Calmer Unbread + +### Overworld +- Unlock Kevin Level (x8) +- Ramp Button +- Bonus Star (Filler Item*) + +**Note: Bonus star count varies with settings* + +## Other Game Modifications + +In addition to shuffling items, the following changes are applied to the game: + +### Quality of Life +- Tutorial is skipped +- Non-linear level order +- "Auto-Complete" feature to finish a level early when a target score is obtained +- Bugfixes for issues present in the base game (including "Sink Bug" and "Double Serving") +- All chef avatars automatically unlocked +- Optionally, level time can be reduced to make progression faster paced + +### Randomization Options + +- *Shuffle Level Order* + - Replaces each level on the overworld with a random level + - DLC levels can show up on the Story Overworld + - Optionally exclude "Horde" Levels + - Optionally exclude "Prep" Levels + +### Difficulty Adjustments +- Stars required to unlock levels have been rebalanced +- Points required to earn stars have been rebalanced + - Based off of the current World Record on the game's [Leaderboard](https://overcooked.greeny.dev) + - 1-Star/2-Star scores are much closer to the 3-Star Score +- Significantly reduced the time allotted to beat the final level +- Reduced penalty for expired order diff --git a/worlds/overcooked2/docs/setup_en.md b/worlds/overcooked2/docs/setup_en.md new file mode 100644 index 0000000000..d724f02f7f --- /dev/null +++ b/worlds/overcooked2/docs/setup_en.md @@ -0,0 +1,84 @@ +# Overcooked! 2 Randomizer Setup Guide + +## Quick Links +- [Main Page](../../../../games/Overcooked!%202/info/en) +- [Settings Page](../../../../games/Overcooked!%202/player-settings) +- [OC2-Modding GitHub](https://github.com/toasterparty/oc2-modding) + +## Required Software + +- Windows 10+ +- [Overcooked! 2](https://store.steampowered.com/bundle/13608/Overcooked_2___Gourmet_Edition/) for PC + - **Steam: Recommended** + - Steam (Beta Branch): Supported + - Epic Games: Supported + - GOG: Not officially supported - Adventurous users may choose to experiment at their own risk + - Windows Store (aka GamePass): Not Supported + - Xbox/PS/Switch: Not Supported +- [OC2-Modding Client](https://github.com/toasterparty/oc2-modding/releases) (instructions below) + +## Overview + +*OC2-Modding* is a general purpose modding framework which doubles as an Archipelago MultiWorld Client. It works by using Harmony to inject custom code into the game at runtime, so none of the orignal game files need to be modified in any way. + +When connecting to an Archipelago session using the in-game login screen, a modfile containing all relevant game modifications is automatically downloaded and applied. + +From this point, the game will communicate with the Archipelago service directly to manage sending/receiving items. Notifications of important events will appear through an in-game console at the top of the screen. + +## Overcooked! 2 Modding Guide + +### Install + +1. Download and extract the contents of the latest [OC2-Modding Release](https://github.com/toasterparty/oc2-modding/releases) anywhere on your PC + +2. Double-Click **oc2-modding-install.bat** follow the instructions. + +Once *OC2-Modding* is installed, you have successfully installed everything you need to play/participate in Archipelago MultiWorld games. + +### Disable + +To temporarily turn off *OC2-Modding* and return to the original game, open **...\Overcooked! 2\BepInEx\config\OC2Modding.cfg** in a text editor like notepad and edit the following: + +`DisableAllMods = true` + +To re-enable, simply change the word **true** back to a **false**. + +### Uninstall + +To completely remove *OC2-Modding*, navigate to your game's installation folder and run **oc2-modding-uninstall.bat**. + +## Generate a MultiWorld Game + +1. Visit the [Player Settings](../../../../games/Overcooked!%202/player-settings) page and configure the game-specific settings to taste + +2. Export your yaml file and use it to generate a new randomized game +- (For instructions on how to generate an Archipelago game, refer to the [Archipelago Web Guide](../../../../tutorial/Archipelago/using_website/en)) + +## Joining a MultiWorld Game + +1. Launch the game + +2. When attempting to enter the main menu from the title screen, the game will freeze and prompt you to sign in: + +![Sign-In Screen](https://i.imgur.com/goMy7o2.png) + +3. Sign-in with server address, username and password of the corresponding room you would like to join. +- Otherwise, if you just want to play the vanilla game without any modifications, you may press "Continue without Archipelago" button. + +4. Upon successful connection to the Archipelago service, you will be granted access to the main menu. The game will act as though you are playing for the first time. ***DO NOT FEAR*** — your original save data has not been overwritten; the Overcooked Randomizer just uses a temporary directory for it's save game data. + +## Playing Co-Op + +- To play local multiplayer (or Parsec/"Steam Play Together"), simply add the additional player to your game session as you would in the base game + +- To play online multiplayer, the guest *must* also have the same version of OC2-Modding installed. In order for the game to work, the guest must sign in using the same information the host used to connect to the Archipelago session. Once both host and client are both connected, they may join one another in-game and proceed as normal. It does not matter who hosts the game, and the game's hosts may be changed at any point. You may notice some things are different when playing this way: + + - Guests will still receive Archipelago messages about sent/received items the same as the host + + - When the host loads the campaign, any connected guests are forced to select "Don't Save" when prompted to pick which save slot to use. This is because randomizer uses the Archipelago service as a pseudo "cloud save", so progress will always be synchronized between all participants of that randomized *Overcooked! 2* instance. + +## Auto-Complete + +Since the goal of randomizer isn't necessarily to achieve new personal high scores, players may find themselves waiting for a level timer to expire once they've met their objective. A new feature called *Auto-Complete* has been added to automatically complete levels once a target star count has been achieved. + +To enable *Auto-Complete*, press the **Show** button near the top of your screen to expand the modding controls. Then, repeatedly press the **Auto-Complete** button until it shows the desired setting. diff --git a/worlds/pokemon_rb/LICENSE b/worlds/pokemon_rb/LICENSE new file mode 100644 index 0000000000..0dc1b2dcd0 --- /dev/null +++ b/worlds/pokemon_rb/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Alex "Alchav" Avery + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py new file mode 100644 index 0000000000..4c37db8450 --- /dev/null +++ b/worlds/pokemon_rb/__init__.py @@ -0,0 +1,252 @@ +from typing import TextIO +import os +import logging + +from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification +from Fill import fill_restrictive, FillError, sweep_from_pool +from ..AutoWorld import World, WebWorld +from ..generic.Rules import add_item_rule +from .items import item_table, item_groups +from .locations import location_data, PokemonRBLocation +from .regions import create_regions +from .logic import PokemonLogic +from .options import pokemon_rb_options +from .rom_addresses import rom_addresses +from .text import encode_text +from .rom import generate_output, get_base_rom_bytes, get_base_rom_path, process_pokemon_data, process_wild_pokemon,\ + process_static_pokemon +from .rules import set_rules + +import worlds.pokemon_rb.poke_data as poke_data + + +class PokemonWebWorld(WebWorld): + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to playing Pokemon Red and Blue with Archipelago.", + "English", + "setup_en.md", + "setup/en", + ["Alchav"] + )] + + +class PokemonRedBlueWorld(World): + """Pokémon Red and Pokémon Blue are the original monster-collecting turn-based RPGs. Explore the Kanto region with + your Pokémon, catch more than 150 unique creatures, earn badges from the region's Gym Leaders, and challenge the + Elite Four to become the champion!""" + # -MuffinJets#4559 + game = "Pokemon Red and Blue" + option_definitions = pokemon_rb_options + remote_items = False + data_version = 1 + topology_present = False + + item_name_to_id = {name: data.id for name, data in item_table.items()} + location_name_to_id = {location.name: location.address for location in location_data if location.type == "Item"} + item_name_groups = item_groups + + web = PokemonWebWorld() + + def __init__(self, world: MultiWorld, player: int): + super().__init__(world, player) + self.fly_map = None + self.fly_map_code = None + self.extra_badges = {} + self.type_chart = None + self.local_poke_data = None + self.learnsets = None + self.trainer_name = None + self.rival_name = None + + @classmethod + def stage_assert_generate(cls, world): + versions = set() + for player in world.player_ids: + if world.worlds[player].game == "Pokemon Red and Blue": + versions.add(world.game_version[player].current_key) + for version in versions: + if not os.path.exists(get_base_rom_path(version)): + raise FileNotFoundError(get_base_rom_path(version)) + + def generate_early(self): + def encode_name(name, t): + try: + if len(encode_text(name)) > 7: + raise IndexError(f"{t} name too long for player {self.world.player_name[self.player]}. Must be 7 characters or fewer.") + return encode_text(name, length=8, whitespace="@", safety=True) + except KeyError as e: + raise KeyError(f"Invalid character(s) in {t} name for player {self.world.player_name[self.player]}") from e + self.trainer_name = encode_name(self.world.trainer_name[self.player].value, "Player") + self.rival_name = encode_name(self.world.rival_name[self.player].value, "Rival") + + if self.world.badges_needed_for_hm_moves[self.player].value >= 2: + badges_to_add = ["Marsh Badge", "Volcano Badge", "Earth Badge"] + if self.world.badges_needed_for_hm_moves[self.player].value == 3: + badges = ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Marsh Badge", + "Soul Badge", "Volcano Badge", "Earth Badge"] + self.world.random.shuffle(badges) + badges_to_add += [badges.pop(), badges.pop()] + hm_moves = ["Cut", "Fly", "Surf", "Strength", "Flash"] + self.world.random.shuffle(hm_moves) + self.extra_badges = {} + for badge in badges_to_add: + self.extra_badges[hm_moves.pop()] = badge + + process_pokemon_data(self) + + def create_items(self) -> None: + locations = [location for location in location_data if location.type == "Item"] + item_pool = [] + for location in locations: + if "Hidden" in location.name and not self.world.randomize_hidden_items[self.player].value: + continue + if "Rock Tunnel B1F" in location.region and not self.world.extra_key_items[self.player].value: + continue + if location.name == "Celadon City - Mansion Lady" and not self.world.tea[self.player].value: + continue + item = self.create_item(location.original_item) + if location.event: + self.world.get_location(location.name, self.player).place_locked_item(item) + elif ("Badge" not in item.name or self.world.badgesanity[self.player].value) and \ + (item.name != "Oak's Parcel" or self.world.old_man[self.player].value != 1): + item_pool.append(item) + self.world.random.shuffle(item_pool) + + self.world.itempool += item_pool + + + def pre_fill(self): + + process_wild_pokemon(self) + process_static_pokemon(self) + + if self.world.old_man[self.player].value == 1: + item = self.create_item("Oak's Parcel") + locations = [] + for location in self.world.get_locations(): + if location.player == self.player and location.item is None and location.can_reach(self.world.state) \ + and location.item_rule(item): + locations.append(location) + self.world.random.choice(locations).place_locked_item(item) + + if not self.world.badgesanity[self.player].value: + self.world.non_local_items[self.player].value -= self.item_name_groups["Badges"] + for i in range(5): + try: + badges = [] + badgelocs = [] + for badge in ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Soul Badge", + "Marsh Badge", "Volcano Badge", "Earth Badge"]: + badges.append(self.create_item(badge)) + for loc in ["Pewter Gym - Brock 1", "Cerulean Gym - Misty 1", "Vermilion Gym - Lt. Surge 1", + "Celadon Gym - Erika 1", "Fuchsia Gym - Koga 1", "Saffron Gym - Sabrina 1", + "Cinnabar Gym - Blaine 1", "Viridian Gym - Giovanni 1"]: + badgelocs.append(self.world.get_location(loc, self.player)) + state = self.world.get_all_state(False) + self.world.random.shuffle(badges) + self.world.random.shuffle(badgelocs) + fill_restrictive(self.world, state, badgelocs.copy(), badges, True, True) + except FillError: + for location in badgelocs: + location.item = None + continue + break + else: + raise FillError(f"Failed to place badges for player {self.player}") + + locs = [self.world.get_location("Fossil - Choice A", self.player), + self.world.get_location("Fossil - Choice B", self.player)] + for loc in locs: + add_item_rule(loc, lambda i: i.advancement or i.name in self.item_name_groups["Unique"] + or i.name == "Master Ball") + + loc = self.world.get_location("Pallet Town - Player's PC", self.player) + if loc.item is None: + locs.append(loc) + + for loc in locs: + unplaced_items = [] + if loc.name in self.world.priority_locations[self.player].value: + add_item_rule(loc, lambda i: i.advancement) + for item in self.world.itempool: + if item.player == self.player and loc.item_rule(item): + self.world.itempool.remove(item) + state = sweep_from_pool(self.world.state, self.world.itempool + unplaced_items) + if state.can_reach(loc, "Location", self.player): + loc.place_locked_item(item) + break + else: + unplaced_items.append(item) + self.world.itempool += unplaced_items + + intervene = False + test_state = self.world.get_all_state(False) + if not test_state.pokemon_rb_can_surf(self.player) or not test_state.pokemon_rb_can_strength(self.player): + intervene = True + elif self.world.accessibility[self.player].current_key != "minimal": + if not test_state.pokemon_rb_can_cut(self.player) or not test_state.pokemon_rb_can_flash(self.player): + intervene = True + if intervene: + # the way this is handled will be improved significantly in the future when I add options to + # let you choose the exact weights for HM compatibility + logging.warning( + f"HM-compatible Pokémon possibly missing, placing Mew on Route 1 for player {self.player}") + loc = self.world.get_location("Route 1 - Wild Pokemon - 1", self.player) + loc.item = self.create_item("Mew") + + def create_regions(self): + if self.world.free_fly_location[self.player].value: + fly_map_code = self.world.random.randint(5, 9) + if fly_map_code == 9: + fly_map_code = 10 + if fly_map_code == 5: + fly_map_code = 4 + else: + fly_map_code = 0 + self.fly_map = ["Pallet Town", "Viridian City", "Pewter City", "Cerulean City", "Lavender Town", + "Vermilion City", "Celadon City", "Fuchsia City", "Cinnabar Island", "Indigo Plateau", + "Saffron City"][fly_map_code] + self.fly_map_code = fly_map_code + create_regions(self.world, self.player) + self.world.completion_condition[self.player] = lambda state, player=self.player: state.has("Become Champion", player=player) + + def set_rules(self): + set_rules(self.world, self.player) + + def create_item(self, name: str) -> Item: + return PokemonRBItem(name, self.player) + + def generate_output(self, output_directory: str): + generate_output(self, output_directory) + + def write_spoiler_header(self, spoiler_handle: TextIO): + if self.world.free_fly_location[self.player].value: + spoiler_handle.write('Fly unlocks: %s\n' % self.fly_map) + if self.extra_badges: + for hm_move, badge in self.extra_badges.items(): + spoiler_handle.write(hm_move + " enabled by: " + (" " * 20)[:20 - len(hm_move)] + badge + "\n") + + def write_spoiler(self, spoiler_handle): + if self.world.randomize_type_matchup_types[self.player].value or \ + self.world.randomize_type_matchup_type_effectiveness[self.player].value: + spoiler_handle.write(f"\n\nType matchups ({self.world.player_name[self.player]}):\n\n") + for matchup in self.type_chart: + spoiler_handle.write(f"{matchup[0]} deals {matchup[2] * 10}% damage to {matchup[1]}\n") + + def get_filler_item_name(self) -> str: + return self.world.random.choice([item for item in item_table if item_table[item].classification in + [ItemClassification.filler, ItemClassification.trap]]) + + +class PokemonRBItem(Item): + game = "Pokemon Red and Blue" + type = None + + def __init__(self, name, player: int = None): + item_data = item_table[name] + super(PokemonRBItem, self).__init__( + name, + item_data.classification, + item_data.id, player + ) diff --git a/worlds/pokemon_rb/basepatch_blue.bsdiff4 b/worlds/pokemon_rb/basepatch_blue.bsdiff4 new file mode 100644 index 0000000000..c688ebede0 Binary files /dev/null and b/worlds/pokemon_rb/basepatch_blue.bsdiff4 differ diff --git a/worlds/pokemon_rb/basepatch_red.bsdiff4 b/worlds/pokemon_rb/basepatch_red.bsdiff4 new file mode 100644 index 0000000000..a9005d3744 Binary files /dev/null and b/worlds/pokemon_rb/basepatch_red.bsdiff4 differ diff --git a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md new file mode 100644 index 0000000000..fe2550d4a1 --- /dev/null +++ b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md @@ -0,0 +1,55 @@ +# Pokémon Red and Blue + +## 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. + +A great many things besides item placement can be randomized, such as the location of Pokémon, their stats, types, etc., depending on your yaml settings. + +Many baseline changes are made to the game, including: + +* Bag item space increased to 128 slots (up from 20) +* PC item storage increased to 64 slots (up from 50) +* You can hold B to run (or bike extra fast!). +* You can hold select while talking to a trainer to re-battle them. +* You can return to route 2 from Diglett's Cave without the use of Cut. +* Mew can be encountered at the S.S. Anne dock truck. This can be randomized depending on your settings. +* The S.S. Anne will never depart. +* Seafoam Islands entrances are swapped. This means you need Strength to travel through from Cinnabar Island to Fuchsia +City +* After obtaining one of the fossil item checks in Mt Moon, the remaining item can be received from the Cinnabar Lab +fossil scientist. This may require reviving a number of fossils, depending on your settings. +* Obedience depends on the total number of badges you have obtained instead of depending on specific badges. +* Pokémon that evolve by trading can also evolve by reaching level 35. +* Evolution stones are reusable. +* Much of the dialogue throughout the game has been removed or shortened. +* If the Old Man is blocking your way through Viridian City, you do not have Oak's Parcel in your inventory, and you've +exhausted your money and Poké Balls, you can get a free Poké Ball from your mom. + +## What items and locations get shuffled? + +All items that go into your bags given by NPCs or found on the ground, as well as gym badges. +Optionally, hidden items (those located with the Item Finder) can be shuffled as well. + +## 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. +By default, gym badges are shuffled across only the 8 gyms, but you can turn on Badgesanity in your yaml to shuffle them +into the general item pool. + +## What does another world's item look like in Pokémon Red and Blue? + +All items for other games will display simply as "AP ITEM," including those for other Pokémon Red and Blue games. + +## When the player receives an item, what happens? + +A "received item" sound effect will play. Currently, there is no in-game message informing you of what the item is. +If you are in battle, have menus or text boxes opened, or scripted events are occurring, the items will not be given to +you until these have ended. diff --git a/worlds/pokemon_rb/docs/setup_en.md b/worlds/pokemon_rb/docs/setup_en.md new file mode 100644 index 0000000000..58ed3b0dc6 --- /dev/null +++ b/worlds/pokemon_rb/docs/setup_en.md @@ -0,0 +1,84 @@ +# Setup Guide for Pokémon Red and Blue: Archipelago + +## Important + +As we are using Bizhawk, this guide is only applicable to Windows and Linux systems. + +## Required Software + +- Bizhawk: [Bizhawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory) + - Version 2.3.1 and later are supported. Version 2.7 is recommended for stability. + - Detailed installation instructions for Bizhawk can be found at the above link. + - Windows users must run the prereq installer first, which can also be found at the above link. +- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases) + (select `Pokemon Client` during installation). +- Pokémon Red and/or Blue ROM files. The Archipelago community cannot provide these. + +## Configuring Bizhawk + +Once Bizhawk has been installed, open Bizhawk and change the following settings: + +- Under Config > Customize > Advanced, make sure the box for AutoSaveRAM is checked, and click the 5s button. + This reduces the possibility of losing save data in emulator crashes. +- Under Config > Customize, check the "Run in background" and "Accept background input" boxes. This will allow you to + continue playing in the background, even if another window is selected. + +It is strongly recommended to associate GB rom extensions (\*.gb) to the Bizhawk we've just installed. +To do so, we simply have to search any Gameboy rom we happened to own, right click and select "Open with...", unfold +the list that appears and select the bottom option "Look for another application", then browse to the Bizhawk folder +and select EmuHawk.exe. + +## Configuring your YAML file + +### What is a YAML file and why do I need one? + +Your YAML file contains a set of configuration options which provide the generator with information about how it should +generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy +an experience customized for their taste, and different players in the same multiworld can all have different options. + +### Where do I get a YAML file? + +You can generate a yaml or download a template by visiting the [Pokemon Red and Blue Player Settings Page](/games/Pokemon Red and Blue/player-settings) + +It is important to note that the `game_version` option determines the ROM file that will be patched. +Both the player and the person generating (if they are generating locally) will need the corresponding ROM file. + +For `trainer_name` and `rival_name` the following regular characters are allowed: + +* `‘’“”·… ABCDEFGHIJKLMNOPQRSTUVWXYZ():;[]abcdefghijklmnopqrstuvwxyzé'-?!.♂$×/,♀0123456789` + +And the following special characters (these each take up one character): +* `<'d>` +* `<'l>` +* `<'t>` +* `<'v>` +* `<'r>` +* `<'m>` +* `` +* `` +* `` alias for `♂` +* `` alias for `♀` + +## Joining a MultiWorld Game + +### Obtain your Pokémon patch file + +When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that is done, +the host will provide you with either a link to download your data file, or with a zip file containing everyone's data +files. Your data file should have a `.apred` or `.apblue` extension. + +Double-click on your patch file to start your client and start the ROM patch process. Once the process is finished +(this can take a while), the client and the emulator will be started automatically (if you associated the extension +to the emulator as recommended). + +### Connect to the Multiserver + +Once both the client and the emulator are started, you must connect them. Within the emulator click on the "Tools" +menu and select "Lua Console". Click the folder button or press Ctrl+O to open a Lua script. + +Navigate to your Archipelago install folder and open `data/lua/PKMN_RB/pkmr_rb.lua`. + +To connect the client to the multiserver simply put `
:` on the textfield on top and press enter (if the +server uses password, type in the bottom textfield `/connect
: [password]`) + +Now you are ready to start your adventure in Kanto. diff --git a/worlds/pokemon_rb/items.py b/worlds/pokemon_rb/items.py new file mode 100644 index 0000000000..53722c02a4 --- /dev/null +++ b/worlds/pokemon_rb/items.py @@ -0,0 +1,176 @@ +from BaseClasses import ItemClassification +from .poke_data import pokemon_data + +class ItemData: + def __init__(self, id, classification, groups): + self.groups = groups + self.classification = classification + self.id = None if id is None else id + 172000000 + +item_table = { + "Master Ball": ItemData(1, ItemClassification.useful, ["Consumables", "Poke Balls"]), + "Ultra Ball": ItemData(2, ItemClassification.filler, ["Consumables", "Poke Balls"]), + "Great Ball": ItemData(3, ItemClassification.filler, ["Consumables", "Poke Balls"]), + "Poke Ball": ItemData(4, ItemClassification.filler, ["Consumables", "Poke Balls"]), + "Town Map": ItemData(5, ItemClassification.progression_skip_balancing, ["Unique", "Key Items"]), + "Bicycle": ItemData(6, ItemClassification.progression, ["Unique", "Key Items"]), + # "Flippers": ItemData(7, ItemClassification.progression), + #"Safari Ball": ItemData(8, ItemClassification.filler), + #"Pokedex": ItemData(9, ItemClassification.filler), + "Moon Stone": ItemData(10, ItemClassification.useful, ["Unique", "Evolution Stones"]), + "Antidote": ItemData(11, ItemClassification.filler, ["Consumables"]), + "Burn Heal": ItemData(12, ItemClassification.filler, ["Consumables"]), + "Ice Heal": ItemData(13, ItemClassification.filler, ["Consumables"]), + "Awakening": ItemData(14, ItemClassification.filler, ["Consumables"]), + "Paralyze Heal": ItemData(15, ItemClassification.filler, ["Consumables"]), + "Full Restore": ItemData(16, ItemClassification.filler, ["Consumables"]), + "Max Potion": ItemData(17, ItemClassification.filler, ["Consumables"]), + "Hyper Potion": ItemData(18, ItemClassification.filler, ["Consumables"]), + "Super Potion": ItemData(19, ItemClassification.filler, ["Consumables"]), + "Potion": ItemData(20, ItemClassification.filler, ["Consumables"]), + "Boulder Badge": ItemData(21, ItemClassification.progression, ["Unique", "Key Items", "Badges"]), + "Cascade Badge": ItemData(22, ItemClassification.progression, ["Unique", "Key Items", "Badges"]), + "Thunder Badge": ItemData(23, ItemClassification.progression, ["Unique", "Key Items", "Badges"]), + "Rainbow Badge": ItemData(24, ItemClassification.progression, ["Unique", "Key Items", "Badges"]), + "Soul Badge": ItemData(25, ItemClassification.progression, ["Unique", "Key Items", "Badges"]), + "Marsh Badge": ItemData(26, ItemClassification.progression, ["Unique", "Key Items", "Badges"]), + "Volcano Badge": ItemData(27, ItemClassification.progression, ["Unique", "Key Items", "Badges"]), + "Earth Badge": ItemData(28, ItemClassification.progression, ["Unique", "Key Items", "Badges"]), + "Escape Rope": ItemData(29, ItemClassification.filler, ["Consumables"]), + "Repel": ItemData(30, ItemClassification.filler, ["Consumables"]), + "Old Amber": ItemData(31, ItemClassification.progression_skip_balancing, ["Unique", "Fossils"]), + "Fire Stone": ItemData(32, ItemClassification.useful, ["Unique", "Evolution Stones"]), + "Thunder Stone": ItemData(33, ItemClassification.useful, ["Unique", "Evolution Stones"]), + "Water Stone": ItemData(34, ItemClassification.useful, ["Unique", "Evolution Stones"]), + "HP Up": ItemData(35, ItemClassification.filler, ["Consumables", "Vitamins"]), + "Protein": ItemData(36, ItemClassification.filler, ["Consumables", "Vitamins"]), + "Iron": ItemData(37, ItemClassification.filler, ["Consumables", "Vitamins"]), + "Carbos": ItemData(38, ItemClassification.filler, ["Consumables", "Vitamins"]), + "Calcium": ItemData(39, ItemClassification.filler, ["Consumables", "Vitamins"]), + "Rare Candy": ItemData(40, ItemClassification.useful, ["Consumables"]), + "Dome Fossil": ItemData(41, ItemClassification.progression_skip_balancing, ["Unique", "Fossils"]), + "Helix Fossil": ItemData(42, ItemClassification.progression_skip_balancing, ["Unique", "Fossils"]), + "Secret Key": ItemData(43, ItemClassification.progression, ["Unique", "Key Items"]), + "Bike Voucher": ItemData(45, ItemClassification.progression, ["Unique", "Key Items"]), + "X Accuracy": ItemData(46, ItemClassification.filler, ["Consumables", "Battle Items"]), + "Leaf Stone": ItemData(47, ItemClassification.useful, ["Unique", "Evolution Stones"]), + "Card Key": ItemData(48, ItemClassification.progression, ["Unique", "Key Items"]), + "Nugget": ItemData(49, ItemClassification.filler, []), + #"Laptop": ItemData(50, ItemClassification.useful, ["Unique"]), + "Poke Doll": ItemData(51, ItemClassification.filler, ["Consumables"]), + "Full Heal": ItemData(52, ItemClassification.filler, ["Consumables"]), + "Revive": ItemData(53, ItemClassification.filler, ["Consumables"]), + "Max Revive": ItemData(54, ItemClassification.filler, ["Consumables"]), + "Guard Spec": ItemData(55, ItemClassification.filler, ["Consumables", "Battle Items"]), + "Super Repel": ItemData(56, ItemClassification.filler, ["Consumables"]), + "Max Repel": ItemData(57, ItemClassification.filler, ["Consumables"]), + "Dire Hit": ItemData(58, ItemClassification.filler, ["Consumables", "Battle Items"]), + #"Coin": ItemData(59, ItemClassification.filler), + "Fresh Water": ItemData(60, ItemClassification.filler, ["Consumables"]), + "Soda Pop": ItemData(61, ItemClassification.filler, ["Consumables"]), + "Lemonade": ItemData(62, ItemClassification.filler, ["Consumables"]), + "S.S. Ticket": ItemData(63, ItemClassification.progression, ["Unique", "Key Items"]), + "Gold Teeth": ItemData(64, ItemClassification.progression, ["Unique", "Key Items"]), + "X Attack": ItemData(65, ItemClassification.filler, ["Consumables", "Battle Items"]), + "X Defend": ItemData(66, ItemClassification.filler, ["Consumables", "Battle Items"]), + "X Speed": ItemData(67, ItemClassification.filler, ["Consumables", "Battle Items"]), + "X Special": ItemData(68, ItemClassification.filler, ["Consumables", "Battle Items"]), + "Coin Case": ItemData(69, ItemClassification.progression_skip_balancing, ["Unique", "Key Items"]), + "Oak's Parcel": ItemData(70, ItemClassification.progression, ["Unique", "Key Items"]), + "Item Finder": ItemData(71, ItemClassification.progression, ["Unique", "Key Items"]), + "Silph Scope": ItemData(72, ItemClassification.progression, ["Unique", "Key Items"]), + "Poke Flute": ItemData(73, ItemClassification.progression, ["Unique", "Key Items"]), + "Lift Key": ItemData(74, ItemClassification.progression, ["Unique", "Key Items"]), + "Exp. All": ItemData(75, ItemClassification.useful, ["Unique"]), + "Old Rod": ItemData(76, ItemClassification.progression_skip_balancing, ["Unique", "Key Items", "Rods"]), + "Good Rod": ItemData(77, ItemClassification.progression_skip_balancing, ["Unique", "Key Items", "Rods"]), + "Super Rod": ItemData(78, ItemClassification.progression_skip_balancing, ["Unique", "Key Items", "Rods"]), + "PP Up": ItemData(79, ItemClassification.filler, ["Consumables"]), + "Ether": ItemData(80, ItemClassification.filler, ["Consumables"]), + "Max Ether": ItemData(81, ItemClassification.filler, ["Consumables"]), + "Elixir": ItemData(82, ItemClassification.filler, ["Consumables"]), + "Max Elixir": ItemData(83, ItemClassification.filler, ["Consumables"]), + "Tea": ItemData(84, ItemClassification.progression, ["Unique", "Key Items"]), + # "Master Sword": ItemData(85, ItemClassification.progression), + # "Flute": ItemData(86, ItemClassification.progression), + # "Titan's Mitt": ItemData(87, ItemClassification.progression), + # "Lamp": ItemData(88, ItemClassification.progression), + "Plant Key": ItemData(89, ItemClassification.progression, ["Unique", "Key Items"]), + "Mansion Key": ItemData(90, ItemClassification.progression, ["Unique", "Key Items"]), + "Hideout Key": ItemData(91, ItemClassification.progression, ["Unique", "Key Items"]), + "Safari Pass": ItemData(93, ItemClassification.progression, ["Unique", "Key Items"]), + "HM01 Cut": ItemData(196, ItemClassification.progression, ["Unique", "HMs"]), + "HM02 Fly": ItemData(197, ItemClassification.progression, ["Unique", "HMs"]), + "HM03 Surf": ItemData(198, ItemClassification.progression, ["Unique", "HMs"]), + "HM04 Strength": ItemData(199, ItemClassification.progression, ["Unique", "HMs"]), + "HM05 Flash": ItemData(200, ItemClassification.progression, ["Unique", "HMs"]), + "TM01 Mega Punch": ItemData(201, ItemClassification.useful, ["Unique", "TMs"]), + "TM02 Razor Wind": ItemData(202, ItemClassification.useful, ["Unique", "TMs"]), + "TM03 Swords Dance": ItemData(203, ItemClassification.useful, ["Unique", "TMs"]), + "TM04 Whirlwind": ItemData(204, ItemClassification.filler, ["Unique", "TMs"]), + "TM05 Mega Kick": ItemData(205, ItemClassification.useful, ["Unique", "TMs"]), + "TM06 Toxic": ItemData(206, ItemClassification.useful, ["Unique", "TMs"]), + "TM07 Horn Drill": ItemData(207, ItemClassification.useful, ["Unique", "TMs"]), + "TM08 Body Slam": ItemData(208, ItemClassification.useful, ["Unique", "TMs"]), + "TM09 Take Down": ItemData(209, ItemClassification.useful, ["Unique", "TMs"]), + "TM10 Double Edge": ItemData(210, ItemClassification.useful, ["Unique", "TMs"]), + "TM11 Bubble Beam": ItemData(211, ItemClassification.useful, ["Unique", "TMs"]), + "TM12 Water Gun": ItemData(212, ItemClassification.useful, ["Unique", "TMs"]), + "TM13 Ice Beam": ItemData(213, ItemClassification.useful, ["Unique", "TMs"]), + "TM14 Blizzard": ItemData(214, ItemClassification.useful, ["Unique", "TMs"]), + "TM15 Hyper Beam": ItemData(215, ItemClassification.useful, ["Unique", "TMs"]), + "TM16 Pay Day": ItemData(216, ItemClassification.useful, ["Unique", "TMs"]), + "TM17 Submission": ItemData(217, ItemClassification.useful, ["Unique", "TMs"]), + "TM18 Counter": ItemData(218, ItemClassification.filler, ["Unique", "TMs"]), + "TM19 Seismic Toss": ItemData(219, ItemClassification.useful, ["Unique", "TMs"]), + "TM20 Rage": ItemData(220, ItemClassification.useful, ["Unique", "TMs"]), + "TM21 Mega Drain": ItemData(221, ItemClassification.useful, ["Unique", "TMs"]), + "TM22 Solar Beam": ItemData(222, ItemClassification.useful, ["Unique", "TMs"]), + "TM23 Dragon Rage": ItemData(223, ItemClassification.useful, ["Unique", "TMs"]), + "TM24 Thunderbolt": ItemData(224, ItemClassification.useful, ["Unique", "TMs"]), + "TM25 Thunder": ItemData(225, ItemClassification.useful, ["Unique", "TMs"]), + "TM26 Earthquake": ItemData(226, ItemClassification.useful, ["Unique", "TMs"]), + "TM27 Fissure": ItemData(227, ItemClassification.useful, ["Unique", "TMs"]), + "TM28 Dig": ItemData(228, ItemClassification.useful, ["Unique", "TMs"]), + "TM29 Psychic": ItemData(229, ItemClassification.useful, ["Unique", "TMs"]), + "TM30 Teleport": ItemData(230, ItemClassification.filler, ["Unique", "TMs"]), + "TM31 Mimic": ItemData(231, ItemClassification.useful, ["Unique", "TMs"]), + "TM32 Double Team": ItemData(232, ItemClassification.useful, ["Unique", "TMs"]), + "TM33 Reflect": ItemData(233, ItemClassification.useful, ["Unique", "TMs"]), + "TM34 Bide": ItemData(234, ItemClassification.filler, ["Unique", "TMs"]), + "TM35 Metronome": ItemData(235, ItemClassification.useful, ["Unique", "TMs"]), + "TM36 Self Destruct": ItemData(236, ItemClassification.useful, ["Unique", "TMs"]), + "TM37 Egg Bomb": ItemData(237, ItemClassification.useful, ["Unique", "TMs"]), + "TM38 Fire Blast": ItemData(238, ItemClassification.useful, ["Unique", "TMs"]), + "TM39 Swift": ItemData(239, ItemClassification.useful, ["Unique", "TMs"]), + "TM40 Skull Bash": ItemData(240, ItemClassification.filler, ["Unique", "TMs"]), + "TM41 Soft Boiled": ItemData(241, ItemClassification.useful, ["Unique", "TMs"]), + "TM42 Dream Eater": ItemData(242, ItemClassification.useful, ["Unique", "TMs"]), + "TM43 Sky Attack": ItemData(243, ItemClassification.filler, ["Unique", "TMs"]), + "TM44 Rest": ItemData(244, ItemClassification.useful, ["Unique", "TMs"]), + "TM45 Thunder Wave": ItemData(245, ItemClassification.useful, ["Unique", "TMs"]), + "TM46 Psywave": ItemData(246, ItemClassification.filler, ["Unique", "TMs"]), + "TM47 Explosion": ItemData(247, ItemClassification.useful, ["Unique", "TMs"]), + "TM48 Rock Slide": ItemData(248, ItemClassification.useful, ["Unique", "TMs"]), + "TM49 Tri Attack": ItemData(249, ItemClassification.useful, ["Unique", "TMs"]), + "TM50 Substitute": ItemData(250, ItemClassification.useful, ["Unique", "TMs"]), + + "Fuji Saved": ItemData(None, ItemClassification.progression, []), + "Silph Co Liberated": ItemData(None, ItemClassification.progression, []), + "Become Champion": ItemData(None, ItemClassification.progression, []) +} +item_table.update( + {pokemon: ItemData(None, ItemClassification.progression, []) for pokemon in pokemon_data.keys()} +) +item_table.update( + {f"Missable {pokemon}": ItemData(None, ItemClassification.useful, []) for pokemon in pokemon_data.keys()} +) +item_table.update( + {f"Static {pokemon}": ItemData(None, ItemClassification.progression, []) for pokemon in pokemon_data.keys()} +) + + +item_groups = {} +for item, data in item_table.items(): + for group in data.groups: + item_groups[group] = item_groups.get(group, []) + [item] \ No newline at end of file diff --git a/worlds/pokemon_rb/locations.py b/worlds/pokemon_rb/locations.py new file mode 100644 index 0000000000..a619336f57 --- /dev/null +++ b/worlds/pokemon_rb/locations.py @@ -0,0 +1,1719 @@ + +from BaseClasses import Location +from .rom_addresses import rom_addresses +loc_id_start = 17200000 + +class LocationData: + def __init__(self, region, name, original_item, rom_address=None, ram_address=None, event=False, type="Item"): + self.region = region + if "Route" in region: + region = " ".join(region.split()[:2]) + self.name = region + " - " + name + self.original_item = original_item + self.rom_address = rom_address + self.ram_address = ram_address + self.event = event + self.type = type + +class EventFlag: + def __init__(self, flag): + self.byte = int(flag / 8) + self.bit = flag % 8 + self.flag = flag + + +class Missable: + def __init__(self, flag): + self.byte = int(flag / 8) + self.bit = flag % 8 + self.flag = flag + + +class Hidden: + def __init__(self, flag): + self.byte = int(flag / 8) + self.bit = flag % 8 + self.flag = flag + + +class Rod: + def __init__(self, flag): + self.byte = 0 + self.bit = flag + self.flag = flag + +# def get_locations(player=None): +location_data = [ + + LocationData("Vermilion City", "Fishing Guru", "Old Rod", rom_addresses["Rod_Vermilion_City_Fishing_Guru"], Rod(3)), + LocationData("Fuchsia City", "Fishing Guru's Brother", "Good Rod", rom_addresses["Rod_Fuchsia_City_Fishing_Brother"], Rod(4)), + LocationData("Route 12 South", "Fishing Guru's Brother", "Super Rod", rom_addresses["Rod_Route12_Fishing_Brother"], Rod(5)), + + LocationData("Pallet Town", "Player's PC", "Potion", rom_addresses['PC_Item'], EventFlag(1),), + LocationData("Celadon City", "Mansion Lady", "Tea", rom_addresses["Event_Mansion_Lady"], EventFlag(2)), + LocationData("Pallet Town", "Rival's Sister", "Town Map", rom_addresses["Event_Rivals_Sister"], EventFlag(24)), + LocationData("Pallet Town", "Oak's Post-Route-22-Rival Gift", "Poke Ball", rom_addresses["Event_Oaks_Gift"], EventFlag(36)), + LocationData("Route 1", "Free Sample Man", "Potion", rom_addresses["Event_Free_Sample"], EventFlag(960)), + LocationData("Viridian City", "Sleepy Guy", "TM42 Dream Eater", rom_addresses["Event_Sleepy_Guy"], + EventFlag(41)), + LocationData("Viridian City", "Pokemart", "Oak's Parcel", rom_addresses["Event_Pokemart_Quest"], + EventFlag(57)), + LocationData("Viridian Gym", "Giovanni 2", "TM27 Fissure", rom_addresses["Event_Viridian_Gym"], EventFlag(80)), + LocationData("Route 2 East", "Oak's Aide", "HM05 Flash", rom_addresses["Event_Route_2_Oaks_Aide"], + EventFlag(984)), + LocationData("Pewter City", "Museum", "Old Amber", rom_addresses["Event_Museum"], EventFlag(105)), + LocationData("Pewter Gym", "Brock 2", "TM34 Bide", rom_addresses["Event_Pewter_Gym"], EventFlag(118)), + LocationData("Cerulean City", "Bicycle Shop", "Bicycle", rom_addresses["Event_Bicycle_Shop"], EventFlag(192)), + LocationData("Cerulean Gym", "Misty 2", "TM11 Bubble Beam", rom_addresses["Event_Cerulean_Gym"], + EventFlag(190)), + LocationData("Route 24", "Nugget Bridge", "Nugget", rom_addresses["Event_Nugget_Bridge"], EventFlag(1344)), + LocationData("Route 25", "Bill", "S.S. Ticket", rom_addresses["Event_Bill"], EventFlag(1372)), + LocationData("Lavender Town", "Mr. Fuji", "Poke Flute", rom_addresses["Event_Fuji"], EventFlag(296)), + LocationData("Route 12 North", "Mourning Girl", "TM39 Swift", rom_addresses["Event_Mourning_Girl"], + EventFlag(1152)), + LocationData("Vermilion City", "Pokemon Fan Club", "Bike Voucher", rom_addresses["Event_Pokemon_Fan_Club"], + EventFlag(337)), + LocationData("Vermilion Gym", "Lt. Surge 2", "TM24 Thunderbolt", rom_addresses["Event_Vermillion_Gym"], + EventFlag(358)), + LocationData("S.S. Anne 2F", "Captain", "HM01 Cut", rom_addresses["Event_SS_Anne_Captain"], EventFlag(1504)), + LocationData("Route 11 East", "Oak's Aide", "Item Finder", rom_addresses["Event_Rt11_Oaks_Aide"], + EventFlag(1151)), + LocationData("Celadon City", "Stranded Man", "TM41 Soft Boiled", rom_addresses["Event_Stranded_Man"], + EventFlag(384)), + LocationData("Celadon City", "Thirsty Girl Gets Water", "TM13 Ice Beam", + rom_addresses["Event_Thirsty_Girl_Water"], EventFlag(396)), + LocationData("Celadon City", "Thirsty Girl Gets Soda Pop", "TM48 Rock Slide", + rom_addresses["Event_Thirsty_Girl_Soda"], EventFlag(397)), + LocationData("Celadon City", "Thirsty Girl Gets Lemonade", "TM49 Tri Attack", + rom_addresses["Event_Thirsty_Girl_Lemonade"], EventFlag(398)), + LocationData("Celadon City", "Counter Man", "TM18 Counter", rom_addresses["Event_Counter"], EventFlag(399)), + LocationData("Celadon City", "Gambling Addict", "Coin Case", rom_addresses["Event_Gambling_Addict"], + EventFlag(480)), + LocationData("Celadon Gym", "Erika 2", "TM21 Mega Drain", rom_addresses["Event_Celadon_Gym"], EventFlag(424)), + LocationData("Silph Co 11F", "Silph Co President", "Master Ball", rom_addresses["Event_Silph_Co_President"], + EventFlag(1933)), + LocationData("Silph Co 2F", "Woman", "TM36 Self Destruct", rom_addresses["Event_Scared_Woman"], + EventFlag(1791)), + LocationData("Route 16 North", "House Woman", "HM02 Fly", rom_addresses["Event_Rt16_House_Woman"], EventFlag(1230)), + LocationData("Route 15", "Oak's Aide", "Exp. All", rom_addresses["Event_Rt_15_Oaks_Aide"], EventFlag(1200)), + LocationData("Fuchsia City", "Safari Zone Warden", "HM04 Strength", rom_addresses["Event_Warden"], EventFlag(568)), + LocationData("Fuchsia Gym", "Koga 2", "TM06 Toxic", rom_addresses["Event_Fuschia_Gym"], EventFlag(600)), + LocationData("Safari Zone West", "Secret House", "HM03 Surf", rom_addresses["Event_Safari_Zone_Secret_House"], EventFlag(2176)), + LocationData("Cinnabar Island", "Lab Scientist", "TM35 Metronome", rom_addresses["Event_Lab_Scientist"], EventFlag(727)), + LocationData("Cinnabar Gym", "Blaine 2", "TM38 Fire Blast", rom_addresses["Event_Cinnabar_Gym"], + EventFlag(664)), + LocationData("Copycat's House", "Copycat", "TM31 Mimic", rom_addresses["Event_Copycat"], EventFlag(832)), + LocationData("Saffron City", "Mr. Psychic", "TM29 Psychic", rom_addresses["Event_Mr_Psychic"], EventFlag(944)), + LocationData("Saffron Gym", "Sabrina 2", "TM46 Psywave", rom_addresses["Event_Saffron_Gym"], EventFlag(864)), + LocationData("Fossil", "Choice A", "Dome Fossil", + [rom_addresses["Event_Dome_Fossil"], rom_addresses["Event_Dome_Fossil_B"], + rom_addresses["Dome_Fossil_Text"]], EventFlag(0x57E)), + LocationData("Fossil", "Choice B", "Helix Fossil", + [rom_addresses["Event_Helix_Fossil"], rom_addresses["Event_Helix_Fossil_B"], + rom_addresses["Helix_Fossil_Text"]], EventFlag(0x57F)), + + LocationData("Cerulean City", "Rocket Thief", "TM28 Dig", rom_addresses["Event_Rocket_Thief"], + Missable(6)), + LocationData("Route 2 East", "South Item", "Moon Stone", rom_addresses["Missable_Route_2_Item_1"], + Missable(25)), + LocationData("Route 2 East", "North Item", "HP Up", rom_addresses["Missable_Route_2_Item_2"], Missable(26)), + LocationData("Route 4", "Item", "TM04 Whirlwind", rom_addresses["Missable_Route_4_Item"], Missable(27)), + LocationData("Route 9", "Item", "TM30 Teleport", rom_addresses["Missable_Route_9_Item"], Missable(28)), + LocationData("Route 12 North", "Island Item", "TM16 Pay Day", rom_addresses["Missable_Route_12_Item_1"], Missable(30)), + LocationData("Route 12 South", "Item Behind Cuttable Tree", "Iron", rom_addresses["Missable_Route_12_Item_2"], Missable(31)), + LocationData("Route 15", "Item", "TM20 Rage", rom_addresses["Missable_Route_15_Item"], Missable(32)), + LocationData("Route 24", "Item", "TM45 Thunder Wave", rom_addresses["Missable_Route_24_Item"], Missable(37)), + LocationData("Route 25", "Item", "TM19 Seismic Toss", rom_addresses["Missable_Route_25_Item"], Missable(38)), + LocationData("Viridian Gym", "Item", "Revive", rom_addresses["Missable_Viridian_Gym_Item"], Missable(51)), + LocationData("Cerulean Cave 1F", "Southwest Item", "Full Restore", rom_addresses["Missable_Cerulean_Cave_1F_Item_1"], + Missable(53)), + LocationData("Cerulean Cave 1F", "Northeast Item", "Max Elixir", rom_addresses["Missable_Cerulean_Cave_1F_Item_2"], + Missable(54)), + LocationData("Cerulean Cave 1F", "Northwest Item", "Nugget", rom_addresses["Missable_Cerulean_Cave_1F_Item_3"], + Missable(55)), + LocationData("Pokemon Tower 3F", "North Item", "Escape Rope", rom_addresses["Missable_Pokemon_Tower_3F_Item"], + Missable(57)), + LocationData("Pokemon Tower 4F", "East Item", "Elixir", rom_addresses["Missable_Pokemon_Tower_4F_Item_1"], + Missable(58)), + LocationData("Pokemon Tower 4F", "West Item", "Awakening", rom_addresses["Missable_Pokemon_Tower_4F_Item_2"], + Missable(59)), + LocationData("Pokemon Tower 4F", "South Item", "HP Up", rom_addresses["Missable_Pokemon_Tower_4F_Item_3"], + Missable(60)), + LocationData("Pokemon Tower 5F", "Southwest Item", "Nugget", rom_addresses["Missable_Pokemon_Tower_5F_Item"], + Missable(61)), + LocationData("Pokemon Tower 6F", "West Item", "Rare Candy", rom_addresses["Missable_Pokemon_Tower_6F_Item_1"], + Missable(62)), + LocationData("Pokemon Tower 6F", "Southeast Item", "X Accuracy", rom_addresses["Missable_Pokemon_Tower_6F_Item_2"], + Missable(63)), + LocationData("Fuchsia City", "Warden's House Item", "Rare Candy", rom_addresses["Missable_Wardens_House_Item"], + Missable(71)), + LocationData("Pokemon Mansion 1F", "North Item", "Escape Rope", + rom_addresses["Missable_Pokemon_Mansion_1F_Item_1"], Missable(72)), + LocationData("Pokemon Mansion 1F", "South Item", "Carbos", rom_addresses["Missable_Pokemon_Mansion_1F_Item_2"], + Missable(73)), + LocationData("Power Plant", "Southwest Item", "Carbos", rom_addresses["Missable_Power_Plant_Item_1"], Missable(86)), + LocationData("Power Plant", "North Item", "HP Up", rom_addresses["Missable_Power_Plant_Item_2"], Missable(87)), + LocationData("Power Plant", "Northeast Item", "Rare Candy", rom_addresses["Missable_Power_Plant_Item_3"], + Missable(88)), + LocationData("Power Plant", "Southeast Item", "TM25 Thunder", rom_addresses["Missable_Power_Plant_Item_4"], + Missable(89)), + LocationData("Power Plant", "South Item", "TM33 Reflect", rom_addresses["Missable_Power_Plant_Item_5"], + Missable(90)), + LocationData("Victory Road 2F", "Northeast Item", "TM17 Submission", rom_addresses["Missable_Victory_Road_2F_Item_1"], + Missable(92)), + LocationData("Victory Road 2F", "East Item", "Full Heal", rom_addresses["Missable_Victory_Road_2F_Item_2"], + Missable(93)), + LocationData("Victory Road 2F", "West Item", "TM05 Mega Kick", rom_addresses["Missable_Victory_Road_2F_Item_3"], + Missable(94)), + LocationData("Victory Road 2F", "North Item Near Moltres", "Guard Spec", rom_addresses["Missable_Victory_Road_2F_Item_4"], + Missable(95)), + LocationData("Viridian Forest", "East Item", "Antidote", rom_addresses["Missable_Viridian_Forest_Item_1"], + Missable(100)), + LocationData("Viridian Forest", "Northwest Item", "Potion", rom_addresses["Missable_Viridian_Forest_Item_2"], + Missable(101)), + LocationData("Viridian Forest", "Southwest Item", "Poke Ball", + rom_addresses["Missable_Viridian_Forest_Item_3"], Missable(102)), + LocationData("Mt Moon 1F", "West Item", "Potion", rom_addresses["Missable_Mt_Moon_1F_Item_1"], Missable(103)), + LocationData("Mt Moon 1F", "Northwest Item", "Moon Stone", rom_addresses["Missable_Mt_Moon_1F_Item_2"], Missable(104)), + LocationData("Mt Moon 1F", "Southeast Item", "Rare Candy", rom_addresses["Missable_Mt_Moon_1F_Item_3"], Missable(105)), + LocationData("Mt Moon 1F", "East Item", "Escape Rope", rom_addresses["Missable_Mt_Moon_1F_Item_4"], + Missable(106)), + LocationData("Mt Moon 1F", "South Item", "Potion", rom_addresses["Missable_Mt_Moon_1F_Item_5"], Missable(107)), + LocationData("Mt Moon 1F", "Southwest Item", "TM12 Water Gun", rom_addresses["Missable_Mt_Moon_1F_Item_6"], + Missable(108)), + LocationData("Mt Moon B2F", "South Item", "HP Up", rom_addresses["Missable_Mt_Moon_B2F_Item_1"], Missable(111)), + LocationData("Mt Moon B2F", "North Item", "TM01 Mega Punch", rom_addresses["Missable_Mt_Moon_B2F_Item_2"], + Missable(112)), + LocationData("S.S. Anne 1F", "Item", "TM08 Body Slam", rom_addresses["Missable_SS_Anne_1F_Item"], + Missable(114)), + LocationData("S.S. Anne 2F", "Item 1", "Max Ether", rom_addresses["Missable_SS_Anne_2F_Item_1"], + Missable(115)), + LocationData("S.S. Anne 2F", "Item 2", "Rare Candy", rom_addresses["Missable_SS_Anne_2F_Item_2"], + Missable(116)), + LocationData("S.S. Anne B1F", "Item 1", "Ether", rom_addresses["Missable_SS_Anne_B1F_Item_1"], Missable(117)), + LocationData("S.S. Anne B1F", "Item 2", "TM44 Rest", rom_addresses["Missable_SS_Anne_B1F_Item_2"], + Missable(118)), + LocationData("S.S. Anne B1F", "Item 3", "Max Potion", rom_addresses["Missable_SS_Anne_B1F_Item_3"], + Missable(119)), + LocationData("Victory Road 3F", "Northeast Item", "Max Revive", rom_addresses["Missable_Victory_Road_3F_Item_1"], + Missable(120)), + LocationData("Victory Road 3F", "Northwest Item", "TM47 Explosion", rom_addresses["Missable_Victory_Road_3F_Item_2"], + Missable(121)), + LocationData("Rocket Hideout B1F", "West Item", "Escape Rope", + rom_addresses["Missable_Rocket_Hideout_B1F_Item_1"], Missable(123)), + LocationData("Rocket Hideout B1F", "Southwest Item", "Hyper Potion", + rom_addresses["Missable_Rocket_Hideout_B1F_Item_2"], Missable(124)), + LocationData("Rocket Hideout B2F", "Northwest Left Item", "Moon Stone", rom_addresses["Missable_Rocket_Hideout_B2F_Item_1"], + Missable(125)), + LocationData("Rocket Hideout B2F", "Northeast Item", "Nugget", rom_addresses["Missable_Rocket_Hideout_B2F_Item_2"], + Missable(126)), + LocationData("Rocket Hideout B2F", "Northwest Right Item", "TM07 Horn Drill", + rom_addresses["Missable_Rocket_Hideout_B2F_Item_3"], Missable(127)), + LocationData("Rocket Hideout B2F", "Southwest Item", "Super Potion", + rom_addresses["Missable_Rocket_Hideout_B2F_Item_4"], Missable(128)), + LocationData("Rocket Hideout B3F", "East Item", "TM10 Double Edge", + rom_addresses["Missable_Rocket_Hideout_B3F_Item_1"], Missable(129)), + LocationData("Rocket Hideout B3F", "Center Item", "Rare Candy", rom_addresses["Missable_Rocket_Hideout_B3F_Item_2"], + Missable(130)), + LocationData("Rocket Hideout B4F", "West Item", "HP Up", rom_addresses["Missable_Rocket_Hideout_B4F_Item_1"], + Missable(132)), + LocationData("Rocket Hideout B4F", "Northwest Item", "TM02 Razor Wind", + rom_addresses["Missable_Rocket_Hideout_B4F_Item_2"], Missable(133)), + LocationData("Rocket Hideout B4F", "Southwest Item (Lift Key)", "Iron", rom_addresses["Missable_Rocket_Hideout_B4F_Item_3"], + Missable(134)), + LocationData("Rocket Hideout B4F", "Giovanni Item (Lift Key)", "Silph Scope", + rom_addresses["Missable_Rocket_Hideout_B4F_Item_4"], [EventFlag(0x6A7), Missable(135)]), + LocationData("Rocket Hideout B4F", "Rocket Grunt Item", "Lift Key", rom_addresses["Missable_Rocket_Hideout_B4F_Item_5"], + [EventFlag(0x6A6), Missable(136)]), + LocationData("Silph Co 3F", "Item (Card Key)", "Hyper Potion", rom_addresses["Missable_Silph_Co_3F_Item"], Missable(144)), + LocationData("Silph Co 4F", "Left Item (Card Key)", "Full Heal", rom_addresses["Missable_Silph_Co_4F_Item_1"], + Missable(148)), + LocationData("Silph Co 4F", "Middle Item (Card Key)", "Max Revive", rom_addresses["Missable_Silph_Co_4F_Item_2"], + Missable(149)), + LocationData("Silph Co 4F", "Right Item (Card Key)", "Escape Rope", rom_addresses["Missable_Silph_Co_4F_Item_3"], + Missable(150)), + LocationData("Silph Co 5F", "Southwest Item", "TM09 Take Down", rom_addresses["Missable_Silph_Co_5F_Item_1"], + Missable(155)), + LocationData("Silph Co 5F", "Northwest Item (Card Key)", "Protein", rom_addresses["Missable_Silph_Co_5F_Item_2"], Missable(156)), + LocationData("Silph Co 5F", "Southeast Item", "Card Key", rom_addresses["Missable_Silph_Co_5F_Item_3"], Missable(157)), + LocationData("Silph Co 6F", "West Item (Card Key)", "HP Up", rom_addresses["Missable_Silph_Co_6F_Item_1"], Missable(161)), + LocationData("Silph Co 6F", "Southwest Item (Card Key)", "X Accuracy", rom_addresses["Missable_Silph_Co_6F_Item_2"], + Missable(162)), + LocationData("Silph Co 7F", "West Item", "Calcium", rom_addresses["Missable_Silph_Co_7F_Item_1"], Missable(168)), + LocationData("Silph Co 7F", "East Item (Card Key)", "TM03 Swords Dance", rom_addresses["Missable_Silph_Co_7F_Item_2"], + Missable(169)), + LocationData("Silph Co 10F", "Left Item", "TM26 Earthquake", rom_addresses["Missable_Silph_Co_10F_Item_1"], + Missable(180)), + LocationData("Silph Co 10F", "Bottom Item", "Rare Candy", rom_addresses["Missable_Silph_Co_10F_Item_2"], + Missable(181)), + LocationData("Silph Co 10F", "Right Item", "Carbos", rom_addresses["Missable_Silph_Co_10F_Item_3"], Missable(182)), + LocationData("Pokemon Mansion 2F", "Northeast Item", "Calcium", rom_addresses["Missable_Pokemon_Mansion_2F_Item"], + Missable(187)), + LocationData("Pokemon Mansion 3F", "Southwest Item", "Max Potion", rom_addresses["Missable_Pokemon_Mansion_3F_Item_1"], + Missable(188)), + LocationData("Pokemon Mansion 3F", "Northeast Item", "Iron", rom_addresses["Missable_Pokemon_Mansion_3F_Item_2"], + Missable(189)), + LocationData("Pokemon Mansion B1F", "North Item", "Rare Candy", + rom_addresses["Missable_Pokemon_Mansion_B1F_Item_1"], Missable(190)), + LocationData("Pokemon Mansion B1F", "Southwest Item", "Full Restore", + rom_addresses["Missable_Pokemon_Mansion_B1F_Item_2"], Missable(191)), + LocationData("Pokemon Mansion B1F", "South Item", "TM14 Blizzard", + rom_addresses["Missable_Pokemon_Mansion_B1F_Item_3"], Missable(192)), + LocationData("Pokemon Mansion B1F", "Northwest Item", "TM22 Solar Beam", + rom_addresses["Missable_Pokemon_Mansion_B1F_Item_4"], Missable(193)), + LocationData("Pokemon Mansion B1F", "West Item", "Secret Key", + rom_addresses["Missable_Pokemon_Mansion_B1F_Item_5"], Missable(194)), + LocationData("Safari Zone East", "Northeast Item", "Full Restore", rom_addresses["Missable_Safari_Zone_East_Item_1"], + Missable(195)), + LocationData("Safari Zone East", "West Item", "Max Potion", rom_addresses["Missable_Safari_Zone_East_Item_2"], + Missable(196)), + LocationData("Safari Zone East", "East Item", "Carbos", rom_addresses["Missable_Safari_Zone_East_Item_3"], + Missable(197)), + LocationData("Safari Zone East", "Center Item", "TM37 Egg Bomb", rom_addresses["Missable_Safari_Zone_East_Item_4"], + Missable(198)), + LocationData("Safari Zone North", "Northeast Item", "Protein", rom_addresses["Missable_Safari_Zone_North_Item_1"], + Missable(199)), + LocationData("Safari Zone North", "North Item", "TM40 Skull Bash", + rom_addresses["Missable_Safari_Zone_North_Item_2"], Missable(200)), + LocationData("Safari Zone West", "Southwest Item", "Max Potion", rom_addresses["Missable_Safari_Zone_West_Item_1"], + Missable(201)), + LocationData("Safari Zone West", "Northwest Item", "TM32 Double Team", + rom_addresses["Missable_Safari_Zone_West_Item_2"], Missable(202)), + LocationData("Safari Zone West", "Southeast Item", "Max Revive", rom_addresses["Missable_Safari_Zone_West_Item_3"], + Missable(203)), + LocationData("Safari Zone West", "Northeast Item", "Gold Teeth", rom_addresses["Missable_Safari_Zone_West_Item_4"], + Missable(204)), + LocationData("Safari Zone Center", "Island Item", "Nugget", rom_addresses["Missable_Safari_Zone_Center_Item"], + Missable(205)), + LocationData("Cerulean Cave 2F", "East Item", "PP Up", rom_addresses["Missable_Cerulean_Cave_2F_Item_1"], + Missable(206)), + LocationData("Cerulean Cave 2F", "Southwest Item", "Ultra Ball", rom_addresses["Missable_Cerulean_Cave_2F_Item_2"], + Missable(207)), + LocationData("Cerulean Cave 2F", "North Item", "Full Restore", rom_addresses["Missable_Cerulean_Cave_2F_Item_3"], + Missable(208)), + LocationData("Cerulean Cave B1F", "Center Item", "Ultra Ball", rom_addresses["Missable_Cerulean_Cave_B1F_Item_1"], + Missable(210)), + LocationData("Cerulean Cave B1F", "North Item", "Max Revive", rom_addresses["Missable_Cerulean_Cave_B1F_Item_2"], + Missable(211)), + LocationData("Victory Road 1F", "Top Item", "TM43 Sky Attack", rom_addresses["Missable_Victory_Road_1F_Item_1"], + Missable(212)), + LocationData("Victory Road 1F", "Left Item", "Rare Candy", rom_addresses["Missable_Victory_Road_1F_Item_2"], + Missable(213)), + LocationData("Rock Tunnel B1F", "Southwest Item", "Hideout Key", rom_addresses["Missable_Rock_Tunnel_B1F_Item_1"], + Missable(231)), + LocationData("Rock Tunnel B1F", "West Item", "Mansion Key", rom_addresses["Missable_Rock_Tunnel_B1F_Item_2"], + Missable(232)), + LocationData("Rock Tunnel B1F", "Northwest Item", "Plant Key", rom_addresses["Missable_Rock_Tunnel_B1F_Item_3"], + Missable(233)), + LocationData("Rock Tunnel B1F", "North Item", "Safari Pass", rom_addresses["Missable_Rock_Tunnel_B1F_Item_4"], + Missable(234)), + + LocationData("Pewter Gym", "Brock 1", "Boulder Badge", rom_addresses['Badge_Pewter_Gym'], EventFlag(0x8A0)), + LocationData("Cerulean Gym", "Misty 1", "Cascade Badge", rom_addresses['Badge_Cerulean_Gym'], EventFlag(0x8A1)), + LocationData("Vermilion Gym", "Lt. Surge 1", "Thunder Badge", rom_addresses['Badge_Vermilion_Gym'], EventFlag(0x8A2)), + LocationData("Celadon Gym", "Erika 1", "Rainbow Badge", rom_addresses['Badge_Celadon_Gym'], EventFlag(0x8A3)), + LocationData("Fuchsia Gym", "Koga 1", "Soul Badge", rom_addresses['Badge_Fuchsia_Gym'], EventFlag(0x8A4)), + LocationData("Saffron Gym", "Sabrina 1", "Marsh Badge", rom_addresses['Badge_Saffron_Gym'], EventFlag(0x8A5)), + LocationData("Cinnabar Gym", "Blaine 1", "Volcano Badge", rom_addresses['Badge_Cinnabar_Gym'], EventFlag(0x8A6)), + LocationData("Viridian Gym", "Giovanni 1", "Earth Badge", rom_addresses['Badge_Viridian_Gym'], EventFlag(0x8A7)), + + LocationData("Viridian Forest", "Hidden Item Northwest by Trainer", "Potion", rom_addresses['Hidden_Item_Viridian_Forest_1'], Hidden(0)), + LocationData("Viridian Forest", "Hidden Item Entrance Tree", "Antidote", rom_addresses['Hidden_Item_Viridian_Forest_2'], Hidden(1)), + LocationData("Mt Moon B2F", "Hidden Item Dead End Before Fossils", "Moon Stone", rom_addresses['Hidden_Item_MtMoonB2F_1'], Hidden(2)), + LocationData("Route 25", "Hidden Item Fence Outside Bill's House", "Ether", rom_addresses['Hidden_Item_Route_25_1'], Hidden(3)), + LocationData("Route 9", "Hidden Item Rock By Grass", "Ether", rom_addresses['Hidden_Item_Route_9'], Hidden(4)), + LocationData("S.S. Anne 1F", "Hidden Item Kitchen Trash", "Great Ball", rom_addresses['Hidden_Item_SS_Anne_Kitchen'], Hidden(5)), + LocationData("S.S. Anne B1F", "Hidden Item Under Pillow", "Hyper Potion", rom_addresses['Hidden_Item_SS_Anne_B1F'], Hidden(6)), + LocationData("Route 10 North", "Hidden Item Behind Rock Tunnel Entrance Tree", "Super Potion", rom_addresses['Hidden_Item_Route_10_1'], Hidden(7)), + LocationData("Route 10 South", "Hidden Item Rock", "Max Ether", rom_addresses['Hidden_Item_Route_10_2'], Hidden(8)), + LocationData("Rocket Hideout B1F", "Hidden Item Pot Plant", "PP Up", rom_addresses['Hidden_Item_Rocket_Hideout_B1F'], Hidden(9)), + LocationData("Rocket Hideout B3F", "Hidden Item Near East Item", "Nugget", rom_addresses['Hidden_Item_Rocket_Hideout_B3F'], Hidden(10)), + LocationData("Rocket Hideout B4F", "Hidden Item Behind Giovanni", "Super Potion", rom_addresses['Hidden_Item_Rocket_Hideout_B4F'], Hidden(11)), + LocationData("Pokemon Tower 5F", "Hidden Item Near West Staircase", "Elixir", rom_addresses['Hidden_Item_Pokemon_Tower_5F'], Hidden(12)), + LocationData("Route 13", "Hidden Item Dead End Boulder", "PP Up", rom_addresses['Hidden_Item_Route_13_1'], Hidden(13)), + LocationData("Route 13", "Hidden Item Dead End By Water Corner", "Calcium", rom_addresses['Hidden_Item_Route_13_2'], Hidden(14)), + LocationData("Pokemon Mansion B1F", "Hidden Item Secret Key Room Corner", "Rare Candy", rom_addresses['Hidden_Item_Pokemon_Mansion_B1F'], Hidden(15)), + LocationData("Safari Zone West", "Hidden Item Secret House Statue", "Revive", rom_addresses['Hidden_Item_Safari_Zone_West'], Hidden(17)), + LocationData("Silph Co 5F", "Hidden Item Pot Plant", "Elixir", rom_addresses['Hidden_Item_Silph_Co_5F'], Hidden(18)), + LocationData("Silph Co 9F", "Hidden Item Nurse Bed", "Max Potion", rom_addresses['Hidden_Item_Silph_Co_9F'], Hidden(19)), + LocationData("Copycat's House", "Hidden Item Desk", "Nugget", rom_addresses['Hidden_Item_Copycats_House'], Hidden(20)), + LocationData("Cerulean Cave 1F", "Hidden Item Center Rocks", "Rare Candy", rom_addresses['Hidden_Item_Cerulean_Cave_1F'], Hidden(21)), + LocationData("Cerulean Cave B1F", "Hidden Item Northeast Rocks", "Ultra Ball", rom_addresses['Hidden_Item_Cerulean_Cave_B1F'], Hidden(22)), + LocationData("Power Plant", "Hidden Item Central Dead End", "Max Elixir", rom_addresses['Hidden_Item_Power_Plant_1'], Hidden(23)), + LocationData("Power Plant", "Hidden Item Before Zapdos", "PP Up", rom_addresses['Hidden_Item_Power_Plant_2'], Hidden(24)), + LocationData("Seafoam Islands B2F", "Hidden Item Rock", "Nugget", rom_addresses['Hidden_Item_Seafoam_Islands_B2F'], Hidden(25)), + LocationData("Seafoam Islands B4F", "Hidden Item Corner Island", "Ultra Ball", rom_addresses['Hidden_Item_Seafoam_Islands_B4F'], Hidden(26)), + LocationData("Pokemon Mansion 1F", "Hidden Item Block Near Entrance Carpet", "Moon Stone", rom_addresses['Hidden_Item_Pokemon_Mansion_1F'], Hidden(27)), + LocationData("Pokemon Mansion 3F", "Hidden Item Behind Burglar", "Max Revive", rom_addresses['Hidden_Item_Pokemon_Mansion_3F'], Hidden(28)), + LocationData("Route 23 North", "Hidden Item Rocks Before Final Guard", "Full Restore", rom_addresses['Hidden_Item_Route_23_1'], Hidden(29)), + LocationData("Route 23 North", "Hidden Item East Tree After Water", "Ultra Ball", rom_addresses['Hidden_Item_Route_23_2'], Hidden(30)), + LocationData("Route 23 South", "Hidden Item On Island", "Max Ether", rom_addresses['Hidden_Item_Route_23_3'], Hidden(31)), + LocationData("Victory Road 2F", "Hidden Item Rock Before Moltres", "Ultra Ball", rom_addresses['Hidden_Item_Victory_Road_2F_1'], Hidden(32)), + LocationData("Victory Road 2F", "Hidden Item Rock In Final Room", "Full Restore", rom_addresses['Hidden_Item_Victory_Road_2F_2'], Hidden(33)), + #LocationData("Vermilion City", "Hidden Item The Truck", "Max Elixir", rom_addresses['Hidden_Item_Unused_6F'], Hidden(34)), + LocationData("Viridian City", "Hidden Item Cuttable Tree", "Potion", rom_addresses['Hidden_Item_Viridian_City'], Hidden(35)), + LocationData("Route 11", "Hidden Item Isolated Tree Near Gate", "Potion", rom_addresses['Hidden_Item_Route_11'], Hidden(36)), + LocationData("Route 12 West", "Hidden Item Tree Near Gate", "Hyper Potion", rom_addresses['Hidden_Item_Route_12'], Hidden(37)), + LocationData("Route 17", "Hidden Item In Grass", "Rare Candy", rom_addresses['Hidden_Item_Route_17_1'], Hidden(38)), + LocationData("Route 17", "Hidden Item Near Northernmost Sign", "Full Restore", rom_addresses['Hidden_Item_Route_17_2'], Hidden(39)), + LocationData("Route 17", "Hidden Item East Center", "PP Up", rom_addresses['Hidden_Item_Route_17_3'], Hidden(40)), + LocationData("Route 17", "Hidden Item West Center", "Max Revive", rom_addresses['Hidden_Item_Route_17_4'], Hidden(41)), + LocationData("Route 17", "Hidden Item Before Final Bridge", "Max Elixir", rom_addresses['Hidden_Item_Route_17_5'], Hidden(42)), + LocationData("Underground Tunnel North-South", "Hidden Item Near Northern Stairs", "Full Restore", rom_addresses['Hidden_Item_Underground_Path_NS_1'], Hidden(43)), + LocationData("Underground Tunnel North-South", "Hidden Item Near Southern Stairs", "X Special", rom_addresses['Hidden_Item_Underground_Path_NS_2'], Hidden(44)), + LocationData("Underground Tunnel West-East", "Hidden Item West", "Nugget", rom_addresses['Hidden_Item_Underground_Path_WE_1'], Hidden(45)), + LocationData("Underground Tunnel West-East", "Hidden Item East", "Elixir", rom_addresses['Hidden_Item_Underground_Path_WE_2'], Hidden(46)), + LocationData("Celadon City", "Hidden Item Dead End Near Cuttable Tree", "PP Up", rom_addresses['Hidden_Item_Celadon_City'], Hidden(47)), + LocationData("Route 25", "Hidden Item Northeast Of Grass", "Elixir", rom_addresses['Hidden_Item_Route_25_2'], Hidden(48)), + LocationData("Mt Moon B2F", "Hidden Item Lone Rock", "Ether", rom_addresses['Hidden_Item_MtMoonB2F_2'], Hidden(49)), + LocationData("Seafoam Islands B3F", "Hidden Item Rock", "Max Elixir", rom_addresses['Hidden_Item_Seafoam_Islands_B3F'], Hidden(50)), + LocationData("Vermilion City", "Hidden Item In Water Near Fan Club", "Max Ether", rom_addresses['Hidden_Item_Vermilion_City'], Hidden(51)), + LocationData("Cerulean City", "Hidden Item Gym Badge Guy's Backyard", "Rare Candy", rom_addresses['Hidden_Item_Cerulean_City'], Hidden(52)), + LocationData("Route 4", "Hidden Item Plateau East Of Mt Moon", "Great Ball", rom_addresses['Hidden_Item_Route_4'], Hidden(53)), + + LocationData("Indigo Plateau", "Become Champion", "Become Champion", event=True), + LocationData("Pokemon Tower 7F", "Fuji Saved", "Fuji Saved", event=True), + LocationData("Silph Co 11F", "Silph Co Liberated", "Silph Co Liberated", event=True), + + LocationData("Pallet Town", "Super Rod Pokemon - 1", "Tentacool", rom_addresses["Wild_Super_Rod_A"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Pallet Town", "Super Rod Pokemon - 2", "Poliwag", rom_addresses["Wild_Super_Rod_A"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Route 22", "Super Rod Pokemon - 1", "Goldeen", rom_addresses["Wild_Super_Rod_B"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 22", "Super Rod Pokemon - 2", "Poliwag", rom_addresses["Wild_Super_Rod_B"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Route 24", "Super Rod Pokemon - 1", "Psyduck", rom_addresses["Wild_Super_Rod_C"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 24", "Super Rod Pokemon - 2", "Goldeen", rom_addresses["Wild_Super_Rod_C"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Route 24", "Super Rod Pokemon - 3", "Krabby", rom_addresses["Wild_Super_Rod_C"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Route 6", "Super Rod Pokemon - 1", "Krabby", rom_addresses["Wild_Super_Rod_D"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 6", "Super Rod Pokemon - 2", "Shellder", rom_addresses["Wild_Super_Rod_D"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Route 10 North", "Super Rod Pokemon - 1", "Poliwhirl", rom_addresses["Wild_Super_Rod_E"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 10 North", "Super Rod Pokemon - 2", "Slowpoke", rom_addresses["Wild_Super_Rod_E"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Safari Zone Center", "Super Rod Pokemon - 1", "Dratini", rom_addresses["Wild_Super_Rod_F"] + 1, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone Center", "Super Rod Pokemon - 2", "Krabby", rom_addresses["Wild_Super_Rod_F"] + 3, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone Center", "Super Rod Pokemon - 3", "Psyduck", rom_addresses["Wild_Super_Rod_F"] + 5, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone Center", "Super Rod Pokemon - 4", "Slowpoke", rom_addresses["Wild_Super_Rod_F"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Route 12 North", "Super Rod Pokemon - 1", "Tentacool", rom_addresses["Wild_Super_Rod_G"] + 1, + None, event=True, type="Wild Encounter"), + LocationData("Route 12 North", "Super Rod Pokemon - 2", "Krabby", rom_addresses["Wild_Super_Rod_G"] + 3, + None, event=True, type="Wild Encounter"), + LocationData("Route 12 North", "Super Rod Pokemon - 3", "Goldeen", rom_addresses["Wild_Super_Rod_G"] + 5, + None, event=True, type="Wild Encounter"), + LocationData("Route 12 North", "Super Rod Pokemon - 4", "Magikarp", rom_addresses["Wild_Super_Rod_G"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Route 19", "Super Rod Pokemon - 1", "Staryu", rom_addresses["Wild_Super_Rod_H"] + 1, + None, event=True, type="Wild Encounter"), + LocationData("Route 19", "Super Rod Pokemon - 2", "Horsea", rom_addresses["Wild_Super_Rod_H"] + 3, + None, event=True, type="Wild Encounter"), + LocationData("Route 19", "Super Rod Pokemon - 3", "Shellder", rom_addresses["Wild_Super_Rod_H"] + 5, + None, event=True, type="Wild Encounter"), + LocationData("Route 19", "Super Rod Pokemon - 4", "Goldeen", rom_addresses["Wild_Super_Rod_H"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Route 23 South", "Super Rod Pokemon - 1", "Slowbro", rom_addresses["Wild_Super_Rod_I"] + 1, + None, event=True, type="Wild Encounter"), + LocationData("Route 23 South", "Super Rod Pokemon - 2", "Seaking", rom_addresses["Wild_Super_Rod_I"] + 3, + None, event=True, type="Wild Encounter"), + LocationData("Route 23 South", "Super Rod Pokemon - 3", "Kingler", rom_addresses["Wild_Super_Rod_I"] + 5, + None, event=True, type="Wild Encounter"), + LocationData("Route 23 South", "Super Rod Pokemon - 4", "Seadra", rom_addresses["Wild_Super_Rod_I"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Fuchsia City", "Super Rod Pokemon - 1", "Seaking", rom_addresses["Wild_Super_Rod_J"] + 1, + None, event=True, type="Wild Encounter"), + LocationData("Fuchsia City", "Super Rod Pokemon - 2", "Krabby", rom_addresses["Wild_Super_Rod_J"] + 3, + None, event=True, type="Wild Encounter"), + LocationData("Fuchsia City", "Super Rod Pokemon - 3", "Goldeen", rom_addresses["Wild_Super_Rod_J"] + 5, + None, event=True, type="Wild Encounter"), + LocationData("Fuchsia City", "Super Rod Pokemon - 4", "Magikarp", rom_addresses["Wild_Super_Rod_J"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Route 1", "Wild Pokemon - 1", "Pidgey", rom_addresses["Wild_Route1"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Route 1", "Wild Pokemon - 2", "Rattata", rom_addresses["Wild_Route1"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 1", "Wild Pokemon - 3", "Rattata", rom_addresses["Wild_Route1"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 1", "Wild Pokemon - 4", "Rattata", rom_addresses["Wild_Route1"] + 7, None, event=True, + type="Wild Encounter"), + LocationData("Route 1", "Wild Pokemon - 5", "Pidgey", rom_addresses["Wild_Route1"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 1", "Wild Pokemon - 6", "Pidgey", rom_addresses["Wild_Route1"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Route 1", "Wild Pokemon - 7", "Pidgey", rom_addresses["Wild_Route1"] + 13, None, event=True, + type="Wild Encounter"), + LocationData("Route 1", "Wild Pokemon - 8", "Rattata", rom_addresses["Wild_Route1"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 1", "Wild Pokemon - 9", "Pidgey", rom_addresses["Wild_Route1"] + 17, None, event=True, + type="Wild Encounter"), + LocationData("Route 1", "Wild Pokemon - 10", "Pidgey", rom_addresses["Wild_Route1"] + 19, None, event=True, + type="Wild Encounter"), + LocationData("Route 2", "Wild Pokemon - 1", "Rattata", rom_addresses["Wild_Route2"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Route 2", "Wild Pokemon - 2", "Pidgey", rom_addresses["Wild_Route2"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 2", "Wild Pokemon - 3", "Pidgey", rom_addresses["Wild_Route2"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 2", "Wild Pokemon - 4", "Rattata", rom_addresses["Wild_Route2"] + 7, None, event=True, + type="Wild Encounter"), + LocationData("Route 2", "Wild Pokemon - 5", "Pidgey", rom_addresses["Wild_Route2"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 2", "Wild Pokemon - 6", ["Weedle", "Caterpie"], rom_addresses["Wild_Route2"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Route 2", "Wild Pokemon - 7", "Rattata", rom_addresses["Wild_Route2"] + 13, None, event=True, + type="Wild Encounter"), + LocationData("Route 2", "Wild Pokemon - 8", "Rattata", rom_addresses["Wild_Route2"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 2", "Wild Pokemon - 9", ["Weedle", "Caterpie"], rom_addresses["Wild_Route2"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Route 2", "Wild Pokemon - 10", ["Weedle", "Caterpie"], rom_addresses["Wild_Route2"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Route 22", "Wild Pokemon - 1", "Rattata", rom_addresses["Wild_Route22"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Route 22", "Wild Pokemon - 2", ["Nidoran M", "Nidoran F"], rom_addresses["Wild_Route22"] + 3, + None, event=True, type="Wild Encounter"), + LocationData("Route 22", "Wild Pokemon - 3", "Rattata", rom_addresses["Wild_Route22"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 22", "Wild Pokemon - 4", ["Nidoran M", "Nidoran F"], rom_addresses["Wild_Route22"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Route 22", "Wild Pokemon - 5", "Rattata", rom_addresses["Wild_Route22"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 22", "Wild Pokemon - 6", ["Nidoran M", "Nidoran F"], rom_addresses["Wild_Route22"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Route 22", "Wild Pokemon - 7", "Spearow", rom_addresses["Wild_Route22"] + 13, None, event=True, + type="Wild Encounter"), + LocationData("Route 22", "Wild Pokemon - 8", "Spearow", rom_addresses["Wild_Route22"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 22", "Wild Pokemon - 9", ["Nidoran F", "Nidoran M"], rom_addresses["Wild_Route22"] + 17, + None, event=True, type="Wild Encounter"), + LocationData("Route 22", "Wild Pokemon - 10", ["Nidoran F", "Nidoran M"], rom_addresses["Wild_Route22"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Viridian Forest", "Wild Pokemon - 1", ["Weedle", "Caterpie"], + rom_addresses["Wild_ViridianForest"] + 1, None, event=True, type="Wild Encounter"), + LocationData("Viridian Forest", "Wild Pokemon - 2", ["Kakuna", "Metapod"], + rom_addresses["Wild_ViridianForest"] + 3, None, event=True, type="Wild Encounter"), + LocationData("Viridian Forest", "Wild Pokemon - 3", ["Weedle", "Caterpie"], + rom_addresses["Wild_ViridianForest"] + 5, None, event=True, type="Wild Encounter"), + LocationData("Viridian Forest", "Wild Pokemon - 4", ["Weedle", "Caterpie"], + rom_addresses["Wild_ViridianForest"] + 7, None, event=True, type="Wild Encounter"), + LocationData("Viridian Forest", "Wild Pokemon - 5", ["Kakuna", "Metapod"], + rom_addresses["Wild_ViridianForest"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Viridian Forest", "Wild Pokemon - 6", ["Kakuna", "Metapod"], + rom_addresses["Wild_ViridianForest"] + 11, None, event=True, type="Wild Encounter"), + LocationData("Viridian Forest", "Wild Pokemon - 7", ["Metapod", "Kakuna"], + rom_addresses["Wild_ViridianForest"] + 13, None, event=True, type="Wild Encounter"), + LocationData("Viridian Forest", "Wild Pokemon - 8", ["Caterpie", "Weedle"], + rom_addresses["Wild_ViridianForest"] + 15, + None, event=True, type="Wild Encounter"), + LocationData("Viridian Forest", "Wild Pokemon - 9", "Pikachu", rom_addresses["Wild_ViridianForest"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Viridian Forest", "Wild Pokemon - 10", "Pikachu", rom_addresses["Wild_ViridianForest"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Route 3", "Wild Pokemon - 1", "Pidgey", rom_addresses["Wild_Route3"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Route 3", "Wild Pokemon - 2", "Spearow", rom_addresses["Wild_Route3"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 3", "Wild Pokemon - 3", "Pidgey", rom_addresses["Wild_Route3"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 3", "Wild Pokemon - 4", "Spearow", rom_addresses["Wild_Route3"] + 7, None, event=True, + type="Wild Encounter"), + LocationData("Route 3", "Wild Pokemon - 5", "Spearow", rom_addresses["Wild_Route3"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 3", "Wild Pokemon - 6", "Pidgey", rom_addresses["Wild_Route3"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Route 3", "Wild Pokemon - 7", "Spearow", rom_addresses["Wild_Route3"] + 13, None, event=True, + type="Wild Encounter"), + LocationData("Route 3", "Wild Pokemon - 8", "Jigglypuff", rom_addresses["Wild_Route3"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 3", "Wild Pokemon - 9", "Jigglypuff", rom_addresses["Wild_Route3"] + 17, None, event=True, + type="Wild Encounter"), + LocationData("Route 3", "Wild Pokemon - 10", "Jigglypuff", rom_addresses["Wild_Route3"] + 19, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon 1F", "Wild Pokemon - 1", "Zubat", rom_addresses["Wild_MtMoon1F"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon 1F", "Wild Pokemon - 2", "Zubat", rom_addresses["Wild_MtMoon1F"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon 1F", "Wild Pokemon - 3", "Zubat", rom_addresses["Wild_MtMoon1F"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon 1F", "Wild Pokemon - 4", "Geodude", rom_addresses["Wild_MtMoon1F"] + 7, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon 1F", "Wild Pokemon - 5", "Zubat", rom_addresses["Wild_MtMoon1F"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon 1F", "Wild Pokemon - 6", "Zubat", rom_addresses["Wild_MtMoon1F"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon 1F", "Wild Pokemon - 7", "Geodude", rom_addresses["Wild_MtMoon1F"] + 13, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon 1F", "Wild Pokemon - 8", "Paras", rom_addresses["Wild_MtMoon1F"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon 1F", "Wild Pokemon - 9", "Zubat", rom_addresses["Wild_MtMoon1F"] + 17, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon 1F", "Wild Pokemon - 10", "Clefairy", rom_addresses["Wild_MtMoon1F"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Mt Moon B1F", "Wild Pokemon - 1", "Zubat", rom_addresses["Wild_MtMoonB1F"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon B1F", "Wild Pokemon - 2", "Zubat", rom_addresses["Wild_MtMoonB1F"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon B1F", "Wild Pokemon - 3", "Geodude", rom_addresses["Wild_MtMoonB1F"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Mt Moon B1F", "Wild Pokemon - 4", "Geodude", rom_addresses["Wild_MtMoonB1F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Mt Moon B1F", "Wild Pokemon - 5", "Zubat", rom_addresses["Wild_MtMoonB1F"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon B1F", "Wild Pokemon - 6", "Paras", rom_addresses["Wild_MtMoonB1F"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon B1F", "Wild Pokemon - 7", "Zubat", rom_addresses["Wild_MtMoonB1F"] + 13, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon B1F", "Wild Pokemon - 8", "Zubat", rom_addresses["Wild_MtMoonB1F"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon B1F", "Wild Pokemon - 9", "Clefairy", rom_addresses["Wild_MtMoonB1F"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Mt Moon B1F", "Wild Pokemon - 10", "Geodude", rom_addresses["Wild_MtMoonB1F"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Mt Moon B2F", "Wild Pokemon - 1", "Zubat", rom_addresses["Wild_MtMoonB2F"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon B2F", "Wild Pokemon - 2", "Geodude", rom_addresses["Wild_MtMoonB2F"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Mt Moon B2F", "Wild Pokemon - 3", "Zubat", rom_addresses["Wild_MtMoonB2F"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon B2F", "Wild Pokemon - 4", "Geodude", rom_addresses["Wild_MtMoonB2F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Mt Moon B2F", "Wild Pokemon - 5", "Zubat", rom_addresses["Wild_MtMoonB2F"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon B2F", "Wild Pokemon - 6", "Paras", rom_addresses["Wild_MtMoonB2F"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon B2F", "Wild Pokemon - 7", "Paras", rom_addresses["Wild_MtMoonB2F"] + 13, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon B2F", "Wild Pokemon - 8", "Clefairy", rom_addresses["Wild_MtMoonB2F"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Mt Moon B2F", "Wild Pokemon - 9", "Zubat", rom_addresses["Wild_MtMoonB2F"] + 17, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon B2F", "Wild Pokemon - 10", "Clefairy", rom_addresses["Wild_MtMoonB2F"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Route 4", "Wild Pokemon - 1", "Rattata", rom_addresses["Wild_Route4"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Route 4", "Wild Pokemon - 2", "Spearow", rom_addresses["Wild_Route4"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 4", "Wild Pokemon - 3", "Rattata", rom_addresses["Wild_Route4"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 4", "Wild Pokemon - 4", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route4"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Route 4", "Wild Pokemon - 5", "Spearow", rom_addresses["Wild_Route4"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 4", "Wild Pokemon - 6", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route4"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Route 4", "Wild Pokemon - 7", "Rattata", rom_addresses["Wild_Route4"] + 13, None, event=True, + type="Wild Encounter"), + LocationData("Route 4", "Wild Pokemon - 8", "Spearow", rom_addresses["Wild_Route4"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 4", "Wild Pokemon - 9", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route4"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Route 4", "Wild Pokemon - 10", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route4"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Route 24", "Wild Pokemon - 1", ["Weedle", "Caterpie"], rom_addresses["Wild_Route24"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 24", "Wild Pokemon - 2", ["Kakuna", "Metapod"], rom_addresses["Wild_Route24"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Route 24", "Wild Pokemon - 3", "Pidgey", rom_addresses["Wild_Route24"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 24", "Wild Pokemon - 4", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route24"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Route 24", "Wild Pokemon - 5", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route24"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Route 24", "Wild Pokemon - 6", "Abra", rom_addresses["Wild_Route24"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Route 24", "Wild Pokemon - 7", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route24"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Route 24", "Wild Pokemon - 8", "Pidgey", rom_addresses["Wild_Route24"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 24", "Wild Pokemon - 9", "Abra", rom_addresses["Wild_Route24"] + 17, None, event=True, + type="Wild Encounter"), + LocationData("Route 24", "Wild Pokemon - 10", "Abra", rom_addresses["Wild_Route24"] + 19, None, event=True, + type="Wild Encounter"), + LocationData("Route 25", "Wild Pokemon - 1", ["Weedle", "Caterpie"], rom_addresses["Wild_Route25"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 25", "Wild Pokemon - 2", ["Kakuna", "Metapod"], rom_addresses["Wild_Route25"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Route 25", "Wild Pokemon - 3", "Pidgey", rom_addresses["Wild_Route25"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 25", "Wild Pokemon - 4", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route25"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Route 25", "Wild Pokemon - 5", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route25"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Route 25", "Wild Pokemon - 6", "Abra", rom_addresses["Wild_Route25"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Route 25", "Wild Pokemon - 7", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route25"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Route 25", "Wild Pokemon - 8", "Abra", rom_addresses["Wild_Route25"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 25", "Wild Pokemon - 9", ["Metapod", "Kakuna"], rom_addresses["Wild_Route25"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Route 25", "Wild Pokemon - 10", ["Caterpie", "Weedle"], rom_addresses["Wild_Route25"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Route 9", "Wild Pokemon - 1", "Rattata", rom_addresses["Wild_Route9"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Route 9", "Wild Pokemon - 2", "Spearow", rom_addresses["Wild_Route9"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 9", "Wild Pokemon - 3", "Rattata", rom_addresses["Wild_Route9"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 9", "Wild Pokemon - 4", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route9"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Route 9", "Wild Pokemon - 5", "Spearow", rom_addresses["Wild_Route9"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 9", "Wild Pokemon - 6", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route9"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Route 9", "Wild Pokemon - 7", "Rattata", rom_addresses["Wild_Route9"] + 13, None, event=True, + type="Wild Encounter"), + LocationData("Route 9", "Wild Pokemon - 8", "Spearow", rom_addresses["Wild_Route9"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 9", "Wild Pokemon - 9", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route9"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Route 9", "Wild Pokemon - 10", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route9"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Route 5", "Wild Pokemon - 1", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route5"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 5", "Wild Pokemon - 2", "Pidgey", rom_addresses["Wild_Route5"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 5", "Wild Pokemon - 3", "Pidgey", rom_addresses["Wild_Route5"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 5", "Wild Pokemon - 4", ["Mankey", "Meowth"], rom_addresses["Wild_Route5"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Route 5", "Wild Pokemon - 5", ["Mankey", "Meowth"], rom_addresses["Wild_Route5"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Route 5", "Wild Pokemon - 6", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route5"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Route 5", "Wild Pokemon - 7", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route5"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Route 5", "Wild Pokemon - 8", "Pidgey", rom_addresses["Wild_Route5"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 5", "Wild Pokemon - 9", ["Mankey", "Meowth"], rom_addresses["Wild_Route5"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Route 5", "Wild Pokemon - 10", ["Mankey", "Meowth"], rom_addresses["Wild_Route5"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Route 6", "Wild Pokemon - 1", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route6"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 6", "Wild Pokemon - 2", "Pidgey", rom_addresses["Wild_Route6"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 6", "Wild Pokemon - 3", "Pidgey", rom_addresses["Wild_Route6"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 6", "Wild Pokemon - 4", ["Mankey", "Meowth"], rom_addresses["Wild_Route6"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Route 6", "Wild Pokemon - 5", ["Mankey", "Meowth"], rom_addresses["Wild_Route6"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Route 6", "Wild Pokemon - 6", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route6"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Route 6", "Wild Pokemon - 7", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route6"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Route 6", "Wild Pokemon - 8", "Pidgey", rom_addresses["Wild_Route6"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 6", "Wild Pokemon - 9", ["Mankey", "Meowth"], rom_addresses["Wild_Route6"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Route 6", "Wild Pokemon - 10", ["Mankey", "Meowth"], rom_addresses["Wild_Route6"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Route 11", "Wild Pokemon - 1", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route11"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 11", "Wild Pokemon - 2", "Spearow", rom_addresses["Wild_Route11"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 11", "Wild Pokemon - 3", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route11"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Route 11", "Wild Pokemon - 4", "Drowzee", rom_addresses["Wild_Route11"] + 7, None, event=True, + type="Wild Encounter"), + LocationData("Route 11", "Wild Pokemon - 5", "Spearow", rom_addresses["Wild_Route11"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 11", "Wild Pokemon - 6", "Drowzee", rom_addresses["Wild_Route11"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Route 11", "Wild Pokemon - 7", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route11"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Route 11", "Wild Pokemon - 8", "Spearow", rom_addresses["Wild_Route11"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 11", "Wild Pokemon - 9", "Drowzee", rom_addresses["Wild_Route11"] + 17, None, event=True, + type="Wild Encounter"), + LocationData("Route 11", "Wild Pokemon - 10", "Drowzee", rom_addresses["Wild_Route11"] + 19, None, event=True, + type="Wild Encounter"), + LocationData("Rock Tunnel 1F", "Wild Pokemon - 1", "Zubat", rom_addresses["Wild_RockTunnel1F"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel 1F", "Wild Pokemon - 2", "Zubat", rom_addresses["Wild_RockTunnel1F"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel 1F", "Wild Pokemon - 3", "Geodude", rom_addresses["Wild_RockTunnel1F"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel 1F", "Wild Pokemon - 4", "Machop", rom_addresses["Wild_RockTunnel1F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel 1F", "Wild Pokemon - 5", "Geodude", rom_addresses["Wild_RockTunnel1F"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel 1F", "Wild Pokemon - 6", "Zubat", rom_addresses["Wild_RockTunnel1F"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel 1F", "Wild Pokemon - 7", "Zubat", rom_addresses["Wild_RockTunnel1F"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel 1F", "Wild Pokemon - 8", "Machop", rom_addresses["Wild_RockTunnel1F"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel 1F", "Wild Pokemon - 9", "Onix", rom_addresses["Wild_RockTunnel1F"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel 1F", "Wild Pokemon - 10", "Onix", rom_addresses["Wild_RockTunnel1F"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel B1F", "Wild Pokemon - 1", "Zubat", rom_addresses["Wild_RockTunnelB1F"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel B1F", "Wild Pokemon - 2", "Zubat", rom_addresses["Wild_RockTunnelB1F"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel B1F", "Wild Pokemon - 3", "Geodude", rom_addresses["Wild_RockTunnelB1F"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel B1F", "Wild Pokemon - 4", "Machop", rom_addresses["Wild_RockTunnelB1F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel B1F", "Wild Pokemon - 5", "Geodude", rom_addresses["Wild_RockTunnelB1F"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel B1F", "Wild Pokemon - 6", "Zubat", rom_addresses["Wild_RockTunnelB1F"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel B1F", "Wild Pokemon - 7", "Machop", rom_addresses["Wild_RockTunnelB1F"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel B1F", "Wild Pokemon - 8", "Onix", rom_addresses["Wild_RockTunnelB1F"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel B1F", "Wild Pokemon - 9", "Onix", rom_addresses["Wild_RockTunnelB1F"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel B1F", "Wild Pokemon - 10", "Geodude", rom_addresses["Wild_RockTunnelB1F"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Route 10 North", "Wild Pokemon - 1", "Voltorb", rom_addresses["Wild_Route10"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 10 North", "Wild Pokemon - 2", "Spearow", rom_addresses["Wild_Route10"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Route 10 North", "Wild Pokemon - 3", "Voltorb", rom_addresses["Wild_Route10"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Route 10 North", "Wild Pokemon - 4", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route10"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Route 10 North", "Wild Pokemon - 5", "Spearow", rom_addresses["Wild_Route10"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Route 10 North", "Wild Pokemon - 6", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route10"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Route 10 North", "Wild Pokemon - 7", "Voltorb", rom_addresses["Wild_Route10"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Route 10 North", "Wild Pokemon - 8", "Spearow", rom_addresses["Wild_Route10"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Route 10 North", "Wild Pokemon - 9", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route10"] + 17, + None, event=True, type="Wild Encounter"), + LocationData("Route 10 North", "Wild Pokemon - 10", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route10"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Route 12 Grass", "Wild Pokemon - 1", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route12"] + 1, + None, event=True, type="Wild Encounter"), + LocationData("Route 12 Grass", "Wild Pokemon - 2", "Pidgey", rom_addresses["Wild_Route12"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Route 12 Grass", "Wild Pokemon - 3", "Pidgey", rom_addresses["Wild_Route12"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Route 12 Grass", "Wild Pokemon - 4", "Venonat", rom_addresses["Wild_Route12"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Route 12 Grass", "Wild Pokemon - 5", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route12"] + 9, + None, event=True, type="Wild Encounter"), + LocationData("Route 12 Grass", "Wild Pokemon - 6", "Venonat", rom_addresses["Wild_Route12"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Route 12 Grass", "Wild Pokemon - 7", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route12"] + 13, + None, event=True, type="Wild Encounter"), + LocationData("Route 12 Grass", "Wild Pokemon - 8", "Pidgey", rom_addresses["Wild_Route12"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Route 12 Grass", "Wild Pokemon - 9", ["Gloom", "Weepinbell"], rom_addresses["Wild_Route12"] + 17, + None, event=True, type="Wild Encounter"), + LocationData("Route 12 Grass", "Wild Pokemon - 10", ["Gloom", "Weepinbell"], rom_addresses["Wild_Route12"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Route 8 Grass", "Wild Pokemon - 1", "Pidgey", rom_addresses["Wild_Route8"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Route 8 Grass", "Wild Pokemon - 2", ["Mankey", "Meowth"], rom_addresses["Wild_Route8"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Route 8 Grass", "Wild Pokemon - 3", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route8"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Route 8 Grass", "Wild Pokemon - 4", ["Growlithe", "Vulpix"], rom_addresses["Wild_Route8"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Route 8 Grass", "Wild Pokemon - 5", "Pidgey", rom_addresses["Wild_Route8"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 8 Grass", "Wild Pokemon - 6", ["Mankey", "Meowth"], rom_addresses["Wild_Route8"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Route 8 Grass", "Wild Pokemon - 7", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route8"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Route 8 Grass", "Wild Pokemon - 8", ["Growlithe", "Vulpix"], rom_addresses["Wild_Route8"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Route 8 Grass", "Wild Pokemon - 9", ["Growlithe", "Vulpix"], rom_addresses["Wild_Route8"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Route 8 Grass", "Wild Pokemon - 10", ["Growlithe", "Vulpix"], rom_addresses["Wild_Route8"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Route 7", "Wild Pokemon - 1", "Pidgey", rom_addresses["Wild_Route7"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Route 7", "Wild Pokemon - 2", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route7"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Route 7", "Wild Pokemon - 3", ["Mankey", "Meowth"], rom_addresses["Wild_Route7"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Route 7", "Wild Pokemon - 4", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route7"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Route 7", "Wild Pokemon - 5", "Pidgey", rom_addresses["Wild_Route7"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 7", "Wild Pokemon - 6", ["Mankey", "Meowth"], rom_addresses["Wild_Route7"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Route 7", "Wild Pokemon - 7", ["Growlithe", "Vulpix"], rom_addresses["Wild_Route7"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Route 7", "Wild Pokemon - 8", ["Growlithe", "Vulpix"], rom_addresses["Wild_Route7"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Route 7", "Wild Pokemon - 9", ["Mankey", "Meowth"], rom_addresses["Wild_Route7"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Route 7", "Wild Pokemon - 10", ["Mankey", "Meowth"], rom_addresses["Wild_Route7"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 3F", "Wild Pokemon - 1", "Gastly", rom_addresses["Wild_PokemonTower3F"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 3F", "Wild Pokemon - 2", "Gastly", rom_addresses["Wild_PokemonTower3F"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 3F", "Wild Pokemon - 3", "Gastly", rom_addresses["Wild_PokemonTower3F"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 3F", "Wild Pokemon - 4", "Gastly", rom_addresses["Wild_PokemonTower3F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 3F", "Wild Pokemon - 5", "Gastly", rom_addresses["Wild_PokemonTower3F"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 3F", "Wild Pokemon - 6", "Gastly", rom_addresses["Wild_PokemonTower3F"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 3F", "Wild Pokemon - 7", "Gastly", rom_addresses["Wild_PokemonTower3F"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 3F", "Wild Pokemon - 8", "Cubone", rom_addresses["Wild_PokemonTower3F"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 3F", "Wild Pokemon - 9", "Cubone", rom_addresses["Wild_PokemonTower3F"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 3F", "Wild Pokemon - 10", "Haunter", rom_addresses["Wild_PokemonTower3F"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 4F", "Wild Pokemon - 1", "Gastly", rom_addresses["Wild_PokemonTower4F"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 4F", "Wild Pokemon - 2", "Gastly", rom_addresses["Wild_PokemonTower4F"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 4F", "Wild Pokemon - 3", "Gastly", rom_addresses["Wild_PokemonTower4F"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 4F", "Wild Pokemon - 4", "Gastly", rom_addresses["Wild_PokemonTower4F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 4F", "Wild Pokemon - 5", "Gastly", rom_addresses["Wild_PokemonTower4F"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 4F", "Wild Pokemon - 6", "Gastly", rom_addresses["Wild_PokemonTower4F"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 4F", "Wild Pokemon - 7", "Haunter", rom_addresses["Wild_PokemonTower4F"] + 13, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 4F", "Wild Pokemon - 8", "Cubone", rom_addresses["Wild_PokemonTower4F"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 4F", "Wild Pokemon - 9", "Cubone", rom_addresses["Wild_PokemonTower4F"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 4F", "Wild Pokemon - 10", "Gastly", rom_addresses["Wild_PokemonTower4F"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 5F", "Wild Pokemon - 1", "Gastly", rom_addresses["Wild_PokemonTower5F"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 5F", "Wild Pokemon - 2", "Gastly", rom_addresses["Wild_PokemonTower5F"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 5F", "Wild Pokemon - 3", "Gastly", rom_addresses["Wild_PokemonTower5F"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 5F", "Wild Pokemon - 4", "Gastly", rom_addresses["Wild_PokemonTower5F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 5F", "Wild Pokemon - 5", "Gastly", rom_addresses["Wild_PokemonTower5F"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 5F", "Wild Pokemon - 6", "Gastly", rom_addresses["Wild_PokemonTower5F"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 5F", "Wild Pokemon - 7", "Haunter", rom_addresses["Wild_PokemonTower5F"] + 13, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 5F", "Wild Pokemon - 8", "Cubone", rom_addresses["Wild_PokemonTower5F"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 5F", "Wild Pokemon - 9", "Cubone", rom_addresses["Wild_PokemonTower5F"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 5F", "Wild Pokemon - 10", "Gastly", rom_addresses["Wild_PokemonTower5F"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 6F", "Wild Pokemon - 1", "Gastly", rom_addresses["Wild_PokemonTower6F"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 6F", "Wild Pokemon - 2", "Gastly", rom_addresses["Wild_PokemonTower6F"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 6F", "Wild Pokemon - 3", "Gastly", rom_addresses["Wild_PokemonTower6F"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 6F", "Wild Pokemon - 4", "Gastly", rom_addresses["Wild_PokemonTower6F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 6F", "Wild Pokemon - 5", "Gastly", rom_addresses["Wild_PokemonTower6F"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 6F", "Wild Pokemon - 6", "Gastly", rom_addresses["Wild_PokemonTower6F"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 6F", "Wild Pokemon - 7", "Haunter", rom_addresses["Wild_PokemonTower6F"] + 13, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 6F", "Wild Pokemon - 8", "Cubone", rom_addresses["Wild_PokemonTower6F"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 6F", "Wild Pokemon - 9", "Cubone", rom_addresses["Wild_PokemonTower6F"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 6F", "Wild Pokemon - 10", "Haunter", rom_addresses["Wild_PokemonTower6F"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 7F", "Wild Pokemon - 1", "Gastly", rom_addresses["Wild_PokemonTower7F"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 7F", "Wild Pokemon - 2", "Gastly", rom_addresses["Wild_PokemonTower7F"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 7F", "Wild Pokemon - 3", "Gastly", rom_addresses["Wild_PokemonTower7F"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 7F", "Wild Pokemon - 4", "Gastly", rom_addresses["Wild_PokemonTower7F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 7F", "Wild Pokemon - 5", "Gastly", rom_addresses["Wild_PokemonTower7F"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 7F", "Wild Pokemon - 6", "Haunter", rom_addresses["Wild_PokemonTower7F"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 7F", "Wild Pokemon - 7", "Cubone", rom_addresses["Wild_PokemonTower7F"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 7F", "Wild Pokemon - 8", "Cubone", rom_addresses["Wild_PokemonTower7F"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 7F", "Wild Pokemon - 9", "Haunter", rom_addresses["Wild_PokemonTower7F"] + 17, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 7F", "Wild Pokemon - 10", "Haunter", rom_addresses["Wild_PokemonTower7F"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Route 13", "Wild Pokemon - 1", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route13"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 13", "Wild Pokemon - 2", "Pidgey", rom_addresses["Wild_Route13"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 13", "Wild Pokemon - 3", "Pidgey", rom_addresses["Wild_Route13"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 13", "Wild Pokemon - 4", "Venonat", rom_addresses["Wild_Route13"] + 7, None, event=True, + type="Wild Encounter"), + LocationData("Route 13", "Wild Pokemon - 5", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route13"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Route 13", "Wild Pokemon - 6", "Venonat", rom_addresses["Wild_Route13"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Route 13", "Wild Pokemon - 7", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route13"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Route 13", "Wild Pokemon - 8", "Ditto", rom_addresses["Wild_Route13"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 13", "Wild Pokemon - 9", ["Gloom", "Weepinbell"], rom_addresses["Wild_Route13"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Route 13", "Wild Pokemon - 10", ["Gloom", "Weepinbell"], rom_addresses["Wild_Route13"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Route 14", "Wild Pokemon - 1", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route14"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 14", "Wild Pokemon - 2", "Pidgey", rom_addresses["Wild_Route14"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 14", "Wild Pokemon - 3", "Ditto", rom_addresses["Wild_Route14"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 14", "Wild Pokemon - 4", "Venonat", rom_addresses["Wild_Route14"] + 7, None, event=True, + type="Wild Encounter"), + LocationData("Route 14", "Wild Pokemon - 5", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route14"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Route 14", "Wild Pokemon - 6", "Venonat", rom_addresses["Wild_Route14"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Route 14", "Wild Pokemon - 7", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route14"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Route 14", "Wild Pokemon - 8", ["Gloom", "Weepinbell"], rom_addresses["Wild_Route14"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Route 14", "Wild Pokemon - 9", "Pidgeotto", rom_addresses["Wild_Route14"] + 17, None, event=True, + type="Wild Encounter"), + LocationData("Route 14", "Wild Pokemon - 10", "Pidgeotto", rom_addresses["Wild_Route14"] + 19, None, event=True, + type="Wild Encounter"), + LocationData("Route 15", "Wild Pokemon - 1", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route15"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 15", "Wild Pokemon - 2", "Ditto", rom_addresses["Wild_Route15"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 15", "Wild Pokemon - 3", "Pidgey", rom_addresses["Wild_Route15"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 15", "Wild Pokemon - 4", "Venonat", rom_addresses["Wild_Route15"] + 7, None, event=True, + type="Wild Encounter"), + LocationData("Route 15", "Wild Pokemon - 5", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route15"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Route 15", "Wild Pokemon - 6", "Venonat", rom_addresses["Wild_Route15"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Route 15", "Wild Pokemon - 7", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route15"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Route 15", "Wild Pokemon - 8", ["Gloom", "Weepinbell"], rom_addresses["Wild_Route15"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Route 15", "Wild Pokemon - 9", "Pidgeotto", rom_addresses["Wild_Route15"] + 17, None, event=True, + type="Wild Encounter"), + LocationData("Route 15", "Wild Pokemon - 10", "Pidgeotto", rom_addresses["Wild_Route15"] + 19, None, event=True, + type="Wild Encounter"), + LocationData("Route 16 North", "Wild Pokemon - 1", "Spearow", rom_addresses["Wild_Route16"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Route 16 North", "Wild Pokemon - 2", "Spearow", rom_addresses["Wild_Route16"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 16 North", "Wild Pokemon - 3", "Rattata", rom_addresses["Wild_Route16"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 16 North", "Wild Pokemon - 4", "Doduo", rom_addresses["Wild_Route16"] + 7, None, event=True, + type="Wild Encounter"), + LocationData("Route 16 North", "Wild Pokemon - 5", "Rattata", rom_addresses["Wild_Route16"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 16 North", "Wild Pokemon - 6", "Doduo", rom_addresses["Wild_Route16"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Route 16 North", "Wild Pokemon - 7", "Doduo", rom_addresses["Wild_Route16"] + 13, None, event=True, + type="Wild Encounter"), + LocationData("Route 16 North", "Wild Pokemon - 8", "Rattata", rom_addresses["Wild_Route16"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 16 North", "Wild Pokemon - 9", "Raticate", rom_addresses["Wild_Route16"] + 17, None, event=True, + type="Wild Encounter"), + LocationData("Route 16 North", "Wild Pokemon - 10", "Raticate", rom_addresses["Wild_Route16"] + 19, None, + event=True, + type="Wild Encounter"), + LocationData("Route 17", "Wild Pokemon - 1", "Spearow", rom_addresses["Wild_Route17"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Route 17", "Wild Pokemon - 2", "Spearow", rom_addresses["Wild_Route17"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 17", "Wild Pokemon - 3", "Raticate", rom_addresses["Wild_Route17"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 17", "Wild Pokemon - 4", "Doduo", rom_addresses["Wild_Route17"] + 7, None, event=True, + type="Wild Encounter"), + LocationData("Route 17", "Wild Pokemon - 5", "Raticate", rom_addresses["Wild_Route17"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 17", "Wild Pokemon - 6", "Doduo", rom_addresses["Wild_Route17"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Route 17", "Wild Pokemon - 7", "Doduo", rom_addresses["Wild_Route17"] + 13, None, event=True, + type="Wild Encounter"), + LocationData("Route 17", "Wild Pokemon - 8", "Raticate", rom_addresses["Wild_Route17"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 17", "Wild Pokemon - 9", "Fearow", rom_addresses["Wild_Route17"] + 17, None, event=True, + type="Wild Encounter"), + LocationData("Route 17", "Wild Pokemon - 10", "Fearow", rom_addresses["Wild_Route17"] + 19, None, event=True, + type="Wild Encounter"), + LocationData("Route 18", "Wild Pokemon - 1", "Spearow", rom_addresses["Wild_Route18"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Route 18", "Wild Pokemon - 2", "Spearow", rom_addresses["Wild_Route18"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 18", "Wild Pokemon - 3", "Raticate", rom_addresses["Wild_Route18"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 18", "Wild Pokemon - 4", "Doduo", rom_addresses["Wild_Route18"] + 7, None, event=True, + type="Wild Encounter"), + LocationData("Route 18", "Wild Pokemon - 5", "Fearow", rom_addresses["Wild_Route18"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 18", "Wild Pokemon - 6", "Doduo", rom_addresses["Wild_Route18"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Route 18", "Wild Pokemon - 7", "Doduo", rom_addresses["Wild_Route18"] + 13, None, event=True, + type="Wild Encounter"), + LocationData("Route 18", "Wild Pokemon - 8", "Raticate", rom_addresses["Wild_Route18"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 18", "Wild Pokemon - 9", "Fearow", rom_addresses["Wild_Route18"] + 17, None, event=True, + type="Wild Encounter"), + LocationData("Route 18", "Wild Pokemon - 10", "Fearow", rom_addresses["Wild_Route18"] + 19, None, event=True, + type="Wild Encounter"), + LocationData("Safari Zone Center", "Wild Pokemon - 1", ["Nidoran M", "Nidoran F"], + rom_addresses["Wild_SafariZoneCenter"] + 1, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone Center", "Wild Pokemon - 2", "Rhyhorn", rom_addresses["Wild_SafariZoneCenter"] + 3, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone Center", "Wild Pokemon - 3", "Venonat", rom_addresses["Wild_SafariZoneCenter"] + 5, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone Center", "Wild Pokemon - 4", "Exeggcute", rom_addresses["Wild_SafariZoneCenter"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone Center", "Wild Pokemon - 5", ["Nidorino", "Nidorina"], + rom_addresses["Wild_SafariZoneCenter"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone Center", "Wild Pokemon - 6", "Exeggcute", rom_addresses["Wild_SafariZoneCenter"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone Center", "Wild Pokemon - 7", ["Nidorina", "Nidorino"], + rom_addresses["Wild_SafariZoneCenter"] + 13, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone Center", "Wild Pokemon - 8", "Parasect", rom_addresses["Wild_SafariZoneCenter"] + 15, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone Center", "Wild Pokemon - 9", ["Scyther", "Pinsir"], + rom_addresses["Wild_SafariZoneCenter"] + 17, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone Center", "Wild Pokemon - 10", "Chansey", rom_addresses["Wild_SafariZoneCenter"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone East", "Wild Pokemon - 1", ["Nidoran M", "Nidoran F"], + rom_addresses["Wild_SafariZoneEast"] + 1, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone East", "Wild Pokemon - 2", "Doduo", rom_addresses["Wild_SafariZoneEast"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Safari Zone East", "Wild Pokemon - 3", "Paras", rom_addresses["Wild_SafariZoneEast"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Safari Zone East", "Wild Pokemon - 4", "Exeggcute", rom_addresses["Wild_SafariZoneEast"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone East", "Wild Pokemon - 5", ["Nidorino", "Nidorina"], + rom_addresses["Wild_SafariZoneEast"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone East", "Wild Pokemon - 6", "Exeggcute", rom_addresses["Wild_SafariZoneEast"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone East", "Wild Pokemon - 7", ["Nidoran F", "Nidoran M"], + rom_addresses["Wild_SafariZoneEast"] + 13, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone East", "Wild Pokemon - 8", "Parasect", rom_addresses["Wild_SafariZoneEast"] + 15, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone East", "Wild Pokemon - 9", "Kangaskhan", rom_addresses["Wild_SafariZoneEast"] + 17, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone East", "Wild Pokemon - 10", ["Scyther", "Pinsir"], + rom_addresses["Wild_SafariZoneEast"] + 19, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone North", "Wild Pokemon - 1", ["Nidoran M", "Nidoran F"], + rom_addresses["Wild_SafariZoneNorth"] + 1, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone North", "Wild Pokemon - 2", "Rhyhorn", rom_addresses["Wild_SafariZoneNorth"] + 3, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone North", "Wild Pokemon - 3", "Paras", rom_addresses["Wild_SafariZoneNorth"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Safari Zone North", "Wild Pokemon - 4", "Exeggcute", rom_addresses["Wild_SafariZoneNorth"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone North", "Wild Pokemon - 5", ["Nidorino", "Nidorina"], + rom_addresses["Wild_SafariZoneNorth"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone North", "Wild Pokemon - 6", "Exeggcute", rom_addresses["Wild_SafariZoneNorth"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone North", "Wild Pokemon - 7", ["Nidorina", "Nidorino"], + rom_addresses["Wild_SafariZoneNorth"] + 13, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone North", "Wild Pokemon - 8", "Venomoth", rom_addresses["Wild_SafariZoneNorth"] + 15, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone North", "Wild Pokemon - 9", "Chansey", rom_addresses["Wild_SafariZoneNorth"] + 17, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone North", "Wild Pokemon - 10", "Tauros", rom_addresses["Wild_SafariZoneNorth"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone West", "Wild Pokemon - 1", ["Nidoran M", "Nidoran F"], + rom_addresses["Wild_SafariZoneWest"] + 1, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone West", "Wild Pokemon - 2", "Doduo", rom_addresses["Wild_SafariZoneWest"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Safari Zone West", "Wild Pokemon - 3", "Venonat", rom_addresses["Wild_SafariZoneWest"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Safari Zone West", "Wild Pokemon - 4", "Exeggcute", rom_addresses["Wild_SafariZoneWest"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone West", "Wild Pokemon - 5", ["Nidorino", "Nidorina"], + rom_addresses["Wild_SafariZoneWest"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone West", "Wild Pokemon - 6", "Exeggcute", rom_addresses["Wild_SafariZoneWest"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone West", "Wild Pokemon - 7", ["Nidoran F", "Nidoran M"], + rom_addresses["Wild_SafariZoneWest"] + 13, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone West", "Wild Pokemon - 8", "Venomoth", rom_addresses["Wild_SafariZoneWest"] + 15, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone West", "Wild Pokemon - 9", "Tauros", rom_addresses["Wild_SafariZoneWest"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Safari Zone West", "Wild Pokemon - 10", "Kangaskhan", rom_addresses["Wild_SafariZoneWest"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Route 20 West", "Surf Pokemon - 1", "Tentacool", rom_addresses["Wild_SeaRoutes"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 20 West", "Surf Pokemon - 2", "Tentacool", rom_addresses["Wild_SeaRoutes"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Route 20 West", "Surf Pokemon - 3", "Tentacool", rom_addresses["Wild_SeaRoutes"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Route 20 West", "Surf Pokemon - 4", "Tentacool", rom_addresses["Wild_SeaRoutes"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Route 20 West", "Surf Pokemon - 5", "Tentacool", rom_addresses["Wild_SeaRoutes"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Route 20 West", "Surf Pokemon - 6", "Tentacool", rom_addresses["Wild_SeaRoutes"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Route 20 West", "Surf Pokemon - 7", "Tentacool", rom_addresses["Wild_SeaRoutes"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Route 20 West", "Surf Pokemon - 8", "Tentacool", rom_addresses["Wild_SeaRoutes"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Route 20 West", "Surf Pokemon - 9", "Tentacool", rom_addresses["Wild_SeaRoutes"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Route 20 West", "Surf Pokemon - 10", "Tentacool", rom_addresses["Wild_SeaRoutes"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Route 21", "Surf Pokemon - 1", "Tentacool", rom_addresses["Wild_Surf_Route21"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 21", "Surf Pokemon - 2", "Tentacool", rom_addresses["Wild_Surf_Route21"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Route 21", "Surf Pokemon - 3", "Tentacool", rom_addresses["Wild_Surf_Route21"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Route 21", "Surf Pokemon - 4", "Tentacool", rom_addresses["Wild_Surf_Route21"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Route 21", "Surf Pokemon - 5", "Tentacool", rom_addresses["Wild_Surf_Route21"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Route 21", "Surf Pokemon - 6", "Tentacool", rom_addresses["Wild_Surf_Route21"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Route 21", "Surf Pokemon - 7", "Tentacool", rom_addresses["Wild_Surf_Route21"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Route 21", "Surf Pokemon - 8", "Tentacool", rom_addresses["Wild_Surf_Route21"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Route 21", "Surf Pokemon - 9", "Tentacool", rom_addresses["Wild_Surf_Route21"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Route 21", "Surf Pokemon - 10", "Tentacool", rom_addresses["Wild_Surf_Route21"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Seafoam Islands 1F", "Wild Pokemon - 1", "Seel", rom_addresses["Wild_SeafoamIslands1F"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Seafoam Islands 1F", "Wild Pokemon - 2", ["Slowpoke", "Psyduck"], + rom_addresses["Wild_SeafoamIslands1F"] + 3, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands 1F", "Wild Pokemon - 3", ["Shellder", "Staryu"], + rom_addresses["Wild_SeafoamIslands1F"] + 5, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands 1F", "Wild Pokemon - 4", ["Horsea", "Krabby"], + rom_addresses["Wild_SeafoamIslands1F"] + 7, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands 1F", "Wild Pokemon - 5", ["Horsea", "Krabby"], + rom_addresses["Wild_SeafoamIslands1F"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands 1F", "Wild Pokemon - 6", "Zubat", rom_addresses["Wild_SeafoamIslands1F"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands 1F", "Wild Pokemon - 7", "Golbat", rom_addresses["Wild_SeafoamIslands1F"] + 13, + None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands 1F", "Wild Pokemon - 8", ["Psyduck", "Slowpoke"], + rom_addresses["Wild_SeafoamIslands1F"] + 15, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands 1F", "Wild Pokemon - 9", ["Shellder", "Staryu"], + rom_addresses["Wild_SeafoamIslands1F"] + 17, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands 1F", "Wild Pokemon - 10", ["Golduck", "Slowbro"], + rom_addresses["Wild_SeafoamIslands1F"] + 19, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B1F", "Wild Pokemon - 1", ["Staryu", "Shellder"], + rom_addresses["Wild_SeafoamIslandsB1F"] + 1, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B1F", "Wild Pokemon - 2", ["Horsea", "Krabby"], + rom_addresses["Wild_SeafoamIslandsB1F"] + 3, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B1F", "Wild Pokemon - 3", ["Shellder", "Staryu"], + rom_addresses["Wild_SeafoamIslandsB1F"] + 5, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B1F", "Wild Pokemon - 4", ["Horsea", "Krabby"], + rom_addresses["Wild_SeafoamIslandsB1F"] + 7, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B1F", "Wild Pokemon - 5", ["Slowpoke", "Psyduck"], + rom_addresses["Wild_SeafoamIslandsB1F"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B1F", "Wild Pokemon - 6", "Seel", rom_addresses["Wild_SeafoamIslandsB1F"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B1F", "Wild Pokemon - 7", ["Slowpoke", "Psyduck"], + rom_addresses["Wild_SeafoamIslandsB1F"] + 13, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B1F", "Wild Pokemon - 8", "Seel", rom_addresses["Wild_SeafoamIslandsB1F"] + 15, + None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B1F", "Wild Pokemon - 9", "Dewgong", rom_addresses["Wild_SeafoamIslandsB1F"] + 17, + None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B1F", "Wild Pokemon - 10", ["Seadra", "Kingler"], + rom_addresses["Wild_SeafoamIslandsB1F"] + 19, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B2F", "Wild Pokemon - 1", "Seel", rom_addresses["Wild_SeafoamIslandsB2F"] + 1, + None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B2F", "Wild Pokemon - 2", ["Slowpoke", "Psyduck"], + rom_addresses["Wild_SeafoamIslandsB2F"] + 3, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B2F", "Wild Pokemon - 3", "Seel", rom_addresses["Wild_SeafoamIslandsB2F"] + 5, + None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B2F", "Wild Pokemon - 4", ["Slowpoke", "Psyduck"], + rom_addresses["Wild_SeafoamIslandsB2F"] + 7, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B2F", "Wild Pokemon - 5", ["Horsea", "Krabby"], + rom_addresses["Wild_SeafoamIslandsB2F"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B2F", "Wild Pokemon - 6", ["Staryu", "Shellder"], + rom_addresses["Wild_SeafoamIslandsB2F"] + 11, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B2F", "Wild Pokemon - 7", ["Horsea", "Krabby"], + rom_addresses["Wild_SeafoamIslandsB2F"] + 13, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B2F", "Wild Pokemon - 8", ["Shellder", "Staryu"], + rom_addresses["Wild_SeafoamIslandsB2F"] + 15, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B2F", "Wild Pokemon - 9", "Golbat", rom_addresses["Wild_SeafoamIslandsB2F"] + 17, + None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B2F", "Wild Pokemon - 10", ["Slowbro", "Golduck"], + rom_addresses["Wild_SeafoamIslandsB2F"] + 19, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B3F", "Wild Pokemon - 1", ["Slowpoke", "Psyduck"], + rom_addresses["Wild_SeafoamIslandsB3F"] + 1, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B3F", "Wild Pokemon - 2", "Seel", rom_addresses["Wild_SeafoamIslandsB3F"] + 3, + None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B3F", "Wild Pokemon - 3", ["Slowpoke", "Psyduck"], + rom_addresses["Wild_SeafoamIslandsB3F"] + 5, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B3F", "Wild Pokemon - 4", "Seel", rom_addresses["Wild_SeafoamIslandsB3F"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B3F", "Wild Pokemon - 5", ["Horsea", "Krabby"], + rom_addresses["Wild_SeafoamIslandsB3F"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B3F", "Wild Pokemon - 6", ["Shellder", "Staryu"], + rom_addresses["Wild_SeafoamIslandsB3F"] + 11, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B3F", "Wild Pokemon - 7", ["Horsea", "Krabby"], + rom_addresses["Wild_SeafoamIslandsB3F"] + 13, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B3F", "Wild Pokemon - 8", ["Shellder", "Staryu"], + rom_addresses["Wild_SeafoamIslandsB3F"] + 15, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B3F", "Wild Pokemon - 9", ["Seadra", "Kingler"], + rom_addresses["Wild_SeafoamIslandsB3F"] + 17, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B3F", "Wild Pokemon - 10", "Dewgong", + rom_addresses["Wild_SeafoamIslandsB3F"] + 19, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B4F", "Wild Pokemon - 1", ["Horsea", "Krabby"], + rom_addresses["Wild_SeafoamIslandsB4F"] + 1, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B4F", "Wild Pokemon - 2", ["Shellder", "Staryu"], + rom_addresses["Wild_SeafoamIslandsB4F"] + 3, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B4F", "Wild Pokemon - 3", ["Horsea", "Krabby"], + rom_addresses["Wild_SeafoamIslandsB4F"] + 5, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B4F", "Wild Pokemon - 4", "Shellder", rom_addresses["Wild_SeafoamIslandsB4F"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B4F", "Wild Pokemon - 5", ["Slowpoke", "Psyduck"], + rom_addresses["Wild_SeafoamIslandsB4F"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B4F", "Wild Pokemon - 6", "Seel", rom_addresses["Wild_SeafoamIslandsB4F"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B4F", "Wild Pokemon - 7", ["Slowpoke", "Psyduck"], + rom_addresses["Wild_SeafoamIslandsB4F"] + 13, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B4F", "Wild Pokemon - 8", "Seel", rom_addresses["Wild_SeafoamIslandsB4F"] + 15, + None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B4F", "Wild Pokemon - 9", ["Slowbro", "Golduck"], + rom_addresses["Wild_SeafoamIslandsB4F"] + 17, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B4F", "Wild Pokemon - 10", "Golbat", rom_addresses["Wild_SeafoamIslandsB4F"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 1F", "Wild Pokemon - 1", ["Koffing", "Grimer"], + rom_addresses["Wild_PokemonMansion1F"] + 1, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 1F", "Wild Pokemon - 2", ["Koffing", "Grimer"], + rom_addresses["Wild_PokemonMansion1F"] + 3, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 1F", "Wild Pokemon - 3", "Ponyta", rom_addresses["Wild_PokemonMansion1F"] + 5, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 1F", "Wild Pokemon - 4", "Ponyta", rom_addresses["Wild_PokemonMansion1F"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 1F", "Wild Pokemon - 5", ["Growlithe", "Vulpix"], + rom_addresses["Wild_PokemonMansion1F"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 1F", "Wild Pokemon - 6", "Ponyta", rom_addresses["Wild_PokemonMansion1F"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 1F", "Wild Pokemon - 7", ["Grimer", "Koffing"], + rom_addresses["Wild_PokemonMansion1F"] + 13, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 1F", "Wild Pokemon - 8", "Ponyta", rom_addresses["Wild_PokemonMansion1F"] + 15, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 1F", "Wild Pokemon - 9", ["Weezing", "Muk"], + rom_addresses["Wild_PokemonMansion1F"] + 17, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 1F", "Wild Pokemon - 10", ["Muk", "Weezing"], + rom_addresses["Wild_PokemonMansion1F"] + 19, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 2F", "Wild Pokemon - 1", ["Growlithe", "Vulpix"], + rom_addresses["Wild_PokemonMansion2F"] + 1, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 2F", "Wild Pokemon - 2", ["Koffing", "Grimer"], + rom_addresses["Wild_PokemonMansion2F"] + 3, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 2F", "Wild Pokemon - 3", ["Koffing", "Grimer"], + rom_addresses["Wild_PokemonMansion2F"] + 5, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 2F", "Wild Pokemon - 4", "Ponyta", rom_addresses["Wild_PokemonMansion2F"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 2F", "Wild Pokemon - 5", ["Koffing", "Grimer"], + rom_addresses["Wild_PokemonMansion2F"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 2F", "Wild Pokemon - 6", "Ponyta", rom_addresses["Wild_PokemonMansion2F"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 2F", "Wild Pokemon - 7", ["Grimer", "Koffing"], + rom_addresses["Wild_PokemonMansion2F"] + 13, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 2F", "Wild Pokemon - 8", "Ponyta", rom_addresses["Wild_PokemonMansion2F"] + 15, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 2F", "Wild Pokemon - 9", ["Weezing", "Muk"], + rom_addresses["Wild_PokemonMansion2F"] + 17, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 2F", "Wild Pokemon - 10", ["Muk", "Weezing"], + rom_addresses["Wild_PokemonMansion2F"] + 19, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 3F", "Wild Pokemon - 1", ["Koffing", "Grimer"], + rom_addresses["Wild_PokemonMansion3F"] + 1, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 3F", "Wild Pokemon - 2", ["Growlithe", "Vulpix"], + rom_addresses["Wild_PokemonMansion3F"] + 3, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 3F", "Wild Pokemon - 3", ["Koffing", "Grimer"], + rom_addresses["Wild_PokemonMansion3F"] + 5, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 3F", "Wild Pokemon - 4", "Ponyta", rom_addresses["Wild_PokemonMansion3F"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 3F", "Wild Pokemon - 5", ["Ponyta", "Magmar"], + rom_addresses["Wild_PokemonMansion3F"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 3F", "Wild Pokemon - 6", ["Weezing", "Muk"], + rom_addresses["Wild_PokemonMansion3F"] + 11, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 3F", "Wild Pokemon - 7", ["Grimer", "Koffing"], + rom_addresses["Wild_PokemonMansion3F"] + 13, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 3F", "Wild Pokemon - 8", ["Weezing", "Muk"], + rom_addresses["Wild_PokemonMansion3F"] + 15, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 3F", "Wild Pokemon - 9", "Ponyta", rom_addresses["Wild_PokemonMansion3F"] + 17, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 3F", "Wild Pokemon - 10", ["Muk", "Weezing"], + rom_addresses["Wild_PokemonMansion3F"] + 19, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion B1F", "Wild Pokemon - 1", ["Koffing", "Grimer"], + rom_addresses["Wild_PokemonMansionB1F"] + 1, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion B1F", "Wild Pokemon - 2", ["Koffing", "Grimer"], + rom_addresses["Wild_PokemonMansionB1F"] + 3, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion B1F", "Wild Pokemon - 3", ["Growlithe", "Vulpix"], + rom_addresses["Wild_PokemonMansionB1F"] + 5, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion B1F", "Wild Pokemon - 4", "Ponyta", rom_addresses["Wild_PokemonMansionB1F"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion B1F", "Wild Pokemon - 5", ["Koffing", "Grimer"], + rom_addresses["Wild_PokemonMansionB1F"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion B1F", "Wild Pokemon - 6", ["Weezing", "Muk"], + rom_addresses["Wild_PokemonMansionB1F"] + 11, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion B1F", "Wild Pokemon - 7", "Ponyta", rom_addresses["Wild_PokemonMansionB1F"] + 13, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion B1F", "Wild Pokemon - 8", ["Grimer", "Koffing"], + rom_addresses["Wild_PokemonMansionB1F"] + 15, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion B1F", "Wild Pokemon - 9", ["Weezing", "Magmar"], + rom_addresses["Wild_PokemonMansionB1F"] + 17, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion B1F", "Wild Pokemon - 10", ["Muk", "Weezing"], + rom_addresses["Wild_PokemonMansionB1F"] + 19, None, event=True, type="Wild Encounter"), + LocationData("Route 21", "Wild Pokemon - 1", "Rattata", rom_addresses["Wild_Route21"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Route 21", "Wild Pokemon - 2", "Pidgey", rom_addresses["Wild_Route21"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 21", "Wild Pokemon - 3", "Raticate", rom_addresses["Wild_Route21"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 21", "Wild Pokemon - 4", "Rattata", rom_addresses["Wild_Route21"] + 7, None, event=True, + type="Wild Encounter"), + LocationData("Route 21", "Wild Pokemon - 5", "Pidgey", rom_addresses["Wild_Route21"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 21", "Wild Pokemon - 6", "Pidgeotto", rom_addresses["Wild_Route21"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Route 21", "Wild Pokemon - 7", "Pidgeotto", rom_addresses["Wild_Route21"] + 13, None, event=True, + type="Wild Encounter"), + LocationData("Route 21", "Wild Pokemon - 8", "Tangela", rom_addresses["Wild_Route21"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 21", "Wild Pokemon - 9", "Tangela", rom_addresses["Wild_Route21"] + 17, None, event=True, + type="Wild Encounter"), + LocationData("Route 21", "Wild Pokemon - 10", "Tangela", rom_addresses["Wild_Route21"] + 19, None, event=True, + type="Wild Encounter"), + LocationData("Cerulean Cave 1F", "Wild Pokemon - 1", "Golbat", rom_addresses["Wild_CeruleanCave1F"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 1F", "Wild Pokemon - 2", "Hypno", rom_addresses["Wild_CeruleanCave1F"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 1F", "Wild Pokemon - 3", "Magneton", rom_addresses["Wild_CeruleanCave1F"] + 5, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 1F", "Wild Pokemon - 4", "Dodrio", rom_addresses["Wild_CeruleanCave1F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 1F", "Wild Pokemon - 5", "Venomoth", rom_addresses["Wild_CeruleanCave1F"] + 9, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 1F", "Wild Pokemon - 6", ["Arbok", "Sandslash"], + rom_addresses["Wild_CeruleanCave1F"] + 11, None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 1F", "Wild Pokemon - 7", "Kadabra", rom_addresses["Wild_CeruleanCave1F"] + 13, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 1F", "Wild Pokemon - 8", "Parasect", rom_addresses["Wild_CeruleanCave1F"] + 15, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 1F", "Wild Pokemon - 9", "Raichu", rom_addresses["Wild_CeruleanCave1F"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 1F", "Wild Pokemon - 10", "Ditto", rom_addresses["Wild_CeruleanCave1F"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 2F", "Wild Pokemon - 1", "Dodrio", rom_addresses["Wild_CeruleanCave2F"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 2F", "Wild Pokemon - 2", "Venomoth", rom_addresses["Wild_CeruleanCave2F"] + 3, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 2F", "Wild Pokemon - 3", "Kadabra", rom_addresses["Wild_CeruleanCave2F"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 2F", "Wild Pokemon - 4", "Rhydon", rom_addresses["Wild_CeruleanCave2F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 2F", "Wild Pokemon - 5", "Marowak", rom_addresses["Wild_CeruleanCave2F"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 2F", "Wild Pokemon - 6", "Electrode", rom_addresses["Wild_CeruleanCave2F"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 2F", "Wild Pokemon - 7", "Chansey", rom_addresses["Wild_CeruleanCave2F"] + 13, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 2F", "Wild Pokemon - 8", "Wigglytuff", rom_addresses["Wild_CeruleanCave2F"] + 15, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 2F", "Wild Pokemon - 9", "Ditto", rom_addresses["Wild_CeruleanCave2F"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 2F", "Wild Pokemon - 10", "Ditto", rom_addresses["Wild_CeruleanCave2F"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Cerulean Cave B1F", "Wild Pokemon - 1", "Rhydon", rom_addresses["Wild_CeruleanCaveB1F"] + 1, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave B1F", "Wild Pokemon - 2", "Marowak", rom_addresses["Wild_CeruleanCaveB1F"] + 3, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave B1F", "Wild Pokemon - 3", "Electrode", rom_addresses["Wild_CeruleanCaveB1F"] + 5, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave B1F", "Wild Pokemon - 4", "Chansey", rom_addresses["Wild_CeruleanCaveB1F"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave B1F", "Wild Pokemon - 5", "Parasect", rom_addresses["Wild_CeruleanCaveB1F"] + 9, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave B1F", "Wild Pokemon - 6", "Raichu", rom_addresses["Wild_CeruleanCaveB1F"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave B1F", "Wild Pokemon - 7", ["Arbok", "Sandslash"], + rom_addresses["Wild_CeruleanCaveB1F"] + 13, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave B1F", "Wild Pokemon - 8", "Ditto", rom_addresses["Wild_CeruleanCaveB1F"] + 15, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave B1F", "Wild Pokemon - 9", "Ditto", rom_addresses["Wild_CeruleanCaveB1F"] + 17, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave B1F", "Wild Pokemon - 10", "Ditto", rom_addresses["Wild_CeruleanCaveB1F"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Power Plant", "Wild Pokemon - 1", "Voltorb", rom_addresses["Wild_PowerPlant"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Power Plant", "Wild Pokemon - 2", "Magnemite", rom_addresses["Wild_PowerPlant"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Power Plant", "Wild Pokemon - 3", "Pikachu", rom_addresses["Wild_PowerPlant"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Power Plant", "Wild Pokemon - 4", "Pikachu", rom_addresses["Wild_PowerPlant"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Power Plant", "Wild Pokemon - 5", "Magnemite", rom_addresses["Wild_PowerPlant"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Power Plant", "Wild Pokemon - 6", "Voltorb", rom_addresses["Wild_PowerPlant"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Power Plant", "Wild Pokemon - 7", "Magneton", rom_addresses["Wild_PowerPlant"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Power Plant", "Wild Pokemon - 8", "Magneton", rom_addresses["Wild_PowerPlant"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Power Plant", "Wild Pokemon - 9", ["Electabuzz", "Raichu"], rom_addresses["Wild_PowerPlant"] + 17, + None, event=True, type="Wild Encounter"), + LocationData("Power Plant", "Wild Pokemon - 10", ["Electabuzz", "Raichu"], + rom_addresses["Wild_PowerPlant"] + 19, None, event=True, type="Wild Encounter"), + LocationData("Route 23 North", "Wild Pokemon - 1", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route23"] + 1, + None, event=True, type="Wild Encounter"), + LocationData("Route 23 North", "Wild Pokemon - 2", "Ditto", rom_addresses["Wild_Route23"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 23 North", "Wild Pokemon - 3", "Spearow", rom_addresses["Wild_Route23"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Route 23 North", "Wild Pokemon - 4", "Fearow", rom_addresses["Wild_Route23"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Route 23 North", "Wild Pokemon - 5", "Ditto", rom_addresses["Wild_Route23"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 23 North", "Wild Pokemon - 6", "Fearow", rom_addresses["Wild_Route23"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Route 23 North", "Wild Pokemon - 7", ["Arbok", "Sandslash"], rom_addresses["Wild_Route23"] + 13, + None, event=True, type="Wild Encounter"), + LocationData("Route 23 North", "Wild Pokemon - 8", "Ditto", rom_addresses["Wild_Route23"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Route 23 North", "Wild Pokemon - 9", "Fearow", rom_addresses["Wild_Route23"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Route 23 North", "Wild Pokemon - 10", "Fearow", rom_addresses["Wild_Route23"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 2F", "Wild Pokemon - 1", "Machop", rom_addresses["Wild_VictoryRoad2F"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 2F", "Wild Pokemon - 2", "Geodude", rom_addresses["Wild_VictoryRoad2F"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 2F", "Wild Pokemon - 3", "Zubat", rom_addresses["Wild_VictoryRoad2F"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 2F", "Wild Pokemon - 4", "Onix", rom_addresses["Wild_VictoryRoad2F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 2F", "Wild Pokemon - 5", "Onix", rom_addresses["Wild_VictoryRoad2F"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 2F", "Wild Pokemon - 6", "Onix", rom_addresses["Wild_VictoryRoad2F"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 2F", "Wild Pokemon - 7", "Machoke", rom_addresses["Wild_VictoryRoad2F"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 2F", "Wild Pokemon - 8", "Golbat", rom_addresses["Wild_VictoryRoad2F"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 2F", "Wild Pokemon - 9", "Marowak", rom_addresses["Wild_VictoryRoad2F"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 2F", "Wild Pokemon - 10", "Graveler", rom_addresses["Wild_VictoryRoad2F"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 3F", "Wild Pokemon - 1", "Machop", rom_addresses["Wild_VictoryRoad3F"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 3F", "Wild Pokemon - 2", "Geodude", rom_addresses["Wild_VictoryRoad3F"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 3F", "Wild Pokemon - 3", "Zubat", rom_addresses["Wild_VictoryRoad3F"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 3F", "Wild Pokemon - 4", "Onix", rom_addresses["Wild_VictoryRoad3F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 3F", "Wild Pokemon - 5", "Venomoth", rom_addresses["Wild_VictoryRoad3F"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 3F", "Wild Pokemon - 6", "Onix", rom_addresses["Wild_VictoryRoad3F"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 3F", "Wild Pokemon - 7", "Graveler", rom_addresses["Wild_VictoryRoad3F"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 3F", "Wild Pokemon - 8", "Golbat", rom_addresses["Wild_VictoryRoad3F"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 3F", "Wild Pokemon - 9", "Machoke", rom_addresses["Wild_VictoryRoad3F"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 3F", "Wild Pokemon - 10", "Machoke", rom_addresses["Wild_VictoryRoad3F"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 1F", "Wild Pokemon - 1", "Machop", rom_addresses["Wild_VictoryRoad1F"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 1F", "Wild Pokemon - 2", "Geodude", rom_addresses["Wild_VictoryRoad1F"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 1F", "Wild Pokemon - 3", "Zubat", rom_addresses["Wild_VictoryRoad1F"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 1F", "Wild Pokemon - 4", "Onix", rom_addresses["Wild_VictoryRoad1F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 1F", "Wild Pokemon - 5", "Onix", rom_addresses["Wild_VictoryRoad1F"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 1F", "Wild Pokemon - 6", "Onix", rom_addresses["Wild_VictoryRoad1F"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 1F", "Wild Pokemon - 7", "Graveler", rom_addresses["Wild_VictoryRoad1F"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 1F", "Wild Pokemon - 8", "Golbat", rom_addresses["Wild_VictoryRoad1F"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 1F", "Wild Pokemon - 9", "Machoke", rom_addresses["Wild_VictoryRoad1F"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 1F", "Wild Pokemon - 10", "Marowak", rom_addresses["Wild_VictoryRoad1F"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Diglett's Cave", "Wild Pokemon - 1", "Diglett", rom_addresses["Wild_DiglettsCave"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Diglett's Cave", "Wild Pokemon - 2", "Diglett", rom_addresses["Wild_DiglettsCave"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Diglett's Cave", "Wild Pokemon - 3", "Diglett", rom_addresses["Wild_DiglettsCave"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Diglett's Cave", "Wild Pokemon - 4", "Diglett", rom_addresses["Wild_DiglettsCave"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Diglett's Cave", "Wild Pokemon - 5", "Diglett", rom_addresses["Wild_DiglettsCave"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Diglett's Cave", "Wild Pokemon - 6", "Diglett", rom_addresses["Wild_DiglettsCave"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Diglett's Cave", "Wild Pokemon - 7", "Diglett", rom_addresses["Wild_DiglettsCave"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Diglett's Cave", "Wild Pokemon - 8", "Diglett", rom_addresses["Wild_DiglettsCave"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Diglett's Cave", "Wild Pokemon - 9", "Dugtrio", rom_addresses["Wild_DiglettsCave"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Diglett's Cave", "Wild Pokemon - 10", "Dugtrio", rom_addresses["Wild_DiglettsCave"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Anywhere", "Good Rod Pokemon - 1", "Goldeen", rom_addresses["Wild_Good_Rod"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Anywhere", "Good Rod Pokemon - 2", "Poliwag", rom_addresses["Wild_Good_Rod"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Anywhere", "Old Rod Pokemon", "Magikarp", rom_addresses["Wild_Old_Rod"] + 1, None, + event=True, type="Wild Encounter"), + + # "Wild encounters" means a Pokemon you cannot miss or release and lose - re-use it for these + LocationData("Celadon Prize Corner", "Pokemon Prize - 1", "Abra", [rom_addresses["Prize_Mon_A"], + rom_addresses["Prize_Mon_A2"]], None, event=True, + type="Wild Encounter"), + LocationData("Celadon Prize Corner", "Pokemon Prize - 2", "Clefairy", [rom_addresses["Prize_Mon_B"], + rom_addresses["Prize_Mon_B2"]], None, + event=True, type="Wild Encounter"), + LocationData("Celadon Prize Corner", "Pokemon Prize - 3", ["Nidorina", "Nidorino"], [rom_addresses["Prize_Mon_C"], + rom_addresses["Prize_Mon_C2"]], + None, + event=True, type="Wild Encounter"), + LocationData("Celadon Prize Corner", "Pokemon Prize - 4", ["Dratini", "Pinsir"], [rom_addresses["Prize_Mon_D"], + rom_addresses["Prize_Mon_D2"]], + None, event=True, type="Wild Encounter"), + LocationData("Celadon Prize Corner", "Pokemon Prize - 5", ["Scyther", "Dratini"], [rom_addresses["Prize_Mon_E"], + rom_addresses["Prize_Mon_E2"]], + None, event=True, type="Wild Encounter"), + LocationData("Celadon Prize Corner", "Pokemon Prize - 6", "Porygon", [rom_addresses["Prize_Mon_F"], + rom_addresses["Prize_Mon_F2"]], None, + event=True, type="Wild Encounter"), + + # counted for pokedex, because they cannot be permanently "missed" but not for HMs since they can be released + # or evolved forms may not be able to learn the same HMs + LocationData("Celadon City", "Celadon Mansion Pokemon", "Eevee", rom_addresses["Gift_Eevee"], None, + event=True, type="Static Pokemon"), + LocationData("Silph Co 7F", "Gift Pokemon", "Lapras", rom_addresses["Gift_Lapras"], None, + event=True, type="Static Pokemon"), + LocationData("Route 3", "Pokemon For Sale", "Magikarp", rom_addresses["Gift_Magikarp"], None, + event=True, type="Static Pokemon"), + LocationData("Cinnabar Island", "Old Amber Pokemon", "Aerodactyl", rom_addresses["Gift_Aerodactyl"], None, + event=True, type="Static Pokemon"), + LocationData("Cinnabar Island", "Helix Fossil Pokemon", "Omanyte", rom_addresses["Gift_Omanyte"], None, + event=True, type="Static Pokemon"), + LocationData("Cinnabar Island", "Dome Fossil Pokemon", "Kabuto", rom_addresses["Gift_Kabuto"], None, + event=True, type="Static Pokemon"), + + # not counted for logic currently. Could perhaps make static encounters resettable in the future? + LocationData("Power Plant", "Fake Pokeball Battle 1", "Voltorb", rom_addresses["Static_Encounter_Voltorb_A"], + None, event=True, type="Missable Pokemon"), + LocationData("Power Plant", "Fake Pokeball Battle 2", "Voltorb", rom_addresses["Static_Encounter_Voltorb_B"], + None, event=True, type="Missable Pokemon"), + LocationData("Power Plant", "Fake Pokeball Battle 3", "Voltorb", rom_addresses["Static_Encounter_Voltorb_C"], + None, event=True, type="Missable Pokemon"), + LocationData("Power Plant", "Fake Pokeball Battle 4", "Voltorb", rom_addresses["Static_Encounter_Voltorb_D"], + None, event=True, type="Missable Pokemon"), + LocationData("Power Plant", "Fake Pokeball Battle 5", "Voltorb", rom_addresses["Static_Encounter_Voltorb_E"], + None, event=True, type="Missable Pokemon"), + LocationData("Power Plant", "Fake Pokeball Battle 6", "Voltorb", rom_addresses["Static_Encounter_Voltorb_F"], + None, event=True, type="Missable Pokemon"), + LocationData("Power Plant", "Fake Pokeball Battle 7", "Voltorb", rom_addresses["Static_Encounter_Electrode_A"], + None, event=True, type="Missable Pokemon"), + LocationData("Power Plant", "Fake Pokeball Battle 8", "Voltorb", rom_addresses["Static_Encounter_Electrode_B"], + None, event=True, type="Missable Pokemon"), + + LocationData("Pokemon Tower 6F", "Restless Soul", "Marowak", [rom_addresses["Ghost_Battle1"], + rom_addresses["Ghost_Battle2"], + rom_addresses["Ghost_Battle3"], + rom_addresses["Ghost_Battle4"], + rom_addresses["Ghost_Battle5"], + rom_addresses["Ghost_Battle6"]], None, event=True, + type="Missable Pokemon"), + + LocationData("Route 12 West", "Sleeping Pokemon", "Snorlax", rom_addresses["Static_Encounter_Snorlax_A"], + None, event=True, type="Missable Pokemon"), + LocationData("Route 16", "Sleeping Pokemon", "Snorlax", rom_addresses["Static_Encounter_Snorlax_B"], + None, event=True, type="Missable Pokemon"), + + LocationData("Saffron City", "Fighting Dojo Gift 1", "Hitmonlee", rom_addresses["Gift_Hitmonlee"], + None, event=True, type="Missable Pokemon"), + LocationData("Saffron City", "Fighting Dojo Gift 2", "Hitmonchan", rom_addresses["Gift_Hitmonchan"], + None, event=True, type="Missable Pokemon"), + + LocationData("Pallet Town", "Starter 1", "Charmander", [rom_addresses["Starter1_A"], + rom_addresses["Starter1_B"], rom_addresses["Starter1_C"], + rom_addresses["Starter1_D"], + rom_addresses["Starter1_F"], rom_addresses["Starter1_H"]], + None, event=True, + type="Starter Pokemon"), + + LocationData("Pallet Town", "Starter 2", "Squirtle", [rom_addresses["Starter2_A"], + rom_addresses["Starter2_B"], rom_addresses["Starter2_E"], + rom_addresses["Starter2_F"], + rom_addresses["Starter2_G"], rom_addresses["Starter2_H"], + rom_addresses["Starter2_I"], + rom_addresses["Starter2_J"], rom_addresses["Starter2_K"], + rom_addresses["Starter2_L"], + rom_addresses["Starter2_M"], rom_addresses["Starter2_N"], + rom_addresses["Starter2_O"]], None, + event=True, type="Starter Pokemon"), + + LocationData("Pallet Town", "Starter 3", "Bulbasaur", [rom_addresses["Starter3_A"], + rom_addresses["Starter3_B"], rom_addresses["Starter3_C"], + rom_addresses["Starter3_D"], + rom_addresses["Starter3_E"], rom_addresses["Starter3_G"], + rom_addresses["Starter3_I"], + rom_addresses["Starter3_J"], rom_addresses["Starter3_K"], + rom_addresses["Starter3_L"], + rom_addresses["Starter3_M"], rom_addresses["Starter3_N"], + rom_addresses["Starter3_O"]], None, + event=True, type="Starter Pokemon"), + + LocationData("Power Plant", "Legendary Pokemon", "Zapdos", rom_addresses["Static_Encounter_Zapdos"], + None, event=True, type="Legendary Pokemon"), + LocationData("Seafoam Islands B4F", "Legendary Pokemon", "Articuno", rom_addresses["Static_Encounter_Articuno"], + None, event=True, type="Legendary Pokemon"), + LocationData("Victory Road 2F", "Legendary Pokemon", "Moltres", rom_addresses["Static_Encounter_Moltres"], + None, event=True, type="Legendary Pokemon"), + LocationData("Cerulean Cave B1F", "Legendary Pokemon", "Mewtwo", rom_addresses["Static_Encounter_Mewtwo"], + None, event=True, type="Legendary Pokemon"), + LocationData("Vermilion City", "Legendary Pokemon", "Mew", rom_addresses["Static_Encounter_Mew"], + None, event=True, type="Legendary Pokemon"), +] + +for i, location in enumerate(location_data): + if location.event or location.rom_address is None: + location.address = None + else: + location.address = loc_id_start + i + + + +class PokemonRBLocation(Location): + game = "Pokemon Red and Blue" + + def __init__(self, player, name, address, rom_address): + super(PokemonRBLocation, self).__init__( + player, name, + address + ) + self.rom_address = rom_address \ No newline at end of file diff --git a/worlds/pokemon_rb/logic.py b/worlds/pokemon_rb/logic.py new file mode 100644 index 0000000000..3b1a3594bd --- /dev/null +++ b/worlds/pokemon_rb/logic.py @@ -0,0 +1,73 @@ +from ..AutoWorld import LogicMixin +import worlds.pokemon_rb.poke_data as poke_data + + +class PokemonLogic(LogicMixin): + def pokemon_rb_can_surf(self, player): + return (((self.has("HM03 Surf", player) and self.can_learn_hm("10000", player)) + or self.has("Flippers", player)) and (self.has("Soul Badge", player) or + self.has(self.world.worlds[player].extra_badges.get("Surf"), player) + or self.world.badges_needed_for_hm_moves[player].value == 0)) + + def pokemon_rb_can_cut(self, player): + return ((self.has("HM01 Cut", player) and self.can_learn_hm("100", player) or self.has("Master Sword", player)) + and (self.has("Cascade Badge", player) or + self.has(self.world.worlds[player].extra_badges.get("Cut"), player) or + self.world.badges_needed_for_hm_moves[player].value == 0)) + + def pokemon_rb_can_fly(self, player): + return (((self.has("HM02 Fly", player) and self.can_learn_hm("1000", player)) or self.has("Flute", player)) and + (self.has("Thunder Badge", player) or self.has(self.world.worlds[player].extra_badges.get("Fly"), player) + or self.world.badges_needed_for_hm_moves[player].value == 0)) + + def pokemon_rb_can_strength(self, player): + return ((self.has("HM04 Strength", player) and self.can_learn_hm("100000", player)) or + self.has("Titan's Mitt", player)) and (self.has("Rainbow Badge", player) or + self.has(self.world.worlds[player].extra_badges.get("Strength"), player) + or self.world.badges_needed_for_hm_moves[player].value == 0) + + def pokemon_rb_can_flash(self, player): + return (((self.has("HM05 Flash", player) and self.can_learn_hm("1000000", player)) or self.has("Lamp", player)) + and (self.has("Boulder Badge", player) or self.has(self.world.worlds[player].extra_badges.get("Flash"), + player) or self.world.badges_needed_for_hm_moves[player].value == 0)) + + def can_learn_hm(self, move, player): + for pokemon, data in self.world.worlds[player].local_poke_data.items(): + if self.has(pokemon, player) and data["tms"][6] & int(move, 2): + return True + return False + + def pokemon_rb_can_get_hidden_items(self, player): + return self.has("Item Finder", player) or not self.world.require_item_finder[player].value + + def pokemon_rb_cerulean_cave(self, count, player): + return len([item for item in + ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Soul Badge", "Marsh Badge", + "Volcano Badge", "Earth Badge", "Bicycle", "Silph Scope", "Item Finder", "Super Rod", "Good Rod", + "Old Rod", "Lift Key", "Card Key", "Town Map", "Coin Case", "S.S. Ticket", "Secret Key", + "Mansion Key", "Safari Pass", "Plant Key", "Hideout Key", "HM01 Cut", "HM02 Fly", "HM03 Surf", + "HM04 Strength", "HM05 Flash"] if self.has(item, player)]) >= count + + def pokemon_rb_can_pass_guards(self, player): + if self.world.tea[player].value: + return self.has("Tea", player) + else: + # this could just be "True", but you never know what weird options I might introduce later ;) + return self.can_reach("Celadon City - Counter Man", "Location", player) + + def pokemon_rb_has_badges(self, count, player): + return len([item for item in ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Marsh Badge", + "Soul Badge", "Volcano Badge", "Earth Badge"] if self.has(item, player)]) >= count + + def pokemon_rb_has_pokemon(self, count, player): + obtained_pokemon = set() + for pokemon in poke_data.pokemon_data.keys(): + if self.has(pokemon, player) or self.has(f"Static {pokemon}", player): + obtained_pokemon.add(pokemon) + + return len(obtained_pokemon) >= count + + def pokemon_rb_fossil_checks(self, count, player): + return (self.can_reach('Mt Moon 1F - Southwest Item', 'Location', player) and + self.can_reach('Cinnabar Island - Lab Scientist', 'Location', player) and len( + [item for item in ["Dome Fossil", "Helix Fossil", "Old Amber"] if self.has(item, player)]) >= count) diff --git a/worlds/pokemon_rb/options.py b/worlds/pokemon_rb/options.py new file mode 100644 index 0000000000..e7e972f3b8 --- /dev/null +++ b/worlds/pokemon_rb/options.py @@ -0,0 +1,480 @@ + +from Options import Toggle, Choice, Range, SpecialRange, FreeText, TextChoice + + +class GameVersion(Choice): + """Select Red or Blue version.""" + display_name = "Game Version" + option_red = 1 + option_blue = 0 + default = "random" + + +class TrainerName(FreeText): + """Your trainer name. Cannot exceed 7 characters. + See the setup guide on archipelago.gg for a list of allowed characters.""" + display_name = "Trainer Name" + default = "ASH" + + +class RivalName(FreeText): + """Your rival's name. Cannot exceed 7 characters. + See the setup guide on archipelago.gg for a list of allowed characters.""" + display_name = "Rival's Name" + default = "GARY" + + +class Goal(Choice): + """If Professor Oak is selected, your victory condition will require challenging and defeating Oak after becoming""" + """Champion and defeating or capturing the Pokemon at the end of Cerulean Cave.""" + display_name = "Goal" + option_pokemon_league = 0 + option_professor_oak = 1 + default = 0 + + +class EliteFourCondition(Range): + """Number of badges required to challenge the Elite Four once the Indigo Plateau has been reached. + Your rival will reveal the amount needed on the first Route 22 battle (after turning in Oak's Parcel).""" + display_name = "Elite Four Condition" + range_start = 0 + range_end = 8 + default = 8 + + +class VictoryRoadCondition(Range): + """Number of badges required to reach Victory Road.""" + display_name = "Victory Road Condition" + range_start = 0 + range_end = 8 + default = 8 + + +class ViridianGymCondition(Range): + """Number of badges required to enter Viridian Gym.""" + display_name = "Viridian Gym Condition" + range_start = 0 + range_end = 7 + default = 7 + + +class CeruleanCaveCondition(Range): + """Number of badges, HMs, and key items (not counting items you can lose) required to access Cerulean Cave.""" + """If extra_key_items is turned on, the number chosen will be increased by 4.""" + display_name = "Cerulean Cave Condition" + range_start = 0 + range_end = 25 + default = 20 + + +class SecondFossilCheckCondition(Range): + """After choosing one of the fossil location items, you can obtain the remaining item from the Cinnabar Lab + Scientist after reviving this number of fossils.""" + display_name = "Second Fossil Check Condition" + range_start = 0 + range_end = 3 + default = 3 + + +class BadgeSanity(Toggle): + """Shuffle gym badges into the general item pool. If turned off, badges will be shuffled across the 8 gyms.""" + display_name = "Badgesanity" + default = 0 + + +class BadgesNeededForHMMoves(Choice): + """Off will remove the requirement for badges to use HM moves. Extra will give the Marsh, Volcano, and Earth + Badges a random HM move to enable. Extra Plus will additionally pick two random badges to enable a second HM move. + A man in Cerulean City will reveal the moves enabled by each Badge.""" + display_name = "Badges Needed For HM Moves" + default = 1 + option_on = 1 + alias_true = 1 + option_off = 0 + alias_false = 0 + option_extra = 2 + option_extra_plus = 3 + + +class OldMan(Choice): + """With Open Viridian City, the Old Man will let you through without needing to turn in Oak's Parcel.""" + """Early Parcel will ensure Oak's Parcel is available at the beginning of your game.""" + display_name = "Old Man" + option_vanilla = 0 + option_early_parcel = 1 + option_open_viridian_city = 2 + default = 1 + + +class Tea(Toggle): + """Adds a Tea item to the item pool which the Saffron guards require instead of the vending machine drinks. + Adds a location check to the Celadon Mansion 1F, where Tea is acquired in FireRed and LeafGreen.""" + display_name = "Tea" + default = 0 + + +class ExtraKeyItems(Toggle): + """Adds key items that are required to access the Rocket Hideout, Cinnabar Mansion, Safari Zone, and Power Plant. + Adds four item pickups to Rock Tunnel B1F.""" + display_name = "Extra Key Items" + default = 0 + + +class ExtraStrengthBoulders(Toggle): + """Adds Strength Boulders blocking the Route 11 gate, and in Route 13 (can be bypassed with Surf). + This potentially increases the usefulness of Strength as well as the Bicycle.""" + display_name = "Extra Strength Boulders" + default = 0 + + +class RequireItemFinder(Toggle): + """Require Item Finder to pick up hidden items.""" + display_name = "Require Item Finder" + default = 0 + + +class RandomizeHiddenItems(Choice): + """Randomize hidden items. If you choose exclude, they will be randomized but will be guaranteed junk items.""" + display_name = "Randomize Hidden Items" + option_on = 1 + option_off = 0 + alias_true = 1 + alias_false = 0 + option_exclude = 2 + default = 0 + + +class FreeFlyLocation(Toggle): + """One random fly destination will be unlocked by default.""" + display_name = "Free Fly Location" + default = 1 + + +class OaksAidRt2(Range): + """Number of Pokemon registered in the Pokedex required to receive the item from Oak's Aide on Route 2""" + display_name = "Oak's Aide Route 2" + range_start = 0 + range_end = 80 + default = 10 + + +class OaksAidRt11(Range): + """Number of Pokemon registered in the Pokedex required to receive the item from Oak's Aide on Route 11""" + display_name = "Oak's Aide Route 11" + range_start = 0 + range_end = 80 + default = 30 + + +class OaksAidRt15(Range): + """Number of Pokemon registered in the Pokedex required to receive the item from Oak's Aide on Route 15""" + display_name = "Oak's Aide Route 15" + range_start = 0 + range_end = 80 + default = 50 + + +class ExpModifier(SpecialRange): + """Modifier for EXP gained. When specifying a number, exp is multiplied by this amount and divided by 16.""" + display_name = "Exp Modifier" + range_start = 0 + range_end = 255 + default = 16 + special_range_names = { + "half": default / 2, + "normal": default, + "double": default * 2, + "triple": default * 3, + "quadruple": default * 4, + "quintuple": default * 5, + "sextuple": default * 6, + "septuple": default * 7, + "octuple": default * 8, + } + + +class RandomizeWildPokemon(Choice): + """Randomize all wild Pokemon and game corner prize Pokemon. match_types will select a Pokemon with at least one + type matching the original type of the original Pokemon. match_base_stats will prefer Pokemon with closer base stat + totals. match_types_and_base_stats will match types and will weight towards similar base stats, but there may not be + many to choose from.""" + display_name = "Randomize Wild Pokemon" + default = 0 + option_vanilla = 0 + option_match_types = 1 + option_match_base_stats = 2 + option_match_types_and_base_stats = 3 + option_completely_random = 4 + + +class RandomizeStarterPokemon(Choice): + """Randomize the starter Pokemon choices.""" + display_name = "Randomize Starter Pokemon" + default = 0 + option_vanilla = 0 + option_match_types = 1 + option_match_base_stats = 2 + option_match_types_and_base_stats = 3 + option_completely_random = 4 + + +class RandomizeStaticPokemon(Choice): + """Randomize one-time gift and encountered Pokemon. These will always be first evolution stage Pokemon.""" + display_name = "Randomize Static Pokemon" + default = 0 + option_vanilla = 0 + option_match_types = 1 + option_match_base_stats = 2 + option_match_types_and_base_stats = 3 + option_completely_random = 4 + + +class RandomizeLegendaryPokemon(Choice): + """Randomize Legendaries. Mew has been added as an encounter at the Vermilion dock truck. + Shuffle will shuffle the legendaries with each other. Static will shuffle them into other static Pokemon locations. + 'Any' will allow legendaries to appear anywhere based on wild and static randomization options, and their locations + will be randomized according to static Pokemon randomization options.""" + display_name = "Randomize Legendary Pokemon" + default = 0 + option_vanilla = 0 + option_shuffle = 1 + option_static = 2 + option_any = 3 + + +class CatchEmAll(Choice): + """Guarantee all first evolution stage Pokemon are available, or all Pokemon of all stages. + Currently only has an effect if wild Pokemon are randomized.""" + display_name = "Catch 'Em All" + default = 0 + option_off = 0 + alias_false = 0 + option_first_stage = 1 + option_all_pokemon = 2 + + +class RandomizeTrainerParties(Choice): + """Randomize enemy Pokemon encountered in trainer battles.""" + display_name = "Randomize Trainer Parties" + default = 0 + option_vanilla = 0 + option_match_types = 1 + option_match_base_stats = 2 + option_match_types_and_base_stats = 3 + option_completely_random = 4 + + +class TrainerLegendaries(Toggle): + """Allow legendary Pokemon in randomized trainer parties.""" + display_name = "Trainer Legendaries" + default = 0 + + +class BlindTrainers(Range): + """Chance each frame that you are standing on a tile in a trainer's line of sight that they will fail to initiate a + battle. If you move into and out of their line of sight without stopping, this chance will only trigger once.""" + display_name = "Blind Trainers" + range_start = 0 + range_end = 100 + default = 0 + + +class MinimumStepsBetweenEncounters(Range): + """Minimum number of steps between wild Pokemon encounters.""" + display_name = "Minimum Steps Between Encounters" + default = 3 + range_start = 0 + range_end = 255 + + +class RandomizePokemonStats(Choice): + """Randomize base stats for each Pokemon. Shuffle will shuffle the 5 base stat values amongst each other. Randomize + will completely randomize each stat, but will still add up to the same base stat total.""" + display_name = "Randomize Pokemon Stats" + default = 0 + option_vanilla = 0 + option_shuffle = 1 + option_randomize = 2 + + +class RandomizePokemonCatchRates(Toggle): + """Randomize the catch rate for each Pokemon.""" + display_name = "Randomize Catch Rates" + default = 0 + + +class MinimumCatchRate(Range): + """Minimum catch rate for each Pokemon. If randomize_catch_rates is on, this will be the minimum value that can be + chosen. Otherwise, it will raise any Pokemon's catch rate up to this value if its normal catch rate is lower.""" + display_name = "Minimum Catch Rate" + range_start = 1 + range_end = 255 + default = 3 + + +class RandomizePokemonMovesets(Choice): + """Randomize the moves learned by Pokemon. prefer_types will prefer moves that match the type of the Pokemon.""" + display_name = "Randomize Pokemon Movesets" + option_vanilla = 0 + option_prefer_types = 1 + option_completely_random = 2 + default = 0 + + +class StartWithFourMoves(Toggle): + """If movesets are randomized, this will give all Pokemon 4 starting moves.""" + display_name = "Start With Four Moves" + default = 0 + + +class TMCompatibility(Choice): + """Randomize which Pokemon can learn each TM. prefer_types: 90% chance if Pokemon's type matches the move, + 50% chance if move is Normal type and the Pokemon is not, and 25% chance otherwise. Pokemon will retain the same + TM compatibility when they evolve if the evolved form has the same type(s). Mew will always be able to learn + every TM.""" + display_name = "TM Compatibility" + default = 0 + option_vanilla = 0 + option_prefer_types = 1 + option_completely_random = 2 + option_full_compatibility = 3 + + +class HMCompatibility(Choice): + """Randomize which Pokemon can learn each HM. prefer_types: 100% chance if Pokemon's type matches the move, + 75% chance if move is Normal type and the Pokemon is not, and 25% chance otherwise. Pokemon will retain the same + HM compatibility when they evolve if the evolved form has the same type(s). Mew will always be able to learn + every HM.""" + display_name = "HM Compatibility" + default = 0 + option_vanilla = 0 + option_prefer_types = 1 + option_completely_random = 2 + option_full_compatibility = 3 + + +class RandomizePokemonTypes(Choice): + """Randomize the types of each Pokemon. Follow Evolutions will ensure Pokemon's types remain the same when evolving + (except possibly gaining a type).""" + display_name = "Pokemon Types" + option_vanilla = 0 + option_follow_evolutions = 1 + option_randomize = 2 + default = 0 + + +class SecondaryTypeChance(SpecialRange): + """If randomize_pokemon_types is on, this is the chance each Pokemon will have a secondary type. If follow_evolutions + is selected, it is the chance a second type will be added at each evolution stage. vanilla will give secondary types + to Pokemon that normally have a secondary type.""" + display_name = "Secondary Type Chance" + range_start = -1 + range_end = 100 + default = -1 + special_range_names = { + "vanilla": -1 + } + + +class RandomizeTypeChartTypes(Choice): + """The game's type chart consists of 3 columns: attacking type, defending type, and type effectiveness. + Matchups that have regular type effectiveness are not in the chart. Shuffle will shuffle the attacking types + across the attacking type column and the defending types across the defending type column (so for example Normal + type will still have exactly 2 types that it receives non-regular damage from, and 2 types it deals non-regular + damage to). Randomize will randomize each type in both columns to any random type.""" + display_name = "Randomize Type Chart Types" + option_vanilla = 0 + option_shuffle = 1 + option_randomize = 2 + default = 0 + + +class RandomizeTypeChartTypeEffectiveness(Choice): + """The game's type chart consists of 3 columns: attacking type, defending type, and type effectiveness. + Matchups that have regular type effectiveness are not in the chart. Shuffle will shuffle the type effectiveness + across the type effectiveness column (so for example there will always be 6 type immunities). Randomize will + randomize each entry in the table to no effect, not very effective, or super effective; with no effect occurring + at a low chance. Chaos will randomize the values to anywhere between 0% and 200% damage, in 10% increments.""" + display_name = "Randomize Type Chart Type Effectiveness" + option_vanilla = 0 + option_shuffle = 1 + option_randomize = 2 + option_chaos = 3 + default = 0 + + +class SafariZoneNormalBattles(Toggle): + """Change the Safari Zone to have standard wild pokemon battles.""" + display_name = "Safari Zone Normal Battles" + default = 0 + + +class NormalizeEncounterChances(Toggle): + """Each wild encounter table has 10 slots for Pokemon. Normally the chance for each being chosen ranges from + 19.9% to 1.2%. Turn this on to normalize them all to 10% each.""" + display_name = "Normalize Encounter Chances" + default = 0 + + +class ReusableTMs(Toggle): + """Makes TMs reusable, so they will not be consumed upon use.""" + display_name = "Reusable TMs" + default = 0 + + +class StartingMoney(Range): + """The amount of money you start with.""" + display_name = "Starting Money" + default = 3000 + range_start = 0 + range_end = 999999 + + +pokemon_rb_options = { + "game_version": GameVersion, + "trainer_name": TrainerName, + "rival_name": RivalName, + #"goal": Goal, + "elite_four_condition": EliteFourCondition, + "victory_road_condition": VictoryRoadCondition, + "viridian_gym_condition": ViridianGymCondition, + "cerulean_cave_condition": CeruleanCaveCondition, + "second_fossil_check_condition": SecondFossilCheckCondition, + "badgesanity": BadgeSanity, + "old_man": OldMan, + "tea": Tea, + "extra_key_items": ExtraKeyItems, + "extra_strength_boulders": ExtraStrengthBoulders, + "require_item_finder": RequireItemFinder, + "randomize_hidden_items": RandomizeHiddenItems, + "badges_needed_for_hm_moves": BadgesNeededForHMMoves, + "free_fly_location": FreeFlyLocation, + "oaks_aide_rt_2": OaksAidRt2, + "oaks_aide_rt_11": OaksAidRt11, + "oaks_aide_rt_15": OaksAidRt15, + "blind_trainers": BlindTrainers, + "minimum_steps_between_encounters": MinimumStepsBetweenEncounters, + "exp_modifier": ExpModifier, + "randomize_wild_pokemon": RandomizeWildPokemon, + "randomize_starter_pokemon": RandomizeStarterPokemon, + "randomize_static_pokemon": RandomizeStaticPokemon, + "randomize_legendary_pokemon": RandomizeLegendaryPokemon, + "catch_em_all": CatchEmAll, + "randomize_pokemon_stats": RandomizePokemonStats, + "randomize_pokemon_catch_rates": RandomizePokemonCatchRates, + "minimum_catch_rate": MinimumCatchRate, + "randomize_trainer_parties": RandomizeTrainerParties, + "trainer_legendaries": TrainerLegendaries, + "randomize_pokemon_movesets": RandomizePokemonMovesets, + "start_with_four_moves": StartWithFourMoves, + "tm_compatibility": TMCompatibility, + "hm_compatibility": HMCompatibility, + "randomize_pokemon_types": RandomizePokemonTypes, + "secondary_type_chance": SecondaryTypeChance, + "randomize_type_matchup_types": RandomizeTypeChartTypes, + "randomize_type_matchup_type_effectiveness": RandomizeTypeChartTypeEffectiveness, + "safari_zone_normal_battles": SafariZoneNormalBattles, + "normalize_encounter_chances": NormalizeEncounterChances, + "reusable_tms": ReusableTMs, + "starting_money": StartingMoney, +} \ No newline at end of file diff --git a/worlds/pokemon_rb/poke_data.py b/worlds/pokemon_rb/poke_data.py new file mode 100644 index 0000000000..75222d570f --- /dev/null +++ b/worlds/pokemon_rb/poke_data.py @@ -0,0 +1,1212 @@ +id_to_mon = {1: 'Rhydon', 2: 'Kangaskhan', 3: 'Nidoran M', 4: 'Clefairy', 5: 'Spearow', 6: 'Voltorb', 7: 'Nidoking', + 8: 'Slowbro', 9: 'Ivysaur', 10: 'Exeggutor', 11: 'Lickitung', 12: 'Exeggcute', 13: 'Grimer', 14: 'Gengar', + 15: 'Nidoran F', 16: 'Nidoqueen', 17: 'Cubone', 18: 'Rhyhorn', 19: 'Lapras', 20: 'Arcanine', 21: 'Mew', + 22: 'Gyarados', 23: 'Shellder', 24: 'Tentacool', 25: 'Gastly', 26: 'Scyther', 27: 'Staryu', + 28: 'Blastoise', 29: 'Pinsir', 30: 'Tangela', 33: 'Growlithe', 34: 'Onix', 35: 'Fearow', 36: 'Pidgey', + 37: 'Slowpoke', 38: 'Kadabra', 39: 'Graveler', 40: 'Chansey', 41: 'Machoke', 42: 'Mr Mime', + 43: 'Hitmonlee', 44: 'Hitmonchan', 45: 'Arbok', 46: 'Parasect', 47: 'Psyduck', 48: 'Drowzee', 49: 'Golem', + 51: 'Magmar', 53: 'Electabuzz', 54: 'Magneton', 55: 'Koffing', 57: 'Mankey', 58: 'Seel', 59: 'Diglett', + 60: 'Tauros', 64: 'Farfetchd', 65: 'Venonat', 66: 'Dragonite', 70: 'Doduo', 71: 'Poliwag', 72: 'Jynx', + 73: 'Moltres', 74: 'Articuno', 75: 'Zapdos', 76: 'Ditto', 77: 'Meowth', 78: 'Krabby', 82: 'Vulpix', + 83: 'Ninetales', 84: 'Pikachu', 85: 'Raichu', 88: 'Dratini', 89: 'Dragonair', 90: 'Kabuto', 91: 'Kabutops', + 92: 'Horsea', 93: 'Seadra', 96: 'Sandshrew', 97: 'Sandslash', 98: 'Omanyte', 99: 'Omastar', + 100: 'Jigglypuff', 101: 'Wigglytuff', 102: 'Eevee', 103: 'Flareon', 104: 'Jolteon', 105: 'Vaporeon', + 106: 'Machop', 107: 'Zubat', 108: 'Ekans', 109: 'Paras', 110: 'Poliwhirl', 111: 'Poliwrath', 112: 'Weedle', + 113: 'Kakuna', 114: 'Beedrill', 116: 'Dodrio', 117: 'Primeape', 118: 'Dugtrio', 119: 'Venomoth', + 120: 'Dewgong', 123: 'Caterpie', 124: 'Metapod', 125: 'Butterfree', 126: 'Machamp', 128: 'Golduck', + 129: 'Hypno', 130: 'Golbat', 131: 'Mewtwo', 132: 'Snorlax', 133: 'Magikarp', 136: 'Muk', 138: 'Kingler', + 139: 'Cloyster', 141: 'Electrode', 142: 'Clefable', 143: 'Weezing', 144: 'Persian', 145: 'Marowak', + 147: 'Haunter', 148: 'Abra', 149: 'Alakazam', 150: 'Pidgeotto', 151: 'Pidgeot', 152: 'Starmie', + 153: 'Bulbasaur', 154: 'Venusaur', 155: 'Tentacruel', 157: 'Goldeen', 158: 'Seaking', 163: 'Ponyta', + 164: 'Rapidash', 165: 'Rattata', 166: 'Raticate', 167: 'Nidorino', 168: 'Nidorina', 169: 'Geodude', + 170: 'Porygon', 171: 'Aerodactyl', 173: 'Magnemite', 176: 'Charmander', 177: 'Squirtle', 178: 'Charmeleon', + 179: 'Wartortle', 180: 'Charizard', 185: 'Oddish', 186: 'Gloom', 187: 'Vileplume', 188: 'Bellsprout', + 189: 'Weepinbell', 190: 'Victreebel'} + + +pokemon_dex = { + 'Bulbasaur': 1, 'Ivysaur': 2, 'Venusaur': 3, 'Charmander': 4, 'Charmeleon': 5, 'Charizard': 6, 'Squirtle': 7, + 'Wartortle': 8, 'Blastoise': 9, 'Caterpie': 10, 'Metapod': 11, 'Butterfree': 12, 'Weedle': 13, 'Kakuna': 14, + 'Beedrill': 15, 'Pidgey': 16, 'Pidgeotto': 17, 'Pidgeot': 18, 'Rattata': 19, 'Raticate': 20, 'Spearow': 21, + 'Fearow': 22, 'Ekans': 23, 'Arbok': 24, 'Pikachu': 25, 'Raichu': 26, 'Sandshrew': 27, 'Sandslash': 28, + 'Nidoran F': 29, 'Nidorina': 30, 'Nidoqueen': 31, 'Nidoran M': 32, 'Nidorino': 33, 'Nidoking': 34, 'Clefairy': 35, + 'Clefable': 36, 'Vulpix': 37, 'Ninetales': 38, 'Jigglypuff': 39, 'Wigglytuff': 40, 'Zubat': 41, 'Golbat': 42, + 'Oddish': 43, 'Gloom': 44, 'Vileplume': 45, 'Paras': 46, 'Parasect': 47, 'Venonat': 48, 'Venomoth': 49, + 'Diglett': 50, 'Dugtrio': 51, 'Meowth': 52, 'Persian': 53, 'Psyduck': 54, 'Golduck': 55, 'Mankey': 56, + 'Primeape': 57, 'Growlithe': 58, 'Arcanine': 59, 'Poliwag': 60, 'Poliwhirl': 61, 'Poliwrath': 62, 'Abra': 63, + 'Kadabra': 64, 'Alakazam': 65, 'Machop': 66, 'Machoke': 67, 'Machamp': 68, 'Bellsprout': 69, 'Weepinbell': 70, + 'Victreebel': 71, 'Tentacool': 72, 'Tentacruel': 73, 'Geodude': 74, 'Graveler': 75, 'Golem': 76, 'Ponyta': 77, + 'Rapidash': 78, 'Slowpoke': 79, 'Slowbro': 80, 'Magnemite': 81, 'Magneton': 82, 'Farfetchd': 83, 'Doduo': 84, + 'Dodrio': 85, 'Seel': 86, 'Dewgong': 87, 'Grimer': 88, 'Muk': 89, 'Shellder': 90, 'Cloyster': 91, 'Gastly': 92, + 'Haunter': 93, 'Gengar': 94, 'Onix': 95, 'Drowzee': 96, 'Hypno': 97, 'Krabby': 98, 'Kingler': 99, 'Voltorb': 100, + 'Electrode': 101, 'Exeggcute': 102, 'Exeggutor': 103, 'Cubone': 104, 'Marowak': 105, 'Hitmonlee': 106, + 'Hitmonchan': 107, 'Lickitung': 108, 'Koffing': 109, 'Weezing': 110, 'Rhyhorn': 111, 'Rhydon': 112, 'Chansey': 113, + 'Tangela': 114, 'Kangaskhan': 115, 'Horsea': 116, 'Seadra': 117, 'Goldeen': 118, 'Seaking': 119, 'Staryu': 120, + 'Starmie': 121, 'Mr Mime': 122, 'Scyther': 123, 'Jynx': 124, 'Electabuzz': 125, 'Magmar': 126, 'Pinsir': 127, + 'Tauros': 128, 'Magikarp': 129, 'Gyarados': 130, 'Lapras': 131, 'Ditto': 132, 'Eevee': 133, 'Vaporeon': 134, + 'Jolteon': 135, 'Flareon': 136, 'Porygon': 137, 'Omanyte': 138, 'Omastar': 139, 'Kabuto': 140, 'Kabutops': 141, + 'Aerodactyl': 142, 'Snorlax': 143, 'Articuno': 144, 'Zapdos': 145, 'Moltres': 146, 'Dratini': 147, 'Dragonair': 148, + 'Dragonite': 149, 'Mewtwo': 150, 'Mew': 151 +} + + +type_ids = { + "Normal": 0x0, + "Fighting": 0x1, + "Flying": 0x2, + "Poison": 0x3, + "Ground": 0x4, + "Rock": 0x5, + "Bug": 0x7, + "Ghost": 0x8, + "Fire": 0x14, + "Water": 0x15, + "Grass": 0x16, + "Electric": 0x17, + "Psychic": 0x18, + "Ice": 0x19, + "Dragon": 0x1A +} +type_names = { + 0x0: "Normal", + 0x1: "Fighting", + 0x2: "Flying", + 0x3: "Poison", + 0x4: "Ground", + 0x5: "Rock", + 0x7: "Bug", + 0x8: "Ghost", + 0x14: "Fire", + 0x15: "Water", + 0x16: "Grass", + 0x17: "Electric", + 0x18: "Psychic", + 0x19: "Ice", + 0x1a: "Dragon" +} + +type_chart = [ + ["Water", "Fire", 20], + ["Fire", "Grass", 20], + ["Fire", "Ice", 20], + ["Grass", "Water", 20], + ["Electric", "Water", 20], + ["Water", "Rock", 20], + ["Ground", "Flying", 0], + ["Water", "Water", 5], + ["Fire", "Fire", 5], + ["Electric", "Electric", 5], + ["Ice", "Ice", 5], + ["Grass", "Grass", 5], + ["Psychic", "Psychic", 5], + ["Fire", "Water", 5], + ["Grass", "Fire", 5], + ["Water", "Grass", 5], + ["Electric", "Grass", 5], + ["Normal", "Rock", 5], + ["Normal", "Ghost", 0], + ["Ghost", "Ghost", 20], + ["Fire", "Bug", 20], + ["Fire", "Rock", 5], + ["Water", "Ground", 20], + ["Electric", "Ground", 0], + ["Electric", "Flying", 20], + ["Grass", "Ground", 20], + ["Grass", "Bug", 5], + ["Grass", "Poison", 5], + ["Grass", "Rock", 20], + ["Grass", "Flying", 5], + ["Ice", "Water", 5], + ["Ice", "Grass", 20], + ["Ice", "Ground", 20], + ["Ice", "Flying", 20], + ["Fighting", "Normal", 20], + ["Fighting", "Poison", 5], + ["Fighting", "Flying", 5], + ["Fighting", "Psychic", 5], + ["Fighting", "Bug", 5], + ["Fighting", "Rock", 20], + ["Fighting", "Ice", 20], + ["Fighting", "Ghost", 0], + ["Poison", "Grass", 20], + ["Poison", "Poison", 5], + ["Poison", "Ground", 5], + ["Poison", "Bug", 20], + ["Poison", "Rock", 5], + ["Poison", "Ghost", 5], + ["Ground", "Fire", 20], + ["Ground", "Electric", 20], + ["Ground", "Grass", 5], + ["Ground", "Bug", 5], + ["Ground", "Rock", 20], + ["Ground", "Poison", 20], + ["Flying", "Electric", 5], + ["Flying", "Fighting", 20], + ["Flying", "Bug", 20], + ["Flying", "Grass", 20], + ["Flying", "Rock", 5], + ["Psychic", "Fighting", 20], + ["Psychic", "Poison", 20], + ["Bug", "Fire", 5], + ["Bug", "Grass", 20], + ["Bug", "Fighting", 5], + ["Bug", "Flying", 5], + ["Bug", "Psychic", 20], + ["Bug", "Ghost", 5], + ["Bug", "Poison", 20], + ["Rock", "Fire", 20], + ["Rock", "Fighting", 5], + ["Rock", "Ground", 5], + ["Rock", "Flying", 20], + ["Rock", "Bug", 20], + ["Rock", "Ice", 20], + ["Ghost", "Normal", 0], + ["Ghost", "Psychic", 0], + ["Fire", "Dragon", 5], + ["Water", "Dragon", 5], + ["Electric", "Dragon", 5], + ["Grass", "Dragon", 5], + ["Ice", "Dragon", 20], + ["Dragon", "Dragon", 20] +] + +pokemon_data = { + 'Bulbasaur': {'id': 153, 'dex': 1, 'hp': 45, 'atk': 49, 'def': 49, 'spd': 45, 'spc': 65, 'type1': 'Grass', + 'type2': 'Poison', 'catch rate': 45, 'base exp': 64, 'start move 1': 'Tackle', + 'start move 2': 'Growl', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xa4\x038\xc0\x03\x08\x06')}, + 'Ivysaur': {'id': 9, 'dex': 2, 'hp': 60, 'atk': 62, 'def': 63, 'spd': 60, 'spc': 80, 'type1': 'Grass', + 'type2': 'Poison', 'catch rate': 45, 'base exp': 141, 'start move 1': 'Tackle', 'start move 2': 'Growl', + 'start move 3': 'Leech Seed', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xa4\x038\xc0\x03\x08\x06')}, + 'Venusaur': {'id': 154, 'dex': 3, 'hp': 80, 'atk': 82, 'def': 83, 'spd': 80, 'spc': 100, 'type1': 'Grass', + 'type2': 'Poison', 'catch rate': 45, 'base exp': 208, 'start move 1': 'Tackle', + 'start move 2': 'Growl', 'start move 3': 'Leech Seed', 'start move 4': 'Vine Whip', 'growth rate': 3, + 'tms': bytearray(b'\xa4C8\xc0\x03\x08\x06')}, + 'Charmander': {'id': 176, 'dex': 4, 'hp': 39, 'atk': 52, 'def': 43, 'spd': 65, 'spc': 50, 'type1': 'Fire', + 'type2': 'Fire', 'catch rate': 45, 'base exp': 65, 'start move 1': 'Scratch', + 'start move 2': 'Growl', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xb5\x03O\xc8\xe3\x08&')}, + 'Charmeleon': {'id': 178, 'dex': 5, 'hp': 58, 'atk': 64, 'def': 58, 'spd': 80, 'spc': 65, 'type1': 'Fire', + 'type2': 'Fire', 'catch rate': 45, 'base exp': 142, 'start move 1': 'Scratch', + 'start move 2': 'Growl', 'start move 3': 'Ember', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xb5\x03O\xc8\xe3\x08&')}, + 'Charizard': {'id': 180, 'dex': 6, 'hp': 78, 'atk': 84, 'def': 78, 'spd': 100, 'spc': 85, 'type1': 'Fire', + 'type2': 'Flying', 'catch rate': 45, 'base exp': 209, 'start move 1': 'Scratch', + 'start move 2': 'Growl', 'start move 3': 'Ember', 'start move 4': 'Leer', 'growth rate': 3, + 'tms': bytearray(b'\xb5CO\xce\xe3\x08&')}, + 'Squirtle': {'id': 177, 'dex': 7, 'hp': 44, 'atk': 48, 'def': 65, 'spd': 43, 'spc': 50, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 45, 'base exp': 66, 'start move 1': 'Tackle', + 'start move 2': 'Tail Whip', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xb1?\x0f\xc8\x83\x082')}, + 'Wartortle': {'id': 179, 'dex': 8, 'hp': 59, 'atk': 63, 'def': 80, 'spd': 58, 'spc': 65, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 45, 'base exp': 143, 'start move 1': 'Tackle', + 'start move 2': 'Tail Whip', 'start move 3': 'Bubble', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xb1?\x0f\xc8\x83\x082')}, + 'Blastoise': {'id': 28, 'dex': 9, 'hp': 79, 'atk': 83, 'def': 100, 'spd': 78, 'spc': 85, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 45, 'base exp': 210, 'start move 1': 'Tackle', + 'start move 2': 'Tail Whip', 'start move 3': 'Bubble', 'start move 4': 'Water Gun', 'growth rate': 3, + 'tms': bytearray(b'\xb1\x7f\x0f\xce\x83\x082')}, + 'Caterpie': {'id': 123, 'dex': 10, 'hp': 45, 'atk': 30, 'def': 35, 'spd': 45, 'spc': 20, 'type1': 'Bug', + 'type2': 'Bug', 'catch rate': 255, 'base exp': 53, 'start move 1': 'Tackle', + 'start move 2': 'String Shot', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\x00\x00\x00\x00\x00\x00\x00')}, + 'Metapod': {'id': 124, 'dex': 11, 'hp': 50, 'atk': 20, 'def': 55, 'spd': 30, 'spc': 25, 'type1': 'Bug', + 'type2': 'Bug', 'catch rate': 120, 'base exp': 72, 'start move 1': 'Harden', 'start move 2': 'No Move', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\x00\x00\x00\x00\x00\x00\x00')}, + 'Butterfree': {'id': 125, 'dex': 12, 'hp': 60, 'atk': 45, 'def': 50, 'spd': 70, 'spc': 80, 'type1': 'Bug', + 'type2': 'Flying', 'catch rate': 45, 'base exp': 160, 'start move 1': 'Confusion', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'*C8\xf0C(\x02')}, + 'Weedle': {'id': 112, 'dex': 13, 'hp': 40, 'atk': 35, 'def': 30, 'spd': 50, 'spc': 20, 'type1': 'Bug', + 'type2': 'Poison', 'catch rate': 255, 'base exp': 52, 'start move 1': 'Poison Sting', + 'start move 2': 'String Shot', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\x00\x00\x00\x00\x00\x00\x00')}, + 'Kakuna': {'id': 113, 'dex': 14, 'hp': 45, 'atk': 25, 'def': 50, 'spd': 35, 'spc': 25, 'type1': 'Bug', + 'type2': 'Poison', 'catch rate': 120, 'base exp': 71, 'start move 1': 'Harden', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\x00\x00\x00\x00\x00\x00\x00')}, + 'Beedrill': {'id': 114, 'dex': 15, 'hp': 65, 'atk': 80, 'def': 40, 'spd': 75, 'spc': 45, 'type1': 'Bug', + 'type2': 'Poison', 'catch rate': 45, 'base exp': 159, 'start move 1': 'Fury Attack', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'$C\x18\xc0\xc3\x08\x06')}, + 'Pidgey': {'id': 36, 'dex': 16, 'hp': 40, 'atk': 45, 'def': 40, 'spd': 56, 'spc': 35, 'type1': 'Normal', + 'type2': 'Flying', 'catch rate': 255, 'base exp': 55, 'start move 1': 'Gust', 'start move 2': 'No Move', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'*\x03\x08\xc0C\x0c\n')}, + 'Pidgeotto': {'id': 150, 'dex': 17, 'hp': 63, 'atk': 60, 'def': 55, 'spd': 71, 'spc': 50, 'type1': 'Normal', + 'type2': 'Flying', 'catch rate': 120, 'base exp': 113, 'start move 1': 'Gust', + 'start move 2': 'Sand Attack', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'*\x03\x08\xc0C\x0c\n')}, + 'Pidgeot': {'id': 151, 'dex': 18, 'hp': 83, 'atk': 80, 'def': 75, 'spd': 91, 'spc': 70, 'type1': 'Normal', + 'type2': 'Flying', 'catch rate': 45, 'base exp': 172, 'start move 1': 'Gust', + 'start move 2': 'Sand Attack', 'start move 3': 'Quick Attack', 'start move 4': 'No Move', + 'growth rate': 3, 'tms': bytearray(b'*C\x08\xc0C\x0c\n')}, + 'Rattata': {'id': 165, 'dex': 19, 'hp': 30, 'atk': 56, 'def': 35, 'spd': 72, 'spc': 25, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 255, 'base exp': 57, 'start move 1': 'Tackle', + 'start move 2': 'Tail Whip', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0/\x88\xc9\xc2\x08\x02')}, + 'Raticate': {'id': 166, 'dex': 20, 'hp': 55, 'atk': 81, 'def': 60, 'spd': 97, 'spc': 50, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 90, 'base exp': 116, 'start move 1': 'Tackle', + 'start move 2': 'Tail Whip', 'start move 3': 'Quick Attack', 'start move 4': 'No Move', + 'growth rate': 0, 'tms': bytearray(b'\xa0\x7f\x88\xc9\xc2\x08\x02')}, + 'Spearow': {'id': 5, 'dex': 21, 'hp': 40, 'atk': 60, 'def': 30, 'spd': 70, 'spc': 31, 'type1': 'Normal', + 'type2': 'Flying', 'catch rate': 255, 'base exp': 58, 'start move 1': 'Peck', 'start move 2': 'Growl', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'*\x03\x08\xc0B\x0c\n')}, + 'Fearow': {'id': 35, 'dex': 22, 'hp': 65, 'atk': 90, 'def': 65, 'spd': 100, 'spc': 61, 'type1': 'Normal', + 'type2': 'Flying', 'catch rate': 90, 'base exp': 162, 'start move 1': 'Peck', 'start move 2': 'Growl', + 'start move 3': 'Leer', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'*C\x08\xc0B\x0c\n')}, + 'Ekans': {'id': 108, 'dex': 23, 'hp': 35, 'atk': 60, 'def': 44, 'spd': 55, 'spc': 40, 'type1': 'Poison', + 'type2': 'Poison', 'catch rate': 255, 'base exp': 62, 'start move 1': 'Wrap', 'start move 2': 'Leer', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0\x03\x18\xce\x82\x88"')}, + 'Arbok': {'id': 45, 'dex': 24, 'hp': 60, 'atk': 85, 'def': 69, 'spd': 80, 'spc': 65, 'type1': 'Poison', + 'type2': 'Poison', 'catch rate': 90, 'base exp': 147, 'start move 1': 'Wrap', 'start move 2': 'Leer', + 'start move 3': 'Poison Sting', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0C\x18\xce\x82\x88"')}, + 'Pikachu': {'id': 84, 'dex': 25, 'hp': 35, 'atk': 55, 'def': 30, 'spd': 90, 'spc': 50, 'type1': 'Electric', + 'type2': 'Electric', 'catch rate': 190, 'base exp': 82, 'start move 1': 'Thundershock', + 'start move 2': 'Growl', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1\x83\x8d\xc1\xc3\x18B')}, + 'Raichu': {'id': 85, 'dex': 26, 'hp': 60, 'atk': 90, 'def': 55, 'spd': 100, 'spc': 90, 'type1': 'Electric', + 'type2': 'Electric', 'catch rate': 75, 'base exp': 122, 'start move 1': 'Thundershock', + 'start move 2': 'Growl', 'start move 3': 'Thunder Wave', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1\xc3\x8d\xc1\xc3\x18B')}, + 'Sandshrew': {'id': 96, 'dex': 27, 'hp': 50, 'atk': 75, 'def': 85, 'spd': 40, 'spc': 30, 'type1': 'Ground', + 'type2': 'Ground', 'catch rate': 255, 'base exp': 93, 'start move 1': 'Scratch', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa4\x03\r\xce\xc2\x88&')}, + 'Sandslash': {'id': 97, 'dex': 28, 'hp': 75, 'atk': 100, 'def': 110, 'spd': 65, 'spc': 55, 'type1': 'Ground', + 'type2': 'Ground', 'catch rate': 90, 'base exp': 163, 'start move 1': 'Scratch', + 'start move 2': 'Sand Attack', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa4C\r\xce\xc2\x88&')}, + 'Nidoran F': {'id': 15, 'dex': 29, 'hp': 55, 'atk': 47, 'def': 52, 'spd': 41, 'spc': 40, 'type1': 'Poison', + 'type2': 'Poison', 'catch rate': 235, 'base exp': 59, 'start move 1': 'Growl', + 'start move 2': 'Tackle', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xa0#\x88\xc1\x83\x08\x02')}, + 'Nidorina': {'id': 168, 'dex': 30, 'hp': 70, 'atk': 62, 'def': 67, 'spd': 56, 'spc': 55, 'type1': 'Poison', + 'type2': 'Poison', 'catch rate': 120, 'base exp': 117, 'start move 1': 'Growl', + 'start move 2': 'Tackle', 'start move 3': 'Scratch', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xe0?\x88\xc1\x83\x08\x02')}, + 'Nidoqueen': {'id': 16, 'dex': 31, 'hp': 90, 'atk': 82, 'def': 87, 'spd': 76, 'spc': 75, 'type1': 'Poison', + 'type2': 'Ground', 'catch rate': 45, 'base exp': 194, 'start move 1': 'Tackle', + 'start move 2': 'Scratch', 'start move 3': 'Tail Whip', 'start move 4': 'Body Slam', 'growth rate': 3, + 'tms': bytearray(b'\xf1\xff\x8f\xc7\xa3\x882')}, + 'Nidoran M': {'id': 3, 'dex': 32, 'hp': 46, 'atk': 57, 'def': 40, 'spd': 50, 'spc': 40, 'type1': 'Poison', + 'type2': 'Poison', 'catch rate': 235, 'base exp': 60, 'start move 1': 'Leer', + 'start move 2': 'Tackle', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xe0#\x88\xc1\x83\x08\x02')}, + 'Nidorino': {'id': 167, 'dex': 33, 'hp': 61, 'atk': 72, 'def': 57, 'spd': 65, 'spc': 55, 'type1': 'Poison', + 'type2': 'Poison', 'catch rate': 120, 'base exp': 118, 'start move 1': 'Leer', + 'start move 2': 'Tackle', 'start move 3': 'Horn Attack', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xe0?\x88\xc1\x83\x08\x02')}, + 'Nidoking': {'id': 7, 'dex': 34, 'hp': 81, 'atk': 92, 'def': 77, 'spd': 85, 'spc': 75, 'type1': 'Poison', + 'type2': 'Ground', 'catch rate': 45, 'base exp': 195, 'start move 1': 'Tackle', + 'start move 2': 'Horn Attack', 'start move 3': 'Poison Sting', 'start move 4': 'Thrash', + 'growth rate': 3, 'tms': bytearray(b'\xf1\xff\x8f\xc7\xa3\x882')}, + 'Clefairy': {'id': 4, 'dex': 35, 'hp': 70, 'atk': 45, 'def': 48, 'spd': 35, 'spc': 60, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 150, 'base exp': 68, 'start move 1': 'Pound', 'start move 2': 'Growl', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 4, + 'tms': bytearray(b'\xb1?\xaf\xf1\xa78c')}, + 'Clefable': {'id': 142, 'dex': 36, 'hp': 95, 'atk': 70, 'def': 73, 'spd': 60, 'spc': 85, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 25, 'base exp': 129, 'start move 1': 'Sing', + 'start move 2': 'Doubleslap', 'start move 3': 'Minimize', 'start move 4': 'Metronome', + 'growth rate': 4, 'tms': bytearray(b'\xb1\x7f\xaf\xf1\xa78c')}, + 'Vulpix': {'id': 82, 'dex': 37, 'hp': 38, 'atk': 41, 'def': 40, 'spd': 65, 'spc': 65, 'type1': 'Fire', + 'type2': 'Fire', 'catch rate': 190, 'base exp': 63, 'start move 1': 'Ember', 'start move 2': 'Tail Whip', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0\x03\x08\xc8\xe3\x08\x02')}, + 'Ninetales': {'id': 83, 'dex': 38, 'hp': 73, 'atk': 76, 'def': 75, 'spd': 100, 'spc': 100, 'type1': 'Fire', + 'type2': 'Fire', 'catch rate': 75, 'base exp': 178, 'start move 1': 'Ember', + 'start move 2': 'Tail Whip', 'start move 3': 'Quick Attack', 'start move 4': 'Roar', 'growth rate': 0, + 'tms': bytearray(b'\xa0C\x08\xc8\xe3\x08\x02')}, + 'Jigglypuff': {'id': 100, 'dex': 39, 'hp': 115, 'atk': 45, 'def': 20, 'spd': 20, 'spc': 25, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 170, 'base exp': 76, 'start move 1': 'Sing', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 4, + 'tms': bytearray(b'\xb1?\xaf\xf1\xa38c')}, + 'Wigglytuff': {'id': 101, 'dex': 40, 'hp': 140, 'atk': 70, 'def': 45, 'spd': 45, 'spc': 50, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 50, 'base exp': 109, 'start move 1': 'Sing', + 'start move 2': 'Disable', 'start move 3': 'Defense Curl', 'start move 4': 'Doubleslap', + 'growth rate': 4, 'tms': bytearray(b'\xb1\x7f\xaf\xf1\xa38c')}, + 'Zubat': {'id': 107, 'dex': 41, 'hp': 40, 'atk': 45, 'def': 35, 'spd': 55, 'spc': 40, 'type1': 'Poison', + 'type2': 'Flying', 'catch rate': 255, 'base exp': 54, 'start move 1': 'Leech Life', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'*\x03\x18\xc0B\x08\x02')}, + 'Golbat': {'id': 130, 'dex': 42, 'hp': 75, 'atk': 80, 'def': 70, 'spd': 90, 'spc': 75, 'type1': 'Poison', + 'type2': 'Flying', 'catch rate': 90, 'base exp': 171, 'start move 1': 'Leech Life', + 'start move 2': 'Screech', 'start move 3': 'Bite', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'*C\x18\xc0B\x08\x02')}, + 'Oddish': {'id': 185, 'dex': 43, 'hp': 45, 'atk': 50, 'def': 55, 'spd': 30, 'spc': 75, 'type1': 'Grass', + 'type2': 'Poison', 'catch rate': 255, 'base exp': 78, 'start move 1': 'Absorb', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'$\x038\xc0\x03\x08\x06')}, + 'Gloom': {'id': 186, 'dex': 44, 'hp': 60, 'atk': 65, 'def': 70, 'spd': 40, 'spc': 85, 'type1': 'Grass', + 'type2': 'Poison', 'catch rate': 120, 'base exp': 132, 'start move 1': 'Absorb', + 'start move 2': 'Poisonpowder', 'start move 3': 'Stun Spore', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'$\x038\xc0\x03\x08\x06')}, + 'Vileplume': {'id': 187, 'dex': 45, 'hp': 75, 'atk': 80, 'def': 85, 'spd': 50, 'spc': 100, 'type1': 'Grass', + 'type2': 'Poison', 'catch rate': 45, 'base exp': 184, 'start move 1': 'Stun Spore', + 'start move 2': 'Sleep Powder', 'start move 3': 'Acid', 'start move 4': 'Petal Dance', + 'growth rate': 3, 'tms': bytearray(b'\xa4C8\xc0\x03\x08\x06')}, + 'Paras': {'id': 109, 'dex': 46, 'hp': 35, 'atk': 70, 'def': 55, 'spd': 25, 'spc': 55, 'type1': 'Bug', + 'type2': 'Grass', 'catch rate': 190, 'base exp': 70, 'start move 1': 'Scratch', 'start move 2': 'No Move', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa4\x038\xc8\x83\x08\x06')}, + 'Parasect': {'id': 46, 'dex': 47, 'hp': 60, 'atk': 95, 'def': 80, 'spd': 30, 'spc': 80, 'type1': 'Bug', + 'type2': 'Grass', 'catch rate': 75, 'base exp': 128, 'start move 1': 'Scratch', + 'start move 2': 'Stun Spore', 'start move 3': 'Leech Life', 'start move 4': 'No Move', + 'growth rate': 0, 'tms': bytearray(b'\xa4C8\xc8\x83\x08\x06')}, + 'Venonat': {'id': 65, 'dex': 48, 'hp': 60, 'atk': 55, 'def': 50, 'spd': 45, 'spc': 40, 'type1': 'Bug', + 'type2': 'Poison', 'catch rate': 190, 'base exp': 75, 'start move 1': 'Tackle', + 'start move 2': 'Disable', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b' \x038\xd0\x03(\x02')}, + 'Venomoth': {'id': 119, 'dex': 49, 'hp': 70, 'atk': 65, 'def': 60, 'spd': 90, 'spc': 90, 'type1': 'Bug', + 'type2': 'Poison', 'catch rate': 75, 'base exp': 138, 'start move 1': 'Tackle', + 'start move 2': 'Disable', 'start move 3': 'Poisonpowder', 'start move 4': 'Leech Life', + 'growth rate': 0, 'tms': bytearray(b'*C8\xf0C(\x02')}, + 'Diglett': {'id': 59, 'dex': 50, 'hp': 10, 'atk': 55, 'def': 25, 'spd': 95, 'spc': 45, 'type1': 'Ground', + 'type2': 'Ground', 'catch rate': 255, 'base exp': 81, 'start move 1': 'Scratch', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0\x03\x08\xce\x02\x88\x02')}, + 'Dugtrio': {'id': 118, 'dex': 51, 'hp': 35, 'atk': 80, 'def': 50, 'spd': 120, 'spc': 70, 'type1': 'Ground', + 'type2': 'Ground', 'catch rate': 50, 'base exp': 153, 'start move 1': 'Scratch', + 'start move 2': 'Growl', 'start move 3': 'Dig', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0C\x08\xce\x02\x88\x02')}, + 'Meowth': {'id': 77, 'dex': 52, 'hp': 40, 'atk': 45, 'def': 35, 'spd': 90, 'spc': 40, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 255, 'base exp': 69, 'start move 1': 'Scratch', 'start move 2': 'Growl', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0\x8f\x88\xc1\xc2\x08\x02')}, + 'Persian': {'id': 144, 'dex': 53, 'hp': 65, 'atk': 70, 'def': 60, 'spd': 115, 'spc': 65, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 90, 'base exp': 148, 'start move 1': 'Scratch', + 'start move 2': 'Growl', 'start move 3': 'Bite', 'start move 4': 'Screech', 'growth rate': 0, + 'tms': bytearray(b'\xa0\xcf\x88\xc1\xc2\x08\x02')}, + 'Psyduck': {'id': 47, 'dex': 54, 'hp': 50, 'atk': 52, 'def': 48, 'spd': 55, 'spc': 50, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 190, 'base exp': 80, 'start move 1': 'Scratch', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1\xbf\x0f\xc8\xc2\x082')}, + 'Golduck': {'id': 128, 'dex': 55, 'hp': 80, 'atk': 82, 'def': 78, 'spd': 85, 'spc': 80, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 75, 'base exp': 174, 'start move 1': 'Scratch', + 'start move 2': 'Tail Whip', 'start move 3': 'Disable', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1\xff\x0f\xc8\xc2\x082')}, + 'Mankey': {'id': 57, 'dex': 56, 'hp': 40, 'atk': 80, 'def': 35, 'spd': 70, 'spc': 35, 'type1': 'Fighting', + 'type2': 'Fighting', 'catch rate': 190, 'base exp': 74, 'start move 1': 'Scratch', + 'start move 2': 'Leer', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1\x83\x8f\xc9\xc6\x88"')}, + 'Primeape': {'id': 117, 'dex': 57, 'hp': 65, 'atk': 105, 'def': 60, 'spd': 95, 'spc': 60, 'type1': 'Fighting', + 'type2': 'Fighting', 'catch rate': 75, 'base exp': 149, 'start move 1': 'Scratch', + 'start move 2': 'Leer', 'start move 3': 'Karate Chop', 'start move 4': 'Fury Swipes', 'growth rate': 0, + 'tms': bytearray(b'\xb1\xc3\x8f\xc9\xc6\x88"')}, + 'Growlithe': {'id': 33, 'dex': 58, 'hp': 55, 'atk': 70, 'def': 45, 'spd': 60, 'spc': 50, 'type1': 'Fire', + 'type2': 'Fire', 'catch rate': 190, 'base exp': 91, 'start move 1': 'Bite', 'start move 2': 'Roar', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'\xa0\x03H\xc8\xe3\x08\x02')}, + 'Arcanine': {'id': 20, 'dex': 59, 'hp': 90, 'atk': 110, 'def': 80, 'spd': 95, 'spc': 80, 'type1': 'Fire', + 'type2': 'Fire', 'catch rate': 75, 'base exp': 213, 'start move 1': 'Roar', 'start move 2': 'Ember', + 'start move 3': 'Leer', 'start move 4': 'Take Down', 'growth rate': 5, + 'tms': bytearray(b'\xa0CH\xe8\xe3\x08\x02')}, + 'Poliwag': {'id': 71, 'dex': 60, 'hp': 40, 'atk': 50, 'def': 40, 'spd': 90, 'spc': 40, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 255, 'base exp': 77, 'start move 1': 'Bubble', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xa0?\x08\xd0\x82(\x12')}, + 'Poliwhirl': {'id': 110, 'dex': 61, 'hp': 65, 'atk': 65, 'def': 65, 'spd': 90, 'spc': 50, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 120, 'base exp': 131, 'start move 1': 'Bubble', + 'start move 2': 'Hypnosis', 'start move 3': 'Water Gun', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xb1?\x0f\xd6\x86(2')}, + 'Poliwrath': {'id': 111, 'dex': 62, 'hp': 90, 'atk': 85, 'def': 95, 'spd': 70, 'spc': 70, 'type1': 'Water', + 'type2': 'Fighting', 'catch rate': 45, 'base exp': 185, 'start move 1': 'Hypnosis', + 'start move 2': 'Water Gun', 'start move 3': 'Doubleslap', 'start move 4': 'Body Slam', + 'growth rate': 3, 'tms': bytearray(b'\xb1\x7f\x0f\xd6\x86(2')}, + 'Abra': {'id': 148, 'dex': 63, 'hp': 25, 'atk': 20, 'def': 15, 'spd': 90, 'spc': 105, 'type1': 'Psychic', + 'type2': 'Psychic', 'catch rate': 200, 'base exp': 73, 'start move 1': 'Teleport', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xb1\x03\x0f\xf0\x878C')}, + 'Kadabra': {'id': 38, 'dex': 64, 'hp': 40, 'atk': 35, 'def': 30, 'spd': 105, 'spc': 120, 'type1': 'Psychic', + 'type2': 'Psychic', 'catch rate': 100, 'base exp': 145, 'start move 1': 'Teleport', + 'start move 2': 'Confusion', 'start move 3': 'Disable', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xb1\x03\x0f\xf8\x878C')}, + 'Alakazam': {'id': 149, 'dex': 65, 'hp': 55, 'atk': 50, 'def': 45, 'spd': 120, 'spc': 135, 'type1': 'Psychic', + 'type2': 'Psychic', 'catch rate': 50, 'base exp': 186, 'start move 1': 'Teleport', + 'start move 2': 'Confusion', 'start move 3': 'Disable', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xb1C\x0f\xf8\x878C')}, + 'Machop': {'id': 106, 'dex': 66, 'hp': 70, 'atk': 80, 'def': 50, 'spd': 35, 'spc': 35, 'type1': 'Fighting', + 'type2': 'Fighting', 'catch rate': 180, 'base exp': 88, 'start move 1': 'Karate Chop', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xb1\x03\x0f\xce\xa6\x88"')}, + 'Machoke': {'id': 41, 'dex': 67, 'hp': 80, 'atk': 100, 'def': 70, 'spd': 45, 'spc': 50, 'type1': 'Fighting', + 'type2': 'Fighting', 'catch rate': 90, 'base exp': 146, 'start move 1': 'Karate Chop', + 'start move 2': 'Low Kick', 'start move 3': 'Leer', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xb1\x03\x0f\xce\xa6\x88"')}, + 'Machamp': {'id': 126, 'dex': 68, 'hp': 90, 'atk': 130, 'def': 80, 'spd': 55, 'spc': 65, 'type1': 'Fighting', + 'type2': 'Fighting', 'catch rate': 45, 'base exp': 193, 'start move 1': 'Karate Chop', + 'start move 2': 'Low Kick', 'start move 3': 'Leer', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xb1C\x0f\xce\xa6\x88"')}, + 'Bellsprout': {'id': 188, 'dex': 69, 'hp': 50, 'atk': 75, 'def': 35, 'spd': 40, 'spc': 70, 'type1': 'Grass', + 'type2': 'Poison', 'catch rate': 255, 'base exp': 84, 'start move 1': 'Vine Whip', + 'start move 2': 'Growth', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'$\x038\xc0\x03\x08\x06')}, + 'Weepinbell': {'id': 189, 'dex': 70, 'hp': 65, 'atk': 90, 'def': 50, 'spd': 55, 'spc': 85, 'type1': 'Grass', + 'type2': 'Poison', 'catch rate': 120, 'base exp': 151, 'start move 1': 'Vine Whip', + 'start move 2': 'Growth', 'start move 3': 'Wrap', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'$\x038\xc0\x03\x08\x06')}, + 'Victreebel': {'id': 190, 'dex': 71, 'hp': 80, 'atk': 105, 'def': 65, 'spd': 70, 'spc': 100, 'type1': 'Grass', + 'type2': 'Poison', 'catch rate': 45, 'base exp': 191, 'start move 1': 'Sleep Powder', + 'start move 2': 'Stun Spore', 'start move 3': 'Acid', 'start move 4': 'Razor Leaf', 'growth rate': 3, + 'tms': bytearray(b'\xa4C8\xc0\x03\x08\x06')}, + 'Tentacool': {'id': 24, 'dex': 72, 'hp': 40, 'atk': 40, 'def': 35, 'spd': 70, 'spc': 100, 'type1': 'Water', + 'type2': 'Poison', 'catch rate': 190, 'base exp': 105, 'start move 1': 'Acid', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'$?\x18\xc0\x83\x08\x16')}, + 'Tentacruel': {'id': 155, 'dex': 73, 'hp': 80, 'atk': 70, 'def': 65, 'spd': 100, 'spc': 120, 'type1': 'Water', + 'type2': 'Poison', 'catch rate': 60, 'base exp': 205, 'start move 1': 'Acid', + 'start move 2': 'Supersonic', 'start move 3': 'Wrap', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'$\x7f\x18\xc0\x83\x08\x16')}, + 'Geodude': {'id': 169, 'dex': 74, 'hp': 40, 'atk': 80, 'def': 100, 'spd': 20, 'spc': 30, 'type1': 'Rock', + 'type2': 'Ground', 'catch rate': 255, 'base exp': 86, 'start move 1': 'Tackle', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xa1\x03\x0f\xce.\xc8"')}, + 'Graveler': {'id': 39, 'dex': 75, 'hp': 55, 'atk': 95, 'def': 115, 'spd': 35, 'spc': 45, 'type1': 'Rock', + 'type2': 'Ground', 'catch rate': 120, 'base exp': 134, 'start move 1': 'Tackle', + 'start move 2': 'Defense Curl', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xa1\x03\x0f\xce.\xc8"')}, + 'Golem': {'id': 49, 'dex': 76, 'hp': 80, 'atk': 110, 'def': 130, 'spd': 45, 'spc': 55, 'type1': 'Rock', + 'type2': 'Ground', 'catch rate': 45, 'base exp': 177, 'start move 1': 'Tackle', + 'start move 2': 'Defense Curl', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xb1C\x0f\xce.\xc8"')}, + 'Ponyta': {'id': 163, 'dex': 77, 'hp': 50, 'atk': 85, 'def': 55, 'spd': 90, 'spc': 65, 'type1': 'Fire', + 'type2': 'Fire', 'catch rate': 190, 'base exp': 152, 'start move 1': 'Ember', 'start move 2': 'No Move', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xe0\x03\x08\xc0\xe3\x08\x02')}, + 'Rapidash': {'id': 164, 'dex': 78, 'hp': 65, 'atk': 100, 'def': 70, 'spd': 105, 'spc': 80, 'type1': 'Fire', + 'type2': 'Fire', 'catch rate': 60, 'base exp': 192, 'start move 1': 'Ember', + 'start move 2': 'Tail Whip', 'start move 3': 'Stomp', 'start move 4': 'Growl', 'growth rate': 0, + 'tms': bytearray(b'\xe0C\x08\xc0\xe3\x08\x02')}, + 'Slowpoke': {'id': 37, 'dex': 79, 'hp': 90, 'atk': 65, 'def': 65, 'spd': 15, 'spc': 40, 'type1': 'Water', + 'type2': 'Psychic', 'catch rate': 190, 'base exp': 99, 'start move 1': 'Confusion', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0\xbf\x08\xfe\xe38s')}, + 'Slowbro': {'id': 8, 'dex': 80, 'hp': 95, 'atk': 75, 'def': 110, 'spd': 30, 'spc': 80, 'type1': 'Water', + 'type2': 'Psychic', 'catch rate': 75, 'base exp': 164, 'start move 1': 'Confusion', + 'start move 2': 'Disable', 'start move 3': 'Headbutt', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1\xff\x0f\xfe\xe38s')}, + 'Magnemite': {'id': 173, 'dex': 81, 'hp': 25, 'atk': 35, 'def': 70, 'spd': 45, 'spc': 95, 'type1': 'Electric', + 'type2': 'Electric', 'catch rate': 190, 'base exp': 89, 'start move 1': 'Tackle', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b' \x03\x88\xe1C\x18B')}, + 'Magneton': {'id': 54, 'dex': 82, 'hp': 50, 'atk': 60, 'def': 95, 'spd': 70, 'spc': 120, 'type1': 'Electric', + 'type2': 'Electric', 'catch rate': 60, 'base exp': 161, 'start move 1': 'Tackle', + 'start move 2': 'Sonicboom', 'start move 3': 'Thundershock', 'start move 4': 'No Move', + 'growth rate': 0, 'tms': bytearray(b' C\x88\xe1C\x18B')}, + 'Farfetchd': {'id': 64, 'dex': 83, 'hp': 52, 'atk': 65, 'def': 55, 'spd': 60, 'spc': 58, 'type1': 'Normal', + 'type2': 'Flying', 'catch rate': 45, 'base exp': 94, 'start move 1': 'Peck', + 'start move 2': 'Sand Attack', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xae\x03\x08\xc0\xc3\x08\x0e')}, + 'Doduo': {'id': 70, 'dex': 84, 'hp': 35, 'atk': 85, 'def': 45, 'spd': 75, 'spc': 35, 'type1': 'Normal', + 'type2': 'Flying', 'catch rate': 190, 'base exp': 96, 'start move 1': 'Peck', 'start move 2': 'No Move', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa8\x03\x08\xc0\x83\x0c\x0b')}, + 'Dodrio': {'id': 116, 'dex': 85, 'hp': 60, 'atk': 110, 'def': 70, 'spd': 100, 'spc': 60, 'type1': 'Normal', + 'type2': 'Flying', 'catch rate': 45, 'base exp': 158, 'start move 1': 'Peck', 'start move 2': 'Growl', + 'start move 3': 'Fury Attack', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa8C\x08\xc0\x83\x0c\x0b')}, + 'Seel': {'id': 58, 'dex': 86, 'hp': 65, 'atk': 45, 'def': 55, 'spd': 45, 'spc': 70, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 190, 'base exp': 100, 'start move 1': 'Headbutt', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xe0\xbf\x08\xc0\x82\x082')}, + 'Dewgong': {'id': 120, 'dex': 87, 'hp': 90, 'atk': 70, 'def': 80, 'spd': 70, 'spc': 95, 'type1': 'Water', + 'type2': 'Ice', 'catch rate': 75, 'base exp': 176, 'start move 1': 'Headbutt', 'start move 2': 'Growl', + 'start move 3': 'Aurora Beam', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xe0\xff\x08\xc0\x82\x082')}, + 'Grimer': {'id': 13, 'dex': 88, 'hp': 80, 'atk': 80, 'def': 50, 'spd': 25, 'spc': 40, 'type1': 'Poison', + 'type2': 'Poison', 'catch rate': 190, 'base exp': 90, 'start move 1': 'Pound', 'start move 2': 'Disable', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0\x00\x98\xc1*H\x02')}, + 'Muk': {'id': 136, 'dex': 89, 'hp': 105, 'atk': 105, 'def': 75, 'spd': 50, 'spc': 65, 'type1': 'Poison', + 'type2': 'Poison', 'catch rate': 75, 'base exp': 157, 'start move 1': 'Pound', 'start move 2': 'Disable', + 'start move 3': 'Poison Gas', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0@\x98\xc1*H\x02')}, + 'Shellder': {'id': 23, 'dex': 90, 'hp': 30, 'atk': 65, 'def': 100, 'spd': 40, 'spc': 45, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 190, 'base exp': 97, 'start move 1': 'Tackle', + 'start move 2': 'Withdraw', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b' ?\x08\xe0KH\x13')}, + 'Cloyster': {'id': 139, 'dex': 91, 'hp': 50, 'atk': 95, 'def': 180, 'spd': 70, 'spc': 85, 'type1': 'Water', + 'type2': 'Ice', 'catch rate': 60, 'base exp': 203, 'start move 1': 'Withdraw', + 'start move 2': 'Supersonic', 'start move 3': 'Clamp', 'start move 4': 'Aurora Beam', 'growth rate': 5, + 'tms': bytearray(b' \x7f\x08\xe0KH\x13')}, + 'Gastly': {'id': 25, 'dex': 92, 'hp': 30, 'atk': 35, 'def': 30, 'spd': 80, 'spc': 100, 'type1': 'Ghost', + 'type2': 'Poison', 'catch rate': 190, 'base exp': 95, 'start move 1': 'Lick', + 'start move 2': 'Confuse Ray', 'start move 3': 'Night Shade', 'start move 4': 'No Move', + 'growth rate': 3, 'tms': bytearray(b' \x00\x98\xd1\nj\x02')}, + 'Haunter': {'id': 147, 'dex': 93, 'hp': 45, 'atk': 50, 'def': 45, 'spd': 95, 'spc': 115, 'type1': 'Ghost', + 'type2': 'Poison', 'catch rate': 90, 'base exp': 126, 'start move 1': 'Lick', + 'start move 2': 'Confuse Ray', 'start move 3': 'Night Shade', 'start move 4': 'No Move', + 'growth rate': 3, 'tms': bytearray(b' \x00\x98\xd1\nj\x02')}, + 'Gengar': {'id': 14, 'dex': 94, 'hp': 60, 'atk': 65, 'def': 60, 'spd': 110, 'spc': 130, 'type1': 'Ghost', + 'type2': 'Poison', 'catch rate': 45, 'base exp': 190, 'start move 1': 'Lick', + 'start move 2': 'Confuse Ray', 'start move 3': 'Night Shade', 'start move 4': 'No Move', + 'growth rate': 3, 'tms': bytearray(b'\xb1C\x9f\xd1\x8ej"')}, + 'Onix': {'id': 34, 'dex': 95, 'hp': 35, 'atk': 45, 'def': 160, 'spd': 70, 'spc': 30, 'type1': 'Rock', + 'type2': 'Ground', 'catch rate': 45, 'base exp': 108, 'start move 1': 'Tackle', 'start move 2': 'Screech', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0\x03\x08\xce\x8a\xc8"')}, + 'Drowzee': {'id': 48, 'dex': 96, 'hp': 60, 'atk': 48, 'def': 45, 'spd': 42, 'spc': 90, 'type1': 'Psychic', + 'type2': 'Psychic', 'catch rate': 190, 'base exp': 102, 'start move 1': 'Pound', + 'start move 2': 'Hypnosis', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1\x03\x0f\xf0\x87:C')}, + 'Hypno': {'id': 129, 'dex': 97, 'hp': 85, 'atk': 73, 'def': 70, 'spd': 67, 'spc': 115, 'type1': 'Psychic', + 'type2': 'Psychic', 'catch rate': 75, 'base exp': 165, 'start move 1': 'Pound', + 'start move 2': 'Hypnosis', 'start move 3': 'Disable', 'start move 4': 'Confusion', 'growth rate': 0, + 'tms': bytearray(b'\xb1C\x0f\xf0\x87:C')}, + 'Krabby': {'id': 78, 'dex': 98, 'hp': 30, 'atk': 105, 'def': 90, 'spd': 50, 'spc': 25, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 225, 'base exp': 115, 'start move 1': 'Bubble', 'start move 2': 'Leer', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa4?\x08\xc0\x02\x086')}, + 'Kingler': {'id': 138, 'dex': 99, 'hp': 55, 'atk': 130, 'def': 115, 'spd': 75, 'spc': 50, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 60, 'base exp': 206, 'start move 1': 'Bubble', 'start move 2': 'Leer', + 'start move 3': 'Vicegrip', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa4\x7f\x08\xc0\x02\x086')}, + 'Voltorb': {'id': 6, 'dex': 100, 'hp': 40, 'atk': 30, 'def': 50, 'spd': 100, 'spc': 55, 'type1': 'Electric', + 'type2': 'Electric', 'catch rate': 190, 'base exp': 103, 'start move 1': 'Tackle', + 'start move 2': 'Screech', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b' \x01\x88\xe1KXB')}, + 'Electrode': {'id': 141, 'dex': 101, 'hp': 60, 'atk': 50, 'def': 70, 'spd': 140, 'spc': 80, 'type1': 'Electric', + 'type2': 'Electric', 'catch rate': 60, 'base exp': 150, 'start move 1': 'Tackle', + 'start move 2': 'Screech', 'start move 3': 'Sonicboom', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b' A\x88\xe1\xcbXB')}, + 'Exeggcute': {'id': 12, 'dex': 102, 'hp': 60, 'atk': 40, 'def': 80, 'spd': 40, 'spc': 60, 'type1': 'Grass', + 'type2': 'Psychic', 'catch rate': 90, 'base exp': 98, 'start move 1': 'Barrage', + 'start move 2': 'Hypnosis', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b' \x03\x08\xf0\x1bh\x02')}, + 'Exeggutor': {'id': 10, 'dex': 103, 'hp': 95, 'atk': 95, 'def': 85, 'spd': 55, 'spc': 125, 'type1': 'Grass', + 'type2': 'Psychic', 'catch rate': 45, 'base exp': 212, 'start move 1': 'Barrage', + 'start move 2': 'Hypnosis', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b' C8\xf0\x1bh"')}, + 'Cubone': {'id': 17, 'dex': 104, 'hp': 50, 'atk': 50, 'def': 95, 'spd': 35, 'spc': 40, 'type1': 'Ground', + 'type2': 'Ground', 'catch rate': 190, 'base exp': 87, 'start move 1': 'Bone Club', + 'start move 2': 'Growl', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1?\x0f\xce\xa2\x08"')}, + 'Marowak': {'id': 145, 'dex': 105, 'hp': 60, 'atk': 80, 'def': 110, 'spd': 45, 'spc': 50, 'type1': 'Ground', + 'type2': 'Ground', 'catch rate': 75, 'base exp': 124, 'start move 1': 'Bone Club', + 'start move 2': 'Growl', 'start move 3': 'Leer', 'start move 4': 'Focus Energy', 'growth rate': 0, + 'tms': bytearray(b'\xb1\x7f\x0f\xce\xa2\x08"')}, + 'Hitmonlee': {'id': 43, 'dex': 106, 'hp': 50, 'atk': 120, 'def': 53, 'spd': 87, 'spc': 35, 'type1': 'Fighting', + 'type2': 'Fighting', 'catch rate': 45, 'base exp': 139, 'start move 1': 'Double Kick', + 'start move 2': 'Meditate', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1\x03\x0f\xc0\xc6\x08"')}, + 'Hitmonchan': {'id': 44, 'dex': 107, 'hp': 50, 'atk': 105, 'def': 79, 'spd': 76, 'spc': 35, 'type1': 'Fighting', + 'type2': 'Fighting', 'catch rate': 45, 'base exp': 140, 'start move 1': 'Comet Punch', + 'start move 2': 'Agility', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1\x03\x0f\xc0\xc6\x08"')}, + 'Lickitung': {'id': 11, 'dex': 108, 'hp': 90, 'atk': 55, 'def': 75, 'spd': 30, 'spc': 60, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 45, 'base exp': 127, 'start move 1': 'Wrap', + 'start move 2': 'Supersonic', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb5\x7f\x8f\xc7\xa2\x086')}, + 'Koffing': {'id': 55, 'dex': 109, 'hp': 40, 'atk': 65, 'def': 95, 'spd': 35, 'spc': 60, 'type1': 'Poison', + 'type2': 'Poison', 'catch rate': 190, 'base exp': 114, 'start move 1': 'Tackle', 'start move 2': 'Smog', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b' \x00\x88\xc1*H\x02')}, + 'Weezing': {'id': 143, 'dex': 110, 'hp': 65, 'atk': 90, 'def': 120, 'spd': 60, 'spc': 85, 'type1': 'Poison', + 'type2': 'Poison', 'catch rate': 60, 'base exp': 173, 'start move 1': 'Tackle', 'start move 2': 'Smog', + 'start move 3': 'Sludge', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b' @\x88\xc1*H\x02')}, + 'Rhyhorn': {'id': 18, 'dex': 111, 'hp': 80, 'atk': 85, 'def': 95, 'spd': 25, 'spc': 30, 'type1': 'Ground', + 'type2': 'Rock', 'catch rate': 120, 'base exp': 135, 'start move 1': 'Horn Attack', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'\xe0\x03\x88\xcf\xa2\x88"')}, + 'Rhydon': {'id': 1, 'dex': 112, 'hp': 105, 'atk': 130, 'def': 120, 'spd': 40, 'spc': 45, 'type1': 'Ground', + 'type2': 'Rock', 'catch rate': 60, 'base exp': 204, 'start move 1': 'Horn Attack', + 'start move 2': 'Stomp', 'start move 3': 'Tail Whip', 'start move 4': 'Fury Attack', 'growth rate': 5, + 'tms': bytearray(b'\xf1\xff\x8f\xcf\xa2\x882')}, + 'Chansey': {'id': 40, 'dex': 113, 'hp': 250, 'atk': 5, 'def': 5, 'spd': 50, 'spc': 105, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 30, 'base exp': 255, 'start move 1': 'Pound', + 'start move 2': 'Doubleslap', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 4, + 'tms': bytearray(b'\xb1\x7f\xaf\xf1\xb79c')}, + 'Tangela': {'id': 30, 'dex': 114, 'hp': 65, 'atk': 55, 'def': 115, 'spd': 60, 'spc': 100, 'type1': 'Grass', + 'type2': 'Grass', 'catch rate': 45, 'base exp': 166, 'start move 1': 'Constrict', + 'start move 2': 'Bind', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa4C8\xc0\x82\x08\x06')}, + 'Kangaskhan': {'id': 2, 'dex': 115, 'hp': 105, 'atk': 95, 'def': 80, 'spd': 90, 'spc': 40, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 45, 'base exp': 175, 'start move 1': 'Comet Punch', + 'start move 2': 'Rage', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1\x7f\x8f\xc7\xa2\x882')}, + 'Horsea': {'id': 92, 'dex': 116, 'hp': 30, 'atk': 40, 'def': 70, 'spd': 60, 'spc': 70, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 225, 'base exp': 83, 'start move 1': 'Bubble', 'start move 2': 'No Move', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b' ?\x08\xc0\xc2\x08\x12')}, + 'Seadra': {'id': 93, 'dex': 117, 'hp': 55, 'atk': 65, 'def': 95, 'spd': 85, 'spc': 95, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 75, 'base exp': 155, 'start move 1': 'Bubble', + 'start move 2': 'Smokescreen', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b' \x7f\x08\xc0\xc2\x08\x12')}, + 'Goldeen': {'id': 157, 'dex': 118, 'hp': 45, 'atk': 67, 'def': 60, 'spd': 63, 'spc': 50, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 225, 'base exp': 111, 'start move 1': 'Peck', + 'start move 2': 'Tail Whip', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'`?\x08\xc0\xc2\x08\x12')}, + 'Seaking': {'id': 158, 'dex': 119, 'hp': 80, 'atk': 92, 'def': 65, 'spd': 68, 'spc': 80, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 60, 'base exp': 170, 'start move 1': 'Peck', + 'start move 2': 'Tail Whip', 'start move 3': 'Supersonic', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'`\x7f\x08\xc0\xc2\x08\x12')}, + 'Staryu': {'id': 27, 'dex': 120, 'hp': 30, 'atk': 45, 'def': 55, 'spd': 85, 'spc': 70, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 225, 'base exp': 106, 'start move 1': 'Tackle', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b' ?\x88\xf1\xc38S')}, + 'Starmie': {'id': 152, 'dex': 121, 'hp': 60, 'atk': 75, 'def': 85, 'spd': 115, 'spc': 100, 'type1': 'Water', + 'type2': 'Psychic', 'catch rate': 60, 'base exp': 207, 'start move 1': 'Tackle', + 'start move 2': 'Water Gun', 'start move 3': 'Harden', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b' \x7f\x88\xf1\xc38S')}, + 'Mr Mime': {'id': 42, 'dex': 122, 'hp': 40, 'atk': 45, 'def': 65, 'spd': 90, 'spc': 100, 'type1': 'Psychic', + 'type2': 'Psychic', 'catch rate': 45, 'base exp': 136, 'start move 1': 'Confusion', + 'start move 2': 'Barrier', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1C\xaf\xf1\x878B')}, + 'Scyther': {'id': 26, 'dex': 123, 'hp': 70, 'atk': 110, 'def': 80, 'spd': 105, 'spc': 55, 'type1': 'Bug', + 'type2': 'Flying', 'catch rate': 45, 'base exp': 187, 'start move 1': 'Quick Attack', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'$C\x08\xc0\xc2\x08\x06')}, + 'Jynx': {'id': 72, 'dex': 124, 'hp': 65, 'atk': 50, 'def': 35, 'spd': 95, 'spc': 95, 'type1': 'Ice', + 'type2': 'Psychic', 'catch rate': 45, 'base exp': 137, 'start move 1': 'Pound', + 'start move 2': 'Lovely Kiss', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1\x7f\x0f\xf0\x87(\x02')}, + 'Electabuzz': {'id': 53, 'dex': 125, 'hp': 65, 'atk': 83, 'def': 57, 'spd': 105, 'spc': 85, 'type1': 'Electric', + 'type2': 'Electric', 'catch rate': 45, 'base exp': 156, 'start move 1': 'Quick Attack', + 'start move 2': 'Leer', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1C\x8f\xf1\xc78b')}, + 'Magmar': {'id': 51, 'dex': 126, 'hp': 65, 'atk': 95, 'def': 57, 'spd': 93, 'spc': 85, 'type1': 'Fire', + 'type2': 'Fire', 'catch rate': 45, 'base exp': 167, 'start move 1': 'Ember', 'start move 2': 'No Move', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1C\x0f\xf0\xa6("')}, + 'Pinsir': {'id': 29, 'dex': 127, 'hp': 65, 'atk': 125, 'def': 100, 'spd': 85, 'spc': 55, 'type1': 'Bug', + 'type2': 'Bug', 'catch rate': 45, 'base exp': 200, 'start move 1': 'Vicegrip', 'start move 2': 'No Move', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'\xa4C\r\xc0\x02\x08&')}, + 'Tauros': {'id': 60, 'dex': 128, 'hp': 75, 'atk': 100, 'def': 95, 'spd': 110, 'spc': 70, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 45, 'base exp': 211, 'start move 1': 'Tackle', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'\xe0s\x88\xc7\xa2\x08"')}, + 'Magikarp': {'id': 133, 'dex': 129, 'hp': 20, 'atk': 10, 'def': 55, 'spd': 80, 'spc': 20, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 255, 'base exp': 20, 'start move 1': 'Splash', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'\x00\x00\x00\x00\x00\x00\x00')}, + 'Gyarados': {'id': 22, 'dex': 130, 'hp': 95, 'atk': 125, 'def': 79, 'spd': 81, 'spc': 100, 'type1': 'Water', + 'type2': 'Flying', 'catch rate': 45, 'base exp': 214, 'start move 1': 'Bite', + 'start move 2': 'Dragon Rage', 'start move 3': 'Leer', 'start move 4': 'Hydro Pump', 'growth rate': 5, + 'tms': bytearray(b'\xa0\x7f\xc8\xc1\xa3\x082')}, + 'Lapras': {'id': 19, 'dex': 131, 'hp': 130, 'atk': 85, 'def': 80, 'spd': 60, 'spc': 95, 'type1': 'Water', + 'type2': 'Ice', 'catch rate': 45, 'base exp': 219, 'start move 1': 'Water Gun', 'start move 2': 'Growl', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'\xe0\x7f\xe8\xd1\x83(2')}, + 'Ditto': {'id': 76, 'dex': 132, 'hp': 48, 'atk': 48, 'def': 48, 'spd': 48, 'spc': 48, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 35, 'base exp': 61, 'start move 1': 'Transform', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\x00\x00\x00\x00\x00\x00\x00')}, + 'Eevee': {'id': 102, 'dex': 133, 'hp': 55, 'atk': 55, 'def': 50, 'spd': 55, 'spc': 65, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 45, 'base exp': 92, 'start move 1': 'Tackle', + 'start move 2': 'Sand Attack', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0\x03\x08\xc0\xc3\x08\x02')}, + 'Vaporeon': {'id': 105, 'dex': 134, 'hp': 130, 'atk': 65, 'def': 60, 'spd': 65, 'spc': 110, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 45, 'base exp': 196, 'start move 1': 'Tackle', + 'start move 2': 'Sand Attack', 'start move 3': 'Quick Attack', 'start move 4': 'Water Gun', + 'growth rate': 0, 'tms': bytearray(b'\xa0\x7f\x08\xc0\xc3\x08\x12')}, + 'Jolteon': {'id': 104, 'dex': 135, 'hp': 65, 'atk': 65, 'def': 60, 'spd': 130, 'spc': 110, 'type1': 'Electric', + 'type2': 'Electric', 'catch rate': 45, 'base exp': 197, 'start move 1': 'Tackle', + 'start move 2': 'Sand Attack', 'start move 3': 'Quick Attack', 'start move 4': 'Thundershock', + 'growth rate': 0, 'tms': bytearray(b'\xa0C\x88\xc1\xc3\x18B')}, + 'Flareon': {'id': 103, 'dex': 136, 'hp': 65, 'atk': 130, 'def': 60, 'spd': 65, 'spc': 110, 'type1': 'Fire', + 'type2': 'Fire', 'catch rate': 45, 'base exp': 198, 'start move 1': 'Tackle', + 'start move 2': 'Sand Attack', 'start move 3': 'Quick Attack', 'start move 4': 'Ember', + 'growth rate': 0, 'tms': bytearray(b'\xa0C\x08\xc0\xe3\x08\x02')}, + 'Porygon': {'id': 170, 'dex': 137, 'hp': 65, 'atk': 60, 'def': 70, 'spd': 40, 'spc': 75, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 45, 'base exp': 130, 'start move 1': 'Tackle', + 'start move 2': 'Sharpen', 'start move 3': 'Conversion', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b' s\x88\xf1\xc38C')}, + 'Omanyte': {'id': 98, 'dex': 138, 'hp': 35, 'atk': 40, 'def': 100, 'spd': 35, 'spc': 90, 'type1': 'Rock', + 'type2': 'Water', 'catch rate': 45, 'base exp': 120, 'start move 1': 'Water Gun', + 'start move 2': 'Withdraw', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0?\x08\xc0\x03\x08\x12')}, + 'Omastar': {'id': 99, 'dex': 139, 'hp': 70, 'atk': 60, 'def': 125, 'spd': 55, 'spc': 115, 'type1': 'Rock', + 'type2': 'Water', 'catch rate': 45, 'base exp': 199, 'start move 1': 'Water Gun', + 'start move 2': 'Withdraw', 'start move 3': 'Horn Attack', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xe0\x7f\r\xc0\x83\x08\x12')}, + 'Kabuto': {'id': 90, 'dex': 140, 'hp': 30, 'atk': 80, 'def': 90, 'spd': 55, 'spc': 45, 'type1': 'Rock', + 'type2': 'Water', 'catch rate': 45, 'base exp': 119, 'start move 1': 'Scratch', 'start move 2': 'Harden', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0?\x08\xc0\x03\x08\x12')}, + 'Kabutops': {'id': 91, 'dex': 141, 'hp': 60, 'atk': 115, 'def': 105, 'spd': 80, 'spc': 70, 'type1': 'Rock', + 'type2': 'Water', 'catch rate': 45, 'base exp': 201, 'start move 1': 'Scratch', + 'start move 2': 'Harden', 'start move 3': 'Absorb', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb6\x7f\r\xc0\x83\x08\x12')}, + 'Aerodactyl': {'id': 171, 'dex': 142, 'hp': 80, 'atk': 105, 'def': 65, 'spd': 130, 'spc': 60, 'type1': 'Rock', + 'type2': 'Flying', 'catch rate': 45, 'base exp': 202, 'start move 1': 'Wing Attack', + 'start move 2': 'Agility', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'*CH\xc0c\x0c\n')}, + 'Snorlax': {'id': 132, 'dex': 143, 'hp': 160, 'atk': 110, 'def': 65, 'spd': 30, 'spc': 65, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 25, 'base exp': 154, 'start move 1': 'Headbutt', + 'start move 2': 'Amnesia', 'start move 3': 'Rest', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'\xb1\xff\xaf\xd7\xaf\xa82')}, + 'Articuno': {'id': 74, 'dex': 144, 'hp': 90, 'atk': 85, 'def': 100, 'spd': 85, 'spc': 125, 'type1': 'Ice', + 'type2': 'Flying', 'catch rate': 3, 'base exp': 215, 'start move 1': 'Peck', + 'start move 2': 'Ice Beam', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'*\x7f\x08\xc0C\x0c\n')}, + 'Zapdos': {'id': 75, 'dex': 145, 'hp': 90, 'atk': 90, 'def': 85, 'spd': 100, 'spc': 125, 'type1': 'Electric', + 'type2': 'Flying', 'catch rate': 3, 'base exp': 216, 'start move 1': 'Thundershock', + 'start move 2': 'Drill Peck', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'*C\x88\xc1C\x1cJ')}, + 'Moltres': {'id': 73, 'dex': 146, 'hp': 90, 'atk': 100, 'def': 90, 'spd': 90, 'spc': 125, 'type1': 'Fire', + 'type2': 'Flying', 'catch rate': 3, 'base exp': 217, 'start move 1': 'Peck', + 'start move 2': 'Fire Spin', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'*C\x08\xc0c\x0c\n')}, + 'Dratini': {'id': 88, 'dex': 147, 'hp': 41, 'atk': 64, 'def': 45, 'spd': 50, 'spc': 50, 'type1': 'Dragon', + 'type2': 'Dragon', 'catch rate': 45, 'base exp': 67, 'start move 1': 'Wrap', 'start move 2': 'Leer', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'\xa0?\xc8\xc1\xe3\x18\x12')}, + 'Dragonair': {'id': 89, 'dex': 148, 'hp': 61, 'atk': 84, 'def': 65, 'spd': 70, 'spc': 70, 'type1': 'Dragon', + 'type2': 'Dragon', 'catch rate': 45, 'base exp': 144, 'start move 1': 'Wrap', 'start move 2': 'Leer', + 'start move 3': 'Thunder Wave', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'\xe0?\xc8\xc1\xe3\x18\x12')}, + 'Dragonite': {'id': 66, 'dex': 149, 'hp': 91, 'atk': 134, 'def': 95, 'spd': 80, 'spc': 100, 'type1': 'Dragon', + 'type2': 'Flying', 'catch rate': 45, 'base exp': 218, 'start move 1': 'Wrap', 'start move 2': 'Leer', + 'start move 3': 'Thunder Wave', 'start move 4': 'Agility', 'growth rate': 5, + 'tms': bytearray(b'\xe2\x7f\xc8\xc1\xe3\x182')}, + 'Mewtwo': {'id': 131, 'dex': 150, 'hp': 106, 'atk': 110, 'def': 90, 'spd': 130, 'spc': 154, 'type1': 'Psychic', + 'type2': 'Psychic', 'catch rate': 3, 'base exp': 220, 'start move 1': 'Confusion', + 'start move 2': 'Disable', 'start move 3': 'Swift', 'start move 4': 'Psychic', 'growth rate': 5, + 'tms': bytearray(b'\xb1\xff\xaf\xf1\xaf8c')}, + 'Mew': {'id': 21, 'dex': 151, 'hp': 100, 'atk': 100, 'def': 100, 'spd': 100, 'spc': 100, 'type1': 'Psychic', + 'type2': 'Psychic', 'catch rate': 45, 'base exp': 64, 'start move 1': 'Pound', 'start move 2': 'No Move', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xff\xff\xff\xff\xff\xff\xff')}} + + + +evolves_from = { + "Ivysaur": "Bulbasaur", + "Venusaur": "Ivysaur", + "Charmeleon": "Charmander", + "Charizard": "Charmeleon", + "Wartortle": "Squirtle", + "Blastoise": "Wartortle", + "Metapod": "Caterpie", + "Butterfree": "Metapod", + "Kakuna": "Weedle", + "Beedrill": "Kakuna", + "Pidgeotto": "Pidgey", + "Pidgeot": "Pidgeotto", + "Raticate": "Rattata", + "Fearow": "Spearow", + "Arbok": "Ekans", + "Raichu": "Pikachu", + "Sandslash": "Sandshrew", + "Nidorina": "Nidoran F", + "Nidoqueen": "Nidorina", + "Nidorino": "Nidoran M", + "Nidoking": "Nidorino", + "Clefable": "Clefairy", + "Ninetales": "Vulpix", + "Wigglytuff": "Jigglypuff", + "Golbat": "Zubat", + "Gloom": "Oddish", + "Vileplume": "Gloom", + "Parasect": "Paras", + "Venomoth": "Venonat", + "Dugtrio": "Diglett", + "Persian": "Meowth", + "Golduck": "Psyduck", + "Primeape": "Mankey", + "Arcanine": "Growlithe", + "Poliwhirl": "Poliwag", + "Poliwrath": "Poliwhirl", + "Kadabra": "Abra", + "Alakazam": "Kadabra", + "Machoke": "Machop", + "Machamp": "Machoke", + "Weepinbell": "Bellsprout", + "Victreebel": "Weepinbell", + "Tentacruel": "Tentacool", + "Graveler": "Geodude", + "Golem": "Graveler", + "Rapidash": "Ponyta", + "Slowbro": "Slowpoke", + "Magneton": "Magnemite", + "Dodrio": "Doduo", + "Dewgong": "Seel", + "Muk": "Grimer", + "Cloyster": "Shellder", + "Haunter": "Gastly", + "Gengar": "Haunter", + "Hypno": "Drowzee", + "Kingler": "Krabby", + "Electrode": "Voltorb", + "Exeggutor": "Exeggcute", + "Marowak": "Cubone", + "Weezing": "Koffing", + "Rhydon": "Rhyhorn", + "Seadra": "Horsea", + "Seaking": "Goldeen", + "Starmie": "Staryu", + "Gyarados": "Magikarp", + "Vaporeon": "Eevee", + "Jolteon": "Eevee", + "Flareon": "Eevee", + "Omastar": "Omanyte", + "Kabutops": "Kabuto", + "Dragonair": "Dratini", + "Dragonite": "Dragonair" +} + +evolves_to = {} +for from_mon, to_mon in zip(evolves_from.values(), evolves_from.keys()): + if from_mon != "Eevee": + evolves_to[from_mon] = to_mon + +# basic_three_stage_pokemon = [] +# for mon in evolves_to.keys(): +# if evolves_to[mon] in evolves_to.keys(): +# basic_three_stage_pokemon.append(mon) +# print(basic_three_stage_pokemon) + +learnsets = { + 'Rhydon': ['Stomp', 'Tail Whip', 'Fury Attack', 'Horn Drill', 'Leer', 'Take Down'], + 'Kangaskhan': ['Bite', 'Tail Whip', 'Mega Punch', 'Leer', 'Dizzy Punch'], + 'Nidoran M': ['Horn Attack', 'Poison Sting', 'Focus Energy', 'Fury Attack', 'Horn Drill', 'Double Kick'], + 'Clefairy': ['Sing', 'Doubleslap', 'Minimize', 'Metronome', 'Defense Curl', 'Light Screen'], + 'Spearow': ['Leer', 'Fury Attack', 'Mirror Move', 'Drill Peck', 'Agility'], + 'Voltorb': ['Sonicboom', 'Selfdestruct', 'Light Screen', 'Swift', 'Explosion'], + 'Nidoking': ['Horn Attack', 'Poison Sting', 'Thrash'], + 'Slowbro': ['Disable', 'Headbutt', 'Growl', 'Water Gun', 'Withdraw', 'Amnesia', 'Psychic'], + 'Ivysaur': ['Leech Seed', 'Vine Whip', 'Poisonpowder', 'Razor Leaf', 'Growth', 'Sleep Powder', 'Solarbeam'], + 'Exeggutor': ['Stomp'], 'Lickitung': ['Stomp', 'Disable', 'Defense Curl', 'Slam', 'Screech'], + 'Exeggcute': ['Reflect', 'Leech Seed', 'Stun Spore', 'Poisonpowder', 'Solarbeam', 'Sleep Powder'], + 'Grimer': ['Poison Gas', 'Minimize', 'Sludge', 'Harden', 'Screech', 'Acid Armor'], + 'Gengar': ['Hypnosis', 'Dream Eater'], + 'Nidoran F': ['Scratch', 'Poison Sting', 'Tail Whip', 'Bite', 'Fury Swipes', 'Double Kick'], + 'Nidoqueen': ['Scratch', 'Poison Sting', 'Body Slam'], + 'Cubone': ['Leer', 'Focus Energy', 'Thrash', 'Bonemerang', 'Rage'], + 'Rhyhorn': ['Stomp', 'Tail Whip', 'Fury Attack', 'Horn Drill', 'Leer', 'Take Down'], + 'Lapras': ['Sing', 'Mist', 'Body Slam', 'Confuse Ray', 'Ice Beam', 'Hydro Pump'], + 'Mew': ['Transform', 'Mega Punch', 'Metronome', 'Psychic'], + 'Gyarados': ['Bite', 'Dragon Rage', 'Leer', 'Hydro Pump', 'Hyper Beam'], + 'Shellder': ['Supersonic', 'Clamp', 'Aurora Beam', 'Leer', 'Ice Beam'], + 'Tentacool': ['Supersonic', 'Wrap', 'Poison Sting', 'Water Gun', 'Constrict', 'Barrier', 'Screech', 'Hydro Pump'], + 'Gastly': ['Hypnosis', 'Dream Eater'], + 'Scyther': ['Leer', 'Focus Energy', 'Double Team', 'Slash', 'Swords Dance', 'Agility'], + 'Staryu': ['Water Gun', 'Harden', 'Recover', 'Swift', 'Minimize', 'Light Screen', 'Hydro Pump'], + 'Blastoise': ['Bubble', 'Water Gun', 'Bite', 'Withdraw', 'Skull Bash', 'Hydro Pump'], + 'Pinsir': ['Seismic Toss', 'Guillotine', 'Focus Energy', 'Harden', 'Slash', 'Swords Dance'], + 'Tangela': ['Absorb', 'Poisonpowder', 'Stun Spore', 'Sleep Powder', 'Slam', 'Growth'], + 'Growlithe': ['Ember', 'Leer', 'Take Down', 'Agility', 'Flamethrower'], + 'Onix': ['Bind', 'Rock Throw', 'Rage', 'Slam', 'Harden'], + 'Fearow': ['Leer', 'Fury Attack', 'Mirror Move', 'Drill Peck', 'Agility'], + 'Pidgey': ['Sand Attack', 'Quick Attack', 'Whirlwind', 'Wing Attack', 'Agility', 'Mirror Move'], + 'Slowpoke': ['Disable', 'Headbutt', 'Growl', 'Water Gun', 'Amnesia', 'Psychic'], + 'Kadabra': ['Confusion', 'Disable', 'Psybeam', 'Recover', 'Psychic', 'Reflect'], + 'Graveler': ['Defense Curl', 'Rock Throw', 'Selfdestruct', 'Harden', 'Earthquake', 'Explosion'], + 'Chansey': ['Sing', 'Growl', 'Minimize', 'Defense Curl', 'Light Screen', 'Double Edge'], + 'Machoke': ['Low Kick', 'Leer', 'Focus Energy', 'Seismic Toss', 'Submission'], + 'Mr Mime': ['Confusion', 'Light Screen', 'Doubleslap', 'Meditate', 'Substitute'], + 'Hitmonlee': ['Rolling Kick', 'Jump Kick', 'Focus Energy', 'Hi Jump Kick', 'Mega Kick'], + 'Hitmonchan': ['Fire Punch', 'Ice Punch', 'Thunderpunch', 'Mega Punch', 'Counter'], + 'Arbok': ['Poison Sting', 'Bite', 'Glare', 'Screech', 'Acid'], + 'Parasect': ['Stun Spore', 'Leech Life', 'Spore', 'Slash', 'Growth'], + 'Psyduck': ['Tail Whip', 'Disable', 'Confusion', 'Fury Swipes', 'Hydro Pump'], + 'Drowzee': ['Disable', 'Confusion', 'Headbutt', 'Poison Gas', 'Psychic', 'Meditate'], + 'Golem': ['Defense Curl', 'Rock Throw', 'Selfdestruct', 'Harden', 'Earthquake', 'Explosion'], + 'Magmar': ['Leer', 'Confuse Ray', 'Fire Punch', 'Smokescreen', 'Smog', 'Flamethrower'], + 'Electabuzz': ['Thundershock', 'Screech', 'Thunderpunch', 'Light Screen', 'Thunder'], + 'Magneton': ['Sonicboom', 'Thundershock', 'Supersonic', 'Thunder Wave', 'Swift', 'Screech'], + 'Koffing': ['Sludge', 'Smokescreen', 'Selfdestruct', 'Haze', 'Explosion'], + 'Mankey': ['Karate Chop', 'Fury Swipes', 'Focus Energy', 'Seismic Toss', 'Thrash'], + 'Seel': ['Growl', 'Aurora Beam', 'Rest', 'Take Down', 'Ice Beam'], + 'Diglett': ['Growl', 'Dig', 'Sand Attack', 'Slash', 'Earthquake'], + 'Tauros': ['Stomp', 'Tail Whip', 'Leer', 'Rage', 'Take Down'], + 'Farfetchd': ['Leer', 'Fury Attack', 'Swords Dance', 'Agility', 'Slash'], + 'Venonat': ['Poisonpowder', 'Leech Life', 'Stun Spore', 'Psybeam', 'Sleep Powder', 'Psychic'], + 'Dragonite': ['Thunder Wave', 'Agility', 'Slam', 'Dragon Rage', 'Hyper Beam'], + 'Doduo': ['Growl', 'Fury Attack', 'Drill Peck', 'Rage', 'Tri Attack', 'Agility'], + 'Poliwag': ['Hypnosis', 'Water Gun', 'Doubleslap', 'Body Slam', 'Amnesia', 'Hydro Pump'], + 'Jynx': ['Lick', 'Doubleslap', 'Ice Punch', 'Body Slam', 'Thrash', 'Blizzard'], + 'Moltres': ['Leer', 'Agility', 'Sky Attack'], + 'Articuno': ['Blizzard', 'Agility', 'Mist'], + 'Zapdos': ['Thunder', 'Agility', 'Light Screen'], + 'Meowth': ['Bite', 'Pay Day', 'Screech', 'Fury Swipes', 'Slash'], + 'Krabby': ['Vicegrip', 'Guillotine', 'Stomp', 'Crabhammer', 'Harden'], + 'Vulpix': ['Quick Attack', 'Roar', 'Confuse Ray', 'Flamethrower', 'Fire Spin'], + 'Pikachu': ['Thunder Wave', 'Quick Attack', 'Swift', 'Agility', 'Thunder'], + 'Dratini': ['Thunder Wave', 'Agility', 'Slam', 'Dragon Rage', 'Hyper Beam'], + 'Dragonair': ['Thunder Wave', 'Agility', 'Slam', 'Dragon Rage', 'Hyper Beam'], + 'Kabuto': ['Absorb', 'Slash', 'Leer', 'Hydro Pump'], + 'Kabutops': ['Absorb', 'Slash', 'Leer', 'Hydro Pump'], + 'Horsea': ['Smokescreen', 'Leer', 'Water Gun', 'Agility', 'Hydro Pump'], + 'Seadra': ['Smokescreen', 'Leer', 'Water Gun', 'Agility', 'Hydro Pump'], + 'Sandshrew': ['Sand Attack', 'Slash', 'Poison Sting', 'Swift', 'Fury Swipes'], + 'Sandslash': ['Sand Attack', 'Slash', 'Poison Sting', 'Swift', 'Fury Swipes'], + 'Omanyte': ['Horn Attack', 'Leer', 'Spike Cannon', 'Hydro Pump'], + 'Omastar': ['Horn Attack', 'Leer', 'Spike Cannon', 'Hydro Pump'], + 'Jigglypuff': ['Pound', 'Disable', 'Defense Curl', 'Doubleslap', 'Rest', 'Body Slam', 'Double Edge'], + 'Eevee': ['Quick Attack', 'Tail Whip', 'Bite', 'Take Down'], + 'Flareon': ['Quick Attack', 'Ember', 'Tail Whip', 'Bite', 'Leer', 'Fire Spin', 'Rage', 'Flamethrower'], + 'Jolteon': ['Quick Attack', 'Thundershock', 'Tail Whip', 'Thunder Wave', 'Double Kick', 'Agility', 'Pin Missile', 'Thunder'], + 'Vaporeon': ['Quick Attack', 'Water Gun', 'Tail Whip', 'Bite', 'Acid Armor', 'Haze', 'Mist', 'Hydro Pump'], + 'Machop': ['Low Kick', 'Leer', 'Focus Energy', 'Seismic Toss', 'Submission'], + 'Zubat': ['Supersonic', 'Bite', 'Confuse Ray', 'Wing Attack', 'Haze'], + 'Ekans': ['Poison Sting', 'Bite', 'Glare', 'Screech', 'Acid'], + 'Paras': ['Stun Spore', 'Leech Life', 'Spore', 'Slash', 'Growth'], + 'Poliwhirl': ['Hypnosis', 'Water Gun', 'Doubleslap', 'Body Slam', 'Amnesia', 'Hydro Pump'], + 'Poliwrath': ['Hypnosis', 'Water Gun'], + 'Beedrill': ['Fury Attack', 'Focus Energy', 'Twineedle', 'Rage', 'Pin Missile', 'Agility'], + 'Dodrio': ['Growl', 'Fury Attack', 'Drill Peck', 'Rage', 'Tri Attack', 'Agility'], + 'Primeape': ['Karate Chop', 'Fury Swipes', 'Focus Energy', 'Seismic Toss', 'Thrash'], + 'Dugtrio': ['Growl', 'Dig', 'Sand Attack', 'Slash', 'Earthquake'], + 'Venomoth': ['Poisonpowder', 'Leech Life', 'Stun Spore', 'Psybeam', 'Sleep Powder', 'Psychic'], + 'Dewgong': ['Growl', 'Aurora Beam', 'Rest', 'Take Down', 'Ice Beam'], + 'Butterfree': ['Confusion', 'Poisonpowder', 'Stun Spore', 'Sleep Powder', 'Supersonic', 'Whirlwind', 'Psybeam'], + 'Machamp': ['Low Kick', 'Leer', 'Focus Energy', 'Seismic Toss', 'Submission'], + 'Golduck': ['Tail Whip', 'Disable', 'Confusion', 'Fury Swipes', 'Hydro Pump'], + 'Hypno': ['Disable', 'Confusion', 'Headbutt', 'Poison Gas', 'Psychic', 'Meditate'], + 'Golbat': ['Supersonic', 'Bite', 'Confuse Ray', 'Wing Attack', 'Haze'], + 'Mewtwo': ['Barrier', 'Psychic', 'Recover', 'Mist', 'Amnesia'], + 'Snorlax': ['Body Slam', 'Harden', 'Double Edge', 'Hyper Beam'], + 'Magikarp': ['Tackle'], + 'Muk': ['Poison Gas', 'Minimize', 'Sludge', 'Harden', 'Screech', 'Acid Armor'], + 'Kingler': ['Vicegrip', 'Guillotine', 'Stomp', 'Crabhammer', 'Harden'], + 'Cloyster': ['Spike Cannon'], + 'Electrode': ['Sonicboom', 'Selfdestruct', 'Light Screen', 'Swift', 'Explosion'], + 'Weezing': ['Sludge', 'Smokescreen', 'Selfdestruct', 'Haze', 'Explosion'], + 'Persian': ['Bite', 'Pay Day', 'Screech', 'Fury Swipes', 'Slash'], + 'Marowak': ['Leer', 'Focus Energy', 'Thrash', 'Bonemerang', 'Rage'], + 'Haunter': ['Hypnosis', 'Dream Eater'], + 'Alakazam': ['Confusion', 'Disable', 'Psybeam', 'Recover', 'Psychic', 'Reflect'], + 'Pidgeotto': ['Sand Attack', 'Quick Attack', 'Whirlwind', 'Wing Attack', 'Agility', 'Mirror Move'], + 'Pidgeot': ['Sand Attack', 'Quick Attack', 'Whirlwind', 'Wing Attack', 'Agility', 'Mirror Move'], + 'Bulbasaur': ['Leech Seed', 'Vine Whip', 'Poisonpowder', 'Razor Leaf', 'Growth', 'Sleep Powder', 'Solarbeam'], + 'Venusaur': ['Leech Seed', 'Vine Whip', 'Poisonpowder', 'Razor Leaf', 'Growth', 'Sleep Powder', 'Solarbeam'], + 'Tentacruel': ['Supersonic', 'Wrap', 'Poison Sting', 'Water Gun', 'Constrict', 'Barrier', 'Screech', 'Hydro Pump'], + 'Goldeen': ['Supersonic', 'Horn Attack', 'Fury Attack', 'Waterfall', 'Horn Drill', 'Agility'], + 'Seaking': ['Supersonic', 'Horn Attack', 'Fury Attack', 'Waterfall', 'Horn Drill', 'Agility'], + 'Ponyta': ['Tail Whip', 'Stomp', 'Growl', 'Fire Spin', 'Take Down', 'Agility'], + 'Rapidash': ['Tail Whip', 'Stomp', 'Growl', 'Fire Spin', 'Take Down', 'Agility'], + 'Rattata': ['Quick Attack', 'Hyper Fang', 'Focus Energy', 'Super Fang'], + 'Raticate': ['Quick Attack', 'Hyper Fang', 'Focus Energy', 'Super Fang'], + 'Nidorino': ['Horn Attack', 'Poison Sting', 'Focus Energy', 'Fury Attack', 'Horn Drill', 'Double Kick'], + 'Nidorina': ['Scratch', 'Poison Sting', 'Tail Whip', 'Bite', 'Fury Swipes', 'Double Kick'], + 'Geodude': ['Defense Curl', 'Rock Throw', 'Selfdestruct', 'Harden', 'Earthquake', 'Explosion'], + 'Porygon': ['Psybeam', 'Recover', 'Agility', 'Tri Attack'], + 'Aerodactyl': ['Supersonic', 'Bite', 'Take Down', 'Hyper Beam'], + 'Magnemite': ['Sonicboom', 'Thundershock', 'Supersonic', 'Thunder Wave', 'Swift', 'Screech'], + 'Charmander': ['Ember', 'Leer', 'Rage', 'Slash', 'Flamethrower', 'Fire Spin'], + 'Squirtle': ['Bubble', 'Water Gun', 'Bite', 'Withdraw', 'Skull Bash', 'Hydro Pump'], + 'Charmeleon': ['Ember', 'Leer', 'Rage', 'Slash', 'Flamethrower', 'Fire Spin'], + 'Wartortle': ['Bubble', 'Water Gun', 'Bite', 'Withdraw', 'Skull Bash', 'Hydro Pump'], + 'Charizard': ['Ember', 'Leer', 'Rage', 'Slash', 'Flamethrower', 'Fire Spin'], + 'Oddish': ['Poisonpowder', 'Stun Spore', 'Sleep Powder', 'Acid', 'Petal Dance', 'Solarbeam'], + 'Gloom': ['Poisonpowder', 'Stun Spore', 'Sleep Powder', 'Acid', 'Petal Dance', 'Solarbeam'], + 'Vileplume': ['Poisonpowder', 'Stun Spore', 'Sleep Powder'], + 'Bellsprout': ['Wrap', 'Poisonpowder', 'Sleep Powder', 'Stun Spore', 'Acid', 'Razor Leaf', 'Slam'], + 'Weepinbell': ['Wrap', 'Poisonpowder', 'Sleep Powder', 'Stun Spore', 'Acid', 'Razor Leaf', 'Slam'], + 'Victreebel': ['Wrap', 'Poisonpowder', 'Sleep Powder'] +} + +moves = { + 'No Move': {'id': 0, 'power': 0, 'type': 'Typeless', 'accuracy': 0, 'pp': 0}, + 'Pound': {'id': 1, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 35}, + 'Karate Chop': {'id': 2, 'power': 50, 'type': 'Normal', 'accuracy': 100, 'pp': 25}, + 'Doubleslap': {'id': 3, 'power': 15, 'type': 'Normal', 'accuracy': 85, 'pp': 10}, + 'Comet Punch': {'id': 4, 'power': 18, 'type': 'Normal', 'accuracy': 85, 'pp': 15}, + 'Mega Punch': {'id': 5, 'power': 80, 'type': 'Normal', 'accuracy': 85, 'pp': 20}, + 'Pay Day': {'id': 6, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, + 'Fire Punch': {'id': 7, 'power': 75, 'type': 'Fire', 'accuracy': 100, 'pp': 15}, + 'Ice Punch': {'id': 8, 'power': 75, 'type': 'Ice', 'accuracy': 100, 'pp': 15}, + 'Thunderpunch': {'id': 9, 'power': 75, 'type': 'Electric', 'accuracy': 100, 'pp': 15}, + 'Scratch': {'id': 10, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 35}, + 'Vicegrip': {'id': 11, 'power': 55, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, + 'Guillotine': {'id': 12, 'power': 1, 'type': 'Normal', 'accuracy': 30, 'pp': 5}, + 'Razor Wind': {'id': 13, 'power': 80, 'type': 'Normal', 'accuracy': 75, 'pp': 10}, + 'Swords Dance': {'id': 14, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, + 'Cut': {'id': 15, 'power': 50, 'type': 'Normal', 'accuracy': 95, 'pp': 30}, + 'Gust': {'id': 16, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 35}, + 'Wing Attack': {'id': 17, 'power': 35, 'type': 'Flying', 'accuracy': 100, 'pp': 35}, + 'Whirlwind': {'id': 18, 'power': 0, 'type': 'Normal', 'accuracy': 85, 'pp': 20}, + 'Fly': {'id': 19, 'power': 70, 'type': 'Flying', 'accuracy': 95, 'pp': 15}, + 'Bind': {'id': 20, 'power': 15, 'type': 'Normal', 'accuracy': 75, 'pp': 20}, + 'Slam': {'id': 21, 'power': 80, 'type': 'Normal', 'accuracy': 75, 'pp': 20}, + 'Vine Whip': {'id': 22, 'power': 35, 'type': 'Grass', 'accuracy': 100, 'pp': 10}, + 'Stomp': {'id': 23, 'power': 65, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, + 'Double Kick': {'id': 24, 'power': 30, 'type': 'Fighting', 'accuracy': 100, 'pp': 30}, + 'Mega Kick': {'id': 25, 'power': 120, 'type': 'Normal', 'accuracy': 75, 'pp': 5}, + 'Jump Kick': {'id': 26, 'power': 70, 'type': 'Fighting', 'accuracy': 95, 'pp': 25}, + 'Rolling Kick': {'id': 27, 'power': 60, 'type': 'Fighting', 'accuracy': 85, 'pp': 15}, + 'Sand Attack': {'id': 28, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, + 'Headbutt': {'id': 29, 'power': 70, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, + 'Horn Attack': {'id': 30, 'power': 65, 'type': 'Normal', 'accuracy': 100, 'pp': 25}, + 'Fury Attack': {'id': 31, 'power': 15, 'type': 'Normal', 'accuracy': 85, 'pp': 20}, + 'Horn Drill': {'id': 32, 'power': 1, 'type': 'Normal', 'accuracy': 30, 'pp': 5}, + 'Tackle': {'id': 33, 'power': 35, 'type': 'Normal', 'accuracy': 95, 'pp': 35}, + 'Body Slam': {'id': 34, 'power': 85, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, + 'Wrap': {'id': 35, 'power': 15, 'type': 'Normal', 'accuracy': 85, 'pp': 20}, + 'Take Down': {'id': 36, 'power': 90, 'type': 'Normal', 'accuracy': 85, 'pp': 20}, + 'Thrash': {'id': 37, 'power': 90, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, + 'Double Edge': {'id': 38, 'power': 100, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, + 'Tail Whip': {'id': 39, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, + 'Poison Sting': {'id': 40, 'power': 15, 'type': 'Poison', 'accuracy': 100, 'pp': 35}, + 'Twineedle': {'id': 41, 'power': 25, 'type': 'Bug', 'accuracy': 100, 'pp': 20}, + 'Pin Missile': {'id': 42, 'power': 14, 'type': 'Bug', 'accuracy': 85, 'pp': 20}, + 'Leer': {'id': 43, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, + 'Bite': {'id': 44, 'power': 60, 'type': 'Normal', 'accuracy': 100, 'pp': 25}, + 'Growl': {'id': 45, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 40}, + 'Roar': {'id': 46, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, + 'Sing': {'id': 47, 'power': 0, 'type': 'Normal', 'accuracy': 55, 'pp': 15}, + 'Supersonic': {'id': 48, 'power': 0, 'type': 'Normal', 'accuracy': 55, 'pp': 20}, + 'Sonicboom': {'id': 49, 'power': 1, 'type': 'Normal', 'accuracy': 90, 'pp': 20}, + 'Disable': {'id': 50, 'power': 0, 'type': 'Normal', 'accuracy': 55, 'pp': 20}, + 'Acid': {'id': 51, 'power': 40, 'type': 'Poison', 'accuracy': 100, 'pp': 30}, + 'Ember': {'id': 52, 'power': 40, 'type': 'Fire', 'accuracy': 100, 'pp': 25}, + 'Flamethrower': {'id': 53, 'power': 95, 'type': 'Fire', 'accuracy': 100, 'pp': 15}, + 'Mist': {'id': 54, 'power': 0, 'type': 'Ice', 'accuracy': 100, 'pp': 30}, + 'Water Gun': {'id': 55, 'power': 40, 'type': 'Water', 'accuracy': 100, 'pp': 25}, + 'Hydro Pump': {'id': 56, 'power': 120, 'type': 'Water', 'accuracy': 80, 'pp': 5}, + 'Surf': {'id': 57, 'power': 95, 'type': 'Water', 'accuracy': 100, 'pp': 15}, + 'Ice Beam': {'id': 58, 'power': 95, 'type': 'Ice', 'accuracy': 100, 'pp': 10}, + 'Blizzard': {'id': 59, 'power': 120, 'type': 'Ice', 'accuracy': 90, 'pp': 5}, + 'Psybeam': {'id': 60, 'power': 65, 'type': 'Psychic', 'accuracy': 100, 'pp': 20}, + 'Bubblebeam': {'id': 61, 'power': 65, 'type': 'Water', 'accuracy': 100, 'pp': 20}, + 'Aurora Beam': {'id': 62, 'power': 65, 'type': 'Ice', 'accuracy': 100, 'pp': 20}, + 'Hyper Beam': {'id': 63, 'power': 150, 'type': 'Normal', 'accuracy': 90, 'pp': 5}, + 'Peck': {'id': 64, 'power': 35, 'type': 'Flying', 'accuracy': 100, 'pp': 35}, + 'Drill Peck': {'id': 65, 'power': 80, 'type': 'Flying', 'accuracy': 100, 'pp': 20}, + 'Submission': {'id': 66, 'power': 80, 'type': 'Fighting', 'accuracy': 80, 'pp': 25}, + 'Low Kick': {'id': 67, 'power': 50, 'type': 'Fighting', 'accuracy': 90, 'pp': 20}, + 'Counter': {'id': 68, 'power': 1, 'type': 'Fighting', 'accuracy': 100, 'pp': 20}, + 'Seismic Toss': {'id': 69, 'power': 1, 'type': 'Fighting', 'accuracy': 100, 'pp': 20}, + 'Strength': {'id': 70, 'power': 80, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, + 'Absorb': {'id': 71, 'power': 20, 'type': 'Grass', 'accuracy': 100, 'pp': 20}, + 'Mega Drain': {'id': 72, 'power': 40, 'type': 'Grass', 'accuracy': 100, 'pp': 10}, + 'Leech Seed': {'id': 73, 'power': 0, 'type': 'Grass', 'accuracy': 90, 'pp': 10}, + 'Growth': {'id': 74, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 40}, + 'Razor Leaf': {'id': 75, 'power': 55, 'type': 'Grass', 'accuracy': 95, 'pp': 25}, + 'Solarbeam': {'id': 76, 'power': 120, 'type': 'Grass', 'accuracy': 100, 'pp': 10}, + 'Poisonpowder': {'id': 77, 'power': 0, 'type': 'Poison', 'accuracy': 75, 'pp': 35}, + 'Stun Spore': {'id': 78, 'power': 0, 'type': 'Grass', 'accuracy': 75, 'pp': 30}, + 'Sleep Powder': {'id': 79, 'power': 0, 'type': 'Grass', 'accuracy': 75, 'pp': 15}, + 'Petal Dance': {'id': 80, 'power': 70, 'type': 'Grass', 'accuracy': 100, 'pp': 20}, + 'String Shot': {'id': 81, 'power': 0, 'type': 'Bug', 'accuracy': 95, 'pp': 40}, + 'Dragon Rage': {'id': 82, 'power': 1, 'type': 'Dragon', 'accuracy': 100, 'pp': 10}, + 'Fire Spin': {'id': 83, 'power': 15, 'type': 'Fire', 'accuracy': 70, 'pp': 15}, + 'Thundershock': {'id': 84, 'power': 40, 'type': 'Electric', 'accuracy': 100, 'pp': 30}, + 'Thunderbolt': {'id': 85, 'power': 95, 'type': 'Electric', 'accuracy': 100, 'pp': 15}, + 'Thunder Wave': {'id': 86, 'power': 0, 'type': 'Electric', 'accuracy': 100, 'pp': 20}, + 'Thunder': {'id': 87, 'power': 120, 'type': 'Electric', 'accuracy': 70, 'pp': 10}, + 'Rock Throw': {'id': 88, 'power': 50, 'type': 'Rock', 'accuracy': 65, 'pp': 15}, + 'Earthquake': {'id': 89, 'power': 100, 'type': 'Ground', 'accuracy': 100, 'pp': 10}, + 'Fissure': {'id': 90, 'power': 1, 'type': 'Ground', 'accuracy': 30, 'pp': 5}, + 'Dig': {'id': 91, 'power': 100, 'type': 'Ground', 'accuracy': 100, 'pp': 10}, + 'Toxic': {'id': 92, 'power': 0, 'type': 'Poison', 'accuracy': 85, 'pp': 10}, + 'Confusion': {'id': 93, 'power': 50, 'type': 'Psychic', 'accuracy': 100, 'pp': 25}, + 'Psychic': {'id': 94, 'power': 90, 'type': 'Psychic', 'accuracy': 100, 'pp': 10}, + 'Hypnosis': {'id': 95, 'power': 0, 'type': 'Psychic', 'accuracy': 60, 'pp': 20}, + 'Meditate': {'id': 96, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 40}, + 'Agility': {'id': 97, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 30}, + 'Quick Attack': {'id': 98, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, + 'Rage': {'id': 99, 'power': 20, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, + 'Teleport': {'id': 100, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 20}, + 'Night Shade': {'id': 101, 'power': 0, 'type': 'Ghost', 'accuracy': 100, 'pp': 15}, + 'Mimic': {'id': 102, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, + 'Screech': {'id': 103, 'power': 0, 'type': 'Normal', 'accuracy': 85, 'pp': 40}, + 'Double Team': {'id': 104, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, + 'Recover': {'id': 105, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, + 'Harden': {'id': 106, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, + 'Minimize': {'id': 107, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, + 'Smokescreen': {'id': 108, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, + 'Confuse Ray': {'id': 109, 'power': 0, 'type': 'Ghost', 'accuracy': 100, 'pp': 10}, + 'Withdraw': {'id': 110, 'power': 0, 'type': 'Water', 'accuracy': 100, 'pp': 40}, + 'Defense Curl': {'id': 111, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 40}, + 'Barrier': {'id': 112, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 30}, + 'Light Screen': {'id': 113, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 30}, + 'Haze': {'id': 114, 'power': 0, 'type': 'Ice', 'accuracy': 100, 'pp': 30}, + 'Reflect': {'id': 115, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 20}, + 'Focus Energy': {'id': 116, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, + 'Bide': {'id': 117, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, + 'Metronome': {'id': 118, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, + 'Mirror Move': {'id': 119, 'power': 0, 'type': 'Flying', 'accuracy': 100, 'pp': 20}, + 'Selfdestruct': {'id': 120, 'power': 130, 'type': 'Normal', 'accuracy': 100, 'pp': 5}, + 'Egg Bomb': {'id': 121, 'power': 100, 'type': 'Normal', 'accuracy': 75, 'pp': 10}, + 'Lick': {'id': 122, 'power': 20, 'type': 'Ghost', 'accuracy': 100, 'pp': 30}, + 'Smog': {'id': 123, 'power': 20, 'type': 'Poison', 'accuracy': 70, 'pp': 20}, + 'Sludge': {'id': 124, 'power': 65, 'type': 'Poison', 'accuracy': 100, 'pp': 20}, + 'Bone Club': {'id': 125, 'power': 65, 'type': 'Ground', 'accuracy': 85, 'pp': 20}, + 'Fire Blast': {'id': 126, 'power': 120, 'type': 'Fire', 'accuracy': 85, 'pp': 5}, + 'Waterfall': {'id': 127, 'power': 80, 'type': 'Water', 'accuracy': 100, 'pp': 15}, + 'Clamp': {'id': 128, 'power': 35, 'type': 'Water', 'accuracy': 75, 'pp': 10}, + 'Swift': {'id': 129, 'power': 60, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, + 'Skull Bash': {'id': 130, 'power': 100, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, + 'Spike Cannon': {'id': 131, 'power': 20, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, + 'Constrict': {'id': 132, 'power': 10, 'type': 'Normal', 'accuracy': 100, 'pp': 35}, + 'Amnesia': {'id': 133, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 20}, + 'Kinesis': {'id': 134, 'power': 0, 'type': 'Psychic', 'accuracy': 80, 'pp': 15}, + 'Softboiled': {'id': 135, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, + 'Hi Jump Kick': {'id': 136, 'power': 85, 'type': 'Fighting', 'accuracy': 90, 'pp': 20}, + 'Glare': {'id': 137, 'power': 0, 'type': 'Normal', 'accuracy': 75, 'pp': 30}, + 'Dream Eater': {'id': 138, 'power': 100, 'type': 'Psychic', 'accuracy': 100, 'pp': 15}, + 'Poison Gas': {'id': 139, 'power': 0, 'type': 'Poison', 'accuracy': 55, 'pp': 40}, + 'Barrage': {'id': 140, 'power': 15, 'type': 'Normal', 'accuracy': 85, 'pp': 20}, + 'Leech Life': {'id': 141, 'power': 20, 'type': 'Bug', 'accuracy': 100, 'pp': 15}, + 'Lovely Kiss': {'id': 142, 'power': 0, 'type': 'Normal', 'accuracy': 75, 'pp': 10}, + 'Sky Attack': {'id': 143, 'power': 140, 'type': 'Flying', 'accuracy': 90, 'pp': 5}, + 'Transform': {'id': 144, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, + 'Bubble': {'id': 145, 'power': 20, 'type': 'Water', 'accuracy': 100, 'pp': 30}, + 'Dizzy Punch': {'id': 146, 'power': 70, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, + 'Spore': {'id': 147, 'power': 0, 'type': 'Grass', 'accuracy': 100, 'pp': 15}, + 'Flash': {'id': 148, 'power': 0, 'type': 'Normal', 'accuracy': 70, 'pp': 20}, + 'Psywave': {'id': 149, 'power': 1, 'type': 'Psychic', 'accuracy': 80, 'pp': 15}, + 'Splash': {'id': 150, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 40}, + 'Acid Armor': {'id': 151, 'power': 0, 'type': 'Poison', 'accuracy': 100, 'pp': 40}, + 'Crabhammer': {'id': 152, 'power': 90, 'type': 'Water', 'accuracy': 85, 'pp': 10}, + 'Explosion': {'id': 153, 'power': 170, 'type': 'Normal', 'accuracy': 100, 'pp': 5}, + 'Fury Swipes': {'id': 154, 'power': 18, 'type': 'Normal', 'accuracy': 80, 'pp': 15}, + 'Bonemerang': {'id': 155, 'power': 50, 'type': 'Ground', 'accuracy': 90, 'pp': 10}, + 'Rest': {'id': 156, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 10}, + 'Rock Slide': {'id': 157, 'power': 75, 'type': 'Rock', 'accuracy': 90, 'pp': 10}, + 'Hyper Fang': {'id': 158, 'power': 80, 'type': 'Normal', 'accuracy': 90, 'pp': 15}, + 'Sharpen': {'id': 159, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, + 'Conversion': {'id': 160, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, + 'Tri Attack': {'id': 161, 'power': 80, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, + 'Super Fang': {'id': 162, 'power': 1, 'type': 'Normal', 'accuracy': 90, 'pp': 10}, + 'Slash': {'id': 163, 'power': 70, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, + 'Substitute': {'id': 164, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, + #'Struggle': {'id': 165, 'power': 50, 'type': 'Struggle_Type', 'accuracy': 100, 'pp': 10} +} + +encounter_tables = {'Wild_Super_Rod_A': 2, 'Wild_Super_Rod_B': 2, 'Wild_Super_Rod_C': 3, 'Wild_Super_Rod_D': 2, + 'Wild_Super_Rod_E': 2, 'Wild_Super_Rod_F': 4, 'Wild_Super_Rod_G': 4, 'Wild_Super_Rod_H': 4, + 'Wild_Super_Rod_I': 4, 'Wild_Super_Rod_J': 4, 'Wild_Route1': 10, 'Wild_Route2': 10, + 'Wild_Route22': 10, 'Wild_ViridianForest': 10, 'Wild_Route3': 10, 'Wild_MtMoon1F': 10, + 'Wild_MtMoonB1F': 10, 'Wild_MtMoonB2F': 10, 'Wild_Route4': 10, 'Wild_Route24': 10, + 'Wild_Route25': 10, 'Wild_Route9': 10, 'Wild_Route5': 10, 'Wild_Route6': 10, + 'Wild_Route11': 10, 'Wild_RockTunnel1F': 10, 'Wild_RockTunnelB1F': 10, 'Wild_Route10': 10, + 'Wild_Route12': 10, 'Wild_Route8': 10, 'Wild_Route7': 10, 'Wild_PokemonTower3F': 10, + 'Wild_PokemonTower4F': 10, 'Wild_PokemonTower5F': 10, 'Wild_PokemonTower6F': 10, + 'Wild_PokemonTower7F': 10, 'Wild_Route13': 10, 'Wild_Route14': 10, 'Wild_Route15': 10, + 'Wild_Route16': 10, 'Wild_Route17': 10, 'Wild_Route18': 10, 'Wild_SafariZoneCenter': 10, + 'Wild_SafariZoneEast': 10, 'Wild_SafariZoneNorth': 10, 'Wild_SafariZoneWest': 10, + 'Wild_SeaRoutes': 10, 'Wild_SeafoamIslands1F': 10, 'Wild_SeafoamIslandsB1F': 10, + 'Wild_SeafoamIslandsB2F': 10, 'Wild_SeafoamIslandsB3F': 10, 'Wild_SeafoamIslandsB4F': 10, + 'Wild_PokemonMansion1F': 10, 'Wild_PokemonMansion2F': 10, 'Wild_PokemonMansion3F': 10, + 'Wild_PokemonMansionB1F': 10, 'Wild_Route21': 10, 'Wild_CeruleanCave1F': 10, + 'Wild_CeruleanCave2F': 10, 'Wild_CeruleanCaveB1F': 10, 'Wild_PowerPlant': 10, + 'Wild_Route23': 10, 'Wild_VictoryRoad2F': 10, 'Wild_VictoryRoad3F': 10, + 'Wild_VictoryRoad1F': 10, 'Wild_DiglettsCave': 10, 'Wild_Good_Rod': 2} + +hm_moves = ["Cut", "Fly", "Surf", "Strength", "Flash"] + +tm_moves = [ + 'Mega Punch', 'Razor Wind', 'Swords Dance', 'Whirlwind', 'Mega Kick', 'Toxic', 'Horn Drill', 'Body Slam', + 'Take Down', 'Double Edge', 'Bubblebeam', 'Water Gun', 'Ice Beam', 'Blizzard', 'Hyper Beam', 'Pay Day', + 'Submission', 'Counter', 'Seismic Toss', 'Rage', 'Mega Drain', 'Solarbeam', 'Dragon Rage', 'Thunderbolt', 'Thunder', + 'Earthquake', 'Fissure', 'Dig', 'Psychic', 'Teleport', 'Mimic', 'Double Team', 'Reflect', 'Bide', 'Metronome', + 'Selfdestruct', 'Egg Bomb', 'Fire Blast', 'Swift', 'Skull Bash', 'Softboiled', 'Dream Eater', 'Sky Attack', 'Rest', + 'Thunder Wave', 'Psywave', 'Explosion', 'Rock Slide', 'Tri Attack', 'Substitute' +] + + + +first_stage_pokemon = [pokemon for pokemon in pokemon_data.keys() if pokemon not in evolves_from] +legendary_pokemon = ["Articuno", "Zapdos", "Moltres", "Mewtwo", "Mew"] + diff --git a/worlds/pokemon_rb/regions.py b/worlds/pokemon_rb/regions.py new file mode 100644 index 0000000000..1650e640cb --- /dev/null +++ b/worlds/pokemon_rb/regions.py @@ -0,0 +1,305 @@ + +from BaseClasses import MultiWorld, Region, Entrance, RegionType, LocationProgressType +from worlds.generic.Rules import add_item_rule +from .locations import location_data, PokemonRBLocation + + +def create_region(world: MultiWorld, player: int, name: str, locations_per_region=None, exits=None): + ret = Region(name, RegionType.Generic, name, player, world) + for location in locations_per_region.get(name, []): + if (world.randomize_hidden_items[player].value or "Hidden" not in location.name) and \ + (world.extra_key_items[player].value or name != "Rock Tunnel B1F" or "Item" not in location.name) and \ + (world.tea[player].value or location.name != "Celadon City - Mansion Lady"): + location.parent_region = ret + ret.locations.append(location) + if world.randomize_hidden_items[player].value == 2 and "Hidden" in location.name: + location.progress_type = LocationProgressType.EXCLUDED + add_item_rule(location, lambda i: not (i.advancement or i.useful)) + if exits: + for exit in exits: + ret.exits.append(Entrance(player, exit, ret)) + locations_per_region[name] = [] + return ret + + +def create_regions(world: MultiWorld, player: int): + locations_per_region = {} + for location in location_data: + locations_per_region.setdefault(location.region, []) + locations_per_region[location.region].append(PokemonRBLocation(player, location.name, location.address, + location.rom_address)) + regions = [ + create_region(world, player, "Menu", locations_per_region), + create_region(world, player, "Anywhere", locations_per_region), + create_region(world, player, "Fossil", locations_per_region), + create_region(world, player, "Pallet Town", locations_per_region), + create_region(world, player, "Route 1", locations_per_region), + create_region(world, player, "Viridian City", locations_per_region), + create_region(world, player, "Viridian City North", locations_per_region), + create_region(world, player, "Viridian Gym", locations_per_region), + create_region(world, player, "Route 2", locations_per_region), + create_region(world, player, "Route 2 East", locations_per_region), + create_region(world, player, "Diglett's Cave", locations_per_region), + create_region(world, player, "Route 22", locations_per_region), + create_region(world, player, "Route 23 South", locations_per_region), + create_region(world, player, "Route 23 North", locations_per_region), + create_region(world, player, "Viridian Forest", locations_per_region), + create_region(world, player, "Pewter City", locations_per_region), + create_region(world, player, "Pewter Gym", locations_per_region), + create_region(world, player, "Route 3", locations_per_region), + create_region(world, player, "Mt Moon 1F", locations_per_region), + create_region(world, player, "Mt Moon B1F", locations_per_region), + create_region(world, player, "Mt Moon B2F", locations_per_region), + create_region(world, player, "Route 4", locations_per_region), + create_region(world, player, "Cerulean City", locations_per_region), + create_region(world, player, "Cerulean Gym", locations_per_region), + create_region(world, player, "Route 24", locations_per_region), + create_region(world, player, "Route 25", locations_per_region), + create_region(world, player, "Route 9", locations_per_region), + create_region(world, player, "Route 10 North", locations_per_region), + create_region(world, player, "Rock Tunnel 1F", locations_per_region), + create_region(world, player, "Rock Tunnel B1F", locations_per_region), + create_region(world, player, "Power Plant", locations_per_region), + create_region(world, player, "Route 10 South", locations_per_region), + create_region(world, player, "Lavender Town", locations_per_region), + create_region(world, player, "Pokemon Tower 1F", locations_per_region), + create_region(world, player, "Pokemon Tower 2F", locations_per_region), + create_region(world, player, "Pokemon Tower 3F", locations_per_region), + create_region(world, player, "Pokemon Tower 4F", locations_per_region), + create_region(world, player, "Pokemon Tower 5F", locations_per_region), + create_region(world, player, "Pokemon Tower 6F", locations_per_region), + create_region(world, player, "Pokemon Tower 7F", locations_per_region), + create_region(world, player, "Route 5", locations_per_region), + create_region(world, player, "Saffron City", locations_per_region), + create_region(world, player, "Saffron Gym", locations_per_region), + create_region(world, player, "Copycat's House", locations_per_region), + create_region(world, player, "Underground Tunnel North-South", locations_per_region), + create_region(world, player, "Route 6", locations_per_region), + create_region(world, player, "Vermilion City", locations_per_region), + create_region(world, player, "Vermilion Gym", locations_per_region), + create_region(world, player, "S.S. Anne 1F", locations_per_region), + create_region(world, player, "S.S. Anne B1F", locations_per_region), + create_region(world, player, "S.S. Anne 2F", locations_per_region), + create_region(world, player, "Route 11", locations_per_region), + create_region(world, player, "Route 11 East", locations_per_region), + create_region(world, player, "Route 12 North", locations_per_region), + create_region(world, player, "Route 12 South", locations_per_region), + create_region(world, player, "Route 12 Grass", locations_per_region), + create_region(world, player, "Route 12 West", locations_per_region), + create_region(world, player, "Route 7", locations_per_region), + create_region(world, player, "Underground Tunnel West-East", locations_per_region), + create_region(world, player, "Route 8", locations_per_region), + create_region(world, player, "Route 8 Grass", locations_per_region), + create_region(world, player, "Celadon City", locations_per_region), + create_region(world, player, "Celadon Prize Corner", locations_per_region), + create_region(world, player, "Celadon Gym", locations_per_region), + create_region(world, player, "Route 16", locations_per_region), + create_region(world, player, "Route 16 North", locations_per_region), + create_region(world, player, "Route 17", locations_per_region), + create_region(world, player, "Route 18", locations_per_region), + create_region(world, player, "Fuchsia City", locations_per_region), + create_region(world, player, "Fuchsia Gym", locations_per_region), + create_region(world, player, "Safari Zone Gate", locations_per_region), + create_region(world, player, "Safari Zone Center", locations_per_region), + create_region(world, player, "Safari Zone East", locations_per_region), + create_region(world, player, "Safari Zone North", locations_per_region), + create_region(world, player, "Safari Zone West", locations_per_region), + create_region(world, player, "Route 15", locations_per_region), + create_region(world, player, "Route 14", locations_per_region), + create_region(world, player, "Route 13", locations_per_region), + create_region(world, player, "Route 19", locations_per_region), + create_region(world, player, "Route 20 East", locations_per_region), + create_region(world, player, "Route 20 West", locations_per_region), + create_region(world, player, "Seafoam Islands 1F", locations_per_region), + create_region(world, player, "Seafoam Islands B1F", locations_per_region), + create_region(world, player, "Seafoam Islands B2F", locations_per_region), + create_region(world, player, "Seafoam Islands B3F", locations_per_region), + create_region(world, player, "Seafoam Islands B4F", locations_per_region), + create_region(world, player, "Cinnabar Island", locations_per_region), + create_region(world, player, "Cinnabar Gym", locations_per_region), + create_region(world, player, "Route 21", locations_per_region), + create_region(world, player, "Silph Co 1F", locations_per_region), + create_region(world, player, "Silph Co 2F", locations_per_region), + create_region(world, player, "Silph Co 3F", locations_per_region), + create_region(world, player, "Silph Co 4F", locations_per_region), + create_region(world, player, "Silph Co 5F", locations_per_region), + create_region(world, player, "Silph Co 6F", locations_per_region), + create_region(world, player, "Silph Co 7F", locations_per_region), + create_region(world, player, "Silph Co 8F", locations_per_region), + create_region(world, player, "Silph Co 9F", locations_per_region), + create_region(world, player, "Silph Co 10F", locations_per_region), + create_region(world, player, "Silph Co 11F", locations_per_region), + create_region(world, player, "Rocket Hideout B1F", locations_per_region), + create_region(world, player, "Rocket Hideout B2F", locations_per_region), + create_region(world, player, "Rocket Hideout B3F", locations_per_region), + create_region(world, player, "Rocket Hideout B4F", locations_per_region), + create_region(world, player, "Pokemon Mansion 1F", locations_per_region), + create_region(world, player, "Pokemon Mansion 2F", locations_per_region), + create_region(world, player, "Pokemon Mansion 3F", locations_per_region), + create_region(world, player, "Pokemon Mansion B1F", locations_per_region), + create_region(world, player, "Victory Road 1F", locations_per_region), + create_region(world, player, "Victory Road 2F", locations_per_region), + create_region(world, player, "Victory Road 3F", locations_per_region), + create_region(world, player, "Indigo Plateau", locations_per_region), + create_region(world, player, "Cerulean Cave 1F", locations_per_region), + create_region(world, player, "Cerulean Cave 2F", locations_per_region), + create_region(world, player, "Cerulean Cave B1F", locations_per_region), + create_region(world, player, "Evolution", locations_per_region), + ] + world.regions += regions + connect(world, player, "Menu", "Anywhere", one_way=True) + connect(world, player, "Menu", "Pallet Town", one_way=True) + connect(world, player, "Menu", "Fossil", lambda state: state.pokemon_rb_fossil_checks( + state.world.second_fossil_check_condition[player].value, player), one_way=True) + connect(world, player, "Pallet Town", "Route 1") + connect(world, player, "Route 1", "Viridian City") + connect(world, player, "Viridian City", "Route 22") + connect(world, player, "Route 22", "Route 23 South", + lambda state: state.pokemon_rb_has_badges(state.world.victory_road_condition[player].value, player)) + connect(world, player, "Route 23 South", "Route 23 North", lambda state: state.pokemon_rb_can_surf(player)) + connect(world, player, "Viridian City North", "Viridian Gym", lambda state: + state.pokemon_rb_has_badges(state.world.viridian_gym_condition[player].value, player), one_way=True) + connect(world, player, "Route 2", "Route 2 East", lambda state: state.pokemon_rb_can_cut(player)) + connect(world, player, "Route 2 East", "Diglett's Cave", lambda state: state.pokemon_rb_can_cut(player)) + connect(world, player, "Route 2", "Viridian City North") + connect(world, player, "Route 2", "Viridian Forest") + connect(world, player, "Route 2", "Pewter City") + connect(world, player, "Pewter City", "Pewter Gym", one_way=True) + connect(world, player, "Pewter City", "Route 3") + connect(world, player, "Route 4", "Route 3", one_way=True) + connect(world, player, "Mt Moon 1F", "Mt Moon B1F", one_way=True) + connect(world, player, "Mt Moon B1F", "Mt Moon B2F", one_way=True) + connect(world, player, "Mt Moon B1F", "Route 4", one_way=True) + connect(world, player, "Route 4", "Cerulean City") + connect(world, player, "Cerulean City", "Cerulean Gym", one_way=True) + connect(world, player, "Cerulean City", "Route 24", one_way=True) + connect(world, player, "Route 24", "Route 25", one_way=True) + connect(world, player, "Cerulean City", "Route 9", lambda state: state.pokemon_rb_can_cut(player)) + connect(world, player, "Route 9", "Route 10 North") + connect(world, player, "Route 10 North", "Rock Tunnel 1F", lambda state: state.pokemon_rb_can_flash(player)) + connect(world, player, "Route 10 North", "Power Plant", lambda state: state.pokemon_rb_can_surf(player) and + (state.has("Plant Key", player) or not state.world.extra_key_items[player].value), one_way=True) + connect(world, player, "Rock Tunnel 1F", "Route 10 South", lambda state: state.pokemon_rb_can_flash(player)) + connect(world, player, "Rock Tunnel 1F", "Rock Tunnel B1F") + connect(world, player, "Lavender Town", "Pokemon Tower 1F", one_way=True) + connect(world, player, "Lavender Town", "Pokemon Tower 1F", one_way=True) + connect(world, player, "Pokemon Tower 1F", "Pokemon Tower 2F", one_way=True) + connect(world, player, "Pokemon Tower 2F", "Pokemon Tower 3F", one_way=True) + connect(world, player, "Pokemon Tower 3F", "Pokemon Tower 4F", one_way=True) + connect(world, player, "Pokemon Tower 4F", "Pokemon Tower 5F", one_way=True) + connect(world, player, "Pokemon Tower 5F", "Pokemon Tower 6F", one_way=True) + connect(world, player, "Pokemon Tower 6F", "Pokemon Tower 7F", lambda state: state.has("Silph Scope", player)) + connect(world, player, "Cerulean City", "Route 5") + connect(world, player, "Route 5", "Saffron City", lambda state: state.pokemon_rb_can_pass_guards(player)) + connect(world, player, "Route 5", "Underground Tunnel North-South") + connect(world, player, "Route 6", "Underground Tunnel North-South") + connect(world, player, "Route 6", "Saffron City", lambda state: state.pokemon_rb_can_pass_guards(player)) + connect(world, player, "Route 7", "Saffron City", lambda state: state.pokemon_rb_can_pass_guards(player)) + connect(world, player, "Route 8", "Saffron City", lambda state: state.pokemon_rb_can_pass_guards(player)) + connect(world, player, "Saffron City", "Copycat's House", lambda state: state.has("Silph Co Liberated", player), one_way=True) + connect(world, player, "Saffron City", "Saffron Gym", lambda state: state.has("Silph Co Liberated", player), one_way=True) + connect(world, player, "Route 6", "Vermilion City") + connect(world, player, "Vermilion City", "Vermilion Gym", lambda state: state.pokemon_rb_can_surf(player) or state.pokemon_rb_can_cut(player), one_way=True) + connect(world, player, "Vermilion City", "S.S. Anne 1F", lambda state: state.has("S.S. Ticket", player), one_way=True) + connect(world, player, "S.S. Anne 1F", "S.S. Anne 2F", one_way=True) + connect(world, player, "S.S. Anne 1F", "S.S. Anne B1F", one_way=True) + connect(world, player, "Vermilion City", "Route 11") + connect(world, player, "Vermilion City", "Diglett's Cave") + connect(world, player, "Route 12 West", "Route 11 East", lambda state: state.pokemon_rb_can_strength(player) or not state.world.extra_strength_boulders[player].value) + connect(world, player, "Route 12 North", "Route 12 South", lambda state: state.has("Poke Flute", player) or state.pokemon_rb_can_surf( player)) + connect(world, player, "Route 12 West", "Route 12 North", lambda state: state.has("Poke Flute", player)) + connect(world, player, "Route 12 West", "Route 12 South", lambda state: state.has("Poke Flute", player)) + connect(world, player, "Route 12 South", "Route 12 Grass", lambda state: state.pokemon_rb_can_cut(player)) + connect(world, player, "Route 12 North", "Lavender Town") + connect(world, player, "Route 7", "Lavender Town") + connect(world, player, "Route 10 South", "Lavender Town") + connect(world, player, "Route 7", "Underground Tunnel West-East") + connect(world, player, "Route 8", "Underground Tunnel West-East") + connect(world, player, "Route 8", "Celadon City") + connect(world, player, "Route 8", "Route 8 Grass", lambda state: state.pokemon_rb_can_cut(player), one_way=True) + connect(world, player, "Route 7", "Celadon City") + connect(world, player, "Celadon City", "Celadon Gym", lambda state: state.pokemon_rb_can_cut(player), one_way=True) + connect(world, player, "Celadon City", "Celadon Prize Corner") + connect(world, player, "Celadon City", "Route 16") + connect(world, player, "Route 16", "Route 16 North", lambda state: state.pokemon_rb_can_cut(player), one_way=True) + connect(world, player, "Route 16", "Route 17", lambda state: state.has("Poke Flute", player) and state.has("Bicycle", player)) + connect(world, player, "Route 17", "Route 18", lambda state: state.has("Bicycle", player)) + connect(world, player, "Fuchsia City", "Fuchsia Gym", one_way=True) + connect(world, player, "Fuchsia City", "Route 18") + connect(world, player, "Fuchsia City", "Safari Zone Gate", one_way=True) + connect(world, player, "Safari Zone Gate", "Safari Zone Center", lambda state: state.has("Safari Pass", player) or not state.world.extra_key_items[player].value, one_way=True) + connect(world, player, "Safari Zone Center", "Safari Zone East", one_way=True) + connect(world, player, "Safari Zone Center", "Safari Zone West", one_way=True) + connect(world, player, "Safari Zone Center", "Safari Zone North", one_way=True) + connect(world, player, "Fuchsia City", "Route 15") + connect(world, player, "Route 15", "Route 14") + connect(world, player, "Route 14", "Route 13") + connect(world, player, "Route 13", "Route 12 South", lambda state: state.pokemon_rb_can_strength(player) or state.pokemon_rb_can_surf(player) or not state.world.extra_strength_boulders[player].value) + connect(world, player, "Fuchsia City", "Route 19", lambda state: state.pokemon_rb_can_surf(player)) + connect(world, player, "Route 20 East", "Route 19") + connect(world, player, "Route 20 West", "Cinnabar Island", lambda state: state.pokemon_rb_can_surf(player)) + connect(world, player, "Route 20 West", "Seafoam Islands 1F") + connect(world, player, "Route 20 East", "Seafoam Islands 1F", one_way=True) + connect(world, player, "Seafoam Islands 1F", "Route 20 East", lambda state: state.pokemon_rb_can_strength(player), one_way=True) + connect(world, player, "Viridian City", "Viridian City North", lambda state: state.has("Oak's Parcel", player) or state.world.old_man[player].value == 2 or state.pokemon_rb_can_cut(player)) + connect(world, player, "Route 3", "Mt Moon 1F", one_way=True) + connect(world, player, "Route 11", "Route 11 East", lambda state: state.pokemon_rb_can_strength(player)) + connect(world, player, "Cinnabar Island", "Cinnabar Gym", lambda state: state.has("Secret Key", player), one_way=True) + connect(world, player, "Cinnabar Island", "Pokemon Mansion 1F", lambda state: state.has("Mansion Key", player) or not state.world.extra_key_items[player].value, one_way=True) + connect(world, player, "Seafoam Islands 1F", "Seafoam Islands B1F", one_way=True) + connect(world, player, "Seafoam Islands B1F", "Seafoam Islands B2F", one_way=True) + connect(world, player, "Seafoam Islands B2F", "Seafoam Islands B3F", one_way=True) + connect(world, player, "Seafoam Islands B3F", "Seafoam Islands B4F", one_way=True) + connect(world, player, "Route 21", "Cinnabar Island", lambda state: state.pokemon_rb_can_surf(player)) + connect(world, player, "Pallet Town", "Route 21", lambda state: state.pokemon_rb_can_surf(player)) + connect(world, player, "Saffron City", "Silph Co 1F", lambda state: state.has("Fuji Saved", player), one_way=True) + connect(world, player, "Silph Co 1F", "Silph Co 2F", one_way=True) + connect(world, player, "Silph Co 2F", "Silph Co 3F", one_way=True) + connect(world, player, "Silph Co 3F", "Silph Co 4F", one_way=True) + connect(world, player, "Silph Co 4F", "Silph Co 5F", one_way=True) + connect(world, player, "Silph Co 5F", "Silph Co 6F", one_way=True) + connect(world, player, "Silph Co 6F", "Silph Co 7F", one_way=True) + connect(world, player, "Silph Co 7F", "Silph Co 8F", one_way=True) + connect(world, player, "Silph Co 8F", "Silph Co 9F", one_way=True) + connect(world, player, "Silph Co 9F", "Silph Co 10F", one_way=True) + connect(world, player, "Silph Co 10F", "Silph Co 11F", one_way=True) + connect(world, player, "Celadon City", "Rocket Hideout B1F", lambda state: state.has("Hideout Key", player) or not state.world.extra_key_items[player].value, one_way=True) + connect(world, player, "Rocket Hideout B1F", "Rocket Hideout B2F", one_way=True) + connect(world, player, "Rocket Hideout B2F", "Rocket Hideout B3F", one_way=True) + connect(world, player, "Rocket Hideout B3F", "Rocket Hideout B4F", one_way=True) + connect(world, player, "Pokemon Mansion 1F", "Pokemon Mansion 2F", one_way=True) + connect(world, player, "Pokemon Mansion 2F", "Pokemon Mansion 3F", one_way=True) + connect(world, player, "Pokemon Mansion 1F", "Pokemon Mansion B1F", one_way=True) + connect(world, player, "Route 23 North", "Victory Road 1F", lambda state: state.pokemon_rb_can_strength(player), one_way=True) + connect(world, player, "Victory Road 1F", "Victory Road 2F", one_way=True) + connect(world, player, "Victory Road 2F", "Victory Road 3F", one_way=True) + connect(world, player, "Victory Road 2F", "Indigo Plateau", lambda state: state.pokemon_rb_has_badges(state.world.elite_four_condition[player], player), one_way=True) + connect(world, player, "Cerulean City", "Cerulean Cave 1F", lambda state: + state.pokemon_rb_cerulean_cave(state.world.cerulean_cave_condition[player].value + (state.world.extra_key_items[player].value * 4), player) and + state.pokemon_rb_can_surf(player), one_way=True) + connect(world, player, "Cerulean Cave 1F", "Cerulean Cave 2F", one_way=True) + connect(world, player, "Cerulean Cave 1F", "Cerulean Cave B1F", lambda state: state.pokemon_rb_can_surf(player), one_way=True) + if world.worlds[player].fly_map != "Pallet Town": + connect(world, player, "Menu", world.worlds[player].fly_map, lambda state: state.pokemon_rb_can_fly(player), one_way=True, + name="Fly to " + world.worlds[player].fly_map) + + +def connect(world: MultiWorld, player: int, source: str, target: str, rule: callable = lambda state: True, one_way=False, name=None): + source_region = world.get_region(source, player) + target_region = world.get_region(target, player) + + if name is None: + name = source + " to " + target + + connection = Entrance( + player, + name, + source_region + ) + + connection.access_rule = rule + + source_region.exits.append(connection) + connection.connect(target_region) + if not one_way: + connect(world, player, target, source, rule, True) diff --git a/worlds/pokemon_rb/rom.py b/worlds/pokemon_rb/rom.py new file mode 100644 index 0000000000..0487387363 --- /dev/null +++ b/worlds/pokemon_rb/rom.py @@ -0,0 +1,631 @@ +import os +import hashlib +import Utils +import bsdiff4 +from copy import deepcopy +from Patch import APDeltaPatch +from .text import encode_text +from .rom_addresses import rom_addresses +from .locations import location_data +import worlds.pokemon_rb.poke_data as poke_data + + +def choose_forced_type(chances, random): + n = random.randint(1, 100) + for chance in chances: + if chance[0] >= n: + return chance[1] + return None + + +def filter_moves(moves, type, random): + ret = [] + for move in moves: + if poke_data.moves[move]["type"] == type or type is None: + ret.append(move) + random.shuffle(ret) + return ret + + +def get_move(moves, chances, random, starting_move=False): + type = choose_forced_type(chances, random) + filtered_moves = filter_moves(moves, type, random) + for move in filtered_moves: + if poke_data.moves[move]["accuracy"] > 80 and poke_data.moves[move]["power"] > 0 or not starting_move: + moves.remove(move) + return move + else: + return get_move(moves, [], random, starting_move) + + +def get_encounter_slots(self): + encounter_slots = [location for location in location_data if location.type == "Wild Encounter"] + + for location in encounter_slots: + if isinstance(location.original_item, list): + location.original_item = location.original_item[not self.world.game_version[self.player].value] + return encounter_slots + + +def get_base_stat_total(mon): + return (poke_data.pokemon_data[mon]["atk"] + poke_data.pokemon_data[mon]["def"] + + poke_data.pokemon_data[mon]["hp"] + poke_data.pokemon_data[mon]["spd"] + + poke_data.pokemon_data[mon]["spc"]) + + +def randomize_pokemon(self, mon, mons_list, randomize_type): + if randomize_type in [1, 3]: + type_mons = [pokemon for pokemon in mons_list if any([poke_data.pokemon_data[mon][ + "type1"] in [self.local_poke_data[pokemon]["type1"], self.local_poke_data[pokemon]["type2"]], + poke_data.pokemon_data[mon]["type2"] in [self.local_poke_data[pokemon]["type1"], + self.local_poke_data[pokemon]["type2"]]])] + if not type_mons: + type_mons = mons_list.copy() + if randomize_type == 3: + stat_base = get_base_stat_total(mon) + type_mons.sort(key=lambda mon: abs(get_base_stat_total(mon) - stat_base)) + mon = type_mons[round(self.world.random.triangular(0, len(type_mons) - 1, 0))] + if randomize_type == 2: + stat_base = get_base_stat_total(mon) + mons_list.sort(key=lambda mon: abs(get_base_stat_total(mon) - stat_base)) + mon = mons_list[round(self.world.random.triangular(0, 50, 0))] + elif randomize_type == 4: + mon = self.world.random.choice(mons_list) + return mon + + +def process_trainer_data(self, data): + mons_list = [pokemon for pokemon in poke_data.pokemon_data.keys() if pokemon not in poke_data.legendary_pokemon + or self.world.trainer_legendaries[self.player].value] + address = rom_addresses["Trainer_Data"] + while address < rom_addresses["Trainer_Data_End"]: + if data[address] == 255: + mode = 1 + else: + mode = 0 + while True: + address += 1 + if data[address] == 0: + address += 1 + break + address += mode + mon = None + for i in range(1, 4): + for l in ["A", "B", "C", "D", "E", "F", "G", "H"]: + if rom_addresses[f"Rival_Starter{i}_{l}"] == address: + mon = " ".join(self.world.get_location(f"Pallet Town - Starter {i}", self.player).item.name.split()[1:]) + if l in ["D", "E", "F", "G", "H"] and mon in poke_data.evolves_to: + mon = poke_data.evolves_to[mon] + if l in ["F", "G", "H"] and mon in poke_data.evolves_to: + mon = poke_data.evolves_to[mon] + if mon is None and self.world.randomize_trainer_parties[self.player].value: + mon = poke_data.id_to_mon[data[address]] + mon = randomize_pokemon(self, mon, mons_list, self.world.randomize_trainer_parties[self.player].value) + if mon is not None: + data[address] = poke_data.pokemon_data[mon]["id"] + + +def process_static_pokemon(self): + starter_slots = [location for location in location_data if location.type == "Starter Pokemon"] + legendary_slots = [location for location in location_data if location.type == "Legendary Pokemon"] + static_slots = [location for location in location_data if location.type in + ["Static Pokemon", "Missable Pokemon"]] + legendary_mons = [slot.original_item for slot in legendary_slots] + + tower_6F_mons = set() + for i in range(1, 11): + tower_6F_mons.add(self.world.get_location(f"Pokemon Tower 6F - Wild Pokemon - {i}", self.player).item.name) + + mons_list = [pokemon for pokemon in poke_data.first_stage_pokemon if pokemon not in poke_data.legendary_pokemon + or self.world.randomize_legendary_pokemon[self.player].value == 3] + if self.world.randomize_legendary_pokemon[self.player].value == 0: + for slot in legendary_slots: + location = self.world.get_location(slot.name, self.player) + location.place_locked_item(self.create_item("Missable " + slot.original_item)) + elif self.world.randomize_legendary_pokemon[self.player].value == 1: + self.world.random.shuffle(legendary_mons) + for slot in legendary_slots: + location = self.world.get_location(slot.name, self.player) + location.place_locked_item(self.create_item("Missable " + legendary_mons.pop())) + elif self.world.randomize_legendary_pokemon[self.player].value == 2: + static_slots = static_slots + legendary_slots + self.world.random.shuffle(static_slots) + static_slots.sort(key=lambda s: 0 if s.name == "Pokemon Tower 6F - Restless Soul" else 1) + while legendary_slots: + swap_slot = legendary_slots.pop() + slot = static_slots.pop() + slot_type = slot.type.split()[0] + if slot_type == "Legendary": + slot_type = "Missable" + location = self.world.get_location(slot.name, self.player) + location.place_locked_item(self.create_item(slot_type + " " + swap_slot.original_item)) + swap_slot.original_item = slot.original_item + elif self.world.randomize_legendary_pokemon[self.player].value == 3: + static_slots = static_slots + legendary_slots + + for slot in static_slots: + location = self.world.get_location(slot.name, self.player) + randomize_type = self.world.randomize_static_pokemon[self.player].value + slot_type = slot.type.split()[0] + if slot_type == "Legendary": + slot_type = "Missable" + if not randomize_type: + location.place_locked_item(self.create_item(slot_type + " " + slot.original_item)) + else: + mon = self.create_item(slot_type + " " + + randomize_pokemon(self, slot.original_item, mons_list, randomize_type)) + while location.name == "Pokemon Tower 6F - Restless Soul" and mon in tower_6F_mons: + mon = self.create_item(slot_type + " " + randomize_pokemon(self, slot.original_item, mons_list, + randomize_type)) + location.place_locked_item(mon) + + for slot in starter_slots: + location = self.world.get_location(slot.name, self.player) + randomize_type = self.world.randomize_starter_pokemon[self.player].value + slot_type = "Missable" + if not randomize_type: + location.place_locked_item(self.create_item(slot_type + " " + slot.original_item)) + else: + location.place_locked_item(self.create_item(slot_type + " " + + randomize_pokemon(self, slot.original_item, mons_list, randomize_type))) + + +def process_wild_pokemon(self): + + encounter_slots = get_encounter_slots(self) + + placed_mons = {pokemon: 0 for pokemon in poke_data.pokemon_data.keys()} + if self.world.randomize_wild_pokemon[self.player].value: + mons_list = [pokemon for pokemon in poke_data.pokemon_data.keys() if pokemon not in poke_data.legendary_pokemon + or self.world.randomize_legendary_pokemon[self.player].value == 3] + self.world.random.shuffle(encounter_slots) + locations = [] + for slot in encounter_slots: + mon = randomize_pokemon(self, slot.original_item, mons_list, self.world.randomize_wild_pokemon[self.player].value) + # if static Pokemon are not randomized, we make sure nothing on Pokemon Tower 6F is a Marowak + # if static Pokemon are randomized we deal with that during static encounter randomization + while (self.world.randomize_static_pokemon[self.player].value == 0 and mon == "Marowak" + and "Pokemon Tower 6F" in slot.name): + # to account for the possibility that only one ground type Pokemon exists, match only stats for this fix + mon = randomize_pokemon(self, slot.original_item, mons_list, 2) + placed_mons[mon] += 1 + location = self.world.get_location(slot.name, self.player) + location.item = self.create_item(mon) + location.event = True + location.locked = True + location.item.location = location + locations.append(location) + + mons_to_add = [] + remaining_pokemon = [pokemon for pokemon in poke_data.pokemon_data.keys() if placed_mons[pokemon] == 0 and + (pokemon not in poke_data.legendary_pokemon or self.world.randomize_legendary_pokemon[self.player].value == 3)] + if self.world.catch_em_all[self.player].value == 1: + mons_to_add = [pokemon for pokemon in poke_data.first_stage_pokemon if placed_mons[pokemon] == 0 and + (pokemon not in poke_data.legendary_pokemon or self.world.randomize_legendary_pokemon[self.player].value == 3)] + elif self.world.catch_em_all[self.player].value == 2: + mons_to_add = remaining_pokemon.copy() + logic_needed_mons = max(self.world.oaks_aide_rt_2[self.player].value, + self.world.oaks_aide_rt_11[self.player].value, + self.world.oaks_aide_rt_15[self.player].value) + if self.world.accessibility[self.player] == "minimal": + logic_needed_mons = 0 + + self.world.random.shuffle(remaining_pokemon) + while (len([pokemon for pokemon in placed_mons if placed_mons[pokemon] > 0]) + + len(mons_to_add) < logic_needed_mons): + mons_to_add.append(remaining_pokemon.pop()) + for mon in mons_to_add: + stat_base = get_base_stat_total(mon) + candidate_locations = get_encounter_slots(self) + if self.world.randomize_wild_pokemon[self.player].value in [1, 3]: + candidate_locations = [slot for slot in candidate_locations if any([poke_data.pokemon_data[slot.original_item][ + "type1"] in [self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]], + poke_data.pokemon_data[slot.original_item]["type2"] in [self.local_poke_data[mon]["type1"], + self.local_poke_data[mon]["type2"]]])] + if not candidate_locations: + candidate_locations = location_data + candidate_locations = [self.world.get_location(location.name, self.player) for location in candidate_locations] + candidate_locations.sort(key=lambda slot: abs(get_base_stat_total(slot.item.name) - stat_base)) + for location in candidate_locations: + if placed_mons[location.item.name] > 1 or location.item.name not in poke_data.first_stage_pokemon: + placed_mons[location.item.name] -= 1 + location.item = self.create_item(mon) + location.item.location = location + placed_mons[mon] += 1 + break + + else: + for slot in encounter_slots: + location = self.world.get_location(slot.name, self.player) + location.item = self.create_item(slot.original_item) + location.event = True + location.locked = True + location.item.location = location + placed_mons[location.item.name] += 1 + + +def process_pokemon_data(self): + + local_poke_data = deepcopy(poke_data.pokemon_data) + learnsets = deepcopy(poke_data.learnsets) + + for mon, mon_data in local_poke_data.items(): + if self.world.randomize_pokemon_stats[self.player].value == 1: + stats = [mon_data["hp"], mon_data["atk"], mon_data["def"], mon_data["spd"], mon_data["spc"]] + self.world.random.shuffle(stats) + mon_data["hp"] = stats[0] + mon_data["atk"] = stats[1] + mon_data["def"] = stats[2] + mon_data["spd"] = stats[3] + mon_data["spc"] = stats[4] + elif self.world.randomize_pokemon_stats[self.player].value == 2: + old_stats = mon_data["hp"] + mon_data["atk"] + mon_data["def"] + mon_data["spd"] + mon_data["spc"] - 5 + stats = [1, 1, 1, 1, 1] + while old_stats > 0: + stat = self.world.random.randint(0, 4) + if stats[stat] < 255: + old_stats -= 1 + stats[stat] += 1 + mon_data["hp"] = stats[0] + mon_data["atk"] = stats[1] + mon_data["def"] = stats[2] + mon_data["spd"] = stats[3] + mon_data["spc"] = stats[4] + if self.world.randomize_pokemon_types[self.player].value: + if self.world.randomize_pokemon_types[self.player].value == 1 and mon in poke_data.evolves_from: + type1 = local_poke_data[poke_data.evolves_from[mon]]["type1"] + type2 = local_poke_data[poke_data.evolves_from[mon]]["type2"] + if type1 == type2: + if self.world.secondary_type_chance[self.player].value == -1: + if mon_data["type1"] != mon_data["type2"]: + while type2 == type1: + type2 = self.world.random.choice(list(poke_data.type_names.values())) + elif self.world.random.randint(1, 100) <= self.world.secondary_type_chance[self.player].value: + type2 = self.world.random.choice(list(poke_data.type_names.values())) + else: + type1 = self.world.random.choice(list(poke_data.type_names.values())) + type2 = type1 + if ((self.world.secondary_type_chance[self.player].value == -1 and mon_data["type1"] + != mon_data["type2"]) or self.world.random.randint(1, 100) + <= self.world.secondary_type_chance[self.player].value): + while type2 == type1: + type2 = self.world.random.choice(list(poke_data.type_names.values())) + + mon_data["type1"] = type1 + mon_data["type2"] = type2 + if self.world.randomize_pokemon_movesets[self.player].value: + if self.world.randomize_pokemon_movesets[self.player].value == 1: + if mon_data["type1"] == "Normal" and mon_data["type2"] == "Normal": + chances = [[75, "Normal"]] + elif mon_data["type1"] == "Normal" or mon_data["type2"] == "Normal": + if mon_data["type1"] == "Normal": + second_type = mon_data["type2"] + else: + second_type = mon_data["type1"] + chances = [[30, "Normal"], [85, second_type]] + elif mon_data["type1"] == mon_data["type2"]: + chances = [[60, mon_data["type1"]], [80, "Normal"]] + else: + chances = [[50, mon_data["type1"]], [80, mon_data["type2"]], [85, "Normal"]] + else: + chances = [] + moves = list(poke_data.moves.keys()) + for move in ["No Move"] + poke_data.hm_moves: + moves.remove(move) + mon_data["start move 1"] = get_move(moves, chances, self.world.random, True) + for i in range(2, 5): + if mon_data[f"start move {i}"] != "No Move" or self.world.start_with_four_moves[ + self.player].value == 1: + mon_data[f"start move {i}"] = get_move(moves, chances, self.world.random) + if mon in learnsets: + for move_num in range(0, len(learnsets[mon])): + learnsets[mon][move_num] = get_move(moves, chances, self.world.random) + if self.world.randomize_pokemon_catch_rates[self.player].value: + mon_data["catch rate"] = self.world.random.randint(self.world.minimum_catch_rate[self.player], 255) + else: + mon_data["catch rate"] = max(self.world.minimum_catch_rate[self.player], mon_data["catch rate"]) + + if mon in poke_data.evolves_from.keys() and mon_data["type1"] == local_poke_data[poke_data.evolves_from[mon]]["type1"] and mon_data["type2"] == local_poke_data[poke_data.evolves_from[mon]]["type2"]: + mon_data["tms"] = local_poke_data[poke_data.evolves_from[mon]]["tms"] + elif mon != "Mew": + tms_hms = poke_data.tm_moves + poke_data.hm_moves + for flag, tm_move in enumerate(tms_hms): + if (flag < 50 and self.world.tm_compatibility[self.player].value == 1) or (flag >= 50 and self.world.hm_compatibility[self.player].value == 1): + type_match = poke_data.moves[tm_move]["type"] in [mon_data["type1"], mon_data["type2"]] + bit = int(self.world.random.randint(1, 100) < [[90, 50, 25], [100, 75, 25]][flag >= 50][0 if type_match else 1 if poke_data.moves[tm_move]["type"] == "Normal" else 2]) + elif (flag < 50 and self.world.tm_compatibility[self.player].value == 2) or (flag >= 50 and self.world.hm_compatibility[self.player].value == 2): + bit = [0, 1][self.world.random.randint(0, 1)] + elif (flag < 50 and self.world.tm_compatibility[self.player].value == 3) or (flag >= 50 and self.world.hm_compatibility[self.player].value == 3): + bit = 1 + else: + continue + if bit: + mon_data["tms"][int(flag / 8)] |= 1 << (flag % 8) + else: + mon_data["tms"][int(flag / 8)] &= ~(1 << (flag % 8)) + + self.local_poke_data = local_poke_data + self.learnsets = learnsets + + + +def generate_output(self, output_directory: str): + random = self.world.slot_seeds[self.player] + game_version = self.world.game_version[self.player].current_key + data = bytearray(get_base_rom_bytes(game_version)) + + basemd5 = hashlib.md5() + basemd5.update(data) + + for location in self.world.get_locations(): + if location.player != self.player or location.rom_address is None: + continue + if location.item and location.item.player == self.player: + if location.rom_address: + rom_address = location.rom_address + if not isinstance(rom_address, list): + rom_address = [rom_address] + for address in rom_address: + if location.item.name in poke_data.pokemon_data.keys(): + data[address] = poke_data.pokemon_data[location.item.name]["id"] + elif " ".join(location.item.name.split()[1:]) in poke_data.pokemon_data.keys(): + data[address] = poke_data.pokemon_data[" ".join(location.item.name.split()[1:])]["id"] + else: + data[address] = self.item_name_to_id[location.item.name] - 172000000 + + else: + data[location.rom_address] = 0x2C # AP Item + data[rom_addresses['Fly_Location']] = self.fly_map_code + + if self.world.tea[self.player].value: + data[rom_addresses["Option_Tea"]] = 1 + data[rom_addresses["Guard_Drink_List"]] = 0x54 + data[rom_addresses["Guard_Drink_List"] + 1] = 0 + data[rom_addresses["Guard_Drink_List"] + 2] = 0 + + if self.world.extra_key_items[self.player].value: + data[rom_addresses['Options']] |= 4 + data[rom_addresses["Option_Blind_Trainers"]] = round(self.world.blind_trainers[self.player].value * 2.55) + data[rom_addresses['Option_Cerulean_Cave_Condition']] = self.world.cerulean_cave_condition[self.player].value + data[rom_addresses['Option_Encounter_Minimum_Steps']] = self.world.minimum_steps_between_encounters[self.player].value + data[rom_addresses['Option_Victory_Road_Badges']] = self.world.victory_road_condition[self.player].value + data[rom_addresses['Option_Pokemon_League_Badges']] = self.world.elite_four_condition[self.player].value + data[rom_addresses['Option_Viridian_Gym_Badges']] = self.world.viridian_gym_condition[self.player].value + data[rom_addresses['Option_EXP_Modifier']] = self.world.exp_modifier[self.player].value + if not self.world.require_item_finder[self.player].value: + data[rom_addresses['Option_Itemfinder']] = 0 + if self.world.extra_strength_boulders[self.player].value: + for i in range(0, 3): + data[rom_addresses['Option_Boulders'] + (i * 3)] = 0x15 + if self.world.extra_key_items[self.player].value: + for i in range(0, 4): + data[rom_addresses['Option_Rock_Tunnel_Extra_Items'] + (i * 3)] = 0x15 + if self.world.old_man[self.player].value == 2: + data[rom_addresses['Option_Old_Man']] = 0x11 + data[rom_addresses['Option_Old_Man_Lying']] = 0x15 + money = str(self.world.starting_money[self.player].value) + while len(money) < 6: + money = "0" + money + data[rom_addresses["Starting_Money_High"]] = int(money[:2], 16) + data[rom_addresses["Starting_Money_Middle"]] = int(money[2:4], 16) + data[rom_addresses["Starting_Money_Low"]] = int(money[4:], 16) + data[rom_addresses["Text_Badges_Needed"]] = encode_text( + str(max(self.world.victory_road_condition[self.player].value, + self.world.elite_four_condition[self.player].value)))[0] + if self.world.badges_needed_for_hm_moves[self.player].value == 0: + for hm_move in poke_data.hm_moves: + write_bytes(data, bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + rom_addresses["HM_" + hm_move + "_Badge_a"]) + elif self.extra_badges: + written_badges = {} + for hm_move, badge in self.extra_badges.items(): + data[rom_addresses["HM_" + hm_move + "_Badge_b"]] = {"Boulder Badge": 0x47, "Cascade Badge": 0x4F, + "Thunder Badge": 0x57, "Rainbow Badge": 0x5F, + "Soul Badge": 0x67, "Marsh Badge": 0x6F, + "Volcano Badge": 0x77, "Earth Badge": 0x7F}[badge] + move_text = hm_move + if badge not in ["Marsh Badge", "Volcano Badge", "Earth Badge"]: + move_text = ", " + move_text + rom_address = rom_addresses["Badge_Text_" + badge.replace(" ", "_")] + if badge in written_badges: + rom_address += len(written_badges[badge]) + move_text = ", " + move_text + write_bytes(data, encode_text(move_text.upper()), rom_address) + written_badges[badge] = move_text + for badge in ["Marsh Badge", "Volcano Badge", "Earth Badge"]: + if badge not in written_badges: + write_bytes(data, encode_text("Nothing"), rom_addresses["Badge_Text_" + badge.replace(" ", "_")]) + + chart = deepcopy(poke_data.type_chart) + if self.world.randomize_type_matchup_types[self.player].value == 1: + attacking_types = [] + defending_types = [] + for matchup in chart: + attacking_types.append(matchup[0]) + defending_types.append(matchup[1]) + random.shuffle(attacking_types) + random.shuffle(defending_types) + matchups = [] + while len(attacking_types) > 0: + if [attacking_types[0], defending_types[0]] not in matchups: + matchups.append([attacking_types.pop(0), defending_types.pop(0)]) + else: + matchup = matchups.pop(0) + attacking_types.append(matchup[0]) + defending_types.append(matchup[1]) + random.shuffle(attacking_types) + random.shuffle(defending_types) + for matchup, chart_row in zip(matchups, chart): + chart_row[0] = matchup[0] + chart_row[1] = matchup[1] + elif self.world.randomize_type_matchup_types[self.player].value == 2: + used_matchups = [] + for matchup in chart: + matchup[0] = random.choice(list(poke_data.type_names.values())) + matchup[1] = random.choice(list(poke_data.type_names.values())) + while [matchup[0], matchup[1]] in used_matchups: + matchup[0] = random.choice(list(poke_data.type_names.values())) + matchup[1] = random.choice(list(poke_data.type_names.values())) + used_matchups.append([matchup[0], matchup[1]]) + if self.world.randomize_type_matchup_type_effectiveness[self.player].value == 1: + effectiveness_list = [] + for matchup in chart: + effectiveness_list.append(matchup[2]) + random.shuffle(effectiveness_list) + for (matchup, effectiveness) in zip(chart, effectiveness_list): + matchup[2] = effectiveness + elif self.world.randomize_type_matchup_type_effectiveness[self.player].value == 2: + for matchup in chart: + matchup[2] = random.choice([0] + ([5, 20] * 5)) + elif self.world.randomize_type_matchup_type_effectiveness[self.player].value == 3: + for matchup in chart: + matchup[2] = random.choice([i for i in range(0, 21) if i != 10]) + type_loc = rom_addresses["Type_Chart"] + for matchup in chart: + data[type_loc] = poke_data.type_ids[matchup[0]] + data[type_loc + 1] = poke_data.type_ids[matchup[1]] + data[type_loc + 2] = matchup[2] + type_loc += 3 + # sort so that super-effective matchups occur first, to prevent dual "not very effective" / "super effective" + # matchups from leading to damage being ultimately divided by 2 and then multiplied by 2, which can lead to + # damage being reduced by 1 which leads to a "not very effective" message appearing due to my changes + # to the way effectiveness messages are generated. + self.type_chart = sorted(chart, key=lambda matchup: 0 - matchup[2]) + + if self.world.normalize_encounter_chances[self.player].value: + chances = [25, 51, 77, 103, 129, 155, 180, 205, 230, 255] + for i, chance in enumerate(chances): + data[rom_addresses['Encounter_Chances'] + (i * 2)] = chance + + for mon, mon_data in self.local_poke_data.items(): + if mon == "Mew": + address = rom_addresses["Base_Stats_Mew"] + else: + address = rom_addresses["Base_Stats"] + (28 * (mon_data["dex"] - 1)) + data[address + 1] = self.local_poke_data[mon]["hp"] + data[address + 2] = self.local_poke_data[mon]["atk"] + data[address + 3] = self.local_poke_data[mon]["def"] + data[address + 4] = self.local_poke_data[mon]["spd"] + data[address + 5] = self.local_poke_data[mon]["spc"] + data[address + 6] = poke_data.type_ids[self.local_poke_data[mon]["type1"]] + data[address + 7] = poke_data.type_ids[self.local_poke_data[mon]["type2"]] + data[address + 8] = self.local_poke_data[mon]["catch rate"] + data[address + 15] = poke_data.moves[self.local_poke_data[mon]["start move 1"]]["id"] + data[address + 16] = poke_data.moves[self.local_poke_data[mon]["start move 2"]]["id"] + data[address + 17] = poke_data.moves[self.local_poke_data[mon]["start move 3"]]["id"] + data[address + 18] = poke_data.moves[self.local_poke_data[mon]["start move 4"]]["id"] + write_bytes(data, self.local_poke_data[mon]["tms"], address + 20) + if mon in self.learnsets: + address = rom_addresses["Learnset_" + mon.replace(" ", "")] + for i, move in enumerate(self.learnsets[mon]): + data[(address + 1) + i * 2] = poke_data.moves[move]["id"] + + data[rom_addresses["Option_Aide_Rt2"]] = self.world.oaks_aide_rt_2[self.player] + data[rom_addresses["Option_Aide_Rt11"]] = self.world.oaks_aide_rt_11[self.player] + data[rom_addresses["Option_Aide_Rt15"]] = self.world.oaks_aide_rt_15[self.player] + + if self.world.safari_zone_normal_battles[self.player].value == 1: + data[rom_addresses["Option_Safari_Zone_Battle_Type"]] = 255 + + if self.world.reusable_tms[self.player].value: + data[rom_addresses["Option_Reusable_TMs"]] = 0xC9 + + process_trainer_data(self, data) + + mons = [mon["id"] for mon in poke_data.pokemon_data.values()] + random.shuffle(mons) + data[rom_addresses['Title_Mon_First']] = mons.pop() + for mon in range(0, 16): + data[rom_addresses['Title_Mons'] + mon] = mons.pop() + if self.world.game_version[self.player].value: + mons.sort(key=lambda mon: 0 if mon == self.world.get_location("Pallet Town - Starter 1", self.player).item.name + else 1 if mon == self.world.get_location("Pallet Town - Starter 2", self.player).item.name else + 2 if mon == self.world.get_location("Pallet Town - Starter 3", self.player).item.name else 3) + else: + mons.sort(key=lambda mon: 0 if mon == self.world.get_location("Pallet Town - Starter 2", self.player).item.name + else 1 if mon == self.world.get_location("Pallet Town - Starter 1", self.player).item.name else + 2 if mon == self.world.get_location("Pallet Town - Starter 3", self.player).item.name else 3) + write_bytes(data, encode_text(self.world.seed_name, 20, True), rom_addresses['Title_Seed']) + + slot_name = self.world.player_name[self.player] + slot_name.replace("@", " ") + slot_name.replace("<", " ") + slot_name.replace(">", " ") + write_bytes(data, encode_text(slot_name, 16, True, True), rom_addresses['Title_Slot_Name']) + + write_bytes(data, self.trainer_name, rom_addresses['Player_Name']) + write_bytes(data, self.rival_name, rom_addresses['Rival_Name']) + + write_bytes(data, basemd5.digest(), 0xFFCC) + write_bytes(data, self.world.seed_name.encode(), 0xFFDC) + write_bytes(data, self.world.player_name[self.player].encode(), 0xFFF0) + + + + outfilepname = f'_P{self.player}' + outfilepname += f"_{self.world.get_file_safe_player_name(self.player).replace(' ', '_')}" \ + if self.world.player_name[self.player] != 'Player%d' % self.player else '' + rompath = os.path.join(output_directory, f'AP_{self.world.seed_name}{outfilepname}.gb') + with open(rompath, 'wb') as outfile: + outfile.write(data) + if self.world.game_version[self.player].current_key == "red": + patch = RedDeltaPatch(os.path.splitext(rompath)[0] + RedDeltaPatch.patch_file_ending, player=self.player, + player_name=self.world.player_name[self.player], patched_path=rompath) + else: + patch = BlueDeltaPatch(os.path.splitext(rompath)[0] + BlueDeltaPatch.patch_file_ending, player=self.player, + player_name=self.world.player_name[self.player], patched_path=rompath) + + patch.write() + os.unlink(rompath) + + +def write_bytes(data, byte_array, address): + for byte in byte_array: + data[address] = byte + address += 1 + + +def get_base_rom_bytes(game_version: str, hash: str="") -> bytes: + file_name = get_base_rom_path(game_version) + with open(file_name, "rb") as file: + base_rom_bytes = bytes(file.read()) + if hash: + basemd5 = hashlib.md5() + basemd5.update(base_rom_bytes) + if hash != 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') + with open(os.path.join(os.path.dirname(__file__), f'basepatch_{game_version}.bsdiff4'), 'rb') as stream: + base_patch = bytes(stream.read()) + base_rom_bytes = bsdiff4.patch(base_rom_bytes, base_patch) + return base_rom_bytes + + +def get_base_rom_path(game_version: str) -> str: + options = Utils.get_options() + file_name = options["pokemon_rb_options"][f"{game_version}_rom_file"] + if not os.path.exists(file_name): + file_name = Utils.local_path(file_name) + return file_name + + +class BlueDeltaPatch(APDeltaPatch): + patch_file_ending = ".apblue" + hash = "50927e843568814f7ed45ec4f944bd8b" + game_version = "blue" + game = "Pokemon Red and Blue" + result_file_ending = ".gb" + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_bytes(cls.game_version, cls.hash) + + +class RedDeltaPatch(APDeltaPatch): + patch_file_ending = ".apred" + hash = "3d45c1ee9abd5738df46d2bdda8b57dc" + game_version = "red" + game = "Pokemon Red and Blue" + result_file_ending = ".gb" + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_bytes(cls.game_version, cls.hash) diff --git a/worlds/pokemon_rb/rom_addresses.py b/worlds/pokemon_rb/rom_addresses.py new file mode 100644 index 0000000000..c04a75bcc4 --- /dev/null +++ b/worlds/pokemon_rb/rom_addresses.py @@ -0,0 +1,588 @@ +rom_addresses = { + "Option_Encounter_Minimum_Steps": 0x3c3, + "Option_Blind_Trainers": 0x317e, + "Base_Stats_Mew": 0x425b, + "Title_Mon_First": 0x436e, + "Title_Mons": 0x4547, + "Player_Name": 0x4569, + "Rival_Name": 0x4571, + "Title_Seed": 0x5dfe, + "Title_Slot_Name": 0x5e1e, + "PC_Item": 0x61ec, + "PC_Item_Quantity": 0x61f1, + "Options": 0x61f9, + "Fly_Location": 0x61fe, + "Option_Old_Man": 0xcaef, + "Option_Old_Man_Lying": 0xcaf2, + "Option_Boulders": 0xcd98, + "Option_Rock_Tunnel_Extra_Items": 0xcda1, + "Wild_Route1": 0xd0fb, + "Wild_Route2": 0xd111, + "Wild_Route22": 0xd127, + "Wild_ViridianForest": 0xd13d, + "Wild_Route3": 0xd153, + "Wild_MtMoon1F": 0xd169, + "Wild_MtMoonB1F": 0xd17f, + "Wild_MtMoonB2F": 0xd195, + "Wild_Route4": 0xd1ab, + "Wild_Route24": 0xd1c1, + "Wild_Route25": 0xd1d7, + "Wild_Route9": 0xd1ed, + "Wild_Route5": 0xd203, + "Wild_Route6": 0xd219, + "Wild_Route11": 0xd22f, + "Wild_RockTunnel1F": 0xd245, + "Wild_RockTunnelB1F": 0xd25b, + "Wild_Route10": 0xd271, + "Wild_Route12": 0xd287, + "Wild_Route8": 0xd29d, + "Wild_Route7": 0xd2b3, + "Wild_PokemonTower3F": 0xd2cd, + "Wild_PokemonTower4F": 0xd2e3, + "Wild_PokemonTower5F": 0xd2f9, + "Wild_PokemonTower6F": 0xd30f, + "Wild_PokemonTower7F": 0xd325, + "Wild_Route13": 0xd33b, + "Wild_Route14": 0xd351, + "Wild_Route15": 0xd367, + "Wild_Route16": 0xd37d, + "Wild_Route17": 0xd393, + "Wild_Route18": 0xd3a9, + "Wild_SafariZoneCenter": 0xd3bf, + "Wild_SafariZoneEast": 0xd3d5, + "Wild_SafariZoneNorth": 0xd3eb, + "Wild_SafariZoneWest": 0xd401, + "Wild_SeaRoutes": 0xd418, + "Wild_SeafoamIslands1F": 0xd42d, + "Wild_SeafoamIslandsB1F": 0xd443, + "Wild_SeafoamIslandsB2F": 0xd459, + "Wild_SeafoamIslandsB3F": 0xd46f, + "Wild_SeafoamIslandsB4F": 0xd485, + "Wild_PokemonMansion1F": 0xd49b, + "Wild_PokemonMansion2F": 0xd4b1, + "Wild_PokemonMansion3F": 0xd4c7, + "Wild_PokemonMansionB1F": 0xd4dd, + "Wild_Route21": 0xd4f3, + "Wild_Surf_Route21": 0xd508, + "Wild_CeruleanCave1F": 0xd51d, + "Wild_CeruleanCave2F": 0xd533, + "Wild_CeruleanCaveB1F": 0xd549, + "Wild_PowerPlant": 0xd55f, + "Wild_Route23": 0xd575, + "Wild_VictoryRoad2F": 0xd58b, + "Wild_VictoryRoad3F": 0xd5a1, + "Wild_VictoryRoad1F": 0xd5b7, + "Wild_DiglettsCave": 0xd5cd, + "Ghost_Battle5": 0xd723, + "HM_Surf_Badge_a": 0xda11, + "HM_Surf_Badge_b": 0xda16, + "Wild_Old_Rod": 0xe313, + "Wild_Good_Rod": 0xe340, + "Option_Reusable_TMs": 0xe60c, + "Wild_Super_Rod_A": 0xea40, + "Wild_Super_Rod_B": 0xea45, + "Wild_Super_Rod_C": 0xea4a, + "Wild_Super_Rod_D": 0xea51, + "Wild_Super_Rod_E": 0xea56, + "Wild_Super_Rod_F": 0xea5b, + "Wild_Super_Rod_G": 0xea64, + "Wild_Super_Rod_H": 0xea6d, + "Wild_Super_Rod_I": 0xea76, + "Wild_Super_Rod_J": 0xea7f, + "Starting_Money_High": 0xf949, + "Starting_Money_Middle": 0xf94c, + "Starting_Money_Low": 0xf94f, + "HM_Fly_Badge_a": 0x1318e, + "HM_Fly_Badge_b": 0x13193, + "HM_Cut_Badge_a": 0x131c4, + "HM_Cut_Badge_b": 0x131c9, + "HM_Strength_Badge_a": 0x131f4, + "HM_Strength_Badge_b": 0x131f9, + "HM_Flash_Badge_a": 0x13208, + "HM_Flash_Badge_b": 0x1320d, + "Encounter_Chances": 0x13911, + "Option_Viridian_Gym_Badges": 0x1901d, + "Event_Sleepy_Guy": 0x191bc, + "Starter2_K": 0x195a8, + "Starter3_K": 0x195b0, + "Event_Rocket_Thief": 0x196cc, + "Option_Cerulean_Cave_Condition": 0x1986c, + "Event_Stranded_Man": 0x19b2b, + "Event_Rivals_Sister": 0x19cf9, + "Option_Pokemon_League_Badges": 0x19e16, + "Missable_Silph_Co_4F_Item_1": 0x1a0d7, + "Missable_Silph_Co_4F_Item_2": 0x1a0de, + "Missable_Silph_Co_4F_Item_3": 0x1a0e5, + "Missable_Silph_Co_5F_Item_1": 0x1a337, + "Missable_Silph_Co_5F_Item_2": 0x1a33e, + "Missable_Silph_Co_5F_Item_3": 0x1a345, + "Missable_Silph_Co_6F_Item_1": 0x1a5ad, + "Missable_Silph_Co_6F_Item_2": 0x1a5b4, + "Event_Free_Sample": 0x1cade, + "Starter1_F": 0x1cca5, + "Starter2_F": 0x1cca9, + "Starter2_G": 0x1cde2, + "Starter3_G": 0x1cdea, + "Starter2_H": 0x1d0e5, + "Starter1_H": 0x1d0ef, + "Starter3_I": 0x1d0f6, + "Starter2_I": 0x1d100, + "Starter1_D": 0x1d107, + "Starter3_D": 0x1d111, + "Starter2_E": 0x1d2eb, + "Starter3_E": 0x1d2f3, + "Event_Oaks_Gift": 0x1d373, + "Event_Pokemart_Quest": 0x1d566, + "Event_Bicycle_Shop": 0x1d805, + "Text_Bicycle": 0x1d898, + "Event_Fuji": 0x1d9cd, + "Static_Encounter_Mew": 0x1dc4e, + "Gift_Eevee": 0x1dcc7, + "Event_Mr_Psychic": 0x1ddcf, + "Static_Encounter_Voltorb_A": 0x1e397, + "Static_Encounter_Voltorb_B": 0x1e39f, + "Static_Encounter_Voltorb_C": 0x1e3a7, + "Static_Encounter_Electrode_A": 0x1e3af, + "Static_Encounter_Voltorb_D": 0x1e3b7, + "Static_Encounter_Voltorb_E": 0x1e3bf, + "Static_Encounter_Electrode_B": 0x1e3c7, + "Static_Encounter_Voltorb_F": 0x1e3cf, + "Static_Encounter_Zapdos": 0x1e3d7, + "Missable_Power_Plant_Item_1": 0x1e3df, + "Missable_Power_Plant_Item_2": 0x1e3e6, + "Missable_Power_Plant_Item_3": 0x1e3ed, + "Missable_Power_Plant_Item_4": 0x1e3f4, + "Missable_Power_Plant_Item_5": 0x1e3fb, + "Event_Rt16_House_Woman": 0x1e5d4, + "Option_Victory_Road_Badges": 0x1e6a5, + "Event_Bill": 0x1e8d6, + "Starter1_O": 0x372b0, + "Starter2_O": 0x372b4, + "Starter3_O": 0x372b8, + "Base_Stats": 0x383de, + "Starter3_C": 0x39cf2, + "Starter1_C": 0x39cf8, + "Trainer_Data": 0x39d99, + "Rival_Starter2_A": 0x3a1e5, + "Rival_Starter3_A": 0x3a1e8, + "Rival_Starter1_A": 0x3a1eb, + "Rival_Starter2_B": 0x3a1f1, + "Rival_Starter3_B": 0x3a1f7, + "Rival_Starter1_B": 0x3a1fd, + "Rival_Starter2_C": 0x3a207, + "Rival_Starter3_C": 0x3a211, + "Rival_Starter1_C": 0x3a21b, + "Rival_Starter2_D": 0x3a409, + "Rival_Starter3_D": 0x3a413, + "Rival_Starter1_D": 0x3a41d, + "Rival_Starter2_E": 0x3a429, + "Rival_Starter3_E": 0x3a435, + "Rival_Starter1_E": 0x3a441, + "Rival_Starter2_F": 0x3a44d, + "Rival_Starter3_F": 0x3a459, + "Rival_Starter1_F": 0x3a465, + "Rival_Starter2_G": 0x3a473, + "Rival_Starter3_G": 0x3a481, + "Rival_Starter1_G": 0x3a48f, + "Rival_Starter2_H": 0x3a49d, + "Rival_Starter3_H": 0x3a4ab, + "Rival_Starter1_H": 0x3a4b9, + "Trainer_Data_End": 0x3a52e, + "Learnset_Rhydon": 0x3b1d9, + "Learnset_Kangaskhan": 0x3b1e7, + "Learnset_NidoranM": 0x3b1f6, + "Learnset_Clefairy": 0x3b208, + "Learnset_Spearow": 0x3b219, + "Learnset_Voltorb": 0x3b228, + "Learnset_Nidoking": 0x3b234, + "Learnset_Slowbro": 0x3b23c, + "Learnset_Ivysaur": 0x3b24f, + "Learnset_Exeggutor": 0x3b25f, + "Learnset_Lickitung": 0x3b263, + "Learnset_Exeggcute": 0x3b273, + "Learnset_Grimer": 0x3b284, + "Learnset_Gengar": 0x3b292, + "Learnset_NidoranF": 0x3b29b, + "Learnset_Nidoqueen": 0x3b2a9, + "Learnset_Cubone": 0x3b2b4, + "Learnset_Rhyhorn": 0x3b2c3, + "Learnset_Lapras": 0x3b2d1, + "Learnset_Mew": 0x3b2e1, + "Learnset_Gyarados": 0x3b2eb, + "Learnset_Shellder": 0x3b2fb, + "Learnset_Tentacool": 0x3b30a, + "Learnset_Gastly": 0x3b31f, + "Learnset_Scyther": 0x3b325, + "Learnset_Staryu": 0x3b337, + "Learnset_Blastoise": 0x3b347, + "Learnset_Pinsir": 0x3b355, + "Learnset_Tangela": 0x3b363, + "Learnset_Growlithe": 0x3b379, + "Learnset_Onix": 0x3b385, + "Learnset_Fearow": 0x3b391, + "Learnset_Pidgey": 0x3b3a0, + "Learnset_Slowpoke": 0x3b3b1, + "Learnset_Kadabra": 0x3b3c9, + "Learnset_Graveler": 0x3b3e1, + "Learnset_Chansey": 0x3b3ef, + "Learnset_Machoke": 0x3b407, + "Learnset_MrMime": 0x3b413, + "Learnset_Hitmonlee": 0x3b41f, + "Learnset_Hitmonchan": 0x3b42b, + "Learnset_Arbok": 0x3b437, + "Learnset_Parasect": 0x3b443, + "Learnset_Psyduck": 0x3b452, + "Learnset_Drowzee": 0x3b461, + "Learnset_Golem": 0x3b46f, + "Learnset_Magmar": 0x3b47f, + "Learnset_Electabuzz": 0x3b48f, + "Learnset_Magneton": 0x3b49b, + "Learnset_Koffing": 0x3b4ac, + "Learnset_Mankey": 0x3b4bd, + "Learnset_Seel": 0x3b4cc, + "Learnset_Diglett": 0x3b4db, + "Learnset_Tauros": 0x3b4e7, + "Learnset_Farfetchd": 0x3b4f9, + "Learnset_Venonat": 0x3b508, + "Learnset_Dragonite": 0x3b516, + "Learnset_Doduo": 0x3b52b, + "Learnset_Poliwag": 0x3b53c, + "Learnset_Jynx": 0x3b54a, + "Learnset_Moltres": 0x3b558, + "Learnset_Articuno": 0x3b560, + "Learnset_Zapdos": 0x3b568, + "Learnset_Meowth": 0x3b575, + "Learnset_Krabby": 0x3b584, + "Learnset_Vulpix": 0x3b59a, + "Learnset_Pikachu": 0x3b5ac, + "Learnset_Dratini": 0x3b5c1, + "Learnset_Dragonair": 0x3b5d0, + "Learnset_Kabuto": 0x3b5df, + "Learnset_Kabutops": 0x3b5e9, + "Learnset_Horsea": 0x3b5f6, + "Learnset_Seadra": 0x3b602, + "Learnset_Sandshrew": 0x3b615, + "Learnset_Sandslash": 0x3b621, + "Learnset_Omanyte": 0x3b630, + "Learnset_Omastar": 0x3b63a, + "Learnset_Jigglypuff": 0x3b648, + "Learnset_Eevee": 0x3b666, + "Learnset_Flareon": 0x3b670, + "Learnset_Jolteon": 0x3b682, + "Learnset_Vaporeon": 0x3b694, + "Learnset_Machop": 0x3b6a9, + "Learnset_Zubat": 0x3b6b8, + "Learnset_Ekans": 0x3b6c7, + "Learnset_Paras": 0x3b6d6, + "Learnset_Poliwhirl": 0x3b6e6, + "Learnset_Poliwrath": 0x3b6f4, + "Learnset_Beedrill": 0x3b704, + "Learnset_Dodrio": 0x3b714, + "Learnset_Primeape": 0x3b722, + "Learnset_Dugtrio": 0x3b72e, + "Learnset_Venomoth": 0x3b73a, + "Learnset_Dewgong": 0x3b748, + "Learnset_Butterfree": 0x3b762, + "Learnset_Machamp": 0x3b772, + "Learnset_Golduck": 0x3b780, + "Learnset_Hypno": 0x3b78c, + "Learnset_Golbat": 0x3b79a, + "Learnset_Mewtwo": 0x3b7a6, + "Learnset_Snorlax": 0x3b7b2, + "Learnset_Magikarp": 0x3b7bf, + "Learnset_Muk": 0x3b7c7, + "Learnset_Kingler": 0x3b7d7, + "Learnset_Cloyster": 0x3b7e3, + "Learnset_Electrode": 0x3b7e9, + "Learnset_Weezing": 0x3b7f7, + "Learnset_Persian": 0x3b803, + "Learnset_Marowak": 0x3b80f, + "Learnset_Haunter": 0x3b827, + "Learnset_Alakazam": 0x3b832, + "Learnset_Pidgeotto": 0x3b843, + "Learnset_Pidgeot": 0x3b851, + "Learnset_Bulbasaur": 0x3b864, + "Learnset_Venusaur": 0x3b874, + "Learnset_Tentacruel": 0x3b884, + "Learnset_Goldeen": 0x3b89b, + "Learnset_Seaking": 0x3b8a9, + "Learnset_Ponyta": 0x3b8c2, + "Learnset_Rapidash": 0x3b8d0, + "Learnset_Rattata": 0x3b8e1, + "Learnset_Raticate": 0x3b8eb, + "Learnset_Nidorino": 0x3b8f9, + "Learnset_Nidorina": 0x3b90b, + "Learnset_Geodude": 0x3b91c, + "Learnset_Porygon": 0x3b92a, + "Learnset_Aerodactyl": 0x3b934, + "Learnset_Magnemite": 0x3b942, + "Learnset_Charmander": 0x3b957, + "Learnset_Squirtle": 0x3b968, + "Learnset_Charmeleon": 0x3b979, + "Learnset_Wartortle": 0x3b98a, + "Learnset_Charizard": 0x3b998, + "Learnset_Oddish": 0x3b9b1, + "Learnset_Gloom": 0x3b9c3, + "Learnset_Vileplume": 0x3b9d1, + "Learnset_Bellsprout": 0x3b9dc, + "Learnset_Weepinbell": 0x3b9f0, + "Learnset_Victreebel": 0x3ba00, + "Type_Chart": 0x3e4b6, + "Type_Chart_Divider": 0x3e5ac, + "Ghost_Battle3": 0x3efd9, + "Missable_Pokemon_Mansion_1F_Item_1": 0x443d6, + "Missable_Pokemon_Mansion_1F_Item_2": 0x443dd, + "Map_Rock_TunnelF": 0x44676, + "Missable_Victory_Road_3F_Item_1": 0x44b07, + "Missable_Victory_Road_3F_Item_2": 0x44b0e, + "Missable_Rocket_Hideout_B1F_Item_1": 0x44d2d, + "Missable_Rocket_Hideout_B1F_Item_2": 0x44d34, + "Missable_Rocket_Hideout_B2F_Item_1": 0x4511d, + "Missable_Rocket_Hideout_B2F_Item_2": 0x45124, + "Missable_Rocket_Hideout_B2F_Item_3": 0x4512b, + "Missable_Rocket_Hideout_B2F_Item_4": 0x45132, + "Missable_Rocket_Hideout_B3F_Item_1": 0x4536f, + "Missable_Rocket_Hideout_B3F_Item_2": 0x45376, + "Missable_Rocket_Hideout_B4F_Item_1": 0x45627, + "Missable_Rocket_Hideout_B4F_Item_2": 0x4562e, + "Missable_Rocket_Hideout_B4F_Item_3": 0x45635, + "Missable_Rocket_Hideout_B4F_Item_4": 0x4563c, + "Missable_Rocket_Hideout_B4F_Item_5": 0x45643, + "Missable_Safari_Zone_East_Item_1": 0x458b2, + "Missable_Safari_Zone_East_Item_2": 0x458b9, + "Missable_Safari_Zone_East_Item_3": 0x458c0, + "Missable_Safari_Zone_East_Item_4": 0x458c7, + "Missable_Safari_Zone_North_Item_1": 0x45a12, + "Missable_Safari_Zone_North_Item_2": 0x45a19, + "Missable_Safari_Zone_Center_Item": 0x45bf9, + "Missable_Cerulean_Cave_2F_Item_1": 0x45e36, + "Missable_Cerulean_Cave_2F_Item_2": 0x45e3d, + "Missable_Cerulean_Cave_2F_Item_3": 0x45e44, + "Static_Encounter_Mewtwo": 0x45f44, + "Missable_Cerulean_Cave_B1F_Item_1": 0x45f4c, + "Missable_Cerulean_Cave_B1F_Item_2": 0x45f53, + "Missable_Rock_Tunnel_B1F_Item_1": 0x4619f, + "Missable_Rock_Tunnel_B1F_Item_2": 0x461a6, + "Missable_Rock_Tunnel_B1F_Item_3": 0x461ad, + "Missable_Rock_Tunnel_B1F_Item_4": 0x461b4, + "Static_Encounter_Articuno": 0x4690c, + "Hidden_Item_Viridian_Forest_1": 0x46e6d, + "Hidden_Item_Viridian_Forest_2": 0x46e73, + "Hidden_Item_MtMoonB2F_1": 0x46e7a, + "Hidden_Item_MtMoonB2F_2": 0x46e80, + "Hidden_Item_Route_25_1": 0x46e94, + "Hidden_Item_Route_25_2": 0x46e9a, + "Hidden_Item_Route_9": 0x46ea1, + "Hidden_Item_SS_Anne_Kitchen": 0x46eb4, + "Hidden_Item_SS_Anne_B1F": 0x46ebb, + "Hidden_Item_Route_10_1": 0x46ec2, + "Hidden_Item_Route_10_2": 0x46ec8, + "Hidden_Item_Rocket_Hideout_B1F": 0x46ecf, + "Hidden_Item_Rocket_Hideout_B3F": 0x46ed6, + "Hidden_Item_Rocket_Hideout_B4F": 0x46edd, + "Hidden_Item_Pokemon_Tower_5F": 0x46ef1, + "Hidden_Item_Route_13_1": 0x46ef8, + "Hidden_Item_Route_13_2": 0x46efe, + "Hidden_Item_Safari_Zone_West": 0x46f0c, + "Hidden_Item_Silph_Co_5F": 0x46f13, + "Hidden_Item_Silph_Co_9F": 0x46f1a, + "Hidden_Item_Copycats_House": 0x46f21, + "Hidden_Item_Cerulean_Cave_1F": 0x46f28, + "Hidden_Item_Cerulean_Cave_B1F": 0x46f2f, + "Hidden_Item_Power_Plant_1": 0x46f36, + "Hidden_Item_Power_Plant_2": 0x46f3c, + "Hidden_Item_Seafoam_Islands_B2F": 0x46f43, + "Hidden_Item_Seafoam_Islands_B4F": 0x46f4a, + "Hidden_Item_Pokemon_Mansion_1F": 0x46f51, + "Hidden_Item_Pokemon_Mansion_3F": 0x46f65, + "Hidden_Item_Pokemon_Mansion_B1F": 0x46f72, + "Hidden_Item_Route_23_1": 0x46f85, + "Hidden_Item_Route_23_2": 0x46f8b, + "Hidden_Item_Route_23_3": 0x46f91, + "Hidden_Item_Victory_Road_2F_1": 0x46f98, + "Hidden_Item_Victory_Road_2F_2": 0x46f9e, + "Hidden_Item_Unused_6F": 0x46fa5, + "Hidden_Item_Viridian_City": 0x46fb3, + "Hidden_Item_Route_11": 0x47060, + "Hidden_Item_Route_12": 0x47067, + "Hidden_Item_Route_17_1": 0x47075, + "Hidden_Item_Route_17_2": 0x4707b, + "Hidden_Item_Route_17_3": 0x47081, + "Hidden_Item_Route_17_4": 0x47087, + "Hidden_Item_Route_17_5": 0x4708d, + "Hidden_Item_Underground_Path_NS_1": 0x47094, + "Hidden_Item_Underground_Path_NS_2": 0x4709a, + "Hidden_Item_Underground_Path_WE_1": 0x470a1, + "Hidden_Item_Underground_Path_WE_2": 0x470a7, + "Hidden_Item_Celadon_City": 0x470ae, + "Hidden_Item_Seafoam_Islands_B3F": 0x470b5, + "Hidden_Item_Vermilion_City": 0x470bc, + "Hidden_Item_Cerulean_City": 0x470c3, + "Hidden_Item_Route_4": 0x470ca, + "Event_Counter": 0x482d3, + "Event_Thirsty_Girl_Lemonade": 0x484f9, + "Event_Thirsty_Girl_Soda": 0x4851d, + "Event_Thirsty_Girl_Water": 0x48541, + "Option_Tea": 0x4871d, + "Event_Mansion_Lady": 0x4872a, + "Badge_Celadon_Gym": 0x48a1b, + "Event_Celadon_Gym": 0x48a2f, + "Event_Gambling_Addict": 0x49293, + "Gift_Magikarp": 0x49430, + "Option_Aide_Rt11": 0x4958d, + "Event_Rt11_Oaks_Aide": 0x49591, + "Event_Mourning_Girl": 0x4968b, + "Option_Aide_Rt15": 0x49776, + "Event_Rt_15_Oaks_Aide": 0x4977a, + "Missable_Mt_Moon_1F_Item_1": 0x49c75, + "Missable_Mt_Moon_1F_Item_2": 0x49c7c, + "Missable_Mt_Moon_1F_Item_3": 0x49c83, + "Missable_Mt_Moon_1F_Item_4": 0x49c8a, + "Missable_Mt_Moon_1F_Item_5": 0x49c91, + "Missable_Mt_Moon_1F_Item_6": 0x49c98, + "Dome_Fossil_Text": 0x4a001, + "Event_Dome_Fossil": 0x4a021, + "Helix_Fossil_Text": 0x4a05d, + "Event_Helix_Fossil": 0x4a07d, + "Missable_Mt_Moon_B2F_Item_1": 0x4a166, + "Missable_Mt_Moon_B2F_Item_2": 0x4a16d, + "Missable_Safari_Zone_West_Item_1": 0x4a34f, + "Missable_Safari_Zone_West_Item_2": 0x4a356, + "Missable_Safari_Zone_West_Item_3": 0x4a35d, + "Missable_Safari_Zone_West_Item_4": 0x4a364, + "Event_Safari_Zone_Secret_House": 0x4a469, + "Missable_Route_24_Item": 0x506e6, + "Missable_Route_25_Item": 0x5080b, + "Starter2_B": 0x50fce, + "Starter3_B": 0x50fd0, + "Starter1_B": 0x50fd2, + "Starter2_A": 0x510f1, + "Starter3_A": 0x510f3, + "Starter1_A": 0x510f5, + "Option_Badge_Goal": 0x51317, + "Event_Nugget_Bridge": 0x5148f, + "Static_Encounter_Moltres": 0x51939, + "Missable_Victory_Road_2F_Item_1": 0x51941, + "Missable_Victory_Road_2F_Item_2": 0x51948, + "Missable_Victory_Road_2F_Item_3": 0x5194f, + "Missable_Victory_Road_2F_Item_4": 0x51956, + "Starter2_L": 0x51c85, + "Starter3_L": 0x51c8d, + "Gift_Lapras": 0x51d83, + "Missable_Silph_Co_7F_Item_1": 0x51f0d, + "Missable_Silph_Co_7F_Item_2": 0x51f14, + "Missable_Pokemon_Mansion_2F_Item": 0x520c9, + "Missable_Pokemon_Mansion_3F_Item_1": 0x522e2, + "Missable_Pokemon_Mansion_3F_Item_2": 0x522e9, + "Missable_Pokemon_Mansion_B1F_Item_1": 0x5248c, + "Missable_Pokemon_Mansion_B1F_Item_2": 0x52493, + "Missable_Pokemon_Mansion_B1F_Item_3": 0x5249a, + "Missable_Pokemon_Mansion_B1F_Item_4": 0x524a1, + "Missable_Pokemon_Mansion_B1F_Item_5": 0x524ae, + "Option_Safari_Zone_Battle_Type": 0x525c3, + "Prize_Mon_A2": 0x5282f, + "Prize_Mon_B2": 0x52830, + "Prize_Mon_C2": 0x52831, + "Prize_Mon_D2": 0x5283a, + "Prize_Mon_E2": 0x5283b, + "Prize_Mon_F2": 0x5283c, + "Prize_Mon_A": 0x52960, + "Prize_Mon_B": 0x52962, + "Prize_Mon_C": 0x52964, + "Prize_Mon_D": 0x52966, + "Prize_Mon_E": 0x52968, + "Prize_Mon_F": 0x5296a, + "Missable_Route_2_Item_1": 0x5404a, + "Missable_Route_2_Item_2": 0x54051, + "Missable_Route_4_Item": 0x543df, + "Missable_Route_9_Item": 0x546fd, + "Option_EXP_Modifier": 0x552c5, + "Rod_Vermilion_City_Fishing_Guru": 0x560df, + "Rod_Fuchsia_City_Fishing_Brother": 0x561eb, + "Rod_Route12_Fishing_Brother": 0x564ee, + "Missable_Route_12_Item_1": 0x58704, + "Missable_Route_12_Item_2": 0x5870b, + "Missable_Route_15_Item": 0x589c7, + "Ghost_Battle6": 0x58df0, + "Static_Encounter_Snorlax_A": 0x5969b, + "Static_Encounter_Snorlax_B": 0x599db, + "Event_Pokemon_Fan_Club": 0x59c8b, + "Event_Scared_Woman": 0x59e1f, + "Missable_Silph_Co_3F_Item": 0x5a0cb, + "Missable_Silph_Co_10F_Item_1": 0x5a281, + "Missable_Silph_Co_10F_Item_2": 0x5a288, + "Missable_Silph_Co_10F_Item_3": 0x5a28f, + "Guard_Drink_List": 0x5a600, + "Event_Museum": 0x5c266, + "Badge_Pewter_Gym": 0x5c3ed, + "Event_Pewter_Gym": 0x5c401, + "Badge_Cerulean_Gym": 0x5c716, + "Event_Cerulean_Gym": 0x5c72a, + "Badge_Vermilion_Gym": 0x5caba, + "Event_Vermillion_Gym": 0x5cace, + "Event_Copycat": 0x5cca9, + "Gift_Hitmonlee": 0x5cf1a, + "Gift_Hitmonchan": 0x5cf62, + "Badge_Saffron_Gym": 0x5d079, + "Event_Saffron_Gym": 0x5d08d, + "Option_Aide_Rt2": 0x5d5f2, + "Event_Route_2_Oaks_Aide": 0x5d5f6, + "Missable_Victory_Road_1F_Item_1": 0x5dae6, + "Missable_Victory_Road_1F_Item_2": 0x5daed, + "Starter2_J": 0x6060e, + "Starter3_J": 0x60616, + "Missable_Pokemon_Tower_3F_Item": 0x60787, + "Missable_Pokemon_Tower_4F_Item_1": 0x608b5, + "Missable_Pokemon_Tower_4F_Item_2": 0x608bc, + "Missable_Pokemon_Tower_4F_Item_3": 0x608c3, + "Missable_Pokemon_Tower_5F_Item": 0x60a80, + "Ghost_Battle1": 0x60b33, + "Ghost_Battle2": 0x60c0a, + "Missable_Pokemon_Tower_6F_Item_1": 0x60c85, + "Missable_Pokemon_Tower_6F_Item_2": 0x60c8c, + "Gift_Aerodactyl": 0x61064, + "Gift_Omanyte": 0x61068, + "Gift_Kabuto": 0x6106c, + "Missable_Viridian_Forest_Item_1": 0x6122c, + "Missable_Viridian_Forest_Item_2": 0x61233, + "Missable_Viridian_Forest_Item_3": 0x6123a, + "Starter2_M": 0x61450, + "Starter3_M": 0x61458, + "Event_SS_Anne_Captain": 0x618c3, + "Missable_SS_Anne_1F_Item": 0x61ac0, + "Missable_SS_Anne_2F_Item_1": 0x61ced, + "Missable_SS_Anne_2F_Item_2": 0x61d00, + "Missable_SS_Anne_B1F_Item_1": 0x61ee3, + "Missable_SS_Anne_B1F_Item_2": 0x61eea, + "Missable_SS_Anne_B1F_Item_3": 0x61ef1, + "Event_Silph_Co_President": 0x622ed, + "Ghost_Battle4": 0x708e1, + "Badge_Viridian_Gym": 0x749ca, + "Event_Viridian_Gym": 0x749de, + "Missable_Viridian_Gym_Item": 0x74c63, + "Missable_Cerulean_Cave_1F_Item_1": 0x74d68, + "Missable_Cerulean_Cave_1F_Item_2": 0x74d6f, + "Missable_Cerulean_Cave_1F_Item_3": 0x74d76, + "Event_Warden": 0x7512a, + "Missable_Wardens_House_Item": 0x751b7, + "Badge_Fuchsia_Gym": 0x755cd, + "Event_Fuschia_Gym": 0x755e1, + "Badge_Cinnabar_Gym": 0x75995, + "Event_Cinnabar_Gym": 0x759a9, + "Event_Lab_Scientist": 0x75dd6, + "Fossils_Needed_For_Second_Item": 0x75ea3, + "Event_Dome_Fossil_B": 0x75f20, + "Event_Helix_Fossil_B": 0x75f40, + "Starter2_N": 0x76169, + "Starter3_N": 0x76171, + "Option_Itemfinder": 0x76864, + "Text_Badges_Needed": 0x92304, + "Badge_Text_Boulder_Badge": 0x990b3, + "Badge_Text_Cascade_Badge": 0x990cb, + "Badge_Text_Thunder_Badge": 0x99111, + "Badge_Text_Rainbow_Badge": 0x9912e, + "Badge_Text_Soul_Badge": 0x99177, + "Badge_Text_Marsh_Badge": 0x9918c, + "Badge_Text_Volcano_Badge": 0x991d6, + "Badge_Text_Earth_Badge": 0x991f3, +} diff --git a/worlds/pokemon_rb/rules.py b/worlds/pokemon_rb/rules.py new file mode 100644 index 0000000000..0e97b705db --- /dev/null +++ b/worlds/pokemon_rb/rules.py @@ -0,0 +1,165 @@ +from ..generic.Rules import add_item_rule, add_rule + +def set_rules(world, player): + + add_item_rule(world.get_location("Pallet Town - Player's PC", player), + lambda i: i.player == player and "Badge" not in i.name) + + access_rules = { + + "Pallet Town - Rival's Sister": lambda state: state.has("Oak's Parcel", player), + "Pallet Town - Oak's Post-Route-22-Rival Gift": lambda state: state.has("Oak's Parcel", player), + "Viridian City - Sleepy Guy": lambda state: state.pokemon_rb_can_cut(player) or state.pokemon_rb_can_surf(player), + "Route 2 - Oak's Aide": lambda state: state.pokemon_rb_has_pokemon(state.world.oaks_aide_rt_2[player].value + 5, player), + "Pewter City - Museum": lambda state: state.pokemon_rb_can_cut(player), + "Cerulean City - Bicycle Shop": lambda state: state.has("Bike Voucher", player), + "Lavender Town - Mr. Fuji": lambda state: state.has("Fuji Saved", player), + "Vermilion Gym - Lt. Surge 1": lambda state: state.pokemon_rb_can_cut(player or state.pokemon_rb_can_surf(player)), + "Vermilion Gym - Lt. Surge 2": lambda state: state.pokemon_rb_can_cut(player or state.pokemon_rb_can_surf(player)), + "Route 11 - Oak's Aide": lambda state: state.pokemon_rb_has_pokemon(state.world.oaks_aide_rt_11[player].value + 5, player), + "Celadon City - Stranded Man": lambda state: state.pokemon_rb_can_surf(player), + "Silph Co 11F - Silph Co President": lambda state: state.has("Card Key", player), + "Fuchsia City - Safari Zone Warden": lambda state: state.has("Gold Teeth", player), + "Route 12 - Island Item": lambda state: state.pokemon_rb_can_surf(player), + "Route 12 - Item Behind Cuttable Tree": lambda state: state.pokemon_rb_can_cut(player), + "Route 15 - Item": lambda state: state.pokemon_rb_can_cut(player), + "Route 25 - Item": lambda state: state.pokemon_rb_can_cut(player), + "Fuchsia City - Warden's House Item": lambda state: state.pokemon_rb_can_strength(player), + "Rocket Hideout B4F - Southwest Item (Lift Key)": lambda state: state.has("Lift Key", player), + "Rocket Hideout B4F - Giovanni Item (Lift Key)": lambda state: state.has("Lift Key", player), + "Silph Co 3F - Item (Card Key)": lambda state: state.has("Card Key", player), + "Silph Co 4F - Left Item (Card Key)": lambda state: state.has("Card Key", player), + "Silph Co 4F - Middle Item (Card Key)": lambda state: state.has("Card Key", player), + "Silph Co 4F - Right Item (Card Key)": lambda state: state.has("Card Key", player), + "Silph Co 5F - Northwest Item (Card Key)": lambda state: state.has("Card Key", player), + "Silph Co 6F - West Item (Card Key)": lambda state: state.has("Card Key", player), + "Silph Co 6F - Southwest Item (Card Key)": lambda state: state.has("Card Key", player), + "Silph Co 7F - East Item (Card Key)": lambda state: state.has("Card Key", player), + "Safari Zone Center - Island Item": lambda state: state.pokemon_rb_can_surf(player), + + "Silph Co 11F - Silph Co Liberated": lambda state: state.has("Card Key", player), + + "Pallet Town - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player), + "Pallet Town - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player), + "Route 22 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player), + "Route 22 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player), + "Route 24 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player), + "Route 24 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player), + "Route 24 - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player), + "Route 6 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player), + "Route 6 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player), + "Route 10 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player), + "Route 10 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player), + "Safari Zone Center - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player), + "Safari Zone Center - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player), + "Safari Zone Center - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player), + "Safari Zone Center - Super Rod Pokemon - 4": lambda state: state.has("Super Rod", player), + "Route 12 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player), + "Route 12 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player), + "Route 12 - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player), + "Route 12 - Super Rod Pokemon - 4": lambda state: state.has("Super Rod", player), + "Route 19 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player), + "Route 19 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player), + "Route 19 - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player), + "Route 19 - Super Rod Pokemon - 4": lambda state: state.has("Super Rod", player), + "Route 23 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player), + "Route 23 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player), + "Route 23 - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player), + "Route 23 - Super Rod Pokemon - 4": lambda state: state.has("Super Rod", player), + "Fuchsia City - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player), + "Fuchsia City - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player), + "Fuchsia City - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player), + "Fuchsia City - Super Rod Pokemon - 4": lambda state: state.has("Super Rod", player), + "Anywhere - Good Rod Pokemon - 1": lambda state: state.has("Good Rod", player), + "Anywhere - Good Rod Pokemon - 2": lambda state: state.has("Good Rod", player), + "Anywhere - Old Rod Pokemon": lambda state: state.has("Old Rod", player), + "Celadon Prize Corner - Pokemon Prize - 1": lambda state: state.has("Coin Case", player), + "Celadon Prize Corner - Pokemon Prize - 2": lambda state: state.has("Coin Case", player), + "Celadon Prize Corner - Pokemon Prize - 3": lambda state: state.has("Coin Case", player), + "Celadon Prize Corner - Pokemon Prize - 4": lambda state: state.has("Coin Case", player), + "Celadon Prize Corner - Pokemon Prize - 5": lambda state: state.has("Coin Case", player), + "Celadon Prize Corner - Pokemon Prize - 6": lambda state: state.has("Coin Case", player), + "Cinnabar Island - Old Amber Pokemon": lambda state: state.has("Old Amber", player), + "Cinnabar Island - Helix Fossil Pokemon": lambda state: state.has("Helix Fossil", player), + "Cinnabar Island - Dome Fossil Pokemon": lambda state: state.has("Dome Fossil", player), + "Route 12 - Sleeping Pokemon": lambda state: state.has("Poke Flute", player), + "Route 16 - Sleeping Pokemon": lambda state: state.has("Poke Flute", player), + "Seafoam Islands B4F - Legendary Pokemon": lambda state: state.pokemon_rb_can_strength(player), + "Vermilion City - Legendary Pokemon": lambda state: state.pokemon_rb_can_surf(player) and state.has("S.S. Ticket", player) + } + + hidden_item_access_rules = { + "Viridian Forest - Hidden Item Northwest by Trainer": lambda state: state.pokemon_rb_can_get_hidden_items( + player), + "Viridian Forest - Hidden Item Entrance Tree": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Mt Moon B2F - Hidden Item Dead End Before Fossils": lambda state: state.pokemon_rb_can_get_hidden_items( + player), + "Route 25 - Hidden Item Fence Outside Bill's House": lambda state: state.pokemon_rb_can_get_hidden_items( + player), + "Route 9 - Hidden Item Rock By Grass": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "S.S. Anne 1F - Hidden Item Kitchen Trash": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "S.S. Anne B1F - Hidden Item Under Pillow": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Route 10 - Hidden Item Behind Rock Tunnel Entrance Tree": lambda + state: state.pokemon_rb_can_get_hidden_items(player), + "Route 10 - Hidden Item Rock": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Rocket Hideout B1F - Hidden Item Pot Plant": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Rocket Hideout B3F - Hidden Item Near East Item": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Rocket Hideout B4F - Hidden Item Behind Giovanni": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Pokemon Tower 5F - Hidden Item Near West Staircase": lambda state: state.pokemon_rb_can_get_hidden_items( + player), + "Route 13 - Hidden Item Dead End Boulder": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Route 13 - Hidden Item Dead End By Water Corner": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Pokemon Mansion B1F - Hidden Item Secret Key Room Corner": lambda state: state.pokemon_rb_can_get_hidden_items( + player), + "Safari Zone West - Hidden Item Secret House Statue": lambda state: state.pokemon_rb_can_get_hidden_items( + player), + "Silph Co 5F - Hidden Item Pot Plant": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Silph Co 9F - Hidden Item Nurse Bed": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Copycat's House - Hidden Item Desk": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Cerulean Cave 1F - Hidden Item Center Rocks": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Cerulean Cave B1F - Hidden Item Northeast Rocks": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Power Plant - Hidden Item Central Dead End": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Power Plant - Hidden Item Before Zapdos": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Seafoam Islands B2F - Hidden Item Rock": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Seafoam Islands B4F - Hidden Item Corner Island": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Pokemon Mansion 1F - Hidden Item Block Near Entrance Carpet": lambda + state: state.pokemon_rb_can_get_hidden_items(player), + "Pokemon Mansion 3F - Hidden Item Behind Burglar": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Route 23 - Hidden Item Rocks Before Final Guard": lambda state: state.pokemon_rb_can_get_hidden_items( + player), + "Route 23 - Hidden Item East Tree After Water": lambda state: state.pokemon_rb_can_get_hidden_items( + player), + "Route 23 - Hidden Item On Island": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Victory Road 2F - Hidden Item Rock Before Moltres": lambda state: state.pokemon_rb_can_get_hidden_items( + player), + "Victory Road 2F - Hidden Item Rock In Final Room": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Viridian City - Hidden Item Cuttable Tree": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Route 11 - Hidden Item Isolated Tree Near Gate": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Route 12 - Hidden Item Tree Near Gate": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Route 17 - Hidden Item In Grass": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Route 17 - Hidden Item Near Northernmost Sign": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Route 17 - Hidden Item East Center": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Route 17 - Hidden Item West Center": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Route 17 - Hidden Item Before Final Bridge": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Underground Tunnel North-South - Hidden Item Near Northern Stairs": lambda + state: state.pokemon_rb_can_get_hidden_items(player), + "Underground Tunnel North-South - Hidden Item Near Southern Stairs": lambda + state: state.pokemon_rb_can_get_hidden_items(player), + "Underground Tunnel West-East - Hidden Item West": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Underground Tunnel West-East - Hidden Item East": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Celadon City - Hidden Item Dead End Near Cuttable Tree": lambda state: state.pokemon_rb_can_get_hidden_items( + player), + "Route 25 - Hidden Item Northeast Of Grass": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Mt Moon B2F - Hidden Item Lone Rock": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Seafoam Islands B3F - Hidden Item Rock": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Vermilion City - Hidden Item In Water Near Fan Club": lambda state: state.pokemon_rb_can_get_hidden_items( + player), + "Cerulean City - Hidden Item Gym Badge Guy's Backyard": lambda state: state.pokemon_rb_can_get_hidden_items( + player), + "Route 4 - Hidden Item Plateau East Of Mt Moon": lambda state: state.pokemon_rb_can_get_hidden_items(player), + } + for loc, rule in access_rules.items(): + add_rule(world.get_location(loc, player), rule) + if world.randomize_hidden_items[player].value != 0: + for loc, rule in hidden_item_access_rules.items(): + add_rule(world.get_location(loc, player), rule) diff --git a/worlds/pokemon_rb/text.py b/worlds/pokemon_rb/text.py new file mode 100644 index 0000000000..e15623d4b8 --- /dev/null +++ b/worlds/pokemon_rb/text.py @@ -0,0 +1,147 @@ +special_chars = { + "PKMN": 0x4A, + "'d": 0xBB, + "'l": 0xBC, + "'t": 0xBE, + "'v": 0xBF, + "PK": 0xE1, + "MN": 0xE2, + "'r": 0xE4, + "'m": 0xE5, + "MALE": 0xEF, + "FEMALE": 0xF5, +} + +char_map = { + "@": 0x50, # String terminator + "#": 0x54, # Poké + "‘": 0x70, + "’": 0x71, + "“": 0x72, + "”": 0x73, + "·": 0x74, + "…": 0x75, + " ": 0x7F, + "A": 0x80, + "B": 0x81, + "C": 0x82, + "D": 0x83, + "E": 0x84, + "F": 0x85, + "G": 0x86, + "H": 0x87, + "I": 0x88, + "J": 0x89, + "K": 0x8A, + "L": 0x8B, + "M": 0x8C, + "N": 0x8D, + "O": 0x8E, + "P": 0x8F, + "Q": 0x90, + "R": 0x91, + "S": 0x92, + "T": 0x93, + "U": 0x94, + "V": 0x95, + "W": 0x96, + "X": 0x97, + "Y": 0x98, + "Z": 0x99, + "(": 0x9A, + ")": 0x9B, + ":": 0x9C, + ";": 0x9D, + "[": 0x9E, + "]": 0x9F, + "a": 0xA0, + "b": 0xA1, + "c": 0xA2, + "d": 0xA3, + "e": 0xA4, + "f": 0xA5, + "g": 0xA6, + "h": 0xA7, + "i": 0xA8, + "j": 0xA9, + "k": 0xAA, + "l": 0xAB, + "m": 0xAC, + "n": 0xAD, + "o": 0xAE, + "p": 0xAF, + "q": 0xB0, + "r": 0xB1, + "s": 0xB2, + "t": 0xB3, + "u": 0xB4, + "v": 0xB5, + "w": 0xB6, + "x": 0xB7, + "y": 0xB8, + "z": 0xB9, + "é": 0xBA, + "'": 0xE0, + "-": 0xE3, + "?": 0xE6, + "!": 0xE7, + ".": 0xE8, + "♂": 0xEF, + "¥": 0xF0, + "$": 0xF0, + "×": 0xF1, + "/": 0xF3, + ",": 0xF4, + "♀": 0xF5, + "0": 0xF6, + "1": 0xF7, + "2": 0xF8, + "3": 0xF9, + "4": 0xFA, + "5": 0xFB, + "6": 0xFC, + "7": 0xFD, + "8": 0xFE, + "9": 0xFF, +} + +unsafe_chars = ["@", "#", "PKMN"] + + +def encode_text(text: str, length: int=0, whitespace=False, force=False, safety=False): + encoded_text = bytearray() + spec_char = "" + special = False + for char in text: + if char == ">": + if spec_char in unsafe_chars and safety: + raise KeyError(f"Disallowed Pokemon text special character '<{spec_char}>'") + try: + encoded_text.append(special_chars[spec_char]) + except KeyError: + if force: + encoded_text.append(char_map[" "]) + else: + raise KeyError(f"Invalid Pokemon text special character '<{spec_char}>'") + spec_char = "" + special = False + elif char == "<": + spec_char = "" + special = True + elif special is True: + spec_char += char + else: + if char in unsafe_chars and safety: + raise KeyError(f"Disallowed Pokemon text character '{char}'") + try: + encoded_text.append(char_map[char]) + except KeyError: + if force: + encoded_text.append(char_map[" "]) + else: + raise KeyError(f"Invalid Pokemon text character '{char}'") + if length > 0: + encoded_text = encoded_text[:length] + while whitespace and len(encoded_text) < length: + encoded_text.append(char_map[" " if whitespace is True else whitespace]) + return encoded_text diff --git a/worlds/raft/Rules.py b/worlds/raft/Rules.py index 0432c2806f..a55c0ef3e8 100644 --- a/worlds/raft/Rules.py +++ b/worlds/raft/Rules.py @@ -98,37 +98,37 @@ class RaftLogic(LogicMixin): return self.raft_can_access_vasagatan(player) def raft_can_access_balboa_island(self, player): - return self.raft_can_drive(player) and self.has("Balboa Island Frequency", player) + return self.raft_can_navigate(player) and self.raft_can_drive(player) and self.has("Balboa Island Frequency", player) def raft_can_complete_balboa_island(self, player): return self.raft_can_access_balboa_island(player) and self.raft_can_craft_machete(player) def raft_can_access_caravan_island(self, player): - return self.raft_can_drive(player) and self.has("Caravan Island Frequency", player) + return self.raft_can_navigate(player) and self.raft_can_drive(player) and self.has("Caravan Island Frequency", player) def raft_can_complete_caravan_island(self, player): return self.raft_can_access_caravan_island(player) and self.raft_can_craft_ziplineTool(player) def raft_can_access_tangaroa(self, player): - return self.raft_can_drive(player) and self.has("Tangaroa Frequency", player) + return self.raft_can_navigate(player) and self.raft_can_drive(player) and self.has("Tangaroa Frequency", player) def raft_can_complete_tangaroa(self, player): return self.raft_can_access_tangaroa(player) and self.raft_can_craft_ziplineTool(player) def raft_can_access_varuna_point(self, player): - return self.raft_can_drive(player) and self.has("Varuna Point Frequency", player) + return self.raft_can_navigate(player) and self.raft_can_drive(player) and self.has("Varuna Point Frequency", player) def raft_can_complete_varuna_point(self, player): return self.raft_can_access_varuna_point(player) and self.raft_can_craft_ziplineTool(player) def raft_can_access_temperance(self, player): - return self.raft_can_drive(player) and self.has("Temperance Frequency", player) + return self.raft_can_navigate(player) and self.raft_can_drive(player) and self.has("Temperance Frequency", player) def raft_can_complete_temperance(self, player): return self.raft_can_access_temperance(player) # No zipline required on Temperance def raft_can_access_utopia(self, player): - return (self.raft_can_drive(player) + return (self.raft_can_navigate(player) and self.raft_can_drive(player) # Access checks are to prevent frequencies for other # islands from appearing in Utopia and self.raft_can_access_radio_tower(player) diff --git a/worlds/rogue_legacy/Names/__init__.py b/worlds/rogue_legacy/Names/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/worlds/rogue_legacy/__init__.py b/worlds/rogue_legacy/__init__.py index ba58e133c1..81e6202787 100644 --- a/worlds/rogue_legacy/__init__.py +++ b/worlds/rogue_legacy/__init__.py @@ -66,7 +66,7 @@ class LegacyWorld(World): def _create_items(self, name: str): data = item_table[name] - return [self.create_item(name)] * data.quantity + return [self.create_item(name) for _ in range(data.quantity)] def fill_slot_data(self) -> dict: slot_data = self._get_slot_data() @@ -89,20 +89,20 @@ class LegacyWorld(World): # Blueprints if self.world.progressive_blueprints[self.player]: - itempool += [self.create_item(ItemName.progressive_blueprints)] * 15 + itempool += [self.create_item(ItemName.progressive_blueprints) for _ in range(15)] else: for item in blueprints_table: itempool += self._create_items(item) # Check Pool settings to add a certain amount of these items. - itempool += [self.create_item(ItemName.health)] * int(self.world.health_pool[self.player]) - itempool += [self.create_item(ItemName.mana)] * int(self.world.mana_pool[self.player]) - itempool += [self.create_item(ItemName.attack)] * int(self.world.attack_pool[self.player]) - itempool += [self.create_item(ItemName.magic_damage)] * int(self.world.magic_damage_pool[self.player]) - itempool += [self.create_item(ItemName.armor)] * int(self.world.armor_pool[self.player]) - itempool += [self.create_item(ItemName.equip)] * int(self.world.equip_pool[self.player]) - itempool += [self.create_item(ItemName.crit_chance)] * int(self.world.crit_chance_pool[self.player]) - itempool += [self.create_item(ItemName.crit_damage)] * int(self.world.crit_damage_pool[self.player]) + itempool += [self.create_item(ItemName.health) for _ in range(self.world.health_pool[self.player])] + itempool += [self.create_item(ItemName.mana) for _ in range(self.world.mana_pool[self.player])] + itempool += [self.create_item(ItemName.attack) for _ in range(self.world.attack_pool[self.player])] + itempool += [self.create_item(ItemName.magic_damage) for _ in range(self.world.magic_damage_pool[self.player])] + itempool += [self.create_item(ItemName.armor) for _ in range(self.world.armor_pool[self.player])] + itempool += [self.create_item(ItemName.equip) for _ in range(self.world.equip_pool[self.player])] + itempool += [self.create_item(ItemName.crit_chance) for _ in range(self.world.crit_chance_pool[self.player])] + itempool += [self.create_item(ItemName.crit_damage) for _ in range(self.world.crit_damage_pool[self.player])] classes = self.world.available_classes[self.player] if "Dragon" in classes: @@ -153,12 +153,12 @@ class LegacyWorld(World): if self.world.architect[self.player] == "start_unlocked": self.world.push_precollected(self.world.create_item(ItemName.architect, self.player)) elif self.world.architect[self.player] != "disabled": - itempool += [self.create_item(ItemName.architect)] + itempool.append(self.create_item(ItemName.architect)) # Fill item pool with the remaining for _ in range(len(itempool), total_required_locations): item = self.world.random.choice(list(misc_items_table.keys())) - itempool += [self.create_item(item)] + itempool.append(self.create_item(item)) self.world.itempool += itempool diff --git a/worlds/sa2b/__init__.py b/worlds/sa2b/__init__.py index ea24809521..7269a66c68 100644 --- a/worlds/sa2b/__init__.py +++ b/worlds/sa2b/__init__.py @@ -86,7 +86,7 @@ class SA2BWorld(World): def _create_items(self, name: str): data = item_table[name] - return [self.create_item(name)] * data.quantity + return [self.create_item(name) for _ in range(data.quantity)] def fill_slot_data(self) -> dict: slot_data = self._get_slot_data() @@ -198,11 +198,11 @@ class SA2BWorld(World): connect_regions(self.world, self.player, gates, self.emblems_for_cannons_core, self.gate_bosses) max_required_emblems = max(max(emblem_requirement_list), self.emblems_for_cannons_core) - itempool += [self.create_item(ItemName.emblem)] * max_required_emblems + itempool += [self.create_item(ItemName.emblem) for _ in range(max_required_emblems)] non_required_emblems = (total_emblem_count - max_required_emblems) junk_count = math.floor(non_required_emblems * (self.world.junk_fill_percentage[self.player].value / 100.0)) - itempool += [self.create_item(ItemName.emblem, True)] * (non_required_emblems - junk_count) + itempool += [self.create_item(ItemName.emblem, True) for _ in range(non_required_emblems - junk_count)] # Carve Traps out of junk_count trap_weights = [] @@ -219,14 +219,14 @@ class SA2BWorld(World): junk_keys = list(junk_table.keys()) for i in range(junk_count): junk_item = self.world.random.choice(junk_keys) - junk_pool += [self.create_item(junk_item)] + junk_pool.append(self.create_item(junk_item)) itempool += junk_pool trap_pool = [] for i in range(trap_count): trap_item = self.world.random.choice(trap_weights) - trap_pool += [self.create_item(trap_item)] + trap_pool.append(self.create_item(trap_item)) itempool += trap_pool diff --git a/worlds/sc2wol/Locations.py b/worlds/sc2wol/Locations.py index f69abd48e3..14dd25fd52 100644 --- a/worlds/sc2wol/Locations.py +++ b/worlds/sc2wol/Locations.py @@ -27,13 +27,10 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("Liberation Day", "Liberation Day: Fourth Statue", SC2WOL_LOC_ID_OFFSET + 104), LocationData("Liberation Day", "Liberation Day: Fifth Statue", SC2WOL_LOC_ID_OFFSET + 105), LocationData("Liberation Day", "Liberation Day: Sixth Statue", SC2WOL_LOC_ID_OFFSET + 106), - LocationData("Liberation Day", "Beat Liberation Day", None), LocationData("The Outlaws", "The Outlaws: Victory", SC2WOL_LOC_ID_OFFSET + 200, lambda state: state._sc2wol_has_common_unit(world, player)), LocationData("The Outlaws", "The Outlaws: Rebel Base", SC2WOL_LOC_ID_OFFSET + 201, lambda state: state._sc2wol_has_common_unit(world, player)), - LocationData("The Outlaws", "Beat The Outlaws", None, - lambda state: state._sc2wol_has_common_unit(world, player)), LocationData("Zero Hour", "Zero Hour: Victory", SC2WOL_LOC_ID_OFFSET + 300, lambda state: state._sc2wol_has_common_unit(world, player)), LocationData("Zero Hour", "Zero Hour: First Group Rescued", SC2WOL_LOC_ID_OFFSET + 301), @@ -41,26 +38,20 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L lambda state: state._sc2wol_has_common_unit(world, player)), LocationData("Zero Hour", "Zero Hour: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 303, lambda state: state._sc2wol_has_common_unit(world, player)), - 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_anti_air(world, player)), + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_competent_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)), LocationData("Evacuation", "Evacuation: Third Chysalis", SC2WOL_LOC_ID_OFFSET + 403, 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_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, lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), LocationData("Outbreak", "Outbreak: Right Infestor", SC2WOL_LOC_ID_OFFSET + 502, lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), - LocationData("Outbreak", "Beat Outbreak", None, - 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_competent_anti_air(world, player)), @@ -73,9 +64,6 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L 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_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_competent_anti_air(world, player)), @@ -88,9 +76,6 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L 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_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_competent_anti_air(world, player)), @@ -101,9 +86,6 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("Smash and Grab", "Smash and Grab: Fourth Relic", SC2WOL_LOC_ID_OFFSET + 804, lambda state: state._sc2wol_has_common_unit(world, player) and state._sc2wol_has_anti_air(world, player)), - LocationData("Smash and Grab", "Beat Smash and Grab", None, - lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_anti_air(world, player)), LocationData("The Dig", "The Dig: Victory", SC2WOL_LOC_ID_OFFSET + 900, lambda state: state._sc2wol_has_common_unit(world, player) and state._sc2wol_has_anti_air(world, player) and @@ -114,10 +96,6 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L lambda state: state._sc2wol_has_common_unit(world, player)), LocationData("The Dig", "The Dig: Right Cliff Relic", SC2WOL_LOC_ID_OFFSET + 903, lambda state: state._sc2wol_has_common_unit(world, player)), - LocationData("The Dig", "Beat The Dig", None, - lambda state: state._sc2wol_has_common_unit(world, player) and - 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) and state._sc2wol_has_anti_air(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003, @@ -132,8 +110,6 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L 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_beats_protoss_deathball(world, player)), LocationData("Supernova", "Supernova: West Relic", SC2WOL_LOC_ID_OFFSET + 1101), @@ -142,8 +118,6 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L lambda state: state._sc2wol_beats_protoss_deathball(world, player)), LocationData("Supernova", "Supernova: East Relic", SC2WOL_LOC_ID_OFFSET + 1104, lambda state: state._sc2wol_beats_protoss_deathball(world, player)), - LocationData("Supernova", "Beat Supernova", None, - 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._sc2wol_has_air(world, player) and @@ -170,19 +144,12 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L 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._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_anti_air(world, player) and ( 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), LocationData("Devil's Playground", "Devil's Playground: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1302, lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), - LocationData("Devil's Playground", "Beat Devil's Playground", None, - 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_competent_anti_air(world, player)), @@ -193,28 +160,21 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L 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_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_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), - LocationData("Breakout", "Beat Breakout", None), LocationData("Ghost of a Chance", "Ghost of a Chance: Victory", SC2WOL_LOC_ID_OFFSET + 1600), LocationData("Ghost of a Chance", "Ghost of a Chance: Terrazine Tank", SC2WOL_LOC_ID_OFFSET + 1601), LocationData("Ghost of a Chance", "Ghost of a Chance: Jorium Stockpile", SC2WOL_LOC_ID_OFFSET + 1602), LocationData("Ghost of a Chance", "Ghost of a Chance: First Island Spectres", SC2WOL_LOC_ID_OFFSET + 1603), LocationData("Ghost of a Chance", "Ghost of a Chance: Second Island Spectres", SC2WOL_LOC_ID_OFFSET + 1604), 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) 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), - LocationData("The Great Train Robbery", "Beat The Great Train Robbery", None, - lambda state: state._sc2wol_has_train_killers(world, player)), LocationData("Cutthroat", "Cutthroat: Victory", SC2WOL_LOC_ID_OFFSET + 1800, lambda state: state._sc2wol_has_common_unit(world, player)), LocationData("Cutthroat", "Cutthroat: Mira Han", SC2WOL_LOC_ID_OFFSET + 1801, @@ -224,10 +184,9 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("Cutthroat", "Cutthroat: Mid Relic", SC2WOL_LOC_ID_OFFSET + 1803), LocationData("Cutthroat", "Cutthroat: Southwest Relic", SC2WOL_LOC_ID_OFFSET + 1804, lambda state: state._sc2wol_has_common_unit(world, player)), - 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_competent_anti_air(world, player)), + 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: Odin", SC2WOL_LOC_ID_OFFSET + 1901), LocationData("Engine of Destruction", "Engine of Destruction: Loki", SC2WOL_LOC_ID_OFFSET + 1902, lambda state: state._sc2wol_has_competent_anti_air(world, player) and @@ -239,9 +198,6 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("Engine of Destruction", "Engine of Destruction: Southeast Devourer", SC2WOL_LOC_ID_OFFSET + 1905, 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_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)), LocationData("Media Blitz", "Media Blitz: Tower 1", SC2WOL_LOC_ID_OFFSET + 2001, @@ -251,8 +207,6 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("Media Blitz", "Media Blitz: Tower 3", SC2WOL_LOC_ID_OFFSET + 2003, lambda state: state._sc2wol_has_competent_comp(world, player)), 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, 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), @@ -264,44 +218,34 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L 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), LocationData("Whispers of Doom", "Whispers of Doom: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 2203), - LocationData("Whispers of Doom", "Beat Whispers of Doom", None), LocationData("A Sinister Turn", "A Sinister Turn: Victory", SC2WOL_LOC_ID_OFFSET + 2300, lambda state: state._sc2wol_has_protoss_medium_units(world, player)), LocationData("A Sinister Turn", "A Sinister Turn: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301), LocationData("A Sinister Turn", "A Sinister Turn: Dark Shrine", SC2WOL_LOC_ID_OFFSET + 2302), LocationData("A Sinister Turn", "A Sinister Turn: Templar Archives", SC2WOL_LOC_ID_OFFSET + 2303, lambda state: state._sc2wol_has_protoss_common_units(world, player)), - LocationData("A Sinister Turn", "Beat A Sinister Turn", None, - lambda state: state._sc2wol_has_protoss_medium_units(world, player)), LocationData("Echoes of the Future", "Echoes of the Future: Victory", SC2WOL_LOC_ID_OFFSET + 2400, lambda state: state._sc2wol_has_protoss_medium_units(world, player)), LocationData("Echoes of the Future", "Echoes of the Future: Close Obelisk", SC2WOL_LOC_ID_OFFSET + 2401), LocationData("Echoes of the Future", "Echoes of the Future: West Obelisk", SC2WOL_LOC_ID_OFFSET + 2402, 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), 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, 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, lambda state: state._sc2wol_has_competent_comp(world, player)), - LocationData("Gates of Hell", "Beat Gates of Hell", None), LocationData("Belly of the Beast", "Belly of the Beast: Victory", SC2WOL_LOC_ID_OFFSET + 2700), LocationData("Belly of the Beast", "Belly of the Beast: First Charge", SC2WOL_LOC_ID_OFFSET + 2701), LocationData("Belly of the Beast", "Belly of the Beast: Second Charge", SC2WOL_LOC_ID_OFFSET + 2702), LocationData("Belly of the Beast", "Belly of the Beast: Third Charge", SC2WOL_LOC_ID_OFFSET + 2703), - LocationData("Belly of the Beast", "Beat Belly of the Beast", None), LocationData("Shatter the Sky", "Shatter the Sky: Victory", SC2WOL_LOC_ID_OFFSET + 2800, lambda state: state._sc2wol_has_competent_comp(world, player)), LocationData("Shatter the Sky", "Shatter the Sky: Close Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2801, @@ -314,9 +258,15 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L lambda state: state._sc2wol_has_competent_comp(world, player)), LocationData("Shatter the Sky", "Shatter the Sky: Leviathan", SC2WOL_LOC_ID_OFFSET + 2805, lambda state: state._sc2wol_has_competent_comp(world, player)), - LocationData("Shatter the Sky", "Beat Shatter the Sky", None, - lambda state: state._sc2wol_has_competent_comp(world, player)), LocationData("All-In", "All-In: Victory", None) ] - return tuple(location_table) + beat_events = [] + + for location_data in location_table: + if location_data.name.endswith((": Victory", ": Defeat")): + beat_events.append( + location_data._replace(name="Beat " + location_data.name.rsplit(": ", 1)[0], code=None) + ) + + return tuple(location_table + beat_events) diff --git a/worlds/sc2wol/LogicMixin.py b/worlds/sc2wol/LogicMixin.py index 7a08142672..52bb6b09a8 100644 --- a/worlds/sc2wol/LogicMixin.py +++ b/worlds/sc2wol/LogicMixin.py @@ -1,5 +1,5 @@ from BaseClasses import MultiWorld -from ..AutoWorld import LogicMixin +from worlds.AutoWorld import LogicMixin class SC2WoLLogic(LogicMixin): diff --git a/worlds/sc2wol/Regions.py b/worlds/sc2wol/Regions.py index 4e20752982..8219a982c9 100644 --- a/worlds/sc2wol/Regions.py +++ b/worlds/sc2wol/Regions.py @@ -1,8 +1,8 @@ -from typing import List, Set, Dict, Tuple, Optional, Callable, NamedTuple +from typing import List, Set, Dict, Tuple, Optional, Callable from BaseClasses import MultiWorld, Region, Entrance, Location, RegionType from .Locations import LocationData from .Options import get_option_value -from worlds.sc2wol.MissionTables import MissionInfo, vanilla_shuffle_order, vanilla_mission_req_table, \ +from .MissionTables import MissionInfo, vanilla_shuffle_order, vanilla_mission_req_table, \ no_build_regions_list, easy_regions_list, medium_regions_list, hard_regions_list import random diff --git a/worlds/sc2wol/__init__.py b/worlds/sc2wol/__init__.py index 4f9b33609f..6d056df808 100644 --- a/worlds/sc2wol/__init__.py +++ b/worlds/sc2wol/__init__.py @@ -1,15 +1,14 @@ import typing -from typing import List, Set, Tuple, NamedTuple +from typing import List, Set, Tuple from BaseClasses import Item, MultiWorld, Location, Tutorial, ItemClassification -from ..AutoWorld import WebWorld +from worlds.AutoWorld import WebWorld, World from .Items import StarcraftWoLItem, item_table, filler_items, item_name_groups, get_full_item_list, \ basic_unit from .Locations import get_locations from .Regions import create_regions from .Options import sc2wol_options, get_option_value from .LogicMixin import SC2WoLLogic -from ..AutoWorld import World class Starcraft2WoLWebWorld(WebWorld): 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 8fa20c86f9..f7c8519a2a 100644 --- a/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md +++ b/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md @@ -15,7 +15,7 @@ missions. When you receive items, they will immediately become available, even d 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.) -Missions are launched only through the text client. The Hyperion is never visited. Aditionally, credits are not used. +Missions are launched only through the text client. The Hyperion is never visited. Additionally, credits are not used. ## What is the goal of this game when randomized? diff --git a/worlds/sc2wol/docs/setup_en.md b/worlds/sc2wol/docs/setup_en.md index 1539a21291..267c8430aa 100644 --- a/worlds/sc2wol/docs/setup_en.md +++ b/worlds/sc2wol/docs/setup_en.md @@ -13,9 +13,10 @@ to obtain a config file for StarCraft 2. 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)). + - Linux users should also follow the instructions found at the bottom of this page + (["Running in Linux"](#running-in-linux)). +2. Run ArchipelagoStarcraft2Client.exe. +3. Type the command `/download_data`. This will automatically install the Maps and Data files from the third link above. ## Where do I get a config file (aka "YAML") for this game? @@ -40,16 +41,9 @@ Check out [Creating a YAML](https://archipelago.gg/tutorial/Archipelago/setup/en ## The game isn't launching when I try to start a mission. -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. - -### Check your installation - -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. +First, check the log file for issues (stored at `[Archipelago Directory]/logs/SC2Client.txt`). If 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. ## Running in Linux diff --git a/worlds/sm/Rom.py b/worlds/sm/Rom.py index e2957fe00f..e5f5bc7a37 100644 --- a/worlds/sm/Rom.py +++ b/worlds/sm/Rom.py @@ -3,7 +3,8 @@ import os import json import Utils -from Patch import read_rom, APDeltaPatch +from Utils import read_snes_rom +from worlds.Files import APDeltaPatch SMJUHASH = '21f3e98df4780ee1c667b84e57d88675' ROM_PLAYER_LIMIT = 65535 # max archipelago player ID. note, SM ROM itself will only store 201 names+ids max @@ -22,7 +23,7 @@ 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"))) + base_rom_bytes = bytes(read_snes_rom(open(file_name, "rb"))) basemd5 = hashlib.md5() basemd5.update(base_rom_bytes) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 0bf12ca7eb..d901303215 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -505,10 +505,8 @@ class SMWorld(World): romPatcher.writeRandoSettings(self.variaRando.randoExec.randoSettings, itemLocs) def generate_output(self, output_directory: str): - outfilebase = 'AP_' + self.world.seed_name - outfilepname = f'_P{self.player}' - outfilepname += f"_{self.world.get_file_safe_player_name(self.player).replace(' ', '_')}" - outputFilename = os.path.join(output_directory, f'{outfilebase}{outfilepname}.sfc') + outfilebase = self.world.get_out_file_name_base(self.player) + outputFilename = os.path.join(output_directory, f"{outfilebase}.sfc") try: self.variaRando.PatchRom(outputFilename, self.APPrePatchRom, self.APPostPatchRom) diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index 88b2261a23..b1eef4ff2c 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -36,7 +36,7 @@ class SM64World(World): location_name_to_id = location_table data_version = 8 - required_client_version = (0, 3, 0) + required_client_version = (0, 3, 5) area_connections: typing.Dict[int, int] @@ -173,7 +173,7 @@ class SM64World(World): } } } - filename = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_file_safe_player_name(self.player)}.apsm64ex" + filename = f"{self.world.get_out_file_name_base(self.player)}.apsm64ex" with open(os.path.join(output_directory, filename), 'w') as f: json.dump(data, f) diff --git a/worlds/sm64ex/docs/setup_en.md b/worlds/sm64ex/docs/setup_en.md index 1b4ae6dfb4..acf9432fe5 100644 --- a/worlds/sm64ex/docs/setup_en.md +++ b/worlds/sm64ex/docs/setup_en.md @@ -3,8 +3,10 @@ ## Required Software - Super Mario 64 US Rom (Japanese may work also. Europe and Shindou not supported) -- Either of [sm64pclauncher](https://github.com/N00byKing/sm64pclauncher/releases) or -- Cloning and building [sm64ex](https://github.com/N00byKing/sm64ex) manually. +- Either of + - [sm64pclauncher](https://github.com/N00byKing/sm64pclauncher/releases) or + - Cloning and building [sm64ex](https://github.com/N00byKing/sm64ex) manually +- Optional, for sending [commands](/tutorial/Archipelago/commands/en) like `!hint`: the TextClient from [the most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases) NOTE: The above linked sm64pclauncher is a special version designed to work with the Archipelago build of sm64ex. You can use other sm64-port based builds with it, but you can't use a different launcher with the Archipelago build of sm64ex. @@ -25,7 +27,9 @@ Then follow the steps below 6. Set the location where you installed MSYS when prompted. Check the "Install Dependencies" Checkbox 7. Set the Repo link to `https://github.com/N00byKing/sm64ex` and the Branch to `archipelago` (Top two boxes). You can choose the folder (Secound Box) at will, as long as it does not exist yet 8. Point the Launcher to your Super Mario 64 US/JP Rom, and set the Region correspondingly -9. Set Build Options. Recommended: `-jn` where `n` is the Number of CPU Cores, to build faster. +9. Set Build Options and press build. + - Recommended: To build faster, use `-jn` where `n` is the number of CPU cores to use (e.g., `-j4` to use 4 cores). + - Optional: Add options from [this list](https://github.com/sm64pc/sm64ex/wiki/Build-options), separated by spaces (e.g., `-j4 BETTERCAMERA=1`). 10. SM64EX will now be compiled. The Launcher will appear to have crashed, but this is not likely the case. Best wait a bit, but there may be a problem if it takes longer than 10 Minutes After it's done, the Build list should have another entry titled with what you named the folder in step 7. @@ -52,7 +56,11 @@ Optionally, add `--sm64ap_passwd "YourPassword"` if the room you are using requi The Name in this case is the one specified in your generated .yaml file. In case you are using the Archipelago Website, the IP should be `archipelago.gg`. -If everything worked out, you will see a textbox informing you the connection has been established after the story intro. +Should the connection fail (for example when using the wrong name or IP/Port combination) the game will inform you of that. +Additionally, any time the game is not connected (for example when the connection is unstable) it will attempt to reconnect and display a status text. + +**Important:** You must start a new file for every new seed you play. Using `⭐x0` files is **not** sufficient. +Failing to use a new file may make some locations unavailable. However, this can be fixed without losing any progress by exiting and starting a new file. # Playing offline @@ -82,6 +90,11 @@ with its name. When using a US Rom, the In-Game messages are missing some letters: `J Q V X Z` and `?`. The Japanese Version should have no problem displaying these. +### Toad does not have an item for me. + +This happens when you load an existing file that had already received an item from that toad. +To resolve this, exit and start from a `NEW` file. The server will automatically restore your progress. + ### What happens if I lose connection? SM64EX tries to reconnect a few times, so be patient. diff --git a/worlds/smw/Aesthetics.py b/worlds/smw/Aesthetics.py new file mode 100644 index 0000000000..624440c55f --- /dev/null +++ b/worlds/smw/Aesthetics.py @@ -0,0 +1,202 @@ + +mario_palettes = [ + [0x5F, 0x63, 0x1D, 0x58, 0x0A, 0x00, 0x1F, 0x39, 0xC4, 0x44, 0x08, 0x4E, 0x70, 0x67, 0xB6, 0x30, 0xDF, 0x35, 0xFF, 0x03], # Mario + [0x3F, 0x4F, 0x1D, 0x58, 0x40, 0x11, 0xE0, 0x3F, 0x07, 0x3C, 0xAE, 0x7C, 0xB3, 0x7D, 0x00, 0x2F, 0x5F, 0x16, 0xFF, 0x03], # Luigi + [0x5F, 0x63, 0x1D, 0x58, 0x0A, 0x00, 0x1F, 0x03, 0xC4, 0x44, 0x08, 0x4E, 0x70, 0x67, 0x16, 0x02, 0xDF, 0x35, 0xFF, 0x03], # Wario + [0x5F, 0x63, 0x1D, 0x58, 0x0A, 0x00, 0x12, 0x7C, 0xC4, 0x44, 0x08, 0x4E, 0x70, 0x67, 0x0D, 0x58, 0xDF, 0x35, 0xFF, 0x03], # Waluigi + [0x5F, 0x63, 0x1D, 0x58, 0x0A, 0x00, 0x00, 0x7C, 0xC4, 0x44, 0x08, 0x4E, 0x70, 0x67, 0x00, 0x58, 0xDF, 0x35, 0xFF, 0x03], # Geno + [0x5F, 0x63, 0x1D, 0x58, 0x0A, 0x00, 0x1F, 0x7C, 0xC4, 0x44, 0x08, 0x4E, 0x70, 0x67, 0x16, 0x58, 0xDF, 0x35, 0xFF, 0x03], # Princess + [0x5F, 0x63, 0x1D, 0x58, 0x0A, 0x00, 0xE0, 0x00, 0xC4, 0x44, 0x08, 0x4E, 0x70, 0x67, 0x80, 0x00, 0xDF, 0x35, 0xFF, 0x03], # Dark + [0x5F, 0x63, 0x1D, 0x58, 0x0A, 0x00, 0xFF, 0x01, 0xC4, 0x44, 0x08, 0x4E, 0x70, 0x67, 0x5F, 0x01, 0xDF, 0x35, 0xFF, 0x03], # Sponge +] + +fire_mario_palettes = [ + [0x5F, 0x63, 0x1D, 0x58, 0x29, 0x25, 0xFF, 0x7F, 0x08, 0x00, 0x17, 0x00, 0x1F, 0x00, 0x7B, 0x57, 0xDF, 0x0D, 0xFF, 0x03], # Mario + [0x1F, 0x3B, 0x1D, 0x58, 0x29, 0x25, 0xFF, 0x7F, 0x40, 0x11, 0xE0, 0x01, 0xE0, 0x02, 0x7B, 0x57, 0xDF, 0x0D, 0xFF, 0x03], # Luigi + [0x5F, 0x63, 0x1D, 0x58, 0x29, 0x25, 0xFF, 0x7F, 0x08, 0x00, 0x16, 0x02, 0x1F, 0x03, 0x7B, 0x57, 0xDF, 0x0D, 0xFF, 0x03], # Wario + [0x5F, 0x63, 0x1D, 0x58, 0x29, 0x25, 0xFF, 0x7F, 0x08, 0x00, 0x0D, 0x58, 0x12, 0x7C, 0x7B, 0x57, 0xDF, 0x0D, 0xFF, 0x03], # Waluigi + [0x5F, 0x63, 0x1D, 0x58, 0x29, 0x25, 0xFF, 0x7F, 0x08, 0x00, 0x00, 0x58, 0x00, 0x7C, 0x7B, 0x57, 0xDF, 0x0D, 0xFF, 0x03], # Geno + [0x5F, 0x63, 0x1D, 0x58, 0x29, 0x25, 0xFF, 0x7F, 0x08, 0x00, 0x16, 0x58, 0x1F, 0x7C, 0x7B, 0x57, 0xDF, 0x0D, 0xFF, 0x03], # Princess + [0x5F, 0x63, 0x1D, 0x58, 0x29, 0x25, 0xFF, 0x7F, 0x08, 0x00, 0x80, 0x00, 0xE0, 0x00, 0x7B, 0x57, 0xDF, 0x0D, 0xFF, 0x03], # Dark + [0x5F, 0x63, 0x1D, 0x58, 0x29, 0x25, 0xFF, 0x7F, 0x08, 0x00, 0x5F, 0x01, 0xFF, 0x01, 0x7B, 0x57, 0xDF, 0x0D, 0xFF, 0x03], # Sponge +] + +ow_mario_palettes = [ + [0x16, 0x00, 0x1F, 0x00], # Mario + [0x80, 0x02, 0xE0, 0x03], # Luigi + [0x16, 0x02, 0x1F, 0x03], # Wario + [0x0D, 0x58, 0x12, 0x7C], # Waluigi + [0x00, 0x58, 0x00, 0x7C], # Geno + [0x16, 0x58, 0x1F, 0x7C], # Princess + [0x80, 0x00, 0xE0, 0x00], # Dark + [0x5F, 0x01, 0xFF, 0x01], # Sponge +] + +level_music_address_data = [ + 0x284DB, + 0x284DC, + 0x284DD, + 0x284DE, + 0x284DF, + 0x284E0, + 0x284E1, + 0x284E2, +] + +level_music_value_data = [ + 0x02, + 0x06, + 0x01, + 0x08, + 0x07, + 0x03, + 0x05, + 0x12, +] + +ow_music_address_data = [ + [0x25BC8, 0x20D8A], + [0x25BC9, 0x20D8B], + [0x25BCA, 0x20D8C], + [0x25BCB, 0x20D8D], + [0x25BCC, 0x20D8E], + [0x25BCD, 0x20D8F], + [0x25BCE, 0x20D90], + [0x16C7] +] + +ow_music_value_data = [ + 0x02, + 0x03, + 0x04, + 0x06, + 0x07, + 0x09, + 0x05, + 0x01, +] + +valid_foreground_palettes = { + 0x00: [0x00, 0x01, 0x03, 0x04, 0x05, 0x07], # Normal 1 + 0x01: [0x03, 0x04, 0x05, 0x07], # Castle 1 + 0x02: [0x01, 0x02, 0x03, 0x04, 0x05, 0x07], # Rope 1 + 0x03: [0x02, 0x03, 0x04, 0x05, 0x07], # Underground 1 + 0x04: [0x01, 0x02, 0x03, 0x04, 0x05, 0x07], # Switch Palace 1 + 0x05: [0x04, 0x05], # Ghost House 1 + 0x06: [0x01, 0x02, 0x03, 0x04, 0x05, 0x07], # Rope 2 + 0x07: [0x00, 0x01, 0x03, 0x04, 0x05, 0x07], # Normal 2 + 0x08: [0x01, 0x02, 0x03, 0x04, 0x05, 0x07], # Rope 3 + 0x09: [0x01, 0x02, 0x03, 0x04, 0x05, 0x07], # Underground 2 + 0x0A: [0x01, 0x02, 0x03, 0x04, 0x05, 0x07], # Switch Palace 2 + 0x0B: [0x03, 0x04, 0x05, 0x07], # Castle 2 + #0x0C: [], # Cloud/Forest + 0x0D: [0x04, 0x05], # Ghost House 2 + 0x0E: [0x02, 0x03, 0x04, 0x05, 0x07], # Underground 3 +} + +valid_background_palettes = { + 0x06861B: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Ghost House Exit + 0xFFD900: [0x01], # P. Hills + 0xFFDAB9: [0x04], # Water + 0xFFDC71: [0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07], # Hills & Clouds + 0xFFDD44: [0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07], # Clouds + 0xFFDE54: [0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07], # Small Hills + 0xFFDF59: [0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07], # Mountains & Clouds + 0xFFE103: [0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07], # Castle Pillars + 0xFFE472: [0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07], # Big Hills + 0xFFE674: [0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07], # Bonus + 0xFFE684: [0x01, 0x03, 0x05, 0x06], # Stars + 0xFFE7C0: [0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07], # Mountains + 0xFFE8EE: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Empty/Layer 2 + 0xFFE8FE: [0x01, 0x06], # Cave + 0xFFEC82: [0x00, 0x02, 0x03, 0x05, 0x06, 0x07], # Bushes + 0xFFEF80: [0x01, 0x03, 0x05, 0x06], # Ghost House + 0xFFF175: [0x00, 0x01, 0x02, 0x03, 0x05, 0x06], # Ghost Ship + 0xFFF45A: [0x01, 0x03, 0x06], # Castle +} + +valid_background_colors = { + 0x06861B: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Ghost House Exit + 0xFFD900: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # P. Hills + 0xFFDAB9: [0x02, 0x03, 0x05], # Water + 0xFFDC71: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Hills & Clouds + 0xFFDD44: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Clouds + 0xFFDE54: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Small Hills + 0xFFDF59: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Mountains & Clouds + 0xFFE103: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Castle Pillars + 0xFFE472: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Big Hills + 0xFFE674: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Bonus + 0xFFE684: [0x02, 0x03, 0x04, 0x05], # Stars + 0xFFE7C0: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Mountains + 0xFFE8EE: [0x03, 0x05], # Empty/Layer 2 + 0xFFE8FE: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Cave + 0xFFEC82: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Bushes + 0xFFEF80: [0x03, 0x04], # Ghost House + 0xFFF175: [0x02, 0x03, 0x04, 0x05], # Ghost Ship + 0xFFF45A: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Castle +} + +def generate_shuffled_level_music(world, player): + shuffled_level_music = level_music_value_data.copy() + + if world.music_shuffle[player] == "consistent": + world.random.shuffle(shuffled_level_music) + elif world.music_shuffle[player] == "singularity": + single_song = world.random.choice(shuffled_level_music) + shuffled_level_music = [single_song for i in range(len(shuffled_level_music))] + + return shuffled_level_music + +def generate_shuffled_ow_music(world, player): + shuffled_ow_music = ow_music_value_data.copy() + + if world.music_shuffle[player] == "consistent" or world.music_shuffle[player] == "full": + world.random.shuffle(shuffled_ow_music) + elif world.music_shuffle[player] == "singularity": + single_song = world.random.choice(shuffled_ow_music) + shuffled_ow_music = [single_song for i in range(len(shuffled_ow_music))] + + return shuffled_ow_music + +def generate_shuffled_header_data(rom, world, player): + if world.music_shuffle[player] != "full" and not world.foreground_palette_shuffle[player] and not world.background_palette_shuffle[player]: + return + + for level_id in range(0, 0x200): + layer1_ptr_list = list(rom.read_bytes(0x2E000 + level_id * 3, 3)) + layer1_ptr = (layer1_ptr_list[2] << 16 | layer1_ptr_list[1] << 8 | layer1_ptr_list[0]) + + if layer1_ptr == 0x68000: + # Unused Levels + continue + + if layer1_ptr >= 0x70000: + layer1_ptr -= 0x8000 + + layer1_ptr -= 0x38000 + + level_header = list(rom.read_bytes(layer1_ptr, 5)) + + tileset = level_header[4] & 0x0F + + if world.music_shuffle[player] == "full": + level_header[2] &= 0x8F + level_header[2] |= (world.random.randint(0, 7) << 5) + + if (world.foreground_palette_shuffle[player] and tileset in valid_foreground_palettes): + level_header[3] &= 0xF8 + level_header[3] |= world.random.choice(valid_foreground_palettes[tileset]) + + if world.background_palette_shuffle[player]: + layer2_ptr_list = list(rom.read_bytes(0x2E600 + level_id * 3, 3)) + layer2_ptr = (layer2_ptr_list[2] << 16 | layer2_ptr_list[1] << 8 | layer2_ptr_list[0]) + + if layer2_ptr in valid_background_palettes: + level_header[0] &= 0x1F + level_header[0] |= (world.random.choice(valid_background_palettes[layer2_ptr]) << 5) + + if layer2_ptr in valid_background_colors: + level_header[1] &= 0x1F + level_header[1] |= (world.random.choice(valid_background_colors[layer2_ptr]) << 5) + + rom.write_bytes(layer1_ptr, bytes(level_header)) diff --git a/worlds/smw/Client.py b/worlds/smw/Client.py new file mode 100644 index 0000000000..6ddd4e1073 --- /dev/null +++ b/worlds/smw/Client.py @@ -0,0 +1,455 @@ +import logging +import asyncio +import time + +from NetUtils import ClientStatus, color +from worlds import AutoWorldRegister +from SNIClient import Context, snes_buffered_write, snes_flush_writes, snes_read +from .Names.TextBox import generate_received_text +from Patch import GAME_SMW + +snes_logger = logging.getLogger("SNES") + +ROM_START = 0x000000 +WRAM_START = 0xF50000 +WRAM_SIZE = 0x20000 +SRAM_START = 0xE00000 + +SAVEDATA_START = WRAM_START + 0xF000 +SAVEDATA_SIZE = 0x500 + +SMW_ROMHASH_START = 0x7FC0 +ROMHASH_SIZE = 0x15 + +SMW_PROGRESS_DATA = WRAM_START + 0x1F02 +SMW_DRAGON_COINS_DATA = WRAM_START + 0x1F2F +SMW_PATH_DATA = WRAM_START + 0x1EA2 +SMW_EVENT_ROM_DATA = ROM_START + 0x2D608 +SMW_ACTIVE_LEVEL_DATA = ROM_START + 0x37F70 + +SMW_GOAL_DATA = ROM_START + 0x01BFA0 +SMW_REQUIRED_BOSSES_DATA = ROM_START + 0x01BFA1 +SMW_REQUIRED_EGGS_DATA = ROM_START + 0x01BFA2 +SMW_SEND_MSG_DATA = ROM_START + 0x01BFA3 +SMW_RECEIVE_MSG_DATA = ROM_START + 0x01BFA4 +SMW_DEATH_LINK_ACTIVE_ADDR = ROM_START + 0x01BFA5 +SMW_DRAGON_COINS_ACTIVE_ADDR = ROM_START + 0x01BFA6 +SMW_SWAMP_DONUT_GH_ADDR = ROM_START + 0x01BFA7 + +SMW_GAME_STATE_ADDR = WRAM_START + 0x100 +SMW_MARIO_STATE_ADDR = WRAM_START + 0x71 +SMW_BOSS_STATE_ADDR = WRAM_START + 0xD9B +SMW_ACTIVE_BOSS_ADDR = WRAM_START + 0x13FC +SMW_CURRENT_LEVEL_ADDR = WRAM_START + 0x13BF +SMW_MESSAGE_BOX_ADDR = WRAM_START + 0x1426 +SMW_BONUS_STAR_ADDR = WRAM_START + 0xF48 +SMW_EGG_COUNT_ADDR = WRAM_START + 0x1F24 +SMW_BOSS_COUNT_ADDR = WRAM_START + 0x1F26 +SMW_NUM_EVENTS_ADDR = WRAM_START + 0x1F2E +SMW_SFX_ADDR = WRAM_START + 0x1DFC +SMW_PAUSE_ADDR = WRAM_START + 0x13D4 +SMW_MESSAGE_QUEUE_ADDR = WRAM_START + 0xC391 + +SMW_RECV_PROGRESS_ADDR = WRAM_START + 0x1F2B + +SMW_GOAL_LEVELS = [0x28, 0x31, 0x32] +SMW_INVALID_MARIO_STATES = [0x05, 0x06, 0x0A, 0x0C, 0x0D] +SMW_BAD_TEXT_BOX_LEVELS = [0x26, 0x02, 0x4B] +SMW_BOSS_STATES = [0x80, 0xC0, 0xC1] +SMW_UNCOLLECTABLE_LEVELS = [0x25, 0x07, 0x0B, 0x40, 0x0E, 0x1F, 0x20, 0x1B, 0x1A, 0x35, 0x34, 0x31, 0x32] + +async def deathlink_kill_player(ctx: Context): + if ctx.game == GAME_SMW: + game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1) + if game_state[0] != 0x14: + return + + mario_state = await snes_read(ctx, SMW_MARIO_STATE_ADDR, 0x1) + if mario_state[0] != 0x00: + return + + message_box = await snes_read(ctx, SMW_MESSAGE_BOX_ADDR, 0x1) + if message_box[0] != 0x00: + return + + pause_state = await snes_read(ctx, SMW_PAUSE_ADDR, 0x1) + if pause_state[0] != 0x00: + return + + snes_buffered_write(ctx, WRAM_START + 0x9D, bytes([0x30])) # Freeze Gameplay + snes_buffered_write(ctx, WRAM_START + 0x1DFB, bytes([0x09])) # Death Music + snes_buffered_write(ctx, WRAM_START + 0x0DDA, bytes([0xFF])) # Flush Music Buffer + snes_buffered_write(ctx, WRAM_START + 0x1407, bytes([0x00])) # Flush Cape Fly Phase + snes_buffered_write(ctx, WRAM_START + 0x140D, bytes([0x00])) # Flush Spin Jump Flag + snes_buffered_write(ctx, WRAM_START + 0x188A, bytes([0x00])) # Flush Empty Byte because the game does it + snes_buffered_write(ctx, WRAM_START + 0x7D, bytes([0x90])) # Mario Y Speed + snes_buffered_write(ctx, WRAM_START + 0x1496, bytes([0x30])) # Death Timer + snes_buffered_write(ctx, SMW_MARIO_STATE_ADDR, bytes([0x09])) # Mario State -> Dead + + await snes_flush_writes(ctx) + + from SNIClient import DeathState + ctx.death_state = DeathState.dead + ctx.last_death_link = time.time() + + return + + +async def smw_rom_init(ctx: Context): + if not ctx.rom: + ctx.finished_game = False + ctx.death_link_allow_survive = False + game_hash = await snes_read(ctx, SMW_ROMHASH_START, ROMHASH_SIZE) + if game_hash is None or game_hash == bytes([0] * ROMHASH_SIZE) or game_hash[:3] != b"SMW": + return False + else: + ctx.game = GAME_SMW + ctx.items_handling = 0b111 # remote items + + ctx.rom = game_hash + + receive_option = await snes_read(ctx, SMW_RECEIVE_MSG_DATA, 0x1) + send_option = await snes_read(ctx, SMW_SEND_MSG_DATA, 0x1) + + ctx.receive_option = receive_option[0] + ctx.send_option = send_option[0] + + ctx.message_queue = [] + + ctx.allow_collect = True + + death_link = await snes_read(ctx, SMW_DEATH_LINK_ACTIVE_ADDR, 1) + if death_link: + await ctx.update_death_link(bool(death_link[0] & 0b1)) + return True + + +def add_message_to_queue(ctx: Context, new_message): + + if not hasattr(ctx, "message_queue"): + ctx.message_queue = [] + + ctx.message_queue.append(new_message) + + return + + +async def handle_message_queue(ctx: Context): + + game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1) + if game_state[0] != 0x14: + return + + mario_state = await snes_read(ctx, SMW_MARIO_STATE_ADDR, 0x1) + if mario_state[0] != 0x00: + return + + message_box = await snes_read(ctx, SMW_MESSAGE_BOX_ADDR, 0x1) + if message_box[0] != 0x00: + return + + pause_state = await snes_read(ctx, SMW_PAUSE_ADDR, 0x1) + if pause_state[0] != 0x00: + return + + current_level = await snes_read(ctx, SMW_CURRENT_LEVEL_ADDR, 0x1) + if current_level[0] in SMW_BAD_TEXT_BOX_LEVELS: + return + + boss_state = await snes_read(ctx, SMW_BOSS_STATE_ADDR, 0x1) + if boss_state[0] in SMW_BOSS_STATES: + return + + active_boss = await snes_read(ctx, SMW_ACTIVE_BOSS_ADDR, 0x1) + if active_boss[0] != 0x00: + return + + if not hasattr(ctx, "message_queue") or len(ctx.message_queue) == 0: + return + + next_message = ctx.message_queue.pop(0) + + snes_buffered_write(ctx, SMW_MESSAGE_QUEUE_ADDR, bytes(next_message)) + snes_buffered_write(ctx, SMW_MESSAGE_BOX_ADDR, bytes([0x03])) + snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([0x22])) + + await snes_flush_writes(ctx) + + return + + +async def smw_game_watcher(ctx: Context): + if ctx.game == GAME_SMW: + # SMW_TODO: Handle Deathlink + game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1) + mario_state = await snes_read(ctx, SMW_MARIO_STATE_ADDR, 0x1) + if game_state is None: + # We're not properly connected + return + elif game_state[0] >= 0x18: + if not ctx.finished_game: + current_level = await snes_read(ctx, SMW_CURRENT_LEVEL_ADDR, 0x1) + + if current_level[0] in SMW_GOAL_LEVELS: + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + return + elif game_state[0] < 0x0B: + # We haven't loaded a save file + ctx.message_queue = [] + return + elif mario_state[0] in SMW_INVALID_MARIO_STATES: + # Mario can't come to the phone right now + return + + if "DeathLink" in ctx.tags and game_state[0] == 0x14 and ctx.last_death_link + 1 < time.time(): + currently_dead = mario_state[0] == 0x09 + await ctx.handle_deathlink_state(currently_dead) + + # Check for Egg Hunt ending + goal = await snes_read(ctx, SMW_GOAL_DATA, 0x1) + if game_state[0] == 0x14 and goal[0] == 1: + current_level = await snes_read(ctx, SMW_CURRENT_LEVEL_ADDR, 0x1) + message_box = await snes_read(ctx, SMW_MESSAGE_BOX_ADDR, 0x1) + egg_count = await snes_read(ctx, SMW_EGG_COUNT_ADDR, 0x1) + required_egg_count = await snes_read(ctx, SMW_REQUIRED_EGGS_DATA, 0x1) + + if current_level[0] == 0x28 and message_box[0] == 0x01 and egg_count[0] >= required_egg_count[0]: + snes_buffered_write(ctx, WRAM_START + 0x13C6, bytes([0x08])) + snes_buffered_write(ctx, WRAM_START + 0x13CE, bytes([0x01])) + snes_buffered_write(ctx, WRAM_START + 0x1DE9, bytes([0x01])) + snes_buffered_write(ctx, SMW_GAME_STATE_ADDR, bytes([0x18])) + + await snes_flush_writes(ctx) + return + + egg_count = await snes_read(ctx, SMW_EGG_COUNT_ADDR, 0x1) + boss_count = await snes_read(ctx, SMW_BOSS_COUNT_ADDR, 0x1) + display_count = await snes_read(ctx, SMW_BONUS_STAR_ADDR, 0x1) + + if goal[0] == 0 and boss_count[0] > display_count[0]: + snes_buffered_write(ctx, SMW_BONUS_STAR_ADDR, bytes([boss_count[0]])) + await snes_flush_writes(ctx) + elif goal[0] == 1 and egg_count[0] > display_count[0]: + snes_buffered_write(ctx, SMW_BONUS_STAR_ADDR, bytes([egg_count[0]])) + await snes_flush_writes(ctx) + + await handle_message_queue(ctx) + + new_checks = [] + event_data = await snes_read(ctx, SMW_EVENT_ROM_DATA, 0x60) + progress_data = bytearray(await snes_read(ctx, SMW_PROGRESS_DATA, 0x0F)) + dragon_coins_data = bytearray(await snes_read(ctx, SMW_DRAGON_COINS_DATA, 0x0C)) + dragon_coins_active = await snes_read(ctx, SMW_DRAGON_COINS_ACTIVE_ADDR, 0x1) + from worlds.smw.Rom import item_rom_data, ability_rom_data + from worlds.smw.Levels import location_id_to_level_id, level_info_dict + for loc_name, level_data in location_id_to_level_id.items(): + loc_id = AutoWorldRegister.world_types[ctx.game].location_name_to_id[loc_name] + if loc_id not in ctx.locations_checked: + + event_id = event_data[level_data[0]] + + if level_data[1] == 2: + # Dragon Coins Check + if not dragon_coins_active or dragon_coins_active[0] == 0: + continue + + progress_byte = (level_data[0] // 8) + progress_bit = 7 - (level_data[0] % 8) + + data = dragon_coins_data[progress_byte] + masked_data = data & (1 << progress_bit) + bit_set = (masked_data != 0) + + if bit_set: + # SMW_TODO: Handle non-included checks + new_checks.append(loc_id) + else: + event_id_value = event_id + level_data[1] + + progress_byte = (event_id_value // 8) + progress_bit = 7 - (event_id_value % 8) + + data = progress_data[progress_byte] + masked_data = data & (1 << progress_bit) + bit_set = (masked_data != 0) + + if bit_set: + # SMW_TODO: Handle non-included checks + new_checks.append(loc_id) + + verify_game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1) + if verify_game_state is None or verify_game_state[0] < 0x0B or verify_game_state[0] > 0x29: + # We have somehow exited the save file (or worse) + print("Exit Save File") + return + + rom = await snes_read(ctx, SMW_ROMHASH_START, ROMHASH_SIZE) + if rom != ctx.rom: + ctx.rom = None + print("Exit ROM") + # 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]}]) + + if game_state[0] != 0x14: + # Don't receive items or collect locations outside of in-level mode + return + + recv_count = await snes_read(ctx, SMW_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))) + + if ctx.receive_option == 1 or (ctx.receive_option == 2 and ((item.flags & 1) != 0)): + if item.item != 0xBC0012: + # Don't send messages for Boss Tokens + item_name = ctx.item_names[item.item] + player_name = ctx.player_names[item.player] + + receive_message = generate_received_text(item_name, player_name) + add_message_to_queue(ctx, receive_message) + + snes_buffered_write(ctx, SMW_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) + increment = item_rom_data[item.item][1] + + new_item_count = item_count[0] + if increment > 1: + new_item_count = increment + else: + new_item_count += increment + + if verify_game_state[0] == 0x14 and len(item_rom_data[item.item]) > 2: + snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([item_rom_data[item.item][2]])) + + snes_buffered_write(ctx, WRAM_START + item_rom_data[item.item][0], bytes([new_item_count])) + elif item.item in ability_rom_data: + # Handle Upgrades + for rom_data in ability_rom_data[item.item]: + data = await snes_read(ctx, WRAM_START + rom_data[0], 1) + masked_data = data[0] | (1 << rom_data[1]) + snes_buffered_write(ctx, WRAM_START + rom_data[0], bytes([masked_data])) + snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([0x3E])) # SMW_TODO: Custom sounds for each + elif item.item == 0xBC000A: + # Handle Progressive Powerup + data = await snes_read(ctx, WRAM_START + 0x1F2D, 1) + mushroom_data = data[0] & (1 << 0) + fire_flower_data = data[0] & (1 << 1) + cape_data = data[0] & (1 << 2) + if mushroom_data == 0: + masked_data = data[0] | (1 << 0) + snes_buffered_write(ctx, WRAM_START + 0x1F2D, bytes([masked_data])) + snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([0x3E])) + elif fire_flower_data == 0: + masked_data = data[0] | (1 << 1) + snes_buffered_write(ctx, WRAM_START + 0x1F2D, bytes([masked_data])) + snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([0x3E])) + elif cape_data == 0: + masked_data = data[0] | (1 << 2) + snes_buffered_write(ctx, WRAM_START + 0x1F2D, bytes([masked_data])) + snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([0x41])) + else: + # Extra Powerup? + pass + elif item.item == 0xBC0015: + # Handle Literature Trap + from .Names.LiteratureTrap import lit_trap_text_list + import random + rand_trap = random.choice(lit_trap_text_list) + + for message in rand_trap: + add_message_to_queue(ctx, message) + + await snes_flush_writes(ctx) + + # Handle Collected Locations + new_events = 0 + path_data = bytearray(await snes_read(ctx, SMW_PATH_DATA, 0x60)) + donut_gh_swapped = await snes_read(ctx, SMW_SWAMP_DONUT_GH_ADDR, 0x1) + new_dragon_coin = False + for loc_id in ctx.checked_locations: + if loc_id not in ctx.locations_checked: + ctx.locations_checked.add(loc_id) + loc_name = ctx.location_names[loc_id] + + if loc_name not in location_id_to_level_id: + continue + + level_data = location_id_to_level_id[loc_name] + + if level_data[1] == 2: + # Dragon Coins Check + + progress_byte = (level_data[0] // 8) + progress_bit = 7 - (level_data[0] % 8) + + data = dragon_coins_data[progress_byte] + new_data = data | (1 << progress_bit) + dragon_coins_data[progress_byte] = new_data + + new_dragon_coin = True + else: + if level_data[0] in SMW_UNCOLLECTABLE_LEVELS: + continue + + event_id = event_data[level_data[0]] + event_id_value = event_id + level_data[1] + + progress_byte = (event_id_value // 8) + progress_bit = 7 - (event_id_value % 8) + + data = progress_data[progress_byte] + masked_data = data & (1 << progress_bit) + bit_set = (masked_data != 0) + + if bit_set: + continue + + new_events += 1 + new_data = data | (1 << progress_bit) + progress_data[progress_byte] = new_data + + tile_id = await snes_read(ctx, SMW_ACTIVE_LEVEL_DATA + level_data[0], 0x1) + + level_info = level_info_dict[tile_id[0]] + + path = level_info.exit1Path if level_data[1] == 0 else level_info.exit2Path + + if donut_gh_swapped[0] != 0 and tile_id[0] == 0x04: + # Handle Swapped Donut GH Exits + path = level_info.exit2Path if level_data[1] == 0 else level_info.exit1Path + + if not path: + continue + + this_end_path = path_data[tile_id[0]] + new_data = this_end_path | path.thisEndDirection + path_data[tile_id[0]] = new_data + + other_end_path = path_data[path.otherLevelID] + new_data = other_end_path | path.otherEndDirection + path_data[path.otherLevelID] = new_data + + if new_dragon_coin: + snes_buffered_write(ctx, SMW_DRAGON_COINS_DATA, bytes(dragon_coins_data)) + if new_events > 0: + snes_buffered_write(ctx, SMW_PROGRESS_DATA, bytes(progress_data)) + snes_buffered_write(ctx, SMW_PATH_DATA, bytes(path_data)) + old_events = await snes_read(ctx, SMW_NUM_EVENTS_ADDR, 0x1) + snes_buffered_write(ctx, SMW_NUM_EVENTS_ADDR, bytes([old_events[0] + new_events])) + + await snes_flush_writes(ctx) diff --git a/worlds/smw/Items.py b/worlds/smw/Items.py new file mode 100644 index 0000000000..e650aef4a5 --- /dev/null +++ b/worlds/smw/Items.py @@ -0,0 +1,69 @@ +import typing + +from BaseClasses import Item, ItemClassification +from .Names import ItemName + + +class ItemData(typing.NamedTuple): + code: typing.Optional[int] + progression: bool + trap: bool = False + quantity: int = 1 + event: bool = False + + +class SMWItem(Item): + game: str = "Super Mario World" + + +# Separate tables for each type of item. +junk_table = { + ItemName.one_up_mushroom: ItemData(0xBC0001, False), +} + +collectable_table = { + ItemName.yoshi_egg: ItemData(0xBC0002, True), +} + +upgrade_table = { + ItemName.mario_run: ItemData(0xBC0003, True), + ItemName.mario_carry: ItemData(0xBC0004, True), + ItemName.mario_swim: ItemData(0xBC0005, True), + ItemName.mario_spin_jump: ItemData(0xBC0006, True), + ItemName.mario_climb: ItemData(0xBC0007, True), + ItemName.yoshi_activate: ItemData(0xBC0008, True), + ItemName.p_switch: ItemData(0xBC0009, True), + ItemName.progressive_powerup: ItemData(0xBC000A, True), + ItemName.p_balloon: ItemData(0xBC000B, True), + ItemName.super_star_active: ItemData(0xBC000D, True), +} + +switch_palace_table = { + ItemName.yellow_switch_palace: ItemData(0xBC000E, True), + ItemName.green_switch_palace: ItemData(0xBC000F, True), + ItemName.red_switch_palace: ItemData(0xBC0010, True), + ItemName.blue_switch_palace: ItemData(0xBC0011, True), +} + +trap_table = { + ItemName.ice_trap: ItemData(0xBC0013, False, True), + ItemName.stun_trap: ItemData(0xBC0014, False, True), + ItemName.literature_trap: ItemData(0xBC0015, False, True), +} + +event_table = { + ItemName.victory: ItemData(0xBC0000, True), + ItemName.koopaling: ItemData(0xBC0012, True), +} + +# Complete item table. +item_table = { + **junk_table, + **collectable_table, + **upgrade_table, + **switch_palace_table, + **trap_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/smw/Levels.py b/worlds/smw/Levels.py new file mode 100644 index 0000000000..0783ebfd39 --- /dev/null +++ b/worlds/smw/Levels.py @@ -0,0 +1,579 @@ + +from .Names import LocationName + +class SMWPath(): + thisEndDirection: int + otherLevelID: int + otherEndDirection: int + + def __init__(self, thisEndDirection: int, otherLevelID: int, otherEndDirection: int): + self.thisEndDirection = thisEndDirection + self.otherLevelID = otherLevelID + self.otherEndDirection = otherEndDirection + + +class SMWLevel(): + levelName: str + levelIDAddress: int + #eventIDAddress: int + eventIDValue: int + #progressByte: int + #progressBit: int + exit1Path: SMWPath + exit2Path: SMWPath + + def __init__(self, levelName: str, levelIDAddress: int, eventIDValue: int, exit1Path: SMWPath = None, exit2Path: SMWPath = None): + self.levelName = levelName + self.levelIDAddress = levelIDAddress + #self.eventIDAddress = eventIDAddress # Inferred from: LevelIDValue (Dict Key): $2D608 + LevelIDValue + self.eventIDValue = eventIDValue + #self.progressByte = progressByte # Inferred from EventIDValue: (ID / 8) + $1F02 + #self.progressBit = progressBit # Inferred from EventIDValue: 1 << (7 - (ID % 8)) + self.exit1Path = exit1Path + self.exit2Path = exit2Path + + +level_info_dict = { + 0x28: SMWLevel(LocationName.yoshis_house, 0x37A76, 0x00), + 0x29: SMWLevel(LocationName.yoshis_island_1_region, 0x37A83, 0x01, SMWPath(0x08, 0x14, 0x04)), + 0x14: SMWLevel(LocationName.yellow_switch_palace, 0x37812, 0x02), + 0x2A: SMWLevel(LocationName.yoshis_island_2_region, 0x37A89, 0x03, SMWPath(0x08, 0x27, 0x04)), + 0x27: SMWLevel(LocationName.yoshis_island_3_region, 0x37A69, 0x04, SMWPath(0x01, 0x26, 0x04)), + 0x26: SMWLevel(LocationName.yoshis_island_4_region, 0x37A4B, 0x05, SMWPath(0x08, 0x25, 0x01)), + 0x25: SMWLevel(LocationName.yoshis_island_castle_region, 0x37A29, 0x06, SMWPath(0x08, 0x15, 0x04)), + + 0x15: SMWLevel(LocationName.donut_plains_1_region, 0x37815, 0x07, SMWPath(0x02, 0x09, 0x04), SMWPath(0x08, 0x0A, 0x04)), + 0x09: SMWLevel(LocationName.donut_plains_2_region, 0x376D3, 0x09, SMWPath(0x08, 0x04, 0x02), SMWPath(0x02, 0x08, 0x01)), + 0x0A: SMWLevel(LocationName.donut_secret_1_region, 0x376E5, 0x10, SMWPath(0x08, 0x04, 0x04), SMWPath(0x01, 0x13, 0x08)), + 0x08: SMWLevel(LocationName.green_switch_palace, 0x376D1, 0x28), + 0x04: SMWLevel(LocationName.donut_ghost_house_region, 0x376A5, 0x0B, SMWPath(0x08, 0x03, 0x04), SMWPath(0x01, 0x05, 0x02)), + 0x13: SMWLevel(LocationName.donut_secret_house_region, 0x37807, 0x12, SMWPath(0x01, 0x2F, 0x04), SMWPath(0x04, 0x16, 0x08)), # SMW_TODO: Check this wrt pipe behavior + 0x05: SMWLevel(LocationName.donut_plains_3_region, 0x376A9, 0x0D, SMWPath(0x01, 0x06, 0x08)), + 0x06: SMWLevel(LocationName.donut_plains_4_region, 0x376CB, 0x0E, SMWPath(0x01, 0x07, 0x02)), + 0x2F: SMWLevel(LocationName.donut_secret_2_region, 0x37B10, 0x14, SMWPath(0x01, 0x05, 0x04)), + 0x07: SMWLevel(LocationName.donut_plains_castle_region, 0x376CD, 0x0F, SMWPath(0x08, 0x3E, 0x04)), + 0x03: SMWLevel(LocationName.donut_plains_top_secret, 0x37685, 0xFF), + 0x16: SMWLevel(LocationName.donut_plains_star_road, 0x37827, 0xFF), + + 0x3E: SMWLevel(LocationName.vanilla_dome_1_region, 0x37C25, 0x15, SMWPath(0x01, 0x3C, 0x04), SMWPath(0x02, 0x2D, 0x04)), + 0x3C: SMWLevel(LocationName.vanilla_dome_2_region, 0x37C08, 0x17, SMWPath(0x08, 0x2B, 0x04), SMWPath(0x01, 0x3F, 0x08)), + 0x2D: SMWLevel(LocationName.vanilla_secret_1_region, 0x37AE3, 0x1D, SMWPath(0x08, 0x01, 0x02), SMWPath(0x02, 0x2C, 0x01)), + 0x2B: SMWLevel(LocationName.vanilla_ghost_house_region, 0x37AC8, 0x19, SMWPath(0x01, 0x2E, 0x08)), + 0x2E: SMWLevel(LocationName.vanilla_dome_3_region, 0x37AEC, 0x1A, SMWPath(0x04, 0x3D, 0x08)), + 0x3D: SMWLevel(LocationName.vanilla_dome_4_region, 0x37C0C, 0x1B, SMWPath(0x04, 0x40, 0x08)), + 0x3F: SMWLevel(LocationName.red_switch_palace, 0x37C2A, 0x29), + 0x01: SMWLevel(LocationName.vanilla_secret_2_region, 0x3763C, 0x1F, SMWPath(0x01, 0x02, 0x02)), + 0x02: SMWLevel(LocationName.vanilla_secret_3_region, 0x3763E, 0x20, SMWPath(0x01, 0x0B, 0x02)), + 0x0B: SMWLevel(LocationName.vanilla_fortress_region, 0x37730, 0x21, SMWPath(0x01, 0x0C, 0x02)), + 0x40: SMWLevel(LocationName.vanilla_dome_castle_region, 0x37C2C, 0x1C, SMWPath(0x04, 0x0F, 0x02)), + 0x2C: SMWLevel(LocationName.vanilla_dome_star_road, 0x37AE0, 0xFF), + + 0x0C: SMWLevel(LocationName.butter_bridge_1_region, 0x37734, 0x22, SMWPath(0x01, 0x0D, 0x02)), + 0x0D: SMWLevel(LocationName.butter_bridge_2_region, 0x37736, 0x23, SMWPath(0x01, 0x0E, 0x02)), + 0x0F: SMWLevel(LocationName.cheese_bridge_region, 0x37754, 0x25, SMWPath(0x01, 0x10, 0x02), SMWPath(0x04, 0x11, 0x08)), + 0x11: SMWLevel(LocationName.soda_lake_region, 0x37784, 0x60, SMWPath(0x04, 0x12, 0x04)), + 0x10: SMWLevel(LocationName.cookie_mountain_region, 0x37757, 0x27, SMWPath(0x04, 0x0E, 0x04)), + 0x0E: SMWLevel(LocationName.twin_bridges_castle_region, 0x3773A, 0x24, SMWPath(0x01, 0x42, 0x08)), + 0x12: SMWLevel(LocationName.twin_bridges_star_road, 0x377F0, 0xFF), + + 0x42: SMWLevel(LocationName.forest_of_illusion_1_region, 0x37C78, 0x2A, SMWPath(0x01, 0x44, 0x08), SMWPath(0x02, 0x41, 0x01)), + 0x44: SMWLevel(LocationName.forest_of_illusion_2_region, 0x37CAA, 0x2C, SMWPath(0x04, 0x47, 0x08), SMWPath(0x01, 0x45, 0x02)), + 0x47: SMWLevel(LocationName.forest_of_illusion_3_region, 0x37CC8, 0x2E, SMWPath(0x02, 0x41, 0x04), SMWPath(0x04, 0x20, 0x01)), + 0x43: SMWLevel(LocationName.forest_of_illusion_4_region, 0x37CA4, 0x32, SMWPath(0x01, 0x44, 0x02), SMWPath(0x04, 0x46, 0x08)), + 0x41: SMWLevel(LocationName.forest_ghost_house_region, 0x37C76, 0x30, SMWPath(0x01, 0x42, 0x02), SMWPath(0x02, 0x43, 0x08)), + 0x46: SMWLevel(LocationName.forest_secret_region, 0x37CC4, 0x34, SMWPath(0x04, 0x1F, 0x01)), + 0x45: SMWLevel(LocationName.blue_switch_palace, 0x37CAC, 0x37), + 0x1F: SMWLevel(LocationName.forest_fortress_region, 0x37906, 0x35, SMWPath(0x02, 0x1E, 0x01)), + 0x20: SMWLevel(LocationName.forest_castle_region, 0x37928, 0x61, SMWPath(0x04, 0x22, 0x08)), + 0x1E: SMWLevel(LocationName.forest_star_road, 0x37904, 0x36), + + 0x22: SMWLevel(LocationName.chocolate_island_1_region, 0x37968, 0x62, SMWPath(0x02, 0x21, 0x01)), + 0x24: SMWLevel(LocationName.chocolate_island_2_region, 0x379B5, 0x46, SMWPath(0x02, 0x23, 0x01), SMWPath(0x04, 0x3B, 0x01)), + 0x23: SMWLevel(LocationName.chocolate_island_3_region, 0x379B3, 0x48, SMWPath(0x04, 0x23, 0x08), SMWPath(0x02, 0x1B, 0x01)), + 0x1D: SMWLevel(LocationName.chocolate_island_4_region, 0x378DF, 0x4B, SMWPath(0x02, 0x1C, 0x01)), + 0x1C: SMWLevel(LocationName.chocolate_island_5_region, 0x378DC, 0x4C, SMWPath(0x08, 0x1A, 0x04)), + 0x21: SMWLevel(LocationName.chocolate_ghost_house_region, 0x37965, 0x63, SMWPath(0x04, 0x24, 0x08)), + 0x1B: SMWLevel(LocationName.chocolate_fortress_region, 0x378BF, 0x4A, SMWPath(0x04, 0x1D, 0x08)), + 0x3B: SMWLevel(LocationName.chocolate_secret_region, 0x37B97, 0x4F, SMWPath(0x02, 0x1A, 0x02)), + 0x1A: SMWLevel(LocationName.chocolate_castle_region, 0x378BC, 0x4D, SMWPath(0x08, 0x18, 0x02)), + + 0x18: SMWLevel(LocationName.sunken_ghost_ship_region, 0x3787E, 0x4E, SMWPath(0x08, 0x3A, 0x01)), + 0x3A: SMWLevel(LocationName.valley_of_bowser_1_region, 0x37B7B, 0x38, SMWPath(0x02, 0x39, 0x01)), + 0x39: SMWLevel(LocationName.valley_of_bowser_2_region, 0x37B79, 0x39, SMWPath(0x02, 0x38, 0x01), SMWPath(0x08, 0x35, 0x04)), + 0x37: SMWLevel(LocationName.valley_of_bowser_3_region, 0x37B74, 0x3D, SMWPath(0x08, 0x33, 0x04)), + 0x33: SMWLevel(LocationName.valley_of_bowser_4_region, 0x37B54, 0x3E, SMWPath(0x01, 0x34, 0x02), SMWPath(0x08, 0x30, 0x04)), + 0x38: SMWLevel(LocationName.valley_ghost_house_region, 0x37B77, 0x3B, SMWPath(0x02, 0x37, 0x01), SMWPath(0x08, 0x34, 0x04)), + 0x35: SMWLevel(LocationName.valley_fortress_region, 0x37B59, 0x41, SMWPath(0x08, 0x32, 0x04)), + 0x34: SMWLevel(LocationName.valley_castle_region, 0x37B57, 0x40, SMWPath(0x08, 0x31, 0x04)), + 0x31: SMWLevel(LocationName.front_door, 0x37B37, 0x45), + 0x81: SMWLevel(LocationName.front_door, 0x37B37, 0x45), # Fake Extra Front Door + 0x32: SMWLevel(LocationName.back_door, 0x37B39, 0x42), + 0x82: SMWLevel(LocationName.back_door, 0x37B39, 0x42), # Fake Extra Back Door + 0x30: SMWLevel(LocationName.valley_star_road, 0x37B34, 0x44), + + 0x5B: SMWLevel(LocationName.star_road_donut, 0x37DD3, 0x50), + 0x58: SMWLevel(LocationName.star_road_1_region, 0x37DA4, 0x51, None, SMWPath(0x02, 0x53, 0x04)), + 0x53: SMWLevel(LocationName.star_road_vanilla, 0x37D82, 0x53), + 0x54: SMWLevel(LocationName.star_road_2_region, 0x37D85, 0x54, None, SMWPath(0x08, 0x52, 0x02)), + 0x52: SMWLevel(LocationName.star_road_twin_bridges, 0x37D67, 0x56), + 0x56: SMWLevel(LocationName.star_road_3_region, 0x37D89, 0x57, None, SMWPath(0x01, 0x57, 0x02)), + 0x57: SMWLevel(LocationName.star_road_forest, 0x37D8C, 0x59), + 0x59: SMWLevel(LocationName.star_road_4_region, 0x37DAA, 0x5A, None, SMWPath(0x04, 0x5C, 0x08)), + 0x5C: SMWLevel(LocationName.star_road_valley, 0x37DDC, 0x5C), + 0x5A: SMWLevel(LocationName.star_road_5_region, 0x37DB7, 0x5D, SMWPath(0x02, 0x5B, 0x01), SMWPath(0x08, 0x55, 0x04)), + 0x55: SMWLevel(LocationName.star_road_special, 0x37D87, 0x5F), + + 0x4D: SMWLevel(LocationName.special_star_road, 0x37D31, 0x64), + 0x4E: SMWLevel(LocationName.special_zone_1_region, 0x37D33, 0x65, SMWPath(0x01, 0x4F, 0x02)), + 0x4F: SMWLevel(LocationName.special_zone_2_region, 0x37D36, 0x66, SMWPath(0x01, 0x50, 0x02)), + 0x50: SMWLevel(LocationName.special_zone_3_region, 0x37D39, 0x67, SMWPath(0x01, 0x51, 0x02)), + 0x51: SMWLevel(LocationName.special_zone_4_region, 0x37D3C, 0x68, SMWPath(0x01, 0x4C, 0x01)), + 0x4C: SMWLevel(LocationName.special_zone_5_region, 0x37D1C, 0x69, SMWPath(0x02, 0x4B, 0x01)), + 0x4B: SMWLevel(LocationName.special_zone_6_region, 0x37D19, 0x6A, SMWPath(0x02, 0x4A, 0x01)), + 0x4A: SMWLevel(LocationName.special_zone_7_region, 0x37D16, 0x6B, SMWPath(0x02, 0x49, 0x01)), + 0x49: SMWLevel(LocationName.special_zone_8_region, 0x37D13, 0x6C, SMWPath(0x02, 0x48, 0x01)), + 0x48: SMWLevel(LocationName.special_complete, 0x37D11, 0x6D), +} + +full_level_list = [ + 0x28, 0x29, 0x14, 0x2A, 0x27, 0x26, 0x25, + 0x15, 0x09, 0x0A, 0x08, 0x04, 0x13, 0x05, 0x06, 0x2F, 0x07, 0x03, 0x16, + 0x3E, 0x3C, 0x2D, 0x2B, 0x2E, 0x3D, 0x3F, 0x01, 0x02, 0x0B, 0x40, 0x2C, + 0x0C, 0x0D, 0x0F, 0x11, 0x10, 0x0E, 0x12, + 0x42, 0x44, 0x47, 0x43, 0x41, 0x46, 0x45, 0x1F, 0x20, 0x1E, + 0x22, 0x24, 0x23, 0x1D, 0x1C, 0x21, 0x1B, 0x3B, 0x1A, + 0x18, 0x3A, 0x39, 0x37, 0x33, 0x38, 0x35, 0x34, 0x31, 0x32, 0x30, + 0x5B, 0x58, 0x53, 0x54, 0x52, 0x56, 0x57, 0x59, 0x5C, 0x5A, 0x55, + 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x4C, 0x4B, 0x4A, 0x49, 0x48, +] + +submap_level_list = [ + 0x28, 0x29, 0x2A, 0x27, 0x26, 0x25, + 0x2F, + 0x3E, 0x3C, 0x2D, 0x2B, 0x2E, 0x3D, 0x3F, 0x40, 0x2C, + 0x42, 0x44, 0x47, 0x43, 0x41, 0x46, 0x45, + 0x3B, + 0x3A, 0x39, 0x37, 0x33, 0x38, 0x35, 0x34, 0x31, 0x32, 0x30, + 0x5B, 0x58, 0x53, 0x54, 0x52, 0x56, 0x57, 0x59, 0x5C, 0x5A, 0x55, + 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x4C, 0x4B, 0x4A, 0x49, 0x48, +] + +easy_castle_fortress_levels = [ + 0x07, + 0x40, + 0x1F, + 0x20, + 0x1B, + 0x34, +] + +hard_castle_fortress_levels = [ + 0x25, + 0x0B, + 0x0E, + 0x1A, + 0x35, +] + +easy_single_levels = [ + 0x29, + 0x2A, + 0x27, + 0x26, + 0x05, + 0x06, + 0x2F, + 0x2E, + 0x3D, + 0x01, + 0x0C, + 0x0D, + 0x46, + 0x1D, +] + +hard_single_levels = [ + 0x2B, + 0x02, + 0x11, + 0x10, + 0x22, + 0x1C, + 0x21, + 0x3B, + 0x3A, + 0x37, + 0x4E, + 0x4F, + 0x50, + 0x51, + 0x4C, + 0x4B, + 0x4A, + 0x49, +] + +easy_double_levels = [ + 0x15, + 0x09, + 0x0F, + 0x42, + 0x43, + 0x24, + 0x38, + 0x58, + 0x54, + 0x56, +] + +hard_double_levels = [ + 0x0A, + 0x04, + 0x13, + 0x3E, + 0x3C, + 0x2D, + 0x44, + 0x47, + 0x41, + 0x23, + 0x33, + 0x39, + 0x59, + 0x5A, +] + +switch_palace_levels = [ + 0x14, + 0x08, + 0x3F, + 0x45, +] + +location_id_to_level_id = { + LocationName.yoshis_island_1_exit_1: [0x29, 0], + LocationName.yoshis_island_1_dragon: [0x29, 2], + LocationName.yoshis_island_2_exit_1: [0x2A, 0], + LocationName.yoshis_island_2_dragon: [0x2A, 2], + LocationName.yoshis_island_3_exit_1: [0x27, 0], + LocationName.yoshis_island_3_dragon: [0x27, 2], + LocationName.yoshis_island_4_exit_1: [0x26, 0], + LocationName.yoshis_island_4_dragon: [0x26, 2], + LocationName.yoshis_island_castle: [0x25, 0], + LocationName.yoshis_island_koopaling: [0x25, 0], + LocationName.yellow_switch_palace: [0x14, 0], + + LocationName.donut_plains_1_exit_1: [0x15, 0], + LocationName.donut_plains_1_exit_2: [0x15, 1], + LocationName.donut_plains_1_dragon: [0x15, 2], + LocationName.donut_plains_2_exit_1: [0x09, 0], + LocationName.donut_plains_2_exit_2: [0x09, 1], + LocationName.donut_plains_2_dragon: [0x09, 2], + LocationName.donut_plains_3_exit_1: [0x05, 0], + LocationName.donut_plains_3_dragon: [0x05, 2], + LocationName.donut_plains_4_exit_1: [0x06, 0], + LocationName.donut_plains_4_dragon: [0x06, 2], + LocationName.donut_secret_1_exit_1: [0x0A, 0], + LocationName.donut_secret_1_exit_2: [0x0A, 1], + LocationName.donut_secret_1_dragon: [0x0A, 2], + LocationName.donut_secret_2_exit_1: [0x2F, 0], + LocationName.donut_secret_2_dragon: [0x2F, 2], + LocationName.donut_ghost_house_exit_1: [0x04, 0], + LocationName.donut_ghost_house_exit_2: [0x04, 1], + LocationName.donut_secret_house_exit_1: [0x13, 0], + LocationName.donut_secret_house_exit_2: [0x13, 1], + LocationName.donut_plains_castle: [0x07, 0], + LocationName.donut_plains_koopaling: [0x07, 0], + LocationName.green_switch_palace: [0x08, 0], + + LocationName.vanilla_dome_1_exit_1: [0x3E, 0], + LocationName.vanilla_dome_1_exit_2: [0x3E, 1], + LocationName.vanilla_dome_1_dragon: [0x3E, 2], + LocationName.vanilla_dome_2_exit_1: [0x3C, 0], + LocationName.vanilla_dome_2_exit_2: [0x3C, 1], + LocationName.vanilla_dome_2_dragon: [0x3C, 2], + LocationName.vanilla_dome_3_exit_1: [0x2E, 0], + LocationName.vanilla_dome_3_dragon: [0x2E, 2], + LocationName.vanilla_dome_4_exit_1: [0x3D, 0], + LocationName.vanilla_dome_4_dragon: [0x3D, 2], + LocationName.vanilla_secret_1_exit_1: [0x2D, 0], + LocationName.vanilla_secret_1_exit_2: [0x2D, 1], + LocationName.vanilla_secret_1_dragon: [0x2D, 2], + LocationName.vanilla_secret_2_exit_1: [0x01, 0], + LocationName.vanilla_secret_2_dragon: [0x01, 2], + LocationName.vanilla_secret_3_exit_1: [0x02, 0], + LocationName.vanilla_secret_3_dragon: [0x02, 2], + LocationName.vanilla_ghost_house_exit_1: [0x2B, 0], + LocationName.vanilla_ghost_house_dragon: [0x2B, 2], + LocationName.vanilla_fortress: [0x0B, 0], + LocationName.vanilla_reznor: [0x0B, 0], + LocationName.vanilla_dome_castle: [0x40, 0], + LocationName.vanilla_dome_koopaling: [0x40, 0], + LocationName.red_switch_palace: [0x3F, 0], + + LocationName.butter_bridge_1_exit_1: [0x0C, 0], + LocationName.butter_bridge_1_dragon: [0x0C, 2], + LocationName.butter_bridge_2_exit_1: [0x0D, 0], + LocationName.butter_bridge_2_dragon: [0x0D, 2], + LocationName.cheese_bridge_exit_1: [0x0F, 0], + LocationName.cheese_bridge_exit_2: [0x0F, 1], + LocationName.cheese_bridge_dragon: [0x0F, 2], + LocationName.cookie_mountain_exit_1: [0x10, 0], + LocationName.cookie_mountain_dragon: [0x10, 2], + LocationName.soda_lake_exit_1: [0x11, 0], + LocationName.soda_lake_dragon: [0x11, 2], + LocationName.twin_bridges_castle: [0x0E, 0], + LocationName.twin_bridges_koopaling: [0x0E, 0], + + LocationName.forest_of_illusion_1_exit_1: [0x42, 0], + LocationName.forest_of_illusion_1_exit_2: [0x42, 1], + LocationName.forest_of_illusion_2_exit_1: [0x44, 0], + LocationName.forest_of_illusion_2_exit_2: [0x44, 1], + LocationName.forest_of_illusion_2_dragon: [0x44, 2], + LocationName.forest_of_illusion_3_exit_1: [0x47, 0], + LocationName.forest_of_illusion_3_exit_2: [0x47, 1], + LocationName.forest_of_illusion_3_dragon: [0x47, 2], + LocationName.forest_of_illusion_4_exit_1: [0x43, 0], + LocationName.forest_of_illusion_4_exit_2: [0x43, 1], + LocationName.forest_of_illusion_4_dragon: [0x43, 2], + LocationName.forest_ghost_house_exit_1: [0x41, 0], + LocationName.forest_ghost_house_exit_2: [0x41, 1], + LocationName.forest_ghost_house_dragon: [0x41, 2], + LocationName.forest_secret_exit_1: [0x46, 0], + LocationName.forest_secret_dragon: [0x46, 2], + LocationName.forest_fortress: [0x1F, 0], + LocationName.forest_reznor: [0x1F, 0], + LocationName.forest_castle: [0x20, 0], + LocationName.forest_koopaling: [0x20, 0], + LocationName.forest_castle_dragon: [0x20, 2], + LocationName.blue_switch_palace: [0x45, 0], + + LocationName.chocolate_island_1_exit_1: [0x22, 0], + LocationName.chocolate_island_1_dragon: [0x22, 2], + LocationName.chocolate_island_2_exit_1: [0x24, 0], + LocationName.chocolate_island_2_exit_2: [0x24, 1], + LocationName.chocolate_island_2_dragon: [0x24, 2], + LocationName.chocolate_island_3_exit_1: [0x23, 0], + LocationName.chocolate_island_3_exit_2: [0x23, 1], + LocationName.chocolate_island_3_dragon: [0x23, 2], + LocationName.chocolate_island_4_exit_1: [0x1D, 0], + LocationName.chocolate_island_4_dragon: [0x1D, 2], + LocationName.chocolate_island_5_exit_1: [0x1C, 0], + LocationName.chocolate_island_5_dragon: [0x1C, 2], + LocationName.chocolate_ghost_house_exit_1: [0x21, 0], + LocationName.chocolate_secret_exit_1: [0x3B, 0], + LocationName.chocolate_fortress: [0x1B, 0], + LocationName.chocolate_reznor: [0x1B, 0], + LocationName.chocolate_castle: [0x1A, 0], + LocationName.chocolate_koopaling: [0x1A, 0], + + LocationName.sunken_ghost_ship: [0x18, 0], + LocationName.sunken_ghost_ship_dragon: [0x18, 2], + + LocationName.valley_of_bowser_1_exit_1: [0x3A, 0], + LocationName.valley_of_bowser_1_dragon: [0x3A, 2], + LocationName.valley_of_bowser_2_exit_1: [0x39, 0], + LocationName.valley_of_bowser_2_exit_2: [0x39, 1], + LocationName.valley_of_bowser_2_dragon: [0x39, 2], + LocationName.valley_of_bowser_3_exit_1: [0x37, 0], + LocationName.valley_of_bowser_3_dragon: [0x37, 2], + LocationName.valley_of_bowser_4_exit_1: [0x33, 0], + LocationName.valley_of_bowser_4_exit_2: [0x33, 1], + LocationName.valley_ghost_house_exit_1: [0x38, 0], + LocationName.valley_ghost_house_exit_2: [0x38, 1], + LocationName.valley_ghost_house_dragon: [0x38, 2], + LocationName.valley_fortress: [0x35, 0], + LocationName.valley_reznor: [0x35, 0], + LocationName.valley_castle: [0x34, 0], + LocationName.valley_koopaling: [0x34, 0], + LocationName.valley_castle_dragon: [0x34, 2], + + LocationName.star_road_1_exit_1: [0x58, 0], + LocationName.star_road_1_exit_2: [0x58, 1], + LocationName.star_road_1_dragon: [0x58, 2], + LocationName.star_road_2_exit_1: [0x54, 0], + LocationName.star_road_2_exit_2: [0x54, 1], + LocationName.star_road_3_exit_1: [0x56, 0], + LocationName.star_road_3_exit_2: [0x56, 1], + LocationName.star_road_4_exit_1: [0x59, 0], + LocationName.star_road_4_exit_2: [0x59, 1], + LocationName.star_road_5_exit_1: [0x5A, 0], + LocationName.star_road_5_exit_2: [0x5A, 1], + + LocationName.special_zone_1_exit_1: [0x4E, 0], + LocationName.special_zone_1_dragon: [0x4E, 2], + LocationName.special_zone_2_exit_1: [0x4F, 0], + LocationName.special_zone_2_dragon: [0x4F, 2], + LocationName.special_zone_3_exit_1: [0x50, 0], + LocationName.special_zone_3_dragon: [0x50, 2], + LocationName.special_zone_4_exit_1: [0x51, 0], + LocationName.special_zone_4_dragon: [0x51, 2], + LocationName.special_zone_5_exit_1: [0x4C, 0], + LocationName.special_zone_5_dragon: [0x4C, 2], + LocationName.special_zone_6_exit_1: [0x4B, 0], + LocationName.special_zone_6_dragon: [0x4B, 2], + LocationName.special_zone_7_exit_1: [0x4A, 0], + LocationName.special_zone_7_dragon: [0x4A, 2], + LocationName.special_zone_8_exit_1: [0x49, 0], + LocationName.special_zone_8_dragon: [0x49, 2], +} + +def generate_level_list(world, player): + + if not world.level_shuffle[player]: + out_level_list = full_level_list.copy() + out_level_list[0x00] = 0x03 + out_level_list[0x11] = 0x28 + + if world.bowser_castle_doors[player] == "fast": + out_level_list[0x41] = 0x82 + out_level_list[0x42] = 0x32 + elif world.bowser_castle_doors[player] == "slow": + out_level_list[0x41] = 0x31 + out_level_list[0x42] = 0x81 + + return out_level_list + + shuffled_level_list = [] + easy_castle_fortress_levels_copy = easy_castle_fortress_levels.copy() + world.random.shuffle(easy_castle_fortress_levels_copy) + hard_castle_fortress_levels_copy = hard_castle_fortress_levels.copy() + world.random.shuffle(hard_castle_fortress_levels_copy) + easy_single_levels_copy = easy_single_levels.copy() + world.random.shuffle(easy_single_levels_copy) + hard_single_levels_copy = hard_single_levels.copy() + world.random.shuffle(hard_single_levels_copy) + easy_double_levels_copy = easy_double_levels.copy() + world.random.shuffle(easy_double_levels_copy) + hard_double_levels_copy = hard_double_levels.copy() + world.random.shuffle(hard_double_levels_copy) + switch_palace_levels_copy = switch_palace_levels.copy() + world.random.shuffle(switch_palace_levels_copy) + + # Yoshi's Island + shuffled_level_list.append(0x03) + shuffled_level_list.append(easy_single_levels_copy.pop(0)) + shuffled_level_list.append(0x14) + shuffled_level_list.append(easy_single_levels_copy.pop(0)) + shuffled_level_list.append(easy_single_levels_copy.pop(0)) + shuffled_level_list.append(easy_single_levels_copy.pop(0)) + shuffled_level_list.append(easy_castle_fortress_levels_copy.pop(0)) + + # Donut Plains + shuffled_level_list.append(easy_double_levels_copy.pop(0)) + shuffled_level_list.append(easy_double_levels_copy.pop(0)) + shuffled_level_list.append(easy_double_levels_copy.pop(0)) + shuffled_level_list.append(0x08) + shuffled_level_list.append(easy_double_levels_copy.pop(0)) + shuffled_level_list.append(easy_double_levels_copy.pop(0)) + shuffled_level_list.append(easy_single_levels_copy.pop(0)) + shuffled_level_list.append(easy_single_levels_copy.pop(0)) + shuffled_level_list.append(easy_single_levels_copy.pop(0)) + shuffled_level_list.append(easy_castle_fortress_levels_copy.pop(0)) + shuffled_level_list.append(0x28) + shuffled_level_list.append(0x16) + + single_levels_copy = (easy_single_levels_copy.copy() + hard_single_levels_copy.copy()) + world.random.shuffle(single_levels_copy) + + castle_fortress_levels_copy = (easy_castle_fortress_levels_copy.copy() + hard_castle_fortress_levels_copy.copy()) + world.random.shuffle(castle_fortress_levels_copy) + + double_levels_copy = (easy_double_levels_copy.copy() + hard_double_levels_copy.copy()) + world.random.shuffle(double_levels_copy) + + # Vanilla Dome + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(0x3F) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(castle_fortress_levels_copy.pop(0)) + shuffled_level_list.append(castle_fortress_levels_copy.pop(0)) + shuffled_level_list.append(0x2C) + + # Twin Bridges + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(castle_fortress_levels_copy.pop(0)) + shuffled_level_list.append(0x12) + + # Forest of Illusion + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(0x45) + shuffled_level_list.append(castle_fortress_levels_copy.pop(0)) + shuffled_level_list.append(castle_fortress_levels_copy.pop(0)) + shuffled_level_list.append(0x1E) + + # Chocolate Island + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(castle_fortress_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(castle_fortress_levels_copy.pop(0)) + + # Valley of Bowser + shuffled_level_list.append(0x18) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(castle_fortress_levels_copy.pop(0)) + shuffled_level_list.append(castle_fortress_levels_copy.pop(0)) + + # Front/Back Door + if world.bowser_castle_doors[player] == "fast": + shuffled_level_list.append(0x82) + shuffled_level_list.append(0x32) + elif world.bowser_castle_doors[player] == "slow": + shuffled_level_list.append(0x31) + shuffled_level_list.append(0x81) + else: + shuffled_level_list.append(0x31) + shuffled_level_list.append(0x32) + + shuffled_level_list.append(0x30) + + # Star Road + shuffled_level_list.append(0x5B) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(0x53) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(0x52) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(0x57) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(0x5C) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(0x55) + + # Special Zone + shuffled_level_list.append(0x4D) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(0x48) + + return shuffled_level_list diff --git a/worlds/smw/Locations.py b/worlds/smw/Locations.py new file mode 100644 index 0000000000..a997f92f65 --- /dev/null +++ b/worlds/smw/Locations.py @@ -0,0 +1,233 @@ +import typing + +from BaseClasses import Location +from .Names import LocationName + + +class SMWLocation(Location): + game: str = "Super Mario World" + + def __init__(self, player: int, name: str = '', address: int = None, parent=None, prog_byte: int = None, prog_bit: int = None): + super().__init__(player, name, address, parent) + self.progress_byte = prog_byte + self.progress_bit = prog_bit + + +level_location_table = { + LocationName.yoshis_island_1_exit_1: 0xBC0000, + LocationName.yoshis_island_2_exit_1: 0xBC0001, + LocationName.yoshis_island_3_exit_1: 0xBC0002, + LocationName.yoshis_island_4_exit_1: 0xBC0003, + LocationName.yoshis_island_castle: 0xBC0004, + LocationName.yoshis_island_koopaling: 0xBC00A0, + + LocationName.yellow_switch_palace: 0xBC0005, + + LocationName.donut_plains_1_exit_1: 0xBC0006, + LocationName.donut_plains_1_exit_2: 0xBC0007, + LocationName.donut_plains_2_exit_1: 0xBC0008, + LocationName.donut_plains_2_exit_2: 0xBC0009, + LocationName.donut_plains_3_exit_1: 0xBC000A, + LocationName.donut_plains_4_exit_1: 0xBC000B, + LocationName.donut_secret_1_exit_1: 0xBC000C, + LocationName.donut_secret_1_exit_2: 0xBC000D, + LocationName.donut_secret_2_exit_1: 0xBC0063, + LocationName.donut_ghost_house_exit_1: 0xBC000E, + LocationName.donut_ghost_house_exit_2: 0xBC000F, + LocationName.donut_secret_house_exit_1: 0xBC0010, + LocationName.donut_secret_house_exit_2: 0xBC0011, + LocationName.donut_plains_castle: 0xBC0012, + LocationName.donut_plains_koopaling: 0xBC00A1, + + LocationName.green_switch_palace: 0xBC0013, + + LocationName.vanilla_dome_1_exit_1: 0xBC0014, + LocationName.vanilla_dome_1_exit_2: 0xBC0015, + LocationName.vanilla_dome_2_exit_1: 0xBC0016, + LocationName.vanilla_dome_2_exit_2: 0xBC0017, + LocationName.vanilla_dome_3_exit_1: 0xBC0018, + LocationName.vanilla_dome_4_exit_1: 0xBC0019, + LocationName.vanilla_secret_1_exit_1: 0xBC001A, + LocationName.vanilla_secret_1_exit_2: 0xBC001B, + LocationName.vanilla_secret_2_exit_1: 0xBC001C, + LocationName.vanilla_secret_3_exit_1: 0xBC001D, + LocationName.vanilla_ghost_house_exit_1: 0xBC001E, + LocationName.vanilla_fortress: 0xBC0020, + LocationName.vanilla_reznor: 0xBC00B0, + LocationName.vanilla_dome_castle: 0xBC0021, + LocationName.vanilla_dome_koopaling: 0xBC00A2, + + LocationName.red_switch_palace: 0xBC0022, + + LocationName.butter_bridge_1_exit_1: 0xBC0023, + LocationName.butter_bridge_2_exit_1: 0xBC0024, + LocationName.cheese_bridge_exit_1: 0xBC0025, + LocationName.cheese_bridge_exit_2: 0xBC0026, + LocationName.cookie_mountain_exit_1: 0xBC0027, + LocationName.soda_lake_exit_1: 0xBC0028, + LocationName.twin_bridges_castle: 0xBC0029, + LocationName.twin_bridges_koopaling: 0xBC00A3, + + LocationName.forest_of_illusion_1_exit_1: 0xBC002A, + LocationName.forest_of_illusion_1_exit_2: 0xBC002B, + LocationName.forest_of_illusion_2_exit_1: 0xBC002C, + LocationName.forest_of_illusion_2_exit_2: 0xBC002D, + LocationName.forest_of_illusion_3_exit_1: 0xBC002E, + LocationName.forest_of_illusion_3_exit_2: 0xBC002F, + LocationName.forest_of_illusion_4_exit_1: 0xBC0030, + LocationName.forest_of_illusion_4_exit_2: 0xBC0031, + LocationName.forest_ghost_house_exit_1: 0xBC0032, + LocationName.forest_ghost_house_exit_2: 0xBC0033, + LocationName.forest_secret_exit_1: 0xBC0034, + LocationName.forest_fortress: 0xBC0035, + LocationName.forest_reznor: 0xBC00B1, + LocationName.forest_castle: 0xBC0036, + LocationName.forest_koopaling: 0xBC00A4, + + LocationName.blue_switch_palace: 0xBC0037, + + LocationName.chocolate_island_1_exit_1: 0xBC0038, + LocationName.chocolate_island_2_exit_1: 0xBC0039, + LocationName.chocolate_island_2_exit_2: 0xBC003A, + LocationName.chocolate_island_3_exit_1: 0xBC003B, + LocationName.chocolate_island_3_exit_2: 0xBC003C, + LocationName.chocolate_island_4_exit_1: 0xBC003D, + LocationName.chocolate_island_5_exit_1: 0xBC003E, + LocationName.chocolate_ghost_house_exit_1: 0xBC003F, + LocationName.chocolate_secret_exit_1: 0xBC0041, + LocationName.chocolate_fortress: 0xBC0042, + LocationName.chocolate_reznor: 0xBC00B2, + LocationName.chocolate_castle: 0xBC0043, + LocationName.chocolate_koopaling: 0xBC00A5, + + LocationName.sunken_ghost_ship: 0xBC0044, + + LocationName.valley_of_bowser_1_exit_1: 0xBC0045, + LocationName.valley_of_bowser_2_exit_1: 0xBC0046, + LocationName.valley_of_bowser_2_exit_2: 0xBC0047, + LocationName.valley_of_bowser_3_exit_1: 0xBC0048, + LocationName.valley_of_bowser_4_exit_1: 0xBC0049, + LocationName.valley_of_bowser_4_exit_2: 0xBC004A, + LocationName.valley_ghost_house_exit_1: 0xBC004B, + LocationName.valley_ghost_house_exit_2: 0xBC004C, + LocationName.valley_fortress: 0xBC004E, + LocationName.valley_reznor: 0xBC00B3, + LocationName.valley_castle: 0xBC004F, + LocationName.valley_koopaling: 0xBC00A6, + + LocationName.star_road_1_exit_1: 0xBC0051, + LocationName.star_road_1_exit_2: 0xBC0052, + LocationName.star_road_2_exit_1: 0xBC0053, + LocationName.star_road_2_exit_2: 0xBC0054, + LocationName.star_road_3_exit_1: 0xBC0055, + LocationName.star_road_3_exit_2: 0xBC0056, + LocationName.star_road_4_exit_1: 0xBC0057, + LocationName.star_road_4_exit_2: 0xBC0058, + LocationName.star_road_5_exit_1: 0xBC0059, + LocationName.star_road_5_exit_2: 0xBC005A, + + LocationName.special_zone_1_exit_1: 0xBC005B, + LocationName.special_zone_2_exit_1: 0xBC005C, + LocationName.special_zone_3_exit_1: 0xBC005D, + LocationName.special_zone_4_exit_1: 0xBC005E, + LocationName.special_zone_5_exit_1: 0xBC005F, + LocationName.special_zone_6_exit_1: 0xBC0060, + LocationName.special_zone_7_exit_1: 0xBC0061, + LocationName.special_zone_8_exit_1: 0xBC0062, +} + +dragon_coin_location_table = { + LocationName.yoshis_island_1_dragon: 0xBC0100, + LocationName.yoshis_island_2_dragon: 0xBC0101, + LocationName.yoshis_island_3_dragon: 0xBC0102, + LocationName.yoshis_island_4_dragon: 0xBC0103, + + LocationName.donut_plains_1_dragon: 0xBC0106, + LocationName.donut_plains_2_dragon: 0xBC0108, + LocationName.donut_plains_3_dragon: 0xBC010A, + LocationName.donut_plains_4_dragon: 0xBC010B, + LocationName.donut_secret_1_dragon: 0xBC010C, + LocationName.donut_secret_2_dragon: 0xBC010D, + + LocationName.vanilla_dome_1_dragon: 0xBC0114, + LocationName.vanilla_dome_2_dragon: 0xBC0116, + LocationName.vanilla_dome_3_dragon: 0xBC0118, + LocationName.vanilla_dome_4_dragon: 0xBC0119, + LocationName.vanilla_secret_1_dragon: 0xBC011A, + LocationName.vanilla_secret_2_dragon: 0xBC011C, + LocationName.vanilla_secret_3_dragon: 0xBC011D, + LocationName.vanilla_ghost_house_dragon: 0xBC011E, + + LocationName.butter_bridge_1_dragon: 0xBC0123, + LocationName.butter_bridge_2_dragon: 0xBC0124, + LocationName.cheese_bridge_dragon: 0xBC0125, + LocationName.cookie_mountain_dragon: 0xBC0127, + LocationName.soda_lake_dragon: 0xBC0128, + + LocationName.forest_of_illusion_2_dragon: 0xBC012C, + LocationName.forest_of_illusion_3_dragon: 0xBC012E, + LocationName.forest_of_illusion_4_dragon: 0xBC0130, + LocationName.forest_ghost_house_dragon: 0xBC0132, + LocationName.forest_secret_dragon: 0xBC0134, + LocationName.forest_castle_dragon: 0xBC0136, + + LocationName.chocolate_island_1_dragon: 0xBC0138, + LocationName.chocolate_island_2_dragon: 0xBC0139, + LocationName.chocolate_island_3_dragon: 0xBC013B, + LocationName.chocolate_island_4_dragon: 0xBC013D, + LocationName.chocolate_island_5_dragon: 0xBC013E, + + LocationName.sunken_ghost_ship_dragon: 0xBC0144, + + LocationName.valley_of_bowser_1_dragon: 0xBC0145, + LocationName.valley_of_bowser_2_dragon: 0xBC0146, + LocationName.valley_of_bowser_3_dragon: 0xBC0148, + LocationName.valley_ghost_house_dragon: 0xBC014B, + LocationName.valley_castle_dragon: 0xBC014F, + + LocationName.star_road_1_dragon: 0xBC0151, + + LocationName.special_zone_1_dragon: 0xBC015B, + LocationName.special_zone_2_dragon: 0xBC015C, + LocationName.special_zone_3_dragon: 0xBC015D, + LocationName.special_zone_4_dragon: 0xBC015E, + LocationName.special_zone_5_dragon: 0xBC015F, + LocationName.special_zone_6_dragon: 0xBC0160, + LocationName.special_zone_7_dragon: 0xBC0161, + LocationName.special_zone_8_dragon: 0xBC0162, +} + +bowser_location_table = { + LocationName.bowser: 0xBC0200, +} + +yoshi_house_location_table = { + LocationName.yoshis_house: 0xBC0201, +} + +all_locations = { + **level_location_table, + **dragon_coin_location_table, + **bowser_location_table, + **yoshi_house_location_table, +} + +location_table = {} + + +def setup_locations(world, player: int): + location_table = {**level_location_table} + + # Dragon Coins here + if world.dragon_coin_checks[player].value: + location_table.update({**dragon_coin_location_table}) + + if world.goal[player] == "yoshi_egg_hunt": + location_table.update({**yoshi_house_location_table}) + else: + location_table.update({**bowser_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/smw/Names/ItemName.py b/worlds/smw/Names/ItemName.py new file mode 100644 index 0000000000..72c984b016 --- /dev/null +++ b/worlds/smw/Names/ItemName.py @@ -0,0 +1,32 @@ +# Junk Definitions +one_up_mushroom = "1-Up Mushroom" + +# Collectable Definitions +yoshi_egg = "Yoshi Egg" + +# Upgrade Definitions +mario_run = "Run" +mario_carry = "Carry" +mario_swim = "Swim" +mario_spin_jump = "Spin Jump" +mario_climb = "Climb" +yoshi_activate = "Yoshi" +p_switch = "P-Switch" +p_balloon = "P-Balloon" +progressive_powerup = "Progressive Powerup" +super_star_active = "Super Star Activate" + +# Switch Palace Definitions +yellow_switch_palace = "Yellow Switch Palace" +green_switch_palace = "Green Switch Palace" +red_switch_palace = "Red Switch Palace" +blue_switch_palace = "Blue Switch Palace" + +# Trap Definitions +ice_trap = "Ice Trap" +stun_trap = "Stun Trap" +literature_trap = "Literature Trap" + +# Other Definitions +victory = "The Princess" +koopaling = "Boss Token" diff --git a/worlds/smw/Names/LiteratureTrap.py b/worlds/smw/Names/LiteratureTrap.py new file mode 100644 index 0000000000..94c038228d --- /dev/null +++ b/worlds/smw/Names/LiteratureTrap.py @@ -0,0 +1,52 @@ +lit_trap_text_list = [ +[[0x8, 0x1f, 0x4c, 0x54, 0x52, 0x53, 0x1f, 0x4d, 0x4e, 0x53, 0x1f, 0x45, 0x44, 0x40, 0x51, 0x1b, 0x9f, 0x5, 0x44, 0x40, 0x51, 0x1f, 0x48, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x4c, 0x48, 0x4d, 0x43, 0x1c, 0x4a, 0x48, 0x4b, 0x4b, 0x44, 0x51, 0x1b, 0x1f, 0x5, 0x44, 0x40, 0x51, 0x9f, 0x48, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x4b, 0x48, 0x53, 0x53, 0x4b, 0x44, 0x1c, 0x43, 0x44, 0x40, 0x53, 0x47, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x9f, 0x41, 0x51, 0x48, 0x4d, 0x46, 0x52, 0x1f, 0x40, 0x41, 0x4e, 0x54, 0x53, 0x1f, 0x53, 0x4e, 0x53, 0x40, 0xcb, 0x4e, 0x41, 0x4b, 0x48, 0x53, 0x44, 0x51, 0x40, 0x53, 0x48, 0x4e, 0x4d, 0x1b, 0x1f, 0x8, 0x9f, 0x56, 0x48, 0x4b, 0x4b, 0x1f, 0x45, 0x40, 0x42, 0x44, 0x1f, 0x4c, 0x58, 0x1f, 0x45, 0x44, 0x40, 0x51, 0x9b, ], [0x8, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x1f, 0x4f, 0x44, 0x51, 0x4c, 0x48, 0x53, 0x1f, 0x48, 0x53, 0x9f, 0x53, 0x4e, 0x1f, 0x4f, 0x40, 0x52, 0x52, 0x1f, 0x4e, 0x55, 0x44, 0x51, 0x1f, 0x4c, 0x44, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x53, 0x47, 0x51, 0x4e, 0x54, 0x46, 0x47, 0x1f, 0x4c, 0x44, 0x1b, 0x9f, 0x0, 0x4d, 0x43, 0x1f, 0x56, 0x47, 0x44, 0x4d, 0x1f, 0x48, 0x53, 0x1f, 0x47, 0x40, 0x52, 0x9f, 0x46, 0x4e, 0x4d, 0x44, 0x1f, 0x4f, 0x40, 0x52, 0x53, 0x1f, 0x8, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x9f, 0x53, 0x54, 0x51, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x48, 0x4d, 0x4d, 0x44, 0x51, 0x1f, 0x44, 0x58, 0xc4, 0x53, 0x4e, 0x1f, 0x52, 0x44, 0x44, 0x1f, 0x48, 0x53, 0x52, 0x1f, 0x4f, 0x40, 0x53, 0x47, 0x1b, 0x9f, 0x16, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x45, 0x44, 0x40, 0x51, 0x1f, 0x47, 0x40, 0xd2, ], [0x46, 0x4e, 0x4d, 0x44, 0x1d, 0x1f, 0x53, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x9f, 0x41, 0x44, 0x1f, 0x4d, 0x4e, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x1b, 0x1f, 0xe, 0x4d, 0x4b, 0x58, 0x1f, 0x88, 0x56, 0x48, 0x4b, 0x4b, 0x1f, 0x51, 0x44, 0x4c, 0x40, 0x48, 0x4d, 0x1b, 0x9f, 0x1c, 0x7, 0x44, 0x51, 0x41, 0x44, 0x51, 0x53, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x0, 0x41, 0x4e, 0x54, 0x53, 0x1f, 0x53, 0x47, 0x51, 0x44, 0x44, 0x1f, 0x53, 0x47, 0x48, 0x4d, 0x46, 0xd2, 0x8, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x40, 0x41, 0x52, 0x4e, 0x4b, 0x54, 0x53, 0x44, 0x4b, 0x58, 0x9f, 0x4f, 0x4e, 0x52, 0x48, 0x53, 0x48, 0x55, 0x44, 0x1b, 0x1f, 0x5, 0x48, 0x51, 0x52, 0x53, 0x1d, 0x9f, 0x4, 0x43, 0x56, 0x40, 0x51, 0x43, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x40, 0x9f, 0x55, 0x40, 0x4c, 0x4f, 0x48, 0x51, 0x44, 0x1b, 0x1f, 0x12, 0x44, 0x42, 0x4e, 0x4d, 0x43, 0x1d, 0x9f, 0x53, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x40, 0x1f, 0x4f, 0x40, 0x51, 0x53, 0x9f, 0x4e, 0x45, 0x1f, 0x47, 0x48, 0x4c, 0x1f, 0x1c, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x8, 0x9f, 0x43, 0x48, 0x43, 0x4d, 0x5d, 0x53, 0x1f, 0x4a, 0x4d, 0x4e, 0x56, 0x1f, 0x47, 0x4e, 0x56, 0x9f, ], [0x4f, 0x4e, 0x53, 0x44, 0x4d, 0x53, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x4f, 0x40, 0x51, 0x53, 0x9f, 0x4c, 0x48, 0x46, 0x47, 0x53, 0x1f, 0x41, 0x44, 0x1f, 0x1c, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x9f, 0x53, 0x47, 0x48, 0x51, 0x52, 0x53, 0x44, 0x43, 0x1f, 0x45, 0x4e, 0x51, 0x1f, 0x4c, 0x58, 0x9f, 0x41, 0x4b, 0x4e, 0x4e, 0x43, 0x1b, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x53, 0x47, 0x48, 0x51, 0x43, 0x1d, 0x9f, 0x8, 0x1f, 0x56, 0x40, 0x52, 0x9f, 0x54, 0x4d, 0x42, 0x4e, 0x4d, 0x43, 0x48, 0x53, 0x48, 0x4e, 0x4d, 0x40, 0x4b, 0x4b, 0x58, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x48, 0x51, 0x51, 0x44, 0x55, 0x4e, 0x42, 0x40, 0x41, 0x4b, 0x58, 0x1f, 0x48, 0xcd, 0x4b, 0x4e, 0x55, 0x44, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x1f, 0x47, 0x48, 0x4c, 0x1b, 0x9f, ], [0x1c, 0xc, 0x44, 0x58, 0x44, 0x51, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x12, 0x40, 0x58, 0x1a, 0x1f, 0x8, 0x1f, 0x4b, 0x48, 0x4a, 0x44, 0x1f, 0x46, 0x51, 0x44, 0x44, 0x4d, 0x9f, 0x44, 0x46, 0x46, 0x52, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x47, 0x40, 0x4c, 0x1a, 0x1f, 0x8, 0x9f, 0x43, 0x4e, 0x1a, 0x1f, 0x8, 0x1f, 0x4b, 0x48, 0x4a, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x1d, 0x9f, 0x12, 0x40, 0x4c, 0x1c, 0x8, 0x1c, 0x40, 0x4c, 0x1a, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x8, 0x9f, 0x56, 0x4e, 0x54, 0x4b, 0x43, 0x1f, 0x44, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x1f, 0x48, 0x4d, 0x9f, 0x40, 0x1f, 0x41, 0x4e, 0x40, 0x53, 0x1b, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x8, 0x9f, 0x56, 0x4e, 0x54, 0x4b, 0x43, 0x1f, 0x44, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x9f, 0x56, 0x48, 0x53, 0x47, 0x1f, 0x40, 0x1f, 0x46, 0x4e, 0x40, 0x53, 0x1b, 0x1b, 0x1b, 0x1f, 0x0, 0x4d, 0xc3, ], [0x8, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x1f, 0x44, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x1f, 0x48, 0xcd, 0x53, 0x47, 0x44, 0x1f, 0x51, 0x40, 0x48, 0x4d, 0x1b, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x48, 0x4d, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x43, 0x40, 0x51, 0x4a, 0x1b, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x4e, 0x4d, 0x1f, 0xc0, 0x53, 0x51, 0x40, 0x48, 0x4d, 0x1b, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x48, 0x4d, 0x1f, 0x40, 0x9f, 0x42, 0x40, 0x51, 0x1b, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x48, 0x4d, 0x1f, 0x40, 0x9f, 0x53, 0x51, 0x44, 0x44, 0x1b, 0x1f, 0x13, 0x47, 0x44, 0x58, 0x1f, 0x40, 0x51, 0x44, 0x1f, 0x52, 0x4e, 0x9f, 0x46, 0x4e, 0x4e, 0x43, 0x1d, 0x1f, 0x52, 0x4e, 0x1f, 0x46, 0x4e, 0x4e, 0x43, 0x1d, 0x1f, 0x58, 0x4e, 0xd4, 0x52, 0x44, 0x44, 0x1a, 0x1f, 0x12, 0x4e, 0x1f, 0x8, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x1f, 0x44, 0x40, 0xd3, ], [0x53, 0x47, 0x44, 0x4c, 0x1f, 0x48, 0x4d, 0x1f, 0x40, 0x1f, 0x41, 0x4e, 0x57, 0x1b, 0x1f, 0x0, 0x4d, 0xc3, 0x8, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x1f, 0x44, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x9f, 0x56, 0x48, 0x53, 0x47, 0x1f, 0x40, 0x1f, 0x45, 0x4e, 0x57, 0x1b, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x8, 0x9f, 0x56, 0x48, 0x4b, 0x4b, 0x1f, 0x44, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x1f, 0x48, 0x4d, 0x1f, 0xc0, 0x47, 0x4e, 0x54, 0x52, 0x44, 0x1b, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x8, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x9f, 0x44, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x1f, 0x40, 0x9f, 0x4c, 0x4e, 0x54, 0x52, 0x44, 0x1b, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x8, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x9f, 0x44, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x1f, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x40, 0x4d, 0x43, 0x9f, ], [0x53, 0x47, 0x44, 0x51, 0x44, 0x1b, 0x1f, 0x12, 0x40, 0x58, 0x1a, 0x1f, 0x8, 0x1f, 0x56, 0x48, 0x4b, 0xcb, 0x44, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x1f, 0x40, 0x4d, 0x58, 0x56, 0x47, 0x44, 0x51, 0x44, 0x9a, 0x8, 0x1f, 0x43, 0x4e, 0x1f, 0x52, 0x4e, 0x1f, 0x4b, 0x48, 0x4a, 0x44, 0x1f, 0x46, 0x51, 0x44, 0x44, 0xcd, 0x44, 0x46, 0x46, 0x52, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x47, 0x40, 0x4c, 0x1a, 0x9f, 0x13, 0x47, 0x40, 0x4d, 0x4a, 0x1f, 0x58, 0x4e, 0x54, 0x1a, 0x1f, 0x13, 0x47, 0x40, 0x4d, 0x4a, 0x9f, 0x58, 0x4e, 0x54, 0x1d, 0x1f, 0x12, 0x40, 0x4c, 0x1c, 0x8, 0x1c, 0x40, 0x4c, 0x1a, 0x9f, 0x1c, 0x12, 0x44, 0x54, 0x52, 0x52, 0x1f, 0x9f, 0x9f, ]], +[[0x1, 0x54, 0x53, 0x1d, 0x1f, 0x52, 0x4e, 0x45, 0x53, 0x1a, 0x1f, 0x56, 0x47, 0x40, 0x53, 0x9f, 0x4b, 0x48, 0x46, 0x47, 0x53, 0x1f, 0x53, 0x47, 0x51, 0x4e, 0x54, 0x46, 0x47, 0x9f, 0x58, 0x4e, 0x4d, 0x43, 0x44, 0x51, 0x1f, 0x56, 0x48, 0x4d, 0x43, 0x4e, 0x56, 0x9f, 0x41, 0x51, 0x44, 0x40, 0x4a, 0x52, 0x1e, 0x1f, 0x8, 0x53, 0x1f, 0x48, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x44, 0x40, 0x52, 0x53, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x9, 0x54, 0x4b, 0x48, 0x44, 0x53, 0x9f, 0x48, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x52, 0x54, 0x4d, 0x1b, 0x1f, 0x0, 0x51, 0x48, 0x52, 0x44, 0x9d, 0x45, 0x40, 0x48, 0x51, 0x1f, 0x52, 0x54, 0x4d, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x4a, 0x48, 0x4b, 0xcb, 0x53, 0x47, 0x44, 0x1f, 0x44, 0x4d, 0x55, 0x48, 0x4e, 0x54, 0x52, 0x1f, 0x4c, 0x4e, 0x4e, 0x4d, 0x1d, 0x9f, ], [0x16, 0x47, 0x4e, 0x1f, 0x48, 0x52, 0x1f, 0x40, 0x4b, 0x51, 0x44, 0x40, 0x43, 0x58, 0x9f, 0x52, 0x48, 0x42, 0x4a, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x4f, 0x40, 0x4b, 0x44, 0x1f, 0x56, 0x48, 0x53, 0xc7, 0x46, 0x51, 0x48, 0x44, 0x45, 0x1d, 0x1f, 0x13, 0x47, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x4e, 0x54, 0x9f, 0x47, 0x44, 0x51, 0x1f, 0x4c, 0x40, 0x48, 0x43, 0x1f, 0x40, 0x51, 0x53, 0x1f, 0x45, 0x40, 0x51, 0x9f, 0x4c, 0x4e, 0x51, 0x44, 0x1f, 0x45, 0x40, 0x48, 0x51, 0x1f, 0x53, 0x47, 0x40, 0x4d, 0x9f, 0x52, 0x47, 0x44, 0x1b, 0x1f, 0x1, 0x44, 0x1f, 0x4d, 0x4e, 0x53, 0x1f, 0x47, 0x44, 0x51, 0x9f, 0x4c, 0x40, 0x48, 0x43, 0x1d, 0x1f, 0x52, 0x48, 0x4d, 0x42, 0x44, 0x1f, 0x52, 0x47, 0x44, 0x1f, 0x48, 0xd2, 0x44, 0x4d, 0x55, 0x48, 0x4e, 0x54, 0x52, 0x1b, 0x1f, 0x7, 0x44, 0x51, 0x9f, ], [0x55, 0x44, 0x52, 0x53, 0x40, 0x4b, 0x1f, 0x4b, 0x48, 0x55, 0x44, 0x51, 0x58, 0x1f, 0x48, 0x52, 0x9f, 0x41, 0x54, 0x53, 0x1f, 0x52, 0x48, 0x42, 0x4a, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x46, 0x51, 0x44, 0x44, 0xcd, 0x0, 0x4d, 0x43, 0x1f, 0x4d, 0x4e, 0x4d, 0x44, 0x1f, 0x41, 0x54, 0x53, 0x1f, 0x45, 0x4e, 0x4e, 0x4b, 0xd2, 0x43, 0x4e, 0x1f, 0x56, 0x44, 0x40, 0x51, 0x1f, 0x48, 0x53, 0x1b, 0x1f, 0x42, 0x40, 0x52, 0x53, 0x9f, 0x48, 0x53, 0x1f, 0x4e, 0x45, 0x45, 0x1b, 0x1f, 0x8, 0x53, 0x1f, 0x48, 0x52, 0x1f, 0x4c, 0x58, 0x9f, 0x4b, 0x40, 0x43, 0x58, 0x1d, 0x1f, 0xe, 0x1d, 0x1f, 0x48, 0x53, 0x1f, 0x48, 0x52, 0x1f, 0x4c, 0x58, 0x9f, 0x4b, 0x4e, 0x55, 0x44, 0x1a, 0x1f, 0xe, 0x1d, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x52, 0x47, 0x44, 0x9f, 0x4a, 0x4d, 0x44, 0x56, 0x1f, 0x52, 0x47, 0x44, 0x1f, 0x56, 0x44, 0x51, 0x44, 0x1a, 0x9f, ], [0x1c, 0x12, 0x47, 0x40, 0x4a, 0x44, 0x52, 0x4f, 0x44, 0x40, 0x51, 0x44, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x8, 0x53, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x41, 0x44, 0x52, 0x53, 0x1f, 0x4e, 0xc5, 0x53, 0x48, 0x4c, 0x44, 0x52, 0x1d, 0x1f, 0x48, 0x53, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x56, 0x4e, 0x51, 0x52, 0x53, 0x1f, 0x4e, 0x45, 0x1f, 0x53, 0x48, 0x4c, 0x44, 0x52, 0x1d, 0x1f, 0x48, 0xd3, 0x56, 0x40, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x40, 0x46, 0x44, 0x1f, 0x4e, 0x45, 0x9f, 0x56, 0x48, 0x52, 0x43, 0x4e, 0x4c, 0x1d, 0x1f, 0x48, 0x53, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x53, 0x47, 0xc4, 0x40, 0x46, 0x44, 0x1f, 0x4e, 0x45, 0x9f, 0x45, 0x4e, 0x4e, 0x4b, 0x48, 0x52, 0x47, 0x4d, 0x44, 0x52, 0x52, 0x1d, 0x1f, 0x48, 0x53, 0x9f, 0x56, 0x40, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x44, 0x4f, 0x4e, 0x42, 0x47, 0x1f, 0x4e, 0x45, 0x9f, ], [0x41, 0x44, 0x4b, 0x48, 0x44, 0x45, 0x1d, 0x1f, 0x48, 0x53, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x53, 0x47, 0xc4, 0x44, 0x4f, 0x4e, 0x42, 0x47, 0x1f, 0x4e, 0x45, 0x9f, 0x48, 0x4d, 0x42, 0x51, 0x44, 0x43, 0x54, 0x4b, 0x48, 0x53, 0x58, 0x1d, 0x1f, 0x48, 0x53, 0x9f, 0x56, 0x40, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x52, 0x44, 0x40, 0x52, 0x4e, 0x4d, 0x1f, 0x4e, 0x45, 0x9f, 0x4b, 0x48, 0x46, 0x47, 0x53, 0x1d, 0x1f, 0x48, 0x53, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x52, 0x44, 0x40, 0x52, 0x4e, 0x4d, 0x1f, 0x4e, 0x45, 0x9f, 0x43, 0x40, 0x51, 0x4a, 0x4d, 0x44, 0x52, 0x52, 0x1d, 0x1f, 0x48, 0x53, 0x1f, 0x56, 0x40, 0x52, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x52, 0x4f, 0x51, 0x48, 0x4d, 0x46, 0x1f, 0x4e, 0x45, 0x9f, ], [0x47, 0x4e, 0x4f, 0x44, 0x1d, 0x1f, 0x48, 0x53, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x56, 0x48, 0x4d, 0x53, 0x44, 0x51, 0x1f, 0x4e, 0x45, 0x1f, 0x43, 0x44, 0x52, 0x4f, 0x40, 0x48, 0x51, 0x9b, 0x1c, 0x3, 0x48, 0x42, 0x4a, 0x44, 0x4d, 0x52, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0xc, 0x40, 0x51, 0x4b, 0x44, 0x58, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x43, 0x44, 0x40, 0x43, 0x1d, 0x9f, 0x53, 0x4e, 0x1f, 0x41, 0x44, 0x46, 0x48, 0x4d, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x1b, 0x9f, 0x13, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x48, 0x52, 0x1f, 0x4d, 0x4e, 0x1f, 0x43, 0x4e, 0x54, 0x41, 0x53, 0x9f, 0x56, 0x47, 0x40, 0x53, 0x44, 0x55, 0x44, 0x51, 0x1d, 0x1f, 0x40, 0x41, 0x4e, 0x54, 0x53, 0x9f, 0x53, 0x47, 0x40, 0x53, 0x1b, 0x1f, 0x13, 0x47, 0x44, 0x1f, 0x51, 0x44, 0x46, 0x48, 0x52, 0x53, 0x44, 0xd1, 0x4e, 0x45, 0x1f, 0x47, 0x48, 0x52, 0x1f, 0x41, 0x54, 0x51, 0x48, 0x40, 0x4b, 0x1f, 0x56, 0x40, 0x52, 0x9f, 0x52, 0x48, 0x46, 0x4d, 0x44, 0x43, 0x1f, 0x41, 0x58, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x42, 0x4b, 0x44, 0x51, 0x46, 0x58, 0x4c, 0x40, 0x4d, 0x1d, 0x1f, 0x53, 0x47, 0x44, 0x9f, ], [0x42, 0x4b, 0x44, 0x51, 0x4a, 0x1d, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x54, 0x4d, 0x43, 0x44, 0x51, 0x53, 0x40, 0x4a, 0x44, 0x51, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x42, 0x47, 0x48, 0x44, 0x45, 0x1f, 0x4c, 0x4e, 0x54, 0x51, 0x4d, 0x44, 0x51, 0x9b, 0x12, 0x42, 0x51, 0x4e, 0x4e, 0x46, 0x44, 0x1f, 0x52, 0x48, 0x46, 0x4d, 0x44, 0x43, 0x1f, 0x48, 0x53, 0x9b, 0x40, 0x4d, 0x43, 0x1f, 0x12, 0x42, 0x51, 0x4e, 0x4e, 0x46, 0x44, 0x5d, 0x52, 0x1f, 0x4d, 0x40, 0x4c, 0xc4, 0x56, 0x40, 0x52, 0x1f, 0x46, 0x4e, 0x4e, 0x43, 0x1f, 0x54, 0x4f, 0x4e, 0x4d, 0x9f, 0x5d, 0x42, 0x47, 0x40, 0x4d, 0x46, 0x44, 0x1d, 0x1f, 0x45, 0x4e, 0x51, 0x9f, 0x40, 0x4d, 0x58, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x1f, 0x47, 0x44, 0x1f, 0x42, 0x47, 0x4e, 0x52, 0x44, 0x9f, ], [0x53, 0x4e, 0x1f, 0x4f, 0x54, 0x53, 0x1f, 0x47, 0x48, 0x52, 0x1f, 0x47, 0x40, 0x4d, 0x43, 0x9f, 0x53, 0x4e, 0x1b, 0x1f, 0xe, 0x4b, 0x43, 0x1f, 0xc, 0x40, 0x51, 0x4b, 0x44, 0x58, 0x1f, 0x56, 0x40, 0xd2, 0x40, 0x52, 0x1f, 0x43, 0x44, 0x40, 0x43, 0x1f, 0x40, 0x52, 0x1f, 0x40, 0x9f, 0x43, 0x4e, 0x4e, 0x51, 0x1c, 0x4d, 0x40, 0x48, 0x4b, 0x1b, 0x9f, 0x1c, 0x3, 0x48, 0x42, 0x4a, 0x44, 0x4d, 0x52, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0xc, 0x40, 0x4d, 0x58, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x4b, 0x48, 0x55, 0x44, 0x9f, 0x43, 0x44, 0x52, 0x44, 0x51, 0x55, 0x44, 0x1f, 0x43, 0x44, 0x40, 0x53, 0x47, 0x1b, 0x1f, 0x0, 0x4d, 0xc3, 0x52, 0x4e, 0x4c, 0x44, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x43, 0x48, 0x44, 0x9f, 0x43, 0x44, 0x52, 0x44, 0x51, 0x55, 0x44, 0x1f, 0x4b, 0x48, 0x45, 0x44, 0x1b, 0x1f, 0x2, 0x40, 0x4d, 0x9f, 0x58, 0x4e, 0x54, 0x1f, 0x46, 0x48, 0x55, 0x44, 0x1f, 0x48, 0x53, 0x1f, 0x53, 0x4e, 0x9f, 0x53, 0x47, 0x44, 0x4c, 0x1e, 0x1f, 0x13, 0x47, 0x44, 0x4d, 0x1f, 0x43, 0x4e, 0x1f, 0x4d, 0x4e, 0x53, 0x9f, 0x41, 0x44, 0x1f, 0x53, 0x4e, 0x4e, 0x1f, 0x44, 0x40, 0x46, 0x44, 0x51, 0x1f, 0x53, 0x4e, 0x9f, 0x43, 0x44, 0x40, 0x4b, 0x1f, 0x4e, 0x54, 0x53, 0x1f, 0x43, 0x44, 0x40, 0x53, 0x47, 0x1f, 0x48, 0x4d, 0x9f, ], [0x49, 0x54, 0x43, 0x46, 0x44, 0x4c, 0x44, 0x4d, 0x53, 0x1b, 0x1f, 0x5, 0x4e, 0x51, 0x9f, 0x44, 0x55, 0x44, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x55, 0x44, 0x51, 0x58, 0x1f, 0x56, 0x48, 0x52, 0xc4, 0x42, 0x40, 0x4d, 0x4d, 0x4e, 0x53, 0x1f, 0x52, 0x44, 0x44, 0x1f, 0x40, 0x4b, 0x4b, 0x9f, 0x44, 0x4d, 0x43, 0x52, 0x1b, 0x1f, 0x1c, 0x13, 0x4e, 0x4b, 0x4a, 0x48, 0x44, 0x4d, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x8, 0x53, 0x1f, 0x48, 0x52, 0x1f, 0x40, 0x1f, 0x53, 0x51, 0x54, 0x53, 0x47, 0x9f, 0x54, 0x4d, 0x48, 0x55, 0x44, 0x51, 0x52, 0x40, 0x4b, 0x4b, 0x58, 0x9f, 0x40, 0x42, 0x4a, 0x4d, 0x4e, 0x56, 0x4b, 0x44, 0x43, 0x46, 0x44, 0x43, 0x1d, 0x1f, 0x53, 0x47, 0x40, 0xd3, 0x40, 0x1f, 0x52, 0x48, 0x4d, 0x46, 0x4b, 0x44, 0x1f, 0x4c, 0x40, 0x4d, 0x1f, 0x48, 0x4d, 0x9f, 0x4f, 0x4e, 0x52, 0x52, 0x44, 0x52, 0x52, 0x48, 0x4e, 0x4d, 0x1f, 0x4e, 0x45, 0x1f, 0x40, 0x9f, 0x46, 0x4e, 0x4e, 0x43, 0x1f, 0x45, 0x4e, 0x51, 0x53, 0x54, 0x4d, 0x44, 0x1d, 0x1f, 0x4c, 0x54, 0x52, 0xd3, 0x41, 0x44, 0x1f, 0x48, 0x4d, 0x1f, 0x56, 0x40, 0x4d, 0x53, 0x1f, 0x4e, 0x45, 0x1f, 0x40, 0x9f, 0x56, 0x48, 0x45, 0x44, 0x1b, 0x1f, 0x1c, 0x0, 0x54, 0x52, 0x53, 0x44, 0x4d, 0x1f, 0x9f, ]], +[[0x13, 0x47, 0x44, 0x1f, 0x53, 0x51, 0x54, 0x53, 0x47, 0x1f, 0x40, 0x4b, 0x56, 0x40, 0x58, 0x52, 0x9f, 0x42, 0x40, 0x51, 0x51, 0x48, 0x44, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x40, 0x4c, 0x41, 0x48, 0x46, 0x54, 0x48, 0x53, 0x58, 0x1f, 0x4e, 0x45, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x56, 0x4e, 0x51, 0x43, 0x52, 0x1f, 0x54, 0x52, 0x44, 0x43, 0x1f, 0x53, 0x4e, 0x9f, 0x44, 0x57, 0x4f, 0x51, 0x44, 0x52, 0x52, 0x1f, 0x48, 0x53, 0x1b, 0x9f, 0x1c, 0x7, 0x44, 0x51, 0x41, 0x44, 0x51, 0x53, 0x1f, 0x9f, 0x9f, 0x9f, ]], +[[0x5d, 0x8, 0x1f, 0x43, 0x40, 0x51, 0x44, 0x52, 0x40, 0x58, 0x1f, 0x58, 0x4e, 0x54, 0x9f, 0x47, 0x40, 0x55, 0x44, 0x4d, 0x5d, 0x53, 0x1f, 0x47, 0x40, 0x43, 0x1f, 0x4c, 0x54, 0x42, 0x47, 0x9f, 0x4f, 0x51, 0x40, 0x42, 0x53, 0x48, 0x42, 0x44, 0x1d, 0x5d, 0x1f, 0x52, 0x40, 0x48, 0x43, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x10, 0x54, 0x44, 0x44, 0x4d, 0x1b, 0x1f, 0x5d, 0x16, 0x47, 0x44, 0x4d, 0x1f, 0x88, 0x56, 0x40, 0x52, 0x1f, 0x58, 0x4e, 0x54, 0x51, 0x1f, 0x40, 0x46, 0x44, 0x1d, 0x1f, 0x8, 0x9f, 0x40, 0x4b, 0x56, 0x40, 0x58, 0x52, 0x1f, 0x43, 0x48, 0x43, 0x1f, 0x48, 0x53, 0x1f, 0x45, 0x4e, 0x51, 0x9f, 0x47, 0x40, 0x4b, 0x45, 0x1c, 0x40, 0x4d, 0x1c, 0x47, 0x4e, 0x54, 0x51, 0x1f, 0x40, 0x9f, 0x43, 0x40, 0x58, 0x1b, 0x1f, 0x16, 0x47, 0x58, 0x1d, 0x9f, ], [0x52, 0x4e, 0x4c, 0x44, 0x53, 0x48, 0x4c, 0x44, 0x52, 0x1f, 0x8, 0x5d, 0x55, 0x44, 0x9f, 0x41, 0x44, 0x4b, 0x48, 0x44, 0x55, 0x44, 0x43, 0x1f, 0x40, 0x52, 0x1f, 0x4c, 0x40, 0x4d, 0x58, 0x9f, 0x40, 0x52, 0x1f, 0x52, 0x48, 0x57, 0x1f, 0x48, 0x4c, 0x4f, 0x4e, 0x52, 0x52, 0x48, 0x41, 0x4b, 0x44, 0x9f, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x52, 0x1f, 0x41, 0x44, 0x45, 0x4e, 0x51, 0x44, 0x9f, 0x41, 0x51, 0x44, 0x40, 0x4a, 0x45, 0x40, 0x52, 0x53, 0x1b, 0x5d, 0x9f, 0x1c, 0x2, 0x40, 0x51, 0x51, 0x4e, 0x4b, 0x4b, 0x1f, 0x9f, 0x9f, 0x9f, ]], +[[0xb, 0x48, 0x45, 0x44, 0x1d, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x1f, 0x48, 0x53, 0x52, 0x9f, 0x51, 0x54, 0x4b, 0x44, 0x52, 0x1d, 0x1f, 0x48, 0x53, 0x52, 0x9f, 0x4e, 0x41, 0x4b, 0x48, 0x46, 0x40, 0x53, 0x48, 0x4e, 0x4d, 0x52, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x48, 0x53, 0x52, 0x1f, 0x45, 0x51, 0x44, 0x44, 0x43, 0x4e, 0x4c, 0x52, 0x1d, 0x1f, 0x48, 0x52, 0x9f, 0x4b, 0x48, 0x4a, 0x44, 0x1f, 0x40, 0x1f, 0x52, 0x4e, 0x4d, 0x4d, 0x44, 0x53, 0x1b, 0x9f, 0x18, 0x4e, 0x54, 0x5d, 0x51, 0x44, 0x1f, 0x46, 0x48, 0x55, 0x44, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x45, 0x4e, 0x51, 0x4c, 0x1d, 0x1f, 0x41, 0x54, 0x53, 0x1f, 0x58, 0x4e, 0x54, 0x1f, 0x47, 0x40, 0x55, 0xc4, 0x53, 0x4e, 0x1f, 0x56, 0x51, 0x48, 0x53, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x9f, ], [0x52, 0x4e, 0x4d, 0x4d, 0x44, 0x53, 0x1f, 0x58, 0x4e, 0x54, 0x51, 0x52, 0x44, 0x4b, 0x45, 0x1b, 0x9f, 0x1c, 0xb, 0x5d, 0x4, 0x4d, 0x46, 0x4b, 0x44, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0xb, 0x48, 0x4a, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x4c, 0x4e, 0x4e, 0x4d, 0x1f, 0x4e, 0x55, 0x44, 0xd1, 0x53, 0x47, 0x44, 0x1f, 0x43, 0x40, 0x58, 0x1f, 0x4c, 0x58, 0x1f, 0x46, 0x44, 0x4d, 0x48, 0x54, 0x52, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x41, 0x51, 0x40, 0x56, 0x4d, 0x1f, 0x40, 0x51, 0x44, 0x1f, 0x4b, 0x4e, 0x52, 0xd3, 0x4e, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x52, 0x44, 0x1f, 0x45, 0x4e, 0x4e, 0x4b, 0x52, 0x9f, 0x1c, 0x7, 0x40, 0x48, 0x4a, 0x54, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x8, 0x4d, 0x1f, 0x40, 0x1f, 0x47, 0x4e, 0x4b, 0x44, 0x1f, 0x48, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x46, 0x51, 0x4e, 0x54, 0x4d, 0x43, 0x1f, 0x53, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x4b, 0x48, 0x55, 0x44, 0xc3, 0x40, 0x1f, 0x47, 0x4e, 0x41, 0x41, 0x48, 0x53, 0x1b, 0x1f, 0xd, 0x4e, 0x53, 0x1f, 0x40, 0x9f, 0x4d, 0x40, 0x52, 0x53, 0x58, 0x1d, 0x1f, 0x43, 0x48, 0x51, 0x53, 0x58, 0x1d, 0x1f, 0x56, 0x44, 0x53, 0x9f, 0x47, 0x4e, 0x4b, 0x44, 0x1d, 0x1f, 0x45, 0x48, 0x4b, 0x4b, 0x44, 0x43, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x44, 0x4d, 0x43, 0x52, 0x1f, 0x4e, 0x45, 0x1f, 0x56, 0x4e, 0x51, 0x4c, 0x52, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x40, 0x4d, 0x1f, 0x4e, 0x4e, 0x59, 0x58, 0x1f, 0x52, 0x4c, 0x44, 0x4b, 0x4b, 0x9d, 0x4d, 0x4e, 0x51, 0x1f, 0x58, 0x44, 0x53, 0x1f, 0x40, 0x1f, 0x43, 0x51, 0x58, 0x1d, 0x9f, ], [0x41, 0x40, 0x51, 0x44, 0x1d, 0x1f, 0x52, 0x40, 0x4d, 0x43, 0x58, 0x1f, 0x47, 0x4e, 0x4b, 0x44, 0x9f, 0x56, 0x48, 0x53, 0x47, 0x1f, 0x4d, 0x4e, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x1f, 0x48, 0x4d, 0x1f, 0x48, 0xd3, 0x53, 0x4e, 0x1f, 0x52, 0x48, 0x53, 0x1f, 0x43, 0x4e, 0x56, 0x4d, 0x1f, 0x4e, 0x4d, 0x1f, 0x4e, 0x51, 0x9f, 0x53, 0x4e, 0x1f, 0x44, 0x40, 0x53, 0x1b, 0x1f, 0x48, 0x53, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x40, 0x9f, 0x47, 0x4e, 0x41, 0x41, 0x48, 0x53, 0x1c, 0x47, 0x4e, 0x4b, 0x44, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x4c, 0x44, 0x40, 0x4d, 0x52, 0x9f, 0x42, 0x4e, 0x4c, 0x45, 0x4e, 0x51, 0x53, 0x1b, 0x1f, 0x1c, 0x13, 0x4e, 0x4b, 0x4a, 0x48, 0x44, 0x4d, 0x1f, 0x9f, 0x9f, ]], +[[0x5d, 0x6, 0x4e, 0x4e, 0x43, 0x1f, 0xc, 0x4e, 0x51, 0x4d, 0x48, 0x4d, 0x46, 0x1a, 0x5d, 0x9f, 0x52, 0x40, 0x48, 0x43, 0x1f, 0x1, 0x48, 0x4b, 0x41, 0x4e, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x47, 0xc4, 0x4c, 0x44, 0x40, 0x4d, 0x53, 0x1f, 0x48, 0x53, 0x1b, 0x1f, 0x13, 0x47, 0x44, 0x1f, 0x52, 0x54, 0x4d, 0x9f, 0x56, 0x40, 0x52, 0x1f, 0x52, 0x47, 0x48, 0x4d, 0x48, 0x4d, 0x46, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x46, 0x51, 0x40, 0x52, 0x52, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x55, 0x44, 0x51, 0xd8, 0x46, 0x51, 0x44, 0x44, 0x4d, 0x1b, 0x1f, 0x1, 0x54, 0x53, 0x1f, 0x6, 0x40, 0x4d, 0x43, 0x40, 0x4b, 0xc5, 0x4b, 0x4e, 0x4e, 0x4a, 0x44, 0x43, 0x1f, 0x40, 0x53, 0x1f, 0x47, 0x48, 0x4c, 0x1f, 0x45, 0x51, 0x4e, 0xcc, 0x54, 0x4d, 0x43, 0x44, 0x51, 0x1f, 0x4b, 0x4e, 0x4d, 0x46, 0x1f, 0x41, 0x54, 0x52, 0x47, 0x58, 0x9f, ], [0x44, 0x58, 0x44, 0x41, 0x51, 0x4e, 0x56, 0x52, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x9f, 0x52, 0x53, 0x54, 0x42, 0x4a, 0x1f, 0x4e, 0x54, 0x53, 0x1f, 0x45, 0x54, 0x51, 0x53, 0x47, 0x44, 0x51, 0x9f, 0x53, 0x47, 0x40, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x41, 0x51, 0x48, 0x4c, 0x1f, 0x4e, 0x45, 0x9f, 0x47, 0x48, 0x52, 0x1f, 0x52, 0x47, 0x40, 0x43, 0x58, 0x1f, 0x47, 0x40, 0x53, 0x1b, 0x9f, 0x5d, 0x16, 0x47, 0x40, 0x53, 0x1f, 0x43, 0x4e, 0x1f, 0x58, 0x4e, 0x54, 0x9f, 0x4c, 0x44, 0x40, 0x4d, 0x1e, 0x5d, 0x1f, 0x47, 0x44, 0x1f, 0x52, 0x40, 0x48, 0x43, 0x1b, 0x9f, 0x5d, 0x3, 0x4e, 0x1f, 0x58, 0x4e, 0x54, 0x1f, 0x56, 0x48, 0x52, 0x47, 0x1f, 0x4c, 0x44, 0x1f, 0x40, 0x9f, 0x46, 0x4e, 0x4e, 0x43, 0x1f, 0x4c, 0x4e, 0x51, 0x4d, 0x48, 0x4d, 0x46, 0x1d, 0x1f, 0x4e, 0x51, 0x9f, ], [0x4c, 0x44, 0x40, 0x4d, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x48, 0x53, 0x1f, 0x48, 0x52, 0x1f, 0x40, 0x9f, 0x46, 0x4e, 0x4e, 0x43, 0x1f, 0x4c, 0x4e, 0x51, 0x4d, 0x48, 0x4d, 0x46, 0x9f, 0x56, 0x47, 0x44, 0x53, 0x47, 0x44, 0x51, 0x1f, 0x8, 0x1f, 0x56, 0x40, 0x4d, 0x53, 0x1f, 0x48, 0x53, 0x9f, 0x4e, 0x51, 0x1f, 0x4d, 0x4e, 0x53, 0x1d, 0x1f, 0x4e, 0x51, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x9f, 0x58, 0x4e, 0x54, 0x1f, 0x45, 0x44, 0x44, 0x4b, 0x1f, 0x46, 0x4e, 0x4e, 0x43, 0x1f, 0x53, 0x47, 0x48, 0xd2, 0x4c, 0x4e, 0x51, 0x4d, 0x48, 0x4d, 0x46, 0x1d, 0x1f, 0x4e, 0x51, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x9f, 0x48, 0x53, 0x1f, 0x48, 0x52, 0x1f, 0x40, 0x1f, 0x4c, 0x4e, 0x51, 0x4d, 0x48, 0x4d, 0x46, 0x1f, 0x53, 0xce, 0x41, 0x44, 0x1f, 0x46, 0x4e, 0x4e, 0x43, 0x1f, 0x4e, 0x4d, 0x1e, 0x5d, 0x1f, 0x5d, 0x0, 0x4b, 0x4b, 0x9f, ], [0x4e, 0x45, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x1f, 0x40, 0x53, 0x1f, 0x4e, 0x4d, 0x42, 0x44, 0x1d, 0x5d, 0x9f, 0x52, 0x40, 0x48, 0x43, 0x1f, 0x1, 0x48, 0x4b, 0x41, 0x4e, 0x1b, 0x9f, 0x1c, 0x13, 0x4e, 0x4b, 0x4a, 0x48, 0x44, 0x4d, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x5d, 0x6, 0x4e, 0x4e, 0x43, 0x1f, 0x4c, 0x4e, 0x51, 0x4d, 0x48, 0x4d, 0x46, 0x1a, 0x5d, 0x1f, 0x47, 0xc4, 0x52, 0x40, 0x48, 0x43, 0x1f, 0x40, 0x53, 0x1f, 0x4b, 0x40, 0x52, 0x53, 0x1b, 0x1f, 0x5d, 0x16, 0x44, 0x9f, 0x43, 0x4e, 0x4d, 0x5d, 0x53, 0x1f, 0x56, 0x40, 0x4d, 0x53, 0x1f, 0x40, 0x4d, 0x58, 0x9f, 0x40, 0x43, 0x55, 0x44, 0x4d, 0x53, 0x54, 0x51, 0x44, 0x52, 0x1f, 0x47, 0x44, 0x51, 0x44, 0x1d, 0x9f, 0x53, 0x47, 0x40, 0x4d, 0x4a, 0x1f, 0x58, 0x4e, 0x54, 0x1a, 0x1f, 0x18, 0x4e, 0x54, 0x9f, 0x4c, 0x48, 0x46, 0x47, 0x53, 0x1f, 0x53, 0x51, 0x58, 0x1f, 0x4e, 0x55, 0x44, 0x51, 0x1f, 0x13, 0x47, 0xc4, 0x7, 0x48, 0x4b, 0x4b, 0x1f, 0x4e, 0x51, 0x1f, 0x40, 0x42, 0x51, 0x4e, 0x52, 0x52, 0x1f, 0x13, 0x47, 0xc4, 0x16, 0x40, 0x53, 0x44, 0x51, 0x1b, 0x5d, 0x1f, 0x1, 0x58, 0x1f, 0x53, 0x47, 0x48, 0x52, 0x1f, 0x47, 0xc4, ], [0x4c, 0x44, 0x40, 0x4d, 0x53, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x42, 0x4e, 0x4d, 0x55, 0x44, 0x51, 0x52, 0x40, 0x53, 0x48, 0x4e, 0x4d, 0x1f, 0x56, 0x40, 0x52, 0x9f, 0x40, 0x53, 0x1f, 0x40, 0x4d, 0x1f, 0x44, 0x4d, 0x43, 0x1b, 0x1f, 0x5d, 0x16, 0x47, 0x40, 0x53, 0x1f, 0xc0, 0x4b, 0x4e, 0x53, 0x1f, 0x4e, 0x45, 0x1f, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x52, 0x1f, 0x58, 0x4e, 0x54, 0x9f, 0x43, 0x4e, 0x1f, 0x54, 0x52, 0x44, 0x1f, 0x6, 0x4e, 0x4e, 0x43, 0x9f, 0x4c, 0x4e, 0x51, 0x4d, 0x48, 0x4d, 0x46, 0x1f, 0x45, 0x4e, 0x51, 0x1a, 0x5d, 0x1f, 0x52, 0x40, 0x48, 0xc3, 0x6, 0x40, 0x4d, 0x43, 0x40, 0x4b, 0x45, 0x1b, 0x1f, 0x5d, 0xd, 0x4e, 0x56, 0x1f, 0x58, 0x4e, 0x54, 0x9f, 0x4c, 0x44, 0x40, 0x4d, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x58, 0x4e, 0x54, 0x1f, 0x56, 0x40, 0x4d, 0xd3, ], [0x53, 0x4e, 0x1f, 0x46, 0x44, 0x53, 0x1f, 0x51, 0x48, 0x43, 0x1f, 0x4e, 0x45, 0x1f, 0x4c, 0x44, 0x1d, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x48, 0x53, 0x1f, 0x56, 0x4e, 0x4d, 0x5d, 0x53, 0x9f, 0x41, 0x44, 0x1f, 0x46, 0x4e, 0x4e, 0x43, 0x1f, 0x53, 0x48, 0x4b, 0x4b, 0x1f, 0x8, 0x9f, 0x4c, 0x4e, 0x55, 0x44, 0x1f, 0x4e, 0x45, 0x45, 0x1b, 0x1b, 0x9f, 0x1c, 0x13, 0x4e, 0x4b, 0x4a, 0x48, 0x44, 0x4d, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x5d, 0x13, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x48, 0x52, 0x1f, 0x4d, 0x4e, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x9f, 0x4b, 0x48, 0x4a, 0x44, 0x1f, 0x4b, 0x4e, 0x4e, 0x4a, 0x48, 0x4d, 0x46, 0x1d, 0x1f, 0x48, 0x45, 0x9f, 0x58, 0x4e, 0x54, 0x1f, 0x56, 0x40, 0x4d, 0x53, 0x1f, 0x53, 0x4e, 0x1f, 0x45, 0x48, 0x4d, 0x43, 0x9f, 0x52, 0x4e, 0x4c, 0x44, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x1b, 0x1f, 0x18, 0x4e, 0x54, 0x9f, 0x42, 0x44, 0x51, 0x53, 0x40, 0x48, 0x4d, 0x4b, 0x58, 0x1f, 0x54, 0x52, 0x54, 0x40, 0x4b, 0x4b, 0x58, 0x9f, 0x45, 0x48, 0x4d, 0x43, 0x1f, 0x52, 0x4e, 0x4c, 0x44, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x1d, 0x1f, 0x48, 0xc5, 0x58, 0x4e, 0x54, 0x1f, 0x4b, 0x4e, 0x4e, 0x4a, 0x1d, 0x1f, 0x41, 0x54, 0x53, 0x1f, 0x48, 0x53, 0x9f, 0x48, 0x52, 0x1f, 0x4d, 0x4e, 0x53, 0x1f, 0x40, 0x4b, 0x56, 0x40, 0x58, 0x52, 0x9f, ], [0x50, 0x54, 0x48, 0x53, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x52, 0x4e, 0x4c, 0x44, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x1f, 0x58, 0x4e, 0x54, 0x1f, 0x56, 0x44, 0x51, 0xc4, 0x40, 0x45, 0x53, 0x44, 0x51, 0x1b, 0x5d, 0x1f, 0x1c, 0x13, 0x4e, 0x4b, 0x4a, 0x48, 0x44, 0x4d, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x3, 0x51, 0x54, 0x4c, 0x4c, 0x44, 0x51, 0x1d, 0x1f, 0x41, 0x44, 0x40, 0x53, 0x1d, 0x1f, 0x40, 0x4d, 0xc3, 0x4f, 0x48, 0x4f, 0x44, 0x51, 0x1d, 0x1f, 0x41, 0x4b, 0x4e, 0x56, 0x1b, 0x9f, 0x7, 0x40, 0x51, 0x4f, 0x44, 0x51, 0x1d, 0x1f, 0x52, 0x53, 0x51, 0x48, 0x4a, 0x44, 0x1d, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x52, 0x4e, 0x4b, 0x43, 0x48, 0x44, 0x51, 0x1d, 0x1f, 0x46, 0x4e, 0x1b, 0x9f, 0x5, 0x51, 0x44, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x45, 0x4b, 0x40, 0x4c, 0x44, 0x1f, 0x40, 0x4d, 0xc3, 0x52, 0x44, 0x40, 0x51, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x46, 0x51, 0x40, 0x52, 0x52, 0x44, 0x52, 0x1d, 0x9f, 0x13, 0x48, 0x4b, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x43, 0x40, 0x56, 0x4d, 0x48, 0x4d, 0x46, 0x9f, 0x11, 0x44, 0x43, 0x1f, 0x12, 0x53, 0x40, 0x51, 0x1f, 0x4f, 0x40, 0x52, 0x52, 0x44, 0x52, 0x1b, 0x9f, ], [0x1c, 0xc, 0x42, 0x2, 0x40, 0x45, 0x45, 0x51, 0x44, 0x58, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x13, 0x47, 0x44, 0x1f, 0x53, 0x44, 0x40, 0x51, 0x52, 0x1f, 0x8, 0x1f, 0x45, 0x44, 0x44, 0x4b, 0x9f, 0x53, 0x4e, 0x43, 0x40, 0x58, 0x1f, 0x8, 0x5d, 0x4b, 0x4b, 0x1f, 0x56, 0x40, 0x48, 0x53, 0x1f, 0x53, 0xce, 0x52, 0x47, 0x44, 0x43, 0x1f, 0x53, 0x4e, 0x4c, 0x4e, 0x51, 0x51, 0x4e, 0x56, 0x1b, 0x9f, 0x13, 0x47, 0x4e, 0x54, 0x46, 0x47, 0x1f, 0x8, 0x5d, 0x4b, 0x4b, 0x1f, 0x4d, 0x4e, 0x53, 0x9f, 0x52, 0x4b, 0x44, 0x44, 0x4f, 0x1f, 0x53, 0x47, 0x48, 0x52, 0x1f, 0x4d, 0x48, 0x46, 0x47, 0x53, 0x9f, 0xd, 0x4e, 0x51, 0x1f, 0x45, 0x48, 0x4d, 0x43, 0x1f, 0x52, 0x54, 0x51, 0x42, 0x44, 0x40, 0x52, 0x44, 0x9f, 0x45, 0x51, 0x4e, 0x4c, 0x1f, 0x52, 0x4e, 0x51, 0x51, 0x4e, 0x56, 0x1b, 0x1f, 0xc, 0x58, 0x9f, 0x44, 0x58, 0x44, 0x52, 0x1f, 0x4c, 0x54, 0x52, 0x53, 0x1f, 0x4a, 0x44, 0x44, 0x4f, 0x9f, ], [0x53, 0x47, 0x44, 0x48, 0x51, 0x1f, 0x52, 0x48, 0x46, 0x47, 0x53, 0x1b, 0x1f, 0x8, 0x9f, 0x43, 0x40, 0x51, 0x44, 0x1f, 0x4d, 0x4e, 0x53, 0x1f, 0x41, 0x44, 0x9f, 0x53, 0x44, 0x40, 0x51, 0x1c, 0x41, 0x4b, 0x48, 0x4d, 0x43, 0x44, 0x43, 0x1b, 0x1f, 0x8, 0x9f, 0x4c, 0x54, 0x52, 0x53, 0x1f, 0x41, 0x44, 0x1f, 0x45, 0x51, 0x44, 0x44, 0x1f, 0x53, 0x4e, 0x9f, 0x53, 0x40, 0x4b, 0x4a, 0x1f, 0xd, 0x4e, 0x53, 0x1f, 0x42, 0x47, 0x4e, 0x4a, 0x44, 0x43, 0x9f, 0x56, 0x48, 0x53, 0x47, 0x1f, 0x46, 0x51, 0x48, 0x44, 0x45, 0x1d, 0x9f, 0x42, 0x4b, 0x44, 0x40, 0x51, 0x1c, 0x4c, 0x48, 0x4d, 0x43, 0x44, 0x43, 0x1b, 0x1f, 0xc, 0x58, 0x9f, 0x4c, 0x4e, 0x54, 0x53, 0x47, 0x1f, 0x42, 0x40, 0x4d, 0x4d, 0x4e, 0x53, 0x9f, ], [0x41, 0x44, 0x53, 0x51, 0x40, 0x58, 0x1f, 0x13, 0x47, 0x44, 0x1f, 0x40, 0x4d, 0x46, 0x54, 0x48, 0x52, 0xc7, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x8, 0x1f, 0x4a, 0x4d, 0x4e, 0x56, 0x1b, 0x1f, 0x18, 0x44, 0x52, 0x1d, 0x9f, 0x8, 0x5d, 0x4b, 0x4b, 0x1f, 0x4a, 0x44, 0x44, 0x4f, 0x1f, 0x4c, 0x58, 0x1f, 0x53, 0x44, 0x40, 0x51, 0xd2, 0x53, 0x48, 0x4b, 0x1f, 0x4b, 0x40, 0x53, 0x44, 0x51, 0x1b, 0x1f, 0x1, 0x54, 0x53, 0x1f, 0x4c, 0x58, 0x9f, 0x46, 0x51, 0x48, 0x44, 0x45, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x1f, 0x4d, 0x44, 0x55, 0x44, 0x51, 0x9f, 0x46, 0x4e, 0x1b, 0x1f, 0x1c, 0xc, 0x42, 0x2, 0x40, 0x45, 0x45, 0x51, 0x44, 0x58, 0x1f, 0x9f, 0x9f, 0x9f, ]], +[[0x16, 0x47, 0x4e, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x52, 0x1d, 0x1f, 0x2, 0x40, 0x4d, 0x1b, 0x9f, 0x16, 0x47, 0x4e, 0x1f, 0x53, 0x51, 0x48, 0x44, 0x52, 0x1d, 0x1f, 0x3, 0x4e, 0x44, 0x52, 0x1b, 0x9f, 0x16, 0x47, 0x4e, 0x1f, 0x4b, 0x4e, 0x55, 0x44, 0x52, 0x1d, 0x1f, 0xb, 0x48, 0x55, 0x44, 0x52, 0x1b, 0x9f, 0x1c, 0xc, 0x42, 0x2, 0x40, 0x45, 0x45, 0x51, 0x44, 0x58, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x13, 0x47, 0x44, 0x1f, 0x4b, 0x48, 0x53, 0x53, 0x4b, 0x44, 0x1f, 0x50, 0x54, 0x44, 0x44, 0x4d, 0x9f, 0x40, 0x4b, 0x4b, 0x1f, 0x46, 0x4e, 0x4b, 0x43, 0x44, 0x4d, 0x1f, 0x5, 0x4b, 0x44, 0x56, 0x9f, 0x47, 0x48, 0x52, 0x52, 0x48, 0x4d, 0x46, 0x1f, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x52, 0x44, 0x40, 0x1b, 0x1f, 0x13, 0x4e, 0x1f, 0x52, 0x53, 0x4e, 0x4f, 0x1f, 0x44, 0x40, 0x42, 0x47, 0x9f, 0x56, 0x40, 0x55, 0x44, 0x1f, 0x7, 0x44, 0x51, 0x1f, 0x42, 0x4b, 0x54, 0x53, 0x42, 0x47, 0x1f, 0x53, 0xce, 0x52, 0x40, 0x55, 0x44, 0x1f, 0x12, 0x47, 0x44, 0x1f, 0x55, 0x44, 0x4d, 0x53, 0x54, 0x51, 0x44, 0x43, 0x9f, 0x41, 0x51, 0x40, 0x55, 0x44, 0x4b, 0x58, 0x1b, 0x1f, 0x0, 0x52, 0x1f, 0x52, 0x47, 0x44, 0x9f, 0x40, 0x53, 0x53, 0x40, 0x42, 0x4a, 0x44, 0x43, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x52, 0x44, 0x40, 0x9f, ], [0x48, 0x4d, 0x1f, 0x51, 0x40, 0x46, 0x44, 0x1f, 0x0, 0x9f, 0x47, 0x4e, 0x4b, 0x43, 0x44, 0x51, 0x4c, 0x40, 0x4d, 0x1f, 0x42, 0x40, 0x4c, 0x44, 0x9f, 0x4d, 0x48, 0x46, 0x47, 0x1f, 0x0, 0x4b, 0x4e, 0x4d, 0x46, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x52, 0x40, 0x4d, 0x43, 0x1f, 0x5, 0x48, 0x52, 0x47, 0x4d, 0x44, 0x53, 0x1f, 0x48, 0x4d, 0x9f, 0x47, 0x40, 0x4d, 0x43, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x52, 0x40, 0x56, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x50, 0x54, 0x44, 0x44, 0x4d, 0x1f, 0x4c, 0x48, 0x43, 0x52, 0x4a, 0x58, 0x1b, 0x1f, 0x7, 0x44, 0x9f, 0x52, 0x53, 0x40, 0x51, 0x44, 0x43, 0x1f, 0x40, 0x53, 0x1f, 0x47, 0x44, 0x51, 0x1f, 0x48, 0x4d, 0x9f, 0x56, 0x4e, 0x4d, 0x43, 0x44, 0x51, 0x1f, 0x5, 0x4e, 0x51, 0x1f, 0x4e, 0x45, 0x53, 0x44, 0x4d, 0x9f, ], [0x47, 0x44, 0x5d, 0x43, 0x1f, 0x41, 0x44, 0x44, 0x4d, 0x1f, 0x53, 0x4e, 0x4b, 0x43, 0x9f, 0x13, 0x47, 0x40, 0x53, 0x1f, 0x52, 0x54, 0x42, 0x47, 0x1f, 0x40, 0x52, 0x1f, 0x52, 0x47, 0x44, 0x9f, 0x2, 0x4e, 0x54, 0x4b, 0x43, 0x1f, 0x4d, 0x44, 0x55, 0x44, 0x51, 0x1f, 0x41, 0x44, 0x1f, 0x16, 0x47, 0xce, 0x47, 0x4e, 0x55, 0x44, 0x51, 0x44, 0x43, 0x1f, 0x53, 0x47, 0x44, 0x51, 0x44, 0x1d, 0x9f, 0x41, 0x51, 0x48, 0x46, 0x47, 0x53, 0x1f, 0x46, 0x4e, 0x4b, 0x43, 0x1b, 0x1f, 0x7, 0x44, 0x9f, 0x52, 0x40, 0x56, 0x1f, 0x47, 0x44, 0x51, 0x1f, 0x4f, 0x4b, 0x48, 0x46, 0x47, 0x53, 0x1f, 0x40, 0x4d, 0xc3, 0x50, 0x54, 0x48, 0x42, 0x4a, 0x4b, 0x58, 0x1f, 0x7, 0x44, 0x1f, 0x4b, 0x4e, 0x4e, 0x4a, 0x44, 0x43, 0x9f, 0x54, 0x4f, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x42, 0x4b, 0x48, 0x45, 0x45, 0x1f, 0x47, 0x44, 0x9f, ], [0x45, 0x40, 0x42, 0x44, 0x43, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x52, 0x40, 0x56, 0x1f, 0x40, 0x9f, 0x42, 0x40, 0x55, 0x44, 0x1f, 0x0, 0x41, 0x4e, 0x55, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x56, 0x40, 0x55, 0x44, 0x1f, 0x8, 0x4d, 0x1f, 0x56, 0x47, 0x48, 0x42, 0x47, 0x1f, 0x47, 0x44, 0x51, 0x9f, 0x44, 0x46, 0x46, 0x52, 0x1f, 0x47, 0x44, 0x1f, 0x4f, 0x4b, 0x40, 0x42, 0x44, 0x43, 0x1b, 0x9f, 0x13, 0x47, 0x44, 0x1f, 0x4b, 0x48, 0x53, 0x53, 0x4b, 0x44, 0x1f, 0x50, 0x54, 0x44, 0x44, 0x4d, 0x9f, 0x40, 0x4b, 0x4b, 0x1f, 0x46, 0x4e, 0x4b, 0x43, 0x44, 0x4d, 0x1f, 0x14, 0x4f, 0x4e, 0x4d, 0x9f, 0x47, 0x48, 0x52, 0x1f, 0x52, 0x47, 0x4e, 0x54, 0x4b, 0x43, 0x44, 0x51, 0x1f, 0x52, 0x53, 0x4e, 0x4e, 0xc3, 0x7, 0x44, 0x51, 0x1f, 0x44, 0x58, 0x44, 0x52, 0x1f, 0x40, 0x4b, 0x4b, 0x1f, 0x41, 0x4b, 0x54, 0x44, 0x9f, ], [0x6, 0x4b, 0x4e, 0x56, 0x44, 0x43, 0x1f, 0x4e, 0x45, 0x1f, 0x47, 0x44, 0x51, 0x1f, 0x53, 0x51, 0x54, 0xc4, 0x14, 0x4d, 0x43, 0x58, 0x48, 0x4d, 0x46, 0x1f, 0x46, 0x51, 0x40, 0x53, 0x48, 0x53, 0x54, 0x43, 0x44, 0x9b, 0x1c, 0xc, 0x42, 0x2, 0x40, 0x45, 0x45, 0x51, 0x44, 0x58, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x7, 0x40, 0x51, 0x4f, 0x44, 0x51, 0x1d, 0x1f, 0x53, 0x51, 0x44, 0x40, 0x53, 0x1f, 0x58, 0x4e, 0x54, 0xd1, 0x56, 0x4e, 0x51, 0x43, 0x52, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x1f, 0x42, 0x40, 0x51, 0x44, 0x9f, 0x5, 0x4e, 0x51, 0x1f, 0x53, 0x47, 0x44, 0x58, 0x1f, 0x4c, 0x40, 0x58, 0x1f, 0x42, 0x40, 0x54, 0x52, 0xc4, 0x49, 0x4e, 0x58, 0x1f, 0x4e, 0x51, 0x1f, 0x43, 0x44, 0x52, 0x4f, 0x40, 0x48, 0x51, 0x9f, 0x12, 0x48, 0x4d, 0x46, 0x1f, 0x58, 0x4e, 0x54, 0x51, 0x1f, 0x52, 0x4e, 0x4d, 0x46, 0x52, 0x1f, 0x4e, 0xc5, 0x47, 0x44, 0x40, 0x4b, 0x53, 0x47, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x4b, 0x4e, 0x55, 0x44, 0x1f, 0xe, 0xc5, 0x43, 0x51, 0x40, 0x46, 0x4e, 0x4d, 0x52, 0x1f, 0x45, 0x4b, 0x40, 0x4c, 0x48, 0x4d, 0x46, 0x9f, 0x45, 0x51, 0x4e, 0x4c, 0x1f, 0x40, 0x41, 0x4e, 0x55, 0x44, 0x1b, 0x9f, ], [0x1c, 0xc, 0x42, 0x2, 0x40, 0x45, 0x45, 0x51, 0x44, 0x58, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x13, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x4e, 0x4d, 0x4b, 0x58, 0x1f, 0x4e, 0x4d, 0xc4, 0x42, 0x40, 0x53, 0x42, 0x47, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x56, 0x40, 0xd2, 0x2, 0x40, 0x53, 0x42, 0x47, 0x1c, 0x24, 0x24, 0x1d, 0x1f, 0x56, 0x47, 0x48, 0x42, 0x47, 0x9f, 0x52, 0x4f, 0x44, 0x42, 0x48, 0x45, 0x48, 0x44, 0x43, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x40, 0x9f, 0x42, 0x4e, 0x4d, 0x42, 0x44, 0x51, 0x4d, 0x1f, 0x45, 0x4e, 0x51, 0x1f, 0x4e, 0x4d, 0x44, 0x5d, 0x52, 0x9f, 0x52, 0x40, 0x45, 0x44, 0x53, 0x58, 0x1f, 0x48, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x45, 0x40, 0x42, 0xc4, 0x4e, 0x45, 0x1f, 0x43, 0x40, 0x4d, 0x46, 0x44, 0x51, 0x52, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x9f, 0x56, 0x44, 0x51, 0x44, 0x1f, 0x51, 0x44, 0x40, 0x4b, 0x1f, 0x40, 0x4d, 0x43, 0x9f, ], [0x48, 0x4c, 0x4c, 0x44, 0x43, 0x48, 0x40, 0x53, 0x44, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x4f, 0x51, 0x4e, 0x42, 0x44, 0x52, 0x52, 0x1f, 0x4e, 0x45, 0x1f, 0x40, 0x9f, 0x51, 0x40, 0x53, 0x48, 0x4e, 0x4d, 0x40, 0x4b, 0x1f, 0x4c, 0x48, 0x4d, 0x43, 0x1b, 0x1f, 0xe, 0x51, 0xd1, 0x56, 0x40, 0x52, 0x1f, 0x42, 0x51, 0x40, 0x59, 0x58, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x42, 0x4e, 0x54, 0x4b, 0x43, 0x1f, 0x41, 0x44, 0x1f, 0x46, 0x51, 0x4e, 0x54, 0x4d, 0x43, 0x44, 0x43, 0x9b, 0x0, 0x4b, 0x4b, 0x1f, 0x47, 0x44, 0x1f, 0x47, 0x40, 0x43, 0x1f, 0x53, 0x4e, 0x1f, 0x43, 0x4e, 0x9f, 0x56, 0x40, 0x52, 0x1f, 0x40, 0x52, 0x4a, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x40, 0x52, 0x9f, 0x52, 0x4e, 0x4e, 0x4d, 0x1f, 0x40, 0x52, 0x1f, 0x47, 0x44, 0x1f, 0x43, 0x48, 0x43, 0x1d, 0x1f, 0x47, 0xc4, ], [0x56, 0x4e, 0x54, 0x4b, 0x43, 0x1f, 0x4d, 0x4e, 0x1f, 0x4b, 0x4e, 0x4d, 0x46, 0x44, 0x51, 0x1f, 0x41, 0xc4, 0x42, 0x51, 0x40, 0x59, 0x58, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x56, 0x4e, 0x54, 0x4b, 0x43, 0x9f, 0x47, 0x40, 0x55, 0x44, 0x1f, 0x53, 0x4e, 0x1f, 0x45, 0x4b, 0x58, 0x1f, 0x4c, 0x4e, 0x51, 0x44, 0x9f, 0x4c, 0x48, 0x52, 0x52, 0x48, 0x4e, 0x4d, 0x52, 0x1b, 0x1f, 0xe, 0x51, 0x51, 0x9f, 0x56, 0x4e, 0x54, 0x4b, 0x43, 0x1f, 0x41, 0x44, 0x1f, 0x42, 0x51, 0x40, 0x59, 0x58, 0x1f, 0x53, 0x4e, 0x9f, 0x45, 0x4b, 0x58, 0x1f, 0x4c, 0x4e, 0x51, 0x44, 0x1f, 0x4c, 0x48, 0x52, 0x52, 0x48, 0x4e, 0x4d, 0x52, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x52, 0x40, 0x4d, 0x44, 0x1f, 0x48, 0x45, 0x1f, 0x47, 0x44, 0x9f, 0x43, 0x48, 0x43, 0x4d, 0x5d, 0x53, 0x1d, 0x1f, 0x41, 0x54, 0x53, 0x1f, 0x48, 0x45, 0x1f, 0x47, 0x44, 0x9f, ], [0x56, 0x40, 0x52, 0x1f, 0x52, 0x40, 0x4d, 0x44, 0x1f, 0x47, 0x44, 0x1f, 0x47, 0x40, 0x43, 0x1f, 0x53, 0xce, 0x45, 0x4b, 0x58, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x1b, 0x1f, 0x8, 0x45, 0x1f, 0x47, 0x44, 0x9f, 0x45, 0x4b, 0x44, 0x56, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x1f, 0x47, 0x44, 0x1f, 0x56, 0x40, 0x52, 0x9f, 0x42, 0x51, 0x40, 0x59, 0x58, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x43, 0x48, 0x43, 0x4d, 0x5d, 0x53, 0x9f, 0x47, 0x40, 0x55, 0x44, 0x1f, 0x53, 0x4e, 0x1d, 0x1f, 0x41, 0x54, 0x53, 0x1f, 0x48, 0x45, 0x1f, 0x47, 0xc4, 0x43, 0x48, 0x43, 0x4d, 0x5d, 0x53, 0x1f, 0x56, 0x40, 0x4d, 0x53, 0x1f, 0x53, 0x4e, 0x1f, 0x47, 0x44, 0x9f, 0x56, 0x40, 0x52, 0x1f, 0x52, 0x40, 0x4d, 0x44, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x47, 0x40, 0x43, 0x9f, 0x53, 0x4e, 0x1b, 0x1f, 0x1c, 0x7, 0x44, 0x4b, 0x4b, 0x44, 0x51, 0x1f, 0x9f, ]], +[[0x5d, 0x13, 0x47, 0x44, 0x58, 0x5d, 0x51, 0x44, 0x1f, 0x53, 0x51, 0x58, 0x48, 0x4d, 0x46, 0x1f, 0x53, 0xce, 0x4a, 0x48, 0x4b, 0x4b, 0x1f, 0x4c, 0x44, 0x1d, 0x5d, 0x9f, 0x18, 0x4e, 0x52, 0x52, 0x40, 0x51, 0x48, 0x40, 0x4d, 0x1f, 0x53, 0x4e, 0x4b, 0x43, 0x1f, 0x47, 0x48, 0xcc, 0x42, 0x40, 0x4b, 0x4c, 0x4b, 0x58, 0x1b, 0x1f, 0x5d, 0xd, 0x4e, 0x1f, 0x4e, 0x4d, 0x44, 0x5d, 0x52, 0x9f, 0x53, 0x51, 0x58, 0x48, 0x4d, 0x46, 0x1f, 0x53, 0x4e, 0x1f, 0x4a, 0x48, 0x4b, 0x4b, 0x9f, 0x58, 0x4e, 0x54, 0x1d, 0x5d, 0x1f, 0x2, 0x4b, 0x44, 0x55, 0x48, 0x4d, 0x46, 0x44, 0x51, 0x9f, 0x42, 0x51, 0x48, 0x44, 0x43, 0x1b, 0x1f, 0x5d, 0x13, 0x47, 0x44, 0x4d, 0x1f, 0x56, 0x47, 0x58, 0x9f, 0x40, 0x51, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x58, 0x1f, 0x52, 0x47, 0x4e, 0x4e, 0x53, 0x48, 0x4d, 0x46, 0x9f, ], [0x40, 0x53, 0x1f, 0x4c, 0x44, 0x1e, 0x5d, 0x1f, 0x18, 0x4e, 0x52, 0x52, 0x40, 0x51, 0x48, 0x40, 0x4d, 0x9f, 0x40, 0x52, 0x4a, 0x44, 0x43, 0x1b, 0x1f, 0x5d, 0x13, 0x47, 0x44, 0x58, 0x5d, 0x51, 0x44, 0x9f, 0x52, 0x47, 0x4e, 0x4e, 0x53, 0x48, 0x4d, 0x46, 0x1f, 0x40, 0x53, 0x9f, 0x44, 0x55, 0x44, 0x51, 0x58, 0x4e, 0x4d, 0x44, 0x1d, 0x5d, 0x9f, 0x2, 0x4b, 0x44, 0x55, 0x48, 0x4d, 0x46, 0x44, 0x51, 0x9f, 0x40, 0x4d, 0x52, 0x56, 0x44, 0x51, 0x44, 0x43, 0x1b, 0x1f, 0x5d, 0x13, 0x47, 0x44, 0x58, 0x5d, 0x51, 0xc4, 0x53, 0x51, 0x58, 0x48, 0x4d, 0x46, 0x1f, 0x53, 0x4e, 0x1f, 0x4a, 0x48, 0x4b, 0x4b, 0x9f, 0x44, 0x55, 0x44, 0x51, 0x58, 0x4e, 0x4d, 0x44, 0x1b, 0x5d, 0x1f, 0x0, 0x4d, 0x43, 0x9f, ], [0x56, 0x47, 0x40, 0x53, 0x1f, 0x43, 0x48, 0x45, 0x45, 0x44, 0x51, 0x44, 0x4d, 0x42, 0x44, 0x9f, 0x43, 0x4e, 0x44, 0x52, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x4c, 0x40, 0x4a, 0x44, 0x1e, 0x9f, 0x1c, 0x7, 0x44, 0x4b, 0x4b, 0x44, 0x51, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x18, 0x4e, 0x54, 0x1f, 0x47, 0x40, 0x55, 0x44, 0x1f, 0x41, 0x51, 0x40, 0x48, 0x4d, 0x52, 0x1f, 0x48, 0xcd, 0x58, 0x4e, 0x54, 0x51, 0x1f, 0x47, 0x44, 0x40, 0x43, 0x1b, 0x1f, 0x18, 0x4e, 0x54, 0x9f, 0x47, 0x40, 0x55, 0x44, 0x1f, 0x45, 0x44, 0x44, 0x53, 0x1f, 0x48, 0x4d, 0x1f, 0x58, 0x4e, 0x54, 0x51, 0x9f, 0x52, 0x47, 0x4e, 0x44, 0x52, 0x1b, 0x1f, 0x18, 0x4e, 0x54, 0x1f, 0x42, 0x40, 0x4d, 0x9f, 0x52, 0x53, 0x44, 0x44, 0x51, 0x1f, 0x58, 0x4e, 0x54, 0x51, 0x52, 0x44, 0x4b, 0x45, 0x1f, 0x40, 0x4d, 0xd8, 0x43, 0x48, 0x51, 0x44, 0x42, 0x53, 0x48, 0x4e, 0x4d, 0x1f, 0x58, 0x4e, 0x54, 0x9f, 0x42, 0x47, 0x4e, 0x4e, 0x52, 0x44, 0x1b, 0x1f, 0x18, 0x4e, 0x54, 0x5d, 0x51, 0x44, 0x1f, 0x4e, 0x4d, 0x9f, 0x58, 0x4e, 0x54, 0x51, 0x1f, 0x4e, 0x56, 0x4d, 0x1b, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x58, 0x4e, 0x54, 0x9f, ], [0x4a, 0x4d, 0x4e, 0x56, 0x1f, 0x56, 0x47, 0x40, 0x53, 0x1f, 0x58, 0x4e, 0x54, 0x9f, 0x4a, 0x4d, 0x4e, 0x56, 0x1b, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x18, 0xe, 0x14, 0x1f, 0x40, 0x51, 0x44, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x4e, 0x4d, 0x44, 0x1f, 0x56, 0x47, 0x4e, 0x5d, 0x4b, 0x4b, 0x9f, 0x43, 0x44, 0x42, 0x48, 0x43, 0x44, 0x1f, 0x56, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x53, 0x4e, 0x9f, 0x46, 0x4e, 0x1b, 0x1b, 0x1b, 0x1f, 0x1c, 0x12, 0x54, 0x44, 0x52, 0x52, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x16, 0x47, 0x44, 0x4d, 0x1f, 0x58, 0x4e, 0x54, 0x1f, 0x47, 0x40, 0x55, 0x44, 0x9f, 0x44, 0x4b, 0x48, 0x4c, 0x48, 0x4d, 0x40, 0x53, 0x44, 0x43, 0x1f, 0x40, 0x4b, 0x4b, 0x9f, 0x56, 0x47, 0x48, 0x42, 0x47, 0x1f, 0x48, 0x52, 0x9f, 0x48, 0x4c, 0x4f, 0x4e, 0x52, 0x52, 0x48, 0x41, 0x4b, 0x44, 0x1d, 0x1f, 0x53, 0x47, 0x44, 0x4d, 0x9f, 0x56, 0x47, 0x40, 0x53, 0x44, 0x55, 0x44, 0x51, 0x1f, 0x51, 0x44, 0x4c, 0x40, 0x48, 0x4d, 0x52, 0x1d, 0x9f, 0x47, 0x4e, 0x56, 0x44, 0x55, 0x44, 0x51, 0x9f, 0x48, 0x4c, 0x4f, 0x51, 0x4e, 0x41, 0x40, 0x41, 0x4b, 0x44, 0x1d, 0x1f, 0x4c, 0x54, 0x52, 0x53, 0x9f, 0x41, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x53, 0x51, 0x54, 0x53, 0x47, 0x1b, 0x9f, ], [0x1c, 0x3, 0x4e, 0x58, 0x4b, 0x44, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x5d, 0xc, 0x58, 0x1f, 0x4c, 0x48, 0x4d, 0x43, 0x1d, 0x5d, 0x1f, 0x47, 0x44, 0x9f, 0x52, 0x40, 0x48, 0x43, 0x1d, 0x1f, 0x5d, 0x51, 0x44, 0x41, 0x44, 0x4b, 0x52, 0x1f, 0x40, 0x53, 0x9f, 0x52, 0x53, 0x40, 0x46, 0x4d, 0x40, 0x53, 0x48, 0x4e, 0x4d, 0x1b, 0x1f, 0x6, 0x48, 0x55, 0x44, 0x9f, 0x4c, 0x44, 0x1f, 0x4f, 0x51, 0x4e, 0x41, 0x4b, 0x44, 0x4c, 0x52, 0x1d, 0x1f, 0x46, 0x48, 0x55, 0x44, 0x9f, 0x4c, 0x44, 0x1f, 0x56, 0x4e, 0x51, 0x4a, 0x1d, 0x1f, 0x46, 0x48, 0x55, 0x44, 0x1f, 0x4c, 0x44, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x4c, 0x4e, 0x52, 0x53, 0x1f, 0x40, 0x41, 0x52, 0x53, 0x51, 0x54, 0x52, 0x44, 0x9f, 0x42, 0x51, 0x58, 0x4f, 0x53, 0x4e, 0x46, 0x51, 0x40, 0x4c, 0x1f, 0x4e, 0x51, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x4c, 0x4e, 0x52, 0x53, 0x1f, 0x48, 0x4d, 0x53, 0x51, 0x48, 0x42, 0x40, 0x53, 0x44, 0x9f, ], [0x40, 0x4d, 0x40, 0x4b, 0x58, 0x52, 0x48, 0x52, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x8, 0x1f, 0x40, 0xcc, 0x48, 0x4d, 0x1f, 0x4c, 0x58, 0x1f, 0x4e, 0x56, 0x4d, 0x1f, 0x4f, 0x51, 0x4e, 0x4f, 0x44, 0x51, 0x9f, 0x40, 0x53, 0x4c, 0x4e, 0x52, 0x4f, 0x47, 0x44, 0x51, 0x44, 0x1b, 0x1f, 0x8, 0x1f, 0x42, 0x40, 0x4d, 0x9f, 0x43, 0x48, 0x52, 0x4f, 0x44, 0x4d, 0x52, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x4d, 0x1f, 0x56, 0x48, 0x53, 0xc7, 0x40, 0x51, 0x53, 0x48, 0x45, 0x48, 0x42, 0x48, 0x40, 0x4b, 0x9f, 0x52, 0x53, 0x48, 0x4c, 0x54, 0x4b, 0x40, 0x4d, 0x53, 0x52, 0x1b, 0x1f, 0x1, 0x54, 0x53, 0x1f, 0x8, 0x9f, 0x40, 0x41, 0x47, 0x4e, 0x51, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x43, 0x54, 0x4b, 0x4b, 0x9f, 0x51, 0x4e, 0x54, 0x53, 0x48, 0x4d, 0x44, 0x1f, 0x4e, 0x45, 0x9f, ], [0x44, 0x57, 0x48, 0x52, 0x53, 0x44, 0x4d, 0x42, 0x44, 0x1b, 0x1f, 0x8, 0x1f, 0x42, 0x51, 0x40, 0x55, 0xc4, 0x45, 0x4e, 0x51, 0x1f, 0x4c, 0x44, 0x4d, 0x53, 0x40, 0x4b, 0x9f, 0x44, 0x57, 0x40, 0x4b, 0x53, 0x40, 0x53, 0x48, 0x4e, 0x4d, 0x1b, 0x1f, 0x13, 0x47, 0x40, 0x53, 0x9f, 0x48, 0x52, 0x1f, 0x56, 0x47, 0x58, 0x1f, 0x8, 0x1f, 0x47, 0x40, 0x55, 0x44, 0x9f, 0x42, 0x47, 0x4e, 0x52, 0x44, 0x4d, 0x1f, 0x4c, 0x58, 0x1f, 0x4e, 0x56, 0x4d, 0x9f, 0x4f, 0x40, 0x51, 0x53, 0x48, 0x42, 0x54, 0x4b, 0x40, 0x51, 0x9f, 0x4f, 0x51, 0x4e, 0x45, 0x44, 0x52, 0x52, 0x48, 0x4e, 0x4d, 0x1d, 0x1f, 0x4e, 0x51, 0x9f, 0x51, 0x40, 0x53, 0x47, 0x44, 0x51, 0x1f, 0x42, 0x51, 0x44, 0x40, 0x53, 0x44, 0x43, 0x1f, 0x48, 0x53, 0x9d, ], [0x45, 0x4e, 0x51, 0x1f, 0x8, 0x1f, 0x40, 0x4c, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x4e, 0x4d, 0x4b, 0x58, 0x9f, 0x4e, 0x4d, 0x44, 0x1f, 0x48, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x56, 0x4e, 0x51, 0x4b, 0x43, 0x1b, 0xdd, 0x1c, 0x3, 0x4e, 0x58, 0x4b, 0x44, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0xb, 0x48, 0x45, 0x44, 0x1f, 0x48, 0x52, 0x1f, 0x48, 0x4d, 0x45, 0x48, 0x4d, 0x48, 0x53, 0x44, 0x4b, 0xd8, 0x52, 0x53, 0x51, 0x40, 0x4d, 0x46, 0x44, 0x51, 0x1f, 0x53, 0x47, 0x40, 0x4d, 0x9f, 0x40, 0x4d, 0x58, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x1f, 0x56, 0x47, 0x48, 0x42, 0x47, 0x1f, 0x53, 0x47, 0xc4, 0x4c, 0x48, 0x4d, 0x43, 0x1f, 0x4e, 0x45, 0x1f, 0x4c, 0x40, 0x4d, 0x1f, 0x42, 0x4e, 0x54, 0x4b, 0x43, 0x9f, 0x48, 0x4d, 0x55, 0x44, 0x4d, 0x53, 0x1b, 0x1f, 0x16, 0x44, 0x1f, 0x56, 0x4e, 0x54, 0x4b, 0x43, 0x9f, 0x4d, 0x4e, 0x53, 0x1f, 0x43, 0x40, 0x51, 0x44, 0x1f, 0x53, 0x4e, 0x9f, 0x42, 0x4e, 0x4d, 0x42, 0x44, 0x48, 0x55, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x52, 0x1f, 0x56, 0x47, 0x48, 0x42, 0x47, 0x1f, 0x40, 0x51, 0x44, 0x9f, ], [0x51, 0x44, 0x40, 0x4b, 0x4b, 0x58, 0x1f, 0x4c, 0x44, 0x51, 0x44, 0x9f, 0x42, 0x4e, 0x4c, 0x4c, 0x4e, 0x4d, 0x4f, 0x4b, 0x40, 0x42, 0x44, 0x52, 0x1f, 0x4e, 0x45, 0x9f, 0x44, 0x57, 0x48, 0x52, 0x53, 0x44, 0x4d, 0x42, 0x44, 0x1b, 0x1f, 0x8, 0x45, 0x1f, 0x56, 0x44, 0x9f, 0x42, 0x4e, 0x54, 0x4b, 0x43, 0x1f, 0x45, 0x4b, 0x58, 0x1f, 0x4e, 0x54, 0x53, 0x1f, 0x4e, 0x45, 0x9f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x56, 0x48, 0x4d, 0x43, 0x4e, 0x56, 0x1f, 0x47, 0x40, 0x4d, 0x43, 0x9f, 0x48, 0x4d, 0x1f, 0x47, 0x40, 0x4d, 0x43, 0x1d, 0x1f, 0x47, 0x4e, 0x55, 0x44, 0x51, 0x9f, 0x4e, 0x55, 0x44, 0x51, 0x1f, 0x53, 0x47, 0x48, 0x52, 0x1f, 0x46, 0x51, 0x44, 0x40, 0x53, 0x9f, 0x42, 0x48, 0x53, 0x58, 0x1d, 0x1f, 0x46, 0x44, 0x4d, 0x53, 0x4b, 0x58, 0x9f, ], [0x51, 0x44, 0x4c, 0x4e, 0x55, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x51, 0x4e, 0x4e, 0x45, 0x52, 0x1d, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x4f, 0x44, 0x44, 0x4f, 0x1f, 0x48, 0x4d, 0x1f, 0x40, 0xd3, 0x53, 0x47, 0x44, 0x1f, 0x50, 0x54, 0x44, 0x44, 0x51, 0x1f, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x52, 0x9f, 0x56, 0x47, 0x48, 0x42, 0x47, 0x1f, 0x40, 0x51, 0x44, 0x1f, 0x46, 0x4e, 0x48, 0x4d, 0x46, 0x9f, 0x4e, 0x4d, 0x1d, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x52, 0x53, 0x51, 0x40, 0x4d, 0x46, 0x44, 0x9f, 0x42, 0x4e, 0x48, 0x4d, 0x42, 0x48, 0x43, 0x44, 0x4d, 0x42, 0x44, 0x52, 0x1d, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x4f, 0x4b, 0x40, 0x4d, 0x4d, 0x48, 0x4d, 0x46, 0x52, 0x1d, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x42, 0x51, 0x4e, 0x52, 0x52, 0x1c, 0x4f, 0x54, 0x51, 0x4f, 0x4e, 0x52, 0x44, 0x52, 0x1d, 0x9f, ], [0x53, 0x47, 0x44, 0x1f, 0x56, 0x4e, 0x4d, 0x43, 0x44, 0x51, 0x45, 0x54, 0x4b, 0x9f, 0x42, 0x47, 0x40, 0x48, 0x4d, 0x52, 0x1f, 0x4e, 0x45, 0x1f, 0x44, 0x55, 0x44, 0x4d, 0x53, 0x52, 0x1d, 0x9f, 0x56, 0x4e, 0x51, 0x4a, 0x48, 0x4d, 0x46, 0x1f, 0x53, 0x47, 0x51, 0x4e, 0x54, 0x46, 0x47, 0x9f, 0x46, 0x44, 0x4d, 0x44, 0x51, 0x40, 0x53, 0x48, 0x4e, 0x4d, 0x52, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x4b, 0x44, 0x40, 0x43, 0x48, 0x4d, 0x46, 0x1f, 0x53, 0x4e, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x4c, 0x4e, 0x52, 0x53, 0x1f, 0x4e, 0x54, 0x53, 0x51, 0x44, 0x9f, 0x51, 0x44, 0x52, 0x54, 0x4b, 0x53, 0x52, 0x1d, 0x1f, 0x48, 0x53, 0x1f, 0x56, 0x4e, 0x54, 0x4b, 0x43, 0x9f, 0x4c, 0x40, 0x4a, 0x44, 0x1f, 0x40, 0x4b, 0x4b, 0x1f, 0x45, 0x48, 0x42, 0x53, 0x48, 0x4e, 0x4d, 0x9f, ], [0x56, 0x48, 0x53, 0x47, 0x1f, 0x48, 0x53, 0x52, 0x9f, 0x42, 0x4e, 0x4d, 0x55, 0x44, 0x4d, 0x53, 0x48, 0x4e, 0x4d, 0x40, 0x4b, 0x48, 0x53, 0x48, 0x44, 0x52, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x45, 0x4e, 0x51, 0x44, 0x52, 0x44, 0x44, 0x4d, 0x9f, 0x42, 0x4e, 0x4d, 0x42, 0x4b, 0x54, 0x52, 0x48, 0x4e, 0x4d, 0x52, 0x1f, 0x4c, 0x4e, 0x52, 0x53, 0x9f, 0x52, 0x53, 0x40, 0x4b, 0x44, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x54, 0x4d, 0x4f, 0x51, 0x4e, 0x45, 0x48, 0x53, 0x40, 0x41, 0x4b, 0x44, 0x1b, 0x9f, 0x1c, 0x3, 0x4e, 0x58, 0x4b, 0x44, 0x1f, 0x9f, 0x9f, ]], +[[0x13, 0x47, 0x44, 0x1f, 0x52, 0x53, 0x4e, 0x51, 0x58, 0x1f, 0x52, 0x4e, 0x1f, 0x45, 0x40, 0x51, 0x1b, 0x9f, 0x8, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x41, 0x44, 0x46, 0x48, 0x4d, 0x4d, 0x48, 0x4d, 0x46, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x14, 0x4d, 0x48, 0x55, 0x44, 0x51, 0x52, 0x44, 0x1f, 0x56, 0x40, 0x52, 0x9f, 0x42, 0x51, 0x44, 0x40, 0x53, 0x44, 0x43, 0x1b, 0x1f, 0x13, 0x47, 0x48, 0x52, 0x1f, 0x47, 0x40, 0x52, 0x9f, 0x4c, 0x40, 0x43, 0x44, 0x1f, 0x40, 0x1f, 0x4b, 0x4e, 0x53, 0x1f, 0x4e, 0x45, 0x9f, 0x4f, 0x44, 0x4e, 0x4f, 0x4b, 0x44, 0x1f, 0x55, 0x44, 0x51, 0x58, 0x1f, 0x40, 0x4d, 0x46, 0x51, 0x58, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x41, 0x44, 0x44, 0x4d, 0x1f, 0x56, 0x48, 0x43, 0x44, 0x4b, 0x58, 0x9f, 0x51, 0x44, 0x46, 0x40, 0x51, 0x43, 0x44, 0x43, 0x1f, 0x40, 0x52, 0x1f, 0x40, 0x1f, 0x41, 0x40, 0x43, 0x9f, ], [0x4c, 0x4e, 0x55, 0x44, 0x1b, 0x1f, 0x1c, 0x0, 0x43, 0x40, 0x4c, 0x52, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x5, 0x4e, 0x51, 0x1f, 0x48, 0x4d, 0x52, 0x53, 0x40, 0x4d, 0x42, 0x44, 0x1d, 0x1f, 0x4e, 0x4d, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x4f, 0x4b, 0x40, 0x4d, 0x44, 0x53, 0x1f, 0x4, 0x40, 0x51, 0x53, 0x47, 0x1d, 0x9f, 0x4c, 0x40, 0x4d, 0x1f, 0x47, 0x40, 0x43, 0x1f, 0x40, 0x4b, 0x56, 0x40, 0x58, 0x52, 0x9f, 0x40, 0x52, 0x52, 0x54, 0x4c, 0x44, 0x43, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x47, 0x44, 0x9f, 0x56, 0x40, 0x52, 0x1f, 0x4c, 0x4e, 0x51, 0x44, 0x9f, 0x48, 0x4d, 0x53, 0x44, 0x4b, 0x4b, 0x48, 0x46, 0x44, 0x4d, 0x53, 0x1f, 0x53, 0x47, 0x40, 0x4d, 0x9f, 0x43, 0x4e, 0x4b, 0x4f, 0x47, 0x48, 0x4d, 0x52, 0x1f, 0x41, 0x44, 0x42, 0x40, 0x54, 0x52, 0x44, 0x9f, 0x47, 0x44, 0x1f, 0x47, 0x40, 0x43, 0x1f, 0x40, 0x42, 0x47, 0x48, 0x44, 0x55, 0x44, 0x43, 0x1f, 0x52, 0xce, ], [0x4c, 0x54, 0x42, 0x47, 0x1c, 0x53, 0x47, 0x44, 0x1f, 0x56, 0x47, 0x44, 0x44, 0x4b, 0x1d, 0x9f, 0xd, 0x44, 0x56, 0x1f, 0x18, 0x4e, 0x51, 0x4a, 0x1d, 0x1f, 0x56, 0x40, 0x51, 0x52, 0x1f, 0x40, 0x4d, 0xc3, 0x52, 0x4e, 0x1f, 0x4e, 0x4d, 0x1c, 0x56, 0x47, 0x48, 0x4b, 0x52, 0x53, 0x1f, 0x40, 0x4b, 0x4b, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x43, 0x4e, 0x4b, 0x4f, 0x47, 0x48, 0x4d, 0x52, 0x1f, 0x47, 0x40, 0x43, 0x9f, 0x44, 0x55, 0x44, 0x51, 0x1f, 0x43, 0x4e, 0x4d, 0x44, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x4c, 0x54, 0x42, 0xca, 0x40, 0x41, 0x4e, 0x54, 0x53, 0x1f, 0x48, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x56, 0x40, 0x53, 0x44, 0xd1, 0x47, 0x40, 0x55, 0x48, 0x4d, 0x46, 0x1f, 0x40, 0x1f, 0x46, 0x4e, 0x4e, 0x43, 0x9f, 0x53, 0x48, 0x4c, 0x44, 0x1b, 0x1f, 0x1, 0x54, 0x53, 0x9f, ], [0x42, 0x4e, 0x4d, 0x55, 0x44, 0x51, 0x52, 0x44, 0x4b, 0x58, 0x1d, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x43, 0x4e, 0x4b, 0x4f, 0x47, 0x48, 0x4d, 0x52, 0x1f, 0x47, 0x40, 0x43, 0x9f, 0x40, 0x4b, 0x56, 0x40, 0x58, 0x52, 0x1f, 0x41, 0x44, 0x4b, 0x48, 0x44, 0x55, 0x44, 0x43, 0x9f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x58, 0x1f, 0x56, 0x44, 0x51, 0x44, 0x1f, 0x45, 0x40, 0xd1, 0x4c, 0x4e, 0x51, 0x44, 0x1f, 0x48, 0x4d, 0x53, 0x44, 0x4b, 0x4b, 0x48, 0x46, 0x44, 0x4d, 0x53, 0x9f, 0x53, 0x47, 0x40, 0x4d, 0x1f, 0x4c, 0x40, 0x4d, 0x1b, 0x45, 0x4e, 0x51, 0x9f, 0x4f, 0x51, 0x44, 0x42, 0x48, 0x52, 0x44, 0x4b, 0x58, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x52, 0x40, 0x4c, 0xc4, 0x51, 0x44, 0x40, 0x52, 0x4e, 0x4d, 0x52, 0x1b, 0x1f, 0x1c, 0x0, 0x43, 0x40, 0x4c, 0x52, 0x1f, 0x9f, ]], +[[0x8, 0x53, 0x1f, 0x48, 0x52, 0x1f, 0x4a, 0x4d, 0x4e, 0x56, 0x4d, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x9f, 0x53, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x40, 0x51, 0x44, 0x1f, 0x40, 0x4d, 0x9f, 0x48, 0x4d, 0x45, 0x48, 0x4d, 0x48, 0x53, 0x44, 0x1f, 0x4d, 0x54, 0x4c, 0x41, 0x44, 0x51, 0x1f, 0x4e, 0xc5, 0x56, 0x4e, 0x51, 0x4b, 0x43, 0x52, 0x1d, 0x1f, 0x52, 0x48, 0x4c, 0x4f, 0x4b, 0x58, 0x9f, 0x41, 0x44, 0x42, 0x40, 0x54, 0x52, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x48, 0x52, 0x9f, 0x40, 0x4d, 0x1f, 0x48, 0x4d, 0x45, 0x48, 0x4d, 0x48, 0x53, 0x44, 0x1f, 0x40, 0x4c, 0x4e, 0x54, 0x4d, 0xd3, 0x4e, 0x45, 0x1f, 0x52, 0x4f, 0x40, 0x42, 0x44, 0x1f, 0x45, 0x4e, 0x51, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x9f, 0x53, 0x4e, 0x1f, 0x41, 0x44, 0x1f, 0x48, 0x4d, 0x1b, 0x1f, 0x7, 0x4e, 0x56, 0x44, 0x55, 0x44, 0x51, 0x9d, ], [0x4d, 0x4e, 0x53, 0x1f, 0x44, 0x55, 0x44, 0x51, 0x58, 0x1f, 0x4e, 0x4d, 0x44, 0x1f, 0x4e, 0x45, 0x9f, 0x53, 0x47, 0x44, 0x4c, 0x1f, 0x48, 0x52, 0x1f, 0x48, 0x4d, 0x47, 0x40, 0x41, 0x48, 0x53, 0x44, 0x43, 0x9b, 0x13, 0x47, 0x44, 0x51, 0x44, 0x45, 0x4e, 0x51, 0x44, 0x1d, 0x1f, 0x53, 0x47, 0x44, 0x51, 0x44, 0x9f, 0x4c, 0x54, 0x52, 0x53, 0x1f, 0x41, 0x44, 0x1f, 0x40, 0x1f, 0x45, 0x48, 0x4d, 0x48, 0x53, 0x44, 0x9f, 0x4d, 0x54, 0x4c, 0x41, 0x44, 0x51, 0x1f, 0x4e, 0x45, 0x9f, 0x48, 0x4d, 0x47, 0x40, 0x41, 0x48, 0x53, 0x44, 0x43, 0x1f, 0x56, 0x4e, 0x51, 0x4b, 0x43, 0x52, 0x1b, 0x9f, 0x0, 0x4d, 0x58, 0x1f, 0x45, 0x48, 0x4d, 0x48, 0x53, 0x44, 0x1f, 0x4d, 0x54, 0x4c, 0x41, 0x44, 0x51, 0x9f, 0x43, 0x48, 0x55, 0x48, 0x43, 0x44, 0x43, 0x1f, 0x41, 0x58, 0x9f, ], [0x48, 0x4d, 0x45, 0x48, 0x4d, 0x48, 0x53, 0x58, 0x1f, 0x48, 0x52, 0x1f, 0x40, 0x52, 0x9f, 0x4d, 0x44, 0x40, 0x51, 0x1f, 0x53, 0x4e, 0x1f, 0x4d, 0x4e, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x1f, 0x40, 0xd2, 0x4c, 0x40, 0x4a, 0x44, 0x52, 0x1f, 0x4d, 0x4e, 0x1f, 0x4e, 0x43, 0x43, 0x52, 0x1d, 0x1f, 0x52, 0x4e, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x40, 0x55, 0x44, 0x51, 0x40, 0x46, 0x44, 0x9f, 0x4f, 0x4e, 0x4f, 0x54, 0x4b, 0x40, 0x53, 0x48, 0x4e, 0x4d, 0x1f, 0x4e, 0x45, 0x1f, 0x40, 0x4b, 0x4b, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x4f, 0x4b, 0x40, 0x4d, 0x44, 0x53, 0x52, 0x1f, 0x48, 0x4d, 0x1f, 0x53, 0x47, 0xc4, 0x14, 0x4d, 0x48, 0x55, 0x44, 0x51, 0x52, 0x44, 0x1f, 0x42, 0x40, 0x4d, 0x1f, 0x41, 0x44, 0x9f, 0x52, 0x40, 0x48, 0x43, 0x1f, 0x53, 0x4e, 0x1f, 0x41, 0x44, 0x1f, 0x59, 0x44, 0x51, 0x4e, 0x1b, 0x9f, ], [0x5, 0x51, 0x4e, 0x4c, 0x1f, 0x53, 0x47, 0x48, 0x52, 0x1f, 0x48, 0x53, 0x9f, 0x45, 0x4e, 0x4b, 0x4b, 0x4e, 0x56, 0x52, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x4f, 0x4e, 0x4f, 0x54, 0x4b, 0x40, 0x53, 0x48, 0x4e, 0x4d, 0x1f, 0x4e, 0x45, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x56, 0x47, 0x4e, 0x4b, 0x44, 0x1f, 0x14, 0x4d, 0x48, 0x55, 0x44, 0x51, 0x52, 0x44, 0x1f, 0x48, 0x52, 0x9f, 0x40, 0x4b, 0x52, 0x4e, 0x1f, 0x59, 0x44, 0x51, 0x4e, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x40, 0x4d, 0x58, 0x1f, 0x4f, 0x44, 0x4e, 0x4f, 0x4b, 0x44, 0x9f, 0x58, 0x4e, 0x54, 0x1f, 0x4c, 0x40, 0x58, 0x1f, 0x4c, 0x44, 0x44, 0x53, 0x1f, 0x45, 0x51, 0x4e, 0x4c, 0x9f, 0x53, 0x48, 0x4c, 0x44, 0x1f, 0x53, 0x4e, 0x1f, 0x53, 0x48, 0x4c, 0x44, 0x1f, 0x40, 0x51, 0x44, 0x9f, ], [0x4c, 0x44, 0x51, 0x44, 0x4b, 0x58, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x4f, 0x51, 0x4e, 0x43, 0x54, 0x42, 0x53, 0x52, 0x1f, 0x4e, 0x45, 0x1f, 0x40, 0x9f, 0x43, 0x44, 0x51, 0x40, 0x4d, 0x46, 0x44, 0x43, 0x9f, 0x48, 0x4c, 0x40, 0x46, 0x48, 0x4d, 0x40, 0x53, 0x48, 0x4e, 0x4d, 0x1b, 0x9f, 0x1c, 0x0, 0x43, 0x40, 0x4c, 0x52, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x5, 0x40, 0x51, 0x1f, 0x4e, 0x55, 0x44, 0x51, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x4c, 0x48, 0x52, 0x53, 0xd8, 0x4c, 0x4e, 0x54, 0x4d, 0x53, 0x40, 0x48, 0x4d, 0x52, 0x1f, 0x42, 0x4e, 0x4b, 0x43, 0x1f, 0x13, 0x4e, 0x9f, 0x43, 0x54, 0x4d, 0x46, 0x44, 0x4e, 0x4d, 0x52, 0x1f, 0x43, 0x44, 0x44, 0x4f, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x42, 0x40, 0x55, 0x44, 0x51, 0x4d, 0x52, 0x1f, 0x4e, 0x4b, 0x43, 0x1f, 0x16, 0x44, 0x9f, 0x4c, 0x54, 0x52, 0x53, 0x1f, 0x40, 0x56, 0x40, 0x58, 0x1d, 0x1f, 0x44, 0x51, 0x44, 0x9f, 0x41, 0x51, 0x44, 0x40, 0x4a, 0x1f, 0x4e, 0x45, 0x1f, 0x43, 0x40, 0x58, 0x1d, 0x1f, 0x13, 0x4e, 0x9f, 0x52, 0x44, 0x44, 0x4a, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x4f, 0x40, 0x4b, 0x44, 0x9f, 0x44, 0x4d, 0x42, 0x47, 0x40, 0x4d, 0x53, 0x44, 0x43, 0x1f, 0x46, 0x4e, 0x4b, 0x43, 0x1b, 0x9f, ], [0x13, 0x47, 0x44, 0x1f, 0x43, 0x56, 0x40, 0x51, 0x55, 0x44, 0x52, 0x1f, 0x4e, 0x45, 0x9f, 0x58, 0x4e, 0x51, 0x44, 0x1f, 0x4c, 0x40, 0x43, 0x44, 0x1f, 0x4c, 0x48, 0x46, 0x47, 0x53, 0x58, 0x9f, 0x52, 0x4f, 0x44, 0x4b, 0x4b, 0x52, 0x1d, 0x1f, 0x16, 0x47, 0x48, 0x4b, 0x44, 0x9f, 0x47, 0x40, 0x4c, 0x4c, 0x44, 0x51, 0x52, 0x1f, 0x45, 0x44, 0x4b, 0x4b, 0x1f, 0x4b, 0x48, 0x4a, 0x44, 0x9f, 0x51, 0x48, 0x4d, 0x46, 0x48, 0x4d, 0x46, 0x1f, 0x41, 0x44, 0x4b, 0x4b, 0x52, 0x1f, 0x8, 0x4d, 0x9f, 0x4f, 0x4b, 0x40, 0x42, 0x44, 0x52, 0x1f, 0x43, 0x44, 0x44, 0x4f, 0x1d, 0x1f, 0x56, 0x47, 0x44, 0x51, 0xc4, 0x43, 0x40, 0x51, 0x4a, 0x1f, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x52, 0x1f, 0x52, 0x4b, 0x44, 0x44, 0x4f, 0x9d, 0x8, 0x4d, 0x1f, 0x47, 0x4e, 0x4b, 0x4b, 0x4e, 0x56, 0x1f, 0x47, 0x40, 0x4b, 0x4b, 0x52, 0x9f, ], [0x41, 0x44, 0x4d, 0x44, 0x40, 0x53, 0x47, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x45, 0x44, 0x4b, 0x4b, 0x52, 0x9b, 0x5, 0x4e, 0x51, 0x1f, 0x40, 0x4d, 0x42, 0x48, 0x44, 0x4d, 0x53, 0x1f, 0x4a, 0x48, 0x4d, 0x46, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x44, 0x4b, 0x55, 0x48, 0x52, 0x47, 0x1f, 0x4b, 0x4e, 0x51, 0x43, 0x9f, 0x13, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x4c, 0x40, 0x4d, 0x58, 0x1f, 0x40, 0x9f, 0x46, 0x4b, 0x44, 0x40, 0x4c, 0x48, 0x4d, 0x46, 0x1f, 0x46, 0x4e, 0x4b, 0x43, 0x44, 0x4d, 0x9f, 0x47, 0x4e, 0x40, 0x51, 0x43, 0x1f, 0x13, 0x47, 0x44, 0x58, 0x1f, 0x52, 0x47, 0x40, 0x4f, 0x44, 0x43, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x56, 0x51, 0x4e, 0x54, 0x46, 0x47, 0x53, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x4b, 0x48, 0x46, 0x47, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x58, 0x1f, 0x42, 0x40, 0x54, 0x46, 0x47, 0x53, 0x9f, ], [0x13, 0x4e, 0x1f, 0x47, 0x48, 0x43, 0x44, 0x1f, 0x48, 0x4d, 0x1f, 0x46, 0x44, 0x4c, 0x52, 0x1f, 0x4e, 0xcd, 0x47, 0x48, 0x4b, 0x53, 0x1f, 0x4e, 0x45, 0x1f, 0x52, 0x56, 0x4e, 0x51, 0x43, 0x1b, 0x1f, 0xe, 0x4d, 0x9f, 0x52, 0x48, 0x4b, 0x55, 0x44, 0x51, 0x1f, 0x4d, 0x44, 0x42, 0x4a, 0x4b, 0x40, 0x42, 0x44, 0x52, 0x9f, 0x53, 0x47, 0x44, 0x58, 0x1f, 0x52, 0x53, 0x51, 0x54, 0x4d, 0x46, 0x1f, 0x13, 0x47, 0x44, 0x9f, 0x45, 0x4b, 0x4e, 0x56, 0x44, 0x51, 0x48, 0x4d, 0x46, 0x1f, 0x52, 0x53, 0x40, 0x51, 0x52, 0x1d, 0x9f, 0x4e, 0x4d, 0x1f, 0x42, 0x51, 0x4e, 0x56, 0x4d, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x58, 0x9f, 0x47, 0x54, 0x4d, 0x46, 0x1f, 0x13, 0x47, 0x44, 0x9f, 0x43, 0x51, 0x40, 0x46, 0x4e, 0x4d, 0x1c, 0x45, 0x48, 0x51, 0x44, 0x1d, 0x1f, 0x48, 0x4d, 0x9f, ], [0x53, 0x56, 0x48, 0x52, 0x53, 0x44, 0x43, 0x1f, 0x56, 0x48, 0x51, 0x44, 0x1f, 0x13, 0x47, 0x44, 0x58, 0x9f, 0x4c, 0x44, 0x52, 0x47, 0x44, 0x43, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x4b, 0x48, 0x46, 0x47, 0x53, 0x9f, 0x4e, 0x45, 0x1f, 0x4c, 0x4e, 0x4e, 0x4d, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x52, 0x54, 0x4d, 0x1b, 0x9f, 0x1c, 0x13, 0x4e, 0x4b, 0x4a, 0x48, 0x44, 0x4d, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x5, 0x40, 0x51, 0x1f, 0x4e, 0x55, 0x44, 0x51, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x4c, 0x48, 0x52, 0x53, 0xd8, 0x4c, 0x4e, 0x54, 0x4d, 0x53, 0x40, 0x48, 0x4d, 0x52, 0x1f, 0x42, 0x4e, 0x4b, 0x43, 0x1f, 0x13, 0x4e, 0x9f, 0x43, 0x54, 0x4d, 0x46, 0x44, 0x4e, 0x4d, 0x52, 0x1f, 0x43, 0x44, 0x44, 0x4f, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x42, 0x40, 0x55, 0x44, 0x51, 0x4d, 0x52, 0x1f, 0x4e, 0x4b, 0x43, 0x1f, 0x16, 0x44, 0x9f, 0x4c, 0x54, 0x52, 0x53, 0x1f, 0x40, 0x56, 0x40, 0x58, 0x1d, 0x1f, 0x44, 0x51, 0x44, 0x9f, 0x41, 0x51, 0x44, 0x40, 0x4a, 0x1f, 0x4e, 0x45, 0x1f, 0x43, 0x40, 0x58, 0x1d, 0x1f, 0x13, 0x4e, 0x9f, 0x42, 0x4b, 0x40, 0x48, 0x4c, 0x1f, 0x4e, 0x54, 0x51, 0x9f, 0x4b, 0x4e, 0x4d, 0x46, 0x1c, 0x45, 0x4e, 0x51, 0x46, 0x4e, 0x53, 0x53, 0x44, 0x4d, 0x9f, ], [0x46, 0x4e, 0x4b, 0x43, 0x1b, 0x1f, 0x6, 0x4e, 0x41, 0x4b, 0x44, 0x53, 0x52, 0x1f, 0x53, 0x47, 0x44, 0xd8, 0x42, 0x40, 0x51, 0x55, 0x44, 0x43, 0x1f, 0x53, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x45, 0x4e, 0x51, 0x9f, 0x53, 0x47, 0x44, 0x4c, 0x52, 0x44, 0x4b, 0x55, 0x44, 0x52, 0x1f, 0x0, 0x4d, 0x43, 0x9f, 0x47, 0x40, 0x51, 0x4f, 0x52, 0x1f, 0x4e, 0x45, 0x1f, 0x46, 0x4e, 0x4b, 0x43, 0x1d, 0x9f, 0x56, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x4d, 0x4e, 0x1f, 0x4c, 0x40, 0x4d, 0x9f, 0x43, 0x44, 0x4b, 0x55, 0x44, 0x52, 0x1f, 0x13, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x4b, 0x40, 0x58, 0x9f, 0x53, 0x47, 0x44, 0x58, 0x1f, 0x4b, 0x4e, 0x4d, 0x46, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x4c, 0x40, 0x4d, 0x58, 0x1f, 0x40, 0x1f, 0x52, 0x4e, 0x4d, 0x46, 0x1f, 0x16, 0x40, 0x52, 0x9f, ], [0x52, 0x54, 0x4d, 0x46, 0x1f, 0x54, 0x4d, 0x47, 0x44, 0x40, 0x51, 0x43, 0x1f, 0x41, 0x58, 0x9f, 0x4c, 0x44, 0x4d, 0x1f, 0x4e, 0x51, 0x1f, 0x44, 0x4b, 0x55, 0x44, 0x52, 0x1b, 0x1f, 0x13, 0x47, 0x44, 0x9f, 0x4f, 0x48, 0x4d, 0x44, 0x52, 0x1f, 0x56, 0x44, 0x51, 0x44, 0x1f, 0x51, 0x4e, 0x40, 0x51, 0x48, 0x4d, 0xc6, 0x4e, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x47, 0x44, 0x48, 0x46, 0x47, 0x53, 0x1d, 0x1f, 0x13, 0x47, 0xc4, 0x56, 0x48, 0x4d, 0x43, 0x52, 0x1f, 0x56, 0x44, 0x51, 0x44, 0x1f, 0x4c, 0x4e, 0x40, 0x4d, 0x48, 0x4d, 0xc6, 0x48, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x4d, 0x48, 0x46, 0x47, 0x53, 0x1b, 0x1f, 0x13, 0x47, 0x44, 0x9f, 0x45, 0x48, 0x51, 0x44, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x51, 0x44, 0x43, 0x1d, 0x1f, 0x48, 0x53, 0x9f, 0x45, 0x4b, 0x40, 0x4c, 0x48, 0x4d, 0x46, 0x1f, 0x52, 0x4f, 0x51, 0x44, 0x40, 0x43, 0x1b, 0x9f, ], [0x13, 0x47, 0x44, 0x1f, 0x53, 0x51, 0x44, 0x44, 0x52, 0x1f, 0x4b, 0x48, 0x4a, 0x44, 0x9f, 0x53, 0x4e, 0x51, 0x42, 0x47, 0x44, 0x52, 0x1f, 0x41, 0x4b, 0x40, 0x59, 0x44, 0x43, 0x9f, 0x56, 0x48, 0x53, 0x47, 0x1f, 0x4b, 0x48, 0x46, 0x47, 0x53, 0x1b, 0x9f, 0x1c, 0x13, 0x4e, 0x4b, 0x4a, 0x48, 0x44, 0x4d, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x13, 0x47, 0x44, 0x1f, 0x41, 0x44, 0x4b, 0x4b, 0x52, 0x1f, 0x56, 0x44, 0x51, 0x44, 0x9f, 0x51, 0x48, 0x4d, 0x46, 0x48, 0x4d, 0x46, 0x1f, 0x48, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x43, 0x40, 0x4b, 0x44, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x4c, 0x44, 0x4d, 0x9f, 0x4b, 0x4e, 0x4e, 0x4a, 0x44, 0x43, 0x1f, 0x54, 0x4f, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x9f, 0x45, 0x40, 0x42, 0x44, 0x52, 0x1f, 0x4f, 0x40, 0x4b, 0x44, 0x1b, 0x1f, 0x13, 0x47, 0x44, 0x4d, 0x9f, 0x43, 0x51, 0x40, 0x46, 0x4e, 0x4d, 0x5d, 0x52, 0x1f, 0x48, 0x51, 0x44, 0x1f, 0x4c, 0x4e, 0x51, 0x44, 0x9f, 0x45, 0x48, 0x44, 0x51, 0x42, 0x44, 0x1f, 0x53, 0x47, 0x40, 0x4d, 0x1f, 0x45, 0x48, 0x51, 0x44, 0x9f, 0xb, 0x40, 0x48, 0x43, 0x1f, 0x4b, 0x4e, 0x56, 0x1f, 0x53, 0x47, 0x44, 0x48, 0x51, 0x9f, ], [0x53, 0x4e, 0x56, 0x44, 0x51, 0x52, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x47, 0x4e, 0x54, 0x52, 0x44, 0x52, 0x9f, 0x45, 0x51, 0x40, 0x48, 0x4b, 0x1b, 0x1f, 0x13, 0x47, 0x44, 0x9f, 0x4c, 0x4e, 0x54, 0x4d, 0x53, 0x40, 0x48, 0x4d, 0x1f, 0x52, 0x4c, 0x4e, 0x4a, 0x44, 0x43, 0x9f, 0x41, 0x44, 0x4d, 0x44, 0x40, 0x53, 0x47, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x4c, 0x4e, 0x4e, 0x4d, 0x1b, 0x9f, 0x13, 0x47, 0x44, 0x1f, 0x43, 0x56, 0x40, 0x51, 0x55, 0x44, 0x52, 0x1d, 0x1f, 0x53, 0x47, 0x44, 0x58, 0x9f, 0x47, 0x44, 0x40, 0x51, 0x43, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x53, 0x51, 0x40, 0x4c, 0x4f, 0x1f, 0x4e, 0xc5, 0x43, 0x4e, 0x4e, 0x4c, 0x1b, 0x1f, 0x13, 0x47, 0x44, 0x58, 0x1f, 0x45, 0x4b, 0x44, 0x43, 0x9f, 0x53, 0x47, 0x44, 0x48, 0x51, 0x1f, 0x47, 0x40, 0x4b, 0x4b, 0x1d, 0x1f, 0x53, 0x4e, 0x9f, ], [0x43, 0x58, 0x48, 0x4d, 0x46, 0x1f, 0x45, 0x40, 0x4b, 0x4b, 0x1f, 0x1, 0x44, 0x4d, 0x44, 0x40, 0x53, 0xc7, 0x47, 0x48, 0x52, 0x1f, 0x45, 0x44, 0x44, 0x53, 0x1d, 0x1f, 0x41, 0x44, 0x4d, 0x44, 0x40, 0x53, 0x47, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x4c, 0x4e, 0x4e, 0x4d, 0x1b, 0x1f, 0x5, 0x40, 0x51, 0x1f, 0x4e, 0x55, 0x44, 0xd1, 0x53, 0x47, 0x44, 0x1f, 0x4c, 0x48, 0x52, 0x53, 0x58, 0x9f, 0x4c, 0x4e, 0x54, 0x4d, 0x53, 0x40, 0x48, 0x4d, 0x52, 0x1f, 0x46, 0x51, 0x48, 0x4c, 0x1f, 0x13, 0x4e, 0x9f, 0x43, 0x54, 0x4d, 0x46, 0x44, 0x4e, 0x4d, 0x52, 0x1f, 0x43, 0x44, 0x44, 0x4f, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x42, 0x40, 0x55, 0x44, 0x51, 0x4d, 0x52, 0x1f, 0x43, 0x48, 0x4c, 0x1f, 0x16, 0x44, 0x9f, 0x4c, 0x54, 0x52, 0x53, 0x1f, 0x40, 0x56, 0x40, 0x58, 0x1d, 0x1f, 0x44, 0x51, 0x44, 0x9f, ], [0x41, 0x51, 0x44, 0x40, 0x4a, 0x1f, 0x4e, 0x45, 0x1f, 0x43, 0x40, 0x58, 0x1d, 0x1f, 0x13, 0x4e, 0x9f, 0x56, 0x48, 0x4d, 0x1f, 0x4e, 0x54, 0x51, 0x1f, 0x47, 0x40, 0x51, 0x4f, 0x52, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x46, 0x4e, 0x4b, 0x43, 0x1f, 0x45, 0x51, 0x4e, 0x4c, 0x1f, 0x47, 0x48, 0x4c, 0x1a, 0x9f, 0x1c, 0x13, 0x4e, 0x4b, 0x4a, 0x48, 0x44, 0x4d, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x11, 0x4e, 0x40, 0x43, 0x52, 0x1f, 0x46, 0x4e, 0x1f, 0x44, 0x55, 0x44, 0x51, 0x1f, 0x44, 0x55, 0x44, 0xd1, 0x4e, 0x4d, 0x1d, 0x1f, 0xe, 0x55, 0x44, 0x51, 0x1f, 0x51, 0x4e, 0x42, 0x4a, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x54, 0x4d, 0x43, 0x44, 0x51, 0x1f, 0x53, 0x51, 0x44, 0x44, 0x1d, 0x1f, 0x1, 0x58, 0x9f, 0x42, 0x40, 0x55, 0x44, 0x52, 0x1f, 0x56, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x4d, 0x44, 0x55, 0x44, 0x51, 0x9f, 0x52, 0x54, 0x4d, 0x1f, 0x47, 0x40, 0x52, 0x1f, 0x52, 0x47, 0x4e, 0x4d, 0x44, 0x1d, 0x1f, 0x1, 0x58, 0x9f, 0x52, 0x53, 0x51, 0x44, 0x40, 0x4c, 0x52, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x4d, 0x44, 0x55, 0x44, 0xd1, 0x45, 0x48, 0x4d, 0x43, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x52, 0x44, 0x40, 0x1b, 0x1f, 0xe, 0x55, 0x44, 0xd1, 0x52, 0x4d, 0x4e, 0x56, 0x1f, 0x41, 0x58, 0x1f, 0x56, 0x48, 0x4d, 0x53, 0x44, 0x51, 0x9f, ], [0x52, 0x4e, 0x56, 0x4d, 0x1d, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x53, 0x47, 0x51, 0x4e, 0x54, 0x46, 0x47, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x4c, 0x44, 0x51, 0x51, 0x58, 0x1f, 0x45, 0x4b, 0x4e, 0x56, 0x44, 0x51, 0x52, 0x9f, 0x4e, 0x45, 0x1f, 0x9, 0x54, 0x4d, 0x44, 0x1d, 0x1f, 0xe, 0x55, 0x44, 0x51, 0x9f, 0x46, 0x51, 0x40, 0x52, 0x52, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x4e, 0x55, 0x44, 0x51, 0x9f, 0x52, 0x53, 0x4e, 0x4d, 0x44, 0x1d, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x54, 0x4d, 0x43, 0x44, 0x51, 0x9f, 0x4c, 0x4e, 0x54, 0x4d, 0x53, 0x40, 0x48, 0x4d, 0x52, 0x1f, 0x4e, 0x45, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x4c, 0x4e, 0x4e, 0x4d, 0x1b, 0x1f, 0x11, 0x4e, 0x40, 0x43, 0x52, 0x1f, 0x46, 0x4e, 0x9f, 0x44, 0x55, 0x44, 0x51, 0x1f, 0x44, 0x55, 0x44, 0x51, 0x1f, 0x4e, 0x4d, 0x1f, 0x14, 0x4d, 0x43, 0x44, 0xd1, ], [0x42, 0x4b, 0x4e, 0x54, 0x43, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x54, 0x4d, 0x43, 0x44, 0x51, 0x9f, 0x52, 0x53, 0x40, 0x51, 0x1d, 0x1f, 0x18, 0x44, 0x53, 0x1f, 0x45, 0x44, 0x44, 0x53, 0x9f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x56, 0x40, 0x4d, 0x43, 0x44, 0x51, 0x48, 0x4d, 0x46, 0x9f, 0x47, 0x40, 0x55, 0x44, 0x1f, 0x46, 0x4e, 0x4d, 0x44, 0x1f, 0x13, 0x54, 0x51, 0x4d, 0x1f, 0x40, 0x53, 0x9f, 0x4b, 0x40, 0x52, 0x53, 0x1f, 0x53, 0x4e, 0x1f, 0x47, 0x4e, 0x4c, 0x44, 0x1f, 0x40, 0x45, 0x40, 0x51, 0x9b, 0x4, 0x58, 0x44, 0x52, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x45, 0x48, 0x51, 0x44, 0x1f, 0x40, 0x4d, 0xc3, 0x52, 0x56, 0x4e, 0x51, 0x43, 0x1f, 0x47, 0x40, 0x55, 0x44, 0x1f, 0x52, 0x44, 0x44, 0x4d, 0x9f, 0x0, 0x4d, 0x43, 0x1f, 0x47, 0x4e, 0x51, 0x51, 0x4e, 0x51, 0x1f, 0x48, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x9f, ], [0x47, 0x40, 0x4b, 0x4b, 0x52, 0x1f, 0x4e, 0x45, 0x1f, 0x52, 0x53, 0x4e, 0x4d, 0x44, 0x9f, 0xb, 0x4e, 0x4e, 0x4a, 0x1f, 0x40, 0x53, 0x1f, 0x4b, 0x40, 0x52, 0x53, 0x1f, 0x4e, 0x4d, 0x9f, 0x4c, 0x44, 0x40, 0x43, 0x4e, 0x56, 0x52, 0x1f, 0x46, 0x51, 0x44, 0x44, 0x4d, 0x1f, 0x0, 0x4d, 0x43, 0x9f, 0x53, 0x51, 0x44, 0x44, 0x52, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x47, 0x48, 0x4b, 0x4b, 0x52, 0x9f, 0x53, 0x47, 0x44, 0x58, 0x1f, 0x4b, 0x4e, 0x4d, 0x46, 0x1f, 0x47, 0x40, 0x55, 0x44, 0x9f, 0x4a, 0x4d, 0x4e, 0x56, 0x4d, 0x1b, 0x1f, 0x1c, 0x13, 0x4e, 0x4b, 0x4a, 0x48, 0x44, 0x4d, 0x1f, 0x9f, 0x9f, 0x9f, ]], +[[0x13, 0x47, 0x51, 0x44, 0x44, 0x1f, 0x11, 0x48, 0x4d, 0x46, 0x52, 0x1f, 0x45, 0x4e, 0x51, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x4, 0x4b, 0x55, 0x44, 0x4d, 0x1c, 0x4a, 0x48, 0x4d, 0x46, 0x52, 0x9f, 0x54, 0x4d, 0x43, 0x44, 0x51, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x52, 0x4a, 0x58, 0x1d, 0x9f, 0x12, 0x44, 0x55, 0x44, 0x4d, 0x1f, 0x45, 0x4e, 0x51, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x3, 0x56, 0x40, 0x51, 0x45, 0x1c, 0x4b, 0x4e, 0x51, 0x43, 0x52, 0x1f, 0x48, 0x4d, 0x9f, 0x53, 0x47, 0x44, 0x48, 0x51, 0x1f, 0x47, 0x40, 0x4b, 0x4b, 0x52, 0x1f, 0x4e, 0x45, 0x9f, 0x52, 0x53, 0x4e, 0x4d, 0x44, 0x1d, 0x1f, 0xd, 0x48, 0x4d, 0x44, 0x1f, 0x45, 0x4e, 0x51, 0x9f, 0xc, 0x4e, 0x51, 0x53, 0x40, 0x4b, 0x1f, 0xc, 0x44, 0x4d, 0x1f, 0x43, 0x4e, 0x4e, 0x4c, 0x44, 0x43, 0x9f, ], [0x53, 0x4e, 0x1f, 0x43, 0x48, 0x44, 0x1d, 0x1f, 0xe, 0x4d, 0x44, 0x1f, 0x45, 0x4e, 0x51, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x3, 0x40, 0x51, 0x4a, 0x1f, 0xb, 0x4e, 0x51, 0x43, 0x1f, 0x4e, 0x4d, 0x9f, 0x47, 0x48, 0x52, 0x1f, 0x43, 0x40, 0x51, 0x4a, 0x1f, 0x53, 0x47, 0x51, 0x4e, 0x4d, 0x44, 0x1f, 0x8, 0xcd, 0x53, 0x47, 0x44, 0x1f, 0xb, 0x40, 0x4d, 0x43, 0x1f, 0x4e, 0x45, 0x1f, 0xc, 0x4e, 0x51, 0x43, 0x4e, 0xd1, 0x56, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x12, 0x47, 0x40, 0x43, 0x4e, 0x56, 0x52, 0x9f, 0x4b, 0x48, 0x44, 0x1b, 0x1f, 0xe, 0x4d, 0x44, 0x1f, 0x11, 0x48, 0x4d, 0x46, 0x1f, 0x53, 0x4e, 0x9f, 0x51, 0x54, 0x4b, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x1f, 0x40, 0x4b, 0x4b, 0x1d, 0x1f, 0xe, 0x4d, 0xc4, 0x11, 0x48, 0x4d, 0x46, 0x1f, 0x53, 0x4e, 0x1f, 0x45, 0x48, 0x4d, 0x43, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x9d, ], [0xe, 0x4d, 0x44, 0x1f, 0x11, 0x48, 0x4d, 0x46, 0x1f, 0x53, 0x4e, 0x1f, 0x41, 0x51, 0x48, 0x4d, 0x46, 0x9f, 0x53, 0x47, 0x44, 0x4c, 0x1f, 0x40, 0x4b, 0x4b, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x48, 0x4d, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x43, 0x40, 0x51, 0x4a, 0x4d, 0x44, 0x52, 0x52, 0x1f, 0x41, 0x48, 0x4d, 0x43, 0x9f, 0x53, 0x47, 0x44, 0x4c, 0x1d, 0x1f, 0x8, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0xb, 0x40, 0x4d, 0x43, 0x9f, 0x4e, 0x45, 0x1f, 0xc, 0x4e, 0x51, 0x43, 0x4e, 0x51, 0x1f, 0x56, 0x47, 0x44, 0x51, 0x44, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x12, 0x47, 0x40, 0x43, 0x4e, 0x56, 0x52, 0x1f, 0x4b, 0x48, 0x44, 0x1b, 0x9f, 0x1c, 0x13, 0x4e, 0x4b, 0x4a, 0x48, 0x44, 0x4d, 0x1f, 0x9f, 0x9f, ]], +[[0x0, 0x4b, 0x4b, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x48, 0x52, 0x1f, 0x46, 0x4e, 0x4b, 0x43, 0x9f, 0x43, 0x4e, 0x44, 0x52, 0x1f, 0x4d, 0x4e, 0x53, 0x1f, 0x46, 0x4b, 0x48, 0x53, 0x53, 0x44, 0x51, 0x1d, 0x9f, 0xd, 0x4e, 0x53, 0x1f, 0x40, 0x4b, 0x4b, 0x1f, 0x53, 0x47, 0x4e, 0x52, 0x44, 0x1f, 0x56, 0x47, 0x4e, 0x9f, 0x56, 0x40, 0x4d, 0x43, 0x44, 0x51, 0x1f, 0x40, 0x51, 0x44, 0x1f, 0x4b, 0x4e, 0x52, 0x53, 0x1b, 0x9f, 0x13, 0x47, 0x44, 0x1f, 0x4e, 0x4b, 0x43, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x48, 0x52, 0x9f, 0x52, 0x53, 0x51, 0x4e, 0x4d, 0x46, 0x1f, 0x43, 0x4e, 0x44, 0x52, 0x1f, 0x4d, 0x4e, 0x53, 0x9f, 0x56, 0x48, 0x53, 0x47, 0x44, 0x51, 0x1d, 0x1f, 0x3, 0x44, 0x44, 0x4f, 0x1f, 0x51, 0x4e, 0x4e, 0x53, 0xd2, 0x40, 0x51, 0x44, 0x1f, 0x4d, 0x4e, 0x53, 0x1f, 0x51, 0x44, 0x40, 0x42, 0x47, 0x44, 0x43, 0x1f, 0x41, 0xd8, ], [0x53, 0x47, 0x44, 0x1f, 0x45, 0x51, 0x4e, 0x52, 0x53, 0x1b, 0x1f, 0x5, 0x51, 0x4e, 0x4c, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x40, 0x52, 0x47, 0x44, 0x52, 0x1f, 0x40, 0x1f, 0x45, 0x48, 0x51, 0x44, 0x9f, 0x52, 0x47, 0x40, 0x4b, 0x4b, 0x1f, 0x41, 0x44, 0x1f, 0x56, 0x4e, 0x4a, 0x44, 0x4d, 0x1d, 0x1f, 0x0, 0x9f, 0x4b, 0x48, 0x46, 0x47, 0x53, 0x1f, 0x45, 0x51, 0x4e, 0x4c, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x52, 0x47, 0x40, 0x43, 0x4e, 0x56, 0x52, 0x1f, 0x52, 0x47, 0x40, 0x4b, 0x4b, 0x9f, 0x52, 0x4f, 0x51, 0x48, 0x4d, 0x46, 0x1b, 0x1f, 0x11, 0x44, 0x4d, 0x44, 0x56, 0x44, 0x43, 0x9f, 0x52, 0x47, 0x40, 0x4b, 0x4b, 0x1f, 0x41, 0x44, 0x1f, 0x41, 0x4b, 0x40, 0x43, 0x44, 0x9f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x41, 0x51, 0x4e, 0x4a, 0x44, 0x4d, 0x1d, 0x9f, ], [0x13, 0x47, 0x44, 0x1f, 0x42, 0x51, 0x4e, 0x56, 0x4d, 0x4b, 0x44, 0x52, 0x52, 0x9f, 0x40, 0x46, 0x40, 0x48, 0x4d, 0x1f, 0x52, 0x47, 0x40, 0x4b, 0x4b, 0x1f, 0x41, 0x44, 0x9f, 0x4a, 0x48, 0x4d, 0x46, 0x1b, 0x1f, 0x1c, 0x13, 0x4e, 0x4b, 0x4a, 0x48, 0x44, 0x4d, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x16, 0x47, 0x4e, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x52, 0x1d, 0x1f, 0x2, 0x40, 0x4d, 0x1b, 0x9f, 0x16, 0x47, 0x4e, 0x1f, 0x53, 0x51, 0x48, 0x44, 0x52, 0x1d, 0x1f, 0x3, 0x4e, 0x44, 0x52, 0x1b, 0x9f, 0x16, 0x47, 0x4e, 0x1f, 0x4b, 0x4e, 0x55, 0x44, 0x52, 0x1d, 0x1f, 0xb, 0x48, 0x55, 0x44, 0x52, 0x1b, 0x9f, 0x1c, 0xc, 0x42, 0x2, 0x40, 0x45, 0x45, 0x51, 0x44, 0x58, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x6, 0x4e, 0x4d, 0x44, 0x1f, 0x40, 0x56, 0x40, 0x58, 0x1d, 0x1f, 0x46, 0x4e, 0x4d, 0x44, 0x9f, 0x40, 0x47, 0x44, 0x40, 0x43, 0x1d, 0x1f, 0x4, 0x42, 0x47, 0x4e, 0x44, 0x52, 0x1f, 0x51, 0x4e, 0x4b, 0xcb, 0x54, 0x4d, 0x40, 0x4d, 0x52, 0x56, 0x44, 0x51, 0x44, 0x43, 0x1b, 0x1f, 0x4, 0x4c, 0x4f, 0x53, 0x58, 0x9d, 0x4e, 0x4f, 0x44, 0x4d, 0x1d, 0x1f, 0x43, 0x54, 0x52, 0x53, 0x58, 0x1d, 0x1f, 0x43, 0x44, 0x40, 0x43, 0x9d, 0x16, 0x47, 0x58, 0x1f, 0x47, 0x40, 0x55, 0x44, 0x1f, 0x40, 0x4b, 0x4b, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x16, 0x44, 0x58, 0x51, 0x45, 0x4e, 0x4b, 0x4a, 0x1f, 0x45, 0x4b, 0x44, 0x43, 0x1e, 0x9f, 0x16, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x47, 0x40, 0x55, 0x44, 0x1f, 0x43, 0x51, 0x40, 0x46, 0x4e, 0x4d, 0xd2, 0x46, 0x4e, 0x4d, 0x44, 0x1f, 0x53, 0x4e, 0x46, 0x44, 0x53, 0x47, 0x44, 0x51, 0x1e, 0x9f, ], [0xb, 0x44, 0x40, 0x55, 0x48, 0x4d, 0x46, 0x1f, 0x16, 0x44, 0x58, 0x51, 0x52, 0x1f, 0x53, 0x4e, 0x9f, 0x56, 0x48, 0x4d, 0x43, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x56, 0x44, 0x40, 0x53, 0x47, 0x44, 0x51, 0x1e, 0x9f, 0x12, 0x44, 0x53, 0x53, 0x48, 0x4d, 0x46, 0x1f, 0x47, 0x44, 0x51, 0x43, 0x41, 0x44, 0x40, 0x52, 0x53, 0xd2, 0x45, 0x51, 0x44, 0x44, 0x1f, 0x4e, 0x45, 0x1f, 0x53, 0x44, 0x53, 0x47, 0x44, 0x51, 0x1e, 0x9f, 0x6, 0x4e, 0x4d, 0x44, 0x1d, 0x1f, 0x4e, 0x54, 0x51, 0x9f, 0x52, 0x40, 0x45, 0x44, 0x46, 0x54, 0x40, 0x51, 0x43, 0x52, 0x1d, 0x1f, 0x46, 0x4e, 0x4d, 0x44, 0x1d, 0x9f, 0x41, 0x54, 0x53, 0x1f, 0x56, 0x47, 0x48, 0x53, 0x47, 0x44, 0x51, 0x1e, 0x1f, 0x7, 0x40, 0x55, 0x44, 0x9f, 0x53, 0x47, 0x44, 0x58, 0x1f, 0x45, 0x4b, 0x4e, 0x56, 0x4d, 0x1f, 0x53, 0x4e, 0x1f, 0x52, 0x4e, 0x4c, 0xc4, ], [0x4d, 0x44, 0x56, 0x1f, 0x16, 0x44, 0x58, 0x51, 0x1f, 0x16, 0x47, 0x44, 0x4d, 0x9f, 0x42, 0x51, 0x54, 0x44, 0x4b, 0x1f, 0x13, 0x47, 0x51, 0x44, 0x40, 0x43, 0x52, 0x1f, 0x52, 0x4e, 0x4c, 0xc4, 0x4e, 0x53, 0x47, 0x44, 0x51, 0x52, 0x1f, 0x45, 0x44, 0x40, 0x51, 0x1e, 0x1f, 0x0, 0x51, 0x44, 0x9f, 0x53, 0x47, 0x44, 0x58, 0x1f, 0x56, 0x4e, 0x51, 0x4b, 0x43, 0x52, 0x1f, 0x40, 0x56, 0x40, 0x58, 0x9f, 0x45, 0x51, 0x4e, 0x4c, 0x1f, 0x47, 0x44, 0x51, 0x44, 0x1e, 0x1f, 0x16, 0x47, 0x58, 0x1d, 0x9f, 0x4e, 0x47, 0x1d, 0x1f, 0x56, 0x47, 0x58, 0x1d, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x44, 0x4c, 0x4f, 0x53, 0xd8, 0x16, 0x44, 0x58, 0x51, 0x1e, 0x1f, 0x1c, 0xc, 0x42, 0x2, 0x40, 0x45, 0x45, 0x51, 0x44, 0x58, 0x1f, 0x9f, 0x9f, ]], +[[0xc, 0x51, 0x1b, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0xc, 0x51, 0x52, 0x1b, 0x9f, 0x3, 0x54, 0x51, 0x52, 0x4b, 0x44, 0x58, 0x1d, 0x1f, 0x4e, 0x45, 0x1f, 0x4d, 0x54, 0x4c, 0x41, 0x44, 0xd1, 0x45, 0x4e, 0x54, 0x51, 0x1d, 0x1f, 0xf, 0x51, 0x48, 0x55, 0x44, 0x53, 0x9f, 0x3, 0x51, 0x48, 0x55, 0x44, 0x1d, 0x1f, 0x56, 0x44, 0x51, 0x44, 0x1f, 0x4f, 0x51, 0x4e, 0x54, 0x43, 0x9f, 0x53, 0x4e, 0x1f, 0x52, 0x40, 0x58, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x58, 0x9f, 0x56, 0x44, 0x51, 0x44, 0x1f, 0x4f, 0x44, 0x51, 0x45, 0x44, 0x42, 0x53, 0x4b, 0x58, 0x9f, 0x4d, 0x4e, 0x51, 0x4c, 0x40, 0x4b, 0x1d, 0x1f, 0x53, 0x47, 0x40, 0x4d, 0x4a, 0x1f, 0x58, 0x4e, 0x54, 0x9f, 0x55, 0x44, 0x51, 0x58, 0x1f, 0x4c, 0x54, 0x42, 0x47, 0x1b, 0x1f, 0x13, 0x47, 0x44, 0x58, 0x9f, ], [0x56, 0x44, 0x51, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x4b, 0x40, 0x52, 0x53, 0x9f, 0x4f, 0x44, 0x4e, 0x4f, 0x4b, 0x44, 0x1f, 0x58, 0x4e, 0x54, 0x5d, 0x43, 0x9f, 0x44, 0x57, 0x4f, 0x44, 0x42, 0x53, 0x1f, 0x53, 0x4e, 0x1f, 0x41, 0x44, 0x9f, 0x48, 0x4d, 0x55, 0x4e, 0x4b, 0x55, 0x44, 0x43, 0x1f, 0x48, 0x4d, 0x9f, 0x40, 0x4d, 0x58, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x1f, 0x52, 0x53, 0x51, 0x40, 0x4d, 0x46, 0x44, 0x9f, 0x4e, 0x51, 0x1f, 0x4c, 0x58, 0x52, 0x53, 0x44, 0x51, 0x48, 0x4e, 0x54, 0x52, 0x1d, 0x9f, 0x41, 0x44, 0x42, 0x40, 0x54, 0x52, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x58, 0x1f, 0x49, 0x54, 0x52, 0x53, 0x9f, 0x43, 0x48, 0x43, 0x4d, 0x5d, 0x53, 0x1f, 0x47, 0x4e, 0x4b, 0x43, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x9f, ], [0x52, 0x54, 0x42, 0x47, 0x1f, 0x4d, 0x4e, 0x4d, 0x52, 0x44, 0x4d, 0x52, 0x44, 0x1b, 0x9f, 0x1c, 0x11, 0x4e, 0x56, 0x4b, 0x48, 0x4d, 0x46, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0xb, 0x4e, 0x55, 0x44, 0x1d, 0x1f, 0x56, 0x47, 0x48, 0x42, 0x47, 0x9f, 0x50, 0x54, 0x48, 0x42, 0x4a, 0x4b, 0x58, 0x1f, 0x40, 0x51, 0x51, 0x44, 0x52, 0x53, 0x52, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x46, 0x44, 0x4d, 0x53, 0x4b, 0x44, 0x1f, 0x47, 0x44, 0x40, 0x51, 0x53, 0x1d, 0x9f, 0x12, 0x44, 0x48, 0x59, 0x44, 0x43, 0x1f, 0x47, 0x48, 0x4c, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x1f, 0x4c, 0xd8, 0x41, 0x44, 0x40, 0x54, 0x53, 0x48, 0x45, 0x54, 0x4b, 0x1f, 0x45, 0x4e, 0x51, 0x4c, 0x9f, 0x13, 0x47, 0x40, 0x53, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x53, 0x40, 0x4a, 0x44, 0x4d, 0x9f, 0x45, 0x51, 0x4e, 0x4c, 0x1f, 0x4c, 0x44, 0x1d, 0x1f, 0x48, 0x4d, 0x1f, 0x40, 0x9f, 0x4c, 0x40, 0x4d, 0x4d, 0x44, 0x51, 0x1f, 0x56, 0x47, 0x48, 0x42, 0x47, 0x1f, 0x52, 0x53, 0x48, 0x4b, 0xcb, ], [0x46, 0x51, 0x48, 0x44, 0x55, 0x44, 0x52, 0x1f, 0x4c, 0x44, 0x1b, 0x1f, 0xb, 0x4e, 0x55, 0x44, 0x1d, 0x9f, 0x56, 0x47, 0x48, 0x42, 0x47, 0x1f, 0x4f, 0x40, 0x51, 0x43, 0x4e, 0x4d, 0x52, 0x1f, 0x4d, 0x4e, 0x9f, 0x41, 0x44, 0x4b, 0x4e, 0x55, 0x44, 0x43, 0x1f, 0x45, 0x51, 0x4e, 0x4c, 0x9f, 0x4b, 0x4e, 0x55, 0x48, 0x4d, 0x46, 0x1d, 0x1f, 0x53, 0x4e, 0x4e, 0x4a, 0x1f, 0x4c, 0x44, 0x1f, 0x52, 0xce, 0x52, 0x53, 0x51, 0x4e, 0x4d, 0x46, 0x4b, 0x58, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x9f, 0x43, 0x44, 0x4b, 0x48, 0x46, 0x47, 0x53, 0x1f, 0x48, 0x4d, 0x1f, 0x47, 0x48, 0x4c, 0x9f, 0x13, 0x47, 0x40, 0x53, 0x1d, 0x1f, 0x40, 0x52, 0x1f, 0x58, 0x4e, 0x54, 0x1f, 0x52, 0x44, 0x44, 0x1d, 0x9f, 0x48, 0x53, 0x1f, 0x52, 0x53, 0x48, 0x4b, 0x4b, 0x1f, 0x40, 0x41, 0x40, 0x4d, 0x43, 0x4e, 0x4d, 0x52, 0x9f, ], [0x4c, 0x44, 0x1f, 0x4d, 0x4e, 0x53, 0x1b, 0x1b, 0x1b, 0x9f, 0x1c, 0x0, 0x4b, 0x48, 0x46, 0x47, 0x48, 0x44, 0x51, 0x48, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x8, 0x1f, 0x42, 0x40, 0x4d, 0x4d, 0x4e, 0x53, 0x1f, 0x44, 0x57, 0x4f, 0x51, 0x44, 0x52, 0x52, 0x9f, 0x48, 0x53, 0x1d, 0x1f, 0x41, 0x54, 0x53, 0x1f, 0x52, 0x54, 0x51, 0x44, 0x4b, 0x58, 0x1f, 0x58, 0x4e, 0xd4, 0x40, 0x4d, 0x43, 0x1f, 0x44, 0x55, 0x44, 0x51, 0x58, 0x41, 0x4e, 0x43, 0x58, 0x1f, 0x47, 0x40, 0x55, 0xc4, 0x40, 0x1f, 0x4d, 0x4e, 0x53, 0x48, 0x4e, 0x4d, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x9f, 0x53, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x48, 0x52, 0x1f, 0x4e, 0x51, 0x1f, 0x52, 0x47, 0x4e, 0x54, 0x4b, 0xc3, 0x41, 0x44, 0x1f, 0x40, 0x4d, 0x1f, 0x44, 0x57, 0x48, 0x52, 0x53, 0x44, 0x4d, 0x42, 0x44, 0x1f, 0x4e, 0xc5, 0x58, 0x4e, 0x54, 0x51, 0x52, 0x1f, 0x41, 0x44, 0x58, 0x4e, 0x4d, 0x43, 0x1f, 0x58, 0x4e, 0x54, 0x1b, 0x9f, 0x16, 0x47, 0x40, 0x53, 0x1f, 0x56, 0x44, 0x51, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x54, 0x52, 0x44, 0x9f, ], [0x4e, 0x45, 0x1f, 0x4c, 0x58, 0x1f, 0x42, 0x51, 0x44, 0x40, 0x53, 0x48, 0x4e, 0x4d, 0x1d, 0x1f, 0x48, 0xc5, 0x8, 0x1f, 0x56, 0x44, 0x51, 0x44, 0x1f, 0x44, 0x4d, 0x53, 0x48, 0x51, 0x44, 0x4b, 0x58, 0x9f, 0x42, 0x4e, 0x4d, 0x53, 0x40, 0x48, 0x4d, 0x44, 0x43, 0x1f, 0x47, 0x44, 0x51, 0x44, 0x1e, 0x1f, 0xc, 0xd8, 0x46, 0x51, 0x44, 0x40, 0x53, 0x1f, 0x4c, 0x48, 0x52, 0x44, 0x51, 0x48, 0x44, 0x52, 0x1f, 0x48, 0x4d, 0x9f, 0x53, 0x47, 0x48, 0x52, 0x1f, 0x56, 0x4e, 0x51, 0x4b, 0x43, 0x1f, 0x47, 0x40, 0x55, 0x44, 0x9f, 0x41, 0x44, 0x44, 0x4d, 0x1f, 0x7, 0x44, 0x40, 0x53, 0x47, 0x42, 0x4b, 0x48, 0x45, 0x45, 0x5d, 0x52, 0x9f, 0x4c, 0x48, 0x52, 0x44, 0x51, 0x48, 0x44, 0x52, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x8, 0x9f, 0x56, 0x40, 0x53, 0x42, 0x47, 0x44, 0x43, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x45, 0x44, 0x4b, 0x53, 0x9f, ], [0x44, 0x40, 0x42, 0x47, 0x1f, 0x45, 0x51, 0x4e, 0x4c, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x41, 0x44, 0x46, 0x48, 0x4d, 0x4d, 0x48, 0x4d, 0x46, 0x1b, 0x1f, 0x4c, 0x58, 0x9f, 0x46, 0x51, 0x44, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x4e, 0x54, 0x46, 0x47, 0x53, 0x1f, 0x48, 0x4d, 0x9f, 0x4b, 0x48, 0x55, 0x48, 0x4d, 0x46, 0x1f, 0x48, 0x52, 0x1f, 0x47, 0x48, 0x4c, 0x52, 0x44, 0x4b, 0x45, 0x9b, 0x8, 0x45, 0x1f, 0x40, 0x4b, 0x4b, 0x1f, 0x44, 0x4b, 0x52, 0x44, 0x9f, 0x4f, 0x44, 0x51, 0x48, 0x52, 0x47, 0x44, 0x43, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x47, 0x44, 0x9f, 0x51, 0x44, 0x4c, 0x40, 0x48, 0x4d, 0x44, 0x43, 0x1d, 0x1f, 0x8, 0x1f, 0x52, 0x47, 0x4e, 0x54, 0x4b, 0xc3, 0x52, 0x53, 0x48, 0x4b, 0x4b, 0x1f, 0x42, 0x4e, 0x4d, 0x53, 0x48, 0x4d, 0x54, 0x44, 0x1f, 0x53, 0x4e, 0x9f, ], [0x41, 0x44, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x48, 0x45, 0x1f, 0x40, 0x4b, 0x4b, 0x9f, 0x44, 0x4b, 0x52, 0x44, 0x1f, 0x51, 0x44, 0x4c, 0x40, 0x48, 0x4d, 0x44, 0x43, 0x1d, 0x1f, 0x40, 0x4d, 0xc3, 0x47, 0x44, 0x1f, 0x56, 0x44, 0x51, 0x44, 0x9f, 0x40, 0x4d, 0x4d, 0x48, 0x47, 0x48, 0x4b, 0x40, 0x53, 0x44, 0x43, 0x1d, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x54, 0x4d, 0x48, 0x55, 0x44, 0x51, 0x52, 0x44, 0x1f, 0x56, 0x4e, 0x54, 0x4b, 0x43, 0x9f, 0x53, 0x54, 0x51, 0x4d, 0x1f, 0x53, 0x4e, 0x1f, 0x40, 0x1f, 0x4c, 0x48, 0x46, 0x47, 0x53, 0x58, 0x9f, 0x52, 0x53, 0x51, 0x40, 0x4d, 0x46, 0x44, 0x51, 0x1b, 0x1f, 0x8, 0x1f, 0x52, 0x47, 0x4e, 0x54, 0x4b, 0xc3, 0x4d, 0x4e, 0x53, 0x1f, 0x52, 0x44, 0x44, 0x4c, 0x1f, 0x40, 0x1f, 0x4f, 0x40, 0x51, 0x53, 0x1f, 0x4e, 0xc5, ], [0x48, 0x53, 0x1b, 0x1f, 0x1c, 0x1, 0x51, 0x4e, 0x4d, 0x53, 0x44, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x8, 0x1f, 0x47, 0x40, 0x55, 0x44, 0x1f, 0x43, 0x51, 0x44, 0x40, 0x4c, 0x53, 0x1f, 0x48, 0x4d, 0x9f, 0x4c, 0x58, 0x1f, 0x4b, 0x48, 0x45, 0x44, 0x1d, 0x1f, 0x43, 0x51, 0x44, 0x40, 0x4c, 0x52, 0x9f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x47, 0x40, 0x55, 0x44, 0x1f, 0x52, 0x53, 0x40, 0x58, 0x44, 0x43, 0x9f, 0x56, 0x48, 0x53, 0x47, 0x1f, 0x4c, 0x44, 0x1f, 0x44, 0x55, 0x44, 0x51, 0x9f, 0x40, 0x45, 0x53, 0x44, 0x51, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x42, 0x47, 0x40, 0x4d, 0x46, 0x44, 0xc3, 0x4c, 0x58, 0x1f, 0x48, 0x43, 0x44, 0x40, 0x52, 0x1b, 0x1f, 0x13, 0x47, 0x44, 0x58, 0x9f, 0x47, 0x40, 0x55, 0x44, 0x1f, 0x46, 0x4e, 0x4d, 0x44, 0x1f, 0x53, 0x47, 0x51, 0x4e, 0x54, 0x46, 0x47, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x53, 0x47, 0x51, 0x4e, 0x54, 0x46, 0x47, 0x1f, 0x4c, 0x44, 0x1d, 0x9f, ], [0x4b, 0x48, 0x4a, 0x44, 0x1f, 0x56, 0x48, 0x4d, 0x44, 0x1f, 0x53, 0x47, 0x51, 0x4e, 0x54, 0x46, 0x47, 0x9f, 0x56, 0x40, 0x53, 0x44, 0x51, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x40, 0x4b, 0x53, 0x44, 0x51, 0x44, 0xc3, 0x53, 0x47, 0x44, 0x1f, 0x42, 0x4e, 0x4b, 0x4e, 0x51, 0x1f, 0x4e, 0x45, 0x1f, 0x4c, 0x58, 0x9f, 0x4c, 0x48, 0x4d, 0x43, 0x1b, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x53, 0x47, 0x48, 0x52, 0x1f, 0x48, 0x52, 0x9f, 0x4e, 0x4d, 0x44, 0x1b, 0x1f, 0x8, 0x5d, 0x4c, 0x1f, 0x46, 0x4e, 0x48, 0x4d, 0x46, 0x1f, 0x53, 0x4e, 0x9f, 0x53, 0x44, 0x4b, 0x4b, 0x1f, 0x48, 0x53, 0x1f, 0x1c, 0x1f, 0x41, 0x54, 0x53, 0x1f, 0x53, 0x40, 0x4a, 0xc4, 0x42, 0x40, 0x51, 0x44, 0x1f, 0x4d, 0x4e, 0x53, 0x1f, 0x53, 0x4e, 0x1f, 0x52, 0x4c, 0x48, 0x4b, 0x44, 0x9f, 0x40, 0x53, 0x1f, 0x40, 0x4d, 0x58, 0x1f, 0x4f, 0x40, 0x51, 0x53, 0x1f, 0x4e, 0x45, 0x1f, 0x48, 0x53, 0x9b, ], [0x1c, 0x1, 0x51, 0x4e, 0x4d, 0x53, 0x44, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x13, 0x47, 0x48, 0x52, 0x1f, 0x52, 0x53, 0x4e, 0x51, 0x58, 0x1f, 0x52, 0x47, 0x40, 0x4b, 0x4b, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x46, 0x4e, 0x4e, 0x43, 0x1f, 0x4c, 0x40, 0x4d, 0x1f, 0x53, 0x44, 0x40, 0x42, 0xc7, 0x47, 0x48, 0x52, 0x1f, 0x52, 0x4e, 0x4d, 0x1d, 0x1f, 0x0, 0x4d, 0x43, 0x9f, 0x2, 0x51, 0x48, 0x52, 0x4f, 0x48, 0x4d, 0x1f, 0x2, 0x51, 0x48, 0x52, 0x4f, 0x48, 0x40, 0x4d, 0x9f, 0x52, 0x47, 0x40, 0x4b, 0x4b, 0x1f, 0x4d, 0x44, 0x5d, 0x44, 0x51, 0x1f, 0x46, 0x4e, 0x1f, 0x41, 0x58, 0x9d, 0x5, 0x51, 0x4e, 0x4c, 0x1f, 0x53, 0x47, 0x48, 0x52, 0x1f, 0x43, 0x40, 0x58, 0x1f, 0x53, 0x4e, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x44, 0x4d, 0x43, 0x48, 0x4d, 0x46, 0x1f, 0x4e, 0x45, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x56, 0x4e, 0x51, 0x4b, 0x43, 0x1d, 0x1f, 0x1, 0x54, 0x53, 0x1f, 0x56, 0x44, 0x1f, 0x48, 0x4d, 0x9f, ], [0x48, 0x53, 0x1f, 0x52, 0x47, 0x40, 0x4b, 0x4b, 0x1f, 0x41, 0x44, 0x9f, 0x51, 0x44, 0x4c, 0x44, 0x4c, 0x41, 0x44, 0x51, 0x44, 0x43, 0x1b, 0x1f, 0x16, 0x44, 0x9f, 0x45, 0x44, 0x56, 0x1d, 0x1f, 0x56, 0x44, 0x1f, 0x47, 0x40, 0x4f, 0x4f, 0x58, 0x1f, 0x45, 0x44, 0x56, 0x9d, 0x56, 0x44, 0x1f, 0x41, 0x40, 0x4d, 0x43, 0x1f, 0x4e, 0x45, 0x9f, 0x41, 0x51, 0x4e, 0x53, 0x47, 0x44, 0x51, 0x52, 0x1b, 0x1f, 0x5, 0x4e, 0x51, 0x1f, 0x47, 0x44, 0x9f, 0x53, 0x4e, 0x1c, 0x43, 0x40, 0x58, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x52, 0x47, 0x44, 0x43, 0x52, 0x9f, 0x47, 0x48, 0x52, 0x1f, 0x41, 0x4b, 0x4e, 0x4e, 0x43, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x1f, 0x4c, 0x44, 0x9f, 0x12, 0x47, 0x40, 0x4b, 0x4b, 0x1f, 0x41, 0x44, 0x1f, 0x4c, 0x58, 0x9f, ], [0x41, 0x51, 0x4e, 0x53, 0x47, 0x44, 0x51, 0x1b, 0x9f, 0x1c, 0x12, 0x47, 0x40, 0x4a, 0x44, 0x52, 0x4f, 0x44, 0x40, 0x51, 0x44, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x7, 0x4e, 0x56, 0x44, 0x55, 0x44, 0x51, 0x1f, 0x4c, 0x44, 0x40, 0x4d, 0x1f, 0x58, 0x4e, 0x54, 0x51, 0x9f, 0x4b, 0x48, 0x45, 0x44, 0x1f, 0x48, 0x52, 0x1d, 0x1f, 0x4c, 0x44, 0x44, 0x53, 0x1f, 0x48, 0x53, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x4b, 0x48, 0x55, 0x44, 0x1f, 0x48, 0x53, 0x1b, 0x1f, 0x3, 0x4e, 0x9f, 0x4d, 0x4e, 0x53, 0x1f, 0x52, 0x47, 0x54, 0x4d, 0x1f, 0x48, 0x53, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x42, 0x40, 0x4b, 0x4b, 0x1f, 0x48, 0x53, 0x1f, 0x47, 0x40, 0x51, 0x43, 0x9f, 0x4d, 0x40, 0x4c, 0x44, 0x52, 0x1b, 0x1f, 0x8, 0x53, 0x1f, 0x48, 0x52, 0x1f, 0x4d, 0x4e, 0x53, 0x9f, 0x52, 0x4e, 0x1f, 0x41, 0x40, 0x43, 0x1f, 0x40, 0x52, 0x1f, 0x58, 0x4e, 0x54, 0x1f, 0x40, 0x51, 0x44, 0x9b, 0x8, 0x53, 0x1f, 0x4b, 0x4e, 0x4e, 0x4a, 0x52, 0x1f, 0x4f, 0x4e, 0x4e, 0x51, 0x44, 0x52, 0x53, 0x9f, ], [0x56, 0x47, 0x44, 0x4d, 0x1f, 0x58, 0x4e, 0x54, 0x1f, 0x40, 0x51, 0x44, 0x9f, 0x51, 0x48, 0x42, 0x47, 0x44, 0x52, 0x53, 0x1b, 0x1f, 0x13, 0x47, 0x44, 0x9f, 0x45, 0x40, 0x54, 0x4b, 0x53, 0x1c, 0x45, 0x48, 0x4d, 0x43, 0x44, 0x51, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x9f, 0x45, 0x48, 0x4d, 0x43, 0x1f, 0x45, 0x40, 0x54, 0x4b, 0x53, 0x52, 0x1f, 0x44, 0x55, 0x44, 0x4d, 0x9f, 0x48, 0x4d, 0x1f, 0x4f, 0x40, 0x51, 0x40, 0x43, 0x48, 0x52, 0x44, 0x1b, 0x1f, 0xb, 0x4e, 0x55, 0x44, 0x9f, 0x58, 0x4e, 0x54, 0x51, 0x1f, 0x4b, 0x48, 0x45, 0x44, 0x1d, 0x1f, 0x4f, 0x4e, 0x4e, 0x51, 0x1f, 0x40, 0xd2, 0x48, 0x53, 0x1f, 0x48, 0x52, 0x1b, 0x1f, 0x18, 0x4e, 0x54, 0x1f, 0x4c, 0x40, 0x58, 0x9f, 0x4f, 0x44, 0x51, 0x47, 0x40, 0x4f, 0x52, 0x1f, 0x47, 0x40, 0x55, 0x44, 0x1f, 0x52, 0x4e, 0x4c, 0x44, 0x9f, ], [0x4f, 0x4b, 0x44, 0x40, 0x52, 0x40, 0x4d, 0x53, 0x1d, 0x9f, 0x53, 0x47, 0x51, 0x48, 0x4b, 0x4b, 0x48, 0x4d, 0x46, 0x1d, 0x9f, 0x46, 0x4b, 0x4e, 0x51, 0x48, 0x4e, 0x54, 0x52, 0x1f, 0x47, 0x4e, 0x54, 0x51, 0x52, 0x1d, 0x9f, 0x44, 0x55, 0x44, 0x4d, 0x1f, 0x48, 0x4d, 0x1f, 0x40, 0x9f, 0x4f, 0x4e, 0x4e, 0x51, 0x47, 0x4e, 0x54, 0x52, 0x44, 0x1b, 0x1f, 0x13, 0x47, 0x44, 0x9f, 0x52, 0x44, 0x53, 0x53, 0x48, 0x4d, 0x46, 0x1f, 0x52, 0x54, 0x4d, 0x1f, 0x48, 0x52, 0x9f, 0x51, 0x44, 0x45, 0x4b, 0x44, 0x42, 0x53, 0x44, 0x43, 0x1f, 0x45, 0x51, 0x4e, 0x4c, 0x1f, 0x53, 0x47, 0xc4, 0x56, 0x48, 0x4d, 0x43, 0x4e, 0x56, 0x52, 0x1f, 0x4e, 0x45, 0x1f, 0x53, 0x47, 0x44, 0x9f, ], [0x40, 0x4b, 0x4c, 0x52, 0x47, 0x4e, 0x54, 0x52, 0x44, 0x1f, 0x40, 0x52, 0x9f, 0x41, 0x51, 0x48, 0x46, 0x47, 0x53, 0x4b, 0x58, 0x1f, 0x40, 0x52, 0x1f, 0x45, 0x51, 0x4e, 0x4c, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x51, 0x48, 0x42, 0x47, 0x1f, 0x4c, 0x40, 0x4d, 0x5d, 0x52, 0x9f, 0x40, 0x41, 0x4e, 0x43, 0x44, 0x1b, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x52, 0x4d, 0x4e, 0x56, 0x9f, 0x4c, 0x44, 0x4b, 0x53, 0x52, 0x1f, 0x41, 0x44, 0x45, 0x4e, 0x51, 0x44, 0x1f, 0x48, 0x53, 0x52, 0x9f, 0x43, 0x4e, 0x4e, 0x51, 0x1f, 0x40, 0x52, 0x1f, 0x44, 0x40, 0x51, 0x4b, 0x58, 0x1f, 0x48, 0x4d, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x52, 0x4f, 0x51, 0x48, 0x4d, 0x46, 0x1b, 0x1f, 0x8, 0x1f, 0x43, 0x4e, 0x9f, 0x4d, 0x4e, 0x53, 0x1f, 0x52, 0x44, 0x44, 0x1f, 0x41, 0x54, 0x53, 0x1f, 0x40, 0x9f, ], [0x50, 0x54, 0x48, 0x44, 0x53, 0x1f, 0x4c, 0x48, 0x4d, 0x43, 0x1f, 0x4c, 0x40, 0x58, 0x9f, 0x4b, 0x48, 0x55, 0x44, 0x1f, 0x40, 0x52, 0x9f, 0x42, 0x4e, 0x4d, 0x53, 0x44, 0x4d, 0x53, 0x44, 0x43, 0x4b, 0x58, 0x1f, 0x53, 0x47, 0x44, 0x51, 0x44, 0x9d, 0x40, 0x4d, 0x43, 0x1f, 0x47, 0x40, 0x55, 0x44, 0x1f, 0x40, 0x52, 0x9f, 0x42, 0x47, 0x44, 0x44, 0x51, 0x48, 0x4d, 0x46, 0x1f, 0x53, 0x47, 0x4e, 0x54, 0x46, 0x47, 0x53, 0x52, 0x9d, 0x40, 0x52, 0x1f, 0x48, 0x4d, 0x1f, 0x40, 0x1f, 0x4f, 0x40, 0x4b, 0x40, 0x42, 0x44, 0x1b, 0x9f, 0x1c, 0x13, 0x47, 0x4e, 0x51, 0x44, 0x40, 0x54, 0x1f, 0x9f, 0x9f, ]], +[[0x16, 0x44, 0x1f, 0x4c, 0x54, 0x52, 0x53, 0x1f, 0x4b, 0x44, 0x40, 0x51, 0x4d, 0x1f, 0x53, 0x4e, 0x9f, 0x51, 0x44, 0x40, 0x56, 0x40, 0x4a, 0x44, 0x4d, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x4a, 0x44, 0x44, 0x4f, 0x9f, 0x4e, 0x54, 0x51, 0x52, 0x44, 0x4b, 0x55, 0x44, 0x52, 0x1f, 0x40, 0x56, 0x40, 0x4a, 0x44, 0x1d, 0x9f, 0x4d, 0x4e, 0x53, 0x1f, 0x41, 0x58, 0x1f, 0x4c, 0x44, 0x42, 0x47, 0x40, 0x4d, 0x48, 0x42, 0x40, 0x4b, 0x9f, 0x40, 0x48, 0x43, 0x52, 0x1d, 0x1f, 0x41, 0x54, 0x53, 0x1f, 0x41, 0x58, 0x1f, 0x40, 0x4d, 0x9f, 0x48, 0x4d, 0x45, 0x48, 0x4d, 0x48, 0x53, 0x44, 0x9f, 0x44, 0x57, 0x4f, 0x44, 0x42, 0x53, 0x40, 0x53, 0x48, 0x4e, 0x4d, 0x1f, 0x4e, 0x45, 0x1f, 0x53, 0x47, 0xc4, 0x43, 0x40, 0x56, 0x4d, 0x1d, 0x1f, 0x56, 0x47, 0x48, 0x42, 0x47, 0x1f, 0x43, 0x4e, 0x44, 0x52, 0x9f, ], [0x4d, 0x4e, 0x53, 0x1f, 0x45, 0x4e, 0x51, 0x52, 0x40, 0x4a, 0x44, 0x1f, 0x54, 0x52, 0x9f, 0x44, 0x55, 0x44, 0x4d, 0x1f, 0x48, 0x4d, 0x1f, 0x4e, 0x54, 0x51, 0x9f, 0x52, 0x4e, 0x54, 0x4d, 0x43, 0x44, 0x52, 0x53, 0x1f, 0x52, 0x4b, 0x44, 0x44, 0x4f, 0x1b, 0x1f, 0x8, 0x9f, 0x4a, 0x4d, 0x4e, 0x56, 0x1f, 0x4e, 0x45, 0x1f, 0x4d, 0x4e, 0x1f, 0x4c, 0x4e, 0x51, 0x44, 0x9f, 0x44, 0x4d, 0x42, 0x4e, 0x54, 0x51, 0x40, 0x46, 0x48, 0x4d, 0x46, 0x1f, 0x45, 0x40, 0x42, 0x53, 0x9f, 0x53, 0x47, 0x40, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x54, 0x4d, 0x50, 0x54, 0x44, 0x52, 0x53, 0x48, 0x4e, 0x4d, 0x40, 0x41, 0x4b, 0x44, 0x9f, 0x40, 0x41, 0x48, 0x4b, 0x48, 0x53, 0x58, 0x1f, 0x4e, 0x45, 0x1f, 0x4c, 0x40, 0x4d, 0x1f, 0x53, 0x4e, 0x9f, ], [0x44, 0x4b, 0x44, 0x55, 0x40, 0x53, 0x44, 0x1f, 0x47, 0x48, 0x52, 0x1f, 0x4b, 0x48, 0x45, 0x44, 0x9f, 0x41, 0x58, 0x1f, 0x40, 0x1f, 0x42, 0x4e, 0x4d, 0x52, 0x42, 0x48, 0x4e, 0x54, 0x52, 0x9f, 0x44, 0x4d, 0x43, 0x44, 0x40, 0x55, 0x4e, 0x54, 0x51, 0x1b, 0x1f, 0x8, 0x53, 0x1f, 0x48, 0x52, 0x9f, 0x52, 0x4e, 0x4c, 0x44, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x1f, 0x53, 0x4e, 0x1f, 0x41, 0x44, 0x9f, 0x40, 0x41, 0x4b, 0x44, 0x1f, 0x53, 0x4e, 0x1f, 0x4f, 0x40, 0x48, 0x4d, 0x53, 0x1f, 0x40, 0x9f, 0x4f, 0x40, 0x51, 0x53, 0x48, 0x42, 0x54, 0x4b, 0x40, 0x51, 0x9f, 0x4f, 0x48, 0x42, 0x53, 0x54, 0x51, 0x44, 0x1d, 0x1f, 0x4e, 0x51, 0x1f, 0x53, 0x4e, 0x9f, 0x42, 0x40, 0x51, 0x55, 0x44, 0x1f, 0x40, 0x1f, 0x52, 0x53, 0x40, 0x53, 0x54, 0x44, 0x1d, 0x9f, ], [0x40, 0x4d, 0x43, 0x1f, 0x52, 0x4e, 0x1f, 0x53, 0x4e, 0x1f, 0x4c, 0x40, 0x4a, 0x44, 0x1f, 0x40, 0x9f, 0x45, 0x44, 0x56, 0x1f, 0x4e, 0x41, 0x49, 0x44, 0x42, 0x53, 0x52, 0x9f, 0x41, 0x44, 0x40, 0x54, 0x53, 0x48, 0x45, 0x54, 0x4b, 0x1d, 0x1f, 0x41, 0x54, 0x53, 0x1f, 0x48, 0x53, 0x9f, 0x48, 0x52, 0x1f, 0x45, 0x40, 0x51, 0x1f, 0x4c, 0x4e, 0x51, 0x44, 0x9f, 0x46, 0x4b, 0x4e, 0x51, 0x48, 0x4e, 0x54, 0x52, 0x1f, 0x53, 0x4e, 0x1f, 0x42, 0x40, 0x51, 0x55, 0x44, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x4f, 0x40, 0x48, 0x4d, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x55, 0x44, 0x51, 0xd8, 0x40, 0x53, 0x4c, 0x4e, 0x52, 0x4f, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x4c, 0x44, 0x43, 0x48, 0x54, 0x4c, 0x1f, 0x53, 0x47, 0x51, 0x4e, 0x54, 0x46, 0x47, 0x9f, ], [0x56, 0x47, 0x48, 0x42, 0x47, 0x1f, 0x56, 0x44, 0x1f, 0x4b, 0x4e, 0x4e, 0x4a, 0x1d, 0x9f, 0x56, 0x47, 0x48, 0x42, 0x47, 0x1f, 0x4c, 0x4e, 0x51, 0x40, 0x4b, 0x4b, 0x58, 0x1f, 0x56, 0x44, 0x9f, 0x42, 0x40, 0x4d, 0x1f, 0x43, 0x4e, 0x1b, 0x1f, 0x13, 0x4e, 0x1f, 0x40, 0x45, 0x45, 0x44, 0x42, 0x53, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x50, 0x54, 0x40, 0x4b, 0x48, 0x53, 0x58, 0x1f, 0x4e, 0x45, 0x1f, 0x53, 0x47, 0xc4, 0x43, 0x40, 0x58, 0x1d, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x48, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x47, 0x48, 0x46, 0x47, 0x44, 0x52, 0x53, 0x1f, 0x4e, 0x45, 0x1f, 0x40, 0x51, 0x53, 0x52, 0x1b, 0x9f, 0x1c, 0x13, 0x47, 0x4e, 0x51, 0x44, 0x40, 0x54, 0x1f, 0x9f, 0x9f, ]], +[[0x8, 0x45, 0x1f, 0x4e, 0x4d, 0x44, 0x1f, 0x40, 0x43, 0x55, 0x40, 0x4d, 0x42, 0x44, 0x52, 0x9f, 0x42, 0x4e, 0x4d, 0x45, 0x48, 0x43, 0x44, 0x4d, 0x53, 0x4b, 0x58, 0x1f, 0x48, 0x4d, 0x1f, 0x53, 0x47, 0xc4, 0x43, 0x48, 0x51, 0x44, 0x42, 0x53, 0x48, 0x4e, 0x4d, 0x1f, 0x4e, 0x45, 0x1f, 0x47, 0x48, 0x52, 0x9f, 0x43, 0x51, 0x44, 0x40, 0x4c, 0x52, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x44, 0x4d, 0x43, 0x44, 0x40, 0x55, 0x4e, 0x51, 0x52, 0x1f, 0x53, 0x4e, 0x1f, 0x4b, 0x48, 0x55, 0x44, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x4b, 0x48, 0x45, 0x44, 0x1f, 0x56, 0x47, 0x48, 0x42, 0x47, 0x1f, 0x47, 0x44, 0x9f, 0x47, 0x40, 0x52, 0x1f, 0x48, 0x4c, 0x40, 0x46, 0x48, 0x4d, 0x44, 0x43, 0x1d, 0x1f, 0x47, 0x44, 0x9f, 0x56, 0x48, 0x4b, 0x4b, 0x1f, 0x4c, 0x44, 0x44, 0x53, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x1f, 0x40, 0x9f, ], [0x52, 0x54, 0x42, 0x42, 0x44, 0x52, 0x52, 0x1f, 0x54, 0x4d, 0x44, 0x57, 0x4f, 0x44, 0x42, 0x53, 0x44, 0xc3, 0x48, 0x4d, 0x1f, 0x42, 0x4e, 0x4c, 0x4c, 0x4e, 0x4d, 0x1f, 0x47, 0x4e, 0x54, 0x51, 0x52, 0x1b, 0x9f, 0x7, 0x44, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x1f, 0x4f, 0x54, 0x53, 0x1f, 0x52, 0x4e, 0x4c, 0x44, 0x9f, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x52, 0x1f, 0x41, 0x44, 0x47, 0x48, 0x4d, 0x43, 0x1d, 0x9f, 0x56, 0x48, 0x4b, 0x4b, 0x1f, 0x4f, 0x40, 0x52, 0x52, 0x1f, 0x40, 0x4d, 0x9f, 0x48, 0x4d, 0x55, 0x48, 0x52, 0x48, 0x41, 0x4b, 0x44, 0x9f, 0x41, 0x4e, 0x54, 0x4d, 0x43, 0x40, 0x51, 0x58, 0x1b, 0x1f, 0xd, 0x44, 0x56, 0x1d, 0x9f, 0x54, 0x4d, 0x48, 0x55, 0x44, 0x51, 0x52, 0x40, 0x4b, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x9f, ], [0x4c, 0x4e, 0x51, 0x44, 0x1f, 0x4b, 0x48, 0x41, 0x44, 0x51, 0x40, 0x4b, 0x1f, 0x4b, 0x40, 0x56, 0x52, 0x9f, 0x56, 0x48, 0x4b, 0x4b, 0x1f, 0x41, 0x44, 0x46, 0x48, 0x4d, 0x1f, 0x53, 0x4e, 0x9f, 0x44, 0x52, 0x53, 0x40, 0x41, 0x4b, 0x48, 0x52, 0x47, 0x9f, 0x53, 0x47, 0x44, 0x4c, 0x52, 0x44, 0x4b, 0x55, 0x44, 0x52, 0x1f, 0x40, 0x51, 0x4e, 0x54, 0x4d, 0x43, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x48, 0x4d, 0x1f, 0x47, 0x48, 0x4c, 0x1d, 0x1f, 0x4e, 0xd1, 0x53, 0x47, 0x44, 0x1f, 0x4e, 0x4b, 0x43, 0x1f, 0x4b, 0x40, 0x56, 0x52, 0x1f, 0x41, 0x44, 0x9f, 0x44, 0x57, 0x4f, 0x40, 0x4d, 0x43, 0x44, 0x43, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x48, 0x4d, 0x53, 0x44, 0x51, 0x4f, 0x51, 0x44, 0x53, 0x44, 0x43, 0x1f, 0x48, 0x4d, 0x1f, 0x47, 0x48, 0xd2, ], [0x45, 0x40, 0x55, 0x4e, 0x51, 0x1f, 0x48, 0x4d, 0x1f, 0x40, 0x1f, 0x4c, 0x4e, 0x51, 0x44, 0x9f, 0x4b, 0x48, 0x41, 0x44, 0x51, 0x40, 0x4b, 0x1f, 0x52, 0x44, 0x4d, 0x52, 0x44, 0x1d, 0x1f, 0x40, 0x4d, 0xc3, 0x47, 0x44, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x1f, 0x4b, 0x48, 0x55, 0x44, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x4b, 0x48, 0x42, 0x44, 0x4d, 0x52, 0x44, 0x1f, 0x4e, 0x45, 0x1f, 0x40, 0x9f, 0x47, 0x48, 0x46, 0x47, 0x44, 0x51, 0x1f, 0x4e, 0x51, 0x43, 0x44, 0x51, 0x1f, 0x4e, 0x45, 0x9f, 0x41, 0x44, 0x48, 0x4d, 0x46, 0x52, 0x1b, 0x1f, 0x9f, 0x9f, 0x9f, ]], +[[0x8, 0x5d, 0x4c, 0x1f, 0x4d, 0x4e, 0x41, 0x4e, 0x43, 0x58, 0x1a, 0x1f, 0x16, 0x47, 0x4e, 0x9f, 0x40, 0x51, 0x44, 0x1f, 0x58, 0x4e, 0x54, 0x1e, 0x1f, 0x0, 0x51, 0x44, 0x1f, 0x58, 0x4e, 0x54, 0x9f, 0x4d, 0x4e, 0x41, 0x4e, 0x43, 0x58, 0x1d, 0x1f, 0x53, 0x4e, 0x4e, 0x1e, 0x1f, 0x13, 0x47, 0x44, 0x4d, 0x9f, 0x53, 0x47, 0x44, 0x51, 0x44, 0x5d, 0x52, 0x1f, 0x40, 0x1f, 0x4f, 0x40, 0x48, 0x51, 0x1f, 0x4e, 0x45, 0x9f, 0x54, 0x52, 0x1d, 0x1f, 0x43, 0x4e, 0x4d, 0x5d, 0x53, 0x1f, 0x53, 0x44, 0x4b, 0x4b, 0x1a, 0x9f, 0x13, 0x47, 0x44, 0x58, 0x5d, 0x43, 0x1f, 0x41, 0x40, 0x4d, 0x48, 0x52, 0x47, 0x1f, 0x54, 0x52, 0x1d, 0x9f, 0x58, 0x4e, 0x54, 0x1f, 0x4a, 0x4d, 0x4e, 0x56, 0x1b, 0x1f, 0x7, 0x4e, 0x56, 0x9f, 0x43, 0x51, 0x44, 0x40, 0x51, 0x58, 0x1f, 0x53, 0x4e, 0x1f, 0x41, 0x44, 0x9f, ], [0x52, 0x4e, 0x4c, 0x44, 0x41, 0x4e, 0x43, 0x58, 0x1a, 0x1f, 0x7, 0x4e, 0x56, 0x9f, 0x4f, 0x54, 0x41, 0x4b, 0x48, 0x42, 0x1d, 0x1f, 0x4b, 0x48, 0x4a, 0x44, 0x1f, 0x40, 0x9f, 0x45, 0x51, 0x4e, 0x46, 0x1f, 0x13, 0x4e, 0x1f, 0x53, 0x44, 0x4b, 0x4b, 0x1f, 0x58, 0x4e, 0x54, 0x51, 0x9f, 0x4d, 0x40, 0x4c, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x4b, 0x48, 0x55, 0x44, 0x4b, 0x4e, 0x4d, 0x46, 0x9f, 0x43, 0x40, 0x58, 0x1f, 0x13, 0x4e, 0x1f, 0x40, 0x4d, 0x1f, 0x40, 0x43, 0x4c, 0x48, 0x51, 0x48, 0x4d, 0xc6, 0x41, 0x4e, 0x46, 0x1a, 0x1f, 0x1c, 0x3, 0x48, 0x42, 0x4a, 0x48, 0x4d, 0x52, 0x4e, 0x4d, 0x1f, 0x9f, 0x9f, 0x9f, ]], +[[0x7, 0x4e, 0x56, 0x1f, 0x47, 0x40, 0x4f, 0x4f, 0x58, 0x1f, 0x48, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x4b, 0x48, 0x53, 0x53, 0x4b, 0x44, 0x1f, 0x52, 0x53, 0x4e, 0x4d, 0x44, 0x1f, 0x13, 0x47, 0x40, 0x53, 0x9f, 0x51, 0x40, 0x4c, 0x41, 0x4b, 0x44, 0x52, 0x1f, 0x48, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x51, 0x4e, 0x40, 0x43, 0x1f, 0x40, 0x4b, 0x4e, 0x4d, 0x44, 0x1d, 0x1f, 0x0, 0x4d, 0x43, 0x9f, 0x43, 0x4e, 0x44, 0x52, 0x4d, 0x5d, 0x53, 0x1f, 0x42, 0x40, 0x51, 0x44, 0x1f, 0x40, 0x41, 0x4e, 0x54, 0xd3, 0x42, 0x40, 0x51, 0x44, 0x44, 0x51, 0x52, 0x1d, 0x1f, 0x0, 0x4d, 0x43, 0x9f, 0x44, 0x57, 0x48, 0x46, 0x44, 0x4d, 0x42, 0x48, 0x44, 0x52, 0x1f, 0x4d, 0x44, 0x55, 0x44, 0x51, 0x9f, 0x45, 0x44, 0x40, 0x51, 0x52, 0x1b, 0x1f, 0x16, 0x47, 0x4e, 0x52, 0x44, 0x1f, 0x42, 0x4e, 0x40, 0x53, 0x9f, ], [0x4e, 0x45, 0x1f, 0x44, 0x4b, 0x44, 0x4c, 0x44, 0x4d, 0x53, 0x40, 0x4b, 0x1f, 0x41, 0x51, 0x4e, 0x56, 0xcd, 0x0, 0x1f, 0x4f, 0x40, 0x52, 0x52, 0x48, 0x4d, 0x46, 0x1f, 0x54, 0x4d, 0x48, 0x55, 0x44, 0x51, 0x52, 0xc4, 0x4f, 0x54, 0x53, 0x1f, 0x4e, 0x4d, 0x1b, 0x1f, 0x0, 0x4d, 0x43, 0x9f, 0x48, 0x4d, 0x43, 0x44, 0x4f, 0x44, 0x4d, 0x43, 0x44, 0x4d, 0x53, 0x1f, 0x40, 0x52, 0x1f, 0x53, 0x47, 0xc4, 0x52, 0x54, 0x4d, 0x1d, 0x1f, 0x0, 0x52, 0x52, 0x4e, 0x42, 0x48, 0x40, 0x53, 0x44, 0x52, 0x1f, 0x4e, 0xd1, 0x46, 0x4b, 0x4e, 0x56, 0x52, 0x1f, 0x40, 0x4b, 0x4e, 0x4d, 0x44, 0x1d, 0x9f, 0x5, 0x54, 0x4b, 0x45, 0x48, 0x4b, 0x4b, 0x48, 0x4d, 0x46, 0x9f, 0x40, 0x41, 0x52, 0x4e, 0x4b, 0x54, 0x53, 0x44, 0x1f, 0x43, 0x44, 0x42, 0x51, 0x44, 0x44, 0x1f, 0x8, 0xcd, ], [0x42, 0x40, 0x52, 0x54, 0x40, 0x4b, 0x1f, 0x52, 0x48, 0x4c, 0x4f, 0x4b, 0x48, 0x42, 0x48, 0x53, 0x58, 0x9b, 0x1c, 0x3, 0x48, 0x42, 0x4a, 0x48, 0x4d, 0x52, 0x4e, 0x4d, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x1, 0x44, 0x42, 0x40, 0x54, 0x52, 0x44, 0x1f, 0x8, 0x1f, 0x42, 0x4e, 0x54, 0x4b, 0x43, 0x9f, 0x4d, 0x4e, 0x53, 0x1f, 0x52, 0x53, 0x4e, 0x4f, 0x1f, 0x45, 0x4e, 0x51, 0x1f, 0x43, 0x44, 0x40, 0x53, 0xc7, 0x7, 0x44, 0x1f, 0x4a, 0x48, 0x4d, 0x43, 0x4b, 0x58, 0x1f, 0x52, 0x53, 0x4e, 0x4f, 0x4f, 0x44, 0x43, 0x9f, 0x45, 0x4e, 0x51, 0x1f, 0x4c, 0x44, 0x1b, 0x1f, 0x13, 0x47, 0x44, 0x9f, 0x42, 0x40, 0x51, 0x51, 0x48, 0x40, 0x46, 0x44, 0x1f, 0x47, 0x44, 0x4b, 0x43, 0x1f, 0x41, 0x54, 0x53, 0x9f, 0x49, 0x54, 0x52, 0x53, 0x1f, 0x4e, 0x54, 0x51, 0x52, 0x44, 0x4b, 0x55, 0x44, 0x52, 0x1f, 0x0, 0x4d, 0xc3, 0x48, 0x4c, 0x4c, 0x4e, 0x51, 0x53, 0x40, 0x4b, 0x48, 0x53, 0x58, 0x9f, 0x1c, 0x3, 0x48, 0x42, 0x4a, 0x48, 0x4d, 0x52, 0x4e, 0x4d, 0x1f, 0x9f, ]], +[[0x5, 0x4e, 0x51, 0x1d, 0x1f, 0x4b, 0x48, 0x4a, 0x44, 0x1f, 0x40, 0x4b, 0x4c, 0x4e, 0x52, 0x53, 0x9f, 0x44, 0x55, 0x44, 0x51, 0x58, 0x4e, 0x4d, 0x44, 0x1f, 0x44, 0x4b, 0x52, 0x44, 0x1f, 0x48, 0x4d, 0x9f, 0x4e, 0x54, 0x51, 0x1f, 0x42, 0x4e, 0x54, 0x4d, 0x53, 0x51, 0x58, 0x1d, 0x1f, 0x8, 0x9f, 0x52, 0x53, 0x40, 0x51, 0x53, 0x44, 0x43, 0x1f, 0x4e, 0x54, 0x53, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x9f, 0x4c, 0x58, 0x1f, 0x52, 0x47, 0x40, 0x51, 0x44, 0x1f, 0x4e, 0x45, 0x9f, 0x4e, 0x4f, 0x53, 0x48, 0x4c, 0x48, 0x52, 0x4c, 0x1b, 0x1f, 0x8, 0x9f, 0x41, 0x44, 0x4b, 0x48, 0x44, 0x55, 0x44, 0x43, 0x1f, 0x48, 0x4d, 0x1f, 0x47, 0x40, 0x51, 0x43, 0x9f, 0x56, 0x4e, 0x51, 0x4a, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x4f, 0x51, 0x4e, 0x46, 0x51, 0x44, 0x52, 0x52, 0x9f, ], [0x40, 0x4d, 0x43, 0x1f, 0x40, 0x42, 0x53, 0x48, 0x4e, 0x4d, 0x1d, 0x1f, 0x41, 0x54, 0x53, 0x9f, 0x4d, 0x4e, 0x56, 0x1d, 0x1f, 0x40, 0x45, 0x53, 0x44, 0x51, 0x1f, 0x45, 0x48, 0x51, 0x52, 0x53, 0x9f, 0x41, 0x44, 0x48, 0x4d, 0x46, 0x1f, 0x5d, 0x45, 0x4e, 0x51, 0x5d, 0x9f, 0x52, 0x4e, 0x42, 0x48, 0x44, 0x53, 0x58, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x53, 0x47, 0x44, 0x4d, 0x9f, 0x5d, 0x40, 0x46, 0x40, 0x48, 0x4d, 0x52, 0x53, 0x5d, 0x1f, 0x48, 0x53, 0x1d, 0x1f, 0x8, 0x9f, 0x40, 0x52, 0x52, 0x48, 0x46, 0x4d, 0x1f, 0x4c, 0x58, 0x52, 0x44, 0x4b, 0x45, 0x1f, 0x4d, 0x4e, 0x9f, 0x51, 0x40, 0x4d, 0x4a, 0x1f, 0x4e, 0x51, 0x1f, 0x40, 0x4d, 0x58, 0x1f, 0x4b, 0x48, 0x4c, 0x48, 0x53, 0x9d, 0x40, 0x4d, 0x43, 0x1f, 0x52, 0x54, 0x42, 0x47, 0x1f, 0x40, 0x4d, 0x9f, ], [0x40, 0x53, 0x53, 0x48, 0x53, 0x54, 0x43, 0x44, 0x1f, 0x48, 0x52, 0x1f, 0x55, 0x44, 0x51, 0x58, 0x9f, 0x4c, 0x54, 0x42, 0x47, 0x1f, 0x40, 0x46, 0x40, 0x48, 0x4d, 0x52, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x53, 0x51, 0x44, 0x4d, 0x43, 0x1f, 0x4e, 0x45, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x53, 0x48, 0x4c, 0x44, 0x52, 0x1b, 0x1f, 0x1, 0x54, 0x53, 0x1f, 0x4c, 0x58, 0x9f, 0x56, 0x4e, 0x51, 0x4b, 0x43, 0x1f, 0x47, 0x40, 0x52, 0x1f, 0x41, 0x44, 0x42, 0x4e, 0x4c, 0x44, 0x9f, 0x4e, 0x4d, 0x44, 0x1f, 0x4e, 0x45, 0x1f, 0x48, 0x4d, 0x45, 0x48, 0x4d, 0x48, 0x53, 0x44, 0x9f, 0x4f, 0x4e, 0x52, 0x52, 0x48, 0x41, 0x48, 0x4b, 0x48, 0x53, 0x48, 0x44, 0x52, 0x1b, 0x9f, 0x16, 0x47, 0x40, 0x53, 0x1f, 0x40, 0x1f, 0x4f, 0x47, 0x51, 0x40, 0x52, 0x44, 0x1f, 0x1c, 0x9f, ], [0x52, 0x53, 0x48, 0x4b, 0x4b, 0x1f, 0x48, 0x53, 0x5d, 0x52, 0x1f, 0x40, 0x1f, 0x46, 0x4e, 0x4e, 0x43, 0x9f, 0x4f, 0x47, 0x51, 0x40, 0x52, 0x44, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x40, 0x1f, 0x46, 0x4e, 0x4e, 0x43, 0x9f, 0x55, 0x48, 0x44, 0x56, 0x1f, 0x4e, 0x45, 0x1f, 0x4b, 0x48, 0x45, 0x44, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x40, 0x1f, 0x4c, 0x40, 0x4d, 0x1f, 0x52, 0x47, 0x4e, 0x54, 0x4b, 0x43, 0x4d, 0x5d, 0x53, 0x9f, 0x40, 0x42, 0x42, 0x44, 0x4f, 0x53, 0x1f, 0x40, 0x4d, 0x58, 0x1f, 0x4e, 0x53, 0x47, 0x44, 0x51, 0x1b, 0x9f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x4c, 0x54, 0x42, 0x47, 0x1f, 0x8, 0x5d, 0x55, 0x44, 0x9f, 0x4b, 0x44, 0x40, 0x51, 0x4d, 0x44, 0x43, 0x9f, 0x54, 0x4d, 0x43, 0x44, 0x51, 0x46, 0x51, 0x4e, 0x54, 0x4d, 0x43, 0x1b, 0x1f, 0x14, 0x4d, 0x53, 0x48, 0xcb, ], [0x52, 0x4e, 0x4c, 0x44, 0x1f, 0x46, 0x40, 0x4d, 0x46, 0x1f, 0x52, 0x54, 0x42, 0x42, 0x44, 0x44, 0x43, 0xd2, 0x48, 0x4d, 0x1f, 0x4f, 0x54, 0x53, 0x53, 0x48, 0x4d, 0x46, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x56, 0x4e, 0x51, 0x4b, 0x43, 0x1f, 0x48, 0x4d, 0x1f, 0x40, 0x1f, 0x52, 0x53, 0x51, 0x40, 0x48, 0x53, 0x9f, 0x49, 0x40, 0x42, 0x4a, 0x44, 0x53, 0x1d, 0x1f, 0x48, 0x53, 0x52, 0x9f, 0x43, 0x44, 0x45, 0x48, 0x4d, 0x48, 0x53, 0x48, 0x4e, 0x4d, 0x1f, 0x48, 0x52, 0x9f, 0x4f, 0x4e, 0x52, 0x52, 0x48, 0x41, 0x48, 0x4b, 0x48, 0x53, 0x58, 0x1b, 0x9f, 0x1c, 0x4, 0x4b, 0x4b, 0x48, 0x52, 0x4e, 0x4d, 0x1f, 0x9f, 0x9f, ]], +] \ No newline at end of file diff --git a/worlds/smw/Names/LocationName.py b/worlds/smw/Names/LocationName.py new file mode 100644 index 0000000000..2b42cd21a4 --- /dev/null +++ b/worlds/smw/Names/LocationName.py @@ -0,0 +1,364 @@ +# Level Definitions +yoshis_house = "Yoshi's House" +yoshis_house_tile = "Yoshi's House - Tile" + +yoshis_island_1_exit_1 = "Yoshi's Island 1 - Normal Exit" +yoshis_island_1_dragon = "Yoshi's Island 1 - Dragon Coins" +yoshis_island_2_exit_1 = "Yoshi's Island 2 - Normal Exit" +yoshis_island_2_dragon = "Yoshi's Island 2 - Dragon Coins" +yoshis_island_3_exit_1 = "Yoshi's Island 3 - Normal Exit" +yoshis_island_3_dragon = "Yoshi's Island 3 - Dragon Coins" +yoshis_island_4_exit_1 = "Yoshi's Island 4 - Normal Exit" +yoshis_island_4_dragon = "Yoshi's Island 4 - Dragon Coins" +yoshis_island_castle = "#1 Iggy's Castle - Normal Exit" +yoshis_island_koopaling = "#1 Iggy's Castle - Boss" + +yellow_switch_palace = "Yellow Switch Palace" + +donut_plains_1_exit_1 = "Donut Plains 1 - Normal Exit" +donut_plains_1_exit_2 = "Donut Plains 1 - Secret Exit" +donut_plains_1_dragon = "Donut Plains 1 - Dragon Coins" +donut_plains_2_exit_1 = "Donut Plains 2 - Normal Exit" +donut_plains_2_exit_2 = "Donut Plains 2 - Secret Exit" +donut_plains_2_dragon = "Donut Plains 2 - Dragon Coins" +donut_plains_3_exit_1 = "Donut Plains 3 - Normal Exit" +donut_plains_3_dragon = "Donut Plains 3 - Dragon Coins" +donut_plains_4_exit_1 = "Donut Plains 4 - Normal Exit" +donut_plains_4_dragon = "Donut Plains 4 - Dragon Coins" +donut_secret_1_exit_1 = "Donut Secret 1 - Normal Exit" +donut_secret_1_exit_2 = "Donut Secret 1 - Secret Exit" +donut_secret_1_dragon = "Donut Secret 1 - Dragon Coins" +donut_secret_2_exit_1 = "Donut Secret 2 - Normal Exit" +donut_secret_2_dragon = "Donut Secret 2 - Dragon Coins" +donut_ghost_house_exit_1 = "Donut Ghost House - Normal Exit" +donut_ghost_house_exit_2 = "Donut Ghost House - Secret Exit" +donut_secret_house_exit_1 = "Donut Secret House - Normal Exit" +donut_secret_house_exit_2 = "Donut Secret House - Secret Exit" +donut_plains_castle = "#2 Morton's Castle - Normal Exit" +donut_plains_koopaling = "#2 Morton's Castle - Boss" + +green_switch_palace = "Green Switch Palace" + +vanilla_dome_1_exit_1 = "Vanilla Dome 1 - Normal Exit" +vanilla_dome_1_exit_2 = "Vanilla Dome 1 - Secret Exit" +vanilla_dome_1_dragon = "Vanilla Dome 1 - Dragon Coins" +vanilla_dome_2_exit_1 = "Vanilla Dome 2 - Normal Exit" +vanilla_dome_2_exit_2 = "Vanilla Dome 2 - Secret Exit" +vanilla_dome_2_dragon = "Vanilla Dome 2 - Dragon Coins" +vanilla_dome_3_exit_1 = "Vanilla Dome 3 - Normal Exit" +vanilla_dome_3_dragon = "Vanilla Dome 3 - Dragon Coins" +vanilla_dome_4_exit_1 = "Vanilla Dome 4 - Normal Exit" +vanilla_dome_4_dragon = "Vanilla Dome 4 - Dragon Coins" +vanilla_secret_1_exit_1 = "Vanilla Secret 1 - Normal Exit" +vanilla_secret_1_exit_2 = "Vanilla Secret 1 - Secret Exit" +vanilla_secret_1_dragon = "Vanilla Secret 1 - Dragon Coins" +vanilla_secret_2_exit_1 = "Vanilla Secret 2 - Normal Exit" +vanilla_secret_2_dragon = "Vanilla Secret 2 - Dragon Coins" +vanilla_secret_3_exit_1 = "Vanilla Secret 3 - Normal Exit" +vanilla_secret_3_dragon = "Vanilla Secret 3 - Dragon Coins" +vanilla_ghost_house_exit_1 = "Vanilla Ghost House - Normal Exit" +vanilla_ghost_house_dragon = "Vanilla Ghost House - Dragon Coins" +vanilla_fortress = "Vanilla Fortress - Normal Exit" +vanilla_reznor = "Vanilla Fortress - Boss" +vanilla_dome_castle = "#3 Lemmy's Castle - Normal Exit" +vanilla_dome_koopaling = "#3 Lemmy's Castle - Boss" + +red_switch_palace = "Red Switch Palace" + +butter_bridge_1_exit_1 = "Butter Bridge 1 - Normal Exit" +butter_bridge_1_dragon = "Butter Bridge 1 - Dragon Coins" +butter_bridge_2_exit_1 = "Butter Bridge 2 - Normal Exit" +butter_bridge_2_dragon = "Butter Bridge 2 - Dragon Coins" +cheese_bridge_exit_1 = "Cheese Bridge - Normal Exit" +cheese_bridge_exit_2 = "Cheese Bridge - Secret Exit" +cheese_bridge_dragon = "Cheese Bridge - Dragon Coins" +cookie_mountain_exit_1 = "Cookie Mountain - Normal Exit" +cookie_mountain_dragon = "Cookie Mountain - Dragon Coins" +soda_lake_exit_1 = "Soda Lake - Normal Exit" +soda_lake_dragon = "Soda Lake - Dragon Coins" +twin_bridges_castle = "#4 Ludwig's Castle - Normal Exit" +twin_bridges_koopaling = "#4 Ludwig's Castle - Boss" + +forest_of_illusion_1_exit_1 = "Forest of Illusion 1 - Normal Exit" +forest_of_illusion_1_exit_2 = "Forest of Illusion 1 - Secret Exit" +forest_of_illusion_2_exit_1 = "Forest of Illusion 2 - Normal Exit" +forest_of_illusion_2_exit_2 = "Forest of Illusion 2 - Secret Exit" +forest_of_illusion_2_dragon = "Forest of Illusion 2 - Dragon Coins" +forest_of_illusion_3_exit_1 = "Forest of Illusion 3 - Normal Exit" +forest_of_illusion_3_exit_2 = "Forest of Illusion 3 - Secret Exit" +forest_of_illusion_3_dragon = "Forest of Illusion 3 - Dragon Coins" +forest_of_illusion_4_exit_1 = "Forest of Illusion 4 - Normal Exit" +forest_of_illusion_4_exit_2 = "Forest of Illusion 4 - Secret Exit" +forest_of_illusion_4_dragon = "Forest of Illusion 4 - Dragon Coins" +forest_ghost_house_exit_1 = "Forest Ghost House - Normal Exit" +forest_ghost_house_exit_2 = "Forest Ghost House - Secret Exit" +forest_ghost_house_dragon = "Forest Ghost House - Dragon Coins" +forest_secret_exit_1 = "Forest Secret - Normal Exit" +forest_secret_dragon = "Forest Secret - Dragon Coins" +forest_fortress = "Forest Fortress - Normal Exit" +forest_reznor = "Forest Fortress - Boss" +forest_castle = "#5 Roy's Castle - Normal Exit" +forest_castle_dragon = "#5 Roy's Castle - Dragon Coins" +forest_koopaling = "#5 Roy's Castle - Boss" + +blue_switch_palace = "Blue Switch Palace" + +chocolate_island_1_exit_1 = "Chocolate Island 1 - Normal Exit" +chocolate_island_1_dragon = "Chocolate Island 1 - Dragon Coins" +chocolate_island_2_exit_1 = "Chocolate Island 2 - Normal Exit" +chocolate_island_2_exit_2 = "Chocolate Island 2 - Secret Exit" +chocolate_island_2_dragon = "Chocolate Island 2 - Dragon Coins" +chocolate_island_3_exit_1 = "Chocolate Island 3 - Normal Exit" +chocolate_island_3_exit_2 = "Chocolate Island 3 - Secret Exit" +chocolate_island_3_dragon = "Chocolate Island 3 - Dragon Coins" +chocolate_island_4_exit_1 = "Chocolate Island 4 - Normal Exit" +chocolate_island_4_dragon = "Chocolate Island 4 - Dragon Coins" +chocolate_island_5_exit_1 = "Chocolate Island 5 - Normal Exit" +chocolate_island_5_dragon = "Chocolate Island 5 - Dragon Coins" +chocolate_ghost_house_exit_1 = "Choco-Ghost House - Normal Exit" +chocolate_secret_exit_1 = "Chocolate Secret - Normal Exit" +chocolate_fortress = "Chocolate Fortress - Normal Exit" +chocolate_reznor = "Chocolate Fortress Defeat" +chocolate_castle = "#6 Wendy's Castle - Normal Exit" +chocolate_koopaling = "#6 Wendy's Castle - Boss" + +sunken_ghost_ship = "Sunken Ghost Ship - Normal Exit" +sunken_ghost_ship_dragon = "Sunken Ghost Ship - Dragon Coins" + +valley_of_bowser_1_exit_1 = "Valley of Bowser 1 - Normal Exit" +valley_of_bowser_1_dragon = "Valley of Bowser 1 - Dragon Coins" +valley_of_bowser_2_exit_1 = "Valley of Bowser 2 - Normal Exit" +valley_of_bowser_2_exit_2 = "Valley of Bowser 2 - Secret Exit" +valley_of_bowser_2_dragon = "Valley of Bowser 2 - Dragon Coins" +valley_of_bowser_3_exit_1 = "Valley of Bowser 3 - Normal Exit" +valley_of_bowser_3_dragon = "Valley of Bowser 3 - Dragon Coins" +valley_of_bowser_4_exit_1 = "Valley of Bowser 4 - Normal Exit" +valley_of_bowser_4_exit_2 = "Valley of Bowser 4 - Secret Exit" +valley_ghost_house_exit_1 = "Valley Ghost House - Normal Exit" +valley_ghost_house_exit_2 = "Valley Ghost House - Secret Exit" +valley_ghost_house_dragon = "Valley Ghost House - Dragon Coins" +valley_fortress = "Valley Fortress - Normal Exit" +valley_reznor = "Valley Fortress - Boss" +valley_castle = "#7 Larry's Castle - Normal Exit" +valley_castle_dragon = "#7 Larry's Castle - Dragon Coins" +valley_koopaling = "#7 Larry's Castle - Boss" + +front_door = "Front Door" +back_door = "Back Door" +bowser = "Bowser" + +star_road_1_exit_1 = "Star Road 1 - Normal Exit" +star_road_1_exit_2 = "Star Road 1 - Secret Exit" +star_road_1_dragon = "Star Road 1 - Dragon Coins" +star_road_2_exit_1 = "Star Road 2 - Normal Exit" +star_road_2_exit_2 = "Star Road 2 - Secret Exit" +star_road_3_exit_1 = "Star Road 3 - Normal Exit" +star_road_3_exit_2 = "Star Road 3 - Secret Exit" +star_road_4_exit_1 = "Star Road 4 - Normal Exit" +star_road_4_exit_2 = "Star Road 4 - Secret Exit" +star_road_5_exit_1 = "Star Road 5 - Normal Exit" +star_road_5_exit_2 = "Star Road 5 - Secret Exit" + +special_zone_1_exit_1 = "Gnarly - Normal Exit" +special_zone_1_dragon = "Gnarly - Dragon Coins" +special_zone_2_exit_1 = "Tubular - Normal Exit" +special_zone_2_dragon = "Tubular - Dragon Coins" +special_zone_3_exit_1 = "Way Cool - Normal Exit" +special_zone_3_dragon = "Way Cool - Dragon Coins" +special_zone_4_exit_1 = "Awesome - Normal Exit" +special_zone_4_dragon = "Awesome - Dragon Coins" +special_zone_5_exit_1 = "Groovy - Normal Exit" +special_zone_5_dragon = "Groovy - Dragon Coins" +special_zone_6_exit_1 = "Mondo - Normal Exit" +special_zone_6_dragon = "Mondo - Dragon Coins" +special_zone_7_exit_1 = "Outrageous - Normal Exit" +special_zone_7_dragon = "Outrageous - Dragon Coins" +special_zone_8_exit_1 = "Funky - Normal Exit" +special_zone_8_dragon = "Funky - Dragon Coins" + + +# Region Definitions +menu_region = "Menu" + +yoshis_island_region = "Yoshi's Island" +donut_plains_region = "Donut Plains" +vanilla_dome_region = "Vanilla Dome" +twin_bridges_region = "Twin Bridges" +forest_of_illusion_region = "Forest of Illusion" +chocolate_island_region = "Chocolate Island" +valley_of_bowser_region = "Valley of Bowser" +star_road_region = "Star Road" +special_zone_region = "Special Zone" + +yoshis_island_1_tile = "Yoshi's Island 1 - Tile" +yoshis_island_1_region = "Yoshi's Island 1" +yoshis_island_2_tile = "Yoshi's Island 2 - Tile" +yoshis_island_2_region = "Yoshi's Island 2" +yoshis_island_3_tile = "Yoshi's Island 3 - Tile" +yoshis_island_3_region = "Yoshi's Island 3" +yoshis_island_4_tile = "Yoshi's Island 4 - Tile" +yoshis_island_4_region = "Yoshi's Island 4" +yoshis_island_castle_tile = "#1 Iggy's Castle - Tile" +yoshis_island_castle_region = "#1 Iggy's Castle" + +yellow_switch_palace_tile = "Yellow Switch Palace - Tile" + +donut_plains_1_tile = "Donut Plains 1 - Tile" +donut_plains_1_region = "Donut Plains 1" +donut_plains_2_tile = "Donut Plains 2 - Tile" +donut_plains_2_region = "Donut Plains 2" +donut_plains_3_tile = "Donut Plains 3 - Tile" +donut_plains_3_region = "Donut Plains 3" +donut_plains_4_tile = "Donut Plains 4 - Tile" +donut_plains_4_region = "Donut Plains 4" +donut_secret_1_tile = "Donut Secret 1 - Tile" +donut_secret_1_region = "Donut Secret 1" +donut_secret_2_tile = "Donut Secret 2 - Tile" +donut_secret_2_region = "Donut Secret 2" +donut_ghost_house_tile = "Donut Ghost House - Tile" +donut_ghost_house_region = "Donut Ghost House" +donut_secret_house_tile = "Donut Secret House - Tile" +donut_secret_house_region = "Donut Secret House" +donut_plains_castle_tile = "#2 Morton's Castle - Tile" +donut_plains_castle_region = "#2 Morton's Castle" +donut_plains_top_secret = "Top Secret Area" +donut_plains_top_secret_tile = "Top Secret Area - Tile" +donut_plains_star_road = "Donut Plains - Star Road" + +green_switch_palace_tile = "Green Switch Palace - Tile" + +vanilla_dome_1_tile = "Vanilla Dome 1 - Tile" +vanilla_dome_1_region = "Vanilla Dome 1" +vanilla_dome_2_tile = "Vanilla Dome 2 - Tile" +vanilla_dome_2_region = "Vanilla Dome 2" +vanilla_dome_3_tile = "Vanilla Dome 3 - Tile" +vanilla_dome_3_region = "Vanilla Dome 3" +vanilla_dome_4_tile = "Vanilla Dome 4 - Tile" +vanilla_dome_4_region = "Vanilla Dome 4" +vanilla_secret_1_tile = "Vanilla Secret 1 - Tile" +vanilla_secret_1_region = "Vanilla Secret 1" +vanilla_secret_2_tile = "Vanilla Secret 2 - Tile" +vanilla_secret_2_region = "Vanilla Secret 2" +vanilla_secret_3_tile = "Vanilla Secret 3 - Tile" +vanilla_secret_3_region = "Vanilla Secret 3" +vanilla_ghost_house_tile = "Vanilla Ghost House - Tile" +vanilla_ghost_house_region = "Vanilla Ghost House" +vanilla_fortress_tile = "Vanilla Fortress - Tile" +vanilla_fortress_region = "Vanilla Fortress" +vanilla_dome_castle_tile = "#3 Lemmy's Castle - Tile" +vanilla_dome_castle_region = "#3 Lemmy's Castle" +vanilla_dome_star_road = "Vanilla Dome - Star Road" + +red_switch_palace_tile = "Red Switch Palace - Tile" + +butter_bridge_1_tile = "Butter Bridge 1 - Tile" +butter_bridge_1_region = "Butter Bridge 1" +butter_bridge_2_tile = "Butter Bridge 2 - Tile" +butter_bridge_2_region = "Butter Bridge 2" +cheese_bridge_tile = "Cheese Bridge - Tile" +cheese_bridge_region = "Cheese Bridge" +cookie_mountain_tile = "Cookie Mountain - Tile" +cookie_mountain_region = "Cookie Mountain" +soda_lake_tile = "Soda Lake - Tile" +soda_lake_region = "Soda Lake" +twin_bridges_castle_tile = "#4 Ludwig's Castle - Tile" +twin_bridges_castle_region = "#4 Ludwig's Castle" +twin_bridges_star_road = "Twin Bridges - Star Road" + +forest_of_illusion_1_tile = "Forest of Illusion 1 - Tile" +forest_of_illusion_1_region = "Forest of Illusion 1" +forest_of_illusion_2_tile = "Forest of Illusion 2 - Tile" +forest_of_illusion_2_region = "Forest of Illusion 2" +forest_of_illusion_3_tile = "Forest of Illusion 3 - Tile" +forest_of_illusion_3_region = "Forest of Illusion 3" +forest_of_illusion_4_tile = "Forest of Illusion 4 - Tile" +forest_of_illusion_4_region = "Forest of Illusion 4" +forest_ghost_house_tile = "Forest Ghost House - Tile" +forest_ghost_house_region = "Forest Ghost House" +forest_secret_tile = "Forest Secret - Tile" +forest_secret_region = "Forest Secret" +forest_fortress_tile = "Forest Fortress - Tile" +forest_fortress_region = "Forest Fortress" +forest_castle_tile = "#5 Roy's Castle - Tile" +forest_castle_region = "#5 Roy's Castle" +forest_star_road = "Forest of Illusion - Star Road" + +blue_switch_palace_tile = "Blue Switch Palace - Tile" + +chocolate_island_1_tile = "Chocolate Island 1 - Tile" +chocolate_island_1_region = "Chocolate Island 1" +chocolate_island_2_tile = "Chocolate Island 2 - Tile" +chocolate_island_2_region = "Chocolate Island 2" +chocolate_island_3_tile = "Chocolate Island 3 - Tile" +chocolate_island_3_region = "Chocolate Island 3" +chocolate_island_4_tile = "Chocolate Island 4 - Tile" +chocolate_island_4_region = "Chocolate Island 4" +chocolate_island_5_tile = "Chocolate Island 5 - Tile" +chocolate_island_5_region = "Chocolate Island 5" +chocolate_ghost_house_tile = "Choco-Ghost House - Tile" +chocolate_ghost_house_region = "Choco-Ghost House" +chocolate_secret_tile = "Chocolate Secret - Tile" +chocolate_secret_region = "Chocolate Secret" +chocolate_fortress_tile = "Chocolate Fortress - Tile" +chocolate_fortress_region = "Chocolate Fortress" +chocolate_castle_tile = "#6 Wendy's Castle - Tile" +chocolate_castle_region = "#6 Wendy's Castle" + +sunken_ghost_ship_tile = "Sunken Ghost Ship - Tile" +sunken_ghost_ship_region = "Sunken Ghost Ship" + +valley_of_bowser_1_tile = "Valley of Bowser 1 - Tile" +valley_of_bowser_1_region = "Valley of Bowser 1" +valley_of_bowser_2_tile = "Valley of Bowser 2 - Tile" +valley_of_bowser_2_region = "Valley of Bowser 2" +valley_of_bowser_3_tile = "Valley of Bowser 3 - Tile" +valley_of_bowser_3_region = "Valley of Bowser 3" +valley_of_bowser_4_tile = "Valley of Bowser 4 - Tile" +valley_of_bowser_4_region = "Valley of Bowser 4" +valley_ghost_house_tile = "Valley Ghost House - Tile" +valley_ghost_house_region = "Valley Ghost House" +valley_fortress_tile = "Valley Fortress - Tile" +valley_fortress_region = "Valley Fortress" +valley_castle_tile = "#7 Larry's Castle - Tile" +valley_castle_region = "#7 Larry's Castle" +valley_star_road = "Valley of Bowser - Star Road" + +front_door_tile = "Front Door - Tile" +back_door_tile = "Back Door - Tile" +bowser_region = "Bowser - Region" + +star_road_donut = "Star Road - Donut Plains" +star_road_1_tile = "Star Road 1 - Tile" +star_road_1_region = "Star Road 1" +star_road_vanilla = "Star Road - Vanilla Dome" +star_road_2_tile = "Star Road 2 - Tile" +star_road_2_region = "Star Road 2" +star_road_twin_bridges = "Star Road - Twin Bridges" +star_road_3_tile = "Star Road 3 - Tile" +star_road_3_region = "Star Road 3" +star_road_forest = "Star Road - Forest of Illusion" +star_road_4_tile = "Star Road 4 - Tile" +star_road_4_region = "Star Road 4" +star_road_valley = "Star Road - Valley of Bowser" +star_road_5_tile = "Star Road 5 - Tile" +star_road_5_region = "Star Road 5" +star_road_special = "Star Road - Special Zone" + +special_star_road = "Special Zone - Star Road" +special_zone_1_tile = "Gnarly - Tile" +special_zone_1_region = "Gnarly" +special_zone_2_tile = "Tubular - Tile" +special_zone_2_region = "Tubular" +special_zone_3_tile = "Way Cool - Tile" +special_zone_3_region = "Way Cool" +special_zone_4_tile = "Awesome - Tile" +special_zone_4_region = "Awesome" +special_zone_5_tile = "Groovy - Tile" +special_zone_5_region = "Groovy" +special_zone_6_tile = "Mondo - Tile" +special_zone_6_region = "Mondo" +special_zone_7_tile = "Outrageous - Tile" +special_zone_7_region = "Outrageous" +special_zone_8_tile = "Funky - Tile" +special_zone_8_region = "Funky" +special_complete = "Special Zone - Star Road - Complete" diff --git a/worlds/smw/Names/TextBox.py b/worlds/smw/Names/TextBox.py new file mode 100644 index 0000000000..cecf088661 --- /dev/null +++ b/worlds/smw/Names/TextBox.py @@ -0,0 +1,140 @@ + +from BaseClasses import MultiWorld + +import math + + +text_mapping = { + "A": 0x00, "B": 0x01, "C": 0x02, "D": 0x03, "E": 0x04, "F": 0x05, "G": 0x06, "H": 0x07, "I": 0x08, "J": 0x09, + "K": 0x0A, "L": 0x0B, "M": 0x0C, "N": 0x0D, "O": 0x0E, "P": 0x0F, "Q": 0x10, "R": 0x11, "S": 0x12, "T": 0x13, + "U": 0x14, "V": 0x15, "W": 0x16, "X": 0x17, "Y": 0x18, "Z": 0x19, + + "!": 0x1A, ".": 0x1B, "-": 0x1C, ",": 0x1D, "?": 0x1E, " ": 0x1F, + + "0": 0x22, "1": 0x23, "2": 0x24, "3": 0x25, "4": 0x26, "5": 0x27, "6": 0x28, "7": 0x29, "8": 0x2A, "9": 0x2B, + + "a": 0x40, "b": 0x41, "c": 0x42, "d": 0x43, "e": 0x44, "f": 0x45, "g": 0x46, "h": 0x47, "i": 0x48, "j": 0x49, + "k": 0x4A, "l": 0x4B, "m": 0x4C, "n": 0x4D, "o": 0x4E, "p": 0x4F, "q": 0x50, "r": 0x51, "s": 0x52, "t": 0x53, + "u": 0x54, "v": 0x55, "w": 0x56, "x": 0x57, "y": 0x58, "z": 0x59, + + "#": 0x5A, "(": 0x5B, ")": 0x5C, "'": 0x5D +} + +title_text_mapping = { + "A": [0x0A, 0x38], "B": [0x0B, 0x38], "C": [0x0C, 0x38], "D": [0x0D, 0x38], "E": [0x0E, 0x38], + "F": [0x0F, 0x38], "G": [0x10, 0x38], "H": [0x11, 0x38], "I": [0x12, 0x38], "J": [0x13, 0x38], + "K": [0x14, 0x38], "L": [0x15, 0x38], "M": [0x16, 0x38], "N": [0x17, 0x38], "O": [0x18, 0x38], + "P": [0x19, 0x38], "Q": [0x1A, 0x38], "R": [0x1B, 0x38], "S": [0x1C, 0x38], "T": [0x1D, 0x38], + "U": [0x1E, 0x38], "V": [0x1F, 0x38], "W": [0x20, 0x38], "X": [0x21, 0x38], "Y": [0x22, 0x38], + "Z": [0x23, 0x38], " ": [0xFC, 0x38], ".": [0x24, 0x38], + "0": [0x00, 0x38], "1": [0x01, 0x38], "2": [0x02, 0x38], "3": [0x03, 0x38], "4": [0x04, 0x38], + "5": [0x05, 0x38], "6": [0x06, 0x38], "7": [0x07, 0x38], "8": [0x08, 0x38], "9": [0x09, 0x38], +} + + +def string_to_bytes(input_string): + out_array = bytearray() + for letter in input_string: + out_array.append(text_mapping[letter] if letter in text_mapping else text_mapping["."]) + + return out_array + + +def generate_text_box(input_string): + out_bytes = bytearray() + box_line_count = 0 + box_line_chr_count = 0 + for word in input_string.split(): + if box_line_chr_count + len(word) > 18: + out_bytes[-1] += 0x80 + box_line_count += 1 + box_line_chr_count = 0 + + out_bytes.extend(string_to_bytes(word)) + box_line_chr_count += len(word) + + if box_line_chr_count < 18: + box_line_chr_count += 1 + out_bytes.append(0x1F) + + for i in range(box_line_count, 8): + out_bytes.append(0x9F) + + return out_bytes + + +def generate_goal_text(world: MultiWorld, player: int): + out_array = bytearray() + if world.goal[player] == "yoshi_egg_hunt": + required_yoshi_eggs = max(math.floor( + world.number_of_yoshi_eggs[player].value * (world.percentage_of_yoshi_eggs[player].value / 100.0)), 1) + out_array += bytearray([0x9F, 0x9F]) + out_array += string_to_bytes(" You must acquire") + out_array[-1] += 0x80 + out_array += string_to_bytes(f' {required_yoshi_eggs:02} Yoshi Eggs,') + out_array[-1] += 0x80 + out_array += string_to_bytes("then return here.") + out_array[-1] += 0x80 + out_array += bytearray([0x9F, 0x9F, 0x9F]) + else: + bosses_required = world.bosses_required[player].value + out_array += bytearray([0x9F, 0x9F]) + out_array += string_to_bytes(" You must defeat") + out_array[-1] += 0x80 + out_array += string_to_bytes(f' {bosses_required:02} Bosses,') + out_array[-1] += 0x80 + out_array += string_to_bytes("then defeat Bowser") + out_array[-1] += 0x80 + out_array += bytearray([0x9F, 0x9F, 0x9F]) + + return out_array + + +def generate_received_text(item_name: str, player_name: str): + out_array = bytearray() + + item_name = item_name[:18] + player_name = player_name[:18] + + item_buffer = max(0, math.floor((18 - len(item_name)) / 2)) + player_buffer = max(0, math.floor((18 - len(player_name)) / 2)) + + out_array += bytearray([0x9F, 0x9F]) + out_array += string_to_bytes(" Received") + out_array[-1] += 0x80 + out_array += bytearray([0x1F] * item_buffer) + out_array += string_to_bytes(item_name) + out_array[-1] += 0x80 + out_array += string_to_bytes(" from") + out_array[-1] += 0x80 + out_array += bytearray([0x1F] * player_buffer) + out_array += string_to_bytes(player_name) + out_array[-1] += 0x80 + out_array += bytearray([0x9F, 0x9F]) + + return out_array + + +def generate_sent_text(item_name: str, player_name: str): + out_array = bytearray() + + item_name = item_name[:18] + player_name = player_name[:18] + + item_buffer = max(0, math.floor((18 - len(item_name)) / 2)) + player_buffer = max(0, math.floor((18 - len(player_name)) / 2)) + + out_array += bytearray([0x9F, 0x9F]) + out_array += string_to_bytes(" Sent") + out_array[-1] += 0x80 + out_array += bytearray([0x1F] * item_buffer) + out_array += string_to_bytes(item_name) + out_array[-1] += 0x80 + out_array += string_to_bytes(" to") + out_array[-1] += 0x80 + out_array += bytearray([0x1F] * player_buffer) + out_array += string_to_bytes(player_name) + out_array[-1] += 0x80 + out_array += bytearray([0x9F, 0x9F]) + + return out_array diff --git a/worlds/smw/Options.py b/worlds/smw/Options.py new file mode 100644 index 0000000000..80af63f5a4 --- /dev/null +++ b/worlds/smw/Options.py @@ -0,0 +1,236 @@ +import typing + +from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, OptionList + + +class Goal(Choice): + """ + Determines the goal of the seed + Bowser: Defeat Koopalings, reach Bowser's Castle and defeat Bowser + Yoshi Egg Hunt: Find a certain number of Yoshi Eggs + """ + display_name = "Goal" + option_bowser = 0 + option_yoshi_egg_hunt = 1 + default = 0 + + +class BossesRequired(Range): + """ + How many Bosses (Koopalings or Reznor) must be defeated in order to defeat Bowser + """ + display_name = "Bosses Required" + range_start = 0 + range_end = 11 + default = 7 + + +class NumberOfYoshiEggs(Range): + """ + How many Yoshi Eggs are in the pool for Yoshi Egg Hunt + """ + display_name = "Total Number of Yoshi Eggs" + range_start = 1 + range_end = 80 + default = 50 + + +class PercentageOfYoshiEggs(Range): + """ + What Percentage of Yoshi Eggs are required to finish Yoshi Egg Hunt + """ + display_name = "Required Percentage of Yoshi Eggs" + range_start = 1 + range_end = 100 + default = 100 + + +class DragonCoinChecks(Toggle): + """ + Whether collecting 5 Dragon Coins in each level will grant a check + """ + display_name = "Dragon Coin Checks" + + +class BowserCastleDoors(Choice): + """ + How the doors of Bowser's Castle behave + Vanilla: Front and Back Doors behave as vanilla + Fast: Both doors behave as the Back Door + Slow: Both doors behave as the Front Door + "Front Door" requires beating all 8 Rooms + "Back Door" only requires going through the dark hallway to Bowser + """ + display_name = "Bowser Castle Doors" + option_vanilla = 0 + option_fast = 1 + option_slow = 2 + default = 0 + + +class LevelShuffle(Toggle): + """ + Whether levels are shuffled + """ + display_name = "Level Shuffle" + + +class SwapDonutGhostHouseExits(Toggle): + """ + If enabled, this option will swap which overworld direction the two exits of the level at the Donut Ghost House overworld tile go: + False: Normal Exit goes up, Secret Exit goes right. + True: Normal Exit goes right, Secret Exit goes up. + """ + display_name = "Swap Donut GH Exits" + + +class DisplaySentItemPopups(Choice): + """ + What messages to display in-game for items sent + """ + display_name = "Display Sent Item Popups" + option_none = 0 + option_all = 1 + default = 1 + + +class DisplayReceivedItemPopups(Choice): + """ + What messages to display in-game for items received + """ + display_name = "Display Received Item Popups" + option_none = 0 + option_all = 1 + option_progression = 2 + default = 2 + + +class TrapFillPercentage(Range): + """ + Replace a percentage of junk items in the item pool with random traps + """ + display_name = "Trap Fill Percentage" + range_start = 0 + range_end = 100 + default = 0 + + +class BaseTrapWeight(Choice): + """ + Base Class for Trap Weights + """ + option_none = 0 + option_low = 1 + option_medium = 2 + option_high = 4 + default = 2 + + +class IceTrapWeight(BaseTrapWeight): + """ + Likelihood of a receiving a trap which causes the level to become slippery + """ + display_name = "Ice Trap Weight" + + +class StunTrapWeight(BaseTrapWeight): + """ + Likelihood of a receiving a trap which briefly stuns Mario + """ + display_name = "Stun Trap Weight" + + +class LiteratureTrapWeight(BaseTrapWeight): + """ + Likelihood of a receiving a trap which causes the player to read literature + """ + display_name = "Literature Trap Weight" + + +class Autosave(DefaultOnToggle): + """ + Whether a save prompt will appear after every level + """ + display_name = "Autosave" + + +class MusicShuffle(Choice): + """ + Music shuffle type + None: No Music is shuffled + Consistent: Each music track is consistently shuffled throughout the game + Full: Each individual level has a random music track + Singularity: The entire game uses one song for overworld and one song for levels + """ + display_name = "Music Shuffle" + option_none = 0 + option_consistent = 1 + option_full = 2 + option_singularity = 3 + default = 0 + + +class MarioPalette(Choice): + """ + Mario palette color + """ + display_name = "Mario Palette" + option_mario = 0 + option_luigi = 1 + option_wario = 2 + option_waluigi = 3 + option_geno = 4 + option_princess = 5 + option_dark = 6 + option_sponge = 7 + default = 0 + + +class ForegroundPaletteShuffle(Toggle): + """ + Whether to shuffle level foreground palettes + """ + display_name = "Foreground Palette Shuffle" + + +class BackgroundPaletteShuffle(Toggle): + """ + Whether to shuffle level background palettes + """ + display_name = "Background Palette Shuffle" + + +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 + + + +smw_options: typing.Dict[str, type(Option)] = { + "death_link": DeathLink, + "goal": Goal, + "bosses_required": BossesRequired, + "number_of_yoshi_eggs": NumberOfYoshiEggs, + "percentage_of_yoshi_eggs": PercentageOfYoshiEggs, + "dragon_coin_checks": DragonCoinChecks, + "bowser_castle_doors": BowserCastleDoors, + "level_shuffle": LevelShuffle, + "swap_donut_gh_exits": SwapDonutGhostHouseExits, + #"display_sent_item_popups": DisplaySentItemPopups, + "display_received_item_popups": DisplayReceivedItemPopups, + "trap_fill_percentage": TrapFillPercentage, + "ice_trap_weight": IceTrapWeight, + "stun_trap_weight": StunTrapWeight, + "literature_trap_weight": LiteratureTrapWeight, + "autosave": Autosave, + "music_shuffle": MusicShuffle, + "mario_palette": MarioPalette, + "foreground_palette_shuffle": ForegroundPaletteShuffle, + "background_palette_shuffle": BackgroundPaletteShuffle, + "starting_life_count": StartingLifeCount, +} diff --git a/worlds/smw/Regions.py b/worlds/smw/Regions.py new file mode 100644 index 0000000000..7af7b888c9 --- /dev/null +++ b/worlds/smw/Regions.py @@ -0,0 +1,1187 @@ +import typing + +from BaseClasses import MultiWorld, Region, RegionType, Entrance +from .Locations import SMWLocation +from .Levels import level_info_dict +from .Names import LocationName, ItemName +from ..generic.Rules import add_rule, set_rule + + +def create_regions(world, player: int, active_locations): + menu_region = create_region(world, player, active_locations, 'Menu', None) + + yoshis_island_region = create_region(world, player, active_locations, LocationName.yoshis_island_region, None) + donut_plains_region = create_region(world, player, active_locations, LocationName.donut_plains_region, None) + vanilla_dome_region = create_region(world, player, active_locations, LocationName.vanilla_dome_region, None) + twin_bridges_region = create_region(world, player, active_locations, LocationName.twin_bridges_region, None) + forest_of_illusion_region = create_region(world, player, active_locations, LocationName.forest_of_illusion_region, None) + chocolate_island_region = create_region(world, player, active_locations, LocationName.chocolate_island_region, None) + valley_of_bowser_region = create_region(world, player, active_locations, LocationName.valley_of_bowser_region, None) + star_road_region = create_region(world, player, active_locations, LocationName.star_road_region, None) + special_zone_region = create_region(world, player, active_locations, LocationName.special_zone_region, None) + + + yoshis_house_tile = create_region(world, player, active_locations, LocationName.yoshis_house_tile, None) + + yoshis_house_region_locations = [] + if world.goal[player] == "yoshi_egg_hunt": + yoshis_house_region_locations.append(LocationName.yoshis_house) + yoshis_house_region = create_region(world, player, active_locations, LocationName.yoshis_house, + yoshis_house_region_locations) + + yoshis_island_1_tile = create_region(world, player, active_locations, LocationName.yoshis_island_1_tile, None) + yoshis_island_1_region = create_region(world, player, active_locations, LocationName.yoshis_island_1_region, None) + yoshis_island_1_exit_1 = create_region(world, player, active_locations, LocationName.yoshis_island_1_exit_1, + [LocationName.yoshis_island_1_exit_1]) + + yoshis_island_2_tile = create_region(world, player, active_locations, LocationName.yoshis_island_2_tile, None) + yoshis_island_2_region = create_region(world, player, active_locations, LocationName.yoshis_island_2_region, None) + yoshis_island_2_exit_1 = create_region(world, player, active_locations, LocationName.yoshis_island_2_exit_1, + [LocationName.yoshis_island_2_exit_1]) + + yoshis_island_3_tile = create_region(world, player, active_locations, LocationName.yoshis_island_3_tile, None) + yoshis_island_3_region = create_region(world, player, active_locations, LocationName.yoshis_island_3_region, None) + yoshis_island_3_exit_1 = create_region(world, player, active_locations, LocationName.yoshis_island_3_exit_1, + [LocationName.yoshis_island_3_exit_1]) + + yoshis_island_4_tile = create_region(world, player, active_locations, LocationName.yoshis_island_4_tile, None) + yoshis_island_4_region = create_region(world, player, active_locations, LocationName.yoshis_island_4_region, None) + yoshis_island_4_exit_1 = create_region(world, player, active_locations, LocationName.yoshis_island_4_exit_1, + [LocationName.yoshis_island_4_exit_1]) + + yoshis_island_castle_tile = create_region(world, player, active_locations, LocationName.yoshis_island_castle_tile, None) + yoshis_island_castle_region = create_region(world, player, active_locations, LocationName.yoshis_island_castle_region, None) + yoshis_island_castle = create_region(world, player, active_locations, LocationName.yoshis_island_castle, + [LocationName.yoshis_island_castle, LocationName.yoshis_island_koopaling]) + + yellow_switch_palace_tile = create_region(world, player, active_locations, LocationName.yellow_switch_palace_tile, None) + yellow_switch_palace = create_region(world, player, active_locations, LocationName.yellow_switch_palace, + [LocationName.yellow_switch_palace]) + + + donut_plains_1_tile = create_region(world, player, active_locations, LocationName.donut_plains_1_tile, None) + donut_plains_1_region = create_region(world, player, active_locations, LocationName.donut_plains_1_region, None) + donut_plains_1_exit_1 = create_region(world, player, active_locations, LocationName.donut_plains_1_exit_1, + [LocationName.donut_plains_1_exit_1]) + donut_plains_1_exit_2 = create_region(world, player, active_locations, LocationName.donut_plains_1_exit_2, + [LocationName.donut_plains_1_exit_2]) + + donut_plains_2_tile = create_region(world, player, active_locations, LocationName.donut_plains_2_tile, None) + donut_plains_2_region = create_region(world, player, active_locations, LocationName.donut_plains_2_region, None) + donut_plains_2_exit_1 = create_region(world, player, active_locations, LocationName.donut_plains_2_exit_1, + [LocationName.donut_plains_2_exit_1]) + donut_plains_2_exit_2 = create_region(world, player, active_locations, LocationName.donut_plains_2_exit_2, + [LocationName.donut_plains_2_exit_2]) + + donut_plains_3_tile = create_region(world, player, active_locations, LocationName.donut_plains_3_tile, None) + donut_plains_3_region = create_region(world, player, active_locations, LocationName.donut_plains_3_region, None) + donut_plains_3_exit_1 = create_region(world, player, active_locations, LocationName.donut_plains_3_exit_1, + [LocationName.donut_plains_3_exit_1]) + + donut_plains_4_tile = create_region(world, player, active_locations, LocationName.donut_plains_4_tile, None) + donut_plains_4_region = create_region(world, player, active_locations, LocationName.donut_plains_4_region, None) + donut_plains_4_exit_1 = create_region(world, player, active_locations, LocationName.donut_plains_4_exit_1, + [LocationName.donut_plains_4_exit_1]) + + donut_secret_1_tile = create_region(world, player, active_locations, LocationName.donut_secret_1_tile, None) + donut_secret_1_region = create_region(world, player, active_locations, LocationName.donut_secret_1_region, None) + donut_secret_1_exit_1 = create_region(world, player, active_locations, LocationName.donut_secret_1_exit_1, + [LocationName.donut_secret_1_exit_1]) + donut_secret_1_exit_2 = create_region(world, player, active_locations, LocationName.donut_secret_1_exit_2, + [LocationName.donut_secret_1_exit_2]) + + donut_secret_2_tile = create_region(world, player, active_locations, LocationName.donut_secret_2_tile, None) + donut_secret_2_region = create_region(world, player, active_locations, LocationName.donut_secret_2_region, None) + donut_secret_2_exit_1 = create_region(world, player, active_locations, LocationName.donut_secret_2_exit_1, + [LocationName.donut_secret_2_exit_1]) + + donut_ghost_house_tile = create_region(world, player, active_locations, LocationName.donut_ghost_house_tile, None) + donut_ghost_house_region = create_region(world, player, active_locations, LocationName.donut_ghost_house_region, None) + donut_ghost_house_exit_1 = create_region(world, player, active_locations, LocationName.donut_ghost_house_exit_1, + [LocationName.donut_ghost_house_exit_1]) + donut_ghost_house_exit_2 = create_region(world, player, active_locations, LocationName.donut_ghost_house_exit_2, + [LocationName.donut_ghost_house_exit_2]) + + donut_secret_house_tile = create_region(world, player, active_locations, LocationName.donut_secret_house_tile, None) + donut_secret_house_region = create_region(world, player, active_locations, LocationName.donut_secret_house_region, None) + donut_secret_house_exit_1 = create_region(world, player, active_locations, LocationName.donut_secret_house_exit_1, + [LocationName.donut_secret_house_exit_1]) + donut_secret_house_exit_2 = create_region(world, player, active_locations, LocationName.donut_secret_house_exit_2, + [LocationName.donut_secret_house_exit_2]) + + donut_plains_castle_tile = create_region(world, player, active_locations, LocationName.donut_plains_castle_tile, None) + donut_plains_castle_region = create_region(world, player, active_locations, LocationName.donut_plains_castle_region, None) + donut_plains_castle = create_region(world, player, active_locations, LocationName.donut_plains_castle, + [LocationName.donut_plains_castle, LocationName.donut_plains_koopaling]) + + green_switch_palace_tile = create_region(world, player, active_locations, LocationName.green_switch_palace_tile, None) + green_switch_palace = create_region(world, player, active_locations, LocationName.green_switch_palace, + [LocationName.green_switch_palace]) + + donut_plains_top_secret_tile = create_region(world, player, active_locations, LocationName.donut_plains_top_secret_tile, None) + donut_plains_top_secret = create_region(world, player, active_locations, LocationName.donut_plains_top_secret, None) + + + vanilla_dome_1_tile = create_region(world, player, active_locations, LocationName.vanilla_dome_1_tile, None) + vanilla_dome_1_region = create_region(world, player, active_locations, LocationName.vanilla_dome_1_region, None) + vanilla_dome_1_exit_1 = create_region(world, player, active_locations, LocationName.vanilla_dome_1_exit_1, + [LocationName.vanilla_dome_1_exit_1]) + vanilla_dome_1_exit_2 = create_region(world, player, active_locations, LocationName.vanilla_dome_1_exit_2, + [LocationName.vanilla_dome_1_exit_2]) + + vanilla_dome_2_tile = create_region(world, player, active_locations, LocationName.vanilla_dome_2_tile, None) + vanilla_dome_2_region = create_region(world, player, active_locations, LocationName.vanilla_dome_2_region, None) + vanilla_dome_2_exit_1 = create_region(world, player, active_locations, LocationName.vanilla_dome_2_exit_1, + [LocationName.vanilla_dome_2_exit_1]) + vanilla_dome_2_exit_2 = create_region(world, player, active_locations, LocationName.vanilla_dome_2_exit_2, + [LocationName.vanilla_dome_2_exit_2]) + + vanilla_dome_3_tile = create_region(world, player, active_locations, LocationName.vanilla_dome_3_tile, None) + vanilla_dome_3_region = create_region(world, player, active_locations, LocationName.vanilla_dome_3_region, None) + vanilla_dome_3_exit_1 = create_region(world, player, active_locations, LocationName.vanilla_dome_3_exit_1, + [LocationName.vanilla_dome_3_exit_1]) + + vanilla_dome_4_tile = create_region(world, player, active_locations, LocationName.vanilla_dome_4_tile, None) + vanilla_dome_4_region = create_region(world, player, active_locations, LocationName.vanilla_dome_4_region, None) + vanilla_dome_4_exit_1 = create_region(world, player, active_locations, LocationName.vanilla_dome_4_exit_1, + [LocationName.vanilla_dome_4_exit_1]) + + vanilla_secret_1_tile = create_region(world, player, active_locations, LocationName.vanilla_secret_1_tile, None) + vanilla_secret_1_region = create_region(world, player, active_locations, LocationName.vanilla_secret_1_region, None) + vanilla_secret_1_exit_1 = create_region(world, player, active_locations, LocationName.vanilla_secret_1_exit_1, + [LocationName.vanilla_secret_1_exit_1]) + vanilla_secret_1_exit_2 = create_region(world, player, active_locations, LocationName.vanilla_secret_1_exit_2, + [LocationName.vanilla_secret_1_exit_2]) + + vanilla_secret_2_tile = create_region(world, player, active_locations, LocationName.vanilla_secret_2_tile, None) + vanilla_secret_2_region = create_region(world, player, active_locations, LocationName.vanilla_secret_2_region, None) + vanilla_secret_2_exit_1 = create_region(world, player, active_locations, LocationName.vanilla_secret_2_exit_1, + [LocationName.vanilla_secret_2_exit_1]) + + vanilla_secret_3_tile = create_region(world, player, active_locations, LocationName.vanilla_secret_3_tile, None) + vanilla_secret_3_region = create_region(world, player, active_locations, LocationName.vanilla_secret_3_region, None) + vanilla_secret_3_exit_1 = create_region(world, player, active_locations, LocationName.vanilla_secret_3_exit_1, + [LocationName.vanilla_secret_3_exit_1]) + + vanilla_ghost_house_tile = create_region(world, player, active_locations, LocationName.vanilla_ghost_house_tile, None) + vanilla_ghost_house_region = create_region(world, player, active_locations, LocationName.vanilla_ghost_house_region, None) + vanilla_ghost_house_exit_1 = create_region(world, player, active_locations, LocationName.vanilla_ghost_house_exit_1, + [LocationName.vanilla_ghost_house_exit_1]) + + vanilla_fortress_tile = create_region(world, player, active_locations, LocationName.vanilla_fortress_tile, None) + vanilla_fortress_region = create_region(world, player, active_locations, LocationName.vanilla_fortress_region, None) + vanilla_fortress = create_region(world, player, active_locations, LocationName.vanilla_fortress, + [LocationName.vanilla_fortress, LocationName.vanilla_reznor]) + + vanilla_dome_castle_tile = create_region(world, player, active_locations, LocationName.vanilla_dome_castle_tile, None) + vanilla_dome_castle_region = create_region(world, player, active_locations, LocationName.vanilla_dome_castle_region, None) + vanilla_dome_castle = create_region(world, player, active_locations, LocationName.vanilla_dome_castle, + [LocationName.vanilla_dome_castle, LocationName.vanilla_dome_koopaling]) + + red_switch_palace_tile = create_region(world, player, active_locations, LocationName.red_switch_palace_tile, None) + red_switch_palace = create_region(world, player, active_locations, LocationName.red_switch_palace, + [LocationName.red_switch_palace]) + + + butter_bridge_1_tile = create_region(world, player, active_locations, LocationName.butter_bridge_1_tile, None) + butter_bridge_1_region = create_region(world, player, active_locations, LocationName.butter_bridge_1_region, None) + butter_bridge_1_exit_1 = create_region(world, player, active_locations, LocationName.butter_bridge_1_exit_1, + [LocationName.butter_bridge_1_exit_1]) + + butter_bridge_2_tile = create_region(world, player, active_locations, LocationName.butter_bridge_2_tile, None) + butter_bridge_2_region = create_region(world, player, active_locations, LocationName.butter_bridge_2_region, None) + butter_bridge_2_exit_1 = create_region(world, player, active_locations, LocationName.butter_bridge_2_exit_1, + [LocationName.butter_bridge_2_exit_1]) + + cheese_bridge_tile = create_region(world, player, active_locations, LocationName.cheese_bridge_tile, None) + cheese_bridge_region = create_region(world, player, active_locations, LocationName.cheese_bridge_region, None) + cheese_bridge_exit_1 = create_region(world, player, active_locations, LocationName.cheese_bridge_exit_1, + [LocationName.cheese_bridge_exit_1]) + cheese_bridge_exit_2 = create_region(world, player, active_locations, LocationName.cheese_bridge_exit_2, + [LocationName.cheese_bridge_exit_2]) + + cookie_mountain_tile = create_region(world, player, active_locations, LocationName.cookie_mountain_tile, None) + cookie_mountain_region = create_region(world, player, active_locations, LocationName.cookie_mountain_region, None) + cookie_mountain_exit_1 = create_region(world, player, active_locations, LocationName.cookie_mountain_exit_1, + [LocationName.cookie_mountain_exit_1]) + + soda_lake_tile = create_region(world, player, active_locations, LocationName.soda_lake_tile, None) + soda_lake_region = create_region(world, player, active_locations, LocationName.soda_lake_region, None) + soda_lake_exit_1 = create_region(world, player, active_locations, LocationName.soda_lake_exit_1, + [LocationName.soda_lake_exit_1]) + + twin_bridges_castle_tile = create_region(world, player, active_locations, LocationName.twin_bridges_castle_tile, None) + twin_bridges_castle_region = create_region(world, player, active_locations, LocationName.twin_bridges_castle_region, None) + twin_bridges_castle = create_region(world, player, active_locations, LocationName.twin_bridges_castle, + [LocationName.twin_bridges_castle, LocationName.twin_bridges_koopaling]) + + + forest_of_illusion_1_tile = create_region(world, player, active_locations, LocationName.forest_of_illusion_1_tile, None) + forest_of_illusion_1_region = create_region(world, player, active_locations, LocationName.forest_of_illusion_1_region, None) + forest_of_illusion_1_exit_1 = create_region(world, player, active_locations, LocationName.forest_of_illusion_1_exit_1, + [LocationName.forest_of_illusion_1_exit_1]) + forest_of_illusion_1_exit_2 = create_region(world, player, active_locations, LocationName.forest_of_illusion_1_exit_2, + [LocationName.forest_of_illusion_1_exit_2]) + + forest_of_illusion_2_tile = create_region(world, player, active_locations, LocationName.forest_of_illusion_2_tile, None) + forest_of_illusion_2_region = create_region(world, player, active_locations, LocationName.forest_of_illusion_2_region, None) + forest_of_illusion_2_exit_1 = create_region(world, player, active_locations, LocationName.forest_of_illusion_2_exit_1, + [LocationName.forest_of_illusion_2_exit_1]) + forest_of_illusion_2_exit_2 = create_region(world, player, active_locations, LocationName.forest_of_illusion_2_exit_2, + [LocationName.forest_of_illusion_2_exit_2]) + + forest_of_illusion_3_tile = create_region(world, player, active_locations, LocationName.forest_of_illusion_3_tile, None) + forest_of_illusion_3_region = create_region(world, player, active_locations, LocationName.forest_of_illusion_3_region, None) + forest_of_illusion_3_exit_1 = create_region(world, player, active_locations, LocationName.forest_of_illusion_3_exit_1, + [LocationName.forest_of_illusion_3_exit_1]) + forest_of_illusion_3_exit_2 = create_region(world, player, active_locations, LocationName.forest_of_illusion_3_exit_2, + [LocationName.forest_of_illusion_3_exit_2]) + + forest_of_illusion_4_tile = create_region(world, player, active_locations, LocationName.forest_of_illusion_4_tile, None) + forest_of_illusion_4_region = create_region(world, player, active_locations, LocationName.forest_of_illusion_4_region, None) + forest_of_illusion_4_exit_1 = create_region(world, player, active_locations, LocationName.forest_of_illusion_4_exit_1, + [LocationName.forest_of_illusion_4_exit_1]) + forest_of_illusion_4_exit_2 = create_region(world, player, active_locations, LocationName.forest_of_illusion_4_exit_2, + [LocationName.forest_of_illusion_4_exit_2]) + + forest_ghost_house_tile = create_region(world, player, active_locations, LocationName.forest_ghost_house_tile, None) + forest_ghost_house_region = create_region(world, player, active_locations, LocationName.forest_ghost_house_region, None) + forest_ghost_house_exit_1 = create_region(world, player, active_locations, LocationName.forest_ghost_house_exit_1, + [LocationName.forest_ghost_house_exit_1]) + forest_ghost_house_exit_2 = create_region(world, player, active_locations, LocationName.forest_ghost_house_exit_2, + [LocationName.forest_ghost_house_exit_2]) + + forest_secret_tile = create_region(world, player, active_locations, LocationName.forest_secret_tile, None) + forest_secret_region = create_region(world, player, active_locations, LocationName.forest_secret_region, None) + forest_secret_exit_1 = create_region(world, player, active_locations, LocationName.forest_secret_exit_1, + [LocationName.forest_secret_exit_1]) + + forest_fortress_tile = create_region(world, player, active_locations, LocationName.forest_fortress_tile, None) + forest_fortress_region = create_region(world, player, active_locations, LocationName.forest_fortress_region, None) + forest_fortress = create_region(world, player, active_locations, LocationName.forest_fortress, + [LocationName.forest_fortress, LocationName.forest_reznor]) + + forest_castle_tile = create_region(world, player, active_locations, LocationName.forest_castle_tile, None) + forest_castle_region = create_region(world, player, active_locations, LocationName.forest_castle_region, None) + forest_castle = create_region(world, player, active_locations, LocationName.forest_castle, + [LocationName.forest_castle, LocationName.forest_koopaling]) + + blue_switch_palace_tile = create_region(world, player, active_locations, LocationName.blue_switch_palace_tile, None) + blue_switch_palace = create_region(world, player, active_locations, LocationName.blue_switch_palace, + [LocationName.blue_switch_palace]) + + + chocolate_island_1_tile = create_region(world, player, active_locations, LocationName.chocolate_island_1_tile, None) + chocolate_island_1_region = create_region(world, player, active_locations, LocationName.chocolate_island_1_region, None) + chocolate_island_1_exit_1 = create_region(world, player, active_locations, LocationName.chocolate_island_1_exit_1, + [LocationName.chocolate_island_1_exit_1]) + + chocolate_island_2_tile = create_region(world, player, active_locations, LocationName.chocolate_island_2_tile, None) + chocolate_island_2_region = create_region(world, player, active_locations, LocationName.chocolate_island_2_region, None) + chocolate_island_2_exit_1 = create_region(world, player, active_locations, LocationName.chocolate_island_2_exit_1, + [LocationName.chocolate_island_2_exit_1]) + chocolate_island_2_exit_2 = create_region(world, player, active_locations, LocationName.chocolate_island_2_exit_2, + [LocationName.chocolate_island_2_exit_2]) + + chocolate_island_3_tile = create_region(world, player, active_locations, LocationName.chocolate_island_3_tile, None) + chocolate_island_3_region = create_region(world, player, active_locations, LocationName.chocolate_island_3_region, None) + chocolate_island_3_exit_1 = create_region(world, player, active_locations, LocationName.chocolate_island_3_exit_1, + [LocationName.chocolate_island_3_exit_1]) + chocolate_island_3_exit_2 = create_region(world, player, active_locations, LocationName.chocolate_island_3_exit_2, + [LocationName.chocolate_island_3_exit_2]) + + chocolate_island_4_tile = create_region(world, player, active_locations, LocationName.chocolate_island_4_tile, None) + chocolate_island_4_region = create_region(world, player, active_locations, LocationName.chocolate_island_4_region, None) + chocolate_island_4_exit_1 = create_region(world, player, active_locations, LocationName.chocolate_island_4_exit_1, + [LocationName.chocolate_island_4_exit_1]) + + chocolate_island_5_tile = create_region(world, player, active_locations, LocationName.chocolate_island_5_tile, None) + chocolate_island_5_region = create_region(world, player, active_locations, LocationName.chocolate_island_5_region, None) + chocolate_island_5_exit_1 = create_region(world, player, active_locations, LocationName.chocolate_island_5_exit_1, + [LocationName.chocolate_island_5_exit_1]) + + chocolate_ghost_house_tile = create_region(world, player, active_locations, LocationName.chocolate_ghost_house_tile, None) + chocolate_ghost_house_region = create_region(world, player, active_locations, LocationName.chocolate_ghost_house_region, None) + chocolate_ghost_house_exit_1 = create_region(world, player, active_locations, LocationName.chocolate_ghost_house_exit_1, + [LocationName.chocolate_ghost_house_exit_1]) + + chocolate_secret_tile = create_region(world, player, active_locations, LocationName.chocolate_secret_tile, None) + chocolate_secret_region = create_region(world, player, active_locations, LocationName.chocolate_secret_region, None) + chocolate_secret_exit_1 = create_region(world, player, active_locations, LocationName.chocolate_secret_exit_1, + [LocationName.chocolate_secret_exit_1]) + + chocolate_fortress_tile = create_region(world, player, active_locations, LocationName.chocolate_fortress_tile, None) + chocolate_fortress_region = create_region(world, player, active_locations, LocationName.chocolate_fortress_region, None) + chocolate_fortress = create_region(world, player, active_locations, LocationName.chocolate_fortress, + [LocationName.chocolate_fortress, LocationName.chocolate_reznor]) + + chocolate_castle_tile = create_region(world, player, active_locations, LocationName.chocolate_castle_tile, None) + chocolate_castle_region = create_region(world, player, active_locations, LocationName.chocolate_castle_region, None) + chocolate_castle = create_region(world, player, active_locations, LocationName.chocolate_castle, + [LocationName.chocolate_castle, LocationName.chocolate_koopaling]) + + sunken_ghost_ship_tile = create_region(world, player, active_locations, LocationName.sunken_ghost_ship_tile, None) + sunken_ghost_ship_region = create_region(world, player, active_locations, LocationName.sunken_ghost_ship_region, None) + sunken_ghost_ship = create_region(world, player, active_locations, LocationName.sunken_ghost_ship, + [LocationName.sunken_ghost_ship]) + + + valley_of_bowser_1_tile = create_region(world, player, active_locations, LocationName.valley_of_bowser_1_tile, None) + valley_of_bowser_1_region = create_region(world, player, active_locations, LocationName.valley_of_bowser_1_region, None) + valley_of_bowser_1_exit_1 = create_region(world, player, active_locations, LocationName.valley_of_bowser_1_exit_1, + [LocationName.valley_of_bowser_1_exit_1]) + + valley_of_bowser_2_tile = create_region(world, player, active_locations, LocationName.valley_of_bowser_2_tile, None) + valley_of_bowser_2_region = create_region(world, player, active_locations, LocationName.valley_of_bowser_2_region, None) + valley_of_bowser_2_exit_1 = create_region(world, player, active_locations, LocationName.valley_of_bowser_2_exit_1, + [LocationName.valley_of_bowser_2_exit_1]) + valley_of_bowser_2_exit_2 = create_region(world, player, active_locations, LocationName.valley_of_bowser_2_exit_2, + [LocationName.valley_of_bowser_2_exit_2]) + + valley_of_bowser_3_tile = create_region(world, player, active_locations, LocationName.valley_of_bowser_3_tile, None) + valley_of_bowser_3_region = create_region(world, player, active_locations, LocationName.valley_of_bowser_3_region, None) + valley_of_bowser_3_exit_1 = create_region(world, player, active_locations, LocationName.valley_of_bowser_3_exit_1, + [LocationName.valley_of_bowser_3_exit_1]) + + valley_of_bowser_4_tile = create_region(world, player, active_locations, LocationName.valley_of_bowser_4_tile, None) + valley_of_bowser_4_region = create_region(world, player, active_locations, LocationName.valley_of_bowser_4_region, None) + valley_of_bowser_4_exit_1 = create_region(world, player, active_locations, LocationName.valley_of_bowser_4_exit_1, + [LocationName.valley_of_bowser_4_exit_1]) + valley_of_bowser_4_exit_2 = create_region(world, player, active_locations, LocationName.valley_of_bowser_4_exit_2, + [LocationName.valley_of_bowser_4_exit_2]) + + valley_ghost_house_tile = create_region(world, player, active_locations, LocationName.valley_ghost_house_tile, None) + valley_ghost_house_region = create_region(world, player, active_locations, LocationName.valley_ghost_house_region, None) + valley_ghost_house_exit_1 = create_region(world, player, active_locations, LocationName.valley_ghost_house_exit_1, + [LocationName.valley_ghost_house_exit_1]) + valley_ghost_house_exit_2 = create_region(world, player, active_locations, LocationName.valley_ghost_house_exit_2, + [LocationName.valley_ghost_house_exit_2]) + + valley_fortress_tile = create_region(world, player, active_locations, LocationName.valley_fortress_tile, None) + valley_fortress_region = create_region(world, player, active_locations, LocationName.valley_fortress_region, None) + valley_fortress = create_region(world, player, active_locations, LocationName.valley_fortress, + [LocationName.valley_fortress, LocationName.valley_reznor]) + + valley_castle_tile = create_region(world, player, active_locations, LocationName.valley_castle_tile, None) + valley_castle_region = create_region(world, player, active_locations, LocationName.valley_castle_region, None) + valley_castle = create_region(world, player, active_locations, LocationName.valley_castle, + [LocationName.valley_castle, LocationName.valley_koopaling]) + + front_door_tile = create_region(world, player, active_locations, LocationName.front_door_tile, None) + front_door_region = create_region(world, player, active_locations, LocationName.front_door, None) + back_door_tile = create_region(world, player, active_locations, LocationName.back_door_tile, None) + back_door_region = create_region(world, player, active_locations, LocationName.back_door, None) + bowser_region_locations = [] + if world.goal[player] == "bowser": + bowser_region_locations += [LocationName.bowser] + bowser_region = create_region(world, player, active_locations, LocationName.bowser_region, bowser_region_locations) + + + donut_plains_star_road = create_region(world, player, active_locations, LocationName.donut_plains_star_road, None) + vanilla_dome_star_road = create_region(world, player, active_locations, LocationName.vanilla_dome_star_road, None) + twin_bridges_star_road = create_region(world, player, active_locations, LocationName.twin_bridges_star_road, None) + forest_star_road = create_region(world, player, active_locations, LocationName.forest_star_road, None) + valley_star_road = create_region(world, player, active_locations, LocationName.valley_star_road, None) + star_road_donut = create_region(world, player, active_locations, LocationName.star_road_donut, None) + star_road_vanilla = create_region(world, player, active_locations, LocationName.star_road_vanilla, None) + star_road_twin_bridges = create_region(world, player, active_locations, LocationName.star_road_twin_bridges, None) + star_road_forest = create_region(world, player, active_locations, LocationName.star_road_forest, None) + star_road_valley = create_region(world, player, active_locations, LocationName.star_road_valley, None) + star_road_special = create_region(world, player, active_locations, LocationName.star_road_special, None) + special_star_road = create_region(world, player, active_locations, LocationName.special_star_road, None) + + star_road_1_tile = create_region(world, player, active_locations, LocationName.star_road_1_tile, None) + star_road_1_region = create_region(world, player, active_locations, LocationName.star_road_1_region, None) + star_road_1_exit_1 = create_region(world, player, active_locations, LocationName.star_road_1_exit_1, + [LocationName.star_road_1_exit_1]) + star_road_1_exit_2 = create_region(world, player, active_locations, LocationName.star_road_1_exit_2, + [LocationName.star_road_1_exit_2]) + + star_road_2_tile = create_region(world, player, active_locations, LocationName.star_road_2_tile, None) + star_road_2_region = create_region(world, player, active_locations, LocationName.star_road_2_region, None) + star_road_2_exit_1 = create_region(world, player, active_locations, LocationName.star_road_2_exit_1, + [LocationName.star_road_2_exit_1]) + star_road_2_exit_2 = create_region(world, player, active_locations, LocationName.star_road_2_exit_2, + [LocationName.star_road_2_exit_2]) + + star_road_3_tile = create_region(world, player, active_locations, LocationName.star_road_3_tile, None) + star_road_3_region = create_region(world, player, active_locations, LocationName.star_road_3_region, None) + star_road_3_exit_1 = create_region(world, player, active_locations, LocationName.star_road_3_exit_1, + [LocationName.star_road_3_exit_1]) + star_road_3_exit_2 = create_region(world, player, active_locations, LocationName.star_road_3_exit_2, + [LocationName.star_road_3_exit_2]) + + star_road_4_tile = create_region(world, player, active_locations, LocationName.star_road_4_tile, None) + star_road_4_region = create_region(world, player, active_locations, LocationName.star_road_4_region, None) + star_road_4_exit_1 = create_region(world, player, active_locations, LocationName.star_road_4_exit_1, + [LocationName.star_road_4_exit_1]) + star_road_4_exit_2 = create_region(world, player, active_locations, LocationName.star_road_4_exit_2, + [LocationName.star_road_4_exit_2]) + + star_road_5_tile = create_region(world, player, active_locations, LocationName.star_road_5_tile, None) + star_road_5_region = create_region(world, player, active_locations, LocationName.star_road_5_region, None) + star_road_5_exit_1 = create_region(world, player, active_locations, LocationName.star_road_5_exit_1, + [LocationName.star_road_5_exit_1]) + star_road_5_exit_2 = create_region(world, player, active_locations, LocationName.star_road_5_exit_2, + [LocationName.star_road_5_exit_2]) + + + special_zone_1_tile = create_region(world, player, active_locations, LocationName.special_zone_1_tile, None) + special_zone_1_region = create_region(world, player, active_locations, LocationName.special_zone_1_region, None) + special_zone_1_exit_1 = create_region(world, player, active_locations, LocationName.special_zone_1_exit_1, + [LocationName.special_zone_1_exit_1]) + + special_zone_2_tile = create_region(world, player, active_locations, LocationName.special_zone_2_tile, None) + special_zone_2_region = create_region(world, player, active_locations, LocationName.special_zone_2_region, None) + special_zone_2_exit_1 = create_region(world, player, active_locations, LocationName.special_zone_2_exit_1, + [LocationName.special_zone_2_exit_1]) + + special_zone_3_tile = create_region(world, player, active_locations, LocationName.special_zone_3_tile, None) + special_zone_3_region = create_region(world, player, active_locations, LocationName.special_zone_3_region, None) + special_zone_3_exit_1 = create_region(world, player, active_locations, LocationName.special_zone_3_exit_1, + [LocationName.special_zone_3_exit_1]) + + special_zone_4_tile = create_region(world, player, active_locations, LocationName.special_zone_4_tile, None) + special_zone_4_region = create_region(world, player, active_locations, LocationName.special_zone_4_region, None) + special_zone_4_exit_1 = create_region(world, player, active_locations, LocationName.special_zone_4_exit_1, + [LocationName.special_zone_4_exit_1]) + + special_zone_5_tile = create_region(world, player, active_locations, LocationName.special_zone_5_tile, None) + special_zone_5_region = create_region(world, player, active_locations, LocationName.special_zone_5_region, None) + special_zone_5_exit_1 = create_region(world, player, active_locations, LocationName.special_zone_5_exit_1, + [LocationName.special_zone_5_exit_1]) + + special_zone_6_tile = create_region(world, player, active_locations, LocationName.special_zone_6_tile, None) + special_zone_6_region = create_region(world, player, active_locations, LocationName.special_zone_6_region, None) + special_zone_6_exit_1 = create_region(world, player, active_locations, LocationName.special_zone_6_exit_1, + [LocationName.special_zone_6_exit_1]) + + special_zone_7_tile = create_region(world, player, active_locations, LocationName.special_zone_7_tile, None) + special_zone_7_region = create_region(world, player, active_locations, LocationName.special_zone_7_region, None) + special_zone_7_exit_1 = create_region(world, player, active_locations, LocationName.special_zone_7_exit_1, + [LocationName.special_zone_7_exit_1]) + + special_zone_8_tile = create_region(world, player, active_locations, LocationName.special_zone_8_tile, None) + special_zone_8_region = create_region(world, player, active_locations, LocationName.special_zone_8_region, None) + special_zone_8_exit_1 = create_region(world, player, active_locations, LocationName.special_zone_8_exit_1, + [LocationName.special_zone_8_exit_1]) + special_complete = create_region(world, player, active_locations, LocationName.special_complete, None) + + + # Set up the regions correctly. + world.regions += [ + menu_region, + yoshis_island_region, + donut_plains_region, + vanilla_dome_region, + twin_bridges_region, + forest_of_illusion_region, + chocolate_island_region, + valley_of_bowser_region, + star_road_region, + special_zone_region, + yoshis_house_tile, + yoshis_house_region, + yoshis_island_1_tile, + yoshis_island_1_region, + yoshis_island_1_exit_1, + yoshis_island_2_tile, + yoshis_island_2_region, + yoshis_island_2_exit_1, + yoshis_island_3_tile, + yoshis_island_3_region, + yoshis_island_3_exit_1, + yoshis_island_4_tile, + yoshis_island_4_region, + yoshis_island_4_exit_1, + yoshis_island_castle_tile, + yoshis_island_castle_region, + yoshis_island_castle, + yellow_switch_palace_tile, + yellow_switch_palace, + donut_plains_1_tile, + donut_plains_1_region, + donut_plains_1_exit_1, + donut_plains_1_exit_2, + donut_plains_2_tile, + donut_plains_2_region, + donut_plains_2_exit_1, + donut_plains_2_exit_2, + donut_plains_3_tile, + donut_plains_3_region, + donut_plains_3_exit_1, + donut_plains_4_tile, + donut_plains_4_region, + donut_plains_4_exit_1, + donut_secret_1_tile, + donut_secret_1_region, + donut_secret_1_exit_1, + donut_secret_1_exit_2, + donut_secret_2_tile, + donut_secret_2_region, + donut_secret_2_exit_1, + donut_ghost_house_tile, + donut_ghost_house_region, + donut_ghost_house_exit_1, + donut_ghost_house_exit_2, + donut_secret_house_tile, + donut_secret_house_region, + donut_secret_house_exit_1, + donut_secret_house_exit_2, + donut_plains_castle_tile, + donut_plains_castle_region, + donut_plains_castle, + green_switch_palace_tile, + green_switch_palace, + donut_plains_top_secret_tile, + donut_plains_top_secret, + vanilla_dome_1_tile, + vanilla_dome_1_region, + vanilla_dome_1_exit_1, + vanilla_dome_1_exit_2, + vanilla_dome_2_tile, + vanilla_dome_2_region, + vanilla_dome_2_exit_1, + vanilla_dome_2_exit_2, + vanilla_dome_3_tile, + vanilla_dome_3_region, + vanilla_dome_3_exit_1, + vanilla_dome_4_tile, + vanilla_dome_4_region, + vanilla_dome_4_exit_1, + vanilla_secret_1_tile, + vanilla_secret_1_region, + vanilla_secret_1_exit_1, + vanilla_secret_1_exit_2, + vanilla_secret_2_tile, + vanilla_secret_2_region, + vanilla_secret_2_exit_1, + vanilla_secret_3_tile, + vanilla_secret_3_region, + vanilla_secret_3_exit_1, + vanilla_ghost_house_tile, + vanilla_ghost_house_region, + vanilla_ghost_house_exit_1, + vanilla_fortress_tile, + vanilla_fortress_region, + vanilla_fortress, + vanilla_dome_castle_tile, + vanilla_dome_castle_region, + vanilla_dome_castle, + red_switch_palace_tile, + red_switch_palace, + butter_bridge_1_tile, + butter_bridge_1_region, + butter_bridge_1_exit_1, + butter_bridge_2_tile, + butter_bridge_2_region, + butter_bridge_2_exit_1, + cheese_bridge_tile, + cheese_bridge_region, + cheese_bridge_exit_1, + cheese_bridge_exit_2, + cookie_mountain_tile, + cookie_mountain_region, + cookie_mountain_exit_1, + soda_lake_tile, + soda_lake_region, + soda_lake_exit_1, + twin_bridges_castle_tile, + twin_bridges_castle_region, + twin_bridges_castle, + forest_of_illusion_1_tile, + forest_of_illusion_1_region, + forest_of_illusion_1_exit_1, + forest_of_illusion_1_exit_2, + forest_of_illusion_2_tile, + forest_of_illusion_2_region, + forest_of_illusion_2_exit_1, + forest_of_illusion_2_exit_2, + forest_of_illusion_3_tile, + forest_of_illusion_3_region, + forest_of_illusion_3_exit_1, + forest_of_illusion_3_exit_2, + forest_of_illusion_4_tile, + forest_of_illusion_4_region, + forest_of_illusion_4_exit_1, + forest_of_illusion_4_exit_2, + forest_ghost_house_tile, + forest_ghost_house_region, + forest_ghost_house_exit_1, + forest_ghost_house_exit_2, + forest_secret_tile, + forest_secret_region, + forest_secret_exit_1, + forest_fortress_tile, + forest_fortress_region, + forest_fortress, + forest_castle_tile, + forest_castle_region, + forest_castle, + blue_switch_palace_tile, + blue_switch_palace, + chocolate_island_1_tile, + chocolate_island_1_region, + chocolate_island_1_exit_1, + chocolate_island_2_tile, + chocolate_island_2_region, + chocolate_island_2_exit_1, + chocolate_island_2_exit_2, + chocolate_island_3_tile, + chocolate_island_3_region, + chocolate_island_3_exit_1, + chocolate_island_3_exit_2, + chocolate_island_4_tile, + chocolate_island_4_region, + chocolate_island_4_exit_1, + chocolate_island_5_tile, + chocolate_island_5_region, + chocolate_island_5_exit_1, + chocolate_ghost_house_tile, + chocolate_ghost_house_region, + chocolate_ghost_house_exit_1, + chocolate_secret_tile, + chocolate_secret_region, + chocolate_secret_exit_1, + chocolate_fortress_tile, + chocolate_fortress_region, + chocolate_fortress, + chocolate_castle_tile, + chocolate_castle_region, + chocolate_castle, + sunken_ghost_ship_tile, + sunken_ghost_ship_region, + sunken_ghost_ship, + valley_of_bowser_1_tile, + valley_of_bowser_1_region, + valley_of_bowser_1_exit_1, + valley_of_bowser_2_tile, + valley_of_bowser_2_region, + valley_of_bowser_2_exit_1, + valley_of_bowser_2_exit_2, + valley_of_bowser_3_tile, + valley_of_bowser_3_region, + valley_of_bowser_3_exit_1, + valley_of_bowser_4_tile, + valley_of_bowser_4_region, + valley_of_bowser_4_exit_1, + valley_of_bowser_4_exit_2, + valley_ghost_house_tile, + valley_ghost_house_region, + valley_ghost_house_exit_1, + valley_ghost_house_exit_2, + valley_fortress_tile, + valley_fortress_region, + valley_fortress, + valley_castle_tile, + valley_castle_region, + valley_castle, + front_door_tile, + front_door_region, + back_door_tile, + back_door_region, + bowser_region, + donut_plains_star_road, + vanilla_dome_star_road, + twin_bridges_star_road, + forest_star_road, + valley_star_road, + star_road_donut, + star_road_vanilla, + star_road_twin_bridges, + star_road_forest, + star_road_valley, + star_road_special, + special_star_road, + star_road_1_tile, + star_road_1_region, + star_road_1_exit_1, + star_road_1_exit_2, + star_road_2_tile, + star_road_2_region, + star_road_2_exit_1, + star_road_2_exit_2, + star_road_3_tile, + star_road_3_region, + star_road_3_exit_1, + star_road_3_exit_2, + star_road_4_tile, + star_road_4_region, + star_road_4_exit_1, + star_road_4_exit_2, + star_road_5_tile, + star_road_5_region, + star_road_5_exit_1, + star_road_5_exit_2, + special_zone_1_tile, + special_zone_1_region, + special_zone_1_exit_1, + special_zone_2_tile, + special_zone_2_region, + special_zone_2_exit_1, + special_zone_3_tile, + special_zone_3_region, + special_zone_3_exit_1, + special_zone_4_tile, + special_zone_4_region, + special_zone_4_exit_1, + special_zone_5_tile, + special_zone_5_region, + special_zone_5_exit_1, + special_zone_6_tile, + special_zone_6_region, + special_zone_6_exit_1, + special_zone_7_tile, + special_zone_7_region, + special_zone_7_exit_1, + special_zone_8_tile, + special_zone_8_region, + special_zone_8_exit_1, + special_complete, + ] + + + if world.dragon_coin_checks[player]: + add_location_to_region(world, player, active_locations, LocationName.yoshis_island_1_region, LocationName.yoshis_island_1_dragon, + lambda state: (state.has(ItemName.mario_spin_jump, player) and + state.has(ItemName.progressive_powerup, player, 1))) + add_location_to_region(world, player, active_locations, LocationName.yoshis_island_2_region, LocationName.yoshis_island_2_dragon, + lambda state: (state.has(ItemName.yoshi_activate, player) or + state.has(ItemName.mario_climb, player))) + add_location_to_region(world, player, active_locations, LocationName.yoshis_island_3_region, LocationName.yoshis_island_3_dragon, + lambda state: state.has(ItemName.p_switch, player)) + add_location_to_region(world, player, active_locations, LocationName.yoshis_island_4_region, LocationName.yoshis_island_4_dragon, + lambda state: (state.has(ItemName.yoshi_activate, player) or + state.has(ItemName.mario_swim, player) or + (state.has(ItemName.mario_carry, player) and state.has(ItemName.p_switch, player)))) + add_location_to_region(world, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_dragon, + lambda state: (state.has(ItemName.mario_climb, player) or + state.has(ItemName.yoshi_activate, player) or + (state.has(ItemName.progressive_powerup, player, 3) and state.has(ItemName.mario_run, player)))) + add_location_to_region(world, player, active_locations, LocationName.donut_plains_2_region, LocationName.donut_plains_2_dragon) + add_location_to_region(world, player, active_locations, LocationName.donut_plains_3_region, LocationName.donut_plains_3_dragon, + lambda state: ((state.has(ItemName.mario_spin_jump, player) and state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_climb, player) or + state.has(ItemName.yoshi_activate, player) or + (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3))))) + add_location_to_region(world, player, active_locations, LocationName.donut_plains_4_region, LocationName.donut_plains_4_dragon) + add_location_to_region(world, player, active_locations, LocationName.donut_secret_1_region, LocationName.donut_secret_1_dragon, + lambda state: state.has(ItemName.mario_swim, player)) + add_location_to_region(world, player, active_locations, LocationName.donut_secret_2_region, LocationName.donut_secret_2_dragon, + lambda state: (state.has(ItemName.mario_climb, player) or state.has(ItemName.yoshi_activate, player))) + add_location_to_region(world, player, active_locations, LocationName.vanilla_dome_1_region, LocationName.vanilla_dome_1_dragon, + lambda state: (state.has(ItemName.mario_carry, player) and + state.has(ItemName.mario_run, player) and + (state.has(ItemName.super_star_active, player) or + state.has(ItemName.progressive_powerup, player, 1)))) + add_location_to_region(world, player, active_locations, LocationName.vanilla_dome_2_region, LocationName.vanilla_dome_2_dragon, + lambda state: (state.has(ItemName.mario_swim, player) and + state.has(ItemName.p_switch, player) and + (state.has(ItemName.mario_climb, player) or state.has(ItemName.yoshi_activate, player)))) + add_location_to_region(world, player, active_locations, LocationName.vanilla_dome_3_region, LocationName.vanilla_dome_3_dragon) + add_location_to_region(world, player, active_locations, LocationName.vanilla_dome_4_region, LocationName.vanilla_dome_4_dragon) + add_location_to_region(world, player, active_locations, LocationName.vanilla_secret_1_region, LocationName.vanilla_secret_1_dragon, + lambda state: (state.has(ItemName.mario_climb, player) and + state.has(ItemName.mario_carry, player))) + add_location_to_region(world, player, active_locations, LocationName.vanilla_secret_2_region, LocationName.vanilla_secret_2_dragon, + lambda state: (state.has(ItemName.mario_run, player) and + state.has(ItemName.progressive_powerup, player, 3))) + add_location_to_region(world, player, active_locations, LocationName.vanilla_secret_3_region, LocationName.vanilla_secret_3_dragon, + lambda state: state.has(ItemName.mario_swim, player)) + add_location_to_region(world, player, active_locations, LocationName.vanilla_ghost_house_region, LocationName.vanilla_ghost_house_dragon, + lambda state: state.has(ItemName.mario_climb, player)) + add_location_to_region(world, player, active_locations, LocationName.butter_bridge_1_region, LocationName.butter_bridge_1_dragon) + add_location_to_region(world, player, active_locations, LocationName.butter_bridge_2_region, LocationName.butter_bridge_2_dragon, + lambda state: (state.has(ItemName.yoshi_activate, player) or + state.has(ItemName.progressive_powerup, player, 3))) + add_location_to_region(world, player, active_locations, LocationName.cheese_bridge_region, LocationName.cheese_bridge_dragon, + lambda state: (state.has(ItemName.yoshi_activate, player) or + state.has(ItemName.mario_climb, player))) + add_location_to_region(world, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_dragon, + lambda state: (state.has(ItemName.yoshi_activate, player) or + state.has(ItemName.mario_climb, player))) + add_location_to_region(world, player, active_locations, LocationName.soda_lake_region, LocationName.soda_lake_dragon, + lambda state: state.has(ItemName.mario_swim, player)) + add_location_to_region(world, player, active_locations, LocationName.forest_of_illusion_2_region, LocationName.forest_of_illusion_2_dragon, + lambda state: state.has(ItemName.mario_swim, player)) + add_location_to_region(world, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_dragon, + lambda state: (state.has(ItemName.yoshi_activate, player) or + state.has(ItemName.mario_carry, player))) + add_location_to_region(world, player, active_locations, LocationName.forest_of_illusion_4_region, LocationName.forest_of_illusion_4_dragon, + lambda state: (state.has(ItemName.yoshi_activate, player) or + state.has(ItemName.mario_carry, player) or + state.has(ItemName.p_switch, player) or + state.has(ItemName.progressive_powerup, player, 2))) + add_location_to_region(world, player, active_locations, LocationName.forest_ghost_house_region, LocationName.forest_ghost_house_dragon, + lambda state: state.has(ItemName.p_switch, player)) + add_location_to_region(world, player, active_locations, LocationName.forest_secret_region, LocationName.forest_secret_dragon) + add_location_to_region(world, player, active_locations, LocationName.forest_castle_region, LocationName.forest_castle_dragon) + add_location_to_region(world, player, active_locations, LocationName.chocolate_island_1_region, LocationName.chocolate_island_1_dragon, + lambda state: state.has(ItemName.mario_swim, player)) + add_location_to_region(world, player, active_locations, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_dragon, + lambda state: (state.has(ItemName.blue_switch_palace, player) and + (state.has(ItemName.p_switch, player) or + state.has(ItemName.green_switch_palace, player) or + (state.has(ItemName.yellow_switch_palace, player) or state.has(ItemName.red_switch_palace, player))))) + add_location_to_region(world, player, active_locations, LocationName.chocolate_island_3_region, LocationName.chocolate_island_3_dragon) + add_location_to_region(world, player, active_locations, LocationName.chocolate_island_4_region, LocationName.chocolate_island_4_dragon, + lambda state: (state.has(ItemName.mario_run, player) and + state.has(ItemName.progressive_powerup, player, 3))) + add_location_to_region(world, player, active_locations, LocationName.chocolate_island_5_region, LocationName.chocolate_island_5_dragon, + lambda state: (state.has(ItemName.mario_swim, player) or + (state.has(ItemName.mario_carry, player) and state.has(ItemName.p_switch, player)))) + add_location_to_region(world, player, active_locations, LocationName.sunken_ghost_ship_region, LocationName.sunken_ghost_ship_dragon, + lambda state: (state.has(ItemName.mario_swim, player) and + state.has(ItemName.super_star_active, player) and + state.has(ItemName.progressive_powerup, player, 3))) + add_location_to_region(world, player, active_locations, LocationName.valley_of_bowser_1_region, LocationName.valley_of_bowser_1_dragon) + add_location_to_region(world, player, active_locations, LocationName.valley_of_bowser_2_region, LocationName.valley_of_bowser_2_dragon) + add_location_to_region(world, player, active_locations, LocationName.valley_of_bowser_3_region, LocationName.valley_of_bowser_3_dragon) + add_location_to_region(world, player, active_locations, LocationName.valley_ghost_house_region, LocationName.valley_ghost_house_dragon, + lambda state: state.has(ItemName.p_switch, player)) + add_location_to_region(world, player, active_locations, LocationName.valley_castle_region, LocationName.valley_castle_dragon) + add_location_to_region(world, player, active_locations, LocationName.star_road_1_region, LocationName.star_road_1_dragon, + lambda state: (state.has(ItemName.mario_spin_jump, player) and + state.has(ItemName.progressive_powerup, player, 1))) + add_location_to_region(world, player, active_locations, LocationName.special_zone_1_region, LocationName.special_zone_1_dragon, + lambda state: state.has(ItemName.mario_climb, player)) + add_location_to_region(world, player, active_locations, LocationName.special_zone_2_region, LocationName.special_zone_2_dragon, + lambda state: state.has(ItemName.p_balloon, player)) + add_location_to_region(world, player, active_locations, LocationName.special_zone_3_region, LocationName.special_zone_3_dragon, + lambda state: state.has(ItemName.yoshi_activate, player)) + add_location_to_region(world, player, active_locations, LocationName.special_zone_4_region, LocationName.special_zone_4_dragon, + lambda state: state.has(ItemName.progressive_powerup, player, 1)) + add_location_to_region(world, player, active_locations, LocationName.special_zone_5_region, LocationName.special_zone_5_dragon, + lambda state: state.has(ItemName.progressive_powerup, player, 1)) + add_location_to_region(world, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_dragon, + lambda state: state.has(ItemName.mario_swim, player)) + add_location_to_region(world, player, active_locations, LocationName.special_zone_7_region, LocationName.special_zone_7_dragon, + lambda state: state.has(ItemName.progressive_powerup, player, 1)) + add_location_to_region(world, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_dragon, + lambda state: state.has(ItemName.progressive_powerup, player, 1)) + + + +def connect_regions(world, player, level_to_tile_dict): + names: typing.Dict[str, int] = {} + + connect(world, player, names, "Menu", LocationName.yoshis_island_region) + connect(world, player, names, LocationName.yoshis_island_region, LocationName.yoshis_island_1_tile) + connect(world, player, names, LocationName.yoshis_island_region, LocationName.yoshis_island_2_tile) + + # Connect regions within levels using rules + connect(world, player, names, LocationName.yoshis_island_1_region, LocationName.yoshis_island_1_exit_1) + connect(world, player, names, LocationName.yoshis_island_2_region, LocationName.yoshis_island_2_exit_1) + connect(world, player, names, LocationName.yoshis_island_3_region, LocationName.yoshis_island_3_exit_1) + connect(world, player, names, LocationName.yoshis_island_4_region, LocationName.yoshis_island_4_exit_1) + connect(world, player, names, LocationName.yoshis_island_castle_region, LocationName.yoshis_island_castle, + lambda state: (state.has(ItemName.mario_climb, player))) + + connect(world, player, names, LocationName.donut_plains_1_region, LocationName.donut_plains_1_exit_1) + connect(world, player, names, LocationName.donut_plains_1_region, LocationName.donut_plains_1_exit_2, + lambda state: (state.has(ItemName.mario_carry, player) and + (state.has(ItemName.yoshi_activate, player) or + state.has(ItemName.green_switch_palace, player) or + (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3))))) + connect(world, player, names, LocationName.donut_plains_2_region, LocationName.donut_plains_2_exit_1) + connect(world, player, names, LocationName.donut_plains_2_region, LocationName.donut_plains_2_exit_2, + lambda state: (state.has(ItemName.mario_carry, player) and + (state.has(ItemName.yoshi_activate, player) or + (state.has(ItemName.mario_spin_jump, player) and state.has(ItemName.mario_climb, player) and state.has(ItemName.progressive_powerup, player, 1))))) + connect(world, player, names, LocationName.donut_secret_1_region, LocationName.donut_secret_1_exit_1, + lambda state: state.has(ItemName.mario_swim, player)) + connect(world, player, names, LocationName.donut_secret_1_region, LocationName.donut_secret_1_exit_2, + lambda state: (state.has(ItemName.mario_carry, player) and + state.has(ItemName.mario_swim, player) and + state.has(ItemName.p_switch, player))) + connect(world, player, names, LocationName.donut_ghost_house_region, LocationName.donut_ghost_house_exit_1, + lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3))) + connect(world, player, names, LocationName.donut_ghost_house_region, LocationName.donut_ghost_house_exit_2, + lambda state: (state.has(ItemName.mario_climb, player) or + (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))) + connect(world, player, names, LocationName.donut_secret_house_region, LocationName.donut_secret_house_exit_1, + lambda state: state.has(ItemName.p_switch, player)) + connect(world, player, names, LocationName.donut_secret_house_region, LocationName.donut_secret_house_exit_2, + lambda state: (state.has(ItemName.p_switch, player) and state.has(ItemName.mario_carry, player) and + (state.has(ItemName.mario_climb, player) or + (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3))))) + connect(world, player, names, LocationName.donut_plains_3_region, LocationName.donut_plains_3_exit_1) + connect(world, player, names, LocationName.donut_plains_4_region, LocationName.donut_plains_4_exit_1) + connect(world, player, names, LocationName.donut_secret_2_region, LocationName.donut_secret_2_exit_1) + connect(world, player, names, LocationName.donut_plains_castle_region, LocationName.donut_plains_castle) + + connect(world, player, names, LocationName.vanilla_dome_1_region, LocationName.vanilla_dome_1_exit_1, + lambda state: (state.has(ItemName.mario_run, player) and + (state.has(ItemName.super_star_active, player) or + state.has(ItemName.progressive_powerup, player, 1)))) + connect(world, player, names, LocationName.vanilla_dome_1_region, LocationName.vanilla_dome_1_exit_2, + lambda state: (state.has(ItemName.mario_carry, player) and + ((state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_climb, player)) or + (state.has(ItemName.yoshi_activate, player) and state.has(ItemName.red_switch_palace, player)) or + (state.has(ItemName.red_switch_palace, player) and state.has(ItemName.mario_climb, player))))) + connect(world, player, names, LocationName.vanilla_dome_2_region, LocationName.vanilla_dome_2_exit_1, + lambda state: (state.has(ItemName.mario_swim, player) and + (state.has(ItemName.mario_climb, player) or state.has(ItemName.yoshi_activate, player)))) + connect(world, player, names, LocationName.vanilla_dome_2_region, LocationName.vanilla_dome_2_exit_2, + lambda state: (state.has(ItemName.mario_swim, player) and + state.has(ItemName.p_switch, player) and + state.has(ItemName.mario_carry, player) and + (state.has(ItemName.mario_climb, player) or state.has(ItemName.yoshi_activate, player)))) + connect(world, player, names, LocationName.vanilla_secret_1_region, LocationName.vanilla_secret_1_exit_1, + lambda state: state.has(ItemName.mario_climb, player)) + connect(world, player, names, LocationName.vanilla_secret_1_region, LocationName.vanilla_secret_1_exit_2, + lambda state: (state.has(ItemName.mario_climb, player) and + (state.has(ItemName.mario_carry, player) and state.has(ItemName.blue_switch_palace, player)))) + connect(world, player, names, LocationName.vanilla_ghost_house_region, LocationName.vanilla_ghost_house_exit_1, + lambda state: state.has(ItemName.p_switch, player)) + connect(world, player, names, LocationName.vanilla_dome_3_region, LocationName.vanilla_dome_3_exit_1) + connect(world, player, names, LocationName.vanilla_dome_4_region, LocationName.vanilla_dome_4_exit_1) + connect(world, player, names, LocationName.vanilla_secret_2_region, LocationName.vanilla_secret_2_exit_1) + connect(world, player, names, LocationName.vanilla_secret_3_region, LocationName.vanilla_secret_3_exit_1, + lambda state: state.has(ItemName.mario_swim, player)) + connect(world, player, names, LocationName.vanilla_fortress_region, LocationName.vanilla_fortress, + lambda state: state.has(ItemName.mario_swim, player)) + connect(world, player, names, LocationName.vanilla_dome_castle_region, LocationName.vanilla_dome_castle) + + connect(world, player, names, LocationName.butter_bridge_1_region, LocationName.butter_bridge_1_exit_1) + connect(world, player, names, LocationName.butter_bridge_2_region, LocationName.butter_bridge_2_exit_1) + connect(world, player, names, LocationName.cheese_bridge_region, LocationName.cheese_bridge_exit_1, + lambda state: state.has(ItemName.mario_climb, player)) + connect(world, player, names, LocationName.cheese_bridge_region, LocationName.cheese_bridge_exit_2, + lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3))) + connect(world, player, names, LocationName.soda_lake_region, LocationName.soda_lake_exit_1, + lambda state: state.has(ItemName.mario_swim, player)) + connect(world, player, names, LocationName.cookie_mountain_region, LocationName.cookie_mountain_exit_1) + connect(world, player, names, LocationName.twin_bridges_castle_region, LocationName.twin_bridges_castle, + lambda state: (state.has(ItemName.mario_run, player) and + state.has(ItemName.mario_climb, player))) + + connect(world, player, names, LocationName.forest_of_illusion_1_region, LocationName.forest_of_illusion_1_exit_1) + connect(world, player, names, LocationName.forest_of_illusion_1_region, LocationName.forest_of_illusion_1_exit_2, + lambda state: (state.has(ItemName.mario_carry, player) and + state.has(ItemName.p_balloon, player))) + connect(world, player, names, LocationName.forest_of_illusion_2_region, LocationName.forest_of_illusion_2_exit_1, + lambda state: state.has(ItemName.mario_swim, player)) + connect(world, player, names, LocationName.forest_of_illusion_2_region, LocationName.forest_of_illusion_2_exit_2, + lambda state: (state.has(ItemName.mario_swim, player) and + state.has(ItemName.mario_carry, player))) + connect(world, player, names, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_exit_1, + lambda state: (state.has(ItemName.mario_carry, player) or + state.has(ItemName.yoshi_activate, player))) + connect(world, player, names, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_exit_2, + lambda state: (state.has(ItemName.mario_swim, player) and + state.has(ItemName.mario_carry, player) and + state.has(ItemName.progressive_powerup, player, 1))) + connect(world, player, names, LocationName.forest_of_illusion_4_region, LocationName.forest_of_illusion_4_exit_1) + connect(world, player, names, LocationName.forest_of_illusion_4_region, LocationName.forest_of_illusion_4_exit_2, + lambda state: state.has(ItemName.mario_carry, player)) + connect(world, player, names, LocationName.forest_ghost_house_region, LocationName.forest_ghost_house_exit_1, + lambda state: state.has(ItemName.p_switch, player)) + connect(world, player, names, LocationName.forest_ghost_house_region, LocationName.forest_ghost_house_exit_2, + lambda state: state.has(ItemName.p_switch, player)) + connect(world, player, names, LocationName.forest_secret_region, LocationName.forest_secret_exit_1) + connect(world, player, names, LocationName.forest_fortress_region, LocationName.forest_fortress) + connect(world, player, names, LocationName.forest_castle_region, LocationName.forest_castle) + + connect(world, player, names, LocationName.chocolate_island_1_region, LocationName.chocolate_island_1_exit_1, + lambda state: state.has(ItemName.p_switch, player)) + connect(world, player, names, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_exit_1) + connect(world, player, names, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_exit_2, + lambda state: state.has(ItemName.mario_carry, player)) + connect(world, player, names, LocationName.chocolate_island_3_region, LocationName.chocolate_island_3_exit_1, + lambda state: (state.has(ItemName.mario_climb, player) or + (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))) + connect(world, player, names, LocationName.chocolate_island_3_region, LocationName.chocolate_island_3_exit_2, + lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3))) + connect(world, player, names, LocationName.chocolate_island_4_region, LocationName.chocolate_island_4_exit_1) + connect(world, player, names, LocationName.chocolate_island_5_region, LocationName.chocolate_island_5_exit_1) + connect(world, player, names, LocationName.chocolate_ghost_house_region, LocationName.chocolate_ghost_house_exit_1) + connect(world, player, names, LocationName.chocolate_fortress_region, LocationName.chocolate_fortress) + connect(world, player, names, LocationName.chocolate_secret_region, LocationName.chocolate_secret_exit_1, + lambda state: state.has(ItemName.mario_run, player)) + connect(world, player, names, LocationName.chocolate_castle_region, LocationName.chocolate_castle, + lambda state: (state.has(ItemName.progressive_powerup, player, 1))) + + connect(world, player, names, LocationName.sunken_ghost_ship_region, LocationName.sunken_ghost_ship, + lambda state: state.has(ItemName.mario_swim, player)) + connect(world, player, names, LocationName.valley_of_bowser_1_region, LocationName.valley_of_bowser_1_exit_1) + connect(world, player, names, LocationName.valley_of_bowser_2_region, LocationName.valley_of_bowser_2_exit_1) + connect(world, player, names, LocationName.valley_of_bowser_2_region, LocationName.valley_of_bowser_2_exit_2, + lambda state: state.has(ItemName.mario_carry, player)) + connect(world, player, names, LocationName.valley_of_bowser_3_region, LocationName.valley_of_bowser_3_exit_1) + connect(world, player, names, LocationName.valley_of_bowser_4_region, LocationName.valley_of_bowser_4_exit_1, + lambda state: state.has(ItemName.mario_climb, player)) + connect(world, player, names, LocationName.valley_of_bowser_4_region, LocationName.valley_of_bowser_4_exit_2, + lambda state: (state.has(ItemName.mario_climb, player) and + state.has(ItemName.mario_carry, player) and + state.has(ItemName.yoshi_activate, player))) + connect(world, player, names, LocationName.valley_ghost_house_region, LocationName.valley_ghost_house_exit_1, + lambda state: state.has(ItemName.p_switch, player)) + connect(world, player, names, LocationName.valley_ghost_house_region, LocationName.valley_ghost_house_exit_2, + lambda state: (state.has(ItemName.p_switch, player) and + state.has(ItemName.mario_carry, player) and + state.has(ItemName.mario_run, player))) + connect(world, player, names, LocationName.valley_fortress_region, LocationName.valley_fortress, + lambda state: state.has(ItemName.progressive_powerup, player, 1)) + connect(world, player, names, LocationName.valley_castle_region, LocationName.valley_castle) + connect(world, player, names, LocationName.front_door, LocationName.bowser_region, + lambda state: (state.has(ItemName.mario_climb, player) and + state.has(ItemName.mario_run, player) and + state.has(ItemName.mario_swim, player) and + state.has(ItemName.progressive_powerup, player, 1) and + state.has(ItemName.koopaling, player, world.bosses_required[player].value))) + connect(world, player, names, LocationName.back_door, LocationName.bowser_region, + lambda state: state.has(ItemName.koopaling, player, world.bosses_required[player].value)) + + connect(world, player, names, LocationName.star_road_1_region, LocationName.star_road_1_exit_1, + lambda state: (state.has(ItemName.mario_spin_jump, player) and + state.has(ItemName.progressive_powerup, player, 1))) + connect(world, player, names, LocationName.star_road_1_region, LocationName.star_road_1_exit_2, + lambda state: (state.has(ItemName.mario_spin_jump, player) and + state.has(ItemName.mario_carry, player) and + state.has(ItemName.progressive_powerup, player, 1))) + connect(world, player, names, LocationName.star_road_2_region, LocationName.star_road_2_exit_1, + lambda state: state.has(ItemName.mario_swim, player)) + connect(world, player, names, LocationName.star_road_2_region, LocationName.star_road_2_exit_2, + lambda state: (state.has(ItemName.mario_swim, player) and + state.has(ItemName.mario_carry, player))) + connect(world, player, names, LocationName.star_road_3_region, LocationName.star_road_3_exit_1) + connect(world, player, names, LocationName.star_road_3_region, LocationName.star_road_3_exit_2, + lambda state: state.has(ItemName.mario_carry, player)) + connect(world, player, names, LocationName.star_road_4_region, LocationName.star_road_4_exit_1) + connect(world, player, names, LocationName.star_road_4_region, LocationName.star_road_4_exit_2, + lambda state: (state.has(ItemName.mario_carry, player) and + (state.has(ItemName.yoshi_activate, player) or + (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.red_switch_palace, player))))) + connect(world, player, names, LocationName.star_road_5_region, LocationName.star_road_5_exit_1, + lambda state: state.has(ItemName.p_switch, player)) + connect(world, player, names, LocationName.star_road_5_region, LocationName.star_road_5_exit_2, + lambda state: (state.has(ItemName.mario_carry, player) and + state.has(ItemName.mario_climb, player) and + state.has(ItemName.p_switch, player) and + state.has(ItemName.yellow_switch_palace, player) and + state.has(ItemName.green_switch_palace, player) and + state.has(ItemName.red_switch_palace, player) and + state.has(ItemName.blue_switch_palace, player))) + + connect(world, player, names, LocationName.special_zone_1_region, LocationName.special_zone_1_exit_1, + lambda state: (state.has(ItemName.mario_climb, player) and + (state.has(ItemName.p_switch, player) or + (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3))))) + connect(world, player, names, LocationName.special_zone_2_region, LocationName.special_zone_2_exit_1, + lambda state: state.has(ItemName.p_balloon, player)) + connect(world, player, names, LocationName.special_zone_3_region, LocationName.special_zone_3_exit_1, + lambda state: (state.has(ItemName.mario_climb, player) or + state.has(ItemName.p_switch, player) or + (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))) + connect(world, player, names, LocationName.special_zone_4_region, LocationName.special_zone_4_exit_1, + lambda state: state.has(ItemName.progressive_powerup, player, 1)) + connect(world, player, names, LocationName.special_zone_5_region, LocationName.special_zone_5_exit_1, + lambda state: state.has(ItemName.progressive_powerup, player, 1)) + connect(world, player, names, LocationName.special_zone_6_region, LocationName.special_zone_6_exit_1, + lambda state: state.has(ItemName.mario_swim, player)) + connect(world, player, names, LocationName.special_zone_7_region, LocationName.special_zone_7_exit_1, + lambda state: state.has(ItemName.progressive_powerup, player, 1)) + connect(world, player, names, LocationName.special_zone_8_region, LocationName.special_zone_8_exit_1, + lambda state: state.has(ItemName.progressive_powerup, player, 1)) + + + + # Connect levels to each other + for current_level_id, current_level_data in level_info_dict.items(): + # Connect tile regions to correct level regions + + if current_level_id not in level_to_tile_dict.keys(): + continue + + current_tile_id = level_to_tile_dict[current_level_id] + current_tile_data = level_info_dict[current_tile_id] + current_tile_name = current_tile_data.levelName + if ("Star Road - " not in current_tile_name) and (" - Star Road" not in current_tile_name): + current_tile_name += " - Tile" + connect(world, player, names, current_tile_name, current_level_data.levelName) + # Connect Exit regions to next tile regions + if current_tile_data.exit1Path: + next_tile_id = current_tile_data.exit1Path.otherLevelID + if world.swap_donut_gh_exits[player] and current_tile_id == 0x04: + next_tile_id = current_tile_data.exit2Path.otherLevelID + next_tile_name = level_info_dict[next_tile_id].levelName + if ("Star Road - " not in next_tile_name) and (" - Star Road" not in next_tile_name): + next_tile_name += " - Tile" + current_exit_name = (current_level_data.levelName + " - Normal Exit") + connect(world, player, names, current_exit_name, next_tile_name) + if current_tile_data.exit2Path: + next_tile_id = current_tile_data.exit2Path.otherLevelID + if world.swap_donut_gh_exits[player] and current_tile_id == 0x04: + next_tile_id = current_tile_data.exit1Path.otherLevelID + next_tile_name = level_info_dict[next_tile_id].levelName + if ("Star Road - " not in next_tile_name) and (" - Star Road" not in next_tile_name): + next_tile_name += " - Tile" + current_exit_name = (current_level_data.levelName + " - Secret Exit") + connect(world, player, names, current_exit_name, next_tile_name) + + connect(world, player, names, LocationName.donut_plains_star_road, LocationName.star_road_donut) + connect(world, player, names, LocationName.star_road_donut, LocationName.donut_plains_star_road) + connect(world, player, names, LocationName.star_road_donut, LocationName.star_road_1_tile) + connect(world, player, names, LocationName.vanilla_dome_star_road, LocationName.star_road_vanilla) + connect(world, player, names, LocationName.star_road_vanilla, LocationName.vanilla_dome_star_road) + connect(world, player, names, LocationName.star_road_vanilla, LocationName.star_road_2_tile) + connect(world, player, names, LocationName.twin_bridges_star_road, LocationName.star_road_twin_bridges) + connect(world, player, names, LocationName.star_road_twin_bridges, LocationName.twin_bridges_star_road) + connect(world, player, names, LocationName.star_road_twin_bridges, LocationName.star_road_3_tile) + connect(world, player, names, LocationName.forest_star_road, LocationName.star_road_forest) + connect(world, player, names, LocationName.star_road_forest, LocationName.forest_star_road) + connect(world, player, names, LocationName.star_road_forest, LocationName.star_road_4_tile) + connect(world, player, names, LocationName.valley_star_road, LocationName.star_road_valley) + connect(world, player, names, LocationName.star_road_valley, LocationName.valley_star_road) + connect(world, player, names, LocationName.star_road_valley, LocationName.star_road_5_tile) + connect(world, player, names, LocationName.star_road_special, LocationName.special_star_road) + connect(world, player, names, LocationName.special_star_road, LocationName.star_road_special) + connect(world, player, names, LocationName.special_star_road, LocationName.special_zone_1_tile) + + connect(world, player, names, LocationName.star_road_valley, LocationName.front_door_tile) + + + +def create_region(world: MultiWorld, player: int, active_locations, name: str, locations=None): + ret = Region(name, RegionType.Generic, name, player) + ret.world = world + if locations: + for locationName in locations: + loc_id = active_locations.get(locationName, 0) + if loc_id: + location = SMWLocation(player, locationName, loc_id, ret) + ret.locations.append(location) + + return ret + +def add_location_to_region(world: MultiWorld, player: int, active_locations, region_name: str, location_name: str, + rule: typing.Optional[typing.Callable] = None): + region = world.get_region(region_name, player) + loc_id = active_locations.get(location_name, 0) + if loc_id: + location = SMWLocation(player, location_name, loc_id, region) + region.locations.append(location) + if rule: + add_rule(location, rule) + + + +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/smw/Rom.py b/worlds/smw/Rom.py new file mode 100644 index 0000000000..34bdd2f0ea --- /dev/null +++ b/worlds/smw/Rom.py @@ -0,0 +1,846 @@ +import Utils +from worlds.Files import APDeltaPatch +from .Aesthetics import generate_shuffled_header_data +from .Levels import level_info_dict +from .Names.TextBox import generate_goal_text, title_text_mapping, generate_text_box + +USHASH = 'cdd3c8c37322978ca8669b34bc89c804' +ROM_PLAYER_LIMIT = 65535 + +import hashlib +import os +import math + + +ability_rom_data = { + 0xBC0003: [[0x1F2C, 0x7]], # Run 0x80 + 0xBC0004: [[0x1F2C, 0x6]], # Carry 0x40 + 0xBC0005: [[0x1F2C, 0x2]], # Swim 0x04 + 0xBC0006: [[0x1F2C, 0x3]], # Spin Jump 0x08 + 0xBC0007: [[0x1F2C, 0x5]], # Climb 0x20 + 0xBC0008: [[0x1F2C, 0x1]], # Yoshi 0x02 + 0xBC0009: [[0x1F2C, 0x4]], # P-Switch 0x10 + #0xBC000A: [[]] + 0xBC000B: [[0x1F2D, 0x3]], # P-Balloon 0x08 + 0xBC000D: [[0x1F2D, 0x4]], # Super Star 0x10 +} + + +item_rom_data = { + 0xBC0001: [0x18E4, 0x1], # 1-Up Mushroom + + 0xBC0002: [0x1F24, 0x1, 0x1F], # Yoshi Egg + 0xBC0012: [0x1F26, 0x1, 0x09], # Boss Token + + 0xBC000E: [0x1F28, 0x1, 0x1C], # Yellow Switch Palace + 0xBC000F: [0x1F27, 0x1, 0x1C], # Green Switch Palace + 0xBC0010: [0x1F2A, 0x1, 0x1C], # Red Switch Palace + 0xBC0011: [0x1F29, 0x1, 0x1C], # Blue Switch Palace + + 0xBC0013: [0x0086, 0x1, 0x0E], # Ice Trap + 0xBC0014: [0x18BD, 0x7F, 0x18], # Stun Trap +} + +music_rom_data = [ + +] + +level_music_ids = [ + +] + + +class SMWDeltaPatch(APDeltaPatch): + hash = USHASH + game = "Super Mario World" + patch_file_ending = ".apsmw" + + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_bytes() + + +class LocalRom: + + 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 = Utils.read_snes_rom(stream) + + 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 handle_ability_code(rom): + # Lock Abilities + + #rom.write_byte(0xC581, 0x01) # No Stars + #rom.write_byte(0x62E6, 0x01) # No Star Music + #rom.write_byte(0xC300, 0x01) # No P-Balloons + #rom.write_byte(0xC305, 0x01) # No P-Balloons + + # Run + rom.write_bytes(0x5977, bytearray([0x22, 0x10, 0xBA, 0x03])) # JSL $03BA10 + rom.write_bytes(0x597B, bytearray([0xEA] * 0x04)) + + RUN_SUB_ADDR = 0x01BA10 + rom.write_bytes(RUN_SUB_ADDR + 0x00, bytearray([0xDA])) # PHX + rom.write_bytes(RUN_SUB_ADDR + 0x01, bytearray([0x08])) # PHP + rom.write_bytes(RUN_SUB_ADDR + 0x02, bytearray([0x90, 0x03])) # BCC +0x03 + rom.write_bytes(RUN_SUB_ADDR + 0x04, bytearray([0xC8])) # INY + rom.write_bytes(RUN_SUB_ADDR + 0x05, bytearray([0xA9, 0x70])) # LDA #70 + rom.write_bytes(RUN_SUB_ADDR + 0x07, bytearray([0xAA])) # TAX + rom.write_bytes(RUN_SUB_ADDR + 0x08, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(RUN_SUB_ADDR + 0x0B, bytearray([0x89, 0x80])) # BIT #80 + rom.write_bytes(RUN_SUB_ADDR + 0x0D, bytearray([0xF0, 0x04])) # BEQ +0x04 + rom.write_bytes(RUN_SUB_ADDR + 0x0F, bytearray([0x8A])) # TXA + rom.write_bytes(RUN_SUB_ADDR + 0x10, bytearray([0x8D, 0xE4, 0x13])) # STA $13E4 + rom.write_bytes(RUN_SUB_ADDR + 0x13, bytearray([0x8A])) # TXA + rom.write_bytes(RUN_SUB_ADDR + 0x14, bytearray([0x28])) # PLP + rom.write_bytes(RUN_SUB_ADDR + 0x15, bytearray([0xFA])) # PLX + rom.write_bytes(RUN_SUB_ADDR + 0x16, bytearray([0x6B])) # RTL + # End Run + + # Purple Block Carry + rom.write_bytes(0x726F, bytearray([0x22, 0x28, 0xBA, 0x03])) # JSL $03BA28 + rom.write_bytes(0x7273, bytearray([0xEA] * 0x02)) + + PURPLE_BLOCK_CARRY_SUB_ADDR = 0x01BA28 + rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x01, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x04, bytearray([0x89, 0x40])) # BIT #40 + rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x06, bytearray([0xF0, 0x09])) # BEQ +0x09 + rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x08, bytearray([0x28])) # PLP + rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x09, bytearray([0xAD, 0x8F, 0x14])) # LDA $148F + rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x0C, bytearray([0x0D, 0x7A, 0x18])) # ORA $187A + rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x0F, bytearray([0x80, 0x03])) # BRA +0x03 + rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x11, bytearray([0x28])) # PLP + rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x12, bytearray([0xA9, 0x01])) # LDA #01 + rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x14, bytearray([0x6B])) # RTL + # End Purple Block Carry + + # Springboard Carry + rom.write_bytes(0xE6DA, bytearray([0x22, 0x40, 0xBA, 0x03])) # JSL $03BA40 + rom.write_bytes(0xE6DE, bytearray([0xEA] * 0x04)) + + SPRINGBOARD_CARRY_SUB_ADDR = 0x01BA40 + rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x00, bytearray([0x48])) # PHA + rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x01, bytearray([0x08])) # PHP + rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x02, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x05, bytearray([0x89, 0x40])) # BIT #40 + rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x07, bytearray([0xF0, 0x08])) # BEQ +0x08 + rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x09, bytearray([0xA9, 0x0B])) # LDA #0B + rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x0B, bytearray([0x9D, 0xC8, 0x14])) # STA $14C8, X + rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x0E, bytearray([0x9E, 0x02, 0x16])) # STZ $1602, X + rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x11, bytearray([0x28])) # PLP + rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x12, bytearray([0x68])) # PLA + rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x13, bytearray([0x6B])) # RTL + # End Springboard Carry + + # Shell Carry + rom.write_bytes(0xAA66, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(0xAA69, bytearray([0x89, 0x40])) # BIT #40 + rom.write_bytes(0xAA6B, bytearray([0xF0, 0x07])) # BEQ +0x07 + rom.write_bytes(0xAA6D, bytearray([0x22, 0x60, 0xBA, 0x03])) # JSL $03BA60 + rom.write_bytes(0xAA71, bytearray([0xEA] * 0x02)) + + SHELL_CARRY_SUB_ADDR = 0x01BA60 + rom.write_bytes(SHELL_CARRY_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(SHELL_CARRY_SUB_ADDR + 0x01, bytearray([0xA9, 0x0B])) # LDA #0B + rom.write_bytes(SHELL_CARRY_SUB_ADDR + 0x03, bytearray([0x9D, 0xC8, 0x14])) # STA $14C8, X + rom.write_bytes(SHELL_CARRY_SUB_ADDR + 0x06, bytearray([0xEE, 0x70, 0x14])) # INC $1470 + rom.write_bytes(SHELL_CARRY_SUB_ADDR + 0x09, bytearray([0xA9, 0x0B])) # LDA #08 + rom.write_bytes(SHELL_CARRY_SUB_ADDR + 0x0B, bytearray([0x8D, 0x98, 0x14])) # STA $1498 + rom.write_bytes(SHELL_CARRY_SUB_ADDR + 0x0E, bytearray([0x28])) # PLP + rom.write_bytes(SHELL_CARRY_SUB_ADDR + 0x0F, bytearray([0x6B])) # RTL + # End Shell Carry + + # Yoshi Carry + rom.write_bytes(0xF309, bytearray([0x22, 0x70, 0xBA, 0x03])) # JSL $03BA70 + rom.write_bytes(0xF30D, bytearray([0xEA] * 0x06)) + + YOSHI_CARRY_SUB_ADDR = 0x01BA70 + rom.write_bytes(YOSHI_CARRY_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(YOSHI_CARRY_SUB_ADDR + 0x01, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(YOSHI_CARRY_SUB_ADDR + 0x04, bytearray([0x89, 0x40])) # BIT #40 + rom.write_bytes(YOSHI_CARRY_SUB_ADDR + 0x06, bytearray([0xF0, 0x0A])) # BEQ +0x0A + rom.write_bytes(YOSHI_CARRY_SUB_ADDR + 0x08, bytearray([0xA9, 0x12])) # LDA #12 + rom.write_bytes(YOSHI_CARRY_SUB_ADDR + 0x0A, bytearray([0x8D, 0xA3, 0x14])) # STA $14A3 + rom.write_bytes(YOSHI_CARRY_SUB_ADDR + 0x0D, bytearray([0xA9, 0x21])) # LDA #21 + rom.write_bytes(YOSHI_CARRY_SUB_ADDR + 0x0F, bytearray([0x8D, 0xFC, 0x1D])) # STA $1DFC + rom.write_bytes(YOSHI_CARRY_SUB_ADDR + 0x12, bytearray([0x28])) # PLP + rom.write_bytes(YOSHI_CARRY_SUB_ADDR + 0x13, bytearray([0x6B])) # RTL + # End Yoshi Carry + + # Climb + rom.write_bytes(0x4D72, bytearray([0x5C, 0x88, 0xBA, 0x03])) # JML $03BA88 + rom.write_bytes(0x4D76, bytearray([0xEA] * 0x03)) + + CLIMB_SUB_ADDR = 0x01BA88 + rom.write_bytes(CLIMB_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(CLIMB_SUB_ADDR + 0x01, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(CLIMB_SUB_ADDR + 0x04, bytearray([0x89, 0x20])) # BIT #20 + rom.write_bytes(CLIMB_SUB_ADDR + 0x06, bytearray([0xF0, 0x09])) # BEQ +0x09 + rom.write_bytes(CLIMB_SUB_ADDR + 0x08, bytearray([0xA5, 0x8B])) # LDA $8B + rom.write_bytes(CLIMB_SUB_ADDR + 0x0A, bytearray([0x85, 0x74])) # STA $74 + rom.write_bytes(CLIMB_SUB_ADDR + 0x0C, bytearray([0x28])) # PLP + rom.write_bytes(CLIMB_SUB_ADDR + 0x0D, bytearray([0x5C, 0x17, 0xDB, 0x00])) # JML $00DB17 + rom.write_bytes(CLIMB_SUB_ADDR + 0x11, bytearray([0x28])) # PLP + rom.write_bytes(CLIMB_SUB_ADDR + 0x12, bytearray([0x5C, 0x76, 0xCD, 0x00])) # JML $00CD76 + # End Climb + + # P-Switch + rom.write_bytes(0xAB1A, bytearray([0x22, 0xA0, 0xBA, 0x03])) # JSL $03BAA0 + rom.write_bytes(0xAB1E, bytearray([0xEA] * 0x01)) + + P_SWITCH_SUB_ADDR = 0x01BAA0 + rom.write_bytes(P_SWITCH_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(P_SWITCH_SUB_ADDR + 0x01, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(P_SWITCH_SUB_ADDR + 0x04, bytearray([0x89, 0x10])) # BIT #10 + rom.write_bytes(P_SWITCH_SUB_ADDR + 0x06, bytearray([0xF0, 0x04])) # BEQ +0x04 + rom.write_bytes(P_SWITCH_SUB_ADDR + 0x08, bytearray([0xA9, 0xB0])) # LDA #B0 + rom.write_bytes(P_SWITCH_SUB_ADDR + 0x0A, bytearray([0x80, 0x02])) # BRA +0x02 + rom.write_bytes(P_SWITCH_SUB_ADDR + 0x0C, bytearray([0xA9, 0x01])) # LDA #01 + rom.write_bytes(P_SWITCH_SUB_ADDR + 0x0E, bytearray([0x99, 0xAD, 0x14])) # STA $14AD + rom.write_bytes(P_SWITCH_SUB_ADDR + 0x11, bytearray([0x28])) # PLP + rom.write_bytes(P_SWITCH_SUB_ADDR + 0x12, bytearray([0x6B])) # RTL + # End P-Switch + + # Spin Jump + rom.write_bytes(0x5645, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(0x5648, bytearray([0x89, 0x08])) # BIT #08 + rom.write_bytes(0x564A, bytearray([0xF0, 0x12])) # BEQ +0x12 + rom.write_bytes(0x564C, bytearray([0x22, 0xB8, 0xBA, 0x03])) # JSL $03BAB8 + + SPIN_JUMP_SUB_ADDR = 0x01BAB8 + rom.write_bytes(SPIN_JUMP_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(SPIN_JUMP_SUB_ADDR + 0x01, bytearray([0x1A])) # INC + rom.write_bytes(SPIN_JUMP_SUB_ADDR + 0x02, bytearray([0x8D, 0x0D, 0x14])) # STA $140D + rom.write_bytes(SPIN_JUMP_SUB_ADDR + 0x05, bytearray([0xA9, 0x04])) # LDA #04 + rom.write_bytes(SPIN_JUMP_SUB_ADDR + 0x07, bytearray([0x8D, 0xFC, 0x1D])) # STA $1DFC + rom.write_bytes(SPIN_JUMP_SUB_ADDR + 0x0A, bytearray([0xA4, 0x76])) # LDY #76 + rom.write_bytes(SPIN_JUMP_SUB_ADDR + 0x0C, bytearray([0x28])) # PLP + rom.write_bytes(SPIN_JUMP_SUB_ADDR + 0x0D, bytearray([0x6B])) # RTL + # End Spin Jump + + # Spin Jump from Water + rom.write_bytes(0x6A89, bytearray([0x22, 0xF8, 0xBB, 0x03])) # JSL $03BBF8 + rom.write_bytes(0x6A8D, bytearray([0xEA] * 0x05)) + + SPIN_JUMP_WATER_SUB_ADDR = 0x01BBF8 + rom.write_bytes(SPIN_JUMP_WATER_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(SPIN_JUMP_WATER_SUB_ADDR + 0x01, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(SPIN_JUMP_WATER_SUB_ADDR + 0x04, bytearray([0x89, 0x08])) # BIT #08 + rom.write_bytes(SPIN_JUMP_WATER_SUB_ADDR + 0x06, bytearray([0xF0, 0x09])) # BEQ +0x09 + rom.write_bytes(SPIN_JUMP_WATER_SUB_ADDR + 0x08, bytearray([0x1A])) # INC + rom.write_bytes(SPIN_JUMP_WATER_SUB_ADDR + 0x09, bytearray([0x8D, 0x0D, 0x14])) # STA $140D + rom.write_bytes(SPIN_JUMP_WATER_SUB_ADDR + 0x0C, bytearray([0xA9, 0x04])) # LDA #04 + rom.write_bytes(SPIN_JUMP_WATER_SUB_ADDR + 0x0E, bytearray([0x8D, 0xFC, 0x1D])) # STA $1DFC + rom.write_bytes(SPIN_JUMP_WATER_SUB_ADDR + 0x11, bytearray([0x28])) # PLP + rom.write_bytes(SPIN_JUMP_WATER_SUB_ADDR + 0x12, bytearray([0x6B])) # RTL + # End Spin Jump from Water + + # Spin Jump from Springboard + rom.write_bytes(0xE693, bytearray([0x22, 0x0C, 0xBC, 0x03])) # JSL $03BC0C + rom.write_bytes(0xE697, bytearray([0xEA] * 0x01)) + + SPIN_JUMP_SPRING_SUB_ADDR = 0x01BC0C + rom.write_bytes(SPIN_JUMP_SPRING_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(SPIN_JUMP_SPRING_SUB_ADDR + 0x01, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(SPIN_JUMP_SPRING_SUB_ADDR + 0x04, bytearray([0x89, 0x08])) # BIT #08 + rom.write_bytes(SPIN_JUMP_SPRING_SUB_ADDR + 0x06, bytearray([0xF0, 0x05])) # BEQ +0x05 + rom.write_bytes(SPIN_JUMP_SPRING_SUB_ADDR + 0x08, bytearray([0xA9, 0x01])) # LDA #01 + rom.write_bytes(SPIN_JUMP_SPRING_SUB_ADDR + 0x0A, bytearray([0x8D, 0x0D, 0x14])) # STA $140D + rom.write_bytes(SPIN_JUMP_SPRING_SUB_ADDR + 0x0D, bytearray([0x28])) # PLP + rom.write_bytes(SPIN_JUMP_SPRING_SUB_ADDR + 0x0E, bytearray([0x6B])) # RTL + # End Spin Jump from Springboard + + # Swim + rom.write_bytes(0x5A25, bytearray([0x22, 0xC8, 0xBA, 0x03])) # JSL $03BAC8 + rom.write_bytes(0x5A29, bytearray([0xEA] * 0x04)) + + SWIM_SUB_ADDR = 0x01BAC8 + rom.write_bytes(SWIM_SUB_ADDR + 0x00, bytearray([0x48])) # PHA + rom.write_bytes(SWIM_SUB_ADDR + 0x01, bytearray([0x08])) # PHP + rom.write_bytes(SWIM_SUB_ADDR + 0x02, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(SWIM_SUB_ADDR + 0x05, bytearray([0x89, 0x04])) # BIT #04 + rom.write_bytes(SWIM_SUB_ADDR + 0x07, bytearray([0xF0, 0x0C])) # BEQ +0x0C + rom.write_bytes(SWIM_SUB_ADDR + 0x09, bytearray([0x28])) # PLP + rom.write_bytes(SWIM_SUB_ADDR + 0x0A, bytearray([0x68])) # PLA + rom.write_bytes(SWIM_SUB_ADDR + 0x0B, bytearray([0xDD, 0x84, 0xD9])) # CMP $D489, X + rom.write_bytes(SWIM_SUB_ADDR + 0x0E, bytearray([0xB0, 0x03])) # BCS +0x03 + rom.write_bytes(SWIM_SUB_ADDR + 0x10, bytearray([0xBD, 0x84, 0xD9])) # LDA $D489, X + rom.write_bytes(SWIM_SUB_ADDR + 0x13, bytearray([0x80, 0x0A])) # BRA +0x0A + rom.write_bytes(SWIM_SUB_ADDR + 0x15, bytearray([0x28])) # PLP + rom.write_bytes(SWIM_SUB_ADDR + 0x16, bytearray([0x68])) # PLA + rom.write_bytes(SWIM_SUB_ADDR + 0x17, bytearray([0xDD, 0xBE, 0xDE])) # CMP $DEBE, X + rom.write_bytes(SWIM_SUB_ADDR + 0x1A, bytearray([0xB0, 0x03])) # BCS +0x03 + rom.write_bytes(SWIM_SUB_ADDR + 0x1C, bytearray([0xBD, 0xBE, 0xDE])) # LDA $DEBE, X + rom.write_bytes(SWIM_SUB_ADDR + 0x1F, bytearray([0x6B])) # RTL + # End Swim + + # Item Swim + rom.write_bytes(0x59D7, bytearray([0x22, 0xE8, 0xBA, 0x03])) # JSL $03BAE8 + rom.write_bytes(0x59DB, bytearray([0xEA] * 0x02)) + + SWIM_SUB_ADDR = 0x01BAE8 + rom.write_bytes(SWIM_SUB_ADDR + 0x00, bytearray([0x48])) # PHA + rom.write_bytes(SWIM_SUB_ADDR + 0x01, bytearray([0x08])) # PHP + rom.write_bytes(SWIM_SUB_ADDR + 0x02, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(SWIM_SUB_ADDR + 0x05, bytearray([0x89, 0x04])) # BIT #04 + rom.write_bytes(SWIM_SUB_ADDR + 0x07, bytearray([0xF0, 0x0A])) # BEQ +0x0A + rom.write_bytes(SWIM_SUB_ADDR + 0x09, bytearray([0x28])) # PLP + rom.write_bytes(SWIM_SUB_ADDR + 0x0A, bytearray([0x68])) # PLA + rom.write_bytes(SWIM_SUB_ADDR + 0x0B, bytearray([0xC9, 0xF0])) # CMP #F0 + rom.write_bytes(SWIM_SUB_ADDR + 0x0D, bytearray([0xB0, 0x02])) # BCS +0x02 + rom.write_bytes(SWIM_SUB_ADDR + 0x0F, bytearray([0xA9, 0xF0])) # LDA #F0 + rom.write_bytes(SWIM_SUB_ADDR + 0x11, bytearray([0x80, 0x08])) # BRA +0x08 + rom.write_bytes(SWIM_SUB_ADDR + 0x13, bytearray([0x28])) # PLP + rom.write_bytes(SWIM_SUB_ADDR + 0x14, bytearray([0x68])) # PLA + rom.write_bytes(SWIM_SUB_ADDR + 0x15, bytearray([0xC9, 0xFF])) # CMP #FF + rom.write_bytes(SWIM_SUB_ADDR + 0x17, bytearray([0xB0, 0x02])) # BCS +0x02 + rom.write_bytes(SWIM_SUB_ADDR + 0x19, bytearray([0xA9, 0x00])) # LDA #00 + rom.write_bytes(SWIM_SUB_ADDR + 0x1B, bytearray([0x6B])) # RTL + # End Item Swim + + # Yoshi + rom.write_bytes(0x109FB, bytearray([0x22, 0x08, 0xBB, 0x03])) # JSL $03BB08 + rom.write_bytes(0x109FF, bytearray([0xEA] * 0x02)) + + YOSHI_SUB_ADDR = 0x01BB08 + rom.write_bytes(YOSHI_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(YOSHI_SUB_ADDR + 0x01, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(YOSHI_SUB_ADDR + 0x04, bytearray([0x89, 0x02])) # BIT #02 + rom.write_bytes(YOSHI_SUB_ADDR + 0x06, bytearray([0xF0, 0x06])) # BEQ +0x06 + rom.write_bytes(YOSHI_SUB_ADDR + 0x08, bytearray([0x28])) # PLP + rom.write_bytes(YOSHI_SUB_ADDR + 0x09, bytearray([0xB9, 0xA1, 0x88])) # LDA $88A1, Y + rom.write_bytes(YOSHI_SUB_ADDR + 0x0C, bytearray([0x80, 0x04])) # BRA +0x04 + rom.write_bytes(YOSHI_SUB_ADDR + 0x0E, bytearray([0x28])) # PLP + rom.write_bytes(YOSHI_SUB_ADDR + 0x0F, bytearray([0xB9, 0xA2, 0x88])) # LDA $88A2, Y + rom.write_bytes(YOSHI_SUB_ADDR + 0x12, bytearray([0x9D, 0x1C, 0x15])) # STA $151C, X + rom.write_bytes(YOSHI_SUB_ADDR + 0x15, bytearray([0x6B])) # RTL + # End Yoshi + + # Baby Yoshi + rom.write_bytes(0xA2B8, bytearray([0x22, 0x20, 0xBB, 0x03])) # JSL $03BB20 + rom.write_bytes(0xA2BC, bytearray([0xEA] * 0x01)) + + YOSHI_SUB_ADDR = 0x01BB20 + rom.write_bytes(YOSHI_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(YOSHI_SUB_ADDR + 0x01, bytearray([0x9C, 0x1E, 0x14])) # STZ $141E + rom.write_bytes(YOSHI_SUB_ADDR + 0x04, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(YOSHI_SUB_ADDR + 0x07, bytearray([0x89, 0x02])) # BIT #02 + rom.write_bytes(YOSHI_SUB_ADDR + 0x09, bytearray([0xF0, 0x05])) # BEQ +0x05 + rom.write_bytes(YOSHI_SUB_ADDR + 0x0B, bytearray([0x28])) # PLP + rom.write_bytes(YOSHI_SUB_ADDR + 0x0C, bytearray([0xA9, 0x35])) # LDA #35 + rom.write_bytes(YOSHI_SUB_ADDR + 0x0E, bytearray([0x80, 0x03])) # BRA +0x03 + rom.write_bytes(YOSHI_SUB_ADDR + 0x10, bytearray([0x28])) # PLP + rom.write_bytes(YOSHI_SUB_ADDR + 0x11, bytearray([0xA9, 0x70])) # LDA #70 + rom.write_bytes(YOSHI_SUB_ADDR + 0x13, bytearray([0x6B])) # RTL + # End Baby Yoshi + + # Midway Gate + rom.write_bytes(0x72E4, bytearray([0x22, 0x38, 0xBB, 0x03])) # JSL $03BB38 + + MIDWAY_GATE_SUB_ADDR = 0x01BB38 + rom.write_bytes(MIDWAY_GATE_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(MIDWAY_GATE_SUB_ADDR + 0x01, bytearray([0xAD, 0x2D, 0x1F])) # LDA $1F2D + rom.write_bytes(MIDWAY_GATE_SUB_ADDR + 0x04, bytearray([0x89, 0x01])) # BIT #01 + rom.write_bytes(MIDWAY_GATE_SUB_ADDR + 0x06, bytearray([0xF0, 0x07])) # BEQ +0x07 + rom.write_bytes(MIDWAY_GATE_SUB_ADDR + 0x08, bytearray([0x28])) # PLP + rom.write_bytes(MIDWAY_GATE_SUB_ADDR + 0x09, bytearray([0xA9, 0x01])) # LDA #01 + rom.write_bytes(MIDWAY_GATE_SUB_ADDR + 0x0B, bytearray([0x85, 0x19])) # STA $19 + rom.write_bytes(MIDWAY_GATE_SUB_ADDR + 0x0D, bytearray([0x80, 0x01])) # BRA +0x01 + rom.write_bytes(MIDWAY_GATE_SUB_ADDR + 0x0F, bytearray([0x28])) # PLP + rom.write_bytes(MIDWAY_GATE_SUB_ADDR + 0x10, bytearray([0x6B])) # RTL + # End Midway Gate + + # Mushroom + rom.write_bytes(0x5156, bytearray([0x22, 0x50, 0xBB, 0x03])) # JSL $03BB50 + rom.write_bytes(0x515A, bytearray([0xEA] * 0x04)) + + MUSHROOM_SUB_ADDR = 0x01BB50 + rom.write_bytes(MUSHROOM_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(MUSHROOM_SUB_ADDR + 0x01, bytearray([0xAD, 0x2D, 0x1F])) # LDA $1F2D + rom.write_bytes(MUSHROOM_SUB_ADDR + 0x04, bytearray([0x89, 0x01])) # BIT #01 + rom.write_bytes(MUSHROOM_SUB_ADDR + 0x06, bytearray([0xF0, 0x05])) # BEQ +0x05 + rom.write_bytes(MUSHROOM_SUB_ADDR + 0x08, bytearray([0x28])) # PLP + rom.write_bytes(MUSHROOM_SUB_ADDR + 0x09, bytearray([0xE6, 0x19])) # INC $19 + rom.write_bytes(MUSHROOM_SUB_ADDR + 0x0B, bytearray([0x80, 0x01])) # BRA +0x01 + rom.write_bytes(MUSHROOM_SUB_ADDR + 0x0D, bytearray([0x28])) # PLP + rom.write_bytes(MUSHROOM_SUB_ADDR + 0x0E, bytearray([0xA9, 0x00])) # LDA #00 + rom.write_bytes(MUSHROOM_SUB_ADDR + 0x10, bytearray([0x85, 0x71])) # STA $72 + rom.write_bytes(MUSHROOM_SUB_ADDR + 0x12, bytearray([0x64, 0x9D])) # STZ $9D + rom.write_bytes(MUSHROOM_SUB_ADDR + 0x14, bytearray([0x6B])) # RTL + # End Mushroom + + # Take Damage + rom.write_bytes(0x5142, bytearray([0x22, 0x65, 0xBB, 0x03])) # JSL $03BB65 + rom.write_bytes(0x5146, bytearray([0x60] * 0x01)) # RTS + + DAMAGE_SUB_ADDR = 0x01BB65 + rom.write_bytes(DAMAGE_SUB_ADDR + 0x00, bytearray([0x8D, 0x97, 0x14])) # STA $1497 + rom.write_bytes(DAMAGE_SUB_ADDR + 0x03, bytearray([0x80, 0xF4])) # BRA -0x0C + # End Take Damage + + # Fire Flower Cycle + rom.write_bytes(0x5187, bytearray([0x22, 0x6A, 0xBB, 0x03])) # JSL $03BB6A + rom.write_bytes(0x518B, bytearray([0x60] * 0x01)) # RTS + + PALETTE_CYCLE_SUB_ADDR = 0x01BB6A + rom.write_bytes(PALETTE_CYCLE_SUB_ADDR + 0x00, bytearray([0xCE, 0x9B, 0x14])) # DEC $149B + rom.write_bytes(PALETTE_CYCLE_SUB_ADDR + 0x03, bytearray([0xF0, 0xEF])) # BEQ -0x11 + rom.write_bytes(PALETTE_CYCLE_SUB_ADDR + 0x05, bytearray([0x6B])) # RTL + # End Fire Flower Cycle + + # Pipe Exit + rom.write_bytes(0x526D, bytearray([0x22, 0x70, 0xBB, 0x03])) # JSL $03BB70 + rom.write_bytes(0x5271, bytearray([0x60, 0xEA] * 0x01)) # RTS, NOP + + PIPE_EXIT_SUB_ADDR = 0x01BB70 + rom.write_bytes(PIPE_EXIT_SUB_ADDR + 0x00, bytearray([0x9C, 0x19, 0x14])) # STZ $1419 + rom.write_bytes(PIPE_EXIT_SUB_ADDR + 0x03, bytearray([0xA9, 0x00])) # LDA #00 + rom.write_bytes(PIPE_EXIT_SUB_ADDR + 0x05, bytearray([0x85, 0x71])) # STA $72 + rom.write_bytes(PIPE_EXIT_SUB_ADDR + 0x07, bytearray([0x64, 0x9D])) # STZ $9D + rom.write_bytes(PIPE_EXIT_SUB_ADDR + 0x09, bytearray([0x6B])) # RTL + # End Pipe Exit + + # Cape Transform + rom.write_bytes(0x5168, bytearray([0x22, 0x7A, 0xBB, 0x03])) # JSL $03BB7A + rom.write_bytes(0x516C, bytearray([0xEA] * 0x01)) # RTS, NOP + rom.write_bytes(0x516D, bytearray([0xF0, 0xD1])) # BEQ -0x2F + + CAPE_TRANSFORM_SUB_ADDR = 0x01BB7A + rom.write_bytes(CAPE_TRANSFORM_SUB_ADDR + 0x00, bytearray([0xA5, 0x19])) # LDA $19 + rom.write_bytes(CAPE_TRANSFORM_SUB_ADDR + 0x02, bytearray([0x4A])) # LSR + rom.write_bytes(CAPE_TRANSFORM_SUB_ADDR + 0x03, bytearray([0xD0, 0xDF])) # BNE -0x21 + rom.write_bytes(CAPE_TRANSFORM_SUB_ADDR + 0x05, bytearray([0x6B])) # RTL + # End Cape Transform + + # Fire Flower + rom.write_bytes(0xC5F7, bytearray([0x22, 0x80, 0xBB, 0x03])) # JSL $03BB80 + + FIRE_FLOWER_SUB_ADDR = 0x01BB80 + rom.write_bytes(FIRE_FLOWER_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(FIRE_FLOWER_SUB_ADDR + 0x01, bytearray([0xAD, 0x2D, 0x1F])) # LDA $1F2D + rom.write_bytes(FIRE_FLOWER_SUB_ADDR + 0x04, bytearray([0x89, 0x02])) # BIT #02 + rom.write_bytes(FIRE_FLOWER_SUB_ADDR + 0x06, bytearray([0xF0, 0x07])) # BEQ +0x07 + rom.write_bytes(FIRE_FLOWER_SUB_ADDR + 0x08, bytearray([0x28])) # PLP + rom.write_bytes(FIRE_FLOWER_SUB_ADDR + 0x09, bytearray([0xA9, 0x03])) # LDA #03 + rom.write_bytes(FIRE_FLOWER_SUB_ADDR + 0x0B, bytearray([0x85, 0x19])) # STA $19 + rom.write_bytes(FIRE_FLOWER_SUB_ADDR + 0x0D, bytearray([0x80, 0x01])) # BRA +0x01 + rom.write_bytes(FIRE_FLOWER_SUB_ADDR + 0x0F, bytearray([0x28])) # PLP + rom.write_bytes(FIRE_FLOWER_SUB_ADDR + 0x10, bytearray([0x6B])) # RTL + # End Fire Flower + + # Cape + rom.write_bytes(0xC598, bytearray([0x22, 0x91, 0xBB, 0x03])) # JSL $03BB91 + + CAPE_SUB_ADDR = 0x01BB91 + rom.write_bytes(CAPE_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(CAPE_SUB_ADDR + 0x01, bytearray([0xAD, 0x2D, 0x1F])) # LDA $1F2D + rom.write_bytes(CAPE_SUB_ADDR + 0x04, bytearray([0x89, 0x04])) # BIT #04 + rom.write_bytes(CAPE_SUB_ADDR + 0x06, bytearray([0xF0, 0x07])) # BEQ +0x07 + rom.write_bytes(CAPE_SUB_ADDR + 0x08, bytearray([0x28])) # PLP + rom.write_bytes(CAPE_SUB_ADDR + 0x09, bytearray([0xA9, 0x02])) # LDA #02 + rom.write_bytes(CAPE_SUB_ADDR + 0x0B, bytearray([0x85, 0x19])) # STA $19 + rom.write_bytes(CAPE_SUB_ADDR + 0x0D, bytearray([0x80, 0x01])) # BRA +0x01 + rom.write_bytes(CAPE_SUB_ADDR + 0x0F, bytearray([0x28])) # PLP + rom.write_bytes(CAPE_SUB_ADDR + 0x10, bytearray([0x6B])) # RTL + # End Cape + + # P-Balloon + rom.write_bytes(0xC2FF, bytearray([0x22, 0xA2, 0xBB, 0x03])) # JSL $03BBA2 + rom.write_bytes(0xC303, bytearray([0xEA] * 0x06)) + + P_BALLOON_SUB_ADDR = 0x01BBA2 + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x01, bytearray([0xAD, 0x2D, 0x1F])) # LDA $1F2D + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x04, bytearray([0x89, 0x08])) # BIT #08 + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x06, bytearray([0xF0, 0x0D])) # BEQ +0x0D + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x08, bytearray([0x28])) # PLP + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x09, bytearray([0xA9, 0x09])) # LDA #09 + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x0B, bytearray([0x8D, 0xF3, 0x13])) # STA $13F3 + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x0E, bytearray([0xA9, 0xFF])) # LDA #FF + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x10, bytearray([0x8D, 0x91, 0x18])) # STA $1891 + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x13, bytearray([0x80, 0x0B])) # BRA +0x0B + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x15, bytearray([0x28])) # PLP + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x16, bytearray([0xA9, 0x01])) # LDA #01 + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x18, bytearray([0x8D, 0xF3, 0x13])) # STA $13F3 + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x1B, bytearray([0xA9, 0x01])) # LDA #01 + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x1D, bytearray([0x8D, 0x91, 0x18])) # STA $1891 + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x20, bytearray([0x6B])) # RTL + # End P-Balloon + + # Star + rom.write_bytes(0xC580, bytearray([0x22, 0xC8, 0xBB, 0x03])) # JSL $03BBC8 + rom.write_bytes(0xC584, bytearray([0xEA] * 0x01)) + + STAR_SUB_ADDR = 0x01BBC8 + rom.write_bytes(STAR_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(STAR_SUB_ADDR + 0x01, bytearray([0xAD, 0x2D, 0x1F])) # LDA $1F2D + rom.write_bytes(STAR_SUB_ADDR + 0x04, bytearray([0x89, 0x10])) # BIT #10 + rom.write_bytes(STAR_SUB_ADDR + 0x06, bytearray([0xF0, 0x08])) # BEQ +0x08 + rom.write_bytes(STAR_SUB_ADDR + 0x08, bytearray([0x28])) # PLP + rom.write_bytes(STAR_SUB_ADDR + 0x09, bytearray([0xA9, 0xFF])) # LDA #FF + rom.write_bytes(STAR_SUB_ADDR + 0x0B, bytearray([0x8D, 0x90, 0x14])) # STA $1490 + rom.write_bytes(STAR_SUB_ADDR + 0x0E, bytearray([0x80, 0x06])) # BRA +0x06 + rom.write_bytes(STAR_SUB_ADDR + 0x10, bytearray([0x28])) # PLP + rom.write_bytes(STAR_SUB_ADDR + 0x11, bytearray([0xA9, 0x01])) # LDA #01 + rom.write_bytes(STAR_SUB_ADDR + 0x13, bytearray([0x8D, 0x90, 0x14])) # STA $1490 + rom.write_bytes(STAR_SUB_ADDR + 0x16, bytearray([0x6B])) # RTL + # End Star + + # Star Timer + rom.write_bytes(0x62E3, bytearray([0x22, 0xE0, 0xBB, 0x03])) # JSL $03BBE0 + + STAR_TIMER_SUB_ADDR = 0x01BBE0 + rom.write_bytes(STAR_TIMER_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(STAR_TIMER_SUB_ADDR + 0x01, bytearray([0xAD, 0x2D, 0x1F])) # LDA $1F2D + rom.write_bytes(STAR_TIMER_SUB_ADDR + 0x04, bytearray([0x89, 0x10])) # BIT #10 + rom.write_bytes(STAR_TIMER_SUB_ADDR + 0x06, bytearray([0xF0, 0x07])) # BEQ +0x07 + rom.write_bytes(STAR_TIMER_SUB_ADDR + 0x08, bytearray([0x28])) # PLP + rom.write_bytes(STAR_TIMER_SUB_ADDR + 0x09, bytearray([0xA5, 0x13])) # LDA $13 + rom.write_bytes(STAR_TIMER_SUB_ADDR + 0x0B, bytearray([0xC0, 0x1E])) # CPY #1E + rom.write_bytes(STAR_TIMER_SUB_ADDR + 0x0D, bytearray([0x80, 0x05])) # BRA +0x05 + rom.write_bytes(STAR_TIMER_SUB_ADDR + 0x0F, bytearray([0x28])) # PLP + rom.write_bytes(STAR_TIMER_SUB_ADDR + 0x10, bytearray([0xA5, 0x13])) # LDA $13 + rom.write_bytes(STAR_TIMER_SUB_ADDR + 0x12, bytearray([0xC0, 0x01])) # CPY #01 + rom.write_bytes(STAR_TIMER_SUB_ADDR + 0x14, bytearray([0x6B])) # RTL + # End Star Timer + + return + + +def handle_yoshi_box(rom): + + rom.write_bytes(0xEC3D, bytearray([0xEA] * 0x03)) # NOP Lines that cause Yoshi Rescue Box normally + + rom.write_bytes(0x2B20F, bytearray([0x20, 0x60, 0xDC])) # JSR $05DC60 + + YOSHI_BOX_SUB_ADDR = 0x02DC60 + rom.write_bytes(YOSHI_BOX_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(YOSHI_BOX_SUB_ADDR + 0x01, bytearray([0xAD, 0x26, 0x14])) # LDA $1426 + rom.write_bytes(YOSHI_BOX_SUB_ADDR + 0x04, bytearray([0xC9, 0x03])) # CMP #03 + rom.write_bytes(YOSHI_BOX_SUB_ADDR + 0x06, bytearray([0xF0, 0x06])) # BEQ +0x06 + rom.write_bytes(YOSHI_BOX_SUB_ADDR + 0x08, bytearray([0x28])) # PLP + rom.write_bytes(YOSHI_BOX_SUB_ADDR + 0x09, bytearray([0xB9, 0xD9, 0xA5])) # LDA $A5B9, Y + rom.write_bytes(YOSHI_BOX_SUB_ADDR + 0x0C, bytearray([0x80, 0x08])) # BRA +0x08 + rom.write_bytes(YOSHI_BOX_SUB_ADDR + 0x0E, bytearray([0x28])) # PLP + rom.write_bytes(YOSHI_BOX_SUB_ADDR + 0x0F, bytearray([0xDA])) # PHX + rom.write_bytes(YOSHI_BOX_SUB_ADDR + 0x10, bytearray([0xBB])) # TYX + rom.write_bytes(YOSHI_BOX_SUB_ADDR + 0x11, bytearray([0xBF, 0x00, 0xC2, 0x7E])) # LDA $7EC200, X + rom.write_bytes(YOSHI_BOX_SUB_ADDR + 0x15, bytearray([0xFA])) # PLX + rom.write_bytes(YOSHI_BOX_SUB_ADDR + 0x16, bytearray([0x60])) # RTS + + return + + +def handle_bowser_damage(rom): + + rom.write_bytes(0x1A509, bytearray([0x20, 0x50, 0xBC])) # JSR $03BC50 + + BOWSER_BALLS_SUB_ADDR = 0x01BC50 + rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x01, bytearray([0xAD, 0x48, 0x0F])) # LDA $F48 + rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x04, bytearray([0xCF, 0xA1, 0xBF, 0x03])) # CMP $03BFA1 + rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x08, bytearray([0x90, 0x06])) # BCC +0x06 + rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x0A, bytearray([0x28])) # PLP + rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x0B, bytearray([0xEE, 0xB8, 0x14])) # INC $14B8 + rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x0E, bytearray([0x80, 0x01])) # BRA +0x01 + rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x10, bytearray([0x28])) # PLP + rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x11, bytearray([0x60])) # RTS + + return + + +def handle_level_shuffle(rom, active_level_dict): + rom.write_bytes(0x37600, bytearray([0x00] * 0x800)) # Duplicate Level Table + + rom.write_bytes(0x2D89C, bytearray([0x00, 0xF6, 0x06])) # Level Load Pointer + rom.write_bytes(0x20F46, bytearray([0x00, 0xF6, 0x06])) # Mid Gate Pointer + rom.write_bytes(0x20E7B, bytearray([0x00, 0xF6, 0x06])) # Level Name Pointer + rom.write_bytes(0x21543, bytearray([0x00, 0xF6, 0x06])) # Also Level Name Pointer? + rom.write_bytes(0x20F64, bytearray([0x00, 0xF6, 0x06])) # Level Beaten Pointer + + ### Fix Translevel Check + rom.write_bytes(0x2D8AE, bytearray([0x20, 0x00, 0xDD])) # JSR $DD00 + rom.write_bytes(0x2D8B1, bytearray([0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA])) # NOP NOP NOP NOP NOP + + rom.write_bytes(0x2D7CB, bytearray([0x20, 0x00, 0xDD])) # JSR $DD00 + rom.write_bytes(0x2D7CE, bytearray([0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA])) # NOP NOP NOP NOP NOP + + rom.write_bytes(0x2DD00, bytearray([0xDA])) # PHX + rom.write_bytes(0x2DD01, bytearray([0x08])) # PHP + rom.write_bytes(0x2DD02, bytearray([0xE2, 0x30])) # SEP #30 + rom.write_bytes(0x2DD04, bytearray([0xAE, 0xBF, 0x13])) # LDX $13BF + rom.write_bytes(0x2DD07, bytearray([0xE0, 0x25])) # CPX #25 + rom.write_bytes(0x2DD09, bytearray([0x90, 0x04])) # BCC $DD0F + rom.write_bytes(0x2DD0B, bytearray([0xA2, 0x01])) # LDX #01 + rom.write_bytes(0x2DD0D, bytearray([0x80, 0x02])) # BRA $DD11 + rom.write_bytes(0x2DD0F, bytearray([0xA2, 0x00])) # LDX #00 + rom.write_bytes(0x2DD11, bytearray([0x86, 0x0F])) # STX $0F + rom.write_bytes(0x2DD13, bytearray([0x28])) # PLP + rom.write_bytes(0x2DD14, bytearray([0xFA])) # PLX + rom.write_bytes(0x2DD15, bytearray([0x60])) # RTS + ### End Fix Translevel Check + + ### Fix Snake Blocks + rom.write_bytes(0x192FB, bytearray([0x20, 0x1D, 0xBC])) # JSR $03BC1D + + SNAKE_BLOCKS_SUB_ADDR = 0x01BC1D + rom.write_bytes(SNAKE_BLOCKS_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(SNAKE_BLOCKS_SUB_ADDR + 0x01, bytearray([0xAD, 0xBF, 0x13])) # LDA $13BF + rom.write_bytes(SNAKE_BLOCKS_SUB_ADDR + 0x04, bytearray([0xC9, 0x20])) # CMP #20 + rom.write_bytes(SNAKE_BLOCKS_SUB_ADDR + 0x06, bytearray([0xF0, 0x05])) # BEQ +0x05 + rom.write_bytes(SNAKE_BLOCKS_SUB_ADDR + 0x08, bytearray([0x28])) # PLP + rom.write_bytes(SNAKE_BLOCKS_SUB_ADDR + 0x09, bytearray([0xA9, 0x01])) # LDA #01 + rom.write_bytes(SNAKE_BLOCKS_SUB_ADDR + 0x0B, bytearray([0x80, 0x03])) # BRA +0x03 + rom.write_bytes(SNAKE_BLOCKS_SUB_ADDR + 0x0D, bytearray([0x28])) # PLP + rom.write_bytes(SNAKE_BLOCKS_SUB_ADDR + 0x0E, bytearray([0xA9, 0x00])) # LDA #00 + rom.write_bytes(SNAKE_BLOCKS_SUB_ADDR + 0x10, bytearray([0x60])) # RTS + ### End Fix Snake Blocks + + for level_id, level_data in level_info_dict.items(): + if level_id not in active_level_dict.keys(): + continue + + tile_id = active_level_dict[level_id] + tile_data = level_info_dict[tile_id] + + if level_id > 0x80: + level_id = level_id - 0x50 + + rom.write_byte(tile_data.levelIDAddress, level_id) + rom.write_byte(0x2D608 + level_id, tile_data.eventIDValue) + + for level_id, tile_id in active_level_dict.items(): + rom.write_byte(0x37F70 + level_id, tile_id) + + +def handle_collected_paths(rom): + rom.write_bytes(0x1F5B, bytearray([0x22, 0x30, 0xBC, 0x03])) # JSL $03BC30 + rom.write_bytes(0x1F5F, bytearray([0xEA] * 0x02)) + + COLLECTED_PATHS_SUB_ADDR = 0x01BC30 + rom.write_bytes(COLLECTED_PATHS_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(COLLECTED_PATHS_SUB_ADDR + 0x01, bytearray([0xAD, 0x00, 0x01])) # LDA $0100 + rom.write_bytes(COLLECTED_PATHS_SUB_ADDR + 0x04, bytearray([0xC9, 0x0B])) # CMP #0B + rom.write_bytes(COLLECTED_PATHS_SUB_ADDR + 0x06, bytearray([0xD0, 0x04])) # BNE +0x04 + rom.write_bytes(COLLECTED_PATHS_SUB_ADDR + 0x08, bytearray([0x22, 0xAD, 0xDA, 0x04])) # JSL $04DAAD + rom.write_bytes(COLLECTED_PATHS_SUB_ADDR + 0x0C, bytearray([0x28])) # PLP + rom.write_bytes(COLLECTED_PATHS_SUB_ADDR + 0x0D, bytearray([0xEE, 0x00, 0x01])) # INC $0100 + rom.write_bytes(COLLECTED_PATHS_SUB_ADDR + 0x10, bytearray([0xAD, 0xAF, 0x0D])) # LDA $0DAF + rom.write_bytes(COLLECTED_PATHS_SUB_ADDR + 0x13, bytearray([0x6B])) # RTL + + +def handle_music_shuffle(rom, world, player): + from .Aesthetics import generate_shuffled_level_music, generate_shuffled_ow_music, level_music_address_data, ow_music_address_data + + shuffled_level_music = generate_shuffled_level_music(world, player) + for i in range(len(shuffled_level_music)): + rom.write_byte(level_music_address_data[i], shuffled_level_music[i]) + + shuffled_ow_music = generate_shuffled_ow_music(world, player) + for i in range(len(shuffled_ow_music)): + for addr in ow_music_address_data[i]: + rom.write_byte(addr, shuffled_ow_music[i]) + + +def handle_mario_palette(rom, world, player): + from .Aesthetics import mario_palettes, fire_mario_palettes, ow_mario_palettes + + chosen_palette = world.mario_palette[player].value + + rom.write_bytes(0x32C8, bytes(mario_palettes[chosen_palette])) + rom.write_bytes(0x32F0, bytes(fire_mario_palettes[chosen_palette])) + rom.write_bytes(0x359C, bytes(ow_mario_palettes[chosen_palette])) + + +def handle_swap_donut_gh_exits(rom): + rom.write_bytes(0x2567C, bytes([0xC0])) + rom.write_bytes(0x25873, bytes([0xA9])) + rom.write_bytes(0x25875, bytes([0x85])) + rom.write_bytes(0x25954, bytes([0x92])) + rom.write_bytes(0x25956, bytes([0x0A])) + rom.write_bytes(0x25E31, bytes([0x00, 0x00, 0xD8, 0x04, 0x24, 0x00, 0x98, 0x04, 0x48, 0x00, 0xD8, 0x03, 0x6C, 0x00, 0x56, 0x03, + 0x90, 0x00, 0x56, 0x03, 0xB4, 0x00, 0x56, 0x03, 0x10, 0x05, 0x18, 0x05, 0x28, 0x09, 0x24, 0x05, + 0x38, 0x0B, 0x14, 0x07, 0xEC, 0x09, 0x12, 0x05, 0xF0, 0x09, 0xD2, 0x04, 0xF4, 0x09, 0x92, 0x04])) + rom.write_bytes(0x26371, bytes([0x32])) + + +def patch_rom(world, rom, player, active_level_dict): + local_random = world.slot_seeds[player] + + goal_text = generate_goal_text(world, player) + + rom.write_bytes(0x2A6E2, goal_text) + rom.write_byte(0x2B1D8, 0x80) + + intro_text = generate_text_box("Bowser has stolen all of Mario's abilities. Can you help Mario travel across Dinosaur land to get them back and save the Princess from him?") + rom.write_bytes(0x2A5D9, intro_text) + + # Force all 8 Bowser's Castle Rooms + rom.write_byte(0x3A680, 0xD4) + rom.write_byte(0x3A684, 0xD4) + rom.write_byte(0x3A688, 0xD4) + rom.write_byte(0x3A68C, 0xD4) + rom.write_byte(0x3A705, 0xD3) + rom.write_byte(0x3A763, 0xD2) + rom.write_byte(0x3A800, 0xD1) + rom.write_byte(0x3A83D, 0xCF) + rom.write_byte(0x3A932, 0xCE) + rom.write_byte(0x3A9E1, 0xCD) + rom.write_byte(0x3AA75, 0xCC) + + # Prevent Title Screen Deaths + rom.write_byte(0x1C6A, 0x80) + + # Title Screen Text + player_name_bytes = bytearray() + player_name = world.get_player_name(player) + for i in range(16): + char = " " + if i < len(player_name): + char = world.get_player_name(player)[i] + upper_char = char.upper() + if upper_char not in title_text_mapping: + for byte in title_text_mapping["."]: + player_name_bytes.append(byte) + else: + for byte in title_text_mapping[upper_char]: + player_name_bytes.append(byte) + + rom.write_bytes(0x2B7F1, player_name_bytes) # MARIO A + rom.write_bytes(0x2B726, player_name_bytes) # MARIO A + + rom.write_bytes(0x2B815, bytearray([0xFC, 0x38] * 0x10)) # MARIO B + rom.write_bytes(0x2B74A, bytearray([0xFC, 0x38] * 0x10)) # MARIO B + rom.write_bytes(0x2B839, bytearray([0x71, 0x31, 0x74, 0x31, 0x2D, 0x31, 0x84, 0x30, + 0x82, 0x30, 0x6F, 0x31, 0x73, 0x31, 0x70, 0x31, + 0x71, 0x31, 0x75, 0x31, 0x83, 0x30, 0xFC, 0x38, + 0xFC, 0x38, 0xFC, 0x38, 0xFC, 0x38, 0xFC, 0x38])) # MARIO C + rom.write_bytes(0x2B76E, bytearray([0xFC, 0x38] * 0x10)) # MARIO C + rom.write_bytes(0x2B79E, bytearray([0xFC, 0x38] * 0x05)) # EMPTY + rom.write_bytes(0x2B7AE, bytearray([0xFC, 0x38] * 0x05)) # EMPTY + rom.write_bytes(0x2B8A8, bytearray([0xFC, 0x38] * 0x0D)) # 2 PLAYER GAME + + rom.write_bytes(0x2B85D, bytearray([0xFC, 0x38] * 0x0A)) # ERASE + + rom.write_bytes(0x2B88E, bytearray([0x2C, 0x31, 0x73, 0x31, 0x75, 0x31, 0x82, 0x30, 0x30, 0x31, 0xFC, 0x38, 0x31, 0x31, 0x73, 0x31, + 0x73, 0x31, 0x7C, 0x30, 0xFC, 0x38, 0xFC, 0x38, 0xFC, 0x38])) # 1 Player Game + + rom.write_bytes(0x2B6D7, bytearray([0xFC, 0x38, 0xFC, 0x38, 0x16, 0x38, 0x18, 0x38, 0x0D, 0x38, 0xFC, 0x38, 0x0B, 0x38, 0x22, 0x38, + 0xFC, 0x38, 0x19, 0x38, 0x18, 0x38, 0x1B, 0x38, 0x22, 0x38, 0x10, 0x38, 0x18, 0x38, 0x17, 0x38, + 0x0E, 0x38, 0xFC, 0x38, 0xFC, 0x38])) # Mod by PoryGone + + # Title Options + rom.write_bytes(0x1E6A, bytearray([0x01])) + rom.write_bytes(0x1E6C, bytearray([0x01])) + rom.write_bytes(0x1E6E, bytearray([0x01])) + + # Always allow Start+Select + rom.write_bytes(0x2267, bytearray([0xEA, 0xEA])) + + # Always bring up save prompt on beating a level + if world.autosave[player]: + rom.write_bytes(0x20F93, bytearray([0x00])) + + # Starting Life Count + rom.write_bytes(0x1E25, bytearray([world.starting_life_count[player].value - 1])) + + # Repurpose Bonus Stars counter for Boss Token or Yoshi Eggs + rom.write_bytes(0x3F1AA, bytearray([0x00] * 0x20)) + rom.write_bytes(0x20F9F, bytearray([0xEA] * 0x3B)) + + # Prevent Switch Palaces setting the Switch Palace flags + rom.write_bytes(0x6EC9A, bytearray([0xEA, 0xEA])) + rom.write_bytes(0x6EB1, bytearray([0xEA, 0xEA])) + rom.write_bytes(0x6EB4, bytearray([0xEA, 0xEA, 0xEA])) + + handle_ability_code(rom) + + handle_yoshi_box(rom) + handle_bowser_damage(rom) + + handle_collected_paths(rom) + + # Handle Level Shuffle + handle_level_shuffle(rom, active_level_dict) + + # Handle Music Shuffle + if world.music_shuffle[player] != "none": + handle_music_shuffle(rom, world, player) + + generate_shuffled_header_data(rom, world, player) + + if world.swap_donut_gh_exits[player]: + handle_swap_donut_gh_exits(rom) + + handle_mario_palette(rom, world, player) + + # Store all relevant option results in ROM + rom.write_byte(0x01BFA0, world.goal[player].value) + rom.write_byte(0x01BFA1, world.bosses_required[player].value) + required_yoshi_eggs = max(math.floor( + world.number_of_yoshi_eggs[player].value * (world.percentage_of_yoshi_eggs[player].value / 100.0)), 1) + rom.write_byte(0x01BFA2, required_yoshi_eggs) + #rom.write_byte(0x01BFA3, world.display_sent_item_popups[player].value) + rom.write_byte(0x01BFA4, world.display_received_item_popups[player].value) + rom.write_byte(0x01BFA5, world.death_link[player].value) + rom.write_byte(0x01BFA6, world.dragon_coin_checks[player].value) + rom.write_byte(0x01BFA7, world.swap_donut_gh_exits[player].value) + + + from Main import __version__ + rom.name = bytearray(f'SMW{__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) + + +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(Utils.read_snes_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["smw_options"]["rom_file"] + if not os.path.exists(file_name): + file_name = Utils.local_path(file_name) + return file_name diff --git a/worlds/smw/Rules.py b/worlds/smw/Rules.py new file mode 100644 index 0000000000..bf9fedb805 --- /dev/null +++ b/worlds/smw/Rules.py @@ -0,0 +1,20 @@ +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 world.goal[player] == "yoshi_egg_hunt": + required_yoshi_eggs = max(math.floor( + world.number_of_yoshi_eggs[player].value * (world.percentage_of_yoshi_eggs[player].value / 100.0)), 1) + + add_rule(world.get_location(LocationName.yoshis_house, player), + lambda state: state.has(ItemName.yoshi_egg, player, required_yoshi_eggs)) + else: + add_rule(world.get_location(LocationName.bowser, player), lambda state: state.has(ItemName.mario_carry, player)) + + world.completion_condition[player] = lambda state: state.has(ItemName.victory, player) diff --git a/worlds/smw/__init__.py b/worlds/smw/__init__.py new file mode 100644 index 0000000000..1dd64f535f --- /dev/null +++ b/worlds/smw/__init__.py @@ -0,0 +1,246 @@ +import os +import typing +import math +import threading + +from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification +from .Items import SMWItem, ItemData, item_table +from .Locations import SMWLocation, all_locations, setup_locations +from .Options import smw_options +from .Regions import create_regions, connect_regions +from .Levels import full_level_list, generate_level_list, location_id_to_level_id +from .Rules import set_rules +from ..generic.Rules import add_rule +from .Names import ItemName, LocationName +from ..AutoWorld import WebWorld, World +from .Rom import LocalRom, patch_rom, get_base_rom_path, SMWDeltaPatch + + +class SMWWeb(WebWorld): + theme = "grass" + + setup_en = Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the Super Mario World randomizer connected to an Archipelago Multiworld.", + "English", + "setup_en.md", + "setup/en", + ["PoryGone"] + ) + + tutorials = [setup_en] + + +class SMWWorld(World): + """ + Super Mario World is an action platforming game. + The Princess has been kidnapped by Bowser again, but Mario has somehow + lost all of his abilities. Can he get them back in time to save the Princess? + """ + game: str = "Super Mario World" + option_definitions = smw_options + topology_present = False + data_version = 1 + required_client_version = (0, 3, 5) + + item_name_to_id = {name: data.code for name, data in item_table.items()} + location_name_to_id = all_locations + + active_level_dict: typing.Dict[int,int] + web = SMWWeb() + + 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_dict, + } + + def fill_slot_data(self) -> dict: + slot_data = self._get_slot_data() + for option_name in smw_options: + option = getattr(self.world, option_name)[self.player] + slot_data[option_name] = option.value + + return slot_data + + def generate_basic(self): + itempool: typing.List[SMWItem] = [] + + self.active_level_dict = dict(zip(generate_level_list(self.world, self.player), full_level_list)) + self.topology_present = self.world.level_shuffle[self.player] + + connect_regions(self.world, self.player, self.active_level_dict) + + # Add Boss Token amount requirements for Worlds + add_rule(self.world.get_region(LocationName.donut_plains_1_tile, self.player).entrances[0], lambda state: state.has(ItemName.koopaling, self.player, 1)) + add_rule(self.world.get_region(LocationName.vanilla_dome_1_tile, self.player).entrances[0], lambda state: state.has(ItemName.koopaling, self.player, 2)) + add_rule(self.world.get_region(LocationName.forest_of_illusion_1_tile, self.player).entrances[0], lambda state: state.has(ItemName.koopaling, self.player, 4)) + add_rule(self.world.get_region(LocationName.chocolate_island_1_tile, self.player).entrances[0], lambda state: state.has(ItemName.koopaling, self.player, 5)) + add_rule(self.world.get_region(LocationName.valley_of_bowser_1_tile, self.player).entrances[0], lambda state: state.has(ItemName.koopaling, self.player, 6)) + + total_required_locations = 96 + if self.world.dragon_coin_checks[self.player]: + total_required_locations += 49 + + itempool += [self.create_item(ItemName.mario_run)] + itempool += [self.create_item(ItemName.mario_carry)] + itempool += [self.create_item(ItemName.mario_swim)] + itempool += [self.create_item(ItemName.mario_spin_jump)] + itempool += [self.create_item(ItemName.mario_climb)] + itempool += [self.create_item(ItemName.yoshi_activate)] + itempool += [self.create_item(ItemName.p_switch)] + itempool += [self.create_item(ItemName.p_balloon)] + itempool += [self.create_item(ItemName.super_star_active)] + itempool += [self.create_item(ItemName.progressive_powerup) for _ in range(3)] + itempool += [self.create_item(ItemName.yellow_switch_palace)] + itempool += [self.create_item(ItemName.green_switch_palace)] + itempool += [self.create_item(ItemName.red_switch_palace)] + itempool += [self.create_item(ItemName.blue_switch_palace)] + + if self.world.goal[self.player] == "yoshi_egg_hunt": + itempool += [self.create_item(ItemName.yoshi_egg) + for _ in range(self.world.number_of_yoshi_eggs[self.player])] + self.world.get_location(LocationName.yoshis_house, self.player).place_locked_item(self.create_item(ItemName.victory)) + else: + self.world.get_location(LocationName.bowser, self.player).place_locked_item(self.create_item(ItemName.victory)) + + junk_count = total_required_locations - len(itempool) + trap_weights = [] + trap_weights += ([ItemName.ice_trap] * self.world.ice_trap_weight[self.player].value) + trap_weights += ([ItemName.stun_trap] * self.world.stun_trap_weight[self.player].value) + trap_weights += ([ItemName.literature_trap] * self.world.literature_trap_weight[self.player].value) + trap_count = 0 if (len(trap_weights) == 0) else math.ceil(junk_count * (self.world.trap_fill_percentage[self.player].value / 100.0)) + junk_count -= trap_count + + trap_pool = [] + for i in range(trap_count): + trap_item = self.world.random.choice(trap_weights) + trap_pool.append(self.create_item(trap_item)) + + itempool += trap_pool + + itempool += [self.create_item(ItemName.one_up_mushroom) for _ in range(junk_count)] + + boss_location_names = [LocationName.yoshis_island_koopaling, LocationName.donut_plains_koopaling, LocationName.vanilla_dome_koopaling, + LocationName.twin_bridges_koopaling, LocationName.forest_koopaling, LocationName.chocolate_koopaling, + LocationName.valley_koopaling, LocationName.vanilla_reznor, LocationName.forest_reznor, LocationName.chocolate_reznor, LocationName.valley_reznor] + + for location_name in boss_location_names: + self.world.get_location(location_name, self.player).place_locked_item(self.create_item(ItemName.koopaling)) + + self.world.itempool += itempool + + + def generate_output(self, output_directory: str): + rompath = "" # if variable is not declared finally clause may fail + try: + world = self.world + player = self.player + + rom = LocalRom(get_base_rom_path()) + patch_rom(self.world, rom, self.player, self.active_level_dict) + + rompath = os.path.join(output_directory, f"{self.world.get_out_file_name_base(self.player)}.sfc") + rom.write_to_file(rompath) + self.rom_name = rom.name + + patch = SMWDeltaPatch(os.path.splitext(rompath)[0]+SMWDeltaPatch.patch_file_ending, player=player, + player_name=world.player_name[player], patched_path=rompath) + patch.write() + except: + raise + finally: + self.rom_name_available_event.set() # make sure threading continues and errors are collected + if os.path.exists(rompath): + os.unlink(rompath) + + 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]] + + def extend_hint_information(self, hint_data: typing.Dict[int, typing.Dict[int, str]]): + if self.topology_present: + world_names = [ + LocationName.yoshis_island_region, + LocationName.donut_plains_region, + LocationName.vanilla_dome_region, + LocationName.twin_bridges_region, + LocationName.forest_of_illusion_region, + LocationName.chocolate_island_region, + LocationName.valley_of_bowser_region, + LocationName.star_road_region, + LocationName.special_zone_region, + ] + world_cutoffs = [ + 0x07, + 0x13, + 0x1F, + 0x26, + 0x30, + 0x39, + 0x44, + 0x4F, + 0x59 + ] + er_hint_data = {} + for loc_name, level_data in location_id_to_level_id.items(): + level_id = level_data[0] + + if level_id not in self.active_level_dict: + continue + + keys_list = list(self.active_level_dict.keys()) + level_index = keys_list.index(level_id) + for i in range(len(world_cutoffs)): + if level_index >= world_cutoffs[i]: + continue + + if self.world.dragon_coin_checks[self.player].value == 0 and "Dragon Coins" in loc_name: + continue + + location = self.world.get_location(loc_name, self.player) + er_hint_data[location.address] = world_names[i] + break + + 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 name == ItemName.yoshi_egg: + classification = ItemClassification.progression_skip_balancing + elif data.progression: + classification = ItemClassification.progression + elif data.trap: + classification = ItemClassification.trap + else: + classification = ItemClassification.filler + + created_item = SMWItem(name, classification, data.code, self.player) + + return created_item + + def set_rules(self): + set_rules(self.world, self.player) diff --git a/worlds/smw/docs/en_Super Mario World.md b/worlds/smw/docs/en_Super Mario World.md new file mode 100644 index 0000000000..87a96e558b --- /dev/null +++ b/worlds/smw/docs/en_Super Mario World.md @@ -0,0 +1,43 @@ +# Super Mario World + +## 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? + +Mario's basic abilities are removed, and placed into the item pool as items that any player can find. This includes: +- Carry +- Climb +- Run +- P-Switch +- Swim +- Spin Jump +- Yoshi + +Additionally, the ability to use powerups (Mushrooms, Fire Flowers, Capes, Stars, and P-Balloons) are shuffled into the item pool, as are the Four Switch Palaces. + +## What is the goal of Super Mario World when randomized? + +There are two goals which can be chosen: +- `Bowser`: Reach Bowser's Castle and defeat Bowser, after defeating a certain number of bosses. +- `Yoshi Egg Hunt`: Collect a certain number of Yoshi Eggs, then return to Yoshi's House + +## What items and locations get shuffled? + +Each unique level exit awards a location check. Optionally, collecting five Dragon Coins in each level can also award a location check. +Mario's various abilities and powerups as described above are placed into the item pool. +If the player is playing Yoshi Egg Hunt, a certain number of Yoshi Eggs will be placed into the item pool. +Any additional items that are needed to fill out the item pool with be 1-Up Mushrooms. + +## 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 Super Mario World + +Items do not have an appearance in Super Mario World. + +## When the player receives an item, what happens? + +The player can choose to receive a text box in-game when they receive an item. Regardless of that choice, items will be queued, and granted when the player next enters a level. diff --git a/worlds/smw/docs/setup_en.md b/worlds/smw/docs/setup_en.md new file mode 100644 index 0000000000..178b7392b7 --- /dev/null +++ b/worlds/smw/docs/setup_en.md @@ -0,0 +1,149 @@ +# Super Mario World Randomizer Setup Guide + +## Required Software + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `SNI Client - Super Mario World Patch Setup` + + +- Hardware or software capable of loading and playing SNES ROM files + - An emulator capable of connecting to SNI such as: + - snes9x-rr from: [snes9x rr](https://github.com/gocha/snes9x-rr/releases), + - 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 Super Mario World ROM file, probably named `Super Mario World (USA).sfc` + +## 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 Super Mario World 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: [Super Mario World Player Settings Page](/games/Super%20Mario%20World/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) + +## 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 whomever 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 `.apsmw` 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-rr + +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/smz3/Rom.py b/worlds/smz3/Rom.py index a355636fed..3fec151dc6 100644 --- a/worlds/smz3/Rom.py +++ b/worlds/smz3/Rom.py @@ -2,7 +2,8 @@ import hashlib import os import Utils -from Patch import read_rom, APDeltaPatch +from Utils import read_snes_rom +from worlds.Files import APDeltaPatch SMJUHASH = '21f3e98df4780ee1c667b84e57d88675' LTTPJPN10HASH = '03a63945398191337e896e5771f77173' @@ -23,7 +24,7 @@ def get_base_rom_bytes() -> bytes: base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None) if not base_rom_bytes: sm_file_name = get_sm_base_rom_path() - sm_base_rom_bytes = bytes(read_rom(open(sm_file_name, "rb"))) + sm_base_rom_bytes = bytes(read_snes_rom(open(sm_file_name, "rb"))) basemd5 = hashlib.md5() basemd5.update(sm_base_rom_bytes) @@ -31,7 +32,7 @@ def get_base_rom_bytes() -> bytes: raise Exception('Supplied Base Rom does not match known MD5 for SM Japan+US release. ' 'Get the correct game and version, then dump it') lttp_file_name = get_lttp_base_rom_path() - lttp_base_rom_bytes = bytes(read_rom(open(lttp_file_name, "rb"))) + lttp_base_rom_bytes = bytes(read_snes_rom(open(lttp_file_name, "rb"))) basemd5 = hashlib.md5() basemd5.update(lttp_base_rom_bytes) diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index b796c2a43c..753fb556ae 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -426,11 +426,9 @@ class SMZ3World(World): base_combined_rom[addr + offset] = byte offset += 1 - outfilebase = 'AP_' + self.world.seed_name - outfilepname = f'_P{self.player}' - outfilepname += f"_{self.world.get_file_safe_player_name(self.player).replace(' ', '_')}" \ + outfilebase = self.world.get_out_file_name_base(self.player) - filename = os.path.join(output_directory, f'{outfilebase}{outfilepname}.sfc') + filename = os.path.join(output_directory, f"{outfilebase}.sfc") with open(filename, "wb") as binary_file: binary_file.write(base_combined_rom) patch = SMZ3DeltaPatch(os.path.splitext(filename)[0]+SMZ3DeltaPatch.patch_file_ending, player=self.player, diff --git a/worlds/smz3/data/zsm.ips b/worlds/smz3/data/zsm.ips index 2d6027d5e5..25e543d987 100644 Binary files a/worlds/smz3/data/zsm.ips and b/worlds/smz3/data/zsm.ips differ diff --git a/worlds/soe/Logic.py b/worlds/soe/Logic.py index 97c73a1bd1..3c173dec2f 100644 --- a/worlds/soe/Logic.py +++ b/worlds/soe/Logic.py @@ -1,10 +1,11 @@ -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? +from typing import Protocol, Set +from BaseClasses import MultiWorld +from worlds.AutoWorld import LogicMixin from . import pyevermizer +from .Options import EnergyCore + +# TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early? # 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) @@ -15,9 +16,16 @@ items = [item for item in filter(lambda item: item.progression, pyevermizer.get_ if item.name not in item_names and not item_names.add(item.name)] +class LogicProtocol(Protocol): + def has(self, name: str, player: int) -> bool: ... + def item_count(self, name: str, player: int) -> int: ... + def soe_has(self, progress: int, world: MultiWorld, player: int, count: int) -> bool: ... + def _soe_count(self, progress: int, world: MultiWorld, player: int, max_count: int) -> int: ... + + # when this module is loaded, this mixin will extend BaseClasses.CollectionState class SecretOfEvermoreLogic(LogicMixin): - def _soe_count(self, progress: int, world: MultiWorld, player: int, max_count: int = 0) -> int: + def _soe_count(self: LogicProtocol, progress: int, world: MultiWorld, player: int, max_count: int = 0) -> int: """ Returns reached count of one of evermizer's progress steps based on collected items. i.e. returns 0-3 for P_DE based on items providing CHECK_BOSS,DIAMOND_EYE_DROP @@ -44,7 +52,7 @@ class SecretOfEvermoreLogic(LogicMixin): return n return n - def soe_has(self, progress: int, world: MultiWorld, player: int, count: int = 1) -> bool: + def soe_has(self: LogicProtocol, progress: int, world: MultiWorld, player: int, count: int = 1) -> bool: """ Returns True if count of one of evermizer's progress steps is reached based on collected items. i.e. 2 * P_DE """ diff --git a/worlds/soe/Options.py b/worlds/soe/Options.py index c718cb4abd..f1a30745f8 100644 --- a/worlds/soe/Options.py +++ b/worlds/soe/Options.py @@ -1,18 +1,33 @@ import typing -from Options import Option, Range, Choice, Toggle, DefaultOnToggle, AssembleOptions, DeathLink, ProgressionBalancing + +from Options import Range, Choice, Toggle, DefaultOnToggle, AssembleOptions, DeathLink, ProgressionBalancing +# typing boilerplate +class FlagsProtocol(typing.Protocol): + value: int + default: int + flags: typing.List[str] + + +class FlagProtocol(typing.Protocol): + value: int + default: int + flag: str + + +# meta options class EvermizerFlags: flags: typing.List[str] - def to_flag(self) -> str: + def to_flag(self: FlagsProtocol) -> str: return self.flags[self.value] class EvermizerFlag: flag: str - def to_flag(self) -> str: + def to_flag(self: FlagProtocol) -> str: return self.flag if self.value != self.default else '' @@ -23,6 +38,7 @@ class OffOnFullChoice(Choice): alias_chaos = 2 +# actual options class Difficulty(EvermizerFlags, Choice): """Changes relative spell cost and stuff""" display_name = "Difficulty" @@ -168,6 +184,7 @@ class TrapCount(Range): default = 0 +# more meta options class ItemChanceMeta(AssembleOptions): def __new__(mcs, name, bases, attrs): if 'item_name' in attrs: @@ -183,6 +200,7 @@ class TrapChance(Range, metaclass=ItemChanceMeta): default = 20 +# more actual options class TrapChanceQuake(TrapChance): """Sets the chance/ratio of quake traps""" item_name = "Quake Trap" @@ -210,11 +228,12 @@ class TrapChanceOHKO(TrapChance): class SoEProgressionBalancing(ProgressionBalancing): default = 30 - __doc__ = ProgressionBalancing.__doc__.replace(f"default {ProgressionBalancing.default}", f"default {default}") + __doc__ = ProgressionBalancing.__doc__.replace(f"default {ProgressionBalancing.default}", f"default {default}") \ + if ProgressionBalancing.__doc__ else None special_range_names = {**ProgressionBalancing.special_range_names, "normal": default} -soe_options: typing.Dict[str, type(Option)] = { +soe_options: typing.Dict[str, AssembleOptions] = { "difficulty": Difficulty, "energy_core": EnergyCore, "required_fragments": RequiredFragments, diff --git a/worlds/soe/Patch.py b/worlds/soe/Patch.py index 21bdd94220..f4de5d06ea 100644 --- a/worlds/soe/Patch.py +++ b/worlds/soe/Patch.py @@ -1,9 +1,8 @@ -import bsdiff4 -import yaml -from typing import Optional -import Utils -from Patch import APDeltaPatch import os +from typing import Optional + +import Utils +from worlds.Files import APDeltaPatch USHASH = '6e9c94511d04fac6e0a1e582c170be3a' @@ -24,6 +23,8 @@ def get_base_rom_path(file_name: Optional[str] = None) -> str: options = Utils.get_options() if not file_name: file_name = options["soe_options"]["rom_file"] + if not file_name: + raise ValueError("Missing soe_options -> rom_file from host.yaml") if not os.path.exists(file_name): file_name = Utils.user_path(file_name) return file_name @@ -37,30 +38,6 @@ def read_rom(stream, strip_header=True) -> bytes: return data -def generate_yaml(patch: bytes, metadata: Optional[dict] = None) -> bytes: - """Generate old (<4) apbp format yaml""" - patch = yaml.dump({"meta": metadata, - "patch": patch, - "game": "Secret of Evermore", - # minimum version of patch system expected for patching to be successful - "compatible_version": 1, - "version": 2, - "base_checksum": USHASH}) - return patch.encode(encoding="utf-8-sig") - - -def generate_patch(vanilla_file, randomized_file, metadata: Optional[dict] = None) -> bytes: - """Generate old (<4) apbp format patch data. Run through lzma to get a complete apbp file.""" - with open(vanilla_file, "rb") as f: - vanilla = read_rom(f) - with open(randomized_file, "rb") as f: - randomized = read_rom(f) - if metadata is None: - metadata = {} - patch = bsdiff4.diff(vanilla, randomized) - return generate_yaml(patch, metadata) - - if __name__ == '__main__': import sys print('Please use ../../Patch.py', file=sys.stderr) diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index a0dc41c3ce..007bc6dc84 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -1,12 +1,12 @@ -from ..AutoWorld import World, WebWorld -from ..generic.Rules import set_rule -from BaseClasses import Region, Location, Entrance, Item, RegionType, Tutorial, ItemClassification -from Utils import output_path -import typing +import itertools import os import os.path import threading -import itertools +import typing +from worlds.AutoWorld import WebWorld, World +from worlds.generic.Rules import add_item_rule, set_rule +from BaseClasses import Entrance, Item, ItemClassification, Location, LocationProgressType, Region, RegionType, Tutorial +from Utils import output_path try: import pyevermizer # from package @@ -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, EnergyCore, RequiredFragments, AvailableFragments +from .Options import soe_options, Difficulty, EnergyCore, RequiredFragments, AvailableFragments from .Patch import SoEDeltaPatch, get_base_rom_path """ @@ -154,9 +154,9 @@ class SoEWorld(World): option_definitions = soe_options topology_present = False remote_items = False - data_version = 3 + data_version = 4 web = SoEWebWorld() - required_client_version = (0, 3, 3) + required_client_version = (0, 3, 5) item_name_to_id, item_id_to_raw = _get_item_mapping() location_name_to_id, location_id_to_raw = _get_location_mapping() @@ -188,7 +188,7 @@ class SoEWorld(World): return SoEItem(event, ItemClassification.progression, None, self.player) def create_item(self, item: typing.Union[pyevermizer.Item, str]) -> Item: - if type(item) is str: + if isinstance(item, str): item = self.item_id_to_raw[self.item_name_to_id[item]] if item.type == pyevermizer.CHECK_TRAP: classification = ItemClassification.trap @@ -208,14 +208,68 @@ class SoEWorld(World): raise FileNotFoundError(rom_file) def create_regions(self): + # exclude 'hidden' on easy + max_difficulty = 1 if self.world.difficulty[self.player] == Difficulty.option_easy else 256 + # TODO: generate *some* regions from locations' requirements? r = Region('Menu', RegionType.Generic, 'Menu', self.player, self.world) r.exits = [Entrance(self.player, 'New Game', r)] self.world.regions += [r] + # group locations into spheres (1, 2, 3+ at index 0, 1, 2) + spheres: typing.Dict[int, typing.Dict[int, typing.List[SoELocation]]] = {} + for loc in _locations: + spheres.setdefault(min(2, len(loc.requires)), {}).setdefault(loc.type, []).append( + SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], r, + loc.difficulty > max_difficulty)) + + # location balancing data + trash_fills: typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int, int]]] = { + 0: {pyevermizer.CHECK_GOURD: (20, 40, 40, 40)}, # remove up to 40 gourds from sphere 1 + 1: {pyevermizer.CHECK_GOURD: (70, 90, 90, 90)}, # remove up to 90 gourds from sphere 2 + } + + # mark some as excluded based on numbers above + for trash_sphere, fills in trash_fills.items(): + for typ, counts in fills.items(): + count = counts[self.world.difficulty[self.player].value] + for location in self.world.random.sample(spheres[trash_sphere][typ], count): + location.progress_type = LocationProgressType.EXCLUDED + # TODO: do we need to set an item rule? + + def sphere1_blocked_items_rule(item): + if isinstance(item, SoEItem): + # disable certain items in sphere 1 + if item.name in {"Gauge", "Wheel"}: + return False + # and some more for non-easy, non-mystery + if self.world.difficulty[item.player] not in (Difficulty.option_easy, Difficulty.option_mystery): + if item.name in {"Laser Lance", "Atom Smasher", "Diamond Eye"}: + return False + return True + + for locations in spheres[0].values(): + for location in locations: + add_item_rule(location, sphere1_blocked_items_rule) + + # make some logically late(r) bosses priority locations to increase complexity + if self.world.difficulty[self.player] == Difficulty.option_mystery: + late_count = self.world.random.randint(0, 2) + else: + late_count = self.world.difficulty[self.player].value + late_bosses = ("Tiny", "Aquagoth", "Megataur", "Rimsala", + "Mungola", "Lightning Storm", "Magmar", "Volcano Viper") + late_locations = self.world.random.sample(late_bosses, late_count) + + # add locations to the world r = Region('Ingame', RegionType.Generic, 'Ingame', self.player, self.world) - r.locations = [SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], r) - for loc in _locations] + for sphere in spheres.values(): + for locations in sphere.values(): + for location in locations: + r.locations.append(location) + if location.name in late_locations: + location.progress_type = LocationProgressType.PRIORITY + r.locations.append(SoELocation(self.player, 'Done', None, r)) self.world.regions += [r] @@ -269,6 +323,7 @@ class SoEWorld(World): if v < c: return self.create_item(trap_names[t]) v -= c + assert False, "Bug in create_trap" for _ in range(trap_count): if len(ingredients) < 1: @@ -289,7 +344,7 @@ class SoEWorld(World): location = self.world.get_location(loc.name, self.player) set_rule(location, self.make_rule(loc.requires)) - def make_rule(self, requires: typing.List[typing.Tuple[int]]) -> typing.Callable[[typing.Any], bool]: + def make_rule(self, requires: typing.List[typing.Tuple[int, int]]) -> typing.Callable[[typing.Any], bool]: def rule(state) -> bool: for count, progress in requires: if not state.soe_has(progress, self.world, self.player, count): @@ -321,8 +376,8 @@ class SoEWorld(World): while len(self.connect_name.encode('utf-8')) > 32: self.connect_name = self.connect_name[:-1] self.connect_name_available_event.set() - placement_file = None - out_file = None + placement_file = "" + out_file = "" try: money = self.world.money_modifier[self.player].value exp = self.world.exp_modifier[self.player].value @@ -333,8 +388,7 @@ class SoEWorld(World): 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)}') + out_base = output_path(output_directory, self.world.get_out_file_name_base(self.player)) out_file = out_base + '.sfc' placement_file = out_base + '.txt' patch_file = out_base + '.apsoe' @@ -347,14 +401,15 @@ class SoEWorld(World): with open(placement_file, "wb") as f: # generate placement file for location in filter(lambda l: l.player == self.player, self.world.get_locations()): item = location.item - if item.code is None: + assert item is not None, "Can't handle unfilled location" + if item.code is None or location.address is None: continue # skip events loc = self.location_id_to_raw[location.address] if item.player != self.player: line = f'{loc.type},{loc.index}:{pyevermizer.CHECK_NONE},{item.code},{item.player}\n' else: - item = self.item_id_to_raw[item.code] - line = f'{loc.type},{loc.index}:{item.type},{item.index}\n' + soe_item = self.item_id_to_raw[item.code] + line = f'{loc.type},{loc.index}:{soe_item.type},{soe_item.index}\n' f.write(line.encode('utf-8')) if not os.path.exists(rom_file): @@ -365,14 +420,14 @@ class SoEWorld(World): patch = SoEDeltaPatch(patch_file, player=self.player, player_name=player_name, patched_path=out_file) patch.write() - except: + except Exception: raise finally: try: os.unlink(placement_file) os.unlink(out_file) os.unlink(out_file[:-4] + '_SPOILER.log') - except: + except FileNotFoundError: pass def modify_multidata(self, multidata: dict): @@ -389,11 +444,15 @@ class SoEWorld(World): class SoEItem(Item): game: str = "Secret of Evermore" + __slots__ = () # disable __dict__ class SoELocation(Location): game: str = "Secret of Evermore" + __slots__ = () # disables __dict__ once Location has __slots__ - def __init__(self, player: int, name: str, address: typing.Optional[int], parent): + def __init__(self, player: int, name: str, address: typing.Optional[int], parent: Region, exclude: bool = False): super().__init__(player, name, address, parent) + # unconditional assignments favor a split dict, saving memory + self.progress_type = LocationProgressType.EXCLUDED if exclude else LocationProgressType.DEFAULT self.event = not address diff --git a/worlds/soe/requirements.txt b/worlds/soe/requirements.txt index 7f6a11e490..54eae8f1de 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.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' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp38-cp38-win_amd64.whl#egg=pyevermizer==0.42.0; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp39-cp39-win_amd64.whl#egg=pyevermizer==0.42.0; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp310-cp310-win_amd64.whl#egg=pyevermizer==0.42.0; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer==0.42.0; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer==0.42.0; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer==0.42.0; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer==0.42.0; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer==0.42.0; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer==0.42.0; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp38-cp38-macosx_10_9_x86_64.whl#egg=pyevermizer==0.42.0; sys_platform == 'darwin' and python_version == '3.8' +#https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp39-cp39-macosx_10_9_x86_64.whl#egg=pyevermizer==0.42.0; sys_platform == 'darwin' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp39-cp39-macosx_10_9_universal2.whl#egg=pyevermizer==0.42.0; sys_platform == 'darwin' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp310-cp310-macosx_10_9_universal2.whl#egg=pyevermizer==0.42.0; sys_platform == 'darwin' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0.tar.gz#egg=pyevermizer==0.42.0; python_version < '3.8' or python_version > '3.10' or (sys_platform != 'win32' and sys_platform != 'linux' and sys_platform != 'darwin') or (platform_machine != 'AMD64' and platform_machine != 'x86_64' and platform_machine != 'aarch64' and platform_machine != 'universal2' and platform_machine != 'arm64') diff --git a/worlds/subnautica/docs/setup_en.md b/worlds/subnautica/docs/setup_en.md index 665cb8b336..bd4a92cc7c 100644 --- a/worlds/subnautica/docs/setup_en.md +++ b/worlds/subnautica/docs/setup_en.md @@ -7,37 +7,50 @@ - Archipelago Mod for Subnautica from: [Subnautica Archipelago Mod Releases Page](https://github.com/Berserker66/ArchipelagoSubnauticaModSrc/releases) -## Installation Procedures +## Installation Procedure 1. Install QModManager4 as per its instructions. -2. The folder you installed QModManager4 into will now have a /QMods directory. It might appear after a start of - Subnautica. You can also create this folder yourself. +2. The Subnautica game directory should now contain a `QMods` folder. Unpack the Archipelago Mod into this folder, so that `Subnautica/QMods/Archipelago/` is a valid path. -3. Unpack the Archipelago Mod into this folder, so that Subnautica/QMods/Archipelago/ is a valid path. - -4. Start Subnautica. You should see a Connect Menu in the topleft of your main Menu. +3. Start Subnautica. You should see a connect form with three text boxes in the top left 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: +Use the connect form in Subnautica's main menu to enter your connection information to connect to an Archipelago multiworld. +Connection information consists of: - 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. + - 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. +Savegames store their connection information and automatically attempt to reestablish the connection upon loading. +If the connection information is no longer valid, such as if the server's IP and/or port have changed, +you need to use the connect form on the main menu beforehand. -Warning: Currently it is not checked if this is the correct multiworld belonging to that savegame, please ensure that yourself beforehand. +Warning: Currently it is not checked whether a loaded savegame belongs to the multiworld you are connecting to. Please ensure that yourself beforehand. + +## Console Commands + +The mod adds the following console commands: + - `silent` toggles Archipelago chat messages appearing. + - `deathlink` toggles death link. + +To enable the console in Subnautica, press `F3` and `F8`, then uncheck "Disable Console" in the top left. Press `F3` and `F8` again to close the menus. +To enter a console command, press `Enter`. + +## Known Issues + +- Do not attempt playing vanilla saves while the mod is installed, as the mod will override the scan information of the savegame. +- When exiting to the main menu the mod's state is not properly reset. Loading a savegame from here will break various things. + If you want to reload a save it is recommended you restart the game entirely. +- Attempting to load a savegame containing no longer valid connection information without entering valid information on the main menu will hang on the loading screen. ## Troubleshooting -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`. If it doesn't show this, then +If you don't see the connect form on the main menu screen, check whether 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 `Loading [Archipelago`. If the file doesn't contain this text, then QModManager4 didn't find the Archipelago mod, so check your paths. diff --git a/worlds/v6/__init__.py b/worlds/v6/__init__.py index 38690e5a00..9c5fdbc8a1 100644 --- a/worlds/v6/__init__.py +++ b/worlds/v6/__init__.py @@ -91,6 +91,6 @@ class V6World(World): } } } - filename = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_file_safe_player_name(self.player)}.apv6" + filename = f"{self.world.get_out_file_name_base(self.player)}.apv6" with open(os.path.join(output_directory, filename), 'w') as f: json.dump(data, f) diff --git a/worlds/witness/Options.py b/worlds/witness/Options.py index 2cb9ade007..ef3c9fdf76 100644 --- a/worlds/witness/Options.py +++ b/worlds/witness/Options.py @@ -7,7 +7,7 @@ from Options import Toggle, DefaultOnToggle, Option, Range, Choice # "Play the randomizer in hardmode" # display_name = "Hard Mode" -class DisableNonRandomizedPuzzles(DefaultOnToggle): +class DisableNonRandomizedPuzzles(Toggle): """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.""" @@ -59,8 +59,9 @@ class ShuffleVaultBoxes(Toggle): class ShufflePostgame(Toggle): - """Adds locations into the pool that are guaranteed to become accessible before or at the same time as your goal. - Use this if you don't play with forfeit on victory.""" + """Adds locations into the pool that are guaranteed to become accessible after or at the same time as your goal. + Use this if you don't play with forfeit on victory. IMPORTANT NOTE: The possibility of your second + "Progressive Dots" showing up in the Caves is ignored, they will still be considered "postgame" in base settings.""" display_name = "Shuffle Postgame" @@ -75,6 +76,13 @@ class VictoryCondition(Choice): option_mountain_box_long = 3 +class PuzzleRandomization(Choice): + """Puzzles in this randomizer are randomly generated. This setting changes the difficulty/types of puzzles.""" + display_name = "Puzzle Randomization" + option_sigma_normal = 0 + option_sigma_expert = 1 + + class MountainLasers(Range): """Sets the amount of beams required to enter the final area.""" display_name = "Required Lasers for Mountain Entry" @@ -104,12 +112,21 @@ class PuzzleSkipAmount(Range): Works on most panels in the game - The only big exception is The Challenge.""" display_name = "Puzzle Skips" range_start = 0 - range_end = 20 - default = 5 + range_end = 30 + default = 10 + + +class HintAmount(Range): + """Adds hints to Audio Logs. Hints will have the same number of duplicates, as many as will fit. Remaining Audio + Logs will have junk hints.""" + display_name = "Hints on Audio Logs" + range_start = 0 + range_end = 49 + default = 10 the_witness_options: Dict[str, type] = { - # "hard_mode": HardMode, + "puzzle_randomization": PuzzleRandomization, "shuffle_symbols": ShuffleSymbols, "shuffle_doors": ShuffleDoors, "shuffle_lasers": ShuffleLasers, @@ -123,6 +140,7 @@ the_witness_options: Dict[str, type] = { "early_secret_area": EarlySecretArea, "trap_percentage": TrapPercentage, "puzzle_skip_amount": PuzzleSkipAmount, + "hint_amount": HintAmount, } diff --git a/worlds/witness/WitnessItems.txt b/worlds/witness/WitnessItems.txt index fd9b10f97a..98e0038b25 100644 --- a/worlds/witness/WitnessItems.txt +++ b/worlds/witness/WitnessItems.txt @@ -15,6 +15,8 @@ Progression: 71 - Black/White Squares 72 - Colored Squares 80 - Arrows +200 - Progressive Dots - Dots,Full Dots +260 - Progressive Stars - Stars,Stars + Same Colored Symbol Usefuls: 101 - Functioning Brain - False @@ -56,7 +58,7 @@ Doors: 1190 - Swamp Entry (Panel) - 0x0056E 1192 - Swamp Sliding Bridge (Panel) - 0x00609,0x18488 1195 - Swamp Rotating Bridge (Panel) - 0x181F5 -1197 - Swamp Maze Control (Panel) - 0x17C0A +1197 - Swamp Maze Control (Panel) - 0x17C0A,0x17E07 1310 - Boat - 0x17CDF,0x17CC8,0x17CA6,0x09DB8,0x17C95,0x0A054 1400 - Caves Mountain Shortcut (Door) - 0x2D73F diff --git a/worlds/witness/WitnessLogic.txt b/worlds/witness/WitnessLogic.txt index 650cf14c52..d9fb9503b0 100644 --- a/worlds/witness/WitnessLogic.txt +++ b/worlds/witness/WitnessLogic.txt @@ -120,7 +120,7 @@ Door - 0x03313 (Second Gate) - 0x032FF Orchard End (Orchard): Desert Outside (Desert) - Main Island - True - Desert Floodlight Room - 0x09FEE: -158652 - 0x0CC7B (Vault) - True - Dots & Shapers & Rotated Shapers & Negative Shapers +158652 - 0x0CC7B (Vault) - True - Dots & Shapers & Rotated Shapers & Negative Shapers & Full Dots 158653 - 0x0339E (Vault Box) - 0x0CC7B - True 158602 - 0x17CE7 (Discard) - True - Triangles 158076 - 0x00698 (Surface 1) - True - True @@ -338,7 +338,7 @@ Keep 3rd Pressure Plate (Keep) - Keep 4th Pressure Plate - 0x01CD5: 158202 - 0x01CD3 (Pressure Plates 3) - 0x0A3BB - Shapers & Black/White Squares & Colored Squares Door - 0x01CD5 (Pressure Plates 3 Exit) - 0x01CD3 -Keep 4th Pressure Plate (Keep) - Keep - 0x09E3D - Keep Tower - 0x01D40: +Keep 4th Pressure Plate (Keep) - Shadows - 0x09E3D - Keep Tower - 0x01D40: 158203 - 0x0A3AD (Reset Pressure Plates 4) - True - True 158204 - 0x01D3F (Pressure Plates 4) - 0x0A3AD - Shapers & Dots & Symmetry Door - 0x01D40 (Pressure Plates 4 Exit) - 0x01D3F @@ -358,7 +358,7 @@ Door - 0x04F8F (Tower Shortcut) - 0x0361B 158705 - 0x03317 (Laser Panel Pressure Plates) - 0x01D3F - Shapers & Black/White Squares & Colored Squares & Stars & Stars + Same Colored Symbol & Dots Laser - 0x014BB (Laser) - 0x0360E | 0x03317 -Outside Monastery (Monastery) - Main Island - True - Main Island - 0x0364E - Inside Monastery - 0x0C128 & 0x0C153 - Monastery Garden - 0x03750: +Outside Monastery (Monastery) - Main Island - True - Inside Monastery - 0x0C128 & 0x0C153 - Monastery Garden - 0x03750: 158207 - 0x03713 (Shortcut Panel) - True - True Door - 0x0364E (Shortcut) - 0x03713 158208 - 0x00B10 (Entry Left) - True - True @@ -390,11 +390,11 @@ Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8 158221 - 0x28AE3 (Vines) - 0x18590 - True 158222 - 0x28938 (Apple Tree) - 0x28AE3 - True 158223 - 0x079DF (Triple Exit) - 0x28938 - True -158235 - 0x2899C (Wooden Roof Lower Row 1) - True - Rotated Shapers & Dots -158236 - 0x28A33 (Wooden Roof Lower Row 2) - 0x2899C - Shapers & Dots -158237 - 0x28ABF (Wooden Roof Lower Row 3) - 0x28A33 - Shapers & Rotated Shapers & Dots -158238 - 0x28AC0 (Wooden Roof Lower Row 4) - 0x28ABF - Rotated Shapers & Dots -158239 - 0x28AC1 (Wooden Roof Lower Row 5) - 0x28AC0 - Rotated Shapers & Dots +158235 - 0x2899C (Wooden Roof Lower Row 1) - True - Rotated Shapers & Dots & Full Dots +158236 - 0x28A33 (Wooden Roof Lower Row 2) - 0x2899C - Shapers & Dots & Full Dots +158237 - 0x28ABF (Wooden Roof Lower Row 3) - 0x28A33 - Shapers & Rotated Shapers & Dots & Full Dots +158238 - 0x28AC0 (Wooden Roof Lower Row 4) - 0x28ABF - Rotated Shapers & Dots & Full Dots +158239 - 0x28AC1 (Wooden Roof Lower Row 5) - 0x28AC0 - Rotated Shapers & Dots & Full Dots Door - 0x034F5 (Wooden Roof Stairs) - 0x28AC1 158225 - 0x28998 (Tinted Glass Door Panel) - True - Stars & Rotated Shapers Door - 0x28A61 (Tinted Glass Door) - 0x28998 @@ -421,7 +421,7 @@ Town Red Rooftop (Town): 158224 - 0x28B39 (Tall Hexagonal) - 0x079DF - True Town Wooden Rooftop (Town): -158240 - 0x28AD9 (Wooden Rooftop) - 0x28AC1 - Rotated Shapers & Dots & Eraser +158240 - 0x28AD9 (Wooden Rooftop) - 0x28AC1 - Rotated Shapers & Dots & Eraser & Full Dots Town Church (Town): 158227 - 0x28A69 (Church Lattice) - 0x03BB0 - True @@ -819,14 +819,14 @@ Mountain Path to Caves (Mountain Bottom Floor) - Mountain Bottom Floor Rock - 0x Door - 0x2D77D (Caves Entry) - 0x00FF8 158448 - 0x334E1 (Rock Control) - True - True -Caves (Caves) - Main Island - 0x2D73F - Main Island - 0x2D859 - Path to Challenge - 0x019A5: +Caves (Caves) - Main Island - 0x2D73F | 0x2D859 - Path to Challenge - 0x019A5: 158451 - 0x335AB (Elevator Inside Control) - True - Dots & Black/White Squares 158452 - 0x335AC (Elevator Upper Outside Control) - 0x335AB - Black/White Squares 158453 - 0x3369D (Elevator Lower Outside Control) - 0x335AB - Black/White Squares & Dots -158454 - 0x00190 (Blue Tunnel Right First 1) - True - Dots & Triangles -158455 - 0x00558 (Blue Tunnel Right First 2) - 0x00190 - Dots & Triangles -158456 - 0x00567 (Blue Tunnel Right First 3) - 0x00558 - Dots & Triangles -158457 - 0x006FE (Blue Tunnel Right First 4) - 0x00567 - Dots & Triangles +158454 - 0x00190 (Blue Tunnel Right First 1) - True - Dots & Triangles & Full Dots +158455 - 0x00558 (Blue Tunnel Right First 2) - 0x00190 - Dots & Triangles & Full Dots +158456 - 0x00567 (Blue Tunnel Right First 3) - 0x00558 - Dots & Triangles & Full Dots +158457 - 0x006FE (Blue Tunnel Right First 4) - 0x00567 - Dots & Triangles & Full Dots 158458 - 0x01A0D (Blue Tunnel Left First 1) - True - Symmetry & Triangles 158459 - 0x008B8 (Blue Tunnel Left Second 1) - True - Black/White Squares & Triangles 158460 - 0x00973 (Blue Tunnel Left Second 2) - 0x008B8 - Stars & Triangles @@ -849,12 +849,12 @@ Caves (Caves) - Main Island - 0x2D73F - Main Island - 0x2D859 - Path to Challeng 158479 - 0x288FC (Second Wooden Beam) - True - Black/White Squares & Shapers & Rotated Shapers 158480 - 0x289E7 (Third Wooden Beam) - True - Stars & Black/White Squares 158481 - 0x288AA (Fourth Wooden Beam) - True - Stars & Shapers -158482 - 0x17FB9 (Left Upstairs Single) - True - Shapers & Dots & Negative Shapers -158483 - 0x0A16B (Left Upstairs Left Row 1) - True - Dots -158484 - 0x0A2CE (Left Upstairs Left Row 2) - 0x0A16B - Stars & Dots -158485 - 0x0A2D7 (Left Upstairs Left Row 3) - 0x0A2CE - Dots & Black/White Squares & Stars + Same Colored Symbol & Stars -158486 - 0x0A2DD (Left Upstairs Left Row 4) - 0x0A2D7 - Shapers & Dots -158487 - 0x0A2EA (Left Upstairs Left Row 5) - 0x0A2DD - Rotated Shapers & Dots +158482 - 0x17FB9 (Left Upstairs Single) - True - Shapers & Dots & Negative Shapers & Full Dots +158483 - 0x0A16B (Left Upstairs Left Row 1) - True - Dots & Full Dots +158484 - 0x0A2CE (Left Upstairs Left Row 2) - 0x0A16B - Stars & Dots & Full Dots +158485 - 0x0A2D7 (Left Upstairs Left Row 3) - 0x0A2CE - Dots & Black/White Squares & Stars + Same Colored Symbol & Stars & Full Dots +158486 - 0x0A2DD (Left Upstairs Left Row 4) - 0x0A2D7 - Shapers & Dots & Full Dots +158487 - 0x0A2EA (Left Upstairs Left Row 5) - 0x0A2DD - Rotated Shapers & Dots & Full Dots 158488 - 0x0008F (Right Upstairs Left Row 1) - True - Dots & Invisible Dots 158489 - 0x0006B (Right Upstairs Left Row 2) - 0x0008F - Dots & Invisible Dots 158490 - 0x0008B (Right Upstairs Left Row 3) - 0x0006B - Dots & Invisible Dots @@ -913,7 +913,7 @@ Door - 0x09E87 (Town Shortcut) - 0x09E85 Final Room (Mountain Final Room) - Elevator - 0x339BB & 0x33961: 158522 - 0x0383A (Right Pillar 1) - True - Stars 158523 - 0x09E56 (Right Pillar 2) - 0x0383A - Stars & Dots -158524 - 0x09E5A (Right Pillar 3) - 0x09E56 - Dots +158524 - 0x09E5A (Right Pillar 3) - 0x09E56 - Dots & Full Dots 158525 - 0x33961 (Right Pillar 4) - 0x09E5A - Dots & Symmetry 158526 - 0x0383D (Left Pillar 1) - True - Dots 158527 - 0x0383F (Left Pillar 2) - 0x0383D - Black/White Squares @@ -927,6 +927,6 @@ Elevator (Mountain Final Room): 158533 - 0x3C114 (Elevator Entry 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 +158536 - 0x3D9A9 (Elevator Start) - 0x3D9AA & 7 Lasers | 0x3D9A8 & 7 Lasers - 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/WitnessLogicExpert.txt b/worlds/witness/WitnessLogicExpert.txt new file mode 100644 index 0000000000..96c7fdc07a --- /dev/null +++ b/worlds/witness/WitnessLogicExpert.txt @@ -0,0 +1,932 @@ +First Hallway (First Hallway) - Entry - True - Tutorial - 0x00182: +158000 - 0x00064 (Straight) - True - True +158001 - 0x00182 (Bend) - 0x00064 - True + +Tutorial (Tutorial) - Outside Tutorial - True: +158002 - 0x00293 (Front Center) - True - Dots +158003 - 0x00295 (Center Left) - 0x00293 - Dots +158004 - 0x002C2 (Front Left) - 0x00295 - Dots +158005 - 0x0A3B5 (Back Left) - True - Full Dots +158006 - 0x0A3B2 (Back Right) - True - Full Dots +158007 - 0x03629 (Gate Open) - 0x002C2 & 0x0A3B5 - Symmetry & Dots +158008 - 0x03505 (Gate Close) - 0x2FAF6 - False +158009 - 0x0C335 (Pillar) - True - Triangles - True +158010 - 0x0C373 (Patio Floor) - 0x0C335 - Dots + +Outside Tutorial (Outside Tutorial) - Outside Tutorial Path To Outpost - 0x03BA2: +158650 - 0x033D4 (Vault) - True - Full Dots & Squares & Black/White Squares +158651 - 0x03481 (Vault Box) - 0x033D4 - True +158013 - 0x0005D (Shed Row 1) - True - Full Dots +158014 - 0x0005E (Shed Row 2) - 0x0005D - Full Dots +158015 - 0x0005F (Shed Row 3) - 0x0005E - Full Dots +158016 - 0x00060 (Shed Row 4) - 0x0005F - Full Dots +158017 - 0x00061 (Shed Row 5) - 0x00060 - Full Dots +158018 - 0x018AF (Tree Row 1) - True - Squares & Black/White Squares +158019 - 0x0001B (Tree Row 2) - 0x018AF - Squares & Black/White Squares +158020 - 0x012C9 (Tree Row 3) - 0x0001B - Squares & Black/White Squares +158021 - 0x0001C (Tree Row 4) - 0x012C9 - Squares & Black/White Squares & Dots +158022 - 0x0001D (Tree Row 5) - 0x0001C - Squares & Black/White Squares & Dots +158023 - 0x0001E (Tree Row 6) - 0x0001D - Squares & Black/White Squares & Dots +158024 - 0x0001F (Tree Row 7) - 0x0001E - Squares & Black/White Squares & Full Dots +158025 - 0x00020 (Tree Row 8) - 0x0001F - Squares & Black/White Squares & Full Dots +158026 - 0x00021 (Tree Row 9) - 0x00020 - Squares & Black/White Squares & Full Dots +Door - 0x03BA2 (Outpost Path) - 0x0A3B5 + +Outside Tutorial Path To Outpost (Outside Tutorial) - Outside Tutorial Outpost - 0x0A170: +158011 - 0x0A171 (Outpost Entry Panel) - True - Full Dots & Triangles +Door - 0x0A170 (Outpost Entry) - 0x0A171 + +Outside Tutorial Outpost (Outside Tutorial) - Outside Tutorial - 0x04CA3: +158012 - 0x04CA4 (Outpost Exit Panel) - True - Full Dots & Shapers & Rotated Shapers +Door - 0x04CA3 (Outpost Exit) - 0x04CA4 +158600 - 0x17CFB (Discard) - True - Arrows + +Main Island () - Outside Tutorial - True: + +Outside Glass Factory (Glass Factory) - Main Island - True - Inside Glass Factory - 0x01A29: +158027 - 0x01A54 (Entry Panel) - True - Symmetry +Door - 0x01A29 (Entry) - 0x01A54 +158601 - 0x3C12B (Discard) - True - Arrows + +Inside Glass Factory (Glass Factory) - Inside Glass Factory Behind Back Wall - 0x0D7ED: +158028 - 0x00086 (Back Wall 1) - True - Symmetry & Dots +158029 - 0x00087 (Back Wall 2) - 0x00086 - Symmetry & Dots +158030 - 0x00059 (Back Wall 3) - 0x00087 - Symmetry & Dots +158031 - 0x00062 (Back Wall 4) - 0x00059 - Symmetry & Dots +158032 - 0x0005C (Back Wall 5) - 0x00062 - Symmetry & Dots +158033 - 0x0008D (Front 1) - 0x0005C - Symmetry +158034 - 0x00081 (Front 2) - 0x0008D - Symmetry +158035 - 0x00083 (Front 3) - 0x00081 - Symmetry +158036 - 0x00084 (Melting 1) - 0x00083 - Symmetry & Dots +158037 - 0x00082 (Melting 2) - 0x00084 - Symmetry & Dots +158038 - 0x0343A (Melting 3) - 0x00082 - Symmetry & Dots +Door - 0x0D7ED (Back Wall) - 0x0005C + +Inside Glass Factory Behind Back Wall (Glass Factory) - Boat - 0x17CC8: +158039 - 0x17CC8 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat + +Outside Symmetry Island (Symmetry Island) - Main Island - True - Symmetry Island Lower - 0x17F3E: +158040 - 0x000B0 (Lower Panel) - 0x0343A - Triangles +Door - 0x17F3E (Lower) - 0x000B0 + +Symmetry Island Lower (Symmetry Island) - Symmetry Island Upper - 0x18269: +158041 - 0x00022 (Right 1) - True - Symmetry & Triangles +158042 - 0x00023 (Right 2) - 0x00022 - Symmetry & Triangles +158043 - 0x00024 (Right 3) - 0x00023 - Symmetry & Triangles +158044 - 0x00025 (Right 4) - 0x00024 - Symmetry & Triangles +158045 - 0x00026 (Right 5) - 0x00025 - Symmetry & Triangles +158046 - 0x0007C (Back 1) - 0x00026 - Symmetry & Colored Dots & Dots +158047 - 0x0007E (Back 2) - 0x0007C - Symmetry & Colored Squares +158048 - 0x00075 (Back 3) - 0x0007E - Symmetry & Stars +158049 - 0x00073 (Back 4) - 0x00075 - Symmetry & Shapers +158050 - 0x00077 (Back 5) - 0x00073 - Symmetry & Triangles +158051 - 0x00079 (Back 6) - 0x00077 - Symmetry & Dots & Colored Dots & Eraser +158052 - 0x00065 (Left 1) - 0x00079 - Symmetry & Colored Dots & Triangles +158053 - 0x0006D (Left 2) - 0x00065 - Symmetry & Colored Dots & Triangles +158054 - 0x00072 (Left 3) - 0x0006D - Symmetry & Colored Dots & Triangles +158055 - 0x0006F (Left 4) - 0x00072 - Symmetry & Colored Dots & Triangles +158056 - 0x00070 (Left 5) - 0x0006F - Symmetry & Colored Dots & Triangles +158057 - 0x00071 (Left 6) - 0x00070 - Symmetry & Triangles +158058 - 0x00076 (Left 7) - 0x00071 - Symmetry & Triangles +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 (Upper Panel) - 0x00076 - Symmetry & Triangles +Door - 0x18269 (Upper) - 0x1C349 + +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 + +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 (First Gate) - 0x00055 + +Orchard Beyond First Gate (Orchard) - Orchard End - 0x03313: +158074 - 0x032F7 (Apple Tree 4) - 0x00055 - Environment +158075 - 0x032FF (Apple Tree 5) - 0x032F7 - Environment +Door - 0x03313 (Second Gate) - 0x032FF + +Orchard End (Orchard): + +Desert Outside (Desert) - Main Island - True - Desert Floodlight Room - 0x09FEE: +158652 - 0x0CC7B (Vault) - True - Full Dots & Stars & Stars + Same Colored Symbol & Eraser & Triangles & Shapers & Negative Shapers & Colored Squares +158653 - 0x0339E (Vault Box) - 0x0CC7B - True +158602 - 0x17CE7 (Discard) - True - Arrows +158076 - 0x00698 (Surface 1) - True - Reflection +158077 - 0x0048F (Surface 2) - 0x00698 - Reflection +158078 - 0x09F92 (Surface 3) - 0x0048F & 0x09FA0 - Reflection +158079 - 0x09FA0 (Surface 3 Control) - 0x0048F - True +158080 - 0x0A036 (Surface 4) - 0x09F92 - Reflection +158081 - 0x09DA6 (Surface 5) - 0x09F92 - Reflection +158082 - 0x0A049 (Surface 6) - 0x09F92 - Reflection +158083 - 0x0A053 (Surface 7) - 0x0A036 & 0x09DA6 & 0x0A049 - Reflection +158084 - 0x09F94 (Surface 8) - 0x0A053 & 0x09F86 - Reflection +158085 - 0x09F86 (Surface 8 Control) - 0x0A053 - True +158086 - 0x0C339 (Light Room Entry Panel) - 0x09F94 - True +Door - 0x09FEE (Light Room Entry) - 0x0C339 - True +158701 - 0x03608 (Laser Panel) - 0x012D7 & 0x0A15F - True +Laser - 0x012FB (Laser) - 0x03608 + +Desert Floodlight Room (Desert) - Desert Pond Room - 0x0C2C3: +158087 - 0x09FAA (Light Control) - True - True +158088 - 0x00422 (Light Room 1) - 0x09FAA - Reflection +158089 - 0x006E3 (Light Room 2) - 0x09FAA - Reflection +158090 - 0x0A02D (Light Room 3) - 0x09FAA & 0x00422 & 0x006E3 - Reflection +Door - 0x0C2C3 (Pond Room Entry) - 0x0A02D + +Desert Pond Room (Desert) - Desert Water Levels Room - 0x0A24B: +158091 - 0x00C72 (Pond Room 1) - True - Reflection +158092 - 0x0129D (Pond Room 2) - 0x00C72 - Reflection +158093 - 0x008BB (Pond Room 3) - 0x0129D - Reflection +158094 - 0x0078D (Pond Room 4) - 0x008BB - Reflection +158095 - 0x18313 (Pond Room 5) - 0x0078D - Reflection +158096 - 0x0A249 (Flood Room Entry Panel) - 0x18313 - Reflection +Door - 0x0A24B (Flood Room Entry) - 0x0A249 + +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 Room 1) - 0x1C260 & 0x1831C - Reflection +158106 - 0x01205 (Flood Room 2) - 0x04D18 & 0x1C260 & 0x1831C - Reflection +158107 - 0x181AB (Flood Room 3) - 0x01205 & 0x1C260 & 0x1831C - Reflection +158108 - 0x0117A (Flood Room 4) - 0x181AB & 0x1C260 & 0x1831C - Reflection +158109 - 0x17ECA (Flood Room 5) - 0x0117A & 0x1C260 & 0x1831C - Reflection +158110 - 0x18076 (Flood Room 6) - 0x17ECA & 0x1C260 & 0x1831C - Reflection +Door - 0x0C316 (Elevator Room Entry) - 0x18076 + +Desert Elevator Room (Desert) - Desert Lowest Level Inbetween Shortcuts - 0x012FB: +158111 - 0x17C31 (Final Transparent) - True - Reflection +158113 - 0x012D7 (Final Hexagonal) - 0x17C31 & 0x0A015 - Reflection +158114 - 0x0A015 (Final Hexagonal Control) - 0x17C31 - True +158115 - 0x0A15C (Final Bent 1) - True - Reflection +158116 - 0x09FFF (Final Bent 2) - 0x0A15C - Reflection +158117 - 0x0A15F (Final Bent 3) - 0x09FFF - Reflection + +Desert Lowest Level Inbetween Shortcuts (Desert): + +Outside Quarry (Quarry) - Main Island - True - Quarry Between Entrys - 0x09D6F: +158118 - 0x09E57 (Entry 1 Panel) - True - Squares & Black/White Squares & Triangles +158120 - 0x17CC4 (Elevator Control) - 0x0367C - Dots & Eraser +158603 - 0x17CF0 (Discard) - True - Arrows +158702 - 0x03612 (Laser Panel) - 0x0A3D0 & 0x0367C - Eraser & Triangles & Stars & Stars + Same Colored Symbol +Laser - 0x01539 (Laser) - 0x03612 +Door - 0x09D6F (Entry 1) - 0x09E57 + +Quarry Between Entrys (Quarry) - Quarry - 0x17C07: +158119 - 0x17C09 (Entry 2 Panel) - True - Shapers & Triangles +Door - 0x17C07 (Entry 2) - 0x17C09 + +Quarry (Quarry) - Quarry Mill Ground Floor - 0x02010: +158121 - 0x01E5A (Mill Entry Left Panel) - True - Squares & Black/White Squares & Stars & Stars + Same Colored Symbol +158122 - 0x01E59 (Mill Entry Right Panel) - True - Triangles +Door - 0x02010 (Mill Entry) - 0x01E59 & 0x01E5A + +Quarry Mill Ground Floor (Quarry Mill) - Quarry - 0x275FF - Quarry Mill Middle Floor - 0x03678 - Outside Quarry - 0x17CE8: +158123 - 0x275ED (Side Exit Panel) - True - True +Door - 0x275FF (Side Exit) - 0x275ED +158124 - 0x03678 (Lower Ramp Control) - True - Dots & Eraser +158145 - 0x17CAC (Roof Exit Panel) - True - True +Door - 0x17CE8 (Roof Exit) - 0x17CAC + +Quarry Mill Middle Floor (Quarry Mill) - Quarry Mill Ground Floor - 0x03675 - Quarry Mill Upper Floor - 0x03679: +158125 - 0x00E0C (Lower Row 1) - True - Dots & Eraser +158126 - 0x01489 (Lower Row 2) - 0x00E0C - Triangles & Eraser +158127 - 0x0148A (Lower Row 3) - 0x01489 - Triangles & Eraser +158128 - 0x014D9 (Lower Row 4) - 0x0148A - Triangles & Eraser +158129 - 0x014E7 (Lower Row 5) - 0x014D9 - Triangles & Eraser +158130 - 0x014E8 (Lower Row 6) - 0x014E7 - Triangles & Eraser +158131 - 0x03679 (Lower Lift Control) - 0x014E8 - Dots & Eraser + +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 (Upper Row 1) - True - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol +158135 - 0x005F1 (Upper Row 2) - 0x00557 - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol +158136 - 0x00620 (Upper Row 3) - 0x005F1 - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol +158137 - 0x009F5 (Upper Row 4) - 0x00620 - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol +158138 - 0x0146C (Upper Row 5) - 0x009F5 - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol +158139 - 0x3C12D (Upper Row 6) - 0x0146C - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol +158140 - 0x03686 (Upper Row 7) - 0x3C12D - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol +158141 - 0x014E9 (Upper Row 8) - 0x03686 - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol +158142 - 0x03677 (Stair Control) - True - Squares & Colored Squares & Eraser +Door - 0x0368A (Stairs) - 0x03677 +158143 - 0x3C125 (Control Room Left) - 0x0367C - Squares & Black/White Squares & Full Dots & Eraser +158144 - 0x0367C (Control Room Right) - 0x014E9 - Squares & Colored Squares & Triangles & Eraser & Stars & Stars + Same Colored Symbol + +Quarry Boathouse (Quarry Boathouse) - Quarry - True - Quarry Boathouse Upper Front - 0x03852 - Quarry Boathouse Behind Staircase - 0x2769B: +158146 - 0x034D4 (Intro Left) - True - Stars & Eraser +158147 - 0x021D5 (Intro Right) - True - Shapers & Eraser +158148 - 0x03852 (Ramp Height Control) - 0x034D4 & 0x021D5 - Rotated Shapers +158166 - 0x17CA6 (Boat Spawn) - True - Boat +Door - 0x2769B (Dock) - 0x17CA6 +Door - 0x27163 (Dock Invis Barrier) - 0x17CA6 + +Quarry Boathouse Behind Staircase (Quarry Boathouse) - Boat - 0x17CA6: + +Quarry Boathouse Upper Front (Quarry Boathouse) - Quarry Boathouse Upper Middle - 0x17C50: +158149 - 0x021B3 (Front Row 1) - True - Shapers & Eraser & Negative Shapers +158150 - 0x021B4 (Front Row 2) - 0x021B3 - Shapers & Eraser & Negative Shapers +158151 - 0x021B0 (Front Row 3) - 0x021B4 - Shapers & Eraser & Negative Shapers +158152 - 0x021AF (Front Row 4) - 0x021B0 - Shapers & Eraser & Negative Shapers +158153 - 0x021AE (Front Row 5) - 0x021AF - Shapers & Eraser & Negative Shapers +Door - 0x17C50 (First Barrier) - 0x021AE + +Quarry Boathouse Upper Middle (Quarry Boathouse) - Quarry Boathouse Upper Back - 0x03858: +158154 - 0x03858 (Ramp Horizontal Control) - True - Shapers & Eraser + +Quarry Boathouse Upper Back (Quarry Boathouse) - Quarry Boathouse Upper Middle - 0x3865F: +158155 - 0x38663 (Second Barrier Panel) - True - True +Door - 0x3865F (Second Barrier) - 0x38663 +158156 - 0x021B5 (Back First Row 1) - True - Stars & Stars + Same Colored Symbol & Eraser +158157 - 0x021B6 (Back First Row 2) - 0x021B5 - Stars & Stars + Same Colored Symbol & Eraser +158158 - 0x021B7 (Back First Row 3) - 0x021B6 - Stars & Stars + Same Colored Symbol & Eraser +158159 - 0x021BB (Back First Row 4) - 0x021B7 - Stars & Stars + Same Colored Symbol & Eraser +158160 - 0x09DB5 (Back First Row 5) - 0x021BB - Stars & Stars + Same Colored Symbol & Eraser +158161 - 0x09DB1 (Back First Row 6) - 0x09DB5 - Eraser & Shapers +158162 - 0x3C124 (Back First Row 7) - 0x09DB1 - Eraser & Shapers +158163 - 0x09DB3 (Back First Row 8) - 0x3C124 - Eraser & Shapers & Stars & Stars + Same Colored Symbol +158164 - 0x09DB4 (Back First Row 9) - 0x09DB3 - Eraser & Shapers & Stars & Stars + Same Colored Symbol +158165 - 0x275FA (Hook Control) - True - Shapers & Eraser +158167 - 0x0A3CB (Back Second Row 1) - 0x09DB4 - Stars & Eraser & Shapers & Negative Shapers & Stars + Same Colored Symbol +158168 - 0x0A3CC (Back Second Row 2) - 0x0A3CB - Stars & Eraser & Shapers & Negative Shapers & Stars + Same Colored Symbol +158169 - 0x0A3D0 (Back Second Row 3) - 0x0A3CC - Stars & Eraser & Shapers & Negative Shapers & Stars + Same Colored Symbol + +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 (Intro 6) - 0x0A8DC - Shadows Avoid +158172 - 0x0AC7A (Intro 7) - 0x0AC74 - Shadows Avoid +158173 - 0x0A8E0 (Intro 8) - 0x0AC7A - Shadows Avoid +158174 - 0x386FA (Far 1) - 0x0A8E0 - Shadows Avoid & Environment +158175 - 0x1C33F (Far 2) - 0x386FA - Shadows Avoid & Environment +158176 - 0x196E2 (Far 3) - 0x1C33F - Shadows Avoid & Environment +158177 - 0x1972A (Far 4) - 0x196E2 - Shadows Avoid & Environment +158178 - 0x19809 (Far 5) - 0x1972A - Shadows Avoid & Environment +158179 - 0x19806 (Far 6) - 0x19809 - Shadows Avoid & Environment +158180 - 0x196F8 (Far 7) - 0x19806 - Shadows Avoid & Environment +158181 - 0x1972F (Far 8) - 0x196F8 - Shadows Avoid & Environment +Door - 0x194B2 (Laser Entry Right) - 0x1972F +158182 - 0x19797 (Near 1) - 0x0A8E0 - Shadows Follow +158183 - 0x1979A (Near 2) - 0x19797 - Shadows Follow +158184 - 0x197E0 (Near 3) - 0x1979A - Shadows Follow +158185 - 0x197E8 (Near 4) - 0x197E0 - Shadows Follow +158186 - 0x197E5 (Near 5) - 0x197E8 - Shadows Follow +Door - 0x19665 (Laser Entry Left) - 0x197E5 + +Shadows Ledge (Shadows) - Shadows - 0x1855B - Quarry - 0x19865 & 0x0A2DF: +158187 - 0x334DC (Door Timer Inside) - True - True +158188 - 0x198B5 (Intro 1) - True - Shadows Avoid +158189 - 0x198BD (Intro 2) - 0x198B5 - Shadows Avoid +158190 - 0x198BF (Intro 3) - 0x198BD & 0x334DC & 0x19B24 - Shadows Avoid +Door - 0x19865 (Quarry Barrier) - 0x198BF +Door - 0x0A2DF (Quarry Barrier 2) - 0x198BF +158191 - 0x19771 (Intro 4) - 0x198BF - Shadows Avoid +158192 - 0x0A8DC (Intro 5) - 0x19771 - Shadows Avoid +Door - 0x1855B (Ledge Barrier) - 0x0A8DC +Door - 0x19ADE (Ledge Barrier 2) - 0x0A8DC + +Shadows Laser Room (Shadows): +158703 - 0x19650 (Laser Panel) - True - Shadows Avoid & Shadows Follow +Laser - 0x181B3 (Laser) - 0x19650 + +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 & Colored Squares & Triangles & Stars & Stars + Same Colored Symbol +Door - 0x01954 (Hedge Maze 1 Exit) - 0x00139 +Door - 0x01BEC (Pressure Plates 1 Exit) - 0x033EA + +Keep 2nd Maze (Keep) - Keep - 0x018CE - Keep 3rd Maze - True: +Door - 0x018CE (Hedge Maze 2 Shortcut) - 0x00139 +158194 - 0x019DC (Hedge Maze 2) - True - Environment +Door - 0x019D8 (Hedge Maze 2 Exit) - 0x019DC + +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) - 0x019E7 + +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) - 0x01A0F + +Keep 2nd Pressure Plate (Keep) - Keep 3rd Pressure Plate - True: +158199 - 0x0A3B9 (Reset Pressure Plates 2) - True - True +158200 - 0x01BE9 (Pressure Plates 2) - PP2 Weirdness - Pressure Plates & Stars & Stars + Same Colored Symbol & Squares & Black/White Squares & Shapers & Rotated Shapers +Door - 0x01BEA (Pressure Plates 2 Exit) - 0x01BE9 + +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 & Black/White Squares & Triangles & Shapers & Rotated Shapers +Door - 0x01CD5 (Pressure Plates 3 Exit) - 0x01CD3 + +Keep 4th Pressure Plate (Keep) - Shadows - 0x09E3D - Keep Tower - 0x01D40: +158203 - 0x0A3AD (Reset Pressure Plates 4) - True - True +158204 - 0x01D3F (Pressure Plates 4) - 0x0A3AD - Pressure Plates & Shapers & Triangles & Stars & Stars + Same Colored Symbol +Door - 0x01D40 (Pressure Plates 4 Exit) - 0x01D3F +158604 - 0x17D27 (Discard) - True - Arrows +158205 - 0x09E49 (Shadows Shortcut Panel) - True - True +Door - 0x09E3D (Shadows Shortcut) - 0x09E49 + +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 - Arrows + +Keep Tower (Keep) - Keep - 0x04F8F: +158206 - 0x0361B (Tower Shortcut Panel) - True - True +Door - 0x04F8F (Tower Shortcut) - 0x0361B +158704 - 0x0360E (Laser Panel Hedges) - 0x01A0F & 0x019E7 & 0x019DC & 0x00139 - Environment & Sound +158705 - 0x03317 (Laser Panel Pressure Plates) - 0x01BE9 - Shapers & Rotated Shapers & Triangles & Stars & Stars + Same Colored Symbol & Colored Squares & Black/White Squares +Laser - 0x014BB (Laser) - 0x0360E | 0x03317 + +Outside Monastery (Monastery) - Main Island - True - Inside Monastery - 0x0C128 & 0x0C153 - Monastery Garden - 0x03750: +158207 - 0x03713 (Shortcut Panel) - True - True +Door - 0x0364E (Shortcut) - 0x03713 +158208 - 0x00B10 (Entry Left) - True - True +158209 - 0x00C92 (Entry Right) - True - True +Door - 0x0C128 (Entry Inner) - 0x00B10 +Door - 0x0C153 (Entry Outer) - 0x00C92 +158210 - 0x00290 (Outside 1) - 0x09D9B - Environment +158211 - 0x00038 (Outside 2) - 0x09D9B & 0x00290 - Environment +158212 - 0x00037 (Outside 3) - 0x09D9B & 0x00038 - Environment +Door - 0x03750 (Garden Entry) - 0x00037 +158706 - 0x17CA4 (Laser Panel) - 0x193A6 - True +Laser - 0x17C65 (Laser) - 0x17CA4 + +Inside Monastery (Monastery): +158213 - 0x09D9B (Shutters Control) - True - Dots +158214 - 0x193A7 (Inside 1) - 0x00037 - Environment +158215 - 0x193AA (Inside 2) - 0x193A7 - Environment +158216 - 0x193AB (Inside 3) - 0x193AA - Environment +158217 - 0x193A6 (Inside 4) - 0x193AB - Environment + +Monastery Garden (Monastery): + +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 Entry Panel) - True - Squares & Black/White Squares & Shapers & Triangles +Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8 +158707 - 0x09F98 (Desert Laser Redirect) - True - True +158220 - 0x18590 (Transparent) - True - Symmetry & Environment +158221 - 0x28AE3 (Vines) - 0x18590 - Shadows Follow & Environment +158222 - 0x28938 (Apple Tree) - 0x28AE3 - Environment +158223 - 0x079DF (Triple Exit) - 0x28938 - Shadows Avoid & Environment & Reflection +158235 - 0x2899C (Wooden Roof Lower Row 1) - True - Triangles & Full Dots +158236 - 0x28A33 (Wooden Roof Lower Row 2) - 0x2899C - Triangles & Full Dots +158237 - 0x28ABF (Wooden Roof Lower Row 3) - 0x28A33 - Triangles & Full Dots +158238 - 0x28AC0 (Wooden Roof Lower Row 4) - 0x28ABF - Triangles & Full Dots +158239 - 0x28AC1 (Wooden Roof Lower Row 5) - 0x28AC0 - Triangles & Full Dots +Door - 0x034F5 (Wooden Roof Stairs) - 0x28AC1 +158225 - 0x28998 (Tinted Glass Door Panel) - True - Stars & Rotated Shapers & Stars + Same Colored Symbol +Door - 0x28A61 (Tinted Glass Door) - 0x28A0D +158226 - 0x28A0D (Church Entry Panel) - 0x28998 - Stars & RGB & Environment +Door - 0x03BB0 (Church Entry) - 0x03C08 +158228 - 0x28A79 (Maze Stair Control) - True - Environment +Door - 0x28AA2 (Maze Stairs) - 0x28A79 +158241 - 0x17F5F (Windmill Entry Panel) - True - Dots +Door - 0x1845B (Windmill Entry) - 0x17F5F + +Town Inside Cargo Box (Town): +158606 - 0x17D01 (Cargo Box Discard) - True - Arrows + +Town Maze Rooftop (Town) - Town Red Rooftop - 0x2896A: +158229 - 0x2896A (Maze Rooftop Bridge Control) - True - Shapers + +Town Red Rooftop (Town): +158607 - 0x17C71 (Rooftop Discard) - True - Arrows +158230 - 0x28AC7 (Red Rooftop 1) - True - Symmetry & Shapers +158231 - 0x28AC8 (Red Rooftop 2) - 0x28AC7 - Symmetry & Shapers +158232 - 0x28ACA (Red Rooftop 3) - 0x28AC8 - Symmetry & Shapers +158233 - 0x28ACB (Red Rooftop 4) - 0x28ACA - Symmetry & Shapers +158234 - 0x28ACC (Red Rooftop 5) - 0x28ACB - Symmetry & Shapers +158224 - 0x28B39 (Tall Hexagonal) - 0x079DF - Reflection + +Town Wooden Rooftop (Town): +158240 - 0x28AD9 (Wooden Rooftop) - 0x28AC1 - Triangles & Full Dots & Eraser + +Town Church (Town): +158227 - 0x28A69 (Church Lattice) - 0x03BB0 - Environment + +RGB House (Town) - RGB Room - 0x2897B: +158242 - 0x034E4 (Sound Room Left) - True - Sound +158243 - 0x034E3 (Sound Room Right) - True - Sound & Sound Dots +Door - 0x2897B (RGB House Stairs) - 0x034E4 & 0x034E3 + +RGB Room (Town): +158244 - 0x334D8 (RGB Control) - True - Rotated Shapers & RGB & Squares & Colored Squares & Triangles +158245 - 0x03C0C (RGB Room Left) - 0x334D8 - RGB & Squares & Colored Squares & Black/White Squares & Eraser +158246 - 0x03C08 (RGB Room Right) - 0x334D8 & 0x03C0C - RGB & Symmetry & Dots & Colored Dots & Triangles + +Town Tower (Town Tower) - Town - True - Town Tower Top - 0x27798 & 0x27799 & 0x2779A & 0x2779C: +Door - 0x27798 (First Door) - 0x28ACC +Door - 0x2779C (Second Door) - 0x28AD9 +Door - 0x27799 (Third Door) - 0x28A69 +Door - 0x2779A (Fourth Door) - 0x28B39 + +Town Tower Top (Town): +158708 - 0x032F5 (Laser Panel) - True - True +Laser - 0x032F9 (Laser) - 0x032F5 + +Windmill Interior (Windmill) - Theater - 0x17F88: +158247 - 0x17D02 (Turn Control) - True - Dots +158248 - 0x17F89 (Theater Entry Panel) - True - Squares & Black/White Squares & Eraser & Triangles +Door - 0x17F88 (Theater Entry) - 0x17F89 + +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 (Exit Left Panel) - True - Black/White Squares & Stars & Stars + Same Colored Symbol & Eraser +158250 - 0x33AB2 (Exit Right Panel) - True - Eraser & Triangles & Shapers +Door - 0x0A16D (Exit Left) - 0x0A168 +Door - 0x3CCDF (Exit Right) - 0x33AB2 +158608 - 0x17CF7 (Discard) - True - Arrows + +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 (First Row 1) - True - Sound +158253 - 0x00767 (First Row 2) - 0x002C4 - Sound +158254 - 0x002C6 (First Row 3) - 0x00767 - Sound +158255 - 0x0070E (Second Row 1) - 0x002C6 - Sound +158256 - 0x0070F (Second Row 2) - 0x0070E - Sound +158257 - 0x0087D (Second Row 3) - 0x0070F - Sound +158258 - 0x002C7 (Second Row 4) - 0x0087D - Sound +158259 - 0x17CAB (Popup Wall Control) - 0x002C7 - True +Door - 0x1475B (Popup Wall) - 0x17CAB +158260 - 0x0026D (Popup Wall 1) - 0x1475B - Sound & Sound Dots & Symmetry +158261 - 0x0026E (Popup Wall 2) - 0x0026D - Sound & Sound Dots & Symmetry +158262 - 0x0026F (Popup Wall 3) - 0x0026E - Sound & Sound Dots & Symmetry +158263 - 0x00C3F (Popup Wall 4) - 0x0026F - Sound & Sound Dots & Symmetry +158264 - 0x00C41 (Popup Wall 5) - 0x00C3F - Sound & Sound Dots & Symmetry +158265 - 0x014B2 (Popup Wall 6) - 0x00C41 - Sound & Sound Dots & Symmetry +158709 - 0x03616 (Laser Panel) - 0x014B2 - True +Laser - 0x00274 (Laser) - 0x03616 +158266 - 0x337FA (Laser Shortcut Panel) - True - True +Door - 0x3873B (Laser Shortcut) - 0x337FA + +Outside Jungle River (River) - Main Island - True - Monastery Garden - 0x0CF2A: +158267 - 0x17CAA (Monastery Shortcut Panel) - True - Environment +Door - 0x0CF2A (Monastery Shortcut) - 0x17CAA +158663 - 0x15ADD (Vault) - True - Environment & Black/White Squares & Dots +158664 - 0x03702 (Vault Box) - 0x15ADD - True + +Outside Bunker (Bunker) - Main Island - True - Bunker - 0x0C2A4: +158268 - 0x17C2E (Entry Panel) - True - Squares & Black/White Squares & Colored Squares +Door - 0x0C2A4 (Entry) - 0x17C2E + +Bunker (Bunker) - Bunker Glass Room - 0x17C79: +158269 - 0x09F7D (Intro Left 1) - True - Squares & Colored Squares +158270 - 0x09FDC (Intro Left 2) - 0x09F7D - Squares & Colored Squares & Black/White Squares +158271 - 0x09FF7 (Intro Left 3) - 0x09FDC - Squares & Colored Squares & Black/White Squares +158272 - 0x09F82 (Intro Left 4) - 0x09FF7 - Squares & Colored Squares & Black/White Squares +158273 - 0x09FF8 (Intro Left 5) - 0x09F82 - Squares & Colored Squares & Black/White Squares +158274 - 0x09D9F (Intro Back 1) - 0x09FF8 - Squares & Colored Squares & Black/White Squares +158275 - 0x09DA1 (Intro Back 2) - 0x09D9F - Squares & Colored Squares +158276 - 0x09DA2 (Intro Back 3) - 0x09DA1 - Squares & Colored Squares +158277 - 0x09DAF (Intro Back 4) - 0x09DA2 - Squares & Colored Squares +158278 - 0x0A099 (Tinted Glass Door Panel) - 0x09DAF - True +Door - 0x17C79 (Tinted Glass Door) - 0x0A099 + +Bunker Glass Room (Bunker) - Bunker Ultraviolet Room - 0x0C2A3: +158279 - 0x0A010 (Glass Room 1) - True - Squares & Colored Squares & RGB & Environment +158280 - 0x0A01B (Glass Room 2) - 0x0A010 - Squares & Colored Squares & Black/White Squares & RGB & Environment +158281 - 0x0A01F (Glass Room 3) - 0x0A01B - Squares & Colored Squares & Black/White Squares & RGB & Environment +Door - 0x0C2A3 (UV Room Entry) - 0x0A01F + +Bunker Ultraviolet Room (Bunker) - Bunker Elevator Section - 0x0A08D: +158282 - 0x34BC5 (Drop-Down Door Open) - True - True +158283 - 0x34BC6 (Drop-Down Door Close) - 0x34BC5 - True +158284 - 0x17E63 (UV Room 1) - 0x34BC5 - Squares & Colored Squares & RGB & Environment +158285 - 0x17E67 (UV Room 2) - 0x17E63 & 0x34BC6 - Squares & Colored Squares & Black/White Squares & RGB +Door - 0x0A08D (Elevator Room Entry) - 0x17E67 + +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 - Rotated Shapers & Black/White Squares & Triangles +Door - 0x00C1C (Entry) - 0x0056E + +Swamp Entry Area (Swamp) - Swamp Sliding Bridge - TrueOneWay: +158288 - 0x00469 (Intro Front 1) - True - Black/White Squares & Shapers +158289 - 0x00472 (Intro Front 2) - 0x00469 - Black/White Squares & Shapers & Rotated Shapers +158290 - 0x00262 (Intro Front 3) - 0x00472 - Black/White Squares & Rotated Shapers +158291 - 0x00474 (Intro Front 4) - 0x00262 - Black/White Squares & Shapers & Rotated Shapers +158292 - 0x00553 (Intro Front 5) - 0x00474 - Black/White Squares & Shapers & Rotated Shapers +158293 - 0x0056F (Intro Front 6) - 0x00553 - Black/White Squares & Shapers & Rotated Shapers +158294 - 0x00390 (Intro Back 1) - 0x0056F - Shapers & Triangles +158295 - 0x010CA (Intro Back 2) - 0x00390 - Shapers & Rotated Shapers & Triangles +158296 - 0x00983 (Intro Back 3) - 0x010CA - Rotated Shapers & Triangles +158297 - 0x00984 (Intro Back 4) - 0x00983 - Shapers & Rotated Shapers & Triangles +158298 - 0x00986 (Intro Back 5) - 0x00984 - Shapers & Rotated Shapers & Triangles +158299 - 0x00985 (Intro Back 6) - 0x00986 - Rotated Shapers & Triangles & Black/White Squares +158300 - 0x00987 (Intro Back 7) - 0x00985 - Shapers & Rotated Shapers & Triangles & Black/White Squares +158301 - 0x181A9 (Intro Back 8) - 0x00987 - Rotated Shapers & Triangles & Black/White Squares + +Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Near Platform - 0x00609: +158302 - 0x00609 (Sliding Bridge) - True - Shapers & Black/White Squares + +Swamp Near Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat - 0x38AE6 - Swamp Between Bridges Near - 0x184B7 - Swamp Sliding Bridge - TrueOneWay: +158313 - 0x00982 (Platform Row 1) - True - Rotated Shapers +158314 - 0x0097F (Platform Row 2) - 0x00982 - Rotated Shapers +158315 - 0x0098F (Platform Row 3) - 0x0097F - Rotated Shapers +158316 - 0x00990 (Platform Row 4) - 0x0098F - Rotated Shapers +Door - 0x184B7 (Between Bridges First Door) - 0x00990 +158317 - 0x17C0D (Platform Shortcut Left Panel) - True - Rotated Shapers +158318 - 0x17C0E (Platform Shortcut Right Panel) - True - Rotated Shapers +Door - 0x38AE6 (Platform Shortcut Door) - 0x17C0E +Door - 0x04B7F (Cyan Water Pump) - 0x00006 + +Swamp Cyan Underwater (Swamp): +158307 - 0x00002 (Cyan Underwater 1) - True - Shapers & Negative Shapers & Black/White Squares +158308 - 0x00004 (Cyan Underwater 2) - 0x00002 - Shapers & Negative Shapers & Triangles +158309 - 0x00005 (Cyan Underwater 3) - 0x00004 - Shapers & Negative Shapers & Triangles & Black/White Squares +158310 - 0x013E6 (Cyan Underwater 4) - 0x00005 - Shapers & Negative Shapers & Triangles & Black/White Squares +158311 - 0x00596 (Cyan Underwater 5) - 0x013E6 - Shapers & Negative Shapers & Triangles & Black/White Squares +158312 - 0x18488 (Cyan Underwater Sliding Bridge Control) - True - Shapers + +Swamp Between Bridges Near (Swamp) - Swamp Between Bridges Far - 0x18507: +158303 - 0x00999 (Between Bridges Near Row 1) - 0x00990 - Rotated Shapers +158304 - 0x0099D (Between Bridges Near Row 2) - 0x00999 - Rotated Shapers +158305 - 0x009A0 (Between Bridges Near Row 3) - 0x0099D - Rotated Shapers +158306 - 0x009A1 (Between Bridges Near Row 4) - 0x009A0 - Rotated Shapers +Door - 0x18507 (Between Bridges Second Door) - 0x009A1 + +Swamp Between Bridges Far (Swamp) - Swamp Red Underwater - 0x183F2 - Swamp Rotating Bridge - TrueOneWay: +158319 - 0x00007 (Between Bridges Far Row 1) - 0x009A1 - Rotated Shapers & Full Dots +158320 - 0x00008 (Between Bridges Far Row 2) - 0x00007 - Rotated Shapers & Full Dots +158321 - 0x00009 (Between Bridges Far Row 3) - 0x00008 - Rotated Shapers & Shapers & Full Dots +158322 - 0x0000A (Between Bridges Far Row 4) - 0x00009 - Rotated Shapers & Shapers & Full Dots +Door - 0x183F2 (Red Water Pump) - 0x00596 + +Swamp Red Underwater (Swamp) - Swamp Maze - 0x014D1: +158323 - 0x00001 (Red Underwater 1) - True - Shapers & Negative Shapers & Full Dots +158324 - 0x014D2 (Red Underwater 2) - True - Shapers & Negative Shapers & Full Dots +158325 - 0x014D4 (Red Underwater 3) - True - Shapers & Negative Shapers & Full Dots +158326 - 0x014D1 (Red Underwater 4) - True - Shapers & Negative Shapers & Full Dots +Door - 0x305D5 (Red Underwater Exit) - 0x014D1 + +Swamp Rotating Bridge (Swamp) - Swamp Between Bridges Far - 0x181F5 - Swamp Near Boat - 0x181F5 - Swamp Purple Area - 0x181F5: +158327 - 0x181F5 (Rotating Bridge) - True - Rotated Shapers & Shapers & Stars & Colored Squares & Triangles & Stars + Same Colored Symbol + +Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482: +158328 - 0x09DB8 (Boat Spawn) - True - Boat +158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Shapers & Full Dots +158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers & Shapers & Full Dots +158331 - 0x00C2E (Beyond Rotating Bridge 3) - 0x00A1E - Shapers & Full Dots +158332 - 0x00E3A (Beyond Rotating Bridge 4) - 0x00C2E - Shapers & Full Dots +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 (Purple Underwater) - True - Shapers & Triangles & Black/White Squares & Rotated Shapers + +Swamp Blue Underwater (Swamp): +158334 - 0x009AB (Blue Underwater 1) - True - Shapers & Negative Shapers +158335 - 0x009AD (Blue Underwater 2) - 0x009AB - Shapers & Negative Shapers +158336 - 0x009AE (Blue Underwater 3) - 0x009AD - Shapers & Negative Shapers +158337 - 0x009AF (Blue Underwater 4) - 0x009AE - Shapers & Negative Shapers +158338 - 0x00006 (Blue Underwater 5) - 0x009AF - Shapers & 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 (Laser Shortcut Left Panel) - True - Shapers & Stars & Negative Shapers & Stars + Same Colored Symbol +158342 - 0x17C02 (Laser Shortcut Right Panel) - 0x17C05 - Shapers & Negative Shapers & Rotated Shapers +Door - 0x2D880 (Laser Shortcut) - 0x17C02 + +Treehouse Entry Area (Treehouse) - Treehouse Between Doors - 0x0C309: +158343 - 0x17C95 (Boat Spawn) - True - Boat +158344 - 0x0288C (First Door Panel) - True - Stars & Stars + Same Colored Symbol & Triangles +Door - 0x0C309 (First Door) - 0x0288C + +Treehouse Between Doors (Treehouse) - Treehouse Yellow Bridge - 0x0C310: +158345 - 0x02886 (Second Door Panel) - True - Stars & Stars + Same Colored Symbol & Triangles +Door - 0x0C310 (Second Door) - 0x02886 + +Treehouse Yellow Bridge (Treehouse) - Treehouse After Yellow Bridge - 0x17DC4: +158346 - 0x17D72 (Yellow Bridge 1) - True - Stars & Stars + Same Colored Symbol & Triangles +158347 - 0x17D8F (Yellow Bridge 2) - 0x17D72 - Stars & Stars + Same Colored Symbol & Triangles +158348 - 0x17D74 (Yellow Bridge 3) - 0x17D8F - Stars & Stars + Same Colored Symbol & Triangles +158349 - 0x17DAC (Yellow Bridge 4) - 0x17D74 - Stars & Stars + Same Colored Symbol & Triangles +158350 - 0x17D9E (Yellow Bridge 5) - 0x17DAC - Stars & Stars + Same Colored Symbol & Triangles +158351 - 0x17DB9 (Yellow Bridge 6) - 0x17D9E - Stars & Stars + Same Colored Symbol & Triangles +158352 - 0x17D9C (Yellow Bridge 7) - 0x17DB9 - Stars & Stars + Same Colored Symbol & Triangles +158353 - 0x17DC2 (Yellow Bridge 8) - 0x17D9C - Stars & Stars + Same Colored Symbol & Triangles +158354 - 0x17DC4 (Yellow Bridge 9) - 0x17DC2 - Stars & Stars + Same Colored Symbol & Triangles + +Treehouse After Yellow Bridge (Treehouse) - Treehouse Junction - 0x0A181: +158355 - 0x0A182 (Third Door Panel) - True - Stars & Stars + Same Colored Symbol & Triangles & Colored Squares +Door - 0x0A181 (Third 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 & Full Dots +158358 - 0x17DC7 (First Purple Bridge 2) - 0x17DC8 - Stars & Full Dots +158359 - 0x17CE4 (First Purple Bridge 3) - 0x17DC7 - Stars & Full Dots +158360 - 0x17D2D (First Purple Bridge 4) - 0x17CE4 - Stars & Full Dots +158361 - 0x17D6C (First Purple Bridge 5) - 0x17D2D - Stars & Full Dots + +Treehouse Right Orange Bridge (Treehouse) - Treehouse Bridge Platform - 0x17DA2: +158391 - 0x17D88 (Right Orange Bridge 1) - True - Stars & Stars + Same Colored Symbol & Triangles +158392 - 0x17DB4 (Right Orange Bridge 2) - 0x17D88 - Stars & Stars + Same Colored Symbol & Triangles +158393 - 0x17D8C (Right Orange Bridge 3) - 0x17DB4 - Stars & Stars + Same Colored Symbol & Triangles +158394 - 0x17CE3 (Right Orange Bridge 4 & Directional) - 0x17D8C - Triangles +158395 - 0x17DCD (Right Orange Bridge 5) - 0x17CE3 - Stars & Stars + Same Colored Symbol & Triangles +158396 - 0x17DB2 (Right Orange Bridge 6) - 0x17DCD - Stars & Stars + Same Colored Symbol & Triangles +158397 - 0x17DCC (Right Orange Bridge 7) - 0x17DB2 - Stars & Stars + Same Colored Symbol & Triangles +158398 - 0x17DCA (Right Orange Bridge 8) - 0x17DCC - Stars & Stars + Same Colored Symbol & Triangles +158399 - 0x17D8E (Right Orange Bridge 9) - 0x17DCA - Stars & Stars + Same Colored Symbol & Triangles +158400 - 0x17DB7 (Right Orange Bridge 10 & Directional) - 0x17D8E - Triangles +158401 - 0x17DB1 (Right Orange Bridge 11) - 0x17DB7 - Stars & Stars + Same Colored Symbol & Triangles +158402 - 0x17DA2 (Right Orange Bridge 12) - 0x17DB1 - Stars & Stars + Same Colored Symbol & Triangles + +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 & Black/White Squares & Triangles & Stars + Same Colored Symbol +158363 - 0x17D99 (Second Purple Bridge 2) - 0x17D9B - Stars & Black/White Squares & Triangles & Stars + Same Colored Symbol +158364 - 0x17DAA (Second Purple Bridge 3) - 0x17D99 - Stars & Black/White Squares & Triangles & Stars + Same Colored Symbol +158365 - 0x17D97 (Second Purple Bridge 4) - 0x17DAA - Stars & Black/White Squares & Colored Squares & Triangles & Stars + Same Colored Symbol +158366 - 0x17BDF (Second Purple Bridge 5) - 0x17D97 - Stars & Colored Squares & Triangles & Stars + Same Colored Symbol +158367 - 0x17D91 (Second Purple Bridge 6) - 0x17BDF - Stars & Colored Squares & Triangles & Stars + Same Colored Symbol +158368 - 0x17DC6 (Second Purple Bridge 7) - 0x17D91 - Stars & Colored Squares & Triangles & Stars + Same Colored Symbol + +Treehouse Left Orange Bridge (Treehouse) - Treehouse Laser Room Front Platform - 0x17DDE - Treehouse Laser Room Back Platform - 0x17DDB: +158376 - 0x17DB3 (Left Orange Bridge 1) - True - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers +158377 - 0x17DB5 (Left Orange Bridge 2) - 0x17DB3 - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers +158378 - 0x17DB6 (Left Orange Bridge 3) - 0x17DB5 - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers +158379 - 0x17DC0 (Left Orange Bridge 4) - 0x17DB6 - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers +158380 - 0x17DD7 (Left Orange Bridge 5) - 0x17DC0 - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers +158381 - 0x17DD9 (Left Orange Bridge 6) - 0x17DD7 - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers +158382 - 0x17DB8 (Left Orange Bridge 7) - 0x17DD9 - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers +158383 - 0x17DDC (Left Orange Bridge 8) - 0x17DB8 - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers +158384 - 0x17DD1 (Left Orange Bridge 9 & Directional) - 0x17DDC - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers & Black/White Squares +158385 - 0x17DDE (Left Orange Bridge 10) - 0x17DD1 - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers & Black/White Squares +158386 - 0x17DE3 (Left Orange Bridge 11) - 0x17DDE - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers & Black/White Squares +158387 - 0x17DEC (Left Orange Bridge 12) - 0x17DE3 - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers & Black/White Squares +158388 - 0x17DAE (Left Orange Bridge 13) - 0x17DEC & 0x03613 - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers & Triangles +158389 - 0x17DB0 (Left Orange Bridge 14) - 0x17DAE - Stars & Black/White Squares & Stars + Same Colored Symbol +158390 - 0x17DDB (Left Orange Bridge 15) - 0x17DB0 - Stars & Black/White Squares & Stars + Same Colored Symbol + +Treehouse Green Bridge (Treehouse): +158369 - 0x17E3C (Green Bridge 1) - True - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol +158370 - 0x17E4D (Green Bridge 2) - 0x17E3C - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol +158371 - 0x17E4F (Green Bridge 3) - 0x17E4D - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol +158372 - 0x17E52 (Green Bridge 4 & Directional) - 0x17E4F - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol +158373 - 0x17E5B (Green Bridge 5) - 0x17E52 - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol +158374 - 0x17E5F (Green Bridge 6) - 0x17E5B - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol & Triangles +158375 - 0x17E61 (Green Bridge 7) - 0x17E5F - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol & Triangles +158610 - 0x17FA9 (Green Bridge Discard) - 0x17E61 - Arrows + +Treehouse Laser Room Front Platform (Treehouse) - Treehouse Laser Room - 0x0C323: +Door - 0x0C323 (Laser House Entry) - 0x17DA2 & 0x2700B & 0x17DEC + +Treehouse Laser Room Back Platform (Treehouse): +158611 - 0x17FA0 (Laser Discard) - True - Arrows + +Treehouse Laser Room (Treehouse): +158712 - 0x03613 (Laser Panel) - True - True +158403 - 0x17CBC (Laser House Door Timer Inside) - True - True +Laser - 0x028A4 (Laser) - 0x03613 + +Mountainside (Mountainside) - Main Island - True - Mountaintop - True: +158612 - 0x17C42 (Discard) - True - Arrows +158665 - 0x002A6 (Vault) - True - Symmetry & Colored Squares & Triangles & Stars & Stars + Same Colored Symbol +158666 - 0x03542 (Vault Box) - 0x002A6 - True + +Mountaintop (Mountaintop) - 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 & Black/White Squares & Stars + Same Colored Symbol & Triangles +158800 - 0xFFF00 (Box Long) - 7 Lasers & 11 Lasers & 0x17C34 - True + +Mountain Top Layer (Mountain Floor 1) - Mountain Top Layer Bridge - 0x09E39: +158408 - 0x09E39 (Light Bridge Controller) - True - Eraser & Triangles + +Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Floor 2 - 0x09E54: +158409 - 0x09E7A (Right Row 1) - True - Black/White Squares & Dots +158410 - 0x09E71 (Right Row 2) - 0x09E7A - Black/White Squares & Triangles +158411 - 0x09E72 (Right Row 3) - 0x09E71 - Black/White Squares & Triangles & Shapers +158412 - 0x09E69 (Right Row 4) - 0x09E72 - Stars & Black/White Squares & Stars + Same Colored Symbol & Rotated Shapers +158413 - 0x09E7B (Right Row 5) - 0x09E69 - Stars & Black/White Squares & Stars + Same Colored Symbol & Eraser & Dots & Triangles & Shapers +158414 - 0x09E73 (Left Row 1) - True - Black/White Squares & Triangles +158415 - 0x09E75 (Left Row 2) - 0x09E73 - Black/White Squares & Shapers & Rotated Shapers +158416 - 0x09E78 (Left Row 3) - 0x09E75 - Stars & Triangles & Stars + Same Colored Symbol & Shapers & Rotated Shapers +158417 - 0x09E79 (Left Row 4) - 0x09E78 - Stars & Colored Squares & Stars + Same Colored Symbol & Triangles +158418 - 0x09E6C (Left Row 5) - 0x09E79 - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol +158419 - 0x09E6F (Left Row 6) - 0x09E6C - Symmetry & Stars & Colored Squares & Black/White Squares & Stars + Same Colored Symbol & Symmetry +158420 - 0x09E6B (Left Row 7) - 0x09E6F - Symmetry & Full Dots & Triangles +158421 - 0x33AF5 (Back Row 1) - True - Symmetry & Black/White Squares & Triangles +158422 - 0x33AF7 (Back Row 2) - 0x33AF5 - Symmetry & Stars & Triangles & Stars + Same Colored Symbol +158423 - 0x09F6E (Back Row 3) - 0x33AF7 - Symmetry & Stars & Shapers & Stars + Same Colored Symbol +158424 - 0x09EAD (Trash Pillar 1) - True - Rotated Shapers & Stars +158425 - 0x09EAF (Trash Pillar 2) - 0x09EAD - Rotated Shapers & Triangles +Door - 0x09E54 (Exit) - 0x09EAF & 0x09F6E & 0x09E6B & 0x09E7B + +Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 0x09FFB - Mountain Floor 2 Blue Bridge - 0x09E86: +158426 - 0x09FD3 (Near Row 1) - True - Stars & Colored Squares & Stars + Same Colored Symbol +158427 - 0x09FD4 (Near Row 2) - 0x09FD3 - Stars & Triangles & Stars + Same Colored Symbol +158428 - 0x09FD6 (Near Row 3) - 0x09FD4 - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol +158429 - 0x09FD7 (Near Row 4) - 0x09FD6 - Stars +158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Stars & Stars + Same Colored Symbol & Rotated Shapers & Eraser +Door - 0x09FFB (Staircase Near) - 0x09FD8 + +Mountain Floor 2 Blue Bridge (Mountain Floor 2) - Mountain Floor 2 Beyond Bridge - TrueOneWay - Mountain Floor 2 At Door - TrueOneWay: + +Mountain Floor 2 At Door (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD: +Door - 0x09EDD (Elevator Room Entry) - 0x09ED8 & 0x09E86 + +Mountain Floor 2 Light Bridge Room Near (Mountain Floor 2): +158431 - 0x09E86 (Light Bridge Controller Near) - True - Shapers & Dots + +Mountain Floor 2 Beyond Bridge (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Far - 0x09E07: +158432 - 0x09FCC (Far Row 1) - True - Triangles +158433 - 0x09FCE (Far Row 2) - 0x09FCC - Black/White Squares & Stars & Stars + Same Colored Symbol +158434 - 0x09FCF (Far Row 3) - 0x09FCE - Stars & Triangles & Stars + Same Colored Symbol +158435 - 0x09FD0 (Far Row 4) - 0x09FCF - Rotated Shapers & Negative Shapers +158436 - 0x09FD1 (Far Row 5) - 0x09FD0 - Dots +158437 - 0x09FD2 (Far Row 6) - 0x09FD1 - Rotated Shapers +Door - 0x09E07 (Staircase Far) - 0x09FD2 + +Mountain Floor 2 Light Bridge Room Far (Mountain Floor 2): +158438 - 0x09ED8 (Light Bridge Controller Far) - True - Shapers & Dots + +Mountain Floor 2 Elevator Room (Mountain Floor 2) - Mountain Floor 2 Elevator - TrueOneWay: +158613 - 0x17F93 (Elevator Discard) - True - Arrows + +Mountain Floor 2 Elevator (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EEB - Mountain Third Layer - 0x09EEB: +158439 - 0x09EEB (Elevator Control Panel) - True - Dots + +Mountain Third Layer (Mountain Bottom Floor) - Mountain Floor 2 Elevator - TrueOneWay - Mountain Bottom Floor - 0x09F89: +158440 - 0x09FC1 (Giant Puzzle Bottom Left) - True - Shapers & Negative Shapers +158441 - 0x09F8E (Giant Puzzle Bottom Right) - True - Shapers & Eraser +158442 - 0x09F01 (Giant Puzzle Top Right) - True - Shapers & Eraser +158443 - 0x09EFF (Giant Puzzle Top Left) - True - Shapers & Eraser +158444 - 0x09FDA (Giant Puzzle) - 0x09FC1 & 0x09F8E & 0x09F01 & 0x09EFF - Shapers & Symmetry +Door - 0x09F89 (Exit) - 0x09FDA + +Mountain Bottom Floor (Mountain Bottom Floor) - Mountain Bottom Floor Rock - 0x17FA2 - Final Room - 0x0C141: +158614 - 0x17FA2 (Discard) - 0xFFF00 - Arrows +158445 - 0x01983 (Final Room Entry Left) - True - Shapers & Stars +158446 - 0x01987 (Final Room Entry Right) - True - Squares & Colored Squares & Dots +Door - 0x0C141 (Final Room Entry) - 0x01983 & 0x01987 + +Mountain Bottom Floor Rock (Mountain Bottom Floor) - Mountain Bottom Floor - 0x17F33 - Mountain Path to Caves - 0x17F33: +Door - 0x17F33 (Rock Open) - True + +Mountain Path to Caves (Mountain Bottom Floor) - Mountain Bottom Floor Rock - 0x334E1 - Caves - 0x2D77D: +158447 - 0x00FF8 (Caves Entry Panel) - True - Arrows & Black/White Squares +Door - 0x2D77D (Caves Entry) - 0x00FF8 +158448 - 0x334E1 (Rock Control) - True - True + +Caves (Caves) - Main Island - 0x2D73F | 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 (Blue Tunnel Right First 1) - True - Arrows +158455 - 0x00558 (Blue Tunnel Right First 2) - 0x00190 - Arrows +158456 - 0x00567 (Blue Tunnel Right First 3) - 0x00558 - Arrows +158457 - 0x006FE (Blue Tunnel Right First 4) - 0x00567 - Arrows +158458 - 0x01A0D (Blue Tunnel Left First 1) - True - Arrows & Symmetry +158459 - 0x008B8 (Blue Tunnel Left Second 1) - True - Arrows & Triangles +158460 - 0x00973 (Blue Tunnel Left Second 2) - 0x008B8 - Arrows & Triangles +158461 - 0x0097B (Blue Tunnel Left Second 3) - 0x00973 - Arrows & Triangles +158462 - 0x0097D (Blue Tunnel Left Second 4) - 0x0097B - Arrows & Triangles +158463 - 0x0097E (Blue Tunnel Left Second 5) - 0x0097D - Arrows & Triangles +158464 - 0x00994 (Blue Tunnel Right Second 1) - True - Arrows & Shapers & Rotated Shapers +158465 - 0x334D5 (Blue Tunnel Right Second 2) - 0x00994 - Arrows & Rotated Shapers +158466 - 0x00995 (Blue Tunnel Right Second 3) - 0x334D5 - Arrows & Shapers & Rotated Shapers +158467 - 0x00996 (Blue Tunnel Right Second 4) - 0x00995 - Arrows & Shapers & Rotated Shapers +158468 - 0x00998 (Blue Tunnel Right Second 5) - 0x00996 - Arrows & Shapers +158469 - 0x009A4 (Blue Tunnel Left Third 1) - True - Arrows & Stars +158470 - 0x018A0 (Blue Tunnel Right Third 1) - True - Arrows & Symmetry +158471 - 0x00A72 (Blue Tunnel Left Fourth 1) - True - Arrows & Shapers & Negative Shapers +158472 - 0x32962 (First Floor Left) - True - Full Dots & Rotated Shapers +158473 - 0x32966 (First Floor Grounded) - True - Stars & Triangles & Rotated Shapers & Black/White Squares & Stars + Same Colored Symbol +158474 - 0x01A31 (First Floor Middle) - True - Stars +158475 - 0x00B71 (First Floor Right) - True - Full Dots & Eraser & Stars & Stars + Same Colored Symbol & Colored Squares & Shapers & Negative Shapers +158478 - 0x288EA (First Wooden Beam) - True - Stars +158479 - 0x288FC (Second Wooden Beam) - True - Shapers & Eraser +158480 - 0x289E7 (Third Wooden Beam) - True - Eraser & Triangles +158481 - 0x288AA (Fourth Wooden Beam) - True - Full Dots & Negative Shapers & Shapers +158482 - 0x17FB9 (Left Upstairs Single) - True - Full Dots & Arrows & Black/White Squares +158483 - 0x0A16B (Left Upstairs Left Row 1) - True - Full Dots & Arrows +158484 - 0x0A2CE (Left Upstairs Left Row 2) - 0x0A16B - Full Dots & Arrows +158485 - 0x0A2D7 (Left Upstairs Left Row 3) - 0x0A2CE - Full Dots & Arrows +158486 - 0x0A2DD (Left Upstairs Left Row 4) - 0x0A2D7 - Full Dots & Arrows +158487 - 0x0A2EA (Left Upstairs Left Row 5) - 0x0A2DD - Full Dots & Arrows +158488 - 0x0008F (Right Upstairs Left Row 1) - True - Dots & Black/White Squares & Colored Squares +158489 - 0x0006B (Right Upstairs Left Row 2) - 0x0008F - Stars & Black/White Squares & Colored Squares & Stars + Same Colored Symbol +158490 - 0x0008B (Right Upstairs Left Row 3) - 0x0006B - Stars & Black/White Squares & Colored Squares & Stars + Same Colored Symbol & Triangles +158491 - 0x0008C (Right Upstairs Left Row 4) - 0x0008B - Stars & Stars + Same Colored Symbol & Shapers & Rotated Shapers +158492 - 0x0008A (Right Upstairs Left Row 5) - 0x0008C - Stars & Stars + Same Colored Symbol & Shapers & Rotated Shapers & Eraser & Triangles +158493 - 0x00089 (Right Upstairs Left Row 6) - 0x0008A - Shapers & Negative Shapers & Dots +158494 - 0x0006A (Right Upstairs Left Row 7) - 0x00089 - Stars & Dots +158495 - 0x0006C (Right Upstairs Left Row 8) - 0x0006A - Dots & Stars & Stars + Same Colored Symbol & Eraser +158496 - 0x00027 (Right Upstairs Right Row 1) - True - Colored Squares & Black/White Squares & Eraser +158497 - 0x00028 (Right Upstairs Right Row 2) - 0x00027 - Shapers & Symmetry +158498 - 0x00029 (Right Upstairs Right Row 3) - 0x00028 - Symmetry & Triangles & Eraser +158476 - 0x09DD5 (Lone Pillar) - True - Arrows +Door - 0x019A5 (Pillar Door) - 0x09DD5 +158449 - 0x021D7 (Mountain Shortcut Panel) - True - Stars & Stars + Same Colored Symbol & Triangles & Eraser +Door - 0x2D73F (Mountain Shortcut Door) - 0x021D7 +158450 - 0x17CF2 (Swamp Shortcut Panel) - True - Arrows +Door - 0x2D859 (Swamp Shortcut Door) - 0x17CF2 + +Path to Challenge (Caves) - Challenge - 0x0A19A: +158477 - 0x0A16E (Challenge Entry Panel) - True - Stars & Arrows & Stars + Same Colored Symbol +Door - 0x0A19A (Challenge Entry) - 0x0A16E + +Challenge (Challenge) - Tunnels - 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 (Tunnels Entry Panel) - True - Arrows +Door - 0x0348A (Tunnels Entry) - 0x039B4 + +Tunnels (Tunnels) - 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 (Theater Shortcut) - 0x27732 +158520 - 0x2773D (Desert Shortcut Panel) - True - True +Door - 0x27263 (Desert Shortcut) - 0x2773D +158521 - 0x09E85 (Town Shortcut Panel) - True - Arrows +Door - 0x09E87 (Town Shortcut) - 0x09E85 + +Final Room (Mountain Final Room) - Elevator - 0x339BB & 0x33961: +158522 - 0x0383A (Right Pillar 1) - True - Stars & Eraser & Triangles & Stars + Same Colored Symbol +158523 - 0x09E56 (Right Pillar 2) - 0x0383A - Full Dots & Triangles +158524 - 0x09E5A (Right Pillar 3) - 0x09E56 - Dots & Shapers & Stars & Negative Shapers & Stars + Same Colored Symbol +158525 - 0x33961 (Right Pillar 4) - 0x09E5A - Eraser & Symmetry & Stars & Stars + Same Colored Symbols & Negative Shapers & Shapers +158526 - 0x0383D (Left Pillar 1) - True - Stars & Black/White Squares & Stars + Same Colored Symbol +158527 - 0x0383F (Left Pillar 2) - 0x0383D - Triangles +158528 - 0x03859 (Left Pillar 3) - 0x0383F - Symmetry & Shapers & Black/White Squares +158529 - 0x339BB (Left Pillar 4) - 0x03859 - Symmetry & Black/White Squares & Stars & Stars + Same Colored Symbol & Triangles + +Elevator (Mountain Final Room): +158530 - 0x3D9A6 (Elevator Door Closer Left) - True - True +158531 - 0x3D9A7 (Elevator Door Close Right) - True - True +158532 - 0x3C113 (Elevator Entry Left) - 0x3D9A6 | 0x3D9A7 - True +158533 - 0x3C114 (Elevator Entry 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 & 7 Lasers | 0x3D9A8 & 7 Lasers - 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 0f8f0d75c0..d4e9a59771 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -1,18 +1,20 @@ """ Archipelago init file for The Witness """ - import typing from BaseClasses import Region, RegionType, Location, MultiWorld, Item, Entrance, Tutorial, ItemClassification +from .hints import get_always_hint_locations, get_always_hint_items, get_priority_hint_locations, \ + get_priority_hint_items, make_hints, generate_joke_hints from ..AutoWorld import World, WebWorld -from .player_logic import StaticWitnessLogic, WitnessPlayerLogic +from .player_logic import WitnessPlayerLogic +from .static_logic import StaticWitnessLogic from .locations import WitnessPlayerLocations, StaticWitnessLocations from .items import WitnessItem, StaticWitnessItems, WitnessPlayerItems from .rules import set_rules from .regions import WitnessRegions from .Options import is_option_enabled, the_witness_options, get_option_value -from .utils import best_junk_to_add_based_on_weights +from .utils import best_junk_to_add_based_on_weights, get_audio_logs from logging import warning @@ -36,7 +38,7 @@ class WitnessWorld(World): """ game = "The Witness" topology_present = False - data_version = 7 + data_version = 8 static_logic = StaticWitnessLogic() static_locat = StaticWitnessLocations() @@ -59,6 +61,8 @@ class WitnessWorld(World): 'door_hexes': self.items.DOORS, 'symbols_not_in_the_game': self.items.SYMBOLS_NOT_IN_THE_GAME, 'disabled_panels': self.player_logic.COMPLETELY_DISABLED_CHECKS, + 'log_ids_to_hints': self.log_ids_to_hints, + 'progressive_item_lists': self.items.MULTI_LISTS_BY_CODE } def generate_early(self): @@ -77,6 +81,8 @@ class WitnessWorld(World): self.items = WitnessPlayerItems(self.locat, self.world, self.player, self.player_logic) self.regio = WitnessRegions(self.locat) + self.log_ids_to_hints = dict() + self.junk_items_created = {key: 0 for key in self.items.JUNK_WEIGHTS.keys()} def generate_basic(self): @@ -84,17 +90,18 @@ class WitnessWorld(World): pool = [] items_by_name = dict() for item in self.items.ITEM_TABLE: - witness_item = self.create_item(item) - if item in self.items.PROGRESSION_TABLE: - pool.append(witness_item) - items_by_name[item] = witness_item + for i in range(0, self.items.PROG_ITEM_AMOUNTS[item]): + if item in self.items.PROGRESSION_TABLE: + witness_item = self.create_item(item) + pool.append(witness_item) + items_by_name[item] = witness_item less_junk = 0 # Put good item on first check if symbol shuffle is on symbols = is_option_enabled(self.world, self.player, "shuffle_symbols") - if symbols: + if symbols and get_option_value(self.world, self.player, "puzzle_randomization") != 1: random_good_item = self.world.random.choice(self.items.GOOD_ITEMS) first_check = self.world.get_location( @@ -110,9 +117,9 @@ class WitnessWorld(World): 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]): if len(pool) < len(self.locat.CHECK_LOCATION_TABLE) - len(self.locat.EVENT_LOCATION_TABLE) - less_junk: + witness_item = self.create_item(item) pool.append(witness_item) # Put in junk items to fill the rest @@ -138,9 +145,39 @@ class WitnessWorld(World): set_rules(self.world, self.player, self.player_logic, self.locat) def fill_slot_data(self) -> dict: - slot_data = self._get_slot_data() + hint_amount = get_option_value(self.world, self.player, "hint_amount") - slot_data["hard_mode"] = False + credits_hint = ("This Randomizer", "is brought to you by", "NewSoupVi, Jarno, jbzdarkid, sigma144", -1) + + audio_logs = get_audio_logs().copy() + + if hint_amount != 0: + generated_hints = make_hints(self.world, self.player, hint_amount) + + self.world.random.shuffle(audio_logs) + + duplicates = len(audio_logs) // hint_amount + + for _ in range(0, hint_amount): + hint = generated_hints.pop() + + for _ in range(0, duplicates): + audio_log = audio_logs.pop() + self.log_ids_to_hints[int(audio_log, 16)] = hint + + if audio_logs: + audio_log = audio_logs.pop() + self.log_ids_to_hints[int(audio_log, 16)] = credits_hint + + joke_hints = generate_joke_hints(self.world, len(audio_logs)) + + while audio_logs: + audio_log = audio_logs.pop() + self.log_ids_to_hints[int(audio_log, 16)] = joke_hints.pop() + + # generate hints done + + slot_data = self._get_slot_data() for option_name in the_witness_options: slot_data[option_name] = get_option_value( diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py new file mode 100644 index 0000000000..3ee010ea2e --- /dev/null +++ b/worlds/witness/hints.py @@ -0,0 +1,281 @@ +from BaseClasses import MultiWorld +from .Options import is_option_enabled, get_option_value + +joke_hints = [ + ("Quaternions", "break", "my brain"), + ("Eclipse", "has nothing", "but you should do it anyway"), + ("", "Beep", ""), + ("Putting in custom subtitles", "shouldn't have been", "as hard as it was..."), + ("BK mode", "is right", "around the corner"), + ("", "You can do it!", ""), + ("", "I believe in you!", ""), + ("The person playing", "is", "cute <3"), + ("dash dot, dash dash dash", "dash, dot dot dot dot, dot dot", "dash dot, dash dash dot"), + ("When you think about it,", "there are actually a lot of", "bubbles in a stream"), + ("Never gonna give you up", "Never gonna let you down", "Never gonna run around and desert you"), + ("Thanks to", "the Archipelago developers", "for making this possible."), + ("Have you tried ChecksFinder?", "If you like puzzles,", "you might enjoy it!"), + ("Have you tried Dark Souls III?", "A tough game like this", "feels better when friends are helping you!"), + ("Have you tried Donkey Kong Country 3?", "A legendary game", "from a golden age of platformers!"), + ("Have you tried Factorio?", "Alone in an unknown world.", "Sound familiar?"), + ("Have you tried Final Fantasy?", "Experience a classic game", "improved to fit modern standards!"), + ("Have you tried Hollow Knight?", "Another independent hit", "revolutionising a genre!"), + ("Have you tried A Link to the Past?", "The Archipelago game", "that started it all!"), + ("Have you tried Meritous?", "You should know that obscure games", "are often groundbreaking!"), + ("Have you tried Ocarine of Time?", "One of the biggest randomizers,", "big inspiration for this one's features!"), + ("Have you tried Raft?", "Haven't you always wanted to explore", "the ocean surrounding this island?"), + ("Have you tried Risk of Rain 2?", "I haven't either.", "But I hear it's incredible!"), + ("Have you tried Rogue Legacy?", "After solving so many puzzles", "it's the perfect way to rest your brain."), + ("Have you tried Secret of Evermore?", "I haven't either", "But I hear it's great!"), + ("Have you tried Slay the Spire?", "Experience the thrill of combat", "without needing fast fingers!"), + ("Have you tried SMZ3?", "Why play one incredible game", "when you can play 2 at once?"), + ("Have you tried Starcraft 2?", "Use strategy and management", "to crush your enemies!"), + ("Have you tried Super Mario 64?", "3-dimensional games like this", "owe everything to that game."), + ("Have you tried Super Metroid?", "A classic game", "that started a whole genre."), + ("Have you tried Timespinner?", "Everyone who plays it", "ends up loving it!"), + ("Have you tried VVVVVV?", "Experience the essence of gaming", "distilled into its purest form!"), + ("Have you tried The Witness?", "Oh. I guess you already have.", " Thanks for playing!"), + ("One day I was fascinated", "by the subject of", "generation of waves by wind"), + ("I don't like sandwiches", "Why would you think I like sandwiches?", "Have you ever seen me with a sandwich?"), + ("Where are you right now?", "I'm at soup!", "What do you mean you're at soup?"), + ("Remember to ask", "in the Archipelago Discord", "what the Functioning Brain does."), + ("", "Don't use your puzzle skips", "you might need them later"), + ("", "For an extra challenge", "Try playing blindfolded"), + ("Go to the top of the mountain", "and see if you can see", "your house"), + ("Yellow = Red + Green", "Cyan = Green + Blue", "Magenta = Red + Blue"), + ("", "Maybe that panel really is unsolvable", ""), + ("", "Did you make sure it was plugged in?", ""), + ("", "Do not look into laser with remaining eye", ""), + ("", "Try pressing Space to jump", ""), + ("The Witness is a Doom clone.", "Just replace the demons", "with puzzles"), + ("", "Test Hint please ignore", ""), + ("Shapers can never be placed", "outside the panel boundaries", "even if subtracted."), + ("", "The Keep laser panels use", "the same trick on both sides!"), + ("Can't get past a door? Try going around.", "Can't go around? Try building a", "nether portal."), + ("", "We've been trying to reach you", "about your car's extended warranty"), + ("I hate this game. I hate this game.", "I hate this game.", "-chess player Bobby Fischer"), + ("Dear Mario,", "Please come to the castle.", "I've baked a cake for you!"), + ("Have you tried waking up?", "", "Yeah, me neither."), + ("Why do they call it The Witness,", "when wit game the player view", "play of with the game."), + ("", "THE WIND FISH IN NAME ONLY", "FOR IT IS NEITHER"), + ("Like this game? Try The Wit.nes,", "Understand, INSIGHT, Taiji", "What the Witness?, and Tametsi."), + ("", "In a race", "It's survival of the Witnesst"), + ("", "This hint has been removed", "We apologize for your inconvenience."), + ("", "O-----------", ""), + ("Circle is draw", "Square is separate", "Line is win"), + ("Circle is draw", "Star is pair", "Line is win"), + ("Circle is draw", "Circle is copy", "Line is win"), + ("Circle is draw", "Dot is eat", "Line is win"), + ("Circle is start", "Walk is draw", "Line is win"), + ("Circle is start", "Line is win", "Witness is you"), + ("Can't find any items?", "Consider a relaxing boat trip", "around the island"), + ("", "Don't forget to like, comment, and subscribe", ""), + ("Ah crap, gimme a second.", "[papers rustling]", "Sorry, nothing."), + ("", "Trying to get a hint?", "Too bad."), + ("", "Here's a hint:", "Get good at the game."), + ("", "I'm still not entirely sure", "what we're witnessing here."), + ("Have you found a red page yet?", "No?", "Then have you found a blue page?"), + ( + "And here we see the Witness player,", + "seeking answers where there are none-", + "Did someone turn on the loudspeaker?" + ), + ( + "Hints suggested by:", + "IHNN, Beaker, MrPokemon11, Ember, TheM8, NewSoupVi,", + "KF, Yoshi348, Berserker, BowlinJim, oddGarrett, Pink Switch." + ), +] + + +def get_always_hint_items(world: MultiWorld, player: int): + priority = [ + "Boat", + "Mountain Bottom Floor Final Room Entry (Door)", + "Caves Mountain Shortcut (Door)", + "Caves Swamp Shortcut (Door)", + "Caves Exits to Main Island", + ] + + difficulty = get_option_value(world, player, "puzzle_randomization") + discards = is_option_enabled(world, player, "shuffle_discards") + + if discards: + if difficulty == 1: + priority.append("Arrows") + else: + priority.append("Triangles") + + return priority + + +def get_always_hint_locations(world: MultiWorld, player: int): + return { + "Swamp Purple Underwater", + "Shipwreck Vault Box", + "Challenge Vault Box", + "Mountain Bottom Floor Discard", + } + + +def get_priority_hint_items(world: MultiWorld, player: int): + priority = { + "Negative Shapers", + "Sound Dots", + "Colored Dots", + "Stars + Same Colored Symbol", + "Swamp Entry (Panel)", + "Swamp Laser Shortcut (Door)", + } + + if is_option_enabled(world, player, "shuffle_lasers"): + lasers = { + "Symmetry Laser", + "Desert Laser", + "Town Laser", + "Keep Laser", + "Swamp Laser", + "Treehouse Laser", + "Monastery Laser", + "Jungle Laser", + "Quarry Laser", + "Bunker Laser", + "Shadows Laser", + } + + if get_option_value(world, player, "doors") >= 2: + priority.add("Desert Laser") + lasers.remove("Desert Laser") + priority.update(world.random.sample(lasers, 2)) + + else: + priority.update(world.random.sample(lasers, 3)) + + return priority + + +def get_priority_hint_locations(world: MultiWorld, player: int): + return { + "Town RGB Room Left", + "Town RGB Room Right", + "Treehouse Green Bridge 7", + "Treehouse Green Bridge Discard", + "Shipwreck Discard", + "Desert Vault Box", + "Mountainside Vault Box", + "Mountainside Discard", + } + + +def make_hint_from_item(world: MultiWorld, player: int, item: str): + location_obj = world.find_item(item, player).item.location + location_name = location_obj.name + if location_obj.player != player: + location_name += " (" + world.get_player_name(location_obj.player) + ")" + + return location_name, item, location_obj.address if(location_obj.player == player) else -1 + + +def make_hint_from_location(world: MultiWorld, player: int, location: str): + location_obj = world.get_location(location, player) + item_obj = world.get_location(location, player).item + item_name = item_obj.name + if item_obj.player != player: + item_name += " (" + world.get_player_name(item_obj.player) + ")" + + return location, item_name, location_obj.address if(location_obj.player == player) else -1 + + +def make_hints(world: MultiWorld, player: int, hint_amount: int): + hints = list() + + prog_items_in_this_world = { + item.name for item in world.get_items() + if item.player == player and item.code and item.advancement + } + loc_in_this_world = { + location.name for location in world.get_locations() + if location.player == player and not location.event + } + + always_locations = [ + location for location in get_always_hint_locations(world, player) + if location in loc_in_this_world + ] + always_items = [ + item for item in get_always_hint_items(world, player) + if item in prog_items_in_this_world + ] + priority_locations = [ + location for location in get_priority_hint_locations(world, player) + if location in loc_in_this_world + ] + priority_items = [ + item for item in get_priority_hint_items(world, player) + if item in prog_items_in_this_world + ] + + always_hint_pairs = dict() + + for item in always_items: + hint_pair = make_hint_from_item(world, player, item) + always_hint_pairs[hint_pair[0]] = (hint_pair[1], True, hint_pair[2]) + + for location in always_locations: + hint_pair = make_hint_from_location(world, player, location) + always_hint_pairs[hint_pair[0]] = (hint_pair[1], False, hint_pair[2]) + + priority_hint_pairs = dict() + + for item in priority_items: + hint_pair = make_hint_from_item(world, player, item) + priority_hint_pairs[hint_pair[0]] = (hint_pair[1], True, hint_pair[2]) + + for location in priority_locations: + hint_pair = make_hint_from_location(world, player, location) + priority_hint_pairs[hint_pair[0]] = (hint_pair[1], False, hint_pair[2]) + + for loc, item in always_hint_pairs.items(): + if item[1]: + hints.append((item[0], "can be found at", loc, item[2])) + else: + hints.append((loc, "contains", item[0], item[2])) + + next_random_hint_is_item = world.random.randint(0, 2) + + prog_items_in_this_world = sorted(list(prog_items_in_this_world)) + locations_in_this_world = sorted(list(loc_in_this_world)) + + world.random.shuffle(prog_items_in_this_world) + world.random.shuffle(locations_in_this_world) + + while len(hints) < hint_amount: + if priority_hint_pairs: + loc = world.random.choice(list(priority_hint_pairs.keys())) + item = priority_hint_pairs[loc] + del priority_hint_pairs[loc] + + if item[1]: + hints.append((item[0], "can be found at", loc, item[2])) + else: + hints.append((loc, "contains", item[0], item[2])) + continue + + if next_random_hint_is_item: + if not prog_items_in_this_world: + next_random_hint_is_item = not next_random_hint_is_item + continue + + hint = make_hint_from_item(world, player, prog_items_in_this_world.pop()) + hints.append((hint[1], "can be found at", hint[0], hint[2])) + else: + hint = make_hint_from_location(world, player, locations_in_this_world.pop()) + hints.append((hint[0], "contains", hint[1], hint[2])) + + next_random_hint_is_item = not next_random_hint_is_item + + return hints + + +def generate_joke_hints(world: MultiWorld, amount: int): + return [(x, y, z, -1) for (x, y, z) in world.random.sample(joke_hints, amount)] diff --git a/worlds/witness/items.py b/worlds/witness/items.py index 9ffd5a1173..cbb1554096 100644 --- a/worlds/witness/items.py +++ b/worlds/witness/items.py @@ -2,6 +2,7 @@ Defines progression, junk and event items for The Witness """ import copy +from collections import defaultdict from typing import Dict, NamedTuple, Optional, Set from BaseClasses import Item, MultiWorld @@ -96,6 +97,10 @@ class WitnessPlayerItems: Class that defines Items for a single world """ + @staticmethod + def code(item_name: str): + return StaticWitnessItems.ALL_ITEM_TABLE[item_name].code + def __init__(self, locat: WitnessPlayerLocations, world: MultiWorld, player: int, player_logic: WitnessPlayerLogic): """Adds event items after logic changes due to options""" self.EVENT_ITEM_TABLE = dict() @@ -105,6 +110,8 @@ class WitnessPlayerItems: self.ITEM_ID_TO_DOOR_HEX = dict() self.DOORS = set() + self.PROG_ITEM_AMOUNTS = defaultdict(lambda: 1) + self.SYMBOLS_NOT_IN_THE_GAME = set() self.EXTRA_AMOUNTS = { @@ -118,8 +125,17 @@ class WitnessPlayerItems: if item in StaticWitnessLogic.ALL_SYMBOL_ITEMS: self.SYMBOLS_NOT_IN_THE_GAME.add(StaticWitnessItems.ALL_ITEM_TABLE[item[0]].code) else: + if item[0] in StaticWitnessLogic.PROGRESSIVE_TO_ITEMS: + self.PROG_ITEM_AMOUNTS[item[0]] = len(player_logic.MULTI_LISTS[item[0]]) + self.PROGRESSION_TABLE[item[0]] = self.ITEM_TABLE[item[0]] + self.MULTI_LISTS_BY_CODE = dict() + + for item in self.PROG_ITEM_AMOUNTS: + multi_list = player_logic.MULTI_LISTS[item] + self.MULTI_LISTS_BY_CODE[self.code(item)] = [self.code(single_item) for single_item in multi_list] + for entity_hex, items in player_logic.DOOR_ITEMS_BY_ID.items(): entity_hex_int = int(entity_hex, 16) @@ -138,11 +154,11 @@ class WitnessPlayerItems: if doors and symbols: self.GOOD_ITEMS = [ - "Dots", "Black/White Squares", "Symmetry" + "Progressive Dots", "Black/White Squares", "Symmetry" ] elif symbols: self.GOOD_ITEMS = [ - "Dots", "Black/White Squares", "Stars", + "Progressive Dots", "Black/White Squares", "Progressive Stars", "Shapers", "Symmetry" ] @@ -151,6 +167,10 @@ class WitnessPlayerItems: if not is_option_enabled(world, player, "disable_non_randomized_puzzles"): self.GOOD_ITEMS.append("Colored Squares") + self.GOOD_ITEMS = [ + StaticWitnessLogic.ITEMS_TO_PROGRESSIVE.get(item, item) for item in self.GOOD_ITEMS + ] + for event_location in locat.EVENT_LOCATION_TABLE: location = player_logic.EVENT_ITEM_PAIRS[event_location] self.EVENT_ITEM_TABLE[location] = ItemData(None, True, True) diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py index ea0728b16c..99bd3ece83 100644 --- a/worlds/witness/locations.py +++ b/worlds/witness/locations.py @@ -3,7 +3,8 @@ Defines constants for different types of locations in the game """ from .Options import is_option_enabled, get_option_value -from .player_logic import StaticWitnessLogic, WitnessPlayerLogic +from .player_logic import WitnessPlayerLogic +from .static_logic import StaticWitnessLogic class StaticWitnessLocations: @@ -52,8 +53,6 @@ class StaticWitnessLocations: "Desert Light Room 3", "Desert Pond Room 5", "Desert Flood Room 6", - "Desert Final Bent 3", - "Desert Final Hexagonal", "Desert Laser Panel", "Quarry Mill Lower Row 6", @@ -247,6 +246,10 @@ class WitnessPlayerLocations: StaticWitnessLocations.GENERAL_LOCATIONS ) + if get_option_value(world, player, "puzzle_randomization") == 1: + self.CHECK_LOCATIONS.remove("Keep Pressure Plates 4") + self.CHECK_LOCATIONS.add("Keep Pressure Plates 2") + doors = get_option_value(world, player, "shuffle_doors") >= 2 earlyutm = is_option_enabled(world, player, "early_secret_area") victory = get_option_value(world, player, "victory_condition") diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index 4840ea0a5d..a58ad8ef7d 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -39,7 +39,7 @@ class WitnessPlayerLogic: if panel_hex in self.COMPLETELY_DISABLED_CHECKS: return frozenset() - check_obj = StaticWitnessLogic.CHECKS_BY_HEX[panel_hex] + check_obj = self.REFERENCE_LOGIC.CHECKS_BY_HEX[panel_hex] these_items = frozenset({frozenset()}) @@ -47,17 +47,21 @@ class WitnessPlayerLogic: these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["items"] these_items = frozenset({ - subset.intersection(self.PROG_ITEMS_ACTUALLY_IN_THE_GAME) + subset.intersection(self.THEORETICAL_ITEMS_NO_MULTI) for subset in these_items }) + for subset in these_items: + self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(subset) + 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: + for dependentItem in door_items: + self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(dependentItem) + for items_option in these_items: all_options.add(items_option.union(dependentItem)) if panel_hex != "0x28A0D": @@ -76,11 +80,11 @@ class WitnessPlayerLogic: dependent_items_for_option = frozenset({frozenset()}) for option_panel in option: - dep_obj = StaticWitnessLogic.CHECKS_BY_HEX.get(option_panel) + dep_obj = self.REFERENCE_LOGIC.CHECKS_BY_HEX.get(option_panel) if option_panel in self.COMPLETELY_DISABLED_CHECKS: new_items = frozenset() - elif option_panel in {"7 Lasers", "11 Lasers"}: + elif option_panel in {"7 Lasers", "11 Lasers", "PP2 Weirdness"}: new_items = frozenset({frozenset([option_panel])}) # If a panel turns on when a panel in a different region turns on, # the latter panel will be an "event panel", unless it ends up being @@ -113,20 +117,26 @@ class WitnessPlayerLogic: """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.") + line_split = line.split(" - ") + item = line_split[0] - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(line) + if item not in StaticWitnessItems.ALL_ITEM_TABLE: + raise RuntimeError("Item \"" + item + "\" does not exit.") - if line in StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT: - panel_hexes = StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT[line][2] + self.THEORETICAL_ITEMS.add(item) + self.THEORETICAL_ITEMS_NO_MULTI.update(StaticWitnessLogic.PROGRESSIVE_TO_ITEMS.get(item, [item])) + + if item in StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT: + panel_hexes = StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT[item][2] for panel_hex in panel_hexes: - self.DOOR_ITEMS_BY_ID.setdefault(panel_hex, set()).add(line) + self.DOOR_ITEMS_BY_ID.setdefault(panel_hex, set()).add(item) return if adj_type == "Remove Items": - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.discard(line) + self.THEORETICAL_ITEMS.discard(line) + for i in StaticWitnessLogic.PROGRESSIVE_TO_ITEMS.get(line, [line]): + self.THEORETICAL_ITEMS_NO_MULTI.discard(i) if line in StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT: panel_hexes = StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT[line][2] @@ -265,11 +275,22 @@ class WitnessPlayerLogic: self.REQUIREMENTS_BY_HEX[check_hex] = indep_requirement + for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI: + if item not in self.THEORETICAL_ITEMS: + corresponding_multi = StaticWitnessLogic.ITEMS_TO_PROGRESSIVE[item] + self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(corresponding_multi) + multi_list = StaticWitnessLogic.PROGRESSIVE_TO_ITEMS[StaticWitnessLogic.ITEMS_TO_PROGRESSIVE[item]] + multi_list = [item for item in multi_list if item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI] + self.MULTI_AMOUNTS[item] = multi_list.index(item) + 1 + self.MULTI_LISTS[corresponding_multi] = multi_list + else: + self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(item) + def make_event_item_pair(self, panel): """ Makes a pair of an event panel and its event item """ - name = StaticWitnessLogic.CHECKS_BY_HEX[panel]["checkName"] + " Solved" + name = self.REFERENCE_LOGIC.CHECKS_BY_HEX[panel]["checkName"] + " Solved" pair = (name, self.EVENT_ITEM_NAMES[panel]) return pair @@ -287,7 +308,7 @@ class WitnessPlayerLogic: if panel == "TrueOneWay": continue - if StaticWitnessLogic.CHECKS_BY_HEX[panel]["region"]["name"] != region_name: + if self.REFERENCE_LOGIC.CHECKS_BY_HEX[panel]["region"]["name"] != region_name: self.EVENT_PANELS_FROM_REGIONS.add(panel) self.EVENT_PANELS.update(self.EVENT_PANELS_FROM_PANELS) @@ -306,12 +327,24 @@ class WitnessPlayerLogic: self.EVENT_PANELS_FROM_PANELS = set() self.EVENT_PANELS_FROM_REGIONS = set() + self.THEORETICAL_ITEMS = set() + self.THEORETICAL_ITEMS_NO_MULTI = set() + self.MULTI_AMOUNTS = dict() + self.MULTI_LISTS = dict() + self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set() self.PROG_ITEMS_ACTUALLY_IN_THE_GAME = 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) + self.DIFFICULTY = get_option_value(world, player, "puzzle_randomization") + + if self.DIFFICULTY == 0: + self.REFERENCE_LOGIC = StaticWitnessLogic.sigma_normal + elif self.DIFFICULTY == 1: + self.REFERENCE_LOGIC = StaticWitnessLogic.sigma_expert + + self.CONNECTIONS_BY_REGION_NAME = copy.copy(self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME) + self.DEPENDENT_REQUIREMENTS_BY_HEX = copy.copy(self.REFERENCE_LOGIC.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX) self.REQUIREMENTS_BY_HEX = dict() # Determining which panels need to be events is a difficult process. @@ -333,6 +366,7 @@ class WitnessPlayerLogic: "0x019DC": "Keep Hedges 2 Knowledge", "0x019E7": "Keep Hedges 3 Knowledge", "0x01D3F": "Keep Laser Panel (Pressure Plates) Activates", + "0x01BE9": "Keep Laser Panel (Pressure Plates) Activates", "0x09F7F": "Mountain Access", "0x0367C": "Quarry Laser Mill Requirement Met", "0x009A1": "Swamp Between Bridges Far 1 Activates", @@ -374,6 +408,9 @@ class WitnessPlayerLogic: "0x0356B": "Challenge Video Pattern Knowledge", "0x0A15F": "Desert Laser Panel Shutters Open (1)", "0x012D7": "Desert Laser Panel Shutters Open (2)", + "0x03613": "Treehouse Orange Bridge 13 Turns On", + "0x17DEC": "Treehouse Laser House Access Requirement", + "0x03C08": "Town Church Entry Opens", } self.ALWAYS_EVENT_NAMES_BY_HEX = { diff --git a/worlds/witness/regions.py b/worlds/witness/regions.py index 143f3e77e5..e17acf7343 100644 --- a/worlds/witness/regions.py +++ b/worlds/witness/regions.py @@ -4,7 +4,8 @@ and connects them with the proper requirements """ from BaseClasses import MultiWorld, Entrance -from . import StaticWitnessLogic +from .static_logic import StaticWitnessLogic +from .Options import get_option_value from .locations import WitnessPlayerLocations from .player_logic import WitnessPlayerLogic @@ -39,7 +40,7 @@ class WitnessRegions: connection = Entrance( player, - source + " to " + target + " via " + str(panel_hex_to_solve_set), + source + " to " + target, source_region ) @@ -58,16 +59,23 @@ class WitnessRegions: create_region(world, player, 'Menu', self.locat, None, ["The Splashscreen?"]), ] + difficulty = get_option_value(world, player, "puzzle_randomization") + + if difficulty == 1: + reference_logic = StaticWitnessLogic.sigma_expert + else: + reference_logic = StaticWitnessLogic.sigma_normal + all_locations = set() - for region_name, region in StaticWitnessLogic.ALL_REGIONS_BY_NAME.items(): + for region_name, region in reference_logic.ALL_REGIONS_BY_NAME.items(): locations_for_this_region = [ - StaticWitnessLogic.CHECKS_BY_HEX[panel]["checkName"] for panel in region["panels"] - if StaticWitnessLogic.CHECKS_BY_HEX[panel]["checkName"] in self.locat.CHECK_LOCATION_TABLE + reference_logic.CHECKS_BY_HEX[panel]["checkName"] for panel in region["panels"] + if reference_logic.CHECKS_BY_HEX[panel]["checkName"] in self.locat.CHECK_LOCATION_TABLE ] locations_for_this_region += [ - StaticWitnessLogic.CHECKS_BY_HEX[panel]["checkName"] + " Solved" for panel in region["panels"] - if StaticWitnessLogic.CHECKS_BY_HEX[panel]["checkName"] + " Solved" in self.locat.EVENT_LOCATION_TABLE + reference_logic.CHECKS_BY_HEX[panel]["checkName"] + " Solved" for panel in region["panels"] + if reference_logic.CHECKS_BY_HEX[panel]["checkName"] + " Solved" in self.locat.EVENT_LOCATION_TABLE ] all_locations = all_locations | set(locations_for_this_region) @@ -76,7 +84,7 @@ class WitnessRegions: create_region(world, player, region_name, self.locat, locations_for_this_region) ] - for region_name, region in StaticWitnessLogic.ALL_REGIONS_BY_NAME.items(): + for region_name, region in reference_logic.ALL_REGIONS_BY_NAME.items(): for connection in player_logic.CONNECTIONS_BY_REGION_NAME[region_name]: if connection[0] == "Entry": continue @@ -87,7 +95,7 @@ class WitnessRegions: 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}): + if all({reference_logic.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]) diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index 2b9888b361..02efc5ef4c 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -91,13 +91,64 @@ class WitnessLogic(LogicMixin): if not self._witness_has_lasers(world, player, get_option_value(world, player, "challenge_lasers")): valid_option = False break + elif item == "PP2 Weirdness": + hedge_2_access = ( + self.can_reach("Keep 2nd Maze to Keep", "Entrance", player) + or self.can_reach("Keep to Keep 2nd Maze", "Entrance", player) + ) + + hedge_3_access = ( + self.can_reach("Keep 3rd Maze to Keep", "Entrance", player) + or self.can_reach("Keep 2nd Maze to Keep 3rd Maze", "Entrance", player) + and hedge_2_access + ) + + hedge_4_access = ( + self.can_reach("Keep 4th Maze to Keep", "Entrance", player) + or self.can_reach("Keep 3rd Maze to Keep 4th Maze", "Entrance", player) + and hedge_3_access + ) + + hedge_access = ( + self.can_reach("Keep 4th Maze to Keep Tower", "Entrance", player) + and self.can_reach("Keep", "Region", player) + and hedge_4_access + ) + + backwards_to_fourth = ( + self.can_reach("Keep", "Region", player) + and self.can_reach("Keep 4th Pressure Plate to Keep Tower", "Entrance", player) + and ( + self.can_reach("Keep Tower to Keep", "Entrance", player) + or hedge_access + ) + ) + + backwards_access = ( + self.can_reach("Keep 3rd Pressure Plate to Keep 4th Pressure Plate", "Entrance", player) + and backwards_to_fourth + + or self.can_reach("Main Island", "Region", player) + and self.can_reach("Keep 4th Pressure Plate to Shadows", "Entrance", player) + ) + + front_access = ( + self.can_reach("Keep to Keep 2nd Pressure Plate", 'Entrance', player) + and self.can_reach("Keep", "Region", player) + ) + + if not (front_access and backwards_access): + valid_option = False + break 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 + prog_dict = StaticWitnessLogic.ITEMS_TO_PROGRESSIVE + if not (item in prog_dict and self.has(prog_dict[item], player, player_logic.MULTI_AMOUNTS[item])): + valid_option = False + break if valid_option: return True diff --git a/worlds/witness/settings/Audio_Logs.txt b/worlds/witness/settings/Audio_Logs.txt new file mode 100644 index 0000000000..27ce126778 --- /dev/null +++ b/worlds/witness/settings/Audio_Logs.txt @@ -0,0 +1,49 @@ +0x3C0F7 +0x3C0FD +0x32A00 +0x3C0FE +0x3C100 +0x3C0F4 +0x3C102 +0x3C10D +0x3C10E +0x3C10B +0x0074F +0x012C7 +0x329FF +0x3C106 +0x33AFF +0x011F9 +0x00763 +0x32A08 +0x3C101 +0x3C0FF +0x3C103 +0x00A0F +0x339A9 +0x015C0 +0x33B36 +0x3C10C +0x32A0E +0x329FE +0x32A07 +0x00761 +0x3C109 +0x33B37 +0x3C107 +0x3C0F3 +0x015B7 +0x3C10A +0x32A0A +0x015C1 +0x3C12A +0x3C104 +0x3C105 +0x339A8 +0x0050A +0x338BD +0x3C135 +0x338C9 +0x338D7 +0x338C1 +0x338CA \ No newline at end of file diff --git a/worlds/witness/settings/Disable_Unrandomized.txt b/worlds/witness/settings/Disable_Unrandomized.txt index 4f26e3136a..43c69409fc 100644 --- a/worlds/witness/settings/Disable_Unrandomized.txt +++ b/worlds/witness/settings/Disable_Unrandomized.txt @@ -52,6 +52,10 @@ Disabled Locations: 0x019E7 (Keep Hedge Maze 3) 0x01A0F (Keep Hedge Maze 4) 0x0360E (Laser Hedges) +0x03307 (First Gate) +0x03313 (Second Gate) +0x0C128 (Entry Inner) +0x0C153 (Entry Outer) 0x00B10 (Monastery Entry Left) 0x00C92 (Monastery Entry Right) 0x00290 (Monastery Outside 1) @@ -83,6 +87,10 @@ Disabled Locations: 0x15ADD (River Outside Vault) 0x03702 (River Vault Box) 0x17CAA (Monastery Shortcut Panel) +0x0C2A4 (Bunker Entry) +0x17C79 (Tinted Glass Door) +0x0C2A3 (UV Room Entry) +0x0A08D (Elevator Room Entry) 0x17C2E (Door to Bunker) 0x09F7D (Bunker Intro Left 1) 0x09FDC (Bunker Intro Left 2) diff --git a/worlds/witness/settings/Symbol_Shuffle.txt b/worlds/witness/settings/Symbol_Shuffle.txt index d03391f5c5..3d0342f5e2 100644 --- a/worlds/witness/settings/Symbol_Shuffle.txt +++ b/worlds/witness/settings/Symbol_Shuffle.txt @@ -1,5 +1,6 @@ Items: -Dots +Arrows +Progressive Dots Colored Dots Sound Dots Symmetry @@ -8,7 +9,6 @@ Eraser Shapers Rotated Shapers Negative Shapers -Stars -Stars + Same Colored Symbol +Progressive Stars 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 646957c462..18a0e19cff 100644 --- a/worlds/witness/static_logic.py +++ b/worlds/witness/static_logic.py @@ -1,73 +1,15 @@ import os -from .utils import define_new_region, parse_lambda +from .utils import define_new_region, parse_lambda, lazy -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() - CONNECTIONS_TO_SEVER_BY_DOOR_HEX = dict() - - EVENT_PANELS_FROM_REGIONS = set() - - # All regions with a list of panels in them and the connections to other regions, before logic adjustments - ALL_REGIONS_BY_NAME = dict() - STATIC_CONNECTIONS_BY_REGION_NAME = dict() - - CHECKS_BY_HEX = dict() - CHECKS_BY_NAME = dict() - STATIC_DEPENDENT_REQUIREMENTS_BY_HEX = dict() - - def parse_items(self): - """ - Parses currently defined items from WitnessItems.txt - """ - - path = os.path.join(os.path.dirname(__file__), "WitnessItems.txt") - with open(path, "r", encoding="utf-8") as file: - current_set = self.ALL_SYMBOL_ITEMS - - for line in file.readlines(): - line = line.strip() - - if line == "Progression:": - current_set = self.ALL_SYMBOL_ITEMS - continue - if line == "Boosts:": - current_set = self.ALL_BOOSTS - continue - if line == "Traps:": - current_set = self.ALL_TRAPS - continue - 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 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]))) - - def read_logic_file(self): +class StaticWitnessLogicObj: + def read_logic_file(self, file_path="WitnessLogic.txt"): """ Reads the logic file and does the initial population of data structures """ - path = os.path.join(os.path.dirname(__file__), "WitnessLogic.txt") + path = os.path.join(os.path.dirname(__file__), file_path) + with open(path, "r", encoding="utf-8") as file: current_region = dict() @@ -157,6 +99,99 @@ class StaticWitnessLogic: current_region["panels"].add(check_hex) + def __init__(self, file_path="WitnessLogic.txt"): + # All regions with a list of panels in them and the connections to other regions, before logic adjustments + self.ALL_REGIONS_BY_NAME = dict() + self.STATIC_CONNECTIONS_BY_REGION_NAME = dict() + + self.CHECKS_BY_HEX = dict() + self.CHECKS_BY_NAME = dict() + self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX = dict() + + self.read_logic_file(file_path) + + +class StaticWitnessLogic: + ALL_SYMBOL_ITEMS = set() + ITEMS_TO_PROGRESSIVE = dict() + PROGRESSIVE_TO_ITEMS = dict() + ALL_DOOR_ITEMS = set() + ALL_DOOR_ITEMS_AS_DICT = dict() + ALL_USEFULS = set() + ALL_TRAPS = set() + ALL_BOOSTS = set() + CONNECTIONS_TO_SEVER_BY_DOOR_HEX = dict() + + ALL_REGIONS_BY_NAME = dict() + STATIC_CONNECTIONS_BY_REGION_NAME = dict() + + CHECKS_BY_HEX = dict() + CHECKS_BY_NAME = dict() + STATIC_DEPENDENT_REQUIREMENTS_BY_HEX = dict() + + def parse_items(self): + """ + Parses currently defined items from WitnessItems.txt + """ + + path = os.path.join(os.path.dirname(__file__), "WitnessItems.txt") + with open(path, "r", encoding="utf-8") as file: + current_set = self.ALL_SYMBOL_ITEMS + + for line in file.readlines(): + line = line.strip() + + if line == "Progression:": + current_set = self.ALL_SYMBOL_ITEMS + continue + if line == "Boosts:": + current_set = self.ALL_BOOSTS + continue + if line == "Traps:": + current_set = self.ALL_TRAPS + continue + 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 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: + if len(line_split) > 2: + progressive_items = line_split[2].split(",") + for i, value in enumerate(progressive_items): + self.ITEMS_TO_PROGRESSIVE[value] = line_split[1] + self.PROGRESSIVE_TO_ITEMS[line_split[1]] = progressive_items + current_set.add((line_split[1], int(line_split[0]))) + continue + current_set.add((line_split[1], int(line_split[0]))) + + @lazy + def sigma_expert(self) -> StaticWitnessLogicObj: + return StaticWitnessLogicObj("WitnessLogicExpert.txt") + + @lazy + def sigma_normal(self) -> StaticWitnessLogicObj: + return StaticWitnessLogicObj("WitnessLogic.txt") + def __init__(self): self.parse_items() - self.read_logic_file() + + self.ALL_REGIONS_BY_NAME.update(self.sigma_normal.ALL_REGIONS_BY_NAME) + self.STATIC_CONNECTIONS_BY_REGION_NAME.update(self.sigma_normal.STATIC_CONNECTIONS_BY_REGION_NAME) + + self.CHECKS_BY_HEX.update(self.sigma_normal.CHECKS_BY_HEX) + self.CHECKS_BY_NAME.update(self.sigma_normal.CHECKS_BY_NAME) + self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX.update(self.sigma_normal.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX) + diff --git a/worlds/witness/utils.py b/worlds/witness/utils.py index 809b2b1c3d..a69dabbb7c 100644 --- a/worlds/witness/utils.py +++ b/worlds/witness/utils.py @@ -89,6 +89,22 @@ def parse_lambda(lambda_string): return lambda_set +class lazy(object): + def __init__(self, func, name=None): + self.func = func + self.name = name if name is not None else func.__name__ + self.__doc__ = func.__doc__ + + def __get__(self, instance, class_): + if instance is None: + res = self.func(class_) + setattr(class_, self.name, res) + return res + res = self.func(instance) + setattr(instance, self.name, res) + return res + + def get_adjustment_file(adjustment_file): path = os.path.join(os.path.dirname(__file__), adjustment_file) @@ -134,3 +150,8 @@ def get_doors_max_list(): @cache_argsless def get_laser_shuffle(): return get_adjustment_file("settings/Laser_Shuffle.txt") + + +@cache_argsless +def get_audio_logs(): + return get_adjustment_file("settings/Audio_Logs.txt") diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py new file mode 100644 index 0000000000..32b84015f1 --- /dev/null +++ b/worlds/zillion/__init__.py @@ -0,0 +1,395 @@ +from collections import deque, Counter +from contextlib import redirect_stdout +import functools +from typing import Any, Dict, List, Set, Tuple, Optional, cast +import os +import logging + +from BaseClasses import ItemClassification, LocationProgressType, \ + MultiWorld, Item, CollectionState, RegionType, \ + Entrance, Tutorial +from Options import AssembleOptions +from .logic import cs_to_zz_locs +from .region import ZillionLocation, ZillionRegion +from .options import zillion_options, validate +from .id_maps import item_name_to_id as _item_name_to_id, \ + loc_name_to_id as _loc_name_to_id, make_id_to_others, \ + zz_reg_name_to_reg_name, base_id +from .item import ZillionItem +from .patch import ZillionDeltaPatch, get_base_rom_path + +from zilliandomizer.randomizer import Randomizer as ZzRandomizer +from zilliandomizer.system import System +from zilliandomizer.logic_components.items import RESCUE, items as zz_items, Item as ZzItem +from zilliandomizer.logic_components.locations import Location as ZzLocation, Req +from zilliandomizer.options import Chars + +from ..AutoWorld import World, WebWorld + + +class ZillionWebWorld(WebWorld): + theme = "stone" + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to playing Zillion randomizer.", + "English", + "setup_en.md", + "setup/en", + ["beauxq"] + )] + + +class ZillionWorld(World): + """ + Zillion is a metroidvania style game released in 1987 for the 8-bit Sega Master System. + + It's based on the anime Zillion (赤い光弾ジリオン, Akai Koudan Zillion). + """ + game = "Zillion" + web = ZillionWebWorld() + + option_definitions: Dict[str, AssembleOptions] = zillion_options + topology_present: bool = True # indicate if world type has any meaningful layout/pathing + + # map names to their IDs + item_name_to_id: Dict[str, int] = _item_name_to_id + location_name_to_id: Dict[str, int] = _loc_name_to_id + + # increment this every time something in your world's names/id mappings changes. + # While this is set to 0 in *any* AutoWorld, the entire DataPackage is considered in testing mode and will be + # retrieved by clients on every connection. + data_version: int = 1 + + # NOTE: remote_items and remote_start_inventory are now available in the network protocol for the client to set. + # These values will be removed. + # if a world is set to remote_items, then it just needs to send location checks to the server and the server + # sends back the items + # if a world is set to remote_items = False, then the server never sends an item where receiver == finder, + # the client finds its own items in its own world. + remote_items: bool = False + + logger: logging.Logger + + class LogStreamInterface: + logger: logging.Logger + buffer: List[str] + + def __init__(self, logger: logging.Logger) -> None: + self.logger = logger + self.buffer = [] + + def write(self, msg: str) -> None: + if msg.endswith('\n'): + self.buffer.append(msg[:-1]) + self.logger.debug("".join(self.buffer)) + self.buffer = [] + else: + self.buffer.append(msg) + + def flush(self) -> None: + pass + + lsi: LogStreamInterface + + id_to_zz_item: Optional[Dict[int, ZzItem]] = None + zz_system: System + _item_counts: "Counter[str]" = Counter() + """ + These are the items counts that will be in the game, + which might be different from the item counts the player asked for in options + (if the player asked for something invalid). + """ + my_locations: List[ZillionLocation] = [] + """ This is kind of a cache to avoid iterating through all the multiworld locations in logic. """ + + def __init__(self, world: MultiWorld, player: int): + super().__init__(world, player) + self.logger = logging.getLogger("Zillion") + self.lsi = ZillionWorld.LogStreamInterface(self.logger) + self.zz_system = System() + + def _make_item_maps(self, start_char: Chars) -> None: + _id_to_name, _id_to_zz_id, id_to_zz_item = make_id_to_others(start_char) + self.id_to_zz_item = id_to_zz_item + + @classmethod + def stage_assert_generate(cls, world: MultiWorld) -> None: + """Checks that a game is capable of generating, usually checks for some base file like a ROM. + Not run for unittests since they don't produce output""" + rom_file = get_base_rom_path() + if not os.path.exists(rom_file): + raise FileNotFoundError(rom_file) + + def generate_early(self) -> None: + if not hasattr(self.world, "zillion_logic_cache"): + setattr(self.world, "zillion_logic_cache", {}) + + zz_op, item_counts = validate(self.world, self.player) + + self._item_counts = item_counts + + rom_dir_name = os.path.dirname(get_base_rom_path()) + with redirect_stdout(self.lsi): # type: ignore + self.zz_system.make_patcher(rom_dir_name) + self.zz_system.make_randomizer(zz_op) + + self.zz_system.make_map() + + # just in case the options changed anything (I don't think they do) + assert self.zz_system.randomizer, "init failed" + for zz_name in self.zz_system.randomizer.locations: + if zz_name != 'main': + assert self.zz_system.randomizer.loc_name_2_pretty[zz_name] in self.location_name_to_id, \ + f"{self.zz_system.randomizer.loc_name_2_pretty[zz_name]} not in location map" + + self._make_item_maps(zz_op.start_char) + + def create_regions(self) -> None: + assert self.zz_system.randomizer, "generate_early hasn't been called" + assert self.id_to_zz_item, "generate_early hasn't been called" + p = self.player + w = self.world + self.my_locations = [] + + self.zz_system.randomizer.place_canister_gun_reqs() + + start = self.zz_system.randomizer.regions['start'] + + all: Dict[str, ZillionRegion] = {} + for here_zz_name, zz_r in self.zz_system.randomizer.regions.items(): + here_name = "Menu" if here_zz_name == "start" else zz_reg_name_to_reg_name(here_zz_name) + all[here_name] = ZillionRegion(zz_r, here_name, RegionType.Generic, here_name, p, w) + self.world.regions.append(all[here_name]) + + limited_skill = Req(gun=3, jump=3, skill=self.zz_system.randomizer.options.skill, hp=940, red=1, floppy=126) + queue = deque([start]) + done: Set[str] = set() + while len(queue): + zz_here = queue.popleft() + here_name = "Menu" if zz_here.name == "start" else zz_reg_name_to_reg_name(zz_here.name) + if here_name in done: + continue + here = all[here_name] + + for zz_loc in zz_here.locations: + # if local gun reqs didn't place "keyword" item + if not zz_loc.item: + + def access_rule_wrapped(zz_loc_local: ZzLocation, + p: int, + zz_r: ZzRandomizer, + id_to_zz_item: Dict[int, ZzItem], + cs: CollectionState) -> bool: + accessible = cs_to_zz_locs(cs, p, zz_r, id_to_zz_item) + return zz_loc_local in accessible + + access_rule = functools.partial(access_rule_wrapped, + zz_loc, self.player, self.zz_system.randomizer, self.id_to_zz_item) + + loc_name = self.zz_system.randomizer.loc_name_2_pretty[zz_loc.name] + loc = ZillionLocation(zz_loc, self.player, loc_name, here) + loc.access_rule = access_rule + if not (limited_skill >= zz_loc.req): + loc.progress_type = LocationProgressType.EXCLUDED + self.world.exclude_locations[p].value.add(loc.name) + here.locations.append(loc) + self.my_locations.append(loc) + + for zz_dest in zz_here.connections.keys(): + dest_name = "Menu" if zz_dest.name == 'start' else zz_reg_name_to_reg_name(zz_dest.name) + dest = all[dest_name] + exit = Entrance(p, f"{here_name} to {dest_name}", here) + here.exits.append(exit) + exit.connect(dest) + + queue.append(zz_dest) + done.add(here.name) + + def create_items(self) -> None: + if not self.id_to_zz_item: + self._make_item_maps("JJ") + self.logger.warning("warning: called `create_items` without calling `generate_early` first") + assert self.id_to_zz_item, "failed to get item maps" + + # in zilliandomizer, the Randomizer class puts empties in the item pool to fill space, + # but here in AP, empties are in the options from options.validate + item_counts = self._item_counts + self.logger.debug(item_counts) + + for item_name, item_id in self.item_name_to_id.items(): + zz_item = self.id_to_zz_item[item_id] + if item_id >= (4 + base_id): # normal item + if item_name in item_counts: + count = item_counts[item_name] + self.logger.debug(f"Zillion Items: {item_name} {count}") + self.world.itempool += [self.create_item(item_name) for _ in range(count)] + elif item_id < (3 + base_id) and zz_item.code == RESCUE: + # One of the 3 rescues will not be in the pool and its zz_item will be 'empty'. + self.logger.debug(f"Zillion Items: {item_name} 1") + self.world.itempool.append(self.create_item(item_name)) + + def set_rules(self) -> None: + # logic for this game is in create_regions + pass + + def generate_basic(self) -> None: + assert self.zz_system.randomizer, "generate_early hasn't been called" + # main location name is an alias + main_loc_name = self.zz_system.randomizer.loc_name_2_pretty[self.zz_system.randomizer.locations['main'].name] + + self.world.get_location(main_loc_name, self.player)\ + .place_locked_item(self.create_item("Win")) + self.world.completion_condition[self.player] = \ + lambda state: state.has("Win", self.player) + + def post_fill(self) -> None: + """Optional Method that is called after regular fill. Can be used to do adjustments before output generation. + This happens before progression balancing, so the items may not be in their final locations yet.""" + + self.zz_system.post_fill() + + def finalize_item_locations(self) -> None: + """ + sync zilliandomizer item locations with AP item locations + """ + assert self.zz_system.randomizer and self.zz_system.patcher, "generate_early hasn't been called" + zz_options = self.zz_system.randomizer.options + + # debug_zz_loc_ids: Dict[str, int] = {} + empty = zz_items[4] + multi_item = empty # a different patcher method differentiates empty from ap multi item + multi_items: Dict[str, Tuple[str, str]] = {} # zz_loc_name to (item_name, player_name) + for loc in self.world.get_locations(): + if loc.player == self.player: + z_loc = cast(ZillionLocation, loc) + # debug_zz_loc_ids[z_loc.zz_loc.name] = id(z_loc.zz_loc) + if z_loc.item is None: + self.logger.warn("generate_output location has no item - is that ok?") + z_loc.zz_loc.item = empty + elif z_loc.item.player == self.player: + z_item = cast(ZillionItem, z_loc.item) + z_loc.zz_loc.item = z_item.zz_item + else: # another player's item + # print(f"put multi item in {z_loc.zz_loc.name}") + z_loc.zz_loc.item = multi_item + multi_items[z_loc.zz_loc.name] = ( + z_loc.item.name, + self.world.get_player_name(z_loc.item.player) + ) + # debug_zz_loc_ids.sort() + # for name, id_ in debug_zz_loc_ids.items(): + # print(id_) + # print("size:", len(debug_zz_loc_ids)) + + # debug_loc_to_id: Dict[str, int] = {} + # regions = self.zz_randomizer.regions + # for region in regions.values(): + # for loc in region.locations: + # if loc.name not in self.zz_randomizer.locations: + # print(f"region {region.name} had location {loc.name} not in locations") + # debug_loc_to_id[loc.name] = id(loc) + + # verify that every location got an item + for zz_loc in self.zz_system.randomizer.locations.values(): + assert zz_loc.item, ( + f"location {self.zz_system.randomizer.loc_name_2_pretty[zz_loc.name]} " + f"in world {self.player} didn't get an item" + ) + + zz_patcher = self.zz_system.patcher + + zz_patcher.write_locations(self.zz_system.randomizer.regions, + zz_options.start_char, + self.zz_system.randomizer.loc_name_2_pretty) + zz_patcher.all_fixes_and_options(zz_options) + zz_patcher.set_external_item_interface(zz_options.start_char, zz_options.max_level) + zz_patcher.set_multiworld_items(multi_items) + zz_patcher.set_rom_to_ram_data(self.world.player_name[self.player].replace(' ', '_').encode()) + + def generate_output(self, output_directory: str) -> None: + """This method gets called from a threadpool, do not use world.random here. + If you need any last-second randomization, use MultiWorld.slot_seeds[slot] instead.""" + self.finalize_item_locations() + + assert self.zz_system.patcher, "didn't get patcher from generate_early" + # original_rom_bytes = self.zz_patcher.rom + patched_rom_bytes = self.zz_system.patcher.get_patched_bytes() + + out_file_base = self.world.get_out_file_name_base(self.player) + + filename = os.path.join( + output_directory, + f'{out_file_base}{ZillionDeltaPatch.result_file_ending}' + ) + with open(filename, "wb") as binary_file: + binary_file.write(patched_rom_bytes) + patch = ZillionDeltaPatch( + os.path.splitext(filename)[0] + ZillionDeltaPatch.patch_file_ending, + player=self.player, + player_name=self.world.player_name[self.player], + patched_path=filename + ) + patch.write() + os.remove(filename) + + def fill_slot_data(self) -> Dict[str, Any]: # json of WebHostLib.models.Slot + """Fill in the `slot_data` field in the `Connected` network package. + This is a way the generator can give custom data to the client. + The client will receive this as JSON in the `Connected` response.""" + + # TODO: share a TypedDict data structure with client + + # TODO: tell client which canisters are keywords + # so it can open and get those when restoring doors + + zz_patcher = self.zz_system.patcher + assert zz_patcher, "didn't get patcher from generate_early" + assert self.zz_system.randomizer, "didn't get randomizer from generate_early" + + rescues: Dict[str, Any] = {} + for i in (0, 1): + if i in zz_patcher.rescue_locations: + ri = zz_patcher.rescue_locations[i] + rescues[str(i)] = { + "start_char": ri.start_char, + "room_code": ri.room_code, + "mask": ri.mask + } + return { + "start_char": self.zz_system.randomizer.options.start_char, + "rescues": rescues, + "loc_mem_to_id": zz_patcher.loc_memory_to_loc_id + } + + # def modify_multidata(self, multidata: Dict[str, Any]) -> None: + # """For deeper modification of server multidata.""" + # # not modifying multidata, just want to call this at the end of the generation process + # cache = getattr(self.world, "zillion_logic_cache") + # import sys + # print(sys.getsizeof(cache)) + + # end of ordered Main.py calls + + 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""" + item_id = _item_name_to_id[name] + + if not self.id_to_zz_item: + self._make_item_maps("JJ") + self.logger.warning("warning: called `create_item` without calling `generate_early` first") + assert self.id_to_zz_item, "failed to get item maps" + + classification = ItemClassification.filler + zz_item = self.id_to_zz_item[item_id] + if zz_item.required: + classification = ItemClassification.progression + if not zz_item.is_progression: + classification = ItemClassification.progression_skip_balancing + + z_item = ZillionItem(name, classification, item_id, self.player, zz_item) + return z_item + + def get_filler_item_name(self) -> str: + """Called when the item pool needs to be filled with additional items to match location count.""" + return "Empty" diff --git a/worlds/zillion/config.py b/worlds/zillion/config.py new file mode 100644 index 0000000000..e08c4f4278 --- /dev/null +++ b/worlds/zillion/config.py @@ -0,0 +1 @@ +base_id = 8675309 diff --git a/worlds/zillion/docs/en_Zillion.md b/worlds/zillion/docs/en_Zillion.md new file mode 100644 index 0000000000..b5d37cc202 --- /dev/null +++ b/worlds/zillion/docs/en_Zillion.md @@ -0,0 +1,74 @@ +# Zillion + +Zillion is a metroidvania-style game released in 1987 for the 8-bit Sega Master System. + +It's based on the anime Zillion (赤い光弾ジリオン, Akai Koudan Zillion). + +## 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 changes are made to this game? + +The way the original game lets the player choose who to level up has a few drawbacks in a multiworld randomizer: + - Possible softlock from making bad choices (example: nobody has jump 3 when it's required) + - In multiworld, you won't be able to choose because you won't know it's coming beforehand. + +So this randomizer uses a new level-up system: + - Everyone levels up together (even if they're not rescued yet). + - You can choose how many opa-opas are required for a level up. + - You can set a max level from 1 to 8. + - The currently active character is still the only one that gets the health refill. + +--- + +You can set these options to choose when characters will be able to attain certain jump levels: + +``` +jump levels + +vanilla balanced low restrictive + +jj ap ch jj ap ch jj ap ch jj ap ch +2 3 1 1 2 1 1 1 1 1 1 1 +2 3 1 2 2 1 1 2 1 1 1 1 +2 3 1 2 3 1 2 2 1 1 2 1 +2 3 1 2 3 2 2 3 1 1 2 1 +3 3 2 3 3 2 2 3 2 2 2 1 +3 3 2 3 3 2 3 3 2 2 2 1 +3 3 3 3 3 3 3 3 2 2 3 1 +3 3 3 3 3 3 3 3 3 2 3 2 +``` + +Note that in "restrictive" mode, Apple is the only one that can get jump level 3. + +--- + +You can set these options to choose when characters will be able to attain certain Zillion power (gun) levels: + +``` +zillion power + +vanilla balanced low restrictive + +jj ap ch jj ap ch jj ap ch jj ap ch +1 1 3 1 1 2 1 1 1 1 1 1 +2 2 3 2 1 2 1 1 2 1 1 2 +3 3 3 2 2 3 2 1 2 2 1 2 + 3 2 3 2 1 3 2 1 3 + 3 3 3 2 2 3 2 2 3 + 3 2 3 + 3 3 3 +``` + +Note that in "restrictive" mode, Champ is the only one that can get Zillion power level 3. + +## What does another world's item look like in Zillion? + +Canisters retain their original appearance, so you won't know if an item belongs to another player until you collect it. + +When you collect an item, you see the name of the player it goes to. You can see in the client log what item was collected. + +## When the player receives an item, what happens? + +The item collect sound is played. You can see in the client log what item was received. diff --git a/worlds/zillion/docs/setup_en.md b/worlds/zillion/docs/setup_en.md new file mode 100644 index 0000000000..43a1296748 --- /dev/null +++ b/worlds/zillion/docs/setup_en.md @@ -0,0 +1,104 @@ +# Zillion Setup Guide + +## Required Software + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `Zillion Client - Zillion Patch Setup` + +- RetroArch 1.10.3 or newer from: [RetroArch Website](https://retroarch.com?page=platforms). + +- Your legally obtained Zillion ROM file, named `Zillion (UE) [!].sms` + +## Installation Procedures + +### RetroArch + +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 Main Menu --> Online Updater --> Core Downloader. Scroll down and install "Sega - MS/GG (SMS Plus GX)". +3. Go to Settings --> User Interface. Set "Show Advanced Settings" to ON. +4. 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) + +### Linux Setup + +Put your Zillion ROM file in the Archipelago directory in your home directory. + +### Windows Setup + +1. During the installation of Archipelago, install the Zillion Client. If you did not do this, + or you are on an older version, you may run the installer again to install the Zillion Client. +2. During setup, you will be asked to locate your base ROM file. This is the Zillion ROM file mentioned above in Required Software. + +--- +# Play + +## 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](/games/Zillion/player-settings) on the website allows you to configure your personal settings and export a config file from +them. + +### 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](/mysterycheck). + +## Generating a Single-Player Game + +1. Navigate to the [player settings page](/games/Zillion/player-settings), configure your options, and click the "Generate Game" button. +2. A "Seed Info" page will appear. +3. Click the "Create New Room" link. +4. A server page will appear. Download your patch file from this page. +5. Patch your ROM file. + - Linux + - In the launcher, choose "Open Patch" and select your patch file. + - Windows + - Double-click on your patch file. + The Zillion Client will launch automatically, and create your ROM in the location of the patch file. +6. Open the ROM in RetroArch using the core "SMS Plus GX". + - For a single player game, any emulator (or a Sega Master System) can be used, but there are additional features with RetroArch and the Zillion Client. + - If you press reset or restore a save state and return to the surface in the game, the Zillion Client will keep open all the doors that you have opened. + +## Joining a MultiWorld Game + +1. Provide your config (yaml) file to the host and obtain your patch file. + - 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 `.apzl` extension. + - If you activate the "room generation" option in your config (yaml), you might want to tell your host that the generation will take longer than normal. It takes approximately 20 seconds longer for each Zillion player that enables this option. +2. Create your ROM. + - Linux + - In the Archipelago Launcher, choose "Open Patch" and select your `.apzl` patch file. + - Windows + - 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. +3. Connect to the client. + - Use RetroArch to open the ROM that was generated. + - Be sure to select the **SMS Plus GX** core. This core will allow external tools to read RAM data. +4. 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". +5. Play the game. + - When the client shows both Game 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 [Generation page](/generate). + - Generate page: [WebHost Seed Generation Page](/generate) +4. Wait a moment while the seed is generated. +5. When the seed is generated, a "Seed Info" page will appear. +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/zillion/id_maps.py b/worlds/zillion/id_maps.py new file mode 100644 index 0000000000..bc9caeeece --- /dev/null +++ b/worlds/zillion/id_maps.py @@ -0,0 +1,93 @@ +from typing import Dict, Tuple +from zilliandomizer.logic_components.items import Item as ZzItem, \ + item_name_to_id as zz_item_name_to_zz_id, items as zz_items, \ + item_name_to_item as zz_item_name_to_zz_item +from zilliandomizer.options import Chars +from zilliandomizer.utils.loc_name_maps import loc_to_id as pretty_loc_name_to_id +from zilliandomizer.utils import parse_reg_name +from .config import base_id as base_id + +item_name_to_id = { + "Apple": 0 + base_id, + "Champ": 1 + base_id, + "JJ": 2 + base_id, + "Win": 3 + base_id, + "Empty": 4 + base_id, + "ID Card": 5 + base_id, + "Red ID Card": 6 + base_id, + "Floppy Disk": 7 + base_id, + "Bread": 8 + base_id, + "Opa-Opa": 9 + base_id, + "Zillion": 10 + base_id, + "Scope": 11 + base_id, +} + + +_zz_rescue_0 = zz_item_name_to_zz_item["rescue_0"] +_zz_rescue_1 = zz_item_name_to_zz_item["rescue_1"] +_zz_empty = zz_item_name_to_zz_item["empty"] + + +def make_id_to_others(start_char: Chars) -> Tuple[ + Dict[int, str], Dict[int, int], Dict[int, ZzItem] +]: + """ returns id_to_name, id_to_zz_id, id_to_zz_item """ + id_to_name: Dict[int, str] = {} + id_to_zz_id: Dict[int, int] = {} + id_to_zz_item: Dict[int, ZzItem] = {} + + if start_char == "JJ": + name_to_zz_item = { + "Apple": _zz_rescue_0, + "Champ": _zz_rescue_1, + "JJ": _zz_empty + } + elif start_char == "Apple": + name_to_zz_item = { + "Apple": _zz_empty, + "Champ": _zz_rescue_1, + "JJ": _zz_rescue_0 + } + else: # Champ + name_to_zz_item = { + "Apple": _zz_rescue_0, + "Champ": _zz_empty, + "JJ": _zz_rescue_1 + } + + for name, ap_id in item_name_to_id.items(): + id_to_name[ap_id] = name + + if ap_id >= 4 + base_id: + index = ap_id - base_id + zz_item = zz_items[index] + assert zz_item.id == index and zz_item.name == name + elif ap_id < 3 + base_id: + # rescue + assert name in {"Apple", "Champ", "JJ"} + zz_item = name_to_zz_item[name] + else: # main + zz_item = zz_item_name_to_zz_item["main"] + + id_to_zz_id[ap_id] = zz_item_name_to_zz_id[zz_item.debug_name] + id_to_zz_item[ap_id] = zz_item + + return id_to_name, id_to_zz_id, id_to_zz_item + + +def make_room_name(row: int, col: int) -> str: + return f"{chr(ord('A') + row - 1)}-{col + 1}" + + +loc_name_to_id: Dict[str, int] = { + name: id_ + base_id + for name, id_ in pretty_loc_name_to_id.items() +} + + +def zz_reg_name_to_reg_name(zz_reg_name: str) -> str: + if zz_reg_name[0] == 'r' and zz_reg_name[3] == 'c': + row, col = parse_reg_name(zz_reg_name) + end = zz_reg_name[5:] + return f"{make_room_name(row, col)} {end.upper()}" + return zz_reg_name diff --git a/worlds/zillion/item.py b/worlds/zillion/item.py new file mode 100644 index 0000000000..fdf0fa8ba2 --- /dev/null +++ b/worlds/zillion/item.py @@ -0,0 +1,12 @@ +from BaseClasses import Item, ItemClassification as IC +from zilliandomizer.logic_components.items import Item as ZzItem + + +class ZillionItem(Item): + game = "Zillion" + __slots__ = ("zz_item",) + zz_item: ZzItem + + def __init__(self, name: str, classification: IC, code: int, player: int, zz_item: ZzItem) -> None: + super().__init__(name, classification, code, player) + self.zz_item = zz_item diff --git a/worlds/zillion/logic.py b/worlds/zillion/logic.py new file mode 100644 index 0000000000..01ed14346a --- /dev/null +++ b/worlds/zillion/logic.py @@ -0,0 +1,77 @@ +from typing import Dict, FrozenSet, Tuple, cast, List, Counter as _Counter +from BaseClasses import CollectionState +from zilliandomizer.logic_components.locations import Location +from zilliandomizer.randomizer import Randomizer +from zilliandomizer.logic_components.items import Item, items +from .region import ZillionLocation +from .item import ZillionItem +from .id_maps import item_name_to_id + +zz_empty = items[4] + +# TODO: unit tests for these + + +def set_randomizer_locs(cs: CollectionState, p: int, zz_r: Randomizer) -> int: + """ + sync up zilliandomizer locations with archipelago locations + + returns a hash of the player and of the set locations with their items + """ + z_world = cs.world.worlds[p] + my_locations = cast(List[ZillionLocation], getattr(z_world, "my_locations")) + + _hash = p + for z_loc in my_locations: + zz_name = z_loc.zz_loc.name + zz_item = z_loc.item.zz_item \ + if isinstance(z_loc.item, ZillionItem) and z_loc.item.player == p \ + else zz_empty + zz_r.locations[zz_name].item = zz_item + _hash += hash(zz_name) ^ hash(zz_item) + return _hash + + +def item_counts(cs: CollectionState, p: int) -> Tuple[Tuple[str, int], ...]: + """ + the zilliandomizer items that player p has collected + + ((item_name, count), (item_name, count), ...) + """ + return tuple((item_name, cs.item_count(item_name, p)) for item_name in item_name_to_id) + + +LogicCacheType = Dict[int, Tuple[_Counter[Tuple[str, int]], FrozenSet[Location]]] + + +def cs_to_zz_locs(cs: CollectionState, p: int, zz_r: Randomizer, id_to_zz_item: Dict[int, Item]) -> FrozenSet[Location]: + """ + given an Archipelago `CollectionState`, + returns frozenset of accessible zilliandomizer locations + """ + # caching this function because it would be slow + logic_cache: LogicCacheType = getattr(cs.world, "zillion_logic_cache", {}) + _hash = set_randomizer_locs(cs, p, zz_r) + counts = item_counts(cs, p) + _hash += hash(counts) + + if _hash in logic_cache and logic_cache[_hash][0] == cs.prog_items: + # print("cache hit") + return logic_cache[_hash][1] + + # print("cache miss") + have_items: List[Item] = [] + for name, count in counts: + have_items.extend([id_to_zz_item[item_name_to_id[name]]] * count) + # have_req is the result of converting AP CollectionState to zilliandomizer collection state + have_req = zz_r.make_ability(have_items) + + # This `get_locations` is where the core of the logic comes in. + # It takes a zilliandomizer collection state (a set of the abilities that I have) + # and returns list of all the zilliandomizer locations I can access with those abilities. + tr = frozenset(zz_r.get_locations(have_req)) + + # save result in cache + logic_cache[_hash] = (cs.prog_items.copy(), tr) + + return tr diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py new file mode 100644 index 0000000000..4e7d3b6a70 --- /dev/null +++ b/worlds/zillion/options.py @@ -0,0 +1,380 @@ +from collections import Counter +# import logging +from typing import TYPE_CHECKING, Any, Dict, Tuple, cast +from Options import AssembleOptions, DefaultOnToggle, Range, SpecialRange, Toggle, Choice +from zilliandomizer.options import \ + Options as ZzOptions, char_to_gun, char_to_jump, ID, \ + VBLR as ZzVBLR, chars, Chars, ItemCounts as ZzItemCounts +from zilliandomizer.options.parsing import validate as zz_validate +if TYPE_CHECKING: + from BaseClasses import MultiWorld + + +class ZillionContinues(SpecialRange): + """ + number of continues before game over + + game over teleports you to your ship, keeping items and open doors + """ + default = 3 + range_start = 0 + range_end = 21 + display_name = "continues" + special_range_names = { + "vanilla": 3, + "infinity": 21 + } + + +class ZillionEarlyScope(Toggle): + """ whether to make sure there is a scope available early """ + display_name = "early scope" + + +class ZillionFloppyReq(Range): + """ how many floppy disks are required """ + range_start = 0 + range_end = 8 + default = 5 + display_name = "floppies required" + + +class VBLR(Choice): + option_vanilla = 0 + option_balanced = 1 + option_low = 2 + option_restrictive = 3 + default = 1 + + +class ZillionGunLevels(VBLR): + """ + Zillion gun power for the number of Zillion power ups you pick up + + For "restrictive", Champ is the only one that can get Zillion gun power level 3. + """ + display_name = "gun levels" + + +class ZillionJumpLevels(VBLR): + """ + jump levels for each character level + + For "restrictive", Apple is the only one that can get jump level 3. + """ + display_name = "jump levels" + + +class ZillionRandomizeAlarms(DefaultOnToggle): + """ whether to randomize the locations of alarm sensors """ + display_name = "randomize alarms" + + +class ZillionMaxLevel(Range): + """ the highest level you can get """ + range_start = 3 + range_end = 8 + default = 8 + display_name = "max level" + + +class ZillionOpasPerLevel(Range): + """ + how many Opa-Opas are required to level up + + Lower makes you level up faster. + """ + range_start = 1 + range_end = 5 + default = 2 + display_name = "Opa-Opas per level" + + +class ZillionStartChar(Choice): + """ which character you start with """ + option_jj = 0 + option_apple = 1 + option_champ = 2 + display_name = "start character" + default = "random" + + +class ZillionIDCardCount(Range): + """ + how many ID Cards are in the game + + Vanilla is 63 + + maximum total for all items is 144 + """ + range_start = 0 + range_end = 126 + default = 42 + display_name = "ID Card count" + + +class ZillionBreadCount(Range): + """ + how many Breads are in the game + + Vanilla is 33 + + maximum total for all items is 144 + """ + range_start = 0 + range_end = 126 + default = 50 + display_name = "Bread count" + + +class ZillionOpaOpaCount(Range): + """ + how many Opa-Opas are in the game + + Vanilla is 26 + + maximum total for all items is 144 + """ + range_start = 0 + range_end = 126 + default = 26 + display_name = "Opa-Opa count" + + +class ZillionZillionCount(Range): + """ + how many Zillion gun power ups are in the game + + Vanilla is 6 + + maximum total for all items is 144 + """ + range_start = 0 + range_end = 126 + default = 8 + display_name = "Zillion power up count" + + +class ZillionFloppyDiskCount(Range): + """ + how many Floppy Disks are in the game + + Vanilla is 5 + + maximum total for all items is 144 + """ + range_start = 0 + range_end = 126 + default = 7 + display_name = "Floppy Disk count" + + +class ZillionScopeCount(Range): + """ + how many Scopes are in the game + + Vanilla is 4 + + maximum total for all items is 144 + """ + range_start = 0 + range_end = 126 + default = 4 + display_name = "Scope count" + + +class ZillionRedIDCardCount(Range): + """ + how many Red ID Cards are in the game + + Vanilla is 1 + + maximum total for all items is 144 + """ + range_start = 0 + range_end = 126 + default = 2 + display_name = "Red ID Card count" + + +class ZillionSkill(Range): + """ the difficulty level of the game """ + range_start = 0 + range_end = 5 + default = 2 + + +class ZillionStartingCards(SpecialRange): + """ + how many ID Cards to start the game with + + Refilling at the ship also ensures you have at least this many cards. + 0 gives vanilla behavior. + """ + default = 2 + range_start = 0 + range_end = 10 + display_name = "starting cards" + special_range_names = { + "vanilla": 0 + } + + +class ZillionRoomGen(Toggle): + """ whether to generate rooms with random terrain """ + display_name = "room generation" + + +zillion_options: Dict[str, AssembleOptions] = { + "continues": ZillionContinues, + # "early_scope": ZillionEarlyScope, # TODO: implement + "floppy_req": ZillionFloppyReq, + "gun_levels": ZillionGunLevels, + "jump_levels": ZillionJumpLevels, + "randomize_alarms": ZillionRandomizeAlarms, + "max_level": ZillionMaxLevel, + "start_char": ZillionStartChar, + "opas_per_level": ZillionOpasPerLevel, + "id_card_count": ZillionIDCardCount, + "bread_count": ZillionBreadCount, + "opa_opa_count": ZillionOpaOpaCount, + "zillion_count": ZillionZillionCount, + "floppy_disk_count": ZillionFloppyDiskCount, + "scope_count": ZillionScopeCount, + "red_id_card_count": ZillionRedIDCardCount, + "skill": ZillionSkill, + "starting_cards": ZillionStartingCards, + "room_gen": ZillionRoomGen, +} + + +def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts: + tr: ZzItemCounts = { + ID.card: ic["ID Card"], + ID.red: ic["Red ID Card"], + ID.floppy: ic["Floppy Disk"], + ID.bread: ic["Bread"], + ID.gun: ic["Zillion"], + ID.opa: ic["Opa-Opa"], + ID.scope: ic["Scope"], + ID.empty: ic["Empty"], + } + return tr + + +def validate(world: "MultiWorld", p: int) -> "Tuple[ZzOptions, Counter[str]]": + """ + adjusts options to make game completion possible + + `world` parameter is MultiWorld object that has my options on it + `p` is my player id + """ + for option_name in zillion_options: + assert hasattr(world, option_name), f"Zillion option {option_name} didn't get put in world object" + wo = cast(Any, world) # so I don't need getattr on all the options + + skill = wo.skill[p].value + + jump_levels = cast(ZillionJumpLevels, wo.jump_levels[p]) + jump_option = jump_levels.get_current_option_name().lower() + required_level = char_to_jump["Apple"][cast(ZzVBLR, jump_option)].index(3) + 1 + if skill == 0: + # because of hp logic on final boss + required_level = 8 + + gun_levels = cast(ZillionGunLevels, wo.gun_levels[p]) + gun_option = gun_levels.get_current_option_name().lower() + guns_required = char_to_gun["Champ"][cast(ZzVBLR, gun_option)].index(3) + + floppy_req = cast(ZillionFloppyReq, wo.floppy_req[p]) + + card = cast(ZillionIDCardCount, wo.id_card_count[p]) + bread = cast(ZillionBreadCount, wo.bread_count[p]) + opa = cast(ZillionOpaOpaCount, wo.opa_opa_count[p]) + gun = cast(ZillionZillionCount, wo.zillion_count[p]) + floppy = cast(ZillionFloppyDiskCount, wo.floppy_disk_count[p]) + scope = cast(ZillionScopeCount, wo.scope_count[p]) + red = cast(ZillionRedIDCardCount, wo.red_id_card_count[p]) + item_counts = Counter({ + "ID Card": card, + "Bread": bread, + "Opa-Opa": opa, + "Zillion": gun, + "Floppy Disk": floppy, + "Scope": scope, + "Red ID Card": red + }) + minimums = Counter({ + "ID Card": 0, + "Bread": 0, + "Opa-Opa": required_level - 1, + "Zillion": guns_required, + "Floppy Disk": floppy_req.value, + "Scope": 0, + "Red ID Card": 1 + }) + for key in minimums: + item_counts[key] = max(minimums[key], item_counts[key]) + max_movables = 144 - sum(minimums.values()) + movables = item_counts - minimums + while sum(movables.values()) > max_movables: + # logging.warning("zillion options validate: player options item counts too high") + total = sum(movables.values()) + scaler = max_movables / total + for key in movables: + movables[key] = int(movables[key] * scaler) + item_counts = movables + minimums + + # now have required items, and <= 144 + + # now fill remaining with empty + total = sum(item_counts.values()) + diff = 144 - total + if "Empty" not in item_counts: + item_counts["Empty"] = 0 + item_counts["Empty"] += diff + assert sum(item_counts.values()) == 144 + + max_level = cast(ZillionMaxLevel, wo.max_level[p]) + max_level.value = max(required_level, max_level.value) + + opas_per_level = cast(ZillionOpasPerLevel, wo.opas_per_level[p]) + while (opas_per_level.value > 1) and (1 + item_counts["Opa-Opa"] // opas_per_level.value < max_level.value): + # logging.warning( + # "zillion options validate: option opas_per_level incompatible with options max_level and opa_opa_count" + # ) + opas_per_level.value -= 1 + + # that should be all of the level requirements met + + start_char = cast(ZillionStartChar, wo.start_char[p]) + start_char_name = start_char.get_current_option_name() + if start_char_name == "Jj": + start_char_name = "JJ" + assert start_char_name in chars + start_char_name = cast(Chars, start_char_name) + + starting_cards = cast(ZillionStartingCards, wo.starting_cards[p]) + + room_gen = cast(ZillionRoomGen, wo.room_gen[p]) + + zz_item_counts = convert_item_counts(item_counts) + zz_op = ZzOptions( + zz_item_counts, + cast(ZzVBLR, jump_option), + cast(ZzVBLR, gun_option), + opas_per_level.value, + max_level.value, + False, # tutorial + skill, + start_char_name, + floppy_req.value, + wo.continues[p].value, + wo.randomize_alarms[p].value, + False, # wo.early_scope[p].value, + True, # balance defense + starting_cards.value, + bool(room_gen.value) + ) + zz_validate(zz_op) + return zz_op, item_counts diff --git a/worlds/zillion/patch.py b/worlds/zillion/patch.py new file mode 100644 index 0000000000..148caac9fb --- /dev/null +++ b/worlds/zillion/patch.py @@ -0,0 +1,34 @@ +from typing import BinaryIO, Optional, cast +import Utils +from worlds.Files import APDeltaPatch +import os + +USHASH = 'd4bf9e7bcf9a48da53785d2ae7bc4270' + + +class ZillionDeltaPatch(APDeltaPatch): + hash = USHASH + game = "Zillion" + patch_file_ending = ".apzl" + result_file_ending = ".sms" + + @classmethod + def get_source_data(cls) -> bytes: + with open(get_base_rom_path(), "rb") as stream: + return read_rom(stream) + + +def get_base_rom_path(file_name: Optional[str] = None) -> str: + options = Utils.get_options() + if not file_name: + file_name = cast(str, options["zillion_options"]["rom_file"]) + if not os.path.exists(file_name): + file_name = Utils.user_path(file_name) + return file_name + + +def read_rom(stream: BinaryIO) -> bytes: + """ reads rom into bytearray """ + data = stream.read() + # I'm not aware of any sms header. + return data diff --git a/worlds/zillion/py.typed b/worlds/zillion/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/worlds/zillion/region.py b/worlds/zillion/region.py new file mode 100644 index 0000000000..53949f7336 --- /dev/null +++ b/worlds/zillion/region.py @@ -0,0 +1,50 @@ +from typing import Optional +from BaseClasses import MultiWorld, Region, RegionType, Location, Item, CollectionState +from zilliandomizer.logic_components.regions import Region as ZzRegion +from zilliandomizer.logic_components.locations import Location as ZzLocation +from zilliandomizer.logic_components.items import RESCUE + +from .id_maps import loc_name_to_id +from .item import ZillionItem + + +class ZillionRegion(Region): + zz_r: ZzRegion + + def __init__(self, + zz_r: ZzRegion, + name: str, + type_: RegionType, + hint: str, + player: int, + world: Optional[MultiWorld] = None) -> None: + super().__init__(name, type_, hint, player, world) + self.zz_r = zz_r + + +class ZillionLocation(Location): + zz_loc: ZzLocation + game: str = "Zillion" + + def __init__(self, + zz_loc: ZzLocation, + player: int, + name: str, + parent: Optional[Region] = None) -> None: + loc_id = loc_name_to_id[name] + super().__init__(player, name, loc_id, parent) + self.zz_loc = zz_loc + + # override + def can_fill(self, state: CollectionState, item: Item, check_access: bool = True) -> bool: + saved_gun_req = -1 + if isinstance(item, ZillionItem) \ + and item.zz_item.code == RESCUE \ + and self.player == item.player: + # RESCUE removes the gun requirement from a location. + saved_gun_req = self.zz_loc.req.gun + self.zz_loc.req.gun = 0 + super_result = super().can_fill(state, item, check_access) + if saved_gun_req != -1: + self.zz_loc.req.gun = saved_gun_req + return super_result diff --git a/worlds/zillion/requirements.txt b/worlds/zillion/requirements.txt new file mode 100644 index 0000000000..0ed98771bd --- /dev/null +++ b/worlds/zillion/requirements.txt @@ -0,0 +1 @@ +git+https://github.com/beauxq/zilliandomizer@45a45eaca4119a4d06d2c31546ad19f3abd77f63#egg=zilliandomizer==0.4.4