Merge pull request #1 from Bonta0/multiworld_31

Multiworld 31
This commit is contained in:
Fabian Dill 2020-01-14 19:49:40 +01:00 committed by GitHub
commit baab6a76ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 398 additions and 379 deletions

View File

@ -2,7 +2,7 @@ import os
import time
import logging
from Utils import output_path, parse_names_string
from Utils import output_path
from Rom import LocalRom, apply_rom_settings
@ -21,7 +21,7 @@ def adjust(args):
else:
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))

View File

@ -11,6 +11,7 @@ class World(object):
def __init__(self, players, shuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, goal, algorithm, accessibility, shuffle_ganon, retro, custom, customitemarray, hints):
self.players = players
self.teams = 1
self.shuffle = shuffle.copy()
self.logic = logic.copy()
self.mode = mode.copy()
@ -58,6 +59,7 @@ class World(object):
def set_player_attr(attr, val):
self.__dict__.setdefault(attr, {})[player] = val
set_player_attr('_region_cache', {})
set_player_attr('player_names', [])
set_player_attr('required_medallions', ['Ether', 'Quake'])
set_player_attr('swamp_patch_required', False)
set_player_attr('powder_patch_required', False)
@ -90,6 +92,12 @@ class World(object):
set_player_attr('treasure_hunt_icon', 'Triforce Piece')
set_player_attr('treasure_hunt_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):
for region in regions if regions else self.regions:
region.world = self
@ -211,6 +219,7 @@ class World(object):
return [location for location in self.get_locations() if location.item is not None and location.item.name == item and location.item.player == player]
def push_precollected(self, item):
item.world = self
if (item.smallkey and self.keyshuffle[item.player]) or (item.bigkey and self.bigkeyshuffle[item.player]):
item.advancement = True
self.precollected_items.append(item)
@ -223,6 +232,7 @@ class World(object):
if location.can_fill(self.state, item, False):
location.item = item
item.location = location
item.world = self
if collect:
self.state.collect(item, location.event, location)
@ -707,10 +717,7 @@ class Region(object):
return str(self.__unicode__())
def __unicode__(self):
if self.world and self.world.players == 1:
return self.name
else:
return '%s (Player %d)' % (self.name, self.player)
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
class Entrance(object):
@ -746,11 +753,8 @@ class Entrance(object):
return str(self.__unicode__())
def __unicode__(self):
if self.parent_region and self.parent_region.world and self.parent_region.world.players == 1:
return self.name
else:
return '%s (Player %d)' % (self.name, self.player)
world = self.parent_region.world if self.parent_region else None
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
class Dungeon(object):
@ -787,10 +791,7 @@ class Dungeon(object):
return str(self.__unicode__())
def __unicode__(self):
if self.world and self.world.players==1:
return self.name
else:
return '%s (Player %d)' % (self.name, self.player)
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
class Boss(object):
def __init__(self, name, enemizer_name, defeat_rule, player):
@ -833,10 +834,8 @@ class Location(object):
return str(self.__unicode__())
def __unicode__(self):
if self.parent_region and self.parent_region.world and self.parent_region.world.players == 1:
return self.name
else:
return '%s (Player %d)' % (self.name, self.player)
world = self.parent_region.world if self.parent_region and self.parent_region.world else None
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
class Item(object):
@ -855,6 +854,7 @@ class Item(object):
self.hint_text = hint_text
self.code = code
self.location = None
self.world = None
self.player = player
@property
@ -881,10 +881,7 @@ class Item(object):
return str(self.__unicode__())
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.name
else:
return '%s (Player %d)' % (self.name, self.player)
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
# have 6 address that need to be filled
@ -957,6 +954,7 @@ class Spoiler(object):
def __init__(self, world):
self.world = world
self.hashes = {}
self.entrances = OrderedDict()
self.medallions = {}
self.playthrough = {}
@ -981,8 +979,8 @@ class Spoiler(object):
self.medallions['Turtle Rock'] = self.world.required_medallions[1][1]
else:
for player in range(1, self.world.players + 1):
self.medallions['Misery Mire (Player %d)' % player] = self.world.required_medallions[player][0]
self.medallions['Turtle Rock (Player %d)' % player] = self.world.required_medallions[player][1]
self.medallions[f'Misery Mire ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][0]
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))
@ -1075,7 +1073,8 @@ class Spoiler(object):
'enemy_shuffle': self.world.enemy_shuffle,
'enemy_health': self.world.enemy_health,
'enemy_damage': self.world.enemy_damage,
'players': self.world.players
'players': self.world.players,
'teams': self.world.teams
}
def to_json(self):
@ -1085,6 +1084,8 @@ class Spoiler(object):
out.update(self.locations)
out['Starting Inventory'] = self.startinventory
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:
out['Shops'] = self.shops
out['playthrough'] = self.playthrough
@ -1098,42 +1099,44 @@ class Spoiler(object):
self.parse_data()
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('Players: %d\n' % self.world.players)
outfile.write('Filling Algorithm: %s\n' % self.world.algorithm)
outfile.write('Logic: %s\n' % self.metadata['logic'])
outfile.write('Mode: %s\n' % self.metadata['mode'])
outfile.write('Retro: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['retro'].items()})
outfile.write('Swords: %s\n' % self.metadata['weapons'])
outfile.write('Goal: %s\n' % self.metadata['goal'])
outfile.write('Difficulty: %s\n' % self.metadata['item_pool'])
outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'])
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'])
outfile.write('Crystals required for GT: %s\n' % self.metadata['gt_crystals'])
outfile.write('Crystals required for Ganon: %s\n' % self.metadata['ganon_crystals'])
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('Accessibility: %s\n' % self.metadata['accessibility'])
outfile.write('Map shuffle: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['mapshuffle'].items()})
outfile.write('Compass shuffle: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['compassshuffle'].items()})
outfile.write('Small Key shuffle: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['keyshuffle'].items()})
outfile.write('Big Key shuffle: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['bigkeyshuffle'].items()})
outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'])
outfile.write('Enemy shuffle: %s\n' % self.metadata['enemy_shuffle'])
outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'])
outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'])
outfile.write('Hints: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['hints'].items()})
outfile.write('Players: %d\n' % self.world.players)
outfile.write('Teams: %d\n' % self.world.teams)
for player in range(1, self.world.players + 1):
if self.world.players > 1:
outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_names(player)))
for team in range(self.world.teams):
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('Logic: %s\n' % self.metadata['logic'][player])
outfile.write('Mode: %s\n' % self.metadata['mode'][player])
outfile.write('Retro: %s\n' % ('Yes' if self.metadata['retro'][player] else 'No'))
outfile.write('Swords: %s\n' % self.metadata['weapons'][player])
outfile.write('Goal: %s\n' % self.metadata['goal'][player])
outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player])
outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player])
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player])
outfile.write('Crystals required for GT: %s\n' % self.metadata['gt_crystals'][player])
outfile.write('Crystals required for Ganon: %s\n' % self.metadata['ganon_crystals'][player])
outfile.write('Pyramid hole pre-opened: %s\n' % ('Yes' if self.metadata['open_pyramid'][player] else 'No'))
outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player])
outfile.write('Map shuffle: %s\n' % ('Yes' if self.metadata['mapshuffle'][player] else 'No'))
outfile.write('Compass shuffle: %s\n' % ('Yes' if self.metadata['compassshuffle'][player] else 'No'))
outfile.write('Small Key shuffle: %s\n' % ('Yes' if self.metadata['keyshuffle'][player] else 'No'))
outfile.write('Big Key shuffle: %s\n' % ('Yes' if self.metadata['bigkeyshuffle'][player] else 'No'))
outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player])
outfile.write('Enemy shuffle: %s\n' % self.metadata['enemy_shuffle'][player])
outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player])
outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player])
outfile.write('Hints: %s\n' % ('Yes' if self.metadata['hints'][player] else 'No'))
if self.entrances:
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\nMedallions\n')
if self.world.players == 1:
outfile.write('\nMisery Mire Medallion: %s' % (self.medallions['Misery Mire']))
outfile.write('\nTurtle Rock Medallion: %s' % (self.medallions['Turtle Rock']))
else:
for player in range(1, self.world.players + 1):
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'.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')
for dungeon, medallion in self.medallions.items():
outfile.write(f'\n{dungeon}: {medallion}')
if self.startinventory:
outfile.write('\n\nStarting Inventory:\n\n')
outfile.write('\n'.join(self.startinventory))
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\nShops:\n\n')

View File

@ -268,6 +268,7 @@ def parse_arguments(argv, no_defaults=False):
parser.add_argument('--beemizer', default=defval(0), type=lambda value: min(max(int(value), 0), 4))
parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255))
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('--race', default=defval(False), action='store_true')
parser.add_argument('--outputname')

24
Gui.py
View File

@ -15,7 +15,7 @@ from EntranceRandomizer import parse_arguments
from GuiUtils import ToolTips, set_icon, BackgroundTaskProgress
from Main import main, __version__ as ESVersion
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):
@ -470,11 +470,7 @@ def guiMain(args=None):
logging.exception(e)
messagebox.showerror(title="Error while creating seed", message=str(e))
else:
msgtxt = "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)
messagebox.showinfo(title="Success", message="Rom patched successfully")
generateButton = Button(bottomFrame, text='Generate Patched Rom', command=generateRom)
@ -574,20 +570,11 @@ def guiMain(args=None):
uwPalettesLabel2 = Label(uwPalettesFrame2, text='Dungeon palettes')
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)
heartcolorFrame2.pack(expand=True, anchor=E)
fastMenuFrame2.pack(expand=True, anchor=E)
owPalettesFrame2.pack(expand=True, anchor=E)
uwPalettesFrame2.pack(expand=True, anchor=E)
namesFrame2.pack(expand=True, anchor=E)
bottomFrame2 = Frame(topFrame2)
@ -603,18 +590,13 @@ def guiMain(args=None):
guiargs.rom = romVar2.get()
guiargs.baserom = romVar.get()
guiargs.sprite = sprite
guiargs.names = namesEntry2.get()
try:
adjust(args=guiargs)
except Exception as e:
logging.exception(e)
messagebox.showerror(title="Error while creating seed", message=str(e))
else:
msgtxt = "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)
messagebox.showinfo(title="Success", message="Rom patched successfully")
adjustButton = Button(bottomFrame2, text='Adjust Rom', command=adjustRom)

108
Main.py
View File

@ -13,12 +13,12 @@ from Items import ItemFactory
from Regions import create_regions, create_shops, mark_light_world_regions
from InvertedRegions import create_inverted_regions, mark_dark_world_regions
from EntranceShuffle import link_entrances, link_inverted_entrances
from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, JsonRom
from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, JsonRom, get_hash_string
from Rules import set_rules
from 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 ItemList import generate_itempool, difficulties, fill_prizes
from Utils import output_path, parse_names_string
from Utils import output_path, parse_player_names
__version__ = '0.6.3-pre'
@ -54,7 +54,16 @@ def main(args, seed=None):
world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)}
logger.info('ALttP Entrance Randomizer Version %s - Seed: %s\n\n', __version__, world.seed)
logger.info('ALttP Entrance Randomizer Version %s - Seed: %s\n', __version__, world.seed)
parsed_names = parse_player_names(args.names, world.players, args.teams)
world.teams = len(parsed_names)
for i, team in enumerate(parsed_names, 1):
if world.players > 1:
logger.info('%s%s', 'Team%d: ' % i if world.teams > 1 else 'Players: ', ', '.join(team))
for player, name in enumerate(team, 1):
world.player_names[player].append(name)
logger.info('')
for player in range(1, world.players + 1):
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
@ -133,60 +142,64 @@ def main(args, seed=None):
logger.info('Patching ROM.')
player_names = parse_names_string(args.names)
outfilebase = 'ER_%s' % (args.outputname if args.outputname else world.seed)
rom_names = []
jsonout = {}
if not args.suppress_rom:
for player in range(1, world.players + 1):
sprite_random_on_hit = type(args.sprite[player]) is str and args.sprite[player].lower() == 'randomonhit'
use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] != 'none'
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
or args.shufflepots[player] or sprite_random_on_hit)
for team in range(world.teams):
for player in range(1, world.players + 1):
sprite_random_on_hit = type(args.sprite[player]) is str and args.sprite[player].lower() == 'randomonhit'
use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] != 'none'
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)
rom_names.append((player, list(rom.name)))
patch_rom(world, rom, player, team, use_enemizer)
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)
if not args.jsonout:
patches = rom.patches
rom = LocalRom(args.rom)
rom.merge_enemizer_patches(patches)
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)
if not args.jsonout:
patches = rom.patches
rom = LocalRom(args.rom)
rom.merge_enemizer_patches(patches)
if args.race:
patch_race_rom(rom)
if args.race:
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:
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 '')
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])
playername = f"{f'_P{player}' if world.players > 1 else ''}{f'_{player_names[player]}' if player in player_names 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}{playername}{outfilesuffix}.sfc'))
if args.jsonout:
jsonout[f'patch_t{team}_p{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 '')
multidata = zlib.compress(json.dumps((world.players,
rom_names,
outfilepname = f'_T{team+1}' if world.teams > 1 else ''
if world.players > 1:
outfilepname += f'_P{player}'
outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" if world.player_names[player][team] != 'Player %d' % player else ''
outfilesuffix = ('_%s_%s-%s-%s-%s%s_%s-%s%s%s%s%s' % (world.logic[player], world.difficulty[player], world.difficulty_adjustments[player],
world.mode[player], world.goal[player],
"" if world.timer in ['none', 'display'] else "-" + world.timer,
world.shuffle[player], world.algorithm, mcsb_name,
"-retro" if world.retro[player] else "",
"-prog_" + world.progressive if world.progressive in ['off', 'random'] else "",
"-nohints" if not world.hints[player] else "")) if not args.outputname else ''
rom.write_to_file(output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc'))
multidata = zlib.compress(json.dumps((parsed_names, rom_names,
[((location.address, location.player), (location.item.code, location.item.player)) for location in world.get_filled_locations() if type(location.address) is int])
).encode("utf-8"))
if args.jsonout:
@ -215,6 +228,8 @@ def main(args, seed=None):
def copy_world(world):
# ToDo: Not good yet
ret = World(world.players, world.shuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints)
ret.teams = world.teams
ret.player_names = copy.deepcopy(world.player_names)
ret.required_medallions = world.required_medallions.copy()
ret.swamp_patch_required = world.swamp_patch_required.copy()
ret.ganon_at_pyramid = world.ganon_at_pyramid.copy()
@ -280,6 +295,7 @@ def copy_world(world):
item = Item(location.item.name, location.item.advancement, location.item.priority, location.item.type, player = location.item.player)
ret.get_location(location.name, location.player).item = item
item.location = ret.get_location(location.name, location.player)
item.world = ret
if location.event:
ret.get_location(location.name, location.player).event = True
if location.locked:
@ -289,9 +305,11 @@ def copy_world(world):
for item in world.itempool:
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
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)}
for player in range(1, world.players + 1):

View File

@ -2,9 +2,10 @@ import argparse
import asyncio
import json
import logging
import re
import shlex
import subprocess
import sys
import urllib.parse
import Items
import Regions
@ -35,14 +36,13 @@ except ImportError:
colorama = None
class ReceivedItem:
def __init__(self, item, location, player_id, player_name):
def __init__(self, item, location, player):
self.item = item
self.location = location
self.player_id = player_id
self.player_name = player_name
self.player = player
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.server_address = server_address
@ -53,6 +53,8 @@ class Context:
self.snes_socket = None
self.snes_state = SNES_DISCONNECTED
self.snes_attached_device = None
self.snes_reconnect_address = None
self.snes_recv_queue = asyncio.Queue()
self.snes_request_lock = asyncio.Lock()
self.is_sd2snes = False
@ -62,15 +64,14 @@ class Context:
self.socket = None
self.password = password
self.name = name
self.team = team
self.slot = slot
self.team = None
self.slot = None
self.player_names = {}
self.locations_checked = set()
self.items_received = []
self.last_rom = None
self.expected_rom = None
self.rom_confirmed = False
self.awaiting_rom = False
self.rom = None
self.auth = None
def color_code(*args):
codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34,
@ -81,6 +82,7 @@ def color_code(*args):
def color(text, *args):
return color_code(*args) + text + color_code('reset')
RECONNECT_DELAY = 30
ROM_START = 0x000000
WRAM_START = 0xF50000
@ -323,7 +325,7 @@ SNES_CONNECTING = 1
SNES_CONNECTED = 2
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:
print('Already connected to snes')
return
@ -331,8 +333,7 @@ async def snes_connect(ctx : Context, address = None):
ctx.snes_state = SNES_CONNECTING
recv_task = None
if address is None:
address = 'ws://' + ctx.snes_address
address = f"ws://{address}" if "://" not in address else address
print("Connecting to QUsb2snes at %s ..." % address)
@ -357,17 +358,25 @@ async def snes_connect(ctx : Context, address = None):
print("[%d] %s" % (id + 1, device))
device = None
while True:
print("Enter a number:")
choice = await console_input(ctx)
if choice is None:
raise Exception('Abort input')
if not choice.isdigit() or int(choice) < 1 or int(choice) > len(devices):
print("Invalid choice (%s)" % choice)
continue
if len(devices) == 1:
device = devices[0]
elif ctx.snes_reconnect_address:
if ctx.snes_attached_device[1] in devices:
device = ctx.snes_attached_device[1]
else:
device = devices[ctx.snes_attached_device[0]]
else:
while True:
print("Select a device:")
choice = await console_input(ctx)
if choice is None:
raise Exception('Abort input')
if not choice.isdigit() or int(choice) < 1 or int(choice) > len(devices):
print("Invalid choice (%s)" % choice)
continue
device = devices[int(choice) - 1]
break
device = devices[int(choice) - 1]
break
print("Attaching to " + device)
@ -378,6 +387,7 @@ async def snes_connect(ctx : Context, address = None):
}
await ctx.snes_socket.send(json.dumps(Attach_Request))
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'):
print("SD2SNES Detected")
@ -389,10 +399,10 @@ async def snes_connect(ctx : Context, address = None):
else:
ctx.is_sd2snes = False
ctx.snes_reconnect_address = address
recv_task = asyncio.create_task(snes_recv_loop(ctx))
except Exception as e:
print("Error connecting to snes (%s)" % e)
if recv_task is not None:
if not ctx.snes_socket.closed:
await ctx.snes_socket.close()
@ -402,16 +412,26 @@ async def snes_connect(ctx : Context, address = None):
await ctx.snes_socket.close()
ctx.snes_socket = None
ctx.snes_state = SNES_DISCONNECTED
if not ctx.snes_reconnect_address:
print("Error connecting to snes (%s)" % e)
else:
print(f"Error connecting to snes, attempt again in {RECONNECT_DELAY}s")
asyncio.create_task(snes_autoreconnect(ctx))
async def snes_autoreconnect(ctx: Context):
await asyncio.sleep(RECONNECT_DELAY)
if ctx.snes_reconnect_address and ctx.snes_socket is None:
await snes_connect(ctx, ctx.snes_reconnect_address)
async def snes_recv_loop(ctx : Context):
try:
async for msg in ctx.snes_socket:
ctx.snes_recv_queue.put_nowait(msg)
print("Snes disconnected, type /snes to reconnect")
print("Snes disconnected")
except Exception as e:
print("Lost connection to the snes, type /snes to reconnect")
if not isinstance(e, websockets.WebSocketException):
logging.exception(e)
print("Lost connection to the snes, type /snes to reconnect")
finally:
socket, ctx.snes_socket = ctx.snes_socket, None
if socket is not None and not socket.closed:
@ -421,8 +441,11 @@ async def snes_recv_loop(ctx : Context):
ctx.snes_recv_queue = asyncio.Queue()
ctx.hud_message_queue = []
ctx.rom_confirmed = False
ctx.last_rom = None
ctx.rom = None
if ctx.snes_reconnect_address:
print(f"...reconnecting in {RECONNECT_DELAY}s")
asyncio.create_task(snes_autoreconnect(ctx))
async def snes_read(ctx : Context, address, size):
try:
@ -534,21 +557,26 @@ async def send_msgs(websocket, msgs):
except websockets.ConnectionClosed:
pass
async def server_loop(ctx : Context):
async def server_loop(ctx : Context, address = None):
if ctx.socket is not None:
print('Already connected')
return
while not ctx.server_address:
print('Enter multiworld server address')
ctx.server_address = await console_input(ctx)
if address is None:
address = ctx.server_address
address = f"ws://{ctx.server_address}" if "://" not in ctx.server_address else ctx.server_address
while not address:
print('Enter multiworld server address')
address = await console_input(ctx)
address = f"ws://{address}" if "://" not in address else address
port = urllib.parse.urlparse(address).port or 38281
print('Connecting to multiworld server at %s' % address)
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')
ctx.server_address = address
async for data in ctx.socket:
for msg in json.loads(data):
@ -564,15 +592,21 @@ async def server_loop(ctx : Context):
if not isinstance(e, websockets.WebSocketException):
logging.exception(e)
finally:
ctx.name = None
ctx.team = None
ctx.slot = None
ctx.expected_rom = None
ctx.rom_confirmed = False
ctx.awaiting_rom = False
ctx.auth = None
ctx.items_received = []
socket, ctx.socket = ctx.socket, None
if socket is not None and not socket.closed:
await socket.close()
ctx.server_task = None
if ctx.server_address:
print(f"... reconnecting in {RECONNECT_DELAY}s")
asyncio.create_task(server_autoreconnect(ctx))
async def server_autoreconnect(ctx: Context):
await asyncio.sleep(RECONNECT_DELAY)
if ctx.server_address and ctx.server_task is None:
ctx.server_task = asyncio.create_task(server_loop(ctx))
async def process_server_cmd(ctx : Context, cmd, args):
if cmd == 'RoomInfo':
@ -581,53 +615,36 @@ async def process_server_cmd(ctx : Context, cmd, args):
print('--------------------------------')
if args['password']:
print('Password required')
print('%d players seed' % args['slots'])
if len(args['players']) < 1:
print('No player connected')
else:
args['players'].sort(key=lambda player: ('' if not player[1] else player[1].lower(), player[2]))
args['players'].sort(key=lambda _, t, s: (t, s))
current_team = 0
print('Connected players:')
print(' Team #1')
for name, team, slot in args['players']:
if team != current_team:
print(' Default team' if not team else ' Team: %s' % team)
print(' Team #d' % team + 1)
current_team = team
print(' %s (Player %d)' % (name, slot))
await server_auth(ctx, args['password'])
if cmd == 'ConnectionRefused':
password_requested = False
if 'InvalidPassword' in args:
print('Invalid password')
ctx.password = None
password_requested = True
if 'InvalidName' in args:
print('Invalid name')
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
await server_auth(ctx, True)
if 'InvalidRom' in args:
raise Exception('Invalid ROM detected, please verify that you have loaded the correct rom and reconnect your snes')
if 'SlotAlreadyTaken' in args:
print('Player slot already in use for that team')
ctx.team = None
ctx.slot = None
await server_auth(ctx, password_requested)
raise Exception('Player slot already in use for that team')
raise Exception('Connection refused by the multiworld host')
if cmd == 'Connected':
ctx.expected_rom = args
if ctx.last_rom is not None:
if ctx.last_rom[:len(args)] == ctx.expected_rom:
rom_confirmed(ctx)
if ctx.locations_checked:
await send_msgs(ctx.socket, [['LocationChecks', [Regions.location_table[loc][0] for loc in ctx.locations_checked]]])
else:
raise Exception('Different ROM expected from server')
ctx.team, ctx.slot = args[0]
ctx.player_names = {p: n for p, n in args[1]}
if ctx.locations_checked:
await send_msgs(ctx.socket, [['LocationChecks', [Regions.location_table[loc][0] for loc in ctx.locations_checked]]])
if cmd == 'ReceivedItems':
start_index, items = args
@ -640,14 +657,14 @@ async def process_server_cmd(ctx : Context, cmd, args):
await send_msgs(ctx.socket, sync_msg)
if start_index == len(ctx.items_received):
for item in items:
ctx.items_received.append(ReceivedItem(item[0], item[1], item[2], item[3]))
ctx.items_received.append(ReceivedItem(*item))
if cmd == 'ItemSent':
player_sent, player_recvd, item, location = args
item = color(get_item_name_from_id(item), 'cyan' if player_sent != ctx.name else 'green')
player_sent = color(player_sent, 'yellow' if player_sent != ctx.name else 'magenta')
player_recvd = color(player_recvd, 'yellow' if player_recvd != ctx.name 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)))
player_sent, location, player_recvd, item = args
item = color(get_item_name_from_id(item), 'cyan' if player_sent != ctx.slot else 'green')
player_sent = color(ctx.player_names[player_sent], 'yellow' if player_sent != ctx.slot else 'magenta')
player_recvd = color(ctx.player_names[player_recvd], 'yellow' if player_recvd != ctx.slot else 'magenta')
print('%s sent %s to %s (%s)' % (player_sent, item, player_recvd, get_location_name_from_address(location)))
if cmd == 'Print':
print(args)
@ -656,23 +673,28 @@ async def server_auth(ctx : Context, password_requested):
if password_requested and not ctx.password:
print('Enter the password required to join this game:')
ctx.password = await console_input(ctx)
while not ctx.name or not re.match(r'\w{1,10}', ctx.name):
print('Enter your name (10 characters):')
ctx.name = await console_input(ctx)
if not ctx.team:
print('Enter your team name (optional):')
ctx.team = await console_input(ctx)
if ctx.team == '': ctx.team = None
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}]])
if ctx.rom is None:
ctx.awaiting_rom = True
print('No ROM detected, awaiting snes connection to authenticate to the multiworld server')
return
ctx.awaiting_rom = False
ctx.auth = ctx.rom.copy()
await send_msgs(ctx.socket, [['Connect', {'password': ctx.password, 'rom': ctx.auth}]])
async def console_input(ctx : Context):
ctx.input_requests += 1
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):
while not ctx.exit_event.is_set():
input = await aioconsole.ainput()
@ -682,7 +704,7 @@ async def console_loop(ctx : Context):
ctx.input_queue.put_nowait(input)
continue
command = input.split()
command = shlex.split(input)
if not command:
continue
@ -696,26 +718,19 @@ async def console_loop(ctx : Context):
colorama.init()
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']:
ctx.snes_reconnect_address = None
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
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 len(command) > 1:
ctx.server_address = command[1]
asyncio.create_task(connect())
ctx.server_address = None
asyncio.create_task(connect(ctx, command[1] if len(command) > 1 else None))
if command[0] == '/disconnect':
asyncio.create_task(disconnect())
ctx.server_address = None
asyncio.create_task(disconnect(ctx))
if command[0][:1] != '/':
asyncio.create_task(send_msgs(ctx.socket, [['Say', input]]))
@ -723,7 +738,7 @@ async def console_loop(ctx : Context):
print('Received items:')
for index, item in enumerate(ctx.items_received, 1):
print('%s from %s (%s) (%d/%d in list)' % (
color(get_item_name_from_id(item.item), 'red', 'bold'), color(item.player_name, 'yellow'),
color(get_item_name_from_id(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
get_location_name_from_address(item.location), index, len(ctx.items_received)))
if command[0] == '/missing':
@ -742,10 +757,6 @@ async def console_loop(ctx : Context):
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):
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'
@ -822,20 +833,19 @@ async def game_watcher(ctx : Context):
while not ctx.exit_event.is_set():
await asyncio.sleep(2)
if not ctx.rom_confirmed:
if not ctx.rom:
rom = await snes_read(ctx, ROMNAME_START, ROMNAME_SIZE)
if rom is None or rom == bytes([0] * ROMNAME_SIZE):
continue
if list(rom) != ctx.last_rom:
ctx.last_rom = list(rom)
ctx.locations_checked = set()
if ctx.expected_rom is not None:
if ctx.last_rom[:len(ctx.expected_rom)] != ctx.expected_rom:
print("Wrong ROM detected")
await ctx.snes_socket.close()
continue
else:
rom_confirmed(ctx)
ctx.rom = list(rom)
ctx.locations_checked = set()
if ctx.awaiting_rom:
await server_auth(ctx, False)
if ctx.auth and ctx.auth != ctx.rom:
print("ROM change detected, please reconnect to the multiworld server")
await disconnect(ctx)
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
if gamemode is None or gamemode[0] not in INGAME_MODES:
@ -858,12 +868,12 @@ async def game_watcher(ctx : Context):
if recv_index < len(ctx.items_received) and recv_item == 0:
item = ctx.items_received[recv_index]
print('Received %s from %s (%s) (%d/%d in list)' % (
color(get_item_name_from_id(item.item), 'red', 'bold'), color(item.player_name, 'yellow'),
color(get_item_name_from_id(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
get_location_name_from_address(item.location), recv_index + 1, len(ctx.items_received)))
recv_index += 1
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_PLAYER_ADDR, bytes([item.player_id]))
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([item.player]))
await snes_flush_writes(ctx)
@ -872,16 +882,13 @@ async def main():
parser.add_argument('--snes', default='localhost:8080', help='Address of the QUsb2snes server.')
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
parser.add_argument('--name', default=None)
parser.add_argument('--team', default=None)
parser.add_argument('--slot', default=None, type=int)
args = parser.parse_args()
ctx = Context(args.snes, args.connect, args.password, args.name, args.team, args.slot)
ctx = Context(args.snes, args.connect, args.password)
input_task = asyncio.create_task(console_loop(ctx))
await snes_connect(ctx)
await snes_connect(ctx, ctx.snes_address)
if ctx.server_task is None:
ctx.server_task = asyncio.create_task(server_loop(ctx))
@ -890,7 +897,8 @@ async def main():
await ctx.exit_event.wait()
ctx.server_address = None
ctx.snes_reconnect_address = None
await watcher_task

View File

@ -5,6 +5,7 @@ import functools
import json
import logging
import re
import shlex
import urllib.request
import websockets
import zlib
@ -27,29 +28,23 @@ class Context:
self.data_filename = None
self.save_filename = None
self.disable_save = False
self.players = 0
self.player_names = {}
self.rom_names = {}
self.locations = {}
self.host = host
self.port = port
self.password = password
self.server = None
self.countdown_timer = 0
self.clients = []
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):
if not websocket or not websocket.open or websocket.closed:
return
@ -65,21 +60,21 @@ def broadcast_all(ctx : Context, msgs):
def broadcast_team(ctx : Context, team, msgs):
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))
def notify_all(ctx : Context, text):
print("Notice (all): %s" % text)
broadcast_all(ctx, [['Print', text]])
def notify_team(ctx : Context, team : str, text : str):
print("Team notice (%s): %s" % ("Default" if not team else team, text))
def notify_team(ctx : Context, team : int, text : str):
print("Notice (Team #%d): %s" % (team+1, text))
broadcast_team(ctx, team, [['Print', text]])
def notify_client(client : Client, text : str):
if not client.auth:
return
print("Player notice (%s): %s" % (client.name, text))
print("Notice (Player %s in team %d): %s" % (client.name, client.team+1, text))
asyncio.create_task(send_msgs(client.socket, [['Print', text]]))
async def server(websocket, path, ctx : Context):
@ -112,47 +107,44 @@ async def on_client_disconnected(ctx : Context, client : Client):
await on_client_left(ctx, client)
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):
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):
auth_clients = [c for c in ctx.clients if c.auth]
if not auth_clients:
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
text = ''
text = 'Team #1: '
for c in auth_clients:
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
text += '%d:%s ' % (c.slot, c.name)
text += f'{c.name} '
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):
for (c_team, c_id), items in ctx.received_items.items():
if c_id == player and same_team(c_team, team):
return items
ctx.received_items[(team, player)] = []
return ctx.received_items[(team, player)]
return ctx.received_items.setdefault((team, player), [])
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):
for client in ctx.clients:
@ -163,12 +155,12 @@ def send_new_items(ctx : Context):
asyncio.create_task(send_msgs(client.socket, [['ReceivedItems', (client.send_index, tuplize_received_items(items)[client.send_index:])]]))
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]
notify_all(ctx, "%s (Player %d) in team %s has forfeited" % (name, slot, team if team else 'default'))
register_location_checks(ctx, name, team, slot, all_locations)
notify_all(ctx, "%s (Team #%d) has forfeited" % (ctx.player_names[(team, slot)], team + 1))
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
for location in locations:
if (location, slot) in ctx.locations:
@ -177,23 +169,21 @@ def register_location_checks(ctx : Context, name, team, slot, locations):
found = False
recvd_items = get_received_items(ctx, team, target_player)
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
break
if not found:
new_item = ReceivedItem(target_item, location, slot, name)
new_item = ReceivedItem(target_item, location, slot)
recvd_items.append(new_item)
target_player_name = get_player_name_in_team(ctx, team, target_player)
broadcast_team(ctx, team, [['ItemSent', (name, target_player_name, target_item, location)]])
print('(%s) %s sent %s to %s (%s)' % (team if team else 'Team', name, get_item_name_from_id(target_item), target_player_name, get_location_name_from_address(location)))
broadcast_team(ctx, team, [['ItemSent', (slot, location, target_player, target_item)]])
print('(Team #%d) %s sent %s to %s (%s)' % (team, ctx.player_names[(team, slot)], get_item_name_from_id(target_item), ctx.player_names[(team, target_player)], get_location_name_from_address(location)))
found_items = True
send_new_items(ctx)
if found_items and not ctx.disable_save:
try:
with open(ctx.save_filename, "wb") as f:
jsonstr = json.dumps((ctx.players,
[(k, v) for k, v in ctx.rom_names.items()],
jsonstr = json.dumps((list(ctx.rom_names.items()),
[(k, [i.__dict__ for i in v]) for k, v in ctx.received_items.items()]))
f.write(zlib.compress(jsonstr.encode("utf-8")))
except Exception as e:
@ -207,50 +197,30 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args):
if cmd == 'Connect':
if not args or type(args) is not dict 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 \
'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)]:
'rom' not in args or type(args['rom']) is not list:
await send_msgs(client.socket, [['InvalidArguments', 'Connect']])
return
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')
if 'name' not in args or not args['name'] or not re.match(r'\w{1,10}', args['name']):
errors.add('InvalidName')
elif any([same_name(c.name, args['name']) for c in ctx.clients if c.auth]):
errors.add('NameAlreadyTaken')
if tuple(args['rom']) not in ctx.rom_names:
errors.add('InvalidRom')
else:
client.name = args['name']
if 'team' in args and args['team'] is not None and not re.match(r'\w{1,15}', args['team']):
errors.add('InvalidTeam')
else:
client.team = args['team'] if 'team' in args else None
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']
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]):
errors.add('SlotAlreadyTaken')
else:
client.name = ctx.player_names[(team, slot)]
client.team = team
client.slot = slot
if errors:
client.name = None
client.team = None
client.slot = None
await send_msgs(client.socket, [['ConnectionRefused', list(errors)]])
else:
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)
if items:
reply.append(['ReceivedItems', (0, tuplize_received_items(items))])
@ -271,7 +241,7 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args):
if type(args) is not list:
await send_msgs(client.socket, [['InvalidArguments', 'LocationChecks']])
return
register_location_checks(ctx, client.name, client.team, client.slot, args)
register_location_checks(ctx, client.team, client.slot, args)
if cmd == 'Say':
if type(args) is not str or not args.isprintable():
@ -280,10 +250,16 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args):
notify_all(ctx, client.name + ': ' + args)
if args[:8] == '!players':
if args.startswith('!players'):
notify_all(ctx, get_connected_players_string(ctx))
if args[:8] == '!forfeit':
forfeit_player(ctx, client.team, client.slot, client.name)
if args.startswith('!forfeit'):
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):
ctx.password = password
@ -293,7 +269,7 @@ async def console(ctx : Context):
while True:
input = await aioconsole.ainput()
command = input.split()
command = shlex.split(input)
if not command:
continue
@ -306,27 +282,34 @@ async def console(ctx : Context):
if command[0] == '/password':
set_password(ctx, command[1] if len(command) > 1 else None)
if command[0] == '/kick' and len(command) > 1:
client = get_client_from_name(ctx, command[1])
if client and client.socket and not client.socket.closed:
await client.socket.close()
team = int(command[2]) - 1 if len(command) > 2 and command[2].isdigit() else None
for client in ctx.clients:
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():
team = command[1] if command[1] != 'default' else None
slot = int(command[2])
name = get_player_name_in_team(ctx, team, slot)
forfeit_player(ctx, team, slot, name)
if command[0] == '/forfeitslot' and len(command) > 1 and command[1].isdigit():
if len(command) > 2 and command[2].isdigit():
team = int(command[1]) - 1
slot = int(command[2])
else:
team = 0
slot = int(command[1])
forfeit_player(ctx, team, slot)
if command[0] == '/forfeitplayer' and len(command) > 1:
client = get_client_from_name(ctx, command[1])
if client:
forfeit_player(ctx, client.team, client.slot, client.name)
team = int(command[2]) - 1 if len(command) > 2 and command[2].isdigit() else None
for client in ctx.clients:
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:
[(player, item)] = re.findall(r'\S* (\S*) (.*)', input)
if item in Items.item_table:
client = get_client_from_name(ctx, player)
if client:
new_item = ReceivedItem(Items.item_table[item][3], "cheat console", 0, "server")
get_received_items(ctx, client.team, client.slot).append(new_item)
notify_all(ctx, 'Cheat console: sending "' + item + '" to ' + client.name)
for client in ctx.clients:
if client.auth and client.name.lower() == player.lower():
new_item = ReceivedItem(Items.item_table[item][3], "cheat console", client.slot)
get_received_items(ctx, client.team, client.slot).append(new_item)
notify_all(ctx, 'Cheat console: sending "' + item + '" to ' + client.name)
send_new_items(ctx)
else:
print("Unknown item: " + item)
@ -358,15 +341,17 @@ async def main():
with open(ctx.data_filename, 'rb') as f:
jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8"))
ctx.players = jsonobj[0]
ctx.rom_names = {k: v for k, v in jsonobj[1]}
for team, names in enumerate(jsonobj[0]):
for player, name in enumerate(names, 1):
ctx.player_names[(team, player)] = name
ctx.rom_names = {tuple(rom): (team, slot) for slot, team, rom in jsonobj[1]}
ctx.locations = {tuple(k): tuple(v) for k, v in jsonobj[2]}
except Exception as e:
print('Failed to read multiworld data (%s)' % e)
return
ip = urllib.request.urlopen('https://v4.ident.me').read().decode('utf8') if not ctx.host else ctx.host
print('Hosting game of %d players (%s) at %s:%d' % (ctx.players, 'No password' if not ctx.password else 'Password: %s' % ctx.password, ip, ctx.port))
print('Hosting game at %s:%d (%s)' % (ip, ctx.port, 'No password' if not ctx.password else 'Password: %s' % ctx.password))
ctx.disable_save = args.disable_save
if not ctx.disable_save:
@ -375,10 +360,9 @@ async def main():
try:
with open(ctx.save_filename, 'rb') as f:
jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8"))
players = 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[2]}
if players != ctx.players or rom_names != ctx.rom_names:
rom_names = jsonobj[0]
received_items = {tuple(k): [ReceivedItem(**i) for i in v] for k, v in jsonobj[1]}
if not all([ctx.rom_names[tuple(rom)] == (team, slot) for rom, (team, slot) in rom_names]):
raise Exception('Save file mismatch, will start a new game')
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)))

View File

@ -39,6 +39,7 @@ def main():
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('--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('--rom')
parser.add_argument('--enemizercli')

View File

@ -24,6 +24,7 @@ def main(args):
# initialize the world
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('')
hasher = hashlib.md5()
@ -69,7 +70,7 @@ def main(args):
logger.info('Patching 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)

40
Rom.py
View File

@ -27,6 +27,7 @@ class JsonRom(object):
def __init__(self):
self.name = None
self.hash = None
self.orig_buffer = None
self.patches = {}
self.addresses = []
@ -72,6 +73,7 @@ class LocalRom(object):
def __init__(self, file, patch=True):
self.name = None
self.hash = None
self.orig_buffer = None
with open(file, 'rb') as stream:
self.buffer = read_rom(stream)
@ -469,7 +471,7 @@ class Sprite(object):
# split into palettes of 15 colors
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])
# progressive bow silver arrow hint hack
@ -1222,13 +1224,18 @@ def patch_rom(world, player, rom, enemized):
rom.write_byte(0xFED31, 0x2A) # preopen bombable exit
rom.write_byte(0xFEE41, 0x2A) # preopen bombable exit
write_strings(rom, world, player)
write_strings(rom, world, player, team)
# set rom name
# 21 bytes
from Main import __version__
rom.name = bytearray('ER{0}_{1}_{2:09}\0'.format(__version__.split('-')[0].replace('.','')[0:3], player, world.seed), 'utf8')
rom.write_bytes(0x7FC0, rom.name[0:21])
rom.name = bytearray(f'ER{__version__.split("-")[0].replace(".","")[0:3]}_{team+1}_{player}_{world.seed:09}\0', 'utf8')[:21]
rom.name.extend([0] * (21 - len(rom.name)))
rom.write_bytes(0x7FC0, rom.name)
# set player names
for p in range(1, min(world.players, 64) + 1):
rom.write_bytes(0x186380 + ((p - 1) * 32), hud_format_text(world.player_names[p][team]))
# Write title screen Code
hashint = int(rom.get_hash(), 16)
@ -1240,6 +1247,7 @@ def patch_rom(world, player, rom, enemized):
hashint & 0x1F,
]
rom.write_bytes(0x180215, code)
rom.hash = code
return rom
@ -1303,7 +1311,7 @@ def hud_format_text(text):
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):
sprite = Sprite(sprite) if os.path.isfile(sprite) else get_sprite_from_name(sprite)
@ -1372,11 +1380,6 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr
elif uw_palettes == 'blackout':
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):
rom.write_crc()
@ -1513,12 +1516,15 @@ def blackout_uw_palettes(rom):
rom.write_bytes(i+44, [0] * 76)
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):
address, maxbytes = text_addresses[target]
rom.write_bytes(address, MultiByteTextMapper.convert(string, maxbytes))
def write_strings(rom, world, player):
def write_strings(rom, world, player, team):
tt = TextTable()
tt.removeUnwantedText()
@ -1536,11 +1542,11 @@ def write_strings(rom, world, player):
hint = dest.hint_text if dest.hint_text else "something"
if dest.player != player:
if ped_hint:
hint += " for p%d!" % dest.player
hint += f" for {world.player_names[dest.player][team]}!"
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:
hint += " for p%d" % dest.player
hint += f" for {world.player_names[dest.player][team]}"
return hint
# For hints, first we write hints about entrances, some from the inconvenient list others from all reasonable entrances.
@ -2281,3 +2287,9 @@ BigKeys = ['Big Key (Eastern Palace)',
'Big Key (Turtle Rock)',
'Big Key (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

@ -3,9 +3,6 @@ import re
import subprocess
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):
value = value & 0xFFFF
return [value & 0xFF, (value >> 8) & 0xFF]
@ -20,6 +17,18 @@ def pc_to_snes(value):
def snes_to_pc(value):
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():
return getattr(sys, 'frozen', False)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long