mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 20:21:32 -06:00
Core: provide a way to add to CollectionState init and copy
SM: use that way OoT: use that way
This commit is contained in:
@@ -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:
|
||||
|
Reference in New Issue
Block a user