Merge branch 'main' into breaking_changes
# Conflicts: # Adjuster.py # AdjusterMain.py # BaseClasses.py # MultiClient.py # MultiServer.py # Mystery.py # Utils.py # WebHostLib/downloads.py # WebHostLib/generate.py # dumpSprites.py # test/TestBase.py # worlds/alttp/EntranceRandomizer.py # worlds/alttp/Main.py # worlds/alttp/Rom.py
|
@ -3,11 +3,7 @@
|
||||||
|
|
||||||
name: unittests
|
name: unittests
|
||||||
|
|
||||||
on:
|
on: [push, pull_request]
|
||||||
push:
|
|
||||||
branches: [ master ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ master ]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
|
@ -30,3 +30,7 @@ weights/
|
||||||
_persistent_storage.yaml
|
_persistent_storage.yaml
|
||||||
mystery_result_*.yaml
|
mystery_result_*.yaml
|
||||||
/db.db3
|
/db.db3
|
||||||
|
*-errors.txt
|
||||||
|
success.txt
|
||||||
|
output/
|
||||||
|
Output Logs/
|
||||||
|
|
17
Adjuster.py
|
@ -6,7 +6,7 @@ import textwrap
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from AdjusterMain import adjust
|
from AdjusterMain import adjust
|
||||||
from worlds.alttp.Rom import get_sprite_from_name
|
from worlds.alttp.Rom import Sprite
|
||||||
|
|
||||||
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
||||||
|
|
||||||
|
@ -34,8 +34,12 @@ def main():
|
||||||
''')
|
''')
|
||||||
parser.add_argument('--heartcolor', default='red', const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'],
|
parser.add_argument('--heartcolor', default='red', const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'],
|
||||||
help='Select the color of Link\'s heart meter. (default: %(default)s)')
|
help='Select the color of Link\'s heart meter. (default: %(default)s)')
|
||||||
parser.add_argument('--ow_palettes', default='default', choices=['default', 'random', 'blackout'])
|
parser.add_argument('--ow_palettes', default='default', choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
|
||||||
parser.add_argument('--uw_palettes', default='default', choices=['default', 'random', 'blackout'])
|
parser.add_argument('--link_palettes', default='default', choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
|
||||||
|
parser.add_argument('--shield_palettes', default='default', choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
|
||||||
|
parser.add_argument('--sword_palettes', default='default', choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
|
||||||
|
parser.add_argument('--hud_palettes', default='default', choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
|
||||||
|
parser.add_argument('--uw_palettes', default='default', choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
|
||||||
parser.add_argument('--sprite', help='''\
|
parser.add_argument('--sprite', help='''\
|
||||||
Path to a sprite sheet to use for Link. Needs to be in
|
Path to a sprite sheet to use for Link. Needs to be in
|
||||||
binary format and have a length of 0x7000 (28672) bytes,
|
binary format and have a length of 0x7000 (28672) bytes,
|
||||||
|
@ -51,7 +55,7 @@ def main():
|
||||||
input(
|
input(
|
||||||
'Could not find valid rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom)
|
'Could not find valid rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if args.sprite is not None and not os.path.isfile(args.sprite) and not get_sprite_from_name(args.sprite):
|
if args.sprite is not None and not os.path.isfile(args.sprite) and not Sprite.get_sprite_from_name(args.sprite):
|
||||||
input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
|
input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
@ -61,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()
|
||||||
|
|
|
@ -10,19 +10,27 @@ def adjust(args):
|
||||||
start = time.perf_counter()
|
start = time.perf_counter()
|
||||||
logger = logging.getLogger('Adjuster')
|
logger = logging.getLogger('Adjuster')
|
||||||
logger.info('Patching ROM.')
|
logger.info('Patching ROM.')
|
||||||
|
vanillaRom = args.baserom
|
||||||
if os.path.splitext(args.rom)[-1].lower() == '.apbp':
|
if os.path.splitext(args.rom)[-1].lower() == '.apbp':
|
||||||
import Patch
|
import Patch
|
||||||
meta, args.rom = Patch.create_rom_file(args.rom)
|
meta, args.rom = Patch.create_rom_file(args.rom)
|
||||||
|
|
||||||
if os.stat(args.rom).st_size in (0x200000, 0x400000) and os.path.splitext(args.rom)[-1].lower() == '.sfc':
|
if os.stat(args.rom).st_size in (0x200000, 0x400000) and os.path.splitext(args.rom)[-1].lower() == '.sfc':
|
||||||
rom = LocalRom(args.rom, patch=False)
|
rom = LocalRom(args.rom, patch=False, vanillaRom=vanillaRom)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
'Provided Rom is not a valid Link to the Past Randomizer Rom. Please provide one for adjusting.')
|
'Provided Rom is not a valid Link to the Past Randomizer Rom. Please provide one for adjusting.')
|
||||||
|
palettes_options={}
|
||||||
|
palettes_options['dungeon']=args.uw_palettes
|
||||||
|
|
||||||
|
palettes_options['overworld']=args.ow_palettes
|
||||||
|
palettes_options['hud']=args.hud_palettes
|
||||||
|
palettes_options['sword']=args.sword_palettes
|
||||||
|
palettes_options['shield']=args.shield_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, args.ow_palettes, args.uw_palettes)
|
args.sprite, palettes_options)
|
||||||
path = output_path(f'{os.path.basename(args.rom)[:-4]}_adjusted.sfc')
|
path = output_path(f'{os.path.basename(args.rom)[:-4]}_adjusted.sfc')
|
||||||
rom.write_to_file(path)
|
rom.write_to_file(path)
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ from enum import Enum, unique
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
from collections import OrderedDict, Counter, deque
|
from collections import OrderedDict, Counter, deque
|
||||||
from typing import Union, Optional, List, Dict
|
from typing import Union, Optional, List, Dict, NamedTuple
|
||||||
import secrets
|
import secrets
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
@ -20,12 +20,15 @@ class World():
|
||||||
|
|
||||||
class MultiWorld():
|
class MultiWorld():
|
||||||
debug_types = False
|
debug_types = False
|
||||||
player_names: list
|
player_names: Dict[int, List[str]]
|
||||||
_region_cache: dict
|
_region_cache: dict
|
||||||
difficulty_requirements: dict
|
difficulty_requirements: dict
|
||||||
required_medallions: dict
|
required_medallions: dict
|
||||||
dark_room_logic: Dict[int, str]
|
dark_room_logic: Dict[int, str]
|
||||||
restrict_dungeon_item_on_boss: Dict[int, bool]
|
restrict_dungeon_item_on_boss: Dict[int, bool]
|
||||||
|
plando_texts: List[Dict[str, str]]
|
||||||
|
plando_items: List[PlandoItem]
|
||||||
|
plando_connections: List[PlandoConnection]
|
||||||
|
|
||||||
def __init__(self, players: int, shuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer,
|
def __init__(self, players: int, shuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer,
|
||||||
progressive,
|
progressive,
|
||||||
|
@ -115,10 +118,15 @@ class MultiWorld():
|
||||||
set_player_attr('treasure_hunt_icon', 'Triforce Piece')
|
set_player_attr('treasure_hunt_icon', 'Triforce Piece')
|
||||||
set_player_attr('treasure_hunt_count', 0)
|
set_player_attr('treasure_hunt_count', 0)
|
||||||
set_player_attr('clock_mode', False)
|
set_player_attr('clock_mode', False)
|
||||||
|
set_player_attr('countdown_start_time', 10)
|
||||||
|
set_player_attr('red_clock_time', -2)
|
||||||
|
set_player_attr('blue_clock_time', 2)
|
||||||
|
set_player_attr('green_clock_time', 4)
|
||||||
set_player_attr('can_take_damage', True)
|
set_player_attr('can_take_damage', True)
|
||||||
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')
|
||||||
|
@ -126,6 +134,9 @@ class MultiWorld():
|
||||||
set_player_attr('sprite_pool', [])
|
set_player_attr('sprite_pool', [])
|
||||||
set_player_attr('dark_room_logic', "lamp")
|
set_player_attr('dark_room_logic', "lamp")
|
||||||
set_player_attr('restrict_dungeon_item_on_boss', False)
|
set_player_attr('restrict_dungeon_item_on_boss', False)
|
||||||
|
set_player_attr('plando_items', [])
|
||||||
|
set_player_attr('plando_texts', {})
|
||||||
|
set_player_attr('plando_connections', [])
|
||||||
|
|
||||||
self.worlds = []
|
self.worlds = []
|
||||||
#for i in range(players):
|
#for i in range(players):
|
||||||
|
@ -578,7 +589,7 @@ class CollectionState(object):
|
||||||
|
|
||||||
def can_retrieve_tablet(self, player:int) -> bool:
|
def can_retrieve_tablet(self, player:int) -> bool:
|
||||||
return self.has('Book of Mudora', player) and (self.has_beam_sword(player) or
|
return self.has('Book of Mudora', player) and (self.has_beam_sword(player) or
|
||||||
((self.world.swords[player] == "swordless" or self.world.difficulty_adjustments[player] == "easy") and
|
(self.world.swords[player] == "swordless" and
|
||||||
self.has("Hammer", player)))
|
self.has("Hammer", player)))
|
||||||
|
|
||||||
def has_sword(self, player: int) -> bool:
|
def has_sword(self, player: int) -> bool:
|
||||||
|
@ -612,7 +623,7 @@ class CollectionState(object):
|
||||||
def can_melt_things(self, player: int) -> bool:
|
def can_melt_things(self, player: int) -> bool:
|
||||||
return self.has('Fire Rod', player) or \
|
return self.has('Fire Rod', player) or \
|
||||||
(self.has('Bombos', player) and
|
(self.has('Bombos', player) and
|
||||||
(self.world.difficulty_adjustments[player] == "easy" or self.world.swords[player] == "swordless" or
|
(self.world.swords[player] == "swordless" or
|
||||||
self.has_sword(player)))
|
self.has_sword(player)))
|
||||||
|
|
||||||
def can_avoid_lasers(self, player: int) -> bool:
|
def can_avoid_lasers(self, player: int) -> bool:
|
||||||
|
@ -987,6 +998,12 @@ class Item(object):
|
||||||
self.world = None
|
self.world = None
|
||||||
self.player = player
|
self.player = player
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.name == other.name and self.player == other.player
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash((self.name, self.player))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def crystal(self) -> bool:
|
def crystal(self) -> bool:
|
||||||
return self.type == 'Crystal'
|
return self.type == 'Crystal'
|
||||||
|
@ -1402,3 +1419,16 @@ class Spoiler(object):
|
||||||
path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines)))
|
path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines)))
|
||||||
|
|
||||||
outfile.write('\n'.join(path_listings))
|
outfile.write('\n'.join(path_listings))
|
||||||
|
|
||||||
|
|
||||||
|
class PlandoItem(NamedTuple):
|
||||||
|
item: str
|
||||||
|
location: str
|
||||||
|
world: Union[bool, str] = False # False -> own world, True -> not own world
|
||||||
|
from_pool: bool = True # if item should be removed from item pool
|
||||||
|
|
||||||
|
|
||||||
|
class PlandoConnection(NamedTuple):
|
||||||
|
entrance: str
|
||||||
|
exit: str
|
||||||
|
direction: str # entrance, exit or both
|
||||||
|
|
13
Fill.py
|
@ -54,8 +54,10 @@ 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)
|
||||||
|
# fill in name of world for item
|
||||||
|
item_to_place.world = world
|
||||||
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)
|
||||||
|
@ -127,9 +129,12 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
|
||||||
world.random.shuffle(fill_locations)
|
world.random.shuffle(fill_locations)
|
||||||
|
|
||||||
# Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots
|
# Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots
|
||||||
progitempool.sort(
|
standard_keyshuffle_players = {player for player, mode in world.mode.items() if mode == 'standard' and
|
||||||
key=lambda item: 1 if item.name == 'Small Key (Hyrule Castle)' and world.mode[item.player] == 'standard' and
|
world.keyshuffle[player] is True}
|
||||||
world.keyshuffle[item.player] else 0)
|
if standard_keyshuffle_players:
|
||||||
|
progitempool.sort(
|
||||||
|
key=lambda item: 1 if item.name == 'Small Key (Hyrule Castle)' and
|
||||||
|
item.player in standard_keyshuffle_players else 0)
|
||||||
|
|
||||||
fill_restrictive(world, world.state, fill_locations, progitempool)
|
fill_restrictive(world, world.state, fill_locations, progitempool)
|
||||||
|
|
||||||
|
|
429
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,14 +96,10 @@ 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)
|
tileShuffleVar = IntVar()
|
||||||
balancingVar = IntVar()
|
tileShuffleButton = Checkbutton(checkBoxFrame, text="Tile shuffle", variable=tileShuffleVar)
|
||||||
balancingVar.set(1) # set default
|
|
||||||
balancingCheckbutton = Checkbutton(checkBoxFrame, text="Multiworld Progression Balancing", variable=balancingVar)
|
|
||||||
patchesVar = IntVar()
|
|
||||||
patchesVar.set(1) # set default
|
|
||||||
patchesCheckbutton = Checkbutton(checkBoxFrame, text="Create Delta Patches", variable=patchesVar)
|
|
||||||
createSpoilerCheckbutton.pack(expand=True, anchor=W)
|
createSpoilerCheckbutton.pack(expand=True, anchor=W)
|
||||||
suppressRomCheckbutton.pack(expand=True, anchor=W)
|
suppressRomCheckbutton.pack(expand=True, anchor=W)
|
||||||
openpyramidCheckbutton.pack(expand=True, anchor=W)
|
openpyramidCheckbutton.pack(expand=True, anchor=W)
|
||||||
|
@ -116,9 +112,8 @@ 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)
|
tileShuffleButton.pack(expand=True, anchor=W)
|
||||||
balancingCheckbutton.pack(expand=True, anchor=W)
|
|
||||||
patchesCheckbutton.pack(expand=True, anchor=W)
|
|
||||||
|
|
||||||
romOptionsFrame = LabelFrame(rightHalfFrame, text="Rom options")
|
romOptionsFrame = LabelFrame(rightHalfFrame, text="Rom options")
|
||||||
romOptionsFrame.columnconfigure(0, weight=1)
|
romOptionsFrame.columnconfigure(0, weight=1)
|
||||||
|
@ -198,7 +193,7 @@ def guiMain(args=None):
|
||||||
owPalettesLabel.pack(side=LEFT)
|
owPalettesLabel.pack(side=LEFT)
|
||||||
owPalettesVar = StringVar()
|
owPalettesVar = StringVar()
|
||||||
owPalettesVar.set('default')
|
owPalettesVar.set('default')
|
||||||
owPalettesOptionMenu = OptionMenu(owPalettesFrame, owPalettesVar, 'default', 'random', 'blackout')
|
owPalettesOptionMenu = OptionMenu(owPalettesFrame, owPalettesVar, 'default', 'random', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||||
owPalettesOptionMenu.pack(side=LEFT)
|
owPalettesOptionMenu.pack(side=LEFT)
|
||||||
|
|
||||||
uwPalettesFrame = Frame(romOptionsFrame)
|
uwPalettesFrame = Frame(romOptionsFrame)
|
||||||
|
@ -207,11 +202,41 @@ def guiMain(args=None):
|
||||||
uwPalettesLabel.pack(side=LEFT)
|
uwPalettesLabel.pack(side=LEFT)
|
||||||
uwPalettesVar = StringVar()
|
uwPalettesVar = StringVar()
|
||||||
uwPalettesVar.set('default')
|
uwPalettesVar.set('default')
|
||||||
uwPalettesOptionMenu = OptionMenu(uwPalettesFrame, uwPalettesVar, 'default', 'random', 'blackout')
|
uwPalettesOptionMenu = OptionMenu(uwPalettesFrame, uwPalettesVar, 'default', 'random', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||||
uwPalettesOptionMenu.pack(side=LEFT)
|
uwPalettesOptionMenu.pack(side=LEFT)
|
||||||
|
|
||||||
|
hudPalettesFrame = Frame(romOptionsFrame)
|
||||||
|
hudPalettesFrame.grid(row=4, column=0, sticky=E)
|
||||||
|
hudPalettesLabel = Label(hudPalettesFrame, text='HUD palettes')
|
||||||
|
hudPalettesLabel.pack(side=LEFT)
|
||||||
|
hudPalettesVar = StringVar()
|
||||||
|
hudPalettesVar.set('default')
|
||||||
|
hudPalettesOptionMenu = OptionMenu(hudPalettesFrame, hudPalettesVar, 'default', 'random', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||||
|
hudPalettesOptionMenu.pack(side=LEFT)
|
||||||
|
|
||||||
|
swordPalettesFrame = Frame(romOptionsFrame)
|
||||||
|
swordPalettesFrame.grid(row=4, column=1, sticky=E)
|
||||||
|
swordPalettesLabel = Label(swordPalettesFrame, text='Sword palettes')
|
||||||
|
swordPalettesLabel.pack(side=LEFT)
|
||||||
|
swordPalettesVar = StringVar()
|
||||||
|
swordPalettesVar.set('default')
|
||||||
|
swordPalettesOptionMenu = OptionMenu(swordPalettesFrame, swordPalettesVar, 'default', 'random', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||||
|
swordPalettesOptionMenu.pack(side=LEFT)
|
||||||
|
|
||||||
|
shieldPalettesFrame = Frame(romOptionsFrame)
|
||||||
|
shieldPalettesFrame.grid(row=5, column=0, sticky=E)
|
||||||
|
shieldPalettesLabel = Label(shieldPalettesFrame, text='Shield palettes')
|
||||||
|
shieldPalettesLabel.pack(side=LEFT)
|
||||||
|
shieldPalettesVar = StringVar()
|
||||||
|
shieldPalettesVar.set('default')
|
||||||
|
shieldPalettesOptionMenu = OptionMenu(shieldPalettesFrame, shieldPalettesVar, 'default', 'random', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||||
|
shieldPalettesOptionMenu.pack(side=LEFT)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
romDialogFrame = Frame(romOptionsFrame)
|
romDialogFrame = Frame(romOptionsFrame)
|
||||||
romDialogFrame.grid(row=4, column=0, columnspan=2, sticky=W+E)
|
romDialogFrame.grid(row=6, column=0, columnspan=2, sticky=W+E)
|
||||||
|
|
||||||
baseRomLabel = Label(romDialogFrame, text='Base Rom: ')
|
baseRomLabel = Label(romDialogFrame, text='Base Rom: ')
|
||||||
romVar = StringVar(value="Zelda no Densetsu - Kamigami no Triforce (Japan).sfc")
|
romVar = StringVar(value="Zelda no Densetsu - Kamigami no Triforce (Japan).sfc")
|
||||||
|
@ -236,6 +261,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)
|
||||||
|
|
||||||
romOptionsFrame.pack(expand=True, fill=BOTH, padx=3)
|
romOptionsFrame.pack(expand=True, fill=BOTH, padx=3)
|
||||||
|
|
||||||
drowDownFrame = Frame(topFrame)
|
drowDownFrame = Frame(topFrame)
|
||||||
|
@ -314,14 +340,6 @@ def guiMain(args=None):
|
||||||
itemfunctionLabel = Label(itemfunctionFrame, text='Difficulty: item functionality')
|
itemfunctionLabel = Label(itemfunctionFrame, text='Difficulty: item functionality')
|
||||||
itemfunctionLabel.pack(side=LEFT)
|
itemfunctionLabel.pack(side=LEFT)
|
||||||
|
|
||||||
timerFrame = Frame(drowDownFrame)
|
|
||||||
timerVar = StringVar()
|
|
||||||
timerVar.set('none')
|
|
||||||
timerOptionMenu = OptionMenu(timerFrame, timerVar, 'none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown')
|
|
||||||
timerOptionMenu.pack(side=RIGHT)
|
|
||||||
timerLabel = Label(timerFrame, text='Timer setting')
|
|
||||||
timerLabel.pack(side=LEFT)
|
|
||||||
|
|
||||||
dungeonCounterFrame = Frame(drowDownFrame)
|
dungeonCounterFrame = Frame(drowDownFrame)
|
||||||
dungeonCounterVar = StringVar()
|
dungeonCounterVar = StringVar()
|
||||||
dungeonCounterVar.set('auto')
|
dungeonCounterVar.set('auto')
|
||||||
|
@ -382,7 +400,6 @@ def guiMain(args=None):
|
||||||
swordFrame.pack(expand=True, anchor=E)
|
swordFrame.pack(expand=True, anchor=E)
|
||||||
difficultyFrame.pack(expand=True, anchor=E)
|
difficultyFrame.pack(expand=True, anchor=E)
|
||||||
itemfunctionFrame.pack(expand=True, anchor=E)
|
itemfunctionFrame.pack(expand=True, anchor=E)
|
||||||
timerFrame.pack(expand=True, anchor=E)
|
|
||||||
dungeonCounterFrame.pack(expand=True, anchor=E)
|
dungeonCounterFrame.pack(expand=True, anchor=E)
|
||||||
progressiveFrame.pack(expand=True, anchor=E)
|
progressiveFrame.pack(expand=True, anchor=E)
|
||||||
accessibilityFrame.pack(expand=True, anchor=E)
|
accessibilityFrame.pack(expand=True, anchor=E)
|
||||||
|
@ -449,17 +466,13 @@ def guiMain(args=None):
|
||||||
potShuffleButton = Checkbutton(enemizerFrame, text="Pot shuffle", variable=potShuffleVar)
|
potShuffleButton = Checkbutton(enemizerFrame, text="Pot shuffle", variable=potShuffleVar)
|
||||||
potShuffleButton.grid(row=2, column=0, sticky=W)
|
potShuffleButton.grid(row=2, column=0, sticky=W)
|
||||||
|
|
||||||
tileShuffleVar = IntVar()
|
|
||||||
tileShuffleButton = Checkbutton(enemizerFrame, text="Tile shuffle", variable=tileShuffleVar)
|
|
||||||
tileShuffleButton.grid(row=2, column=1, sticky=W)
|
|
||||||
|
|
||||||
bushShuffleVar = IntVar()
|
bushShuffleVar = IntVar()
|
||||||
bushShuffleButton = Checkbutton(enemizerFrame, text="Bush shuffle", variable=bushShuffleVar)
|
bushShuffleButton = Checkbutton(enemizerFrame, text="Bush shuffle", variable=bushShuffleVar)
|
||||||
bushShuffleButton.grid(row=2, column=2, sticky=W)
|
bushShuffleButton.grid(row=2, column=1, sticky=W)
|
||||||
|
|
||||||
killableThievesVar = IntVar()
|
killableThievesVar = IntVar()
|
||||||
killable_thievesShuffleButton = Checkbutton(enemizerFrame, text="Killable Thieves", variable=killableThievesVar)
|
killable_thievesShuffleButton = Checkbutton(enemizerFrame, text="Killable Thieves", variable=killableThievesVar)
|
||||||
killable_thievesShuffleButton.grid(row=2, column=3, sticky=W)
|
killable_thievesShuffleButton.grid(row=2, column=2, sticky=W)
|
||||||
|
|
||||||
shopframe = LabelFrame(randomizerWindow, text="Shops", padx=5, pady=2)
|
shopframe = LabelFrame(randomizerWindow, text="Shops", padx=5, pady=2)
|
||||||
|
|
||||||
|
@ -477,7 +490,7 @@ def guiMain(args=None):
|
||||||
|
|
||||||
multiworldframe = LabelFrame(randomizerWindow, text="Multiworld", padx=5, pady=2)
|
multiworldframe = LabelFrame(randomizerWindow, text="Multiworld", padx=5, pady=2)
|
||||||
|
|
||||||
worldLabel = Label(multiworldframe, text='Worlds')
|
worldLabel = Label(multiworldframe, text='Players per Team')
|
||||||
worldVar = StringVar()
|
worldVar = StringVar()
|
||||||
worldSpinbox = Spinbox(multiworldframe, from_=1, to=255, width=5, textvariable=worldVar)
|
worldSpinbox = Spinbox(multiworldframe, from_=1, to=255, width=5, textvariable=worldVar)
|
||||||
namesLabel = Label(multiworldframe, text='Player names')
|
namesLabel = Label(multiworldframe, text='Player names')
|
||||||
|
@ -486,10 +499,17 @@ def guiMain(args=None):
|
||||||
seedLabel = Label(multiworldframe, text='Seed #')
|
seedLabel = Label(multiworldframe, text='Seed #')
|
||||||
seedVar = StringVar()
|
seedVar = StringVar()
|
||||||
seedEntry = Entry(multiworldframe, width=20, textvariable=seedVar)
|
seedEntry = Entry(multiworldframe, width=20, textvariable=seedVar)
|
||||||
countLabel = Label(multiworldframe, text='Count')
|
countLabel = Label(multiworldframe, text='Amount of Multiworlds')
|
||||||
countVar = StringVar()
|
countVar = StringVar()
|
||||||
countSpinbox = Spinbox(multiworldframe, from_=1, to=100, width=5, textvariable=countVar)
|
countSpinbox = Spinbox(multiworldframe, from_=1, to=100, width=5, textvariable=countVar)
|
||||||
|
|
||||||
|
balancingVar = IntVar()
|
||||||
|
balancingVar.set(1) # set default
|
||||||
|
balancingCheckbutton = Checkbutton(multiworldframe, text="Progression Balancing", variable=balancingVar)
|
||||||
|
patchesVar = IntVar()
|
||||||
|
patchesVar.set(1) # set default
|
||||||
|
patchesCheckbutton = Checkbutton(multiworldframe, text="Create Delta Patches", variable=patchesVar)
|
||||||
|
|
||||||
def generateRom():
|
def generateRom():
|
||||||
guiargs = Namespace()
|
guiargs = Namespace()
|
||||||
guiargs.multi = int(worldVar.get())
|
guiargs.multi = int(worldVar.get())
|
||||||
|
@ -508,6 +528,10 @@ def guiMain(args=None):
|
||||||
guiargs.difficulty = difficultyVar.get()
|
guiargs.difficulty = difficultyVar.get()
|
||||||
guiargs.item_functionality = itemfunctionVar.get()
|
guiargs.item_functionality = itemfunctionVar.get()
|
||||||
guiargs.timer = timerVar.get()
|
guiargs.timer = timerVar.get()
|
||||||
|
guiargs.countdown_start_time = timerCountdownVar.get()
|
||||||
|
guiargs.red_clock_time = timerRedVar.get()
|
||||||
|
guiargs.blue_clock_time = timerBlueVar.get()
|
||||||
|
guiargs.green_clock_time = timerGreenVar.get()
|
||||||
guiargs.skip_progression_balancing = not balancingVar.get()
|
guiargs.skip_progression_balancing = not balancingVar.get()
|
||||||
if guiargs.timer == "none":
|
if guiargs.timer == "none":
|
||||||
guiargs.timer = False
|
guiargs.timer = False
|
||||||
|
@ -538,6 +562,9 @@ def guiMain(args=None):
|
||||||
guiargs.disablemusic = bool(disableMusicVar.get())
|
guiargs.disablemusic = bool(disableMusicVar.get())
|
||||||
guiargs.ow_palettes = owPalettesVar.get()
|
guiargs.ow_palettes = owPalettesVar.get()
|
||||||
guiargs.uw_palettes = uwPalettesVar.get()
|
guiargs.uw_palettes = uwPalettesVar.get()
|
||||||
|
guiargs.hud_palettes = hudPalettesVar.get()
|
||||||
|
guiargs.sword_palettes = swordPalettesVar.get()
|
||||||
|
guiargs.shield_palettes = shieldPalettesVar.get()
|
||||||
guiargs.shuffleganon = bool(shuffleGanonVar.get())
|
guiargs.shuffleganon = bool(shuffleGanonVar.get())
|
||||||
guiargs.hints = bool(hintsVar.get())
|
guiargs.hints = bool(hintsVar.get())
|
||||||
guiargs.enemizercli = enemizerCLIpathVar.get()
|
guiargs.enemizercli = enemizerCLIpathVar.get()
|
||||||
|
@ -614,16 +641,20 @@ def guiMain(args=None):
|
||||||
else:
|
else:
|
||||||
messagebox.showinfo(title="Success", message="Multiworld created successfully")
|
messagebox.showinfo(title="Success", message="Multiworld created successfully")
|
||||||
|
|
||||||
generateButton = Button(farBottomFrame, text='Generate Patched Rom', command=generateRom)
|
generateButton = Button(farBottomFrame, text='Generate Multiworld', command=generateRom)
|
||||||
|
|
||||||
|
worldLabel.grid(row=0, column=0, sticky=W)
|
||||||
|
worldSpinbox.grid(row=0, column=1, sticky=W)
|
||||||
|
namesLabel.grid(row=0, column=2, sticky=W)
|
||||||
|
namesEntry.grid(row=0, column=3, sticky=W + E)
|
||||||
|
multiworldframe.grid_columnconfigure(3, weight=1) # stretch name field
|
||||||
|
seedLabel.grid(row=0, column=4, sticky=W)
|
||||||
|
seedEntry.grid(row=0, column=5, sticky=W)
|
||||||
|
countLabel.grid(row=1, column=0, sticky=W)
|
||||||
|
countSpinbox.grid(row=1, column=1, sticky=W)
|
||||||
|
balancingCheckbutton.grid(row=1, column=2, sticky=W, columnspan=2)
|
||||||
|
patchesCheckbutton.grid(row=1, column=4, sticky=W, columnspan=2)
|
||||||
|
|
||||||
worldLabel.pack(side=LEFT)
|
|
||||||
worldSpinbox.pack(side=LEFT)
|
|
||||||
namesLabel.pack(side=LEFT)
|
|
||||||
namesEntry.pack(side=LEFT, expand=True, fill=X)
|
|
||||||
seedLabel.pack(side=LEFT, padx=(5, 0))
|
|
||||||
seedEntry.pack(side=LEFT)
|
|
||||||
countLabel.pack(side=LEFT, padx=(5, 0))
|
|
||||||
countSpinbox.pack(side=LEFT)
|
|
||||||
generateButton.pack(side=RIGHT, padx=(5, 0))
|
generateButton.pack(side=RIGHT, padx=(5, 0))
|
||||||
|
|
||||||
openOutputButton.pack(side=LEFT)
|
openOutputButton.pack(side=LEFT)
|
||||||
|
@ -702,22 +733,43 @@ def guiMain(args=None):
|
||||||
fastMenuLabel2.pack(side=LEFT)
|
fastMenuLabel2.pack(side=LEFT)
|
||||||
|
|
||||||
owPalettesFrame2 = Frame(drowDownFrame2)
|
owPalettesFrame2 = Frame(drowDownFrame2)
|
||||||
owPalettesOptionMenu2 = OptionMenu(owPalettesFrame2, owPalettesVar, 'default', 'random', 'blackout')
|
owPalettesOptionMenu2 = OptionMenu(owPalettesFrame2, owPalettesVar, 'default', 'random', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||||
owPalettesOptionMenu2.pack(side=RIGHT)
|
owPalettesOptionMenu2.pack(side=RIGHT)
|
||||||
owPalettesLabel2 = Label(owPalettesFrame2, text='Overworld palettes')
|
owPalettesLabel2 = Label(owPalettesFrame2, text='Overworld palettes')
|
||||||
owPalettesLabel2.pack(side=LEFT)
|
owPalettesLabel2.pack(side=LEFT)
|
||||||
|
|
||||||
uwPalettesFrame2 = Frame(drowDownFrame2)
|
uwPalettesFrame2 = Frame(drowDownFrame2)
|
||||||
uwPalettesOptionMenu2 = OptionMenu(uwPalettesFrame2, uwPalettesVar, 'default', 'random', 'blackout')
|
uwPalettesOptionMenu2 = OptionMenu(uwPalettesFrame2, uwPalettesVar, 'default', 'random', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||||
uwPalettesOptionMenu2.pack(side=RIGHT)
|
uwPalettesOptionMenu2.pack(side=RIGHT)
|
||||||
uwPalettesLabel2 = Label(uwPalettesFrame2, text='Dungeon palettes')
|
uwPalettesLabel2 = Label(uwPalettesFrame2, text='Dungeon palettes')
|
||||||
uwPalettesLabel2.pack(side=LEFT)
|
uwPalettesLabel2.pack(side=LEFT)
|
||||||
|
|
||||||
|
hudPalettesFrame2 = Frame(drowDownFrame2)
|
||||||
|
hudPalettesOptionMenu2 = OptionMenu(hudPalettesFrame2, hudPalettesVar, 'default', 'random', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||||
|
hudPalettesOptionMenu2.pack(side=RIGHT)
|
||||||
|
hudPalettesLabel2 = Label(hudPalettesFrame2, text='HUD palettes')
|
||||||
|
hudPalettesLabel2.pack(side=LEFT)
|
||||||
|
|
||||||
|
swordPalettesFrame2 = Frame(drowDownFrame2)
|
||||||
|
swordPalettesOptionMenu2 = OptionMenu(swordPalettesFrame2, swordPalettesVar, 'default', 'random', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||||
|
swordPalettesOptionMenu2.pack(side=RIGHT)
|
||||||
|
swordPalettesLabel2 = Label(swordPalettesFrame2, text='Sword palettes')
|
||||||
|
swordPalettesLabel2.pack(side=LEFT)
|
||||||
|
|
||||||
|
shieldPalettesFrame2 = Frame(drowDownFrame2)
|
||||||
|
shieldPalettesOptionMenu2 = OptionMenu(shieldPalettesFrame2, shieldPalettesVar, 'default', 'random', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||||
|
shieldPalettesOptionMenu2.pack(side=RIGHT)
|
||||||
|
shieldPalettesLabel2 = Label(shieldPalettesFrame2, text='Shield palettes')
|
||||||
|
shieldPalettesLabel2.pack(side=LEFT)
|
||||||
|
|
||||||
heartbeepFrame2.pack(expand=True, anchor=E)
|
heartbeepFrame2.pack(expand=True, anchor=E)
|
||||||
heartcolorFrame2.pack(expand=True, anchor=E)
|
heartcolorFrame2.pack(expand=True, anchor=E)
|
||||||
fastMenuFrame2.pack(expand=True, anchor=E)
|
fastMenuFrame2.pack(expand=True, anchor=E)
|
||||||
owPalettesFrame2.pack(expand=True, anchor=E)
|
owPalettesFrame2.pack(expand=True, anchor=E)
|
||||||
uwPalettesFrame2.pack(expand=True, anchor=E)
|
uwPalettesFrame2.pack(expand=True, anchor=E)
|
||||||
|
hudPalettesFrame2.pack(expand=True, anchor=E)
|
||||||
|
swordPalettesFrame2.pack(expand=True, anchor=E)
|
||||||
|
shieldPalettesFrame2.pack(expand=True, anchor=E)
|
||||||
|
|
||||||
bottomFrame2 = Frame(topFrame2)
|
bottomFrame2 = Frame(topFrame2)
|
||||||
|
|
||||||
|
@ -728,6 +780,9 @@ def guiMain(args=None):
|
||||||
guiargs.fastmenu = fastMenuVar.get()
|
guiargs.fastmenu = fastMenuVar.get()
|
||||||
guiargs.ow_palettes = owPalettesVar.get()
|
guiargs.ow_palettes = owPalettesVar.get()
|
||||||
guiargs.uw_palettes = uwPalettesVar.get()
|
guiargs.uw_palettes = uwPalettesVar.get()
|
||||||
|
guiargs.hud_palettes = hudPalettesVar.get()
|
||||||
|
guiargs.sword_palettes = swordPalettesVar.get()
|
||||||
|
guiargs.shield_palettes = shieldPalettesVar.get()
|
||||||
guiargs.quickswap = bool(quickSwapVar.get())
|
guiargs.quickswap = bool(quickSwapVar.get())
|
||||||
guiargs.disablemusic = bool(disableMusicVar.get())
|
guiargs.disablemusic = bool(disableMusicVar.get())
|
||||||
guiargs.rom = romVar2.get()
|
guiargs.rom = romVar2.get()
|
||||||
|
@ -741,7 +796,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)
|
||||||
|
|
||||||
|
@ -763,12 +821,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')
|
||||||
|
@ -1367,6 +1478,10 @@ def guiMain(args=None):
|
||||||
difficultyVar.set(args.difficulty)
|
difficultyVar.set(args.difficulty)
|
||||||
itemfunctionVar.set(args.item_functionality)
|
itemfunctionVar.set(args.item_functionality)
|
||||||
timerVar.set(args.timer)
|
timerVar.set(args.timer)
|
||||||
|
timerCountdownVar.set(args.countdown_start_time)
|
||||||
|
timerRedVar.set(args.red_clock_time)
|
||||||
|
timerBlueVar.set(args.blue_clock_time)
|
||||||
|
timerGreenVar.set(args.green_clock_time)
|
||||||
progressiveVar.set(args.progressive)
|
progressiveVar.set(args.progressive)
|
||||||
accessibilityVar.set(args.accessibility)
|
accessibilityVar.set(args.accessibility)
|
||||||
goalVar.set(args.goal)
|
goalVar.set(args.goal)
|
||||||
|
@ -1385,7 +1500,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()
|
||||||
|
@ -1411,8 +1527,8 @@ class SpriteSelector(object):
|
||||||
title_link.pack(side=LEFT)
|
title_link.pack(side=LEFT)
|
||||||
title_link.bind("<Button-1>", open_custom_sprite_dir)
|
title_link.bind("<Button-1>", open_custom_sprite_dir)
|
||||||
|
|
||||||
self.icon_section(alttpr_frametitle, self.alttpr_sprite_dir + '/*', 'ALTTPR sprites not found. Click "Update alttpr sprites" to download them.')
|
self.icon_section(alttpr_frametitle, self.alttpr_sprite_dir, 'ALTTPR sprites not found. Click "Update alttpr sprites" to download them.')
|
||||||
self.icon_section(custom_frametitle, self.custom_sprite_dir + '/*', 'Put sprites in the custom sprites folder (see open link above) to have them appear here.')
|
self.icon_section(custom_frametitle, self.custom_sprite_dir, 'Put sprites in the custom sprites folder (see open link above) to have them appear here.')
|
||||||
|
|
||||||
frame = Frame(self.window)
|
frame = Frame(self.window)
|
||||||
frame.pack(side=BOTTOM, fill=X, pady=5)
|
frame.pack(side=BOTTOM, fill=X, pady=5)
|
||||||
|
@ -1471,19 +1587,21 @@ class SpriteSelector(object):
|
||||||
|
|
||||||
sprites = []
|
sprites = []
|
||||||
|
|
||||||
for file in glob(output_path(path)):
|
for file in os.listdir(path):
|
||||||
sprites.append(Sprite(file))
|
sprites.append((file, Sprite(os.path.join(path, file))))
|
||||||
|
|
||||||
sprites.sort(key=lambda s: str.lower(s.name or "").strip())
|
sprites.sort(key=lambda s: str.lower(s[1].name or "").strip())
|
||||||
|
|
||||||
frame.buttons = []
|
frame.buttons = []
|
||||||
for sprite in sprites:
|
for file, sprite in sprites:
|
||||||
image = get_image_for_sprite(sprite)
|
image = get_image_for_sprite(sprite)
|
||||||
if image is None:
|
if image is None:
|
||||||
continue
|
continue
|
||||||
self.all_sprites.append(sprite)
|
self.all_sprites.append(sprite)
|
||||||
button = Button(frame, image=image, command=lambda spr=sprite: self.select_sprite(spr))
|
button = Button(frame, image=image, command=lambda spr=sprite: self.select_sprite(spr))
|
||||||
ToolTips.register(button, sprite.name + ("\nBy: %s" % sprite.author_name if sprite.author_name else ""))
|
ToolTips.register(button, sprite.name +
|
||||||
|
("\nBy: %s" % sprite.author_name if sprite.author_name else "") +
|
||||||
|
f"\nFrom: {file}")
|
||||||
button.image = image
|
button.image = image
|
||||||
frame.buttons.append(button)
|
frame.buttons.append(button)
|
||||||
|
|
||||||
|
@ -1508,93 +1626,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):
|
||||||
|
@ -1638,34 +1678,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
|
||||||
|
@ -1770,5 +1880,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):
|
||||||
|
|
5
LICENSE
|
@ -22,3 +22,8 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
||||||
|
Subdirectories of this repository may contain their own LICENSE file. Files within
|
||||||
|
subdirectories containing their own LICENSE file are exempt from the above license
|
||||||
|
and are subject to the license contained within the LICENSE file in their containing
|
||||||
|
directory.
|
||||||
|
|
161
MultiClient.py
|
@ -86,14 +86,17 @@ class Context():
|
||||||
self.slot = None
|
self.slot = None
|
||||||
self.player_names: typing.Dict[int: str] = {}
|
self.player_names: typing.Dict[int: str] = {}
|
||||||
self.locations_checked = set()
|
self.locations_checked = set()
|
||||||
|
self.unsafe_locations_checked = set()
|
||||||
self.locations_scouted = set()
|
self.locations_scouted = set()
|
||||||
self.items_received = []
|
self.items_received = []
|
||||||
|
self.items_missing = []
|
||||||
self.locations_info = {}
|
self.locations_info = {}
|
||||||
self.awaiting_rom = False
|
self.awaiting_rom = False
|
||||||
self.rom = None
|
self.rom = None
|
||||||
self.prev_rom = None
|
self.prev_rom = None
|
||||||
self.auth = None
|
self.auth = None
|
||||||
self.found_items = found_items
|
self.found_items = found_items
|
||||||
|
self.send_unsafe = False
|
||||||
self.finished_game = False
|
self.finished_game = False
|
||||||
self.slow_mode = False
|
self.slow_mode = False
|
||||||
|
|
||||||
|
@ -196,23 +199,34 @@ location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
|
||||||
'Desert Palace - Map Chest': (0x74, 0x10),
|
'Desert Palace - Map Chest': (0x74, 0x10),
|
||||||
'Desert Palace - Compass Chest': (0x85, 0x10),
|
'Desert Palace - Compass Chest': (0x85, 0x10),
|
||||||
'Desert Palace - Big Key Chest': (0x75, 0x10),
|
'Desert Palace - Big Key Chest': (0x75, 0x10),
|
||||||
|
'Desert Palace - Desert Tiles 1 Pot Key': (0x63, 0x400),
|
||||||
|
'Desert Palace - Beamos Hall Pot Key': (0x53, 0x400),
|
||||||
|
'Desert Palace - Desert Tiles 2 Pot Key': (0x43, 0x400),
|
||||||
'Desert Palace - Boss': (0x33, 0x800),
|
'Desert Palace - Boss': (0x33, 0x800),
|
||||||
'Eastern Palace - Compass Chest': (0xa8, 0x10),
|
'Eastern Palace - Compass Chest': (0xa8, 0x10),
|
||||||
'Eastern Palace - Big Chest': (0xa9, 0x10),
|
'Eastern Palace - Big Chest': (0xa9, 0x10),
|
||||||
|
'Eastern Palace - Dark Square Pot Key': (0xba, 0x400),
|
||||||
|
'Eastern Palace - Dark Eyegore Key Drop': (0x99, 0x400),
|
||||||
'Eastern Palace - Cannonball Chest': (0xb9, 0x10),
|
'Eastern Palace - Cannonball Chest': (0xb9, 0x10),
|
||||||
'Eastern Palace - Big Key Chest': (0xb8, 0x10),
|
'Eastern Palace - Big Key Chest': (0xb8, 0x10),
|
||||||
'Eastern Palace - Map Chest': (0xaa, 0x10),
|
'Eastern Palace - Map Chest': (0xaa, 0x10),
|
||||||
'Eastern Palace - Boss': (0xc8, 0x800),
|
'Eastern Palace - Boss': (0xc8, 0x800),
|
||||||
'Hyrule Castle - Boomerang Chest': (0x71, 0x10),
|
'Hyrule Castle - Boomerang Chest': (0x71, 0x10),
|
||||||
|
'Hyrule Castle - Boomerang Guard Key Drop': (0x71, 0x400),
|
||||||
'Hyrule Castle - Map Chest': (0x72, 0x10),
|
'Hyrule Castle - Map Chest': (0x72, 0x10),
|
||||||
|
'Hyrule Castle - Map Guard Key Drop': (0x72, 0x400),
|
||||||
"Hyrule Castle - Zelda's Chest": (0x80, 0x10),
|
"Hyrule Castle - Zelda's Chest": (0x80, 0x10),
|
||||||
|
'Hyrule Castle - Big Key Drop': (0x80, 0x400),
|
||||||
'Sewers - Dark Cross': (0x32, 0x10),
|
'Sewers - Dark Cross': (0x32, 0x10),
|
||||||
|
'Hyrule Castle - Key Rat Key Drop': (0x21, 0x400),
|
||||||
'Sewers - Secret Room - Left': (0x11, 0x10),
|
'Sewers - Secret Room - Left': (0x11, 0x10),
|
||||||
'Sewers - Secret Room - Middle': (0x11, 0x20),
|
'Sewers - Secret Room - Middle': (0x11, 0x20),
|
||||||
'Sewers - Secret Room - Right': (0x11, 0x40),
|
'Sewers - Secret Room - Right': (0x11, 0x40),
|
||||||
'Sanctuary': (0x12, 0x10),
|
'Sanctuary': (0x12, 0x10),
|
||||||
'Castle Tower - Room 03': (0xe0, 0x10),
|
'Castle Tower - Room 03': (0xe0, 0x10),
|
||||||
'Castle Tower - Dark Maze': (0xd0, 0x10),
|
'Castle Tower - Dark Maze': (0xd0, 0x10),
|
||||||
|
'Castle Tower - Dark Archer Key Drop': (0xc0, 0x400),
|
||||||
|
'Castle Tower - Circle of Pots Key Drop': (0xb0, 0x400),
|
||||||
'Spectacle Rock Cave': (0xea, 0x400),
|
'Spectacle Rock Cave': (0xea, 0x400),
|
||||||
'Paradox Cave Lower - Far Left': (0xef, 0x10),
|
'Paradox Cave Lower - Far Left': (0xef, 0x10),
|
||||||
'Paradox Cave Lower - Left': (0xef, 0x20),
|
'Paradox Cave Lower - Left': (0xef, 0x20),
|
||||||
|
@ -251,18 +265,25 @@ location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
|
||||||
'Mimic Cave': (0x10c, 0x10),
|
'Mimic Cave': (0x10c, 0x10),
|
||||||
'Swamp Palace - Entrance': (0x28, 0x10),
|
'Swamp Palace - Entrance': (0x28, 0x10),
|
||||||
'Swamp Palace - Map Chest': (0x37, 0x10),
|
'Swamp Palace - Map Chest': (0x37, 0x10),
|
||||||
|
'Swamp Palace - Pot Row Pot Key': (0x38, 0x400),
|
||||||
|
'Swamp Palace - Trench 1 Pot Key': (0x37, 0x400),
|
||||||
|
'Swamp Palace - Hookshot Pot Key': (0x36, 0x400),
|
||||||
'Swamp Palace - Big Chest': (0x36, 0x10),
|
'Swamp Palace - Big Chest': (0x36, 0x10),
|
||||||
'Swamp Palace - Compass Chest': (0x46, 0x10),
|
'Swamp Palace - Compass Chest': (0x46, 0x10),
|
||||||
|
'Swamp Palace - Trench 2 Pot Key': (0x35, 0x400),
|
||||||
'Swamp Palace - Big Key Chest': (0x35, 0x10),
|
'Swamp Palace - Big Key Chest': (0x35, 0x10),
|
||||||
'Swamp Palace - West Chest': (0x34, 0x10),
|
'Swamp Palace - West Chest': (0x34, 0x10),
|
||||||
'Swamp Palace - Flooded Room - Left': (0x76, 0x10),
|
'Swamp Palace - Flooded Room - Left': (0x76, 0x10),
|
||||||
'Swamp Palace - Flooded Room - Right': (0x76, 0x20),
|
'Swamp Palace - Flooded Room - Right': (0x76, 0x20),
|
||||||
'Swamp Palace - Waterfall Room': (0x66, 0x10),
|
'Swamp Palace - Waterfall Room': (0x66, 0x10),
|
||||||
|
'Swamp Palace - Waterway Pot Key': (0x16, 0x400),
|
||||||
'Swamp Palace - Boss': (0x6, 0x800),
|
'Swamp Palace - Boss': (0x6, 0x800),
|
||||||
"Thieves' Town - Big Key Chest": (0xdb, 0x20),
|
"Thieves' Town - Big Key Chest": (0xdb, 0x20),
|
||||||
"Thieves' Town - Map Chest": (0xdb, 0x10),
|
"Thieves' Town - Map Chest": (0xdb, 0x10),
|
||||||
"Thieves' Town - Compass Chest": (0xdc, 0x10),
|
"Thieves' Town - Compass Chest": (0xdc, 0x10),
|
||||||
"Thieves' Town - Ambush Chest": (0xcb, 0x10),
|
"Thieves' Town - Ambush Chest": (0xcb, 0x10),
|
||||||
|
"Thieves' Town - Hallway Pot Key": (0xbc, 0x400),
|
||||||
|
"Thieves' Town - Spike Switch Pot Key": (0xab, 0x400),
|
||||||
"Thieves' Town - Attic": (0x65, 0x10),
|
"Thieves' Town - Attic": (0x65, 0x10),
|
||||||
"Thieves' Town - Big Chest": (0x44, 0x10),
|
"Thieves' Town - Big Chest": (0x44, 0x10),
|
||||||
"Thieves' Town - Blind's Cell": (0x45, 0x10),
|
"Thieves' Town - Blind's Cell": (0x45, 0x10),
|
||||||
|
@ -273,28 +294,39 @@ location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
|
||||||
'Skull Woods - Pot Prison': (0x57, 0x20),
|
'Skull Woods - Pot Prison': (0x57, 0x20),
|
||||||
'Skull Woods - Pinball Room': (0x68, 0x10),
|
'Skull Woods - Pinball Room': (0x68, 0x10),
|
||||||
'Skull Woods - Big Key Chest': (0x57, 0x10),
|
'Skull Woods - Big Key Chest': (0x57, 0x10),
|
||||||
|
'Skull Woods - West Lobby Pot Key': (0x56, 0x400),
|
||||||
'Skull Woods - Bridge Room': (0x59, 0x10),
|
'Skull Woods - Bridge Room': (0x59, 0x10),
|
||||||
|
'Skull Woods - Spike Corner Key Drop': (0x39, 0x400),
|
||||||
'Skull Woods - Boss': (0x29, 0x800),
|
'Skull Woods - Boss': (0x29, 0x800),
|
||||||
|
'Ice Palace - Jelly Key Drop': (0x0e, 0x400),
|
||||||
'Ice Palace - Compass Chest': (0x2e, 0x10),
|
'Ice Palace - Compass Chest': (0x2e, 0x10),
|
||||||
|
'Ice Palace - Conveyor Key Drop': (0x3e, 0x400),
|
||||||
'Ice Palace - Freezor Chest': (0x7e, 0x10),
|
'Ice Palace - Freezor Chest': (0x7e, 0x10),
|
||||||
'Ice Palace - Big Chest': (0x9e, 0x10),
|
'Ice Palace - Big Chest': (0x9e, 0x10),
|
||||||
'Ice Palace - Iced T Room': (0xae, 0x10),
|
'Ice Palace - Iced T Room': (0xae, 0x10),
|
||||||
|
'Ice Palace - Many Pots Pot Key': (0x9f, 0x400),
|
||||||
'Ice Palace - Spike Room': (0x5f, 0x10),
|
'Ice Palace - Spike Room': (0x5f, 0x10),
|
||||||
'Ice Palace - Big Key Chest': (0x1f, 0x10),
|
'Ice Palace - Big Key Chest': (0x1f, 0x10),
|
||||||
|
'Ice Palace - Hammer Block Key Drop': (0x3f, 0x400),
|
||||||
'Ice Palace - Map Chest': (0x3f, 0x10),
|
'Ice Palace - Map Chest': (0x3f, 0x10),
|
||||||
'Ice Palace - Boss': (0xde, 0x800),
|
'Ice Palace - Boss': (0xde, 0x800),
|
||||||
'Misery Mire - Big Chest': (0xc3, 0x10),
|
'Misery Mire - Big Chest': (0xc3, 0x10),
|
||||||
'Misery Mire - Map Chest': (0xc3, 0x20),
|
'Misery Mire - Map Chest': (0xc3, 0x20),
|
||||||
'Misery Mire - Main Lobby': (0xc2, 0x10),
|
'Misery Mire - Main Lobby': (0xc2, 0x10),
|
||||||
'Misery Mire - Bridge Chest': (0xa2, 0x10),
|
'Misery Mire - Bridge Chest': (0xa2, 0x10),
|
||||||
|
'Misery Mire - Spikes Pot Key': (0xb3, 0x400),
|
||||||
'Misery Mire - Spike Chest': (0xb3, 0x10),
|
'Misery Mire - Spike Chest': (0xb3, 0x10),
|
||||||
|
'Misery Mire - Fishbone Pot Key': (0xa1, 0x400),
|
||||||
|
'Misery Mire - Conveyor Crystal Key Drop': (0xc1, 0x400),
|
||||||
'Misery Mire - Compass Chest': (0xc1, 0x10),
|
'Misery Mire - Compass Chest': (0xc1, 0x10),
|
||||||
'Misery Mire - Big Key Chest': (0xd1, 0x10),
|
'Misery Mire - Big Key Chest': (0xd1, 0x10),
|
||||||
'Misery Mire - Boss': (0x90, 0x800),
|
'Misery Mire - Boss': (0x90, 0x800),
|
||||||
'Turtle Rock - Compass Chest': (0xd6, 0x10),
|
'Turtle Rock - Compass Chest': (0xd6, 0x10),
|
||||||
'Turtle Rock - Roller Room - Left': (0xb7, 0x10),
|
'Turtle Rock - Roller Room - Left': (0xb7, 0x10),
|
||||||
'Turtle Rock - Roller Room - Right': (0xb7, 0x20),
|
'Turtle Rock - Roller Room - Right': (0xb7, 0x20),
|
||||||
|
'Turtle Rock - Pokey 1 Key Drop': (0xb6, 0x400),
|
||||||
'Turtle Rock - Chain Chomps': (0xb6, 0x10),
|
'Turtle Rock - Chain Chomps': (0xb6, 0x10),
|
||||||
|
'Turtle Rock - Pokey 2 Key Drop': (0x13, 0x400),
|
||||||
'Turtle Rock - Big Key Chest': (0x14, 0x10),
|
'Turtle Rock - Big Key Chest': (0x14, 0x10),
|
||||||
'Turtle Rock - Big Chest': (0x24, 0x10),
|
'Turtle Rock - Big Chest': (0x24, 0x10),
|
||||||
'Turtle Rock - Crystaroller Room': (0x4, 0x10),
|
'Turtle Rock - Crystaroller Room': (0x4, 0x10),
|
||||||
|
@ -317,6 +349,7 @@ location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
|
||||||
'Palace of Darkness - Big Chest': (0x1a, 0x10),
|
'Palace of Darkness - Big Chest': (0x1a, 0x10),
|
||||||
'Palace of Darkness - Harmless Hellway': (0x1a, 0x40),
|
'Palace of Darkness - Harmless Hellway': (0x1a, 0x40),
|
||||||
'Palace of Darkness - Boss': (0x5a, 0x800),
|
'Palace of Darkness - Boss': (0x5a, 0x800),
|
||||||
|
'Ganons Tower - Conveyor Cross Pot Key': (0x8b, 0x400),
|
||||||
"Ganons Tower - Bob's Torch": (0x8c, 0x400),
|
"Ganons Tower - Bob's Torch": (0x8c, 0x400),
|
||||||
'Ganons Tower - Hope Room - Left': (0x8c, 0x20),
|
'Ganons Tower - Hope Room - Left': (0x8c, 0x20),
|
||||||
'Ganons Tower - Hope Room - Right': (0x8c, 0x40),
|
'Ganons Tower - Hope Room - Right': (0x8c, 0x40),
|
||||||
|
@ -325,11 +358,13 @@ location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
|
||||||
'Ganons Tower - Compass Room - Top Right': (0x9d, 0x20),
|
'Ganons Tower - Compass Room - Top Right': (0x9d, 0x20),
|
||||||
'Ganons Tower - Compass Room - Bottom Left': (0x9d, 0x40),
|
'Ganons Tower - Compass Room - Bottom Left': (0x9d, 0x40),
|
||||||
'Ganons Tower - Compass Room - Bottom Right': (0x9d, 0x80),
|
'Ganons Tower - Compass Room - Bottom Right': (0x9d, 0x80),
|
||||||
|
'Ganons Tower - Conveyor Star Pits Pot Key': (0x7b, 0x400),
|
||||||
'Ganons Tower - DMs Room - Top Left': (0x7b, 0x10),
|
'Ganons Tower - DMs Room - Top Left': (0x7b, 0x10),
|
||||||
'Ganons Tower - DMs Room - Top Right': (0x7b, 0x20),
|
'Ganons Tower - DMs Room - Top Right': (0x7b, 0x20),
|
||||||
'Ganons Tower - DMs Room - Bottom Left': (0x7b, 0x40),
|
'Ganons Tower - DMs Room - Bottom Left': (0x7b, 0x40),
|
||||||
'Ganons Tower - DMs Room - Bottom Right': (0x7b, 0x80),
|
'Ganons Tower - DMs Room - Bottom Right': (0x7b, 0x80),
|
||||||
'Ganons Tower - Map Chest': (0x8b, 0x10),
|
'Ganons Tower - Map Chest': (0x8b, 0x10),
|
||||||
|
'Ganons Tower - Double Switch Pot Key': (0x9b, 0x400),
|
||||||
'Ganons Tower - Firesnake Room': (0x7d, 0x10),
|
'Ganons Tower - Firesnake Room': (0x7d, 0x10),
|
||||||
'Ganons Tower - Randomizer Room - Top Left': (0x7c, 0x10),
|
'Ganons Tower - Randomizer Room - Top Left': (0x7c, 0x10),
|
||||||
'Ganons Tower - Randomizer Room - Top Right': (0x7c, 0x20),
|
'Ganons Tower - Randomizer Room - Top Right': (0x7c, 0x20),
|
||||||
|
@ -342,6 +377,7 @@ location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
|
||||||
'Ganons Tower - Big Key Chest': (0x1c, 0x10),
|
'Ganons Tower - Big Key Chest': (0x1c, 0x10),
|
||||||
'Ganons Tower - Mini Helmasaur Room - Left': (0x3d, 0x10),
|
'Ganons Tower - Mini Helmasaur Room - Left': (0x3d, 0x10),
|
||||||
'Ganons Tower - Mini Helmasaur Room - Right': (0x3d, 0x20),
|
'Ganons Tower - Mini Helmasaur Room - Right': (0x3d, 0x20),
|
||||||
|
'Ganons Tower - Mini Helmasaur Key Drop': (0x3d, 0x400),
|
||||||
'Ganons Tower - Pre-Moldorm Chest': (0x3d, 0x40),
|
'Ganons Tower - Pre-Moldorm Chest': (0x3d, 0x40),
|
||||||
'Ganons Tower - Validation Chest': (0x4d, 0x10)}
|
'Ganons Tower - Validation Chest': (0x4d, 0x10)}
|
||||||
location_table_npc = {'Mushroom': 0x1000,
|
location_table_npc = {'Mushroom': 0x1000,
|
||||||
|
@ -487,8 +523,8 @@ 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'):
|
if 'sd2snes' in device.lower() or 'COM' in device:
|
||||||
ctx.ui_node.log_info("SD2SNES Detected")
|
ctx.ui_node.log_info("SD2SNES/FXPAK Detected")
|
||||||
ctx.is_sd2snes = True
|
ctx.is_sd2snes = True
|
||||||
await ctx.snes_socket.send(dumps({"Opcode" : "Info", "Space" : "SNES"}))
|
await ctx.snes_socket.send(dumps({"Opcode" : "Info", "Space" : "SNES"}))
|
||||||
reply = loads(await ctx.snes_socket.recv())
|
reply = loads(await ctx.snes_socket.recv())
|
||||||
|
@ -609,10 +645,7 @@ 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:
|
if ctx.is_sd2snes:
|
||||||
cmd = b'\x00\xE2\x20\x48\xEB\x48'
|
cmd = b'\x00\xE2\x20\x48\xEB\x48'
|
||||||
|
@ -634,8 +667,9 @@ async def snes_write(ctx : Context, write_list):
|
||||||
try:
|
try:
|
||||||
if ctx.snes_socket is not None:
|
if ctx.snes_socket is not None:
|
||||||
await ctx.snes_socket.send(dumps(PutAddress_Request))
|
await ctx.snes_socket.send(dumps(PutAddress_Request))
|
||||||
if ctx.snes_socket is not None:
|
|
||||||
await ctx.snes_socket.send(cmd)
|
await ctx.snes_socket.send(cmd)
|
||||||
|
else:
|
||||||
|
logging.warning(f"Could not send data to SNES: {cmd}")
|
||||||
except websockets.ConnectionClosed:
|
except websockets.ConnectionClosed:
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
|
@ -646,8 +680,9 @@ async def snes_write(ctx : Context, write_list):
|
||||||
PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]]
|
PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]]
|
||||||
if ctx.snes_socket is not None:
|
if ctx.snes_socket is not None:
|
||||||
await ctx.snes_socket.send(dumps(PutAddress_Request))
|
await ctx.snes_socket.send(dumps(PutAddress_Request))
|
||||||
if ctx.snes_socket is not None:
|
|
||||||
await ctx.snes_socket.send(data)
|
await ctx.snes_socket.send(data)
|
||||||
|
else:
|
||||||
|
logging.warning(f"Could not send data to SNES: {data}")
|
||||||
except websockets.ConnectionClosed:
|
except websockets.ConnectionClosed:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -657,7 +692,8 @@ async def snes_write(ctx : Context, write_list):
|
||||||
|
|
||||||
|
|
||||||
def snes_buffered_write(ctx : Context, address, data):
|
def snes_buffered_write(ctx : Context, address, data):
|
||||||
if len(ctx.snes_write_buffer) > 0 and (ctx.snes_write_buffer[-1][0] + len(ctx.snes_write_buffer[-1][1])) == address:
|
if ctx.snes_write_buffer and (ctx.snes_write_buffer[-1][0] + len(ctx.snes_write_buffer[-1][1])) == address:
|
||||||
|
# append to existing write command, bundling them
|
||||||
ctx.snes_write_buffer[-1] = (ctx.snes_write_buffer[-1][0], ctx.snes_write_buffer[-1][1] + data)
|
ctx.snes_write_buffer[-1] = (ctx.snes_write_buffer[-1][0], ctx.snes_write_buffer[-1][1] + data)
|
||||||
else:
|
else:
|
||||||
ctx.snes_write_buffer.append((address, data))
|
ctx.snes_write_buffer.append((address, data))
|
||||||
|
@ -667,8 +703,9 @@ async def snes_flush_writes(ctx : Context):
|
||||||
if not ctx.snes_write_buffer:
|
if not ctx.snes_write_buffer:
|
||||||
return
|
return
|
||||||
|
|
||||||
await snes_write(ctx, ctx.snes_write_buffer)
|
# swap buffers
|
||||||
ctx.snes_write_buffer = []
|
ctx.snes_write_buffer, writes = [], ctx.snes_write_buffer
|
||||||
|
await snes_write(ctx, writes)
|
||||||
|
|
||||||
|
|
||||||
async def send_msgs(websocket, msgs):
|
async def send_msgs(websocket, msgs):
|
||||||
|
@ -773,8 +810,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'])
|
||||||
|
@ -796,35 +833,47 @@ async def process_server_cmd(ctx: Context, cmd, args):
|
||||||
await server_auth(ctx, args['password'])
|
await server_auth(ctx, args['password'])
|
||||||
|
|
||||||
elif cmd == 'ConnectionRefused':
|
elif cmd == 'ConnectionRefused':
|
||||||
if 'InvalidPassword' in args:
|
|
||||||
ctx.ui_node.log_error('Invalid password')
|
|
||||||
ctx.password = None
|
|
||||||
await server_auth(ctx, True)
|
|
||||||
if 'InvalidRom' in args:
|
if 'InvalidRom' in args:
|
||||||
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
|
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
|
||||||
asyncio.create_task(ctx.snes_socket.close())
|
asyncio.create_task(ctx.snes_socket.close())
|
||||||
raise Exception(
|
raise Exception('Invalid ROM detected, '
|
||||||
'Invalid ROM detected, please verify that you have loaded the correct rom and reconnect your snes (/snes)')
|
'please verify that you have loaded the correct rom and reconnect your snes (/snes)')
|
||||||
if 'SlotAlreadyTaken' in args:
|
elif 'SlotAlreadyTaken' in args:
|
||||||
Utils.persistent_store("servers", ctx.rom, ctx.server_address)
|
Utils.persistent_store("servers", ctx.rom, ctx.server_address)
|
||||||
raise Exception('Player slot already in use for that team')
|
raise Exception('Player slot already in use for that team')
|
||||||
if 'IncompatibleVersion' in args:
|
elif 'IncompatibleVersion' in args:
|
||||||
raise Exception('Server reported your client version as incompatible')
|
raise Exception('Server reported your client version as incompatible')
|
||||||
raise Exception('Connection refused by the multiworld host')
|
#last to check, recoverable problem
|
||||||
|
elif 'InvalidPassword' in args:
|
||||||
|
ctx.ui_node.log_error('Invalid password')
|
||||||
|
ctx.password = None
|
||||||
|
await server_auth(ctx, True)
|
||||||
|
else:
|
||||||
|
raise Exception("Unknown connection errors: "+str(args))
|
||||||
|
raise Exception('Connection refused by the multiworld host, no reason provided')
|
||||||
|
|
||||||
elif cmd == 'Connected':
|
elif cmd == 'Connected':
|
||||||
|
if ctx.send_unsafe:
|
||||||
|
ctx.send_unsafe = False
|
||||||
|
ctx.ui_node.log_info(f'Turning off sending of ALL location checks not declared as missing. If you want it on, please use /send_unsafe true')
|
||||||
Utils.persistent_store("servers", ctx.rom, ctx.server_address)
|
Utils.persistent_store("servers", ctx.rom, ctx.server_address)
|
||||||
ctx.team, ctx.slot = args[0]
|
ctx.team, ctx.slot = args[0]
|
||||||
ctx.player_names = {p: n for p, n in args[1]}
|
ctx.player_names = {p: n for p, n in args[1]}
|
||||||
msgs = []
|
msgs = []
|
||||||
if ctx.locations_checked:
|
if ctx.locations_checked:
|
||||||
msgs.append(['LocationChecks', [Regions.location_table[loc][0] for loc in ctx.locations_checked]])
|
msgs.append(['LocationChecks', [Regions.lookup_name_to_id[loc] for loc in ctx.locations_checked]])
|
||||||
if ctx.locations_scouted:
|
if ctx.locations_scouted:
|
||||||
msgs.append(['LocationScouts', list(ctx.locations_scouted)])
|
msgs.append(['LocationScouts', list(ctx.locations_scouted)])
|
||||||
if msgs:
|
if msgs:
|
||||||
await ctx.send_msgs(msgs)
|
await ctx.send_msgs(msgs)
|
||||||
if ctx.finished_game:
|
if ctx.finished_game:
|
||||||
await send_finished_game(ctx)
|
await send_finished_game(ctx)
|
||||||
|
ctx.items_missing = args[2] if len(args) >= 3 else [] # Get the server side view of missing as of time of connecting.
|
||||||
|
# This list is used to only send to the server what is reported as ACTUALLY Missing.
|
||||||
|
# This also serves to allow an easy visual of what locations were already checked previously
|
||||||
|
# when /missing is used for the client side view of what is missing.
|
||||||
|
if not ctx.items_missing:
|
||||||
|
asyncio.create_task(ctx.send_msgs([['Say', '!missing']]))
|
||||||
|
|
||||||
elif cmd == 'ReceivedItems':
|
elif cmd == 'ReceivedItems':
|
||||||
start_index, items = args
|
start_index, items = args
|
||||||
|
@ -833,7 +882,7 @@ async def process_server_cmd(ctx: Context, cmd, args):
|
||||||
elif start_index != len(ctx.items_received):
|
elif start_index != len(ctx.items_received):
|
||||||
sync_msg = [['Sync']]
|
sync_msg = [['Sync']]
|
||||||
if ctx.locations_checked:
|
if ctx.locations_checked:
|
||||||
sync_msg.append(['LocationChecks', [Regions.location_table[loc][0] for loc in ctx.locations_checked]])
|
sync_msg.append(['LocationChecks', [Regions.lookup_name_to_id[loc] for loc in ctx.locations_checked]])
|
||||||
await ctx.send_msgs(sync_msg)
|
await ctx.send_msgs(sync_msg)
|
||||||
if start_index == len(ctx.items_received):
|
if start_index == len(ctx.items_received):
|
||||||
for item in items:
|
for item in items:
|
||||||
|
@ -999,13 +1048,34 @@ class ClientCommandProcessor(CommandProcessor):
|
||||||
def _cmd_missing(self) -> bool:
|
def _cmd_missing(self) -> bool:
|
||||||
"""List all missing location checks, from your local game state"""
|
"""List all missing location checks, from your local game state"""
|
||||||
count = 0
|
count = 0
|
||||||
|
checked_count = 0
|
||||||
for location in [k for k, v in Regions.location_table.items() if type(v[0]) is int]:
|
for location in [k for k, v in Regions.location_table.items() if type(v[0]) is int]:
|
||||||
if location not in self.ctx.locations_checked:
|
if location not in self.ctx.locations_checked:
|
||||||
self.output('Missing: ' + location)
|
if location not in self.ctx.items_missing:
|
||||||
|
self.output('Checked: ' + location)
|
||||||
|
checked_count += 1
|
||||||
|
else:
|
||||||
|
self.output('Missing: ' + location)
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
|
key_drop_count = 0
|
||||||
|
for location in [k for k, v in Regions.key_drop_data.items()]:
|
||||||
|
if location not in self.ctx.items_missing:
|
||||||
|
key_drop_count += 1
|
||||||
|
|
||||||
|
# No point on reporting on missing key drop locations if the server doesn't declare ANY of them missing.
|
||||||
|
if key_drop_count != len(Regions.key_drop_data.items()):
|
||||||
|
for location in [k for k, v in Regions.key_drop_data.items()]:
|
||||||
|
if location not in self.ctx.locations_checked:
|
||||||
|
if location not in self.ctx.items_missing:
|
||||||
|
self.output('Checked: ' + location)
|
||||||
|
key_drop_count += 1
|
||||||
|
else:
|
||||||
|
self.output('Missing: ' + location)
|
||||||
|
count += 1
|
||||||
|
|
||||||
if count:
|
if count:
|
||||||
self.output(f"Found {count} missing location checks")
|
self.output(f"Found {count} missing location checks{f'. {checked_count} locations checks previously visited.' if checked_count else ''}")
|
||||||
else:
|
else:
|
||||||
self.output("No missing location checks found.")
|
self.output("No missing location checks found.")
|
||||||
return True
|
return True
|
||||||
|
@ -1035,6 +1105,15 @@ class ClientCommandProcessor(CommandProcessor):
|
||||||
else:
|
else:
|
||||||
self.output("Web UI was never started.")
|
self.output("Web UI was never started.")
|
||||||
|
|
||||||
|
def _cmd_send_unsafe(self, toggle: str = ""):
|
||||||
|
"""Force sending of locations the server did not specify was actually missing. WARNING: This may brick online trackers. Turned off on reconnect."""
|
||||||
|
if toggle:
|
||||||
|
self.ctx.send_unsafe = toggle.lower() in {"1", "true", "on"}
|
||||||
|
self.ctx.ui_node.log_info(f'Turning {("on" if self.ctx.send_unsafe else "off")} the option to send ALL location checks to the multiserver.')
|
||||||
|
else:
|
||||||
|
self.ctx.ui_node.log_info("You must specify /send_unsafe true explicitly.")
|
||||||
|
self.ctx.send_unsafe = False
|
||||||
|
|
||||||
def default(self, raw: str):
|
def default(self, raw: str):
|
||||||
asyncio.create_task(self.ctx.send_msgs([['Say', raw]]))
|
asyncio.create_task(self.ctx.send_msgs([['Say', raw]]))
|
||||||
|
|
||||||
|
@ -1064,20 +1143,22 @@ async def track_locations(ctx : Context, roomid, roomdata):
|
||||||
new_locations = []
|
new_locations = []
|
||||||
|
|
||||||
def new_check(location):
|
def new_check(location):
|
||||||
ctx.locations_checked.add(location)
|
ctx.unsafe_locations_checked.add(location)
|
||||||
ctx.ui_node.log_info("New check: %s (%d/216)" % (location, len(ctx.locations_checked)))
|
ctx.ui_node.log_info("New check: %s (%d/216)" % (location, len(ctx.unsafe_locations_checked)))
|
||||||
ctx.ui_node.send_location_check(ctx, location)
|
ctx.ui_node.send_location_check(ctx, location)
|
||||||
new_locations.append(Regions.location_table[location][0])
|
|
||||||
|
|
||||||
for location, (loc_roomid, loc_mask) in location_table_uw.items():
|
for location, (loc_roomid, loc_mask) in location_table_uw.items():
|
||||||
if location not in ctx.locations_checked and loc_roomid == roomid and (roomdata << 4) & loc_mask != 0:
|
try:
|
||||||
new_check(location)
|
if location not in ctx.unsafe_locations_checked and loc_roomid == roomid and (roomdata << 4) & loc_mask != 0:
|
||||||
|
new_check(location)
|
||||||
|
except Exception as e:
|
||||||
|
ctx.ui_node.log_info(f"Exception: {e}")
|
||||||
|
|
||||||
uw_begin = 0x129
|
uw_begin = 0x129
|
||||||
uw_end = 0
|
uw_end = 0
|
||||||
uw_unchecked = {}
|
uw_unchecked = {}
|
||||||
for location, (roomid, mask) in location_table_uw.items():
|
for location, (roomid, mask) in location_table_uw.items():
|
||||||
if location not in ctx.locations_checked:
|
if location not in ctx.unsafe_locations_checked:
|
||||||
uw_unchecked[location] = (roomid, mask)
|
uw_unchecked[location] = (roomid, mask)
|
||||||
uw_begin = min(uw_begin, roomid)
|
uw_begin = min(uw_begin, roomid)
|
||||||
uw_end = max(uw_end, roomid + 1)
|
uw_end = max(uw_end, roomid + 1)
|
||||||
|
@ -1094,7 +1175,7 @@ async def track_locations(ctx : Context, roomid, roomdata):
|
||||||
ow_end = 0
|
ow_end = 0
|
||||||
ow_unchecked = {}
|
ow_unchecked = {}
|
||||||
for location, screenid in location_table_ow.items():
|
for location, screenid in location_table_ow.items():
|
||||||
if location not in ctx.locations_checked:
|
if location not in ctx.unsafe_locations_checked:
|
||||||
ow_unchecked[location] = screenid
|
ow_unchecked[location] = screenid
|
||||||
ow_begin = min(ow_begin, screenid)
|
ow_begin = min(ow_begin, screenid)
|
||||||
ow_end = max(ow_end, screenid + 1)
|
ow_end = max(ow_end, screenid + 1)
|
||||||
|
@ -1105,22 +1186,27 @@ async def track_locations(ctx : Context, roomid, roomdata):
|
||||||
if ow_data[screenid - ow_begin] & 0x40 != 0:
|
if ow_data[screenid - ow_begin] & 0x40 != 0:
|
||||||
new_check(location)
|
new_check(location)
|
||||||
|
|
||||||
if not all([location in ctx.locations_checked for location in location_table_npc.keys()]):
|
if not all([location in ctx.unsafe_locations_checked for location in location_table_npc.keys()]):
|
||||||
npc_data = await snes_read(ctx, SAVEDATA_START + 0x410, 2)
|
npc_data = await snes_read(ctx, SAVEDATA_START + 0x410, 2)
|
||||||
if npc_data is not None:
|
if npc_data is not None:
|
||||||
npc_value = npc_data[0] | (npc_data[1] << 8)
|
npc_value = npc_data[0] | (npc_data[1] << 8)
|
||||||
for location, mask in location_table_npc.items():
|
for location, mask in location_table_npc.items():
|
||||||
if npc_value & mask != 0 and location not in ctx.locations_checked:
|
if npc_value & mask != 0 and location not in ctx.unsafe_locations_checked:
|
||||||
new_check(location)
|
new_check(location)
|
||||||
|
|
||||||
if not all([location in ctx.locations_checked for location in location_table_misc.keys()]):
|
if not all([location in ctx.unsafe_locations_checked for location in location_table_misc.keys()]):
|
||||||
misc_data = await snes_read(ctx, SAVEDATA_START + 0x3c6, 4)
|
misc_data = await snes_read(ctx, SAVEDATA_START + 0x3c6, 4)
|
||||||
if misc_data is not None:
|
if misc_data is not None:
|
||||||
for location, (offset, mask) in location_table_misc.items():
|
for location, (offset, mask) in location_table_misc.items():
|
||||||
assert(0x3c6 <= offset <= 0x3c9)
|
assert(0x3c6 <= offset <= 0x3c9)
|
||||||
if misc_data[offset - 0x3c6] & mask != 0 and location not in ctx.locations_checked:
|
if misc_data[offset - 0x3c6] & mask != 0 and location not in ctx.unsafe_locations_checked:
|
||||||
new_check(location)
|
new_check(location)
|
||||||
|
|
||||||
|
for location in ctx.unsafe_locations_checked:
|
||||||
|
if (location in ctx.items_missing and location not in ctx.locations_checked) or ctx.send_unsafe:
|
||||||
|
ctx.locations_checked.add(location)
|
||||||
|
new_locations.append(Regions.lookup_name_to_id[location])
|
||||||
|
|
||||||
await ctx.send_msgs([['LocationChecks', new_locations]])
|
await ctx.send_msgs([['LocationChecks', new_locations]])
|
||||||
|
|
||||||
|
|
||||||
|
@ -1151,6 +1237,7 @@ async def game_watcher(ctx : Context):
|
||||||
ctx.rom = rom
|
ctx.rom = rom
|
||||||
if not ctx.prev_rom or ctx.prev_rom != ctx.rom:
|
if not ctx.prev_rom or ctx.prev_rom != ctx.rom:
|
||||||
ctx.locations_checked = set()
|
ctx.locations_checked = set()
|
||||||
|
ctx.unsafe_locations_checked = set()
|
||||||
ctx.locations_scouted = set()
|
ctx.locations_scouted = set()
|
||||||
ctx.prev_rom = ctx.rom
|
ctx.prev_rom = ctx.rom
|
||||||
|
|
||||||
|
|
|
@ -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,56 +48,64 @@ 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"]
|
||||||
|
plando_options = multi_mystery_options["plando_options"]
|
||||||
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"]
|
||||||
zip_diffs = multi_mystery_options["zip_diffs"]
|
zip_diffs = multi_mystery_options["zip_diffs"]
|
||||||
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} --plando \"{plando_options}\""
|
||||||
|
|
||||||
if create_spoiler:
|
if create_spoiler:
|
||||||
command += " --create_spoiler"
|
command += " --create_spoiler"
|
||||||
|
@ -107,13 +117,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 +148,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 +163,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"AP_{seedname}.{typical_zip_ending}")
|
zipname = os.path.join(output_path, f"AP_{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 +222,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")
|
||||||
|
|
|
@ -30,7 +30,7 @@ from Utils import get_item_name_from_id, get_location_name_from_address, \
|
||||||
ReceivedItem, _version_tuple, restricted_loads
|
ReceivedItem, _version_tuple, restricted_loads
|
||||||
from NetUtils import Node, Endpoint
|
from NetUtils import Node, Endpoint
|
||||||
|
|
||||||
console_names = frozenset(set(Items.item_table) | set(Regions.location_table) | set(Items.item_name_groups))
|
console_names = frozenset(set(Items.item_table) | set(Regions.location_table) | set(Items.item_name_groups) | set(Regions.key_drop_data))
|
||||||
|
|
||||||
CLIENT_PLAYING = 0
|
CLIENT_PLAYING = 0
|
||||||
CLIENT_GOAL = 1
|
CLIENT_GOAL = 1
|
||||||
|
@ -59,6 +59,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):
|
||||||
|
@ -104,6 +113,7 @@ class Context(Node):
|
||||||
self.auto_saver_thread = None
|
self.auto_saver_thread = None
|
||||||
self.save_dirty = False
|
self.save_dirty = False
|
||||||
self.tags = ['AP']
|
self.tags = ['AP']
|
||||||
|
self.minimum_client_versions: typing.Dict[typing.Tuple[int, int], Utils.Version] = {}
|
||||||
|
|
||||||
def load(self, multidatapath: str, use_embedded_server_options: bool = False):
|
def load(self, multidatapath: str, use_embedded_server_options: bool = False):
|
||||||
with open(multidatapath, 'rb') as f:
|
with open(multidatapath, 'rb') as f:
|
||||||
|
@ -113,6 +123,16 @@ class Context(Node):
|
||||||
self.data_filename = multidatapath
|
self.data_filename = multidatapath
|
||||||
|
|
||||||
def _load(self, decoded_obj: dict, use_embedded_server_options: bool):
|
def _load(self, decoded_obj: dict, use_embedded_server_options: bool):
|
||||||
|
if "minimum_versions" in jsonobj:
|
||||||
|
mdata_ver = tuple(jsonobj["minimum_versions"]["server"])
|
||||||
|
if mdata_ver > Utils._version_tuple:
|
||||||
|
raise RuntimeError(f"Supplied Multidata requires a server of at least version {mdata_ver},"
|
||||||
|
f"however this server is of version {Utils._version_tuple}")
|
||||||
|
clients_ver = jsonobj["minimum_versions"].get("clients", [])
|
||||||
|
self.minimum_client_versions = {}
|
||||||
|
for team, player, version in clients_ver:
|
||||||
|
self.minimum_client_versions[team, player] = Utils.Version(*version)
|
||||||
|
|
||||||
for team, names in enumerate(decoded_obj['names']):
|
for team, names in enumerate(decoded_obj['names']):
|
||||||
for player, name in enumerate(names, 1):
|
for player, name in enumerate(names, 1):
|
||||||
self.player_names[(team, player)] = name
|
self.player_names[(team, player)] = name
|
||||||
|
@ -127,15 +147,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:
|
||||||
|
@ -435,6 +463,7 @@ def send_new_items(ctx: Context):
|
||||||
|
|
||||||
def forfeit_player(ctx: Context, team: int, slot: int):
|
def forfeit_player(ctx: Context, team: int, slot: int):
|
||||||
all_locations = {values[0] for values in Regions.location_table.values() if type(values[0]) is int}
|
all_locations = {values[0] for values in Regions.location_table.values() if type(values[0]) is int}
|
||||||
|
all_locations.update({values[1] for values in Regions.key_drop_data.values()})
|
||||||
ctx.notify_all("%s (Team #%d) has forfeited" % (ctx.player_names[(team, slot)], team + 1))
|
ctx.notify_all("%s (Team #%d) has forfeited" % (ctx.player_names[(team, slot)], team + 1))
|
||||||
register_location_checks(ctx, team, slot, all_locations)
|
register_location_checks(ctx, team, slot, all_locations)
|
||||||
|
|
||||||
|
@ -450,10 +479,12 @@ def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]:
|
||||||
def register_location_checks(ctx: Context, team: int, slot: int, locations):
|
def register_location_checks(ctx: Context, team: int, slot: int, locations):
|
||||||
found_items = False
|
found_items = False
|
||||||
new_locations = set(locations) - ctx.location_checks[team, slot]
|
new_locations = set(locations) - ctx.location_checks[team, slot]
|
||||||
|
known_locations = set()
|
||||||
if new_locations:
|
if new_locations:
|
||||||
ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc)
|
ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc)
|
||||||
for location in new_locations:
|
for location in new_locations:
|
||||||
if (location, slot) in ctx.locations:
|
if (location, slot) in ctx.locations:
|
||||||
|
known_locations.add(location)
|
||||||
target_item, target_player = ctx.locations[(location, slot)]
|
target_item, target_player = ctx.locations[(location, slot)]
|
||||||
if target_player != slot or slot in ctx.remote_items:
|
if target_player != slot or slot in ctx.remote_items:
|
||||||
found = False
|
found = False
|
||||||
|
@ -478,7 +509,7 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations):
|
||||||
if client.team == team and client.wants_item_notification:
|
if client.team == team and client.wants_item_notification:
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
ctx.send_msgs(client, [['ItemFound', (target_item, location, slot)]]))
|
ctx.send_msgs(client, [['ItemFound', (target_item, location, slot)]]))
|
||||||
ctx.location_checks[team, slot] |= new_locations
|
ctx.location_checks[team, slot] |= known_locations
|
||||||
send_new_items(ctx)
|
send_new_items(ctx)
|
||||||
|
|
||||||
if found_items:
|
if found_items:
|
||||||
|
@ -510,7 +541,7 @@ def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[
|
||||||
|
|
||||||
def collect_hints_location(ctx: Context, team: int, slot: int, location: str) -> typing.List[Utils.Hint]:
|
def collect_hints_location(ctx: Context, team: int, slot: int, location: str) -> typing.List[Utils.Hint]:
|
||||||
hints = []
|
hints = []
|
||||||
seeked_location = Regions.location_table[location][0]
|
seeked_location = Regions.lookup_name_to_id[location]
|
||||||
for check, result in ctx.locations.items():
|
for check, result in ctx.locations.items():
|
||||||
location_id, finding_player = check
|
location_id, finding_player = check
|
||||||
if finding_player == slot and location_id == seeked_location:
|
if finding_player == slot and location_id == seeked_location:
|
||||||
|
@ -644,15 +675,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:
|
||||||
|
@ -665,7 +687,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:
|
||||||
|
@ -802,10 +824,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||||
def _cmd_missing(self) -> bool:
|
def _cmd_missing(self) -> bool:
|
||||||
"""List all missing location checks from the server's perspective"""
|
"""List all missing location checks from the server's perspective"""
|
||||||
|
|
||||||
locations = []
|
locations = get_missing_checks(self.ctx, self.client)
|
||||||
for location_id, location_name in Regions.lookup_id_to_name.items(): # cheat console is -1, keep in mind
|
|
||||||
if location_id != -1 and location_id not in self.ctx.location_checks[self.client.team, self.client.slot]:
|
|
||||||
locations.append(location_name)
|
|
||||||
|
|
||||||
if len(locations) > 0:
|
if len(locations) > 0:
|
||||||
texts = [f'Missing: {location}\n' for location in locations]
|
texts = [f'Missing: {location}\n' for location in locations]
|
||||||
|
@ -930,6 +949,15 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||||
self.output(response)
|
self.output(response)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def get_missing_checks(ctx: Context, client: Client) -> list:
|
||||||
|
locations = []
|
||||||
|
#for location_id in [k[0] for k, v in ctx.locations if k[1] == client.slot]:
|
||||||
|
# if location_id not in ctx.location_checks[client.team, client.slot]:
|
||||||
|
# locations.append(Regions.lookup_id_to_name.get(location_id, f'Unknown Location ID: {location_id}'))
|
||||||
|
for location_id, location_name in Regions.lookup_id_to_name.items(): # cheat console is -1, keep in mind
|
||||||
|
if location_id != -1 and location_id not in ctx.location_checks[client.team, client.slot] and (location_id, client.slot) in ctx.locations:
|
||||||
|
locations.append(location_name)
|
||||||
|
return locations
|
||||||
|
|
||||||
def get_client_points(ctx: Context, client: Client) -> int:
|
def get_client_points(ctx: Context, client: Client) -> int:
|
||||||
return (ctx.location_check_points * len(ctx.location_checks[client.team, client.slot]) -
|
return (ctx.location_check_points * len(ctx.location_checks[client.team, client.slot]) -
|
||||||
|
@ -972,9 +1000,14 @@ async def process_client_cmd(ctx: Context, client: Client, cmd, args):
|
||||||
client.name = ctx.player_names[(team, slot)]
|
client.name = ctx.player_names[(team, slot)]
|
||||||
client.team = team
|
client.team = team
|
||||||
client.slot = slot
|
client.slot = slot
|
||||||
|
minver = Utils.Version(*(ctx.minimum_client_versions.get((team, slot), (0,0,0))))
|
||||||
|
if minver > tuple(args.get('version', Client.version)):
|
||||||
|
errors.add('IncompatibleVersion')
|
||||||
|
|
||||||
if ctx.compatibility == 1 and "AP" not in args.get('tags', Client.tags):
|
if ctx.compatibility == 1 and "AP" not in args.get('tags', Client.tags):
|
||||||
errors.add('IncompatibleVersion')
|
errors.add('IncompatibleVersion')
|
||||||
elif ctx.compatibility == 0 and args.get('version', Client.version) != list(_version_tuple):
|
#only exact version match allowed
|
||||||
|
elif ctx.compatibility == 0 and tuple(args.get('version', Client.version)) != _version_tuple:
|
||||||
errors.add('IncompatibleVersion')
|
errors.add('IncompatibleVersion')
|
||||||
if errors:
|
if errors:
|
||||||
logging.info(f"A client connection was refused due to: {errors}")
|
logging.info(f"A client connection was refused due to: {errors}")
|
||||||
|
@ -986,7 +1019,7 @@ async def process_client_cmd(ctx: Context, client: Client, cmd, args):
|
||||||
client.tags = args.get('tags', Client.tags)
|
client.tags = args.get('tags', Client.tags)
|
||||||
reply = [['Connected', [(client.team, client.slot),
|
reply = [['Connected', [(client.team, client.slot),
|
||||||
[(p, ctx.get_aliased_name(t, p)) for (t, p), n in ctx.player_names.items() if
|
[(p, ctx.get_aliased_name(t, p)) for (t, p), n in ctx.player_names.items() if
|
||||||
t == client.team]]]]
|
t == client.team], get_missing_checks(ctx, client)]]]
|
||||||
items = get_received_items(ctx, client.team, client.slot)
|
items = get_received_items(ctx, client.team, client.slot)
|
||||||
if items:
|
if items:
|
||||||
reply.append(['ReceivedItems', (0, tuplize_received_items(items))])
|
reply.append(['ReceivedItems', (0, tuplize_received_items(items))])
|
||||||
|
@ -1215,7 +1248,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):
|
||||||
|
@ -1229,7 +1262,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
|
||||||
|
|
140
Mystery.py
|
@ -7,11 +7,13 @@ import typing
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import ModuleUpdate
|
import ModuleUpdate
|
||||||
|
from BaseClasses import PlandoItem, PlandoConnection
|
||||||
|
|
||||||
ModuleUpdate.update()
|
ModuleUpdate.update()
|
||||||
|
|
||||||
|
import Bosses
|
||||||
from Utils import parse_yaml
|
from Utils import parse_yaml
|
||||||
from worlds.alttp.Rom import get_sprite_from_name
|
from worlds.alttp.Rom import Sprite
|
||||||
from worlds.alttp.EntranceRandomizer import parse_arguments
|
from worlds.alttp.EntranceRandomizer import parse_arguments
|
||||||
from worlds.alttp.Main import main as ERmain
|
from worlds.alttp.Main import main as ERmain
|
||||||
from worlds.alttp.Main import get_seed, seeddigits
|
from worlds.alttp.Main import get_seed, seeddigits
|
||||||
|
@ -43,10 +45,13 @@ def mystery_argparse():
|
||||||
parser.add_argument('--create_diff', action="store_true")
|
parser.add_argument('--create_diff', action="store_true")
|
||||||
parser.add_argument('--yaml_output', default=0, type=lambda value: min(max(int(value), 0), 255),
|
parser.add_argument('--yaml_output', default=0, type=lambda value: min(max(int(value), 0), 255),
|
||||||
help='Output rolled mystery results to yaml up to specified number (made for async multiworld)')
|
help='Output rolled mystery results to yaml up to specified number (made for async multiworld)')
|
||||||
|
parser.add_argument('--plando', default="bosses",
|
||||||
|
help='List of options that can be set manually. Can be combined, for example "bosses, items"')
|
||||||
|
|
||||||
for player in range(1, multiargs.multi + 1):
|
for player in range(1, multiargs.multi + 1):
|
||||||
parser.add_argument(f'--p{player}', help=argparse.SUPPRESS)
|
parser.add_argument(f'--p{player}', help=argparse.SUPPRESS)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
args.plando: typing.Set[str] = {arg.strip().lower() for arg in args.plando.split(",")}
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
@ -57,12 +62,12 @@ def main(args=None, callback=ERmain):
|
||||||
seed = get_seed(args.seed)
|
seed = get_seed(args.seed)
|
||||||
random.seed(seed)
|
random.seed(seed)
|
||||||
|
|
||||||
if args.race:
|
|
||||||
random.seed() # reset to time-based random source
|
|
||||||
|
|
||||||
seedname = "M" + (f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits))
|
seedname = "M" + (f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits))
|
||||||
print(f"Generating mystery for {args.multi} player{'s' if args.multi > 1 else ''}, {seedname} Seed {seed}")
|
print(f"Generating mystery for {args.multi} player{'s' if args.multi > 1 else ''}, {seedname} Seed {seed}")
|
||||||
|
|
||||||
|
if args.race:
|
||||||
|
random.seed() # reset to time-based random source
|
||||||
|
|
||||||
weights_cache = {}
|
weights_cache = {}
|
||||||
if args.weights:
|
if args.weights:
|
||||||
try:
|
try:
|
||||||
|
@ -143,7 +148,8 @@ def main(args=None, callback=ERmain):
|
||||||
if args.enemizercli:
|
if args.enemizercli:
|
||||||
erargs.enemizercli = args.enemizercli
|
erargs.enemizercli = args.enemizercli
|
||||||
|
|
||||||
settings_cache = {k: (roll_settings(v) if args.samesettings else None) for k, v in weights_cache.items()}
|
settings_cache = {k: (roll_settings(v, args.plando) if args.samesettings else None)
|
||||||
|
for k, v in weights_cache.items()}
|
||||||
player_path_cache = {}
|
player_path_cache = {}
|
||||||
for player in range(1, args.multi + 1):
|
for player in range(1, args.multi + 1):
|
||||||
player_path_cache[player] = getattr(args, f'p{player}') if getattr(args, f'p{player}') else args.weights
|
player_path_cache[player] = getattr(args, f'p{player}') if getattr(args, f'p{player}') else args.weights
|
||||||
|
@ -166,8 +172,9 @@ def main(args=None, callback=ERmain):
|
||||||
path = player_path_cache[player]
|
path = player_path_cache[player]
|
||||||
if path:
|
if path:
|
||||||
try:
|
try:
|
||||||
settings = settings_cache[path] if settings_cache[path] else roll_settings(weights_cache[path])
|
settings = settings_cache[path] if settings_cache[path] else \
|
||||||
if settings.sprite and not os.path.isfile(settings.sprite) and not get_sprite_from_name(
|
roll_settings(weights_cache[path], args.plando)
|
||||||
|
if settings.sprite and not os.path.isfile(settings.sprite) and not Sprite.get_sprite_from_name(
|
||||||
settings.sprite):
|
settings.sprite):
|
||||||
logging.warning(
|
logging.warning(
|
||||||
f"Warning: The chosen sprite, \"{settings.sprite}\", for yaml \"{path}\", does not exist.")
|
f"Warning: The chosen sprite, \"{settings.sprite}\", for yaml \"{path}\", does not exist.")
|
||||||
|
@ -238,6 +245,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]:
|
||||||
|
@ -245,7 +254,7 @@ def get_choice(option, root, value=None) -> typing.Any:
|
||||||
if any(root[option].values()):
|
if any(root[option].values()):
|
||||||
return interpret_on_off(
|
return interpret_on_off(
|
||||||
random.choices(list(root[option].keys()), weights=list(map(int, root[option].values())))[0])
|
random.choices(list(root[option].keys()), weights=list(map(int, root[option].values())))[0])
|
||||||
raise RuntimeError(f"All options specified in {option} are weighted as zero.")
|
raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.")
|
||||||
|
|
||||||
|
|
||||||
def handle_name(name: str):
|
def handle_name(name: str):
|
||||||
|
@ -258,7 +267,27 @@ def prefer_int(input_data: str) -> typing.Union[str, int]:
|
||||||
except:
|
except:
|
||||||
return input_data
|
return input_data
|
||||||
|
|
||||||
def roll_settings(weights):
|
|
||||||
|
available_boss_names: typing.Set[str] = {boss.lower() for boss in Bosses.boss_table if boss not in
|
||||||
|
{'Agahnim', 'Agahnim2', 'Ganon'}}
|
||||||
|
|
||||||
|
boss_shuffle_options = {None: 'none',
|
||||||
|
'none': 'none',
|
||||||
|
'simple': 'basic',
|
||||||
|
'full': 'normal',
|
||||||
|
'random': 'chaos',
|
||||||
|
'singularity': 'singularity',
|
||||||
|
'duality': 'singularity'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def roll_percentage(percentage: typing.Union[int, float]) -> bool:
|
||||||
|
"""Roll a percentage chance.
|
||||||
|
percentage is expected to be in range [0, 100]"""
|
||||||
|
return random.random() < (float(percentage) / 100)
|
||||||
|
|
||||||
|
|
||||||
|
def roll_settings(weights, plando_options: typing.Set[str] = frozenset(("bosses"))):
|
||||||
ret = argparse.Namespace()
|
ret = argparse.Namespace()
|
||||||
if "linked_options" in weights:
|
if "linked_options" in weights:
|
||||||
weights = weights.copy() # make sure we don't write back to other weights sets in same_settings
|
weights = weights.copy() # make sure we don't write back to other weights sets in same_settings
|
||||||
|
@ -266,8 +295,18 @@ def roll_settings(weights):
|
||||||
if "name" not in option_set:
|
if "name" not in option_set:
|
||||||
raise ValueError("One of your linked options does not have a name.")
|
raise ValueError("One of your linked options does not have a name.")
|
||||||
try:
|
try:
|
||||||
if random.random() < (float(option_set["percentage"]) / 100):
|
if roll_percentage(option_set["percentage"]):
|
||||||
|
logging.debug(f"Linked option {option_set['name']} triggered.")
|
||||||
|
logging.debug(f'Applying {option_set["options"]}')
|
||||||
|
new_options = set(option_set["options"]) - set(weights)
|
||||||
weights.update(option_set["options"])
|
weights.update(option_set["options"])
|
||||||
|
if new_options:
|
||||||
|
for new_option in new_options:
|
||||||
|
logging.warning(f'Linked Suboption "{new_option}" of "{option_set["name"]}" did not '
|
||||||
|
f'overwrite a root option. '
|
||||||
|
f"This is probably in error.")
|
||||||
|
else:
|
||||||
|
logging.debug(f"linked option {option_set['name']} skipped.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"Linked option {option_set['name']} is destroyed. "
|
raise ValueError(f"Linked option {option_set['name']} is destroyed. "
|
||||||
f"Please fix your linked option.") from e
|
f"Please fix your linked option.") from e
|
||||||
|
@ -384,14 +423,24 @@ def roll_settings(weights):
|
||||||
|
|
||||||
ret.item_functionality = get_choice('item_functionality', weights)
|
ret.item_functionality = get_choice('item_functionality', weights)
|
||||||
|
|
||||||
ret.shufflebosses = {None: 'none',
|
boss_shuffle = get_choice('boss_shuffle', weights)
|
||||||
'none': 'none',
|
|
||||||
'simple': 'basic',
|
if boss_shuffle in boss_shuffle_options:
|
||||||
'full': 'normal',
|
ret.shufflebosses = boss_shuffle_options[boss_shuffle]
|
||||||
'random': 'chaos',
|
elif "bosses" in plando_options:
|
||||||
'singularity': 'singularity',
|
options = boss_shuffle.lower().split(";")
|
||||||
'duality': 'singularity'
|
remainder_shuffle = "none" # vanilla
|
||||||
}[get_choice('boss_shuffle', weights)]
|
bosses = []
|
||||||
|
for boss in options:
|
||||||
|
if boss in boss_shuffle_options:
|
||||||
|
remainder_shuffle = boss
|
||||||
|
elif boss not in available_boss_names and not "-" in boss:
|
||||||
|
raise ValueError(f"Unknown Boss name or Boss shuffle option {boss}.")
|
||||||
|
else:
|
||||||
|
bosses.append(boss)
|
||||||
|
ret.shufflebosses = ";".join(bosses + [remainder_shuffle])
|
||||||
|
else:
|
||||||
|
raise Exception(f"Boss Shuffle {boss_shuffle} is unknown and boss plando is turned off.")
|
||||||
|
|
||||||
ret.enemy_shuffle = {'none': False,
|
ret.enemy_shuffle = {'none': False,
|
||||||
'shuffled': 'shuffled',
|
'shuffled': 'shuffled',
|
||||||
|
@ -444,12 +493,22 @@ def roll_settings(weights):
|
||||||
'timed_countdown': 'timed-countdown',
|
'timed_countdown': 'timed-countdown',
|
||||||
'display': 'display'}[get_choice('timer', weights, False)]
|
'display': 'display'}[get_choice('timer', weights, False)]
|
||||||
|
|
||||||
|
ret.countdown_start_time = int(get_choice('countdown_start_time', weights, 10))
|
||||||
|
ret.red_clock_time = int(get_choice('red_clock_time', weights, -2))
|
||||||
|
ret.blue_clock_time = int(get_choice('blue_clock_time', weights, 2))
|
||||||
|
ret.green_clock_time = int(get_choice('green_clock_time', weights, 4))
|
||||||
|
|
||||||
ret.dungeon_counters = get_choice('dungeon_counters', weights, 'default')
|
ret.dungeon_counters = get_choice('dungeon_counters', weights, 'default')
|
||||||
|
|
||||||
ret.progressive = convert_to_on_off(get_choice('progressive', weights, 'on'))
|
ret.progressive = convert_to_on_off(get_choice('progressive', weights, 'on'))
|
||||||
|
|
||||||
ret.shuffle_prizes = get_choice('shuffle_prizes', weights, "g")
|
ret.shuffle_prizes = get_choice('shuffle_prizes', weights, "g")
|
||||||
|
|
||||||
|
ret.required_medallions = (get_choice("misery_mire_medallion", weights, "random"),
|
||||||
|
get_choice("turtle_rock_medallion", weights, "random"))
|
||||||
|
for medallion in ret.required_medallions:
|
||||||
|
if medallion not in {"random", "Ether", "Bombos", "Quake"}:
|
||||||
|
raise Exception(f"unknown Medallion {medallion}")
|
||||||
inventoryweights = weights.get('startinventory', {})
|
inventoryweights = weights.get('startinventory', {})
|
||||||
startitems = []
|
startitems = []
|
||||||
for item in inventoryweights.keys():
|
for item in inventoryweights.keys():
|
||||||
|
@ -483,6 +542,46 @@ 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)
|
||||||
|
|
||||||
|
ret.plando_items = []
|
||||||
|
if "items" in plando_options:
|
||||||
|
options = weights.get("plando_items", [])
|
||||||
|
for placement in options:
|
||||||
|
if roll_percentage(get_choice("percentage", placement, 100)):
|
||||||
|
item = get_choice("item", placement)
|
||||||
|
location = get_choice("location", placement)
|
||||||
|
from_pool = get_choice("from_pool", placement, True)
|
||||||
|
location_world = get_choice("world", placement, False)
|
||||||
|
ret.plando_items.append(PlandoItem(item, location, location_world, from_pool))
|
||||||
|
|
||||||
|
ret.plando_texts = {}
|
||||||
|
if "texts" in plando_options:
|
||||||
|
options = weights.get("plando_texts", [])
|
||||||
|
for placement in options:
|
||||||
|
if roll_percentage(get_choice("percentage", placement, 100)):
|
||||||
|
ret.plando_texts[str(get_choice("at", placement))] = str(get_choice("text", placement))
|
||||||
|
|
||||||
|
ret.plando_connections = []
|
||||||
|
if "connections" in plando_options:
|
||||||
|
options = weights.get("plando_connections", [])
|
||||||
|
for placement in options:
|
||||||
|
if roll_percentage(get_choice("percentage", placement, 100)):
|
||||||
|
ret.plando_connections.append(PlandoConnection(
|
||||||
|
get_choice("entrance", placement),
|
||||||
|
get_choice("exit", placement),
|
||||||
|
get_choice("direction", placement, "both")
|
||||||
|
))
|
||||||
|
|
||||||
if 'rom' in weights:
|
if 'rom' in weights:
|
||||||
romweights = weights['rom']
|
romweights = weights['rom']
|
||||||
|
|
||||||
|
@ -516,6 +615,11 @@ def roll_settings(weights):
|
||||||
ret.heartbeep = convert_to_on_off(get_choice('heartbeep', romweights, "normal"))
|
ret.heartbeep = convert_to_on_off(get_choice('heartbeep', romweights, "normal"))
|
||||||
ret.ow_palettes = get_choice('ow_palettes', romweights, "default")
|
ret.ow_palettes = get_choice('ow_palettes', romweights, "default")
|
||||||
ret.uw_palettes = get_choice('uw_palettes', romweights, "default")
|
ret.uw_palettes = get_choice('uw_palettes', romweights, "default")
|
||||||
|
ret.hud_palettes = get_choice('hud_palettes', romweights, "default")
|
||||||
|
ret.sword_palettes = get_choice('sword_palettes', romweights, "default")
|
||||||
|
ret.shield_palettes = get_choice('shield_palettes', romweights, "default")
|
||||||
|
ret.link_palettes = get_choice('link_palettes', romweights, "default")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
ret.quickswap = True
|
ret.quickswap = True
|
||||||
ret.sprite = "Link"
|
ret.sprite = "Link"
|
||||||
|
|
128
Utils.py
|
@ -1,12 +1,18 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
|
|
||||||
def tuplize_version(version: str) -> typing.Tuple[int, ...]:
|
def tuplize_version(version: str) -> typing.Tuple[int, ...]:
|
||||||
return tuple(int(piece, 10) for piece in version.split("."))
|
return Version(*(int(piece, 10) for piece in version.split(".")))
|
||||||
|
|
||||||
|
|
||||||
__version__ = "1.0.0"
|
class Version(typing.NamedTuple):
|
||||||
|
major: int
|
||||||
|
minor: int
|
||||||
|
micro: int
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
_version_tuple = tuplize_version(__version__)
|
_version_tuple = tuplize_version(__version__)
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
@ -37,11 +43,11 @@ def int32_as_bytes(value):
|
||||||
|
|
||||||
|
|
||||||
def pc_to_snes(value):
|
def pc_to_snes(value):
|
||||||
return ((value<<1) & 0x7F0000)|(value & 0x7FFF)|0x8000
|
return ((value << 1) & 0x7F0000) | (value & 0x7FFF) | 0x8000
|
||||||
|
|
||||||
|
|
||||||
def snes_to_pc(value):
|
def snes_to_pc(value):
|
||||||
return ((value & 0x7F0000)>>1)|(value & 0x7FFF)
|
return ((value & 0x7F0000) >> 1) | (value & 0x7FFF)
|
||||||
|
|
||||||
|
|
||||||
def parse_player_names(names, players, teams):
|
def parse_player_names(names, players, teams):
|
||||||
|
@ -82,6 +88,7 @@ def local_path(*path):
|
||||||
|
|
||||||
return os.path.join(local_path.cached_path, *path)
|
return os.path.join(local_path.cached_path, *path)
|
||||||
|
|
||||||
|
|
||||||
local_path.cached_path = None
|
local_path.cached_path = None
|
||||||
|
|
||||||
|
|
||||||
|
@ -93,8 +100,10 @@ def output_path(*path):
|
||||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
output_path.cached_path = None
|
output_path.cached_path = None
|
||||||
|
|
||||||
|
|
||||||
def open_file(filename):
|
def open_file(filename):
|
||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32':
|
||||||
os.startfile(filename)
|
os.startfile(filename)
|
||||||
|
@ -102,9 +111,10 @@ def open_file(filename):
|
||||||
open_command = 'open' if sys.platform == 'darwin' else 'xdg-open'
|
open_command = 'open' if sys.platform == 'darwin' else 'xdg-open'
|
||||||
subprocess.call([open_command, filename])
|
subprocess.call([open_command, filename])
|
||||||
|
|
||||||
|
|
||||||
def close_console():
|
def close_console():
|
||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32':
|
||||||
#windows
|
# windows
|
||||||
import ctypes.wintypes
|
import ctypes.wintypes
|
||||||
try:
|
try:
|
||||||
ctypes.windll.kernel32.FreeConsole()
|
ctypes.windll.kernel32.FreeConsole()
|
||||||
|
@ -138,6 +148,7 @@ class Hint(typing.NamedTuple):
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
return hash((self.receiving_player, self.finding_player, self.location, self.item, self.entrance))
|
return hash((self.receiving_player, self.finding_player, self.location, self.item, self.entrance))
|
||||||
|
|
||||||
|
|
||||||
def get_public_ipv4() -> str:
|
def get_public_ipv4() -> str:
|
||||||
import socket
|
import socket
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
@ -153,6 +164,7 @@ def get_public_ipv4() -> str:
|
||||||
pass # we could be offline, in a local game, so no point in erroring out
|
pass # we could be offline, in a local game, so no point in erroring out
|
||||||
return ip
|
return ip
|
||||||
|
|
||||||
|
|
||||||
def get_public_ipv6() -> str:
|
def get_public_ipv6() -> str:
|
||||||
import socket
|
import socket
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
@ -165,6 +177,91 @@ 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"):
|
||||||
|
# Refer to host.yaml for comments as to what all these options mean.
|
||||||
|
options = {
|
||||||
|
"general_options": {
|
||||||
|
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
|
||||||
|
"qusb2snes": "QUsb2Snes\\QUsb2Snes.exe",
|
||||||
|
"rom_start": True,
|
||||||
|
"output_path": "output",
|
||||||
|
},
|
||||||
|
"server_options": {
|
||||||
|
"host": None,
|
||||||
|
"port": 38281,
|
||||||
|
"password": None,
|
||||||
|
"multidata": None,
|
||||||
|
"savefile": None,
|
||||||
|
"disable_save": False,
|
||||||
|
"loglevel": "info",
|
||||||
|
"server_password": None,
|
||||||
|
"disable_item_cheat": False,
|
||||||
|
"location_check_points": 1,
|
||||||
|
"hint_cost": 1000,
|
||||||
|
"forfeit_mode": "goal",
|
||||||
|
"remaining_mode": "goal",
|
||||||
|
"auto_shutdown": 0,
|
||||||
|
"compatibility": 2,
|
||||||
|
},
|
||||||
|
"multi_mystery_options": {
|
||||||
|
"teams": 1,
|
||||||
|
"enemizer_path": "EnemizerCLI/EnemizerCLI.Core.exe",
|
||||||
|
"player_files_path": "Players",
|
||||||
|
"players": 0,
|
||||||
|
"weights_file_path": "weights.yaml",
|
||||||
|
"meta_file_path": "meta.yaml",
|
||||||
|
"player_name": "",
|
||||||
|
"create_spoiler": 1,
|
||||||
|
"zip_roms": 0,
|
||||||
|
"zip_diffs": 2,
|
||||||
|
"zip_spoiler": 0,
|
||||||
|
"zip_multidata": 1,
|
||||||
|
"zip_format": 1,
|
||||||
|
"race": 0,
|
||||||
|
"cpu_threads": 0,
|
||||||
|
"max_attempts": 0,
|
||||||
|
"take_first_working": False,
|
||||||
|
"keep_all_seeds": False,
|
||||||
|
"log_output_path": "Output Logs",
|
||||||
|
"log_level": None,
|
||||||
|
"plando_options": "bosses",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 +270,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 +321,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
|
||||||
|
@ -255,7 +358,6 @@ def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]:
|
||||||
return romfile, False
|
return romfile, False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ReceivedItem(typing.NamedTuple):
|
class ReceivedItem(typing.NamedTuple):
|
||||||
item: int
|
item: int
|
||||||
location: int
|
location: int
|
||||||
|
|
|
@ -4,6 +4,7 @@ So unless you're Berserker you need to include license information."""
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
import base64
|
import base64
|
||||||
|
import socket
|
||||||
|
|
||||||
from pony.flask import Pony
|
from pony.flask import Pony
|
||||||
from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory
|
from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory
|
||||||
|
@ -30,8 +31,8 @@ app.config["DEBUG"] = False
|
||||||
app.config["PORT"] = 80
|
app.config["PORT"] = 80
|
||||||
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
||||||
app.config['MAX_CONTENT_LENGTH'] = 4 * 1024 * 1024 # 4 megabyte limit
|
app.config['MAX_CONTENT_LENGTH'] = 4 * 1024 * 1024 # 4 megabyte limit
|
||||||
# if you want persistent sessions on your server, make sure you make this a constant in your config.yaml
|
# if you want to deploy, make sure you have a non-guessable secret key
|
||||||
app.config["SECRET_KEY"] = os.urandom(32)
|
app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
|
||||||
# at what amount of worlds should scheduling be used, instead of rolling in the webthread
|
# at what amount of worlds should scheduling be used, instead of rolling in the webthread
|
||||||
app.config["JOB_THRESHOLD"] = 2
|
app.config["JOB_THRESHOLD"] = 2
|
||||||
app.config['SESSION_PERMANENT'] = True
|
app.config['SESSION_PERMANENT'] = True
|
||||||
|
@ -47,6 +48,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
|
||||||
app.config["HOSTNAME"] = "berserkermulti.world"
|
app.config["HOSTNAME"] = "berserkermulti.world"
|
||||||
|
|
||||||
|
@ -85,16 +88,21 @@ def tutorial(lang='en'):
|
||||||
|
|
||||||
|
|
||||||
@app.route('/player-settings')
|
@app.route('/player-settings')
|
||||||
|
def player_settings_simple():
|
||||||
|
return render_template("playerSettings.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/weighted-settings')
|
||||||
def player_settings():
|
def player_settings():
|
||||||
return render_template("player-settings.html")
|
return render_template("weightedSettings.html")
|
||||||
|
|
||||||
|
|
||||||
@app.route('/seed/<suuid:seed>')
|
@app.route('/seed/<suuid:seed>')
|
||||||
def view_seed(seed: UUID):
|
def viewSeed(seed: UUID):
|
||||||
seed = Seed.get(id=seed)
|
seed = Seed.get(id=seed)
|
||||||
if not seed:
|
if not seed:
|
||||||
abort(404)
|
abort(404)
|
||||||
return render_template("view_seed.html", seed=seed,
|
return render_template("viewSeed.html", seed=seed,
|
||||||
rooms=[room for room in seed.rooms if room.owner == session["_id"]])
|
rooms=[room for room in seed.rooms if room.owner == session["_id"]])
|
||||||
|
|
||||||
|
|
||||||
|
@ -105,7 +113,7 @@ def new_room(seed: UUID):
|
||||||
abort(404)
|
abort(404)
|
||||||
room = Room(seed=seed, owner=session["_id"], tracker=uuid4())
|
room = Room(seed=seed, owner=session["_id"], tracker=uuid4())
|
||||||
commit()
|
commit()
|
||||||
return redirect(url_for("host_room", room=room.id))
|
return redirect(url_for("hostRoom", room=room.id))
|
||||||
|
|
||||||
|
|
||||||
def _read_log(path: str):
|
def _read_log(path: str):
|
||||||
|
@ -124,7 +132,7 @@ def display_log(room: UUID):
|
||||||
|
|
||||||
|
|
||||||
@app.route('/hosted/<suuid:room>', methods=['GET', 'POST'])
|
@app.route('/hosted/<suuid:room>', methods=['GET', 'POST'])
|
||||||
def host_room(room: UUID):
|
def hostRoom(room: UUID):
|
||||||
room = Room.get(id=room)
|
room = Room.get(id=room)
|
||||||
if room is None:
|
if room is None:
|
||||||
return abort(404)
|
return abort(404)
|
||||||
|
@ -137,7 +145,7 @@ def host_room(room: UUID):
|
||||||
with db_session:
|
with db_session:
|
||||||
room.last_activity = datetime.utcnow() # will trigger a spinup, if it's not already running
|
room.last_activity = datetime.utcnow() # will trigger a spinup, if it's not already running
|
||||||
|
|
||||||
return render_template("host_room.html", room=room)
|
return render_template("hostRoom.html", room=room)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/favicon.ico')
|
@app.route('/favicon.ico')
|
||||||
|
@ -147,4 +155,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,24 @@
|
||||||
|
"""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, user # trigger registration
|
||||||
|
|
||||||
|
|
||||||
|
# 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("api.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
|
|
@ -0,0 +1,33 @@
|
||||||
|
from flask import session, jsonify
|
||||||
|
|
||||||
|
from WebHostLib.models import *
|
||||||
|
from . import api_endpoints
|
||||||
|
|
||||||
|
|
||||||
|
@api_endpoints.route('/get_rooms')
|
||||||
|
def get_rooms():
|
||||||
|
response = []
|
||||||
|
for room in select(room for room in Room if room.owner == session["_id"]):
|
||||||
|
response.append({
|
||||||
|
"room_id": room.id,
|
||||||
|
"seed_id": room.seed.id,
|
||||||
|
"creation_time": room.creation_time,
|
||||||
|
"last_activity": room.last_activity,
|
||||||
|
"last_port": room.last_port,
|
||||||
|
"timeout": room.timeout,
|
||||||
|
"tracker": room.tracker,
|
||||||
|
"players": room.seed.multidata["names"] if room.seed.multidata else [["Singleplayer"]],
|
||||||
|
})
|
||||||
|
return jsonify(response)
|
||||||
|
|
||||||
|
|
||||||
|
@api_endpoints.route('/get_seeds')
|
||||||
|
def get_seeds():
|
||||||
|
response = []
|
||||||
|
for seed in select(seed for seed in Seed if seed.owner == session["_id"]):
|
||||||
|
response.append({
|
||||||
|
"seed_id": seed.id,
|
||||||
|
"creation_time": seed.creation_time,
|
||||||
|
"players": seed.multidata["names"] if seed.multidata else [["Singleplayer"]],
|
||||||
|
})
|
||||||
|
return jsonify(response)
|
|
@ -28,8 +28,8 @@ def mysterycheck():
|
||||||
if type(options) == str:
|
if type(options) == str:
|
||||||
flash(options)
|
flash(options)
|
||||||
else:
|
else:
|
||||||
results, _ = roll_yamls(options)
|
results, _ = roll_options(options)
|
||||||
return render_template("checkresult.html", results=results)
|
return render_template("checkResult.html", results=results)
|
||||||
|
|
||||||
return render_template("check.html")
|
return render_template("check.html")
|
||||||
|
|
||||||
|
@ -60,17 +60,20 @@ def get_yaml_data(file) -> Union[Dict[str, str], str]:
|
||||||
return options
|
return options
|
||||||
|
|
||||||
|
|
||||||
def roll_yamls(options: Dict[str, Union[str, str]]) -> Tuple[Dict[str, Union[str, bool]], Dict[str, dict]]:
|
def roll_options(options: Dict[str, Union[dict, str]]) -> Tuple[Dict[str, Union[str, bool]], Dict[str, dict]]:
|
||||||
results = {}
|
results = {}
|
||||||
rolled_results = {}
|
rolled_results = {}
|
||||||
for filename, text in options.items():
|
for filename, text in options.items():
|
||||||
try:
|
try:
|
||||||
yaml_data = parse_yaml(text)
|
if type(text) is dict:
|
||||||
|
yaml_data = text
|
||||||
|
else:
|
||||||
|
yaml_data = parse_yaml(text)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results[filename] = f"Failed to parse YAML data in {filename}: {e}"
|
results[filename] = f"Failed to parse YAML data in {filename}: {e}"
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
rolled_results[filename] = roll_settings(yaml_data)
|
rolled_results[filename] = roll_settings(yaml_data, plando_options={"bosses"})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results[filename] = f"Failed to generate mystery in {filename}: {e}"
|
results[filename] = f"Failed to generate mystery in {filename}: {e}"
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -48,7 +48,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 = ["AP", "WebHost"]
|
self.tags = ["AP", "WebHost"]
|
||||||
|
|
|
@ -11,7 +11,7 @@ import pickle
|
||||||
|
|
||||||
from .models import *
|
from .models import *
|
||||||
from WebHostLib import app
|
from WebHostLib import app
|
||||||
from .check import get_yaml_data, roll_yamls
|
from .check import get_yaml_data, roll_options
|
||||||
|
|
||||||
|
|
||||||
@app.route('/generate', methods=['GET', 'POST'])
|
@app.route('/generate', methods=['GET', 'POST'])
|
||||||
|
@ -27,9 +27,9 @@ def generate(race=False):
|
||||||
if type(options) == str:
|
if type(options) == str:
|
||||||
flash(options)
|
flash(options)
|
||||||
else:
|
else:
|
||||||
results, gen_options = roll_yamls(options)
|
results, gen_options = roll_options(options)
|
||||||
if any(type(result) == str for result in results.values()):
|
if any(type(result) == str for result in results.values()):
|
||||||
return render_template("checkresult.html", results=results)
|
return render_template("checkResult.html", results=results)
|
||||||
elif len(gen_options) > app.config["MAX_ROLL"]:
|
elif len(gen_options) > app.config["MAX_ROLL"]:
|
||||||
flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players for now. "
|
flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players for now. "
|
||||||
f"If you have a larger group, please generate it yourself and upload it.")
|
f"If you have a larger group, please generate it yourself and upload it.")
|
||||||
|
@ -43,9 +43,15 @@ def generate(race=False):
|
||||||
|
|
||||||
return redirect(url_for("wait_seed", seed=gen.id))
|
return redirect(url_for("wait_seed", seed=gen.id))
|
||||||
else:
|
else:
|
||||||
seed_id = gen_game({name: vars(options) for name, options in gen_options.items()},
|
try:
|
||||||
race=race, owner=session["_id"].int)
|
seed_id = gen_game({name: vars(options) for name, options in gen_options.items()},
|
||||||
return redirect(url_for("view_seed", seed=seed_id))
|
race=race, owner=session["_id"].int)
|
||||||
|
except BaseException as e:
|
||||||
|
from .autolauncher import handle_generation_failure
|
||||||
|
handle_generation_failure(e)
|
||||||
|
return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": "+ str(e)))
|
||||||
|
|
||||||
|
return redirect(url_for("viewSeed", seed=seed_id))
|
||||||
|
|
||||||
return render_template("generate.html", race=race)
|
return render_template("generate.html", race=race)
|
||||||
|
|
||||||
|
@ -90,13 +96,14 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
|
||||||
del (erargs.progression_balancing)
|
del (erargs.progression_balancing)
|
||||||
ERmain(erargs, seed)
|
ERmain(erargs, seed)
|
||||||
|
|
||||||
return upload_to_db(target.name, owner, sid)
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@ -105,19 +112,20 @@ def wait_seed(seed: UUID):
|
||||||
seed_id = seed
|
seed_id = seed
|
||||||
seed = Seed.get(id=seed_id)
|
seed = Seed.get(id=seed_id)
|
||||||
if seed:
|
if seed:
|
||||||
return redirect(url_for("view_seed", seed=seed_id))
|
return redirect(url_for("viewSeed", seed=seed_id))
|
||||||
generation = Generation.get(id=seed_id)
|
generation = Generation.get(id=seed_id)
|
||||||
|
|
||||||
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."
|
return render_template("seedError.html", seed_error=generation.meta.decode())
|
||||||
return render_template("wait_seed.html", seed_id=seed_id)
|
return render_template("waitSeed.html", seed_id=seed_id)
|
||||||
|
|
||||||
|
|
||||||
def upload_to_db(folder, owner, sid):
|
def upload_to_db(folder, owner, sid, race:bool):
|
||||||
patches = set()
|
patches = set()
|
||||||
spoiler = ""
|
spoiler = ""
|
||||||
|
|
||||||
multidata = None
|
multidata = None
|
||||||
for file in os.listdir(folder):
|
for file in os.listdir(folder):
|
||||||
file = os.path.join(folder, file)
|
file = os.path.join(folder, file)
|
||||||
|
@ -129,7 +137,7 @@ def upload_to_db(folder, owner, sid):
|
||||||
player_id=player_id, player_name = player_name))
|
player_id=player_id, player_name = player_name))
|
||||||
elif file.endswith(".txt"):
|
elif file.endswith(".txt"):
|
||||||
spoiler = open(file, "rt", encoding="utf-8-sig").read()
|
spoiler = open(file, "rt", encoding="utf-8-sig").read()
|
||||||
elif file.endswith(".multidata"):
|
elif file.endswith(".archipelago"):
|
||||||
multidata = open(file, "rb").read()
|
multidata = open(file, "rb").read()
|
||||||
if multidata:
|
if multidata:
|
||||||
with db_session:
|
with db_session:
|
||||||
|
|
|
@ -52,5 +52,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.5.0
|
Flask-Compress>=1.8.0
|
||||||
Flask-Limiter>=1.4
|
Flask-Limiter>=1.4
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
let tables = $(".autodatatable").DataTable({
|
||||||
|
"paging": false,
|
||||||
|
"ordering": true,
|
||||||
|
"info": false,
|
||||||
|
"dom": "t",
|
||||||
|
});
|
||||||
|
console.log(tables);
|
||||||
|
});
|
|
@ -0,0 +1,9 @@
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
document.getElementById('check-button').addEventListener('click', () => {
|
||||||
|
document.getElementById('file-input').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('file-input').addEventListener('change', () => {
|
||||||
|
document.getElementById('check-form').submit();
|
||||||
|
});
|
||||||
|
});
|
|
@ -4,14 +4,11 @@ window.addEventListener('load', () => {
|
||||||
|
|
||||||
const cookieNotice = document.createElement('div');
|
const cookieNotice = document.createElement('div');
|
||||||
cookieNotice.innerText = "This website uses cookies to store information about the games you play.";
|
cookieNotice.innerText = "This website uses cookies to store information about the games you play.";
|
||||||
cookieNotice.style.position = "fixed";
|
cookieNotice.setAttribute('id', 'cookie-notice');
|
||||||
cookieNotice.style.bottom = "0";
|
const closeButton = document.createElement('span');
|
||||||
cookieNotice.style.left = "0";
|
closeButton.setAttribute('id', 'close-button');
|
||||||
cookieNotice.style.width = "100%";
|
closeButton.innerText = 'X';
|
||||||
cookieNotice.style.lineHeight = "40px";
|
cookieNotice.appendChild(closeButton);
|
||||||
cookieNotice.style.backgroundColor = "#c7cda5";
|
|
||||||
cookieNotice.style.textAlign = "center";
|
|
||||||
cookieNotice.style.cursor = "pointer";
|
|
||||||
document.body.appendChild(cookieNotice);
|
document.body.appendChild(cookieNotice);
|
||||||
cookieNotice.addEventListener('click', () => {
|
cookieNotice.addEventListener('click', () => {
|
||||||
localStorage.setItem('cookieNotice', "1");
|
localStorage.setItem('cookieNotice', "1");
|
|
@ -1,9 +1,9 @@
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
document.getElementById('upload-button').addEventListener('click', () => {
|
document.getElementById('generate-game-button').addEventListener('click', () => {
|
||||||
document.getElementById('file-input').click();
|
document.getElementById('file-input').click();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('file-input').addEventListener('change', () => {
|
document.getElementById('file-input').addEventListener('change', () => {
|
||||||
document.getElementById('upload-form').submit();
|
document.getElementById('generate-game-form').submit();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
document.getElementById('host-game-button').addEventListener('click', () => {
|
||||||
|
document.getElementById('file-input').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('file-input').addEventListener('change', () => {
|
||||||
|
document.getElementById('host-game-form').submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
adjustFooterHeight();
|
||||||
|
});
|
|
@ -0,0 +1,188 @@
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
Promise.all([fetchSettingData(), fetchSpriteData()]).then((results) => {
|
||||||
|
// Page setup
|
||||||
|
createDefaultSettings(results[0]);
|
||||||
|
buildUI(results[0]);
|
||||||
|
adjustHeaderWidth();
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
document.getElementById('export-settings').addEventListener('click', () => exportSettings());
|
||||||
|
document.getElementById('generate-race').addEventListener('click', () => generateGame(true))
|
||||||
|
document.getElementById('generate-game').addEventListener('click', () => generateGame());
|
||||||
|
|
||||||
|
// Name input field
|
||||||
|
const playerSettings = JSON.parse(localStorage.getItem('playerSettings'));
|
||||||
|
const nameInput = document.getElementById('player-name');
|
||||||
|
nameInput.addEventListener('keyup', (event) => updateSetting(event));
|
||||||
|
nameInput.value = playerSettings.name;
|
||||||
|
|
||||||
|
// Sprite options
|
||||||
|
const spriteData = JSON.parse(results[1]);
|
||||||
|
const spriteSelect = document.getElementById('sprite');
|
||||||
|
spriteData.sprites.forEach((sprite) => {
|
||||||
|
if (sprite.name.trim().length === 0) { return; }
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.setAttribute('value', sprite.name.trim());
|
||||||
|
if (playerSettings.rom.sprite === sprite.name.trim()) { option.selected = true; }
|
||||||
|
option.innerText = sprite.name;
|
||||||
|
spriteSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchSettingData = () => new Promise((resolve, reject) => {
|
||||||
|
const ajax = new XMLHttpRequest();
|
||||||
|
ajax.onreadystatechange = () => {
|
||||||
|
if (ajax.readyState !== 4) { return; }
|
||||||
|
if (ajax.status !== 200) {
|
||||||
|
reject(ajax.responseText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try{ resolve(JSON.parse(ajax.responseText)); }
|
||||||
|
catch(error){ reject(error); }
|
||||||
|
};
|
||||||
|
ajax.open('GET', `${window.location.origin}/static/static/playerSettings.json`, true);
|
||||||
|
ajax.send();
|
||||||
|
});
|
||||||
|
|
||||||
|
const createDefaultSettings = (settingData) => {
|
||||||
|
if (!localStorage.getItem('playerSettings')) {
|
||||||
|
const newSettings = {};
|
||||||
|
for (let roSetting of Object.keys(settingData.readOnly)){
|
||||||
|
newSettings[roSetting] = settingData.readOnly[roSetting];
|
||||||
|
}
|
||||||
|
for (let generalOption of Object.keys(settingData.generalOptions)){
|
||||||
|
newSettings[generalOption] = settingData.generalOptions[generalOption];
|
||||||
|
}
|
||||||
|
for (let gameOption of Object.keys(settingData.gameOptions)){
|
||||||
|
newSettings[gameOption] = settingData.gameOptions[gameOption].defaultValue;
|
||||||
|
}
|
||||||
|
newSettings.rom = {};
|
||||||
|
for (let romOption of Object.keys(settingData.romOptions)){
|
||||||
|
newSettings.rom[romOption] = settingData.romOptions[romOption].defaultValue;
|
||||||
|
}
|
||||||
|
localStorage.setItem('playerSettings', JSON.stringify(newSettings));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildUI = (settingData) => {
|
||||||
|
// Game Options
|
||||||
|
const leftGameOpts = {};
|
||||||
|
const rightGameOpts = {};
|
||||||
|
Object.keys(settingData.gameOptions).forEach((key, index) => {
|
||||||
|
if (index < Object.keys(settingData.gameOptions).length / 2) { leftGameOpts[key] = settingData.gameOptions[key]; }
|
||||||
|
else { rightGameOpts[key] = settingData.gameOptions[key]; }
|
||||||
|
});
|
||||||
|
document.getElementById('game-options-left').appendChild(buildOptionsTable(leftGameOpts));
|
||||||
|
document.getElementById('game-options-right').appendChild(buildOptionsTable(rightGameOpts));
|
||||||
|
|
||||||
|
// ROM Options
|
||||||
|
const leftRomOpts = {};
|
||||||
|
const rightRomOpts = {};
|
||||||
|
Object.keys(settingData.romOptions).forEach((key, index) => {
|
||||||
|
if (index < Object.keys(settingData.romOptions).length / 2) { leftRomOpts[key] = settingData.romOptions[key]; }
|
||||||
|
else { rightRomOpts[key] = settingData.romOptions[key]; }
|
||||||
|
});
|
||||||
|
document.getElementById('rom-options-left').appendChild(buildOptionsTable(leftRomOpts, true));
|
||||||
|
document.getElementById('rom-options-right').appendChild(buildOptionsTable(rightRomOpts, true));
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildOptionsTable = (settings, romOpts = false) => {
|
||||||
|
const currentSettings = JSON.parse(localStorage.getItem('playerSettings'));
|
||||||
|
const table = document.createElement('table');
|
||||||
|
const tbody = document.createElement('tbody');
|
||||||
|
|
||||||
|
Object.keys(settings).forEach((setting) => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
|
||||||
|
// td Left
|
||||||
|
const tdl = document.createElement('td');
|
||||||
|
const label = document.createElement('label');
|
||||||
|
label.setAttribute('for', setting);
|
||||||
|
label.setAttribute('data-tooltip', settings[setting].description);
|
||||||
|
label.innerText = `${settings[setting].friendlyName}:`;
|
||||||
|
tdl.appendChild(label);
|
||||||
|
tr.appendChild(tdl);
|
||||||
|
|
||||||
|
// td Right
|
||||||
|
const tdr = document.createElement('td');
|
||||||
|
const select = document.createElement('select');
|
||||||
|
select.setAttribute('id', setting);
|
||||||
|
select.setAttribute('data-key', setting);
|
||||||
|
if (romOpts) { select.setAttribute('data-romOpt', '1'); }
|
||||||
|
settings[setting].options.forEach((opt) => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.setAttribute('value', opt.value);
|
||||||
|
option.innerText = opt.name;
|
||||||
|
if ((isNaN(currentSettings[setting]) && (parseInt(opt.value, 10) === parseInt(currentSettings[setting]))) ||
|
||||||
|
(opt.value === currentSettings[setting])) {
|
||||||
|
option.selected = true;
|
||||||
|
}
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
select.addEventListener('change', (event) => updateSetting(event));
|
||||||
|
tdr.appendChild(select);
|
||||||
|
tr.appendChild(tdr);
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
|
||||||
|
table.appendChild(tbody);
|
||||||
|
return table;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSetting = (event) => {
|
||||||
|
const options = JSON.parse(localStorage.getItem('playerSettings'));
|
||||||
|
if (event.target.getAttribute('data-romOpt')) {
|
||||||
|
options.rom[event.target.getAttribute('data-key')] = isNaN(event.target.value) ?
|
||||||
|
event.target.value : parseInt(event.target.value, 10);
|
||||||
|
} else {
|
||||||
|
options[event.target.getAttribute('data-key')] = isNaN(event.target.value) ?
|
||||||
|
event.target.value : parseInt(event.target.value, 10);
|
||||||
|
}
|
||||||
|
localStorage.setItem('playerSettings', JSON.stringify(options));
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportSettings = () => {
|
||||||
|
const settings = JSON.parse(localStorage.getItem('playerSettings'));
|
||||||
|
if (!settings.name || settings.name.trim().length === 0) { settings.name = "noname"; }
|
||||||
|
const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
|
||||||
|
download(`${document.getElementById('player-name').value}.yaml`, yamlText);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Create an anchor and trigger a download of a text file. */
|
||||||
|
const download = (filename, text) => {
|
||||||
|
const downloadLink = document.createElement('a');
|
||||||
|
downloadLink.setAttribute('href','data:text/yaml;charset=utf-8,'+ encodeURIComponent(text))
|
||||||
|
downloadLink.setAttribute('download', filename);
|
||||||
|
downloadLink.style.display = 'none';
|
||||||
|
document.body.appendChild(downloadLink);
|
||||||
|
downloadLink.click();
|
||||||
|
document.body.removeChild(downloadLink);
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateGame = (raceMode = false) => {
|
||||||
|
axios.post('/api/generate', {
|
||||||
|
weights: { player: localStorage.getItem('playerSettings') },
|
||||||
|
presetData: { player: localStorage.getItem('playerSettings') },
|
||||||
|
playerCount: 1,
|
||||||
|
race: raceMode ? '1' : '0',
|
||||||
|
}).then((response) => {
|
||||||
|
window.location.href = response.data.url;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchSpriteData = () => new Promise((resolve, reject) => {
|
||||||
|
const ajax = new XMLHttpRequest();
|
||||||
|
ajax.onreadystatechange = () => {
|
||||||
|
if (ajax.readyState !== 4) { return; }
|
||||||
|
if (ajax.status !== 200) {
|
||||||
|
reject('Unable to fetch sprite data.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(ajax.responseText);
|
||||||
|
};
|
||||||
|
ajax.open('GET', `${window.location.origin}/static/static/spriteData.json`, true);
|
||||||
|
ajax.send();
|
||||||
|
});
|
|
@ -0,0 +1,47 @@
|
||||||
|
const adjustFooterHeight = () => {
|
||||||
|
// If there is no footer on this page, do nothing
|
||||||
|
const footer = document.getElementById('island-footer');
|
||||||
|
if (!footer) { return; }
|
||||||
|
|
||||||
|
// If the body is taller than the window, also do nothing
|
||||||
|
if (document.body.offsetHeight > window.innerHeight) {
|
||||||
|
footer.style.marginTop = '0';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a margin-top to the footer to position it at the bottom of the screen
|
||||||
|
const sibling = footer.previousElementSibling;
|
||||||
|
const margin = (window.innerHeight - sibling.offsetTop - sibling.offsetHeight - footer.offsetHeight);
|
||||||
|
if (margin < 1) {
|
||||||
|
footer.style.marginTop = '0';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
footer.style.marginTop = `${margin}px`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const adjustHeaderWidth = () => {
|
||||||
|
// If there is no header, do nothing
|
||||||
|
const header = document.getElementById('base-header');
|
||||||
|
if (!header) { return; }
|
||||||
|
|
||||||
|
const tempDiv = document.createElement('div');
|
||||||
|
tempDiv.style.width = '100px';
|
||||||
|
tempDiv.style.height = '100px';
|
||||||
|
tempDiv.style.overflow = 'scroll';
|
||||||
|
tempDiv.style.position = 'absolute';
|
||||||
|
tempDiv.style.top = '-500px';
|
||||||
|
document.body.appendChild(tempDiv);
|
||||||
|
const scrollbarWidth = tempDiv.offsetWidth - tempDiv.clientWidth;
|
||||||
|
document.body.removeChild(tempDiv);
|
||||||
|
|
||||||
|
const documentRoot = document.compatMode === 'BackCompat' ? document.body : document.documentElement;
|
||||||
|
const margin = (documentRoot.scrollHeight > documentRoot.clientHeight) ? 0-scrollbarWidth : 0;
|
||||||
|
document.getElementById('base-header-right').style.marginRight = `${margin}px`;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
window.addEventListener('resize', adjustFooterHeight);
|
||||||
|
window.addEventListener('resize', adjustHeaderWidth);
|
||||||
|
adjustFooterHeight();
|
||||||
|
adjustHeaderWidth();
|
||||||
|
});
|
|
@ -44,6 +44,7 @@ window.addEventListener('load', () => {
|
||||||
|
|
||||||
// Populate page with HTML generated from markdown
|
// Populate page with HTML generated from markdown
|
||||||
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
|
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
|
||||||
|
adjustHeaderWidth();
|
||||||
|
|
||||||
// Reset the id of all header divs to something nicer
|
// Reset the id of all header divs to something nicer
|
||||||
const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
|
const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
|
||||||
|
|
|
@ -50,30 +50,33 @@ each player to enjoy an experience customized for their taste, and different pla
|
||||||
can all have different options.
|
can all have different options.
|
||||||
|
|
||||||
### Where do I get a YAML file?
|
### Where do I get a YAML file?
|
||||||
The [Player Settings](/player-settings) page on the website allows you to configure your personal settings
|
The [Generate Game](/player-settings) page on the website allows you to configure your personal settings and
|
||||||
and download a `yaml` file. You may configure up to three presets on this page.
|
export a YAML file from them.
|
||||||
|
|
||||||
### Your YAML file is weighted
|
### Advanced YAML configuration
|
||||||
The Player Settings page has many options which are primarily represented with sliders. This allows you to
|
A more advanced version of the YAML file can be created using the [Weighted Settings](/weighted-settings) page,
|
||||||
choose how likely certain options are to occur relative to other options within a category.
|
which allows you to configure up to three presets. The Weighted Settings page has many options which are
|
||||||
|
primarily represented with sliders. This allows you to choose how likely certain options are to occur relative
|
||||||
|
to other options within a category.
|
||||||
|
|
||||||
For example, imagine the generator creates a bucket labeled "Map Shuffle", and places folded pieces of paper
|
For example, imagine the generator creates a bucket labeled "Map Shuffle", and places folded pieces of paper
|
||||||
into the bucket for each sub-option. Also imagine your chosen value for "On" is 20 and your value for "Off" is 40.
|
into the bucket for each sub-option. Also imagine your chosen value for "On" is 20, and your value for "Off" is 40.
|
||||||
|
|
||||||
In this example, sixty pieces of paper are put into the bucket. Twenty for "On" and forty for "Off". When the
|
In this example, sixty pieces of paper are put into the bucket. Twenty for "On" and forty for "Off". When the
|
||||||
generator is deciding whether or not to turn on map shuffle for your game, it reaches into this bucket and pulls
|
generator is deciding whether or not to turn on map shuffle for your game, it reaches into this bucket and pulls
|
||||||
out a piece of paper at random. In this example, you are much more likely to have map shuffle turned off.
|
out a piece of paper at random. In this example, you are much more likely to have map shuffle turned off.
|
||||||
|
|
||||||
If you never want an option to be chosen, simply set its value to zero.
|
If you never want an option to be chosen, simply set its value to zero. Remember that each setting must have at
|
||||||
|
lease one option set to a number greater than zero.
|
||||||
|
|
||||||
### Verifying your YAML file
|
### Verifying your YAML file
|
||||||
If you would like to validate your YAML file to make sure it works, you may do so on the
|
If you would like to validate your YAML file to make sure it works, you may do so on the
|
||||||
[YAML Validator](/mysterycheck) page.
|
[YAML Validator](/mysterycheck) page.
|
||||||
|
|
||||||
## Generating a Single-Player Game
|
## Generating a Single-Player Game
|
||||||
1. Navigate to [the Generator Page](/generate) and upload your YAML file.
|
1. Navigate to the [Generate Game](/player-settings), configure your options, and click the "Generate Game" button.
|
||||||
2. You will be presented with a "Seed Info" page, where you can download your patch file.
|
2. You will be presented with a "Seed Info" page, where you can download your patch file.
|
||||||
3. Double-click on your patch file and the emulator should launch with your game automatically. As the
|
3. Double-click on your patch file, and the emulator should launch with your game automatically. As the
|
||||||
Client is unnecessary for single player games, you may close it and the WebUI.
|
Client is unnecessary for single player games, you may close it and the WebUI.
|
||||||
|
|
||||||
## Joining a MultiWorld Game
|
## Joining a MultiWorld Game
|
||||||
|
@ -122,10 +125,6 @@ done so already, please do this now. SD2SNES and FXPak Pro users may download th
|
||||||
[here](https://github.com/RedGuyyyy/sd2snes/releases). Other hardware may find helpful information
|
[here](https://github.com/RedGuyyyy/sd2snes/releases). Other hardware may find helpful information
|
||||||
[on this page](http://usb2snes.com/#supported-platforms).
|
[on this page](http://usb2snes.com/#supported-platforms).
|
||||||
|
|
||||||
**To connect with hardware you must use an old version of QUsb2Snes
|
|
||||||
([v0.7.16](https://github.com/Skarsnik/QUsb2snes/releases/tag/v0.7.16)).**
|
|
||||||
Versions of QUsb2Snes later than this break compatibility with hardware for multiworld.
|
|
||||||
|
|
||||||
1. Close your emulator, which may have auto-launched.
|
1. Close your emulator, which may have auto-launched.
|
||||||
2. Close QUsb2Snes, which launched automatically with the client.
|
2. Close QUsb2Snes, which launched automatically with the client.
|
||||||
3. Launch the appropriate version of QUsb2Snes (v0.7.16).
|
3. Launch the appropriate version of QUsb2Snes (v0.7.16).
|
||||||
|
@ -154,11 +153,30 @@ The recommended way to host a game is to use the hosting service provided on
|
||||||
3. Upload that zip file to the website linked above.
|
3. Upload that zip file to the website linked above.
|
||||||
4. Wait a moment while the seed is generated.
|
4. Wait a moment while the seed is generated.
|
||||||
5. When the seed is generated, you will be redirected to a "Seed Info" page.
|
5. When the seed is generated, you will be redirected to a "Seed Info" page.
|
||||||
6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players
|
6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players,
|
||||||
so they may download their patch files from here.
|
so they may download their patch files from there.
|
||||||
**Note:** The patch files provided on this page will allow players to automatically connect to the server,
|
**Note:** The patch files provided on this page will allow players to automatically connect to the server,
|
||||||
while the patch files on the "Seed Info" page will not.
|
while the patch files on the "Seed Info" page will not.
|
||||||
7. Note that a link to a MultiWorld Tracker is at the top of the room page. You should also provide this link
|
7. Note that a link to a MultiWorld Tracker is at the top of the room page. You should also provide this link
|
||||||
to your players so they can watch the progress of the game. Any observers may also be given the link to
|
to your players, so they can watch the progress of the game. Any observers may also be given the link to
|
||||||
this page.
|
this page.
|
||||||
8. Once all players have joined, you may begin playing.
|
8. Once all players have joined, you may begin playing.
|
||||||
|
|
||||||
|
## Auto-Tracking
|
||||||
|
If you would like to use auto-tracking for your game, several pieces of software provide this functionality.
|
||||||
|
The recommended software for auto-tracking is currently
|
||||||
|
[OpenTracker](https://github.com/trippsc2/OpenTracker/releases).
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
1. Download the appropriate installation file for your computer (Windows users want the `.msi` file).
|
||||||
|
2. During the installation process, you may be asked to install the Microsoft Visual Studio Build Tools. A link
|
||||||
|
to this software is provided during the installation procedure, and it must be installed manually.
|
||||||
|
|
||||||
|
### Enable auto-tracking
|
||||||
|
1. With OpenTracker launched, click the Tracking menu at the top of the window, then choose **AutoTracker...**
|
||||||
|
2. Click the **Get Devices** button
|
||||||
|
3. Select your SNES device from the drop-down list
|
||||||
|
4. If you would like to track small keys and dungeon items, check the box labeled **Race Illegal Tracking**
|
||||||
|
5. Click the **Start Autotracking** button
|
||||||
|
6. Close the AutoTracker window, as it is no longer necessary
|
||||||
|
|
||||||
|
|
|
@ -43,11 +43,12 @@ Cada jugador en una partida de multiworld proveerá su propio fichero YAML. Esta
|
||||||
que cada jugador disfrute de una experiencia personalizada a su gusto, y cada jugador dentro de la misma partida de multiworld puede tener diferentes opciones.
|
que cada jugador disfrute de una experiencia personalizada a su gusto, y cada jugador dentro de la misma partida de multiworld puede tener diferentes opciones.
|
||||||
|
|
||||||
### Donde puedo obtener un fichero YAML?
|
### Donde puedo obtener un fichero YAML?
|
||||||
La página "[Player Settings](/player-settings)" en el sitio web te permite configurar tu configuración personal y
|
La página "[Generate Game](/player-settings)" en el sitio web te permite configurar tu configuración personal y
|
||||||
descargar un fichero "YAML". Puedes tener hasta 3 configuraciones guardadas en esta página.
|
descargar un fichero "YAML".
|
||||||
|
|
||||||
### Tu fichero YAML esta ponderado
|
### Configuración YAML avanzada
|
||||||
La página "Player settings" tiene muchas opciones representadas con controles deslizantes. Esto permite
|
Una version mas avanzada del fichero Yaml puede ser creada usando la pagina ["Weighted settings"](/weighted-settings),
|
||||||
|
la cual te permite tener almacenadas hasta 3 preajustes. La pagina "Weighted Settings" tiene muchas opciones representadas con controles deslizantes. Esto permite
|
||||||
elegir cuan probable los valores de una categoría pueden ser elegidos sobre otros de la misma.
|
elegir cuan probable los valores de una categoría pueden ser elegidos sobre otros de la misma.
|
||||||
|
|
||||||
Por ejemplo, imagina que el generador crea un cubo llamado "map_shuffle", y pone trozos de papel doblado en él por cada sub-opción.
|
Por ejemplo, imagina que el generador crea un cubo llamado "map_shuffle", y pone trozos de papel doblado en él por cada sub-opción.
|
||||||
|
@ -58,16 +59,17 @@ Cuando el generador esta decidiendo si activar o no "map shuffle" para tu partid
|
||||||
meterá la mano en el cubo y sacara un trozo de papel al azar. En este ejemplo,
|
meterá la mano en el cubo y sacara un trozo de papel al azar. En este ejemplo,
|
||||||
es mucho mas probable (2 de cada 3 veces (40/60)) que "map shuffle" esté desactivado.
|
es mucho mas probable (2 de cada 3 veces (40/60)) que "map shuffle" esté desactivado.
|
||||||
|
|
||||||
Si quieres que una opción no pueda ser escogida, simplemente asigna el valor 0 a dicha opción.
|
Si quieres que una opción no pueda ser escogida, simplemente asigna el valor 0 a dicha opción. Recuerda que cada opción debe tener
|
||||||
|
al menos un valor mayor que cero, si no la generación fallará.
|
||||||
|
|
||||||
### Verificando tu archivo YAML
|
### Verificando tu archivo YAML
|
||||||
Si quieres validar que tu fichero YAML para asegurarte que funciona correctamente, puedes hacerlo en la pagina
|
Si quieres validar que tu fichero YAML para asegurarte que funciona correctamente, puedes hacerlo en la pagina
|
||||||
[YAML Validator](/mysterycheck).
|
[YAML Validator](/mysterycheck).
|
||||||
|
|
||||||
## Generar una partida para un jugador
|
## Generar una partida para un jugador
|
||||||
1. Navega a [la pagina Generator](/generate) y carga tu fichero YAML.
|
1. Navega a [la pagina Generate game](/player-settings), configura tus opciones, haz click en el boton "Generate game".
|
||||||
2. Se te redigirá a una pagina "Seed Info", donde puedes descargar tu archivo de parche.
|
2. Se te redigirá a una pagina "Seed Info", donde puedes descargar tu archivo de parche.
|
||||||
3. Haz doble click en tu fichero de parche y el emulador debería ejecutar tu juego automáticamente. Como el
|
3. Haz doble click en tu fichero de parche, y el emulador debería ejecutar tu juego automáticamente. Como el
|
||||||
Cliente no es necesario para partidas de un jugador, puedes cerrarlo junto a la pagina web (que tiene como titulo "Multiworld WebUI") que se ha abierto automáticamente.
|
Cliente no es necesario para partidas de un jugador, puedes cerrarlo junto a la pagina web (que tiene como titulo "Multiworld WebUI") que se ha abierto automáticamente.
|
||||||
|
|
||||||
## Unirse a una partida MultiWorld
|
## Unirse a una partida MultiWorld
|
||||||
|
@ -113,11 +115,6 @@ Esta guía asume que ya has descargado el firmware correcto para tu dispositivo.
|
||||||
Los usuarios de SD2SNES y FXPak Pro pueden descargar el firmware apropiado
|
Los usuarios de SD2SNES y FXPak Pro pueden descargar el firmware apropiado
|
||||||
[aqui](https://github.com/RedGuyyyy/sd2snes/releases). Los usuarios de otros dispositivos pueden encontrar información
|
[aqui](https://github.com/RedGuyyyy/sd2snes/releases). Los usuarios de otros dispositivos pueden encontrar información
|
||||||
[en esta página](http://usb2snes.com/#supported-platforms).
|
[en esta página](http://usb2snes.com/#supported-platforms).
|
||||||
|
|
||||||
**Para conectar con hardware debe usarse una version antigua de QUsb2Snes
|
|
||||||
([v0.7.16](https://github.com/Skarsnik/QUsb2snes/releases/tag/v0.7.16)).**
|
|
||||||
Las versiones mas actuales que esta son incompatibles con hardware para multiworld
|
|
||||||
|
|
||||||
1. Cierra tu emulador, el cual debe haberse autoejecutado.
|
1. Cierra tu emulador, el cual debe haberse autoejecutado.
|
||||||
2. Cierra QUsb2Snes, el cual fue ejecutado junto al cliente.
|
2. Cierra QUsb2Snes, el cual fue ejecutado junto al cliente.
|
||||||
3. Ejecuta la version correcta de QUsb2Snes (v0.7.16).
|
3. Ejecuta la version correcta de QUsb2Snes (v0.7.16).
|
||||||
|
@ -153,3 +150,21 @@ La manera recomendad para hospedar una partida es usar el servicio proveído en
|
||||||
7. Hay un enlace a un MultiWorld Tracker en la parte superior de la pagina de la sala. Deberías pasar también este enlace
|
7. Hay un enlace a un MultiWorld Tracker en la parte superior de la pagina de la sala. Deberías pasar también este enlace
|
||||||
a los jugadores para que puedan ver el progreso de la partida. A los observadores también se les puede pasar este enlace.
|
a los jugadores para que puedan ver el progreso de la partida. A los observadores también se les puede pasar este enlace.
|
||||||
8. Una vez todos los jugadores se han unido, podeis empezar a jugar.
|
8. Una vez todos los jugadores se han unido, podeis empezar a jugar.
|
||||||
|
|
||||||
|
## Auto-Tracking
|
||||||
|
Si deseas usar auto-tracking para tu partida, varios programas ofrecen esta funcionalidad.
|
||||||
|
El programa recomentdado actualmente es:
|
||||||
|
[OpenTracker](https://github.com/trippsc2/OpenTracker/releases).
|
||||||
|
|
||||||
|
### Instalación
|
||||||
|
1. Descarga el fichero de instalacion apropiado para tu ordenador (Usuarios de windows quieren el fichero ".msi").
|
||||||
|
2. Durante el proceso de insatalación, puede que se te pida instalar Microsoft Visual Studio Build Tools. Un enlace
|
||||||
|
este programa se muestra durante la proceso, y debe ser ejecutado manualmente.
|
||||||
|
|
||||||
|
### Activar auto-tracking
|
||||||
|
1. Con OpenTracker ejecutado, haz click en el menu Tracking en la parte superior de la ventana, y elige **AutoTracker...**
|
||||||
|
2. Click the **Get Devices** button
|
||||||
|
3. Selecciona tu "SNES device" de la lista
|
||||||
|
4. Si quieres que las llaves y los objetos de mazmorra tambien sean marcados, activa la caja con nombre **Race Illegal Tracking**
|
||||||
|
5. Haz click en el boton **Start Autotracking**
|
||||||
|
6. Cierra la ventana AutoTracker, ya que deja de ser necesaria
|
|
@ -9,7 +9,7 @@
|
||||||
## Logiciels requis
|
## Logiciels requis
|
||||||
- [Utilitaires du MultiWorld](https://github.com/Berserker66/MultiWorld-Utilities/releases)
|
- [Utilitaires du MultiWorld](https://github.com/Berserker66/MultiWorld-Utilities/releases)
|
||||||
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Inclus dans les utilitaires précédents)
|
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Inclus dans les utilitaires précédents)
|
||||||
- Une solution logicielle ou matérielle capable de charger et de jouer des fichiers ROM de SNES
|
- Une solution logicielle ou matérielle capable de charger et de lancer des fichiers ROM de SNES
|
||||||
- Un émulateur capable d'éxécuter des scripts Lua
|
- Un émulateur capable d'éxécuter des scripts Lua
|
||||||
([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
|
([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
|
||||||
[BizHawk](http://tasvideos.org/BizHawk.html))
|
[BizHawk](http://tasvideos.org/BizHawk.html))
|
||||||
|
@ -21,17 +21,17 @@
|
||||||
### Installation sur Windows
|
### Installation sur Windows
|
||||||
1. Téléchargez et installez les utilitaires du MultiWorld à l'aide du lien au-dessus, faites attention à bien installer la version la plus récente.
|
1. Téléchargez et installez les utilitaires du MultiWorld à l'aide du lien au-dessus, faites attention à bien installer la version la plus récente.
|
||||||
**Le fichier se situe dans la section "assets" en bas des informations de version**. Si vous voulez jouer des parties classiques de multiworld,
|
**Le fichier se situe dans la section "assets" en bas des informations de version**. Si vous voulez jouer des parties classiques de multiworld,
|
||||||
vous voudrez télécharger `Setup.BerserkerMultiWorld.exe`
|
téléchargez `Setup.BerserkerMultiWorld.exe`
|
||||||
- Si vous voulez jouer à la version alternative avec le mélangeur de portes dans les donjons, vous voudrez télécharger le fichier
|
- Si vous voulez jouer à la version alternative avec le mélangeur de portes dans les donjons, vous téléchargez le fichier
|
||||||
`Setup.BerserkerMultiWorld.Doors.exe`.
|
`Setup.BerserkerMultiWorld.Doors.exe`.
|
||||||
- Durant le processus d'installation, il vous sera demandé de localiser votre ROM v1.0 japonaise. Si vous avez déjà installé le logiciel
|
- Durant le processus d'installation, il vous sera demandé de localiser votre ROM v1.0 japonaise. Si vous avez déjà installé le logiciel
|
||||||
auparavant et qu'il s'agit simplement d'une mise à jour, la localisation de la ROM originale ne sera pas requise.
|
auparavant et qu'il s'agit simplement d'une mise à jour, la localisation de la ROM originale ne sera pas requise.
|
||||||
- Il vous sera peut-être également demandé d'installer Microsoft Visual C++. Si vous le possédez déjà (possiblement parce qu'un
|
- Il vous sera peut-être également demandé d'installer Microsoft Visual C++. Si vous le possédez déjà (possiblement parce qu'un
|
||||||
jeu Steam l'a déjà installé), l'installateur ne reproposera pas de l'installer.
|
jeu Steam l'a déjà installé), l'installateur ne reproposera pas de l'installer.
|
||||||
|
|
||||||
2. Si vous utilisez un émulateur, vous devriez assigner votre émulateur capable d'éxécuter des scripts Lua comme programme
|
2. Si vous utilisez un émulateur, il est recommandé d'assigner votre émulateur capable d'éxécuter des scripts Lua comme programme
|
||||||
par défaut pour ouvrir vos ROMs.
|
par défaut pour ouvrir vos ROMs.
|
||||||
1. Extrayez votre dossier d'émulateur sur votre Bureau, ou quelque part dont vous vous souviendrez.
|
1. Extrayez votre dossier d'émulateur sur votre Bureau, ou à un endroit dont vous vous souviendrez.
|
||||||
2. Faites un clic droit sur un fichier ROM et sélectionnez **Ouvrir avec...**
|
2. Faites un clic droit sur un fichier ROM et sélectionnez **Ouvrir avec...**
|
||||||
3. Cochez la case à côté de **Toujours utiliser cette application pour ouvrir les fichiers .sfc**
|
3. Cochez la case à côté de **Toujours utiliser cette application pour ouvrir les fichiers .sfc**
|
||||||
4. Descendez jusqu'en bas de la liste et sélectionnez **Rechercher une autre application sur ce PC**
|
4. Descendez jusqu'en bas de la liste et sélectionnez **Rechercher une autre application sur ce PC**
|
||||||
|
@ -49,14 +49,12 @@ sur comment il devrait générer votre seed. Chaque joueur d'un multiwolrd devra
|
||||||
joueur d'apprécier une expérience customisée selon ses goûts, et les différents joueurs d'un même multiworld peuvent avoir différentes options.
|
joueur d'apprécier une expérience customisée selon ses goûts, et les différents joueurs d'un même multiworld peuvent avoir différentes options.
|
||||||
|
|
||||||
### Où est-ce que j'obtiens un fichier YAML ?
|
### Où est-ce que j'obtiens un fichier YAML ?
|
||||||
Un fichier YAML de base est disponible dans le dossier où les utilitaires du MultiWorld sont installés. Il est situé dans le dossier
|
La page [Génération de partie](/player-settings) vous permet de configurer vos paramètres personnels et de les exporter vers un fichier YAML.
|
||||||
`players` et se nomme `easy.yaml`
|
|
||||||
La page des [paramètres du joueur](/player-settings) vous permet de configurer vos paramètres personnels et de télécharger un fichier `yaml`.
|
|
||||||
Vous pouvez configurez jusqu'à trois pré-paramétrages sur cette page.
|
|
||||||
|
|
||||||
### Votre fichier YAML est pondéré
|
### Configuration avancée du fichier YAML
|
||||||
La page de paramétrage a de nombreuses options qui sont essentiellement représentées avec des curseurs glissants. Cela vous permet de choisir quelles
|
Une version plus avancée du fichier YAML peut être créée en utilisant la page des [paramètres de pondération](/weighted-settings), qui vous permet
|
||||||
sont les chances qu'une certaine option apparaisse par rapport aux autres disponibles.
|
de configurer jusqu'à trois préréglages. Cette page a de nombreuses options qui sont essentiellement représentées avec des curseurs glissants.
|
||||||
|
Cela vous permet de choisir quelles sont les chances qu'une certaine option apparaisse par rapport aux autres disponibles dans une même catégorie.
|
||||||
|
|
||||||
Par exemple, imaginez que le générateur crée un seau étiqueté "Mélange des cartes", et qu'il place un morceau de papier
|
Par exemple, imaginez que le générateur crée un seau étiqueté "Mélange des cartes", et qu'il place un morceau de papier
|
||||||
pour chaque sous-option. Imaginez également que la valeur pour "On" est 20 et la valeur pour "Off" est 40.
|
pour chaque sous-option. Imaginez également que la valeur pour "On" est 20 et la valeur pour "Off" est 40.
|
||||||
|
@ -65,14 +63,15 @@ Dans cet exemple, il y a soixante morceaux de papier dans le seau : vingt pour "
|
||||||
décide s'il doit oui ou non activer le mélange des cartes pour votre partie, , il tire aléatoirement un papier dans le seau.
|
décide s'il doit oui ou non activer le mélange des cartes pour votre partie, , il tire aléatoirement un papier dans le seau.
|
||||||
Dans cet exemple, il y a de plus grandes chances d'avoir le mélange de cartes désactivé.
|
Dans cet exemple, il y a de plus grandes chances d'avoir le mélange de cartes désactivé.
|
||||||
|
|
||||||
S'il y a une option dont vous ne voulez jamais, mettez simplement sa valeur à zéro.
|
S'il y a une option dont vous ne voulez jamais, mettez simplement sa valeur à zéro. N'oubliez pas qu'il faut que pour chaque paramètre il faut
|
||||||
|
au moins une option qui soit paramétrée sur un nombre strictement positif.
|
||||||
|
|
||||||
### Vérifier son fichier YAML
|
### Vérifier son fichier YAML
|
||||||
Si vous voulez valider votre fichier YAML pour être sûr qu'il fonctionne, vous pouvez le vérifier sur la page du
|
Si vous voulez valider votre fichier YAML pour être sûr qu'il fonctionne, vous pouvez le vérifier sur la page du
|
||||||
[Validateur de YAML](/mysterycheck).
|
[Validateur de YAML](/mysterycheck).
|
||||||
|
|
||||||
## Générer une partie pour un joueur
|
## Générer une partie pour un joueur
|
||||||
1. Aller sur la [page du générateur](/generate) et téléversez votre fichier YAML.
|
1. Aller sur la page [Génération de partie](/player-settings), configurez vos options, et cliquez sur le bouton "Generate Game".
|
||||||
2. Il vous sera alors présenté une page d'informations sur la seed, où vous pourrez télécharger votre patch.
|
2. Il vous sera alors présenté une page d'informations sur la seed, où vous pourrez télécharger votre patch.
|
||||||
3. Double-cliquez sur le patch et l'émulateur devrait se lancer automatiquement avec la seed. Etant donné que le client
|
3. Double-cliquez sur le patch et l'émulateur devrait se lancer automatiquement avec la seed. Etant donné que le client
|
||||||
n'est pas requis pour les parties à un joueur, vous pouvez le fermer ainsi que l'interface Web (WebUI).
|
n'est pas requis pour les parties à un joueur, vous pouvez le fermer ainsi que l'interface Web (WebUI).
|
||||||
|
@ -120,10 +119,6 @@ Les utilisateurs de SD2SNES et de FXPak Pro peuvent télécharger le micro-logic
|
||||||
[ici](https://github.com/RedGuyyyy/sd2snes/releases). Pour les autres solutions, de l'aide peut être trouvée
|
[ici](https://github.com/RedGuyyyy/sd2snes/releases). Pour les autres solutions, de l'aide peut être trouvée
|
||||||
[sur cette page](http://usb2snes.com/#supported-platforms).
|
[sur cette page](http://usb2snes.com/#supported-platforms).
|
||||||
|
|
||||||
**Pour vous connecter avec une solution matérielle vous devez utiliser une ancienne version de QUsb2Snes
|
|
||||||
([v0.7.16](https://github.com/Skarsnik/QUsb2snes/releases/tag/v0.7.16)).**
|
|
||||||
Les versions postérieures brisent la compatibilité avec le multiworld.
|
|
||||||
|
|
||||||
1. Fermez votre émulateur, qui s'est potentiellement lancé automatiquement.
|
1. Fermez votre émulateur, qui s'est potentiellement lancé automatiquement.
|
||||||
2. Fermez QUsb2Snes, qui s'est lancé automatiquement avec le client.
|
2. Fermez QUsb2Snes, qui s'est lancé automatiquement avec le client.
|
||||||
3. Lancez la version appropriée de QUsb2Snes (v0.7.16).
|
3. Lancez la version appropriée de QUsb2Snes (v0.7.16).
|
||||||
|
@ -149,13 +144,31 @@ La méthode recommandée pour héberger une partie est d'utiliser le service d'h
|
||||||
|
|
||||||
1. Récupérez les fichiers YAML des joueurs.
|
1. Récupérez les fichiers YAML des joueurs.
|
||||||
2. Créez une archive zip contenant ces fichiers YAML.
|
2. Créez une archive zip contenant ces fichiers YAML.
|
||||||
3. Téléversez l'archive zip sur le lien au-dessus.
|
3. Téléversez l'archive zip sur le lien ci-dessus.
|
||||||
4. Attendez un moment que les seed soient générées.
|
4. Attendez un moment que les seed soient générées.
|
||||||
5. Lorsque les seeds sont générées, vous serez redirigé vers une page d'informations.
|
5. Lorsque les seeds sont générées, vous serez redirigé vers une page d'informations "Seed Info".
|
||||||
6. Cliquez sur "Create New Room". Cela vous amènera à la page du serveur. Fournissez le lien de cette page aux autres joueurs
|
6. Cliquez sur "Create New Room". Cela vous amènera à la page du serveur. Fournissez le lien de cette page aux autres joueurs
|
||||||
afin qu'ils puissent récupérer leurs patchs.
|
afin qu'ils puissent récupérer leurs patchs.
|
||||||
**Note:** Les patchs fournis sur cette page permettront aux joueurs de se connecteur automatiquement au serveur,
|
**Note:** Les patchs fournis sur cette page permettront aux joueurs de se connecteur automatiquement au serveur,
|
||||||
tandis que ceux de la page "Seed Info" non.
|
tandis que ceux de la page "Seed Info" non.
|
||||||
7. Remarquez qu'un lien vers le traqueur du MultiWorld est en haut de la page de la salle. Vous devriez également fournir ce lien aux joueurs
|
7. Remarquez qu'un lien vers le traqueur du MultiWorld est en haut de la page de la salle. Vous devriez également fournir ce lien aux joueurs
|
||||||
pour qu'ils puissent la progression de la partie. N'importe quel personne voulant observer devrait avoir accès à ce lien.
|
pour qu'ils puissent suivre la progression de la partie. N'importe quel personne voulant observer devrait avoir accès à ce lien.
|
||||||
8. Une fois que tous les joueurs ont rejoint, vous pouvez commencer à jouer.
|
8. Une fois que tous les joueurs ont rejoint, vous pouvez commencer à jouer.
|
||||||
|
|
||||||
|
## Auto-tracking
|
||||||
|
Si vous voulez utiliser l'auto-tracking, plusieurs logiciels offrent cette possibilité.
|
||||||
|
Le logiciel recommandé pour l'auto-tracking actuellement est
|
||||||
|
[OpenTracker](https://github.com/trippsc2/OpenTracker/releases).
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
1. Téléchargez le fichier d'installation approprié pour votre ordinateur (Les utilisateurs Windows voudront le fichier `.msi`).
|
||||||
|
2. Durant le processus d'installation, il vous sera peut-être demandé d'installer les outils "Microsoft Visual Studio Build Tools". Un
|
||||||
|
lien est fourni durant l'installation d'OpenTracker, et celle des outils doit se faire manuellement.
|
||||||
|
|
||||||
|
### Activer l'auto-tracking
|
||||||
|
1. Une fois OpenTracker démarré, cliquez sur le menu "Tracking" en haut de la fenêtre, puis choisissez **AutoTracker...**
|
||||||
|
2. Appuyez sur le bouton **Get Devices**
|
||||||
|
3. Sélectionnez votre appareil SNES dans la liste déroulante.
|
||||||
|
4. Si vous voulez tracquer les petites clés ainsi que les objets des donjons, cochez la case **Race Illegal Tracking**
|
||||||
|
5. Cliquez sur le bouton **Start Autotracking**
|
||||||
|
6. Fermez la fenêtre "AutoTracker" maintenant, elle n'est plus nécessaire
|
|
@ -1,17 +0,0 @@
|
||||||
window.addEventListener('load', () => {
|
|
||||||
document.getElementById('upload-button').addEventListener('click', () => {
|
|
||||||
document.getElementById('file-input').click();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('file-input').addEventListener('change', () => {
|
|
||||||
document.getElementById('upload-form').submit();
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#uploads-table").DataTable({
|
|
||||||
"paging": false,
|
|
||||||
"ordering": true,
|
|
||||||
"order": [[ 3, "desc" ]],
|
|
||||||
"info": false,
|
|
||||||
"dom": "t",
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
console.log("loaded");
|
||||||
|
$("#rooms-table").DataTable({
|
||||||
|
"paging": false,
|
||||||
|
"ordering": true,
|
||||||
|
"order": [[ 3, "desc" ]],
|
||||||
|
"info": false,
|
||||||
|
"dom": "t",
|
||||||
|
});
|
||||||
|
$("#seeds-table").DataTable({
|
||||||
|
"paging": false,
|
||||||
|
"ordering": true,
|
||||||
|
"order": [[ 2, "desc" ]],
|
||||||
|
"info": false,
|
||||||
|
"dom": "t",
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,23 +1,20 @@
|
||||||
let spriteData = null;
|
let spriteData = null;
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
const gameSettings = document.getElementById('game-settings');
|
const gameSettings = document.getElementById('weighted-settings');
|
||||||
Promise.all([fetchPlayerSettingsYaml(), fetchPlayerSettingsJson(), fetchSpriteData()]).then((results) => {
|
Promise.all([fetchPlayerSettingsYaml(), fetchPlayerSettingsJson(), fetchSpriteData()]).then((results) => {
|
||||||
// Load YAML into object
|
// Load YAML into object
|
||||||
const sourceData = jsyaml.safeLoad(results[0], { json: true });
|
const sourceData = jsyaml.safeLoad(results[0], { json: true });
|
||||||
|
|
||||||
// Update localStorage with three settings objects. Preserve original objects if present.
|
// Update localStorage with three settings objects. Preserve original objects if present.
|
||||||
for (let i=1; i<=3; i++) {
|
for (let i=1; i<=3; i++) {
|
||||||
const localSettings = JSON.parse(localStorage.getItem(`playerSettings${i}`));
|
const localSettings = JSON.parse(localStorage.getItem(`weightedSettings${i}`));
|
||||||
const updatedObj = localSettings ? Object.assign(sourceData, localSettings) : sourceData;
|
const updatedObj = localSettings ? Object.assign(sourceData, localSettings) : sourceData;
|
||||||
localStorage.setItem(`playerSettings${i}`, JSON.stringify(updatedObj));
|
localStorage.setItem(`weightedSettings${i}`, JSON.stringify(updatedObj));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse spriteData into useful sets
|
|
||||||
spriteData = JSON.parse(results[2]);
|
|
||||||
|
|
||||||
// Build the entire UI
|
// Build the entire UI
|
||||||
buildUI(JSON.parse(results[1]));
|
buildUI(JSON.parse(results[1]), JSON.parse(results[2]));
|
||||||
|
|
||||||
// Populate the UI and add event listeners
|
// Populate the UI and add event listeners
|
||||||
populateSettings();
|
populateSettings();
|
||||||
|
@ -27,13 +24,17 @@ window.addEventListener('load', () => {
|
||||||
|
|
||||||
document.getElementById('export-button').addEventListener('click', exportSettings);
|
document.getElementById('export-button').addEventListener('click', exportSettings);
|
||||||
document.getElementById('reset-to-default').addEventListener('click', resetToDefaults);
|
document.getElementById('reset-to-default').addEventListener('click', resetToDefaults);
|
||||||
|
adjustHeaderWidth();
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
|
console.error(error);
|
||||||
gameSettings.innerHTML = `
|
gameSettings.innerHTML = `
|
||||||
<h2>Something went wrong while loading your game settings page.</h2>
|
<h2>Something went wrong while loading your game settings page.</h2>
|
||||||
<h2>${error}</h2>
|
<h2>${error}</h2>
|
||||||
<h2><a href="${window.location.origin}">Click here to return to safety!</a></h2>
|
<h2><a href="${window.location.origin}">Click here to return to safety!</a></h2>
|
||||||
`
|
`
|
||||||
});
|
});
|
||||||
|
document.getElementById('generate-game').addEventListener('click', () => generateGame());
|
||||||
|
document.getElementById('generate-race').addEventListener('click', () => generateGame(true));
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchPlayerSettingsYaml = () => new Promise((resolve, reject) => {
|
const fetchPlayerSettingsYaml = () => new Promise((resolve, reject) => {
|
||||||
|
@ -46,7 +47,7 @@ const fetchPlayerSettingsYaml = () => new Promise((resolve, reject) => {
|
||||||
}
|
}
|
||||||
resolve(ajax.responseText);
|
resolve(ajax.responseText);
|
||||||
};
|
};
|
||||||
ajax.open('GET', `${window.location.origin}/static/static/playerSettings.yaml` ,true);
|
ajax.open('GET', `${window.location.origin}/static/static/weightedSettings.yaml` ,true);
|
||||||
ajax.send();
|
ajax.send();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -60,7 +61,7 @@ const fetchPlayerSettingsJson = () => new Promise((resolve, reject) => {
|
||||||
}
|
}
|
||||||
resolve(ajax.responseText);
|
resolve(ajax.responseText);
|
||||||
};
|
};
|
||||||
ajax.open('GET', `${window.location.origin}/static/static/playerSettings.json`, true);
|
ajax.open('GET', `${window.location.origin}/static/static/weightedSettings.json`, true);
|
||||||
ajax.send();
|
ajax.send();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -81,7 +82,7 @@ const fetchSpriteData = () => new Promise((resolve, reject) => {
|
||||||
const handleOptionChange = (event) => {
|
const handleOptionChange = (event) => {
|
||||||
if(!event.target.matches('.setting')) { return; }
|
if(!event.target.matches('.setting')) { return; }
|
||||||
const presetNumber = document.getElementById('preset-number').value;
|
const presetNumber = document.getElementById('preset-number').value;
|
||||||
const settings = JSON.parse(localStorage.getItem(`playerSettings${presetNumber}`))
|
const settings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`))
|
||||||
const settingString = event.target.getAttribute('data-setting');
|
const settingString = event.target.getAttribute('data-setting');
|
||||||
document.getElementById(settingString).innerText = event.target.value;
|
document.getElementById(settingString).innerText = event.target.value;
|
||||||
if(getSettingValue(settings, settingString) !== false){
|
if(getSettingValue(settings, settingString) !== false){
|
||||||
|
@ -105,7 +106,7 @@ const handleOptionChange = (event) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the updated settings object bask to localStorage
|
// Save the updated settings object bask to localStorage
|
||||||
localStorage.setItem(`playerSettings${presetNumber}`, JSON.stringify(settings));
|
localStorage.setItem(`weightedSettings${presetNumber}`, JSON.stringify(settings));
|
||||||
}else{
|
}else{
|
||||||
console.warn(`Unknown setting string received: ${settingString}`)
|
console.warn(`Unknown setting string received: ${settingString}`)
|
||||||
}
|
}
|
||||||
|
@ -113,7 +114,7 @@ const handleOptionChange = (event) => {
|
||||||
|
|
||||||
const populateSettings = () => {
|
const populateSettings = () => {
|
||||||
const presetNumber = document.getElementById('preset-number').value;
|
const presetNumber = document.getElementById('preset-number').value;
|
||||||
const settings = JSON.parse(localStorage.getItem(`playerSettings${presetNumber}`))
|
const settings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`))
|
||||||
const settingsInputs = Array.from(document.querySelectorAll('.setting'));
|
const settingsInputs = Array.from(document.querySelectorAll('.setting'));
|
||||||
settingsInputs.forEach((input) => {
|
settingsInputs.forEach((input) => {
|
||||||
const settingString = input.getAttribute('data-setting');
|
const settingString = input.getAttribute('data-setting');
|
||||||
|
@ -146,13 +147,13 @@ const getSettingValue = (settings, keyString) => {
|
||||||
|
|
||||||
const exportSettings = () => {
|
const exportSettings = () => {
|
||||||
const presetNumber = document.getElementById('preset-number').value;
|
const presetNumber = document.getElementById('preset-number').value;
|
||||||
const settings = JSON.parse(localStorage.getItem(`playerSettings${presetNumber}`));
|
const settings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`));
|
||||||
const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
|
const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
|
||||||
download(`${settings.description}.yaml`, yamlText);
|
download(`${settings.description}.yaml`, yamlText);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetToDefaults = () => {
|
const resetToDefaults = () => {
|
||||||
[1, 2, 3].forEach((presetNumber) => localStorage.removeItem(`playerSettings${presetNumber}`));
|
[1, 2, 3].forEach((presetNumber) => localStorage.removeItem(`weightedSettings${presetNumber}`));
|
||||||
location.reload();
|
location.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -167,7 +168,7 @@ const download = (filename, text) => {
|
||||||
document.body.removeChild(downloadLink);
|
document.body.removeChild(downloadLink);
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildUI = (settings) => {
|
const buildUI = (settings, spriteData) => {
|
||||||
const settingsWrapper = document.getElementById('settings-wrapper');
|
const settingsWrapper = document.getElementById('settings-wrapper');
|
||||||
const settingTypes = {
|
const settingTypes = {
|
||||||
gameOptions: 'Game Options',
|
gameOptions: 'Game Options',
|
||||||
|
@ -175,7 +176,7 @@ const buildUI = (settings) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.keys(settingTypes).forEach((settingTypeKey) => {
|
Object.keys(settingTypes).forEach((settingTypeKey) => {
|
||||||
const sectionHeader = document.createElement('h1');
|
const sectionHeader = document.createElement('h2');
|
||||||
sectionHeader.innerText = settingTypes[settingTypeKey];
|
sectionHeader.innerText = settingTypes[settingTypeKey];
|
||||||
settingsWrapper.appendChild(sectionHeader);
|
settingsWrapper.appendChild(sectionHeader);
|
||||||
|
|
||||||
|
@ -200,7 +201,7 @@ const buildUI = (settings) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build sprite options
|
// Build sprite options
|
||||||
const spriteOptionsHeader = document.createElement('h1');
|
const spriteOptionsHeader = document.createElement('h2');
|
||||||
spriteOptionsHeader.innerText = 'Sprite Options';
|
spriteOptionsHeader.innerText = 'Sprite Options';
|
||||||
settingsWrapper.appendChild(spriteOptionsHeader);
|
settingsWrapper.appendChild(spriteOptionsHeader);
|
||||||
|
|
||||||
|
@ -224,7 +225,7 @@ const buildUI = (settings) => {
|
||||||
tbody.setAttribute('id', 'sprites-tbody');
|
tbody.setAttribute('id', 'sprites-tbody');
|
||||||
|
|
||||||
const currentPreset = document.getElementById('preset-number').value;
|
const currentPreset = document.getElementById('preset-number').value;
|
||||||
const playerSettings = JSON.parse(localStorage.getItem(`playerSettings${currentPreset}`));
|
const playerSettings = JSON.parse(localStorage.getItem(`weightedSettings${currentPreset}`));
|
||||||
|
|
||||||
// Manually add a row for random sprites
|
// Manually add a row for random sprites
|
||||||
addSpriteRow(tbody, playerSettings, 'random');
|
addSpriteRow(tbody, playerSettings, 'random');
|
||||||
|
@ -241,7 +242,7 @@ const buildUI = (settings) => {
|
||||||
settingsWrapper.appendChild(spriteOptionsWrapper);
|
settingsWrapper.appendChild(spriteOptionsWrapper);
|
||||||
|
|
||||||
// Append sprite picker
|
// Append sprite picker
|
||||||
settingsWrapper.appendChild(buildSpritePicker());
|
settingsWrapper.appendChild(buildSpritePicker(spriteData));
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildRangeSettings = (parentElement, settings) => {
|
const buildRangeSettings = (parentElement, settings) => {
|
||||||
|
@ -368,7 +369,7 @@ const addSpriteRow = (tbody, playerSettings, spriteName) => {
|
||||||
|
|
||||||
const addSpriteOption = (event) => {
|
const addSpriteOption = (event) => {
|
||||||
const presetNumber = document.getElementById('preset-number').value;
|
const presetNumber = document.getElementById('preset-number').value;
|
||||||
const playerSettings = JSON.parse(localStorage.getItem(`playerSettings${presetNumber}`));
|
const playerSettings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`));
|
||||||
const spriteName = event.target.getAttribute('data-sprite');
|
const spriteName = event.target.getAttribute('data-sprite');
|
||||||
console.log(event.target);
|
console.log(event.target);
|
||||||
console.log(spriteName);
|
console.log(spriteName);
|
||||||
|
@ -380,7 +381,7 @@ const addSpriteOption = (event) => {
|
||||||
|
|
||||||
// Add option to playerSettings object
|
// Add option to playerSettings object
|
||||||
playerSettings.rom.sprite[event.target.getAttribute('data-sprite')] = 50;
|
playerSettings.rom.sprite[event.target.getAttribute('data-sprite')] = 50;
|
||||||
localStorage.setItem(`playerSettings${presetNumber}`, JSON.stringify(playerSettings));
|
localStorage.setItem(`weightedSettings${presetNumber}`, JSON.stringify(playerSettings));
|
||||||
|
|
||||||
// Add <tr> to #sprite-options-table
|
// Add <tr> to #sprite-options-table
|
||||||
const tbody = document.getElementById('sprites-tbody');
|
const tbody = document.getElementById('sprites-tbody');
|
||||||
|
@ -389,19 +390,19 @@ const addSpriteOption = (event) => {
|
||||||
|
|
||||||
const removeSpriteOption = (event) => {
|
const removeSpriteOption = (event) => {
|
||||||
const presetNumber = document.getElementById('preset-number').value;
|
const presetNumber = document.getElementById('preset-number').value;
|
||||||
const playerSettings = JSON.parse(localStorage.getItem(`playerSettings${presetNumber}`));
|
const playerSettings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`));
|
||||||
const spriteName = event.target.getAttribute('data-sprite');
|
const spriteName = event.target.getAttribute('data-sprite');
|
||||||
|
|
||||||
// Remove option from playerSettings object
|
// Remove option from playerSettings object
|
||||||
delete playerSettings.rom.sprite[spriteName];
|
delete playerSettings.rom.sprite[spriteName];
|
||||||
localStorage.setItem(`playerSettings${presetNumber}`, JSON.stringify(playerSettings));
|
localStorage.setItem(`weightedSettings${presetNumber}`, JSON.stringify(playerSettings));
|
||||||
|
|
||||||
// Remove <tr> from #sprite-options-table
|
// Remove <tr> from #sprite-options-table
|
||||||
const tr = document.getElementById(event.target.getAttribute('data-row-id'));
|
const tr = document.getElementById(event.target.getAttribute('data-row-id'));
|
||||||
tr.parentNode.removeChild(tr);
|
tr.parentNode.removeChild(tr);
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildSpritePicker = () => {
|
const buildSpritePicker = (spriteData) => {
|
||||||
const spritePicker = document.createElement('div');
|
const spritePicker = document.createElement('div');
|
||||||
spritePicker.setAttribute('id', 'sprite-picker');
|
spritePicker.setAttribute('id', 'sprite-picker');
|
||||||
|
|
||||||
|
@ -412,18 +413,18 @@ const buildSpritePicker = () => {
|
||||||
|
|
||||||
const sprites = document.createElement('div');
|
const sprites = document.createElement('div');
|
||||||
sprites.setAttribute('id', 'sprite-picker-sprites');
|
sprites.setAttribute('id', 'sprite-picker-sprites');
|
||||||
Object.keys(spriteData).forEach((spriteName) => {
|
spriteData.sprites.forEach((sprite) => {
|
||||||
const spriteImg = document.createElement('img');
|
const spriteImg = document.createElement('img');
|
||||||
spriteImg.setAttribute('src', `static/static/sprites/${spriteName}.gif`);
|
spriteImg.setAttribute('src', `static/static/sprites/${sprite.name}.gif`);
|
||||||
spriteImg.setAttribute('data-sprite', spriteName);
|
spriteImg.setAttribute('data-sprite', sprite.name);
|
||||||
spriteImg.setAttribute('alt', spriteName);
|
spriteImg.setAttribute('alt', sprite.name);
|
||||||
|
|
||||||
// Wrap the image in a span to allow for tooltip presence
|
// Wrap the image in a span to allow for tooltip presence
|
||||||
const imgWrapper = document.createElement('span');
|
const imgWrapper = document.createElement('span');
|
||||||
imgWrapper.className = 'sprite-img-wrapper';
|
imgWrapper.className = 'sprite-img-wrapper';
|
||||||
imgWrapper.setAttribute('data-tooltip', spriteName);
|
imgWrapper.setAttribute('data-tooltip', `${sprite.name}${sprite.author ? `, by ${sprite.author}` : ''}`);
|
||||||
imgWrapper.appendChild(spriteImg);
|
imgWrapper.appendChild(spriteImg);
|
||||||
imgWrapper.setAttribute('data-sprite', spriteName);
|
imgWrapper.setAttribute('data-sprite', sprite.name);
|
||||||
sprites.appendChild(imgWrapper);
|
sprites.appendChild(imgWrapper);
|
||||||
imgWrapper.addEventListener('click', addSpriteOption);
|
imgWrapper.addEventListener('click', addSpriteOption);
|
||||||
});
|
});
|
||||||
|
@ -431,3 +432,15 @@ const buildSpritePicker = () => {
|
||||||
spritePicker.appendChild(sprites);
|
spritePicker.appendChild(sprites);
|
||||||
return spritePicker;
|
return spritePicker;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const generateGame = (raceMode = false) => {
|
||||||
|
const presetNumber = document.getElementById('preset-number').value;
|
||||||
|
axios.post('/api/generate', {
|
||||||
|
weights: { player: localStorage.getItem(`weightedSettings${presetNumber}`) },
|
||||||
|
presetData: { player: localStorage.getItem(`weightedSettings${presetNumber}`) },
|
||||||
|
playerCount: 1,
|
||||||
|
race: raceMode ? '1' : '0',
|
||||||
|
}).then((response) => {
|
||||||
|
window.location.href = response.data.url;
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,4 @@
|
||||||
|
Copyright 2020 Berserker66 (Fabian Dill)
|
||||||
|
Copyright 2020 LegendaryLinux (Chris Wilson)
|
||||||
|
|
||||||
|
All rights reserved.
|
After Width: | Height: | Size: 78 KiB |
After Width: | Height: | Size: 71 KiB |
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 57 KiB |
After Width: | Height: | Size: 64 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 9.5 KiB |
After Width: | Height: | Size: 6.7 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 9.8 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 39 KiB |
After Width: | Height: | Size: 39 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 250 KiB |
After Width: | Height: | Size: 210 KiB |
After Width: | Height: | Size: 292 KiB |
After Width: | Height: | Size: 162 KiB |
After Width: | Height: | Size: 161 KiB |
After Width: | Height: | Size: 163 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 541 B |
Before Width: | Height: | Size: 541 B After Width: | Height: | Size: 541 B |
After Width: | Height: | Size: 541 B |
After Width: | Height: | Size: 541 B |
After Width: | Height: | Size: 541 B |
After Width: | Height: | Size: 541 B |
Before Width: | Height: | Size: 541 B After Width: | Height: | Size: 541 B |
After Width: | Height: | Size: 541 B |
After Width: | Height: | Size: 541 B |
After Width: | Height: | Size: 541 B |
After Width: | Height: | Size: 541 B |