Merge branch 'main' into player-tracker

This commit is contained in:
Chris Wilson
2022-09-24 18:35:59 -04:00
144 changed files with 4851 additions and 3263 deletions

View File

@@ -129,6 +129,8 @@ def getItemGenericName(item):
def isRestrictedDungeonItem(dungeon, item):
if not isinstance(item, OOTItem):
return False
if (item.map or item.compass) and dungeon.world.shuffle_mapcompass == 'dungeon':
return item in dungeon.dungeon_items
if item.type == 'SmallKey' and dungeon.world.shuffle_smallkeys == 'dungeon':

View File

@@ -1388,6 +1388,10 @@ def get_pool_core(world):
remove_junk_pool = list(remove_junk_pool) + ['Recovery Heart', 'Bombs (20)', 'Arrows (30)', 'Ice Trap']
junk_candidates = [item for item in pool if item in remove_junk_pool]
if len(pending_junk_pool) > len(junk_candidates):
excess = len(pending_junk_pool) - len(junk_candidates)
if world.triforce_hunt:
raise RuntimeError(f"Items in the pool for player {world.player} exceed locations. Add {excess} location(s) or remove {excess} triforce piece(s).")
while pending_junk_pool:
pending_item = pending_junk_pool.pop()
if not junk_candidates:

View File

@@ -22,6 +22,12 @@ def ap_id_to_oot_data(ap_id):
raise Exception(f'Could not find desired item ID: {ap_id}')
def oot_is_item_of_type(item, item_type):
if not isinstance(item, OOTItem):
return False
return item.type == item_type
class OOTItem(Item):
game: str = "Ocarina of Time"
type: str
@@ -43,7 +49,6 @@ class OOTItem(Item):
self.type = type
self.index = index
self.special = special or {}
self.looks_like_item = None
self.price = special.get('price', None) if special else None
self.internal = False

View File

@@ -101,7 +101,6 @@ class InteriorEntrances(Choice):
option_off = 0
option_simple = 1
option_all = 2
alias_false = 0
alias_true = 2
@@ -141,7 +140,6 @@ class MixEntrancePools(Choice):
option_off = 0
option_indoor = 1
option_all = 2
alias_false = 0
class DecoupleEntrances(Toggle):
@@ -158,12 +156,12 @@ class TriforceGoal(Range):
"""Number of Triforce pieces required to complete the game."""
display_name = "Required Triforce Pieces"
range_start = 1
range_end = 100
range_end = 80
default = 20
class ExtraTriforces(Range):
"""Percentage of additional Triforce pieces in the pool, separate from the item pool setting."""
"""Percentage of additional Triforce pieces in the pool. With high numbers, you may need to randomize additional locations to have enough items."""
display_name = "Percentage of Extra Triforce Pieces"
range_start = 0
range_end = 100
@@ -308,7 +306,6 @@ class ShopShuffle(Choice):
option_off = 0
option_fixed_number = 1
option_random_number = 2
alias_false = 0
class ShopSlots(Range):
@@ -326,7 +323,6 @@ class TokenShuffle(Choice):
option_dungeons = 1
option_overworld = 2
option_all = 3
alias_false = 0
class ScrubShuffle(Choice):
@@ -336,7 +332,6 @@ class ScrubShuffle(Choice):
option_low = 1
option_regular = 2
option_random_prices = 3
alias_false = 0
alias_affordable = 1
alias_expensive = 2
@@ -569,7 +564,6 @@ class Hints(Choice):
option_agony = 2
option_always = 3
default = 3
alias_false = 0
class MiscHints(DefaultOnToggle):
@@ -673,8 +667,6 @@ class IceTraps(Choice):
option_mayhem = 3
option_onslaught = 4
default = 1
alias_false = 0
alias_true = 2
alias_extra = 2
@@ -742,7 +734,6 @@ class Music(Choice):
option_normal = 0
option_off = 1
option_randomized = 2
alias_false = 1
class BackgroundMusic(Music):

View File

@@ -1844,7 +1844,7 @@ def write_rom_item(rom, item_id, item):
def get_override_table(world):
return list(filter(lambda val: val != None, map(partial(get_override_entry, world.player), world.world.get_filled_locations(world.player))))
return list(filter(lambda val: val != None, map(partial(get_override_entry, world), world.world.get_filled_locations(world.player))))
override_struct = struct.Struct('>xBBBHBB') # match override_t in get_items.c
@@ -1852,10 +1852,10 @@ def get_override_table_bytes(override_table):
return b''.join(sorted(itertools.starmap(override_struct.pack, override_table)))
def get_override_entry(player_id, location):
def get_override_entry(ootworld, location):
scene = location.scene
default = location.default
player_id = 0 if player_id == location.item.player else min(location.item.player, 255)
player_id = 0 if ootworld.player == location.item.player else min(location.item.player, 255)
if location.item.game != 'Ocarina of Time':
# This is an AP sendable. It's guaranteed to not be None.
if location.item.advancement:
@@ -1869,7 +1869,7 @@ def get_override_entry(player_id, location):
if location.item.trap:
item_id = 0x7C # Ice Trap ID, to get "X is a fool" message
looks_like_item_id = location.item.looks_like_item.index
looks_like_item_id = ootworld.trap_appearances[location.address].index
else:
looks_like_item_id = 0
@@ -2091,7 +2091,8 @@ def get_locked_doors(rom, world):
return [0x00D4 + scene * 0x1C + 0x04 + flag_byte, flag_bits]
# If boss door, set the door's unlock flag
if (world.shuffle_bosskeys == 'remove' and scene != 0x0A) or (world.shuffle_ganon_bosskey == 'remove' and scene == 0x0A):
if (world.shuffle_bosskeys == 'remove' and scene != 0x0A) or (
world.shuffle_ganon_bosskey == 'remove' and scene == 0x0A and not world.triforce_hunt):
if actor_id == 0x002E and actor_type == 0x05:
return [0x00D4 + scene * 0x1C + 0x04 + flag_byte, flag_bits]
@@ -2109,23 +2110,20 @@ def place_shop_items(rom, world, shop_items, messages, locations, init_shop_id=F
rom.write_int16(location.address1, location.item.index)
else:
if location.item.trap:
item_display = location.item.looks_like_item
elif location.item.game != "Ocarina of Time":
item_display = location.item
if location.item.advancement:
item_display.index = 0xCB
else:
item_display.index = 0xCC
item_display.special = {}
item_display = world.trap_appearances[location.address]
else:
item_display = location.item
# bottles in shops should look like empty bottles
# so that that are different than normal shop refils
if 'shop_object' in item_display.special:
rom_item = read_rom_item(rom, item_display.special['shop_object'])
if location.item.trap or location.item.game == "Ocarina of Time":
if 'shop_object' in item_display.special:
rom_item = read_rom_item(rom, item_display.special['shop_object'])
else:
rom_item = read_rom_item(rom, item_display.index)
else:
rom_item = read_rom_item(rom, item_display.index)
display_index = 0xCB if location.item.advancement else 0xCC
rom_item = read_rom_item(rom, display_index)
shop_objs.add(rom_item['object_id'])
shop_id = world.current_shop_id

View File

@@ -3,6 +3,7 @@ import logging
from .SaveContext import SaveContext
from .Regions import TimeOfDay
from .Items import oot_is_item_of_type
from BaseClasses import CollectionState
from worlds.generic.Rules import set_rule, add_rule, add_item_rule, forbid_item
@@ -138,7 +139,7 @@ def set_rules(ootworld):
# Sheik in Ice Cavern is the only song location in a dungeon; need to ensure that it cannot be anything else.
# This is required if map/compass included, or any_dungeon shuffle.
location = world.get_location('Sheik in Ice Cavern', player)
add_item_rule(location, lambda item: item.player == player and item.type == 'Song')
add_item_rule(location, lambda item: item.player == player and oot_is_item_of_type(item, 'Song'))
if ootworld.skip_child_zelda:
# If skip child zelda is on, the item at Song from Impa must be giveable by the save context.
@@ -181,7 +182,7 @@ def set_shop_rules(ootworld):
wallet = ootworld.parser.parse_rule('Progressive_Wallet')
wallet2 = ootworld.parser.parse_rule('(Progressive_Wallet, 2)')
for location in filter(lambda location: location.item and location.item.type == 'Shop', ootworld.get_locations()):
for location in filter(lambda location: location.item and oot_is_item_of_type(location.item, 'Shop'), ootworld.get_locations()):
# Add wallet requirements
if location.item.name in ['Buy Arrows (50)', 'Buy Fish', 'Buy Goron Tunic', 'Buy Bombchu (20)', 'Buy Bombs (30)']:
add_rule(location, wallet)

View File

@@ -8,7 +8,7 @@ logger = logging.getLogger("Ocarina of Time")
from .Location import OOTLocation, LocationFactory, location_name_to_id
from .Entrance import OOTEntrance
from .EntranceShuffle import shuffle_random_entrances, entrance_shuffle_table, EntranceShuffleError
from .Items import OOTItem, item_table, oot_data_to_ap_id
from .Items import OOTItem, item_table, oot_data_to_ap_id, oot_is_item_of_type
from .ItemPool import generate_itempool, add_dungeon_items, get_junk_item, get_junk_pool, normal_bottles
from .Regions import OOTRegion, TimeOfDay
from .Rules import set_rules, set_shop_rules, set_entrances_based_rules
@@ -305,6 +305,10 @@ class OOTWorld(World):
if self.skip_child_zelda:
self.shuffle_weird_egg = False
# Ganon boss key should not be in itempool in triforce hunt
if self.triforce_hunt:
self.shuffle_ganon_bosskey = 'remove'
# Determine skipped trials in GT
# This needs to be done before the logic rules in GT are parsed
trial_list = ['Forest', 'Fire', 'Water', 'Spirit', 'Shadow', 'Light']
@@ -313,7 +317,11 @@ class OOTWorld(World):
# Determine which dungeons are MQ
# Possible future plan: allow user to pick which dungeons are MQ
mq_dungeons = self.world.random.sample(dungeon_table, self.mq_dungeons)
if self.logic_rules == 'glitchless':
mq_dungeons = self.world.random.sample(dungeon_table, self.mq_dungeons)
else:
self.mq_dungeons = 0
mq_dungeons = []
self.dungeon_mq = {item['name']: (item in mq_dungeons) for item in dungeon_table}
# Determine tricks in logic
@@ -920,7 +928,7 @@ class OOTWorld(World):
# This includes all locations for which show_in_spoiler is false, and shuffled shop items.
for loc in self.get_locations():
if loc.address is not None and (
not loc.show_in_spoiler or (loc.item is not None and loc.item.type == 'Shop')
not loc.show_in_spoiler or oot_is_item_of_type(loc.item, 'Shop')
or (self.skip_child_zelda and loc.name in ['HC Zeldas Letter', 'Song from Impa'])):
loc.address = None
@@ -930,9 +938,10 @@ class OOTWorld(World):
with i_o_limiter:
# Make traps appear as other random items
ice_traps = [loc.item for loc in self.get_locations() if loc.item.trap]
for trap in ice_traps:
trap.looks_like_item = self.create_item(self.world.slot_seeds[self.player].choice(self.fake_items).name)
trap_location_ids = [loc.address for loc in self.get_locations() if loc.item.trap]
self.trap_appearances = {}
for loc_id in trap_location_ids:
self.trap_appearances[loc_id] = self.create_item(self.world.slot_seeds[self.player].choice(self.fake_items).name)
# Seed hint RNG, used for ganon text lines also
self.hint_rng = self.world.slot_seeds[self.player]
@@ -996,11 +1005,11 @@ class OOTWorld(World):
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
(loc.item.type == 'SmallKey' and world.worlds[loc.player].shuffle_smallkeys == 'any_dungeon') or
(loc.item.type == 'HideoutSmallKey' and world.worlds[loc.player].shuffle_fortresskeys == 'any_dungeon') or
(loc.item.type == 'BossKey' and world.worlds[loc.player].shuffle_bosskeys == 'any_dungeon') or
(loc.item.type == 'GanonBossKey' and world.worlds[loc.player].shuffle_ganon_bosskey == 'any_dungeon'))):
(oot_is_item_of_type(loc.item, 'Song') or
(oot_is_item_of_type(loc.item, 'SmallKey') and world.worlds[loc.player].shuffle_smallkeys == 'any_dungeon') or
(oot_is_item_of_type(loc.item, 'HideoutSmallKey') and world.worlds[loc.player].shuffle_fortresskeys == 'any_dungeon') or
(oot_is_item_of_type(loc.item, 'BossKey') and world.worlds[loc.player].shuffle_bosskeys == 'any_dungeon') or
(oot_is_item_of_type(loc.item, 'GanonBossKey') and world.worlds[loc.player].shuffle_ganon_bosskey == 'any_dungeon'))):
if loc.player in barren_hint_players:
hint_area = get_hint_area(loc)
items_by_region[loc.player][hint_area]['weight'] += 1
@@ -1015,7 +1024,7 @@ class OOTWorld(World):
elif barren_hint_players or woth_hint_players: # Check only relevant oot locations for barren/woth
for player in (barren_hint_players | woth_hint_players):
for loc in world.worlds[player].get_locations():
if loc.item.code and (not loc.locked or loc.item.type == 'Song'):
if loc.item.code and (not loc.locked or oot_is_item_of_type(loc.item, 'Song')):
if player in barren_hint_players:
hint_area = get_hint_area(loc)
items_by_region[player][hint_area]['weight'] += 1