Core: provide a way to add to CollectionState init and copy

SM: use that way
OoT: use that way
This commit is contained in:
Fabian Dill 2022-02-17 07:07:34 +01:00
parent c525c80b49
commit daea0f3e5e
8 changed files with 125 additions and 153 deletions

View File

@ -6,7 +6,7 @@ import logging
import json import json
import functools import functools
from collections import OrderedDict, Counter, deque from collections import OrderedDict, Counter, deque
from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple, TypedDict, TYPE_CHECKING from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple, TypedDict, TYPE_CHECKING, Callable
import secrets import secrets
import random import random
@ -16,6 +16,7 @@ import NetUtils
if TYPE_CHECKING: if TYPE_CHECKING:
from worlds import AutoWorld from worlds import AutoWorld
auto_world = AutoWorld.World auto_world = AutoWorld.World
else: else:
auto_world = object auto_world = object
@ -193,17 +194,20 @@ class MultiWorld():
def set_options(self, args): def set_options(self, args):
from worlds import AutoWorld from worlds import AutoWorld
for option_key in Options.common_options:
setattr(self, option_key, getattr(args, option_key, {}))
for option_key in Options.per_game_common_options:
setattr(self, option_key, getattr(args, option_key, {}))
for player in self.player_ids: for player in self.player_ids:
self.custom_data[player] = {} self.custom_data[player] = {}
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]] world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
for option_key in world_type.options: for option_key in world_type.options:
setattr(self, option_key, getattr(args, option_key, {})) setattr(self, option_key, getattr(args, option_key, {}))
for option_key in Options.common_options:
setattr(self, option_key, getattr(args, option_key, {}))
for option_key in Options.per_game_common_options:
setattr(self, option_key, getattr(args, option_key, {}))
self.worlds[player] = world_type(self, player) self.worlds[player] = world_type(self, player)
def set_item_links(self):
item_links = {} item_links = {}
for player in self.player_ids: for player in self.player_ids:
@ -239,6 +243,7 @@ class MultiWorld():
setattr(self, option_key, {player_id: option(option.default) for player_id in self.player_ids}) setattr(self, option_key, {player_id: option(option.default) for player_id in self.player_ids})
for option_key, option in Options.per_game_common_options.items(): for option_key, option in Options.per_game_common_options.items():
setattr(self, option_key, {player_id: option(option.default) for player_id in self.player_ids}) setattr(self, option_key, {player_id: option(option.default) for player_id in self.player_ids})
self.state = CollectionState(self)
def secure(self): def secure(self):
self.random = secrets.SystemRandom() self.random = secrets.SystemRandom()
@ -551,7 +556,9 @@ class MultiWorld():
return False return False
class CollectionState(object): class CollectionState():
additional_init_functions: List[Callable] = []
additional_copy_functions: List[Callable] = []
def __init__(self, parent: MultiWorld): def __init__(self, parent: MultiWorld):
self.prog_items = Counter() self.prog_items = Counter()
@ -565,6 +572,8 @@ class CollectionState(object):
for items in parent.precollected_items.values(): for items in parent.precollected_items.values():
for item in items: for item in items:
self.collect(item, True) self.collect(item, True)
for function in self.additional_init_functions:
function(self, parent)
def update_reachable_regions(self, player: int): def update_reachable_regions(self, player: int):
from worlds.alttp.EntranceShuffle import indirect_connections from worlds.alttp.EntranceShuffle import indirect_connections
@ -609,6 +618,8 @@ class CollectionState(object):
ret.events = copy.copy(self.events) ret.events = copy.copy(self.events)
ret.path = copy.copy(self.path) ret.path = copy.copy(self.path)
ret.locations_checked = copy.copy(self.locations_checked) ret.locations_checked = copy.copy(self.locations_checked)
for function in self.additional_copy_functions:
ret = function(self, ret)
return ret return ret
def can_reach(self, spot, resolution_hint=None, player=None) -> bool: def can_reach(self, spot, resolution_hint=None, player=None) -> bool:

View File

@ -77,6 +77,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option. world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
world.set_options(args) world.set_options(args)
world.set_item_links()
world.state = CollectionState(world) world.state = CollectionState(world)
logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed) logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)

View File

@ -1,6 +1,6 @@
from argparse import Namespace from argparse import Namespace
from BaseClasses import MultiWorld from BaseClasses import MultiWorld, CollectionState
from worlds.AutoWorld import call_all from worlds.AutoWorld import call_all
gen_steps = ["generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill"] gen_steps = ["generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill"]

View File

@ -1,10 +0,0 @@
from test.hollow_knight import TestVanilla
class TestBasic(TestVanilla):
def testSimple(self):
self.run_location_tests([
["200_Geo-False_Knight_Chest", True, [], []],
["380_Geo-Soul_Master_Chest", False, [], ["Mantis_Claw"]],
])

View File

@ -1,20 +0,0 @@
from worlds.hk import HKWorld
from BaseClasses import MultiWorld
from worlds import AutoWorld
from worlds.hk.Options import hollow_knight_randomize_options, hollow_knight_skip_options
from test.TestBase import TestBase
class TestVanilla(TestBase):
def setUp(self):
self.world = MultiWorld(1)
self.world.game[1] = "Hollow Knight"
self.world.worlds[1] = HKWorld(self.world, 1)
for hk_option in hollow_knight_randomize_options:
setattr(self.world, hk_option, {1: True})
for hk_option, option in hollow_knight_skip_options.items():
setattr(self.world, hk_option, {1: option.default})
AutoWorld.call_single(self.world, "create_regions", 1)
AutoWorld.call_single(self.world, "generate_basic", 1)
AutoWorld.call_single(self.world, "set_rules", 1)

View File

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import Dict, Set, Tuple, List, Optional, TextIO, Any from typing import Dict, Set, Tuple, List, Optional, TextIO, Any, Callable
from BaseClasses import MultiWorld, Item, CollectionState, Location from BaseClasses import MultiWorld, Item, CollectionState, Location
from Options import Option from Options import Option
@ -35,8 +35,13 @@ class AutoWorldRegister(type):
class AutoLogicRegister(type): class AutoLogicRegister(type):
def __new__(cls, name, bases, dct): def __new__(cls, name, bases, dct):
new_class = super().__new__(cls, name, bases, dct) new_class = super().__new__(cls, name, bases, dct)
function: Callable
for item_name, function in dct.items(): for item_name, function in dct.items():
if not item_name.startswith("__"): if item_name == "copy_mixin":
CollectionState.additional_copy_functions.append(function)
elif item_name == "init_mixin":
CollectionState.additional_init_functions.append(function)
elif not item_name.startswith("__"):
if hasattr(CollectionState, item_name): if hasattr(CollectionState, item_name):
raise Exception(f"Name conflict on Logic Mixin {name} trying to overwrite {item_name}") raise Exception(f"Name conflict on Logic Mixin {name} trying to overwrite {item_name}")
setattr(CollectionState, item_name, function) setattr(CollectionState, item_name, function)
@ -193,6 +198,7 @@ class World(metaclass=AutoWorldRegister):
def write_spoiler_end(self, spoiler_handle: TextIO): def write_spoiler_end(self, spoiler_handle: TextIO):
"""Write to the end of the spoiler""" """Write to the end of the spoiler"""
pass pass
# end of ordered Main.py calls # end of ordered Main.py calls
def create_item(self, name: str) -> Item: def create_item(self, name: str) -> Item:
@ -240,7 +246,6 @@ class World(metaclass=AutoWorldRegister):
self.world.itempool.append(self.create_item(self.get_filler_item_name())) self.world.itempool.append(self.create_item(self.get_filler_item_name()))
# any methods attached to this can be used as part of CollectionState, # any methods attached to this can be used as part of CollectionState,
# please use a prefix as all of them get clobbered together # please use a prefix as all of them get clobbered together
class LogicMixin(metaclass=AutoLogicRegister): class LogicMixin(metaclass=AutoLogicRegister):

View File

@ -31,7 +31,7 @@ from BaseClasses import MultiWorld, CollectionState, RegionType
from Options import Range, Toggle, OptionList from Options import Range, Toggle, OptionList
from Fill import fill_restrictive, FillError from Fill import fill_restrictive, FillError
from worlds.generic.Rules import exclusion_rules from worlds.generic.Rules import exclusion_rules
from ..AutoWorld import World from ..AutoWorld import World, AutoLogicRegister
location_id_offset = 67000 location_id_offset = 67000
@ -39,6 +39,33 @@ location_id_offset = 67000
i_o_limiter = threading.Semaphore(2) i_o_limiter = threading.Semaphore(2)
class OOTCollectionState(metaclass=AutoLogicRegister):
def init_mixin(self, parent: MultiWorld):
all_ids = parent.get_all_ids()
self.child_reachable_regions = {player: set() for player in all_ids}
self.adult_reachable_regions = {player: set() for player in all_ids}
self.child_blocked_connections = {player: set() for player in all_ids}
self.adult_blocked_connections = {player: set() for player in all_ids}
self.day_reachable_regions = {player: set() for player in all_ids}
self.dampe_reachable_regions = {player: set() for player in all_ids}
self.age = {player: None for player in all_ids}
def copy_mixin(self, ret) -> CollectionState:
ret.child_reachable_regions = {player: copy.copy(self.child_reachable_regions[player]) for player in
self.child_reachable_regions}
ret.adult_reachable_regions = {player: copy.copy(self.adult_reachable_regions[player]) for player in
self.adult_reachable_regions}
ret.child_blocked_connections = {player: copy.copy(self.child_blocked_connections[player]) for player in
self.child_blocked_connections}
ret.adult_blocked_connections = {player: copy.copy(self.adult_blocked_connections[player]) for player in
self.adult_blocked_connections}
ret.day_reachable_regions = {player: copy.copy(self.adult_reachable_regions[player]) for player in
self.day_reachable_regions}
ret.dampe_reachable_regions = {player: copy.copy(self.adult_reachable_regions[player]) for player in
self.dampe_reachable_regions}
return ret
class OOTWorld(World): class OOTWorld(World):
""" """
The Legend of Zelda: Ocarina of Time is a 3D action/adventure game. Travel through Hyrule in two time periods, The Legend of Zelda: Ocarina of Time is a 3D action/adventure game. Travel through Hyrule in two time periods,
@ -56,51 +83,6 @@ class OOTWorld(World):
data_version = 1 data_version = 1
def __new__(cls, world, player):
# Add necessary objects to CollectionState on initialization
orig_init = CollectionState.__init__
orig_copy = CollectionState.copy
def oot_init(self, parent: MultiWorld):
orig_init(self, parent)
self.child_reachable_regions = {player: set() for player in range(1, parent.players + 1)}
self.adult_reachable_regions = {player: set() for player in range(1, parent.players + 1)}
self.child_blocked_connections = {player: set() for player in range(1, parent.players + 1)}
self.adult_blocked_connections = {player: set() for player in range(1, parent.players + 1)}
self.day_reachable_regions = {player: set() for player in range(1, parent.players + 1)}
self.dampe_reachable_regions = {player: set() for player in range(1, parent.players + 1)}
self.age = {player: None for player in range(1, parent.players + 1)}
def oot_copy(self):
ret = orig_copy(self)
ret.child_reachable_regions = {player: copy.copy(self.child_reachable_regions[player]) for player in
range(1, self.world.players + 1)}
ret.adult_reachable_regions = {player: copy.copy(self.adult_reachable_regions[player]) for player in
range(1, self.world.players + 1)}
ret.child_blocked_connections = {player: copy.copy(self.child_blocked_connections[player]) for player in
range(1, self.world.players + 1)}
ret.adult_blocked_connections = {player: copy.copy(self.adult_blocked_connections[player]) for player in
range(1, self.world.players + 1)}
ret.day_reachable_regions = {player: copy.copy(self.adult_reachable_regions[player]) for player in
range(1, self.world.players + 1)}
ret.dampe_reachable_regions = {player: copy.copy(self.adult_reachable_regions[player]) for player in
range(1, self.world.players + 1)}
return ret
CollectionState.__init__ = oot_init
CollectionState.copy = oot_copy
# also need to add the names to the passed MultiWorld's CollectionState, since it was initialized before we could get to it
if world:
world.state.child_reachable_regions = {player: set() for player in range(1, world.players + 1)}
world.state.adult_reachable_regions = {player: set() for player in range(1, world.players + 1)}
world.state.child_blocked_connections = {player: set() for player in range(1, world.players + 1)}
world.state.adult_blocked_connections = {player: set() for player in range(1, world.players + 1)}
world.state.day_reachable_regions = {player: set() for player in range(1, world.players + 1)}
world.state.dampe_reachable_regions = {player: set() for player in range(1, world.players + 1)}
world.state.age = {player: None for player in range(1, world.players + 1)}
return super().__new__(cls)
def __init__(self, world, player): def __init__(self, world, player):
self.hint_data_available = threading.Event() self.hint_data_available = threading.Event()
super(OOTWorld, self).__init__(world, player) super(OOTWorld, self).__init__(world, player)
@ -217,7 +199,8 @@ class OOTWorld(World):
self.shopsanity = str(self.shop_slots) self.shopsanity = str(self.shop_slots)
# fixing some options # fixing some options
self.starting_tod = self.starting_tod.replace('_', '-') # Fixes starting time spelling: "witching_hour" -> "witching-hour" # Fixes starting time spelling: "witching_hour" -> "witching-hour"
self.starting_tod = self.starting_tod.replace('_', '-')
self.shuffle_scrubs = self.shuffle_scrubs.replace('_prices', '') self.shuffle_scrubs = self.shuffle_scrubs.replace('_prices', '')
# Get hint distribution # Get hint distribution
@ -429,7 +412,8 @@ class OOTWorld(World):
def create_item(self, name: str): def create_item(self, name: str):
if name in item_table: if name in item_table:
return OOTItem(name, self.player, item_table[name], False, return OOTItem(name, self.player, item_table[name], False,
(name in self.nonadvancement_items if getattr(self, 'nonadvancement_items', None) else False)) (name in self.nonadvancement_items if getattr(self, 'nonadvancement_items',
None) else False))
return OOTItem(name, self.player, ('Event', True, None, None), True, False) return OOTItem(name, self.player, ('Event', True, None, None), True, False)
def make_event_item(self, name, location, item=None): def make_event_item(self, name, location, item=None):
@ -507,7 +491,8 @@ class OOTWorld(World):
shuffle_random_entrances(self) shuffle_random_entrances(self)
except EntranceShuffleError as e: except EntranceShuffleError as e:
tries -= 1 tries -= 1
logging.getLogger('').debug(f"Failed shuffling entrances for world {self.player}, retrying {tries} more times") logger.debug(
f"Failed shuffling entrances for world {self.player}, retrying {tries} more times")
if tries == 0: if tries == 0:
raise e raise e
# Restore original state and delete assumed entrances # Restore original state and delete assumed entrances
@ -586,8 +571,10 @@ class OOTWorld(World):
"Spirit Temple Twinrova Heart", "Spirit Temple Twinrova Heart",
"Song from Impa", "Song from Impa",
"Sheik in Ice Cavern", "Sheik in Ice Cavern",
"Bottom of the Well Lens of Truth Chest", "Bottom of the Well MQ Lens of Truth Chest", # only one exists # only one exists
"Gerudo Training Grounds Maze Path Final Chest", "Gerudo Training Grounds MQ Ice Arrows Chest", # only one exists "Bottom of the Well Lens of Truth Chest", "Bottom of the Well MQ Lens of Truth Chest",
# only one exists
"Gerudo Training Grounds Maze Path Final Chest", "Gerudo Training Grounds MQ Ice Arrows Chest",
] ]
# Place/set rules for dungeon items # Place/set rules for dungeon items
@ -623,7 +610,8 @@ class OOTWorld(World):
# Now fill items that can go into any dungeon. Retrieve the Gerudo Fortress keys from the pool if necessary # Now fill items that can go into any dungeon. Retrieve the Gerudo Fortress keys from the pool if necessary
if self.shuffle_fortresskeys == 'any_dungeon': if self.shuffle_fortresskeys == 'any_dungeon':
fortresskeys = filter(lambda item: item.player == self.player and item.type == 'FortressSmallKey', self.world.itempool) fortresskeys = filter(lambda item: item.player == self.player and item.type == 'FortressSmallKey',
self.world.itempool)
itempools['any_dungeon'].extend(fortresskeys) itempools['any_dungeon'].extend(fortresskeys)
if itempools['any_dungeon']: if itempools['any_dungeon']:
for item in itempools['any_dungeon']: for item in itempools['any_dungeon']:
@ -636,15 +624,18 @@ class OOTWorld(World):
# If anything is overworld-only, fill into local non-dungeon locations # If anything is overworld-only, fill into local non-dungeon locations
if self.shuffle_fortresskeys == 'overworld': if self.shuffle_fortresskeys == 'overworld':
fortresskeys = filter(lambda item: item.player == self.player and item.type == 'FortressSmallKey', self.world.itempool) fortresskeys = filter(lambda item: item.player == self.player and item.type == 'FortressSmallKey',
self.world.itempool)
itempools['overworld'].extend(fortresskeys) itempools['overworld'].extend(fortresskeys)
if itempools['overworld']: if itempools['overworld']:
for item in itempools['overworld']: for item in itempools['overworld']:
self.world.itempool.remove(item) self.world.itempool.remove(item)
itempools['overworld'].sort(key=lambda item: itempools['overworld'].sort(key=lambda item:
{'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'FortressSmallKey': 1}.get(item.type, 0)) {'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'FortressSmallKey': 1}.get(item.type, 0))
non_dungeon_locations = [loc for loc in self.get_locations() if not loc.item and loc not in any_dungeon_locations non_dungeon_locations = [loc for loc in self.get_locations() if
and loc.type != 'Shop' and (loc.type != 'Song' or self.shuffle_song_items != 'song')] not loc.item and loc not in any_dungeon_locations
and loc.type != 'Shop' and (
loc.type != 'Song' or self.shuffle_song_items != 'song')]
self.world.random.shuffle(non_dungeon_locations) self.world.random.shuffle(non_dungeon_locations)
fill_restrictive(self.world, self.world.get_all_state(False), non_dungeon_locations, fill_restrictive(self.world, self.world.get_all_state(False), non_dungeon_locations,
itempools['overworld'], True, True) itempools['overworld'], True, True)
@ -709,7 +700,8 @@ class OOTWorld(World):
# Place shop items # Place shop items
# fast fill will fail because there is some logic on the shop items. we'll gather them up and place the shop items # fast fill will fail because there is some logic on the shop items. we'll gather them up and place the shop items
if self.shopsanity != 'off': if self.shopsanity != 'off':
shop_items = list(filter(lambda item: item.player == self.player and item.type == 'Shop', self.world.itempool)) shop_items = list(
filter(lambda item: item.player == self.player and item.type == 'Shop', self.world.itempool))
shop_locations = list( shop_locations = list(
filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices, filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices,
self.world.get_unfilled_locations(player=self.player))) self.world.get_unfilled_locations(player=self.player)))
@ -717,7 +709,8 @@ class OOTWorld(World):
'Buy Deku Shield': 3 * int(self.open_forest == 'closed'), 'Buy Deku Shield': 3 * int(self.open_forest == 'closed'),
'Buy Goron Tunic': 2, 'Buy Goron Tunic': 2,
'Buy Zora Tunic': 2 'Buy Zora Tunic': 2
}.get(item.name, int(item.advancement))) # place Deku Shields if needed, then tunics, then other advancement, then junk }.get(item.name,
int(item.advancement))) # place Deku Shields if needed, then tunics, then other advancement, then junk
self.world.random.shuffle(shop_locations) self.world.random.shuffle(shop_locations)
for item in shop_items: for item in shop_items:
self.world.itempool.remove(item) self.world.itempool.remove(item)
@ -870,7 +863,8 @@ class OOTWorld(World):
if not world.can_beat_game(state): if not world.can_beat_game(state):
world.worlds[player].required_locations.append(loc) world.worlds[player].required_locations.append(loc)
for player in barren_hint_players: for player in barren_hint_players:
world.worlds[player].empty_areas = {region: info for (region, info) in items_by_region[player].items() if info['is_barren']} world.worlds[player].empty_areas = {region: info for (region, info) in items_by_region[player].items()
if info['is_barren']}
except Exception as e: except Exception as e:
raise e raise e
finally: finally:
@ -900,7 +894,8 @@ class OOTWorld(World):
try: try:
multidata["precollected_items"][self.player].remove(item_id) multidata["precollected_items"][self.player].remove(item_id)
except ValueError as e: except ValueError as e:
logger.warning(f"Attempted to remove nonexistent item id {item_id} from OoT precollected items ({item_name})") logger.warning(
f"Attempted to remove nonexistent item id {item_id} from OoT precollected items ({item_name})")
# Add ER hint data # Add ER hint data
if self.shuffle_interior_entrances != 'off' or self.shuffle_dungeon_entrances or self.shuffle_grotto_entrances: if self.shuffle_interior_entrances != 'off' or self.shuffle_dungeon_entrances or self.shuffle_grotto_entrances:
@ -913,7 +908,6 @@ class OOTWorld(World):
er_hint_data[location.address] = main_entrance.name er_hint_data[location.address] = main_entrance.name
multidata['er_hint_data'][self.player] = er_hint_data multidata['er_hint_data'][self.player] = er_hint_data
# Helper functions # Helper functions
def get_shufflable_entrances(self, type=None, only_primary=False): def get_shufflable_entrances(self, type=None, only_primary=False):
return [entrance for entrance in self.world.get_entrances() if (entrance.player == self.player and return [entrance for entrance in self.world.get_entrances() if (entrance.player == self.player and
@ -921,7 +915,8 @@ class OOTWorld(World):
(not only_primary or entrance.primary))] (not only_primary or entrance.primary))]
def get_shuffled_entrances(self, type=None, only_primary=False): def get_shuffled_entrances(self, type=None, only_primary=False):
return [entrance for entrance in self.get_shufflable_entrances(type=type, only_primary=only_primary) if entrance.shuffled] return [entrance for entrance in self.get_shufflable_entrances(type=type, only_primary=only_primary) if
entrance.shuffled]
def get_locations(self): def get_locations(self):
for region in self.regions: for region in self.regions:

View File

@ -15,7 +15,7 @@ from .Rom import get_base_rom_path, ROM_PLAYER_LIMIT
import Utils import Utils
from BaseClasses import Region, Entrance, Location, MultiWorld, Item, RegionType, CollectionState from BaseClasses import Region, Entrance, Location, MultiWorld, Item, RegionType, CollectionState
from ..AutoWorld import World from ..AutoWorld import World, AutoLogicRegister
import Patch import Patch
from logic.smboolmanager import SMBoolManager from logic.smboolmanager import SMBoolManager
@ -28,6 +28,21 @@ from logic.logic import Logic
from randomizer import VariaRandomizer from randomizer import VariaRandomizer
class SMCollectionState(metaclass=AutoLogicRegister):
def init_mixin(self, parent: MultiWorld):
# for unit tests where MultiWorld is instanciated before worlds
if hasattr(parent, "state"):
self.smbm = {player: SMBoolManager(player, parent.state.smbm[player].maxDiff,
parent.state.smbm[player].onlyBossLeft) for player in
parent.get_game_players("Super Metroid")}
else:
self.smbm = {}
def copy_mixin(self, ret) -> CollectionState:
ret.smbm = {player: copy.deepcopy(self.smbm[player]) for player in self.world.get_game_players("Super Metroid")}
return ret
class SMWorld(World): class SMWorld(World):
game: str = "Super Metroid" game: str = "Super Metroid"
topology_present = True topology_present = True
@ -52,31 +67,6 @@ class SMWorld(World):
self.rom_name_available_event = threading.Event() self.rom_name_available_event = threading.Event()
super().__init__(world, player) super().__init__(world, player)
def __new__(cls, world, player):
# Add necessary objects to CollectionState on initialization
orig_init = CollectionState.__init__
orig_copy = CollectionState.copy
def sm_init(self, parent: MultiWorld):
if (hasattr(parent, "state")): # for unit tests where MultiWorld is instanciated before worlds
self.smbm = {player: SMBoolManager(player, parent.state.smbm[player].maxDiff, parent.state.smbm[player].onlyBossLeft) for player in parent.get_game_players("Super Metroid")}
orig_init(self, parent)
def sm_copy(self):
ret = orig_copy(self)
ret.smbm = {player: copy.deepcopy(self.smbm[player]) for player in self.world.get_game_players("Super Metroid")}
return ret
CollectionState.__init__ = sm_init
CollectionState.copy = sm_copy
if world:
world.state.smbm = {}
return super().__new__(cls)
def generate_early(self): def generate_early(self):
Logic.factory('vanilla') Logic.factory('vanilla')