* SMZ3: Make GT fill behave like upstream SMZ3 multiworld GT fill This means: All items local, 50% guaranteed filler, followed by possible useful items, never progression. * Fix item links * SMZ3: Ensure in all cases, we remove the right item from the pool Previously front fill would cause erratic errors on frozen, with the cause immediately revealed by, on source, tripping the assert that was added in #5109 * SMZ3: Truly, *properly* fix GT junk fill After hours of diving deep into the upstream SMZ3 randomizer, it finally behaves identically to how it does there
704 lines
33 KiB
Python
704 lines
33 KiB
Python
import logging
|
|
import copy
|
|
import os
|
|
import random
|
|
import threading
|
|
from typing import Dict, Set, TextIO
|
|
|
|
from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassification, CollectionState, \
|
|
Tutorial
|
|
from worlds.generic.Rules import set_rule
|
|
from .TotalSMZ3.Item import ItemType
|
|
from .TotalSMZ3 import Item as TotalSMZ3Item
|
|
from .TotalSMZ3.World import World as TotalSMZ3World
|
|
from .TotalSMZ3.Regions.Zelda.GanonsTower import GanonsTower
|
|
from .TotalSMZ3.Config import Config, GameMode, Goal, KeyShuffle, MorphLocation, SMLogic, SwordLocation, Z3Logic, OpenTower, GanonVulnerable, OpenTourian
|
|
from .TotalSMZ3.Location import LocationType, locations_start_id, Location as TotalSMZ3Location
|
|
from .TotalSMZ3.Patch import Patch as TotalSMZ3Patch, getWord, getWordArray
|
|
from .TotalSMZ3.WorldState import WorldState
|
|
from .TotalSMZ3.Region import IReward, IMedallionAccess
|
|
from .TotalSMZ3.Text.Texts import openFile
|
|
from worlds.AutoWorld import World, AutoLogicRegister, WebWorld
|
|
from .Rom import SMZ3ProcedurePatch
|
|
from .Options import SMZ3Options
|
|
from Options import ItemsAccessibility
|
|
from .Client import SMZ3SNIClient
|
|
|
|
world_folder = os.path.dirname(__file__)
|
|
logger = logging.getLogger("SMZ3")
|
|
|
|
# Location IDs in the range 256+196 to 256+202 shifted +34 between 11.2 and 11.3
|
|
# this is required to keep backward compatibility
|
|
def convertLocSMZ3IDToAPID(value):
|
|
return (value - 34) if value >= 256+230 and value <= 256+236 else value
|
|
|
|
class SMZ3CollectionState(metaclass=AutoLogicRegister):
|
|
def init_mixin(self, parent: MultiWorld):
|
|
# for unit tests where MultiWorld is instantiated before worlds
|
|
if hasattr(parent, "state"):
|
|
self.smz3state = {player: TotalSMZ3Item.Progression([]) for player in parent.get_game_players("SMZ3")}
|
|
for player, group in parent.groups.items():
|
|
if (group["game"] == "SMZ3"):
|
|
self.smz3state[player] = TotalSMZ3Item.Progression([])
|
|
if player not in parent.state.smz3state:
|
|
parent.state.smz3state[player] = TotalSMZ3Item.Progression([])
|
|
else:
|
|
self.smz3state = {}
|
|
|
|
def copy_mixin(self, ret) -> CollectionState:
|
|
ret.smz3state = {player: copy.deepcopy(self.smz3state[player]) for player in self.smz3state}
|
|
return ret
|
|
|
|
class SMZ3Web(WebWorld):
|
|
tutorials = [Tutorial(
|
|
"Multiworld Setup Guide",
|
|
"A guide to setting up the Archipelago Super Metroid and A Link to the Past Crossover randomizer on your computer. This guide covers single-player, multiworld, and related software.",
|
|
"English",
|
|
"multiworld_en.md",
|
|
"multiworld/en",
|
|
["lordlou"]
|
|
)]
|
|
|
|
|
|
class SMZ3World(World):
|
|
"""
|
|
A python port of Super Metroid & A Link To The Past Crossover Item Randomizer based on v11.2 of Total's SMZ3.
|
|
This is allowed as long as we keep features and logic as close as possible as the original.
|
|
"""
|
|
game: str = "SMZ3"
|
|
topology_present = False
|
|
options_dataclass = SMZ3Options
|
|
options: SMZ3Options
|
|
|
|
item_names: Set[str] = frozenset(TotalSMZ3Item.lookup_name_to_id)
|
|
location_names: Set[str]
|
|
item_name_to_id = TotalSMZ3Item.lookup_name_to_id
|
|
location_name_to_id: Dict[str, int] = {key : locations_start_id + convertLocSMZ3IDToAPID(value.Id)
|
|
for key, value in TotalSMZ3World(Config(), "", 0, "").locationLookup.items()}
|
|
web = SMZ3Web()
|
|
|
|
locationNamesGT: Set[str] = {loc.Name for loc in GanonsTower(None, None).Locations}
|
|
|
|
# first added for 0.2.6
|
|
# optimized message queues for 0.4.4
|
|
required_client_version = (0, 4, 4)
|
|
|
|
def __init__(self, world: MultiWorld, player: int):
|
|
self.rom_name_available_event = threading.Event()
|
|
self.locations: Dict[str, Location] = {}
|
|
self.unreachable = []
|
|
self.junkItemsNames = [item.name for item in [
|
|
ItemType.Arrow,
|
|
ItemType.OneHundredRupees,
|
|
ItemType.TenArrows,
|
|
ItemType.ThreeBombs,
|
|
ItemType.OneRupee,
|
|
ItemType.FiveRupees,
|
|
ItemType.TwentyRupees,
|
|
ItemType.FiftyRupees,
|
|
ItemType.ThreeHundredRupees,
|
|
ItemType.Missile,
|
|
ItemType.Super,
|
|
ItemType.PowerBomb
|
|
]]
|
|
super().__init__(world, player)
|
|
|
|
@classmethod
|
|
def isProgression(cls, itemType):
|
|
progressionTypes = {
|
|
ItemType.ProgressiveShield,
|
|
ItemType.ProgressiveSword,
|
|
ItemType.Bow,
|
|
ItemType.Hookshot,
|
|
ItemType.Mushroom,
|
|
ItemType.Powder,
|
|
ItemType.Firerod,
|
|
ItemType.Icerod,
|
|
ItemType.Bombos,
|
|
ItemType.Ether,
|
|
ItemType.Quake,
|
|
ItemType.Lamp,
|
|
ItemType.Hammer,
|
|
ItemType.Shovel,
|
|
ItemType.Flute,
|
|
ItemType.Bugnet,
|
|
ItemType.Book,
|
|
ItemType.Bottle,
|
|
ItemType.Somaria,
|
|
ItemType.Byrna,
|
|
ItemType.Cape,
|
|
ItemType.Mirror,
|
|
ItemType.Boots,
|
|
ItemType.ProgressiveGlove,
|
|
ItemType.Flippers,
|
|
ItemType.MoonPearl,
|
|
ItemType.HalfMagic,
|
|
|
|
ItemType.Grapple,
|
|
ItemType.Charge,
|
|
ItemType.Ice,
|
|
ItemType.Wave,
|
|
ItemType.Plasma,
|
|
ItemType.Varia,
|
|
ItemType.Gravity,
|
|
ItemType.Morph,
|
|
ItemType.Bombs,
|
|
ItemType.SpringBall,
|
|
ItemType.ScrewAttack,
|
|
ItemType.HiJump,
|
|
ItemType.SpaceJump,
|
|
ItemType.SpeedBooster,
|
|
|
|
ItemType.ETank,
|
|
ItemType.ReserveTank,
|
|
|
|
ItemType.BigKeyGT,
|
|
ItemType.KeyGT,
|
|
ItemType.BigKeyEP,
|
|
ItemType.BigKeyDP,
|
|
ItemType.BigKeyTH,
|
|
ItemType.BigKeyPD,
|
|
ItemType.BigKeySP,
|
|
ItemType.BigKeySW,
|
|
ItemType.BigKeyTT,
|
|
ItemType.BigKeyIP,
|
|
ItemType.BigKeyMM,
|
|
ItemType.BigKeyTR,
|
|
|
|
ItemType.KeyHC,
|
|
ItemType.KeyCT,
|
|
ItemType.KeyDP,
|
|
ItemType.KeyTH,
|
|
ItemType.KeyPD,
|
|
ItemType.KeySP,
|
|
ItemType.KeySW,
|
|
ItemType.KeyTT,
|
|
ItemType.KeyIP,
|
|
ItemType.KeyMM,
|
|
ItemType.KeyTR,
|
|
|
|
ItemType.CardCrateriaL1,
|
|
ItemType.CardCrateriaL2,
|
|
ItemType.CardCrateriaBoss,
|
|
ItemType.CardBrinstarL1,
|
|
ItemType.CardBrinstarL2,
|
|
ItemType.CardBrinstarBoss,
|
|
ItemType.CardNorfairL1,
|
|
ItemType.CardNorfairL2,
|
|
ItemType.CardNorfairBoss,
|
|
ItemType.CardMaridiaL1,
|
|
ItemType.CardMaridiaL2,
|
|
ItemType.CardMaridiaBoss,
|
|
ItemType.CardWreckedShipL1,
|
|
ItemType.CardWreckedShipBoss,
|
|
ItemType.CardLowerNorfairL1,
|
|
ItemType.CardLowerNorfairBoss,
|
|
}
|
|
return itemType in progressionTypes
|
|
|
|
def generate_early(self):
|
|
self.config = Config()
|
|
self.config.GameMode = GameMode.Multiworld
|
|
self.config.Z3Logic = Z3Logic.Normal
|
|
self.config.SMLogic = SMLogic(self.options.sm_logic.value)
|
|
self.config.SwordLocation = SwordLocation(self.options.sword_location.value)
|
|
self.config.MorphLocation = MorphLocation(self.options.morph_location.value)
|
|
self.config.Goal = Goal(self.options.goal.value)
|
|
self.config.KeyShuffle = KeyShuffle(self.options.key_shuffle.value)
|
|
self.config.OpenTower = OpenTower(self.options.open_tower.value)
|
|
self.config.GanonVulnerable = GanonVulnerable(self.options.ganon_vulnerable.value)
|
|
self.config.OpenTourian = OpenTourian(self.options.open_tourian.value)
|
|
|
|
self.local_random = random.Random(self.multiworld.random.randint(0, 1000))
|
|
self.smz3World = TotalSMZ3World(self.config, self.multiworld.get_player_name(self.player), self.player, self.multiworld.seed_name)
|
|
self.smz3World.Setup(WorldState.Generate(self.config, self.multiworld.random))
|
|
self.smz3DungeonItems = []
|
|
SMZ3World.location_names = frozenset(self.smz3World.locationLookup.keys())
|
|
|
|
self.multiworld.state.smz3state[self.player] = TotalSMZ3Item.Progression([])
|
|
|
|
if not self.smz3World.Config.Keysanity:
|
|
# Dungeons items here are not in the itempool and will be prefilled locally so they must stay local
|
|
self.options.non_local_items.value -= frozenset(item_name for item_name in self.item_names if TotalSMZ3Item.Item.IsNameDungeonItem(item_name))
|
|
|
|
def create_items(self):
|
|
self.dungeon = TotalSMZ3Item.Item.CreateDungeonPool(self.smz3World)
|
|
self.dungeon.reverse()
|
|
self.progression = TotalSMZ3Item.Item.CreateProgressionPool(self.smz3World)
|
|
self.keyCardsItems = TotalSMZ3Item.Item.CreateKeycards(self.smz3World)
|
|
self.SmMapsItems = TotalSMZ3Item.Item.CreateSmMaps(self.smz3World)
|
|
|
|
niceItems = TotalSMZ3Item.Item.CreateNicePool(self.smz3World)
|
|
junkItems = TotalSMZ3Item.Item.CreateJunkPool(self.smz3World)
|
|
|
|
if (self.smz3World.Config.Keysanity):
|
|
progressionItems = self.progression + self.dungeon + self.keyCardsItems + self.SmMapsItems
|
|
else:
|
|
progressionItems = self.progression
|
|
for item in self.keyCardsItems:
|
|
self.multiworld.push_precollected(SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item))
|
|
|
|
itemPool = [SMZ3Item(item.Type.name, ItemClassification.progression, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in progressionItems] + \
|
|
[SMZ3Item(item.Type.name, ItemClassification.useful, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in niceItems] + \
|
|
[SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in junkItems]
|
|
self.smz3DungeonItems = [SMZ3Item(item.Type.name, ItemClassification.progression, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in self.dungeon]
|
|
self.multiworld.itempool += itemPool
|
|
|
|
def set_rules(self):
|
|
# SM G4 is logically required to complete Ganon's Tower
|
|
self.multiworld.completion_condition[self.player] = lambda state: \
|
|
self.smz3World.GetRegion("Ganon's Tower").CanEnter(state.smz3state[self.player]) and \
|
|
self.smz3World.GetRegion("Ganon's Tower").TowerAscend(state.smz3state[self.player]) and \
|
|
self.smz3World.GetRegion("Ganon's Tower").CanComplete(state.smz3state[self.player])
|
|
|
|
for region in self.smz3World.Regions:
|
|
entrance = self.multiworld.get_entrance('Menu' + "->" + region.Name, self.player)
|
|
set_rule(entrance, lambda state, region=region: region.CanEnter(state.smz3state[self.player]))
|
|
for loc in region.Locations:
|
|
l = self.locations[loc.Name]
|
|
if self.options.accessibility.value != ItemsAccessibility.option_full:
|
|
l.always_allow = lambda state, item, loc=loc: \
|
|
item.game == "SMZ3" and \
|
|
loc.alwaysAllow(item.item, state.smz3state[self.player])
|
|
l.item_rule = lambda item, loc=loc, region=region, old_rule=l.item_rule: (\
|
|
item.game != "SMZ3" or \
|
|
loc.allow(item.item, None) and \
|
|
region.CanFill(item.item)) and old_rule(item)
|
|
set_rule(l, lambda state, loc=loc: loc.Available(state.smz3state[self.player]))
|
|
|
|
# In multiworlds, GT is disallowed from having progression items.
|
|
# This item rule replicates this behavior for non-SMZ3 games
|
|
for loc in self.smz3World.GetRegion("Ganon's Tower").Locations:
|
|
l = self.locations[loc.Name]
|
|
l.item_rule = lambda item, old_rule=l.item_rule: \
|
|
(item.game == "SMZ3" or not item.advancement) and old_rule(item)
|
|
|
|
def create_regions(self):
|
|
self.create_locations(self.player)
|
|
startRegion = self.create_region(self.multiworld, self.player, 'Menu')
|
|
self.multiworld.regions.append(startRegion)
|
|
|
|
for region in self.smz3World.Regions:
|
|
currentRegion = self.create_region(self.multiworld, self.player, region.Name, region.locationLookup.keys(), [region.Name + "->" + 'Menu'])
|
|
self.multiworld.regions.append(currentRegion)
|
|
entrance = self.multiworld.get_entrance(region.Name + "->" + 'Menu', self.player)
|
|
entrance.connect(startRegion)
|
|
exit = Entrance(self.player, 'Menu' + "->" + region.Name, startRegion)
|
|
startRegion.exits.append(exit)
|
|
exit.connect(currentRegion)
|
|
|
|
def apply_sm_custom_sprite(self):
|
|
itemSprites = ["off_world_prog_item.bin", "off_world_item.bin"]
|
|
itemSpritesAddress = [0xF800, 0xF900]
|
|
idx = 0
|
|
offworldSprites = {}
|
|
for fileName in itemSprites:
|
|
with openFile(world_folder + "/data/custom_sprite/" + fileName, 'rb') as stream:
|
|
buffer = bytearray(stream.read())
|
|
offworldSprites[0x04Eff2 + 10*((0x6B + 0x40) + idx)] = bytearray(getWordArray(itemSpritesAddress[idx])) + buffer[0:8]
|
|
offworldSprites[0x090000 + itemSpritesAddress[idx]] = buffer[8:264]
|
|
idx += 1
|
|
return offworldSprites
|
|
|
|
def convert_to_sm_item_name(self, itemName):
|
|
# SMZ3 uses a different font; this map is not compatible with just SM alone.
|
|
charMap = {
|
|
"A": 0x3CE0,
|
|
"B": 0x3CE1,
|
|
"C": 0x3CE2,
|
|
"D": 0x3CE3,
|
|
"E": 0x3CE4,
|
|
"F": 0x3CE5,
|
|
"G": 0x3CE6,
|
|
"H": 0x3CE7,
|
|
"I": 0x3CE8,
|
|
"J": 0x3CE9,
|
|
"K": 0x3CEA,
|
|
"L": 0x3CEB,
|
|
"M": 0x3CEC,
|
|
"N": 0x3CED,
|
|
"O": 0x3CEE,
|
|
"P": 0x3CEF,
|
|
"Q": 0x3CF0,
|
|
"R": 0x3CF1,
|
|
"S": 0x3CF2,
|
|
"T": 0x3CF3,
|
|
"U": 0x3CF4,
|
|
"V": 0x3CF5,
|
|
"W": 0x3CF6,
|
|
"X": 0x3CF7,
|
|
"Y": 0x3CF8,
|
|
"Z": 0x3CF9,
|
|
" ": 0x3C4E,
|
|
"!": 0x3CFF,
|
|
"?": 0x3CFE,
|
|
"'": 0x3CFD,
|
|
",": 0x3CFB,
|
|
".": 0x3CFA,
|
|
"-": 0x3CCF,
|
|
"1": 0x3C80,
|
|
"2": 0x3C81,
|
|
"3": 0x3C82,
|
|
"4": 0x3C83,
|
|
"5": 0x3C84,
|
|
"6": 0x3C85,
|
|
"7": 0x3C86,
|
|
"8": 0x3C87,
|
|
"9": 0x3C88,
|
|
"0": 0x3C89,
|
|
"%": 0x3C0A,
|
|
"a": 0x3C90,
|
|
"b": 0x3C91,
|
|
"c": 0x3C92,
|
|
"d": 0x3C93,
|
|
"e": 0x3C94,
|
|
"f": 0x3C95,
|
|
"g": 0x3C96,
|
|
"h": 0x3C97,
|
|
"i": 0x3C98,
|
|
"j": 0x3C99,
|
|
"k": 0x3C9A,
|
|
"l": 0x3C9B,
|
|
"m": 0x3C9C,
|
|
"n": 0x3C9D,
|
|
"o": 0x3C9E,
|
|
"p": 0x3C9F,
|
|
"q": 0x3CA0,
|
|
"r": 0x3CA1,
|
|
"s": 0x3CA2,
|
|
"t": 0x3CA3,
|
|
"u": 0x3CA4,
|
|
"v": 0x3CA5,
|
|
"w": 0x3CA6,
|
|
"x": 0x3CA7,
|
|
"y": 0x3CA8,
|
|
"z": 0x3CA9,
|
|
'"': 0x3CAA,
|
|
":": 0x3CAB,
|
|
"~": 0x3CAC,
|
|
"@": 0x3CAD,
|
|
"#": 0x3CAE,
|
|
"+": 0x3CAF,
|
|
"_": 0x000E
|
|
}
|
|
data = []
|
|
|
|
itemName = itemName.replace("_", "-").strip()[:26]
|
|
itemName = itemName.center(26, " ")
|
|
itemName = "___" + itemName + "___"
|
|
|
|
for char in itemName:
|
|
(w0, w1) = getWord(charMap.get(char, 0x3C4E))
|
|
data.append(w0)
|
|
data.append(w1)
|
|
return data
|
|
|
|
def convert_to_lttp_item_name(self, itemName):
|
|
return bytearray(itemName[:19].center(19, " "), 'utf8') + bytearray(0)
|
|
|
|
def apply_item_names(self):
|
|
patch = {}
|
|
sm_remote_idx = 0
|
|
lttp_remote_idx = 0
|
|
for location in self.smz3World.Locations:
|
|
if self.multiworld.worlds[location.APLocation.item.player].game != self.game:
|
|
if location.Type == LocationType.Visible or location.Type == LocationType.Chozo or location.Type == LocationType.Hidden:
|
|
patch[0x390000 + sm_remote_idx*64] = self.convert_to_sm_item_name(location.APLocation.item.name)
|
|
sm_remote_idx += 1
|
|
progressionItem = (0 if location.APLocation.item.advancement else 0x8000) + sm_remote_idx
|
|
patch[0x386000 + (location.Id * 8) + 6] = bytearray(getWordArray(progressionItem))
|
|
else:
|
|
patch[0x390000 + 100 * 64 + lttp_remote_idx * 20] = self.convert_to_lttp_item_name(location.APLocation.item.name)
|
|
lttp_remote_idx += 1
|
|
progressionItem = (0 if location.APLocation.item.advancement else 0x8000) + lttp_remote_idx
|
|
patch[0x386000 + (location.Id * 8) + 6] = bytearray(getWordArray(progressionItem))
|
|
|
|
return patch
|
|
|
|
def SnesCustomization(self, addr: int):
|
|
addr = (0x400000 if addr < 0x800000 else 0)| (addr & 0x3FFFFF)
|
|
return addr
|
|
|
|
def apply_customization(self):
|
|
patch = {}
|
|
|
|
# smSpinjumps
|
|
if (self.options.spin_jumps_animation.value == 1):
|
|
patch[self.SnesCustomization(0x9B93FE)] = bytearray([0x01])
|
|
|
|
# z3HeartBeep
|
|
values = [ 0x00, 0x80, 0x40, 0x20, 0x10]
|
|
index = self.options.heart_beep_speed.value
|
|
patch[0x400033] = bytearray([values[index if index < len(values) else 2]])
|
|
|
|
# z3HeartColor
|
|
values = [
|
|
[0x24, [0x18, 0x00]],
|
|
[0x3C, [0x04, 0x17]],
|
|
[0x2C, [0xC9, 0x69]],
|
|
[0x28, [0xBC, 0x02]]
|
|
]
|
|
index = self.options.heart_color.value
|
|
(hud, fileSelect) = values[index if index < len(values) else 0]
|
|
for i in range(0, 20, 2):
|
|
patch[self.SnesCustomization(0xDFA1E + i)] = bytearray([hud])
|
|
patch[self.SnesCustomization(0x1BD6AA)] = bytearray(fileSelect)
|
|
|
|
# z3QuickSwap
|
|
patch[0x40004B] = bytearray([0x01 if self.options.quick_swap.value else 0x00])
|
|
|
|
# smEnergyBeepOff
|
|
if (self.options.energy_beep.value == 0):
|
|
for ([addr, value]) in [
|
|
[0x90EA9B, 0x80],
|
|
[0x90F337, 0x80],
|
|
[0x91E6D5, 0x80]
|
|
]:
|
|
patch[self.SnesCustomization(addr)] = bytearray([value])
|
|
|
|
return patch
|
|
|
|
def generate_output(self, output_directory: str):
|
|
try:
|
|
patcher = TotalSMZ3Patch(self.smz3World,
|
|
[world.smz3World for key, world in self.multiworld.worlds.items() if isinstance(world, SMZ3World) and hasattr(world, "smz3World")],
|
|
self.multiworld.seed_name,
|
|
self.multiworld.seed,
|
|
self.local_random,
|
|
{v: k for k, v in self.multiworld.player_name.items()},
|
|
next(iter(loc.player for loc in self.multiworld.get_locations() if (loc.item.name == "SilverArrows" and loc.item.player == self.player))))
|
|
patches = patcher.Create(self.smz3World.Config)
|
|
patches.update(self.apply_sm_custom_sprite())
|
|
patches.update(self.apply_item_names())
|
|
patches.update(self.apply_customization())
|
|
|
|
patch = SMZ3ProcedurePatch(player=self.player, player_name=self.player_name)
|
|
patch.write_tokens(patches)
|
|
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)
|
|
|
|
self.rom_name = bytearray(patcher.title, 'utf8')
|
|
except:
|
|
raise
|
|
finally:
|
|
self.rom_name_available_event.set() # make sure threading continues and errors are collected
|
|
|
|
def modify_multidata(self, multidata: dict):
|
|
import base64
|
|
if (not self.smz3World.Config.Keysanity):
|
|
for item_name in self.keyCardsItems:
|
|
item_id = self.item_name_to_id.get(item_name.Type.name, None)
|
|
try:
|
|
multidata["precollected_items"][self.player].remove(item_id)
|
|
except ValueError as e:
|
|
logger.warning(f"Attempted to remove nonexistent item id {item_id} from smz3 precollected items ({item_name})")
|
|
|
|
# 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()
|
|
payload = multidata["connect_names"][self.multiworld.player_name[self.player]]
|
|
multidata["connect_names"][new_name] = payload
|
|
|
|
def fill_slot_data(self):
|
|
slot_data = {
|
|
"goal": self.options.goal.value,
|
|
"open_tower": self.options.open_tower.value,
|
|
"ganon_vulnerable": self.options.ganon_vulnerable.value,
|
|
"open_tourian": self.options.open_tourian.value,
|
|
"sm_logic": self.options.sm_logic.value,
|
|
"key_shuffle": self.options.key_shuffle.value,
|
|
}
|
|
return slot_data
|
|
|
|
def collect(self, state: CollectionState, item: Item) -> bool:
|
|
state.smz3state[self.player].Add([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World if hasattr(self, "smz3World") else None)])
|
|
if item.advancement:
|
|
state.prog_items[item.player][item.name] += 1
|
|
return True # indicate that a logical state change has occured
|
|
return False
|
|
|
|
def remove(self, state: CollectionState, item: Item) -> bool:
|
|
name = self.collect_item(state, item, True)
|
|
if name:
|
|
state.smz3state[item.player].Remove([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World if hasattr(self, "smz3World") else None)])
|
|
state.prog_items[item.player][item.name] -= 1
|
|
if state.prog_items[item.player][item.name] < 1:
|
|
del (state.prog_items[item.player][item.name])
|
|
return True
|
|
return False
|
|
|
|
def create_item(self, name: str) -> Item:
|
|
return SMZ3Item(name,
|
|
ItemClassification.progression if SMZ3World.isProgression(TotalSMZ3Item.ItemType[name]) else ItemClassification.filler,
|
|
TotalSMZ3Item.ItemType[name], self.item_name_to_id[name],
|
|
self.player,
|
|
TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[name], getattr(self, "smz3World", None)))
|
|
|
|
def pre_fill(self):
|
|
from Fill import fill_restrictive
|
|
self.InitialFillInOwnWorld()
|
|
|
|
if (not self.smz3World.Config.Keysanity):
|
|
locations = [loc for loc in self.locations.values() if loc.item is None]
|
|
self.multiworld.random.shuffle(locations)
|
|
|
|
all_state = self.multiworld.get_all_state(False)
|
|
for item in self.smz3DungeonItems:
|
|
all_state.remove(item)
|
|
|
|
all_dungeonItems = self.smz3DungeonItems[:]
|
|
fill_restrictive(self.multiworld, all_state, locations, all_dungeonItems, True, True)
|
|
self.JunkFillGT(0.5)
|
|
|
|
def get_pre_fill_items(self):
|
|
if (not self.smz3World.Config.Keysanity):
|
|
return self.smz3DungeonItems
|
|
else:
|
|
return []
|
|
|
|
def post_fill(self):
|
|
# some small or big keys (those always_allow) can be unreachable in-game
|
|
# while logic still collects some of them (probably to simulate the player collecting pot keys in the logic), some others don't
|
|
# so we need to remove those exceptions as progression items
|
|
if self.options.accessibility.value == ItemsAccessibility.option_items:
|
|
state = CollectionState(self.multiworld)
|
|
locs = [self.multiworld.get_location("Swamp Palace - Big Chest", self.player),
|
|
self.multiworld.get_location("Skull Woods - Big Chest", self.player),
|
|
self.multiworld.get_location("Tower of Hera - Big Key Chest", self.player)]
|
|
for loc in locs:
|
|
if (loc.item.player == self.player and loc.always_allow(state, loc.item)):
|
|
loc.item.classification = ItemClassification.filler
|
|
loc.item.item.Progression = False
|
|
self.unreachable.append(loc)
|
|
|
|
def get_filler_item_name(self) -> str:
|
|
return self.multiworld.random.choice(self.junkItemsNames)
|
|
|
|
def write_spoiler(self, spoiler_handle: TextIO):
|
|
self.multiworld.spoiler.unreachables.update(self.unreachable)
|
|
player_name = f'{self.multiworld.get_player_name(self.player)}: ' if self.multiworld.players > 1 else ''
|
|
spoiler_handle.write('\n\nRewards:\n\n')
|
|
spoiler_handle.write('\n'.join([
|
|
f"{player_name}{region.Name}: {region.Reward.name}"
|
|
for region in self.smz3World.Regions
|
|
if isinstance(region, IReward)
|
|
]))
|
|
spoiler_handle.write('\n\nMedallions:\n\n')
|
|
spoiler_handle.write('\n'.join([
|
|
f"{player_name}{region.Name}: {region.Medallion.name}"
|
|
for region in self.smz3World.Regions
|
|
if isinstance(region, IMedallionAccess)
|
|
]))
|
|
|
|
def JunkFillGT(self, factor):
|
|
junkPoolIdx = [idx for idx, i in enumerate(self.multiworld.itempool) if i.excludable]
|
|
self.random.shuffle(junkPoolIdx)
|
|
junkLocations = [loc for loc in self.locations.values() if loc.name in self.locationNamesGT and loc.item is None]
|
|
self.random.shuffle(junkLocations)
|
|
toRemove = []
|
|
for loc in junkLocations:
|
|
# Note: Upstream GT junk fill uses FastFill, which ignores item rules
|
|
if len(junkPoolIdx) == 0 or len(toRemove) >= int(len(junkLocations) * factor * self.smz3World.TowerCrystals / 7):
|
|
break
|
|
itemFromPool = self.multiworld.itempool[junkPoolIdx[0]]
|
|
toRemove.append(junkPoolIdx.pop(0))
|
|
loc.place_locked_item(itemFromPool)
|
|
toRemove.sort(reverse = True)
|
|
for i in toRemove:
|
|
self.multiworld.itempool.pop(i)
|
|
|
|
def FillItemAtLocation(self, itemPool, itemType, location):
|
|
itemToPlace = TotalSMZ3Item.Item.Get(itemPool, itemType, self.smz3World)
|
|
if (itemToPlace == None):
|
|
raise Exception(f"Tried to place item {itemType} at {location.Name}, but there is no such item in the item pool")
|
|
else:
|
|
location.Item = itemToPlace
|
|
itemPoolIdx = next((idx for idx, i in enumerate(self.multiworld.itempool) if i.player == self.player and i.name == itemToPlace.Type.name), None)
|
|
if itemPoolIdx is not None:
|
|
itemFromPool = self.multiworld.itempool.pop(itemPoolIdx)
|
|
self.multiworld.get_location(location.Name, self.player).place_locked_item(itemFromPool)
|
|
else:
|
|
itemPoolIdx = next((idx for idx, i in enumerate(self.smz3DungeonItems) if i.player == self.player and i.name == itemToPlace.Type.name), None)
|
|
if itemPoolIdx is not None:
|
|
itemFromPool = self.smz3DungeonItems.pop(itemPoolIdx)
|
|
self.multiworld.get_location(location.Name, self.player).place_locked_item(itemFromPool)
|
|
itemPool.remove(itemToPlace)
|
|
|
|
def FrontFillItemInOwnWorld(self, itemPool, itemType):
|
|
item = TotalSMZ3Item.Item.Get(itemPool, itemType, self.smz3World)
|
|
location = next(iter(self.multiworld.random.sample(TotalSMZ3Location.AvailableGlobal(TotalSMZ3Location.Empty(self.smz3World.Locations), self.smz3World.Items()), 1)), None)
|
|
if (location == None):
|
|
raise Exception(f"Tried to front fill {item.Name} in, but no location was available")
|
|
|
|
location.Item = item
|
|
itemPoolIdx = next((idx for idx, i in enumerate(self.multiworld.itempool) if i.player == self.player and i.name == item.Type.name and i.advancement == item.Progression), None)
|
|
if itemPoolIdx is not None:
|
|
itemFromPool = self.multiworld.itempool.pop(itemPoolIdx)
|
|
self.multiworld.get_location(location.Name, self.player).place_locked_item(itemFromPool)
|
|
itemPool.remove(item)
|
|
|
|
def InitialFillInOwnWorld(self):
|
|
self.FillItemAtLocation(self.dungeon, TotalSMZ3Item.ItemType.KeySW, self.smz3World.GetLocation("Skull Woods - Pinball Room"))
|
|
if (not self.smz3World.Config.Keysanity):
|
|
self.FillItemAtLocation(self.dungeon, TotalSMZ3Item.ItemType.KeySP, self.smz3World.GetLocation("Swamp Palace - Entrance"))
|
|
|
|
# /* Check Swords option and place as needed */
|
|
if self.smz3World.Config.SwordLocation == SwordLocation.Uncle:
|
|
self.FillItemAtLocation(self.progression, TotalSMZ3Item.ItemType.ProgressiveSword, self.smz3World.GetLocation("Link's Uncle"))
|
|
|
|
# /* Check Morph option and place as needed */
|
|
if self.smz3World.Config.MorphLocation == MorphLocation.Original:
|
|
self.FillItemAtLocation(self.progression, TotalSMZ3Item.ItemType.Morph, self.smz3World.GetLocation("Morphing Ball"))
|
|
elif self.smz3World.Config.MorphLocation == MorphLocation.Early:
|
|
self.FrontFillItemInOwnWorld(self.progression, TotalSMZ3Item.ItemType.Morph)
|
|
|
|
# We do early Sword placement after Morph in case its Original location
|
|
if self.smz3World.Config.SwordLocation == SwordLocation.Early:
|
|
self.FrontFillItemInOwnWorld(self.progression, TotalSMZ3Item.ItemType.ProgressiveSword)
|
|
|
|
# /* We place a PB and Super in Sphere 1 to make sure the filler
|
|
# * doesn't start locking items behind this when there are a
|
|
# * high chance of the trash fill actually making them available */
|
|
self.FrontFillItemInOwnWorld(self.progression, TotalSMZ3Item.ItemType.Super)
|
|
self.FrontFillItemInOwnWorld(self.progression, TotalSMZ3Item.ItemType.PowerBomb)
|
|
|
|
def create_locations(self, player: int):
|
|
for name, id in SMZ3World.location_name_to_id.items():
|
|
newLoc = SMZ3Location(player, name, id)
|
|
self.locations[name] = newLoc
|
|
self.smz3World.locationLookup[name].APLocation = newLoc
|
|
|
|
def create_region(self, world: MultiWorld, player: int, name: str, locations=None, exits=None):
|
|
ret = Region(name, player, world)
|
|
if locations:
|
|
for loc in locations:
|
|
location = self.locations[loc]
|
|
location.parent_region = ret
|
|
ret.locations.append(location)
|
|
if exits:
|
|
for exit in exits:
|
|
ret.exits.append(Entrance(player, exit, ret))
|
|
return ret
|
|
|
|
|
|
class SMZ3Location(Location):
|
|
game: str = "SMZ3"
|
|
|
|
def __init__(self, player: int, name: str, address=None, parent=None):
|
|
super(SMZ3Location, self).__init__(player, name, address, parent)
|
|
|
|
|
|
class SMZ3Item(Item):
|
|
game = "SMZ3"
|
|
type: ItemType
|
|
item: Item
|
|
|
|
def __init__(self, name, classification, type: ItemType, code, player: int, item: Item):
|
|
super(SMZ3Item, self).__init__(name, classification, code, player)
|
|
self.type = type
|
|
self.item = item
|