Merge branch 'master' into website-redesign
This commit is contained in:
		
						commit
						016d09150d
					
				| 
						 | 
				
			
			@ -119,6 +119,10 @@ 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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			@ -366,6 +374,7 @@ 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',
 | 
			
		||||
                         'shufflebosses', 'enemy_shuffle', 'enemy_health', 'enemy_damage', 'shufflepots',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										68
									
								
								Gui.py
								
								
								
								
							
							
						
						
									
										68
									
								
								Gui.py
								
								
								
								
							| 
						 | 
				
			
			@ -120,6 +120,56 @@ def guiMain(args=None):
 | 
			
		|||
    balancingCheckbutton.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.columnconfigure(0, weight=1)
 | 
			
		||||
    romOptionsFrame.columnconfigure(1, weight=1)
 | 
			
		||||
| 
						 | 
				
			
			@ -266,6 +316,7 @@ def guiMain(args=None):
 | 
			
		|||
    romSelectButton.pack(side=LEFT)
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
 | 
			
		||||
    drowDownFrame = Frame(topFrame)
 | 
			
		||||
| 
						 | 
				
			
			@ -344,14 +395,6 @@ def guiMain(args=None):
 | 
			
		|||
    itemfunctionLabel = Label(itemfunctionFrame, text='Difficulty: item functionality')
 | 
			
		||||
    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)
 | 
			
		||||
    dungeonCounterVar = StringVar()
 | 
			
		||||
    dungeonCounterVar.set('auto')
 | 
			
		||||
| 
						 | 
				
			
			@ -412,7 +455,6 @@ def guiMain(args=None):
 | 
			
		|||
    swordFrame.pack(expand=True, anchor=E)
 | 
			
		||||
    difficultyFrame.pack(expand=True, anchor=E)
 | 
			
		||||
    itemfunctionFrame.pack(expand=True, anchor=E)
 | 
			
		||||
    timerFrame.pack(expand=True, anchor=E)
 | 
			
		||||
    dungeonCounterFrame.pack(expand=True, anchor=E)
 | 
			
		||||
    progressiveFrame.pack(expand=True, anchor=E)
 | 
			
		||||
    accessibilityFrame.pack(expand=True, anchor=E)
 | 
			
		||||
| 
						 | 
				
			
			@ -538,6 +580,10 @@ def guiMain(args=None):
 | 
			
		|||
        guiargs.difficulty = difficultyVar.get()
 | 
			
		||||
        guiargs.item_functionality = itemfunctionVar.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()
 | 
			
		||||
        if guiargs.timer == "none":
 | 
			
		||||
            guiargs.timer = False
 | 
			
		||||
| 
						 | 
				
			
			@ -1424,6 +1470,10 @@ def guiMain(args=None):
 | 
			
		|||
        difficultyVar.set(args.difficulty)
 | 
			
		||||
        itemfunctionVar.set(args.item_functionality)
 | 
			
		||||
        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)
 | 
			
		||||
        accessibilityVar.set(args.accessibility)
 | 
			
		||||
        goalVar.set(args.goal)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										29
									
								
								Main.py
								
								
								
								
							
							
						
						
									
										29
									
								
								Main.py
								
								
								
								
							| 
						 | 
				
			
			@ -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()
 | 
			
		||||
| 
						 | 
				
			
			@ -297,6 +301,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 +351,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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										121
									
								
								MultiClient.py
								
								
								
								
							
							
						
						
									
										121
									
								
								MultiClient.py
								
								
								
								
							| 
						 | 
				
			
			@ -85,14 +85,17 @@ class Context():
 | 
			
		|||
        self.slot = None
 | 
			
		||||
        self.player_names: typing.Dict[int: str] = {}
 | 
			
		||||
        self.locations_checked = set()
 | 
			
		||||
        self.unsafe_locations_checked = set()
 | 
			
		||||
        self.locations_scouted = set()
 | 
			
		||||
        self.items_received = []
 | 
			
		||||
        self.items_missing = []
 | 
			
		||||
        self.locations_info = {}
 | 
			
		||||
        self.awaiting_rom = False
 | 
			
		||||
        self.rom = None
 | 
			
		||||
        self.prev_rom = None
 | 
			
		||||
        self.auth = None
 | 
			
		||||
        self.found_items = found_items
 | 
			
		||||
        self.send_unsafe = False
 | 
			
		||||
        self.finished_game = 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 - Compass Chest': (0x85, 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),
 | 
			
		||||
                     'Eastern Palace - Compass Chest': (0xa8, 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 - Big Key Chest': (0xb8, 0x10),
 | 
			
		||||
                     'Eastern Palace - Map Chest': (0xaa, 0x10),
 | 
			
		||||
                     'Eastern Palace - Boss': (0xc8, 0x800),
 | 
			
		||||
                     'Hyrule Castle - Boomerang Chest': (0x71, 0x10),
 | 
			
		||||
                     'Hyrule Castle - Boomerang Guard Key Drop': (0x71, 0x400),
 | 
			
		||||
                     'Hyrule Castle - Map Chest': (0x72, 0x10),
 | 
			
		||||
                     'Hyrule Castle - Map Guard Key Drop': (0x72, 0x400),
 | 
			
		||||
                     "Hyrule Castle - Zelda's Chest": (0x80, 0x10),
 | 
			
		||||
                     'Hyrule Castle - Big Key Drop': (0x80, 0x400),
 | 
			
		||||
                     'Sewers - Dark Cross': (0x32, 0x10),
 | 
			
		||||
                     'Hyrule Castle - Key Rat Key Drop': (0x21, 0x400),
 | 
			
		||||
                     'Sewers - Secret Room - Left': (0x11, 0x10),
 | 
			
		||||
                     'Sewers - Secret Room - Middle': (0x11, 0x20),
 | 
			
		||||
                     'Sewers - Secret Room - Right': (0x11, 0x40),
 | 
			
		||||
                     'Sanctuary': (0x12, 0x10),
 | 
			
		||||
                     'Castle Tower - Room 03': (0xe0, 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),
 | 
			
		||||
                     'Paradox Cave Lower - Far Left': (0xef, 0x10),
 | 
			
		||||
                     'Paradox Cave Lower - Left': (0xef, 0x20),
 | 
			
		||||
| 
						 | 
				
			
			@ -250,18 +264,25 @@ location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
 | 
			
		|||
                     'Mimic Cave': (0x10c, 0x10),
 | 
			
		||||
                     'Swamp Palace - Entrance': (0x28, 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 - Compass Chest': (0x46, 0x10),
 | 
			
		||||
                     'Swamp Palace - Trench 2 Pot Key': (0x35, 0x400),
 | 
			
		||||
                     'Swamp Palace - Big Key Chest': (0x35, 0x10),
 | 
			
		||||
                     'Swamp Palace - West Chest': (0x34, 0x10),
 | 
			
		||||
                     'Swamp Palace - Flooded Room - Left': (0x76, 0x10),
 | 
			
		||||
                     'Swamp Palace - Flooded Room - Right': (0x76, 0x20),
 | 
			
		||||
                     'Swamp Palace - Waterfall Room': (0x66, 0x10),
 | 
			
		||||
                     'Swamp Palace - Waterway Pot Key': (0x16, 0x400),
 | 
			
		||||
                     'Swamp Palace - Boss': (0x6, 0x800),
 | 
			
		||||
                     "Thieves' Town - Big Key Chest": (0xdb, 0x20),
 | 
			
		||||
                     "Thieves' Town - Map Chest": (0xdb, 0x10),
 | 
			
		||||
                     "Thieves' Town - Compass Chest": (0xdc, 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 - Big Chest": (0x44, 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 - Pinball Room': (0x68, 0x10),
 | 
			
		||||
                     'Skull Woods - Big Key Chest': (0x57, 0x10),
 | 
			
		||||
                     'Skull Woods - West Lobby Pot Key': (0x56, 0x400),
 | 
			
		||||
                     'Skull Woods - Bridge Room': (0x59, 0x10),
 | 
			
		||||
                     'Skull Woods - Spike Corner Key Drop': (0x39, 0x400),
 | 
			
		||||
                     'Skull Woods - Boss': (0x29, 0x800),
 | 
			
		||||
                     'Ice Palace - Jelly Key Drop': (0x0e, 0x400),
 | 
			
		||||
                     'Ice Palace - Compass Chest': (0x2e, 0x10),
 | 
			
		||||
                     'Ice Palace - Conveyor Key Drop': (0x3e, 0x400),
 | 
			
		||||
                     'Ice Palace - Freezor Chest': (0x7e, 0x10),
 | 
			
		||||
                     'Ice Palace - Big Chest': (0x9e, 0x10),
 | 
			
		||||
                     'Ice Palace - Iced T Room': (0xae, 0x10),
 | 
			
		||||
                     'Ice Palace - Many Pots Pot Key': (0x9f, 0x400),
 | 
			
		||||
                     'Ice Palace - Spike Room': (0x5f, 0x10),
 | 
			
		||||
                     'Ice Palace - Big Key Chest': (0x1f, 0x10),
 | 
			
		||||
                     'Ice Palace - Hammer Block Key Drop': (0x3f, 0x400),
 | 
			
		||||
                     'Ice Palace - Map Chest': (0x3f, 0x10),
 | 
			
		||||
                     'Ice Palace - Boss': (0xde, 0x800),
 | 
			
		||||
                     'Misery Mire - Big Chest': (0xc3, 0x10),
 | 
			
		||||
                     'Misery Mire - Map Chest': (0xc3, 0x20),
 | 
			
		||||
                     'Misery Mire - Main Lobby': (0xc2, 0x10),
 | 
			
		||||
                     'Misery Mire - Bridge Chest': (0xa2, 0x10),
 | 
			
		||||
                     'Misery Mire - Spikes Pot Key': (0xb3, 0x400),
 | 
			
		||||
                     '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 - Big Key Chest': (0xd1, 0x10),
 | 
			
		||||
                     'Misery Mire - Boss': (0x90, 0x800),
 | 
			
		||||
                     'Turtle Rock - Compass Chest': (0xd6, 0x10),
 | 
			
		||||
                     'Turtle Rock - Roller Room - Left': (0xb7, 0x10),
 | 
			
		||||
                     'Turtle Rock - Roller Room - Right': (0xb7, 0x20),
 | 
			
		||||
                     'Turtle Rock - Pokey 1 Key Drop': (0xb6, 0x400),
 | 
			
		||||
                     'Turtle Rock - Chain Chomps': (0xb6, 0x10),
 | 
			
		||||
                     'Turtle Rock - Pokey 2 Key Drop': (0x13, 0x400),
 | 
			
		||||
                     'Turtle Rock - Big Key Chest': (0x14, 0x10),
 | 
			
		||||
                     'Turtle Rock - Big Chest': (0x24, 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 - Harmless Hellway': (0x1a, 0x40),
 | 
			
		||||
                     'Palace of Darkness - Boss': (0x5a, 0x800),
 | 
			
		||||
                     'Ganons Tower - Conveyor Cross Pot Key': (0x8b, 0x400),
 | 
			
		||||
                     "Ganons Tower - Bob's Torch": (0x8c, 0x400),
 | 
			
		||||
                     'Ganons Tower - Hope Room - Left': (0x8c, 0x20),
 | 
			
		||||
                     '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 - Bottom Left': (0x9d, 0x40),
 | 
			
		||||
                     '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 Right': (0x7b, 0x20),
 | 
			
		||||
                     'Ganons Tower - DMs Room - Bottom Left': (0x7b, 0x40),
 | 
			
		||||
                     'Ganons Tower - DMs Room - Bottom Right': (0x7b, 0x80),
 | 
			
		||||
                     'Ganons Tower - Map Chest': (0x8b, 0x10),
 | 
			
		||||
                     'Ganons Tower - Double Switch Pot Key': (0x9b, 0x400),
 | 
			
		||||
                     'Ganons Tower - Firesnake Room': (0x7d, 0x10),
 | 
			
		||||
                     'Ganons Tower - Randomizer Room - Top Left': (0x7c, 0x10),
 | 
			
		||||
                     '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 - Mini Helmasaur Room - Left': (0x3d, 0x10),
 | 
			
		||||
                     '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 - Validation Chest': (0x4d, 0x10)}
 | 
			
		||||
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')
 | 
			
		||||
 | 
			
		||||
    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", ctx.rom, ctx.server_address)
 | 
			
		||||
        ctx.team, ctx.slot = args[0]
 | 
			
		||||
        ctx.player_names = {p: n for p, n in args[1]}
 | 
			
		||||
        msgs = []
 | 
			
		||||
        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:
 | 
			
		||||
            msgs.append(['LocationScouts', list(ctx.locations_scouted)])
 | 
			
		||||
        if msgs:
 | 
			
		||||
            await ctx.send_msgs(msgs)
 | 
			
		||||
        if ctx.finished_game:
 | 
			
		||||
            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':
 | 
			
		||||
        start_index, items = args
 | 
			
		||||
| 
						 | 
				
			
			@ -837,7 +882,7 @@ async def process_server_cmd(ctx: Context, cmd, args):
 | 
			
		|||
        elif start_index != len(ctx.items_received):
 | 
			
		||||
            sync_msg = [['Sync']]
 | 
			
		||||
            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)
 | 
			
		||||
        if start_index == len(ctx.items_received):
 | 
			
		||||
            for item in items:
 | 
			
		||||
| 
						 | 
				
			
			@ -878,9 +923,11 @@ async def process_server_cmd(ctx: Context, cmd, args):
 | 
			
		|||
    elif cmd == 'Missing':
 | 
			
		||||
        if 'locations' in args:
 | 
			
		||||
            locations = json.loads(args['locations'])
 | 
			
		||||
            for location in locations:
 | 
			
		||||
                ctx.ui_node.log_info(f'Missing: {location}')
 | 
			
		||||
            ctx.ui_node.log_info(f'Found {len(locations)} missing location checks')
 | 
			
		||||
            if ctx.items_missing:
 | 
			
		||||
                for location in locations:
 | 
			
		||||
                    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':
 | 
			
		||||
        hints = [Utils.Hint(*hint) for hint in args]
 | 
			
		||||
| 
						 | 
				
			
			@ -1009,13 +1056,34 @@ class ClientCommandProcessor(CommandProcessor):
 | 
			
		|||
    def _cmd_missing(self) -> bool:
 | 
			
		||||
        """List all missing location checks, from your local game state"""
 | 
			
		||||
        count = 0
 | 
			
		||||
        checked_count = 0
 | 
			
		||||
        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:
 | 
			
		||||
                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
 | 
			
		||||
 | 
			
		||||
        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:
 | 
			
		||||
            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:
 | 
			
		||||
            self.output("No missing location checks found.")
 | 
			
		||||
        return True
 | 
			
		||||
| 
						 | 
				
			
			@ -1045,6 +1113,15 @@ class ClientCommandProcessor(CommandProcessor):
 | 
			
		|||
        else:
 | 
			
		||||
            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):
 | 
			
		||||
        asyncio.create_task(self.ctx.send_msgs([['Say', raw]]))
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1074,20 +1151,22 @@ async def track_locations(ctx : Context, roomid, roomdata):
 | 
			
		|||
    new_locations = []
 | 
			
		||||
 | 
			
		||||
    def new_check(location):
 | 
			
		||||
        ctx.locations_checked.add(location)
 | 
			
		||||
        ctx.ui_node.log_info("New check: %s (%d/216)" % (location, len(ctx.locations_checked)))
 | 
			
		||||
        ctx.unsafe_locations_checked.add(location)
 | 
			
		||||
        ctx.ui_node.log_info("New check: %s (%d/216)" % (location, len(ctx.unsafe_locations_checked)))
 | 
			
		||||
        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():
 | 
			
		||||
        if location not in ctx.locations_checked and loc_roomid == roomid and (roomdata << 4) & loc_mask != 0:
 | 
			
		||||
            new_check(location)
 | 
			
		||||
        try:
 | 
			
		||||
            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_end = 0
 | 
			
		||||
    uw_unchecked = {}
 | 
			
		||||
    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_begin = min(uw_begin, roomid)
 | 
			
		||||
            uw_end = max(uw_end, roomid + 1)
 | 
			
		||||
| 
						 | 
				
			
			@ -1104,7 +1183,7 @@ async def track_locations(ctx : Context, roomid, roomdata):
 | 
			
		|||
    ow_end = 0
 | 
			
		||||
    ow_unchecked = {}
 | 
			
		||||
    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_begin = min(ow_begin, screenid)
 | 
			
		||||
            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:
 | 
			
		||||
                    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)
 | 
			
		||||
        if npc_data is not None:
 | 
			
		||||
            npc_value = npc_data[0] | (npc_data[1] << 8)
 | 
			
		||||
            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)
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
        if misc_data is not None:
 | 
			
		||||
            for location, (offset, mask) in location_table_misc.items():
 | 
			
		||||
                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)
 | 
			
		||||
 | 
			
		||||
    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]])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1161,6 +1245,7 @@ async def game_watcher(ctx : Context):
 | 
			
		|||
            ctx.rom = rom.decode()
 | 
			
		||||
            if not ctx.prev_rom or ctx.prev_rom != ctx.rom:
 | 
			
		||||
                ctx.locations_checked = set()
 | 
			
		||||
                ctx.unsafe_locations_checked = set()
 | 
			
		||||
                ctx.locations_scouted = set()
 | 
			
		||||
            ctx.prev_rom = ctx.rom
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,7 +29,7 @@ import Utils
 | 
			
		|||
from Utils import get_item_name_from_id, get_location_name_from_address, ReceivedItem, _version_tuple
 | 
			
		||||
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_GOAL = 1
 | 
			
		||||
| 
						 | 
				
			
			@ -439,6 +439,7 @@ def send_new_items(ctx: Context):
 | 
			
		|||
 | 
			
		||||
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.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))
 | 
			
		||||
    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):
 | 
			
		||||
    found_items = False
 | 
			
		||||
    new_locations = set(locations) - ctx.location_checks[team, slot]
 | 
			
		||||
    known_locations = set()
 | 
			
		||||
    if new_locations:
 | 
			
		||||
        ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc)
 | 
			
		||||
        for location in new_locations:
 | 
			
		||||
            if (location, slot) in ctx.locations:
 | 
			
		||||
                known_locations.add(location)
 | 
			
		||||
                target_item, target_player = ctx.locations[(location, slot)]
 | 
			
		||||
                if target_player != slot or slot in ctx.remote_items:
 | 
			
		||||
                    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:
 | 
			
		||||
                                    asyncio.create_task(
 | 
			
		||||
                                        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)
 | 
			
		||||
 | 
			
		||||
        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]:
 | 
			
		||||
    hints = []
 | 
			
		||||
    seeked_location = Regions.location_table[location][0]
 | 
			
		||||
    seeked_location = Regions.lookup_name_to_id[location]
 | 
			
		||||
    for check, result in ctx.locations.items():
 | 
			
		||||
        location_id, finding_player = check
 | 
			
		||||
        if finding_player == slot and location_id == seeked_location:
 | 
			
		||||
| 
						 | 
				
			
			@ -806,10 +809,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
 | 
			
		|||
    def _cmd_missing(self) -> bool:
 | 
			
		||||
        """List all missing location checks from the server's perspective"""
 | 
			
		||||
 | 
			
		||||
        locations = []
 | 
			
		||||
        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)
 | 
			
		||||
        locations = get_missing_checks(self.ctx, self.client)
 | 
			
		||||
 | 
			
		||||
        if len(locations) > 0:
 | 
			
		||||
            if self.client.version < [2, 3, 0]:
 | 
			
		||||
| 
						 | 
				
			
			@ -938,6 +938,15 @@ class ClientMessageProcessor(CommonCommandProcessor):
 | 
			
		|||
                self.output(response)
 | 
			
		||||
                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:
 | 
			
		||||
    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)
 | 
			
		||||
            reply = [['Connected', [(client.team, client.slot),
 | 
			
		||||
                                    [(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)
 | 
			
		||||
            if items:
 | 
			
		||||
                reply.append(['ReceivedItems', (0, tuplize_received_items(items))])
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -444,6 +444,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'))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										80
									
								
								Regions.py
								
								
								
								
							
							
						
						
									
										80
									
								
								Regions.py
								
								
								
								
							| 
						 | 
				
			
			@ -397,6 +397,59 @@ shop_table = {
 | 
			
		|||
    '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 +693,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 +800,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")}
 | 
			
		||||
							
								
								
									
										80
									
								
								Rom.py
								
								
								
								
							
							
						
						
									
										80
									
								
								Rom.py
								
								
								
								
							| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
JAP10HASH = '03a63945398191337e896e5771f77173'
 | 
			
		||||
RANDOMIZERBASEHASH = '0fc63d72970ab96ffb18699f4d12a594'
 | 
			
		||||
RANDOMIZERBASEHASH = 'e3714804e3fae1c6ac6100b94d1aee62'
 | 
			
		||||
 | 
			
		||||
import io
 | 
			
		||||
import json
 | 
			
		||||
| 
						 | 
				
			
			@ -19,7 +19,7 @@ from typing import Optional
 | 
			
		|||
 | 
			
		||||
from BaseClasses import CollectionState, ShopType, Region, Location
 | 
			
		||||
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 Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, \
 | 
			
		||||
    BombShop2_texts, junk_texts
 | 
			
		||||
| 
						 | 
				
			
			@ -93,8 +93,10 @@ class LocalRom(object):
 | 
			
		|||
        self.write_bytes(0x186140, [0] * 0x150)
 | 
			
		||||
        self.write_bytes(0x186140 + 0x150, itemplayertable)
 | 
			
		||||
        self.encrypt_range(0x186140 + 0x150, 168, key)
 | 
			
		||||
        self.encrypt_range(0x186338, 56, key)
 | 
			
		||||
        self.encrypt_range(0x180000, 32, key)
 | 
			
		||||
        self.encrypt_range(0x180140, 32, key)
 | 
			
		||||
        self.encrypt_range(0xEDA1, 8, key)
 | 
			
		||||
 | 
			
		||||
    def write_to_file(self, file, hide_enemizer=False):
 | 
			
		||||
        with open(file, 'wb') as outfile:
 | 
			
		||||
| 
						 | 
				
			
			@ -595,11 +597,8 @@ class Sprite(object):
 | 
			
		|||
        def expand_color(i):
 | 
			
		||||
            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
 | 
			
		||||
        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
 | 
			
		||||
        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)
 | 
			
		||||
                    else:
 | 
			
		||||
                        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:
 | 
			
		||||
            # crystals
 | 
			
		||||
            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
 | 
			
		||||
 | 
			
		||||
    # set up clocks for timed modes
 | 
			
		||||
    if world.shuffle[player] == 'vanilla':
 | 
			
		||||
        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':
 | 
			
		||||
    if world.clock_mode[player] in ['ohko', 'countdown-ohko']:
 | 
			
		||||
        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)
 | 
			
		||||
        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':
 | 
			
		||||
    elif world.clock_mode[player] == 'stopwatch':
 | 
			
		||||
        rom.write_bytes(0x180190, [0x02, 0x01, 0x00])  # set stopwatch mode
 | 
			
		||||
        rom.write_int32(0x180200, -2 * 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)
 | 
			
		||||
        rom.write_int32(0x18020C, 0)  # starting time (in frames, sint32)
 | 
			
		||||
    if world.clock_mode[player] == 'countdown':
 | 
			
		||||
    elif world.clock_mode[player] == 'countdown':
 | 
			
		||||
        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)
 | 
			
		||||
        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, (40 + ERtimeincrease) * 60 * 60)  # starting time (in frames, sint32)
 | 
			
		||||
    else:
 | 
			
		||||
        rom.write_bytes(0x180190, [0x00, 0x00, 0x00])  # turn off clock mode
 | 
			
		||||
 | 
			
		||||
    # 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
 | 
			
		||||
    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)
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
    # 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(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:
 | 
			
		||||
        def buildAndRandomize(option_name, mode):
 | 
			
		||||
            options = {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										3
									
								
								Text.py
								
								
								
								
							
							
						
						
									
										3
									
								
								Text.py
								
								
								
								
							| 
						 | 
				
			
			@ -1230,7 +1230,8 @@ class GoldCreditMapper(CharTextMapper):
 | 
			
		|||
 | 
			
		||||
class GreenCreditMapper(CharTextMapper):
 | 
			
		||||
    char_map = {' ': 0x9F,
 | 
			
		||||
                '·': 0x52}
 | 
			
		||||
                '·': 0x52,
 | 
			
		||||
                '.': 0x52}
 | 
			
		||||
    alpha_offset = -0x29
 | 
			
		||||
 | 
			
		||||
class RedCreditMapper(CharTextMapper):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										2
									
								
								Utils.py
								
								
								
								
							
							
						
						
									
										2
									
								
								Utils.py
								
								
								
								
							| 
						 | 
				
			
			@ -6,7 +6,7 @@ def tuplize_version(version: str) -> typing.Tuple[int, ...]:
 | 
			
		|||
    return tuple(int(piece, 10) for piece in version.split("."))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__version__ = "3.2.1"
 | 
			
		||||
__version__ = "3.3.0"
 | 
			
		||||
_version_tuple = tuplize_version(__version__)
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,7 +28,7 @@ def mysterycheck():
 | 
			
		|||
            if type(options) == str:
 | 
			
		||||
                flash(options)
 | 
			
		||||
            else:
 | 
			
		||||
                results, _ = roll_yamls(options)
 | 
			
		||||
                results, _ = roll_options(options)
 | 
			
		||||
                return render_template("checkresult.html", results=results)
 | 
			
		||||
 | 
			
		||||
    return render_template("check.html")
 | 
			
		||||
| 
						 | 
				
			
			@ -60,12 +60,15 @@ def get_yaml_data(file) -> Union[Dict[str, str], str]:
 | 
			
		|||
    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 = {}
 | 
			
		||||
    rolled_results = {}
 | 
			
		||||
    for filename, text in options.items():
 | 
			
		||||
        try:
 | 
			
		||||
            yaml_data = parse_yaml(text)
 | 
			
		||||
            if type(text) is dict:
 | 
			
		||||
                yaml_data = text
 | 
			
		||||
            else:
 | 
			
		||||
                yaml_data = parse_yaml(text)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            results[filename] = f"Failed to parse YAML data in {filename}: {e}"
 | 
			
		||||
        else:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,7 +13,7 @@ import pickle
 | 
			
		|||
 | 
			
		||||
from .models import *
 | 
			
		||||
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'])
 | 
			
		||||
| 
						 | 
				
			
			@ -29,7 +29,7 @@ def generate(race=False):
 | 
			
		|||
            if type(options) == str:
 | 
			
		||||
                flash(options)
 | 
			
		||||
            else:
 | 
			
		||||
                results, gen_options = roll_yamls(options)
 | 
			
		||||
                results, gen_options = roll_options(options)
 | 
			
		||||
                if any(type(result) == str for result in results.values()):
 | 
			
		||||
                    return render_template("checkresult.html", results=results)
 | 
			
		||||
                elif len(gen_options) > app.config["MAX_ROLL"]:
 | 
			
		||||
| 
						 | 
				
			
			@ -52,6 +52,55 @@ def generate(race=False):
 | 
			
		|||
    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):
 | 
			
		||||
    try:
 | 
			
		||||
        target = tempfile.TemporaryDirectory()
 | 
			
		||||
| 
						 | 
				
			
			@ -92,7 +141,7 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
 | 
			
		|||
        del (erargs.progression_balancing)
 | 
			
		||||
        ERmain(erargs, seed)
 | 
			
		||||
 | 
			
		||||
        return upload_to_db(target.name, owner, sid)
 | 
			
		||||
        return upload_to_db(target.name, owner, sid, race)
 | 
			
		||||
    except BaseException:
 | 
			
		||||
        if sid:
 | 
			
		||||
            with db_session:
 | 
			
		||||
| 
						 | 
				
			
			@ -117,9 +166,25 @@ def wait_seed(seed: UUID):
 | 
			
		|||
    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()
 | 
			
		||||
    spoiler = ""
 | 
			
		||||
 | 
			
		||||
    multidata = None
 | 
			
		||||
    for file in os.listdir(folder):
 | 
			
		||||
        file = os.path.join(folder, file)
 | 
			
		||||
| 
						 | 
				
			
			@ -129,20 +194,26 @@ def upload_to_db(folder, owner, sid):
 | 
			
		|||
        elif file.endswith(".txt"):
 | 
			
		||||
            spoiler = open(file, "rt").read()
 | 
			
		||||
        elif file.endswith("multidata"):
 | 
			
		||||
            try:
 | 
			
		||||
                multidata = json.loads(zlib.decompress(open(file, "rb").read()))
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                flash(e)
 | 
			
		||||
    if multidata:
 | 
			
		||||
        with db_session:
 | 
			
		||||
            if sid:
 | 
			
		||||
                seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=owner, id=sid)
 | 
			
		||||
            else:
 | 
			
		||||
                seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=owner)
 | 
			
		||||
            for patch in patches:
 | 
			
		||||
                patch.seed = seed
 | 
			
		||||
            if sid:
 | 
			
		||||
                gen = Generation.get(id=sid)
 | 
			
		||||
                if gen is not None:
 | 
			
		||||
                    gen.delete()
 | 
			
		||||
        return seed.id
 | 
			
		||||
            multidata = file
 | 
			
		||||
 | 
			
		||||
    if not race or len(patches) > 1:
 | 
			
		||||
        try:
 | 
			
		||||
            multidata = json.loads(zlib.decompress(open(multidata, "rb").read()))
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            flash(e)
 | 
			
		||||
            raise e
 | 
			
		||||
    else:
 | 
			
		||||
        multidata = {}
 | 
			
		||||
 | 
			
		||||
    with db_session:
 | 
			
		||||
        if sid:
 | 
			
		||||
            seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=owner, id=sid)
 | 
			
		||||
        else:
 | 
			
		||||
            seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=owner)
 | 
			
		||||
        for patch in patches:
 | 
			
		||||
            patch.seed = seed
 | 
			
		||||
        if sid:
 | 
			
		||||
            gen = Generation.get(id=sid)
 | 
			
		||||
            if gen is not None:
 | 
			
		||||
                gen.delete()
 | 
			
		||||
    return seed.id
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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": {
 | 
			
		||||
      "keyString": "glitch_boots",
 | 
			
		||||
      "friendlyName": "Glitch Boots",
 | 
			
		||||
| 
						 | 
				
			
			@ -1572,26 +1676,26 @@
 | 
			
		|||
    },
 | 
			
		||||
	"hud_palettes": {
 | 
			
		||||
      "keyString": "rom.hud_palettes",
 | 
			
		||||
      "friendlyName": "Underworld Palettes",
 | 
			
		||||
      "description": "Randomize the colors of the underworld (caves, dungeons, etc.), within reason.",
 | 
			
		||||
      "friendlyName": "HUD Palettes",
 | 
			
		||||
      "description": "Randomize the colors of the HUD (user interface), within reason.",
 | 
			
		||||
      "inputType": "range",
 | 
			
		||||
      "subOptions": {
 | 
			
		||||
        "default": {
 | 
			
		||||
          "keyString": "rom.hud_palettes.default",
 | 
			
		||||
          "friendlyName": "Vanilla",
 | 
			
		||||
          "description": "Overworld colors will remain unchanged.",
 | 
			
		||||
          "description": "HUD colors will remain unchanged.",
 | 
			
		||||
          "defaultValue": 50
 | 
			
		||||
        },
 | 
			
		||||
        "random": {
 | 
			
		||||
          "keyString": "rom.hud_palettes.random",
 | 
			
		||||
          "friendlyName": "Random",
 | 
			
		||||
          "description": "Shuffles the colors of the overworld palette.",
 | 
			
		||||
          "description": "Shuffles the colors of the HUD palette.",
 | 
			
		||||
          "defaultValue": 0
 | 
			
		||||
        },
 | 
			
		||||
        "blackout": {
 | 
			
		||||
          "keyString": "rom.hud_palettes.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
 | 
			
		||||
        },
 | 
			
		||||
		"grayscale": {
 | 
			
		||||
| 
						 | 
				
			
			@ -1634,26 +1738,26 @@
 | 
			
		|||
    },
 | 
			
		||||
	"shield_palettes": {
 | 
			
		||||
      "keyString": "rom.shield_palettes",
 | 
			
		||||
      "friendlyName": "Underworld Palettes",
 | 
			
		||||
      "description": "Randomize the colors of the underworld (caves, dungeons, etc.), within reason.",
 | 
			
		||||
      "friendlyName": "Shield Palettes",
 | 
			
		||||
      "description": "Randomize the colors of the shield, within reason.",
 | 
			
		||||
      "inputType": "range",
 | 
			
		||||
      "subOptions": {
 | 
			
		||||
        "default": {
 | 
			
		||||
          "keyString": "rom.shield_palettes.default",
 | 
			
		||||
          "friendlyName": "Vanilla",
 | 
			
		||||
          "description": "Overworld colors will remain unchanged.",
 | 
			
		||||
          "description": "Shield colors will remain unchanged.",
 | 
			
		||||
          "defaultValue": 50
 | 
			
		||||
        },
 | 
			
		||||
        "random": {
 | 
			
		||||
          "keyString": "rom.shield_palettes.random",
 | 
			
		||||
          "friendlyName": "Random",
 | 
			
		||||
          "description": "Shuffles the colors of the overworld palette.",
 | 
			
		||||
          "description": "Shuffles the colors of the shield palette.",
 | 
			
		||||
          "defaultValue": 0
 | 
			
		||||
        },
 | 
			
		||||
        "blackout": {
 | 
			
		||||
          "keyString": "rom.shield_palettes.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
 | 
			
		||||
        },
 | 
			
		||||
		"grayscale": {
 | 
			
		||||
| 
						 | 
				
			
			@ -1696,26 +1800,26 @@
 | 
			
		|||
    },
 | 
			
		||||
	"sword_palettes": {
 | 
			
		||||
      "keyString": "rom.sword_palettes",
 | 
			
		||||
      "friendlyName": "Underworld Palettes",
 | 
			
		||||
      "description": "Randomize the colors of the underworld (caves, dungeons, etc.), within reason.",
 | 
			
		||||
      "friendlyName": "Sword Palettes",
 | 
			
		||||
      "description": "Randomize the colors of the sword, within reason.",
 | 
			
		||||
      "inputType": "range",
 | 
			
		||||
      "subOptions": {
 | 
			
		||||
        "default": {
 | 
			
		||||
          "keyString": "rom.sword_palettes.default",
 | 
			
		||||
          "friendlyName": "Vanilla",
 | 
			
		||||
          "description": "Overworld colors will remain unchanged.",
 | 
			
		||||
          "description": "Sword colors will remain unchanged.",
 | 
			
		||||
          "defaultValue": 50
 | 
			
		||||
        },
 | 
			
		||||
        "random": {
 | 
			
		||||
          "keyString": "rom.sword_palettes.random",
 | 
			
		||||
          "friendlyName": "Random",
 | 
			
		||||
          "description": "Shuffles the colors of the overworld palette.",
 | 
			
		||||
          "description": "Shuffles the colors of the sword palette.",
 | 
			
		||||
          "defaultValue": 0
 | 
			
		||||
        },
 | 
			
		||||
        "blackout": {
 | 
			
		||||
          "keyString": "rom.sword_palettes.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
 | 
			
		||||
        },
 | 
			
		||||
		"grayscale": {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -232,6 +232,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"
 | 
			
		||||
| 
						 | 
				
			
			@ -391,13 +407,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
 | 
			
		||||
| 
						 | 
				
			
			@ -19,10 +19,10 @@
 | 
			
		|||
                You can also upload a .zip with multiple YAMLs.
 | 
			
		||||
                A proper menu is in the works.
 | 
			
		||||
                {% 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 -%}
 | 
			
		||||
                    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 -%}
 | 
			
		||||
            </p>
 | 
			
		||||
            <p>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -122,7 +122,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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,6 +11,11 @@
 | 
			
		|||
    <div id="view-seed-wrapper">
 | 
			
		||||
        <div class="main-content">
 | 
			
		||||
            <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>
 | 
			
		||||
                <tbody>
 | 
			
		||||
                <tr>
 | 
			
		||||
| 
						 | 
				
			
			@ -27,6 +32,7 @@
 | 
			
		|||
                        <td><a href="{{ url_for("download_spoiler", seed_id=seed.id) }}">Download</a></td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {% if seed.multidata %}
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td>Players: </td>
 | 
			
		||||
                    <td>
 | 
			
		||||
| 
						 | 
				
			
			@ -55,6 +61,23 @@
 | 
			
		|||
                            {% endcall %}
 | 
			
		||||
                        </td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                {% else  %}
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td>Patches: </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>
 | 
			
		||||
            </table>
 | 
			
		||||
        </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 get_tracker(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 get_tracker(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 get_tracker(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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											Binary file not shown.
										
									
								
							| 
						 | 
				
			
			@ -11,6 +11,7 @@ AppName={#MyAppName}
 | 
			
		|||
AppVerName={#MyAppName}
 | 
			
		||||
DefaultDirName={commonappdata}\{#MyAppName}
 | 
			
		||||
DisableProgramGroupPage=yes
 | 
			
		||||
DefaultGroupName=Berserker's Multiworld
 | 
			
		||||
OutputDir=setups
 | 
			
		||||
OutputBaseFilename=Setup {#MyAppName}
 | 
			
		||||
Compression=lzma2
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -232,6 +232,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"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue