Merge remote-tracking branch 'origin/main'

This commit is contained in:
Fabian Dill 2021-01-30 02:02:20 +01:00
commit d3129e25e1
8 changed files with 109 additions and 30 deletions

View File

@ -222,9 +222,16 @@ def parse_arguments(argv, no_defaults=False):
Random: Picks a random value between 0 and 7 (inclusive). Random: Picks a random value between 0 and 7 (inclusive).
0-7: Number of crystals needed 0-7: Number of crystals needed
''') ''')
parser.add_argument('--open_pyramid', default=defval(False), help='''\ parser.add_argument('--open_pyramid', default=defval('auto'), help='''\
Pre-opens the pyramid hole, this removes the Agahnim 2 requirement for it Pre-opens the pyramid hole, this removes the Agahnim 2 requirement for it.
''', action='store_true') 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'), 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.') 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.') parser.add_argument('--loglevel', default=defval('info'), const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')

13
Gui.py
View File

@ -64,8 +64,13 @@ def guiMain(args=None):
createSpoilerCheckbutton = Checkbutton(checkBoxFrame, text="Create Spoiler Log", variable=createSpoilerVar) createSpoilerCheckbutton = Checkbutton(checkBoxFrame, text="Create Spoiler Log", variable=createSpoilerVar)
suppressRomVar = IntVar() suppressRomVar = IntVar()
suppressRomCheckbutton = Checkbutton(checkBoxFrame, text="Do not create patched Rom", variable=suppressRomVar) suppressRomCheckbutton = Checkbutton(checkBoxFrame, text="Do not create patched Rom", variable=suppressRomVar)
openpyramidVar = IntVar() openpyramidFrame = Frame(checkBoxFrame)
openpyramidCheckbutton = Checkbutton(checkBoxFrame, text="Pre-open Pyramid Hole", variable=openpyramidVar) 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) mcsbshuffleFrame = Frame(checkBoxFrame)
mcsbLabel = Label(mcsbshuffleFrame, text="Shuffle: ") mcsbLabel = Label(mcsbshuffleFrame, text="Shuffle: ")
@ -102,7 +107,7 @@ def guiMain(args=None):
createSpoilerCheckbutton.pack(expand=True, anchor=W) createSpoilerCheckbutton.pack(expand=True, anchor=W)
suppressRomCheckbutton.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) mcsbshuffleFrame.pack(expand=True, anchor=W)
mcsbLabel.grid(row=0, column=0) mcsbLabel.grid(row=0, column=0)
mapshuffleCheckbutton.grid(row=0, column=1) mapshuffleCheckbutton.grid(row=0, column=1)
@ -564,7 +569,7 @@ def guiMain(args=None):
guiargs.create_spoiler = bool(createSpoilerVar.get()) guiargs.create_spoiler = bool(createSpoilerVar.get())
guiargs.skip_playthrough = not bool(createSpoilerVar.get()) guiargs.skip_playthrough = not bool(createSpoilerVar.get())
guiargs.suppress_rom = bool(suppressRomVar.get()) guiargs.suppress_rom = bool(suppressRomVar.get())
guiargs.open_pyramid = bool(openpyramidVar.get()) guiargs.open_pyramid = openpyramidVar.get()
guiargs.mapshuffle = bool(mapshuffleVar.get()) guiargs.mapshuffle = bool(mapshuffleVar.get())
guiargs.compassshuffle = bool(compassshuffleVar.get()) guiargs.compassshuffle = bool(compassshuffleVar.get())
guiargs.keyshuffle = {"on": True, "universal": "universal", "off": False}[keyshuffleVar.get()] guiargs.keyshuffle = {"on": True, "universal": "universal", "off": False}[keyshuffleVar.get()]

25
Main.py
View File

@ -10,7 +10,7 @@ import zlib
import concurrent.futures import concurrent.futures
from BaseClasses import World, CollectionState, Item, Region, Location 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 Items import ItemFactory, item_table, item_name_groups
from Regions import create_regions, mark_light_world_regions, lookup_vanilla_location_to_entrance from Regions import create_regions, mark_light_world_regions, lookup_vanilla_location_to_entrance
from InvertedRegions import create_inverted_regions, mark_dark_world_regions 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): for player in range(1, world.players + 1):
world.difficulty_requirements[player] = difficulties[world.difficulty[player]] 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(',')): for tok in filter(None, args.startinventory[player].split(',')):
item = ItemFactory(tok.strip(), player) item = ItemFactory(tok.strip(), player)
if item: if item:
@ -148,6 +156,7 @@ def main(args, seed=None):
world.triforce_pieces_available[player] = max(world.triforce_pieces_available[player], world.triforce_pieces_required[player]) world.triforce_pieces_available[player] = max(world.triforce_pieces_available[player], world.triforce_pieces_required[player])
for player in range(1, world.players + 1):
if world.mode[player] != 'inverted': if world.mode[player] != 'inverted':
create_regions(world, player) create_regions(world, player)
else: else:
@ -371,18 +380,21 @@ def main(args, seed=None):
checks_in_area[location.player]["Total"] += 1 checks_in_area[location.player]["Total"] += 1
oldmancaves = [] oldmancaves = []
for region in [world.get_region("Old Man Sword Cave", player) for player in range(1, world.players + 1) if world.retro[player]]: takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"]
item = ItemFactory(region.shop.inventory[0]['item'], region.player) 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 player = region.player
location_id = SHOP_ID_START + 33 location_id = SHOP_ID_START + total_shop_slots + index
if region.type == RegionType.LightWorld: main_entrance = get_entrance_to_region(region)
if main_entrance.parent_region.type == RegionType.LightWorld:
checks_in_area[player]["Light World"].append(location_id) checks_in_area[player]["Light World"].append(location_id)
else: else:
checks_in_area[player]["Dark World"].append(location_id) checks_in_area[player]["Dark World"].append(location_id)
checks_in_area[player]["Total"] += 1 checks_in_area[player]["Total"] += 1
er_hint_data[player][location_id] = get_entrance_to_region(region).name er_hint_data[player][location_id] = main_entrance.name
oldmancaves.append(((location_id, player), (item.code, player))) oldmancaves.append(((location_id, player), (item.code, player)))
precollected_items = [[] for player in range(world.players)] precollected_items = [[] for player in range(world.players)]
@ -449,6 +461,7 @@ def main(args, seed=None):
return world return world
def copy_world(world): def copy_world(world):
# ToDo: Not good yet # 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) 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)

View File

@ -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 # TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when
# fast ganon + ganon at hole # 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)) ret.crystals_gt = prefer_int(get_choice('tower_open', weights))

53
Rom.py
View File

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
JAP10HASH = '03a63945398191337e896e5771f77173' JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = '93538d51eb018955a90181600e3384ba' RANDOMIZERBASEHASH = '7d9778b7c0a90d71fa5f32a3b56cdd87'
import io import io
import json import json
@ -18,7 +18,7 @@ import concurrent.futures
from typing import Optional from typing import Optional
from BaseClasses import CollectionState, Region, Location from BaseClasses import CollectionState, Region, Location
from Shops import ShopType from Shops import ShopType, total_shop_slots
from Dungeons import dungeon_music_addresses from Dungeons import dungeon_music_addresses
from Regions import location_table, old_location_address_to_new_location_address from Regions import location_table, old_location_address_to_new_location_address
from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable 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) 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 # patch medallion requirements
if world.required_medallions[player][0] == 'Bombos': if world.required_medallions[player][0] == 'Bombos':
rom.write_byte(0x180022, 0x00) # requirement rom.write_byte(0x180022, 0x00) # requirement
@ -1560,6 +1583,7 @@ def write_custom_shops(rom, world, player):
shop_data = bytearray() shop_data = bytearray()
items_data = bytearray() items_data = bytearray()
retro_shop_slots = bytearray()
for shop_id, shop in enumerate(shops): for shop_id, shop in enumerate(shops):
if shop_id == len(shops) - 1: if shop_id == len(shops) - 1:
@ -1568,10 +1592,27 @@ def write_custom_shops(rom, world, player):
bytes[0] = shop_id bytes[0] = shop_id
bytes[-1] = shop.sram_offset bytes[-1] = shop.sram_offset
shop_data.extend(bytes) 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: if item is None:
break 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_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] + \ [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']] 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]) items_data.extend([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
rom.write_bytes(0x184900, items_data) 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): def hud_format_text(text):
output = bytes() output = bytes()

View File

@ -96,6 +96,9 @@ class Shop():
if not self.inventory[slot]: if not self.inventory[slot]:
raise ValueError("Inventory can't be pushed back if it doesn't exist") 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] = { self.inventory[slot] = {
'item': item, 'item': item,
'price': price, 'price': price,
@ -145,6 +148,7 @@ def ShopSlotFill(world):
slot_num = int(location.name[-1]) - 1 slot_num = int(location.name[-1]) - 1
shop: Shop = location.parent_region.shop shop: Shop = location.parent_region.shop
if not shop.can_push_inventory(slot_num) or location.shop_slot_disabled: if not shop.can_push_inventory(slot_num) or location.shop_slot_disabled:
location.shop_slot_disabled = True
removed.add(location) removed.add(location)
if removed: if removed:

Binary file not shown.

View File

@ -97,6 +97,11 @@ goals:
ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then kill Ganon 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 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. 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. triforce_pieces_mode: #Determine how to calculate the extra available triforce pieces.
extra: 0 # available = triforce_pieces_extra + triforce_pieces_required extra: 0 # available = triforce_pieces_extra + triforce_pieces_required
percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required