Merge master into website-redesign
This commit is contained in:
commit
ddfcef09ec
|
@ -30,3 +30,5 @@ weights/
|
||||||
_persistent_storage.yaml
|
_persistent_storage.yaml
|
||||||
mystery_result_*.yaml
|
mystery_result_*.yaml
|
||||||
/db.db3
|
/db.db3
|
||||||
|
*-errors.txt
|
||||||
|
success.txt
|
||||||
|
|
|
@ -65,7 +65,10 @@ def main():
|
||||||
logging.basicConfig(format='%(message)s', level=loglevel)
|
logging.basicConfig(format='%(message)s', level=loglevel)
|
||||||
args, path = adjust(args=args)
|
args, path = adjust(args=args)
|
||||||
from Utils import persistent_store
|
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__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -27,7 +27,7 @@ def adjust(args):
|
||||||
palettes_options['hud']=args.hud_palettes
|
palettes_options['hud']=args.hud_palettes
|
||||||
palettes_options['sword']=args.sword_palettes
|
palettes_options['sword']=args.sword_palettes
|
||||||
palettes_options['shield']=args.shield_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,
|
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic,
|
||||||
args.sprite, palettes_options)
|
args.sprite, palettes_options)
|
||||||
|
|
|
@ -127,6 +127,7 @@ class World(object):
|
||||||
set_player_attr('glitch_boots', True)
|
set_player_attr('glitch_boots', True)
|
||||||
set_player_attr('progression_balancing', True)
|
set_player_attr('progression_balancing', True)
|
||||||
set_player_attr('local_items', set())
|
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_available', 30)
|
||||||
set_player_attr('triforce_pieces_required', 20)
|
set_player_attr('triforce_pieces_required', 20)
|
||||||
set_player_attr('shop_shuffle', 'off')
|
set_player_attr('shop_shuffle', 'off')
|
||||||
|
|
98
Bosses.py
98
Bosses.py
|
@ -123,7 +123,7 @@ def GanonDefeatRule(state, player: int):
|
||||||
state.has('Silver Bow', player) and \
|
state.has('Silver Bow', player) and \
|
||||||
state.can_shoot_arrows(player)
|
state.can_shoot_arrows(player)
|
||||||
easy_hammer = state.world.difficulty_adjustments[player] == "easy" and state.has("Hammer", player) and \
|
easy_hammer = state.world.difficulty_adjustments[player] == "easy" and state.has("Hammer", player) and \
|
||||||
state.has('Silver Bow', player) and state.can_shoot_arrows(player)
|
state.has('Silver Bow', player) and state.can_shoot_arrows(player)
|
||||||
can_hurt = state.has_beam_sword(player) or easy_hammer
|
can_hurt = state.has_beam_sword(player) or easy_hammer
|
||||||
common = can_hurt and state.has_fire_source(player)
|
common = can_hurt and state.has_fire_source(player)
|
||||||
# silverless ganon may be needed in minor glitches
|
# silverless ganon may be needed in minor glitches
|
||||||
|
@ -131,7 +131,7 @@ def GanonDefeatRule(state, player: int):
|
||||||
# need to light torch a sufficient amount of times
|
# need to light torch a sufficient amount of times
|
||||||
return common and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or (
|
return common and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or (
|
||||||
state.has('Silver Bow', player) and state.can_shoot_arrows(player)) or
|
state.has('Silver Bow', player) and state.can_shoot_arrows(player)) or
|
||||||
state.has('Lamp', player) or state.can_extend_magic(player, 12))
|
state.has('Lamp', player) or state.can_extend_magic(player, 12))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return common and state.has('Silver Bow', player) and state.can_shoot_arrows(player)
|
return common and state.has('Silver Bow', player) and state.can_shoot_arrows(player)
|
||||||
|
@ -153,27 +153,33 @@ boss_table = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def can_place_boss(world, player: int, boss: str, dungeon_name: str, level: Optional[str] = None) -> bool:
|
def can_place_boss(boss: str, dungeon_name: str, level: Optional[str] = None) -> bool:
|
||||||
if dungeon_name in ['Ganons Tower', 'Inverted Ganons Tower'] and level == 'top':
|
#blacklist approach
|
||||||
if boss in ["Armos Knights", "Arrghus", "Blind", "Trinexx", "Lanmolas"]:
|
if boss in {"Agahnim", "Agahnim2", "Ganon"}:
|
||||||
|
return False
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
elif dungeon_name == 'Tower of Hera':
|
||||||
|
if boss in {"Armos Knights", "Arrghus", "Blind", "Trinexx", "Lanmolas"}:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if dungeon_name in ['Ganons Tower', 'Inverted Ganons Tower'] and level == 'middle':
|
elif dungeon_name == 'Skull Woods' :
|
||||||
if boss in ["Blind"]:
|
if boss == "Trinexx":
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if dungeon_name == 'Tower of Hera' and boss in ["Armos Knights", "Arrghus", "Blind", "Trinexx", "Lanmolas"]:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if dungeon_name == 'Skull Woods' and boss in ["Trinexx"]:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if boss in ["Agahnim", "Agahnim2", "Ganon"]:
|
|
||||||
return False
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def place_boss(world, player: int, boss: str, location: str, level: Optional[str]):
|
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 ''))
|
logging.debug('Placing boss %s at %s', boss, location + (' (' + level + ')' if level else ''))
|
||||||
world.get_dungeon(location, player).bosses[level] = BossFactory(boss, player)
|
world.get_dungeon(location, player).bosses[level] = BossFactory(boss, player)
|
||||||
|
|
||||||
|
@ -182,43 +188,25 @@ def place_bosses(world, player: int):
|
||||||
if world.boss_shuffle[player] == 'none':
|
if world.boss_shuffle[player] == 'none':
|
||||||
return
|
return
|
||||||
# Most to least restrictive order
|
# Most to least restrictive order
|
||||||
if world.mode[player] != 'inverted':
|
boss_locations = [
|
||||||
boss_locations = [
|
['Ganons Tower', 'top'],
|
||||||
['Ganons Tower', 'top'],
|
['Tower of Hera', None],
|
||||||
['Tower of Hera', None],
|
['Skull Woods', None],
|
||||||
['Skull Woods', None],
|
['Ganons Tower', 'middle'],
|
||||||
['Ganons Tower', 'middle'],
|
['Eastern Palace', None],
|
||||||
['Eastern Palace', None],
|
['Desert Palace', None],
|
||||||
['Desert Palace', None],
|
['Palace of Darkness', None],
|
||||||
['Palace of Darkness', None],
|
['Swamp Palace', None],
|
||||||
['Swamp Palace', None],
|
['Thieves Town', None],
|
||||||
['Thieves Town', None],
|
['Ice Palace', None],
|
||||||
['Ice Palace', None],
|
['Misery Mire', None],
|
||||||
['Misery Mire', None],
|
['Turtle Rock', None],
|
||||||
['Turtle Rock', None],
|
['Ganons Tower', 'bottom'],
|
||||||
['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
|
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']]
|
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] in ["basic", "normal"]:
|
||||||
if world.boss_shuffle[player] == "basic": # vanilla bosses shuffled
|
if world.boss_shuffle[player] == "basic": # vanilla bosses shuffled
|
||||||
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
|
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
|
||||||
|
@ -228,8 +216,8 @@ def place_bosses(world, player: int):
|
||||||
logging.debug('Bosses chosen %s', bosses)
|
logging.debug('Bosses chosen %s', bosses)
|
||||||
|
|
||||||
world.random.shuffle(bosses)
|
world.random.shuffle(bosses)
|
||||||
for [loc, level] in boss_locations:
|
for loc, level in boss_locations:
|
||||||
boss = next((b for b in bosses if can_place_boss(world, player, b, loc, level)), None)
|
boss = next((b for b in bosses if can_place_boss(b, loc, level)), None)
|
||||||
if not boss:
|
if not boss:
|
||||||
loc_text = loc + (' (' + level + ')' if level else '')
|
loc_text = loc + (' (' + level + ')' if level else '')
|
||||||
raise FillError('Could not place boss for location %s' % loc_text)
|
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)
|
place_boss(world, player, boss, loc, level)
|
||||||
|
|
||||||
elif world.boss_shuffle[player] == "chaos": # all bosses chosen at random
|
elif world.boss_shuffle[player] == "chaos": # all bosses chosen at random
|
||||||
for [loc, level] in boss_locations:
|
for loc, level in boss_locations:
|
||||||
try:
|
try:
|
||||||
boss = world.random.choice(
|
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:
|
except IndexError:
|
||||||
loc_text = loc + (' (' + level + ')' if level else '')
|
loc_text = loc + (' (' + level + ')' if level else '')
|
||||||
raise FillError('Could not place boss for location %s' % loc_text)
|
raise FillError('Could not place boss for location %s' % loc_text)
|
||||||
|
@ -252,14 +240,14 @@ def place_bosses(world, player: int):
|
||||||
remaining_boss_locations = []
|
remaining_boss_locations = []
|
||||||
for loc, level in boss_locations:
|
for loc, level in boss_locations:
|
||||||
# place that boss where it can go
|
# 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)
|
place_boss(world, player, primary_boss, loc, level)
|
||||||
else:
|
else:
|
||||||
remaining_boss_locations.append((loc, level))
|
remaining_boss_locations.append((loc, level))
|
||||||
if remaining_boss_locations:
|
if remaining_boss_locations:
|
||||||
# pick a boss to go into the remaining locations
|
# pick a boss to go into the remaining locations
|
||||||
remaining_boss = world.random.choice([boss for boss in placeable_bosses if all(
|
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:
|
for loc, level in remaining_boss_locations:
|
||||||
place_boss(world, player, remaining_boss, loc, level)
|
place_boss(world, player, remaining_boss, loc, level)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -181,7 +181,7 @@ def parse_arguments(argv, no_defaults=False):
|
||||||
slightly biased to placing progression items with
|
slightly biased to placing progression items with
|
||||||
less restrictions.
|
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='''\
|
help='''\
|
||||||
Select Entrance Shuffling Algorithm. (default: %(default)s)
|
Select Entrance Shuffling Algorithm. (default: %(default)s)
|
||||||
Full: Mix cave and dungeon entrances freely while limiting
|
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)')
|
help='Specifies a list of items that will be in your starting inventory (separated by commas)')
|
||||||
parser.add_argument('--local_items', default=defval(''),
|
parser.add_argument('--local_items', default=defval(''),
|
||||||
help='Specifies a list of items that will not spread across the multiworld (separated by commas)')
|
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('--custom', default=defval(False), help='Not supported.')
|
||||||
parser.add_argument('--customitemarray', 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='''\
|
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',
|
'shuffle', 'crystals_ganon', 'crystals_gt', 'open_pyramid', 'timer',
|
||||||
'countdown_start_time', 'red_clock_time', 'blue_clock_time', 'green_clock_time',
|
'countdown_start_time', 'red_clock_time', 'blue_clock_time', 'green_clock_time',
|
||||||
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
|
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
|
||||||
'local_items', 'retro', 'accessibility', 'hints', 'beemizer',
|
'local_items', 'non_local_items', 'retro', 'accessibility', 'hints', 'beemizer',
|
||||||
'shufflebosses', 'enemy_shuffle', 'enemy_health', 'enemy_damage', 'shufflepots',
|
'shufflebosses', 'enemy_shuffle', 'enemy_health', 'enemy_damage', 'shufflepots',
|
||||||
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor',
|
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor',
|
||||||
'heartbeep', "skip_progression_balancing", "triforce_pieces_available",
|
'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:
|
for location in region.locations:
|
||||||
if location.item and not location.event:
|
if location.item and not location.event:
|
||||||
placements.append(location)
|
placements.append(location)
|
||||||
|
|
||||||
raise FillError(f'No more spots to place {item_to_place}, locations {locations} are invalid. '
|
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)
|
world.push_item(spot_to_fill, item_to_place, False)
|
||||||
locations.remove(spot_to_fill)
|
locations.remove(spot_to_fill)
|
||||||
|
|
326
Gui.py
326
Gui.py
|
@ -34,7 +34,7 @@ def guiMain(args=None):
|
||||||
customWindow = ttk.Frame(notebook)
|
customWindow = ttk.Frame(notebook)
|
||||||
notebook.add(randomizerWindow, text='Randomize')
|
notebook.add(randomizerWindow, text='Randomize')
|
||||||
notebook.add(adjustWindow, text='Adjust')
|
notebook.add(adjustWindow, text='Adjust')
|
||||||
notebook.add(customWindow, text='Custom')
|
notebook.add(customWindow, text='Custom Items')
|
||||||
notebook.pack()
|
notebook.pack()
|
||||||
|
|
||||||
# Shared Controls
|
# Shared Controls
|
||||||
|
@ -96,8 +96,7 @@ def guiMain(args=None):
|
||||||
hintsVar = IntVar()
|
hintsVar = IntVar()
|
||||||
hintsVar.set(1) # set default
|
hintsVar.set(1) # set default
|
||||||
hintsCheckbutton = Checkbutton(checkBoxFrame, text="Include Helpful Hints", variable=hintsVar)
|
hintsCheckbutton = Checkbutton(checkBoxFrame, text="Include Helpful Hints", variable=hintsVar)
|
||||||
customVar = IntVar()
|
|
||||||
customCheckbutton = Checkbutton(checkBoxFrame, text="Use custom item pool", variable=customVar)
|
|
||||||
balancingVar = IntVar()
|
balancingVar = IntVar()
|
||||||
balancingVar.set(1) # set default
|
balancingVar.set(1) # set default
|
||||||
balancingCheckbutton = Checkbutton(checkBoxFrame, text="Multiworld Progression Balancing", variable=balancingVar)
|
balancingCheckbutton = Checkbutton(checkBoxFrame, text="Multiworld Progression Balancing", variable=balancingVar)
|
||||||
|
@ -116,60 +115,11 @@ def guiMain(args=None):
|
||||||
retroCheckbutton.pack(expand=True, anchor=W)
|
retroCheckbutton.pack(expand=True, anchor=W)
|
||||||
shuffleGanonCheckbutton.pack(expand=True, anchor=W)
|
shuffleGanonCheckbutton.pack(expand=True, anchor=W)
|
||||||
hintsCheckbutton.pack(expand=True, anchor=W)
|
hintsCheckbutton.pack(expand=True, anchor=W)
|
||||||
customCheckbutton.pack(expand=True, anchor=W)
|
|
||||||
balancingCheckbutton.pack(expand=True, anchor=W)
|
balancingCheckbutton.pack(expand=True, anchor=W)
|
||||||
patchesCheckbutton.pack(expand=True, anchor=W)
|
patchesCheckbutton.pack(expand=True, anchor=W)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
timerOptionsFrame = LabelFrame(rightHalfFrame, text="Timer options")
|
|
||||||
for i in range(3):
|
|
||||||
timerOptionsFrame.columnconfigure(i, weight=1)
|
|
||||||
timerOptionsFrame.rowconfigure(i, weight=1)
|
|
||||||
|
|
||||||
timerModeFrame = Frame(timerOptionsFrame)
|
|
||||||
timerModeFrame.grid(row=0, column=0, columnspan=3, sticky=E, padx=3)
|
|
||||||
timerVar = StringVar()
|
|
||||||
timerVar.set('none')
|
|
||||||
timerModeMenu = OptionMenu(timerModeFrame, timerVar, 'none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown')
|
|
||||||
timerLabel = Label(timerModeFrame, text='Timer setting')
|
|
||||||
timerLabel.pack(side=LEFT)
|
|
||||||
timerModeMenu.pack(side=LEFT)
|
|
||||||
|
|
||||||
timerCountdownFrame = Frame(timerOptionsFrame)
|
|
||||||
timerCountdownFrame.grid(row=1, column=0, columnspan=3, sticky=E, padx=3)
|
|
||||||
timerCountdownLabel = Label(timerCountdownFrame, text='Countdown starting time')
|
|
||||||
timerCountdownLabel.pack(side=LEFT)
|
|
||||||
timerCountdownVar = IntVar(value=10)
|
|
||||||
timerCountdownSpinbox = Spinbox(timerCountdownFrame, from_=0, to=480, width=3, textvariable=timerCountdownVar)
|
|
||||||
timerCountdownSpinbox.pack(side=LEFT)
|
|
||||||
|
|
||||||
timerRedFrame = Frame(timerOptionsFrame)
|
|
||||||
timerRedFrame.grid(row=2, column=0, sticky=E, padx=3)
|
|
||||||
timerRedLabel = Label(timerRedFrame, text='Clock adjustments: Red')
|
|
||||||
timerRedLabel.pack(side=LEFT)
|
|
||||||
timerRedVar = IntVar(value=-2)
|
|
||||||
timerRedSpinbox = Spinbox(timerRedFrame, from_=-60, to=60, width=3, textvariable=timerRedVar)
|
|
||||||
timerRedSpinbox.pack(side=LEFT)
|
|
||||||
|
|
||||||
timerBlueFrame = Frame(timerOptionsFrame)
|
|
||||||
timerBlueFrame.grid(row=2, column=1, sticky=E, padx=3)
|
|
||||||
timerBlueLabel = Label(timerBlueFrame, text='Blue')
|
|
||||||
timerBlueLabel.pack(side=LEFT)
|
|
||||||
timerBlueVar = IntVar(value=2)
|
|
||||||
timerBlueSpinbox = Spinbox(timerBlueFrame, from_=-60, to=60, width=3, textvariable=timerBlueVar)
|
|
||||||
timerBlueSpinbox.pack(side=LEFT)
|
|
||||||
|
|
||||||
timerGreenFrame = Frame(timerOptionsFrame)
|
|
||||||
timerGreenFrame.grid(row=2, column=2, sticky=E, padx=3)
|
|
||||||
timerGreenLabel = Label(timerGreenFrame, text='Green')
|
|
||||||
timerGreenLabel.pack(side=LEFT)
|
|
||||||
timerGreenVar = IntVar(value=4)
|
|
||||||
timerGreenSpinbox = Spinbox(timerGreenFrame, from_=-60, to=60, width=3, textvariable=timerGreenVar)
|
|
||||||
timerGreenSpinbox.pack(side=LEFT)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
romOptionsFrame = LabelFrame(rightHalfFrame, text="Rom options")
|
romOptionsFrame = LabelFrame(rightHalfFrame, text="Rom options")
|
||||||
romOptionsFrame.columnconfigure(0, weight=1)
|
romOptionsFrame.columnconfigure(0, weight=1)
|
||||||
romOptionsFrame.columnconfigure(1, weight=1)
|
romOptionsFrame.columnconfigure(1, weight=1)
|
||||||
|
@ -316,7 +266,7 @@ def guiMain(args=None):
|
||||||
romSelectButton.pack(side=LEFT)
|
romSelectButton.pack(side=LEFT)
|
||||||
|
|
||||||
checkBoxFrame.pack(side=TOP, anchor=W, padx=5, pady=10)
|
checkBoxFrame.pack(side=TOP, anchor=W, padx=5, pady=10)
|
||||||
timerOptionsFrame.pack(expand=True, fill=BOTH, padx=3)
|
|
||||||
romOptionsFrame.pack(expand=True, fill=BOTH, padx=3)
|
romOptionsFrame.pack(expand=True, fill=BOTH, padx=3)
|
||||||
|
|
||||||
drowDownFrame = Frame(topFrame)
|
drowDownFrame = Frame(topFrame)
|
||||||
|
@ -844,7 +794,10 @@ def guiMain(args=None):
|
||||||
else:
|
else:
|
||||||
messagebox.showinfo(title="Success", message="Rom patched successfully")
|
messagebox.showinfo(title="Success", message="Rom patched successfully")
|
||||||
from Utils import persistent_store
|
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)
|
adjustButton = Button(bottomFrame2, text='Adjust Rom', command=adjustRom)
|
||||||
|
|
||||||
|
@ -866,12 +819,65 @@ def guiMain(args=None):
|
||||||
return False
|
return False
|
||||||
vcmd=(topFrame3.register(validation), '%P')
|
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)
|
itemList1 = Frame(topFrame3)
|
||||||
itemList2 = Frame(topFrame3)
|
itemList2 = Frame(topFrame3)
|
||||||
itemList3 = Frame(topFrame3)
|
itemList3 = Frame(topFrame3)
|
||||||
itemList4 = Frame(topFrame3)
|
itemList4 = Frame(topFrame3)
|
||||||
itemList5 = 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)
|
bowFrame = Frame(itemList1)
|
||||||
bowLabel = Label(bowFrame, text='Bow')
|
bowLabel = Label(bowFrame, text='Bow')
|
||||||
bowVar = StringVar(value='0')
|
bowVar = StringVar(value='0')
|
||||||
|
@ -1492,7 +1498,8 @@ def guiMain(args=None):
|
||||||
|
|
||||||
mainWindow.mainloop()
|
mainWindow.mainloop()
|
||||||
|
|
||||||
class SpriteSelector(object):
|
|
||||||
|
class SpriteSelector():
|
||||||
def __init__(self, parent, callback, adjuster=False):
|
def __init__(self, parent, callback, adjuster=False):
|
||||||
if is_bundled():
|
if is_bundled():
|
||||||
self.deploy_icons()
|
self.deploy_icons()
|
||||||
|
@ -1615,93 +1622,15 @@ class SpriteSelector(object):
|
||||||
self.window.destroy()
|
self.window.destroy()
|
||||||
self.parent.update()
|
self.parent.update()
|
||||||
|
|
||||||
def work(task):
|
def on_finish(successful, resultmessage):
|
||||||
resultmessage = ""
|
|
||||||
successful = True
|
|
||||||
|
|
||||||
def finished():
|
|
||||||
task.close_window()
|
|
||||||
if successful:
|
|
||||||
messagebox.showinfo("Sprite Updater", resultmessage)
|
|
||||||
else:
|
|
||||||
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:
|
if successful:
|
||||||
resultmessage = "alttpr sprites updated successfully"
|
messagebox.showinfo("Sprite Updater", resultmessage)
|
||||||
|
else:
|
||||||
|
logging.error(resultmessage)
|
||||||
|
messagebox.showerror("Sprite Updater", resultmessage)
|
||||||
|
SpriteSelector(self.parent, self.callback, self.adjuster)
|
||||||
|
|
||||||
task.queue_event(finished)
|
BackgroundTaskProgress(self.parent, update_sprites, "Updating Sprites", on_finish)
|
||||||
|
|
||||||
BackgroundTaskProgress(self.parent, work, "Updating Sprites")
|
|
||||||
|
|
||||||
|
|
||||||
def browse_for_sprite(self):
|
def browse_for_sprite(self):
|
||||||
|
@ -1745,34 +1674,104 @@ class SpriteSelector(object):
|
||||||
self.callback(spritename)
|
self.callback(spritename)
|
||||||
self.window.destroy()
|
self.window.destroy()
|
||||||
|
|
||||||
|
|
||||||
def deploy_icons(self):
|
def deploy_icons(self):
|
||||||
if not os.path.exists(self.custom_sprite_dir):
|
if not os.path.exists(self.custom_sprite_dir):
|
||||||
os.makedirs(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
|
@property
|
||||||
def alttpr_sprite_dir(self):
|
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")
|
return local_path("data", "sprites", "alttpr")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def custom_sprite_dir(self):
|
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")
|
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):
|
def get_image_for_sprite(sprite, gif_only: bool = False):
|
||||||
if not sprite.valid:
|
if not sprite.valid:
|
||||||
return None
|
return None
|
||||||
|
@ -1877,5 +1876,16 @@ def get_image_for_sprite(sprite, gif_only: bool = False):
|
||||||
return image.zoom(2)
|
return image.zoom(2)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
logging.basicConfig(format='%(message)s', level=logging.INFO)
|
import sys
|
||||||
guiMain()
|
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
|
# 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
|
# multithreading support. Therefore I will assume it is not thread safe to avoid any possible problems
|
||||||
class BackgroundTask(object):
|
class BackgroundTask(object):
|
||||||
def __init__(self, window, code_to_run):
|
def __init__(self, window, code_to_run, *args):
|
||||||
self.window = window
|
self.window = window
|
||||||
self.queue = queue.Queue()
|
self.queue = queue.Queue()
|
||||||
self.running = True
|
self.running = True
|
||||||
self.process_queue()
|
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()
|
self.task.start()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
@ -45,7 +45,7 @@ class BackgroundTask(object):
|
||||||
self.window.after(100, self.process_queue)
|
self.window.after(100, self.process_queue)
|
||||||
|
|
||||||
class BackgroundTaskProgress(BackgroundTask):
|
class BackgroundTaskProgress(BackgroundTask):
|
||||||
def __init__(self, parent, code_to_run, title):
|
def __init__(self, parent, code_to_run, title, *args):
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.window = tk.Toplevel(parent)
|
self.window = tk.Toplevel(parent)
|
||||||
self.window['padx'] = 5
|
self.window['padx'] = 5
|
||||||
|
@ -65,7 +65,7 @@ class BackgroundTaskProgress(BackgroundTask):
|
||||||
|
|
||||||
set_icon(self.window)
|
set_icon(self.window)
|
||||||
self.window.focus()
|
self.window.focus()
|
||||||
super().__init__(self.window, code_to_run)
|
super().__init__(self.window, code_to_run, *args)
|
||||||
|
|
||||||
#safe to call from worker thread
|
#safe to call from worker thread
|
||||||
def update_status(self, text):
|
def update_status(self, text):
|
||||||
|
|
10
Main.py
10
Main.py
|
@ -10,7 +10,7 @@ import zlib
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
|
|
||||||
from BaseClasses import World, CollectionState, Item, Region, Location, Shop
|
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 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 InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
||||||
from EntranceShuffle import link_entrances, link_inverted_entrances
|
from EntranceShuffle import link_entrances, link_inverted_entrances
|
||||||
|
@ -110,7 +110,13 @@ def main(args, seed=None):
|
||||||
item = ItemFactory(tok.strip(), player)
|
item = ItemFactory(tok.strip(), player)
|
||||||
if item:
|
if item:
|
||||||
world.push_precollected(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])
|
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_reconnect_address = None
|
||||||
self.snes_recv_queue = asyncio.Queue()
|
self.snes_recv_queue = asyncio.Queue()
|
||||||
self.snes_request_lock = asyncio.Lock()
|
self.snes_request_lock = asyncio.Lock()
|
||||||
self.is_sd2snes = False
|
|
||||||
self.snes_write_buffer = []
|
self.snes_write_buffer = []
|
||||||
|
|
||||||
self.server_task = None
|
self.server_task = None
|
||||||
|
@ -522,15 +521,6 @@ async def snes_connect(ctx: Context, address):
|
||||||
ctx.snes_attached_device = (devices.index(device), device)
|
ctx.snes_attached_device = (devices.index(device), device)
|
||||||
ctx.ui_node.send_connection_status(ctx)
|
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
|
ctx.snes_reconnect_address = address
|
||||||
recv_task = asyncio.create_task(snes_recv_loop(ctx))
|
recv_task = asyncio.create_task(snes_recv_loop(ctx))
|
||||||
|
@ -645,47 +635,19 @@ 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:
|
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
|
return False
|
||||||
|
|
||||||
PutAddress_Request = {
|
PutAddress_Request = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'}
|
||||||
"Opcode" : "PutAddress",
|
|
||||||
"Operands" : []
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.is_sd2snes:
|
|
||||||
cmd = b'\x00\xE2\x20\x48\xEB\x48'
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
#will pack those requests as soon as qusb2snes actually supports that for real
|
||||||
for address, data in write_list:
|
for address, data in write_list:
|
||||||
if (address < WRAM_START) or ((address + len(data)) > (WRAM_START + WRAM_SIZE)):
|
PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]]
|
||||||
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:
|
if ctx.snes_socket is not None:
|
||||||
await ctx.snes_socket.send(json.dumps(PutAddress_Request))
|
await ctx.snes_socket.send(json.dumps(PutAddress_Request))
|
||||||
if ctx.snes_socket is not None:
|
if ctx.snes_socket is not None:
|
||||||
await ctx.snes_socket.send(cmd)
|
await ctx.snes_socket.send(data)
|
||||||
except websockets.ConnectionClosed:
|
except websockets.ConnectionClosed:
|
||||||
return False
|
logging.warning("Could not write data to SNES")
|
||||||
else:
|
return False
|
||||||
PutAddress_Request['Space'] = 'SNES'
|
|
||||||
try:
|
|
||||||
#will pack those requests as soon as qusb2snes actually supports that for real
|
|
||||||
for address, data in write_list:
|
|
||||||
PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]]
|
|
||||||
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(data)
|
|
||||||
except websockets.ConnectionClosed:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
finally:
|
finally:
|
||||||
|
@ -811,8 +773,8 @@ async def process_server_cmd(ctx: Context, cmd, args):
|
||||||
if args['password']:
|
if args['password']:
|
||||||
ctx.ui_node.log_info('Password required')
|
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
|
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(f"Forfeit setting: {args['forfeit_mode']}")
|
||||||
logging.info("Remaining setting: "+args["remaining_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']}"
|
logging.info(f"A !hint costs {args['hint_cost']} points and you get {args['location_check_points']}"
|
||||||
f" for each location checked.")
|
f" for each location checked.")
|
||||||
ctx.hint_cost = int(args['hint_cost'])
|
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
|
This script launches a Multiplayer "Multiworld" Mystery Game
|
||||||
|
@ -18,16 +18,18 @@ import sys
|
||||||
import threading
|
import threading
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import argparse
|
import argparse
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
def feedback(text: str):
|
def feedback(text: str):
|
||||||
print(text)
|
logging.info(text)
|
||||||
input("Press Enter to ignore and probably crash.")
|
input("Press Enter to ignore and probably crash.")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
logging.basicConfig(format='%(message)s', level=logging.INFO)
|
||||||
try:
|
try:
|
||||||
print(f"{__author__}'s MultiMystery Launcher")
|
logging.info(f"{__author__}'s MultiMystery Launcher")
|
||||||
import ModuleUpdate
|
import ModuleUpdate
|
||||||
|
|
||||||
ModuleUpdate.update()
|
ModuleUpdate.update()
|
||||||
|
@ -46,6 +48,7 @@ if __name__ == "__main__":
|
||||||
output_path = options["general_options"]["output_path"]
|
output_path = options["general_options"]["output_path"]
|
||||||
enemizer_path = multi_mystery_options["enemizer_path"]
|
enemizer_path = multi_mystery_options["enemizer_path"]
|
||||||
player_files_path = multi_mystery_options["player_files_path"]
|
player_files_path = multi_mystery_options["player_files_path"]
|
||||||
|
target_player_count = multi_mystery_options["players"]
|
||||||
race = multi_mystery_options["race"]
|
race = multi_mystery_options["race"]
|
||||||
create_spoiler = multi_mystery_options["create_spoiler"]
|
create_spoiler = multi_mystery_options["create_spoiler"]
|
||||||
zip_roms = multi_mystery_options["zip_roms"]
|
zip_roms = multi_mystery_options["zip_roms"]
|
||||||
|
@ -53,47 +56,53 @@ if __name__ == "__main__":
|
||||||
zip_spoiler = multi_mystery_options["zip_spoiler"]
|
zip_spoiler = multi_mystery_options["zip_spoiler"]
|
||||||
zip_multidata = multi_mystery_options["zip_multidata"]
|
zip_multidata = multi_mystery_options["zip_multidata"]
|
||||||
zip_format = multi_mystery_options["zip_format"]
|
zip_format = multi_mystery_options["zip_format"]
|
||||||
#zip_password = multi_mystery_options["zip_password"] not at this time
|
# zip_password = multi_mystery_options["zip_password"] not at this time
|
||||||
player_name = multi_mystery_options["player_name"]
|
player_name = multi_mystery_options["player_name"]
|
||||||
meta_file_path = multi_mystery_options["meta_file_path"]
|
meta_file_path = multi_mystery_options["meta_file_path"]
|
||||||
|
weights_file_path = multi_mystery_options["weights_file_path"]
|
||||||
teams = multi_mystery_options["teams"]
|
teams = multi_mystery_options["teams"]
|
||||||
rom_file = options["general_options"]["rom_file"]
|
rom_file = options["general_options"]["rom_file"]
|
||||||
host = options["server_options"]["host"]
|
host = options["server_options"]["host"]
|
||||||
port = options["server_options"]["port"]
|
port = options["server_options"]["port"]
|
||||||
|
|
||||||
|
|
||||||
py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
||||||
|
|
||||||
if not os.path.exists(enemizer_path):
|
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):
|
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.")
|
feedback(f"Base rom is expected as {rom_file} in the Multiworld root folder please place/rename it there.")
|
||||||
player_files = []
|
player_files = []
|
||||||
os.makedirs(player_files_path, exist_ok=True)
|
os.makedirs(player_files_path, exist_ok=True)
|
||||||
for file in os.listdir(player_files_path):
|
for file in os.listdir(player_files_path):
|
||||||
lfile = file.lower()
|
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)
|
player_files.append(file)
|
||||||
print(f"Found player's file {file}.")
|
logging.info(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.")
|
|
||||||
|
|
||||||
player_string = ""
|
player_string = ""
|
||||||
for i, file in enumerate(player_files, 1):
|
for i, file in enumerate(player_files, 1):
|
||||||
player_string += f"--p{i} \"{os.path.join(player_files_path, file)}\" "
|
player_string += f"--p{i} \"{os.path.join(player_files_path, file)}\" "
|
||||||
|
|
||||||
|
|
||||||
if os.path.exists("BerserkerMultiServer.exe"):
|
if os.path.exists("BerserkerMultiServer.exe"):
|
||||||
basemysterycommand = "BerserkerMystery.exe" #compiled windows
|
basemysterycommand = "BerserkerMystery.exe" # compiled windows
|
||||||
elif os.path.exists("BerserkerMultiServer"):
|
elif os.path.exists("BerserkerMultiServer"):
|
||||||
basemysterycommand = "BerserkerMystery" # compiled linux
|
basemysterycommand = "BerserkerMystery" # compiled linux
|
||||||
else:
|
else:
|
||||||
basemysterycommand = f"py -{py_version} Mystery.py" # source
|
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"--rom \"{rom_file}\" --enemizercli \"{enemizer_path}\" " \
|
||||||
f"--outputpath \"{output_path}\" --teams {teams}"
|
f"--outputpath \"{output_path}\" --teams {teams}"
|
||||||
|
|
||||||
|
@ -107,13 +116,15 @@ if __name__ == "__main__":
|
||||||
command += " --race"
|
command += " --race"
|
||||||
if os.path.exists(os.path.join(player_files_path, meta_file_path)):
|
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)}"
|
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
|
import time
|
||||||
|
|
||||||
start = time.perf_counter()
|
start = time.perf_counter()
|
||||||
text = subprocess.check_output(command, shell=True).decode()
|
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 = ""
|
seedname = ""
|
||||||
|
|
||||||
for segment in text.split():
|
for segment in text.split():
|
||||||
|
@ -136,9 +147,10 @@ if __name__ == "__main__":
|
||||||
|
|
||||||
if any((zip_roms, zip_multidata, zip_spoiler, zip_diffs)):
|
if any((zip_roms, zip_multidata, zip_spoiler, zip_diffs)):
|
||||||
import zipfile
|
import zipfile
|
||||||
compression = {1 : zipfile.ZIP_DEFLATED,
|
|
||||||
2 : zipfile.ZIP_LZMA,
|
compression = {1: zipfile.ZIP_DEFLATED,
|
||||||
3 : zipfile.ZIP_BZIP2}[zip_format]
|
2: zipfile.ZIP_LZMA,
|
||||||
|
3: zipfile.ZIP_BZIP2}[zip_format]
|
||||||
|
|
||||||
typical_zip_ending = {1: "zip",
|
typical_zip_ending = {1: "zip",
|
||||||
2: "7z",
|
2: "7z",
|
||||||
|
@ -150,17 +162,17 @@ if __name__ == "__main__":
|
||||||
def pack_file(file: str):
|
def pack_file(file: str):
|
||||||
with ziplock:
|
with ziplock:
|
||||||
zf.write(os.path.join(output_path, file), file)
|
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):
|
def remove_zipped_file(file: str):
|
||||||
os.remove(os.path.join(output_path, file))
|
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}")
|
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)
|
ipv4 = (host if host else get_public_ipv4()) + ":" + str(port)
|
||||||
|
|
||||||
|
|
||||||
|
@ -209,10 +221,11 @@ if __name__ == "__main__":
|
||||||
baseservercommand = "BerserkerMultiServer" # compiled linux
|
baseservercommand = "BerserkerMultiServer" # compiled linux
|
||||||
else:
|
else:
|
||||||
baseservercommand = f"py -{py_version} MultiServer.py" # source
|
baseservercommand = f"py -{py_version} MultiServer.py" # source
|
||||||
#don't have a mac to test that. If you try to run compiled on mac, good luck.
|
# don't have a mac to test that. If you try to run compiled on mac, good luck.
|
||||||
|
|
||||||
subprocess.call(f"{baseservercommand} --multidata {os.path.join(output_path, multidataname)}")
|
subprocess.call(f"{baseservercommand} --multidata {os.path.join(output_path, multidataname)}")
|
||||||
except:
|
except:
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
input("Press enter to close")
|
input("Press enter to close")
|
||||||
|
|
|
@ -58,6 +58,15 @@ class Client(Endpoint):
|
||||||
|
|
||||||
|
|
||||||
class Context(Node):
|
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,
|
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",
|
hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", remaining_mode: str = "disabled",
|
||||||
auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2):
|
auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2):
|
||||||
|
@ -131,15 +140,23 @@ class Context(Node):
|
||||||
self._set_options(server_options)
|
self._set_options(server_options)
|
||||||
|
|
||||||
def _set_options(self, server_options: dict):
|
def _set_options(self, server_options: dict):
|
||||||
|
|
||||||
sentinel = object()
|
|
||||||
for key, value in server_options.items():
|
for key, value in server_options.items():
|
||||||
if key not in self.embedded_blacklist:
|
data_type = self.simple_options.get(key, None)
|
||||||
current = getattr(self, key, sentinel)
|
if data_type is not None:
|
||||||
if current is not sentinel:
|
if value not in {False, True, None}: # some can be boolean OR text, such as password
|
||||||
logging.debug(f"Setting server option {key} to {value} from supplied multidata")
|
try:
|
||||||
setattr(self, key, value)
|
value = data_type(value)
|
||||||
self.item_cheat = not server_options.get("disable_item_cheat", True)
|
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)
|
||||||
|
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:
|
def save(self, now=False) -> bool:
|
||||||
if self.saving:
|
if self.saving:
|
||||||
|
@ -651,15 +668,6 @@ class CommandProcessor(metaclass=CommandMeta):
|
||||||
class CommonCommandProcessor(CommandProcessor):
|
class CommonCommandProcessor(CommandProcessor):
|
||||||
ctx: Context
|
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:
|
def _cmd_countdown(self, seconds: str = "10") -> bool:
|
||||||
"""Start a countdown in seconds"""
|
"""Start a countdown in seconds"""
|
||||||
try:
|
try:
|
||||||
|
@ -672,7 +680,7 @@ class CommonCommandProcessor(CommandProcessor):
|
||||||
def _cmd_options(self):
|
def _cmd_options(self):
|
||||||
"""List all current options. Warning: lists password."""
|
"""List all current options. Warning: lists password."""
|
||||||
self.output("Current options:")
|
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.
|
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))}")
|
self.output(f"Option server_password is set to {('*' * random.randint(4,16))}")
|
||||||
else:
|
else:
|
||||||
|
@ -1231,7 +1239,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||||
def _cmd_option(self, option_name: str, option: str):
|
def _cmd_option(self, option_name: str, option: str):
|
||||||
"""Set options for the server. Warning: expires on restart"""
|
"""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:
|
||||||
if attrtype == bool:
|
if attrtype == bool:
|
||||||
def attrtype(input_text: str):
|
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)}")
|
self.output(f"Set option {option_name} to {getattr(self.ctx, option_name)}")
|
||||||
return True
|
return True
|
||||||
else:
|
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: "
|
self.output(f"Unrecognized Option {option_name}, known: "
|
||||||
f"{', '.join(known)}")
|
f"{', '.join(known)}")
|
||||||
return False
|
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:
|
def get_choice(option, root, value=None) -> typing.Any:
|
||||||
if option not in root:
|
if option not in root:
|
||||||
return value
|
return value
|
||||||
|
if type(root[option]) is list:
|
||||||
|
return interpret_on_off(random.choices(root[option])[0])
|
||||||
if type(root[option]) is not dict:
|
if type(root[option]) is not dict:
|
||||||
return interpret_on_off(root[option])
|
return interpret_on_off(root[option])
|
||||||
if not root[option]:
|
if not root[option]:
|
||||||
|
@ -488,6 +490,17 @@ def roll_settings(weights):
|
||||||
|
|
||||||
ret.local_items = ",".join(ret.local_items)
|
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:
|
if 'rom' in weights:
|
||||||
romweights = weights['rom']
|
romweights = weights['rom']
|
||||||
|
|
||||||
|
|
26
Rom.py
26
Rom.py
|
@ -1,7 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
JAP10HASH = '03a63945398191337e896e5771f77173'
|
JAP10HASH = '03a63945398191337e896e5771f77173'
|
||||||
RANDOMIZERBASEHASH = 'e3714804e3fae1c6ac6100b94d1aee62'
|
RANDOMIZERBASEHASH = '5a607e36a82bbd14180536c8ec3ae49b'
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
|
@ -697,12 +697,12 @@ def patch_rom(world, rom, player, team, enemized):
|
||||||
# Thanks to Zarby89 for originally finding these values
|
# Thanks to Zarby89 for originally finding these values
|
||||||
# todo fix screen scrolling
|
# todo fix screen scrolling
|
||||||
|
|
||||||
if world.shuffle[player] not in ['insanity', 'insanity_legacy', 'madness_legacy'] and \
|
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',
|
exit.name in {'Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit',
|
||||||
'Skull Woods Final Section Exit', 'Ice Palace Exit', 'Misery Mire Exit',
|
'Skull Woods Final Section Exit', 'Ice Palace Exit', 'Misery Mire Exit',
|
||||||
'Palace of Darkness Exit', 'Swamp Palace Exit', 'Ganons Tower Exit',
|
'Palace of Darkness Exit', 'Swamp Palace Exit', 'Ganons Tower Exit',
|
||||||
'Desert Palace Exit (North)', 'Agahnims Tower Exit', 'Spiral Cave Exit (Top)',
|
'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.
|
# 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
|
rom.write_int16(0x15DB5 + 2 * offset, link_y) # same as final else
|
||||||
elif room_id == 0x0059 and world.fix_skullwoods_exit[player]:
|
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(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(0x180188, [0, 0, 0]) # Zelda respawn refills (magic, bombs, arrows)
|
||||||
rom.write_bytes(0x18018B, [0, 0, 0]) # Mantle respawn refills (magic, bombs, arrows)
|
rom.write_bytes(0x18018B, [0, 0, 0]) # Mantle respawn refills (magic, bombs, arrows)
|
||||||
if world.mode[player] == 'standard':
|
if world.mode[player] == 'standard' and uncle_location.item and uncle_location.item.player == player:
|
||||||
if uncle_location.item is not None and uncle_location.item.name in ['Bow', 'Progressive Bow']:
|
if uncle_location.item.name in {'Bow', 'Progressive Bow'}:
|
||||||
rom.write_byte(0x18004E, 1) # Escape Fill (arrows)
|
rom.write_byte(0x18004E, 1) # Escape Fill (arrows)
|
||||||
rom.write_int16(0x180183, 300) # Escape fill rupee bow
|
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(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(0x180188, [0, 0, 10]) # Zelda respawn refills (magic, bombs, arrows)
|
||||||
rom.write_bytes(0x18018B, [0, 0, 10]) # Mantle 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_byte(0x18004E, 2) # Escape Fill (bombs)
|
||||||
rom.write_bytes(0x180185, [0, 50, 0]) # Uncle respawn refills (magic, bombs, arrows)
|
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(0x180188, [0, 3, 0]) # Zelda respawn refills (magic, bombs, arrows)
|
||||||
rom.write_bytes(0x18018B, [0, 3, 0]) # Mantle 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',
|
elif uncle_location.item.name in {'Cane of Somaria', 'Cane of Byrna', 'Fire Rod'}:
|
||||||
'Fire Rod']:
|
|
||||||
rom.write_byte(0x18004E, 4) # Escape Fill (magic)
|
rom.write_byte(0x18004E, 4) # Escape Fill (magic)
|
||||||
rom.write_bytes(0x180185, [0x80, 0, 0]) # Uncle respawn refills (magic, bombs, arrows)
|
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)
|
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']
|
hud_palettes = palettes_options['hud']
|
||||||
sword_palettes = palettes_options['sword']
|
sword_palettes = palettes_options['sword']
|
||||||
shield_palettes = palettes_options['shield']
|
shield_palettes = palettes_options['shield']
|
||||||
link_palettes = palettes_options['link']
|
# link_palettes = palettes_options['link']
|
||||||
buildAndRandomize("randomize_dungeon", uw_palettes)
|
buildAndRandomize("randomize_dungeon", uw_palettes)
|
||||||
buildAndRandomize("randomize_overworld", ow_palettes)
|
buildAndRandomize("randomize_overworld", ow_palettes)
|
||||||
buildAndRandomize("randomize_hud", hud_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]
|
greenpendant = world.find_items('Green Pendant', player)[0]
|
||||||
tt['sahasrahla_bring_courage'] = 'I lost my family heirloom in %s' % greenpendant.hint_text
|
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[
|
if world.crystals_needed_for_gt[player] == 1:
|
||||||
player] == 1 else 'You need %d crystals to enter.') % \
|
tt['sign_ganons_tower'] = 'You need a crystal to enter.'
|
||||||
world.crystals_needed_for_gt[player]
|
else:
|
||||||
|
tt['sign_ganons_tower'] = f'You need {world.crystals_needed_for_gt[player]} crystals to enter.'
|
||||||
|
|
||||||
if world.goal[player] == 'dungeons':
|
if world.goal[player] == 'dungeons':
|
||||||
tt['sign_ganon'] = 'You need to complete all the 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:
|
else:
|
||||||
queue.append((entrance.connected_region, new_path))
|
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):
|
def set_rule(spot, rule):
|
||||||
spot.access_rule = rule
|
spot.access_rule = rule
|
||||||
|
@ -179,6 +179,10 @@ def locality_rules(world, player):
|
||||||
for location in world.get_locations():
|
for location in world.get_locations():
|
||||||
if location.player != player:
|
if location.player != player:
|
||||||
forbid_items_for_player(location, world.local_items[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) - {
|
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}\n>Secret power\nis said to be\nin the arrow.",
|
||||||
"{C:GREEN}\nAim at the\neyes of Gohma.\n >",
|
"{C:GREEN}\nAim at the\neyes of Gohma.\n >",
|
||||||
"{C:GREEN}\nGrumble,\ngrumble…\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}\nGo to the\nnext room.\n >",
|
||||||
"{C:GREEN}\n>Thanks, @\nYou’re the\nhero of Hyrule",
|
"{C:GREEN}\n>Thanks, @\nYou’re the\nhero of Hyrule",
|
||||||
"{C:GREEN}\nThere’s always\nmoney in the\nBanana Stand>",
|
"{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("."))
|
return tuple(int(piece, 10) for piece in version.split("."))
|
||||||
|
|
||||||
|
|
||||||
__version__ = "3.3.0"
|
__version__ = "3.4.1"
|
||||||
_version_tuple = tuplize_version(__version__)
|
_version_tuple = tuplize_version(__version__)
|
||||||
|
|
||||||
import os
|
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
|
pass # we could be offline, in a local game, or ipv6 may not be available
|
||||||
return ip
|
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:
|
def get_options() -> dict:
|
||||||
if not hasattr(get_options, "options"):
|
if not hasattr(get_options, "options"):
|
||||||
locations = ("options.yaml", "host.yaml",
|
locations = ("options.yaml", "host.yaml",
|
||||||
|
@ -173,7 +257,9 @@ def get_options() -> dict:
|
||||||
for location in locations:
|
for location in locations:
|
||||||
if os.path.exists(location):
|
if os.path.exists(location):
|
||||||
with open(location) as f:
|
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
|
break
|
||||||
else:
|
else:
|
||||||
raise FileNotFoundError(f"Could not find {locations[1]} to load options.")
|
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"):
|
if hasattr(get_adjuster_settings, "adjuster_settings"):
|
||||||
adjuster_settings = getattr(get_adjuster_settings, "adjuster_settings")
|
adjuster_settings = getattr(get_adjuster_settings, "adjuster_settings")
|
||||||
else:
|
else:
|
||||||
adjuster_settings = persistent_load().get("adjuster", {}).get("last_settings", {})
|
adjuster_settings = persistent_load().get("adjuster", {}).get("last_settings_3", {})
|
||||||
|
|
||||||
if adjuster_settings:
|
if adjuster_settings:
|
||||||
import pprint
|
import pprint
|
||||||
import Patch
|
import Patch
|
||||||
adjuster_settings.rom = romfile
|
adjuster_settings.rom = romfile
|
||||||
adjuster_settings.baserom = Patch.get_base_rom_path()
|
adjuster_settings.baserom = Patch.get_base_rom_path()
|
||||||
whitelist = {"disablemusic", "fastmenu", "heartbeep", "heartcolor", "ow_palettes", "quickswap",
|
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}
|
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"):
|
if hasattr(get_adjuster_settings, "adjust_wanted"):
|
||||||
adjust_wanted = getattr(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:
|
else:
|
||||||
adjust_wanted = input(f"Last used adjuster settings were found. Would you like to apply these? \n"
|
adjust_wanted = input(f"Last used adjuster settings were found. Would you like to apply these? \n"
|
||||||
f"{pprint.pformat(printed_options)}\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"):
|
if adjust_wanted and adjust_wanted.startswith("y"):
|
||||||
adjusted = True
|
adjusted = True
|
||||||
import AdjusterMain
|
import AdjusterMain
|
||||||
_, romfile = AdjusterMain.adjust(adjuster_settings)
|
_, romfile = AdjusterMain.adjust(adjuster_settings)
|
||||||
|
elif adjust_wanted and "never" in adjust_wanted:
|
||||||
|
persistent_store("adjuster", "never_adjust", True)
|
||||||
|
return romfile, False
|
||||||
else:
|
else:
|
||||||
adjusted = False
|
adjusted = False
|
||||||
import logging
|
import logging
|
||||||
|
|
|
@ -47,6 +47,8 @@ app.config["PONY"] = {
|
||||||
}
|
}
|
||||||
app.config["MAX_ROLL"] = 20
|
app.config["MAX_ROLL"] = 20
|
||||||
app.config["CACHE_TYPE"] = "simple"
|
app.config["CACHE_TYPE"] = "simple"
|
||||||
|
app.config["JSON_AS_ASCII"] = False
|
||||||
|
|
||||||
app.autoversion = True
|
app.autoversion = True
|
||||||
av = Autoversion(app)
|
av = Autoversion(app)
|
||||||
cache = Cache(app)
|
cache = Cache(app)
|
||||||
|
@ -145,4 +147,5 @@ def favicon():
|
||||||
|
|
||||||
|
|
||||||
from WebHostLib.customserver import run_server_process
|
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):
|
class WebHostContext(Context):
|
||||||
def __init__(self):
|
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.main_loop = asyncio.get_running_loop()
|
||||||
self.video = {}
|
self.video = {}
|
||||||
self.tags = ["Berserker", "WebHost"]
|
self.tags = ["Berserker", "WebHost"]
|
||||||
|
|
|
@ -52,55 +52,6 @@ def generate(race=False):
|
||||||
return render_template("generate.html", race=race)
|
return render_template("generate.html", race=race)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/generate', methods=['POST'])
|
|
||||||
def generate_api():
|
|
||||||
try:
|
|
||||||
options = {}
|
|
||||||
race = False
|
|
||||||
|
|
||||||
if 'file' in request.files:
|
|
||||||
file = request.files['file']
|
|
||||||
options = get_yaml_data(file)
|
|
||||||
if type(options) == str:
|
|
||||||
return {"text": options}, 400
|
|
||||||
if "race" in request.form:
|
|
||||||
race = bool(0 if request.form["race"] in {"false"} else int(request.form["race"]))
|
|
||||||
|
|
||||||
json_data = request.get_json()
|
|
||||||
if json_data:
|
|
||||||
if 'weights' in json_data:
|
|
||||||
# example: options = {"player1weights" : {<weightsdata>}}
|
|
||||||
options = json_data["weights"]
|
|
||||||
if "race" in json_data:
|
|
||||||
race = bool(0 if json_data["race"] in {"false"} else int(json_data["race"]))
|
|
||||||
if not options:
|
|
||||||
return {"text": "No options found. Expected file attachment or json weights."
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
if len(options) > app.config["MAX_ROLL"]:
|
|
||||||
return {"text": "Max size of multiworld exceeded",
|
|
||||||
"detail": app.config["MAX_ROLL"]}, 409
|
|
||||||
|
|
||||||
results, gen_options = roll_options(options)
|
|
||||||
if any(type(result) == str for result in results.values()):
|
|
||||||
return {"text": str(results),
|
|
||||||
"detail": results}, 400
|
|
||||||
else:
|
|
||||||
gen = Generation(
|
|
||||||
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
|
|
||||||
# convert to json compatible
|
|
||||||
meta=pickle.dumps({"race": race}), state=STATE_QUEUED,
|
|
||||||
owner=session["_id"])
|
|
||||||
commit()
|
|
||||||
return {"text": f"Generation of seed {gen.id} started successfully.",
|
|
||||||
"detail": gen.id,
|
|
||||||
"encoded": app.url_map.converters["suuid"].to_url(None, gen.id),
|
|
||||||
"wait_api_url": url_for("wait_seed_api", seed=gen.id),
|
|
||||||
"url": url_for("wait_seed", seed=gen.id)}, 201
|
|
||||||
except Exception as e:
|
|
||||||
return {"text": "Uncaught Exception:" + str(e)}, 500
|
|
||||||
|
|
||||||
|
|
||||||
def gen_game(gen_options, race=False, owner=None, sid=None):
|
def gen_game(gen_options, race=False, owner=None, sid=None):
|
||||||
try:
|
try:
|
||||||
target = tempfile.TemporaryDirectory()
|
target = tempfile.TemporaryDirectory()
|
||||||
|
@ -142,12 +93,13 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
|
||||||
ERmain(erargs, seed)
|
ERmain(erargs, seed)
|
||||||
|
|
||||||
return upload_to_db(target.name, owner, sid, race)
|
return upload_to_db(target.name, owner, sid, race)
|
||||||
except BaseException:
|
except BaseException as e:
|
||||||
if sid:
|
if sid:
|
||||||
with db_session:
|
with db_session:
|
||||||
gen = Generation.get(id=sid)
|
gen = Generation.get(id=sid)
|
||||||
if gen is not None:
|
if gen is not None:
|
||||||
gen.state = STATE_ERROR
|
gen.state = STATE_ERROR
|
||||||
|
gen.meta = (e.__class__.__name__ + ": "+ str(e)).encode()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
@ -162,25 +114,11 @@ def wait_seed(seed: UUID):
|
||||||
if not generation:
|
if not generation:
|
||||||
return "Generation not found."
|
return "Generation not found."
|
||||||
elif generation.state == STATE_ERROR:
|
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)
|
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):
|
def upload_to_db(folder, owner, sid, race:bool):
|
||||||
patches = set()
|
patches = set()
|
||||||
spoiler = ""
|
spoiler = ""
|
||||||
|
|
|
@ -50,5 +50,5 @@ class Generation(db.Entity):
|
||||||
id = PrimaryKey(UUID, default=uuid4)
|
id = PrimaryKey(UUID, default=uuid4)
|
||||||
owner = Required(UUID)
|
owner = Required(UUID)
|
||||||
options = Required(bytes, lazy=True) # these didn't work as JSON on mariaDB, so they're getting pickled now
|
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)
|
state = Required(int, default=0, index=True)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
flask>=1.1.2
|
flask>=1.1.2
|
||||||
pony>=0.7.13
|
pony>=0.7.14
|
||||||
waitress>=1.4.4
|
waitress>=1.4.4
|
||||||
flask-caching>=1.9.0
|
flask-caching>=1.9.0
|
||||||
Flask-Autoversion>=0.2.0
|
Flask-Autoversion>=0.2.0
|
||||||
Flask-Compress>=1.7.0
|
Flask-Compress>=1.8.0
|
||||||
Flask-Limiter>=1.4
|
Flask-Limiter>=1.4
|
||||||
|
|
|
@ -791,12 +791,6 @@
|
||||||
"friendlyName": "Expert",
|
"friendlyName": "Expert",
|
||||||
"description": "Minimum upgrade availability (max: 8 hearts, green mail, master sword, fighter shield, no silvers unless swordless).",
|
"description": "Minimum upgrade availability (max: 8 hearts, green mail, master sword, fighter shield, no silvers unless swordless).",
|
||||||
"defaultValue": 0
|
"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
|
"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": {
|
"romOptions": {
|
||||||
|
|
|
@ -165,7 +165,6 @@ item_pool:
|
||||||
normal: 50 # Item availability remains unchanged from vanilla game
|
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)
|
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)
|
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:
|
item_functionality:
|
||||||
easy: 0 # Allow Hammer to damage ganon, Allow Hammer tablet collection, Allow swordless medallion use everywhere.
|
easy: 0 # Allow Hammer to damage ganon, Allow Hammer tablet collection, Allow swordless medallion use everywhere.
|
||||||
normal: 50 # Vanilla item functionality
|
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
|
2: 0 # And shuffles open edges and straight staircases
|
||||||
3: 0 # And shuffles dungeon lobbies
|
3: 0 # And shuffles dungeon lobbies
|
||||||
random: 0 # Picks one of those at random
|
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
|
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.
|
on: 0 # Enables experimental features. Currently, this is just the dungeon keys in chest counter.
|
||||||
off: 50
|
off: 50
|
||||||
|
|
|
@ -55,6 +55,7 @@ html{
|
||||||
#player-settings #settings-wrapper #sprite-picker .sprite-img-wrapper{
|
#player-settings #settings-wrapper #sprite-picker .sprite-img-wrapper{
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
|
image-rendering: pixelated;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Center tooltip text for sprite images */
|
/* 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