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