Merge branch 'master' into website-redesign

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

View File

@ -119,6 +119,10 @@ class World(object):
set_player_attr('treasure_hunt_icon', 'Triforce Piece')
set_player_attr('treasure_hunt_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)

View File

@ -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
View File

@ -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
View File

@ -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,

View File

@ -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

View File

@ -29,7 +29,7 @@ import Utils
from Utils import get_item_name_from_id, get_location_name_from_address, ReceivedItem, _version_tuple
from 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))])

View File

@ -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'))

View File

@ -397,6 +397,59 @@ shop_table = {
'Capacity Upgrade': (0x0115, ShopType.UpgradeShop, 0x04, True, True, [('Bomb Upgrade (+5)', 100, 7), ('Arrow Upgrade (+5)', 100, 7)])
}
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
View File

@ -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 = {

View File

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

View File

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

View File

@ -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:

View File

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

View File

@ -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

View File

@ -1206,6 +1206,110 @@
}
}
},
"countdown_start_time": {
"keyString": "countdown_start_time",
"friendlyName": "Countdown Starting Time",
"description": "The amount of time, in minutes, to start with in Timed Countdown and Timed OHKO modes.",
"inputType": "range",
"subOptions": {
"0": {
"keyString": "countdown_start_time.0",
"friendlyName": 0,
"description": "Start with no time on the timer. In Timed OHKO mode, start in OHKO mode.",
"defaultValue": 0
},
"10": {
"keyString": "countdown_start_time.10",
"friendlyName": 10,
"description": "Start with 10 minutes on the timer.",
"defaultValue": 50
},
"20": {
"keyString": "countdown_start_time.20",
"friendlyName": 20,
"description": "Start with 20 minutes on the timer.",
"defaultValue": 0
},
"30": {
"keyString": "countdown_start_time.30",
"friendlyName": 30,
"description": "Start with 30 minutes on the timer.",
"defaultValue": 0
},
"60": {
"keyString": "countdown_start_time.60",
"friendlyName": 60,
"description": "Start with an hour on the timer.",
"defaultValue": 0
}
}
},
"red_clock_time": {
"keyString": "red_clock_time",
"friendlyName": "Red Clock Time",
"description": "The amount of time, in minutes, to add to or subtract from the timer upon picking up a red clock.",
"inputType": "range",
"subOptions": {
"-2": {
"keyString": "red_clock_time.-2",
"friendlyName": -2,
"description": "Subtract 2 minutes from the timer upon picking up a red clock.",
"defaultValue": 0
},
"1": {
"keyString": "red_clock_time.1",
"friendlyName": 1,
"description": "Add a minute to the timer upon picking up a red clock.",
"defaultValue": 50
}
}
},
"blue_clock_time": {
"keyString": "blue_clock_time",
"friendlyName": "Blue Clock Time",
"description": "The amount of time, in minutes, to add to or subtract from the timer upon picking up a blue clock.",
"inputType": "range",
"subOptions": {
"1": {
"keyString": "blue_clock_time.1",
"friendlyName": 1,
"description": "Add a minute to the timer upon picking up a blue clock.",
"defaultValue": 0
},
"2": {
"keyString": "blue_clock_time.2",
"friendlyName": 2,
"description": "Add 2 minutes to the timer upon picking up a blue clock.",
"defaultValue": 50
}
}
},
"green_clock_time": {
"keyString": "green_clock_time",
"friendlyName": "Green Clock Time",
"description": "The amount of time, in minutes, to add to or subtract from the timer upon picking up a green clock.",
"inputType": "range",
"subOptions": {
"4": {
"keyString": "green_clock_time.4",
"friendlyName": 4,
"description": "Add 4 minutes to the timer upon picking up a green clock.",
"defaultValue": 50
},
"10": {
"keyString": "green_clock_time.10",
"friendlyName": 10,
"description": "Add 10 minutes to the timer upon picking up a green clock.",
"defaultValue": 0
},
"15": {
"keyString": "green_clock_time.15",
"friendlyName": 15,
"description": "Add 15 minutes to the timer upon picking up a green clock.",
"defaultValue": 0
}
}
},
"glitch_boots": {
"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": {

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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:&nbsp;</td>
<td>
@ -55,6 +61,23 @@
{% endcall %}
</td>
</tr>
{% else %}
<tr>
<td>Patches:&nbsp;</td>
<td>
<ul>
{% for patch in seed.patches %}
<li>
<a href="{{ url_for("download_raw_patch", seed_id=seed.id, player_id=patch.player) }}">Player {{ patch.player }}</a>
</li>
{% endfor %}
</ul>
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>

View File

@ -180,6 +180,25 @@ default_locations = {
60121, 60124, 60127, 1573217, 60130, 60133, 60136, 60139, 60142, 60145, 60148, 60151, 60157},
'Total': set()}
key_only_locations = {
'Light World': set(),
'Dark World': set(),
'Desert Palace': {0x140031, 0x14002b, 0x140061, 0x140028},
'Eastern Palace': {0x14005b, 0x140049},
'Hyrule Castle': {0x140037, 0x140034, 0x14000d, 0x14003d},
'Agahnims Tower': {0x140061, 0x140052},
'Tower of Hera': set(),
'Swamp Palace': {0x140019, 0x140016, 0x140013, 0x140010, 0x14000a},
'Thieves Town': {0x14005e, 0x14004f},
'Skull Woods': {0x14002e, 0x14001c},
'Ice Palace': {0x140004, 0x140022, 0x140025, 0x140046},
'Misery Mire': {0x140055, 0x14004c, 0x140064},
'Turtle Rock': {0x140058, 0x140007},
'Palace of Darkness': set(),
'Ganons Tower': {0x140040, 0x140043, 0x14003a, 0x14001f},
'Total': set()
}
key_locations = {"Desert Palace", "Eastern Palace", "Hyrule Castle", "Agahnims Tower", "Tower of Hera", "Swamp Palace",
"Thieves Town", "Skull Woods", "Ice Palace", "Misery Mire", "Turtle Rock", "Palace of Darkness",
"Ganons Tower"}
@ -191,6 +210,10 @@ for area, locations in default_locations.items():
for location in locations:
location_to_area[location] = area
for area, locations in key_only_locations.items():
for location in locations:
location_to_area[location] = area
checks_in_area = {area: len(checks) for area, checks in default_locations.items()}
checks_in_area["Total"] = 216
@ -235,6 +258,14 @@ def render_timedelta(delta: datetime.timedelta):
_multidata_cache = {}
def get_location_table(checks_table: dict) -> dict:
loc_to_area = {}
for area, locations in checks_table.items():
if area == "Total":
continue
for location in locations:
loc_to_area[location] = area
return loc_to_area
def get_static_room_data(room: Room):
result = _multidata_cache.get(room.seed.id, None)
@ -244,11 +275,30 @@ def get_static_room_data(room: Room):
# in > 100 players this can take a bit of time and is the main reason for the cache
locations = {tuple(k): tuple(v) for k, v in multidata['locations']}
names = multidata["names"]
seed_checks_in_area = checks_in_area.copy()
use_door_tracker = False
if "tags" in multidata:
use_door_tracker = "DR" in multidata["tags"]
result = locations, names, use_door_tracker
if use_door_tracker:
for area, checks in key_only_locations.items():
seed_checks_in_area[area] += len(checks)
seed_checks_in_area["Total"] = 249
if "checks_in_area" not in multidata:
player_checks_in_area = {playernumber: (seed_checks_in_area if use_door_tracker and
(0x140031, playernumber) in locations else checks_in_area)
for playernumber in range(1, len(names[0]) + 1)}
player_location_to_area = {playernumber: location_to_area
for playernumber in range(1, len(names[0]) + 1)}
else:
player_checks_in_area = {playernumber: {areaname: len(multidata["checks_in_area"][f'{playernumber}'][areaname])
if areaname != "Total" else multidata["checks_in_area"][f'{playernumber}']["Total"]
for areaname in ordered_areas}
for playernumber in range(1, len(names[0]) + 1)}
player_location_to_area = {playernumber: get_location_table(multidata["checks_in_area"][f'{playernumber}'])
for playernumber in range(1, len(names[0]) + 1)}
result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area
_multidata_cache[room.seed.id] = result
return result
@ -259,7 +309,7 @@ def 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.

View File

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

View File

@ -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"