commit
baab6a76ef
|
@ -2,7 +2,7 @@ import os
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from Utils import output_path, parse_names_string
|
from Utils import output_path
|
||||||
from Rom import LocalRom, apply_rom_settings
|
from Rom import LocalRom, apply_rom_settings
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ def adjust(args):
|
||||||
else:
|
else:
|
||||||
raise RuntimeError('Provided Rom is not a valid Link to the Past Randomizer Rom. Please provide one for adjusting.')
|
raise RuntimeError('Provided Rom is not a valid Link to the Past Randomizer Rom. Please provide one for adjusting.')
|
||||||
|
|
||||||
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, args.sprite, args.ow_palettes, args.uw_palettes, parse_names_string(args.names))
|
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, args.sprite, args.ow_palettes, args.uw_palettes)
|
||||||
|
|
||||||
rom.write_to_file(output_path('%s.sfc' % outfilebase))
|
rom.write_to_file(output_path('%s.sfc' % outfilebase))
|
||||||
|
|
||||||
|
|
117
BaseClasses.py
117
BaseClasses.py
|
@ -11,6 +11,7 @@ class World(object):
|
||||||
|
|
||||||
def __init__(self, players, shuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, goal, algorithm, accessibility, shuffle_ganon, retro, custom, customitemarray, hints):
|
def __init__(self, players, shuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, goal, algorithm, accessibility, shuffle_ganon, retro, custom, customitemarray, hints):
|
||||||
self.players = players
|
self.players = players
|
||||||
|
self.teams = 1
|
||||||
self.shuffle = shuffle.copy()
|
self.shuffle = shuffle.copy()
|
||||||
self.logic = logic.copy()
|
self.logic = logic.copy()
|
||||||
self.mode = mode.copy()
|
self.mode = mode.copy()
|
||||||
|
@ -58,6 +59,7 @@ class World(object):
|
||||||
def set_player_attr(attr, val):
|
def set_player_attr(attr, val):
|
||||||
self.__dict__.setdefault(attr, {})[player] = val
|
self.__dict__.setdefault(attr, {})[player] = val
|
||||||
set_player_attr('_region_cache', {})
|
set_player_attr('_region_cache', {})
|
||||||
|
set_player_attr('player_names', [])
|
||||||
set_player_attr('required_medallions', ['Ether', 'Quake'])
|
set_player_attr('required_medallions', ['Ether', 'Quake'])
|
||||||
set_player_attr('swamp_patch_required', False)
|
set_player_attr('swamp_patch_required', False)
|
||||||
set_player_attr('powder_patch_required', False)
|
set_player_attr('powder_patch_required', False)
|
||||||
|
@ -90,6 +92,12 @@ class World(object):
|
||||||
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)
|
||||||
|
|
||||||
|
def get_name_string_for_object(self, obj):
|
||||||
|
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})'
|
||||||
|
|
||||||
|
def get_player_names(self, player):
|
||||||
|
return ", ".join([name for i, name in enumerate(self.player_names[player]) if self.player_names[player].index(name) == i])
|
||||||
|
|
||||||
def initialize_regions(self, regions=None):
|
def initialize_regions(self, regions=None):
|
||||||
for region in regions if regions else self.regions:
|
for region in regions if regions else self.regions:
|
||||||
region.world = self
|
region.world = self
|
||||||
|
@ -211,6 +219,7 @@ class World(object):
|
||||||
return [location for location in self.get_locations() if location.item is not None and location.item.name == item and location.item.player == player]
|
return [location for location in self.get_locations() if location.item is not None and location.item.name == item and location.item.player == player]
|
||||||
|
|
||||||
def push_precollected(self, item):
|
def push_precollected(self, item):
|
||||||
|
item.world = self
|
||||||
if (item.smallkey and self.keyshuffle[item.player]) or (item.bigkey and self.bigkeyshuffle[item.player]):
|
if (item.smallkey and self.keyshuffle[item.player]) or (item.bigkey and self.bigkeyshuffle[item.player]):
|
||||||
item.advancement = True
|
item.advancement = True
|
||||||
self.precollected_items.append(item)
|
self.precollected_items.append(item)
|
||||||
|
@ -223,6 +232,7 @@ class World(object):
|
||||||
if location.can_fill(self.state, item, False):
|
if location.can_fill(self.state, item, False):
|
||||||
location.item = item
|
location.item = item
|
||||||
item.location = location
|
item.location = location
|
||||||
|
item.world = self
|
||||||
if collect:
|
if collect:
|
||||||
self.state.collect(item, location.event, location)
|
self.state.collect(item, location.event, location)
|
||||||
|
|
||||||
|
@ -707,10 +717,7 @@ class Region(object):
|
||||||
return str(self.__unicode__())
|
return str(self.__unicode__())
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
if self.world and self.world.players == 1:
|
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
|
||||||
return self.name
|
|
||||||
else:
|
|
||||||
return '%s (Player %d)' % (self.name, self.player)
|
|
||||||
|
|
||||||
|
|
||||||
class Entrance(object):
|
class Entrance(object):
|
||||||
|
@ -746,11 +753,8 @@ class Entrance(object):
|
||||||
return str(self.__unicode__())
|
return str(self.__unicode__())
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
if self.parent_region and self.parent_region.world and self.parent_region.world.players == 1:
|
world = self.parent_region.world if self.parent_region else None
|
||||||
return self.name
|
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
|
||||||
else:
|
|
||||||
return '%s (Player %d)' % (self.name, self.player)
|
|
||||||
|
|
||||||
|
|
||||||
class Dungeon(object):
|
class Dungeon(object):
|
||||||
|
|
||||||
|
@ -787,10 +791,7 @@ class Dungeon(object):
|
||||||
return str(self.__unicode__())
|
return str(self.__unicode__())
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
if self.world and self.world.players==1:
|
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
|
||||||
return self.name
|
|
||||||
else:
|
|
||||||
return '%s (Player %d)' % (self.name, self.player)
|
|
||||||
|
|
||||||
class Boss(object):
|
class Boss(object):
|
||||||
def __init__(self, name, enemizer_name, defeat_rule, player):
|
def __init__(self, name, enemizer_name, defeat_rule, player):
|
||||||
|
@ -833,10 +834,8 @@ class Location(object):
|
||||||
return str(self.__unicode__())
|
return str(self.__unicode__())
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
if self.parent_region and self.parent_region.world and self.parent_region.world.players == 1:
|
world = self.parent_region.world if self.parent_region and self.parent_region.world else None
|
||||||
return self.name
|
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
|
||||||
else:
|
|
||||||
return '%s (Player %d)' % (self.name, self.player)
|
|
||||||
|
|
||||||
|
|
||||||
class Item(object):
|
class Item(object):
|
||||||
|
@ -855,6 +854,7 @@ class Item(object):
|
||||||
self.hint_text = hint_text
|
self.hint_text = hint_text
|
||||||
self.code = code
|
self.code = code
|
||||||
self.location = None
|
self.location = None
|
||||||
|
self.world = None
|
||||||
self.player = player
|
self.player = player
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -881,10 +881,7 @@ class Item(object):
|
||||||
return str(self.__unicode__())
|
return str(self.__unicode__())
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
if self.location and self.location.parent_region and self.location.parent_region.world and self.location.parent_region.world.players == 1:
|
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
|
||||||
return self.name
|
|
||||||
else:
|
|
||||||
return '%s (Player %d)' % (self.name, self.player)
|
|
||||||
|
|
||||||
|
|
||||||
# have 6 address that need to be filled
|
# have 6 address that need to be filled
|
||||||
|
@ -957,6 +954,7 @@ class Spoiler(object):
|
||||||
|
|
||||||
def __init__(self, world):
|
def __init__(self, world):
|
||||||
self.world = world
|
self.world = world
|
||||||
|
self.hashes = {}
|
||||||
self.entrances = OrderedDict()
|
self.entrances = OrderedDict()
|
||||||
self.medallions = {}
|
self.medallions = {}
|
||||||
self.playthrough = {}
|
self.playthrough = {}
|
||||||
|
@ -981,8 +979,8 @@ class Spoiler(object):
|
||||||
self.medallions['Turtle Rock'] = self.world.required_medallions[1][1]
|
self.medallions['Turtle Rock'] = self.world.required_medallions[1][1]
|
||||||
else:
|
else:
|
||||||
for player in range(1, self.world.players + 1):
|
for player in range(1, self.world.players + 1):
|
||||||
self.medallions['Misery Mire (Player %d)' % player] = self.world.required_medallions[player][0]
|
self.medallions[f'Misery Mire ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][0]
|
||||||
self.medallions['Turtle Rock (Player %d)' % player] = self.world.required_medallions[player][1]
|
self.medallions[f'Turtle Rock ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][1]
|
||||||
|
|
||||||
self.startinventory = list(map(str, self.world.precollected_items))
|
self.startinventory = list(map(str, self.world.precollected_items))
|
||||||
|
|
||||||
|
@ -1075,7 +1073,8 @@ class Spoiler(object):
|
||||||
'enemy_shuffle': self.world.enemy_shuffle,
|
'enemy_shuffle': self.world.enemy_shuffle,
|
||||||
'enemy_health': self.world.enemy_health,
|
'enemy_health': self.world.enemy_health,
|
||||||
'enemy_damage': self.world.enemy_damage,
|
'enemy_damage': self.world.enemy_damage,
|
||||||
'players': self.world.players
|
'players': self.world.players,
|
||||||
|
'teams': self.world.teams
|
||||||
}
|
}
|
||||||
|
|
||||||
def to_json(self):
|
def to_json(self):
|
||||||
|
@ -1085,6 +1084,8 @@ class Spoiler(object):
|
||||||
out.update(self.locations)
|
out.update(self.locations)
|
||||||
out['Starting Inventory'] = self.startinventory
|
out['Starting Inventory'] = self.startinventory
|
||||||
out['Special'] = self.medallions
|
out['Special'] = self.medallions
|
||||||
|
if self.hashes:
|
||||||
|
out['Hashes'] = {f"{self.world.player_names[player][team]} (Team {team+1})": hash for (player, team), hash in self.hashes.items()}
|
||||||
if self.shops:
|
if self.shops:
|
||||||
out['Shops'] = self.shops
|
out['Shops'] = self.shops
|
||||||
out['playthrough'] = self.playthrough
|
out['playthrough'] = self.playthrough
|
||||||
|
@ -1098,42 +1099,44 @@ class Spoiler(object):
|
||||||
self.parse_data()
|
self.parse_data()
|
||||||
with open(filename, 'w') as outfile:
|
with open(filename, 'w') as outfile:
|
||||||
outfile.write('ALttP Entrance Randomizer Version %s - Seed: %s\n\n' % (self.metadata['version'], self.world.seed))
|
outfile.write('ALttP Entrance Randomizer Version %s - Seed: %s\n\n' % (self.metadata['version'], self.world.seed))
|
||||||
outfile.write('Players: %d\n' % self.world.players)
|
|
||||||
outfile.write('Filling Algorithm: %s\n' % self.world.algorithm)
|
outfile.write('Filling Algorithm: %s\n' % self.world.algorithm)
|
||||||
outfile.write('Logic: %s\n' % self.metadata['logic'])
|
outfile.write('Players: %d\n' % self.world.players)
|
||||||
outfile.write('Mode: %s\n' % self.metadata['mode'])
|
outfile.write('Teams: %d\n' % self.world.teams)
|
||||||
outfile.write('Retro: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['retro'].items()})
|
for player in range(1, self.world.players + 1):
|
||||||
outfile.write('Swords: %s\n' % self.metadata['weapons'])
|
if self.world.players > 1:
|
||||||
outfile.write('Goal: %s\n' % self.metadata['goal'])
|
outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_names(player)))
|
||||||
outfile.write('Difficulty: %s\n' % self.metadata['item_pool'])
|
for team in range(self.world.teams):
|
||||||
outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'])
|
outfile.write('%s%s\n' % (f"Hash - {self.world.player_names[player][team]} (Team {team+1}): " if self.world.teams > 1 else 'Hash: ', self.hashes[player, team]))
|
||||||
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'])
|
outfile.write('Logic: %s\n' % self.metadata['logic'][player])
|
||||||
outfile.write('Crystals required for GT: %s\n' % self.metadata['gt_crystals'])
|
outfile.write('Mode: %s\n' % self.metadata['mode'][player])
|
||||||
outfile.write('Crystals required for Ganon: %s\n' % self.metadata['ganon_crystals'])
|
outfile.write('Retro: %s\n' % ('Yes' if self.metadata['retro'][player] else 'No'))
|
||||||
outfile.write('Pyramid hole pre-opened: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['open_pyramid'].items()})
|
outfile.write('Swords: %s\n' % self.metadata['weapons'][player])
|
||||||
outfile.write('Accessibility: %s\n' % self.metadata['accessibility'])
|
outfile.write('Goal: %s\n' % self.metadata['goal'][player])
|
||||||
outfile.write('Map shuffle: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['mapshuffle'].items()})
|
outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player])
|
||||||
outfile.write('Compass shuffle: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['compassshuffle'].items()})
|
outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player])
|
||||||
outfile.write('Small Key shuffle: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['keyshuffle'].items()})
|
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player])
|
||||||
outfile.write('Big Key shuffle: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['bigkeyshuffle'].items()})
|
outfile.write('Crystals required for GT: %s\n' % self.metadata['gt_crystals'][player])
|
||||||
outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'])
|
outfile.write('Crystals required for Ganon: %s\n' % self.metadata['ganon_crystals'][player])
|
||||||
outfile.write('Enemy shuffle: %s\n' % self.metadata['enemy_shuffle'])
|
outfile.write('Pyramid hole pre-opened: %s\n' % ('Yes' if self.metadata['open_pyramid'][player] else 'No'))
|
||||||
outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'])
|
outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player])
|
||||||
outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'])
|
outfile.write('Map shuffle: %s\n' % ('Yes' if self.metadata['mapshuffle'][player] else 'No'))
|
||||||
outfile.write('Hints: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['hints'].items()})
|
outfile.write('Compass shuffle: %s\n' % ('Yes' if self.metadata['compassshuffle'][player] else 'No'))
|
||||||
|
outfile.write('Small Key shuffle: %s\n' % ('Yes' if self.metadata['keyshuffle'][player] else 'No'))
|
||||||
|
outfile.write('Big Key shuffle: %s\n' % ('Yes' if self.metadata['bigkeyshuffle'][player] else 'No'))
|
||||||
|
outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player])
|
||||||
|
outfile.write('Enemy shuffle: %s\n' % self.metadata['enemy_shuffle'][player])
|
||||||
|
outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player])
|
||||||
|
outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player])
|
||||||
|
outfile.write('Hints: %s\n' % ('Yes' if self.metadata['hints'][player] else 'No'))
|
||||||
if self.entrances:
|
if self.entrances:
|
||||||
outfile.write('\n\nEntrances:\n\n')
|
outfile.write('\n\nEntrances:\n\n')
|
||||||
outfile.write('\n'.join(['%s%s %s %s' % ('Player {0}: '.format(entry['player']) if self.world.players >1 else '', entry['entrance'], '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', entry['exit']) for entry in self.entrances.values()]))
|
outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', entry['entrance'], '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', entry['exit']) for entry in self.entrances.values()]))
|
||||||
outfile.write('\n\nMedallions\n')
|
outfile.write('\n\nMedallions:\n')
|
||||||
if self.world.players == 1:
|
for dungeon, medallion in self.medallions.items():
|
||||||
outfile.write('\nMisery Mire Medallion: %s' % (self.medallions['Misery Mire']))
|
outfile.write(f'\n{dungeon}: {medallion}')
|
||||||
outfile.write('\nTurtle Rock Medallion: %s' % (self.medallions['Turtle Rock']))
|
if self.startinventory:
|
||||||
else:
|
outfile.write('\n\nStarting Inventory:\n\n')
|
||||||
for player in range(1, self.world.players + 1):
|
outfile.write('\n'.join(self.startinventory))
|
||||||
outfile.write('\nMisery Mire Medallion (Player %d): %s' % (player, self.medallions['Misery Mire (Player %d)' % player]))
|
|
||||||
outfile.write('\nTurtle Rock Medallion (Player %d): %s' % (player, self.medallions['Turtle Rock (Player %d)' % player]))
|
|
||||||
outfile.write('\n\nStarting Inventory:\n\n')
|
|
||||||
outfile.write('\n'.join(self.startinventory))
|
|
||||||
outfile.write('\n\nLocations:\n\n')
|
outfile.write('\n\nLocations:\n\n')
|
||||||
outfile.write('\n'.join(['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in grouping.items()]))
|
outfile.write('\n'.join(['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in grouping.items()]))
|
||||||
outfile.write('\n\nShops:\n\n')
|
outfile.write('\n\nShops:\n\n')
|
||||||
|
|
|
@ -268,6 +268,7 @@ def parse_arguments(argv, no_defaults=False):
|
||||||
parser.add_argument('--beemizer', default=defval(0), type=lambda value: min(max(int(value), 0), 4))
|
parser.add_argument('--beemizer', default=defval(0), type=lambda value: min(max(int(value), 0), 4))
|
||||||
parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255))
|
parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255))
|
||||||
parser.add_argument('--names', default=defval(''))
|
parser.add_argument('--names', default=defval(''))
|
||||||
|
parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1))
|
||||||
parser.add_argument('--outputpath')
|
parser.add_argument('--outputpath')
|
||||||
parser.add_argument('--race', default=defval(False), action='store_true')
|
parser.add_argument('--race', default=defval(False), action='store_true')
|
||||||
parser.add_argument('--outputname')
|
parser.add_argument('--outputname')
|
||||||
|
|
24
Gui.py
24
Gui.py
|
@ -15,7 +15,7 @@ from EntranceRandomizer import parse_arguments
|
||||||
from GuiUtils import ToolTips, set_icon, BackgroundTaskProgress
|
from GuiUtils import ToolTips, set_icon, BackgroundTaskProgress
|
||||||
from Main import main, __version__ as ESVersion
|
from Main import main, __version__ as ESVersion
|
||||||
from Rom import Sprite
|
from Rom import Sprite
|
||||||
from Utils import is_bundled, local_path, output_path, open_file, parse_names_string
|
from Utils import is_bundled, local_path, output_path, open_file
|
||||||
|
|
||||||
|
|
||||||
def guiMain(args=None):
|
def guiMain(args=None):
|
||||||
|
@ -470,11 +470,7 @@ def guiMain(args=None):
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
messagebox.showerror(title="Error while creating seed", message=str(e))
|
messagebox.showerror(title="Error while creating seed", message=str(e))
|
||||||
else:
|
else:
|
||||||
msgtxt = "Rom patched successfully"
|
messagebox.showinfo(title="Success", message="Rom patched successfully")
|
||||||
if guiargs.names:
|
|
||||||
for player, name in parse_names_string(guiargs.names).items():
|
|
||||||
msgtxt += "\nPlayer %d => %s" % (player, name)
|
|
||||||
messagebox.showinfo(title="Success", message=msgtxt)
|
|
||||||
|
|
||||||
generateButton = Button(bottomFrame, text='Generate Patched Rom', command=generateRom)
|
generateButton = Button(bottomFrame, text='Generate Patched Rom', command=generateRom)
|
||||||
|
|
||||||
|
@ -574,20 +570,11 @@ def guiMain(args=None):
|
||||||
uwPalettesLabel2 = Label(uwPalettesFrame2, text='Dungeon palettes')
|
uwPalettesLabel2 = Label(uwPalettesFrame2, text='Dungeon palettes')
|
||||||
uwPalettesLabel2.pack(side=LEFT)
|
uwPalettesLabel2.pack(side=LEFT)
|
||||||
|
|
||||||
namesFrame2 = Frame(drowDownFrame2)
|
|
||||||
namesLabel2 = Label(namesFrame2, text='Player names')
|
|
||||||
namesVar2 = StringVar()
|
|
||||||
namesEntry2 = Entry(namesFrame2, textvariable=namesVar2)
|
|
||||||
|
|
||||||
namesLabel2.pack(side=LEFT)
|
|
||||||
namesEntry2.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)
|
||||||
namesFrame2.pack(expand=True, anchor=E)
|
|
||||||
|
|
||||||
bottomFrame2 = Frame(topFrame2)
|
bottomFrame2 = Frame(topFrame2)
|
||||||
|
|
||||||
|
@ -603,18 +590,13 @@ def guiMain(args=None):
|
||||||
guiargs.rom = romVar2.get()
|
guiargs.rom = romVar2.get()
|
||||||
guiargs.baserom = romVar.get()
|
guiargs.baserom = romVar.get()
|
||||||
guiargs.sprite = sprite
|
guiargs.sprite = sprite
|
||||||
guiargs.names = namesEntry2.get()
|
|
||||||
try:
|
try:
|
||||||
adjust(args=guiargs)
|
adjust(args=guiargs)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
messagebox.showerror(title="Error while creating seed", message=str(e))
|
messagebox.showerror(title="Error while creating seed", message=str(e))
|
||||||
else:
|
else:
|
||||||
msgtxt = "Rom patched successfully"
|
messagebox.showinfo(title="Success", message="Rom patched successfully")
|
||||||
if guiargs.names:
|
|
||||||
for player, name in parse_names_string(guiargs.names).items():
|
|
||||||
msgtxt += "\nPlayer %d => %s" % (player, name)
|
|
||||||
messagebox.showinfo(title="Success", message=msgtxt)
|
|
||||||
|
|
||||||
adjustButton = Button(bottomFrame2, text='Adjust Rom', command=adjustRom)
|
adjustButton = Button(bottomFrame2, text='Adjust Rom', command=adjustRom)
|
||||||
|
|
||||||
|
|
108
Main.py
108
Main.py
|
@ -13,12 +13,12 @@ from Items import ItemFactory
|
||||||
from Regions import create_regions, create_shops, mark_light_world_regions
|
from Regions import create_regions, create_shops, mark_light_world_regions
|
||||||
from InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
from InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
||||||
from EntranceShuffle import link_entrances, link_inverted_entrances
|
from EntranceShuffle import link_entrances, link_inverted_entrances
|
||||||
from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, JsonRom
|
from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, JsonRom, get_hash_string
|
||||||
from Rules import set_rules
|
from Rules import set_rules
|
||||||
from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive
|
from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive
|
||||||
from Fill import distribute_items_cutoff, distribute_items_staleness, distribute_items_restrictive, flood_items, balance_multiworld_progression
|
from Fill import distribute_items_cutoff, distribute_items_staleness, distribute_items_restrictive, flood_items, balance_multiworld_progression
|
||||||
from ItemList import generate_itempool, difficulties, fill_prizes
|
from ItemList import generate_itempool, difficulties, fill_prizes
|
||||||
from Utils import output_path, parse_names_string
|
from Utils import output_path, parse_player_names
|
||||||
|
|
||||||
__version__ = '0.6.3-pre'
|
__version__ = '0.6.3-pre'
|
||||||
|
|
||||||
|
@ -54,7 +54,16 @@ def main(args, seed=None):
|
||||||
|
|
||||||
world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)}
|
world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)}
|
||||||
|
|
||||||
logger.info('ALttP Entrance Randomizer Version %s - Seed: %s\n\n', __version__, world.seed)
|
logger.info('ALttP Entrance Randomizer Version %s - Seed: %s\n', __version__, world.seed)
|
||||||
|
|
||||||
|
parsed_names = parse_player_names(args.names, world.players, args.teams)
|
||||||
|
world.teams = len(parsed_names)
|
||||||
|
for i, team in enumerate(parsed_names, 1):
|
||||||
|
if world.players > 1:
|
||||||
|
logger.info('%s%s', 'Team%d: ' % i if world.teams > 1 else 'Players: ', ', '.join(team))
|
||||||
|
for player, name in enumerate(team, 1):
|
||||||
|
world.player_names[player].append(name)
|
||||||
|
logger.info('')
|
||||||
|
|
||||||
for player in range(1, world.players + 1):
|
for player in range(1, world.players + 1):
|
||||||
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
|
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
|
||||||
|
@ -133,60 +142,64 @@ def main(args, seed=None):
|
||||||
|
|
||||||
logger.info('Patching ROM.')
|
logger.info('Patching ROM.')
|
||||||
|
|
||||||
player_names = parse_names_string(args.names)
|
|
||||||
outfilebase = 'ER_%s' % (args.outputname if args.outputname else world.seed)
|
outfilebase = 'ER_%s' % (args.outputname if args.outputname else world.seed)
|
||||||
|
|
||||||
rom_names = []
|
rom_names = []
|
||||||
jsonout = {}
|
jsonout = {}
|
||||||
if not args.suppress_rom:
|
if not args.suppress_rom:
|
||||||
for player in range(1, world.players + 1):
|
for team in range(world.teams):
|
||||||
sprite_random_on_hit = type(args.sprite[player]) is str and args.sprite[player].lower() == 'randomonhit'
|
for player in range(1, world.players + 1):
|
||||||
use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] != 'none'
|
sprite_random_on_hit = type(args.sprite[player]) is str and args.sprite[player].lower() == 'randomonhit'
|
||||||
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
|
use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] != 'none'
|
||||||
or args.shufflepots[player] or sprite_random_on_hit)
|
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
|
||||||
|
or args.shufflepots[player] or sprite_random_on_hit)
|
||||||
|
|
||||||
rom = JsonRom() if args.jsonout or use_enemizer else LocalRom(args.rom)
|
rom = JsonRom() if args.jsonout or use_enemizer else LocalRom(args.rom)
|
||||||
|
|
||||||
patch_rom(world, player, rom, use_enemizer)
|
patch_rom(world, rom, player, team, use_enemizer)
|
||||||
rom_names.append((player, list(rom.name)))
|
|
||||||
|
|
||||||
if use_enemizer and (args.enemizercli or not args.jsonout):
|
if use_enemizer and (args.enemizercli or not args.jsonout):
|
||||||
patch_enemizer(world, player, rom, args.rom, args.enemizercli, args.shufflepots[player], sprite_random_on_hit)
|
patch_enemizer(world, player, rom, args.rom, args.enemizercli, args.shufflepots[player], sprite_random_on_hit)
|
||||||
if not args.jsonout:
|
if not args.jsonout:
|
||||||
patches = rom.patches
|
patches = rom.patches
|
||||||
rom = LocalRom(args.rom)
|
rom = LocalRom(args.rom)
|
||||||
rom.merge_enemizer_patches(patches)
|
rom.merge_enemizer_patches(patches)
|
||||||
|
|
||||||
if args.race:
|
if args.race:
|
||||||
patch_race_rom(rom)
|
patch_race_rom(rom)
|
||||||
|
|
||||||
apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player], args.fastmenu[player], args.disablemusic[player], args.sprite[player], args.ow_palettes[player], args.uw_palettes[player], player_names)
|
rom_names.append((player, team, list(rom.name)))
|
||||||
|
world.spoiler.hashes[(player, team)] = get_hash_string(rom.hash)
|
||||||
|
|
||||||
if args.jsonout:
|
apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player], args.fastmenu[player], args.disablemusic[player], args.sprite[player], args.ow_palettes[player], args.uw_palettes[player])
|
||||||
jsonout[f'patch{player}'] = rom.patches
|
|
||||||
else:
|
|
||||||
mcsb_name = ''
|
|
||||||
if all([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]]):
|
|
||||||
mcsb_name = '-keysanity'
|
|
||||||
elif [world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]].count(True) == 1:
|
|
||||||
mcsb_name = '-mapshuffle' if world.mapshuffle[player] else '-compassshuffle' if world.compassshuffle[player] else '-keyshuffle' if world.keyshuffle[player] else '-bigkeyshuffle'
|
|
||||||
elif any([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]]):
|
|
||||||
mcsb_name = '-%s%s%s%sshuffle' % (
|
|
||||||
'M' if world.mapshuffle[player] else '', 'C' if world.compassshuffle[player] else '',
|
|
||||||
'S' if world.keyshuffle[player] else '', 'B' if world.bigkeyshuffle[player] else '')
|
|
||||||
|
|
||||||
playername = f"{f'_P{player}' if world.players > 1 else ''}{f'_{player_names[player]}' if player in player_names else ''}"
|
if args.jsonout:
|
||||||
outfilesuffix = ('_%s_%s-%s-%s-%s%s_%s-%s%s%s%s%s' % (world.logic[player], world.difficulty[player], world.difficulty_adjustments[player],
|
jsonout[f'patch_t{team}_p{player}'] = rom.patches
|
||||||
world.mode[player], world.goal[player],
|
else:
|
||||||
"" if world.timer in ['none', 'display'] else "-" + world.timer,
|
mcsb_name = ''
|
||||||
world.shuffle[player], world.algorithm, mcsb_name,
|
if all([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]]):
|
||||||
"-retro" if world.retro[player] else "",
|
mcsb_name = '-keysanity'
|
||||||
"-prog_" + world.progressive if world.progressive in ['off', 'random'] else "",
|
elif [world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]].count(True) == 1:
|
||||||
"-nohints" if not world.hints[player] else "")) if not args.outputname else ''
|
mcsb_name = '-mapshuffle' if world.mapshuffle[player] else '-compassshuffle' if world.compassshuffle[player] else '-keyshuffle' if world.keyshuffle[player] else '-bigkeyshuffle'
|
||||||
rom.write_to_file(output_path(f'{outfilebase}{playername}{outfilesuffix}.sfc'))
|
elif any([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]]):
|
||||||
|
mcsb_name = '-%s%s%s%sshuffle' % (
|
||||||
|
'M' if world.mapshuffle[player] else '', 'C' if world.compassshuffle[player] else '',
|
||||||
|
'S' if world.keyshuffle[player] else '', 'B' if world.bigkeyshuffle[player] else '')
|
||||||
|
|
||||||
multidata = zlib.compress(json.dumps((world.players,
|
outfilepname = f'_T{team+1}' if world.teams > 1 else ''
|
||||||
rom_names,
|
if world.players > 1:
|
||||||
|
outfilepname += f'_P{player}'
|
||||||
|
outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" if world.player_names[player][team] != 'Player %d' % player else ''
|
||||||
|
outfilesuffix = ('_%s_%s-%s-%s-%s%s_%s-%s%s%s%s%s' % (world.logic[player], world.difficulty[player], world.difficulty_adjustments[player],
|
||||||
|
world.mode[player], world.goal[player],
|
||||||
|
"" if world.timer in ['none', 'display'] else "-" + world.timer,
|
||||||
|
world.shuffle[player], world.algorithm, mcsb_name,
|
||||||
|
"-retro" if world.retro[player] else "",
|
||||||
|
"-prog_" + world.progressive if world.progressive in ['off', 'random'] else "",
|
||||||
|
"-nohints" if not world.hints[player] else "")) if not args.outputname else ''
|
||||||
|
rom.write_to_file(output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc'))
|
||||||
|
|
||||||
|
multidata = zlib.compress(json.dumps((parsed_names, rom_names,
|
||||||
[((location.address, location.player), (location.item.code, location.item.player)) for location in world.get_filled_locations() if type(location.address) is int])
|
[((location.address, location.player), (location.item.code, location.item.player)) for location in world.get_filled_locations() if type(location.address) is int])
|
||||||
).encode("utf-8"))
|
).encode("utf-8"))
|
||||||
if args.jsonout:
|
if args.jsonout:
|
||||||
|
@ -215,6 +228,8 @@ def main(args, seed=None):
|
||||||
def copy_world(world):
|
def copy_world(world):
|
||||||
# ToDo: Not good yet
|
# ToDo: Not good yet
|
||||||
ret = World(world.players, world.shuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints)
|
ret = World(world.players, world.shuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints)
|
||||||
|
ret.teams = world.teams
|
||||||
|
ret.player_names = copy.deepcopy(world.player_names)
|
||||||
ret.required_medallions = world.required_medallions.copy()
|
ret.required_medallions = world.required_medallions.copy()
|
||||||
ret.swamp_patch_required = world.swamp_patch_required.copy()
|
ret.swamp_patch_required = world.swamp_patch_required.copy()
|
||||||
ret.ganon_at_pyramid = world.ganon_at_pyramid.copy()
|
ret.ganon_at_pyramid = world.ganon_at_pyramid.copy()
|
||||||
|
@ -280,6 +295,7 @@ def copy_world(world):
|
||||||
item = Item(location.item.name, location.item.advancement, location.item.priority, location.item.type, player = location.item.player)
|
item = Item(location.item.name, location.item.advancement, location.item.priority, location.item.type, player = location.item.player)
|
||||||
ret.get_location(location.name, location.player).item = item
|
ret.get_location(location.name, location.player).item = item
|
||||||
item.location = ret.get_location(location.name, location.player)
|
item.location = ret.get_location(location.name, location.player)
|
||||||
|
item.world = ret
|
||||||
if location.event:
|
if location.event:
|
||||||
ret.get_location(location.name, location.player).event = True
|
ret.get_location(location.name, location.player).event = True
|
||||||
if location.locked:
|
if location.locked:
|
||||||
|
@ -289,9 +305,11 @@ def copy_world(world):
|
||||||
for item in world.itempool:
|
for item in world.itempool:
|
||||||
ret.itempool.append(Item(item.name, item.advancement, item.priority, item.type, player = item.player))
|
ret.itempool.append(Item(item.name, item.advancement, item.priority, item.type, player = item.player))
|
||||||
|
|
||||||
|
for item in world.precollected_items:
|
||||||
|
ret.push_precollected(ItemFactory(item.name, item.player))
|
||||||
|
|
||||||
# copy progress items in state
|
# copy progress items in state
|
||||||
ret.state.prog_items = world.state.prog_items.copy()
|
ret.state.prog_items = world.state.prog_items.copy()
|
||||||
ret.precollected_items = world.precollected_items.copy()
|
|
||||||
ret.state.stale = {player: True for player in range(1, world.players + 1)}
|
ret.state.stale = {player: True for player in range(1, world.players + 1)}
|
||||||
|
|
||||||
for player in range(1, world.players + 1):
|
for player in range(1, world.players + 1):
|
||||||
|
|
262
MultiClient.py
262
MultiClient.py
|
@ -2,9 +2,10 @@ import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
import Items
|
import Items
|
||||||
import Regions
|
import Regions
|
||||||
|
@ -35,14 +36,13 @@ except ImportError:
|
||||||
colorama = None
|
colorama = None
|
||||||
|
|
||||||
class ReceivedItem:
|
class ReceivedItem:
|
||||||
def __init__(self, item, location, player_id, player_name):
|
def __init__(self, item, location, player):
|
||||||
self.item = item
|
self.item = item
|
||||||
self.location = location
|
self.location = location
|
||||||
self.player_id = player_id
|
self.player = player
|
||||||
self.player_name = player_name
|
|
||||||
|
|
||||||
class Context:
|
class Context:
|
||||||
def __init__(self, snes_address, server_address, password, name, team, slot):
|
def __init__(self, snes_address, server_address, password):
|
||||||
self.snes_address = snes_address
|
self.snes_address = snes_address
|
||||||
self.server_address = server_address
|
self.server_address = server_address
|
||||||
|
|
||||||
|
@ -53,6 +53,8 @@ class Context:
|
||||||
|
|
||||||
self.snes_socket = None
|
self.snes_socket = None
|
||||||
self.snes_state = SNES_DISCONNECTED
|
self.snes_state = SNES_DISCONNECTED
|
||||||
|
self.snes_attached_device = None
|
||||||
|
self.snes_reconnect_address = None
|
||||||
self.snes_recv_queue = asyncio.Queue()
|
self.snes_recv_queue = asyncio.Queue()
|
||||||
self.snes_request_lock = asyncio.Lock()
|
self.snes_request_lock = asyncio.Lock()
|
||||||
self.is_sd2snes = False
|
self.is_sd2snes = False
|
||||||
|
@ -62,15 +64,14 @@ class Context:
|
||||||
self.socket = None
|
self.socket = None
|
||||||
self.password = password
|
self.password = password
|
||||||
|
|
||||||
self.name = name
|
self.team = None
|
||||||
self.team = team
|
self.slot = None
|
||||||
self.slot = slot
|
self.player_names = {}
|
||||||
|
|
||||||
self.locations_checked = set()
|
self.locations_checked = set()
|
||||||
self.items_received = []
|
self.items_received = []
|
||||||
self.last_rom = None
|
self.awaiting_rom = False
|
||||||
self.expected_rom = None
|
self.rom = None
|
||||||
self.rom_confirmed = False
|
self.auth = None
|
||||||
|
|
||||||
def color_code(*args):
|
def color_code(*args):
|
||||||
codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34,
|
codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34,
|
||||||
|
@ -81,6 +82,7 @@ def color_code(*args):
|
||||||
def color(text, *args):
|
def color(text, *args):
|
||||||
return color_code(*args) + text + color_code('reset')
|
return color_code(*args) + text + color_code('reset')
|
||||||
|
|
||||||
|
RECONNECT_DELAY = 30
|
||||||
|
|
||||||
ROM_START = 0x000000
|
ROM_START = 0x000000
|
||||||
WRAM_START = 0xF50000
|
WRAM_START = 0xF50000
|
||||||
|
@ -323,7 +325,7 @@ SNES_CONNECTING = 1
|
||||||
SNES_CONNECTED = 2
|
SNES_CONNECTED = 2
|
||||||
SNES_ATTACHED = 3
|
SNES_ATTACHED = 3
|
||||||
|
|
||||||
async def snes_connect(ctx : Context, address = None):
|
async def snes_connect(ctx : Context, address):
|
||||||
if ctx.snes_socket is not None:
|
if ctx.snes_socket is not None:
|
||||||
print('Already connected to snes')
|
print('Already connected to snes')
|
||||||
return
|
return
|
||||||
|
@ -331,8 +333,7 @@ async def snes_connect(ctx : Context, address = None):
|
||||||
ctx.snes_state = SNES_CONNECTING
|
ctx.snes_state = SNES_CONNECTING
|
||||||
recv_task = None
|
recv_task = None
|
||||||
|
|
||||||
if address is None:
|
address = f"ws://{address}" if "://" not in address else address
|
||||||
address = 'ws://' + ctx.snes_address
|
|
||||||
|
|
||||||
print("Connecting to QUsb2snes at %s ..." % address)
|
print("Connecting to QUsb2snes at %s ..." % address)
|
||||||
|
|
||||||
|
@ -357,17 +358,25 @@ async def snes_connect(ctx : Context, address = None):
|
||||||
print("[%d] %s" % (id + 1, device))
|
print("[%d] %s" % (id + 1, device))
|
||||||
|
|
||||||
device = None
|
device = None
|
||||||
while True:
|
if len(devices) == 1:
|
||||||
print("Enter a number:")
|
device = devices[0]
|
||||||
choice = await console_input(ctx)
|
elif ctx.snes_reconnect_address:
|
||||||
if choice is None:
|
if ctx.snes_attached_device[1] in devices:
|
||||||
raise Exception('Abort input')
|
device = ctx.snes_attached_device[1]
|
||||||
if not choice.isdigit() or int(choice) < 1 or int(choice) > len(devices):
|
else:
|
||||||
print("Invalid choice (%s)" % choice)
|
device = devices[ctx.snes_attached_device[0]]
|
||||||
continue
|
else:
|
||||||
|
while True:
|
||||||
|
print("Select a device:")
|
||||||
|
choice = await console_input(ctx)
|
||||||
|
if choice is None:
|
||||||
|
raise Exception('Abort input')
|
||||||
|
if not choice.isdigit() or int(choice) < 1 or int(choice) > len(devices):
|
||||||
|
print("Invalid choice (%s)" % choice)
|
||||||
|
continue
|
||||||
|
|
||||||
device = devices[int(choice) - 1]
|
device = devices[int(choice) - 1]
|
||||||
break
|
break
|
||||||
|
|
||||||
print("Attaching to " + device)
|
print("Attaching to " + device)
|
||||||
|
|
||||||
|
@ -378,6 +387,7 @@ async def snes_connect(ctx : Context, address = None):
|
||||||
}
|
}
|
||||||
await ctx.snes_socket.send(json.dumps(Attach_Request))
|
await ctx.snes_socket.send(json.dumps(Attach_Request))
|
||||||
ctx.snes_state = SNES_ATTACHED
|
ctx.snes_state = SNES_ATTACHED
|
||||||
|
ctx.snes_attached_device = (devices.index(device), device)
|
||||||
|
|
||||||
if 'SD2SNES'.lower() in device.lower() or (len(device) == 4 and device[:3] == 'COM'):
|
if 'SD2SNES'.lower() in device.lower() or (len(device) == 4 and device[:3] == 'COM'):
|
||||||
print("SD2SNES Detected")
|
print("SD2SNES Detected")
|
||||||
|
@ -389,10 +399,10 @@ async def snes_connect(ctx : Context, address = None):
|
||||||
else:
|
else:
|
||||||
ctx.is_sd2snes = False
|
ctx.is_sd2snes = False
|
||||||
|
|
||||||
|
ctx.snes_reconnect_address = address
|
||||||
recv_task = asyncio.create_task(snes_recv_loop(ctx))
|
recv_task = asyncio.create_task(snes_recv_loop(ctx))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Error connecting to snes (%s)" % e)
|
|
||||||
if recv_task is not None:
|
if recv_task is not None:
|
||||||
if not ctx.snes_socket.closed:
|
if not ctx.snes_socket.closed:
|
||||||
await ctx.snes_socket.close()
|
await ctx.snes_socket.close()
|
||||||
|
@ -402,16 +412,26 @@ async def snes_connect(ctx : Context, address = None):
|
||||||
await ctx.snes_socket.close()
|
await ctx.snes_socket.close()
|
||||||
ctx.snes_socket = None
|
ctx.snes_socket = None
|
||||||
ctx.snes_state = SNES_DISCONNECTED
|
ctx.snes_state = SNES_DISCONNECTED
|
||||||
|
if not ctx.snes_reconnect_address:
|
||||||
|
print("Error connecting to snes (%s)" % e)
|
||||||
|
else:
|
||||||
|
print(f"Error connecting to snes, attempt again in {RECONNECT_DELAY}s")
|
||||||
|
asyncio.create_task(snes_autoreconnect(ctx))
|
||||||
|
|
||||||
|
async def snes_autoreconnect(ctx: Context):
|
||||||
|
await asyncio.sleep(RECONNECT_DELAY)
|
||||||
|
if ctx.snes_reconnect_address and ctx.snes_socket is None:
|
||||||
|
await snes_connect(ctx, ctx.snes_reconnect_address)
|
||||||
|
|
||||||
async def snes_recv_loop(ctx : Context):
|
async def snes_recv_loop(ctx : Context):
|
||||||
try:
|
try:
|
||||||
async for msg in ctx.snes_socket:
|
async for msg in ctx.snes_socket:
|
||||||
ctx.snes_recv_queue.put_nowait(msg)
|
ctx.snes_recv_queue.put_nowait(msg)
|
||||||
print("Snes disconnected, type /snes to reconnect")
|
print("Snes disconnected")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Lost connection to the snes, type /snes to reconnect")
|
|
||||||
if not isinstance(e, websockets.WebSocketException):
|
if not isinstance(e, websockets.WebSocketException):
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
|
print("Lost connection to the snes, type /snes to reconnect")
|
||||||
finally:
|
finally:
|
||||||
socket, ctx.snes_socket = ctx.snes_socket, None
|
socket, ctx.snes_socket = ctx.snes_socket, None
|
||||||
if socket is not None and not socket.closed:
|
if socket is not None and not socket.closed:
|
||||||
|
@ -421,8 +441,11 @@ async def snes_recv_loop(ctx : Context):
|
||||||
ctx.snes_recv_queue = asyncio.Queue()
|
ctx.snes_recv_queue = asyncio.Queue()
|
||||||
ctx.hud_message_queue = []
|
ctx.hud_message_queue = []
|
||||||
|
|
||||||
ctx.rom_confirmed = False
|
ctx.rom = None
|
||||||
ctx.last_rom = None
|
|
||||||
|
if ctx.snes_reconnect_address:
|
||||||
|
print(f"...reconnecting in {RECONNECT_DELAY}s")
|
||||||
|
asyncio.create_task(snes_autoreconnect(ctx))
|
||||||
|
|
||||||
async def snes_read(ctx : Context, address, size):
|
async def snes_read(ctx : Context, address, size):
|
||||||
try:
|
try:
|
||||||
|
@ -534,21 +557,26 @@ async def send_msgs(websocket, msgs):
|
||||||
except websockets.ConnectionClosed:
|
except websockets.ConnectionClosed:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def server_loop(ctx : Context):
|
async def server_loop(ctx : Context, address = None):
|
||||||
if ctx.socket is not None:
|
if ctx.socket is not None:
|
||||||
print('Already connected')
|
print('Already connected')
|
||||||
return
|
return
|
||||||
|
|
||||||
while not ctx.server_address:
|
if address is None:
|
||||||
print('Enter multiworld server address')
|
address = ctx.server_address
|
||||||
ctx.server_address = await console_input(ctx)
|
|
||||||
|
|
||||||
address = f"ws://{ctx.server_address}" if "://" not in ctx.server_address else ctx.server_address
|
while not address:
|
||||||
|
print('Enter multiworld server address')
|
||||||
|
address = await console_input(ctx)
|
||||||
|
|
||||||
|
address = f"ws://{address}" if "://" not in address else address
|
||||||
|
port = urllib.parse.urlparse(address).port or 38281
|
||||||
|
|
||||||
print('Connecting to multiworld server at %s' % address)
|
print('Connecting to multiworld server at %s' % address)
|
||||||
try:
|
try:
|
||||||
ctx.socket = await websockets.connect(address, ping_timeout=None, ping_interval=None)
|
ctx.socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None)
|
||||||
print('Connected')
|
print('Connected')
|
||||||
|
ctx.server_address = address
|
||||||
|
|
||||||
async for data in ctx.socket:
|
async for data in ctx.socket:
|
||||||
for msg in json.loads(data):
|
for msg in json.loads(data):
|
||||||
|
@ -564,15 +592,21 @@ async def server_loop(ctx : Context):
|
||||||
if not isinstance(e, websockets.WebSocketException):
|
if not isinstance(e, websockets.WebSocketException):
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
finally:
|
finally:
|
||||||
ctx.name = None
|
ctx.awaiting_rom = False
|
||||||
ctx.team = None
|
ctx.auth = None
|
||||||
ctx.slot = None
|
ctx.items_received = []
|
||||||
ctx.expected_rom = None
|
|
||||||
ctx.rom_confirmed = False
|
|
||||||
socket, ctx.socket = ctx.socket, None
|
socket, ctx.socket = ctx.socket, None
|
||||||
if socket is not None and not socket.closed:
|
if socket is not None and not socket.closed:
|
||||||
await socket.close()
|
await socket.close()
|
||||||
ctx.server_task = None
|
ctx.server_task = None
|
||||||
|
if ctx.server_address:
|
||||||
|
print(f"... reconnecting in {RECONNECT_DELAY}s")
|
||||||
|
asyncio.create_task(server_autoreconnect(ctx))
|
||||||
|
|
||||||
|
async def server_autoreconnect(ctx: Context):
|
||||||
|
await asyncio.sleep(RECONNECT_DELAY)
|
||||||
|
if ctx.server_address and ctx.server_task is None:
|
||||||
|
ctx.server_task = asyncio.create_task(server_loop(ctx))
|
||||||
|
|
||||||
async def process_server_cmd(ctx : Context, cmd, args):
|
async def process_server_cmd(ctx : Context, cmd, args):
|
||||||
if cmd == 'RoomInfo':
|
if cmd == 'RoomInfo':
|
||||||
|
@ -581,53 +615,36 @@ async def process_server_cmd(ctx : Context, cmd, args):
|
||||||
print('--------------------------------')
|
print('--------------------------------')
|
||||||
if args['password']:
|
if args['password']:
|
||||||
print('Password required')
|
print('Password required')
|
||||||
print('%d players seed' % args['slots'])
|
|
||||||
if len(args['players']) < 1:
|
if len(args['players']) < 1:
|
||||||
print('No player connected')
|
print('No player connected')
|
||||||
else:
|
else:
|
||||||
args['players'].sort(key=lambda player: ('' if not player[1] else player[1].lower(), player[2]))
|
args['players'].sort(key=lambda _, t, s: (t, s))
|
||||||
current_team = 0
|
current_team = 0
|
||||||
print('Connected players:')
|
print('Connected players:')
|
||||||
|
print(' Team #1')
|
||||||
for name, team, slot in args['players']:
|
for name, team, slot in args['players']:
|
||||||
if team != current_team:
|
if team != current_team:
|
||||||
print(' Default team' if not team else ' Team: %s' % team)
|
print(' Team #d' % team + 1)
|
||||||
current_team = team
|
current_team = team
|
||||||
print(' %s (Player %d)' % (name, slot))
|
print(' %s (Player %d)' % (name, slot))
|
||||||
await server_auth(ctx, args['password'])
|
await server_auth(ctx, args['password'])
|
||||||
|
|
||||||
if cmd == 'ConnectionRefused':
|
if cmd == 'ConnectionRefused':
|
||||||
password_requested = False
|
|
||||||
if 'InvalidPassword' in args:
|
if 'InvalidPassword' in args:
|
||||||
print('Invalid password')
|
print('Invalid password')
|
||||||
ctx.password = None
|
ctx.password = None
|
||||||
password_requested = True
|
await server_auth(ctx, True)
|
||||||
if 'InvalidName' in args:
|
if 'InvalidRom' in args:
|
||||||
print('Invalid name')
|
raise Exception('Invalid ROM detected, please verify that you have loaded the correct rom and reconnect your snes')
|
||||||
ctx.name = None
|
|
||||||
if 'NameAlreadyTaken' in args:
|
|
||||||
print('Name already taken')
|
|
||||||
ctx.name = None
|
|
||||||
if 'InvalidTeam' in args:
|
|
||||||
print('Invalid team name')
|
|
||||||
ctx.team = None
|
|
||||||
if 'InvalidSlot' in args:
|
|
||||||
print('Invalid player slot')
|
|
||||||
ctx.slot = None
|
|
||||||
if 'SlotAlreadyTaken' in args:
|
if 'SlotAlreadyTaken' in args:
|
||||||
print('Player slot already in use for that team')
|
raise Exception('Player slot already in use for that team')
|
||||||
ctx.team = None
|
raise Exception('Connection refused by the multiworld host')
|
||||||
ctx.slot = None
|
|
||||||
await server_auth(ctx, password_requested)
|
|
||||||
|
|
||||||
if cmd == 'Connected':
|
if cmd == 'Connected':
|
||||||
ctx.expected_rom = args
|
ctx.team, ctx.slot = args[0]
|
||||||
if ctx.last_rom is not None:
|
ctx.player_names = {p: n for p, n in args[1]}
|
||||||
if ctx.last_rom[:len(args)] == ctx.expected_rom:
|
if ctx.locations_checked:
|
||||||
rom_confirmed(ctx)
|
await send_msgs(ctx.socket, [['LocationChecks', [Regions.location_table[loc][0] for loc in ctx.locations_checked]]])
|
||||||
if ctx.locations_checked:
|
|
||||||
await send_msgs(ctx.socket, [['LocationChecks', [Regions.location_table[loc][0] for loc in ctx.locations_checked]]])
|
|
||||||
else:
|
|
||||||
raise Exception('Different ROM expected from server')
|
|
||||||
|
|
||||||
if cmd == 'ReceivedItems':
|
if cmd == 'ReceivedItems':
|
||||||
start_index, items = args
|
start_index, items = args
|
||||||
|
@ -640,14 +657,14 @@ async def process_server_cmd(ctx : Context, cmd, args):
|
||||||
await send_msgs(ctx.socket, sync_msg)
|
await send_msgs(ctx.socket, sync_msg)
|
||||||
if start_index == len(ctx.items_received):
|
if start_index == len(ctx.items_received):
|
||||||
for item in items:
|
for item in items:
|
||||||
ctx.items_received.append(ReceivedItem(item[0], item[1], item[2], item[3]))
|
ctx.items_received.append(ReceivedItem(*item))
|
||||||
|
|
||||||
if cmd == 'ItemSent':
|
if cmd == 'ItemSent':
|
||||||
player_sent, player_recvd, item, location = args
|
player_sent, location, player_recvd, item = args
|
||||||
item = color(get_item_name_from_id(item), 'cyan' if player_sent != ctx.name else 'green')
|
item = color(get_item_name_from_id(item), 'cyan' if player_sent != ctx.slot else 'green')
|
||||||
player_sent = color(player_sent, 'yellow' if player_sent != ctx.name else 'magenta')
|
player_sent = color(ctx.player_names[player_sent], 'yellow' if player_sent != ctx.slot else 'magenta')
|
||||||
player_recvd = color(player_recvd, 'yellow' if player_recvd != ctx.name else 'magenta')
|
player_recvd = color(ctx.player_names[player_recvd], 'yellow' if player_recvd != ctx.slot else 'magenta')
|
||||||
print('(%s) %s sent %s to %s (%s)' % (ctx.team if ctx.team else 'Team', player_sent, item, player_recvd, get_location_name_from_address(location)))
|
print('%s sent %s to %s (%s)' % (player_sent, item, player_recvd, get_location_name_from_address(location)))
|
||||||
|
|
||||||
if cmd == 'Print':
|
if cmd == 'Print':
|
||||||
print(args)
|
print(args)
|
||||||
|
@ -656,23 +673,28 @@ async def server_auth(ctx : Context, password_requested):
|
||||||
if password_requested and not ctx.password:
|
if password_requested and not ctx.password:
|
||||||
print('Enter the password required to join this game:')
|
print('Enter the password required to join this game:')
|
||||||
ctx.password = await console_input(ctx)
|
ctx.password = await console_input(ctx)
|
||||||
while not ctx.name or not re.match(r'\w{1,10}', ctx.name):
|
if ctx.rom is None:
|
||||||
print('Enter your name (10 characters):')
|
ctx.awaiting_rom = True
|
||||||
ctx.name = await console_input(ctx)
|
print('No ROM detected, awaiting snes connection to authenticate to the multiworld server')
|
||||||
if not ctx.team:
|
return
|
||||||
print('Enter your team name (optional):')
|
ctx.awaiting_rom = False
|
||||||
ctx.team = await console_input(ctx)
|
ctx.auth = ctx.rom.copy()
|
||||||
if ctx.team == '': ctx.team = None
|
await send_msgs(ctx.socket, [['Connect', {'password': ctx.password, 'rom': ctx.auth}]])
|
||||||
if not ctx.slot:
|
|
||||||
print('Choose your player slot (optional):')
|
|
||||||
slot = await console_input(ctx)
|
|
||||||
ctx.slot = int(slot) if slot.isdigit() else None
|
|
||||||
await send_msgs(ctx.socket, [['Connect', {'password': ctx.password, 'name': ctx.name, 'team': ctx.team, 'slot': ctx.slot}]])
|
|
||||||
|
|
||||||
async def console_input(ctx : Context):
|
async def console_input(ctx : Context):
|
||||||
ctx.input_requests += 1
|
ctx.input_requests += 1
|
||||||
return await ctx.input_queue.get()
|
return await ctx.input_queue.get()
|
||||||
|
|
||||||
|
async def disconnect(ctx: Context):
|
||||||
|
if ctx.socket is not None and not ctx.socket.closed:
|
||||||
|
await ctx.socket.close()
|
||||||
|
if ctx.server_task is not None:
|
||||||
|
await ctx.server_task
|
||||||
|
|
||||||
|
async def connect(ctx: Context, address=None):
|
||||||
|
await disconnect(ctx)
|
||||||
|
ctx.server_task = asyncio.create_task(server_loop(ctx, address))
|
||||||
|
|
||||||
async def console_loop(ctx : Context):
|
async def console_loop(ctx : Context):
|
||||||
while not ctx.exit_event.is_set():
|
while not ctx.exit_event.is_set():
|
||||||
input = await aioconsole.ainput()
|
input = await aioconsole.ainput()
|
||||||
|
@ -682,7 +704,7 @@ async def console_loop(ctx : Context):
|
||||||
ctx.input_queue.put_nowait(input)
|
ctx.input_queue.put_nowait(input)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
command = input.split()
|
command = shlex.split(input)
|
||||||
if not command:
|
if not command:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -696,26 +718,19 @@ async def console_loop(ctx : Context):
|
||||||
colorama.init()
|
colorama.init()
|
||||||
|
|
||||||
if command[0] == '/snes':
|
if command[0] == '/snes':
|
||||||
asyncio.create_task(snes_connect(ctx, command[1] if len(command) > 1 else None))
|
ctx.snes_reconnect_address = None
|
||||||
|
asyncio.create_task(snes_connect(ctx, command[1] if len(command) > 1 else ctx.snes_address))
|
||||||
if command[0] in ['/snes_close', '/snes_quit']:
|
if command[0] in ['/snes_close', '/snes_quit']:
|
||||||
|
ctx.snes_reconnect_address = None
|
||||||
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:
|
||||||
await ctx.snes_socket.close()
|
await ctx.snes_socket.close()
|
||||||
|
|
||||||
async def disconnect():
|
|
||||||
if ctx.socket is not None and not ctx.socket.closed:
|
|
||||||
await ctx.socket.close()
|
|
||||||
if ctx.server_task is not None:
|
|
||||||
await ctx.server_task
|
|
||||||
async def connect():
|
|
||||||
await disconnect()
|
|
||||||
ctx.server_task = asyncio.create_task(server_loop(ctx))
|
|
||||||
|
|
||||||
if command[0] in ['/connect', '/reconnect']:
|
if command[0] in ['/connect', '/reconnect']:
|
||||||
if len(command) > 1:
|
ctx.server_address = None
|
||||||
ctx.server_address = command[1]
|
asyncio.create_task(connect(ctx, command[1] if len(command) > 1 else None))
|
||||||
asyncio.create_task(connect())
|
|
||||||
if command[0] == '/disconnect':
|
if command[0] == '/disconnect':
|
||||||
asyncio.create_task(disconnect())
|
ctx.server_address = None
|
||||||
|
asyncio.create_task(disconnect(ctx))
|
||||||
if command[0][:1] != '/':
|
if command[0][:1] != '/':
|
||||||
asyncio.create_task(send_msgs(ctx.socket, [['Say', input]]))
|
asyncio.create_task(send_msgs(ctx.socket, [['Say', input]]))
|
||||||
|
|
||||||
|
@ -723,7 +738,7 @@ async def console_loop(ctx : Context):
|
||||||
print('Received items:')
|
print('Received items:')
|
||||||
for index, item in enumerate(ctx.items_received, 1):
|
for index, item in enumerate(ctx.items_received, 1):
|
||||||
print('%s from %s (%s) (%d/%d in list)' % (
|
print('%s from %s (%s) (%d/%d in list)' % (
|
||||||
color(get_item_name_from_id(item.item), 'red', 'bold'), color(item.player_name, 'yellow'),
|
color(get_item_name_from_id(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
|
||||||
get_location_name_from_address(item.location), index, len(ctx.items_received)))
|
get_location_name_from_address(item.location), index, len(ctx.items_received)))
|
||||||
|
|
||||||
if command[0] == '/missing':
|
if command[0] == '/missing':
|
||||||
|
@ -742,10 +757,6 @@ async def console_loop(ctx : Context):
|
||||||
|
|
||||||
await snes_flush_writes(ctx)
|
await snes_flush_writes(ctx)
|
||||||
|
|
||||||
def rom_confirmed(ctx : Context):
|
|
||||||
ctx.rom_confirmed = True
|
|
||||||
print('ROM hash Confirmed')
|
|
||||||
|
|
||||||
def get_item_name_from_id(code):
|
def get_item_name_from_id(code):
|
||||||
items = [k for k, i in Items.item_table.items() if type(i[3]) is int and i[3] == code]
|
items = [k for k, i in Items.item_table.items() if type(i[3]) is int and i[3] == code]
|
||||||
return items[0] if items else 'Unknown item'
|
return items[0] if items else 'Unknown item'
|
||||||
|
@ -822,20 +833,19 @@ async def game_watcher(ctx : Context):
|
||||||
while not ctx.exit_event.is_set():
|
while not ctx.exit_event.is_set():
|
||||||
await asyncio.sleep(2)
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
if not ctx.rom_confirmed:
|
if not ctx.rom:
|
||||||
rom = await snes_read(ctx, ROMNAME_START, ROMNAME_SIZE)
|
rom = await snes_read(ctx, ROMNAME_START, ROMNAME_SIZE)
|
||||||
if rom is None or rom == bytes([0] * ROMNAME_SIZE):
|
if rom is None or rom == bytes([0] * ROMNAME_SIZE):
|
||||||
continue
|
continue
|
||||||
if list(rom) != ctx.last_rom:
|
|
||||||
ctx.last_rom = list(rom)
|
ctx.rom = list(rom)
|
||||||
ctx.locations_checked = set()
|
ctx.locations_checked = set()
|
||||||
if ctx.expected_rom is not None:
|
if ctx.awaiting_rom:
|
||||||
if ctx.last_rom[:len(ctx.expected_rom)] != ctx.expected_rom:
|
await server_auth(ctx, False)
|
||||||
print("Wrong ROM detected")
|
|
||||||
await ctx.snes_socket.close()
|
if ctx.auth and ctx.auth != ctx.rom:
|
||||||
continue
|
print("ROM change detected, please reconnect to the multiworld server")
|
||||||
else:
|
await disconnect(ctx)
|
||||||
rom_confirmed(ctx)
|
|
||||||
|
|
||||||
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
|
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
|
||||||
if gamemode is None or gamemode[0] not in INGAME_MODES:
|
if gamemode is None or gamemode[0] not in INGAME_MODES:
|
||||||
|
@ -858,12 +868,12 @@ async def game_watcher(ctx : Context):
|
||||||
if recv_index < len(ctx.items_received) and recv_item == 0:
|
if recv_index < len(ctx.items_received) and recv_item == 0:
|
||||||
item = ctx.items_received[recv_index]
|
item = ctx.items_received[recv_index]
|
||||||
print('Received %s from %s (%s) (%d/%d in list)' % (
|
print('Received %s from %s (%s) (%d/%d in list)' % (
|
||||||
color(get_item_name_from_id(item.item), 'red', 'bold'), color(item.player_name, 'yellow'),
|
color(get_item_name_from_id(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
|
||||||
get_location_name_from_address(item.location), recv_index + 1, len(ctx.items_received)))
|
get_location_name_from_address(item.location), recv_index + 1, len(ctx.items_received)))
|
||||||
recv_index += 1
|
recv_index += 1
|
||||||
snes_buffered_write(ctx, RECV_PROGRESS_ADDR, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
|
snes_buffered_write(ctx, RECV_PROGRESS_ADDR, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
|
||||||
snes_buffered_write(ctx, RECV_ITEM_ADDR, bytes([item.item]))
|
snes_buffered_write(ctx, RECV_ITEM_ADDR, bytes([item.item]))
|
||||||
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([item.player_id]))
|
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([item.player]))
|
||||||
|
|
||||||
await snes_flush_writes(ctx)
|
await snes_flush_writes(ctx)
|
||||||
|
|
||||||
|
@ -872,16 +882,13 @@ async def main():
|
||||||
parser.add_argument('--snes', default='localhost:8080', help='Address of the QUsb2snes server.')
|
parser.add_argument('--snes', default='localhost:8080', help='Address of the QUsb2snes server.')
|
||||||
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
|
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
|
||||||
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
|
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
|
||||||
parser.add_argument('--name', default=None)
|
|
||||||
parser.add_argument('--team', default=None)
|
|
||||||
parser.add_argument('--slot', default=None, type=int)
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
ctx = Context(args.snes, args.connect, args.password, args.name, args.team, args.slot)
|
ctx = Context(args.snes, args.connect, args.password)
|
||||||
|
|
||||||
input_task = asyncio.create_task(console_loop(ctx))
|
input_task = asyncio.create_task(console_loop(ctx))
|
||||||
|
|
||||||
await snes_connect(ctx)
|
await snes_connect(ctx, ctx.snes_address)
|
||||||
|
|
||||||
if ctx.server_task is None:
|
if ctx.server_task is None:
|
||||||
ctx.server_task = asyncio.create_task(server_loop(ctx))
|
ctx.server_task = asyncio.create_task(server_loop(ctx))
|
||||||
|
@ -890,7 +897,8 @@ async def main():
|
||||||
|
|
||||||
|
|
||||||
await ctx.exit_event.wait()
|
await ctx.exit_event.wait()
|
||||||
|
ctx.server_address = None
|
||||||
|
ctx.snes_reconnect_address = None
|
||||||
|
|
||||||
await watcher_task
|
await watcher_task
|
||||||
|
|
||||||
|
|
198
MultiServer.py
198
MultiServer.py
|
@ -5,6 +5,7 @@ import functools
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import shlex
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import websockets
|
import websockets
|
||||||
import zlib
|
import zlib
|
||||||
|
@ -27,29 +28,23 @@ class Context:
|
||||||
self.data_filename = None
|
self.data_filename = None
|
||||||
self.save_filename = None
|
self.save_filename = None
|
||||||
self.disable_save = False
|
self.disable_save = False
|
||||||
self.players = 0
|
self.player_names = {}
|
||||||
self.rom_names = {}
|
self.rom_names = {}
|
||||||
self.locations = {}
|
self.locations = {}
|
||||||
self.host = host
|
self.host = host
|
||||||
self.port = port
|
self.port = port
|
||||||
self.password = password
|
self.password = password
|
||||||
self.server = None
|
self.server = None
|
||||||
|
self.countdown_timer = 0
|
||||||
self.clients = []
|
self.clients = []
|
||||||
self.received_items = {}
|
self.received_items = {}
|
||||||
|
|
||||||
def get_room_info(ctx : Context):
|
def get_room_info(ctx : Context):
|
||||||
return {
|
return {
|
||||||
'password': ctx.password is not None,
|
'password': ctx.password is not None,
|
||||||
'slots': ctx.players,
|
|
||||||
'players': [(client.name, client.team, client.slot) for client in ctx.clients if client.auth]
|
'players': [(client.name, client.team, client.slot) for client in ctx.clients if client.auth]
|
||||||
}
|
}
|
||||||
|
|
||||||
def same_name(lhs, rhs):
|
|
||||||
return lhs.lower() == rhs.lower()
|
|
||||||
|
|
||||||
def same_team(lhs, rhs):
|
|
||||||
return (type(lhs) is type(rhs)) and ((not lhs and not rhs) or (lhs.lower() == rhs.lower()))
|
|
||||||
|
|
||||||
async def send_msgs(websocket, msgs):
|
async def send_msgs(websocket, msgs):
|
||||||
if not websocket or not websocket.open or websocket.closed:
|
if not websocket or not websocket.open or websocket.closed:
|
||||||
return
|
return
|
||||||
|
@ -65,21 +60,21 @@ def broadcast_all(ctx : Context, msgs):
|
||||||
|
|
||||||
def broadcast_team(ctx : Context, team, msgs):
|
def broadcast_team(ctx : Context, team, msgs):
|
||||||
for client in ctx.clients:
|
for client in ctx.clients:
|
||||||
if client.auth and same_team(client.team, team):
|
if client.auth and client.team == team:
|
||||||
asyncio.create_task(send_msgs(client.socket, msgs))
|
asyncio.create_task(send_msgs(client.socket, msgs))
|
||||||
|
|
||||||
def notify_all(ctx : Context, text):
|
def notify_all(ctx : Context, text):
|
||||||
print("Notice (all): %s" % text)
|
print("Notice (all): %s" % text)
|
||||||
broadcast_all(ctx, [['Print', text]])
|
broadcast_all(ctx, [['Print', text]])
|
||||||
|
|
||||||
def notify_team(ctx : Context, team : str, text : str):
|
def notify_team(ctx : Context, team : int, text : str):
|
||||||
print("Team notice (%s): %s" % ("Default" if not team else team, text))
|
print("Notice (Team #%d): %s" % (team+1, text))
|
||||||
broadcast_team(ctx, team, [['Print', text]])
|
broadcast_team(ctx, team, [['Print', text]])
|
||||||
|
|
||||||
def notify_client(client : Client, text : str):
|
def notify_client(client : Client, text : str):
|
||||||
if not client.auth:
|
if not client.auth:
|
||||||
return
|
return
|
||||||
print("Player notice (%s): %s" % (client.name, text))
|
print("Notice (Player %s in team %d): %s" % (client.name, client.team+1, text))
|
||||||
asyncio.create_task(send_msgs(client.socket, [['Print', text]]))
|
asyncio.create_task(send_msgs(client.socket, [['Print', text]]))
|
||||||
|
|
||||||
async def server(websocket, path, ctx : Context):
|
async def server(websocket, path, ctx : Context):
|
||||||
|
@ -112,47 +107,44 @@ async def on_client_disconnected(ctx : Context, client : Client):
|
||||||
await on_client_left(ctx, client)
|
await on_client_left(ctx, client)
|
||||||
|
|
||||||
async def on_client_joined(ctx : Context, client : Client):
|
async def on_client_joined(ctx : Context, client : Client):
|
||||||
notify_all(ctx, "%s has joined the game as player %d for %s" % (client.name, client.slot, "the default team" if not client.team else "team %s" % client.team))
|
notify_all(ctx, "%s (Team #%d) has joined the game" % (client.name, client.team + 1))
|
||||||
|
|
||||||
async def on_client_left(ctx : Context, client : Client):
|
async def on_client_left(ctx : Context, client : Client):
|
||||||
notify_all(ctx, "%s (Player %d, %s) has left the game" % (client.name, client.slot, "Default team" if not client.team else "Team %s" % client.team))
|
notify_all(ctx, "%s (Team #%d) has left the game" % (client.name, client.team + 1))
|
||||||
|
|
||||||
|
async def countdown(ctx : Context, timer):
|
||||||
|
notify_all(ctx, f'[Server]: Starting countdown of {timer}s')
|
||||||
|
if ctx.countdown_timer:
|
||||||
|
ctx.countdown_timer = timer
|
||||||
|
return
|
||||||
|
|
||||||
|
ctx.countdown_timer = timer
|
||||||
|
while ctx.countdown_timer > 0:
|
||||||
|
notify_all(ctx, f'[Server]: {ctx.countdown_timer}')
|
||||||
|
ctx.countdown_timer -= 1
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
notify_all(ctx, f'[Server]: GO')
|
||||||
|
|
||||||
def get_connected_players_string(ctx : Context):
|
def get_connected_players_string(ctx : Context):
|
||||||
auth_clients = [c for c in ctx.clients if c.auth]
|
auth_clients = [c for c in ctx.clients if c.auth]
|
||||||
if not auth_clients:
|
if not auth_clients:
|
||||||
return 'No player connected'
|
return 'No player connected'
|
||||||
|
|
||||||
auth_clients.sort(key=lambda c: ('' if not c.team else c.team.lower(), c.slot))
|
auth_clients.sort(key=lambda c: (c.team, c.slot))
|
||||||
current_team = 0
|
current_team = 0
|
||||||
text = ''
|
text = 'Team #1: '
|
||||||
for c in auth_clients:
|
for c in auth_clients:
|
||||||
if c.team != current_team:
|
if c.team != current_team:
|
||||||
text += '::' + ('default team' if not c.team else c.team) + ':: '
|
text += f':: Team #{c.team + 1}: '
|
||||||
current_team = c.team
|
current_team = c.team
|
||||||
text += '%d:%s ' % (c.slot, c.name)
|
text += f'{c.name} '
|
||||||
return 'Connected players: ' + text[:-1]
|
return 'Connected players: ' + text[:-1]
|
||||||
|
|
||||||
def get_player_name_in_team(ctx : Context, team, slot):
|
|
||||||
for client in ctx.clients:
|
|
||||||
if client.auth and same_team(team, client.team) and client.slot == slot:
|
|
||||||
return client.name
|
|
||||||
return "Player %d" % slot
|
|
||||||
|
|
||||||
def get_client_from_name(ctx : Context, name):
|
|
||||||
for client in ctx.clients:
|
|
||||||
if client.auth and same_name(name, client.name):
|
|
||||||
return client
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_received_items(ctx : Context, team, player):
|
def get_received_items(ctx : Context, team, player):
|
||||||
for (c_team, c_id), items in ctx.received_items.items():
|
return ctx.received_items.setdefault((team, player), [])
|
||||||
if c_id == player and same_team(c_team, team):
|
|
||||||
return items
|
|
||||||
ctx.received_items[(team, player)] = []
|
|
||||||
return ctx.received_items[(team, player)]
|
|
||||||
|
|
||||||
def tuplize_received_items(items):
|
def tuplize_received_items(items):
|
||||||
return [(item.item, item.location, item.player_id, item.player_name) for item in items]
|
return [(item.item, item.location, item.player) for item in items]
|
||||||
|
|
||||||
def send_new_items(ctx : Context):
|
def send_new_items(ctx : Context):
|
||||||
for client in ctx.clients:
|
for client in ctx.clients:
|
||||||
|
@ -163,12 +155,12 @@ def send_new_items(ctx : Context):
|
||||||
asyncio.create_task(send_msgs(client.socket, [['ReceivedItems', (client.send_index, tuplize_received_items(items)[client.send_index:])]]))
|
asyncio.create_task(send_msgs(client.socket, [['ReceivedItems', (client.send_index, tuplize_received_items(items)[client.send_index:])]]))
|
||||||
client.send_index = len(items)
|
client.send_index = len(items)
|
||||||
|
|
||||||
def forfeit_player(ctx : Context, team, slot, name):
|
def forfeit_player(ctx : Context, team, slot):
|
||||||
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]
|
||||||
notify_all(ctx, "%s (Player %d) in team %s has forfeited" % (name, slot, team if team else 'default'))
|
notify_all(ctx, "%s (Team #%d) has forfeited" % (ctx.player_names[(team, slot)], team + 1))
|
||||||
register_location_checks(ctx, name, team, slot, all_locations)
|
register_location_checks(ctx, team, slot, all_locations)
|
||||||
|
|
||||||
def register_location_checks(ctx : Context, name, team, slot, locations):
|
def register_location_checks(ctx : Context, team, slot, locations):
|
||||||
found_items = False
|
found_items = False
|
||||||
for location in locations:
|
for location in locations:
|
||||||
if (location, slot) in ctx.locations:
|
if (location, slot) in ctx.locations:
|
||||||
|
@ -177,23 +169,21 @@ def register_location_checks(ctx : Context, name, team, slot, locations):
|
||||||
found = False
|
found = False
|
||||||
recvd_items = get_received_items(ctx, team, target_player)
|
recvd_items = get_received_items(ctx, team, target_player)
|
||||||
for recvd_item in recvd_items:
|
for recvd_item in recvd_items:
|
||||||
if recvd_item.location == location and recvd_item.player_id == slot:
|
if recvd_item.location == location and recvd_item.player == slot:
|
||||||
found = True
|
found = True
|
||||||
break
|
break
|
||||||
if not found:
|
if not found:
|
||||||
new_item = ReceivedItem(target_item, location, slot, name)
|
new_item = ReceivedItem(target_item, location, slot)
|
||||||
recvd_items.append(new_item)
|
recvd_items.append(new_item)
|
||||||
target_player_name = get_player_name_in_team(ctx, team, target_player)
|
broadcast_team(ctx, team, [['ItemSent', (slot, location, target_player, target_item)]])
|
||||||
broadcast_team(ctx, team, [['ItemSent', (name, target_player_name, target_item, location)]])
|
print('(Team #%d) %s sent %s to %s (%s)' % (team, ctx.player_names[(team, slot)], get_item_name_from_id(target_item), ctx.player_names[(team, target_player)], get_location_name_from_address(location)))
|
||||||
print('(%s) %s sent %s to %s (%s)' % (team if team else 'Team', name, get_item_name_from_id(target_item), target_player_name, get_location_name_from_address(location)))
|
|
||||||
found_items = True
|
found_items = True
|
||||||
send_new_items(ctx)
|
send_new_items(ctx)
|
||||||
|
|
||||||
if found_items and not ctx.disable_save:
|
if found_items and not ctx.disable_save:
|
||||||
try:
|
try:
|
||||||
with open(ctx.save_filename, "wb") as f:
|
with open(ctx.save_filename, "wb") as f:
|
||||||
jsonstr = json.dumps((ctx.players,
|
jsonstr = json.dumps((list(ctx.rom_names.items()),
|
||||||
[(k, v) for k, v in ctx.rom_names.items()],
|
|
||||||
[(k, [i.__dict__ for i in v]) for k, v in ctx.received_items.items()]))
|
[(k, [i.__dict__ for i in v]) for k, v in ctx.received_items.items()]))
|
||||||
f.write(zlib.compress(jsonstr.encode("utf-8")))
|
f.write(zlib.compress(jsonstr.encode("utf-8")))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -207,50 +197,30 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args):
|
||||||
if cmd == 'Connect':
|
if cmd == 'Connect':
|
||||||
if not args or type(args) is not dict or \
|
if not args or type(args) is not dict or \
|
||||||
'password' not in args or type(args['password']) not in [str, type(None)] or \
|
'password' not in args or type(args['password']) not in [str, type(None)] or \
|
||||||
'name' not in args or type(args['name']) is not str or \
|
'rom' not in args or type(args['rom']) is not list:
|
||||||
'team' not in args or type(args['team']) not in [str, type(None)] or \
|
|
||||||
'slot' not in args or type(args['slot']) not in [int, type(None)]:
|
|
||||||
await send_msgs(client.socket, [['InvalidArguments', 'Connect']])
|
await send_msgs(client.socket, [['InvalidArguments', 'Connect']])
|
||||||
return
|
return
|
||||||
|
|
||||||
errors = set()
|
errors = set()
|
||||||
if ctx.password is not None and ('password' not in args or args['password'] != ctx.password):
|
if ctx.password is not None and args['password'] != ctx.password:
|
||||||
errors.add('InvalidPassword')
|
errors.add('InvalidPassword')
|
||||||
|
|
||||||
if 'name' not in args or not args['name'] or not re.match(r'\w{1,10}', args['name']):
|
if tuple(args['rom']) not in ctx.rom_names:
|
||||||
errors.add('InvalidName')
|
errors.add('InvalidRom')
|
||||||
elif any([same_name(c.name, args['name']) for c in ctx.clients if c.auth]):
|
|
||||||
errors.add('NameAlreadyTaken')
|
|
||||||
else:
|
else:
|
||||||
client.name = args['name']
|
team, slot = ctx.rom_names[tuple(args['rom'])]
|
||||||
|
if any([c.slot == slot and c.team == team for c in ctx.clients if c.auth]):
|
||||||
if 'team' in args and args['team'] is not None and not re.match(r'\w{1,15}', args['team']):
|
errors.add('SlotAlreadyTaken')
|
||||||
errors.add('InvalidTeam')
|
else:
|
||||||
else:
|
client.name = ctx.player_names[(team, slot)]
|
||||||
client.team = args['team'] if 'team' in args else None
|
client.team = team
|
||||||
|
client.slot = slot
|
||||||
if 'slot' in args and any([c.slot == args['slot'] for c in ctx.clients if c.auth and same_team(c.team, client.team)]):
|
|
||||||
errors.add('SlotAlreadyTaken')
|
|
||||||
elif 'slot' not in args or not args['slot']:
|
|
||||||
for slot in range(1, ctx.players + 1):
|
|
||||||
if slot not in [c.slot for c in ctx.clients if c.auth and same_team(c.team, client.team)]:
|
|
||||||
client.slot = slot
|
|
||||||
break
|
|
||||||
elif slot == ctx.players:
|
|
||||||
errors.add('SlotAlreadyTaken')
|
|
||||||
elif args['slot'] not in range(1, ctx.players + 1):
|
|
||||||
errors.add('InvalidSlot')
|
|
||||||
else:
|
|
||||||
client.slot = args['slot']
|
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
client.name = None
|
|
||||||
client.team = None
|
|
||||||
client.slot = None
|
|
||||||
await send_msgs(client.socket, [['ConnectionRefused', list(errors)]])
|
await send_msgs(client.socket, [['ConnectionRefused', list(errors)]])
|
||||||
else:
|
else:
|
||||||
client.auth = True
|
client.auth = True
|
||||||
reply = [['Connected', ctx.rom_names[client.slot]]]
|
reply = [['Connected', [(client.team, client.slot), [(p, n) for (t, p), n in ctx.player_names.items() if t == client.team]]]]
|
||||||
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))])
|
||||||
|
@ -271,7 +241,7 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args):
|
||||||
if type(args) is not list:
|
if type(args) is not list:
|
||||||
await send_msgs(client.socket, [['InvalidArguments', 'LocationChecks']])
|
await send_msgs(client.socket, [['InvalidArguments', 'LocationChecks']])
|
||||||
return
|
return
|
||||||
register_location_checks(ctx, client.name, client.team, client.slot, args)
|
register_location_checks(ctx, client.team, client.slot, args)
|
||||||
|
|
||||||
if cmd == 'Say':
|
if cmd == 'Say':
|
||||||
if type(args) is not str or not args.isprintable():
|
if type(args) is not str or not args.isprintable():
|
||||||
|
@ -280,10 +250,16 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args):
|
||||||
|
|
||||||
notify_all(ctx, client.name + ': ' + args)
|
notify_all(ctx, client.name + ': ' + args)
|
||||||
|
|
||||||
if args[:8] == '!players':
|
if args.startswith('!players'):
|
||||||
notify_all(ctx, get_connected_players_string(ctx))
|
notify_all(ctx, get_connected_players_string(ctx))
|
||||||
if args[:8] == '!forfeit':
|
if args.startswith('!forfeit'):
|
||||||
forfeit_player(ctx, client.team, client.slot, client.name)
|
forfeit_player(ctx, client.team, client.slot)
|
||||||
|
if args.startswith('!countdown'):
|
||||||
|
try:
|
||||||
|
timer = int(args.split()[1])
|
||||||
|
except (IndexError, ValueError):
|
||||||
|
timer = 10
|
||||||
|
asyncio.create_task(countdown(ctx, timer))
|
||||||
|
|
||||||
def set_password(ctx : Context, password):
|
def set_password(ctx : Context, password):
|
||||||
ctx.password = password
|
ctx.password = password
|
||||||
|
@ -293,7 +269,7 @@ async def console(ctx : Context):
|
||||||
while True:
|
while True:
|
||||||
input = await aioconsole.ainput()
|
input = await aioconsole.ainput()
|
||||||
|
|
||||||
command = input.split()
|
command = shlex.split(input)
|
||||||
if not command:
|
if not command:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -306,27 +282,34 @@ async def console(ctx : Context):
|
||||||
if command[0] == '/password':
|
if command[0] == '/password':
|
||||||
set_password(ctx, command[1] if len(command) > 1 else None)
|
set_password(ctx, command[1] if len(command) > 1 else None)
|
||||||
if command[0] == '/kick' and len(command) > 1:
|
if command[0] == '/kick' and len(command) > 1:
|
||||||
client = get_client_from_name(ctx, command[1])
|
team = int(command[2]) - 1 if len(command) > 2 and command[2].isdigit() else None
|
||||||
if client and client.socket and not client.socket.closed:
|
for client in ctx.clients:
|
||||||
await client.socket.close()
|
if client.auth and client.name.lower() == command[1].lower() and (team is None or team == client.team):
|
||||||
|
if client.socket and not client.socket.closed:
|
||||||
|
await client.socket.close()
|
||||||
|
|
||||||
if command[0] == '/forfeitslot' and len(command) == 3 and command[2].isdigit():
|
if command[0] == '/forfeitslot' and len(command) > 1 and command[1].isdigit():
|
||||||
team = command[1] if command[1] != 'default' else None
|
if len(command) > 2 and command[2].isdigit():
|
||||||
slot = int(command[2])
|
team = int(command[1]) - 1
|
||||||
name = get_player_name_in_team(ctx, team, slot)
|
slot = int(command[2])
|
||||||
forfeit_player(ctx, team, slot, name)
|
else:
|
||||||
|
team = 0
|
||||||
|
slot = int(command[1])
|
||||||
|
forfeit_player(ctx, team, slot)
|
||||||
if command[0] == '/forfeitplayer' and len(command) > 1:
|
if command[0] == '/forfeitplayer' and len(command) > 1:
|
||||||
client = get_client_from_name(ctx, command[1])
|
team = int(command[2]) - 1 if len(command) > 2 and command[2].isdigit() else None
|
||||||
if client:
|
for client in ctx.clients:
|
||||||
forfeit_player(ctx, client.team, client.slot, client.name)
|
if client.auth and client.name.lower() == command[1].lower() and (team is None or team == client.team):
|
||||||
|
if client.socket and not client.socket.closed:
|
||||||
|
forfeit_player(ctx, client.team, client.slot)
|
||||||
if command[0] == '/senditem' and len(command) > 2:
|
if command[0] == '/senditem' and len(command) > 2:
|
||||||
[(player, item)] = re.findall(r'\S* (\S*) (.*)', input)
|
[(player, item)] = re.findall(r'\S* (\S*) (.*)', input)
|
||||||
if item in Items.item_table:
|
if item in Items.item_table:
|
||||||
client = get_client_from_name(ctx, player)
|
for client in ctx.clients:
|
||||||
if client:
|
if client.auth and client.name.lower() == player.lower():
|
||||||
new_item = ReceivedItem(Items.item_table[item][3], "cheat console", 0, "server")
|
new_item = ReceivedItem(Items.item_table[item][3], "cheat console", client.slot)
|
||||||
get_received_items(ctx, client.team, client.slot).append(new_item)
|
get_received_items(ctx, client.team, client.slot).append(new_item)
|
||||||
notify_all(ctx, 'Cheat console: sending "' + item + '" to ' + client.name)
|
notify_all(ctx, 'Cheat console: sending "' + item + '" to ' + client.name)
|
||||||
send_new_items(ctx)
|
send_new_items(ctx)
|
||||||
else:
|
else:
|
||||||
print("Unknown item: " + item)
|
print("Unknown item: " + item)
|
||||||
|
@ -358,15 +341,17 @@ async def main():
|
||||||
|
|
||||||
with open(ctx.data_filename, 'rb') as f:
|
with open(ctx.data_filename, 'rb') as f:
|
||||||
jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8"))
|
jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8"))
|
||||||
ctx.players = jsonobj[0]
|
for team, names in enumerate(jsonobj[0]):
|
||||||
ctx.rom_names = {k: v for k, v in jsonobj[1]}
|
for player, name in enumerate(names, 1):
|
||||||
|
ctx.player_names[(team, player)] = name
|
||||||
|
ctx.rom_names = {tuple(rom): (team, slot) for slot, team, rom in jsonobj[1]}
|
||||||
ctx.locations = {tuple(k): tuple(v) for k, v in jsonobj[2]}
|
ctx.locations = {tuple(k): tuple(v) for k, v in jsonobj[2]}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print('Failed to read multiworld data (%s)' % e)
|
print('Failed to read multiworld data (%s)' % e)
|
||||||
return
|
return
|
||||||
|
|
||||||
ip = urllib.request.urlopen('https://v4.ident.me').read().decode('utf8') if not ctx.host else ctx.host
|
ip = urllib.request.urlopen('https://v4.ident.me').read().decode('utf8') if not ctx.host else ctx.host
|
||||||
print('Hosting game of %d players (%s) at %s:%d' % (ctx.players, 'No password' if not ctx.password else 'Password: %s' % ctx.password, ip, ctx.port))
|
print('Hosting game at %s:%d (%s)' % (ip, ctx.port, 'No password' if not ctx.password else 'Password: %s' % ctx.password))
|
||||||
|
|
||||||
ctx.disable_save = args.disable_save
|
ctx.disable_save = args.disable_save
|
||||||
if not ctx.disable_save:
|
if not ctx.disable_save:
|
||||||
|
@ -375,10 +360,9 @@ async def main():
|
||||||
try:
|
try:
|
||||||
with open(ctx.save_filename, 'rb') as f:
|
with open(ctx.save_filename, 'rb') as f:
|
||||||
jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8"))
|
jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8"))
|
||||||
players = jsonobj[0]
|
rom_names = jsonobj[0]
|
||||||
rom_names = {k: v for k, v in jsonobj[1]}
|
received_items = {tuple(k): [ReceivedItem(**i) for i in v] for k, v in jsonobj[1]}
|
||||||
received_items = {tuple(k): [ReceivedItem(**i) for i in v] for k, v in jsonobj[2]}
|
if not all([ctx.rom_names[tuple(rom)] == (team, slot) for rom, (team, slot) in rom_names]):
|
||||||
if players != ctx.players or rom_names != ctx.rom_names:
|
|
||||||
raise Exception('Save file mismatch, will start a new game')
|
raise Exception('Save file mismatch, will start a new game')
|
||||||
ctx.received_items = received_items
|
ctx.received_items = received_items
|
||||||
print('Loaded save file with %d received items for %d players' % (sum([len(p) for p in received_items.values()]), len(received_items)))
|
print('Loaded save file with %d received items for %d players' % (sum([len(p) for p in received_items.values()]), len(received_items)))
|
||||||
|
|
|
@ -39,6 +39,7 @@ def main():
|
||||||
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
|
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
|
||||||
parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255))
|
parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255))
|
||||||
parser.add_argument('--names', default='')
|
parser.add_argument('--names', default='')
|
||||||
|
parser.add_argument('--teams', default=1, type=lambda value: max(int(value), 1))
|
||||||
parser.add_argument('--create_spoiler', action='store_true')
|
parser.add_argument('--create_spoiler', action='store_true')
|
||||||
parser.add_argument('--rom')
|
parser.add_argument('--rom')
|
||||||
parser.add_argument('--enemizercli')
|
parser.add_argument('--enemizercli')
|
||||||
|
|
|
@ -24,6 +24,7 @@ def main(args):
|
||||||
|
|
||||||
# initialize the world
|
# initialize the world
|
||||||
world = World(1, 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, False, False, False, None, False)
|
world = World(1, 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, False, False, False, None, False)
|
||||||
|
world.player_names[1].append("Player 1")
|
||||||
logger = logging.getLogger('')
|
logger = logging.getLogger('')
|
||||||
|
|
||||||
hasher = hashlib.md5()
|
hasher = hashlib.md5()
|
||||||
|
@ -69,7 +70,7 @@ def main(args):
|
||||||
logger.info('Patching ROM.')
|
logger.info('Patching ROM.')
|
||||||
|
|
||||||
rom = LocalRom(args.rom)
|
rom = LocalRom(args.rom)
|
||||||
patch_rom(world, 1, rom, False)
|
patch_rom(world, rom, 1, 1, False)
|
||||||
|
|
||||||
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, args.sprite, args.ow_palettes, args.uw_palettes)
|
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, args.sprite, args.ow_palettes, args.uw_palettes)
|
||||||
|
|
||||||
|
|
40
Rom.py
40
Rom.py
|
@ -27,6 +27,7 @@ class JsonRom(object):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.name = None
|
self.name = None
|
||||||
|
self.hash = None
|
||||||
self.orig_buffer = None
|
self.orig_buffer = None
|
||||||
self.patches = {}
|
self.patches = {}
|
||||||
self.addresses = []
|
self.addresses = []
|
||||||
|
@ -72,6 +73,7 @@ class LocalRom(object):
|
||||||
|
|
||||||
def __init__(self, file, patch=True):
|
def __init__(self, file, patch=True):
|
||||||
self.name = None
|
self.name = None
|
||||||
|
self.hash = None
|
||||||
self.orig_buffer = None
|
self.orig_buffer = None
|
||||||
with open(file, 'rb') as stream:
|
with open(file, 'rb') as stream:
|
||||||
self.buffer = read_rom(stream)
|
self.buffer = read_rom(stream)
|
||||||
|
@ -469,7 +471,7 @@ class Sprite(object):
|
||||||
# split into palettes of 15 colors
|
# split into palettes of 15 colors
|
||||||
return array_chunk(palette_as_colors, 15)
|
return array_chunk(palette_as_colors, 15)
|
||||||
|
|
||||||
def patch_rom(world, player, rom, enemized):
|
def patch_rom(world, rom, player, team, enemized):
|
||||||
random.seed(world.rom_seeds[player])
|
random.seed(world.rom_seeds[player])
|
||||||
|
|
||||||
# progressive bow silver arrow hint hack
|
# progressive bow silver arrow hint hack
|
||||||
|
@ -1222,13 +1224,18 @@ def patch_rom(world, player, rom, enemized):
|
||||||
rom.write_byte(0xFED31, 0x2A) # preopen bombable exit
|
rom.write_byte(0xFED31, 0x2A) # preopen bombable exit
|
||||||
rom.write_byte(0xFEE41, 0x2A) # preopen bombable exit
|
rom.write_byte(0xFEE41, 0x2A) # preopen bombable exit
|
||||||
|
|
||||||
write_strings(rom, world, player)
|
write_strings(rom, world, player, team)
|
||||||
|
|
||||||
# set rom name
|
# set rom name
|
||||||
# 21 bytes
|
# 21 bytes
|
||||||
from Main import __version__
|
from Main import __version__
|
||||||
rom.name = bytearray('ER{0}_{1}_{2:09}\0'.format(__version__.split('-')[0].replace('.','')[0:3], player, world.seed), 'utf8')
|
rom.name = bytearray(f'ER{__version__.split("-")[0].replace(".","")[0:3]}_{team+1}_{player}_{world.seed:09}\0', 'utf8')[:21]
|
||||||
rom.write_bytes(0x7FC0, rom.name[0:21])
|
rom.name.extend([0] * (21 - len(rom.name)))
|
||||||
|
rom.write_bytes(0x7FC0, rom.name)
|
||||||
|
|
||||||
|
# set player names
|
||||||
|
for p in range(1, min(world.players, 64) + 1):
|
||||||
|
rom.write_bytes(0x186380 + ((p - 1) * 32), hud_format_text(world.player_names[p][team]))
|
||||||
|
|
||||||
# Write title screen Code
|
# Write title screen Code
|
||||||
hashint = int(rom.get_hash(), 16)
|
hashint = int(rom.get_hash(), 16)
|
||||||
|
@ -1240,6 +1247,7 @@ def patch_rom(world, player, rom, enemized):
|
||||||
hashint & 0x1F,
|
hashint & 0x1F,
|
||||||
]
|
]
|
||||||
rom.write_bytes(0x180215, code)
|
rom.write_bytes(0x180215, code)
|
||||||
|
rom.hash = code
|
||||||
|
|
||||||
return rom
|
return rom
|
||||||
|
|
||||||
|
@ -1303,7 +1311,7 @@ def hud_format_text(text):
|
||||||
return output[:32]
|
return output[:32]
|
||||||
|
|
||||||
|
|
||||||
def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite, ow_palettes, uw_palettes, names = None):
|
def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite, ow_palettes, uw_palettes):
|
||||||
if sprite and not isinstance(sprite, Sprite):
|
if sprite and not isinstance(sprite, Sprite):
|
||||||
sprite = Sprite(sprite) if os.path.isfile(sprite) else get_sprite_from_name(sprite)
|
sprite = Sprite(sprite) if os.path.isfile(sprite) else get_sprite_from_name(sprite)
|
||||||
|
|
||||||
|
@ -1372,11 +1380,6 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr
|
||||||
elif uw_palettes == 'blackout':
|
elif uw_palettes == 'blackout':
|
||||||
blackout_uw_palettes(rom)
|
blackout_uw_palettes(rom)
|
||||||
|
|
||||||
# set player names
|
|
||||||
for player, name in names.items():
|
|
||||||
if 0 < player <= 64:
|
|
||||||
rom.write_bytes(0x186380 + ((player - 1) * 32), hud_format_text(name))
|
|
||||||
|
|
||||||
if isinstance(rom, LocalRom):
|
if isinstance(rom, LocalRom):
|
||||||
rom.write_crc()
|
rom.write_crc()
|
||||||
|
|
||||||
|
@ -1513,12 +1516,15 @@ def blackout_uw_palettes(rom):
|
||||||
rom.write_bytes(i+44, [0] * 76)
|
rom.write_bytes(i+44, [0] * 76)
|
||||||
rom.write_bytes(i+136, [0] * 44)
|
rom.write_bytes(i+136, [0] * 44)
|
||||||
|
|
||||||
|
def get_hash_string(hash):
|
||||||
|
return ", ".join([hash_alphabet[code & 0x1F] for code in hash])
|
||||||
|
|
||||||
def write_string_to_rom(rom, target, string):
|
def write_string_to_rom(rom, target, string):
|
||||||
address, maxbytes = text_addresses[target]
|
address, maxbytes = text_addresses[target]
|
||||||
rom.write_bytes(address, MultiByteTextMapper.convert(string, maxbytes))
|
rom.write_bytes(address, MultiByteTextMapper.convert(string, maxbytes))
|
||||||
|
|
||||||
|
|
||||||
def write_strings(rom, world, player):
|
def write_strings(rom, world, player, team):
|
||||||
tt = TextTable()
|
tt = TextTable()
|
||||||
tt.removeUnwantedText()
|
tt.removeUnwantedText()
|
||||||
|
|
||||||
|
@ -1536,11 +1542,11 @@ def write_strings(rom, world, player):
|
||||||
hint = dest.hint_text if dest.hint_text else "something"
|
hint = dest.hint_text if dest.hint_text else "something"
|
||||||
if dest.player != player:
|
if dest.player != player:
|
||||||
if ped_hint:
|
if ped_hint:
|
||||||
hint += " for p%d!" % dest.player
|
hint += f" for {world.player_names[dest.player][team]}!"
|
||||||
elif type(dest) in [Region, Location]:
|
elif type(dest) in [Region, Location]:
|
||||||
hint += " in p%d's world" % dest.player
|
hint += f" in {world.player_names[dest.player][team]}'s world"
|
||||||
else:
|
else:
|
||||||
hint += " for p%d" % dest.player
|
hint += f" for {world.player_names[dest.player][team]}"
|
||||||
return hint
|
return hint
|
||||||
|
|
||||||
# For hints, first we write hints about entrances, some from the inconvenient list others from all reasonable entrances.
|
# For hints, first we write hints about entrances, some from the inconvenient list others from all reasonable entrances.
|
||||||
|
@ -2281,3 +2287,9 @@ BigKeys = ['Big Key (Eastern Palace)',
|
||||||
'Big Key (Turtle Rock)',
|
'Big Key (Turtle Rock)',
|
||||||
'Big Key (Ganons Tower)'
|
'Big Key (Ganons Tower)'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
hash_alphabet = [
|
||||||
|
"Bow", "Boomerang", "Hookshot", "Bomb", "Mushroom", "Powder", "Rod", "Pendant", "Bombos", "Ether", "Quake",
|
||||||
|
"Lamp", "Hammer", "Shovel", "Ocarina", "Bug Net", "Book", "Bottle", "Potion", "Cane", "Cape", "Mirror", "Boots",
|
||||||
|
"Gloves", "Flippers", "Pearl", "Shield", "Tunic", "Heart", "Map", "Compass", "Key"
|
||||||
|
]
|
||||||
|
|
15
Utils.py
15
Utils.py
|
@ -3,9 +3,6 @@ import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
def parse_names_string(names):
|
|
||||||
return {player: name for player, name in enumerate([n for n in re.split(r'[, ]', names) if n], 1)}
|
|
||||||
|
|
||||||
def int16_as_bytes(value):
|
def int16_as_bytes(value):
|
||||||
value = value & 0xFFFF
|
value = value & 0xFFFF
|
||||||
return [value & 0xFF, (value >> 8) & 0xFF]
|
return [value & 0xFF, (value >> 8) & 0xFF]
|
||||||
|
@ -20,6 +17,18 @@ def pc_to_snes(value):
|
||||||
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):
|
||||||
|
names = [n for n in re.split(r'[, ]', names) if n]
|
||||||
|
ret = []
|
||||||
|
while names or len(ret) < teams:
|
||||||
|
team = [n[:16] for n in names[:players]]
|
||||||
|
while len(team) != players:
|
||||||
|
team.append(f"Player {len(team) + 1}")
|
||||||
|
ret.append(team)
|
||||||
|
|
||||||
|
names = names[players:]
|
||||||
|
return ret
|
||||||
|
|
||||||
def is_bundled():
|
def is_bundled():
|
||||||
return getattr(sys, 'frozen', False)
|
return getattr(sys, 'frozen', False)
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue