Ocarina of Time: long-awaited bugfixes (#2344)

- Added location name groups, so you can make your entire Water Temple priority to annoy everyone else
- Significant improvement to ER generation success rate (~80% to >99%)
- Changed `adult_trade_start` option to a choice option instead of a list (this shouldn't actually break any YAMLs though, due to the lesser-known property of lists parsing as a uniformly-weighted choice)
- Major improvements to the option tooltips where needed. (Possibly too much text now)
- Changed default hint distribution to `async` to help people's generation times. The tooltip explains that it removes WOTH hints so people hopefully don't get tripped up.
- Makes stick and nut capacity upgrades useful items
- Added shop prices and required trials to spoiler log
- Added Cojiro to adult trade item group, because it had been forgotten previously
- Fixed size-modified chests not being moved properly due to trap appearance changing the size
- Fixed Thieves Hideout keyring not being allowed in start inventory
- Fixed hint generation not accurately flagging barren locations on certain dungeon item shuffle settings
- Fixed bug where you could plando arbitrarily-named items into the world, breaking everything
This commit is contained in:
espeon65536 2023-10-22 10:38:47 -06:00 committed by GitHub
parent 50244342d9
commit 724999fc43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 424 additions and 252 deletions

View File

@ -350,7 +350,7 @@ def generate_itempool(ootworld):
ootworld.itempool = [ootworld.create_item(item) for item in pool] ootworld.itempool = [ootworld.create_item(item) for item in pool]
for (location_name, item) in placed_items.items(): for (location_name, item) in placed_items.items():
location = world.get_location(location_name, player) location = world.get_location(location_name, player)
location.place_locked_item(ootworld.create_item(item)) location.place_locked_item(ootworld.create_item(item, allow_arbitrary_name=True))
def get_pool_core(world): def get_pool_core(world):
@ -675,7 +675,7 @@ def get_pool_core(world):
world.remove_from_start_inventory.append('Scarecrow Song') world.remove_from_start_inventory.append('Scarecrow Song')
if world.no_epona_race: if world.no_epona_race:
world.multiworld.push_precollected(world.create_item('Epona')) world.multiworld.push_precollected(world.create_item('Epona', allow_arbitrary_name=True))
world.remove_from_start_inventory.append('Epona') world.remove_from_start_inventory.append('Epona')
if world.shuffle_smallkeys == 'vanilla': if world.shuffle_smallkeys == 'vanilla':

View File

@ -2,6 +2,8 @@ from enum import Enum
from .LocationList import location_table from .LocationList import location_table
from BaseClasses import Location from BaseClasses import Location
non_indexed_location_types = {'Boss', 'Event', 'Drop', 'HintStone', 'Hint'}
location_id_offset = 67000 location_id_offset = 67000
locnames_pre_70 = { locnames_pre_70 = {
"Gift from Sages", "Gift from Sages",
@ -18,7 +20,7 @@ new_name_order = sorted(location_table.keys(),
else 0) else 0)
location_name_to_id = {name: (location_id_offset + index) for (index, name) in enumerate(new_name_order) location_name_to_id = {name: (location_id_offset + index) for (index, name) in enumerate(new_name_order)
if location_table[name][0] not in {'Boss', 'Event', 'Drop', 'HintStone', 'Hint'}} if location_table[name][0] not in non_indexed_location_types}
class DisableType(Enum): class DisableType(Enum):
ENABLED = 0 ENABLED = 0
@ -83,3 +85,57 @@ def LocationFactory(locations, player: int):
return ret return ret
def build_location_name_groups() -> dict:
def fix_sing(t) -> tuple:
if isinstance(t, str):
return (t,)
return t
def rename(d, k1, k2) -> None:
d[k2] = d[k1]
del d[k1]
# whoever wrote the location table didn't realize they need to add a comma to mark a singleton as a tuple
# so we have to check types unfortunately
tags = set()
for v in location_table.values():
if v[5] is not None:
tags.update(fix_sing(v[5]))
sorted_tags = sorted(list(tags))
ret = {
tag: {k for k, v in location_table.items()
if v[5] is not None
and tag in fix_sing(v[5])
and v[0] not in non_indexed_location_types}
for tag in sorted_tags
}
# Delete tags which are a combination of other tags
del ret['Death Mountain']
del ret['Forest']
del ret['Gerudo']
del ret['Kakariko']
del ret['Market']
# Delete Vanilla and MQ tags because they are just way too broad
del ret['Vanilla']
del ret['Master Quest']
rename(ret, 'Beehive', 'Beehives')
rename(ret, 'Cow', 'Cows')
rename(ret, 'Crate', 'Crates')
rename(ret, 'Deku Scrub', 'Deku Scrubs')
rename(ret, 'FlyingPot', 'Flying Pots')
rename(ret, 'Freestanding', 'Freestanding Items')
rename(ret, 'Pot', 'Pots')
rename(ret, 'RupeeTower', 'Rupee Groups')
rename(ret, 'SmallCrate', 'Small Crates')
rename(ret, 'the Market', 'Market')
rename(ret, 'the Graveyard', 'Graveyard')
rename(ret, 'the Lost Woods', 'Lost Woods')
return ret

View File

@ -238,7 +238,7 @@ location_table = OrderedDict([
("Market Night Green Rupee Crate 1", ("Crate", 0x21, (0,0,24), None, 'Rupee (1)', ("the Market", "Market", "Crate"))), ("Market Night Green Rupee Crate 1", ("Crate", 0x21, (0,0,24), None, 'Rupee (1)', ("the Market", "Market", "Crate"))),
("Market Night Green Rupee Crate 2", ("Crate", 0x21, (0,0,25), None, 'Rupee (1)', ("the Market", "Market", "Crate"))), ("Market Night Green Rupee Crate 2", ("Crate", 0x21, (0,0,25), None, 'Rupee (1)', ("the Market", "Market", "Crate"))),
("Market Night Green Rupee Crate 3", ("Crate", 0x21, (0,0,26), None, 'Rupee (1)', ("the Market", "Market", "Crate"))), ("Market Night Green Rupee Crate 3", ("Crate", 0x21, (0,0,26), None, 'Rupee (1)', ("the Market", "Market", "Crate"))),
("Market Dog Lady House Crate", ("Crate", 0x35, (0,0,3), None, 'Rupees (5)', ("Market", "Market", "Crate"))), ("Market Dog Lady House Crate", ("Crate", 0x35, (0,0,3), None, 'Rupees (5)', ("the Market", "Market", "Crate"))),
("Market Guard House Child Crate", ("Crate", 0x4D, (0,0,6), None, 'Rupee (1)', ("the Market", "Market", "Crate"))), ("Market Guard House Child Crate", ("Crate", 0x4D, (0,0,6), None, 'Rupee (1)', ("the Market", "Market", "Crate"))),
("Market Guard House Child Pot 1", ("Pot", 0x4D, (0,0,9), None, 'Rupee (1)', ("the Market", "Market", "Pot"))), ("Market Guard House Child Pot 1", ("Pot", 0x4D, (0,0,9), None, 'Rupee (1)', ("the Market", "Market", "Pot"))),
("Market Guard House Child Pot 2", ("Pot", 0x4D, (0,0,10), None, 'Rupee (1)', ("the Market", "Market", "Pot"))), ("Market Guard House Child Pot 2", ("Pot", 0x4D, (0,0,10), None, 'Rupee (1)', ("the Market", "Market", "Pot"))),

View File

@ -30,7 +30,17 @@ class TrackRandomRange(Range):
class Logic(Choice): class Logic(Choice):
"""Set the logic used for the generator.""" """Set the logic used for the generator.
Glitchless: Normal gameplay. Can enable more difficult logical paths using the Logic Tricks option.
Glitched: Many powerful glitches expected, such as bomb hovering and clipping.
Glitched is incompatible with the following settings:
- All forms of entrance randomizer
- MQ dungeons
- Pot shuffle
- Freestanding item shuffle
- Crate shuffle
- Beehive shuffle
No Logic: No logic is used when placing items. Not recommended for most players."""
display_name = "Logic Rules" display_name = "Logic Rules"
option_glitchless = 0 option_glitchless = 0
option_glitched = 1 option_glitched = 1
@ -38,12 +48,16 @@ class Logic(Choice):
class NightTokens(Toggle): class NightTokens(Toggle):
"""Nighttime skulltulas will logically require Sun's Song.""" """When enabled, nighttime skulltulas logically require Sun's Song."""
display_name = "Nighttime Skulltulas Expect Sun's Song" display_name = "Nighttime Skulltulas Expect Sun's Song"
class Forest(Choice): class Forest(Choice):
"""Set the state of Kokiri Forest and the path to Deku Tree.""" """Set the state of Kokiri Forest and the path to Deku Tree.
Open: Neither the forest exit nor the path to Deku Tree is blocked.
Closed Deku: The forest exit is not blocked; the path to Deku Tree requires Kokiri Sword and Deku Shield.
Closed: Path to Deku Tree requires sword and shield. The forest exit is blocked until Deku Tree is beaten.
Closed forest will force child start, and becomes Closed Deku if interior entrances, overworld entrances, warp songs, or random spawn positions are enabled."""
display_name = "Forest" display_name = "Forest"
option_open = 0 option_open = 0
option_closed_deku = 1 option_closed_deku = 1
@ -53,7 +67,10 @@ class Forest(Choice):
class Gate(Choice): class Gate(Choice):
"""Set the state of the Kakariko Village gate.""" """Set the state of the Kakariko Village gate for child. The gate is always open as adult.
Open: The gate starts open. Happy Mask Shop opens upon receiving Zelda's Letter.
Zelda: The gate and Mask Shop open upon receiving Zelda's Letter, without needing to show it to the guard.
Closed: Vanilla behavior; the gate and Mask Shop open upon showing Zelda's Letter to the gate guard."""
display_name = "Kakariko Gate" display_name = "Kakariko Gate"
option_open = 0 option_open = 0
option_zelda = 1 option_zelda = 1
@ -61,12 +78,15 @@ class Gate(Choice):
class DoorOfTime(DefaultOnToggle): class DoorOfTime(DefaultOnToggle):
"""Open the Door of Time by default, without the Song of Time.""" """When enabled, the Door of Time starts opened, without needing Song of Time."""
display_name = "Open Door of Time" display_name = "Open Door of Time"
class Fountain(Choice): class Fountain(Choice):
"""Set the state of King Zora, blocking the way to Zora's Fountain.""" """Set the state of King Zora, blocking the way to Zora's Fountain.
Open: King Zora starts moved as both ages. Ruto's Letter is removed.
Adult: King Zora must be moved as child, but is always moved for adult.
Closed: Vanilla behavior; King Zora must be shown Ruto's Letter as child to move him as both ages."""
display_name = "Zora's Fountain" display_name = "Zora's Fountain"
option_open = 0 option_open = 0
option_adult = 1 option_adult = 1
@ -75,7 +95,10 @@ class Fountain(Choice):
class Fortress(Choice): class Fortress(Choice):
"""Set the requirements for access to Gerudo Fortress.""" """Set the requirements for access to Gerudo Fortress.
Normal: Vanilla behavior; all four carpenters must be rescued.
Fast: Only one carpenter must be rescued, which is the one in the bottom-left of the fortress.
Open: The Gerudo Valley bridge starts repaired. Gerudo Membership Card is given to start if not shuffled."""
display_name = "Gerudo Fortress" display_name = "Gerudo Fortress"
option_normal = 0 option_normal = 0
option_fast = 1 option_fast = 1
@ -84,7 +107,14 @@ class Fortress(Choice):
class Bridge(Choice): class Bridge(Choice):
"""Set the requirements for the Rainbow Bridge.""" """Set the requirements for the Rainbow Bridge.
Open: The bridge is always present.
Vanilla: Bridge requires Shadow Medallion, Spirit Medallion, and Light Arrows.
Stones: Bridge requires a configurable amount of Spiritual Stones.
Medallions: Bridge requires a configurable amount of medallions.
Dungeons: Bridge requires a configurable amount of rewards (stones + medallions).
Tokens: Bridge requires a configurable amount of gold skulltula tokens.
Hearts: Bridge requires a configurable amount of hearts."""
display_name = "Rainbow Bridge Requirement" display_name = "Rainbow Bridge Requirement"
option_open = 0 option_open = 0
option_vanilla = 1 option_vanilla = 1
@ -122,8 +152,9 @@ class StartingAge(Choice):
class InteriorEntrances(Choice): class InteriorEntrances(Choice):
"""Shuffles interior entrances. "Simple" shuffles houses and Great Fairies; "All" includes Windmill, Link's House, """Shuffles interior entrances.
Temple of Time, and Kak potion shop.""" Simple: Houses and Great Fairies are shuffled.
All: In addition to Simple, includes Windmill, Link's House, Temple of Time, and the Kakariko potion shop."""
display_name = "Shuffle Interior Entrances" display_name = "Shuffle Interior Entrances"
option_off = 0 option_off = 0
option_simple = 1 option_simple = 1
@ -137,7 +168,9 @@ class GrottoEntrances(Toggle):
class DungeonEntrances(Choice): class DungeonEntrances(Choice):
"""Shuffles dungeon entrances. Opens Deku, Fire and BotW to both ages. "All" includes Ganon's Castle.""" """Shuffles dungeon entrances. When enabled, both ages will have access to Fire Temple, Bottom of the Well, and Deku Tree.
Simple: Shuffle dungeon entrances except for Ganon's Castle.
All: Include Ganon's Castle as well."""
display_name = "Shuffle Dungeon Entrances" display_name = "Shuffle Dungeon Entrances"
option_off = 0 option_off = 0
option_simple = 1 option_simple = 1
@ -146,7 +179,9 @@ class DungeonEntrances(Choice):
class BossEntrances(Choice): class BossEntrances(Choice):
"""Shuffles boss entrances. "Limited" prevents age-mixing of bosses.""" """Shuffles boss entrances.
Limited: Bosses will be limited to the ages that typically fight them.
Full: Bosses may be fought as different ages than usual. Child can defeat Phantom Ganon and Bongo Bongo."""
display_name = "Shuffle Boss Entrances" display_name = "Shuffle Boss Entrances"
option_off = 0 option_off = 0
option_limited = 1 option_limited = 1
@ -178,19 +213,19 @@ class SpawnPositions(Choice):
alias_true = 3 alias_true = 3
class MixEntrancePools(Choice): # class MixEntrancePools(Choice):
"""Shuffles entrances into a mixed pool instead of separate ones. "indoor" keeps overworld entrances separate; "all" # """Shuffles entrances into a mixed pool instead of separate ones. "indoor" keeps overworld entrances separate; "all"
mixes them in.""" # mixes them in."""
display_name = "Mix Entrance Pools" # display_name = "Mix Entrance Pools"
option_off = 0 # option_off = 0
option_indoor = 1 # option_indoor = 1
option_all = 2 # option_all = 2
class DecoupleEntrances(Toggle): # class DecoupleEntrances(Toggle):
"""Decouple entrances when shuffling them. Also adds the one-way entrance from Gerudo Valley to Lake Hylia if # """Decouple entrances when shuffling them. Also adds the one-way entrance from Gerudo Valley to Lake Hylia if
overworld is shuffled.""" # overworld is shuffled."""
display_name = "Decouple Entrances" # display_name = "Decouple Entrances"
class TriforceHunt(Toggle): class TriforceHunt(Toggle):
@ -216,13 +251,17 @@ class ExtraTriforces(Range):
class LogicalChus(Toggle): class LogicalChus(Toggle):
"""Bombchus are properly considered in logic. The first found pack will have 20 chus; Kokiri Shop and Bazaar sell """Bombchus are properly considered in logic.
refills; bombchus open Bombchu Bowling.""" The first found pack will always have 20 chus.
Kokiri Shop and Bazaar will sell refills at reduced cost.
Bombchus open Bombchu Bowling."""
display_name = "Bombchus Considered in Logic" display_name = "Bombchus Considered in Logic"
class DungeonShortcuts(Choice): class DungeonShortcuts(Choice):
"""Shortcuts to dungeon bosses are available without any requirements.""" """Shortcuts to dungeon bosses are available without any requirements.
If enabled, this will impact the logic of dungeons where shortcuts are available.
Choice: Use the option "dungeon_shortcuts_list" to choose shortcuts."""
display_name = "Dungeon Boss Shortcuts Mode" display_name = "Dungeon Boss Shortcuts Mode"
option_off = 0 option_off = 0
option_choice = 1 option_choice = 1
@ -246,7 +285,11 @@ class DungeonShortcutsList(OptionSet):
class MQDungeons(Choice): class MQDungeons(Choice):
"""Choose between vanilla and Master Quest dungeon layouts.""" """Choose between vanilla and Master Quest dungeon layouts.
Vanilla: All layouts are vanilla.
MQ: All layouts are Master Quest.
Specific: Use the option "mq_dungeons_list" to choose which dungeons are MQ.
Count: Use the option "mq_dungeons_count" to choose a number of random dungeons as MQ."""
display_name = "MQ Dungeon Mode" display_name = "MQ Dungeon Mode"
option_vanilla = 0 option_vanilla = 0
option_mq = 1 option_mq = 1
@ -255,7 +298,7 @@ class MQDungeons(Choice):
class MQDungeonList(OptionSet): class MQDungeonList(OptionSet):
"""Chosen dungeons to be MQ layout.""" """With MQ dungeons as Specific: chosen dungeons to be MQ layout."""
display_name = "MQ Dungeon List" display_name = "MQ Dungeon List"
valid_keys = { valid_keys = {
"Deku Tree", "Deku Tree",
@ -274,41 +317,41 @@ class MQDungeonList(OptionSet):
class MQDungeonCount(TrackRandomRange): class MQDungeonCount(TrackRandomRange):
"""Number of MQ dungeons, chosen randomly.""" """With MQ dungeons as Count: number of randomly-selected dungeons to be MQ layout."""
display_name = "MQ Dungeon Count" display_name = "MQ Dungeon Count"
range_start = 0 range_start = 0
range_end = 12 range_end = 12
default = 0 default = 0
class EmptyDungeons(Choice): # class EmptyDungeons(Choice):
"""Pre-completed dungeons are barren and rewards are given for free.""" # """Pre-completed dungeons are barren and rewards are given for free."""
display_name = "Pre-completed Dungeons Mode" # display_name = "Pre-completed Dungeons Mode"
option_none = 0 # option_none = 0
option_specific = 1 # option_specific = 1
option_count = 2 # option_count = 2
class EmptyDungeonList(OptionSet): # class EmptyDungeonList(OptionSet):
"""Chosen dungeons to be pre-completed.""" # """Chosen dungeons to be pre-completed."""
display_name = "Pre-completed Dungeon List" # display_name = "Pre-completed Dungeon List"
valid_keys = { # valid_keys = {
"Deku Tree", # "Deku Tree",
"Dodongo's Cavern", # "Dodongo's Cavern",
"Jabu Jabu's Belly", # "Jabu Jabu's Belly",
"Forest Temple", # "Forest Temple",
"Fire Temple", # "Fire Temple",
"Water Temple", # "Water Temple",
"Shadow Temple", # "Shadow Temple",
"Spirit Temple", # "Spirit Temple",
} # }
class EmptyDungeonCount(Range): # class EmptyDungeonCount(Range):
display_name = "Pre-completed Dungeon Count" # display_name = "Pre-completed Dungeon Count"
range_start = 1 # range_start = 1
range_end = 8 # range_end = 8
default = 2 # default = 2
world_options: typing.Dict[str, type(Option)] = { world_options: typing.Dict[str, type(Option)] = {
@ -341,59 +384,8 @@ world_options: typing.Dict[str, type(Option)] = {
} }
# class LacsCondition(Choice):
# """Set the requirements for the Light Arrow Cutscene in the Temple of Time."""
# display_name = "Light Arrow Cutscene Requirement"
# option_vanilla = 0
# option_stones = 1
# option_medallions = 2
# option_dungeons = 3
# option_tokens = 4
# class LacsStones(Range):
# """Set the number of Spiritual Stones required for LACS."""
# display_name = "Spiritual Stones Required for LACS"
# range_start = 0
# range_end = 3
# default = 3
# class LacsMedallions(Range):
# """Set the number of medallions required for LACS."""
# display_name = "Medallions Required for LACS"
# range_start = 0
# range_end = 6
# default = 6
# class LacsRewards(Range):
# """Set the number of dungeon rewards required for LACS."""
# display_name = "Dungeon Rewards Required for LACS"
# range_start = 0
# range_end = 9
# default = 9
# class LacsTokens(Range):
# """Set the number of Gold Skulltula Tokens required for LACS."""
# display_name = "Tokens Required for LACS"
# range_start = 0
# range_end = 100
# default = 40
# lacs_options: typing.Dict[str, type(Option)] = {
# "lacs_condition": LacsCondition,
# "lacs_stones": LacsStones,
# "lacs_medallions": LacsMedallions,
# "lacs_rewards": LacsRewards,
# "lacs_tokens": LacsTokens,
# }
class BridgeStones(Range): class BridgeStones(Range):
"""Set the number of Spiritual Stones required for the rainbow bridge.""" """With Stones bridge: set the number of Spiritual Stones required."""
display_name = "Spiritual Stones Required for Bridge" display_name = "Spiritual Stones Required for Bridge"
range_start = 0 range_start = 0
range_end = 3 range_end = 3
@ -401,7 +393,7 @@ class BridgeStones(Range):
class BridgeMedallions(Range): class BridgeMedallions(Range):
"""Set the number of medallions required for the rainbow bridge.""" """With Medallions bridge: set the number of medallions required."""
display_name = "Medallions Required for Bridge" display_name = "Medallions Required for Bridge"
range_start = 0 range_start = 0
range_end = 6 range_end = 6
@ -409,7 +401,7 @@ class BridgeMedallions(Range):
class BridgeRewards(Range): class BridgeRewards(Range):
"""Set the number of dungeon rewards required for the rainbow bridge.""" """With Dungeons bridge: set the number of dungeon rewards required."""
display_name = "Dungeon Rewards Required for Bridge" display_name = "Dungeon Rewards Required for Bridge"
range_start = 0 range_start = 0
range_end = 9 range_end = 9
@ -417,7 +409,7 @@ class BridgeRewards(Range):
class BridgeTokens(Range): class BridgeTokens(Range):
"""Set the number of Gold Skulltula Tokens required for the rainbow bridge.""" """With Tokens bridge: set the number of Gold Skulltula Tokens required."""
display_name = "Tokens Required for Bridge" display_name = "Tokens Required for Bridge"
range_start = 0 range_start = 0
range_end = 100 range_end = 100
@ -425,7 +417,7 @@ class BridgeTokens(Range):
class BridgeHearts(Range): class BridgeHearts(Range):
"""Set the number of hearts required for the rainbow bridge.""" """With Hearts bridge: set the number of hearts required."""
display_name = "Hearts Required for Bridge" display_name = "Hearts Required for Bridge"
range_start = 4 range_start = 4
range_end = 20 range_end = 20
@ -442,7 +434,15 @@ bridge_options: typing.Dict[str, type(Option)] = {
class SongShuffle(Choice): class SongShuffle(Choice):
"""Set where songs can appear.""" """Set where songs can appear.
Song: Songs are shuffled into other song locations.
Dungeon: Songs are placed into end-of-dungeon locations:
- The 8 boss heart containers
- Sheik in Ice Cavern
- Lens of Truth chest in Bottom of the Well
- Ice Arrows chest in Gerudo Training Ground
- Impa at Hyrule Castle
Any: Songs can appear anywhere in the multiworld."""
display_name = "Shuffle Songs" display_name = "Shuffle Songs"
option_song = 0 option_song = 0
option_dungeon = 1 option_dungeon = 1
@ -450,8 +450,10 @@ class SongShuffle(Choice):
class ShopShuffle(Choice): class ShopShuffle(Choice):
"""Randomizes shop contents. "fixed_number" randomizes a specific number of items per shop; """Randomizes shop contents.
"random_number" randomizes the value for each shop. """ Off: Shops are not randomized at all.
Fixed Number: Shop contents are shuffled, and a specific number of multiworld locations exist in each shop, controlled by the "shop_slots" option.
Random Number: Same as Fixed Number, but the number of locations per shop is random and may differ between shops."""
display_name = "Shopsanity" display_name = "Shopsanity"
option_off = 0 option_off = 0
option_fixed_number = 1 option_fixed_number = 1
@ -459,15 +461,20 @@ class ShopShuffle(Choice):
class ShopSlots(Range): class ShopSlots(Range):
"""Number of items per shop to be randomized into the main itempool. """With Shopsanity fixed number: quantity of multiworld locations per shop to be randomized."""
Only active if Shopsanity is set to "fixed_number." """
display_name = "Shuffled Shop Slots" display_name = "Shuffled Shop Slots"
range_start = 0 range_start = 0
range_end = 4 range_end = 4
class ShopPrices(Choice): class ShopPrices(Choice):
"""Controls prices of shop items. "Normal" is a distribution from 0 to 300. "X Wallet" requires that wallet at max. "Affordable" is always 10 rupees.""" """Controls prices of shop locations.
Normal: Balanced distribution from 0 to 300.
Affordable: Every shop location costs 10 rupees.
Starting Wallet: Prices capped at 99 rupees.
Adult's Wallet: Prices capped at 200 rupees.
Giant's Wallet: Prices capped at 500 rupees.
Tycoon's Wallet: Prices capped at 999 rupees."""
display_name = "Shopsanity Prices" display_name = "Shopsanity Prices"
option_normal = 0 option_normal = 0
option_affordable = 1 option_affordable = 1
@ -478,7 +485,10 @@ class ShopPrices(Choice):
class TokenShuffle(Choice): class TokenShuffle(Choice):
"""Token rewards from Gold Skulltulas are shuffled into the pool.""" """Token rewards from Gold Skulltulas can be shuffled into the pool.
Dungeons: Only skulltulas in dungeons are shuffled.
Overworld: Only skulltulas on the overworld (all skulltulas not in dungeons) are shuffled.
All: Every skulltula is shuffled."""
display_name = "Tokensanity" display_name = "Tokensanity"
option_off = 0 option_off = 0
option_dungeons = 1 option_dungeons = 1
@ -487,7 +497,11 @@ class TokenShuffle(Choice):
class ScrubShuffle(Choice): class ScrubShuffle(Choice):
"""Shuffle the items sold by Business Scrubs, and set the prices.""" """Shuffle the items sold by Business Scrubs, and set the prices.
Off: Only the three business scrubs that sell one-time upgrades in vanilla will have items at their vanilla prices.
Low/"Affordable": All scrub prices are 10 rupees.
Regular/"Expensive": All scrub prices are vanilla.
Random Prices: All scrub prices are randomized between 0 and 99 rupees."""
display_name = "Scrub Shuffle" display_name = "Scrub Shuffle"
option_off = 0 option_off = 0
option_low = 1 option_low = 1
@ -513,7 +527,11 @@ class ShuffleOcarinas(Toggle):
class ShuffleChildTrade(Choice): class ShuffleChildTrade(Choice):
"""Controls the behavior of the start of the child trade quest.""" """Controls the behavior of the start of the child trade quest.
Vanilla: Malon will give you the Weird Egg at Hyrule Castle.
Shuffle: Malon will give you a random item, and the Weird Egg is shuffled.
Skip Child Zelda: The game starts with Zelda already met, Zelda's Letter obtained, and the item from Impa obtained.
"""
display_name = "Shuffle Child Trade Item" display_name = "Shuffle Child Trade Item"
option_vanilla = 0 option_vanilla = 0
option_shuffle = 1 option_shuffle = 1
@ -538,30 +556,39 @@ class ShuffleMedigoronCarpet(Toggle):
class ShuffleFreestanding(Choice): class ShuffleFreestanding(Choice):
"""Shuffles freestanding rupees, recovery hearts, Shadow Temple Spinning Pots, and Goron Pot.""" """Shuffles freestanding rupees, recovery hearts, Shadow Temple Spinning Pots, and Goron Pot drops.
Dungeons: Only freestanding items in dungeons are shuffled.
Overworld: Only freestanding items in the overworld are shuffled.
All: All freestanding items are shuffled."""
display_name = "Shuffle Rupees & Hearts" display_name = "Shuffle Rupees & Hearts"
option_off = 0 option_off = 0
option_all = 1 option_dungeons = 1
option_overworld = 2 option_overworld = 2
option_dungeons = 3 option_all = 3
class ShufflePots(Choice): class ShufflePots(Choice):
"""Shuffles pots and flying pots which normally contain an item.""" """Shuffles pots and flying pots which normally contain an item.
Dungeons: Only pots in dungeons are shuffled.
Overworld: Only pots in the overworld are shuffled.
All: All pots are shuffled."""
display_name = "Shuffle Pots" display_name = "Shuffle Pots"
option_off = 0 option_off = 0
option_all = 1 option_dungeons = 1
option_overworld = 2 option_overworld = 2
option_dungeons = 3 option_all = 3
class ShuffleCrates(Choice): class ShuffleCrates(Choice):
"""Shuffles large and small crates containing an item.""" """Shuffles large and small crates containing an item.
Dungeons: Only crates in dungeons are shuffled.
Overworld: Only crates in the overworld are shuffled.
All: All crates are shuffled."""
display_name = "Shuffle Crates" display_name = "Shuffle Crates"
option_off = 0 option_off = 0
option_all = 1 option_dungeons = 1
option_overworld = 2 option_overworld = 2
option_dungeons = 3 option_all = 3
class ShuffleBeehives(Toggle): class ShuffleBeehives(Toggle):
@ -597,72 +624,113 @@ shuffle_options: typing.Dict[str, type(Option)] = {
class ShuffleMapCompass(Choice): class ShuffleMapCompass(Choice):
"""Control where to shuffle dungeon maps and compasses.""" """Control where to shuffle dungeon maps and compasses.
Remove: There will be no maps or compasses in the itempool.
Startwith: You start with all maps and compasses.
Vanilla: Maps and compasses remain vanilla.
Dungeon: Maps and compasses are shuffled within their original dungeon.
Regional: Maps and compasses are shuffled only in regions near the original dungeon.
Overworld: Maps and compasses are shuffled locally outside of dungeons.
Any Dungeon: Maps and compasses are shuffled locally in any dungeon.
Keysanity: Maps and compasses can be anywhere in the multiworld."""
display_name = "Maps & Compasses" display_name = "Maps & Compasses"
option_remove = 0 option_remove = 0
option_startwith = 1 option_startwith = 1
option_vanilla = 2 option_vanilla = 2
option_dungeon = 3 option_dungeon = 3
option_overworld = 4 option_regional = 4
option_any_dungeon = 5 option_overworld = 5
option_keysanity = 6 option_any_dungeon = 6
option_regional = 7 option_keysanity = 7
default = 1 default = 1
alias_anywhere = 6 alias_anywhere = 7
class ShuffleKeys(Choice): class ShuffleKeys(Choice):
"""Control where to shuffle dungeon small keys.""" """Control where to shuffle dungeon small keys.
Remove/"Keysy": There will be no small keys in the itempool. All small key doors are automatically unlocked.
Vanilla: Small keys remain vanilla. You may start with extra small keys in some dungeons to prevent softlocks.
Dungeon: Small keys are shuffled within their original dungeon.
Regional: Small keys are shuffled only in regions near the original dungeon.
Overworld: Small keys are shuffled locally outside of dungeons.
Any Dungeon: Small keys are shuffled locally in any dungeon.
Keysanity: Small keys can be anywhere in the multiworld."""
display_name = "Small Keys" display_name = "Small Keys"
option_remove = 0 option_remove = 0
option_vanilla = 2 option_vanilla = 2
option_dungeon = 3 option_dungeon = 3
option_overworld = 4 option_regional = 4
option_any_dungeon = 5 option_overworld = 5
option_keysanity = 6 option_any_dungeon = 6
option_regional = 7 option_keysanity = 7
default = 3 default = 3
alias_keysy = 0 alias_keysy = 0
alias_anywhere = 6 alias_anywhere = 7
class ShuffleGerudoKeys(Choice): class ShuffleGerudoKeys(Choice):
"""Control where to shuffle the Thieves' Hideout small keys.""" """Control where to shuffle the Thieves' Hideout small keys.
Vanilla: Hideout keys remain vanilla.
Regional: Hideout keys are shuffled only in the Gerudo Valley/Desert Colossus area.
Overworld: Hideout keys are shuffled locally outside of dungeons.
Any Dungeon: Hideout keys are shuffled locally in any dungeon.
Keysanity: Hideout keys can be anywhere in the multiworld."""
display_name = "Thieves' Hideout Keys" display_name = "Thieves' Hideout Keys"
option_vanilla = 0 option_vanilla = 0
option_overworld = 1 option_regional = 1
option_any_dungeon = 2 option_overworld = 2
option_keysanity = 3 option_any_dungeon = 3
option_regional = 4 option_keysanity = 4
alias_anywhere = 3 alias_anywhere = 4
class ShuffleBossKeys(Choice): class ShuffleBossKeys(Choice):
"""Control where to shuffle boss keys, except the Ganon's Castle Boss Key.""" """Control where to shuffle boss keys, except the Ganon's Castle Boss Key.
Remove/"Keysy": There will be no boss keys in the itempool. All boss key doors are automatically unlocked.
Vanilla: Boss keys remain vanilla. You may start with extra small keys in some dungeons to prevent softlocks.
Dungeon: Boss keys are shuffled within their original dungeon.
Regional: Boss keys are shuffled only in regions near the original dungeon.
Overworld: Boss keys are shuffled locally outside of dungeons.
Any Dungeon: Boss keys are shuffled locally in any dungeon.
Keysanity: Boss keys can be anywhere in the multiworld."""
display_name = "Boss Keys" display_name = "Boss Keys"
option_remove = 0 option_remove = 0
option_vanilla = 2 option_vanilla = 2
option_dungeon = 3 option_dungeon = 3
option_overworld = 4 option_regional = 4
option_any_dungeon = 5 option_overworld = 5
option_keysanity = 6 option_any_dungeon = 6
option_regional = 7 option_keysanity = 7
default = 3 default = 3
alias_keysy = 0 alias_keysy = 0
alias_anywhere = 6 alias_anywhere = 7
class ShuffleGanonBK(Choice): class ShuffleGanonBK(Choice):
"""Control how to shuffle the Ganon's Castle Boss Key.""" """Control how to shuffle the Ganon's Castle Boss Key (GCBK).
Remove: GCBK is removed, and the boss key door is automatically unlocked.
Vanilla: GCBK remains vanilla.
Dungeon: GCBK is shuffled within its original dungeon.
Regional: GCBK is shuffled only in Hyrule Field, Market, and Hyrule Castle areas.
Overworld: GCBK is shuffled locally outside of dungeons.
Any Dungeon: GCBK is shuffled locally in any dungeon.
Keysanity: GCBK can be anywhere in the multiworld.
On LACS: GCBK is on the Light Arrow Cutscene, which requires Shadow and Spirit Medallions.
Stones: GCBK will be awarded when reaching the target number of Spiritual Stones.
Medallions: GCBK will be awarded when reaching the target number of medallions.
Dungeons: GCBK will be awarded when reaching the target number of dungeon rewards.
Tokens: GCBK will be awarded when reaching the target number of Gold Skulltula Tokens.
Hearts: GCBK will be awarded when reaching the target number of hearts.
"""
display_name = "Ganon's Boss Key" display_name = "Ganon's Boss Key"
option_remove = 0 option_remove = 0
option_vanilla = 2 option_vanilla = 2
option_dungeon = 3 option_dungeon = 3
option_overworld = 4 option_regional = 4
option_any_dungeon = 5 option_overworld = 5
option_keysanity = 6 option_any_dungeon = 6
option_on_lacs = 7 option_keysanity = 7
option_regional = 8 option_on_lacs = 8
option_stones = 9 option_stones = 9
option_medallions = 10 option_medallions = 10
option_dungeons = 11 option_dungeons = 11
@ -670,7 +738,7 @@ class ShuffleGanonBK(Choice):
option_hearts = 13 option_hearts = 13
default = 0 default = 0
alias_keysy = 0 alias_keysy = 0
alias_anywhere = 6 alias_anywhere = 7
class EnhanceMC(Toggle): class EnhanceMC(Toggle):
@ -679,7 +747,7 @@ class EnhanceMC(Toggle):
class GanonBKMedallions(Range): class GanonBKMedallions(Range):
"""Set how many medallions are required to receive Ganon BK.""" """With medallions GCBK: set how many medallions are required to receive GCBK."""
display_name = "Medallions Required for Ganon's BK" display_name = "Medallions Required for Ganon's BK"
range_start = 1 range_start = 1
range_end = 6 range_end = 6
@ -687,7 +755,7 @@ class GanonBKMedallions(Range):
class GanonBKStones(Range): class GanonBKStones(Range):
"""Set how many Spiritual Stones are required to receive Ganon BK.""" """With stones GCBK: set how many Spiritual Stones are required to receive GCBK."""
display_name = "Spiritual Stones Required for Ganon's BK" display_name = "Spiritual Stones Required for Ganon's BK"
range_start = 1 range_start = 1
range_end = 3 range_end = 3
@ -695,7 +763,7 @@ class GanonBKStones(Range):
class GanonBKRewards(Range): class GanonBKRewards(Range):
"""Set how many dungeon rewards are required to receive Ganon BK.""" """With dungeons GCBK: set how many dungeon rewards are required to receive GCBK."""
display_name = "Dungeon Rewards Required for Ganon's BK" display_name = "Dungeon Rewards Required for Ganon's BK"
range_start = 1 range_start = 1
range_end = 9 range_end = 9
@ -703,7 +771,7 @@ class GanonBKRewards(Range):
class GanonBKTokens(Range): class GanonBKTokens(Range):
"""Set how many Gold Skulltula Tokens are required to receive Ganon BK.""" """With tokens GCBK: set how many Gold Skulltula Tokens are required to receive GCBK."""
display_name = "Tokens Required for Ganon's BK" display_name = "Tokens Required for Ganon's BK"
range_start = 1 range_start = 1
range_end = 100 range_end = 100
@ -711,7 +779,7 @@ class GanonBKTokens(Range):
class GanonBKHearts(Range): class GanonBKHearts(Range):
"""Set how many hearts are required to receive Ganon BK.""" """With hearts GCBK: set how many hearts are required to receive GCBK."""
display_name = "Hearts Required for Ganon's BK" display_name = "Hearts Required for Ganon's BK"
range_start = 4 range_start = 4
range_end = 20 range_end = 20
@ -719,7 +787,9 @@ class GanonBKHearts(Range):
class KeyRings(Choice): class KeyRings(Choice):
"""Dungeons have all small keys found at once, rather than individually.""" """A key ring grants all dungeon small keys at once, rather than individually.
Choose: Use the option "key_rings_list" to choose which dungeons have key rings.
All: All dungeons have key rings instead of small keys."""
display_name = "Key Rings Mode" display_name = "Key Rings Mode"
option_off = 0 option_off = 0
option_choose = 1 option_choose = 1
@ -728,7 +798,7 @@ class KeyRings(Choice):
class KeyRingList(OptionSet): class KeyRingList(OptionSet):
"""Select areas with keyrings rather than individual small keys.""" """With key rings as Choose: select areas with key rings rather than individual small keys."""
display_name = "Key Ring Areas" display_name = "Key Ring Areas"
valid_keys = { valid_keys = {
"Thieves' Hideout", "Thieves' Hideout",
@ -828,7 +898,8 @@ class BigPoeCount(Range):
class FAETorchCount(Range): class FAETorchCount(Range):
"""Number of lit torches required to open Shadow Temple.""" """Number of lit torches required to open Shadow Temple.
Does not affect logic; use the trick Shadow Temple Entry with Fire Arrows if desired."""
display_name = "Fire Arrow Entry Torch Count" display_name = "Fire Arrow Entry Torch Count"
range_start = 1 range_start = 1
range_end = 24 range_end = 24
@ -853,7 +924,11 @@ timesavers_options: typing.Dict[str, type(Option)] = {
class CorrectChestAppearance(Choice): class CorrectChestAppearance(Choice):
"""Changes chest textures and/or sizes to match their contents. "Classic" is the old behavior of CSMC.""" """Changes chest textures and/or sizes to match their contents.
Off: All chests have their vanilla size/appearance.
Textures: Chest textures reflect their contents.
Both: Like Textures, but progression items and boss keys get big chests, and other items get small chests.
Classic: Old behavior of CSMC; textures distinguish keys from non-keys, and size distinguishes importance."""
display_name = "Chest Appearance Matches Contents" display_name = "Chest Appearance Matches Contents"
option_off = 0 option_off = 0
option_textures = 1 option_textures = 1
@ -872,15 +947,24 @@ class InvisibleChests(Toggle):
class CorrectPotCrateAppearance(Choice): class CorrectPotCrateAppearance(Choice):
"""Unchecked pots and crates have a different texture; unchecked beehives will wiggle. With textures_content, pots and crates have an appearance based on their contents; with textures_unchecked, all unchecked pots/crates have the same appearance.""" """Changes the appearance of pots, crates, and beehives that contain items.
Off: Vanilla appearance for all containers.
Textures (Content): Unchecked pots and crates have a texture reflecting their contents. Unchecked beehives with progression items will wiggle.
Textures (Unchecked): Unchecked pots and crates are golden. Unchecked beehives will wiggle.
"""
display_name = "Pot, Crate, and Beehive Appearance" display_name = "Pot, Crate, and Beehive Appearance"
option_off = 0 option_off = 0
option_textures_content = 1 option_textures_content = 1
option_textures_unchecked = 2 option_textures_unchecked = 2
default = 2
class Hints(Choice): class Hints(Choice):
"""Gossip Stones can give hints about item locations.""" """Gossip Stones can give hints about item locations.
None: Gossip Stones do not give hints.
Mask: Gossip Stones give hints with Mask of Truth.
Agony: Gossip Stones give hints wtih Stone of Agony.
Always: Gossip Stones always give hints."""
display_name = "Gossip Stones" display_name = "Gossip Stones"
option_none = 0 option_none = 0
option_mask = 1 option_mask = 1
@ -895,7 +979,9 @@ class MiscHints(DefaultOnToggle):
class HintDistribution(Choice): class HintDistribution(Choice):
"""Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc.""" """Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc.
Detailed documentation on hint distributions can be found on the Archipelago GitHub or OoTRandomizer.com.
The Async hint distribution is intended for async multiworlds. It removes Way of the Hero hints to improve generation times, since they are not very useful in asyncs."""
display_name = "Hint Distribution" display_name = "Hint Distribution"
option_balanced = 0 option_balanced = 0
option_ddr = 1 option_ddr = 1
@ -907,10 +993,13 @@ class HintDistribution(Choice):
option_useless = 7 option_useless = 7
option_very_strong = 8 option_very_strong = 8
option_async = 9 option_async = 9
default = 9
class TextShuffle(Choice): class TextShuffle(Choice):
"""Randomizes text in the game for comedic effect.""" """Randomizes text in the game for comedic effect.
Except Hints: does not randomize important text such as hints, small/boss key information, and item prices.
Complete: randomizes every textbox, including the useful ones."""
display_name = "Text Shuffle" display_name = "Text Shuffle"
option_none = 0 option_none = 0
option_except_hints = 1 option_except_hints = 1
@ -946,7 +1035,8 @@ class HeroMode(Toggle):
class StartingToD(Choice): class StartingToD(Choice):
"""Change the starting time of day.""" """Change the starting time of day.
Daytime starts at Sunrise and ends at Sunset. Default is between Morning and Noon."""
display_name = "Starting Time of Day" display_name = "Starting Time of Day"
option_default = 0 option_default = 0
option_sunrise = 1 option_sunrise = 1
@ -999,7 +1089,11 @@ misc_options: typing.Dict[str, type(Option)] = {
} }
class ItemPoolValue(Choice): class ItemPoolValue(Choice):
"""Changes the number of items available in the game.""" """Changes the number of items available in the game.
Plentiful: One extra copy of every major item.
Balanced: Original item pool.
Scarce: Extra copies of major items are removed. Heart containers are removed.
Minimal: All major item upgrades not used for locations are removed. All health is removed."""
display_name = "Item Pool" display_name = "Item Pool"
option_plentiful = 0 option_plentiful = 0
option_balanced = 1 option_balanced = 1
@ -1009,7 +1103,12 @@ class ItemPoolValue(Choice):
class IceTraps(Choice): class IceTraps(Choice):
"""Adds ice traps to the item pool.""" """Adds ice traps to the item pool.
Off: All ice traps are removed.
Normal: The vanilla quantity of ice traps are placed.
On/"Extra": There is a chance for some extra ice traps to be placed.
Mayhem: All added junk items are ice traps.
Onslaught: All junk items are replaced by ice traps, even those in the base pool."""
display_name = "Ice Traps" display_name = "Ice Traps"
option_off = 0 option_off = 0
option_normal = 1 option_normal = 1
@ -1021,34 +1120,27 @@ class IceTraps(Choice):
class IceTrapVisual(Choice): class IceTrapVisual(Choice):
"""Changes the appearance of ice traps as freestanding items.""" """Changes the appearance of traps, including other games' traps, as freestanding items."""
display_name = "Ice Trap Appearance" display_name = "Trap Appearance"
option_major_only = 0 option_major_only = 0
option_junk_only = 1 option_junk_only = 1
option_anything = 2 option_anything = 2
class AdultTradeStart(OptionSet): class AdultTradeStart(Choice):
"""Choose the items that can appear to start the adult trade sequence. By default it is Claim Check only.""" """Choose the item that starts the adult trade sequence."""
display_name = "Adult Trade Sequence Items" display_name = "Adult Trade Sequence Start"
default = {"Claim Check"} option_pocket_egg = 0
valid_keys = { option_pocket_cucco = 1
"Pocket Egg", option_cojiro = 2
"Pocket Cucco", option_odd_mushroom = 3
"Cojiro", option_poachers_saw = 4
"Odd Mushroom", option_broken_sword = 5
"Poachers Saw", option_prescription = 6
"Broken Sword", option_eyeball_frog = 7
"Prescription", option_eyedrops = 8
"Eyeball Frog", option_claim_check = 9
"Eyedrops", default = 9
"Claim Check",
}
def __init__(self, value: typing.Iterable[str]):
if not value:
value = self.default
super().__init__(value)
itempool_options: typing.Dict[str, type(Option)] = { itempool_options: typing.Dict[str, type(Option)] = {
@ -1068,7 +1160,7 @@ class Targeting(Choice):
class DisplayDpad(DefaultOnToggle): class DisplayDpad(DefaultOnToggle):
"""Show dpad icon on HUD for quick actions (ocarina, hover boots, iron boots).""" """Show dpad icon on HUD for quick actions (ocarina, hover boots, iron boots, mask)."""
display_name = "Display D-Pad HUD" display_name = "Display D-Pad HUD"
@ -1191,7 +1283,6 @@ oot_options: typing.Dict[str, type(Option)] = {
**world_options, **world_options,
**bridge_options, **bridge_options,
**dungeon_items_options, **dungeon_items_options,
# **lacs_options,
**shuffle_options, **shuffle_options,
**timesavers_options, **timesavers_options,
**misc_options, **misc_options,

View File

@ -2094,10 +2094,14 @@ def patch_rom(world, rom):
if not world.dungeon_mq['Ganons Castle']: if not world.dungeon_mq['Ganons Castle']:
chest_name = 'Ganons Castle Light Trial Lullaby Chest' chest_name = 'Ganons Castle Light Trial Lullaby Chest'
location = world.get_location(chest_name) location = world.get_location(chest_name)
if not location.item.trap:
if location.item.game == 'Ocarina of Time': if location.item.game == 'Ocarina of Time':
item = read_rom_item(rom, location.item.index) item = read_rom_item(rom, location.item.index)
else: else:
item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK) item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK)
else:
looks_like_index = get_override_entry(world, location)[5]
item = read_rom_item(rom, looks_like_index)
if item['chest_type'] in (GOLD_CHEST, GILDED_CHEST, SKULL_CHEST_BIG): if item['chest_type'] in (GOLD_CHEST, GILDED_CHEST, SKULL_CHEST_BIG):
rom.write_int16(0x321B176, 0xFC40) # original 0xFC48 rom.write_int16(0x321B176, 0xFC40) # original 0xFC48
@ -2106,10 +2110,14 @@ def patch_rom(world, rom):
chest_name = 'Spirit Temple Compass Chest' chest_name = 'Spirit Temple Compass Chest'
chest_address = 0x2B6B07C chest_address = 0x2B6B07C
location = world.get_location(chest_name) location = world.get_location(chest_name)
if not location.item.trap:
if location.item.game == 'Ocarina of Time': if location.item.game == 'Ocarina of Time':
item = read_rom_item(rom, location.item.index) item = read_rom_item(rom, location.item.index)
else: else:
item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK) item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK)
else:
looks_like_index = get_override_entry(world, location)[5]
item = read_rom_item(rom, looks_like_index)
if item['chest_type'] in (BROWN_CHEST, SILVER_CHEST, SKULL_CHEST_SMALL): if item['chest_type'] in (BROWN_CHEST, SILVER_CHEST, SKULL_CHEST_SMALL):
rom.write_int16(chest_address + 2, 0x0190) # X pos rom.write_int16(chest_address + 2, 0x0190) # X pos
rom.write_int16(chest_address + 6, 0xFABC) # Z pos rom.write_int16(chest_address + 6, 0xFABC) # Z pos
@ -2120,10 +2128,14 @@ def patch_rom(world, rom):
chest_address_0 = 0x21A02D0 # Address in setup 0 chest_address_0 = 0x21A02D0 # Address in setup 0
chest_address_2 = 0x21A06E4 # Address in setup 2 chest_address_2 = 0x21A06E4 # Address in setup 2
location = world.get_location(chest_name) location = world.get_location(chest_name)
if not location.item.trap:
if location.item.game == 'Ocarina of Time': if location.item.game == 'Ocarina of Time':
item = read_rom_item(rom, location.item.index) item = read_rom_item(rom, location.item.index)
else: else:
item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK) item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK)
else:
looks_like_index = get_override_entry(world, location)[5]
item = read_rom_item(rom, looks_like_index)
if item['chest_type'] in (BROWN_CHEST, SILVER_CHEST, SKULL_CHEST_SMALL): if item['chest_type'] in (BROWN_CHEST, SILVER_CHEST, SKULL_CHEST_SMALL):
rom.write_int16(chest_address_0 + 6, 0x0172) # Z pos rom.write_int16(chest_address_0 + 6, 0x0172) # Z pos
rom.write_int16(chest_address_2 + 6, 0x0172) # Z pos rom.write_int16(chest_address_2 + 6, 0x0172) # Z pos

View File

@ -223,9 +223,6 @@ def set_shop_rules(ootworld):
# The goal is to automatically set item rules based on age requirements in case entrances were shuffled # The goal is to automatically set item rules based on age requirements in case entrances were shuffled
def set_entrances_based_rules(ootworld): def set_entrances_based_rules(ootworld):
if ootworld.multiworld.accessibility == 'beatable':
return
all_state = ootworld.multiworld.get_all_state(False) all_state = ootworld.multiworld.get_all_state(False)
for location in filter(lambda location: location.type == 'Shop', ootworld.get_locations()): for location in filter(lambda location: location.type == 'Shop', ootworld.get_locations()):

View File

@ -10,7 +10,7 @@ from string import printable
logger = logging.getLogger("Ocarina of Time") logger = logging.getLogger("Ocarina of Time")
from .Location import OOTLocation, LocationFactory, location_name_to_id from .Location import OOTLocation, LocationFactory, location_name_to_id, build_location_name_groups
from .Entrance import OOTEntrance from .Entrance import OOTEntrance
from .EntranceShuffle import shuffle_random_entrances, entrance_shuffle_table, EntranceShuffleError from .EntranceShuffle import shuffle_random_entrances, entrance_shuffle_table, EntranceShuffleError
from .HintList import getRequiredHints from .HintList import getRequiredHints
@ -163,11 +163,13 @@ class OOTWorld(World):
"Bottle with Big Poe", "Bottle with Red Potion", "Bottle with Green Potion", "Bottle with Big Poe", "Bottle with Red Potion", "Bottle with Green Potion",
"Bottle with Blue Potion", "Bottle with Fairy", "Bottle with Fish", "Bottle with Blue Potion", "Bottle with Fairy", "Bottle with Fish",
"Bottle with Blue Fire", "Bottle with Bugs", "Bottle with Poe"}, "Bottle with Blue Fire", "Bottle with Bugs", "Bottle with Poe"},
"Adult Trade Item": {"Pocket Egg", "Pocket Cucco", "Odd Mushroom", "Adult Trade Item": {"Pocket Egg", "Pocket Cucco", "Cojiro", "Odd Mushroom",
"Odd Potion", "Poachers Saw", "Broken Sword", "Prescription", "Odd Potion", "Poachers Saw", "Broken Sword", "Prescription",
"Eyeball Frog", "Eyedrops", "Claim Check"} "Eyeball Frog", "Eyedrops", "Claim Check"},
} }
location_name_groups = build_location_name_groups()
def __init__(self, world, player): def __init__(self, world, player):
self.hint_data_available = threading.Event() self.hint_data_available = threading.Event()
self.collectible_flags_available = threading.Event() self.collectible_flags_available = threading.Event()
@ -384,6 +386,7 @@ class OOTWorld(World):
self.mq_dungeons_mode = 'count' self.mq_dungeons_mode = 'count'
self.mq_dungeons_count = 0 self.mq_dungeons_count = 0
self.dungeon_mq = {item['name']: (item['name'] in mq_dungeons) for item in dungeon_table} self.dungeon_mq = {item['name']: (item['name'] in mq_dungeons) for item in dungeon_table}
self.dungeon_mq['Thieves Hideout'] = False # fix for bug in SaveContext:287
# Empty dungeon placeholder for the moment # Empty dungeon placeholder for the moment
self.empty_dungeons = {name: False for name in self.dungeon_mq} self.empty_dungeons = {name: False for name in self.dungeon_mq}
@ -409,6 +412,9 @@ class OOTWorld(World):
self.starting_tod = self.starting_tod.replace('_', '-') self.starting_tod = self.starting_tod.replace('_', '-')
self.shuffle_scrubs = self.shuffle_scrubs.replace('_prices', '') self.shuffle_scrubs = self.shuffle_scrubs.replace('_prices', '')
# Convert adult trade option to expected Set
self.adult_trade_start = {self.adult_trade_start.title().replace('_', ' ')}
# Get hint distribution # Get hint distribution
self.hint_dist_user = read_json(data_path('Hints', f'{self.hint_dist}.json')) self.hint_dist_user = read_json(data_path('Hints', f'{self.hint_dist}.json'))
@ -446,7 +452,7 @@ class OOTWorld(World):
self.always_hints = [hint.name for hint in getRequiredHints(self)] self.always_hints = [hint.name for hint in getRequiredHints(self)]
# Determine items which are not considered advancement based on settings. They will never be excluded. # Determine items which are not considered advancement based on settings. They will never be excluded.
self.nonadvancement_items = {'Double Defense'} self.nonadvancement_items = {'Double Defense', 'Deku Stick Capacity', 'Deku Nut Capacity'}
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): self.shuffle_scrubs == 'off' and not self.shuffle_grotto_entrances):
# nayru's love may be required to prevent forced damage # nayru's love may be required to prevent forced damage
@ -633,16 +639,18 @@ class OOTWorld(World):
self.multiworld.itempool.remove(item) self.multiworld.itempool.remove(item)
self.hinted_dungeon_reward_locations[item.name] = loc self.hinted_dungeon_reward_locations[item.name] = loc
def create_item(self, name: str): def create_item(self, name: str, allow_arbitrary_name: bool = False):
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', (name in self.nonadvancement_items if getattr(self, 'nonadvancement_items',
None) else False)) None) else False))
if allow_arbitrary_name:
return OOTItem(name, self.player, ('Event', True, None, None), True, False) return OOTItem(name, self.player, ('Event', True, None, None), True, False)
raise Exception(f"Invalid item name: {name}")
def make_event_item(self, name, location, item=None): def make_event_item(self, name, location, item=None):
if item is None: if item is None:
item = self.create_item(name) item = self.create_item(name, allow_arbitrary_name=True)
self.multiworld.push_item(location, item, collect=False) self.multiworld.push_item(location, item, collect=False)
location.locked = True location.locked = True
location.event = True location.event = True
@ -800,23 +808,25 @@ class OOTWorld(World):
self.multiworld.itempool.remove(item) self.multiworld.itempool.remove(item)
self.multiworld.random.shuffle(locations) self.multiworld.random.shuffle(locations)
fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, stage_items, fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, stage_items,
single_player_placement=True, lock=True) single_player_placement=True, lock=True, allow_excluded=True)
else: else:
for dungeon_info in dungeon_table: for dungeon_info in dungeon_table:
dungeon_name = dungeon_info['name'] dungeon_name = dungeon_info['name']
locations = gather_locations(self.multiworld, fill_stage, self.player, dungeon=dungeon_name) locations = gather_locations(self.multiworld, fill_stage, self.player, dungeon=dungeon_name)
if isinstance(locations, list): if isinstance(locations, list):
dungeon_items = list(filter(lambda item: dungeon_name in item.name, stage_items)) dungeon_items = list(filter(lambda item: dungeon_name in item.name, stage_items))
if not dungeon_items:
continue
for item in dungeon_items: for item in dungeon_items:
self.multiworld.itempool.remove(item) self.multiworld.itempool.remove(item)
self.multiworld.random.shuffle(locations) self.multiworld.random.shuffle(locations)
fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, dungeon_items, fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, dungeon_items,
single_player_placement=True, lock=True) single_player_placement=True, lock=True, allow_excluded=True)
# Place songs # Place songs
# 5 built-in retries because this section can fail sometimes # 5 built-in retries because this section can fail sometimes
if self.shuffle_song_items != 'any': if self.shuffle_song_items != 'any':
tries = 5 tries = 10
if self.shuffle_song_items == 'song': if self.shuffle_song_items == 'song':
song_locations = list(filter(lambda location: location.type == 'Song', song_locations = list(filter(lambda location: location.type == 'Song',
self.multiworld.get_unfilled_locations(player=self.player))) self.multiworld.get_unfilled_locations(player=self.player)))
@ -852,7 +862,7 @@ class OOTWorld(World):
try: try:
self.multiworld.random.shuffle(song_locations) self.multiworld.random.shuffle(song_locations)
fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), song_locations[:], songs[:], fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), song_locations[:], songs[:],
True, True) single_player_placement=True, lock=True, allow_excluded=True)
logger.debug(f"Successfully placed songs for player {self.player} after {6 - tries} attempt(s)") logger.debug(f"Successfully placed songs for player {self.player} after {6 - tries} attempt(s)")
except FillError as e: except FillError as e:
tries -= 1 tries -= 1
@ -888,7 +898,8 @@ class OOTWorld(World):
self.multiworld.random.shuffle(shop_locations) self.multiworld.random.shuffle(shop_locations)
for item in shop_prog + shop_junk: for item in shop_prog + shop_junk:
self.multiworld.itempool.remove(item) self.multiworld.itempool.remove(item)
fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), shop_locations, shop_prog, True, True) fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), shop_locations, shop_prog,
single_player_placement=True, lock=True, allow_excluded=True)
fast_fill(self.multiworld, shop_junk, shop_locations) fast_fill(self.multiworld, shop_junk, shop_locations)
for loc in shop_locations: for loc in shop_locations:
loc.locked = True loc.locked = True
@ -963,7 +974,7 @@ class OOTWorld(World):
multiworld.itempool.remove(item) multiworld.itempool.remove(item)
multiworld.random.shuffle(locations) multiworld.random.shuffle(locations)
fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_stage_items, fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_stage_items,
single_player_placement=False, lock=True) single_player_placement=False, lock=True, allow_excluded=True)
if fill_stage == 'Song': if fill_stage == 'Song':
# We don't want song locations to contain progression unless it's a song # We don't want song locations to contain progression unless it's a song
# or it was marked as priority. # or it was marked as priority.
@ -984,7 +995,7 @@ class OOTWorld(World):
multiworld.itempool.remove(item) multiworld.itempool.remove(item)
multiworld.random.shuffle(locations) multiworld.random.shuffle(locations)
fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_dungeon_items, fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_dungeon_items,
single_player_placement=False, lock=True) single_player_placement=False, lock=True, allow_excluded=True)
def generate_output(self, output_directory: str): def generate_output(self, output_directory: str):
if self.hints != 'none': if self.hints != 'none':
@ -1051,7 +1062,10 @@ class OOTWorld(World):
def stage_generate_output(cls, multiworld: MultiWorld, output_directory: str): def stage_generate_output(cls, multiworld: MultiWorld, output_directory: str):
def hint_type_players(hint_type: str) -> set: def hint_type_players(hint_type: str) -> set:
return {autoworld.player for autoworld in multiworld.get_game_worlds("Ocarina of Time") return {autoworld.player for autoworld in multiworld.get_game_worlds("Ocarina of Time")
if autoworld.hints != 'none' and autoworld.hint_dist_user['distribution'][hint_type]['copies'] > 0} if autoworld.hints != 'none'
and autoworld.hint_dist_user['distribution'][hint_type]['copies'] > 0
and (autoworld.hint_dist_user['distribution'][hint_type]['fixed'] > 0
or autoworld.hint_dist_user['distribution'][hint_type]['weight'] > 0)}
try: try:
item_hint_players = hint_type_players('item') item_hint_players = hint_type_players('item')
@ -1078,10 +1092,10 @@ class OOTWorld(World):
if loc.game == "Ocarina of Time" and loc.item.code and (not loc.locked or if loc.game == "Ocarina of Time" and loc.item.code and (not loc.locked or
(oot_is_item_of_type(loc.item, 'Song') or (oot_is_item_of_type(loc.item, 'Song') or
(oot_is_item_of_type(loc.item, 'SmallKey') and multiworld.worlds[loc.player].shuffle_smallkeys == 'any_dungeon') or (oot_is_item_of_type(loc.item, 'SmallKey') and multiworld.worlds[loc.player].shuffle_smallkeys in ('overworld', 'any_dungeon', 'regional')) or
(oot_is_item_of_type(loc.item, 'HideoutSmallKey') and multiworld.worlds[loc.player].shuffle_hideoutkeys == 'any_dungeon') or (oot_is_item_of_type(loc.item, 'HideoutSmallKey') and multiworld.worlds[loc.player].shuffle_hideoutkeys in ('overworld', 'any_dungeon', 'regional')) or
(oot_is_item_of_type(loc.item, 'BossKey') and multiworld.worlds[loc.player].shuffle_bosskeys == 'any_dungeon') or (oot_is_item_of_type(loc.item, 'BossKey') and multiworld.worlds[loc.player].shuffle_bosskeys in ('overworld', 'any_dungeon', 'regional')) or
(oot_is_item_of_type(loc.item, 'GanonBossKey') and multiworld.worlds[loc.player].shuffle_ganon_bosskey == 'any_dungeon'))): (oot_is_item_of_type(loc.item, 'GanonBossKey') and multiworld.worlds[loc.player].shuffle_ganon_bosskey in ('overworld', 'any_dungeon', 'regional')))):
if loc.player in barren_hint_players: if loc.player in barren_hint_players:
hint_area = get_hint_area(loc) hint_area = get_hint_area(loc)
items_by_region[loc.player][hint_area]['weight'] += 1 items_by_region[loc.player][hint_area]['weight'] += 1
@ -1096,7 +1110,12 @@ class OOTWorld(World):
elif barren_hint_players or woth_hint_players: # Check only relevant oot locations for barren/woth 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 player in (barren_hint_players | woth_hint_players):
for loc in multiworld.worlds[player].get_locations(): for loc in multiworld.worlds[player].get_locations():
if loc.item.code and (not loc.locked or oot_is_item_of_type(loc.item, 'Song')): if loc.item.code and (not loc.locked or
(oot_is_item_of_type(loc.item, 'Song') or
(oot_is_item_of_type(loc.item, 'SmallKey') and multiworld.worlds[loc.player].shuffle_smallkeys in ('overworld', 'any_dungeon', 'regional')) or
(oot_is_item_of_type(loc.item, 'HideoutSmallKey') and multiworld.worlds[loc.player].shuffle_hideoutkeys in ('overworld', 'any_dungeon', 'regional')) or
(oot_is_item_of_type(loc.item, 'BossKey') and multiworld.worlds[loc.player].shuffle_bosskeys in ('overworld', 'any_dungeon', 'regional')) or
(oot_is_item_of_type(loc.item, 'GanonBossKey') and multiworld.worlds[loc.player].shuffle_ganon_bosskey in ('overworld', 'any_dungeon', 'regional')))):
if player in barren_hint_players: if player in barren_hint_players:
hint_area = get_hint_area(loc) hint_area = get_hint_area(loc)
items_by_region[player][hint_area]['weight'] += 1 items_by_region[player][hint_area]['weight'] += 1
@ -1183,6 +1202,15 @@ class OOTWorld(World):
er_hint_data[self.player][location.address] = main_entrance.name er_hint_data[self.player][location.address] = main_entrance.name
logger.debug(f"Set {location.name} hint data to {main_entrance.name}") logger.debug(f"Set {location.name} hint data to {main_entrance.name}")
def write_spoiler(self, spoiler_handle: typing.TextIO) -> None:
required_trials_str = ", ".join(t for t in self.skipped_trials if not self.skipped_trials[t])
spoiler_handle.write(f"\n\nTrials ({self.multiworld.get_player_name(self.player)}): {required_trials_str}\n")
if self.shopsanity != 'off':
spoiler_handle.write(f"\nShop Prices ({self.multiworld.get_player_name(self.player)}):\n")
for k, v in self.shop_prices.items():
spoiler_handle.write(f"{k}: {v} Rupees\n")
# Key ring handling: # Key ring handling:
# Key rings are multiple items glued together into one, so we need to give # Key rings are multiple items glued together into one, so we need to give
# the appropriate number of keys in the collection state when they are # the appropriate number of keys in the collection state when they are
@ -1265,25 +1293,13 @@ class OOTWorld(World):
# Specifically ensures that only real items are gotten, not any events. # Specifically ensures that only real items are gotten, not any events.
# In particular, ensures that Time Travel needs to be found. # In particular, ensures that Time Travel needs to be found.
def get_state_with_complete_itempool(self): def get_state_with_complete_itempool(self):
all_state = self.multiworld.get_all_state(use_cache=False) all_state = CollectionState(self.multiworld)
# Remove event progression items for item in self.multiworld.itempool:
for item, player in all_state.prog_items: if item.player == self.player:
if player == self.player and (item not in item_table or item_table[item][2] is None): self.multiworld.worlds[item.player].collect(all_state, item)
all_state.prog_items[(item, player)] = 0
# Remove all events and checked locations
all_state.locations_checked = {loc for loc in all_state.locations_checked if loc.player != self.player}
all_state.events = {loc for loc in all_state.events if loc.player != self.player}
# If free_scarecrow give Scarecrow Song # If free_scarecrow give Scarecrow Song
if self.free_scarecrow: if self.free_scarecrow:
all_state.collect(self.create_item("Scarecrow Song"), event=True) all_state.collect(self.create_item("Scarecrow Song"), event=True)
# Invalidate caches
all_state.child_reachable_regions[self.player] = set()
all_state.adult_reachable_regions[self.player] = set()
all_state.child_blocked_connections[self.player] = set()
all_state.adult_blocked_connections[self.player] = set()
all_state.day_reachable_regions[self.player] = set()
all_state.dampe_reachable_regions[self.player] = set()
all_state.stale[self.player] = True all_state.stale[self.player] = True
return all_state return all_state
@ -1349,7 +1365,7 @@ def gather_locations(multiworld: MultiWorld,
condition = lambda location: location.name in dungeon_song_locations condition = lambda location: location.name in dungeon_song_locations
locations += filter(condition, multiworld.get_unfilled_locations(player=player)) locations += filter(condition, multiworld.get_unfilled_locations(player=player))
else: else:
if any(map(lambda v: v in {'keysanity'}, fill_opts.values())): if any(map(lambda v: v == 'keysanity', fill_opts.values())):
return None return None
for player, option in fill_opts.items(): for player, option in fill_opts.items():
condition = functools.partial(valid_dungeon_item_location, condition = functools.partial(valid_dungeon_item_location,