mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 04:01:32 -06:00

* First Pass removal of game-specific code * SMW, DKC3, and SM hooked into AutoClient * All SNES autoclients functional * Fix ALttP Deathlink * Don't default to being ALttP, and properly error check ctx.game * Adjust variable naming * In response to: > we should probably document usage somewhere. I'm open to suggestions of where this should be documented. I think the most valuable documentation for APIs is docstrings and full typing. about websockets change in imports - from websockets documentation: > For convenience, many public APIs can be imported from the websockets package. However, this feature is incompatible with static code analysis. It breaks autocompletion in an IDE or type checking with mypy. If you’re using such tools, use the real import paths. * todo note for python 3.11 typing.NotRequired * missed staging in previous commit * added missing death Game States for DeathLink Co-authored-by: beauxq <beauxq@users.noreply.github.com> Co-authored-by: lordlou <87331798+lordlou@users.noreply.github.com>
248 lines
11 KiB
Python
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)
|