Files
dockipelago/worlds/papermariottyd/__init__.py
Jonathan Tinney 7971961166
Some checks failed
Analyze modified files / flake8 (push) Failing after 2m28s
Build / build-win (push) Has been cancelled
Build / build-ubuntu2204 (push) Has been cancelled
ctest / Test C++ ubuntu-latest (push) Has been cancelled
ctest / Test C++ windows-latest (push) Has been cancelled
Analyze modified files / mypy (push) Has been cancelled
Build and Publish Docker Images / Push Docker image to Docker Hub (push) Successful in 5m4s
Native Code Static Analysis / scan-build (push) Failing after 5m2s
type check / pyright (push) Successful in 1m7s
unittests / Test Python 3.11.2 ubuntu-latest (push) Failing after 16m23s
unittests / Test Python 3.12 ubuntu-latest (push) Failing after 28m19s
unittests / Test Python 3.13 ubuntu-latest (push) Failing after 14m49s
unittests / Test hosting with 3.13 on ubuntu-latest (push) Successful in 5m0s
unittests / Test Python 3.13 macos-latest (push) Has been cancelled
unittests / Test Python 3.11 windows-latest (push) Has been cancelled
unittests / Test Python 3.13 windows-latest (push) Has been cancelled
add schedule I, sonic 1/frontiers/heroes, spirit island
2026-04-02 23:46:36 -07:00

542 lines
29 KiB
Python

import logging
import os
from Fill import fill_restrictive, fast_fill
from typing import List, Dict, ClassVar, Any, Set
from settings import UserFilePath, Group
from BaseClasses import Tutorial, ItemClassification, CollectionState, Item, Location
from worlds.AutoWorld import WebWorld, World
from .Data import starting_partners, stars, limit_pit, \
pit_exclusive_tattle_stars_required, dazzle_counts, dazzle_location_names, chapter_keysanity_tags, \
chapter_keys, limited_tags, limited_tag_items
from .Enemy import Encounter, parse_json_encounters, randomize_encounters
from .Locations import all_locations, location_table, location_id_to_name, TTYDLocation, locationName_to_data, \
get_locations_by_tags, get_vanilla_item_names, get_location_names, LocationData
from .Options import Piecesanity, TTYDOptions, YoshiColor, StartingPartner, PitItems, LimitChapterEight, Goal, \
DazzleRewards, StarShuffle, EnemyRandomizer
from .Items import TTYDItem, itemList, item_table, ItemData, items_by_id
from .Regions import create_regions, connect_regions, get_regions_dict, register_indirect_connections
from .Rom import TTYDProcedurePatch, write_files
from .Rules import set_rules, get_tattle_rules_dict, set_tattle_rules, get_random_enemy_tattle_rules_dict
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, launch_subprocess
def launch_client(*args):
from .TTYDClient import launch
launch_subprocess(launch, name="TTYDClient", args=args)
components.append(
Component(
"TTYDClient",
func=launch_client,
component_type=Type.CLIENT,
file_identifier=SuffixIdentifier(".apttyd"),
description="Open the Paper Mario: The Thousand-Year Door client.",
),
)
class TTYDWebWorld(WebWorld):
theme = 'partyTime'
bug_report_page = "https://github.com/jamesbrq/ArchipelagoTTYD/issues"
tutorials = [
Tutorial(
tutorial_name='Setup Guide',
description='A guide to setting up Paper Mario: The Thousand-Year Door for Archipelago.',
language='English',
file_name='setup_en.md',
link='setup/en',
authors=['jamesbrq']
)
]
class TTYDSettings(Group):
class DolphinPath(UserFilePath):
"""
The location of the Dolphin you want to auto launch patched ROMs with
"""
is_exe = True
description = "Dolphin Executable"
class RomFile(UserFilePath):
"""File name of the TTYD US iso"""
copy_to = "Paper Mario - The Thousand-Year Door (USA).iso"
description = "US TTYD .iso File"
dolphin_path: DolphinPath = DolphinPath(None)
rom_file: RomFile = RomFile(RomFile.copy_to)
rom_start: bool = True
class TTYDWorld(World):
"""
Paper Mario: The Thousand-Year Door is a quirky, turn-based RPG with a paper-craft twist.
Mario teams up with oddball allies to stop an ancient evil sealed behind a magical door.
Set in Rogueport, the game mixes platforming, puzzles, and witty, self-aware dialogue.
Battles play out on a stage with timed button presses and a live audience cheering you on.
"""
game = "Paper Mario: The Thousand-Year Door"
web = TTYDWebWorld()
options_dataclass = TTYDOptions
options: TTYDOptions
settings: ClassVar[TTYDSettings]
item_name_to_id = {name: data.id for name, data in item_table.items()}
location_name_to_id = {loc_data.name: loc_data.id for loc_data in all_locations}
required_client_version = (0, 6, 2)
disabled_locations: set
excluded_regions: set
required_chapters: List[int]
limited_chapters: List[int]
limited_chapter_locations: Dict[int, Dict[str, Set[Location]]]
limited_misc_locations: Set[Location]
limited_misc_items: List[Item]
limited_items: Dict[int, Dict[str, List[Item]]]
limited_state: CollectionState = None
locked_item_frequencies: Dict[str, int]
in_pre_fill: bool
encounters: list[Encounter] = None
ut_can_gen_without_yaml = True
def generate_early(self) -> None:
self.disabled_locations = set()
self.excluded_regions = set()
self.required_chapters = []
self.limited_chapters = []
self.in_pre_fill = False
self.limited_chapter_locations = {chapter: {tag: set() for tag in limited_tags[chapter]} for chapter in
range(1, 9)}
self.limited_items = {chapter: {tag: list() for tag in limited_tags[chapter]} for chapter in range(1, 9)}
self.limited_misc_locations = set()
self.locked_item_frequencies = {}
self.encounters = parse_json_encounters()
# implementing yaml-less UT support
if hasattr(self.multiworld, "re_gen_passthrough"):
if self.game in self.multiworld.re_gen_passthrough:
slot_data = self.multiworld.re_gen_passthrough[self.game]
self.options.goal.value = slot_data["goal"]
self.options.goal_stars.value = slot_data["goal_stars"]
self.options.palace_stars.value = slot_data["palace_stars"]
self.options.pit_items.value = slot_data["pit_items"]
self.options.limit_chapter_logic.value = slot_data["limit_chapter_logic"]
self.options.limit_chapter_eight.value = slot_data["limit_chapter_eight"]
self.options.palace_skip.value = slot_data["palace_skip"]
self.options.open_westside.value = slot_data["westside"]
self.options.tattlesanity.value = slot_data["tattlesanity"]
self.options.dazzle_rewards.value = slot_data["dazzle_rewards"]
self.options.star_shuffle.value = slot_data["star_shuffle"]
self.options.disable_intermissions.value = slot_data["disable_intermissions"]
self.options.piecesanity.value = slot_data["piecesanity"]
self.options.shinesanity.value = slot_data["shinesanity"]
self.options.blue_pipe_toggle.value = slot_data["blue_pipe_toggle"]
self.options.enemy_randomizer.value = slot_data["enemy_randomizer"]
return
if self.options.limit_chapter_eight and self.options.palace_skip:
logging.warning(f"{self.player_name}'s has enabled both Palace Skip and Limit Chapter 8. "
f"Disabling the Limit Chapter 8 option due to incompatibility.")
self.options.limit_chapter_eight.value = LimitChapterEight.option_false
if self.options.goal == Goal.option_bonetail and self.options.goal_stars < 5:
logging.warning(f"{self.player_name}'s has Bonetail as the goal with less than 5 stars required. "
f"Increasing number of goal stars to 5 for accessibility.")
self.options.goal_stars.value = 5
if self.options.palace_stars > self.options.goal_stars:
logging.warning(f"{self.player_name}'s has more palace stars required than goal stars. "
f"Reducing number of stars required to enter the palace of shadow for accessibility.")
self.options.palace_stars.value = self.options.goal_stars.value
chapters = [i for i in range(1, 8)]
if not self.options.required_stars_toggle:
for i in range(self.options.goal_stars.value):
self.required_chapters.append(chapters.pop(self.multiworld.random.randint(0, len(chapters) - 1)))
else:
star_names = self.options.required_stars.value
self.required_chapters = [chapter for name in star_names for chapter, star in stars.items() if star == name][:self.options.goal_stars.value]
if len(self.required_chapters) < self.options.goal_stars.value:
remaining_chapters = [i for i in range(1, 8) if i not in self.required_chapters]
for _ in range(self.options.goal_stars.value - len(self.required_chapters)):
self.required_chapters.append(
remaining_chapters.pop(self.multiworld.random.randint(0, len(remaining_chapters) - 1)))
if self.options.limit_chapter_logic:
self.limited_chapters += [chapter for chapter in chapters if chapter not in self.required_chapters]
if self.options.limit_chapter_eight:
self.limited_chapters += [8]
if self.options.palace_skip:
self.excluded_regions.update(["Palace of Shadow", "Palace of Shadow (Post-Riddle Tower)"])
if not self.options.tattlesanity:
self.excluded_regions.update(["Tattlesanity"])
if self.options.goal != Goal.option_shadow_queen:
self.excluded_regions.update(["Shadow Queen"])
if self.options.tattlesanity:
self.disabled_locations.update(["Tattle: Shadow Queen"])
if self.options.tattlesanity and self.options.disable_intermissions:
self.disabled_locations.update(["Tattle: Lord Crump"])
if self.options.enemy_randomizer != EnemyRandomizer.option_vanilla:
randomize_encounters(self)
if self.options.tattlesanity:
extra_disabled = [location.name for name, locations in get_regions_dict().items()
if name in self.excluded_regions for location in locations]
for location_name, locations in get_tattle_rules_dict().items():
if len(locations) == 0:
if "Palace of Shadow (Post-Riddle Tower)" in self.excluded_regions:
self.disabled_locations.update([location_name])
else:
if all([location_id_to_name[location] in self.disabled_locations or location_id_to_name[
location] in extra_disabled for location in locations]):
self.disabled_locations.update([location_name])
def create_regions(self) -> None:
create_regions(self)
connect_regions(self)
register_indirect_connections(self)
self.lock_item_remove_from_pool("Rogueport Center: Goombella",
starting_partners[self.options.starting_partner.value - 1])
if self.options.star_shuffle == StarShuffle.option_vanilla:
self.lock_vanilla_items_remove_from_pool(get_locations_by_tags("star"))
elif self.options.star_shuffle == StarShuffle.option_stars_only:
locations = get_locations_by_tags("star")
items = [location.vanilla_item for location in locations]
self.multiworld.random.shuffle(items)
for i, location in enumerate(locations):
self.lock_item_remove_from_pool(location.name, items_by_id[items[i]].item_name)
if self.options.goal == Goal.option_shadow_queen:
self.lock_item("Shadow Queen", "Victory")
if self.options.limit_chapter_eight:
for location in [location for location in get_locations_by_tags("chapter_8")]:
if "Palace Key (Tower)" in location.name:
self.lock_item_remove_from_pool(location.name, "Palace Key (Tower)")
elif "Palace Key" in location.name:
self.lock_item_remove_from_pool(location.name, "Palace Key")
self.lock_item_remove_from_pool("Palace of Shadow Gloomtail Room: Star Key", "Star Key")
if self.options.palace_skip:
self.locked_item_frequencies["Palace Key"] = 3
self.locked_item_frequencies["Palace Key (Tower)"] = 8
self.locked_item_frequencies["Star Key"] = 1
if self.options.pit_items == PitItems.option_vanilla:
self.lock_vanilla_items_remove_from_pool(get_locations_by_tags("pit_floor"))
if self.options.piecesanity == Piecesanity.option_vanilla:
self.lock_vanilla_items_remove_from_pool(get_locations_by_tags(["star_piece", "panel"]))
if self.options.piecesanity == Piecesanity.option_nonpanel_only:
self.lock_vanilla_items_remove_from_pool(get_locations_by_tags("panel"))
if not self.options.shinesanity:
self.lock_vanilla_items_remove_from_pool(get_locations_by_tags("shine"))
if not self.options.shopsanity:
self.lock_vanilla_items_remove_from_pool(get_locations_by_tags("shop"))
if self.options.pit_items == PitItems.option_filler:
self.lock_filler_items_remove_from_pool(get_locations_by_tags("pit_floor"))
if self.options.dazzle_rewards == DazzleRewards.option_vanilla:
self.lock_vanilla_items_remove_from_pool(get_locations_by_tags("dazzle"))
elif self.options.dazzle_rewards == DazzleRewards.option_filler:
self.lock_filler_items_remove_from_pool(get_locations_by_tags("dazzle"))
else:
for i, location in enumerate(dazzle_location_names):
if dazzle_counts[i] > 100 - self.locked_item_frequencies.get("Star Piece", 0):
self.lock_item(location, self.get_filler_item_name())
for chapter in self.limited_chapters:
self.lock_vanilla_items_remove_from_pool(
[location for location in get_locations_by_tags(f"chapter_{chapter}")
if items_by_id[location.vanilla_item].item_name == "Star Piece" and self.get_location(
location.name).item is None])
for chapter in self.limited_chapters:
for tag in limited_tags[chapter]:
locations = [self.get_location(location.name) for location in get_locations_by_tags(tag)
if location.name not in self.disabled_locations]
locations = [location for location in locations if location.item is None]
self.limited_chapter_locations[chapter][tag].update(locations)
if 3 in self.limited_chapters and self.options.limit_chapter_logic:
if self.get_location("Rogueport Blimp Room: Star Piece 1").item is None:
self.lock_item_remove_from_pool("Rogueport Blimp Room: Star Piece 1", self.get_filler_item_name())
if 5 in self.limited_chapters and self.options.limit_chapter_logic:
self.lock_item_remove_from_pool("Rogueport Westside: Train Ticket", self.get_filler_item_name())
if not self.options.keysanity:
for i in range(1, 9):
if i == 8 and self.options.limit_chapter_eight:
continue
tags = [chapter_keysanity_tags[i]] + (["riddle_tower"] if i == 8 else [])
locations = [self.get_location(location.name) for location in get_locations_by_tags(tags) if
location.name not in self.disabled_locations]
locations = [location for location in locations if location.item is None]
self.limited_chapter_locations[i][chapter_keysanity_tags[i]].update(locations)
if self.options.tattlesanity:
self.limit_tattle_locations()
def limit_tattle_locations(self) -> None:
# Existing pit-exclusive logic unchanged
for stars_required, locations in pit_exclusive_tattle_stars_required.items():
if stars_required > len(self.required_chapters):
self.limited_misc_locations.update(
self.get_location(loc)
for loc in locations
if loc not in self.disabled_locations
)
# Flatten limited chapter location-ids
all_limited_locations: set[int] = {
loc.address
for chapter_locs in self.limited_chapter_locations.values()
for locs in chapter_locs.values()
for loc in locs
}
base_rules = get_tattle_rules_dict()
# Use randomized rules if enabled; otherwise base.
# IMPORTANT: your get_random_enemy_tattle_rules_dict() should already do the
# "if no matches -> use base rule" fallback.
rules_dict = (
get_random_enemy_tattle_rules_dict(self)
if self.options.enemy_randomizer != EnemyRandomizer.option_vanilla
else base_rules
)
for location_name, locations in rules_dict.items():
if location_name in self.disabled_locations:
continue
# Chapter 8 clamp: if a tattle location has no gating locations, force-limit it.
if self.options.limit_chapter_eight and not locations:
self.limited_misc_locations.add(self.get_location(location_name))
continue
enabled_locations = [
loc_id
for loc_id in locations
if location_id_to_name[loc_id] not in self.disabled_locations
]
if not enabled_locations:
continue
if self.options.pit_items != PitItems.option_all:
if all(loc_id in limit_pit for loc_id in enabled_locations):
self.limited_misc_locations.add(self.get_location(location_name))
if self.options.limit_chapter_logic:
# Keep your special-case, but apply it to the *effective* rules being used.
if len(enabled_locations) == 1 and enabled_locations[0] == 78780511:
if 5 in self.limited_chapters:
self.limited_misc_locations.add(self.get_location(location_name))
if all(loc_id in all_limited_locations for loc_id in enabled_locations):
self.limited_misc_locations.add(self.get_location(location_name))
def create_items(self) -> None:
required_items = []
useful_items = []
filler_items = []
self.limited_state = CollectionState(self.multiworld)
precollected_item_names = [item.name for item in self.multiworld.precollected_items[self.player]]
item_names = [item.item_name for item in itemList for _ in
range(max(item.frequency - self.locked_item_frequencies.get(item.item_name, 0), 0))]
for item_name in item_names:
item = self.create_item(item_name)
if item_name in precollected_item_names:
precollected_item_names.remove(item_name)
continue
self.limited_state.collect(item, prevent_sweep=True)
if ItemClassification.progression in item.classification:
required_items.append(item)
elif ItemClassification.useful in item.classification:
useful_items.append(item)
else:
filler_items.append(item)
if not self.options.keysanity:
for chapter in range(1, 9):
if chapter == 8 and self.options.limit_chapter_eight:
continue
keys = [item for item in required_items if item.name in chapter_keys[chapter]]
required_items = [item for item in required_items if item.name not in chapter_keys[chapter]]
self.limited_items[chapter][chapter_keysanity_tags[chapter]].extend(keys)
for chapter in self.limited_chapters:
for tag in limited_tags[chapter]:
items = []
progressive_item_names = [item_name for item_name in limited_tag_items[tag]]
items += [item for item in required_items if item.name in progressive_item_names]
required_items = [item for item in required_items if item.name not in progressive_item_names]
location_len = len(self.limited_chapter_locations[chapter][tag])
item_len = len(self.limited_items[chapter][tag])
self.limited_items[chapter][tag] += items + [self.create_item(self.get_filler_item_name()) for _ in
range(location_len - item_len - len(items))]
self.limited_misc_items = [self.create_item(self.get_filler_item_name()) for _ in
range(len(self.limited_misc_locations))]
unfilled = len(self.multiworld.get_unfilled_locations(self.player))
unfilled -= len(self.limited_misc_items)
unfilled -= sum(
len(self.limited_items[chapter][tag]) for chapter in range(1, 9) for tag in limited_tags[chapter])
self.random.shuffle(filler_items)
self.random.shuffle(useful_items)
self.random.shuffle(required_items)
for item in required_items:
self.multiworld.itempool.append(item)
unfilled -= 1
useful_count = min(int(unfilled * 0.7), len(useful_items))
self.multiworld.itempool.extend(useful_items[:useful_count])
unfilled -= useful_count
for _ in range(unfilled):
if len(filler_items) > 0:
self.multiworld.itempool.append(filler_items.pop())
else:
self.multiworld.itempool.append(self.create_item(self.get_filler_item_name()))
def pre_fill(self) -> None:
_ = {self.limited_state.collect(location.item, prevent_sweep=True) for location in
self.multiworld.get_filled_locations(self.player)
if location.item is not None and location.item.name not in stars.values() and location.item.name != "Victory"}
for chapter, locations in self.limited_chapter_locations.items().__reversed__():
self.in_pre_fill = chapter != 8
for tag, locs in locations.items():
state = self.limited_state.copy()
if chapter == 8:
state.prog_items[self.player]["stars"] = len(self.required_chapters)
state.prog_items[self.player]["required_stars"] = len(self.required_chapters)
_ = {state.remove(item) for item in self.limited_items[chapter][tag]}
_ = {state.remove(item) for chapters, locations in self.limited_chapter_locations.items() for tag in
locations.keys() if chapters != chapter for item in self.limited_items[chapters][tag]}
if len(self.limited_items[chapter][tag]) == 0:
continue
fill_restrictive(
self.multiworld,
state,
list(locs),
self.limited_items[chapter][tag],
single_player_placement=True,
lock=True
)
self.in_pre_fill = False
fast_fill(self.multiworld, self.limited_misc_items, list(self.limited_misc_locations))
def set_rules(self) -> None:
set_rules(self)
set_tattle_rules(self)
if self.options.goal == Goal.option_shadow_queen:
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
elif self.options.goal == Goal.option_crystal_stars:
self.multiworld.completion_condition[self.player] = lambda state: state.has("required_stars", self.player,
self.options.goal_stars.value)
else:
self.multiworld.completion_condition[self.player] = lambda state: state.can_reach(
"Pit of 100 Trials Floor 100: Return Postage", "Location", self.player)
def fill_slot_data(self) -> Dict[str, Any]:
return {
"goal": self.options.goal.value,
"goal_stars": self.options.goal_stars.value,
"palace_stars": self.options.palace_stars.value,
"pit_items": self.options.pit_items.value,
"limit_chapter_logic": self.options.limit_chapter_logic.value,
"limit_chapter_eight": self.options.limit_chapter_eight.value,
"palace_skip": self.options.palace_skip.value,
"yoshi_color": self.options.yoshi_color.value,
"westside": self.options.open_westside.value,
"tattlesanity": self.options.tattlesanity.value,
"dazzle_rewards": self.options.dazzle_rewards.value,
"star_shuffle": self.options.star_shuffle.value,
"disable_intermissions": self.options.disable_intermissions.value,
"cutscene_skip": self.options.cutscene_skip.value,
"death_link": self.options.death_link.value,
"piecesanity": self.options.piecesanity.value,
"shinesanity": self.options.shinesanity.value,
"blue_pipe_toggle": self.options.blue_pipe_toggle.value,
"enemy_randomizer": self.options.enemy_randomizer.value,
"tattle_rules": get_random_enemy_tattle_rules_dict(self)
if self.options.enemy_randomizer != EnemyRandomizer.option_vanilla
else get_tattle_rules_dict(),
}
def create_item(self, name: str) -> TTYDItem:
item = item_table.get(name, ItemData(None, name, "progression"))
progression = (ItemClassification.useful if (item.item_name == "Goombella" and not self.options.tattlesanity) else item.progression)
return TTYDItem(item.item_name, progression, item.id, self.player)
def lock_item(self, location: str, item_name: str):
item = self.create_item(item_name)
item.location = self.get_location(location)
if location not in self.disabled_locations:
self.get_location(location).place_locked_item(item)
def lock_vanilla_items(self, locations: LocationData | List[LocationData]) -> None:
if isinstance(locations, LocationData):
locations = [locations]
for location in locations:
if location.name not in self.disabled_locations:
item = self.create_item(items_by_id[location.vanilla_item].item_name)
item.location = self.get_location(location.name)
self.get_location(location.name).place_locked_item(item)
def lock_vanilla_items_remove_from_pool(self, locations: LocationData | List[LocationData]) -> None:
if isinstance(locations, LocationData):
locations = [locations]
for location in locations:
self.locked_item_frequencies[
items_by_id[location.vanilla_item].item_name] = self.locked_item_frequencies.get(
items_by_id[location.vanilla_item].item_name, 0) + 1
if location.name not in self.disabled_locations:
item = self.create_item(items_by_id[location.vanilla_item].item_name)
item.location = self.get_location(location.name)
self.get_location(location.name).place_locked_item(item)
def lock_filler_items_remove_from_pool(self, locations: LocationData | List[LocationData]) -> None:
if isinstance(locations, LocationData):
locations = [locations]
for location in locations:
filler_item_name = self.get_filler_item_name()
self.locked_item_frequencies[filler_item_name] = self.locked_item_frequencies.get(filler_item_name, 0) + 1
if location.name not in self.disabled_locations:
item = self.create_item(filler_item_name)
item.location = self.get_location(location.name)
self.get_location(location.name).place_locked_item(item)
def lock_item_remove_from_pool(self, location: str, item_name: str):
self.locked_item_frequencies[item_name] = self.locked_item_frequencies.get(item_name, 0) + 1
item = self.create_item(item_name)
item.location = self.get_location(location)
if location not in self.disabled_locations:
self.get_location(location).place_locked_item(item)
def get_filler_item_name(self) -> str:
return self.random.choice(
list(filter(lambda item: item.progression == ItemClassification.filler, itemList))).item_name
def collect(self, state: "CollectionState", item: "Item") -> bool:
change = super().collect(state, item)
# Skip counting stars during pre_fill to prevent sweep from making the game
# appear beatable (which causes fill_restrictive to skip placement logic)
if change and not self.in_pre_fill:
if item.name in stars.values():
state.prog_items[item.player]["stars"] += 1
for star in self.required_chapters:
if item.location is not None:
if item.name == stars[star]:
state.prog_items[item.player]["required_stars"] += 1
return change
def remove(self, state: "CollectionState", item: "Item") -> bool:
change = super().remove(state, item)
if change:
if item.name in stars.values():
state.prog_items[item.player]["stars"] -= 1
for star in self.required_chapters:
if item.location is not None:
if item.name == stars[star]:
state.prog_items[item.player]["required_stars"] -= 1
return change
def generate_output(self, output_directory: str) -> None:
patch = TTYDProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player])
write_files(self, patch)
rom_path = os.path.join(
output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}" f"{patch.patch_file_ending}"
)
patch.write(rom_path)