import base64 import os import typing import threading from typing import List, Set, TextIO, Dict from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification from worlds.AutoWorld import World, WebWorld import settings from .Items import get_item_names_per_category, item_table, filler_items, trap_items from .Locations import get_locations from .Regions import init_areas from .Options import YoshisIslandOptions, PlayerGoal, ObjectVis, StageLogic, MinigameChecks from .setup_game import setup_gamevars from .Client import YoshisIslandSNIClient from .Rules import set_easy_rules, set_normal_rules, set_hard_rules from .Rom import LocalRom, patch_rom, get_base_rom_path, YoshisIslandDeltaPatch, USHASH class YoshisIslandSettings(settings.Group): class RomFile(settings.SNESRomPath): """File name of the Yoshi's Island 1.0 US rom""" description = "Yoshi's Island ROM File" copy_to = "Super Mario World 2 - Yoshi's Island (U).sfc" md5s = [USHASH] rom_file: RomFile = RomFile(RomFile.copy_to) class YoshisIslandWeb(WebWorld): theme = "ocean" setup_en = Tutorial( "Multiworld Setup Guide", "A guide to setting up the Yoshi's Island randomizer and connecting to an Archipelago server.", "English", "setup_en.md", "setup/en", ["Pink Switch"] ) tutorials = [setup_en] class YoshisIslandWorld(World): """ Yoshi's Island is a 2D platforming game. During a delivery, Bowser's evil ward, Kamek, attacked the stork, kidnapping Luigi and dropping Mario onto Yoshi's Island. As Yoshi, you must run, jump, and throw eggs to escort the baby Mario across the island to defeat Bowser and reunite the two brothers with their parents. """ game = "Yoshi's Island" option_definitions = YoshisIslandOptions required_client_version = (0, 4, 4) item_name_to_id = {item: item_table[item].code for item in item_table} location_name_to_id = {location.name: location.code for location in get_locations(None)} item_name_groups = get_item_names_per_category() web = YoshisIslandWeb() settings: typing.ClassVar[YoshisIslandSettings] # topology_present = True options_dataclass = YoshisIslandOptions options: YoshisIslandOptions locked_locations: List[str] set_req_bosses: str lives_high: int lives_low: int castle_bosses: int bowser_bosses: int baby_mario_sfx: int leader_color: int boss_order: list boss_burt: int luigi_count: int rom_name: bytearray def __init__(self, multiworld: MultiWorld, player: int): self.rom_name_available_event = threading.Event() super().__init__(multiworld, player) self.locked_locations = [] @classmethod def stage_assert_generate(cls, multiworld: MultiWorld) -> None: rom_file = get_base_rom_path() if not os.path.exists(rom_file): raise FileNotFoundError(rom_file) def fill_slot_data(self) -> Dict[str, List[int]]: return { "world_1": self.world_1_stages, "world_2": self.world_2_stages, "world_3": self.world_3_stages, "world_4": self.world_4_stages, "world_5": self.world_5_stages, "world_6": self.world_6_stages } def write_spoiler_header(self, spoiler_handle: TextIO) -> None: spoiler_handle.write(f"Burt The Bashful's Boss Door: {self.boss_order[0]}\n") spoiler_handle.write(f"Salvo The Slime's Boss Door: {self.boss_order[1]}\n") spoiler_handle.write(f"Bigger Boo's Boss Door: {self.boss_order[2]}\n") spoiler_handle.write(f"Roger The Ghost's Boss Door: {self.boss_order[3]}\n") spoiler_handle.write(f"Prince Froggy's Boss Door: {self.boss_order[4]}\n") spoiler_handle.write(f"Naval Piranha's Boss Door: {self.boss_order[5]}\n") spoiler_handle.write(f"Marching Milde's Boss Door: {self.boss_order[6]}\n") spoiler_handle.write(f"Hookbill The Koopa's Boss Door: {self.boss_order[7]}\n") spoiler_handle.write(f"Sluggy The Unshaven's Boss Door: {self.boss_order[8]}\n") spoiler_handle.write(f"Raphael The Raven's Boss Door: {self.boss_order[9]}\n") spoiler_handle.write(f"Tap-Tap The Red Nose's Boss Door: {self.boss_order[10]}\n") spoiler_handle.write(f"\nLevels:\n1-1: {self.level_name_list[0]}\n") spoiler_handle.write(f"1-2: {self.level_name_list[1]}\n") spoiler_handle.write(f"1-3: {self.level_name_list[2]}\n") spoiler_handle.write(f"1-4: {self.level_name_list[3]}\n") spoiler_handle.write(f"1-5: {self.level_name_list[4]}\n") spoiler_handle.write(f"1-6: {self.level_name_list[5]}\n") spoiler_handle.write(f"1-7: {self.level_name_list[6]}\n") spoiler_handle.write(f"1-8: {self.level_name_list[7]}\n") spoiler_handle.write(f"\n2-1: {self.level_name_list[8]}\n") spoiler_handle.write(f"2-2: {self.level_name_list[9]}\n") spoiler_handle.write(f"2-3: {self.level_name_list[10]}\n") spoiler_handle.write(f"2-4: {self.level_name_list[11]}\n") spoiler_handle.write(f"2-5: {self.level_name_list[12]}\n") spoiler_handle.write(f"2-6: {self.level_name_list[13]}\n") spoiler_handle.write(f"2-7: {self.level_name_list[14]}\n") spoiler_handle.write(f"2-8: {self.level_name_list[15]}\n") spoiler_handle.write(f"\n3-1: {self.level_name_list[16]}\n") spoiler_handle.write(f"3-2: {self.level_name_list[17]}\n") spoiler_handle.write(f"3-3: {self.level_name_list[18]}\n") spoiler_handle.write(f"3-4: {self.level_name_list[19]}\n") spoiler_handle.write(f"3-5: {self.level_name_list[20]}\n") spoiler_handle.write(f"3-6: {self.level_name_list[21]}\n") spoiler_handle.write(f"3-7: {self.level_name_list[22]}\n") spoiler_handle.write(f"3-8: {self.level_name_list[23]}\n") spoiler_handle.write(f"\n4-1: {self.level_name_list[24]}\n") spoiler_handle.write(f"4-2: {self.level_name_list[25]}\n") spoiler_handle.write(f"4-3: {self.level_name_list[26]}\n") spoiler_handle.write(f"4-4: {self.level_name_list[27]}\n") spoiler_handle.write(f"4-5: {self.level_name_list[28]}\n") spoiler_handle.write(f"4-6: {self.level_name_list[29]}\n") spoiler_handle.write(f"4-7: {self.level_name_list[30]}\n") spoiler_handle.write(f"4-8: {self.level_name_list[31]}\n") spoiler_handle.write(f"\n5-1: {self.level_name_list[32]}\n") spoiler_handle.write(f"5-2: {self.level_name_list[33]}\n") spoiler_handle.write(f"5-3: {self.level_name_list[34]}\n") spoiler_handle.write(f"5-4: {self.level_name_list[35]}\n") spoiler_handle.write(f"5-5: {self.level_name_list[36]}\n") spoiler_handle.write(f"5-6: {self.level_name_list[37]}\n") spoiler_handle.write(f"5-7: {self.level_name_list[38]}\n") spoiler_handle.write(f"5-8: {self.level_name_list[39]}\n") spoiler_handle.write(f"\n6-1: {self.level_name_list[40]}\n") spoiler_handle.write(f"6-2: {self.level_name_list[41]}\n") spoiler_handle.write(f"6-3: {self.level_name_list[42]}\n") spoiler_handle.write(f"6-4: {self.level_name_list[43]}\n") spoiler_handle.write(f"6-5: {self.level_name_list[44]}\n") spoiler_handle.write(f"6-6: {self.level_name_list[45]}\n") spoiler_handle.write(f"6-7: {self.level_name_list[46]}\n") spoiler_handle.write("6-8: King Bowser's Castle") def create_item(self, name: str) -> Item: data = item_table[name] return Item(name, data.classification, data.code, self.player) def create_regions(self) -> None: init_areas(self, get_locations(self)) def get_filler_item_name(self) -> str: trap_chance: int = self.options.trap_percent.value if self.random.random() < (trap_chance / 100) and self.options.traps_enabled: return self.random.choice(trap_items) else: return self.random.choice(filler_items) def set_rules(self) -> None: rules_per_difficulty = { 0: set_easy_rules, 1: set_normal_rules, 2: set_hard_rules } rules_per_difficulty[self.options.stage_logic.value](self) self.multiworld.completion_condition[self.player] = lambda state: state.has("Saved Baby Luigi", self.player) self.get_location("Burt The Bashful's Boss Room").place_locked_item(self.create_item("Boss Clear")) self.get_location("Salvo The Slime's Boss Room").place_locked_item(self.create_item("Boss Clear")) self.get_location("Bigger Boo's Boss Room", ).place_locked_item(self.create_item("Boss Clear")) self.get_location("Roger The Ghost's Boss Room").place_locked_item(self.create_item("Boss Clear")) self.get_location("Prince Froggy's Boss Room").place_locked_item(self.create_item("Boss Clear")) self.get_location("Naval Piranha's Boss Room").place_locked_item(self.create_item("Boss Clear")) self.get_location("Marching Milde's Boss Room").place_locked_item(self.create_item("Boss Clear")) self.get_location("Hookbill The Koopa's Boss Room").place_locked_item(self.create_item("Boss Clear")) self.get_location("Sluggy The Unshaven's Boss Room").place_locked_item(self.create_item("Boss Clear")) self.get_location("Raphael The Raven's Boss Room").place_locked_item(self.create_item("Boss Clear")) self.get_location("Tap-Tap The Red Nose's Boss Room").place_locked_item(self.create_item("Boss Clear")) if self.options.goal == PlayerGoal.option_luigi_hunt: self.get_location("Reconstituted Luigi").place_locked_item(self.create_item("Saved Baby Luigi")) else: self.get_location("King Bowser's Castle: Level Clear").place_locked_item( self.create_item("Saved Baby Luigi") ) self.get_location("Touch Fuzzy Get Dizzy: Gather Coins").place_locked_item( self.create_item("Bandit Consumables") ) self.get_location("The Cave Of the Mystery Maze: Seed Spitting Contest").place_locked_item( self.create_item("Bandit Watermelons") ) self.get_location("Lakitu's Wall: Gather Coins").place_locked_item(self.create_item("Bandit Consumables")) self.get_location("Ride Like The Wind: Gather Coins").place_locked_item(self.create_item("Bandit Consumables")) def generate_early(self) -> None: setup_gamevars(self) def get_excluded_items(self) -> Set[str]: excluded_items: Set[str] = set() starting_gate = ["World 1 Gate", "World 2 Gate", "World 3 Gate", "World 4 Gate", "World 5 Gate", "World 6 Gate"] excluded_items.add(starting_gate[self.options.starting_world]) if not self.options.shuffle_midrings: excluded_items.add("Middle Ring") if not self.options.add_secretlens: excluded_items.add("Secret Lens") if not self.options.extras_enabled: excluded_items.add("Extra Panels") excluded_items.add("Extra 1") excluded_items.add("Extra 2") excluded_items.add("Extra 3") excluded_items.add("Extra 4") excluded_items.add("Extra 5") excluded_items.add("Extra 6") if self.options.split_extras: excluded_items.add("Extra Panels") else: excluded_items.add("Extra 1") excluded_items.add("Extra 2") excluded_items.add("Extra 3") excluded_items.add("Extra 4") excluded_items.add("Extra 5") excluded_items.add("Extra 6") if self.options.split_bonus: excluded_items.add("Bonus Panels") else: excluded_items.add("Bonus 1") excluded_items.add("Bonus 2") excluded_items.add("Bonus 3") excluded_items.add("Bonus 4") excluded_items.add("Bonus 5") excluded_items.add("Bonus 6") return excluded_items def create_item_with_correct_settings(self, name: str) -> Item: data = item_table[name] item = Item(name, data.classification, data.code, self.player) if not item.advancement: return item if name == "Car Morph" and self.options.stage_logic != StageLogic.option_strict: item.classification = ItemClassification.useful secret_lens_visibility_check = ( self.options.hidden_object_visibility >= ObjectVis.option_clouds_only or self.options.stage_logic != StageLogic.option_strict ) if name == "Secret Lens" and secret_lens_visibility_check: item.classification = ItemClassification.useful is_bonus_location = name in {"Bonus 1", "Bonus 2", "Bonus 3", "Bonus 4", "Bonus 5", "Bonus 6", "Bonus Panels"} bonus_games_disabled = ( self.options.minigame_checks not in {MinigameChecks.option_bonus_games, MinigameChecks.option_both} ) if is_bonus_location and bonus_games_disabled: item.classification = ItemClassification.useful if name in {"Bonus 1", "Bonus 3", "Bonus 4", "Bonus Panels"} and self.options.item_logic: item.classification = ItemClassification.progression if name == "Piece of Luigi" and self.options.goal == PlayerGoal.option_luigi_hunt: if self.luigi_count >= self.options.luigi_pieces_required: item.classification = ItemClassification.useful else: item.classification = ItemClassification.progression_skip_balancing self.luigi_count += 1 return item def generate_filler(self, pool: List[Item]) -> None: if self.options.goal == PlayerGoal.option_luigi_hunt: for _ in range(self.options.luigi_pieces_in_pool.value): item = self.create_item_with_correct_settings("Piece of Luigi") pool.append(item) for _ in range(len(self.multiworld.get_unfilled_locations(self.player)) - len(pool) - 16): item = self.create_item_with_correct_settings(self.get_filler_item_name()) pool.append(item) def get_item_pool(self, excluded_items: Set[str]) -> List[Item]: pool: List[Item] = [] for name, data in item_table.items(): if name not in excluded_items: for _ in range(data.amount): item = self.create_item_with_correct_settings(name) pool.append(item) return pool def create_items(self) -> None: self.luigi_count = 0 if self.options.minigame_checks in {MinigameChecks.option_bonus_games, MinigameChecks.option_both}: self.multiworld.get_location("Flip Cards", self.player).place_locked_item( self.create_item("Bonus Consumables")) self.multiworld.get_location("Drawing Lots", self.player).place_locked_item( self.create_item("Bonus Consumables")) self.multiworld.get_location("Match Cards", self.player).place_locked_item( self.create_item("Bonus Consumables")) pool = self.get_item_pool(self.get_excluded_items()) self.generate_filler(pool) self.multiworld.itempool += pool def generate_output(self, output_directory: str) -> None: rompath = "" # if variable is not declared finally clause may fail try: world = self.multiworld player = self.player rom = LocalRom(get_base_rom_path()) patch_rom(self, rom, self.player) rompath = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc") rom.write_to_file(rompath) self.rom_name = rom.name patch = YoshisIslandDeltaPatch(os.path.splitext(rompath)[0] + YoshisIslandDeltaPatch.patch_file_ending, player=player, player_name=world.player_name[player], patched_path=rompath) patch.write() finally: self.rom_name_available_event.set() if os.path.exists(rompath): os.unlink(rompath) def modify_multidata(self, multidata: dict) -> None: # wait for self.rom_name to be available. self.rom_name_available_event.wait() rom_name = getattr(self, "rom_name", None) if rom_name: new_name = base64.b64encode(bytes(self.rom_name)).decode() multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]] def extend_hint_information(self, hint_data: typing.Dict[int, typing.Dict[int, str]]) -> None: world_names = [f"World {i}" for i in range(1, 7)] world_stages = [ self.world_1_stages, self.world_2_stages, self.world_3_stages, self.world_4_stages, self.world_5_stages, self.world_6_stages ] stage_pos_data = {} for loc in self.multiworld.get_locations(self.player): if loc.address is None: continue level_id = getattr(loc, "level_id") for level, stages in zip(world_names, world_stages): if level_id in stages: stage_pos_data[loc.address] = level break hint_data[self.player] = stage_pos_data