Archipelago/worlds/smw/__init__.py

248 lines
11 KiB
Python

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 .Client import SMWSNIClient
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)