Merge remote-tracking branch 'remotes/multi/multiworld_31' into multidoors

This commit is contained in:
compiling
2020-01-19 18:18:43 +11:00
12 changed files with 583 additions and 512 deletions

View File

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

View File

@@ -14,6 +14,7 @@ class World(object):
def __init__(self, players, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, goal, algorithm, accessibility, shuffle_ganon, retro, custom, customitemarray, hints): def __init__(self, players, shuffle, doorShuffle, 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.doorShuffle = doorShuffle.copy() self.doorShuffle = doorShuffle.copy()
self.logic = logic.copy() self.logic = logic.copy()
@@ -73,6 +74,8 @@ 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('remote_items', False)
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)
@@ -105,6 +108,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
@@ -279,6 +288,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)
@@ -291,6 +301,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)
@@ -876,10 +887,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):
@@ -916,11 +924,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):
@@ -969,10 +974,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)
@unique @unique
@@ -1314,10 +1316,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):
@@ -1336,6 +1336,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
@@ -1362,10 +1363,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
@@ -1438,6 +1436,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.doors = OrderedDict() self.doors = OrderedDict()
self.doorTypes = OrderedDict() self.doorTypes = OrderedDict()
@@ -1476,8 +1475,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))
@@ -1566,7 +1565,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):
@@ -1578,6 +1578,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
@@ -1591,30 +1593,35 @@ 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('Door Shuffle: %s\n' % self.metadata['door_shuffle']) outfile.write('Mode: %s\n' % self.metadata['mode'][player])
outfile.write('Crystals required for GT: %s\n' % self.metadata['gt_crystals']) outfile.write('Retro: %s\n' % ('Yes' if self.metadata['retro'][player] else 'No'))
outfile.write('Crystals required for Ganon: %s\n' % self.metadata['ganon_crystals']) outfile.write('Swords: %s\n' % self.metadata['weapons'][player])
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('Goal: %s\n' % self.metadata['goal'][player])
outfile.write('Accessibility: %s\n' % self.metadata['accessibility']) outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player])
outfile.write('Map shuffle: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['mapshuffle'].items()}) outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player])
outfile.write('Compass shuffle: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['compassshuffle'].items()}) outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player])
outfile.write('Small Key shuffle: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['keyshuffle'].items()}) outfile.write('Crystals required for GT: %s\n' % self.metadata['gt_crystals'][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 Ganon: %s\n' % self.metadata['ganon_crystals'][player])
outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle']) outfile.write('Pyramid hole pre-opened: %s\n' % ('Yes' if self.metadata['open_pyramid'][player] else 'No'))
outfile.write('Enemy shuffle: %s\n' % self.metadata['enemy_shuffle']) outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player])
outfile.write('Enemy health: %s\n' % self.metadata['enemy_health']) outfile.write('Map shuffle: %s\n' % ('Yes' if self.metadata['mapshuffle'][player] else 'No'))
outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage']) outfile.write('Compass shuffle: %s\n' % ('Yes' if self.metadata['compassshuffle'][player] else 'No'))
outfile.write('Hints: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['hints'].items()}) 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.doors: if self.doors:
outfile.write('\n\nDoors:\n\n') outfile.write('\n\nDoors:\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.doors.values()])) 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.doors.values()]))
@@ -1623,17 +1630,13 @@ class Spoiler(object):
outfile.write('\n'.join(['%s%s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', entry['doorNames'], entry['type']) for entry in self.doorTypes.values()])) outfile.write('\n'.join(['%s%s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', entry['doorNames'], entry['type']) for entry in self.doorTypes.values()]))
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')

View File

@@ -278,8 +278,10 @@ def parse_arguments(argv, no_defaults=False):
parser.add_argument('--enemy_damage', default=defval('default'), choices=['default', 'shuffled', 'chaos']) parser.add_argument('--enemy_damage', default=defval('default'), choices=['default', 'shuffled', 'chaos'])
parser.add_argument('--shufflepots', default=defval(False), action='store_true') parser.add_argument('--shufflepots', default=defval(False), action='store_true')
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('--remote_items', default=defval(False), action='store_true')
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')
@@ -302,7 +304,8 @@ def parse_arguments(argv, no_defaults=False):
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
'retro', 'accessibility', 'hints', 'beemizer', 'retro', 'accessibility', 'hints', 'beemizer',
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots', 'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots',
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep']: 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep',
'remote_items']:
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
if player == 1: if player == 1:
setattr(ret, name, {1: value}) setattr(ret, name, {1: value})

24
Gui.py
View File

@@ -15,7 +15,7 @@ from DungeonRandomizer 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):
@@ -509,11 +509,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)
@@ -613,20 +609,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)
@@ -642,18 +629,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)

116
Main.py
View File

@@ -13,7 +13,7 @@ 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 Doors import create_doors from Doors import create_doors
from DoorShuffle import link_doors from DoorShuffle import link_doors
from RoomData import create_rooms from RoomData import create_rooms
@@ -21,7 +21,7 @@ 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.0.1-pre' __version__ = '0.0.1-pre'
@@ -42,6 +42,7 @@ def main(args, seed=None):
world.seed = int(seed) world.seed = int(seed)
random.seed(world.seed) random.seed(world.seed)
world.remote_items = args.remote_items.copy()
world.mapshuffle = args.mapshuffle.copy() world.mapshuffle = args.mapshuffle.copy()
world.compassshuffle = args.compassshuffle.copy() world.compassshuffle = args.compassshuffle.copy()
world.keyshuffle = args.keyshuffle.copy() world.keyshuffle = args.keyshuffle.copy()
@@ -57,7 +58,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 Door Randomizer Version %s - Seed: %s\n\n', __version__, world.seed) logger.info('ALttP Door 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]]
@@ -144,62 +154,68 @@ def main(args, seed=None):
logger.info('Patching ROM.') logger.info('Patching ROM.')
player_names = parse_names_string(args.names)
outfilebase = 'DR_%s' % (args.outputname if args.outputname else world.seed) outfilebase = 'DR_%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 rom = LocalRom.fromJsonRom(rom, args.rom, 0x400000)
rom = LocalRom(args.rom)
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%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.doorShuffle[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:
[((location.address, location.player), (location.item.code, location.item.player)) for location in world.get_filled_locations() if type(location.address) is int]) outfilepname += f'_P{player}'
).encode("utf-8")) if world.players > 1 or world.teams > 1:
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({"names": parsed_names,
"roms": rom_names,
"remote_items": [player for player in range(1, world.players + 1) if world.remote_items[player]],
"locations": [((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"))
if args.jsonout: if args.jsonout:
jsonout["multidata"] = list(multidata) jsonout["multidata"] = list(multidata)
else: else:
@@ -230,6 +246,9 @@ 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.doorShuffle, 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.doorShuffle, 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.remote_items = world.remote_items.copy()
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()
@@ -297,6 +316,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:
@@ -306,9 +326,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)}
ret.doors = world.doors ret.doors = world.doors

View File

@@ -1,58 +1,38 @@
import aioconsole
import argparse import argparse
import asyncio import asyncio
import colorama
import json import json
import logging import logging
import re import shlex
import subprocess import urllib.parse
import sys import websockets
import Items import Items
import Regions import Regions
while True:
try:
import aioconsole
break
except ImportError:
aioconsole = None
print('Required python module "aioconsole" not found, press enter to install it')
input()
subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'aioconsole'])
while True:
try:
import websockets
break
except ImportError:
websockets = None
print('Required python module "websockets" not found, press enter to install it')
input()
subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'websockets'])
try:
import colorama
except ImportError:
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
self.exit_event = asyncio.Event() self.exit_event = asyncio.Event()
self.watcher_event = asyncio.Event()
self.input_queue = asyncio.Queue() self.input_queue = asyncio.Queue()
self.input_requests = 0 self.input_requests = 0
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 +42,16 @@ 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.locations_scouted = set()
self.items_received = [] self.items_received = []
self.last_rom = None self.locations_info = {}
self.expected_rom = None self.awaiting_rom = False
self.rom_confirmed = False self.rom = None
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 +62,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
@@ -95,11 +77,15 @@ INGAME_MODES = {0x07, 0x09, 0x0b}
SAVEDATA_START = WRAM_START + 0xF000 SAVEDATA_START = WRAM_START + 0xF000
SAVEDATA_SIZE = 0x500 SAVEDATA_SIZE = 0x500
RECV_PROGRESS_ADDR = SAVEDATA_START + 0x4D0 # 2 bytes RECV_PROGRESS_ADDR = SAVEDATA_START + 0x4D0 # 2 bytes
RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte
RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte
ROOMID_ADDR = SAVEDATA_START + 0x4D4 # 2 bytes ROOMID_ADDR = SAVEDATA_START + 0x4D4 # 2 bytes
ROOMDATA_ADDR = SAVEDATA_START + 0x4D6 # 1 byte ROOMDATA_ADDR = SAVEDATA_START + 0x4D6 # 1 byte
SCOUT_LOCATION_ADDR = SAVEDATA_START + 0x4D7 # 1 byte
SCOUTREPLY_LOCATION_ADDR = SAVEDATA_START + 0x4D8 # 1 byte
SCOUTREPLY_ITEM_ADDR = SAVEDATA_START + 0x4D9 # 1 byte
SCOUTREPLY_PLAYER_ADDR = SAVEDATA_START + 0x4DA # 1 byte
location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10), location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
"Blind's Hideout - Left": (0x11d, 0x20), "Blind's Hideout - Left": (0x11d, 0x20),
@@ -323,18 +309,17 @@ 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') logging.error('Already connected to snes')
return return
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) logging.info("Connecting to QUsb2snes at %s ..." % address)
try: try:
ctx.snes_socket = await websockets.connect(address, ping_timeout=None, ping_interval=None) ctx.snes_socket = await websockets.connect(address, ping_timeout=None, ping_interval=None)
@@ -352,24 +337,32 @@ async def snes_connect(ctx : Context, address = None):
if not devices: if not devices:
raise Exception('No device found') raise Exception('No device found')
print("Available devices:") logging.info("Available devices:")
for id, device in enumerate(devices): for id, device in enumerate(devices):
print("[%d] %s" % (id + 1, device)) logging.info("[%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:
logging.info("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):
logging.warning("Invalid choice (%s)" % choice)
continue
device = devices[int(choice) - 1] device = devices[int(choice) - 1]
break break
print("Attaching to " + device) logging.info("Attaching to " + device)
Attach_Request = { Attach_Request = {
"Opcode" : "Attach", "Opcode" : "Attach",
@@ -378,21 +371,22 @@ 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") logging.info("SD2SNES Detected")
ctx.is_sd2snes = True ctx.is_sd2snes = True
await ctx.snes_socket.send(json.dumps({"Opcode" : "Info", "Space" : "SNES"})) await ctx.snes_socket.send(json.dumps({"Opcode" : "Info", "Space" : "SNES"}))
reply = json.loads(await ctx.snes_socket.recv()) reply = json.loads(await ctx.snes_socket.recv())
if reply and 'Results' in reply: if reply and 'Results' in reply:
print(reply['Results']) logging.info(reply['Results'])
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 +396,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:
logging.error("Error connecting to snes (%s)" % e)
else:
logging.error(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") logging.warning("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)
logging.error("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 +425,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:
logging.info(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:
@@ -449,9 +456,9 @@ async def snes_read(ctx : Context, address, size):
break break
if len(data) != size: if len(data) != size:
print('Error reading %s, requested %d bytes, received %d' % (hex(address), size, len(data))) logging.error('Error reading %s, requested %d bytes, received %d' % (hex(address), size, len(data)))
if len(data): if len(data):
print(str(data)) logging.error(str(data))
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()
return None return None
@@ -477,7 +484,7 @@ async def snes_write(ctx : Context, write_list):
for address, data in write_list: for address, data in write_list:
if (address < WRAM_START) or ((address + len(data)) > (WRAM_START + WRAM_SIZE)): if (address < WRAM_START) or ((address + len(data)) > (WRAM_START + WRAM_SIZE)):
print("SD2SNES: Write out of range %s (%d)" % (hex(address), len(data))) logging.error("SD2SNES: Write out of range %s (%d)" % (hex(address), len(data)))
return False return False
for ptr, byte in enumerate(data, address + 0x7E0000 - WRAM_START): for ptr, byte in enumerate(data, address + 0x7E0000 - WRAM_START):
cmd += b'\xA9' # LDA cmd += b'\xA9' # LDA
@@ -534,100 +541,100 @@ 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') logging.error('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:
logging.info('Enter multiworld server address')
address = await console_input(ctx)
print('Connecting to multiworld server at %s' % address) address = f"ws://{address}" if "://" not in address else address
port = urllib.parse.urlparse(address).port or 38281
logging.info('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') logging.info('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):
cmd, args = (msg[0], msg[1]) if len(msg) > 1 else (msg, None) cmd, args = (msg[0], msg[1]) if len(msg) > 1 else (msg, None)
await process_server_cmd(ctx, cmd, args) await process_server_cmd(ctx, cmd, args)
print('Disconnected from multiworld server, type /connect to reconnect') logging.warning('Disconnected from multiworld server, type /connect to reconnect')
except ConnectionRefusedError: except ConnectionRefusedError:
print('Connection refused by the multiworld server') logging.error('Connection refused by the multiworld server')
except (OSError, websockets.InvalidURI): except (OSError, websockets.InvalidURI):
print('Failed to connect to the multiworld server') logging.error('Failed to connect to the multiworld server')
except Exception as e: except Exception as e:
print('Lost connection to the multiworld server, type /connect to reconnect') logging.error('Lost connection to the multiworld server, type /connect to reconnect')
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.locations_info = {}
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:
logging.info(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':
print('--------------------------------') logging.info('--------------------------------')
print('Room Information:') logging.info('Room Information:')
print('--------------------------------') logging.info('--------------------------------')
if args['password']: if args['password']:
print('Password required') logging.info('Password required')
print('%d players seed' % args['slots'])
if len(args['players']) < 1: if len(args['players']) < 1:
print('No player connected') logging.info('No player connected')
else: else:
args['players'].sort(key=lambda player: ('' if not player[1] else player[1].lower(), player[2])) args['players'].sort()
current_team = 0 current_team = 0
print('Connected players:') logging.info('Connected players:')
for name, team, slot in args['players']: logging.info(' Team #1')
for team, slot, name in args['players']:
if team != current_team: if team != current_team:
print(' Default team' if not team else ' Team: %s' % team) logging.info(f' Team #{team + 1}')
current_team = team current_team = team
print(' %s (Player %d)' % (name, slot)) logging.info(' %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') logging.error('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: msgs = []
rom_confirmed(ctx) if ctx.locations_checked:
if ctx.locations_checked: msgs.append(['LocationChecks', [Regions.location_table[loc][0] for loc in ctx.locations_checked]])
await send_msgs(ctx.socket, [['LocationChecks', [Regions.location_table[loc][0] for loc in ctx.locations_checked]]]) if ctx.locations_scouted:
else: msgs.append(['LocationScouts', list(ctx.locations_scouted)])
raise Exception('Different ROM expected from server') if msgs:
await send_msgs(ctx.socket, msgs)
if cmd == 'ReceivedItems': if cmd == 'ReceivedItems':
start_index, items = args start_index, items = args
@@ -640,39 +647,54 @@ 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))
ctx.watcher_event.set()
if cmd == 'LocationInfo':
for location, item, player in args:
if location not in ctx.locations_info:
replacements = {0xA2: 'Small Key', 0x9D: 'Big Key', 0x8D: 'Compass', 0x7D: 'Map'}
item_name = replacements.get(item, get_item_name_from_id(item))
logging.info(f"Saw {color(item_name, 'red', 'bold')} at {list(Regions.location_table.keys())[location - 1]}")
ctx.locations_info[location] = (item, player)
ctx.watcher_event.set()
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))) logging.info('%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) logging.info(args)
async def server_auth(ctx : Context, password_requested): 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:') logging.info('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) logging.info('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,70 +704,53 @@ 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
if command[0] == '/exit': if command[0] == '/exit':
ctx.exit_event.set() ctx.exit_event.set()
if command[0] == '/installcolors' and 'colorama' not in sys.modules:
subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'colorama'])
global colorama
import colorama
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]]))
if command[0] == '/received': if command[0] == '/received':
print('Received items:') logging.info('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)' % ( logging.info('%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':
for location in [k for k, v in Regions.location_table.items() if type(v[0]) is int]: for location in [k for k, v in Regions.location_table.items() if type(v[0]) is int]:
if location not in ctx.locations_checked: if location not in ctx.locations_checked:
print('Missing: ' + location) logging.info('Missing: ' + location)
if command[0] == '/getitem' and len(command) > 1: if command[0] == '/getitem' and len(command) > 1:
item = input[9:] item = input[9:]
item_id = Items.item_table[item][3] if item in Items.item_table else None item_id = Items.item_table[item][3] if item in Items.item_table else None
if type(item_id) is int and item_id in range(0x100): if type(item_id) is int and item_id in range(0x100):
print('Sending item: ' + item) logging.info('Sending item: ' + item)
snes_buffered_write(ctx, RECV_ITEM_ADDR, bytes([item_id])) snes_buffered_write(ctx, RECV_ITEM_ADDR, bytes([item_id]))
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([0])) snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([0]))
else: else:
print('Invalid item: ' + item) logging.info('Invalid item: ' + item)
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'
@@ -761,7 +766,7 @@ async def track_locations(ctx : Context, roomid, roomdata):
new_locations = [] new_locations = []
def new_check(location): def new_check(location):
ctx.locations_checked.add(location) ctx.locations_checked.add(location)
print("New check: %s (%d/216)" % (location, len(ctx.locations_checked))) logging.info("New check: %s (%d/216)" % (location, len(ctx.locations_checked)))
new_locations.append(Regions.location_table[location][0]) new_locations.append(Regions.location_table[location][0])
for location, (loc_roomid, loc_mask) in location_table_uw.items(): for location, (loc_roomid, loc_mask) in location_table_uw.items():
@@ -820,68 +825,82 @@ async def track_locations(ctx : Context, roomid, roomdata):
async def game_watcher(ctx : Context): async def game_watcher(ctx : Context):
while not ctx.exit_event.is_set(): while not ctx.exit_event.is_set():
await asyncio.sleep(2) try:
await asyncio.wait_for(ctx.watcher_event.wait(), 2)
except asyncio.TimeoutError:
pass
ctx.watcher_event.clear()
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: ctx.locations_scouted = set()
if ctx.last_rom[:len(ctx.expected_rom)] != ctx.expected_rom: if ctx.awaiting_rom:
print("Wrong ROM detected") await server_auth(ctx, False)
await ctx.snes_socket.close()
continue if ctx.auth and ctx.auth != ctx.rom:
else: logging.warning("ROM change detected, please reconnect to the multiworld server")
rom_confirmed(ctx) await disconnect(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:
continue continue
data = await snes_read(ctx, RECV_PROGRESS_ADDR, 7) data = await snes_read(ctx, RECV_PROGRESS_ADDR, 8)
if data is None: if data is None:
continue continue
recv_index = data[0] | (data[1] << 8) recv_index = data[0] | (data[1] << 8)
assert(RECV_ITEM_ADDR == RECV_PROGRESS_ADDR + 2) assert RECV_ITEM_ADDR == RECV_PROGRESS_ADDR + 2
recv_item = data[2] recv_item = data[2]
assert(ROOMID_ADDR == RECV_PROGRESS_ADDR + 4) assert ROOMID_ADDR == RECV_PROGRESS_ADDR + 4
roomid = data[4] | (data[5] << 8) roomid = data[4] | (data[5] << 8)
assert(ROOMDATA_ADDR == RECV_PROGRESS_ADDR + 6) assert ROOMDATA_ADDR == RECV_PROGRESS_ADDR + 6
roomdata = data[6] roomdata = data[6]
assert SCOUT_LOCATION_ADDR == RECV_PROGRESS_ADDR + 7
await track_locations(ctx, roomid, roomdata) scout_location = data[7]
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)' % ( logging.info('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 if item.player != ctx.slot else 0]))
if scout_location > 0 and scout_location in ctx.locations_info:
snes_buffered_write(ctx, SCOUTREPLY_LOCATION_ADDR, bytes([scout_location]))
snes_buffered_write(ctx, SCOUTREPLY_ITEM_ADDR, bytes([ctx.locations_info[scout_location][0]]))
snes_buffered_write(ctx, SCOUTREPLY_PLAYER_ADDR, bytes([ctx.locations_info[scout_location][1]]))
await snes_flush_writes(ctx) await snes_flush_writes(ctx)
if scout_location > 0 and scout_location not in ctx.locations_scouted:
ctx.locations_scouted.add(scout_location)
logging.info(f'Scouting item at {list(Regions.location_table.keys())[scout_location - 1]}')
await send_msgs(ctx.socket, [['LocationScouts', [scout_location]]])
await track_locations(ctx, roomid, roomdata)
async def main(): async def main():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
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('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
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) logging.basicConfig(format='%(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
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 +909,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
@@ -909,13 +929,9 @@ async def main():
await input_task await input_task
if __name__ == '__main__': if __name__ == '__main__':
if 'colorama' in sys.modules: colorama.init()
colorama.init()
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
loop.run_until_complete(main()) loop.run_until_complete(main())
loop.run_until_complete(asyncio.gather(*asyncio.Task.all_tasks())) loop.run_until_complete(asyncio.gather(*asyncio.Task.all_tasks()))
loop.close() loop.close()
colorama.deinit()
if 'colorama' in sys.modules:
colorama.deinit()

View File

@@ -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,18 @@ 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.remote_items = set()
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):
return {
'password': ctx.password is not None,
'slots': ctx.players,
'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 +55,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) logging.info("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)) logging.info("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)) logging.info("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):
@@ -105,54 +95,54 @@ async def server(websocket, path, ctx : Context):
ctx.clients.remove(client) ctx.clients.remove(client)
async def on_client_connected(ctx : Context, client : Client): async def on_client_connected(ctx : Context, client : Client):
await send_msgs(client.socket, [['RoomInfo', get_room_info(ctx)]]) await send_msgs(client.socket, [['RoomInfo', {
'password': ctx.password is not None,
'players': [(client.team, client.slot, client.name) for client in ctx.clients if client.auth]
}]])
async def on_client_disconnected(ctx : Context, client : Client): async def on_client_disconnected(ctx : Context, client : Client):
if client.auth: if client.auth:
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,37 +153,36 @@ 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:
target_item, target_player = ctx.locations[(location, slot)] target_item, target_player = ctx.locations[(location, slot)]
if target_player != slot: if target_player != slot or slot in ctx.remote_items:
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) if slot != target_player:
broadcast_team(ctx, team, [['ItemSent', (name, target_player_name, target_item, location)]]) broadcast_team(ctx, team, [['ItemSent', (slot, location, target_player, target_item)]])
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))) logging.info('(Team #%d) %s sent %s to %s (%s)' % (team+1, ctx.player_names[(team, slot)], get_item_name_from_id(target_item), ctx.player_names[(team, target_player)], 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 +196,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))])
@@ -265,13 +234,35 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args):
items = get_received_items(ctx, client.team, client.slot) items = get_received_items(ctx, client.team, client.slot)
if items: if items:
client.send_index = len(items) client.send_index = len(items)
await send_msgs(client.socket, ['ReceivedItems', (0, tuplize_received_items(items))]) await send_msgs(client.socket, [['ReceivedItems', (0, tuplize_received_items(items))]])
if cmd == 'LocationChecks': if cmd == 'LocationChecks':
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 == 'LocationScouts':
if type(args) is not list:
await send_msgs(client.socket, [['InvalidArguments', 'LocationScouts']])
return
locs = []
for location in args:
if type(location) is not int or 0 >= location > len(Regions.location_table):
await send_msgs(client.socket, [['InvalidArguments', 'LocationScouts']])
return
loc_name = list(Regions.location_table.keys())[location - 1]
target_item, target_player = ctx.locations[(Regions.location_table[loc_name][0], client.slot)]
replacements = {'SmallKey': 0xA2, 'BigKey': 0x9D, 'Compass': 0x8D, 'Map': 0x7D}
item_type = [i[2] for i in Items.item_table.values() if type(i[3]) is int and i[3] == target_item]
if item_type:
target_item = replacements.get(item_type[0], target_item)
locs.append([loc_name, location, target_item, target_player])
logging.info(f"{client.name} in team {client.team+1} scouted {', '.join([l[0] for l in locs])}")
await send_msgs(client.socket, [['LocationInfo', [l[1:] for l in locs]]])
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,20 +271,26 @@ 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
print('Password set to ' + password if password is not None else 'Password disabled') logging.warning('Password set to ' + password if password is not None else 'Password disabled')
async def console(ctx : Context): 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
@@ -302,34 +299,41 @@ async def console(ctx : Context):
break break
if command[0] == '/players': if command[0] == '/players':
print(get_connected_players_string(ctx)) logging.info(get_connected_players_string(ctx))
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) logging.warning("Unknown item: " + item)
if command[0][0] != '/': if command[0][0] != '/':
notify_all(ctx, '[Server]: ' + input) notify_all(ctx, '[Server]: ' + input)
@@ -342,8 +346,11 @@ async def main():
parser.add_argument('--multidata', default=None) parser.add_argument('--multidata', default=None)
parser.add_argument('--savefile', default=None) parser.add_argument('--savefile', default=None)
parser.add_argument('--disable_save', default=False, action='store_true') parser.add_argument('--disable_save', default=False, action='store_true')
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
args = parser.parse_args() args = parser.parse_args()
logging.basicConfig(format='[%(asctime)s] %(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
ctx = Context(args.host, args.port, args.password) ctx = Context(args.host, args.port, args.password)
ctx.data_filename = args.multidata ctx.data_filename = args.multidata
@@ -358,15 +365,18 @@ 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['names']):
ctx.rom_names = {k: v for k, v in jsonobj[1]} for player, name in enumerate(names, 1):
ctx.locations = {tuple(k): tuple(v) for k, v in jsonobj[2]} ctx.player_names[(team, player)] = name
ctx.rom_names = {tuple(rom): (team, slot) for slot, team, rom in jsonobj['roms']}
ctx.remote_items = set(jsonobj['remote_items'])
ctx.locations = {tuple(k): tuple(v) for k, v in jsonobj['locations']}
except Exception as e: except Exception as e:
print('Failed to read multiworld data (%s)' % e) logging.error('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)) logging.info('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,17 +385,16 @@ 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))) logging.info('Loaded save file with %d received items for %d players' % (sum([len(p) for p in received_items.values()]), len(received_items)))
except FileNotFoundError: except FileNotFoundError:
print('No save data found, starting a new game') logging.error('No save data found, starting a new game')
except Exception as e: except Exception as e:
print(e) logging.info(e)
ctx.server = websockets.serve(functools.partial(server,ctx=ctx), ctx.host, ctx.port, ping_timeout=None, ping_interval=None) ctx.server = websockets.serve(functools.partial(server,ctx=ctx), ctx.host, ctx.port, ping_timeout=None, ping_interval=None)
await ctx.server await ctx.server

View File

@@ -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')
@@ -146,13 +147,14 @@ def roll_settings(weights):
door_shuffle = get_choice('door_shuffle') door_shuffle = get_choice('door_shuffle')
ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla' ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla'
goal = get_choice('goals')
ret.goal = {'ganon': 'ganon', ret.goal = {'ganon': 'ganon',
'fast_ganon': 'crystals', 'fast_ganon': 'crystals',
'dungeons': 'dungeons', 'dungeons': 'dungeons',
'pedestal': 'pedestal', 'pedestal': 'pedestal',
'triforce-hunt': 'triforcehunt' 'triforce-hunt': 'triforcehunt'
}[get_choice('goals')] }[goal]
ret.openpyramid = ret.goal == 'fast_ganon' ret.openpyramid = goal == 'fast_ganon'
ret.crystals_gt = get_choice('tower_open') ret.crystals_gt = get_choice('tower_open')

View File

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

101
Rom.py
View File

@@ -12,6 +12,7 @@ import subprocess
from BaseClasses import CollectionState, ShopType, Region, Location, Item, DoorType from BaseClasses import CollectionState, ShopType, Region, Location, Item, DoorType
from DoorShuffle import compass_data from DoorShuffle import compass_data
from Dungeons import dungeon_music_addresses from Dungeons import dungeon_music_addresses
from Regions import location_table
from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable
from Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, BombShop2_texts, junk_texts from Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, BombShop2_texts, junk_texts
from Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts, LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names from Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts, LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names
@@ -26,8 +27,9 @@ JAP10HASH = '03a63945398191337e896e5771f77173'
class JsonRom(object): class JsonRom(object):
def __init__(self): def __init__(self, name=None, hash=None):
self.name = None self.name = name
self.hash = hash
self.orig_buffer = None self.orig_buffer = None
self.patches = {} self.patches = {}
self.addresses = [] self.addresses = []
@@ -72,8 +74,9 @@ class JsonRom(object):
class LocalRom(object): class LocalRom(object):
def __init__(self, file, patch=True): def __init__(self, file, patch=True, name=None, hash=None):
self.name = None self.name = name
self.hash = hash
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)
@@ -92,6 +95,14 @@ class LocalRom(object):
with open(file, 'wb') as outfile: with open(file, 'wb') as outfile:
outfile.write(self.buffer) outfile.write(self.buffer)
@staticmethod
def fromJsonRom(rom, file, rom_size = 0x200000):
ret = LocalRom(file, True, rom.name, rom.hash)
ret.buffer.extend(bytearray([0x00] * (rom_size - len(ret.buffer))))
for address, values in rom.patches.items():
ret.write_bytes(int(address), values)
return ret
def patch_base_rom(self): def patch_base_rom(self):
# verify correct checksum of baserom # verify correct checksum of baserom
basemd5 = hashlib.md5() basemd5 = hashlib.md5()
@@ -116,11 +127,6 @@ class LocalRom(object):
if RANDOMIZERBASEHASH != patchedmd5.hexdigest(): if RANDOMIZERBASEHASH != patchedmd5.hexdigest():
raise RuntimeError('Provided Base Rom unsuitable for patching. Please provide a JAP(1.0) "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" rom to use as a base.') raise RuntimeError('Provided Base Rom unsuitable for patching. Please provide a JAP(1.0) "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" rom to use as a base.')
def merge_enemizer_patches(self, patches):
self.buffer.extend(bytearray([0x00] * (0x400000 - len(self.buffer))))
for address, values in patches.items():
self.write_bytes(int(address), values)
def write_crc(self): def write_crc(self):
crc = (sum(self.buffer[:0x7FDC] + self.buffer[0x7FE0:]) + 0x01FE) & 0xFFFF crc = (sum(self.buffer[:0x7FDC] + self.buffer[0x7FE0:]) + 0x01FE) & 0xFFFF
inv = crc ^ 0xFFFF inv = crc ^ 0xFFFF
@@ -471,7 +477,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
@@ -492,23 +498,27 @@ def patch_rom(world, player, rom, enemized):
continue continue
if not location.crystal: if not location.crystal:
# Keys in their native dungeon should use the orignal item code for keys if location.item is not None:
if location.parent_region.dungeon: # Keys in their native dungeon should use the orignal item code for keys
dungeon = location.parent_region.dungeon if location.parent_region.dungeon:
if location.item is not None and dungeon.is_dungeon_item(location.item): if location.parent_region.dungeon.is_dungeon_item(location.item):
if location.item.bigkey: if location.item.bigkey:
itemid = 0x32 itemid = 0x32
if location.item.smallkey: if location.item.smallkey:
itemid = 0x24 itemid = 0x24
if location.item.map: if location.item.map:
itemid = 0x33 itemid = 0x33
if location.item.compass: if location.item.compass:
itemid = 0x25 itemid = 0x25
if location.item and location.item.player != player: if world.remote_items[player]:
if location.player_address is not None: itemid = list(location_table.keys()).index(location.name) + 1
rom.write_byte(location.player_address, location.item.player) assert itemid < 0x100
else: rom.write_byte(location.player_address, 0xFF)
itemid = 0x5A elif location.item.player != player:
if location.player_address is not None:
rom.write_byte(location.player_address, location.item.player)
else:
itemid = 0x5A
rom.write_byte(location.address, itemid) rom.write_byte(location.address, itemid)
else: else:
# crystals # crystals
@@ -1255,16 +1265,22 @@ 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)
rom.write_byte(0x18636C, 1 if world.remote_items[player] else 0)
# set rom name # set rom name
# 21 bytes # 21 bytes
from Main import __version__ from Main import __version__
# todo: change to DR when Enemizer is okay with DR # todo: change to DR when Enemizer is okay with DR
rom.name = bytearray('ER_{0}_{1:09}\0'.format(__version__[0:7], world.seed), 'utf8') rom.name = bytearray(f'ER{__version__.split("-")[0].replace(".","")[0:3]}_{team+1}_{player}_{world.seed:09}\0', 'utf8')[:21]
assert len(rom.name) <= 21 rom.name.extend([0] * (21 - len(rom.name)))
rom.write_bytes(0x7FC0, 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)
code = [ code = [
@@ -1275,6 +1291,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
@@ -1338,7 +1355,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)
@@ -1407,11 +1424,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()
@@ -1548,12 +1560,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()
@@ -1571,11 +1586,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.
@@ -2366,3 +2381,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"
]

View File

@@ -4,9 +4,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]
@@ -21,6 +18,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)

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
aioconsole==0.1.15
colorama==0.4.3
websockets==8.1