Multiworld: clients will now be automatically be identified from the rom name and have their names and teams set by the host, meaning those need to be configured during seed gen
Player names will show up in spoiler log and hint tiles instead of player id MultiClient: autoreconnect to mw server
This commit is contained in:
parent
d9592e68fb
commit
ad278f91d6
|
@ -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))
|
||||
|
||||
|
|
117
BaseClasses.py
117
BaseClasses.py
|
@ -11,6 +11,7 @@ class World(object):
|
|||
|
||||
def __init__(self, players, shuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, goal, algorithm, accessibility, shuffle_ganon, retro, custom, customitemarray, hints):
|
||||
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')
|
||||
|
|
|
@ -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
24
Gui.py
|
@ -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
108
Main.py
|
@ -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):
|
||||
|
|
199
MultiClient.py
199
MultiClient.py
|
@ -2,7 +2,7 @@ import argparse
|
|||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.parse
|
||||
|
@ -36,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
|
||||
|
||||
|
@ -65,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,
|
||||
|
@ -418,9 +416,9 @@ async def snes_connect(ctx : Context, 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_reconnect(ctx))
|
||||
asyncio.create_task(snes_autoreconnect(ctx))
|
||||
|
||||
async def snes_reconnect(ctx: Context):
|
||||
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)
|
||||
|
@ -443,12 +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_reconnect(ctx))
|
||||
asyncio.create_task(snes_autoreconnect(ctx))
|
||||
|
||||
async def snes_read(ctx : Context, address, size):
|
||||
try:
|
||||
|
@ -560,22 +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, 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):
|
||||
|
@ -591,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':
|
||||
|
@ -608,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
|
||||
|
@ -667,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)
|
||||
|
@ -683,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()
|
||||
|
@ -709,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
|
||||
|
||||
|
@ -730,21 +725,12 @@ async def console_loop(ctx : Context):
|
|||
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]]))
|
||||
|
||||
|
@ -752,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':
|
||||
|
@ -771,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'
|
||||
|
@ -851,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:
|
||||
|
@ -887,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)
|
||||
|
||||
|
@ -901,12 +882,9 @@ 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))
|
||||
|
||||
|
@ -919,6 +897,7 @@ async def main():
|
|||
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
ctx.server_address = None
|
||||
ctx.snes_reconnect_address = None
|
||||
|
||||
await watcher_task
|
||||
|
|
174
MultiServer.py
174
MultiServer.py
|
@ -5,6 +5,7 @@ import functools
|
|||
import json
|
||||
import logging
|
||||
import re
|
||||
import shlex
|
||||
import urllib.request
|
||||
import websockets
|
||||
import zlib
|
||||
|
@ -27,7 +28,7 @@ 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
|
||||
|
@ -41,16 +42,9 @@ class Context:
|
|||
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
|
||||
|
@ -66,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):
|
||||
|
@ -113,10 +107,10 @@ 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')
|
||||
|
@ -136,37 +130,21 @@ def get_connected_players_string(ctx : Context):
|
|||
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:
|
||||
|
@ -177,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:
|
||||
|
@ -191,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:
|
||||
|
@ -221,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))])
|
||||
|
@ -285,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():
|
||||
|
@ -297,7 +253,7 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args):
|
|||
if args.startswith('!players'):
|
||||
notify_all(ctx, get_connected_players_string(ctx))
|
||||
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])
|
||||
|
@ -313,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
|
||||
|
||||
|
@ -326,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)
|
||||
|
@ -378,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:
|
||||
|
@ -395,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)))
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
40
Rom.py
|
@ -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"
|
||||
]
|
||||
|
|
15
Utils.py
15
Utils.py
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in New Issue