Merge branch 'master' into website-redesign

This commit is contained in:
Chris Wilson 2020-11-23 17:49:53 -05:00
commit 016d09150d
23 changed files with 673 additions and 149 deletions

View File

@ -119,6 +119,10 @@ class World(object):
set_player_attr('treasure_hunt_icon', 'Triforce Piece') set_player_attr('treasure_hunt_icon', 'Triforce Piece')
set_player_attr('treasure_hunt_count', 0) set_player_attr('treasure_hunt_count', 0)
set_player_attr('clock_mode', False) 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('can_take_damage', True)
set_player_attr('glitch_boots', True) set_player_attr('glitch_boots', True)
set_player_attr('progression_balancing', True) set_player_attr('progression_balancing', True)

View File

@ -129,6 +129,14 @@ def parse_arguments(argv, no_defaults=False):
Timed mode. If time runs out, you lose (but can Timed mode. If time runs out, you lose (but can
still keep playing). 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'], parser.add_argument('--dungeon_counters', default=defval('default'), const='default', nargs='?', choices=['default', 'on', 'pickup', 'off'],
help='''\ help='''\
Select dungeon counter display settings. (default: %(default)s) Select dungeon counter display settings. (default: %(default)s)
@ -366,6 +374,7 @@ def parse_arguments(argv, no_defaults=False):
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality',
'shuffle', 'crystals_ganon', 'crystals_gt', 'open_pyramid', 'timer', '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', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
'local_items', 'retro', 'accessibility', 'hints', 'beemizer', 'local_items', 'retro', 'accessibility', 'hints', 'beemizer',
'shufflebosses', 'enemy_shuffle', 'enemy_health', 'enemy_damage', 'shufflepots', 'shufflebosses', 'enemy_shuffle', 'enemy_health', 'enemy_damage', 'shufflepots',

68
Gui.py
View File

@ -120,6 +120,56 @@ def guiMain(args=None):
balancingCheckbutton.pack(expand=True, anchor=W) balancingCheckbutton.pack(expand=True, anchor=W)
patchesCheckbutton.pack(expand=True, anchor=W) patchesCheckbutton.pack(expand=True, anchor=W)
timerOptionsFrame = LabelFrame(rightHalfFrame, text="Timer options")
for i in range(3):
timerOptionsFrame.columnconfigure(i, weight=1)
timerOptionsFrame.rowconfigure(i, weight=1)
timerModeFrame = Frame(timerOptionsFrame)
timerModeFrame.grid(row=0, column=0, columnspan=3, sticky=E, padx=3)
timerVar = StringVar()
timerVar.set('none')
timerModeMenu = OptionMenu(timerModeFrame, timerVar, 'none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown')
timerLabel = Label(timerModeFrame, text='Timer setting')
timerLabel.pack(side=LEFT)
timerModeMenu.pack(side=LEFT)
timerCountdownFrame = Frame(timerOptionsFrame)
timerCountdownFrame.grid(row=1, column=0, columnspan=3, sticky=E, padx=3)
timerCountdownLabel = Label(timerCountdownFrame, text='Countdown starting time')
timerCountdownLabel.pack(side=LEFT)
timerCountdownVar = IntVar(value=10)
timerCountdownSpinbox = Spinbox(timerCountdownFrame, from_=0, to=480, width=3, textvariable=timerCountdownVar)
timerCountdownSpinbox.pack(side=LEFT)
timerRedFrame = Frame(timerOptionsFrame)
timerRedFrame.grid(row=2, column=0, sticky=E, padx=3)
timerRedLabel = Label(timerRedFrame, text='Clock adjustments: Red')
timerRedLabel.pack(side=LEFT)
timerRedVar = IntVar(value=-2)
timerRedSpinbox = Spinbox(timerRedFrame, from_=-60, to=60, width=3, textvariable=timerRedVar)
timerRedSpinbox.pack(side=LEFT)
timerBlueFrame = Frame(timerOptionsFrame)
timerBlueFrame.grid(row=2, column=1, sticky=E, padx=3)
timerBlueLabel = Label(timerBlueFrame, text='Blue')
timerBlueLabel.pack(side=LEFT)
timerBlueVar = IntVar(value=2)
timerBlueSpinbox = Spinbox(timerBlueFrame, from_=-60, to=60, width=3, textvariable=timerBlueVar)
timerBlueSpinbox.pack(side=LEFT)
timerGreenFrame = Frame(timerOptionsFrame)
timerGreenFrame.grid(row=2, column=2, sticky=E, padx=3)
timerGreenLabel = Label(timerGreenFrame, text='Green')
timerGreenLabel.pack(side=LEFT)
timerGreenVar = IntVar(value=4)
timerGreenSpinbox = Spinbox(timerGreenFrame, from_=-60, to=60, width=3, textvariable=timerGreenVar)
timerGreenSpinbox.pack(side=LEFT)
romOptionsFrame = LabelFrame(rightHalfFrame, text="Rom options") romOptionsFrame = LabelFrame(rightHalfFrame, text="Rom options")
romOptionsFrame.columnconfigure(0, weight=1) romOptionsFrame.columnconfigure(0, weight=1)
romOptionsFrame.columnconfigure(1, weight=1) romOptionsFrame.columnconfigure(1, weight=1)
@ -266,6 +316,7 @@ def guiMain(args=None):
romSelectButton.pack(side=LEFT) romSelectButton.pack(side=LEFT)
checkBoxFrame.pack(side=TOP, anchor=W, padx=5, pady=10) checkBoxFrame.pack(side=TOP, anchor=W, padx=5, pady=10)
timerOptionsFrame.pack(expand=True, fill=BOTH, padx=3)
romOptionsFrame.pack(expand=True, fill=BOTH, padx=3) romOptionsFrame.pack(expand=True, fill=BOTH, padx=3)
drowDownFrame = Frame(topFrame) drowDownFrame = Frame(topFrame)
@ -344,14 +395,6 @@ def guiMain(args=None):
itemfunctionLabel = Label(itemfunctionFrame, text='Difficulty: item functionality') itemfunctionLabel = Label(itemfunctionFrame, text='Difficulty: item functionality')
itemfunctionLabel.pack(side=LEFT) itemfunctionLabel.pack(side=LEFT)
timerFrame = Frame(drowDownFrame)
timerVar = StringVar()
timerVar.set('none')
timerOptionMenu = OptionMenu(timerFrame, timerVar, 'none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown')
timerOptionMenu.pack(side=RIGHT)
timerLabel = Label(timerFrame, text='Timer setting')
timerLabel.pack(side=LEFT)
dungeonCounterFrame = Frame(drowDownFrame) dungeonCounterFrame = Frame(drowDownFrame)
dungeonCounterVar = StringVar() dungeonCounterVar = StringVar()
dungeonCounterVar.set('auto') dungeonCounterVar.set('auto')
@ -412,7 +455,6 @@ def guiMain(args=None):
swordFrame.pack(expand=True, anchor=E) swordFrame.pack(expand=True, anchor=E)
difficultyFrame.pack(expand=True, anchor=E) difficultyFrame.pack(expand=True, anchor=E)
itemfunctionFrame.pack(expand=True, anchor=E) itemfunctionFrame.pack(expand=True, anchor=E)
timerFrame.pack(expand=True, anchor=E)
dungeonCounterFrame.pack(expand=True, anchor=E) dungeonCounterFrame.pack(expand=True, anchor=E)
progressiveFrame.pack(expand=True, anchor=E) progressiveFrame.pack(expand=True, anchor=E)
accessibilityFrame.pack(expand=True, anchor=E) accessibilityFrame.pack(expand=True, anchor=E)
@ -538,6 +580,10 @@ def guiMain(args=None):
guiargs.difficulty = difficultyVar.get() guiargs.difficulty = difficultyVar.get()
guiargs.item_functionality = itemfunctionVar.get() guiargs.item_functionality = itemfunctionVar.get()
guiargs.timer = timerVar.get() guiargs.timer = timerVar.get()
guiargs.countdown_start_time = timerCountdownVar.get()
guiargs.red_clock_time = timerRedVar.get()
guiargs.blue_clock_time = timerBlueVar.get()
guiargs.green_clock_time = timerGreenVar.get()
guiargs.skip_progression_balancing = not balancingVar.get() guiargs.skip_progression_balancing = not balancingVar.get()
if guiargs.timer == "none": if guiargs.timer == "none":
guiargs.timer = False guiargs.timer = False
@ -1424,6 +1470,10 @@ def guiMain(args=None):
difficultyVar.set(args.difficulty) difficultyVar.set(args.difficulty)
itemfunctionVar.set(args.item_functionality) itemfunctionVar.set(args.item_functionality)
timerVar.set(args.timer) timerVar.set(args.timer)
timerCountdownVar.set(args.countdown_start_time)
timerRedVar.set(args.red_clock_time)
timerBlueVar.set(args.blue_clock_time)
timerGreenVar.set(args.green_clock_time)
progressiveVar.set(args.progressive) progressiveVar.set(args.progressive)
accessibilityVar.set(args.accessibility) accessibilityVar.set(args.accessibility)
goalVar.set(args.goal) goalVar.set(args.goal)

29
Main.py
View File

@ -72,6 +72,10 @@ def main(args, seed=None):
world.tile_shuffle = args.tile_shuffle.copy() world.tile_shuffle = args.tile_shuffle.copy()
world.beemizer = args.beemizer.copy() world.beemizer = args.beemizer.copy()
world.timer = args.timer.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.shufflepots = args.shufflepots.copy()
world.progressive = args.progressive.copy() world.progressive = args.progressive.copy()
world.dungeon_counters = args.dungeon_counters.copy() world.dungeon_counters = args.dungeon_counters.copy()
@ -297,6 +301,30 @@ def main(args, seed=None):
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name: if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
er_hint_data[region.player][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)] precollected_items = [[] for player in range(world.players)]
for item in world.precollected_items: for item in world.precollected_items:
precollected_items[item.player - 1].append(item.code) precollected_items[item.player - 1].append(item.code)
@ -323,6 +351,7 @@ def main(args, seed=None):
(location.item.code, location.item.player)) (location.item.code, location.item.player))
for location in world.get_filled_locations() if for location in world.get_filled_locations() if
type(location.address) is int], type(location.address) is int],
"checks_in_area": checks_in_area,
"server_options": get_options()["server_options"], "server_options": get_options()["server_options"],
"er_hint_data": er_hint_data, "er_hint_data": er_hint_data,
"precollected_items": precollected_items, "precollected_items": precollected_items,

View File

@ -85,14 +85,17 @@ class Context():
self.slot = None self.slot = None
self.player_names: typing.Dict[int: str] = {} self.player_names: typing.Dict[int: str] = {}
self.locations_checked = set() self.locations_checked = set()
self.unsafe_locations_checked = set()
self.locations_scouted = set() self.locations_scouted = set()
self.items_received = [] self.items_received = []
self.items_missing = []
self.locations_info = {} self.locations_info = {}
self.awaiting_rom = False self.awaiting_rom = False
self.rom = None self.rom = None
self.prev_rom = None self.prev_rom = None
self.auth = None self.auth = None
self.found_items = found_items self.found_items = found_items
self.send_unsafe = False
self.finished_game = False self.finished_game = False
self.slow_mode = False self.slow_mode = False
@ -195,23 +198,34 @@ location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
'Desert Palace - Map Chest': (0x74, 0x10), 'Desert Palace - Map Chest': (0x74, 0x10),
'Desert Palace - Compass Chest': (0x85, 0x10), 'Desert Palace - Compass Chest': (0x85, 0x10),
'Desert Palace - Big Key Chest': (0x75, 0x10), 'Desert Palace - Big Key Chest': (0x75, 0x10),
'Desert Palace - Desert Tiles 1 Pot Key': (0x63, 0x400),
'Desert Palace - Beamos Hall Pot Key': (0x53, 0x400),
'Desert Palace - Desert Tiles 2 Pot Key': (0x43, 0x400),
'Desert Palace - Boss': (0x33, 0x800), 'Desert Palace - Boss': (0x33, 0x800),
'Eastern Palace - Compass Chest': (0xa8, 0x10), 'Eastern Palace - Compass Chest': (0xa8, 0x10),
'Eastern Palace - Big Chest': (0xa9, 0x10), 'Eastern Palace - Big Chest': (0xa9, 0x10),
'Eastern Palace - Dark Square Pot Key': (0xba, 0x400),
'Eastern Palace - Dark Eyegore Key Drop': (0x99, 0x400),
'Eastern Palace - Cannonball Chest': (0xb9, 0x10), 'Eastern Palace - Cannonball Chest': (0xb9, 0x10),
'Eastern Palace - Big Key Chest': (0xb8, 0x10), 'Eastern Palace - Big Key Chest': (0xb8, 0x10),
'Eastern Palace - Map Chest': (0xaa, 0x10), 'Eastern Palace - Map Chest': (0xaa, 0x10),
'Eastern Palace - Boss': (0xc8, 0x800), 'Eastern Palace - Boss': (0xc8, 0x800),
'Hyrule Castle - Boomerang Chest': (0x71, 0x10), 'Hyrule Castle - Boomerang Chest': (0x71, 0x10),
'Hyrule Castle - Boomerang Guard Key Drop': (0x71, 0x400),
'Hyrule Castle - Map Chest': (0x72, 0x10), 'Hyrule Castle - Map Chest': (0x72, 0x10),
'Hyrule Castle - Map Guard Key Drop': (0x72, 0x400),
"Hyrule Castle - Zelda's Chest": (0x80, 0x10), "Hyrule Castle - Zelda's Chest": (0x80, 0x10),
'Hyrule Castle - Big Key Drop': (0x80, 0x400),
'Sewers - Dark Cross': (0x32, 0x10), 'Sewers - Dark Cross': (0x32, 0x10),
'Hyrule Castle - Key Rat Key Drop': (0x21, 0x400),
'Sewers - Secret Room - Left': (0x11, 0x10), 'Sewers - Secret Room - Left': (0x11, 0x10),
'Sewers - Secret Room - Middle': (0x11, 0x20), 'Sewers - Secret Room - Middle': (0x11, 0x20),
'Sewers - Secret Room - Right': (0x11, 0x40), 'Sewers - Secret Room - Right': (0x11, 0x40),
'Sanctuary': (0x12, 0x10), 'Sanctuary': (0x12, 0x10),
'Castle Tower - Room 03': (0xe0, 0x10), 'Castle Tower - Room 03': (0xe0, 0x10),
'Castle Tower - Dark Maze': (0xd0, 0x10), 'Castle Tower - Dark Maze': (0xd0, 0x10),
'Castle Tower - Dark Archer Key Drop': (0xc0, 0x400),
'Castle Tower - Circle of Pots Key Drop': (0xb0, 0x400),
'Spectacle Rock Cave': (0xea, 0x400), 'Spectacle Rock Cave': (0xea, 0x400),
'Paradox Cave Lower - Far Left': (0xef, 0x10), 'Paradox Cave Lower - Far Left': (0xef, 0x10),
'Paradox Cave Lower - Left': (0xef, 0x20), 'Paradox Cave Lower - Left': (0xef, 0x20),
@ -250,18 +264,25 @@ location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
'Mimic Cave': (0x10c, 0x10), 'Mimic Cave': (0x10c, 0x10),
'Swamp Palace - Entrance': (0x28, 0x10), 'Swamp Palace - Entrance': (0x28, 0x10),
'Swamp Palace - Map Chest': (0x37, 0x10), 'Swamp Palace - Map Chest': (0x37, 0x10),
'Swamp Palace - Pot Row Pot Key': (0x38, 0x400),
'Swamp Palace - Trench 1 Pot Key': (0x37, 0x400),
'Swamp Palace - Hookshot Pot Key': (0x36, 0x400),
'Swamp Palace - Big Chest': (0x36, 0x10), 'Swamp Palace - Big Chest': (0x36, 0x10),
'Swamp Palace - Compass Chest': (0x46, 0x10), 'Swamp Palace - Compass Chest': (0x46, 0x10),
'Swamp Palace - Trench 2 Pot Key': (0x35, 0x400),
'Swamp Palace - Big Key Chest': (0x35, 0x10), 'Swamp Palace - Big Key Chest': (0x35, 0x10),
'Swamp Palace - West Chest': (0x34, 0x10), 'Swamp Palace - West Chest': (0x34, 0x10),
'Swamp Palace - Flooded Room - Left': (0x76, 0x10), 'Swamp Palace - Flooded Room - Left': (0x76, 0x10),
'Swamp Palace - Flooded Room - Right': (0x76, 0x20), 'Swamp Palace - Flooded Room - Right': (0x76, 0x20),
'Swamp Palace - Waterfall Room': (0x66, 0x10), 'Swamp Palace - Waterfall Room': (0x66, 0x10),
'Swamp Palace - Waterway Pot Key': (0x16, 0x400),
'Swamp Palace - Boss': (0x6, 0x800), 'Swamp Palace - Boss': (0x6, 0x800),
"Thieves' Town - Big Key Chest": (0xdb, 0x20), "Thieves' Town - Big Key Chest": (0xdb, 0x20),
"Thieves' Town - Map Chest": (0xdb, 0x10), "Thieves' Town - Map Chest": (0xdb, 0x10),
"Thieves' Town - Compass Chest": (0xdc, 0x10), "Thieves' Town - Compass Chest": (0xdc, 0x10),
"Thieves' Town - Ambush Chest": (0xcb, 0x10), "Thieves' Town - Ambush Chest": (0xcb, 0x10),
"Thieves' Town - Hallway Pot Key": (0xbc, 0x400),
"Thieves' Town - Spike Switch Pot Key": (0xab, 0x400),
"Thieves' Town - Attic": (0x65, 0x10), "Thieves' Town - Attic": (0x65, 0x10),
"Thieves' Town - Big Chest": (0x44, 0x10), "Thieves' Town - Big Chest": (0x44, 0x10),
"Thieves' Town - Blind's Cell": (0x45, 0x10), "Thieves' Town - Blind's Cell": (0x45, 0x10),
@ -272,28 +293,39 @@ location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
'Skull Woods - Pot Prison': (0x57, 0x20), 'Skull Woods - Pot Prison': (0x57, 0x20),
'Skull Woods - Pinball Room': (0x68, 0x10), 'Skull Woods - Pinball Room': (0x68, 0x10),
'Skull Woods - Big Key Chest': (0x57, 0x10), 'Skull Woods - Big Key Chest': (0x57, 0x10),
'Skull Woods - West Lobby Pot Key': (0x56, 0x400),
'Skull Woods - Bridge Room': (0x59, 0x10), 'Skull Woods - Bridge Room': (0x59, 0x10),
'Skull Woods - Spike Corner Key Drop': (0x39, 0x400),
'Skull Woods - Boss': (0x29, 0x800), 'Skull Woods - Boss': (0x29, 0x800),
'Ice Palace - Jelly Key Drop': (0x0e, 0x400),
'Ice Palace - Compass Chest': (0x2e, 0x10), 'Ice Palace - Compass Chest': (0x2e, 0x10),
'Ice Palace - Conveyor Key Drop': (0x3e, 0x400),
'Ice Palace - Freezor Chest': (0x7e, 0x10), 'Ice Palace - Freezor Chest': (0x7e, 0x10),
'Ice Palace - Big Chest': (0x9e, 0x10), 'Ice Palace - Big Chest': (0x9e, 0x10),
'Ice Palace - Iced T Room': (0xae, 0x10), 'Ice Palace - Iced T Room': (0xae, 0x10),
'Ice Palace - Many Pots Pot Key': (0x9f, 0x400),
'Ice Palace - Spike Room': (0x5f, 0x10), 'Ice Palace - Spike Room': (0x5f, 0x10),
'Ice Palace - Big Key Chest': (0x1f, 0x10), 'Ice Palace - Big Key Chest': (0x1f, 0x10),
'Ice Palace - Hammer Block Key Drop': (0x3f, 0x400),
'Ice Palace - Map Chest': (0x3f, 0x10), 'Ice Palace - Map Chest': (0x3f, 0x10),
'Ice Palace - Boss': (0xde, 0x800), 'Ice Palace - Boss': (0xde, 0x800),
'Misery Mire - Big Chest': (0xc3, 0x10), 'Misery Mire - Big Chest': (0xc3, 0x10),
'Misery Mire - Map Chest': (0xc3, 0x20), 'Misery Mire - Map Chest': (0xc3, 0x20),
'Misery Mire - Main Lobby': (0xc2, 0x10), 'Misery Mire - Main Lobby': (0xc2, 0x10),
'Misery Mire - Bridge Chest': (0xa2, 0x10), 'Misery Mire - Bridge Chest': (0xa2, 0x10),
'Misery Mire - Spikes Pot Key': (0xb3, 0x400),
'Misery Mire - Spike Chest': (0xb3, 0x10), 'Misery Mire - Spike Chest': (0xb3, 0x10),
'Misery Mire - Fishbone Pot Key': (0xa1, 0x400),
'Misery Mire - Conveyor Crystal Key Drop': (0xc1, 0x400),
'Misery Mire - Compass Chest': (0xc1, 0x10), 'Misery Mire - Compass Chest': (0xc1, 0x10),
'Misery Mire - Big Key Chest': (0xd1, 0x10), 'Misery Mire - Big Key Chest': (0xd1, 0x10),
'Misery Mire - Boss': (0x90, 0x800), 'Misery Mire - Boss': (0x90, 0x800),
'Turtle Rock - Compass Chest': (0xd6, 0x10), 'Turtle Rock - Compass Chest': (0xd6, 0x10),
'Turtle Rock - Roller Room - Left': (0xb7, 0x10), 'Turtle Rock - Roller Room - Left': (0xb7, 0x10),
'Turtle Rock - Roller Room - Right': (0xb7, 0x20), 'Turtle Rock - Roller Room - Right': (0xb7, 0x20),
'Turtle Rock - Pokey 1 Key Drop': (0xb6, 0x400),
'Turtle Rock - Chain Chomps': (0xb6, 0x10), 'Turtle Rock - Chain Chomps': (0xb6, 0x10),
'Turtle Rock - Pokey 2 Key Drop': (0x13, 0x400),
'Turtle Rock - Big Key Chest': (0x14, 0x10), 'Turtle Rock - Big Key Chest': (0x14, 0x10),
'Turtle Rock - Big Chest': (0x24, 0x10), 'Turtle Rock - Big Chest': (0x24, 0x10),
'Turtle Rock - Crystaroller Room': (0x4, 0x10), 'Turtle Rock - Crystaroller Room': (0x4, 0x10),
@ -316,6 +348,7 @@ location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
'Palace of Darkness - Big Chest': (0x1a, 0x10), 'Palace of Darkness - Big Chest': (0x1a, 0x10),
'Palace of Darkness - Harmless Hellway': (0x1a, 0x40), 'Palace of Darkness - Harmless Hellway': (0x1a, 0x40),
'Palace of Darkness - Boss': (0x5a, 0x800), 'Palace of Darkness - Boss': (0x5a, 0x800),
'Ganons Tower - Conveyor Cross Pot Key': (0x8b, 0x400),
"Ganons Tower - Bob's Torch": (0x8c, 0x400), "Ganons Tower - Bob's Torch": (0x8c, 0x400),
'Ganons Tower - Hope Room - Left': (0x8c, 0x20), 'Ganons Tower - Hope Room - Left': (0x8c, 0x20),
'Ganons Tower - Hope Room - Right': (0x8c, 0x40), 'Ganons Tower - Hope Room - Right': (0x8c, 0x40),
@ -324,11 +357,13 @@ location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
'Ganons Tower - Compass Room - Top Right': (0x9d, 0x20), 'Ganons Tower - Compass Room - Top Right': (0x9d, 0x20),
'Ganons Tower - Compass Room - Bottom Left': (0x9d, 0x40), 'Ganons Tower - Compass Room - Bottom Left': (0x9d, 0x40),
'Ganons Tower - Compass Room - Bottom Right': (0x9d, 0x80), 'Ganons Tower - Compass Room - Bottom Right': (0x9d, 0x80),
'Ganons Tower - Conveyor Star Pits Pot Key': (0x7b, 0x400),
'Ganons Tower - DMs Room - Top Left': (0x7b, 0x10), 'Ganons Tower - DMs Room - Top Left': (0x7b, 0x10),
'Ganons Tower - DMs Room - Top Right': (0x7b, 0x20), 'Ganons Tower - DMs Room - Top Right': (0x7b, 0x20),
'Ganons Tower - DMs Room - Bottom Left': (0x7b, 0x40), 'Ganons Tower - DMs Room - Bottom Left': (0x7b, 0x40),
'Ganons Tower - DMs Room - Bottom Right': (0x7b, 0x80), 'Ganons Tower - DMs Room - Bottom Right': (0x7b, 0x80),
'Ganons Tower - Map Chest': (0x8b, 0x10), 'Ganons Tower - Map Chest': (0x8b, 0x10),
'Ganons Tower - Double Switch Pot Key': (0x9b, 0x400),
'Ganons Tower - Firesnake Room': (0x7d, 0x10), 'Ganons Tower - Firesnake Room': (0x7d, 0x10),
'Ganons Tower - Randomizer Room - Top Left': (0x7c, 0x10), 'Ganons Tower - Randomizer Room - Top Left': (0x7c, 0x10),
'Ganons Tower - Randomizer Room - Top Right': (0x7c, 0x20), 'Ganons Tower - Randomizer Room - Top Right': (0x7c, 0x20),
@ -341,6 +376,7 @@ location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
'Ganons Tower - Big Key Chest': (0x1c, 0x10), 'Ganons Tower - Big Key Chest': (0x1c, 0x10),
'Ganons Tower - Mini Helmasaur Room - Left': (0x3d, 0x10), 'Ganons Tower - Mini Helmasaur Room - Left': (0x3d, 0x10),
'Ganons Tower - Mini Helmasaur Room - Right': (0x3d, 0x20), 'Ganons Tower - Mini Helmasaur Room - Right': (0x3d, 0x20),
'Ganons Tower - Mini Helmasaur Key Drop': (0x3d, 0x400),
'Ganons Tower - Pre-Moldorm Chest': (0x3d, 0x40), 'Ganons Tower - Pre-Moldorm Chest': (0x3d, 0x40),
'Ganons Tower - Validation Chest': (0x4d, 0x10)} 'Ganons Tower - Validation Chest': (0x4d, 0x10)}
location_table_npc = {'Mushroom': 0x1000, location_table_npc = {'Mushroom': 0x1000,
@ -816,19 +852,28 @@ async def process_server_cmd(ctx: Context, cmd, args):
raise Exception('Connection refused by the multiworld host') raise Exception('Connection refused by the multiworld host')
elif cmd == 'Connected': elif cmd == 'Connected':
if ctx.send_unsafe:
ctx.send_unsafe = False
ctx.ui_node.log_info(f'Turning off sending of ALL location checks not declared as missing. If you want it on, please use /send_unsafe true')
Utils.persistent_store("servers", "default", ctx.server_address) Utils.persistent_store("servers", "default", ctx.server_address)
Utils.persistent_store("servers", ctx.rom, ctx.server_address) Utils.persistent_store("servers", ctx.rom, ctx.server_address)
ctx.team, ctx.slot = args[0] ctx.team, ctx.slot = args[0]
ctx.player_names = {p: n for p, n in args[1]} ctx.player_names = {p: n for p, n in args[1]}
msgs = [] msgs = []
if ctx.locations_checked: if ctx.locations_checked:
msgs.append(['LocationChecks', [Regions.location_table[loc][0] for loc in ctx.locations_checked]]) msgs.append(['LocationChecks', [Regions.lookup_name_to_id[loc] for loc in ctx.locations_checked]])
if ctx.locations_scouted: if ctx.locations_scouted:
msgs.append(['LocationScouts', list(ctx.locations_scouted)]) msgs.append(['LocationScouts', list(ctx.locations_scouted)])
if msgs: if msgs:
await ctx.send_msgs(msgs) await ctx.send_msgs(msgs)
if ctx.finished_game: if ctx.finished_game:
await send_finished_game(ctx) await send_finished_game(ctx)
ctx.items_missing = args[2] if len(args) >= 3 else [] # Get the server side view of missing as of time of connecting.
# This list is used to only send to the server what is reported as ACTUALLY Missing.
# This also serves to allow an easy visual of what locations were already checked previously
# when /missing is used for the client side view of what is missing.
if not ctx.items_missing:
asyncio.create_task(ctx.send_msgs([['Say', '!missing']]))
elif cmd == 'ReceivedItems': elif cmd == 'ReceivedItems':
start_index, items = args start_index, items = args
@ -837,7 +882,7 @@ async def process_server_cmd(ctx: Context, cmd, args):
elif start_index != len(ctx.items_received): elif start_index != len(ctx.items_received):
sync_msg = [['Sync']] sync_msg = [['Sync']]
if ctx.locations_checked: if ctx.locations_checked:
sync_msg.append(['LocationChecks', [Regions.location_table[loc][0] for loc in ctx.locations_checked]]) sync_msg.append(['LocationChecks', [Regions.lookup_name_to_id[loc] for loc in ctx.locations_checked]])
await ctx.send_msgs(sync_msg) await ctx.send_msgs(sync_msg)
if start_index == len(ctx.items_received): if start_index == len(ctx.items_received):
for item in items: for item in items:
@ -878,9 +923,11 @@ async def process_server_cmd(ctx: Context, cmd, args):
elif cmd == 'Missing': elif cmd == 'Missing':
if 'locations' in args: if 'locations' in args:
locations = json.loads(args['locations']) locations = json.loads(args['locations'])
for location in locations: if ctx.items_missing:
ctx.ui_node.log_info(f'Missing: {location}') for location in locations:
ctx.ui_node.log_info(f'Found {len(locations)} missing location checks') ctx.ui_node.log_info(f'Missing: {location}')
ctx.ui_node.log_info(f'Found {len(locations)} missing location checks')
ctx.items_missing = [location for location in locations]
elif cmd == 'Hint': elif cmd == 'Hint':
hints = [Utils.Hint(*hint) for hint in args] hints = [Utils.Hint(*hint) for hint in args]
@ -1009,13 +1056,34 @@ class ClientCommandProcessor(CommandProcessor):
def _cmd_missing(self) -> bool: def _cmd_missing(self) -> bool:
"""List all missing location checks, from your local game state""" """List all missing location checks, from your local game state"""
count = 0 count = 0
checked_count = 0
for location in [k for k, v in Regions.location_table.items() if type(v[0]) is int]: for location in [k for k, v in Regions.location_table.items() if type(v[0]) is int]:
if location not in self.ctx.locations_checked: if location not in self.ctx.locations_checked:
self.output('Missing: ' + location) if location not in self.ctx.items_missing:
self.output('Checked: ' + location)
checked_count += 1
else:
self.output('Missing: ' + location)
count += 1 count += 1
key_drop_count = 0
for location in [k for k, v in Regions.key_drop_data.items()]:
if location not in self.ctx.items_missing:
key_drop_count += 1
# No point on reporting on missing key drop locations if the server doesn't declare ANY of them missing.
if key_drop_count != len(Regions.key_drop_data.items()):
for location in [k for k, v in Regions.key_drop_data.items()]:
if location not in self.ctx.locations_checked:
if location not in self.ctx.items_missing:
self.output('Checked: ' + location)
key_drop_count += 1
else:
self.output('Missing: ' + location)
count += 1
if count: if count:
self.output(f"Found {count} missing location checks") self.output(f"Found {count} missing location checks{f'. {checked_count} locations checks previously visited.' if checked_count else ''}")
else: else:
self.output("No missing location checks found.") self.output("No missing location checks found.")
return True return True
@ -1045,6 +1113,15 @@ class ClientCommandProcessor(CommandProcessor):
else: else:
self.output("Web UI was never started.") self.output("Web UI was never started.")
def _cmd_send_unsafe(self, toggle: str = ""):
"""Force sending of locations the server did not specify was actually missing. WARNING: This may brick online trackers. Turned off on reconnect."""
if toggle:
self.ctx.send_unsafe = toggle.lower() in {"1", "true", "on"}
self.ctx.ui_node.log_info(f'Turning {("on" if self.ctx.send_unsafe else "off")} the option to send ALL location checks to the multiserver.')
else:
self.ctx.ui_node.log_info("You must specify /send_unsafe true explicitly.")
self.ctx.send_unsafe = False
def default(self, raw: str): def default(self, raw: str):
asyncio.create_task(self.ctx.send_msgs([['Say', raw]])) asyncio.create_task(self.ctx.send_msgs([['Say', raw]]))
@ -1074,20 +1151,22 @@ async def track_locations(ctx : Context, roomid, roomdata):
new_locations = [] new_locations = []
def new_check(location): def new_check(location):
ctx.locations_checked.add(location) ctx.unsafe_locations_checked.add(location)
ctx.ui_node.log_info("New check: %s (%d/216)" % (location, len(ctx.locations_checked))) ctx.ui_node.log_info("New check: %s (%d/216)" % (location, len(ctx.unsafe_locations_checked)))
ctx.ui_node.send_location_check(ctx, location) ctx.ui_node.send_location_check(ctx, location)
new_locations.append(Regions.location_table[location][0])
for location, (loc_roomid, loc_mask) in location_table_uw.items(): for location, (loc_roomid, loc_mask) in location_table_uw.items():
if location not in ctx.locations_checked and loc_roomid == roomid and (roomdata << 4) & loc_mask != 0: try:
new_check(location) if location not in ctx.unsafe_locations_checked and loc_roomid == roomid and (roomdata << 4) & loc_mask != 0:
new_check(location)
except Exception as e:
ctx.ui_node.log_info(f"Exception: {e}")
uw_begin = 0x129 uw_begin = 0x129
uw_end = 0 uw_end = 0
uw_unchecked = {} uw_unchecked = {}
for location, (roomid, mask) in location_table_uw.items(): for location, (roomid, mask) in location_table_uw.items():
if location not in ctx.locations_checked: if location not in ctx.unsafe_locations_checked:
uw_unchecked[location] = (roomid, mask) uw_unchecked[location] = (roomid, mask)
uw_begin = min(uw_begin, roomid) uw_begin = min(uw_begin, roomid)
uw_end = max(uw_end, roomid + 1) uw_end = max(uw_end, roomid + 1)
@ -1104,7 +1183,7 @@ async def track_locations(ctx : Context, roomid, roomdata):
ow_end = 0 ow_end = 0
ow_unchecked = {} ow_unchecked = {}
for location, screenid in location_table_ow.items(): for location, screenid in location_table_ow.items():
if location not in ctx.locations_checked: if location not in ctx.unsafe_locations_checked:
ow_unchecked[location] = screenid ow_unchecked[location] = screenid
ow_begin = min(ow_begin, screenid) ow_begin = min(ow_begin, screenid)
ow_end = max(ow_end, screenid + 1) ow_end = max(ow_end, screenid + 1)
@ -1115,22 +1194,27 @@ async def track_locations(ctx : Context, roomid, roomdata):
if ow_data[screenid - ow_begin] & 0x40 != 0: if ow_data[screenid - ow_begin] & 0x40 != 0:
new_check(location) new_check(location)
if not all([location in ctx.locations_checked for location in location_table_npc.keys()]): if not all([location in ctx.unsafe_locations_checked for location in location_table_npc.keys()]):
npc_data = await snes_read(ctx, SAVEDATA_START + 0x410, 2) npc_data = await snes_read(ctx, SAVEDATA_START + 0x410, 2)
if npc_data is not None: if npc_data is not None:
npc_value = npc_data[0] | (npc_data[1] << 8) npc_value = npc_data[0] | (npc_data[1] << 8)
for location, mask in location_table_npc.items(): for location, mask in location_table_npc.items():
if npc_value & mask != 0 and location not in ctx.locations_checked: if npc_value & mask != 0 and location not in ctx.unsafe_locations_checked:
new_check(location) new_check(location)
if not all([location in ctx.locations_checked for location in location_table_misc.keys()]): if not all([location in ctx.unsafe_locations_checked for location in location_table_misc.keys()]):
misc_data = await snes_read(ctx, SAVEDATA_START + 0x3c6, 4) misc_data = await snes_read(ctx, SAVEDATA_START + 0x3c6, 4)
if misc_data is not None: if misc_data is not None:
for location, (offset, mask) in location_table_misc.items(): for location, (offset, mask) in location_table_misc.items():
assert(0x3c6 <= offset <= 0x3c9) assert(0x3c6 <= offset <= 0x3c9)
if misc_data[offset - 0x3c6] & mask != 0 and location not in ctx.locations_checked: if misc_data[offset - 0x3c6] & mask != 0 and location not in ctx.unsafe_locations_checked:
new_check(location) new_check(location)
for location in ctx.unsafe_locations_checked:
if (location in ctx.items_missing and location not in ctx.locations_checked) or ctx.send_unsafe:
ctx.locations_checked.add(location)
new_locations.append(Regions.lookup_name_to_id[location])
await ctx.send_msgs([['LocationChecks', new_locations]]) await ctx.send_msgs([['LocationChecks', new_locations]])
@ -1161,6 +1245,7 @@ async def game_watcher(ctx : Context):
ctx.rom = rom.decode() ctx.rom = rom.decode()
if not ctx.prev_rom or ctx.prev_rom != ctx.rom: if not ctx.prev_rom or ctx.prev_rom != ctx.rom:
ctx.locations_checked = set() ctx.locations_checked = set()
ctx.unsafe_locations_checked = set()
ctx.locations_scouted = set() ctx.locations_scouted = set()
ctx.prev_rom = ctx.rom ctx.prev_rom = ctx.rom

View File

@ -29,7 +29,7 @@ import Utils
from Utils import get_item_name_from_id, get_location_name_from_address, ReceivedItem, _version_tuple from Utils import get_item_name_from_id, get_location_name_from_address, ReceivedItem, _version_tuple
from NetUtils import Node, Endpoint from NetUtils import Node, Endpoint
console_names = frozenset(set(Items.item_table) | set(Regions.location_table) | set(Items.item_name_groups)) console_names = frozenset(set(Items.item_table) | set(Regions.location_table) | set(Items.item_name_groups) | set(Regions.key_drop_data))
CLIENT_PLAYING = 0 CLIENT_PLAYING = 0
CLIENT_GOAL = 1 CLIENT_GOAL = 1
@ -439,6 +439,7 @@ def send_new_items(ctx: Context):
def forfeit_player(ctx: Context, team: int, slot: int): def forfeit_player(ctx: Context, team: int, slot: int):
all_locations = {values[0] for values in Regions.location_table.values() if type(values[0]) is int} all_locations = {values[0] for values in Regions.location_table.values() if type(values[0]) is int}
all_locations.update({values[1] for values in Regions.key_drop_data.values()})
ctx.notify_all("%s (Team #%d) has forfeited" % (ctx.player_names[(team, slot)], team + 1)) ctx.notify_all("%s (Team #%d) has forfeited" % (ctx.player_names[(team, slot)], team + 1))
register_location_checks(ctx, team, slot, all_locations) register_location_checks(ctx, team, slot, all_locations)
@ -454,10 +455,12 @@ def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]:
def register_location_checks(ctx: Context, team: int, slot: int, locations): def register_location_checks(ctx: Context, team: int, slot: int, locations):
found_items = False found_items = False
new_locations = set(locations) - ctx.location_checks[team, slot] new_locations = set(locations) - ctx.location_checks[team, slot]
known_locations = set()
if new_locations: if new_locations:
ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc) ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc)
for location in new_locations: for location in new_locations:
if (location, slot) in ctx.locations: if (location, slot) in ctx.locations:
known_locations.add(location)
target_item, target_player = ctx.locations[(location, slot)] target_item, target_player = ctx.locations[(location, slot)]
if target_player != slot or slot in ctx.remote_items: if target_player != slot or slot in ctx.remote_items:
found = False found = False
@ -482,7 +485,7 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations):
if client.team == team and client.wants_item_notification: if client.team == team and client.wants_item_notification:
asyncio.create_task( asyncio.create_task(
ctx.send_msgs(client, [['ItemFound', (target_item, location, slot)]])) ctx.send_msgs(client, [['ItemFound', (target_item, location, slot)]]))
ctx.location_checks[team, slot] |= new_locations ctx.location_checks[team, slot] |= known_locations
send_new_items(ctx) send_new_items(ctx)
if found_items: if found_items:
@ -514,7 +517,7 @@ def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[
def collect_hints_location(ctx: Context, team: int, slot: int, location: str) -> typing.List[Utils.Hint]: def collect_hints_location(ctx: Context, team: int, slot: int, location: str) -> typing.List[Utils.Hint]:
hints = [] hints = []
seeked_location = Regions.location_table[location][0] seeked_location = Regions.lookup_name_to_id[location]
for check, result in ctx.locations.items(): for check, result in ctx.locations.items():
location_id, finding_player = check location_id, finding_player = check
if finding_player == slot and location_id == seeked_location: if finding_player == slot and location_id == seeked_location:
@ -806,10 +809,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
def _cmd_missing(self) -> bool: def _cmd_missing(self) -> bool:
"""List all missing location checks from the server's perspective""" """List all missing location checks from the server's perspective"""
locations = [] locations = get_missing_checks(self.ctx, self.client)
for location_id, location_name in Regions.lookup_id_to_name.items(): # cheat console is -1, keep in mind
if location_id != -1 and location_id not in self.ctx.location_checks[self.client.team, self.client.slot]:
locations.append(location_name)
if len(locations) > 0: if len(locations) > 0:
if self.client.version < [2, 3, 0]: if self.client.version < [2, 3, 0]:
@ -938,6 +938,15 @@ class ClientMessageProcessor(CommonCommandProcessor):
self.output(response) self.output(response)
return False return False
def get_missing_checks(ctx: Context, client: Client) -> list:
locations = []
#for location_id in [k[0] for k, v in ctx.locations if k[1] == client.slot]:
# if location_id not in ctx.location_checks[client.team, client.slot]:
# locations.append(Regions.lookup_id_to_name.get(location_id, f'Unknown Location ID: {location_id}'))
for location_id, location_name in Regions.lookup_id_to_name.items(): # cheat console is -1, keep in mind
if location_id != -1 and location_id not in ctx.location_checks[client.team, client.slot] and (location_id, client.slot) in ctx.locations:
locations.append(location_name)
return locations
def get_client_points(ctx: Context, client: Client) -> int: def get_client_points(ctx: Context, client: Client) -> int:
return (ctx.location_check_points * len(ctx.location_checks[client.team, client.slot]) - return (ctx.location_check_points * len(ctx.location_checks[client.team, client.slot]) -
@ -993,7 +1002,7 @@ async def process_client_cmd(ctx: Context, client: Client, cmd, args):
client.tags = args.get('tags', Client.tags) client.tags = args.get('tags', Client.tags)
reply = [['Connected', [(client.team, client.slot), reply = [['Connected', [(client.team, client.slot),
[(p, ctx.get_aliased_name(t, p)) for (t, p), n in ctx.player_names.items() if [(p, ctx.get_aliased_name(t, p)) for (t, p), n in ctx.player_names.items() if
t == client.team]]]] t == client.team], get_missing_checks(ctx, client)]]]
items = get_received_items(ctx, client.team, client.slot) items = get_received_items(ctx, client.team, client.slot)
if items: if items:
reply.append(['ReceivedItems', (0, tuplize_received_items(items))]) reply.append(['ReceivedItems', (0, tuplize_received_items(items))])

View File

@ -444,6 +444,11 @@ def roll_settings(weights):
'timed_countdown': 'timed-countdown', 'timed_countdown': 'timed-countdown',
'display': 'display'}[get_choice('timer', weights, False)] '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.dungeon_counters = get_choice('dungeon_counters', weights, 'default')
ret.progressive = convert_to_on_off(get_choice('progressive', weights, 'on')) ret.progressive = convert_to_on_off(get_choice('progressive', weights, 'on'))

View File

@ -397,6 +397,59 @@ shop_table = {
'Capacity Upgrade': (0x0115, ShopType.UpgradeShop, 0x04, True, True, [('Bomb Upgrade (+5)', 100, 7), ('Arrow Upgrade (+5)', 100, 7)]) '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'), location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'),
'Bottle Merchant': (0x2eb18, 0x186339, False, 'with a merchant'), 'Bottle Merchant': (0x2eb18, 0x186339, False, 'with a merchant'),
'Flute Spot': (0x18014a, 0x18633d, False, 'underground'), 'Flute Spot': (0x18014a, 0x18633d, False, 'underground'),
@ -640,7 +693,9 @@ location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'),
[0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], None, True, 'Turtle Rock')} [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 = {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', 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', 1573194: 'Kings Grave Inner Rocks', 1573189: 'Kings Grave Inner Rocks',
@ -745,7 +800,28 @@ lookup_vanilla_location_to_entrance = {1572883: 'Kings Grave Inner Rocks', 19125
60103: 'Ganons Tower', 60106: 'Ganons Tower', 60109: 'Ganons Tower', 60103: 'Ganons Tower', 60106: 'Ganons Tower', 60109: 'Ganons Tower',
60127: 'Ganons Tower', 60118: 'Ganons Tower', 60148: 'Ganons Tower', 60127: 'Ganons Tower', 60118: 'Ganons Tower', 60148: 'Ganons Tower',
60151: 'Ganons Tower', 60145: 'Ganons Tower', 60157: '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_prizes = {location for location in location_table if location.endswith(" - Prize")}
lookup_boss_drops = {location for location in location_table if location.endswith(" - Boss")} lookup_boss_drops = {location for location in location_table if location.endswith(" - Boss")}

80
Rom.py
View File

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
JAP10HASH = '03a63945398191337e896e5771f77173' JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = '0fc63d72970ab96ffb18699f4d12a594' RANDOMIZERBASEHASH = 'e3714804e3fae1c6ac6100b94d1aee62'
import io import io
import json import json
@ -19,7 +19,7 @@ from typing import Optional
from BaseClasses import CollectionState, ShopType, Region, Location from BaseClasses import CollectionState, ShopType, Region, Location
from Dungeons import dungeon_music_addresses from Dungeons import dungeon_music_addresses
from Regions import location_table 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
from Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, \ from Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, \
BombShop2_texts, junk_texts BombShop2_texts, junk_texts
@ -93,8 +93,10 @@ class LocalRom(object):
self.write_bytes(0x186140, [0] * 0x150) self.write_bytes(0x186140, [0] * 0x150)
self.write_bytes(0x186140 + 0x150, itemplayertable) self.write_bytes(0x186140 + 0x150, itemplayertable)
self.encrypt_range(0x186140 + 0x150, 168, key) self.encrypt_range(0x186140 + 0x150, 168, key)
self.encrypt_range(0x186338, 56, key)
self.encrypt_range(0x180000, 32, key) self.encrypt_range(0x180000, 32, key)
self.encrypt_range(0x180140, 32, key) self.encrypt_range(0x180140, 32, key)
self.encrypt_range(0xEDA1, 8, key)
def write_to_file(self, file, hide_enemizer=False): def write_to_file(self, file, hide_enemizer=False):
with open(file, 'wb') as outfile: with open(file, 'wb') as outfile:
@ -595,11 +597,8 @@ class Sprite(object):
def expand_color(i): def expand_color(i):
return ((i & 0x1F) * 8, (i >> 5 & 0x1F) * 8, (i >> 10 & 0x1F) * 8) return ((i & 0x1F) * 8, (i >> 5 & 0x1F) * 8, (i >> 10 & 0x1F) * 8)
raw_palette = self.palette
if raw_palette is None:
raw_palette = Sprite.default_palette
# turn palette data into a list of RGB tuples with 8 bit values # turn palette data into a list of RGB tuples with 8 bit values
palette_as_colors = [expand_color(make_int16(chnk)) for chnk in array_chunk(raw_palette, 2)] palette_as_colors = [expand_color(make_int16(chnk)) for chnk in array_chunk(self.palette, 2)]
# split into palettes of 15 colors # split into palettes of 15 colors
return array_chunk(palette_as_colors, 15) return array_chunk(palette_as_colors, 15)
@ -661,7 +660,8 @@ def patch_rom(world, rom, player, team, enemized):
rom.write_byte(location.player_address, location.item.player) rom.write_byte(location.player_address, location.item.player)
else: else:
itemid = 0x5A itemid = 0x5A
rom.write_byte(location.address, itemid) location_address = old_location_address_to_new_location_address.get(location.address, location.address)
rom.write_byte(location_address, itemid)
else: else:
# crystals # crystals
for address, value in zip(location.address, itemid): for address, value in zip(location.address, itemid):
@ -1021,47 +1021,30 @@ def patch_rom(world, rom, player, team, enemized):
rom.write_byte(0x180044, 0x01) # hammer activates tablets rom.write_byte(0x180044, 0x01) # hammer activates tablets
# set up clocks for timed modes # set up clocks for timed modes
if world.shuffle[player] == 'vanilla': if world.clock_mode[player] in ['ohko', 'countdown-ohko']:
ERtimeincrease = 0
elif world.shuffle[player] in ['dungeonssimple', 'dungeonsfull']:
ERtimeincrease = 10
else:
ERtimeincrease = 20
if world.keyshuffle[player] or world.bigkeyshuffle[player] or world.mapshuffle[player]:
ERtimeincrease = ERtimeincrease + 15
if world.clock_mode[player] == False:
rom.write_bytes(0x180190, [0x00, 0x00, 0x00]) # turn off clock mode
rom.write_int32(0x180200, 0) # red clock adjustment time (in frames, sint32)
rom.write_int32(0x180204, 0) # blue clock adjustment time (in frames, sint32)
rom.write_int32(0x180208, 0) # green clock adjustment time (in frames, sint32)
rom.write_int32(0x18020C, 0) # starting time (in frames, sint32)
elif world.clock_mode[player] == 'ohko':
rom.write_bytes(0x180190, [0x01, 0x02, 0x01]) # ohko timer with resetable timer functionality rom.write_bytes(0x180190, [0x01, 0x02, 0x01]) # ohko timer with resetable timer functionality
rom.write_int32(0x180200, 0) # red clock adjustment time (in frames, sint32) elif world.clock_mode[player] == 'stopwatch':
rom.write_int32(0x180204, 0) # blue clock adjustment time (in frames, sint32)
rom.write_int32(0x180208, 0) # green clock adjustment time (in frames, sint32)
rom.write_int32(0x18020C, 0) # starting time (in frames, sint32)
elif world.clock_mode[player] == 'countdown-ohko':
rom.write_bytes(0x180190, [0x01, 0x02, 0x01]) # ohko timer with resetable timer functionality
rom.write_int32(0x180200, -100 * 60 * 60 * 60) # red clock adjustment time (in frames, sint32)
rom.write_int32(0x180204, 2 * 60 * 60) # blue clock adjustment time (in frames, sint32)
rom.write_int32(0x180208, 4 * 60 * 60) # green clock adjustment time (in frames, sint32)
if world.difficulty_adjustments[player] in ['easy', 'normal']:
rom.write_int32(0x18020C, (10 + ERtimeincrease) * 60 * 60) # starting time (in frames, sint32)
else:
rom.write_int32(0x18020C, int((5 + ERtimeincrease / 2) * 60 * 60)) # starting time (in frames, sint32)
if world.clock_mode[player] == 'stopwatch':
rom.write_bytes(0x180190, [0x02, 0x01, 0x00]) # set stopwatch mode rom.write_bytes(0x180190, [0x02, 0x01, 0x00]) # set stopwatch mode
rom.write_int32(0x180200, -2 * 60 * 60) # red clock adjustment time (in frames, sint32) elif world.clock_mode[player] == 'countdown':
rom.write_int32(0x180204, 2 * 60 * 60) # blue clock adjustment time (in frames, sint32)
rom.write_int32(0x180208, 4 * 60 * 60) # green clock adjustment time (in frames, sint32)
rom.write_int32(0x18020C, 0) # starting time (in frames, sint32)
if world.clock_mode[player] == 'countdown':
rom.write_bytes(0x180190, [0x01, 0x01, 0x00]) # set countdown, with no reset available rom.write_bytes(0x180190, [0x01, 0x01, 0x00]) # set countdown, with no reset available
rom.write_int32(0x180200, -2 * 60 * 60) # red clock adjustment time (in frames, sint32) else:
rom.write_int32(0x180204, 2 * 60 * 60) # blue clock adjustment time (in frames, sint32) rom.write_bytes(0x180190, [0x00, 0x00, 0x00]) # turn off clock mode
rom.write_int32(0x180208, 4 * 60 * 60) # green clock adjustment time (in frames, sint32)
rom.write_int32(0x18020C, (40 + ERtimeincrease) * 60 * 60) # starting time (in frames, sint32) # Set up requested clock settings
if world.clock_mode[player] in ['countdown-ohko', 'stopwatch', 'countdown']:
rom.write_int32(0x180200, world.red_clock_time[player] * 60 * 60) # red clock adjustment time (in frames, sint32)
rom.write_int32(0x180204, world.blue_clock_time[player] * 60 * 60) # blue clock adjustment time (in frames, sint32)
rom.write_int32(0x180208, world.green_clock_time[player] * 60 * 60) # green clock adjustment time (in frames, sint32)
else:
rom.write_int32(0x180200, 0) # red clock adjustment time (in frames, sint32)
rom.write_int32(0x180204, 0) # blue clock adjustment time (in frames, sint32)
rom.write_int32(0x180208, 0) # green clock adjustment time (in frames, sint32)
# Set up requested start time for countdown modes
if world.clock_mode[player] in ['countdown-ohko', 'countdown']:
rom.write_int32(0x18020C, world.countdown_start_time[player] * 60 * 60) # starting time (in frames, sint32)
else:
rom.write_int32(0x18020C, 0) # starting time (in frames, sint32)
# set up goals for treasure hunt # set up goals for treasure hunt
rom.write_bytes(0x180165, [0x0E, 0x28] if world.treasure_hunt_icon[player] == 'Triforce Piece' else [0x0D, 0x28]) rom.write_bytes(0x180165, [0x0E, 0x28] if world.treasure_hunt_icon[player] == 'Triforce Piece' else [0x0D, 0x28])
@ -1476,7 +1459,7 @@ def patch_rom(world, rom, player, team, enemized):
write_strings(rom, world, player, team) write_strings(rom, world, player, team)
rom.write_byte(0x18636C, 1 if world.remote_items[player] else 0) rom.write_byte(0x18637C, 1 if world.remote_items[player] else 0)
# set rom name # set rom name
# 21 bytes # 21 bytes
@ -1620,11 +1603,6 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr
rom.write_byte(0x6FA30, {'red': 0x24, 'blue': 0x2C, 'green': 0x3C, 'yellow': 0x28}[color]) rom.write_byte(0x6FA30, {'red': 0x24, 'blue': 0x2C, 'green': 0x3C, 'yellow': 0x28}[color])
rom.write_byte(0x65561, {'red': 0x05, 'blue': 0x0D, 'green': 0x19, 'yellow': 0x09}[color]) rom.write_byte(0x65561, {'red': 0x05, 'blue': 0x0D, 'green': 0x19, 'yellow': 0x09}[color])
# write link sprite if required
if sprite:
sprite = Sprite(sprite) if os.path.isfile(sprite) else Sprite.get_sprite_from_name(sprite, local_random)
sprite.write_to_rom(rom)
if z3pr: if z3pr:
def buildAndRandomize(option_name, mode): def buildAndRandomize(option_name, mode):
options = { options = {

View File

@ -1230,7 +1230,8 @@ class GoldCreditMapper(CharTextMapper):
class GreenCreditMapper(CharTextMapper): class GreenCreditMapper(CharTextMapper):
char_map = {' ': 0x9F, char_map = {' ': 0x9F,
'·': 0x52} '·': 0x52,
'.': 0x52}
alpha_offset = -0x29 alpha_offset = -0x29
class RedCreditMapper(CharTextMapper): class RedCreditMapper(CharTextMapper):

View File

@ -6,7 +6,7 @@ def tuplize_version(version: str) -> typing.Tuple[int, ...]:
return tuple(int(piece, 10) for piece in version.split(".")) return tuple(int(piece, 10) for piece in version.split("."))
__version__ = "3.2.1" __version__ = "3.3.0"
_version_tuple = tuplize_version(__version__) _version_tuple = tuplize_version(__version__)
import os import os

View File

@ -28,7 +28,7 @@ def mysterycheck():
if type(options) == str: if type(options) == str:
flash(options) flash(options)
else: else:
results, _ = roll_yamls(options) results, _ = roll_options(options)
return render_template("checkresult.html", results=results) return render_template("checkresult.html", results=results)
return render_template("check.html") return render_template("check.html")
@ -60,12 +60,15 @@ def get_yaml_data(file) -> Union[Dict[str, str], str]:
return options return options
def roll_yamls(options: Dict[str, Union[str, str]]) -> Tuple[Dict[str, Union[str, bool]], Dict[str, dict]]: def roll_options(options: Dict[str, Union[dict, str]]) -> Tuple[Dict[str, Union[str, bool]], Dict[str, dict]]:
results = {} results = {}
rolled_results = {} rolled_results = {}
for filename, text in options.items(): for filename, text in options.items():
try: try:
yaml_data = parse_yaml(text) if type(text) is dict:
yaml_data = text
else:
yaml_data = parse_yaml(text)
except Exception as e: except Exception as e:
results[filename] = f"Failed to parse YAML data in {filename}: {e}" results[filename] = f"Failed to parse YAML data in {filename}: {e}"
else: else:

View File

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

View File

@ -13,7 +13,7 @@ import pickle
from .models import * from .models import *
from WebHostLib import app from WebHostLib import app
from .check import get_yaml_data, roll_yamls from .check import get_yaml_data, roll_options
@app.route('/generate', methods=['GET', 'POST']) @app.route('/generate', methods=['GET', 'POST'])
@ -29,7 +29,7 @@ def generate(race=False):
if type(options) == str: if type(options) == str:
flash(options) flash(options)
else: else:
results, gen_options = roll_yamls(options) results, gen_options = roll_options(options)
if any(type(result) == str for result in results.values()): if any(type(result) == str for result in results.values()):
return render_template("checkresult.html", results=results) return render_template("checkresult.html", results=results)
elif len(gen_options) > app.config["MAX_ROLL"]: elif len(gen_options) > app.config["MAX_ROLL"]:
@ -52,6 +52,55 @@ def generate(race=False):
return render_template("generate.html", race=race) return render_template("generate.html", race=race)
@app.route('/api/generate', methods=['POST'])
def generate_api():
try:
options = {}
race = False
if 'file' in request.files:
file = request.files['file']
options = get_yaml_data(file)
if type(options) == str:
return {"text": options}, 400
if "race" in request.form:
race = bool(0 if request.form["race"] in {"false"} else int(request.form["race"]))
json_data = request.get_json()
if json_data:
if 'weights' in json_data:
# example: options = {"player1weights" : {<weightsdata>}}
options = json_data["weights"]
if "race" in json_data:
race = bool(0 if json_data["race"] in {"false"} else int(json_data["race"]))
if not options:
return {"text": "No options found. Expected file attachment or json weights."
}, 400
if len(options) > app.config["MAX_ROLL"]:
return {"text": "Max size of multiworld exceeded",
"detail": app.config["MAX_ROLL"]}, 409
results, gen_options = roll_options(options)
if any(type(result) == str for result in results.values()):
return {"text": str(results),
"detail": results}, 400
else:
gen = Generation(
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
# convert to json compatible
meta=pickle.dumps({"race": race}), state=STATE_QUEUED,
owner=session["_id"])
commit()
return {"text": f"Generation of seed {gen.id} started successfully.",
"detail": gen.id,
"encoded": app.url_map.converters["suuid"].to_url(None, gen.id),
"wait_api_url": url_for("wait_seed_api", seed=gen.id),
"url": url_for("wait_seed", seed=gen.id)}, 201
except Exception as e:
return {"text": "Uncaught Exception:" + str(e)}, 500
def gen_game(gen_options, race=False, owner=None, sid=None): def gen_game(gen_options, race=False, owner=None, sid=None):
try: try:
target = tempfile.TemporaryDirectory() target = tempfile.TemporaryDirectory()
@ -92,7 +141,7 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
del (erargs.progression_balancing) del (erargs.progression_balancing)
ERmain(erargs, seed) ERmain(erargs, seed)
return upload_to_db(target.name, owner, sid) return upload_to_db(target.name, owner, sid, race)
except BaseException: except BaseException:
if sid: if sid:
with db_session: with db_session:
@ -117,9 +166,25 @@ def wait_seed(seed: UUID):
return render_template("waitSeed.html", seed_id=seed_id) return render_template("waitSeed.html", seed_id=seed_id)
def upload_to_db(folder, owner, sid): @app.route('/api/status/<suuid:seed>')
def wait_seed_api(seed: UUID):
seed_id = seed
seed = Seed.get(id=seed_id)
if seed:
return {"text": "Generation done"}, 201
generation = Generation.get(id=seed_id)
if not generation:
return {"text": "Generation not found"}, 404
elif generation.state == STATE_ERROR:
return {"text": "Generation failed"}, 500
return {"text": "Generation running"}, 202
def upload_to_db(folder, owner, sid, race:bool):
patches = set() patches = set()
spoiler = "" spoiler = ""
multidata = None multidata = None
for file in os.listdir(folder): for file in os.listdir(folder):
file = os.path.join(folder, file) file = os.path.join(folder, file)
@ -129,20 +194,26 @@ def upload_to_db(folder, owner, sid):
elif file.endswith(".txt"): elif file.endswith(".txt"):
spoiler = open(file, "rt").read() spoiler = open(file, "rt").read()
elif file.endswith("multidata"): elif file.endswith("multidata"):
try: multidata = file
multidata = json.loads(zlib.decompress(open(file, "rb").read()))
except Exception as e: if not race or len(patches) > 1:
flash(e) try:
if multidata: multidata = json.loads(zlib.decompress(open(multidata, "rb").read()))
with db_session: except Exception as e:
if sid: flash(e)
seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=owner, id=sid) raise e
else: else:
seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=owner) multidata = {}
for patch in patches:
patch.seed = seed with db_session:
if sid: if sid:
gen = Generation.get(id=sid) seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=owner, id=sid)
if gen is not None: else:
gen.delete() seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=owner)
return seed.id for patch in patches:
patch.seed = seed
if sid:
gen = Generation.get(id=sid)
if gen is not None:
gen.delete()
return seed.id

View File

@ -1206,6 +1206,110 @@
} }
} }
}, },
"countdown_start_time": {
"keyString": "countdown_start_time",
"friendlyName": "Countdown Starting Time",
"description": "The amount of time, in minutes, to start with in Timed Countdown and Timed OHKO modes.",
"inputType": "range",
"subOptions": {
"0": {
"keyString": "countdown_start_time.0",
"friendlyName": 0,
"description": "Start with no time on the timer. In Timed OHKO mode, start in OHKO mode.",
"defaultValue": 0
},
"10": {
"keyString": "countdown_start_time.10",
"friendlyName": 10,
"description": "Start with 10 minutes on the timer.",
"defaultValue": 50
},
"20": {
"keyString": "countdown_start_time.20",
"friendlyName": 20,
"description": "Start with 20 minutes on the timer.",
"defaultValue": 0
},
"30": {
"keyString": "countdown_start_time.30",
"friendlyName": 30,
"description": "Start with 30 minutes on the timer.",
"defaultValue": 0
},
"60": {
"keyString": "countdown_start_time.60",
"friendlyName": 60,
"description": "Start with an hour on the timer.",
"defaultValue": 0
}
}
},
"red_clock_time": {
"keyString": "red_clock_time",
"friendlyName": "Red Clock Time",
"description": "The amount of time, in minutes, to add to or subtract from the timer upon picking up a red clock.",
"inputType": "range",
"subOptions": {
"-2": {
"keyString": "red_clock_time.-2",
"friendlyName": -2,
"description": "Subtract 2 minutes from the timer upon picking up a red clock.",
"defaultValue": 0
},
"1": {
"keyString": "red_clock_time.1",
"friendlyName": 1,
"description": "Add a minute to the timer upon picking up a red clock.",
"defaultValue": 50
}
}
},
"blue_clock_time": {
"keyString": "blue_clock_time",
"friendlyName": "Blue Clock Time",
"description": "The amount of time, in minutes, to add to or subtract from the timer upon picking up a blue clock.",
"inputType": "range",
"subOptions": {
"1": {
"keyString": "blue_clock_time.1",
"friendlyName": 1,
"description": "Add a minute to the timer upon picking up a blue clock.",
"defaultValue": 0
},
"2": {
"keyString": "blue_clock_time.2",
"friendlyName": 2,
"description": "Add 2 minutes to the timer upon picking up a blue clock.",
"defaultValue": 50
}
}
},
"green_clock_time": {
"keyString": "green_clock_time",
"friendlyName": "Green Clock Time",
"description": "The amount of time, in minutes, to add to or subtract from the timer upon picking up a green clock.",
"inputType": "range",
"subOptions": {
"4": {
"keyString": "green_clock_time.4",
"friendlyName": 4,
"description": "Add 4 minutes to the timer upon picking up a green clock.",
"defaultValue": 50
},
"10": {
"keyString": "green_clock_time.10",
"friendlyName": 10,
"description": "Add 10 minutes to the timer upon picking up a green clock.",
"defaultValue": 0
},
"15": {
"keyString": "green_clock_time.15",
"friendlyName": 15,
"description": "Add 15 minutes to the timer upon picking up a green clock.",
"defaultValue": 0
}
}
},
"glitch_boots": { "glitch_boots": {
"keyString": "glitch_boots", "keyString": "glitch_boots",
"friendlyName": "Glitch Boots", "friendlyName": "Glitch Boots",
@ -1572,26 +1676,26 @@
}, },
"hud_palettes": { "hud_palettes": {
"keyString": "rom.hud_palettes", "keyString": "rom.hud_palettes",
"friendlyName": "Underworld Palettes", "friendlyName": "HUD Palettes",
"description": "Randomize the colors of the underworld (caves, dungeons, etc.), within reason.", "description": "Randomize the colors of the HUD (user interface), within reason.",
"inputType": "range", "inputType": "range",
"subOptions": { "subOptions": {
"default": { "default": {
"keyString": "rom.hud_palettes.default", "keyString": "rom.hud_palettes.default",
"friendlyName": "Vanilla", "friendlyName": "Vanilla",
"description": "Overworld colors will remain unchanged.", "description": "HUD colors will remain unchanged.",
"defaultValue": 50 "defaultValue": 50
}, },
"random": { "random": {
"keyString": "rom.hud_palettes.random", "keyString": "rom.hud_palettes.random",
"friendlyName": "Random", "friendlyName": "Random",
"description": "Shuffles the colors of the overworld palette.", "description": "Shuffles the colors of the HUD palette.",
"defaultValue": 0 "defaultValue": 0
}, },
"blackout": { "blackout": {
"keyString": "rom.hud_palettes.blackout", "keyString": "rom.hud_palettes.blackout",
"friendlyName": "Blackout", "friendlyName": "Blackout",
"description": "Never use this. Makes all overworld palette colors black.", "description": "Never use this. Makes all HUD palette colors black.",
"defaultValue": 0 "defaultValue": 0
}, },
"grayscale": { "grayscale": {
@ -1634,26 +1738,26 @@
}, },
"shield_palettes": { "shield_palettes": {
"keyString": "rom.shield_palettes", "keyString": "rom.shield_palettes",
"friendlyName": "Underworld Palettes", "friendlyName": "Shield Palettes",
"description": "Randomize the colors of the underworld (caves, dungeons, etc.), within reason.", "description": "Randomize the colors of the shield, within reason.",
"inputType": "range", "inputType": "range",
"subOptions": { "subOptions": {
"default": { "default": {
"keyString": "rom.shield_palettes.default", "keyString": "rom.shield_palettes.default",
"friendlyName": "Vanilla", "friendlyName": "Vanilla",
"description": "Overworld colors will remain unchanged.", "description": "Shield colors will remain unchanged.",
"defaultValue": 50 "defaultValue": 50
}, },
"random": { "random": {
"keyString": "rom.shield_palettes.random", "keyString": "rom.shield_palettes.random",
"friendlyName": "Random", "friendlyName": "Random",
"description": "Shuffles the colors of the overworld palette.", "description": "Shuffles the colors of the shield palette.",
"defaultValue": 0 "defaultValue": 0
}, },
"blackout": { "blackout": {
"keyString": "rom.shield_palettes.blackout", "keyString": "rom.shield_palettes.blackout",
"friendlyName": "Blackout", "friendlyName": "Blackout",
"description": "Never use this. Makes all overworld palette colors black.", "description": "Never use this. Makes all shield palette colors black.",
"defaultValue": 0 "defaultValue": 0
}, },
"grayscale": { "grayscale": {
@ -1696,26 +1800,26 @@
}, },
"sword_palettes": { "sword_palettes": {
"keyString": "rom.sword_palettes", "keyString": "rom.sword_palettes",
"friendlyName": "Underworld Palettes", "friendlyName": "Sword Palettes",
"description": "Randomize the colors of the underworld (caves, dungeons, etc.), within reason.", "description": "Randomize the colors of the sword, within reason.",
"inputType": "range", "inputType": "range",
"subOptions": { "subOptions": {
"default": { "default": {
"keyString": "rom.sword_palettes.default", "keyString": "rom.sword_palettes.default",
"friendlyName": "Vanilla", "friendlyName": "Vanilla",
"description": "Overworld colors will remain unchanged.", "description": "Sword colors will remain unchanged.",
"defaultValue": 50 "defaultValue": 50
}, },
"random": { "random": {
"keyString": "rom.sword_palettes.random", "keyString": "rom.sword_palettes.random",
"friendlyName": "Random", "friendlyName": "Random",
"description": "Shuffles the colors of the overworld palette.", "description": "Shuffles the colors of the sword palette.",
"defaultValue": 0 "defaultValue": 0
}, },
"blackout": { "blackout": {
"keyString": "rom.sword_palettes.blackout", "keyString": "rom.sword_palettes.blackout",
"friendlyName": "Blackout", "friendlyName": "Blackout",
"description": "Never use this. Makes all overworld palette colors black.", "description": "Never use this. Makes all sword palette colors black.",
"defaultValue": 0 "defaultValue": 0
}, },
"grayscale": { "grayscale": {

View File

@ -232,6 +232,22 @@ timer:
ohko: 0 # Timer always at zero. Permanent OHKO. 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. 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. 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 # 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" # local_items: # Force certain items to appear in your world only, not across the multiworld. Recognizes some group names, like "Swords"
# - "Moon Pearl" # - "Moon Pearl"
@ -391,13 +407,3 @@ rom:
dizzy: 0 dizzy: 0
sick: 0 sick: 0
puke: 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

@ -19,10 +19,10 @@
You can also upload a .zip with multiple YAMLs. You can also upload a .zip with multiple YAMLs.
A proper menu is in the works. A proper menu is in the works.
{% if race -%} {% if race -%}
Race Mode means the spoiler log will be unavailable. Race Mode means the spoiler log will be unavailable, roms will be encrypted and single-player has no multidata.
{%- else -%} {%- else -%}
You can go to <a href="{{ url_for("generate", race=True) }}">Race Mode</a> to create a game without You can go to <a href="{{ url_for("generate", race=True) }}">Race Mode</a> to create a game without
spoiler log. spoiler log and with encryption.
{%- endif -%} {%- endif -%}
</p> </p>
<p> <p>

View File

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

View File

@ -11,6 +11,11 @@
<div id="view-seed-wrapper"> <div id="view-seed-wrapper">
<div class="main-content"> <div class="main-content">
<h3>Seed Info</h3> <h3>Seed Info</h3>
{% if not seed.multidata and not seed.spoiler %}
<h4>
Single Player Race Rom: No spoiler or multidata exists, parts of the rom are encrypted and rooms cannot be created.
</h4>
{% endif %}
<table> <table>
<tbody> <tbody>
<tr> <tr>
@ -27,6 +32,7 @@
<td><a href="{{ url_for("download_spoiler", seed_id=seed.id) }}">Download</a></td> <td><a href="{{ url_for("download_spoiler", seed_id=seed.id) }}">Download</a></td>
</tr> </tr>
{% endif %} {% endif %}
{% if seed.multidata %}
<tr> <tr>
<td>Players:&nbsp;</td> <td>Players:&nbsp;</td>
<td> <td>
@ -55,6 +61,23 @@
{% endcall %} {% endcall %}
</td> </td>
</tr> </tr>
{% else %}
<tr>
<td>Patches:&nbsp;</td>
<td>
<ul>
{% for patch in seed.patches %}
<li>
<a href="{{ url_for("download_raw_patch", seed_id=seed.id, player_id=patch.player) }}">Player {{ patch.player }}</a>
</li>
{% endfor %}
</ul>
</td>
</tr>
{% endif %}
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -180,6 +180,25 @@ default_locations = {
60121, 60124, 60127, 1573217, 60130, 60133, 60136, 60139, 60142, 60145, 60148, 60151, 60157}, 60121, 60124, 60127, 1573217, 60130, 60133, 60136, 60139, 60142, 60145, 60148, 60151, 60157},
'Total': set()} '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", 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", "Thieves Town", "Skull Woods", "Ice Palace", "Misery Mire", "Turtle Rock", "Palace of Darkness",
"Ganons Tower"} "Ganons Tower"}
@ -191,6 +210,10 @@ for area, locations in default_locations.items():
for location in locations: for location in locations:
location_to_area[location] = area 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 = {area: len(checks) for area, checks in default_locations.items()}
checks_in_area["Total"] = 216 checks_in_area["Total"] = 216
@ -235,6 +258,14 @@ def render_timedelta(delta: datetime.timedelta):
_multidata_cache = {} _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): def get_static_room_data(room: Room):
result = _multidata_cache.get(room.seed.id, None) 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 # 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']} locations = {tuple(k): tuple(v) for k, v in multidata['locations']}
names = multidata["names"] names = multidata["names"]
seed_checks_in_area = checks_in_area.copy()
use_door_tracker = False use_door_tracker = False
if "tags" in multidata: if "tags" in multidata:
use_door_tracker = "DR" in multidata["tags"] 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 _multidata_cache[room.seed.id] = result
return result return result
@ -259,7 +309,7 @@ def get_tracker(tracker: UUID):
room = Room.get(tracker=tracker) room = Room.get(tracker=tracker)
if not room: if not room:
abort(404) 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)} inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1)}
for teamnumber, team in enumerate(names)} for teamnumber, team in enumerate(names)}
@ -280,9 +330,12 @@ def get_tracker(tracker: UUID):
for item_id in precollected: for item_id in precollected:
attribute_item(inventory, team, player, item_id) attribute_item(inventory, team, player, item_id)
for location in locations_checked: 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] item, recipient = locations[location, player]
attribute_item(inventory, team, recipient, item) 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 checks_done[team][player]["Total"] += 1
for (team, player), game_state in room.multisave.get("client_game_state", []): for (team, player), game_state in room.multisave.get("client_game_state", []):
@ -311,7 +364,7 @@ def get_tracker(tracker: UUID):
lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names, 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, tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=icons,
multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas, 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, 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, video=video, big_key_locations=key_locations if use_door_tracker else big_key_locations,
hints=hints, long_player_names = long_player_names) hints=hints, long_player_names = long_player_names)

Binary file not shown.

View File

@ -11,6 +11,7 @@ AppName={#MyAppName}
AppVerName={#MyAppName} AppVerName={#MyAppName}
DefaultDirName={commonappdata}\{#MyAppName} DefaultDirName={commonappdata}\{#MyAppName}
DisableProgramGroupPage=yes DisableProgramGroupPage=yes
DefaultGroupName=Berserker's Multiworld
OutputDir=setups OutputDir=setups
OutputBaseFilename=Setup {#MyAppName} OutputBaseFilename=Setup {#MyAppName}
Compression=lzma2 Compression=lzma2

View File

@ -232,6 +232,22 @@ timer:
ohko: 0 # Timer always at zero. Permanent OHKO. 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. 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. 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 # 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" # local_items: # Force certain items to appear in your world only, not across the multiworld. Recognizes some group names, like "Swords"
# - "Moon Pearl" # - "Moon Pearl"