Revert "Revert "Merge branch 'pr/151'""

This reverts commit ce23369b0b.
This commit is contained in:
Edos512 2020-12-04 23:52:03 +01:00
parent ce23369b0b
commit 76cdabd2cb
315 changed files with 398 additions and 70 deletions

2
.gitignore vendored
View File

@ -30,3 +30,5 @@ weights/
_persistent_storage.yaml
mystery_result_*.yaml
/db.db3
*-errors.txt
success.txt

View File

@ -6,7 +6,7 @@ import textwrap
import sys
from AdjusterMain import adjust
from Rom import get_sprite_from_name
from Rom import Sprite
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
@ -55,7 +55,7 @@ def main():
input(
'Could not find valid rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom)
sys.exit(1)
if args.sprite is not None and not os.path.isfile(args.sprite) and not get_sprite_from_name(args.sprite):
if args.sprite is not None and not os.path.isfile(args.sprite) and not Sprite.get_sprite_from_name(args.sprite):
input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
sys.exit(1)
@ -65,7 +65,10 @@ def main():
logging.basicConfig(format='%(message)s', level=loglevel)
args, path = adjust(args=args)
from Utils import persistent_store
persistent_store("adjuster", "last_settings", args)
from Rom import Sprite
if isinstance(args.sprite, Sprite):
args.sprite = args.sprite.name
persistent_store("adjuster", "last_settings_3", args)
if __name__ == '__main__':
main()

View File

@ -27,7 +27,7 @@ def adjust(args):
palettes_options['hud']=args.hud_palettes
palettes_options['sword']=args.sword_palettes
palettes_options['shield']=args.shield_palettes
palettes_options['link']=args.link_palettes
# palettes_options['link']=args.link_palettesvera
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic,
args.sprite, palettes_options)

View File

@ -119,13 +119,19 @@ class World(object):
set_player_attr('treasure_hunt_icon', 'Triforce Piece')
set_player_attr('treasure_hunt_count', 0)
set_player_attr('clock_mode', False)
set_player_attr('countdown_start_time', 10)
set_player_attr('red_clock_time', -2)
set_player_attr('blue_clock_time', 2)
set_player_attr('green_clock_time', 4)
set_player_attr('can_take_damage', True)
set_player_attr('glitch_boots', True)
set_player_attr('progression_balancing', True)
set_player_attr('local_items', set())
set_player_attr('non_local_items', set())
set_player_attr('triforce_pieces_available', 30)
set_player_attr('triforce_pieces_required', 20)
set_player_attr('shop_shuffle', 'off')
set_player_attr('shop_shuffle_slots', 0)
set_player_attr('shuffle_prizes', "g")
set_player_attr('sprite_pool', [])
set_player_attr('dark_room_logic', "lamp")
@ -335,6 +341,27 @@ class World(object):
if collect:
self.state.collect(item, location.event, location)
# TODO: Prevents fast_filling certain items. Move this to a proper filter.
if location.parent_region.shop is not None and location.name != 'Potion Shop': # includes potion shop slots but not potion shop powder
slot_num = int(location.name[-1]) - 1
my_item = location.parent_region.shop.inventory[slot_num]
if (my_item is not None and my_item['item'] == item.name) or 'Rupee' in item.name or ('Bee' in item.name and 'Trap' not in item.name):
# this will filter items that match the item in the shop or Rupees, or single bees
# really not a way for the player to know a renewable item from a player pool item
# bombs can be sitting on top of arrows or a potion refill, but dunno if that's a big deal
logging.debug('skipping item shop {}'.format(item.name))
else:
if my_item is None:
location.parent_region.shop.add_inventory(slot_num, 'None', 0)
my_item = location.parent_region.shop.inventory[slot_num]
else:
my_item['replacement'] = my_item['item']
my_item['replacement_price'] = my_item['price']
my_item['item'] = item.name
my_item['price'] = self.random.randrange(1, 61) * 5 # can probably replace this with a price chart
my_item['max'] = 1
my_item['player'] = item.player if item.player != location.player else 0
logging.debug('Placed %s at %s', item, location)
else:
raise RuntimeError('Cannot assign item %s to location %s.' % (item, location))
@ -1135,7 +1162,8 @@ class Shop():
'max': max,
'replacement': replacement,
'replacement_price': replacement_price,
'create_location': create_location
'create_location': create_location,
'player': 0
}
def push_inventory(self, slot: int, item: str, price: int, max: int = 1):
@ -1148,7 +1176,8 @@ class Shop():
'max': max,
'replacement': self.inventory[slot]["item"],
'replacement_price': self.inventory[slot]["price"],
'create_location': self.inventory[slot]["create_location"]
'create_location': self.inventory[slot]["create_location"],
'player': self.inventory[slot]["player"]
}
@ -1234,6 +1263,10 @@ class Spoiler(object):
if item is None:
continue
shopdata['item_{}'.format(index)] = "{}{}".format(item['item'], item['price']) if item['price'] else item['item']
if item['player'] > 0:
shopdata['item_{}'.format(index)] = shopdata['item_{}'.format(index)].replace('', '(Player {}) — '.format(item['player']))
if item['max'] == 0:
continue
shopdata['item_{}'.format(index)] += " x {}".format(item['max'])
@ -1307,6 +1340,7 @@ class Spoiler(object):
'triforce_pieces_available': self.world.triforce_pieces_available,
'triforce_pieces_required': self.world.triforce_pieces_required,
'shop_shuffle': self.world.shop_shuffle,
'shop_shuffle_slots': self.world.shop_shuffle_slots,
'shuffle_prizes': self.world.shuffle_prizes,
'sprite_pool': self.world.sprite_pool,
'restrict_dungeon_item_on_boss': self.world.restrict_dungeon_item_on_boss

View File

@ -8,7 +8,7 @@ import shlex
import sys
from Main import main, get_seed
from Rom import get_sprite_from_name
from Rom import Sprite
from Utils import is_bundled, close_console
@ -129,6 +129,14 @@ def parse_arguments(argv, no_defaults=False):
Timed mode. If time runs out, you lose (but can
still keep playing).
''')
parser.add_argument('--countdown_start_time', default=defval(10), type=int,
help='''Set amount of time, in minutes, to start with in Timed Countdown and Timed OHKO modes''')
parser.add_argument('--red_clock_time', default=defval(-2), type=int,
help='''Set amount of time, in minutes, to add from picking up red clocks; negative removes time instead''')
parser.add_argument('--blue_clock_time', default=defval(2), type=int,
help='''Set amount of time, in minutes, to add from picking up blue clocks; negative removes time instead''')
parser.add_argument('--green_clock_time', default=defval(4), type=int,
help='''Set amount of time, in minutes, to add from picking up green clocks; negative removes time instead''')
parser.add_argument('--dungeon_counters', default=defval('default'), const='default', nargs='?', choices=['default', 'on', 'pickup', 'off'],
help='''\
Select dungeon counter display settings. (default: %(default)s)
@ -173,7 +181,7 @@ def parse_arguments(argv, no_defaults=False):
slightly biased to placing progression items with
less restrictions.
''')
parser.add_argument('--shuffle', default=defval('full'), const='full', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeonsfull', 'dungeonssimple'],
parser.add_argument('--shuffle', default=defval('vanilla'), const='vanilla', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeonsfull', 'dungeonssimple'],
help='''\
Select Entrance Shuffling Algorithm. (default: %(default)s)
Full: Mix cave and dungeon entrances freely while limiting
@ -258,6 +266,8 @@ def parse_arguments(argv, no_defaults=False):
help='Specifies a list of items that will be in your starting inventory (separated by commas)')
parser.add_argument('--local_items', default=defval(''),
help='Specifies a list of items that will not spread across the multiworld (separated by commas)')
parser.add_argument('--non_local_items', default=defval(''),
help='Specifies a list of items that will spread across the multiworld (separated by commas)')
parser.add_argument('--custom', default=defval(False), help='Not supported.')
parser.add_argument('--customitemarray', default=defval(False), help='Not supported.')
parser.add_argument('--accessibility', default=defval('items'), const='items', nargs='?', choices=['items', 'locations', 'none'], help='''\
@ -320,6 +330,11 @@ def parse_arguments(argv, no_defaults=False):
p: randomize the prices of the items in shop inventories
u: shuffle capacity upgrades into the item pool
''')
parser.add_argument('--shop_shuffle_slots', default=defval(0),
type=lambda value: min(max(int(value), 1), 96),
help='''
Maximum amount of shop slots able to be filled by items from the item pool.
''')
parser.add_argument('--shuffle_prizes', default=defval('g'), choices=['', 'g', 'b', 'gb'])
parser.add_argument('--sprite_pool', help='''\
Specifies a colon separated list of sprites used for random/randomonevent. If not specified, the full sprite pool is used.''')
@ -366,14 +381,16 @@ def parse_arguments(argv, no_defaults=False):
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality',
'shuffle', 'crystals_ganon', 'crystals_gt', 'open_pyramid', 'timer',
'countdown_start_time', 'red_clock_time', 'blue_clock_time', 'green_clock_time',
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
'local_items', 'retro', 'accessibility', 'hints', 'beemizer',
'local_items', 'non_local_items', 'retro', 'accessibility', 'hints', 'beemizer',
'shufflebosses', 'enemy_shuffle', 'enemy_health', 'enemy_damage', 'shufflepots',
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor',
'heartbeep', "skip_progression_balancing", "triforce_pieces_available",
"triforce_pieces_required", "shop_shuffle",
"triforce_pieces_required", "shop_shuffle", "shop_shuffle_slots",
'remote_items', 'progressive', 'dungeon_counters', 'glitch_boots', 'killable_thieves',
'tile_shuffle', 'bush_shuffle', 'shuffle_prizes', 'sprite_pool', 'dark_room_logic', 'restrict_dungeon_item_on_boss']:
'tile_shuffle', 'bush_shuffle', 'shuffle_prizes', 'sprite_pool', 'dark_room_logic', 'restrict_dungeon_item_on_boss',
'hud_palettes', 'sword_palettes', 'shield_palettes', 'link_palettes']:
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
if player == 1:
setattr(ret, name, {1: value})
@ -400,7 +417,7 @@ def start():
input(
'Could not find valid base rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom)
sys.exit(1)
if any([sprite is not None and not os.path.isfile(sprite) and not get_sprite_from_name(sprite) for sprite in
if any([sprite is not None and not os.path.isfile(sprite) and not Sprite.get_sprite_from_name(sprite) for sprite in
args.sprite.values()]):
input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
sys.exit(1)

View File

@ -54,8 +54,9 @@ def fill_restrictive(world, base_state: CollectionState, locations, itempool, si
for location in region.locations:
if location.item and not location.event:
placements.append(location)
raise FillError(f'No more spots to place {item_to_place}, locations {locations} are invalid. '
f'Already placed {len(placements)}: {", ".join(placements)}')
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
world.push_item(spot_to_fill, item_to_place, False)
locations.remove(spot_to_fill)

View File

@ -14,12 +14,12 @@ def set_icon(window):
# some which may be platform specific, or depend on if the TCL library was compiled without
# multithreading support. Therefore I will assume it is not thread safe to avoid any possible problems
class BackgroundTask(object):
def __init__(self, window, code_to_run):
def __init__(self, window, code_to_run, *args):
self.window = window
self.queue = queue.Queue()
self.running = True
self.process_queue()
self.task = threading.Thread(target=code_to_run, args=(self,))
self.task = threading.Thread(target=code_to_run, args=(self, *args))
self.task.start()
def stop(self):
@ -45,7 +45,7 @@ class BackgroundTask(object):
self.window.after(100, self.process_queue)
class BackgroundTaskProgress(BackgroundTask):
def __init__(self, parent, code_to_run, title):
def __init__(self, parent, code_to_run, title, *args):
self.parent = parent
self.window = tk.Toplevel(parent)
self.window['padx'] = 5
@ -65,7 +65,7 @@ class BackgroundTaskProgress(BackgroundTask):
set_icon(self.window)
self.window.focus()
super().__init__(self.window, code_to_run)
super().__init__(self.window, code_to_run, *args)
#safe to call from worker thread
def update_status(self, text):

40
Main.py
View File

@ -10,7 +10,7 @@ import zlib
import concurrent.futures
from BaseClasses import World, CollectionState, Item, Region, Location, Shop
from Items import ItemFactory
from Items import ItemFactory, item_table
from Regions import create_regions, create_shops, mark_light_world_regions, lookup_vanilla_location_to_entrance
from InvertedRegions import create_inverted_regions, mark_dark_world_regions
from EntranceShuffle import link_entrances, link_inverted_entrances
@ -72,6 +72,10 @@ def main(args, seed=None):
world.tile_shuffle = args.tile_shuffle.copy()
world.beemizer = args.beemizer.copy()
world.timer = args.timer.copy()
world.countdown_start_time = args.countdown_start_time.copy()
world.red_clock_time = args.red_clock_time.copy()
world.blue_clock_time = args.blue_clock_time.copy()
world.green_clock_time = args.green_clock_time.copy()
world.shufflepots = args.shufflepots.copy()
world.progressive = args.progressive.copy()
world.dungeon_counters = args.dungeon_counters.copy()
@ -79,6 +83,7 @@ def main(args, seed=None):
world.triforce_pieces_available = args.triforce_pieces_available.copy()
world.triforce_pieces_required = args.triforce_pieces_required.copy()
world.shop_shuffle = args.shop_shuffle.copy()
world.shop_shuffle_slots = args.shop_shuffle_slots.copy()
world.progression_balancing = {player: not balance for player, balance in args.skip_progression_balancing.items()}
world.shuffle_prizes = args.shuffle_prizes.copy()
world.sprite_pool = args.sprite_pool.copy()
@ -106,7 +111,13 @@ def main(args, seed=None):
item = ItemFactory(tok.strip(), player)
if item:
world.push_precollected(item)
world.local_items[player] = {item.strip() for item in args.local_items[player].split(',')}
# item in item_table gets checked in mystery, but not CLI - so we double-check here
world.local_items[player] = {item.strip() for item in args.local_items[player].split(',') if
item.strip() in item_table}
world.non_local_items[player] = {item.strip() for item in args.non_local_items[player].split(',') if
item.strip() in item_table}
# items can't be both local and non-local, prefer local
world.non_local_items[player] -= world.local_items[player]
world.triforce_pieces_available[player] = max(world.triforce_pieces_available[player], world.triforce_pieces_required[player])
@ -297,6 +308,30 @@ def main(args, seed=None):
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
er_hint_data[region.player][location.address] = main_entrance.name
ordered_areas = ('Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total")
checks_in_area = {player: {area: list() for area in ordered_areas}
for player in range(1, world.players + 1)}
for player in range(1, world.players + 1):
checks_in_area[player]["Total"] = 0
for location in [loc for loc in world.get_filled_locations() if type(loc.address) is int]:
main_entrance = get_entrance_to_region(location.parent_region)
if location.parent_region.dungeon:
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
'Inverted Ganons Tower': 'Ganons Tower'}\
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
checks_in_area[location.player][dungeonname].append(location.address)
elif main_entrance.parent_region.type == RegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address)
elif main_entrance.parent_region.type == RegionType.DarkWorld:
checks_in_area[location.player]["Dark World"].append(location.address)
checks_in_area[location.player]["Total"] += 1
precollected_items = [[] for player in range(world.players)]
for item in world.precollected_items:
precollected_items[item.player - 1].append(item.code)
@ -323,6 +358,7 @@ def main(args, seed=None):
(location.item.code, location.item.player))
for location in world.get_filled_locations() if
type(location.address) is int],
"checks_in_area": checks_in_area,
"server_options": get_options()["server_options"],
"er_hint_data": er_hint_data,
"precollected_items": precollected_items,

View File

@ -11,7 +11,7 @@ import ModuleUpdate
ModuleUpdate.update()
from Utils import parse_yaml
from Rom import get_sprite_from_name
from Rom import Sprite
from EntranceRandomizer import parse_arguments
from Main import main as ERmain
from Main import get_seed, seeddigits
@ -167,7 +167,7 @@ def main(args=None, callback=ERmain):
if path:
try:
settings = settings_cache[path] if settings_cache[path] else roll_settings(weights_cache[path])
if settings.sprite and not os.path.isfile(settings.sprite) and not get_sprite_from_name(
if settings.sprite and not os.path.isfile(settings.sprite) and not Sprite.get_sprite_from_name(
settings.sprite):
logging.warning(
f"Warning: The chosen sprite, \"{settings.sprite}\", for yaml \"{path}\", does not exist.")
@ -238,6 +238,8 @@ def convert_to_on_off(value):
def get_choice(option, root, value=None) -> typing.Any:
if option not in root:
return value
if type(root[option]) is list:
return interpret_on_off(random.choices(root[option])[0])
if type(root[option]) is not dict:
return interpret_on_off(root[option])
if not root[option]:
@ -360,6 +362,8 @@ def roll_settings(weights):
# change minimum to required pieces to avoid problems
ret.triforce_pieces_available = min(max(ret.triforce_pieces_required, int(ret.triforce_pieces_available)), 90)
ret.shop_shuffle_slots = int(get_choice('shop_shuffle_slots', weights, '0'))
ret.shop_shuffle = get_choice('shop_shuffle', weights, '')
if not ret.shop_shuffle:
ret.shop_shuffle = ''
@ -448,6 +452,11 @@ def roll_settings(weights):
'timed_countdown': 'timed-countdown',
'display': 'display'}[get_choice('timer', weights, False)]
ret.countdown_start_time = int(get_choice('countdown_start_time', weights, 10))
ret.red_clock_time = int(get_choice('red_clock_time', weights, -2))
ret.blue_clock_time = int(get_choice('blue_clock_time', weights, 2))
ret.green_clock_time = int(get_choice('green_clock_time', weights, 4))
ret.dungeon_counters = get_choice('dungeon_counters', weights, 'default')
ret.progressive = convert_to_on_off(get_choice('progressive', weights, 'on'))
@ -487,6 +496,17 @@ def roll_settings(weights):
ret.local_items = ",".join(ret.local_items)
ret.non_local_items = set()
for item_name in weights.get('non_local_items', []):
items = item_name_groups.get(item_name, {item_name})
for item in items:
if item in item_table:
ret.non_local_items.add(item)
else:
raise Exception(f"Could not force item {item} to be world-non-local, as it was not recognized.")
ret.non_local_items = ",".join(ret.non_local_items)
if 'rom' in weights:
romweights = weights['rom']

View File

@ -368,7 +368,17 @@ def create_shops(world, player: int):
cls_mapping = {ShopType.UpgradeShop: UpgradeShop,
ShopType.Shop: Shop,
ShopType.TakeAny: TakeAny}
for region_name, (room_id, type, shopkeeper, custom, locked, inventory) in shop_table.items():
option = world.shop_shuffle[player]
my_shop_table = dict(shop_table)
num_slots = int(world.shop_shuffle_slots[player])
my_shop_slots = ([True] * num_slots + [False] * (len(shop_table) * 3))[:len(shop_table)*3 - 2]
world.random.shuffle(my_shop_slots)
from Items import ItemFactory
for region_name, (room_id, type, shopkeeper, custom, locked, inventory) in my_shop_table.items():
if world.mode[player] == 'inverted' and region_name == 'Dark Lake Hylia Shop':
locked = True
inventory = [('Blue Potion', 160), ('Blue Shield', 50), ('Bombs (10)', 50)]
@ -378,6 +388,19 @@ def create_shops(world, player: int):
world.shops.append(shop)
for index, item in enumerate(inventory):
shop.add_inventory(index, *item)
if region_name == 'Potion Shop':
pass
elif region_name == 'Capacity Upgrade':
pass
else:
if my_shop_slots.pop():
additional_item = world.random.choice(['Rupees (20)', 'Rupees (50)', 'Rupees (100)'])
world.itempool.append(ItemFactory(additional_item, player))
loc = Location(player, "{} Slot Item {}".format(shop.region.name, index+1), parent=shop.region)
shop.region.locations.append(loc)
world.dynamic_locations.append(loc)
world.clear_location_cache()
# (type, room_id, shopkeeper, custom, locked, [items])
# item = (item, price, max=0, replacement=None, replacement_price=0)
@ -393,10 +416,63 @@ shop_table = {
'Light World Death Mountain Shop': (0x00FF, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults),
'Kakariko Shop': (0x011F, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults),
'Cave Shop (Lake Hylia)': (0x0112, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults),
'Potion Shop': (0x0109, ShopType.Shop, 0xFF, False, True, [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)]),
'Potion Shop': (0x0109, ShopType.Shop, 0xA0, True, False, [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)]),
'Capacity Upgrade': (0x0115, ShopType.UpgradeShop, 0x04, True, True, [('Bomb Upgrade (+5)', 100, 7), ('Arrow Upgrade (+5)', 100, 7)])
}
old_location_address_to_new_location_address = {
0x2eb18: 0x18001b, # Bottle Merchant
0x33d68: 0x18001a, # Purple Chest
0x2df45: 0x18001d, # Link's Uncle
0x2f1fc: 0x180008, # Sahasrahla
0x18002a: 0x18001c, # Black Smith
0x339cf: 0x180009, # Sick Kid
0x33e7d: 0x180019, # Hobo
0x180160: 0x18000b, # Desert Palace - Desert Torch
0x289b0: 0x180018, # Master Sword Pedestal
0xf69fa: 0x180007, # Old Man
0x180162: 0x18000d, # Tower of Hera - Basement Cage
0x330c7: 0x18000a, # Stumpy
0x180161: 0x18000c # Ganons Tower - Bob's Torch
}
key_drop_data = {
'Hyrule Castle - Map Guard Key Drop': [0x140036, 0x140037],
'Hyrule Castle - Boomerang Guard Key Drop': [0x140033, 0x140034],
'Hyrule Castle - Key Rat Key Drop': [0x14000c, 0x14000d],
'Hyrule Castle - Big Key Drop': [0x14003c, 0x14003d],
'Eastern Palace - Dark Square Pot Key': [0x14005a, 0x14005b],
'Eastern Palace - Dark Eyegore Key Drop': [0x140048, 0x140049],
'Desert Palace - Desert Tiles 1 Pot Key': [0x140030, 0x140031],
'Desert Palace - Beamos Hall Pot Key': [0x14002a, 0x14002b],
'Desert Palace - Desert Tiles 2 Pot Key': [0x140027, 0x140028],
'Castle Tower - Dark Archer Key Drop': [0x140060, 0x140061],
'Castle Tower - Circle of Pots Key Drop': [0x140051, 0x140052],
'Swamp Palace - Pot Row Pot Key': [0x140018, 0x140019],
'Swamp Palace - Trench 1 Pot Key': [0x140015, 0x140016],
'Swamp Palace - Hookshot Pot Key': [0x140012, 0x140013],
'Swamp Palace - Trench 2 Pot Key': [0x14000f, 0x140010],
'Swamp Palace - Waterway Pot Key': [0x140009, 0x14000a],
'Skull Woods - West Lobby Pot Key': [0x14002d, 0x14002e],
'Skull Woods - Spike Corner Key Drop': [0x14001b, 0x14001c],
'Thieves\' Town - Hallway Pot Key': [0x14005d, 0x14005e],
'Thieves\' Town - Spike Switch Pot Key': [0x14004e, 0x14004f],
'Ice Palace - Jelly Key Drop': [0x140003, 0x140004],
'Ice Palace - Conveyor Key Drop': [0x140021, 0x140022],
'Ice Palace - Hammer Block Key Drop': [0x140024, 0x140025],
'Ice Palace - Many Pots Pot Key': [0x140045, 0x140046],
'Misery Mire - Spikes Pot Key': [0x140054, 0x140055],
'Misery Mire - Fishbone Pot Key': [0x14004b, 0x14004c],
'Misery Mire - Conveyor Crystal Key Drop': [0x140063, 0x140064],
'Turtle Rock - Pokey 1 Key Drop': [0x140057, 0x140058],
'Turtle Rock - Pokey 2 Key Drop': [0x140006, 0x140007],
'Ganons Tower - Conveyor Cross Pot Key': [0x14003f, 0x140040],
'Ganons Tower - Double Switch Pot Key': [0x140042, 0x140043],
'Ganons Tower - Conveyor Star Pits Pot Key': [0x140039, 0x14003a],
'Ganons Tower - Mini Helmasaur Key Drop': [0x14001e, 0x14001f]
}
location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'),
'Bottle Merchant': (0x2eb18, 0x186339, False, 'with a merchant'),
'Flute Spot': (0x18014a, 0x18633d, False, 'underground'),
@ -640,7 +716,9 @@ location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'),
[0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], None, True, 'Turtle Rock')}
lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int}
lookup_id_to_name[-1] = "cheat console"
lookup_id_to_name = {**lookup_id_to_name, **{data[1]: name for name, data in key_drop_data.items()}, -1: "cheat console"}
lookup_name_to_id = {name: data[0] for name, data in location_table.items() if type(data[0]) == int}
lookup_name_to_id = {**lookup_name_to_id, **{name: data[1] for name, data in key_drop_data.items()}, "cheat console": -1}
lookup_vanilla_location_to_entrance = {1572883: 'Kings Grave Inner Rocks', 191256: 'Kings Grave Inner Rocks',
1573194: 'Kings Grave Inner Rocks', 1573189: 'Kings Grave Inner Rocks',
@ -745,7 +823,28 @@ lookup_vanilla_location_to_entrance = {1572883: 'Kings Grave Inner Rocks', 19125
60103: 'Ganons Tower', 60106: 'Ganons Tower', 60109: 'Ganons Tower',
60127: 'Ganons Tower', 60118: 'Ganons Tower', 60148: 'Ganons Tower',
60151: 'Ganons Tower', 60145: 'Ganons Tower', 60157: 'Ganons Tower',
60160: 'Ganons Tower', 60163: 'Ganons Tower', 60166: 'Ganons Tower'}
60160: 'Ganons Tower', 60163: 'Ganons Tower', 60166: 'Ganons Tower',
0x140037: 'Hyrule Castle Entrance (South)',
0x140034: 'Hyrule Castle Entrance (South)',
0x14000d: 'Hyrule Castle Entrance (South)',
0x14003d: 'Hyrule Castle Entrance (South)',
0x14005b: 'Eastern Palace', 0x140049: 'Eastern Palace',
0x140031: 'Desert Palace Entrance (North)',
0x14002b: 'Desert Palace Entrance (North)',
0x140028: 'Desert Palace Entrance (North)',
0x140061: 'Agahnims Tower', 0x140052: 'Agahnims Tower',
0x140019: 'Swamp Palace', 0x140016: 'Swamp Palace', 0x140013: 'Swamp Palace',
0x140010: 'Swamp Palace', 0x14000a: 'Swamp Palace',
0x14002e: 'Skull Woods Second Section Door (East)',
0x14001c: 'Skull Woods Final Section',
0x14005e: 'Thieves Town', 0x14004f: 'Thieves Town',
0x140004: 'Ice Palace', 0x140022: 'Ice Palace',
0x140025: 'Ice Palace', 0x140046: 'Ice Palace',
0x140055: 'Misery Mire', 0x14004c: 'Misery Mire',
0x140064: 'Misery Mire',
0x140058: 'Turtle Rock', 0x140007: 'Dark Death Mountain Ledge (West)',
0x140040: 'Ganons Tower', 0x140043: 'Ganons Tower',
0x14003a: 'Ganons Tower', 0x14001f: 'Ganons Tower'}
lookup_prizes = {location for location in location_table if location.endswith(" - Prize")}
lookup_boss_drops = {location for location in location_table if location.endswith(" - Boss")}

View File

@ -104,7 +104,7 @@ def mirrorless_path_to_castle_courtyard(world, player):
else:
queue.append((entrance.connected_region, new_path))
raise Exception(f"Could not find mirrorless path to castle courtyard for Player {player}")
raise Exception(f"Could not find mirrorless path to castle courtyard for Player {player} ({world.get_player_names(player)})")
def set_rule(spot, rule):
spot.access_rule = rule
@ -179,6 +179,10 @@ def locality_rules(world, player):
for location in world.get_locations():
if location.player != player:
forbid_items_for_player(location, world.local_items[player], player)
if world.non_local_items[player]:
for location in world.get_locations():
if location.player == player:
forbid_items_for_player(location, world.non_local_items[player], player)
non_crossover_items = (item_name_groups["Small Keys"] | item_name_groups["Big Keys"] | progression_items) - {

View File

@ -266,7 +266,7 @@ junk_texts = [
"{C:GREEN}\n>Secret power\nis said to be\nin the arrow.",
"{C:GREEN}\nAim at the\neyes of Gohma.\n >",
"{C:GREEN}\nGrumble,\ngrumble…\n >",
"{C:GREEN}\n10th enemy\nhas the bomb.\n >",
# "{C:GREEN}\n10th enemy\nhas the bomb.\n >", removed as people may assume it applies to this game
"{C:GREEN}\nGo to the\nnext room.\n >",
"{C:GREEN}\n>Thanks, @\nYoure the\nhero of Hyrule",
"{C:GREEN}\nTheres always\nmoney in the\nBanana Stand>",
@ -1228,7 +1228,8 @@ class GoldCreditMapper(CharTextMapper):
class GreenCreditMapper(CharTextMapper):
char_map = {' ': 0x9F,
'·': 0x52}
'·': 0x52,
'.': 0x52}
alpha_offset = -0x29
class RedCreditMapper(CharTextMapper):

View File

@ -47,6 +47,8 @@ app.config["PONY"] = {
}
app.config["MAX_ROLL"] = 20
app.config["CACHE_TYPE"] = "simple"
app.config["JSON_AS_ASCII"] = False
app.autoversion = True
av = Autoversion(app)
cache = Cache(app)

View File

@ -37,9 +37,10 @@ def download_raw_patch(seed_id, player_id):
return "Patch not found"
else:
import io
pname = patch.seed.multidata["names"][0][patch.player - 1]
if patch.seed.multidata:
pname = patch.seed.multidata["names"][0][patch.player - 1]
else:
pname = "unknown"
patch_data = update_patch_data(patch.data, server="")
patch_data = io.BytesIO(patch_data)

View File

@ -50,5 +50,5 @@ class Generation(db.Entity):
id = PrimaryKey(UUID, default=uuid4)
owner = Required(UUID)
options = Required(bytes, lazy=True) # these didn't work as JSON on mariaDB, so they're getting pickled now
meta = Required(bytes, lazy=True)
meta = Required(bytes, lazy=True) # if state is -1 (error) this will contain an utf-8 encoded error message
state = Required(int, default=0, index=True)

View File

@ -3,5 +3,5 @@ pony>=0.7.14
waitress>=1.4.4
flask-caching>=1.9.0
Flask-Autoversion>=0.2.0
Flask-Compress>=1.7.0
Flask-Compress>=1.8.0
Flask-Limiter>=1.4

View File

@ -165,7 +165,6 @@ item_pool:
normal: 50 # Item availability remains unchanged from vanilla game
hard: 0 # Reduced upgrade availability (max: 14 hearts, blue mail, tempered sword, fire shield, no silvers unless swordless)
expert: 0 # Minimum upgrade availability (max: 8 hearts, green mail, master sword, fighter shield, no silvers unless swordless)
crowd_control: 0 # Sets up the item pool for the crowd control extension. Do not use it without crowd control
item_functionality:
easy: 0 # Allow Hammer to damage ganon, Allow Hammer tablet collection, Allow swordless medallion use everywhere.
normal: 50 # Vanilla item functionality
@ -232,6 +231,22 @@ timer:
ohko: 0 # Timer always at zero. Permanent OHKO.
timed_countdown: 0 # Starts the clock with forty minutes. Same clocks as timed mode, but if the clock hits zero you lose. You can still keep playing, though.
display: 0 # Displays a timer, but otherwise does not affect gameplay or the item pool.
countdown_start_time: # For timed_ohko and timed_countdown timer modes, the amount of time in minutes to start with
0: 0 # For timed_ohko, starts in OHKO mode when starting the game
10: 50
20: 0
30: 0
60: 0
red_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a red clock
-2: 50
1: 0
blue_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a blue clock
1: 0
2: 50
green_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a green clock
4: 50
10: 0
15: 0
# Can be uncommented to use it
# local_items: # Force certain items to appear in your world only, not across the multiworld. Recognizes some group names, like "Swords"
# - "Moon Pearl"
@ -289,6 +304,9 @@ intensity: # Only available if the host uses the doors branch, it is ignored oth
2: 0 # And shuffles open edges and straight staircases
3: 0 # And shuffles dungeon lobbies
random: 0 # Picks one of those at random
key_drop_shuffle: # Only available if the host uses the doors branch, it is ignored otherwise
on: 0 # Enables the small keys dropped by enemies or under pots, and the big key dropped by the Ball & Chain guard to be shuffled into the pool. This extends the number of checks to 249.
off: 50
experimental: # Only available if the host uses the doors branch, it is ignored otherwise
on: 0 # Enables experimental features. Currently, this is just the dungeon keys in chest counter.
off: 50
@ -391,13 +409,3 @@ rom:
dizzy: 0
sick: 0
puke: 0
uw_palettes: # Change the colors of shields
default: 50 # No changes
random: 0 # Shuffle the colors
blackout: 0 # Never use this
grayscale: 0
negative: 0
classic: 0
dizzy: 0
sick: 0
puke: 0

View File

@ -124,7 +124,7 @@
<td>{{ player_names[(team, loop.index)]|e }}</td>
{%- for area in ordered_areas -%}
{%- set checks_done = checks[area] -%}
{%- set checks_total = checks_in_area[area] -%}
{%- set checks_total = checks_in_area[player][area] -%}
{%- if checks_done == checks_total -%}
<td class="item-acquired center-column">
{{ checks_done }}/{{ checks_total }}</td>

View File

@ -180,6 +180,25 @@ default_locations = {
60121, 60124, 60127, 1573217, 60130, 60133, 60136, 60139, 60142, 60145, 60148, 60151, 60157},
'Total': set()}
key_only_locations = {
'Light World': set(),
'Dark World': set(),
'Desert Palace': {0x140031, 0x14002b, 0x140061, 0x140028},
'Eastern Palace': {0x14005b, 0x140049},
'Hyrule Castle': {0x140037, 0x140034, 0x14000d, 0x14003d},
'Agahnims Tower': {0x140061, 0x140052},
'Tower of Hera': set(),
'Swamp Palace': {0x140019, 0x140016, 0x140013, 0x140010, 0x14000a},
'Thieves Town': {0x14005e, 0x14004f},
'Skull Woods': {0x14002e, 0x14001c},
'Ice Palace': {0x140004, 0x140022, 0x140025, 0x140046},
'Misery Mire': {0x140055, 0x14004c, 0x140064},
'Turtle Rock': {0x140058, 0x140007},
'Palace of Darkness': set(),
'Ganons Tower': {0x140040, 0x140043, 0x14003a, 0x14001f},
'Total': set()
}
key_locations = {"Desert Palace", "Eastern Palace", "Hyrule Castle", "Agahnims Tower", "Tower of Hera", "Swamp Palace",
"Thieves Town", "Skull Woods", "Ice Palace", "Misery Mire", "Turtle Rock", "Palace of Darkness",
"Ganons Tower"}
@ -191,6 +210,10 @@ for area, locations in default_locations.items():
for location in locations:
location_to_area[location] = area
for area, locations in key_only_locations.items():
for location in locations:
location_to_area[location] = area
checks_in_area = {area: len(checks) for area, checks in default_locations.items()}
checks_in_area["Total"] = 216
@ -235,6 +258,14 @@ def render_timedelta(delta: datetime.timedelta):
_multidata_cache = {}
def get_location_table(checks_table: dict) -> dict:
loc_to_area = {}
for area, locations in checks_table.items():
if area == "Total":
continue
for location in locations:
loc_to_area[location] = area
return loc_to_area
def get_static_room_data(room: Room):
result = _multidata_cache.get(room.seed.id, None)
@ -244,11 +275,30 @@ def get_static_room_data(room: Room):
# in > 100 players this can take a bit of time and is the main reason for the cache
locations = {tuple(k): tuple(v) for k, v in multidata['locations']}
names = multidata["names"]
seed_checks_in_area = checks_in_area.copy()
use_door_tracker = False
if "tags" in multidata:
use_door_tracker = "DR" in multidata["tags"]
result = locations, names, use_door_tracker
if use_door_tracker:
for area, checks in key_only_locations.items():
seed_checks_in_area[area] += len(checks)
seed_checks_in_area["Total"] = 249
if "checks_in_area" not in multidata:
player_checks_in_area = {playernumber: (seed_checks_in_area if use_door_tracker and
(0x140031, playernumber) in locations else checks_in_area)
for playernumber in range(1, len(names[0]) + 1)}
player_location_to_area = {playernumber: location_to_area
for playernumber in range(1, len(names[0]) + 1)}
else:
player_checks_in_area = {playernumber: {areaname: len(multidata["checks_in_area"][f'{playernumber}'][areaname])
if areaname != "Total" else multidata["checks_in_area"][f'{playernumber}']["Total"]
for areaname in ordered_areas}
for playernumber in range(1, len(names[0]) + 1)}
player_location_to_area = {playernumber: get_location_table(multidata["checks_in_area"][f'{playernumber}'])
for playernumber in range(1, len(names[0]) + 1)}
result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area
_multidata_cache[room.seed.id] = result
return result
@ -259,7 +309,7 @@ def getTracker(tracker: UUID):
room = Room.get(tracker=tracker)
if not room:
abort(404)
locations, names, use_door_tracker = get_static_room_data(room)
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area = get_static_room_data(room)
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1)}
for teamnumber, team in enumerate(names)}
@ -280,9 +330,12 @@ def getTracker(tracker: UUID):
for item_id in precollected:
attribute_item(inventory, team, player, item_id)
for location in locations_checked:
if (location, player) not in locations or location not in player_location_to_area[player]:
continue
item, recipient = locations[location, player]
attribute_item(inventory, team, recipient, item)
checks_done[team][player][location_to_area[location]] += 1
checks_done[team][player][player_location_to_area[player][location]] += 1
checks_done[team][player]["Total"] += 1
for (team, player), game_state in room.multisave.get("client_game_state", []):
@ -311,7 +364,7 @@ def getTracker(tracker: UUID):
lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names,
tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=icons,
multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas,
checks_in_area=checks_in_area, activity_timers=activity_timers,
checks_in_area=seed_checks_in_area, activity_timers=activity_timers,
key_locations=key_locations, small_key_ids=small_key_ids, big_key_ids=big_key_ids,
video=video, big_key_locations=key_locations if use_door_tracker else big_key_locations,
hints=hints, long_player_names = long_player_names)

2
data/sprites/alttpr/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More