Merge master into website-redesign
This commit is contained in:
commit
ddfcef09ec
|
@ -30,3 +30,5 @@ weights/
|
|||
_persistent_storage.yaml
|
||||
mystery_result_*.yaml
|
||||
/db.db3
|
||||
*-errors.txt
|
||||
success.txt
|
||||
|
|
|
@ -65,7 +65,10 @@ def main():
|
|||
logging.basicConfig(format='%(message)s', level=loglevel)
|
||||
args, path = adjust(args=args)
|
||||
from Utils import persistent_store
|
||||
persistent_store("adjuster", "last_settings", args)
|
||||
from Rom import Sprite
|
||||
if isinstance(args.sprite, Sprite):
|
||||
args.sprite = args.sprite.name
|
||||
persistent_store("adjuster", "last_settings_3", args)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
|
@ -27,7 +27,7 @@ def adjust(args):
|
|||
palettes_options['hud']=args.hud_palettes
|
||||
palettes_options['sword']=args.sword_palettes
|
||||
palettes_options['shield']=args.shield_palettes
|
||||
palettes_options['link']=args.link_palettes
|
||||
# palettes_options['link']=args.link_palettesvera
|
||||
|
||||
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic,
|
||||
args.sprite, palettes_options)
|
||||
|
|
|
@ -127,6 +127,7 @@ class World(object):
|
|||
set_player_attr('glitch_boots', True)
|
||||
set_player_attr('progression_balancing', True)
|
||||
set_player_attr('local_items', set())
|
||||
set_player_attr('non_local_items', set())
|
||||
set_player_attr('triforce_pieces_available', 30)
|
||||
set_player_attr('triforce_pieces_required', 20)
|
||||
set_player_attr('shop_shuffle', 'off')
|
||||
|
|
56
Bosses.py
56
Bosses.py
|
@ -153,27 +153,33 @@ boss_table = {
|
|||
}
|
||||
|
||||
|
||||
def can_place_boss(world, player: int, boss: str, dungeon_name: str, level: Optional[str] = None) -> bool:
|
||||
if dungeon_name in ['Ganons Tower', 'Inverted Ganons Tower'] and level == 'top':
|
||||
if boss in ["Armos Knights", "Arrghus", "Blind", "Trinexx", "Lanmolas"]:
|
||||
def can_place_boss(boss: str, dungeon_name: str, level: Optional[str] = None) -> bool:
|
||||
#blacklist approach
|
||||
if boss in {"Agahnim", "Agahnim2", "Ganon"}:
|
||||
return False
|
||||
|
||||
if dungeon_name in ['Ganons Tower', 'Inverted Ganons Tower'] and level == 'middle':
|
||||
if boss in ["Blind"]:
|
||||
if dungeon_name == 'Ganons Tower':
|
||||
if level == 'top':
|
||||
if boss in {"Armos Knights", "Arrghus", "Blind", "Trinexx", "Lanmolas"}:
|
||||
return False
|
||||
elif level == 'middle':
|
||||
if boss == "Blind":
|
||||
return False
|
||||
|
||||
if dungeon_name == 'Tower of Hera' and boss in ["Armos Knights", "Arrghus", "Blind", "Trinexx", "Lanmolas"]:
|
||||
elif dungeon_name == 'Tower of Hera':
|
||||
if boss in {"Armos Knights", "Arrghus", "Blind", "Trinexx", "Lanmolas"}:
|
||||
return False
|
||||
|
||||
if dungeon_name == 'Skull Woods' and boss in ["Trinexx"]:
|
||||
elif dungeon_name == 'Skull Woods' :
|
||||
if boss == "Trinexx":
|
||||
return False
|
||||
|
||||
if boss in ["Agahnim", "Agahnim2", "Ganon"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def place_boss(world, player: int, boss: str, location: str, level: Optional[str]):
|
||||
if location == 'Ganons Tower' and world.mode[player] == 'inverted':
|
||||
location = 'Inverted Ganons Tower'
|
||||
logging.debug('Placing boss %s at %s', boss, location + (' (' + level + ')' if level else ''))
|
||||
world.get_dungeon(location, player).bosses[level] = BossFactory(boss, player)
|
||||
|
||||
|
@ -182,7 +188,6 @@ def place_bosses(world, player: int):
|
|||
if world.boss_shuffle[player] == 'none':
|
||||
return
|
||||
# Most to least restrictive order
|
||||
if world.mode[player] != 'inverted':
|
||||
boss_locations = [
|
||||
['Ganons Tower', 'top'],
|
||||
['Tower of Hera', None],
|
||||
|
@ -198,27 +203,10 @@ def place_bosses(world, player: int):
|
|||
['Turtle Rock', None],
|
||||
['Ganons Tower', 'bottom'],
|
||||
]
|
||||
else:
|
||||
boss_locations = [
|
||||
['Inverted Ganons Tower', 'top'],
|
||||
['Tower of Hera', None],
|
||||
['Skull Woods', None],
|
||||
['Inverted Ganons Tower', 'middle'],
|
||||
['Eastern Palace', None],
|
||||
['Desert Palace', None],
|
||||
['Palace of Darkness', None],
|
||||
['Swamp Palace', None],
|
||||
['Thieves Town', None],
|
||||
['Ice Palace', None],
|
||||
['Misery Mire', None],
|
||||
['Turtle Rock', None],
|
||||
['Inverted Ganons Tower', 'bottom'],
|
||||
]
|
||||
|
||||
all_bosses = sorted(boss_table.keys()) # sorted to be deterministic on older pythons
|
||||
placeable_bosses = [boss for boss in all_bosses if boss not in ['Agahnim', 'Agahnim2', 'Ganon']]
|
||||
anywhere_bosses = [boss for boss in placeable_bosses if all(
|
||||
can_place_boss(world, player, boss, loc, level) for loc, level in boss_locations)]
|
||||
|
||||
if world.boss_shuffle[player] in ["basic", "normal"]:
|
||||
if world.boss_shuffle[player] == "basic": # vanilla bosses shuffled
|
||||
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
|
||||
|
@ -228,8 +216,8 @@ def place_bosses(world, player: int):
|
|||
logging.debug('Bosses chosen %s', bosses)
|
||||
|
||||
world.random.shuffle(bosses)
|
||||
for [loc, level] in boss_locations:
|
||||
boss = next((b for b in bosses if can_place_boss(world, player, b, loc, level)), None)
|
||||
for loc, level in boss_locations:
|
||||
boss = next((b for b in bosses if can_place_boss(b, loc, level)), None)
|
||||
if not boss:
|
||||
loc_text = loc + (' (' + level + ')' if level else '')
|
||||
raise FillError('Could not place boss for location %s' % loc_text)
|
||||
|
@ -237,10 +225,10 @@ def place_bosses(world, player: int):
|
|||
place_boss(world, player, boss, loc, level)
|
||||
|
||||
elif world.boss_shuffle[player] == "chaos": # all bosses chosen at random
|
||||
for [loc, level] in boss_locations:
|
||||
for loc, level in boss_locations:
|
||||
try:
|
||||
boss = world.random.choice(
|
||||
[b for b in placeable_bosses if can_place_boss(world, player, b, loc, level)])
|
||||
[b for b in placeable_bosses if can_place_boss(b, loc, level)])
|
||||
except IndexError:
|
||||
loc_text = loc + (' (' + level + ')' if level else '')
|
||||
raise FillError('Could not place boss for location %s' % loc_text)
|
||||
|
@ -252,14 +240,14 @@ def place_bosses(world, player: int):
|
|||
remaining_boss_locations = []
|
||||
for loc, level in boss_locations:
|
||||
# place that boss where it can go
|
||||
if can_place_boss(world, player, primary_boss, loc, level):
|
||||
if can_place_boss(primary_boss, loc, level):
|
||||
place_boss(world, player, primary_boss, loc, level)
|
||||
else:
|
||||
remaining_boss_locations.append((loc, level))
|
||||
if remaining_boss_locations:
|
||||
# pick a boss to go into the remaining locations
|
||||
remaining_boss = world.random.choice([boss for boss in placeable_bosses if all(
|
||||
can_place_boss(world, player, boss, loc, level) for loc, level in remaining_boss_locations)])
|
||||
can_place_boss(boss, loc, level) for loc, level in remaining_boss_locations)])
|
||||
for loc, level in remaining_boss_locations:
|
||||
place_boss(world, player, remaining_boss, loc, level)
|
||||
else:
|
||||
|
|
|
@ -181,7 +181,7 @@ def parse_arguments(argv, no_defaults=False):
|
|||
slightly biased to placing progression items with
|
||||
less restrictions.
|
||||
''')
|
||||
parser.add_argument('--shuffle', default=defval('full'), const='full', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeonsfull', 'dungeonssimple'],
|
||||
parser.add_argument('--shuffle', default=defval('vanilla'), const='vanilla', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeonsfull', 'dungeonssimple'],
|
||||
help='''\
|
||||
Select Entrance Shuffling Algorithm. (default: %(default)s)
|
||||
Full: Mix cave and dungeon entrances freely while limiting
|
||||
|
@ -266,6 +266,8 @@ def parse_arguments(argv, no_defaults=False):
|
|||
help='Specifies a list of items that will be in your starting inventory (separated by commas)')
|
||||
parser.add_argument('--local_items', default=defval(''),
|
||||
help='Specifies a list of items that will not spread across the multiworld (separated by commas)')
|
||||
parser.add_argument('--non_local_items', default=defval(''),
|
||||
help='Specifies a list of items that will spread across the multiworld (separated by commas)')
|
||||
parser.add_argument('--custom', default=defval(False), help='Not supported.')
|
||||
parser.add_argument('--customitemarray', default=defval(False), help='Not supported.')
|
||||
parser.add_argument('--accessibility', default=defval('items'), const='items', nargs='?', choices=['items', 'locations', 'none'], help='''\
|
||||
|
@ -376,7 +378,7 @@ def parse_arguments(argv, no_defaults=False):
|
|||
'shuffle', 'crystals_ganon', 'crystals_gt', 'open_pyramid', 'timer',
|
||||
'countdown_start_time', 'red_clock_time', 'blue_clock_time', 'green_clock_time',
|
||||
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
|
||||
'local_items', 'retro', 'accessibility', 'hints', 'beemizer',
|
||||
'local_items', 'non_local_items', 'retro', 'accessibility', 'hints', 'beemizer',
|
||||
'shufflebosses', 'enemy_shuffle', 'enemy_health', 'enemy_damage', 'shufflepots',
|
||||
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor',
|
||||
'heartbeep', "skip_progression_balancing", "triforce_pieces_available",
|
||||
|
|
3
Fill.py
3
Fill.py
|
@ -54,8 +54,9 @@ def fill_restrictive(world, base_state: CollectionState, locations, itempool, si
|
|||
for location in region.locations:
|
||||
if location.item and not location.event:
|
||||
placements.append(location)
|
||||
|
||||
raise FillError(f'No more spots to place {item_to_place}, locations {locations} are invalid. '
|
||||
f'Already placed {len(placements)}: {", ".join(placements)}')
|
||||
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
|
||||
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
locations.remove(spot_to_fill)
|
||||
|
|
314
Gui.py
314
Gui.py
|
@ -34,7 +34,7 @@ def guiMain(args=None):
|
|||
customWindow = ttk.Frame(notebook)
|
||||
notebook.add(randomizerWindow, text='Randomize')
|
||||
notebook.add(adjustWindow, text='Adjust')
|
||||
notebook.add(customWindow, text='Custom')
|
||||
notebook.add(customWindow, text='Custom Items')
|
||||
notebook.pack()
|
||||
|
||||
# Shared Controls
|
||||
|
@ -96,8 +96,7 @@ def guiMain(args=None):
|
|||
hintsVar = IntVar()
|
||||
hintsVar.set(1) # set default
|
||||
hintsCheckbutton = Checkbutton(checkBoxFrame, text="Include Helpful Hints", variable=hintsVar)
|
||||
customVar = IntVar()
|
||||
customCheckbutton = Checkbutton(checkBoxFrame, text="Use custom item pool", variable=customVar)
|
||||
|
||||
balancingVar = IntVar()
|
||||
balancingVar.set(1) # set default
|
||||
balancingCheckbutton = Checkbutton(checkBoxFrame, text="Multiworld Progression Balancing", variable=balancingVar)
|
||||
|
@ -116,60 +115,11 @@ def guiMain(args=None):
|
|||
retroCheckbutton.pack(expand=True, anchor=W)
|
||||
shuffleGanonCheckbutton.pack(expand=True, anchor=W)
|
||||
hintsCheckbutton.pack(expand=True, anchor=W)
|
||||
customCheckbutton.pack(expand=True, anchor=W)
|
||||
|
||||
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)
|
||||
|
@ -316,7 +266,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)
|
||||
|
@ -844,7 +794,10 @@ def guiMain(args=None):
|
|||
else:
|
||||
messagebox.showinfo(title="Success", message="Rom patched successfully")
|
||||
from Utils import persistent_store
|
||||
persistent_store("adjuster", "last_settings", guiargs)
|
||||
from Rom import Sprite
|
||||
if isinstance(guiargs.sprite, Sprite):
|
||||
guiargs.sprite = guiargs.sprite.name
|
||||
persistent_store("adjuster", "last_settings_3", guiargs)
|
||||
|
||||
adjustButton = Button(bottomFrame2, text='Adjust Rom', command=adjustRom)
|
||||
|
||||
|
@ -866,12 +819,65 @@ def guiMain(args=None):
|
|||
return False
|
||||
vcmd=(topFrame3.register(validation), '%P')
|
||||
|
||||
timerOptionsFrame = LabelFrame(topFrame3, 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)
|
||||
|
||||
timerOptionsFrame.pack(expand=True, fill=BOTH, padx=3)
|
||||
|
||||
|
||||
itemList1 = Frame(topFrame3)
|
||||
itemList2 = Frame(topFrame3)
|
||||
itemList3 = Frame(topFrame3)
|
||||
itemList4 = Frame(topFrame3)
|
||||
itemList5 = Frame(topFrame3)
|
||||
|
||||
customVar = IntVar()
|
||||
customCheckbutton = Checkbutton(topFrame3, text="Use custom item pool", variable=customVar)
|
||||
customCheckbutton.pack(expand=True, anchor=W)
|
||||
|
||||
bowFrame = Frame(itemList1)
|
||||
bowLabel = Label(bowFrame, text='Bow')
|
||||
bowVar = StringVar(value='0')
|
||||
|
@ -1492,7 +1498,8 @@ def guiMain(args=None):
|
|||
|
||||
mainWindow.mainloop()
|
||||
|
||||
class SpriteSelector(object):
|
||||
|
||||
class SpriteSelector():
|
||||
def __init__(self, parent, callback, adjuster=False):
|
||||
if is_bundled():
|
||||
self.deploy_icons()
|
||||
|
@ -1615,93 +1622,15 @@ class SpriteSelector(object):
|
|||
self.window.destroy()
|
||||
self.parent.update()
|
||||
|
||||
def work(task):
|
||||
resultmessage = ""
|
||||
successful = True
|
||||
|
||||
def finished():
|
||||
task.close_window()
|
||||
def on_finish(successful, resultmessage):
|
||||
if successful:
|
||||
messagebox.showinfo("Sprite Updater", resultmessage)
|
||||
else:
|
||||
logging.error(resultmessage)
|
||||
messagebox.showerror("Sprite Updater", resultmessage)
|
||||
SpriteSelector(self.parent, self.callback, self.adjuster)
|
||||
|
||||
try:
|
||||
task.update_status("Downloading alttpr sprites list")
|
||||
with urlopen('https://alttpr.com/sprites') as response:
|
||||
sprites_arr = json.loads(response.read().decode("utf-8"))
|
||||
except Exception as e:
|
||||
resultmessage = "Error getting list of alttpr sprites. Sprites not updated.\n\n%s: %s" % (type(e).__name__, e)
|
||||
successful = False
|
||||
task.queue_event(finished)
|
||||
return
|
||||
|
||||
try:
|
||||
task.update_status("Determining needed sprites")
|
||||
current_sprites = [os.path.basename(file) for file in glob(self.alttpr_sprite_dir + '/*')]
|
||||
alttpr_sprites = [(sprite['file'], os.path.basename(urlparse(sprite['file']).path)) for sprite in sprites_arr]
|
||||
needed_sprites = [(sprite_url, filename) for (sprite_url, filename) in alttpr_sprites if filename not in current_sprites]
|
||||
|
||||
alttpr_filenames = [filename for (_, filename) in alttpr_sprites]
|
||||
obsolete_sprites = [sprite for sprite in current_sprites if sprite not in alttpr_filenames]
|
||||
except Exception as e:
|
||||
resultmessage = "Error Determining which sprites to update. Sprites not updated.\n\n%s: %s" % (type(e).__name__, e)
|
||||
successful = False
|
||||
task.queue_event(finished)
|
||||
return
|
||||
|
||||
|
||||
def dl(sprite_url, filename):
|
||||
target = os.path.join(self.alttpr_sprite_dir, filename)
|
||||
with urlopen(sprite_url) as response, open(target, 'wb') as out:
|
||||
shutil.copyfileobj(response, out)
|
||||
|
||||
def rem(sprite):
|
||||
os.remove(os.path.join(self.alttpr_sprite_dir, sprite))
|
||||
|
||||
|
||||
with ThreadPoolExecutor() as pool:
|
||||
dl_tasks = []
|
||||
rem_tasks = []
|
||||
|
||||
for (sprite_url, filename) in needed_sprites:
|
||||
dl_tasks.append(pool.submit(dl, sprite_url, filename))
|
||||
|
||||
for sprite in obsolete_sprites:
|
||||
rem_tasks.append(pool.submit(rem, sprite))
|
||||
|
||||
deleted = 0
|
||||
updated = 0
|
||||
|
||||
for dl_task in as_completed(dl_tasks):
|
||||
updated += 1
|
||||
task.update_status("Downloading needed sprite %g/%g" % (updated, len(needed_sprites)))
|
||||
try:
|
||||
dl_task.result()
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
resultmessage = "Error downloading sprite. Not all sprites updated.\n\n%s: %s" % (
|
||||
type(e).__name__, e)
|
||||
successful = False
|
||||
|
||||
for rem_task in as_completed(rem_tasks):
|
||||
deleted += 1
|
||||
task.update_status("Removing obsolete sprite %g/%g" % (deleted, len(obsolete_sprites)))
|
||||
try:
|
||||
rem_task.result()
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
resultmessage = "Error removing obsolete sprite. Not all sprites updated.\n\n%s: %s" % (
|
||||
type(e).__name__, e)
|
||||
successful = False
|
||||
|
||||
if successful:
|
||||
resultmessage = "alttpr sprites updated successfully"
|
||||
|
||||
task.queue_event(finished)
|
||||
|
||||
BackgroundTaskProgress(self.parent, work, "Updating Sprites")
|
||||
BackgroundTaskProgress(self.parent, update_sprites, "Updating Sprites", on_finish)
|
||||
|
||||
|
||||
def browse_for_sprite(self):
|
||||
|
@ -1745,34 +1674,104 @@ class SpriteSelector(object):
|
|||
self.callback(spritename)
|
||||
self.window.destroy()
|
||||
|
||||
|
||||
def deploy_icons(self):
|
||||
if not os.path.exists(self.custom_sprite_dir):
|
||||
os.makedirs(self.custom_sprite_dir)
|
||||
if not os.path.exists(self.alttpr_sprite_dir):
|
||||
shutil.copytree(self.local_alttpr_sprite_dir, self.alttpr_sprite_dir)
|
||||
|
||||
@property
|
||||
def alttpr_sprite_dir(self):
|
||||
if is_bundled():
|
||||
return output_path("sprites", "alttpr")
|
||||
return self.local_alttpr_sprite_dir
|
||||
|
||||
@property
|
||||
def local_alttpr_sprite_dir(self):
|
||||
return local_path("data", "sprites", "alttpr")
|
||||
|
||||
@property
|
||||
def custom_sprite_dir(self):
|
||||
if is_bundled():
|
||||
return output_path("sprites", "custom")
|
||||
return self.local_custom_sprite_dir
|
||||
|
||||
@property
|
||||
def local_custom_sprite_dir(self):
|
||||
return local_path("data", "sprites", "custom")
|
||||
|
||||
|
||||
def update_sprites(task, on_finish=None):
|
||||
resultmessage = ""
|
||||
successful = True
|
||||
sprite_dir = local_path("data", "sprites", "alttpr")
|
||||
os.makedirs(sprite_dir, exist_ok=True)
|
||||
|
||||
def finished():
|
||||
task.close_window()
|
||||
if on_finish:
|
||||
on_finish(successful, resultmessage)
|
||||
|
||||
try:
|
||||
task.update_status("Downloading alttpr sprites list")
|
||||
with urlopen('https://alttpr.com/sprites') as response:
|
||||
sprites_arr = json.loads(response.read().decode("utf-8"))
|
||||
except Exception as e:
|
||||
resultmessage = "Error getting list of alttpr sprites. Sprites not updated.\n\n%s: %s" % (type(e).__name__, e)
|
||||
successful = False
|
||||
task.queue_event(finished)
|
||||
return
|
||||
|
||||
try:
|
||||
task.update_status("Determining needed sprites")
|
||||
current_sprites = [os.path.basename(file) for file in glob(sprite_dir + '/*')]
|
||||
alttpr_sprites = [(sprite['file'], os.path.basename(urlparse(sprite['file']).path)) for sprite in sprites_arr]
|
||||
needed_sprites = [(sprite_url, filename) for (sprite_url, filename) in alttpr_sprites if filename not in current_sprites]
|
||||
|
||||
alttpr_filenames = [filename for (_, filename) in alttpr_sprites]
|
||||
obsolete_sprites = [sprite for sprite in current_sprites if sprite not in alttpr_filenames]
|
||||
except Exception as e:
|
||||
resultmessage = "Error Determining which sprites to update. Sprites not updated.\n\n%s: %s" % (type(e).__name__, e)
|
||||
successful = False
|
||||
task.queue_event(finished)
|
||||
return
|
||||
|
||||
|
||||
def dl(sprite_url, filename):
|
||||
target = os.path.join(sprite_dir, filename)
|
||||
with urlopen(sprite_url) as response, open(target, 'wb') as out:
|
||||
shutil.copyfileobj(response, out)
|
||||
|
||||
def rem(sprite):
|
||||
os.remove(os.path.join(sprite_dir, sprite))
|
||||
|
||||
|
||||
with ThreadPoolExecutor() as pool:
|
||||
dl_tasks = []
|
||||
rem_tasks = []
|
||||
|
||||
for (sprite_url, filename) in needed_sprites:
|
||||
dl_tasks.append(pool.submit(dl, sprite_url, filename))
|
||||
|
||||
for sprite in obsolete_sprites:
|
||||
rem_tasks.append(pool.submit(rem, sprite))
|
||||
|
||||
deleted = 0
|
||||
updated = 0
|
||||
|
||||
for dl_task in as_completed(dl_tasks):
|
||||
updated += 1
|
||||
task.update_status("Downloading needed sprite %g/%g" % (updated, len(needed_sprites)))
|
||||
try:
|
||||
dl_task.result()
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
resultmessage = "Error downloading sprite. Not all sprites updated.\n\n%s: %s" % (
|
||||
type(e).__name__, e)
|
||||
successful = False
|
||||
|
||||
for rem_task in as_completed(rem_tasks):
|
||||
deleted += 1
|
||||
task.update_status("Removing obsolete sprite %g/%g" % (deleted, len(obsolete_sprites)))
|
||||
try:
|
||||
rem_task.result()
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
resultmessage = "Error removing obsolete sprite. Not all sprites updated.\n\n%s: %s" % (
|
||||
type(e).__name__, e)
|
||||
successful = False
|
||||
|
||||
if successful:
|
||||
resultmessage = "alttpr sprites updated successfully"
|
||||
|
||||
task.queue_event(finished)
|
||||
|
||||
def get_image_for_sprite(sprite, gif_only: bool = False):
|
||||
if not sprite.valid:
|
||||
return None
|
||||
|
@ -1877,5 +1876,16 @@ def get_image_for_sprite(sprite, gif_only: bool = False):
|
|||
return image.zoom(2)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
if "update_sprites" in sys.argv:
|
||||
import threading
|
||||
done = threading.Event()
|
||||
top = Tk()
|
||||
top.withdraw()
|
||||
BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
|
||||
while not done.isSet():
|
||||
top.update()
|
||||
print("Done updating sprites")
|
||||
else:
|
||||
logging.basicConfig(format='%(message)s', level=logging.INFO)
|
||||
guiMain()
|
||||
|
|
|
@ -14,12 +14,12 @@ def set_icon(window):
|
|||
# some which may be platform specific, or depend on if the TCL library was compiled without
|
||||
# multithreading support. Therefore I will assume it is not thread safe to avoid any possible problems
|
||||
class BackgroundTask(object):
|
||||
def __init__(self, window, code_to_run):
|
||||
def __init__(self, window, code_to_run, *args):
|
||||
self.window = window
|
||||
self.queue = queue.Queue()
|
||||
self.running = True
|
||||
self.process_queue()
|
||||
self.task = threading.Thread(target=code_to_run, args=(self,))
|
||||
self.task = threading.Thread(target=code_to_run, args=(self, *args))
|
||||
self.task.start()
|
||||
|
||||
def stop(self):
|
||||
|
@ -45,7 +45,7 @@ class BackgroundTask(object):
|
|||
self.window.after(100, self.process_queue)
|
||||
|
||||
class BackgroundTaskProgress(BackgroundTask):
|
||||
def __init__(self, parent, code_to_run, title):
|
||||
def __init__(self, parent, code_to_run, title, *args):
|
||||
self.parent = parent
|
||||
self.window = tk.Toplevel(parent)
|
||||
self.window['padx'] = 5
|
||||
|
@ -65,7 +65,7 @@ class BackgroundTaskProgress(BackgroundTask):
|
|||
|
||||
set_icon(self.window)
|
||||
self.window.focus()
|
||||
super().__init__(self.window, code_to_run)
|
||||
super().__init__(self.window, code_to_run, *args)
|
||||
|
||||
#safe to call from worker thread
|
||||
def update_status(self, text):
|
||||
|
|
10
Main.py
10
Main.py
|
@ -10,7 +10,7 @@ import zlib
|
|||
import concurrent.futures
|
||||
|
||||
from BaseClasses import World, CollectionState, Item, Region, Location, Shop
|
||||
from Items import ItemFactory
|
||||
from Items import ItemFactory, item_table
|
||||
from Regions import create_regions, create_shops, mark_light_world_regions, lookup_vanilla_location_to_entrance
|
||||
from InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
||||
from EntranceShuffle import link_entrances, link_inverted_entrances
|
||||
|
@ -110,7 +110,13 @@ def main(args, seed=None):
|
|||
item = ItemFactory(tok.strip(), player)
|
||||
if item:
|
||||
world.push_precollected(item)
|
||||
world.local_items[player] = {item.strip() for item in args.local_items[player].split(',')}
|
||||
# item in item_table gets checked in mystery, but not CLI - so we double-check here
|
||||
world.local_items[player] = {item.strip() for item in args.local_items[player].split(',') if
|
||||
item.strip() in item_table}
|
||||
world.non_local_items[player] = {item.strip() for item in args.non_local_items[player].split(',') if
|
||||
item.strip() in item_table}
|
||||
# items can't be both local and non-local, prefer local
|
||||
world.non_local_items[player] -= world.local_items[player]
|
||||
|
||||
world.triforce_pieces_available[player] = max(world.triforce_pieces_available[player], world.triforce_pieces_required[player])
|
||||
|
||||
|
|
|
@ -73,7 +73,6 @@ class Context():
|
|||
self.snes_reconnect_address = None
|
||||
self.snes_recv_queue = asyncio.Queue()
|
||||
self.snes_request_lock = asyncio.Lock()
|
||||
self.is_sd2snes = False
|
||||
self.snes_write_buffer = []
|
||||
|
||||
self.server_task = None
|
||||
|
@ -522,15 +521,6 @@ async def snes_connect(ctx: Context, address):
|
|||
ctx.snes_attached_device = (devices.index(device), device)
|
||||
ctx.ui_node.send_connection_status(ctx)
|
||||
|
||||
if 'sd2snes' in device.lower() or (len(device) == 4 and device[:3] == 'COM'):
|
||||
ctx.ui_node.log_info("SD2SNES Detected")
|
||||
ctx.is_sd2snes = True
|
||||
await ctx.snes_socket.send(json.dumps({"Opcode" : "Info", "Space" : "SNES"}))
|
||||
reply = json.loads(await ctx.snes_socket.recv())
|
||||
if reply and 'Results' in reply:
|
||||
ctx.ui_node.log_info(reply['Results'])
|
||||
else:
|
||||
ctx.is_sd2snes = False
|
||||
|
||||
ctx.snes_reconnect_address = address
|
||||
recv_task = asyncio.create_task(snes_recv_loop(ctx))
|
||||
|
@ -645,37 +635,8 @@ async def snes_write(ctx : Context, write_list):
|
|||
if ctx.snes_state != SNES_ATTACHED or ctx.snes_socket is None or not ctx.snes_socket.open or ctx.snes_socket.closed:
|
||||
return False
|
||||
|
||||
PutAddress_Request = {
|
||||
"Opcode" : "PutAddress",
|
||||
"Operands" : []
|
||||
}
|
||||
PutAddress_Request = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'}
|
||||
|
||||
if ctx.is_sd2snes:
|
||||
cmd = b'\x00\xE2\x20\x48\xEB\x48'
|
||||
|
||||
for address, data in write_list:
|
||||
if (address < WRAM_START) or ((address + len(data)) > (WRAM_START + WRAM_SIZE)):
|
||||
ctx.ui_node.log_error("SD2SNES: Write out of range %s (%d)" % (hex(address), len(data)))
|
||||
return False
|
||||
for ptr, byte in enumerate(data, address + 0x7E0000 - WRAM_START):
|
||||
cmd += b'\xA9' # LDA
|
||||
cmd += bytes([byte])
|
||||
cmd += b'\x8F' # STA.l
|
||||
cmd += bytes([ptr & 0xFF, (ptr >> 8) & 0xFF, (ptr >> 16) & 0xFF])
|
||||
|
||||
cmd += b'\xA9\x00\x8F\x00\x2C\x00\x68\xEB\x68\x28\x6C\xEA\xFF\x08'
|
||||
|
||||
PutAddress_Request['Space'] = 'CMD'
|
||||
PutAddress_Request['Operands'] = ["2C00", hex(len(cmd)-1)[2:], "2C00", "1"]
|
||||
try:
|
||||
if ctx.snes_socket is not None:
|
||||
await ctx.snes_socket.send(json.dumps(PutAddress_Request))
|
||||
if ctx.snes_socket is not None:
|
||||
await ctx.snes_socket.send(cmd)
|
||||
except websockets.ConnectionClosed:
|
||||
return False
|
||||
else:
|
||||
PutAddress_Request['Space'] = 'SNES'
|
||||
try:
|
||||
#will pack those requests as soon as qusb2snes actually supports that for real
|
||||
for address, data in write_list:
|
||||
|
@ -685,6 +646,7 @@ async def snes_write(ctx : Context, write_list):
|
|||
if ctx.snes_socket is not None:
|
||||
await ctx.snes_socket.send(data)
|
||||
except websockets.ConnectionClosed:
|
||||
logging.warning("Could not write data to SNES")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
@ -811,8 +773,8 @@ async def process_server_cmd(ctx: Context, cmd, args):
|
|||
if args['password']:
|
||||
ctx.ui_node.log_info('Password required')
|
||||
if "forfeit_mode" in args: # could also be version > 2.2.1, but going with implicit content here
|
||||
logging.info("Forfeit setting: "+args["forfeit_mode"])
|
||||
logging.info("Remaining setting: "+args["remaining_mode"])
|
||||
logging.info(f"Forfeit setting: {args['forfeit_mode']}")
|
||||
logging.info(f"Remaining setting: {args['remaining_mode']}")
|
||||
logging.info(f"A !hint costs {args['hint_cost']} points and you get {args['location_check_points']}"
|
||||
f" for each location checked.")
|
||||
ctx.hint_cost = int(args['hint_cost'])
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
__author__ = "Berserker55" # you can find me on the ALTTP Randomizer Discord
|
||||
__author__ = "Berserker55" # you can find me on discord.gg/8Z65BR2
|
||||
|
||||
"""
|
||||
This script launches a Multiplayer "Multiworld" Mystery Game
|
||||
|
@ -18,16 +18,18 @@ import sys
|
|||
import threading
|
||||
import concurrent.futures
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
|
||||
def feedback(text: str):
|
||||
print(text)
|
||||
logging.info(text)
|
||||
input("Press Enter to ignore and probably crash.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(format='%(message)s', level=logging.INFO)
|
||||
try:
|
||||
print(f"{__author__}'s MultiMystery Launcher")
|
||||
logging.info(f"{__author__}'s MultiMystery Launcher")
|
||||
import ModuleUpdate
|
||||
|
||||
ModuleUpdate.update()
|
||||
|
@ -46,6 +48,7 @@ if __name__ == "__main__":
|
|||
output_path = options["general_options"]["output_path"]
|
||||
enemizer_path = multi_mystery_options["enemizer_path"]
|
||||
player_files_path = multi_mystery_options["player_files_path"]
|
||||
target_player_count = multi_mystery_options["players"]
|
||||
race = multi_mystery_options["race"]
|
||||
create_spoiler = multi_mystery_options["create_spoiler"]
|
||||
zip_roms = multi_mystery_options["zip_roms"]
|
||||
|
@ -56,36 +59,31 @@ if __name__ == "__main__":
|
|||
# zip_password = multi_mystery_options["zip_password"] not at this time
|
||||
player_name = multi_mystery_options["player_name"]
|
||||
meta_file_path = multi_mystery_options["meta_file_path"]
|
||||
weights_file_path = multi_mystery_options["weights_file_path"]
|
||||
teams = multi_mystery_options["teams"]
|
||||
rom_file = options["general_options"]["rom_file"]
|
||||
host = options["server_options"]["host"]
|
||||
port = options["server_options"]["port"]
|
||||
|
||||
|
||||
py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
||||
|
||||
if not os.path.exists(enemizer_path):
|
||||
feedback(f"Enemizer not found at {enemizer_path}, please adjust the path in MultiMystery.py's config or put Enemizer in the default location.")
|
||||
feedback(
|
||||
f"Enemizer not found at {enemizer_path}, please adjust the path in MultiMystery.py's config or put Enemizer in the default location.")
|
||||
if not os.path.exists(rom_file):
|
||||
feedback(f"Base rom is expected as {rom_file} in the Multiworld root folder please place/rename it there.")
|
||||
player_files = []
|
||||
os.makedirs(player_files_path, exist_ok=True)
|
||||
for file in os.listdir(player_files_path):
|
||||
lfile = file.lower()
|
||||
if lfile.endswith(".yaml") and lfile != meta_file_path.lower():
|
||||
if lfile.endswith(".yaml") and lfile != meta_file_path.lower() and lfile != weights_file_path.lower():
|
||||
player_files.append(file)
|
||||
print(f"Found player's file {file}.")
|
||||
player_count = len(player_files)
|
||||
if player_count == 0:
|
||||
feedback(f"No player files found. Please put them in a {player_files_path} folder.")
|
||||
else:
|
||||
print(player_count, "Players found.")
|
||||
logging.info(f"Found player's file {file}.")
|
||||
|
||||
player_string = ""
|
||||
for i, file in enumerate(player_files, 1):
|
||||
player_string += f"--p{i} \"{os.path.join(player_files_path, file)}\" "
|
||||
|
||||
|
||||
if os.path.exists("BerserkerMultiServer.exe"):
|
||||
basemysterycommand = "BerserkerMystery.exe" # compiled windows
|
||||
elif os.path.exists("BerserkerMultiServer"):
|
||||
|
@ -93,7 +91,18 @@ if __name__ == "__main__":
|
|||
else:
|
||||
basemysterycommand = f"py -{py_version} Mystery.py" # source
|
||||
|
||||
command = f"{basemysterycommand} --multi {len(player_files)} {player_string} " \
|
||||
weights_file_path = os.path.join(player_files_path, weights_file_path)
|
||||
if os.path.exists(weights_file_path):
|
||||
target_player_count = max(len(player_files), target_player_count)
|
||||
else:
|
||||
target_player_count = len(player_files)
|
||||
|
||||
if target_player_count == 0:
|
||||
feedback(f"No player files found. Please put them in a {player_files_path} folder.")
|
||||
else:
|
||||
logging.info(f"{target_player_count} Players found.")
|
||||
|
||||
command = f"{basemysterycommand} --multi {target_player_count} {player_string} " \
|
||||
f"--rom \"{rom_file}\" --enemizercli \"{enemizer_path}\" " \
|
||||
f"--outputpath \"{output_path}\" --teams {teams}"
|
||||
|
||||
|
@ -107,13 +116,15 @@ if __name__ == "__main__":
|
|||
command += " --race"
|
||||
if os.path.exists(os.path.join(player_files_path, meta_file_path)):
|
||||
command += f" --meta {os.path.join(player_files_path, meta_file_path)}"
|
||||
if os.path.exists(weights_file_path):
|
||||
command += f" --weights {weights_file_path}"
|
||||
|
||||
print(command)
|
||||
logging.info(command)
|
||||
import time
|
||||
|
||||
start = time.perf_counter()
|
||||
text = subprocess.check_output(command, shell=True).decode()
|
||||
print(f"Took {time.perf_counter() - start:.3f} seconds to generate multiworld.")
|
||||
logging.info(f"Took {time.perf_counter() - start:.3f} seconds to generate multiworld.")
|
||||
seedname = ""
|
||||
|
||||
for segment in text.split():
|
||||
|
@ -136,6 +147,7 @@ if __name__ == "__main__":
|
|||
|
||||
if any((zip_roms, zip_multidata, zip_spoiler, zip_diffs)):
|
||||
import zipfile
|
||||
|
||||
compression = {1: zipfile.ZIP_DEFLATED,
|
||||
2: zipfile.ZIP_LZMA,
|
||||
3: zipfile.ZIP_BZIP2}[zip_format]
|
||||
|
@ -150,17 +162,17 @@ if __name__ == "__main__":
|
|||
def pack_file(file: str):
|
||||
with ziplock:
|
||||
zf.write(os.path.join(output_path, file), file)
|
||||
print(f"Packed {file} into zipfile {zipname}")
|
||||
logging.info(f"Packed {file} into zipfile {zipname}")
|
||||
|
||||
|
||||
def remove_zipped_file(file: str):
|
||||
os.remove(os.path.join(output_path, file))
|
||||
print(f"Removed {file} which is now present in the zipfile")
|
||||
logging.info(f"Removed {file} which is now present in the zipfile")
|
||||
|
||||
|
||||
zipname = os.path.join(output_path, f"BM_{seedname}.{typical_zip_ending}")
|
||||
|
||||
print(f"Creating zipfile {zipname}")
|
||||
logging.info(f"Creating zipfile {zipname}")
|
||||
ipv4 = (host if host else get_public_ipv4()) + ":" + str(port)
|
||||
|
||||
|
||||
|
@ -214,5 +226,6 @@ if __name__ == "__main__":
|
|||
subprocess.call(f"{baseservercommand} --multidata {os.path.join(output_path, multidataname)}")
|
||||
except:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
input("Press enter to close")
|
||||
|
|
|
@ -58,6 +58,15 @@ class Client(Endpoint):
|
|||
|
||||
|
||||
class Context(Node):
|
||||
simple_options = {"hint_cost": int,
|
||||
"location_check_points": int,
|
||||
"server_password": str,
|
||||
"password": str,
|
||||
"forfeit_mode": str,
|
||||
"remaining_mode": str,
|
||||
"item_cheat": bool,
|
||||
"compatibility": int}
|
||||
|
||||
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
|
||||
hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", remaining_mode: str = "disabled",
|
||||
auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2):
|
||||
|
@ -131,15 +140,23 @@ class Context(Node):
|
|||
self._set_options(server_options)
|
||||
|
||||
def _set_options(self, server_options: dict):
|
||||
|
||||
sentinel = object()
|
||||
for key, value in server_options.items():
|
||||
if key not in self.embedded_blacklist:
|
||||
current = getattr(self, key, sentinel)
|
||||
if current is not sentinel:
|
||||
data_type = self.simple_options.get(key, None)
|
||||
if data_type is not None:
|
||||
if value not in {False, True, None}: # some can be boolean OR text, such as password
|
||||
try:
|
||||
value = data_type(value)
|
||||
except Exception as e:
|
||||
try:
|
||||
raise Exception(f"Could not set server option {key}, skipping.") from e
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
logging.debug(f"Setting server option {key} to {value} from supplied multidata")
|
||||
setattr(self, key, value)
|
||||
self.item_cheat = not server_options.get("disable_item_cheat", True)
|
||||
elif key == "disable_item_cheat":
|
||||
self.item_cheat = not bool(value)
|
||||
else:
|
||||
logging.debug(f"Unrecognized server option {key}")
|
||||
|
||||
def save(self, now=False) -> bool:
|
||||
if self.saving:
|
||||
|
@ -651,15 +668,6 @@ class CommandProcessor(metaclass=CommandMeta):
|
|||
class CommonCommandProcessor(CommandProcessor):
|
||||
ctx: Context
|
||||
|
||||
simple_options = {"hint_cost": int,
|
||||
"location_check_points": int,
|
||||
"server_password": str,
|
||||
"password": str,
|
||||
"forfeit_mode": str,
|
||||
"item_cheat": bool,
|
||||
"auto_save_interval": int,
|
||||
"compatibility": int}
|
||||
|
||||
def _cmd_countdown(self, seconds: str = "10") -> bool:
|
||||
"""Start a countdown in seconds"""
|
||||
try:
|
||||
|
@ -672,7 +680,7 @@ class CommonCommandProcessor(CommandProcessor):
|
|||
def _cmd_options(self):
|
||||
"""List all current options. Warning: lists password."""
|
||||
self.output("Current options:")
|
||||
for option in self.simple_options:
|
||||
for option in self.ctx.simple_options:
|
||||
if option == "server_password" and self.marker == "!": #Do not display the server password to the client.
|
||||
self.output(f"Option server_password is set to {('*' * random.randint(4,16))}")
|
||||
else:
|
||||
|
@ -1231,7 +1239,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
|||
def _cmd_option(self, option_name: str, option: str):
|
||||
"""Set options for the server. Warning: expires on restart"""
|
||||
|
||||
attrtype = self.simple_options.get(option_name, None)
|
||||
attrtype = self.ctx.simple_options.get(option_name, None)
|
||||
if attrtype:
|
||||
if attrtype == bool:
|
||||
def attrtype(input_text: str):
|
||||
|
@ -1245,7 +1253,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
|||
self.output(f"Set option {option_name} to {getattr(self.ctx, option_name)}")
|
||||
return True
|
||||
else:
|
||||
known = (f"{option}:{otype}" for option, otype in self.simple_options.items())
|
||||
known = (f"{option}:{otype}" for option, otype in self.ctx.simple_options.items())
|
||||
self.output(f"Unrecognized Option {option_name}, known: "
|
||||
f"{', '.join(known)}")
|
||||
return False
|
||||
|
|
13
Mystery.py
13
Mystery.py
|
@ -238,6 +238,8 @@ def convert_to_on_off(value):
|
|||
def get_choice(option, root, value=None) -> typing.Any:
|
||||
if option not in root:
|
||||
return value
|
||||
if type(root[option]) is list:
|
||||
return interpret_on_off(random.choices(root[option])[0])
|
||||
if type(root[option]) is not dict:
|
||||
return interpret_on_off(root[option])
|
||||
if not root[option]:
|
||||
|
@ -488,6 +490,17 @@ def roll_settings(weights):
|
|||
|
||||
ret.local_items = ",".join(ret.local_items)
|
||||
|
||||
ret.non_local_items = set()
|
||||
for item_name in weights.get('non_local_items', []):
|
||||
items = item_name_groups.get(item_name, {item_name})
|
||||
for item in items:
|
||||
if item in item_table:
|
||||
ret.non_local_items.add(item)
|
||||
else:
|
||||
raise Exception(f"Could not force item {item} to be world-non-local, as it was not recognized.")
|
||||
|
||||
ret.non_local_items = ",".join(ret.non_local_items)
|
||||
|
||||
if 'rom' in weights:
|
||||
romweights = weights['rom']
|
||||
|
||||
|
|
26
Rom.py
26
Rom.py
|
@ -1,7 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
JAP10HASH = '03a63945398191337e896e5771f77173'
|
||||
RANDOMIZERBASEHASH = 'e3714804e3fae1c6ac6100b94d1aee62'
|
||||
RANDOMIZERBASEHASH = '5a607e36a82bbd14180536c8ec3ae49b'
|
||||
|
||||
import io
|
||||
import json
|
||||
|
@ -697,12 +697,12 @@ def patch_rom(world, rom, player, team, enemized):
|
|||
# Thanks to Zarby89 for originally finding these values
|
||||
# todo fix screen scrolling
|
||||
|
||||
if world.shuffle[player] not in ['insanity', 'insanity_legacy', 'madness_legacy'] and \
|
||||
exit.name in ['Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit',
|
||||
if world.shuffle[player] not in {'insanity', 'insanity_legacy', 'madness_legacy'} and \
|
||||
exit.name in {'Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit',
|
||||
'Skull Woods Final Section Exit', 'Ice Palace Exit', 'Misery Mire Exit',
|
||||
'Palace of Darkness Exit', 'Swamp Palace Exit', 'Ganons Tower Exit',
|
||||
'Desert Palace Exit (North)', 'Agahnims Tower Exit', 'Spiral Cave Exit (Top)',
|
||||
'Superbunny Cave Exit (Bottom)', 'Turtle Rock Ledge Exit (East)']:
|
||||
'Superbunny Cave Exit (Bottom)', 'Turtle Rock Ledge Exit (East)'}:
|
||||
# For exits that connot be reached from another, no need to apply offset fixes.
|
||||
rom.write_int16(0x15DB5 + 2 * offset, link_y) # same as final else
|
||||
elif room_id == 0x0059 and world.fix_skullwoods_exit[player]:
|
||||
|
@ -1407,20 +1407,19 @@ def patch_rom(world, rom, player, team, enemized):
|
|||
rom.write_bytes(0x180185, [0, 0, 0]) # Uncle respawn refills (magic, bombs, arrows)
|
||||
rom.write_bytes(0x180188, [0, 0, 0]) # Zelda respawn refills (magic, bombs, arrows)
|
||||
rom.write_bytes(0x18018B, [0, 0, 0]) # Mantle respawn refills (magic, bombs, arrows)
|
||||
if world.mode[player] == 'standard':
|
||||
if uncle_location.item is not None and uncle_location.item.name in ['Bow', 'Progressive Bow']:
|
||||
if world.mode[player] == 'standard' and uncle_location.item and uncle_location.item.player == player:
|
||||
if uncle_location.item.name in {'Bow', 'Progressive Bow'}:
|
||||
rom.write_byte(0x18004E, 1) # Escape Fill (arrows)
|
||||
rom.write_int16(0x180183, 300) # Escape fill rupee bow
|
||||
rom.write_bytes(0x180185, [0, 0, 70]) # Uncle respawn refills (magic, bombs, arrows)
|
||||
rom.write_bytes(0x180188, [0, 0, 10]) # Zelda respawn refills (magic, bombs, arrows)
|
||||
rom.write_bytes(0x18018B, [0, 0, 10]) # Mantle respawn refills (magic, bombs, arrows)
|
||||
elif uncle_location.item is not None and uncle_location.item.name in ['Bombs (10)']:
|
||||
elif uncle_location.item.name in {'Bombs (10)'}:
|
||||
rom.write_byte(0x18004E, 2) # Escape Fill (bombs)
|
||||
rom.write_bytes(0x180185, [0, 50, 0]) # Uncle respawn refills (magic, bombs, arrows)
|
||||
rom.write_bytes(0x180188, [0, 3, 0]) # Zelda respawn refills (magic, bombs, arrows)
|
||||
rom.write_bytes(0x18018B, [0, 3, 0]) # Mantle respawn refills (magic, bombs, arrows)
|
||||
elif uncle_location.item is not None and uncle_location.item.name in ['Cane of Somaria', 'Cane of Byrna',
|
||||
'Fire Rod']:
|
||||
elif uncle_location.item.name in {'Cane of Somaria', 'Cane of Byrna', 'Fire Rod'}:
|
||||
rom.write_byte(0x18004E, 4) # Escape Fill (magic)
|
||||
rom.write_bytes(0x180185, [0x80, 0, 0]) # Uncle respawn refills (magic, bombs, arrows)
|
||||
rom.write_bytes(0x180188, [0x20, 0, 0]) # Zelda respawn refills (magic, bombs, arrows)
|
||||
|
@ -1629,7 +1628,7 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr
|
|||
hud_palettes = palettes_options['hud']
|
||||
sword_palettes = palettes_options['sword']
|
||||
shield_palettes = palettes_options['shield']
|
||||
link_palettes = palettes_options['link']
|
||||
# link_palettes = palettes_options['link']
|
||||
buildAndRandomize("randomize_dungeon", uw_palettes)
|
||||
buildAndRandomize("randomize_overworld", ow_palettes)
|
||||
buildAndRandomize("randomize_hud", hud_palettes)
|
||||
|
@ -2062,9 +2061,10 @@ def write_strings(rom, world, player, team):
|
|||
greenpendant = world.find_items('Green Pendant', player)[0]
|
||||
tt['sahasrahla_bring_courage'] = 'I lost my family heirloom in %s' % greenpendant.hint_text
|
||||
|
||||
tt['sign_ganons_tower'] = ('You need %d crystal to enter.' if world.crystals_needed_for_gt[
|
||||
player] == 1 else 'You need %d crystals to enter.') % \
|
||||
world.crystals_needed_for_gt[player]
|
||||
if world.crystals_needed_for_gt[player] == 1:
|
||||
tt['sign_ganons_tower'] = 'You need a crystal to enter.'
|
||||
else:
|
||||
tt['sign_ganons_tower'] = f'You need {world.crystals_needed_for_gt[player]} crystals to enter.'
|
||||
|
||||
if world.goal[player] == 'dungeons':
|
||||
tt['sign_ganon'] = 'You need to complete all the dungeons.'
|
||||
|
|
6
Rules.py
6
Rules.py
|
@ -104,7 +104,7 @@ def mirrorless_path_to_castle_courtyard(world, player):
|
|||
else:
|
||||
queue.append((entrance.connected_region, new_path))
|
||||
|
||||
raise Exception(f"Could not find mirrorless path to castle courtyard for Player {player}")
|
||||
raise Exception(f"Could not find mirrorless path to castle courtyard for Player {player} ({world.get_player_names(player)})")
|
||||
|
||||
def set_rule(spot, rule):
|
||||
spot.access_rule = rule
|
||||
|
@ -179,6 +179,10 @@ def locality_rules(world, player):
|
|||
for location in world.get_locations():
|
||||
if location.player != player:
|
||||
forbid_items_for_player(location, world.local_items[player], player)
|
||||
if world.non_local_items[player]:
|
||||
for location in world.get_locations():
|
||||
if location.player == player:
|
||||
forbid_items_for_player(location, world.non_local_items[player], player)
|
||||
|
||||
|
||||
non_crossover_items = (item_name_groups["Small Keys"] | item_name_groups["Big Keys"] | progression_items) - {
|
||||
|
|
2
Text.py
2
Text.py
|
@ -267,7 +267,7 @@ junk_texts = [
|
|||
"{C:GREEN}\n>Secret power\nis said to be\nin the arrow.",
|
||||
"{C:GREEN}\nAim at the\neyes of Gohma.\n >",
|
||||
"{C:GREEN}\nGrumble,\ngrumble…\n >",
|
||||
"{C:GREEN}\n10th enemy\nhas the bomb.\n >",
|
||||
# "{C:GREEN}\n10th enemy\nhas the bomb.\n >", removed as people may assume it applies to this game
|
||||
"{C:GREEN}\nGo to the\nnext room.\n >",
|
||||
"{C:GREEN}\n>Thanks, @\nYou’re the\nhero of Hyrule",
|
||||
"{C:GREEN}\nThere’s always\nmoney in the\nBanana Stand>",
|
||||
|
|
106
Utils.py
106
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.3.0"
|
||||
__version__ = "3.4.1"
|
||||
_version_tuple = tuplize_version(__version__)
|
||||
|
||||
import os
|
||||
|
@ -165,6 +165,90 @@ def get_public_ipv6() -> str:
|
|||
pass # we could be offline, in a local game, or ipv6 may not be available
|
||||
return ip
|
||||
|
||||
|
||||
def get_default_options() -> dict:
|
||||
if not hasattr(get_default_options, "options"):
|
||||
options = dict()
|
||||
|
||||
# Refer to host.yaml for comments as to what all these options mean.
|
||||
generaloptions = dict()
|
||||
generaloptions["rom_file"] = "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
|
||||
generaloptions["qusb2snes"] = "QUsb2Snes\\QUsb2Snes.exe"
|
||||
generaloptions["rom_start"] = True
|
||||
generaloptions["output_path"] = "output"
|
||||
options["general_options"] = generaloptions
|
||||
|
||||
serveroptions = dict()
|
||||
serveroptions["host"] = None
|
||||
serveroptions["port"] = 38281
|
||||
serveroptions["password"] = None
|
||||
serveroptions["multidata"] = None
|
||||
serveroptions["savefile"] = None
|
||||
serveroptions["disable_save"] = False
|
||||
serveroptions["loglevel"] = "info"
|
||||
serveroptions["server_password"] = None
|
||||
serveroptions["disable_item_cheat"] = False
|
||||
serveroptions["location_check_points"] = 1
|
||||
serveroptions["hint_cost"] = 1000
|
||||
serveroptions["forfeit_mode"] = "goal"
|
||||
serveroptions["remaining_mode"] = "goal"
|
||||
serveroptions["auto_shutdown"] = 0
|
||||
serveroptions["compatibility"] = 2
|
||||
options["server_options"] = serveroptions
|
||||
|
||||
multimysteryoptions = dict()
|
||||
multimysteryoptions["teams"] = 1
|
||||
multimysteryoptions["enemizer_path"] = "EnemizerCLI/EnemizerCLI.Core.exe"
|
||||
multimysteryoptions["player_files_path"] = "Players"
|
||||
multimysteryoptions["players"] = 0
|
||||
multimysteryoptions["weights_file_path"] = "weights.yaml"
|
||||
multimysteryoptions["meta_file_path"] = "meta.yaml"
|
||||
multimysteryoptions["player_name"] = ""
|
||||
multimysteryoptions["create_spoiler"] = 1
|
||||
multimysteryoptions["zip_roms"] = 0
|
||||
multimysteryoptions["zip_diffs"] = 2
|
||||
multimysteryoptions["zip_spoiler"] = 0
|
||||
multimysteryoptions["zip_multidata"] = 1
|
||||
multimysteryoptions["zip_format"] = 1
|
||||
multimysteryoptions["race"] = 0
|
||||
multimysteryoptions["cpu_threads"] = 0
|
||||
multimysteryoptions["max_attempts"] = 0
|
||||
multimysteryoptions["take_first_working"] = False
|
||||
multimysteryoptions["keep_all_seeds"] = False
|
||||
multimysteryoptions["log_output_path"] = "Output Logs"
|
||||
multimysteryoptions["log_level"] = None
|
||||
options["multi_mystery_options"] = multimysteryoptions
|
||||
get_default_options.options = options
|
||||
return get_default_options.options
|
||||
|
||||
|
||||
blacklisted_options = {"multi_mystery_options.cpu_threads",
|
||||
"multi_mystery_options.max_attempts",
|
||||
"multi_mystery_options.take_first_working",
|
||||
"multi_mystery_options.keep_all_seeds",
|
||||
"multi_mystery_options.log_output_path",
|
||||
"multi_mystery_options.log_level"}
|
||||
|
||||
|
||||
def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict:
|
||||
import logging
|
||||
for key, value in src.items():
|
||||
new_keys = keys.copy()
|
||||
new_keys.append(key)
|
||||
option_name = '.'.join(new_keys)
|
||||
if key not in dest:
|
||||
dest[key] = value
|
||||
if filename.endswith("options.yaml") and option_name not in blacklisted_options:
|
||||
logging.info(f"Warning: {filename} is missing {option_name}")
|
||||
elif isinstance(value, dict):
|
||||
if not isinstance(dest.get(key, None), dict):
|
||||
if filename.endswith("options.yaml") and option_name not in blacklisted_options:
|
||||
logging.info(f"Warning: {filename} has {option_name}, but it is not a dictionary. overwriting.")
|
||||
dest[key] = value
|
||||
else:
|
||||
dest[key] = update_options(value, dest[key], filename, new_keys)
|
||||
return dest
|
||||
|
||||
def get_options() -> dict:
|
||||
if not hasattr(get_options, "options"):
|
||||
locations = ("options.yaml", "host.yaml",
|
||||
|
@ -173,7 +257,9 @@ def get_options() -> dict:
|
|||
for location in locations:
|
||||
if os.path.exists(location):
|
||||
with open(location) as f:
|
||||
get_options.options = parse_yaml(f.read())
|
||||
options = parse_yaml(f.read())
|
||||
|
||||
get_options.options = update_options(get_default_options(), options, location, list())
|
||||
break
|
||||
else:
|
||||
raise FileNotFoundError(f"Could not find {locations[1]} to load options.")
|
||||
|
@ -222,28 +308,32 @@ def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]:
|
|||
if hasattr(get_adjuster_settings, "adjuster_settings"):
|
||||
adjuster_settings = getattr(get_adjuster_settings, "adjuster_settings")
|
||||
else:
|
||||
adjuster_settings = persistent_load().get("adjuster", {}).get("last_settings", {})
|
||||
adjuster_settings = persistent_load().get("adjuster", {}).get("last_settings_3", {})
|
||||
|
||||
if adjuster_settings:
|
||||
import pprint
|
||||
import Patch
|
||||
adjuster_settings.rom = romfile
|
||||
adjuster_settings.baserom = Patch.get_base_rom_path()
|
||||
whitelist = {"disablemusic", "fastmenu", "heartbeep", "heartcolor", "ow_palettes", "quickswap",
|
||||
"uw_palettes"}
|
||||
"uw_palettes", "sprite"}
|
||||
printed_options = {name: value for name, value in vars(adjuster_settings).items() if name in whitelist}
|
||||
sprite = getattr(adjuster_settings, "sprite", None)
|
||||
if sprite:
|
||||
printed_options["sprite"] = adjuster_settings.sprite.name
|
||||
|
||||
if hasattr(get_adjuster_settings, "adjust_wanted"):
|
||||
adjust_wanted = getattr(get_adjuster_settings, "adjust_wanted")
|
||||
elif persistent_load().get("adjuster", {}).get("never_adjust", False): # never adjust, per user request
|
||||
return romfile, False
|
||||
else:
|
||||
adjust_wanted = input(f"Last used adjuster settings were found. Would you like to apply these? \n"
|
||||
f"{pprint.pformat(printed_options)}\n"
|
||||
f"Enter yes or no: ")
|
||||
f"Enter yes, no or never: ")
|
||||
if adjust_wanted and adjust_wanted.startswith("y"):
|
||||
adjusted = True
|
||||
import AdjusterMain
|
||||
_, romfile = AdjusterMain.adjust(adjuster_settings)
|
||||
elif adjust_wanted and "never" in adjust_wanted:
|
||||
persistent_store("adjuster", "never_adjust", True)
|
||||
return romfile, False
|
||||
else:
|
||||
adjusted = False
|
||||
import logging
|
||||
|
|
|
@ -47,6 +47,8 @@ app.config["PONY"] = {
|
|||
}
|
||||
app.config["MAX_ROLL"] = 20
|
||||
app.config["CACHE_TYPE"] = "simple"
|
||||
app.config["JSON_AS_ASCII"] = False
|
||||
|
||||
app.autoversion = True
|
||||
av = Autoversion(app)
|
||||
cache = Cache(app)
|
||||
|
@ -145,4 +147,5 @@ def favicon():
|
|||
|
||||
|
||||
from WebHostLib.customserver import run_server_process
|
||||
from . import tracker, upload, landing, check, generate, downloads # to trigger app routing picking up on it
|
||||
from . import tracker, upload, landing, check, generate, downloads, api # to trigger app routing picking up on it
|
||||
app.register_blueprint(api.api_endpoints)
|
|
@ -0,0 +1,23 @@
|
|||
"""API endpoints package."""
|
||||
from uuid import UUID
|
||||
|
||||
from flask import Blueprint, abort
|
||||
|
||||
from ..models import Room
|
||||
|
||||
api_endpoints = Blueprint('api', __name__, url_prefix="/api")
|
||||
|
||||
from . import generate
|
||||
|
||||
# unsorted/misc endpoints
|
||||
|
||||
@api_endpoints.route('/room_status/<suuid:room>')
|
||||
def room_info(room: UUID):
|
||||
room = Room.get(id=room)
|
||||
if room is None:
|
||||
return abort(404)
|
||||
return {"tracker": room.tracker,
|
||||
"players": room.seed.multidata["names"],
|
||||
"last_port": room.last_port,
|
||||
"last_activity": room.last_activity,
|
||||
"timeout": room.timeout}
|
|
@ -0,0 +1,73 @@
|
|||
import pickle
|
||||
from uuid import UUID
|
||||
|
||||
from . import api_endpoints
|
||||
from flask import request, session, url_for
|
||||
from pony.orm import commit
|
||||
|
||||
from WebHostLib import app, Generation, STATE_QUEUED, Seed, STATE_ERROR
|
||||
from WebHostLib.check import get_yaml_data, roll_options
|
||||
|
||||
|
||||
@api_endpoints.route('/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, _external=True),
|
||||
"url": url_for("wait_seed", seed=gen.id, _external=True)}, 201
|
||||
except Exception as e:
|
||||
return {"text": "Uncaught Exception:" + str(e)}, 500
|
||||
|
||||
|
||||
@api_endpoints.route('/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
|
|
@ -47,7 +47,7 @@ class DBCommandProcessor(ServerCommandProcessor):
|
|||
|
||||
class WebHostContext(Context):
|
||||
def __init__(self):
|
||||
super(WebHostContext, self).__init__("", 0, "", 1, 40, True, "enabled", "enabled", 0, 2)
|
||||
super(WebHostContext, self).__init__("", 0, "", "", 1, 40, True, "enabled", "enabled", 0, 2)
|
||||
self.main_loop = asyncio.get_running_loop()
|
||||
self.video = {}
|
||||
self.tags = ["Berserker", "WebHost"]
|
||||
|
|
|
@ -52,55 +52,6 @@ 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()
|
||||
|
@ -142,12 +93,13 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
|
|||
ERmain(erargs, seed)
|
||||
|
||||
return upload_to_db(target.name, owner, sid, race)
|
||||
except BaseException:
|
||||
except BaseException as e:
|
||||
if sid:
|
||||
with db_session:
|
||||
gen = Generation.get(id=sid)
|
||||
if gen is not None:
|
||||
gen.state = STATE_ERROR
|
||||
gen.meta = (e.__class__.__name__ + ": "+ str(e)).encode()
|
||||
raise
|
||||
|
||||
|
||||
|
@ -162,25 +114,11 @@ def wait_seed(seed: UUID):
|
|||
if not generation:
|
||||
return "Generation not found."
|
||||
elif generation.state == STATE_ERROR:
|
||||
return "Generation failed, please retry."
|
||||
import html
|
||||
return f"Generation failed, please retry. <br> {html.escape(generation.meta.decode())}"
|
||||
return render_template("waitSeed.html", seed_id=seed_id)
|
||||
|
||||
|
||||
@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 = ""
|
||||
|
|
|
@ -50,5 +50,5 @@ class Generation(db.Entity):
|
|||
id = PrimaryKey(UUID, default=uuid4)
|
||||
owner = Required(UUID)
|
||||
options = Required(bytes, lazy=True) # these didn't work as JSON on mariaDB, so they're getting pickled now
|
||||
meta = Required(bytes, lazy=True)
|
||||
meta = Required(bytes, lazy=True) # if state is -1 (error) this will contain an utf-8 encoded error message
|
||||
state = Required(int, default=0, index=True)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
flask>=1.1.2
|
||||
pony>=0.7.13
|
||||
pony>=0.7.14
|
||||
waitress>=1.4.4
|
||||
flask-caching>=1.9.0
|
||||
Flask-Autoversion>=0.2.0
|
||||
Flask-Compress>=1.7.0
|
||||
Flask-Compress>=1.8.0
|
||||
Flask-Limiter>=1.4
|
||||
|
|
|
@ -791,12 +791,6 @@
|
|||
"friendlyName": "Expert",
|
||||
"description": "Minimum upgrade availability (max: 8 hearts, green mail, master sword, fighter shield, no silvers unless swordless).",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"crowd_control": {
|
||||
"keyString": "item_pool.crowd_control",
|
||||
"friendlyName": "Crowd Control",
|
||||
"description": "Configures the item pool for the crowd control extension. Do not use this unless you are using crowd control.",
|
||||
"defaultValue": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1387,6 +1381,26 @@
|
|||
"defaultValue": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"key_drop_shuffle": {
|
||||
"keyString": "key_drop_shuffle",
|
||||
"friendlyName": "Key Drop Shuffle",
|
||||
"description": "Allows the small/big keys dropped by enemies/pots to be shuffled into the item pool. This extends the number of checks from 216 to 249",
|
||||
"inputType": "range",
|
||||
"subOptions": {
|
||||
"on": {
|
||||
"keyString": "key_drop_shuffle.on",
|
||||
"friendlyName": "Enabled",
|
||||
"description": "Enables key drop shuffle",
|
||||
"defaultValue": 0
|
||||
},
|
||||
"off": {
|
||||
"keyString": "key_drop_shuffle.off",
|
||||
"friendlyName": "Disabled",
|
||||
"description": "Disables key drop shuffle",
|
||||
"defaultValue": 50
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"romOptions": {
|
||||
|
|
|
@ -165,7 +165,6 @@ item_pool:
|
|||
normal: 50 # Item availability remains unchanged from vanilla game
|
||||
hard: 0 # Reduced upgrade availability (max: 14 hearts, blue mail, tempered sword, fire shield, no silvers unless swordless)
|
||||
expert: 0 # Minimum upgrade availability (max: 8 hearts, green mail, master sword, fighter shield, no silvers unless swordless)
|
||||
crowd_control: 0 # Sets up the item pool for the crowd control extension. Do not use it without crowd control
|
||||
item_functionality:
|
||||
easy: 0 # Allow Hammer to damage ganon, Allow Hammer tablet collection, Allow swordless medallion use everywhere.
|
||||
normal: 50 # Vanilla item functionality
|
||||
|
@ -305,6 +304,9 @@ intensity: # Only available if the host uses the doors branch, it is ignored oth
|
|||
2: 0 # And shuffles open edges and straight staircases
|
||||
3: 0 # And shuffles dungeon lobbies
|
||||
random: 0 # Picks one of those at random
|
||||
key_drop_shuffle: # Only available if the host uses the doors branch, it is ignored otherwise
|
||||
on: 0 # Enables the small keys dropped by enemies or under pots, and the big key dropped by the Ball & Chain guard to be shuffled into the pool. This extends the number of checks to 249.
|
||||
off: 50
|
||||
experimental: # Only available if the host uses the doors branch, it is ignored otherwise
|
||||
on: 0 # Enables experimental features. Currently, this is just the dungeon keys in chest counter.
|
||||
off: 50
|
||||
|
|
|
@ -55,6 +55,7 @@ html{
|
|||
#player-settings #settings-wrapper #sprite-picker .sprite-img-wrapper{
|
||||
cursor: pointer;
|
||||
margin: 10px;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
/* Center tooltip text for sprite images */
|
||||
|
|
Binary file not shown.
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue