Core: provide a way to add to CollectionState init and copy
SM: use that way OoT: use that way
This commit is contained in:
parent
c525c80b49
commit
daea0f3e5e
|
@ -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
|
||||
|
|
1
Main.py
1
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)
|
||||
|
||||
|
|
|
@ -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
|
||||
return world
|
||||
|
|
|
@ -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"]],
|
||||
])
|
|
@ -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)
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in New Issue