388 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			388 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| 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
 | 
