diff --git a/EntranceRandomizer.py b/EntranceRandomizer.py index 7b3ac397..f8404fe7 100755 --- a/EntranceRandomizer.py +++ b/EntranceRandomizer.py @@ -222,9 +222,16 @@ def parse_arguments(argv, no_defaults=False): Random: Picks a random value between 0 and 7 (inclusive). 0-7: Number of crystals needed ''') - parser.add_argument('--open_pyramid', default=defval(False), help='''\ - Pre-opens the pyramid hole, this removes the Agahnim 2 requirement for it - ''', action='store_true') + parser.add_argument('--open_pyramid', default=defval('auto'), help='''\ + Pre-opens the pyramid hole, this removes the Agahnim 2 requirement for it. + Depending on goal, you might still need to beat Agahnim 2 in order to beat ganon. + fast ganon goals are crystals, ganontriforcehunt, localganontriforcehunt, pedestalganon + auto - Only opens pyramid hole if the goal specifies a fast ganon, and entrance shuffle + is vanilla, dungeonssimple or dungeonsfull. + goal - Opens pyramid hole if the goal specifies a fast ganon. + yes - Always opens the pyramid hole. + no - Never opens the pyramid hole. + ''', choices=['auto', 'goal', 'yes', 'no']) parser.add_argument('--rom', default=defval('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc'), help='Path to an ALttP JAP(1.0) rom to use as a base.') parser.add_argument('--loglevel', default=defval('info'), const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.') diff --git a/Gui.py b/Gui.py index d48e8dac..19c62a85 100755 --- a/Gui.py +++ b/Gui.py @@ -64,8 +64,13 @@ def guiMain(args=None): createSpoilerCheckbutton = Checkbutton(checkBoxFrame, text="Create Spoiler Log", variable=createSpoilerVar) suppressRomVar = IntVar() suppressRomCheckbutton = Checkbutton(checkBoxFrame, text="Do not create patched Rom", variable=suppressRomVar) - openpyramidVar = IntVar() - openpyramidCheckbutton = Checkbutton(checkBoxFrame, text="Pre-open Pyramid Hole", variable=openpyramidVar) + openpyramidFrame = Frame(checkBoxFrame) + openpyramidVar = StringVar() + openpyramidVar.set('auto') + openpyramidOptionMenu = OptionMenu(openpyramidFrame, openpyramidVar, 'auto', 'goal', 'yes', 'no') + openpyramidLabel = Label(openpyramidFrame, text='Pre-open Pyramid Hole') + openpyramidLabel.pack(side=LEFT) + openpyramidOptionMenu.pack(side=LEFT) mcsbshuffleFrame = Frame(checkBoxFrame) mcsbLabel = Label(mcsbshuffleFrame, text="Shuffle: ") @@ -102,7 +107,7 @@ def guiMain(args=None): createSpoilerCheckbutton.pack(expand=True, anchor=W) suppressRomCheckbutton.pack(expand=True, anchor=W) - openpyramidCheckbutton.pack(expand=True, anchor=W) + openpyramidFrame.pack(expand=True, anchor=W) mcsbshuffleFrame.pack(expand=True, anchor=W) mcsbLabel.grid(row=0, column=0) mapshuffleCheckbutton.grid(row=0, column=1) @@ -564,7 +569,7 @@ def guiMain(args=None): guiargs.create_spoiler = bool(createSpoilerVar.get()) guiargs.skip_playthrough = not bool(createSpoilerVar.get()) guiargs.suppress_rom = bool(suppressRomVar.get()) - guiargs.open_pyramid = bool(openpyramidVar.get()) + guiargs.open_pyramid = openpyramidVar.get() guiargs.mapshuffle = bool(mapshuffleVar.get()) guiargs.compassshuffle = bool(compassshuffleVar.get()) guiargs.keyshuffle = {"on": True, "universal": "universal", "off": False}[keyshuffleVar.get()] diff --git a/Main.py b/Main.py index 0c24a46e..12027925 100644 --- a/Main.py +++ b/Main.py @@ -10,7 +10,7 @@ import zlib import concurrent.futures from BaseClasses import World, CollectionState, Item, Region, Location -from Shops import ShopSlotFill, create_shops, SHOP_ID_START, FillDisabledShopSlots +from Shops import ShopSlotFill, create_shops, SHOP_ID_START, FillDisabledShopSlots, total_shop_slots from Items import ItemFactory, item_table, item_name_groups from Regions import create_regions, mark_light_world_regions, lookup_vanilla_location_to_entrance from InvertedRegions import create_inverted_regions, mark_dark_world_regions @@ -112,6 +112,14 @@ def main(args, seed=None): for player in range(1, world.players + 1): world.difficulty_requirements[player] = difficulties[world.difficulty[player]] + if world.open_pyramid[player] == 'goal': + world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} + elif world.open_pyramid[player] == 'auto': + world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} and \ + (world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull'} or not world.shuffle_ganon) + else: + world.open_pyramid[player] = {'on': True, 'off': False, 'yes': True, 'no': False}.get(world.open_pyramid[player], world.open_pyramid[player]) + for tok in filter(None, args.startinventory[player].split(',')): item = ItemFactory(tok.strip(), player) if item: @@ -148,12 +156,13 @@ def main(args, seed=None): world.triforce_pieces_available[player] = max(world.triforce_pieces_available[player], world.triforce_pieces_required[player]) - if world.mode[player] != 'inverted': - create_regions(world, player) - else: - create_inverted_regions(world, player) - create_shops(world, player) - create_dungeons(world, player) + for player in range(1, world.players + 1): + if world.mode[player] != 'inverted': + create_regions(world, player) + else: + create_inverted_regions(world, player) + create_shops(world, player) + create_dungeons(world, player) logger.info('Shuffling the World about.') @@ -371,19 +380,22 @@ def main(args, seed=None): checks_in_area[location.player]["Total"] += 1 oldmancaves = [] - for region in [world.get_region("Old Man Sword Cave", player) for player in range(1, world.players + 1) if world.retro[player]]: - item = ItemFactory(region.shop.inventory[0]['item'], region.player) - player = region.player - location_id = SHOP_ID_START + 33 + takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"] + for index, take_any in enumerate(takeanyregions): + for region in [world.get_region(take_any, player) for player in range(1, world.players + 1) if world.retro[player]]: + item = ItemFactory(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'], region.player) + player = region.player + location_id = SHOP_ID_START + total_shop_slots + index - if region.type == RegionType.LightWorld: - checks_in_area[player]["Light World"].append(location_id) - else: - checks_in_area[player]["Dark World"].append(location_id) - checks_in_area[player]["Total"] += 1 + main_entrance = get_entrance_to_region(region) + if main_entrance.parent_region.type == RegionType.LightWorld: + checks_in_area[player]["Light World"].append(location_id) + else: + checks_in_area[player]["Dark World"].append(location_id) + checks_in_area[player]["Total"] += 1 - er_hint_data[player][location_id] = get_entrance_to_region(region).name - oldmancaves.append(((location_id, player), (item.code, player))) + er_hint_data[player][location_id] = main_entrance.name + oldmancaves.append(((location_id, player), (item.code, player))) precollected_items = [[] for player in range(world.players)] for item in world.precollected_items: @@ -449,6 +461,7 @@ def main(args, seed=None): return world + def copy_world(world): # ToDo: Not good yet ret = World(world.players, world.shuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints) diff --git a/Mystery.py b/Mystery.py index 8158950c..503f6ce3 100644 --- a/Mystery.py +++ b/Mystery.py @@ -379,7 +379,7 @@ def roll_settings(weights, plando_options: typing.Set[str] = frozenset(("bosses" # TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when # fast ganon + ganon at hole - ret.open_pyramid = ret.goal in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} + ret.open_pyramid = get_choice('open_pyramid', weights, 'goal') ret.crystals_gt = prefer_int(get_choice('tower_open', weights)) diff --git a/Rom.py b/Rom.py index 6c434813..6eadb87a 100644 --- a/Rom.py +++ b/Rom.py @@ -1,7 +1,7 @@ from __future__ import annotations JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '93538d51eb018955a90181600e3384ba' +RANDOMIZERBASEHASH = '7d9778b7c0a90d71fa5f32a3b56cdd87' import io import json @@ -18,7 +18,7 @@ import concurrent.futures from typing import Optional from BaseClasses import CollectionState, Region, Location -from Shops import ShopType +from Shops import ShopType, total_shop_slots from Dungeons import dungeon_music_addresses from Regions import location_table, old_location_address_to_new_location_address from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable @@ -791,6 +791,29 @@ def patch_rom(world, rom, player, team, enemized): write_custom_shops(rom, world, player) + def credits_digit(num): + # top: $54 is 1, 55 2, etc , so 57=4, 5C=9 + # bot: $7A is 1, 7B is 2, etc so 7D=4, 82=9 (zero unknown...) + return 0x53 + int(num), 0x79 + int(num) + + credits_total = 216 + if world.goal[player] == 'icerodhunt': # Impossible to get 216/216 with Ice rod hunt. Most possible is 215/216. + credits_total -= 1 + if world.retro[player]: # Old man cave and Take any caves will count towards collection rate. + credits_total += 5 + if world.shop_shuffle_slots[player]: # Potion shop only counts towards collection rate if included in the shuffle. + credits_total += 30 if 'w' in world.shop_shuffle[player] else 27 + + rom.write_byte(0x187010, credits_total) # dynamic credits + # collection rate address: 238C37 + first_top, first_bot = credits_digit((credits_total / 100) % 10) + mid_top, mid_bot = credits_digit((credits_total / 10) % 10) + last_top, last_bot = credits_digit(credits_total % 10) + # top half + rom.write_bytes(0x118C46, [first_top, mid_top, last_top]) + # bottom half + rom.write_bytes(0x118C64, [first_bot, mid_bot, last_bot]) + # patch medallion requirements if world.required_medallions[player][0] == 'Bombos': rom.write_byte(0x180022, 0x00) # requirement @@ -1560,6 +1583,7 @@ def write_custom_shops(rom, world, player): shop_data = bytearray() items_data = bytearray() + retro_shop_slots = bytearray() for shop_id, shop in enumerate(shops): if shop_id == len(shops) - 1: @@ -1568,10 +1592,27 @@ def write_custom_shops(rom, world, player): bytes[0] = shop_id bytes[-1] = shop.sram_offset shop_data.extend(bytes) - # [id][item][price-low][price-high][max][repl_id][repl_price-low][repl_price-high][player] - for item in shop.inventory: + + arrow_mask = 0x00 + for index, item in enumerate(shop.inventory): + slot = 0 if shop.type == ShopType.TakeAny else index if item is None: break + if world.shop_shuffle_slots[player] or shop.type == ShopType.TakeAny: + count_shop = (shop.region.name != 'Potion Shop' or 'w' in world.shop_shuffle[player]) and \ + shop.region.name != 'Capacity Upgrade' + rom.write_byte(0x186560 + shop.sram_offset + slot, 1 if count_shop else 0) + if item['item'] == 'Single Arrow' and item['player'] == 0: + arrow_mask |= 1 << index + retro_shop_slots.append(shop.sram_offset + slot) + + # [id][item][price-low][price-high][max][repl_id][repl_price-low][repl_price-high][player] + for index, item in enumerate(shop.inventory): + slot = 0 if shop.type == ShopType.TakeAny else index + if item is None: + break + if item['item'] == 'Single Arrow' and item['player'] == 0 and world.retro[player]: + rom.write_byte(0x186500 + shop.sram_offset + slot, arrow_mask) item_data = [shop_id, ItemFactory(item['item'], player).code] + int16_as_bytes(item['price']) + \ [item['max'], ItemFactory(item['replacement'], player).code if item['replacement'] else 0xFF] + \ int16_as_bytes(item['replacement_price']) + [0 if item['player'] == player else item['player']] @@ -1582,6 +1623,10 @@ def write_custom_shops(rom, world, player): items_data.extend([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) rom.write_bytes(0x184900, items_data) + if world.retro[player]: + retro_shop_slots.append(0xFF) + rom.write_bytes(0x186540, retro_shop_slots) + def hud_format_text(text): output = bytes() diff --git a/Shops.py b/Shops.py index 3ab556dc..d9611039 100644 --- a/Shops.py +++ b/Shops.py @@ -96,6 +96,9 @@ class Shop(): if not self.inventory[slot]: raise ValueError("Inventory can't be pushed back if it doesn't exist") + if not self.can_push_inventory(slot): + logging.warning(f'Warning, there is already an item pushed into this slot.') + self.inventory[slot] = { 'item': item, 'price': price, @@ -145,6 +148,7 @@ def ShopSlotFill(world): slot_num = int(location.name[-1]) - 1 shop: Shop = location.parent_region.shop if not shop.can_push_inventory(slot_num) or location.shop_slot_disabled: + location.shop_slot_disabled = True removed.add(location) if removed: diff --git a/data/basepatch.bmbp b/data/basepatch.bmbp index 3b1c9e10..c2f6f354 100644 Binary files a/data/basepatch.bmbp and b/data/basepatch.bmbp differ diff --git a/playerSettings.yaml b/playerSettings.yaml index 2caf91f6..5b1008b7 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -97,6 +97,11 @@ goals: ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then kill Ganon local_ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then kill Ganon ice_rod_hunt: 0 # You start with everything needed to 216 the seed. Find the Ice rod, then kill Trinexx at Turtle rock. +pyramid_open: + goal: 50 # Opens pyrymid if goal is fast_ganon, ganon_pedestal, ganon_triforce_hunt, or local_ganon_triforce_hunt + auto: 0 # Opens pyramid same as goal, except when an entrance shuffle other than vanilla, dungeonssimple or dungeonsfull is in effect. + yes: 0 # pyramid is opened unconditionally. You still have to beat agahnim 2 for ganon and dungeons. + no: 0 # access to pyramid requires beating agahnim 2. triforce_pieces_mode: #Determine how to calculate the extra available triforce pieces. extra: 0 # available = triforce_pieces_extra + triforce_pieces_required percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required