Files
dockipelago/worlds/papermario/__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

891 lines
47 KiB
Python

import settings
import typing
import os
import logging
from typing import Dict, Any, TextIO
from BaseClasses import (Tutorial, CollectionState, MultiWorld, ItemClassification as ic, LocationProgressType)
from .modules.random_battles import get_boss_battles
from .SettingsString import load_settings_from_site_string
from worlds.AutoWorld import World, WebWorld
from . import Locations, options
from .data.chapter_logic import areas_by_chapter, get_chapter_excluded_location_names
from .modules.modify_entrances import get_bowser_rush_pairs, get_bowser_shortened_pairs
from .modules.random_audio import get_randomized_audio
from .modules.random_map_mirroring import get_mirrored_map_list
from .modules.random_movecosts import get_randomized_moves
from .modules.random_palettes import get_randomized_palettes
from .Regions import PMRegion
from .RuleParser import Rule_AST_Transformer
from .Entrance import PMEntrance
from .Utils import load_json_data
from .Locations import PMLocation, location_factory, location_name_to_id
from .ItemPool import generate_itempool
from .items import PMItem, pm_is_item_of_type, pm_data_to_ap_id, ap_id_to_pm_data, item_id_prefix
from .data.ItemList import item_table, item_groups, progression_miscitems, item_multiples_ids
from .data.itemlocation_special import limited_by_item_areas
from .data.itemlocation_replenish import replenishing_itemlocations
from .data.LocationsList import location_table, location_groups, ch8_locations
from .modules.random_actor_stats import get_shuffled_chapter_difficulty
from .Rules import set_rules
from .modules.random_partners import get_rnd_starting_partners
from .options import (SeedGoal, PaperMarioOptions, ShuffleKootFavors, PartnerUpgradeShuffle, HiddenBlockMode,
ShuffleSuperMultiBlocks, GearShuffleMode, StartingMap, BowserCastleMode, ShuffleLetters,
ItemTraps, MirrorMode, ShufflePartners, ShuffleKeys, ShuffleDungeonEntrances, BossShuffle,
SpiritRequirements, ConsumableItemPool, StartingBoots)
from .data.node import Node
from .data.starting_maps import starting_maps
from .Rom import generate_output, PaperMarioDeltaPatch
from Fill import fill_restrictive, remaining_fill
import pkg_resources
from .client import PaperMarioClient # unused but required for generic client to hook onto
logger = logging.getLogger("Paper Mario")
class PaperMarioSettings(settings.Group):
class RomFile(settings.UserFilePath):
"""File name of the Paper Mario USA ROM"""
description = "Paper Mario ROM File"
copy_to = "Paper Mario (USA).z64"
md5s = [PaperMarioDeltaPatch.hash]
class RomStart(str):
"""
Set this to false to never autostart a rom (such as after patching),
true for operating system default program
Alternatively, a path to a program to open the .z64 file with
"""
rom_file: RomFile = RomFile(RomFile.copy_to)
rom_start: typing.Union[RomStart, bool] = True
# information for the supported games setup guide; set up to make it easier to add more guides
class PaperMarioWeb(WebWorld):
setup = Tutorial(
"Multiworld Setup Guide",
"A Guide to setting up the Paper Mario randomizer connected to an Archipelago Multiworld",
"English",
"setup_en.md",
"setup/en",
["JKB"]
)
# set tutorials to the list of each setup
tutorials = [setup]
class PaperMarioWorld(World):
"""
Paper Mario is a turn-based adventure RPG. Bowser has kidnapped Princess Peach along with her castle using the
power of the Star Rod, which grants the wishes of the holder. You must rescue the Star Spirits so that they can
help you take back the Star Rod from Bowser and save Peach. You will have to defeat powerful foes
and venture through dangerous lands with the help of partners you meet along the way.
"""
game = "Paper Mario"
web = PaperMarioWeb()
topology_present = True
options_dataclass = PaperMarioOptions
options: PaperMarioOptions
settings_key = "paper_mario_settings"
settings: typing.ClassVar[PaperMarioSettings]
item_name_to_id = {item_name: pm_data_to_ap_id(data, False) for item_name, data in item_table.items()}
location_name_to_id = location_name_to_id
item_name_groups = item_groups
location_name_groups = location_groups
data_version = 1
required_client_version = (0, 4, 4)
auth: bytes
def __init__(self, multiworld, player):
super(PaperMarioWorld, self).__init__(multiworld, player)
# For generation
self.placed_items = []
self.entrance_list = []
self.required_spirits = []
self.excluded_spirits = []
self.excluded_areas = []
self.ch_excluded_locations = []
self.ch_excluded_location_names = []
self.itempool = []
self.pre_fill_items = []
self.dungeon_restricted_items = {}
self.dro_shop_puzzle_items = []
self.remove_from_start_inventory = []
self.web_start_inventory = []
self.trappable_item_names = []
self._regions_cache = {}
self.parser = Rule_AST_Transformer(self, self.player)
self.regions = []
self.battle_list = []
self.spoilerlog_puzzles = {}
@classmethod
def stage_assert_generate(cls, multiworld: MultiWorld) -> None:
if not os.path.exists(cls.settings.rom_file):
raise FileNotFoundError(cls.settings.rom_file)
# Do some housekeeping before generating, namely fixing some options that might be incompatible with each other
def generate_early(self) -> None:
# load settings from pmr string before anything else, since almost all settings can be loaded this way
if self.options.pmr_settings_string.value != "None":
load_settings_from_site_string(self)
# fail generation if attempting to use options that are not fully implemented yet
nyi_warnings = ""
if self.options.shuffle_dungeon_entrances.value != ShuffleDungeonEntrances.option_Off: # NYI
nyi_warnings += "\n'shuffle_dungeon_entrances' must be set to Off"
if self.options.boss_shuffle.value: # NYI
nyi_warnings += "\n'boss_shuffle' must be set to False"
if self.options.mirror_mode.value == MirrorMode.option_Static_Random: # NYI
nyi_warnings += "\n'mirror_mode' cannot be set to Static_Random"
if nyi_warnings:
nyi_warnings = ((f"Paper Mario: {self.player} ({self.multiworld.player_name[self.player]}) has settings "
"are not yet implemented in the .apworld being used for generation. "
"Please check for a newer release and/or adjust the settings below : ") + nyi_warnings)
raise ValueError(nyi_warnings)
# LCL is not compatible with several options
# Rather than generate with drastically different settings, compile list of incompatible settings
if self.options.spirit_requirements.value == SpiritRequirements.option_Specific_And_Limit_Chapter_Logic:
lcl_warnings = ""
if self.options.koot_favors.value != ShuffleKootFavors.option_Vanilla:
lcl_warnings += "\n'koot_favors' must be set to vanilla"
if self.options.letter_rewards.value == ShuffleLetters.option_Final_Letter_Chain_Reward:
lcl_warnings += "\n'letter_rewards' cannot be set to Final_Letter_Chain_Reward"
if self.options.gear_shuffle_mode.value != GearShuffleMode.option_Full_Shuffle:
lcl_warnings += "\n'gear_shuffle_mode' must be set to full_shuffle"
if not self.options.keysanity.value:
lcl_warnings += "\n'keysanity' must be set to True"
if self.options.partners.value != ShufflePartners.option_Full_Shuffle:
lcl_warnings += "\n'partners' must be set to full_shuffle"
if lcl_warnings:
lcl_warnings = (f"Paper Mario: {self.player} ({self.multiworld.player_name[self.player]}) has limit "
"chapter logic set to true, but the following settings are incompatible with limiting "
"chapter logic: ") + lcl_warnings
raise ValueError(lcl_warnings)
# Make sure it doesn't try to shuffle Koot coins if rewards aren't shuffled
if self.options.koot_favors.value == ShuffleKootFavors.option_Vanilla:
self.options.koot_coins.value = False
# turn off individual partner toggles if starting with random partners was selected
if self.options.start_random_partners.value:
self.options.start_with_goombario.value = False
self.options.start_with_kooper.value = False
self.options.start_with_bombette.value = False
self.options.start_with_parakarry.value = False
self.options.start_with_bow.value = False
self.options.start_with_watt.value = False
self.options.start_with_sushie.value = False
self.options.start_with_lakilester.value = False
# turn on random partner if no partners were selected; debatable whether to use min/max here or set both to 1.
elif not (self.options.start_with_goombario.value or self.options.start_with_kooper.value
or self.options.start_with_bombette.value or self.options.start_with_parakarry.value
or self.options.start_with_bow.value or self.options.start_with_watt.value
or self.options.start_with_sushie.value or self.options.start_with_lakilester.value):
logging.warning(f"Paper Mario: {self.player} ({self.multiworld.player_name[self.player]}) did not select a "
f"starting partner and will be given one at random.")
self.options.start_random_partners.value = True
if self.options.start_random_partners.value:
starting_partners = get_rnd_starting_partners(self.options.start_partners.value, self.random)
for partner in starting_partners:
if partner == "Goombario":
self.options.start_with_goombario.value = True
elif partner == "Kooper":
self.options.start_with_kooper.value = True
elif partner == "Bombette":
self.options.start_with_bombette.value = True
elif partner == "Parakarry":
self.options.start_with_parakarry.value = True
elif partner == "Bow":
self.options.start_with_bow.value = True
elif partner == "Watt":
self.options.start_with_watt.value = True
elif partner == "Sushie":
self.options.start_with_sushie.value = True
elif partner == "Lakilester":
self.options.start_with_lakilester.value = True
# limit chapter logic only applies when using the specific star spirits setting
if self.options.spirit_requirements.value == SpiritRequirements.option_Any:
self.required_spirits = []
self.excluded_spirits = []
else:
# determine which star spirits are needed
remaining_spirits = [i for i in range(1, 8)]
chosen_spirits = []
for _ in range(self.options.star_way_spirits.value):
rnd_spirit = self.random.randint(0, len(remaining_spirits) - 1)
chosen_spirits.append(remaining_spirits.pop(rnd_spirit))
self.required_spirits = chosen_spirits
if self.options.spirit_requirements.value == SpiritRequirements.option_Specific_And_Limit_Chapter_Logic:
self.excluded_spirits = remaining_spirits
for chapter in remaining_spirits:
self.excluded_areas.extend(areas_by_chapter[chapter])
self.ch_excluded_location_names = get_chapter_excluded_location_names(self.excluded_spirits,
self.options.letter_rewards.value)
if self.options.seed_goal.value == SeedGoal.option_Open_Star_Way:
self.ch_excluded_location_names.extend(ch8_locations)
# set power star counts to 0 if option is not being used
if not self.options.power_star_hunt.value:
self.options.star_way_power_stars.value = 0
self.options.star_beam_power_stars.value = 0
self.options.total_power_stars.value = 0
else:
# ensure there are at least as many power stars as there are required for star way and star beam
# if total power stars is less than either, then put it 15% higher than the bigger requirement, max 120
required_power_stars = self.options.star_way_power_stars.value
if self.options.seed_goal.value == SeedGoal.option_Open_Star_Way:
required_power_stars = max(required_power_stars, self.options.star_beam_power_stars.value)
if self.options.total_power_stars.value < required_power_stars:
self.options.total_power_stars.value = min(120, int(1.15 * required_power_stars))
logger.info(f"Paper Mario: {self.player} ({self.multiworld.player_name[self.player]}) had less total "
f"power stars than the required amount. New total set to 15% more than the larger "
f"requirement, restricted to 120 or fewer.")
# shuffle bosses - NYI
self.battles, self.boss_chapter_map = get_boss_battles(self.options.boss_shuffle.value, self.random)
def create_regions(self) -> None:
# Create base regions
menu = PMRegion("Menu", self.player, self.multiworld)
start = PMEntrance(self.player, self.multiworld, 'New Game', menu)
menu.exits.append(start)
self.multiworld.regions.append(menu)
# Load region json files
for file in pkg_resources.resource_listdir(__name__, "data/regions"):
if not pkg_resources.resource_isdir(__name__, "data/regions/" + file):
readfile = True
match file:
case "bowser's_castle.json":
readfile = (self.options.bowser_castle_mode.value == BowserCastleMode.option_Vanilla and
self.options.seed_goal.value != SeedGoal.option_Open_Star_Way)
case "bowser's_castle_shortened.json":
readfile = (self.options.bowser_castle_mode.value == BowserCastleMode.option_Shortened and
self.options.seed_goal.value != SeedGoal.option_Open_Star_Way)
case "bowser's_castle_boss_rush.json":
readfile = (self.options.bowser_castle_mode.value == BowserCastleMode.option_Boss_Rush and
self.options.seed_goal.value != SeedGoal.option_Open_Star_Way)
case "shooting_star_summit_no_star_way.json":
readfile = self.options.seed_goal.value == SeedGoal.option_Open_Star_Way
case "shooting_star_summit.json":
readfile = self.options.seed_goal.value != SeedGoal.option_Open_Star_Way
case "peachs_castle.json":
readfile = self.options.seed_goal.value != SeedGoal.option_Open_Star_Way
if readfile:
self.load_regions_from_json("regions/" + file)
# Connect start to chosen starting map
start.connect(self.get_region(starting_maps[self.options.starting_map.value][1]))
self.parser.create_delayed_rules()
# Connect exits
for region in self.regions:
for exit in region.exits:
exit.connect(self.get_region(exit.vanilla_connected_region))
# handle any changed entrances
if self.options.bowser_castle_mode.value == BowserCastleMode.option_Boss_Rush:
self.entrance_list = get_bowser_rush_pairs()
elif self.options.bowser_castle_mode.value == BowserCastleMode.option_Shortened:
self.entrance_list = get_bowser_shortened_pairs()
def create_items(self) -> None:
# This checks what locations are being included, gets those items, places non-shuffled items,
# adds any desired beta items and badges, ensures we have the correct number of items by removing coins or
# adding Tayce T items, and randomizes the consumables pool according to the player's settings.
generate_itempool(self)
# Starting inventory
# Gear
for boots in range(1, self.options.starting_boots.value + 2):
self.multiworld.push_precollected(self.create_item("Progressive Boots"))
self.remove_from_start_inventory.append("Progressive Boots")
for hammer in range(1, self.options.starting_hammer.value + 2):
self.multiworld.push_precollected(self.create_item("Progressive Hammer"))
self.remove_from_start_inventory.append("Progressive Hammer")
# Partners
if self.options.start_with_goombario.value:
self.multiworld.push_precollected(self.create_item("Goombario"))
self.remove_from_start_inventory.append("Goombario")
if self.options.start_with_kooper.value:
self.multiworld.push_precollected(self.create_item("Kooper"))
self.remove_from_start_inventory.append("Kooper")
if self.options.start_with_bombette.value:
self.multiworld.push_precollected(self.create_item("Bombette"))
self.remove_from_start_inventory.append("Bombette")
if self.options.start_with_parakarry.value:
self.multiworld.push_precollected(self.create_item("Parakarry"))
self.remove_from_start_inventory.append("Parakarry")
if self.options.start_with_bow.value:
self.multiworld.push_precollected(self.create_item("Bow"))
self.remove_from_start_inventory.append("Bow")
if self.options.start_with_watt.value:
self.multiworld.push_precollected(self.create_item("Watt"))
self.remove_from_start_inventory.append("Watt")
if self.options.start_with_sushie.value:
self.multiworld.push_precollected(self.create_item("Sushie"))
self.remove_from_start_inventory.append("Sushie")
if self.options.start_with_lakilester.value:
self.multiworld.push_precollected(self.create_item("Lakilester"))
self.remove_from_start_inventory.append("Lakilester")
starting_items = []
# Items from setting string and/or random starting items
if self.web_start_inventory:
for item_name in self.web_start_inventory:
item_to_add = self.create_item(item_name)
starting_items.append(item_to_add)
# Start with up to 16 random items on top of anything else already included
if self.options.random_start_items.value:
self.random.shuffle(self.itempool)
# Mario can only hold 10 consumables, so disallow more than 10 from being sent to his inventory
popped_consumables = []
consumable_count = 0
while len(starting_items) < self.options.random_start_items.value:
item_to_add = self.itempool.pop()
if item_to_add.type == "ITEM" and consumable_count == 10:
popped_consumables.append(item_to_add)
else:
starting_items.append(item_to_add)
if item_to_add.type == "ITEM":
consumable_count += 1
# add items back to itempool regardless of if they were in starting_items or not
# removed items are handled in next block
self.itempool.extend(starting_items)
self.itempool.extend(popped_consumables)
for item in starting_items:
self.multiworld.push_precollected(item)
# handle start inventory, be it from the AP option or from
removed_items = []
for item in self.multiworld.precollected_items[self.player]:
if item.name in self.remove_from_start_inventory:
self.remove_from_start_inventory.remove(item.name)
removed_items.append(item.name)
elif item in self.itempool:
self.itempool.remove(item)
self.itempool.append(self.create_item(self.get_filler_item_name()))
# remove prefill items from item pool to be randomized
(self.itempool, self.pre_fill_items,
self.dungeon_restricted_items, self.dro_shop_puzzle_items) = self.divide_itempools()
self.multiworld.itempool.extend(self.itempool)
self.remove_from_start_inventory.extend(removed_items)
# get valid trap items from remaining pool if needed
if self.options.item_traps.value > ItemTraps.option_No_Traps:
trappable_items = list(filter(lambda item: item.type not in ["ITEM", "COIN"], self.itempool))
self.trappable_item_names = [item.name for item in trappable_items]
# Bias towards placing Ultra Stone or upgrade traps, done in base PMR as well as requested by clover
if self.options.partner_upgrades.value == PartnerUpgradeShuffle.option_Vanilla:
self.trappable_item_names.extend(["Ultra Stone"] * 9)
else:
self.trappable_item_names.extend(item_groups["PartnerUpgrade"])
def set_rules(self) -> None:
set_rules(self)
def generate_basic(self):
self.auth = bytearray(self.multiworld.random.getrandbits(8) for _ in range(16))
# remove internal event locations that are not going to exist in this seed
all_state = self.get_state_with_complete_itempool()
all_locations = self.get_locations()
all_state.sweep_for_advancements(locations=all_locations)
reachable = self.multiworld.get_reachable_locations(all_state, self.player)
unreachable = [loc for loc in all_locations if
loc.internal and loc.event and loc.locked and loc not in reachable]
for loc in unreachable:
loc.parent_region.locations.remove(loc)
if self.options.open_forest.value:
loc = self.multiworld.get_location("TT Southern District Fice T. Forest Pass", self.player)
loc.parent_region.locations.remove(loc)
def load_regions_from_json(self, file_path):
region_json = load_json_data(file_path)
region: Dict[str, Any]
for region in region_json:
region_prefix = region['region_name'][:3]
new_region = PMRegion(region['region_name'], self.player, self.multiworld)
if 'map_id' in region:
new_region.map_id = region['map_id']
if 'area_id' in region:
new_region.font_color = region['area_id']
if 'map_name' in region:
new_region.scene = region['map_name']
if 'locations' in region and region_prefix not in self.excluded_areas:
for location, rule in region['locations'].items():
if location not in self.ch_excluded_location_names:
new_location = location_factory(location, self.player)
new_location.parent_region = new_region
new_location.rule_string = rule
self.parser.parse_spot_rule(new_location)
if new_location.never:
# We still need to fill the location even if ALR is off.
logger.debug('Unreachable location: %s', new_location.name)
new_location.player = self.player
new_region.locations.append(new_location)
if 'events' in region and region_prefix not in self.excluded_areas:
for event, rule in region['events'].items():
# Allow duplicate placement of events
lname = '%s from %s' % (event, new_region.name)
new_location = PMLocation(self.player, lname, event=True, parent=new_region)
new_location.rule_string = rule
self.parser.parse_spot_rule(new_location)
if new_location.never:
logger.debug('Dropping unreachable event: %s', new_location.name)
else:
new_location.player = self.player
new_region.locations.append(new_location)
self.make_event_item(event, new_location)
new_location.show_in_spoiler = False
if 'exits' in region:
for exit, rule in region['exits'].items():
new_exit = PMEntrance(self.player, self.multiworld, f"{new_region.name} -> {exit}", new_region)
new_exit.vanilla_connected_region = exit
new_exit.rule_string = rule
self.parser.parse_spot_rule(new_exit)
if new_exit.never:
logger.debug('Dropping unreachable exit: %s', new_exit.name)
else:
new_region.exits.append(new_exit)
self.multiworld.regions.append(new_region)
self.regions.append(new_region)
self._regions_cache[new_region.name] = new_region
# Note on allow_arbitrary_name:
# PM defines many helper items and event names that are treated indistinguishably from regular items,
# but are only defined in the logic files. This means we need to create items for any name.
# Allowing any item name to be created is dangerous in case of plando, so this is a middle ground.
def create_item(self, name: str, allow_arbitrary_name: bool = False):
if name in item_table:
return PMItem(name, self.player, item_table[name], False)
if allow_arbitrary_name:
return PMItem(name, self.player, ('Event', ic.progression, None, None, False, False, False), True)
raise Exception(f"Invalid item name: {name}")
def make_event_item(self, name, location, item=None):
if item is None:
item = self.create_item(name, allow_arbitrary_name=True)
self.multiworld.push_item(location, item, collect=False)
location.locked = True
location.event = True
if name not in item_table:
location.internal = True
return item
def divide_itempools(self):
main_items = []
prefill_item_names = []
dungeon_restricted_items = {}
dro_shop_puzzle_item_names = []
# progression items that need to be in replenishable locations
for item in progression_miscitems:
prefill_item_names.append(item)
# key items shuffled within their own dungeons
if not self.options.keysanity.value:
for dungeon in limited_by_item_areas:
for itemlist in limited_by_item_areas[dungeon].values():
for item in itemlist:
dungeon_restricted_items[item] = dungeon
prefill_item_names.append(item)
# gear items shuffled among gear locations
if self.options.gear_shuffle_mode.value == GearShuffleMode.option_Gear_Location_Shuffle:
for item in self.itempool:
if item.name in item_groups["Gear"]:
prefill_item_names.append(item.name)
# partners if shuffled among partner locations
if self.options.partners.value == ShufflePartners.option_Partner_Locations:
for item in self.itempool:
if item.name in item_groups["Partner"]:
prefill_item_names.append(item.name)
# partner upgrades shuffled among super block locations, which may be shuffled
if self.options.partner_upgrades.value == PartnerUpgradeShuffle.option_Super_Block_Locations:
for item in self.itempool:
if item.name in item_groups["PartnerUpgrade"]:
prefill_item_names.append(item.name)
if self.options.super_multi_blocks.value == ShuffleSuperMultiBlocks.option_Shuffle:
for item in self.itempool:
if item.name == "Coin Bag":
prefill_item_names.append(item.name)
# ensure DDO shop has 3 cheap consumables for puzzle purposes if needed
if self.options.random_puzzles.value and self.options.include_shops.value and not (
self.options.spirit_requirements.value == SpiritRequirements.option_Specific_And_Limit_Chapter_Logic and
2 in self.excluded_spirits):
for item in self.itempool:
if (item_table[item.name][0] == "ITEM"
and item_table[item.name][3] <= 10 and item_table[item.name][2] <= 0xFF
and len(dro_shop_puzzle_item_names) < 3 and item.name not in dro_shop_puzzle_item_names
and item.classification == ic.filler):
dro_shop_puzzle_item_names.append(item.name)
# sometimes the item pool is restricted, such as when the item pool is set to be mystery only
# account for that by putting in consumables that need to be in replenishable locations anyways
if len(dro_shop_puzzle_item_names) < 3:
while len(dro_shop_puzzle_item_names) < 3:
consumable = self.random.choice(progression_miscitems)
if consumable not in dro_shop_puzzle_item_names:
dro_shop_puzzle_item_names.append(consumable)
prefill_item_names.remove(consumable)
prefill_items = []
dro_shop_puzzle_items = []
local_consumable_chance = self.options.local_consumables.value
for item in self.itempool:
if item.name in prefill_item_names:
prefill_items.append(item)
prefill_item_names.remove(item.name)
elif item.name in dro_shop_puzzle_item_names and item not in dro_shop_puzzle_items:
dro_shop_puzzle_items.append(item)
else:
# check if this item gets kept local or not
# sets extra copies of consumable progression items to be filler so that they aren't considered in logic
keep_local = False
if item.type == "ITEM" and item.classification != ic.trap:
item.classification = ic.filler
keep_local = self.random.randint(0, 100) <= local_consumable_chance
elif item.type == "PARTNERUPGRADE":
keep_local = (self.options.partner_upgrades.value !=
PartnerUpgradeShuffle.option_Full_Shuffle)
elif item.name in prefill_item_names and item.type == "KEYITEM":
keep_local = (self.options.keysanity.value == ShuffleKeys.option_false)
elif item.name in prefill_item_names and item.type == "GEAR":
keep_local = (self.options.gear_shuffle_mode.value <= GearShuffleMode.option_Full_Shuffle)
elif item.name in prefill_item_names and item.type == "PARTNER":
keep_local = (self.options.partners.value <= ShufflePartners.option_Full_Shuffle)
if keep_local:
prefill_items.append(item)
else:
main_items.append(item)
return main_items, prefill_items, dungeon_restricted_items, dro_shop_puzzle_items
# handle player-specific stuff like cosmetics, audio, enemy stats, etc.
# only returns proper result after create_items and divide_itempools are run
def get_pre_fill_items(self):
return self.pre_fill_items
def pre_fill(self):
def prefill_state(base_state):
state = base_state.copy()
for item in self.get_pre_fill_items():
self.collect(state, item)
state.sweep_for_advancements(locations=self.get_locations())
return state
# Prefill required replenishable items, local key items depending on settings
locations = list(self.multiworld.get_unfilled_locations(self.player))
self.multiworld.random.shuffle(locations)
# Set up initial state
state = CollectionState(self.multiworld)
for item in self.itempool:
self.collect(state, item)
state.sweep_for_advancements(locations=self.get_locations())
if self.options.random_puzzles.value and self.options.include_shops.value and not (
self.options.spirit_requirements.value == SpiritRequirements.option_Specific_And_Limit_Chapter_Logic and
2 in self.excluded_spirits):
dro_shop_locations = [self.multiworld.get_location(location, self.player)
for location in self.random.sample([location for location in location_table.keys()
if "DDO Outpost 1 Shop Item" in location], 3)]
remaining_fill(self.multiworld, dro_shop_locations, self.dro_shop_puzzle_items)
# place progression items that are also consumables in locations that are replenishable
replenish_locations = [name for name, data in location_table.items() if data[0] in replenishing_itemlocations]
replenish_items = list(filter(lambda item: item.name in progression_miscitems and
item.classification == ic.progression, self.pre_fill_items))
for item in replenish_items:
self.pre_fill_items.remove(item)
locations = list(filter(lambda location: location.name in replenish_locations
and location.progress_type != LocationProgressType.PRIORITY,
self.multiworld.get_unfilled_locations(player=self.player)))
self.multiworld.random.shuffle(locations)
fill_restrictive(self.multiworld, prefill_state(state), locations, replenish_items,
single_player_placement=True, lock=True, allow_excluded=False)
# Place gear items in gear locations
if self.options.gear_shuffle_mode.value == GearShuffleMode.option_Gear_Location_Shuffle:
gear_items = list(filter(lambda item: pm_is_item_of_type(item, "GEAR"), self.pre_fill_items))
# if starting jumpless, the first set of boots has to go elsewhere as there isn't a gear location for it
# not to mention, no gear locations are reachable jumpless
if self.options.starting_boots.value == StartingBoots.option_Jumpless:
for item in gear_items:
if item.name == "Progressive Boots":
gear_items.remove(item)
break
gear_locations = location_groups["Gear"]
locations = list(filter(lambda location: location.name in gear_locations,
self.multiworld.get_unfilled_locations(player=self.player)))
if isinstance(locations, list):
for item in gear_items:
self.pre_fill_items.remove(item)
self.multiworld.random.shuffle(locations)
fill_restrictive(self.multiworld, prefill_state(state), locations, gear_items,
single_player_placement=True, lock=True, allow_excluded=False)
# Place partner upgrade items in potential super block locations
if self.options.partner_upgrades.value == PartnerUpgradeShuffle.option_Super_Block_Locations:
upgrade_items = list(filter(lambda item: pm_is_item_of_type(item, "PARTNERUPGRADE"), self.pre_fill_items))
super_block_locations = location_groups["SuperBlock"]
# multi coin block locations are also candidates if they're shuffled
if self.options.super_multi_blocks.value >= ShuffleSuperMultiBlocks.option_Shuffle:
super_block_locations.extend(location_groups["MultiCoinBlock"])
locations = list(filter(lambda location: location.name in super_block_locations,
self.multiworld.get_unfilled_locations(player=self.player)))
if isinstance(locations, list):
for item in upgrade_items:
self.pre_fill_items.remove(item)
self.multiworld.random.shuffle(locations)
fill_restrictive(self.multiworld, prefill_state(state), locations, upgrade_items,
single_player_placement=True, lock=True, allow_excluded=False)
# Place coin bags in super or multi coin block locations
if self.options.super_multi_blocks.value == ShuffleSuperMultiBlocks.option_Shuffle:
coin_bag_items = list(filter(lambda item: item.name == "Coin Bag", self.pre_fill_items))
multicoin_locations = location_groups["RandomBlock"]
locations = list(filter(lambda location: location.name in multicoin_locations,
self.multiworld.get_unfilled_locations(player=self.player)))
if isinstance(locations, list):
for item in coin_bag_items:
self.pre_fill_items.remove(item)
fill_restrictive(self.multiworld, prefill_state(state), locations, coin_bag_items,
single_player_placement=True, lock=True, allow_excluded=False)
if self.options.partners.value == ShufflePartners.option_Partner_Locations:
partner_items = list(filter(lambda item: pm_is_item_of_type(item, "PARTNER"), self.pre_fill_items))
partner_locations = location_groups["Partner"]
locations = list(filter(lambda location: location.name in partner_locations,
self.multiworld.get_unfilled_locations(player=self.player)))
if isinstance(locations, list):
for item in partner_items:
self.pre_fill_items.remove(item)
self.multiworld.random.shuffle(locations)
fill_restrictive(self.multiworld, prefill_state(state), locations, partner_items,
single_player_placement=True, lock=True, allow_excluded=False)
# Place dungeon key items in their own dungeon
if not self.options.keysanity.value:
for dungeon in limited_by_item_areas:
# get key items for this dungeon
key_names = list(filter(lambda item: self.dungeon_restricted_items[item] == dungeon,
self.dungeon_restricted_items.keys()))
key_items = list(filter(lambda item: item.name in key_names,
self.pre_fill_items))
# get locations for this dungeon
dungeon_locations = [name for name, data in location_table.items() if data[0][:3] == dungeon]
# remove edge case location since it isn't actually in the dungeon
if "KBF Fortress Exterior Chest On Ledge" in dungeon_locations:
dungeon_locations.remove("KBF Fortress Exterior Chest On Ledge")
locations = list(filter(lambda location: location.name in dungeon_locations,
self.multiworld.get_unfilled_locations(player=self.player)))
if isinstance(locations, list):
for item in key_items:
self.pre_fill_items.remove(item)
self.multiworld.random.shuffle(locations)
fill_restrictive(self.multiworld, prefill_state(state), locations, key_items,
single_player_placement=True, lock=True, allow_excluded=True)
# Anything remaining in pre fill items is a consumable that got selected randomly to be kept local
# LCL can really skew the item pool, so fill up the excluded locations to prevent generation errors
if self.options.spirit_requirements.value == SpiritRequirements.option_Specific_And_Limit_Chapter_Logic:
locations = list(filter(lambda location: location.progress_type == LocationProgressType.EXCLUDED,
self.multiworld.get_unfilled_locations(player=self.player)))
if len(locations) <= len(self.pre_fill_items):
self.random.shuffle(self.pre_fill_items)
items_for_excluded = []
for _ in locations:
items_for_excluded.append(self.pre_fill_items.pop())
fill_restrictive(self.multiworld, prefill_state(state), locations, items_for_excluded,
single_player_placement=True, lock=True, allow_excluded=True)
for loc in locations:
if loc.item:
loc.locked = True
# Now throw the rest wherever
locations = list(filter(lambda location: location.progress_type != LocationProgressType.PRIORITY,
self.multiworld.get_unfilled_locations(player=self.player)))
self.multiworld.random.shuffle(locations)
fill_restrictive(self.multiworld, prefill_state(state), locations, self.pre_fill_items,
single_player_placement=True, lock=True, allow_excluded=True)
# Locations with unrandomized junk should be changed to events
for loc in self.get_locations():
if loc.address is not None and not loc.show_in_spoiler:
loc.address = None
def generate_output(self, output_directory: str):
generate_output(self, output_directory)
def write_spoiler(self, spoiler_handle: TextIO) -> None:
if self.spoilerlog_puzzles:
spoiler_handle.write(f"\n\nPuzzles ({self.multiworld.player_name[self.player]}):\n")
for puzzle, solution in self.spoilerlog_puzzles.items():
spoiler_handle.write(f"\n{puzzle}: {solution}")
# handle star pieces from quizmo, triple star piece items
def collect(self, state: CollectionState, item: PMItem) -> bool:
if item.name == "3x Star Pieces":
state.prog_items[self.player]["Star Piece"] += 3
# Quizmo star pieces are events that can exist in multiple places, format "StarPiece_MAC_1"
elif item.name.startswith("StarPiece_") and state.prog_items[self.player][item.name] == 1:
state.prog_items[self.player]["Star Piece"] += 1
return super().collect(state, item)
def get_locations(self):
return self.multiworld.get_locations(self.player)
def get_location(self, location):
return self.multiworld.get_location(location, self.player)
def get_region(self, region_name):
try:
return self._regions_cache[region_name]
except KeyError:
ret = self.multiworld.get_region(region_name, self.player)
self._regions_cache[region_name] = ret
return ret
# Specifically ensures that only real items are gotten, not any events.
def get_state_with_complete_itempool(self):
all_state = CollectionState(self.multiworld)
for item in self.itempool + self.pre_fill_items:
self.multiworld.worlds[item.player].collect(all_state, item)
all_state.stale[self.player] = True
return all_state
def fill_slot_data(self) -> Dict[str, Any]:
return {
"keysanity": self.options.keysanity.value,
"shuffle_hidden_panels": self.options.shuffle_hidden_panels.value,
"gear_shuffle_mode": self.options.gear_shuffle_mode.value,
"trading_events": self.options.trading_events.value,
"koot_favors": self.options.koot_favors.value,
"koot_coins": self.options.koot_coins.value,
"overworld_coins": self.options.overworld_coins.value,
"foliage_coins": self.options.foliage_coins.value,
"coin_blocks": self.options.coin_blocks.value,
"include_shops": self.options.include_shops.value,
"dojo": self.options.dojo.value,
"partner_upgrades": self.options.partner_upgrades.value,
"letter_rewards": self.options.letter_rewards.value,
"super_multi_blocks": self.options.super_multi_blocks.value,
"rowf_items": self.options.rowf_items.value,
"merlow_items": self.options.merlow_items.value,
"cheato_items": self.options.cheato_items.value,
"partners": self.options.partners.value,
"partners_always_usable": self.options.partners_always_usable.value,
"start_with_goombario": self.options.start_with_goombario.value,
"start_with_kooper": self.options.start_with_kooper.value,
"start_with_bombette": self.options.start_with_bombette.value,
"start_with_parakarry": self.options.start_with_parakarry.value,
"start_with_bow": self.options.start_with_bow.value,
"start_with_watt": self.options.start_with_watt.value,
"start_with_sushie": self.options.start_with_sushie.value,
"start_with_lakilester": self.options.start_with_lakilester.value,
"enemy_difficulty": self.options.enemy_difficulty.value,
"spirit_requirements": self.options.spirit_requirements.value,
"starting_boots": self.options.starting_boots.value,
"starting_hammer": self.options.starting_hammer.value,
"starting_map": self.options.starting_map.value,
"open_prologue": self.options.open_prologue.value,
"open_mt_rugged": self.options.open_mt_rugged.value,
"open_forest": self.options.open_forest.value,
"open_toybox": self.options.open_toybox.value,
"magical_seeds": self.options.magical_seeds.value,
"open_whale": self.options.open_whale.value,
"open_blue_house": self.options.open_blue_house.value,
"ch7_bridge_visible": self.options.ch7_bridge_visible.value,
"bowser_castle_mode": self.options.bowser_castle_mode.value,
"shuffle_dungeon_entrances": self.options.shuffle_dungeon_entrances.value,
"seed_goal": self.options.seed_goal.value,
"shuffle_star_beam": self.options.shuffle_star_beam.value,
"star_way_spirits": self.options.star_way_spirits.value,
"star_beam_spirits": self.options.star_beam_spirits.value,
"power_star_hunt": self.options.power_star_hunt.value,
"star_way_power_stars": self.options.star_way_power_stars.value,
"star_beam_power_stars": self.options.star_beam_power_stars.value,
"total_power_stars": self.options.total_power_stars.value,
"hidden_block_mode": self.options.hidden_block_mode.value,
"cook_without_frying_pan": self.options.cook_without_frying_pan.value,
"merlow_rewards_pricing": self.options.merlow_rewards_pricing.value,
"required_spirits": self.required_spirits
}
def modify_multidata(self, multidata: dict):
import base64
# Replace connect name
multidata['connect_names'][base64.b64encode(self.auth).decode("ascii")] = multidata['connect_names'][
self.multiworld.player_name[self.player]]
def get_filler_item_name(self) -> str:
if self.options.consumable_item_pool.value == ConsumableItemPool.option_Mystery_Only:
return "Mystery"
else:
return "Super Shroom"