Archipelago/worlds/yoshisisland/__init__.py

388 lines
17 KiB
Python
Raw Normal View History

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",
2024-04-10 04:05:02 +00:00
"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