diff --git a/BaseClasses.py b/BaseClasses.py index d9b3bf90..373af525 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -6,7 +6,7 @@ import logging import json import functools 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 random @@ -16,6 +16,7 @@ import NetUtils if TYPE_CHECKING: from worlds import AutoWorld + auto_world = AutoWorld.World else: auto_world = object @@ -193,17 +194,20 @@ class MultiWorld(): def set_options(self, args): 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: self.custom_data[player] = {} world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]] for option_key in world_type.options: 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) + def set_item_links(self): item_links = {} 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}) 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}) + self.state = CollectionState(self) def secure(self): self.random = secrets.SystemRandom() @@ -551,7 +556,9 @@ class MultiWorld(): return False -class CollectionState(object): +class CollectionState(): + additional_init_functions: List[Callable] = [] + additional_copy_functions: List[Callable] = [] def __init__(self, parent: MultiWorld): self.prog_items = Counter() @@ -565,6 +572,8 @@ class CollectionState(object): for items in parent.precollected_items.values(): for item in items: self.collect(item, True) + for function in self.additional_init_functions: + function(self, parent) def update_reachable_regions(self, player: int): from worlds.alttp.EntranceShuffle import indirect_connections @@ -609,6 +618,8 @@ class CollectionState(object): ret.events = copy.copy(self.events) ret.path = copy.copy(self.path) ret.locations_checked = copy.copy(self.locations_checked) + for function in self.additional_copy_functions: + ret = function(self, ret) return ret def can_reach(self, spot, resolution_hint=None, player=None) -> bool: @@ -921,7 +932,7 @@ class Entrance(object): return False - def connect(self, region: Region, addresses=None, target = None): + def connect(self, region: Region, addresses=None, target=None): self.connected_region = region self.target = target self.addresses = addresses diff --git a/Main.py b/Main.py index 42b72026..75b737ae 100644 --- a/Main.py +++ b/Main.py @@ -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.set_options(args) + world.set_item_links() world.state = CollectionState(world) logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed) diff --git a/test/general/__init__.py b/test/general/__init__.py index b95399f8..c59af4d4 100644 --- a/test/general/__init__.py +++ b/test/general/__init__.py @@ -1,6 +1,6 @@ from argparse import Namespace -from BaseClasses import MultiWorld +from BaseClasses import MultiWorld, CollectionState from worlds.AutoWorld import call_all gen_steps = ["generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill"] @@ -18,4 +18,4 @@ def setup_default_world(world_type): world.set_default_common_options() for step in gen_steps: call_all(world, step) - return world \ No newline at end of file + return world diff --git a/test/hollow_knight/TestBasic.py b/test/hollow_knight/TestBasic.py deleted file mode 100644 index 65c4dc8c..00000000 --- a/test/hollow_knight/TestBasic.py +++ /dev/null @@ -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"]], - ]) \ No newline at end of file diff --git a/test/hollow_knight/__init__.py b/test/hollow_knight/__init__.py deleted file mode 100644 index 60e98cbc..00000000 --- a/test/hollow_knight/__init__.py +++ /dev/null @@ -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) \ No newline at end of file diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index bd5861c5..c906c97c 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -1,7 +1,7 @@ from __future__ import annotations 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 Options import Option @@ -35,8 +35,13 @@ class AutoWorldRegister(type): class AutoLogicRegister(type): def __new__(cls, name, bases, dct): new_class = super().__new__(cls, name, bases, dct) + function: Callable 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): raise Exception(f"Name conflict on Logic Mixin {name} trying to overwrite {item_name}") setattr(CollectionState, item_name, function) @@ -193,6 +198,7 @@ class World(metaclass=AutoWorldRegister): def write_spoiler_end(self, spoiler_handle: TextIO): """Write to the end of the spoiler""" pass + # end of ordered Main.py calls 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())) - # any methods attached to this can be used as part of CollectionState, # please use a prefix as all of them get clobbered together class LogicMixin(metaclass=AutoLogicRegister): diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index c210ea4d..727413d2 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -31,7 +31,7 @@ from BaseClasses import MultiWorld, CollectionState, RegionType from Options import Range, Toggle, OptionList from Fill import fill_restrictive, FillError from worlds.generic.Rules import exclusion_rules -from ..AutoWorld import World +from ..AutoWorld import World, AutoLogicRegister location_id_offset = 67000 @@ -39,6 +39,33 @@ location_id_offset = 67000 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): """ The Legend of Zelda: Ocarina of Time is a 3D action/adventure game. Travel through Hyrule in two time periods, @@ -56,55 +83,10 @@ class OOTWorld(World): 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): self.hint_data_available = threading.Event() super(OOTWorld, self).__init__(world, player) - + def generate_early(self): # Player name MUST be at most 16 bytes ascii-encoded, otherwise won't write to ROM correctly if len(bytes(self.world.get_player_name(self.player), 'ascii')) > 16: @@ -217,7 +199,8 @@ class OOTWorld(World): self.shopsanity = str(self.shop_slots) # 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', '') # Get hint distribution @@ -258,14 +241,14 @@ class OOTWorld(World): # Determine items which are not considered advancement based on settings. They will never be excluded. self.nonadvancement_items = {'Double Defense', 'Ice Arrows'} - if (self.damage_multiplier != 'ohko' and self.damage_multiplier != 'quadruple' and + if (self.damage_multiplier != 'ohko' and self.damage_multiplier != 'quadruple' and self.shuffle_scrubs == 'off' and not self.shuffle_grotto_entrances): # nayru's love may be required to prevent forced damage self.nonadvancement_items.add('Nayrus Love') if getattr(self, 'logic_grottos_without_agony', False) and self.hints != 'agony': # Stone of Agony skippable if not used for hints or grottos self.nonadvancement_items.add('Stone of Agony') - if (not self.shuffle_special_interior_entrances and not self.shuffle_overworld_entrances and + if (not self.shuffle_special_interior_entrances and not self.shuffle_overworld_entrances and not self.warp_songs and not self.spawn_positions): # Serenade and Prelude are never required unless one of those settings is enabled self.nonadvancement_items.add('Serenade of Water') @@ -277,7 +260,7 @@ class OOTWorld(World): if not getattr(self, 'logic_water_central_gs_fw', False): # Farore's Wind skippable if not used for this logic trick in Water Temple self.nonadvancement_items.add('Farores Wind') - + def load_regions_from_json(self, file_path): region_json = read_json(file_path) @@ -428,8 +411,9 @@ class OOTWorld(World): def create_item(self, name: str): if name in item_table: - return OOTItem(name, self.player, item_table[name], False, - (name in self.nonadvancement_items if getattr(self, 'nonadvancement_items', None) else False)) + return OOTItem(name, self.player, item_table[name], 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) def make_event_item(self, name, location, item=None): @@ -507,7 +491,8 @@ class OOTWorld(World): shuffle_random_entrances(self) except EntranceShuffleError as e: 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: raise e # Restore original state and delete assumed entrances @@ -586,8 +571,10 @@ class OOTWorld(World): "Spirit Temple Twinrova Heart", "Song from Impa", "Sheik in Ice Cavern", - "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", # only one exists + # 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 @@ -612,7 +599,7 @@ class OOTWorld(World): # We can't put a dungeon item on the end of a dungeon if a song is supposed to go there. Make sure not to include it. dungeon_locations = [loc for region in dungeon.regions for loc in region.locations if loc.item is None and ( - self.shuffle_song_items != 'dungeon' or loc.name not in dungeon_song_locations)] + self.shuffle_song_items != 'dungeon' or loc.name not in dungeon_song_locations)] if itempools['dungeon']: # only do this if there's anything to shuffle for item in itempools['dungeon']: self.world.itempool.remove(item) @@ -623,28 +610,32 @@ class OOTWorld(World): # 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': - 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) if itempools['any_dungeon']: for item in itempools['any_dungeon']: self.world.itempool.remove(item) - itempools['any_dungeon'].sort(key=lambda item: - {'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'FortressSmallKey': 1}.get(item.type, 0)) + itempools['any_dungeon'].sort(key=lambda item: + {'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'FortressSmallKey': 1}.get(item.type, 0)) self.world.random.shuffle(any_dungeon_locations) fill_restrictive(self.world, self.world.get_all_state(False), any_dungeon_locations, itempools['any_dungeon'], True, True) # If anything is overworld-only, fill into local non-dungeon locations 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) if itempools['overworld']: for item in itempools['overworld']: self.world.itempool.remove(item) - itempools['overworld'].sort(key=lambda item: - {'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 - and loc.type != 'Shop' and (loc.type != 'Song' or self.shuffle_song_items != 'song')] + itempools['overworld'].sort(key=lambda item: + {'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 + and loc.type != 'Shop' and ( + loc.type != 'Song' or self.shuffle_song_items != 'song')] self.world.random.shuffle(non_dungeon_locations) fill_restrictive(self.world, self.world.get_all_state(False), non_dungeon_locations, itempools['overworld'], True, True) @@ -666,8 +657,8 @@ class OOTWorld(World): for song in songs: self.world.itempool.remove(song) - important_warps = (self.shuffle_special_interior_entrances or self.shuffle_overworld_entrances or - self.warp_songs or self.spawn_positions) + important_warps = (self.shuffle_special_interior_entrances or self.shuffle_overworld_entrances or + self.warp_songs or self.spawn_positions) song_order = { 'Zeldas Lullaby': 1, 'Eponas Song': 1, @@ -709,15 +700,17 @@ class OOTWorld(World): # 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 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( filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices, self.world.get_unfilled_locations(player=self.player))) shop_items.sort(key=lambda item: { - 'Buy Deku Shield': 3*int(self.open_forest == 'closed'), - 'Buy Goron Tunic': 2, + 'Buy Deku Shield': 3 * int(self.open_forest == 'closed'), + 'Buy Goron 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) for item in shop_items: self.world.itempool.remove(item) @@ -812,13 +805,13 @@ class OOTWorld(World): @classmethod def stage_generate_output(cls, world: MultiWorld, output_directory: str): def hint_type_players(hint_type: str) -> set: - return {autoworld.player for autoworld in world.get_game_worlds("Ocarina of Time") + return {autoworld.player for autoworld in world.get_game_worlds("Ocarina of Time") if autoworld.hints != 'none' and autoworld.hint_dist_user['distribution'][hint_type]['copies'] > 0} try: - item_hint_players = hint_type_players('item') + item_hint_players = hint_type_players('item') barren_hint_players = hint_type_players('barren') - woth_hint_players = hint_type_players('woth') + woth_hint_players = hint_type_players('woth') items_by_region = {} for player in barren_hint_players: @@ -834,12 +827,12 @@ class OOTWorld(World): for loc in world.get_locations(): player = loc.item.player autoworld = world.worlds[player] - if ((player in item_hint_players and (autoworld.is_major_item(loc.item) or loc.item.name in autoworld.item_added_hint_types['item'])) + if ((player in item_hint_players and (autoworld.is_major_item(loc.item) or loc.item.name in autoworld.item_added_hint_types['item'])) or (loc.player in item_hint_players and loc.name in world.worlds[loc.player].added_hint_types['item'])): autoworld.major_item_locations.append(loc) - if loc.game == "Ocarina of Time" and loc.item.code and (not loc.locked or - (loc.item.type == 'Song' or + if loc.game == "Ocarina of Time" and loc.item.code and (not loc.locked or + (loc.item.type == 'Song' or (loc.item.type == 'SmallKey' and world.worlds[loc.player].shuffle_smallkeys == 'any_dungeon') or (loc.item.type == 'FortressSmallKey' and world.worlds[loc.player].shuffle_fortresskeys == 'any_dungeon') or (loc.item.type == 'BossKey' and world.worlds[loc.player].shuffle_bosskeys == 'any_dungeon') or @@ -870,7 +863,8 @@ class OOTWorld(World): if not world.can_beat_game(state): world.worlds[player].required_locations.append(loc) 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: raise e finally: @@ -886,7 +880,7 @@ class OOTWorld(World): hint_entrances.add(entrance[2][0]) def get_entrance_to_region(region): - if region.name == 'Root': + if region.name == 'Root': return None for entrance in region.entrances: if entrance.name in hint_entrances: @@ -900,7 +894,8 @@ class OOTWorld(World): 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 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 if self.shuffle_interior_entrances != 'off' or self.shuffle_dungeon_entrances or self.shuffle_grotto_entrances: @@ -913,15 +908,15 @@ class OOTWorld(World): er_hint_data[location.address] = main_entrance.name multidata['er_hint_data'][self.player] = er_hint_data - # Helper functions 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 - (type == None or entrance.type == type) and - (not only_primary or entrance.primary))] + return [entrance for entrance in self.world.get_entrances() if (entrance.player == self.player and + (type == None or entrance.type == type) and + (not only_primary or entrance.primary))] 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): for region in self.regions: diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index df49f6ad..d829a6d6 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -15,7 +15,7 @@ from .Rom import get_base_rom_path, ROM_PLAYER_LIMIT import Utils from BaseClasses import Region, Entrance, Location, MultiWorld, Item, RegionType, CollectionState -from ..AutoWorld import World +from ..AutoWorld import World, AutoLogicRegister import Patch from logic.smboolmanager import SMBoolManager @@ -28,6 +28,21 @@ from logic.logic import Logic 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): game: str = "Super Metroid" topology_present = True @@ -51,31 +66,6 @@ class SMWorld(World): def __init__(self, world: MultiWorld, player: int): self.rom_name_available_event = threading.Event() 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): Logic.factory('vanilla')