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