merge conflicts
This commit is contained in:
commit
348887ce92
|
@ -13,4 +13,5 @@ README.html
|
|||
*multisave
|
||||
EnemizerCLI/
|
||||
.mypy_cache/
|
||||
RaceRom.py
|
||||
RaceRom.py
|
||||
weights/
|
||||
|
|
12
Adjuster.py
12
Adjuster.py
|
@ -6,6 +6,7 @@ import textwrap
|
|||
import sys
|
||||
|
||||
from AdjusterMain import adjust
|
||||
from Rom import get_sprite_from_name
|
||||
|
||||
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
||||
|
||||
|
@ -15,7 +16,8 @@ class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
|||
def main():
|
||||
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
|
||||
|
||||
parser.add_argument('--rom', default='ER_base.sfc', help='Path to an ALttP JAP(1.0) rom to use as a base.')
|
||||
parser.add_argument('--rom', default='ER_base.sfc', help='Path to an ALttPR rom to adjust.')
|
||||
parser.add_argument('--baserom', default='Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', help='Path to an ALttP JAP(1.0) rom to use as a base.')
|
||||
parser.add_argument('--loglevel', default='info', const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
|
||||
parser.add_argument('--fastmenu', default='normal', const='normal', nargs='?', choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
|
||||
help='''\
|
||||
|
@ -31,6 +33,8 @@ def main():
|
|||
''')
|
||||
parser.add_argument('--heartcolor', default='red', const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'],
|
||||
help='Select the color of Link\'s heart meter. (default: %(default)s)')
|
||||
parser.add_argument('--ow_palettes', default='default', choices=['default', 'random', 'blackout'])
|
||||
parser.add_argument('--uw_palettes', default='default', choices=['default', 'random', 'blackout'])
|
||||
parser.add_argument('--sprite', help='''\
|
||||
Path to a sprite sheet to use for Link. Needs to be in
|
||||
binary format and have a length of 0x7000 (28672) bytes,
|
||||
|
@ -43,10 +47,10 @@ def main():
|
|||
|
||||
# ToDo: Validate files further than mere existance
|
||||
if not os.path.isfile(args.rom):
|
||||
input('Could not find valid base rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom)
|
||||
input('Could not find valid rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom)
|
||||
sys.exit(1)
|
||||
if args.sprite is not None and not os.path.isfile(args.sprite):
|
||||
input('Could not find link sprite sheet at given location. \nPress Enter to exit.' % args.sprite)
|
||||
if args.sprite is not None and not os.path.isfile(args.sprite) and not get_sprite_from_name(args.sprite):
|
||||
input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
|
||||
sys.exit(1)
|
||||
|
||||
# set up logger
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import os
|
||||
import re
|
||||
import time
|
||||
import logging
|
||||
|
||||
from Utils import output_path, parse_names_string
|
||||
from Rom import LocalRom, Sprite, apply_rom_settings
|
||||
from Utils import output_path
|
||||
from Rom import LocalRom, apply_rom_settings
|
||||
|
||||
|
||||
def adjust(args):
|
||||
|
@ -12,22 +11,17 @@ def adjust(args):
|
|||
logger = logging.getLogger('')
|
||||
logger.info('Patching ROM.')
|
||||
|
||||
if args.sprite is not None:
|
||||
if isinstance(args.sprite, Sprite):
|
||||
sprite = args.sprite
|
||||
else:
|
||||
sprite = Sprite(args.sprite)
|
||||
else:
|
||||
sprite = None
|
||||
|
||||
outfilebase = os.path.basename(args.rom)[:-4] + '_adjusted'
|
||||
|
||||
if os.stat(args.rom).st_size in (0x200000, 0x400000) and os.path.splitext(args.rom)[-1].lower() == '.sfc':
|
||||
rom = LocalRom(args.rom, False)
|
||||
if os.path.isfile(args.baserom):
|
||||
baserom = LocalRom(args.baserom, True)
|
||||
rom.orig_buffer = baserom.orig_buffer
|
||||
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, sprite, 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))
|
||||
|
||||
|
|
138
BaseClasses.py
138
BaseClasses.py
|
@ -9,8 +9,9 @@ from Utils import int16_as_bytes
|
|||
|
||||
class World(object):
|
||||
|
||||
def __init__(self, players, shuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, goal, algorithm, accessibility, shuffle_ganon, quickswap, fastmenu, disable_music, retro, custom, customitemarray, hints):
|
||||
def __init__(self, players, shuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, goal, algorithm, accessibility, shuffle_ganon, retro, custom, customitemarray, hints):
|
||||
self.players = players
|
||||
self.teams = 1
|
||||
self.shuffle = shuffle.copy()
|
||||
self.logic = logic.copy()
|
||||
self.mode = mode.copy()
|
||||
|
@ -44,9 +45,6 @@ class World(object):
|
|||
self.accessibility = accessibility.copy()
|
||||
self.shuffle_ganon = shuffle_ganon
|
||||
self.fix_gtower_exit = self.shuffle_ganon
|
||||
self.quickswap = quickswap
|
||||
self.fastmenu = fastmenu
|
||||
self.disable_music = disable_music
|
||||
self.retro = retro.copy()
|
||||
self.custom = custom
|
||||
self.customitemarray = customitemarray
|
||||
|
@ -61,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)
|
||||
|
@ -93,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
|
||||
|
@ -214,6 +219,9 @@ 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)
|
||||
self.state.collect(item, True)
|
||||
|
||||
|
@ -224,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)
|
||||
|
||||
|
@ -708,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):
|
||||
|
@ -747,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):
|
||||
|
||||
|
@ -788,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):
|
||||
|
@ -834,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):
|
||||
|
@ -856,6 +854,7 @@ class Item(object):
|
|||
self.hint_text = hint_text
|
||||
self.code = code
|
||||
self.location = None
|
||||
self.world = None
|
||||
self.player = player
|
||||
|
||||
@property
|
||||
|
@ -882,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
|
||||
|
@ -899,14 +895,14 @@ class ShopType(Enum):
|
|||
UpgradeShop = 2
|
||||
|
||||
class Shop(object):
|
||||
def __init__(self, region, room_id, type, shopkeeper_config, replaceable):
|
||||
def __init__(self, region, room_id, type, shopkeeper_config, custom, locked):
|
||||
self.region = region
|
||||
self.room_id = room_id
|
||||
self.type = type
|
||||
self.inventory = [None, None, None]
|
||||
self.shopkeeper_config = shopkeeper_config
|
||||
self.replaceable = replaceable
|
||||
self.active = False
|
||||
self.custom = custom
|
||||
self.locked = locked
|
||||
|
||||
@property
|
||||
def item_count(self):
|
||||
|
@ -958,10 +954,12 @@ class Spoiler(object):
|
|||
|
||||
def __init__(self, world):
|
||||
self.world = world
|
||||
self.hashes = {}
|
||||
self.entrances = OrderedDict()
|
||||
self.medallions = {}
|
||||
self.playthrough = {}
|
||||
self.unreachables = []
|
||||
self.startinventory = []
|
||||
self.locations = {}
|
||||
self.paths = {}
|
||||
self.metadata = {}
|
||||
|
@ -981,8 +979,10 @@ 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))
|
||||
|
||||
self.locations = OrderedDict()
|
||||
listed_locations = set()
|
||||
|
@ -1011,7 +1011,7 @@ class Spoiler(object):
|
|||
|
||||
self.shops = []
|
||||
for shop in self.world.shops:
|
||||
if not shop.active:
|
||||
if not shop.custom:
|
||||
continue
|
||||
shopdata = {'location': str(shop.region),
|
||||
'type': 'Take Any' if shop.type == ShopType.TakeAny else 'Shop'
|
||||
|
@ -1073,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):
|
||||
|
@ -1081,7 +1082,10 @@ class Spoiler(object):
|
|||
out = OrderedDict()
|
||||
out['Entrances'] = list(self.entrances.values())
|
||||
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
|
||||
|
@ -1095,48 +1099,50 @@ 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('L\\R Quickswap enabled: %s\n' % ('Yes' if self.world.quickswap else 'No'))
|
||||
outfile.write('Menu speed: %s' % self.world.fastmenu)
|
||||
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'.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')
|
||||
outfile.write('\n'.join("{} [{}]\n {}".format(shop['location'], shop['type'], "\n ".join(item for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if item)) for shop in self.shops))
|
||||
outfile.write('\n\nPlaythrough:\n\n')
|
||||
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join([' %s: %s' % (location, item) for (location, item) in sphere.items()])) for (sphere_nr, sphere) in self.playthrough.items()]))
|
||||
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join([' %s: %s' % (location, item) for (location, item) in sphere.items()] if sphere_nr != '0' else [f' {item}' for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))
|
||||
if self.unreachables:
|
||||
outfile.write('\n\nUnreachable Items:\n\n')
|
||||
outfile.write('\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))
|
||||
|
|
|
@ -9,6 +9,7 @@ import shlex
|
|||
import sys
|
||||
|
||||
from Main import main
|
||||
from Rom import get_sprite_from_name
|
||||
from Utils import is_bundled, close_console
|
||||
|
||||
|
||||
|
@ -214,10 +215,12 @@ def parse_arguments(argv, no_defaults=False):
|
|||
parser.add_argument('--compassshuffle', default=defval(False), help='Compasses are no longer restricted to their dungeons, but can be anywhere', action='store_true')
|
||||
parser.add_argument('--keyshuffle', default=defval(False), help='Small Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true')
|
||||
parser.add_argument('--bigkeyshuffle', default=defval(False), help='Big Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true')
|
||||
parser.add_argument('--keysanity', default=defval(False), help=argparse.SUPPRESS, action='store_true')
|
||||
parser.add_argument('--retro', default=defval(False), help='''\
|
||||
Keys are universal, shooting arrows costs rupees,
|
||||
and a few other little things make this more like Zelda-1.
|
||||
''', action='store_true')
|
||||
parser.add_argument('--startinventory', default=defval(''), help='Specifies a list of items that will be in your starting inventory (separated by commas)')
|
||||
parser.add_argument('--custom', default=defval(False), help='Not supported.')
|
||||
parser.add_argument('--customitemarray', default=defval(False), help='Not supported.')
|
||||
parser.add_argument('--accessibility', default=defval('items'), const='items', nargs='?', choices=['items', 'locations', 'none'], help='''\
|
||||
|
@ -243,6 +246,8 @@ def parse_arguments(argv, no_defaults=False):
|
|||
''')
|
||||
parser.add_argument('--heartcolor', default=defval('red'), const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'],
|
||||
help='Select the color of Link\'s heart meter. (default: %(default)s)')
|
||||
parser.add_argument('--ow_palettes', default=defval('default'), choices=['default', 'random', 'blackout'])
|
||||
parser.add_argument('--uw_palettes', default=defval('default'), choices=['default', 'random', 'blackout'])
|
||||
parser.add_argument('--sprite', help='''\
|
||||
Path to a sprite sheet to use for Link. Needs to be in
|
||||
binary format and have a length of 0x7000 (28672) bytes,
|
||||
|
@ -257,16 +262,16 @@ def parse_arguments(argv, no_defaults=False):
|
|||
for VT site integration, do not use otherwise.
|
||||
''')
|
||||
parser.add_argument('--skip_playthrough', action='store_true', default=defval(False))
|
||||
parser.add_argument('--enemizercli', default=defval(''))
|
||||
parser.add_argument('--enemizercli', default=defval('EnemizerCLI/EnemizerCLI.Core'))
|
||||
parser.add_argument('--shufflebosses', default=defval('none'), choices=['none', 'basic', 'normal', 'chaos'])
|
||||
parser.add_argument('--shuffleenemies', default=defval('none'), choices=['none', 'shuffled', 'chaos'])
|
||||
parser.add_argument('--enemy_health', default=defval('default'), choices=['default', 'easy', 'normal', 'hard', 'expert'])
|
||||
parser.add_argument('--enemy_damage', default=defval('default'), choices=['default', 'shuffled', 'chaos'])
|
||||
parser.add_argument('--shufflepalette', default=defval(False), action='store_true')
|
||||
parser.add_argument('--shufflepots', default=defval(False), action='store_true')
|
||||
parser.add_argument('--beemizer', default=defval(0), type=lambda value: min(max(int(value), 0), 4))
|
||||
parser.add_argument('--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')
|
||||
|
@ -276,6 +281,8 @@ def parse_arguments(argv, no_defaults=False):
|
|||
parser.add_argument(f'--p{player}', default=defval(''), help=argparse.SUPPRESS)
|
||||
|
||||
ret = parser.parse_args(argv)
|
||||
if ret.keysanity:
|
||||
ret.mapshuffle, ret.compassshuffle, ret.keyshuffle, ret.bigkeyshuffle = [True] * 4
|
||||
|
||||
if multiargs.multi:
|
||||
defaults = copy.deepcopy(ret)
|
||||
|
@ -284,9 +291,10 @@ def parse_arguments(argv, no_defaults=False):
|
|||
|
||||
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality',
|
||||
'shuffle', 'crystals_ganon', 'crystals_gt', 'openpyramid',
|
||||
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle',
|
||||
'retro', 'accessibility', 'hints', 'shufflepalette', 'shufflepots', 'beemizer',
|
||||
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage']:
|
||||
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
|
||||
'retro', 'accessibility', 'hints', 'beemizer',
|
||||
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots',
|
||||
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep']:
|
||||
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
|
||||
if player == 1:
|
||||
setattr(ret, name, {1: value})
|
||||
|
@ -312,9 +320,9 @@ def start():
|
|||
if not args.jsonout and not os.path.isfile(args.rom):
|
||||
input('Could not find valid base rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom)
|
||||
sys.exit(1)
|
||||
if args.sprite is not None and not os.path.isfile(args.sprite):
|
||||
if any([sprite is not None and not os.path.isfile(sprite) and not get_sprite_from_name(sprite) for sprite in args.sprite.values()]):
|
||||
if not args.jsonout:
|
||||
input('Could not find link sprite sheet at given location. \nPress Enter to exit.' % args.sprite)
|
||||
input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
|
||||
sys.exit(1)
|
||||
else:
|
||||
raise IOError('Cannot find sprite file at %s' % args.sprite)
|
||||
|
|
209
Gui.py
209
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):
|
||||
|
@ -60,8 +60,6 @@ def guiMain(args=None):
|
|||
createSpoilerCheckbutton = Checkbutton(checkBoxFrame, text="Create Spoiler Log", variable=createSpoilerVar)
|
||||
suppressRomVar = IntVar()
|
||||
suppressRomCheckbutton = Checkbutton(checkBoxFrame, text="Do not create patched Rom", variable=suppressRomVar)
|
||||
quickSwapVar = IntVar()
|
||||
quickSwapCheckbutton = Checkbutton(checkBoxFrame, text="Enabled L/R Item quickswapping", variable=quickSwapVar)
|
||||
openpyramidVar = IntVar()
|
||||
openpyramidCheckbutton = Checkbutton(checkBoxFrame, text="Pre-open Pyramid Hole", variable=openpyramidVar)
|
||||
mcsbshuffleFrame = Frame(checkBoxFrame)
|
||||
|
@ -76,8 +74,6 @@ def guiMain(args=None):
|
|||
bigkeyshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="BigKeys", variable=bigkeyshuffleVar)
|
||||
retroVar = IntVar()
|
||||
retroCheckbutton = Checkbutton(checkBoxFrame, text="Retro mode (universal keys)", variable=retroVar)
|
||||
disableMusicVar = IntVar()
|
||||
disableMusicCheckbutton = Checkbutton(checkBoxFrame, text="Disable game music", variable=disableMusicVar)
|
||||
shuffleGanonVar = IntVar()
|
||||
shuffleGanonVar.set(1) #set default
|
||||
shuffleGanonCheckbutton = Checkbutton(checkBoxFrame, text="Include Ganon's Tower and Pyramid Hole in shuffle pool", variable=shuffleGanonVar)
|
||||
|
@ -89,7 +85,6 @@ def guiMain(args=None):
|
|||
|
||||
createSpoilerCheckbutton.pack(expand=True, anchor=W)
|
||||
suppressRomCheckbutton.pack(expand=True, anchor=W)
|
||||
quickSwapCheckbutton.pack(expand=True, anchor=W)
|
||||
openpyramidCheckbutton.pack(expand=True, anchor=W)
|
||||
mcsbshuffleFrame.pack(expand=True, anchor=W)
|
||||
mcsbLabel.grid(row=0, column=0)
|
||||
|
@ -98,57 +93,23 @@ def guiMain(args=None):
|
|||
keyshuffleCheckbutton.grid(row=0, column=3)
|
||||
bigkeyshuffleCheckbutton.grid(row=0, column=4)
|
||||
retroCheckbutton.pack(expand=True, anchor=W)
|
||||
disableMusicCheckbutton.pack(expand=True, anchor=W)
|
||||
shuffleGanonCheckbutton.pack(expand=True, anchor=W)
|
||||
hintsCheckbutton.pack(expand=True, anchor=W)
|
||||
customCheckbutton.pack(expand=True, anchor=W)
|
||||
|
||||
fileDialogFrame = Frame(rightHalfFrame)
|
||||
romOptionsFrame = LabelFrame(rightHalfFrame, text="Rom options")
|
||||
romOptionsFrame.columnconfigure(0, weight=1)
|
||||
romOptionsFrame.columnconfigure(1, weight=1)
|
||||
for i in range(5):
|
||||
romOptionsFrame.rowconfigure(i, weight=1)
|
||||
|
||||
heartbeepFrame = Frame(fileDialogFrame)
|
||||
heartbeepVar = StringVar()
|
||||
heartbeepVar.set('normal')
|
||||
heartbeepOptionMenu = OptionMenu(heartbeepFrame, heartbeepVar, 'double', 'normal', 'half', 'quarter', 'off')
|
||||
heartbeepOptionMenu.pack(side=RIGHT)
|
||||
heartbeepLabel = Label(heartbeepFrame, text='Heartbeep sound rate')
|
||||
heartbeepLabel.pack(side=LEFT, padx=(0,52))
|
||||
disableMusicVar = IntVar()
|
||||
disableMusicCheckbutton = Checkbutton(romOptionsFrame, text="Disable music", variable=disableMusicVar)
|
||||
disableMusicCheckbutton.grid(row=0, column=0, sticky=E)
|
||||
|
||||
heartcolorFrame = Frame(fileDialogFrame)
|
||||
heartcolorVar = StringVar()
|
||||
heartcolorVar.set('red')
|
||||
heartcolorOptionMenu = OptionMenu(heartcolorFrame, heartcolorVar, 'red', 'blue', 'green', 'yellow', 'random')
|
||||
heartcolorOptionMenu.pack(side=RIGHT)
|
||||
heartcolorLabel = Label(heartcolorFrame, text='Heart color')
|
||||
heartcolorLabel.pack(side=LEFT, padx=(0,127))
|
||||
|
||||
fastMenuFrame = Frame(fileDialogFrame)
|
||||
fastMenuVar = StringVar()
|
||||
fastMenuVar.set('normal')
|
||||
fastMenuOptionMenu = OptionMenu(fastMenuFrame, fastMenuVar, 'normal', 'instant', 'double', 'triple', 'quadruple', 'half')
|
||||
fastMenuOptionMenu.pack(side=RIGHT)
|
||||
fastMenuLabel = Label(fastMenuFrame, text='Menu speed')
|
||||
fastMenuLabel.pack(side=LEFT, padx=(0,100))
|
||||
|
||||
heartbeepFrame.pack(expand=True, anchor=E)
|
||||
heartcolorFrame.pack(expand=True, anchor=E)
|
||||
fastMenuFrame.pack(expand=True, anchor=E)
|
||||
|
||||
romDialogFrame = Frame(fileDialogFrame)
|
||||
baseRomLabel = Label(romDialogFrame, text='Base Rom')
|
||||
romVar = StringVar(value="Zelda no Densetsu - Kamigami no Triforce (Japan).sfc")
|
||||
romEntry = Entry(romDialogFrame, textvariable=romVar)
|
||||
|
||||
def RomSelect():
|
||||
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")])
|
||||
romVar.set(rom)
|
||||
romSelectButton = Button(romDialogFrame, text='Select Rom', command=RomSelect)
|
||||
|
||||
baseRomLabel.pack(side=LEFT)
|
||||
romEntry.pack(side=LEFT)
|
||||
romSelectButton.pack(side=LEFT)
|
||||
|
||||
spriteDialogFrame = Frame(fileDialogFrame)
|
||||
baseSpriteLabel = Label(spriteDialogFrame, text='Link Sprite:')
|
||||
spriteDialogFrame = Frame(romOptionsFrame)
|
||||
spriteDialogFrame.grid(row=0, column=1)
|
||||
baseSpriteLabel = Label(spriteDialogFrame, text='Sprite:')
|
||||
|
||||
spriteNameVar = StringVar()
|
||||
sprite = None
|
||||
|
@ -168,17 +129,79 @@ def guiMain(args=None):
|
|||
def SpriteSelect():
|
||||
SpriteSelector(mainWindow, set_sprite)
|
||||
|
||||
spriteSelectButton = Button(spriteDialogFrame, text='Open Sprite Picker', command=SpriteSelect)
|
||||
spriteSelectButton = Button(spriteDialogFrame, text='...', command=SpriteSelect)
|
||||
|
||||
baseSpriteLabel.pack(side=LEFT)
|
||||
spriteEntry.pack(side=LEFT)
|
||||
spriteSelectButton.pack(side=LEFT)
|
||||
|
||||
romDialogFrame.pack()
|
||||
spriteDialogFrame.pack()
|
||||
quickSwapVar = IntVar()
|
||||
quickSwapCheckbutton = Checkbutton(romOptionsFrame, text="L/R Quickswapping", variable=quickSwapVar)
|
||||
quickSwapCheckbutton.grid(row=1, column=0, sticky=E)
|
||||
|
||||
checkBoxFrame.pack()
|
||||
fileDialogFrame.pack()
|
||||
fastMenuFrame = Frame(romOptionsFrame)
|
||||
fastMenuFrame.grid(row=1, column=1, sticky=E)
|
||||
fastMenuLabel = Label(fastMenuFrame, text='Menu speed')
|
||||
fastMenuLabel.pack(side=LEFT)
|
||||
fastMenuVar = StringVar()
|
||||
fastMenuVar.set('normal')
|
||||
fastMenuOptionMenu = OptionMenu(fastMenuFrame, fastMenuVar, 'normal', 'instant', 'double', 'triple', 'quadruple', 'half')
|
||||
fastMenuOptionMenu.pack(side=LEFT)
|
||||
|
||||
heartcolorFrame = Frame(romOptionsFrame)
|
||||
heartcolorFrame.grid(row=2, column=0, sticky=E)
|
||||
heartcolorLabel = Label(heartcolorFrame, text='Heart color')
|
||||
heartcolorLabel.pack(side=LEFT)
|
||||
heartcolorVar = StringVar()
|
||||
heartcolorVar.set('red')
|
||||
heartcolorOptionMenu = OptionMenu(heartcolorFrame, heartcolorVar, 'red', 'blue', 'green', 'yellow', 'random')
|
||||
heartcolorOptionMenu.pack(side=LEFT)
|
||||
|
||||
heartbeepFrame = Frame(romOptionsFrame)
|
||||
heartbeepFrame.grid(row=2, column=1, sticky=E)
|
||||
heartbeepLabel = Label(heartbeepFrame, text='Heartbeep')
|
||||
heartbeepLabel.pack(side=LEFT)
|
||||
heartbeepVar = StringVar()
|
||||
heartbeepVar.set('normal')
|
||||
heartbeepOptionMenu = OptionMenu(heartbeepFrame, heartbeepVar, 'double', 'normal', 'half', 'quarter', 'off')
|
||||
heartbeepOptionMenu.pack(side=LEFT)
|
||||
|
||||
owPalettesFrame = Frame(romOptionsFrame)
|
||||
owPalettesFrame.grid(row=3, column=0, sticky=E)
|
||||
owPalettesLabel = Label(owPalettesFrame, text='Overworld palettes')
|
||||
owPalettesLabel.pack(side=LEFT)
|
||||
owPalettesVar = StringVar()
|
||||
owPalettesVar.set('default')
|
||||
owPalettesOptionMenu = OptionMenu(owPalettesFrame, owPalettesVar, 'default', 'random', 'blackout')
|
||||
owPalettesOptionMenu.pack(side=LEFT)
|
||||
|
||||
uwPalettesFrame = Frame(romOptionsFrame)
|
||||
uwPalettesFrame.grid(row=3, column=1, sticky=E)
|
||||
uwPalettesLabel = Label(uwPalettesFrame, text='Dungeon palettes')
|
||||
uwPalettesLabel.pack(side=LEFT)
|
||||
uwPalettesVar = StringVar()
|
||||
uwPalettesVar.set('default')
|
||||
uwPalettesOptionMenu = OptionMenu(uwPalettesFrame, uwPalettesVar, 'default', 'random', 'blackout')
|
||||
uwPalettesOptionMenu.pack(side=LEFT)
|
||||
|
||||
romDialogFrame = Frame(romOptionsFrame)
|
||||
romDialogFrame.grid(row=4, column=0, columnspan=2, sticky=W+E)
|
||||
|
||||
baseRomLabel = Label(romDialogFrame, text='Base Rom: ')
|
||||
romVar = StringVar(value="Zelda no Densetsu - Kamigami no Triforce (Japan).sfc")
|
||||
romEntry = Entry(romDialogFrame, textvariable=romVar)
|
||||
|
||||
def RomSelect():
|
||||
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")])
|
||||
romVar.set(rom)
|
||||
romSelectButton = Button(romDialogFrame, text='Select Rom', command=RomSelect)
|
||||
|
||||
baseRomLabel.pack(side=LEFT)
|
||||
romEntry.pack(side=LEFT, expand=True, fill=X)
|
||||
romSelectButton.pack(side=LEFT)
|
||||
|
||||
checkBoxFrame.pack(side=TOP, anchor=W, padx=5, pady=10)
|
||||
romOptionsFrame.pack(expand=True, fill=BOTH, padx=3)
|
||||
|
||||
drowDownFrame = Frame(topFrame)
|
||||
|
||||
|
@ -300,18 +323,19 @@ def guiMain(args=None):
|
|||
algorithmFrame.pack(expand=True, anchor=E)
|
||||
shuffleFrame.pack(expand=True, anchor=E)
|
||||
|
||||
enemizerFrame = LabelFrame(randomizerWindow, text="Enemizer", padx=5, pady=5)
|
||||
enemizerFrame = LabelFrame(randomizerWindow, text="Enemizer", padx=5, pady=2)
|
||||
enemizerFrame.columnconfigure(0, weight=1)
|
||||
enemizerFrame.columnconfigure(1, weight=1)
|
||||
enemizerFrame.columnconfigure(2, weight=1)
|
||||
enemizerFrame.columnconfigure(3, weight=1)
|
||||
|
||||
enemizerPathFrame = Frame(enemizerFrame)
|
||||
enemizerPathFrame.grid(row=0, column=0, columnspan=3, sticky=W)
|
||||
enemizerPathFrame.grid(row=0, column=0, columnspan=3, sticky=W+E, padx=3)
|
||||
enemizerCLIlabel = Label(enemizerPathFrame, text="EnemizerCLI path: ")
|
||||
enemizerCLIlabel.pack(side=LEFT)
|
||||
enemizerCLIpathVar = StringVar()
|
||||
enemizerCLIpathEntry = Entry(enemizerPathFrame, textvariable=enemizerCLIpathVar, width=80)
|
||||
enemizerCLIpathEntry.pack(side=LEFT)
|
||||
enemizerCLIpathVar = StringVar(value="EnemizerCLI/EnemizerCLI.Core")
|
||||
enemizerCLIpathEntry = Entry(enemizerPathFrame, textvariable=enemizerCLIpathVar)
|
||||
enemizerCLIpathEntry.pack(side=LEFT, expand=True, fill=X)
|
||||
def EnemizerSelectPath():
|
||||
path = filedialog.askopenfilename(filetypes=[("EnemizerCLI executable", "*EnemizerCLI*")])
|
||||
if path:
|
||||
|
@ -319,18 +343,21 @@ def guiMain(args=None):
|
|||
enemizerCLIbrowseButton = Button(enemizerPathFrame, text='...', command=EnemizerSelectPath)
|
||||
enemizerCLIbrowseButton.pack(side=LEFT)
|
||||
|
||||
enemyShuffleVar = IntVar()
|
||||
enemyShuffleButton = Checkbutton(enemizerFrame, text="Enemy shuffle", variable=enemyShuffleVar)
|
||||
enemyShuffleButton.grid(row=1, column=0)
|
||||
paletteShuffleVar = IntVar()
|
||||
paletteShuffleButton = Checkbutton(enemizerFrame, text="Palette shuffle", variable=paletteShuffleVar)
|
||||
paletteShuffleButton.grid(row=1, column=1)
|
||||
potShuffleVar = IntVar()
|
||||
potShuffleButton = Checkbutton(enemizerFrame, text="Pot shuffle", variable=potShuffleVar)
|
||||
potShuffleButton.grid(row=1, column=2)
|
||||
potShuffleButton.grid(row=0, column=3)
|
||||
|
||||
enemizerEnemyFrame = Frame(enemizerFrame)
|
||||
enemizerEnemyFrame.grid(row=1, column=0, pady=5)
|
||||
enemizerEnemyLabel = Label(enemizerEnemyFrame, text='Enemy shuffle')
|
||||
enemizerEnemyLabel.pack(side=LEFT)
|
||||
enemyShuffleVar = StringVar()
|
||||
enemyShuffleVar.set('none')
|
||||
enemizerEnemyOption = OptionMenu(enemizerEnemyFrame, enemyShuffleVar, 'none', 'shuffled', 'chaos')
|
||||
enemizerEnemyOption.pack(side=LEFT)
|
||||
|
||||
enemizerBossFrame = Frame(enemizerFrame)
|
||||
enemizerBossFrame.grid(row=2, column=0)
|
||||
enemizerBossFrame.grid(row=1, column=1)
|
||||
enemizerBossLabel = Label(enemizerBossFrame, text='Boss shuffle')
|
||||
enemizerBossLabel.pack(side=LEFT)
|
||||
enemizerBossVar = StringVar()
|
||||
|
@ -339,7 +366,7 @@ def guiMain(args=None):
|
|||
enemizerBossOption.pack(side=LEFT)
|
||||
|
||||
enemizerDamageFrame = Frame(enemizerFrame)
|
||||
enemizerDamageFrame.grid(row=2, column=1)
|
||||
enemizerDamageFrame.grid(row=1, column=2)
|
||||
enemizerDamageLabel = Label(enemizerDamageFrame, text='Enemy damage')
|
||||
enemizerDamageLabel.pack(side=LEFT)
|
||||
enemizerDamageVar = StringVar()
|
||||
|
@ -348,7 +375,7 @@ def guiMain(args=None):
|
|||
enemizerDamageOption.pack(side=LEFT)
|
||||
|
||||
enemizerHealthFrame = Frame(enemizerFrame)
|
||||
enemizerHealthFrame.grid(row=2, column=2)
|
||||
enemizerHealthFrame.grid(row=1, column=3)
|
||||
enemizerHealthLabel = Label(enemizerHealthFrame, text='Enemy health')
|
||||
enemizerHealthLabel.pack(side=LEFT)
|
||||
enemizerHealthVar = StringVar()
|
||||
|
@ -403,14 +430,15 @@ def guiMain(args=None):
|
|||
guiargs.retro = bool(retroVar.get())
|
||||
guiargs.quickswap = bool(quickSwapVar.get())
|
||||
guiargs.disablemusic = bool(disableMusicVar.get())
|
||||
guiargs.ow_palettes = owPalettesVar.get()
|
||||
guiargs.uw_palettes = uwPalettesVar.get()
|
||||
guiargs.shuffleganon = bool(shuffleGanonVar.get())
|
||||
guiargs.hints = bool(hintsVar.get())
|
||||
guiargs.enemizercli = enemizerCLIpathVar.get()
|
||||
guiargs.shufflebosses = enemizerBossVar.get()
|
||||
guiargs.shuffleenemies = 'chaos' if bool(enemyShuffleVar.get()) else 'none'
|
||||
guiargs.shuffleenemies = enemyShuffleVar.get()
|
||||
guiargs.enemy_health = enemizerHealthVar.get()
|
||||
guiargs.enemy_damage = enemizerDamageVar.get()
|
||||
guiargs.shufflepalette = bool(paletteShuffleVar.get())
|
||||
guiargs.shufflepots = bool(potShuffleVar.get())
|
||||
guiargs.custom = bool(customVar.get())
|
||||
guiargs.customitemarray = [int(bowVar.get()), int(silverarrowVar.get()), int(boomerangVar.get()), int(magicboomerangVar.get()), int(hookshotVar.get()), int(mushroomVar.get()), int(magicpowderVar.get()), int(firerodVar.get()),
|
||||
|
@ -442,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)
|
||||
|
||||
|
@ -534,18 +558,23 @@ def guiMain(args=None):
|
|||
fastMenuLabel2 = Label(fastMenuFrame2, text='Menu speed')
|
||||
fastMenuLabel2.pack(side=LEFT)
|
||||
|
||||
namesFrame2 = Frame(drowDownFrame2)
|
||||
namesLabel2 = Label(namesFrame2, text='Player names')
|
||||
namesVar2 = StringVar()
|
||||
namesEntry2 = Entry(namesFrame2, textvariable=namesVar2)
|
||||
owPalettesFrame2 = Frame(drowDownFrame2)
|
||||
owPalettesOptionMenu2 = OptionMenu(owPalettesFrame2, owPalettesVar, 'default', 'random', 'blackout')
|
||||
owPalettesOptionMenu2.pack(side=RIGHT)
|
||||
owPalettesLabel2 = Label(owPalettesFrame2, text='Overworld palettes')
|
||||
owPalettesLabel2.pack(side=LEFT)
|
||||
|
||||
namesLabel2.pack(side=LEFT)
|
||||
namesEntry2.pack(side=LEFT)
|
||||
uwPalettesFrame2 = Frame(drowDownFrame2)
|
||||
uwPalettesOptionMenu2 = OptionMenu(uwPalettesFrame2, uwPalettesVar, 'default', 'random', 'blackout')
|
||||
uwPalettesOptionMenu2.pack(side=RIGHT)
|
||||
uwPalettesLabel2 = Label(uwPalettesFrame2, text='Dungeon palettes')
|
||||
uwPalettesLabel2.pack(side=LEFT)
|
||||
|
||||
heartbeepFrame2.pack(expand=True, anchor=E)
|
||||
heartcolorFrame2.pack(expand=True, anchor=E)
|
||||
fastMenuFrame2.pack(expand=True, anchor=E)
|
||||
namesFrame2.pack(expand=True, anchor=E)
|
||||
owPalettesFrame2.pack(expand=True, anchor=E)
|
||||
uwPalettesFrame2.pack(expand=True, anchor=E)
|
||||
|
||||
bottomFrame2 = Frame(topFrame2)
|
||||
|
||||
|
@ -554,22 +583,20 @@ def guiMain(args=None):
|
|||
guiargs.heartbeep = heartbeepVar.get()
|
||||
guiargs.heartcolor = heartcolorVar.get()
|
||||
guiargs.fastmenu = fastMenuVar.get()
|
||||
guiargs.ow_palettes = owPalettesVar.get()
|
||||
guiargs.uw_palettes = uwPalettesVar.get()
|
||||
guiargs.quickswap = bool(quickSwapVar.get())
|
||||
guiargs.disablemusic = bool(disableMusicVar.get())
|
||||
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)
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import collections
|
||||
from BaseClasses import Region, Location, Entrance, RegionType, Shop, ShopType
|
||||
from BaseClasses import RegionType
|
||||
from Regions import create_lw_region, create_dw_region, create_cave_region, create_dungeon_region
|
||||
|
||||
|
||||
def create_inverted_regions(world, player):
|
||||
|
@ -302,47 +303,8 @@ def create_inverted_regions(world, player):
|
|||
create_cave_region(player, 'The Sky', 'A Dark Sky', None, ['DDM Landing','NEDW Landing', 'WDW Landing', 'SDW Landing', 'EDW Landing', 'DD Landing', 'DLHL Landing'])
|
||||
]
|
||||
|
||||
for region_name, (room_id, shopkeeper, replaceable) in shop_table.items():
|
||||
region = world.get_region(region_name, player)
|
||||
shop = Shop(region, room_id, ShopType.Shop, shopkeeper, replaceable)
|
||||
region.shop = shop
|
||||
world.shops.append(shop)
|
||||
for index, (item, price) in enumerate(default_shop_contents[region_name]):
|
||||
shop.add_inventory(index, item, price)
|
||||
|
||||
region = world.get_region('Capacity Upgrade', player)
|
||||
shop = Shop(region, 0x0115, ShopType.UpgradeShop, 0x04, True)
|
||||
region.shop = shop
|
||||
world.shops.append(shop)
|
||||
shop.add_inventory(0, 'Bomb Upgrade (+5)', 100, 7)
|
||||
shop.add_inventory(1, 'Arrow Upgrade (+5)', 100, 7)
|
||||
world.initialize_regions()
|
||||
|
||||
def create_lw_region(player, name, locations=None, exits=None):
|
||||
return _create_region(player, name, RegionType.LightWorld, 'Light World', locations, exits)
|
||||
|
||||
def create_dw_region(player, name, locations=None, exits=None):
|
||||
return _create_region(player, name, RegionType.DarkWorld, 'Dark World', locations, exits)
|
||||
|
||||
def create_cave_region(player, name, hint='Hyrule', locations=None, exits=None):
|
||||
return _create_region(player, name, RegionType.Cave, hint, locations, exits)
|
||||
|
||||
def create_dungeon_region(player, name, hint='Hyrule', locations=None, exits=None):
|
||||
return _create_region(player, name, RegionType.Dungeon, hint, locations, exits)
|
||||
|
||||
def _create_region(player, name, type, hint='Hyrule', locations=None, exits=None):
|
||||
ret = Region(name, type, hint, player)
|
||||
if locations is None:
|
||||
locations = []
|
||||
if exits is None:
|
||||
exits = []
|
||||
|
||||
for exit in exits:
|
||||
ret.exits.append(Entrance(player, exit, ret))
|
||||
for location in locations:
|
||||
address, player_address, crystal, hint_text = location_table[location]
|
||||
ret.locations.append(Location(player, location, address, crystal, hint_text, ret, player_address))
|
||||
return ret
|
||||
|
||||
def mark_dark_world_regions(world, player):
|
||||
# cross world caves may have some sections marked as both in_light_world, and in_dark_work.
|
||||
|
@ -372,270 +334,3 @@ def mark_dark_world_regions(world, player):
|
|||
if exit.connected_region not in seen:
|
||||
seen.add(exit.connected_region)
|
||||
queue.append(exit.connected_region)
|
||||
|
||||
# (room_id, shopkeeper, replaceable)
|
||||
shop_table = {
|
||||
'Cave Shop (Dark Death Mountain)': (0x0112, 0xC1, True),
|
||||
'Red Shield Shop': (0x0110, 0xC1, True),
|
||||
'Dark Lake Hylia Shop': (0x010F, 0xC1, False),
|
||||
'Dark World Lumberjack Shop': (0x010F, 0xC1, True),
|
||||
'Village of Outcasts Shop': (0x010F, 0xC1, True),
|
||||
'Dark World Potion Shop': (0x010F, 0xC1, True),
|
||||
'Light World Death Mountain Shop': (0x00FF, 0xA0, True),
|
||||
'Kakariko Shop': (0x011F, 0xA0, True),
|
||||
'Cave Shop (Lake Hylia)': (0x0112, 0xA0, True),
|
||||
'Potion Shop': (0x0109, 0xFF, False),
|
||||
# Bomb Shop not currently modeled as a shop, due to special nature of items
|
||||
}
|
||||
# region, [item]
|
||||
# slot, item, price, max=0, replacement=None, replacement_price=0
|
||||
# item = (item, price)
|
||||
|
||||
_basic_shop_defaults = [('Red Potion', 150), ('Small Heart', 10), ('Bombs (10)', 50)]
|
||||
_dark_world_shop_defaults = [('Red Potion', 150), ('Blue Shield', 50), ('Bombs (10)', 50)]
|
||||
default_shop_contents = {
|
||||
'Cave Shop (Dark Death Mountain)': _basic_shop_defaults,
|
||||
'Red Shield Shop': [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)],
|
||||
'Dark Lake Hylia Shop': [('Blue Potion', 160), ('Blue Shield', 50), ('Bombs (10)', 50)],
|
||||
'Dark World Lumberjack Shop': _dark_world_shop_defaults,
|
||||
'Village of Outcasts Shop': _dark_world_shop_defaults,
|
||||
'Dark World Potion Shop': _dark_world_shop_defaults,
|
||||
'Light World Death Mountain Shop': _basic_shop_defaults,
|
||||
'Kakariko Shop': _basic_shop_defaults,
|
||||
'Cave Shop (Lake Hylia)': _basic_shop_defaults,
|
||||
'Potion Shop': [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)],
|
||||
}
|
||||
|
||||
location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'),
|
||||
'Bottle Merchant': (0x2eb18, 0x186339, False, 'with a merchant'),
|
||||
'Flute Spot': (0x18014a, 0x18633d, False, 'underground'),
|
||||
'Sunken Treasure': (0x180145, 0x186354, False, 'underwater'),
|
||||
'Purple Chest': (0x33d68, 0x186359, False, 'from a box'),
|
||||
"Blind's Hideout - Top": (0xeb0f, 0x1862e3, False, 'in a basement'),
|
||||
"Blind's Hideout - Left": (0xeb12, 0x1862e6, False, 'in a basement'),
|
||||
"Blind's Hideout - Right": (0xeb15, 0x1862e9, False, 'in a basement'),
|
||||
"Blind's Hideout - Far Left": (0xeb18, 0x1862ec, False, 'in a basement'),
|
||||
"Blind's Hideout - Far Right": (0xeb1b, 0x1862ef, False, 'in a basement'),
|
||||
"Link's Uncle": (0x2df45, 0x18635f, False, 'with your uncle'),
|
||||
'Secret Passage': (0xe971, 0x186145, False, 'near your uncle'),
|
||||
'King Zora': (0xee1c3, 0x186360, False, 'at a high price'),
|
||||
"Zora's Ledge": (0x180149, 0x186358, False, 'near Zora'),
|
||||
'Waterfall Fairy - Left': (0xe9b0, 0x186184, False, 'near a fairy'),
|
||||
'Waterfall Fairy - Right': (0xe9d1, 0x1861a5, False, 'near a fairy'),
|
||||
"King's Tomb": (0xe97a, 0x18614e, False, 'alone in a cave'),
|
||||
'Floodgate Chest': (0xe98c, 0x186160, False, 'in the dam'),
|
||||
"Link's House": (0xe9bc, 0x186190, False, 'in your home'),
|
||||
'Kakariko Tavern': (0xe9ce, 0x1861a2, False, 'in the bar'),
|
||||
'Chicken House': (0xe9e9, 0x1861bd, False, 'near poultry'),
|
||||
"Aginah's Cave": (0xe9f2, 0x1861c6, False, 'with Aginah'),
|
||||
"Sahasrahla's Hut - Left": (0xea82, 0x186256, False, 'near the elder'),
|
||||
"Sahasrahla's Hut - Middle": (0xea85, 0x186259, False, 'near the elder'),
|
||||
"Sahasrahla's Hut - Right": (0xea88, 0x18625c, False, 'near the elder'),
|
||||
'Sahasrahla': (0x2f1fc, 0x186365, False, 'with the elder'),
|
||||
'Kakariko Well - Top': (0xea8e, 0x186262, False, 'in a well'),
|
||||
'Kakariko Well - Left': (0xea91, 0x186265, False, 'in a well'),
|
||||
'Kakariko Well - Middle': (0xea94, 0x186268, False, 'in a well'),
|
||||
'Kakariko Well - Right': (0xea97, 0x18626b, False, 'in a well'),
|
||||
'Kakariko Well - Bottom': (0xea9a, 0x18626e, False, 'in a well'),
|
||||
'Blacksmith': (0x18002a, 0x186366, False, 'with the smith'),
|
||||
'Magic Bat': (0x180015, 0x18635e, False, 'with the bat'),
|
||||
'Sick Kid': (0x339cf, 0x186367, False, 'with the sick'),
|
||||
'Hobo': (0x33e7d, 0x186368, False, 'with the hobo'),
|
||||
'Lost Woods Hideout': (0x180000, 0x186348, False, 'near a thief'),
|
||||
'Lumberjack Tree': (0x180001, 0x186349, False, 'in a hole'),
|
||||
'Cave 45': (0x180003, 0x18634b, False, 'alone in a cave'),
|
||||
'Graveyard Cave': (0x180004, 0x18634c, False, 'alone in a cave'),
|
||||
'Checkerboard Cave': (0x180005, 0x18634d, False, 'alone in a cave'),
|
||||
'Mini Moldorm Cave - Far Left': (0xeb42, 0x186316, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Left': (0xeb45, 0x186319, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Right': (0xeb48, 0x18631c, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Far Right': (0xeb4b, 0x18631f, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Generous Guy': (0x180010, 0x18635a, False, 'near Moldorms'),
|
||||
'Ice Rod Cave': (0xeb4e, 0x186322, False, 'in a frozen cave'),
|
||||
'Bonk Rock Cave': (0xeb3f, 0x186313, False, 'alone in a cave'),
|
||||
'Library': (0x180012, 0x18635c, False, 'near books'),
|
||||
'Potion Shop': (0x180014, 0x18635d, False, 'near potions'),
|
||||
'Lake Hylia Island': (0x180144, 0x186353, False, 'on an island'),
|
||||
'Maze Race': (0x180142, 0x186351, False, 'at the race'),
|
||||
'Desert Ledge': (0x180143, 0x186352, False, 'in the desert'),
|
||||
'Desert Palace - Big Chest': (0xe98f, 0x186163, False, 'in Desert Palace'),
|
||||
'Desert Palace - Torch': (0x180160, 0x186362, False, 'in Desert Palace'),
|
||||
'Desert Palace - Map Chest': (0xe9b6, 0x18618a, False, 'in Desert Palace'),
|
||||
'Desert Palace - Compass Chest': (0xe9cb, 0x18619f, False, 'in Desert Palace'),
|
||||
'Desert Palace - Big Key Chest': (0xe9c2, 0x186196, False, 'in Desert Palace'),
|
||||
'Desert Palace - Boss': (0x180151, 0x18633f, False, 'with Lanmolas'),
|
||||
'Eastern Palace - Compass Chest': (0xe977, 0x18614b, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Big Chest': (0xe97d, 0x186151, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Cannonball Chest': (0xe9b3, 0x186187, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Big Key Chest': (0xe9b9, 0x18618d, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Map Chest': (0xe9f5, 0x1861c9, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Boss': (0x180150, 0x18633e, False, 'with the Armos'),
|
||||
'Master Sword Pedestal': (0x289b0, 0x186369, False, 'at the pedestal'),
|
||||
'Hyrule Castle - Boomerang Chest': (0xe974, 0x186148, False, 'in Hyrule Castle'),
|
||||
'Hyrule Castle - Map Chest': (0xeb0c, 0x1862e0, False, 'in Hyrule Castle'),
|
||||
"Hyrule Castle - Zelda's Chest": (0xeb09, 0x1862dd, False, 'in Hyrule Castle'),
|
||||
'Sewers - Dark Cross': (0xe96e, 0x186142, False, 'in the sewers'),
|
||||
'Sewers - Secret Room - Left': (0xeb5d, 0x186331, False, 'in the sewers'),
|
||||
'Sewers - Secret Room - Middle': (0xeb60, 0x186334, False, 'in the sewers'),
|
||||
'Sewers - Secret Room - Right': (0xeb63, 0x186337, False, 'in the sewers'),
|
||||
'Sanctuary': (0xea79, 0x18624d, False, 'in Sanctuary'),
|
||||
'Castle Tower - Room 03': (0xeab5, 0x186289, False, 'in Castle Tower'),
|
||||
'Castle Tower - Dark Maze': (0xeab2, 0x186286, False, 'in Castle Tower'),
|
||||
'Old Man': (0xf69fa, 0x186364, False, 'with the old man'),
|
||||
'Spectacle Rock Cave': (0x180002, 0x18634a, False, 'alone in a cave'),
|
||||
'Paradox Cave Lower - Far Left': (0xeb2a, 0x1862fe, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Left': (0xeb2d, 0x186301, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Right': (0xeb30, 0x186304, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Far Right': (0xeb33, 0x186307, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Middle': (0xeb36, 0x18630a, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Upper - Left': (0xeb39, 0x18630d, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Upper - Right': (0xeb3c, 0x186310, False, 'in a cave with seven chests'),
|
||||
'Spiral Cave': (0xe9bf, 0x186193, False, 'in spiral cave'),
|
||||
'Ether Tablet': (0x180016, 0x18633b, False, 'at a monolith'),
|
||||
'Spectacle Rock': (0x180140, 0x18634f, False, 'atop a rock'),
|
||||
'Tower of Hera - Basement Cage': (0x180162, 0x18633a, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Map Chest': (0xe9ad, 0x186181, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Big Key Chest': (0xe9e6, 0x1861ba, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Compass Chest': (0xe9fb, 0x1861cf, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Big Chest': (0xe9f8, 0x1861cc, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Boss': (0x180152, 0x186340, False, 'with Moldorm'),
|
||||
'Pyramid': (0x180147, 0x186356, False, 'on the pyramid'),
|
||||
'Catfish': (0xee185, 0x186361, False, 'with a catfish'),
|
||||
'Stumpy': (0x330c7, 0x18636a, False, 'with tree boy'),
|
||||
'Digging Game': (0x180148, 0x186357, False, 'underground'),
|
||||
'Bombos Tablet': (0x180017, 0x18633c, False, 'at a monolith'),
|
||||
'Hype Cave - Top': (0xeb1e, 0x1862f2, False, 'near a bat-like man'),
|
||||
'Hype Cave - Middle Right': (0xeb21, 0x1862f5, False, 'near a bat-like man'),
|
||||
'Hype Cave - Middle Left': (0xeb24, 0x1862f8, False, 'near a bat-like man'),
|
||||
'Hype Cave - Bottom': (0xeb27, 0x1862fb, False, 'near a bat-like man'),
|
||||
'Hype Cave - Generous Guy': (0x180011, 0x18635b, False, 'with a bat-like man'),
|
||||
'Peg Cave': (0x180006, 0x18634e, False, 'alone in a cave'),
|
||||
'Pyramid Fairy - Left': (0xe980, 0x186154, False, 'near a fairy'),
|
||||
'Pyramid Fairy - Right': (0xe983, 0x186157, False, 'near a fairy'),
|
||||
'Brewery': (0xe9ec, 0x1861c0, False, 'alone in a home'),
|
||||
'C-Shaped House': (0xe9ef, 0x1861c3, False, 'alone in a home'),
|
||||
'Chest Game': (0xeda8, 0x18636b, False, 'as a prize'),
|
||||
'Bumper Cave Ledge': (0x180146, 0x186355, False, 'on a ledge'),
|
||||
'Mire Shed - Left': (0xea73, 0x186247, False, 'near sparks'),
|
||||
'Mire Shed - Right': (0xea76, 0x18624a, False, 'near sparks'),
|
||||
'Superbunny Cave - Top': (0xea7c, 0x186250, False, 'in a connection'),
|
||||
'Superbunny Cave - Bottom': (0xea7f, 0x186253, False, 'in a connection'),
|
||||
'Spike Cave': (0xea8b, 0x18625f, False, 'beyond spikes'),
|
||||
'Hookshot Cave - Top Right': (0xeb51, 0x186325, False, 'across pits'),
|
||||
'Hookshot Cave - Top Left': (0xeb54, 0x186328, False, 'across pits'),
|
||||
'Hookshot Cave - Bottom Right': (0xeb5a, 0x18632e, False, 'across pits'),
|
||||
'Hookshot Cave - Bottom Left': (0xeb57, 0x18632b, False, 'across pits'),
|
||||
'Floating Island': (0x180141, 0x186350, False, 'on an island'),
|
||||
'Mimic Cave': (0xe9c5, 0x186199, False, 'in a cave of mimicry'),
|
||||
'Swamp Palace - Entrance': (0xea9d, 0x186271, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Map Chest': (0xe986, 0x18615a, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Big Chest': (0xe989, 0x18615d, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Compass Chest': (0xeaa0, 0x186274, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Big Key Chest': (0xeaa6, 0x18627a, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - West Chest': (0xeaa3, 0x186277, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Flooded Room - Left': (0xeaa9, 0x18627d, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Flooded Room - Right': (0xeaac, 0x186280, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Waterfall Room': (0xeaaf, 0x186283, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Boss': (0x180154, 0x186342, False, 'with Arrghus'),
|
||||
"Thieves' Town - Big Key Chest": (0xea04, 0x1861d8, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Map Chest": (0xea01, 0x1861d5, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Compass Chest": (0xea07, 0x1861db, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Ambush Chest": (0xea0a, 0x1861de, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Attic": (0xea0d, 0x1861e1, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Big Chest": (0xea10, 0x1861e4, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Blind's Cell": (0xea13, 0x1861e7, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Boss": (0x180156, 0x186344, False, 'with Blind'),
|
||||
'Skull Woods - Compass Chest': (0xe992, 0x186166, False, 'in Skull Woods'),
|
||||
'Skull Woods - Map Chest': (0xe99b, 0x18616f, False, 'in Skull Woods'),
|
||||
'Skull Woods - Big Chest': (0xe998, 0x18616c, False, 'in Skull Woods'),
|
||||
'Skull Woods - Pot Prison': (0xe9a1, 0x186175, False, 'in Skull Woods'),
|
||||
'Skull Woods - Pinball Room': (0xe9c8, 0x18619c, False, 'in Skull Woods'),
|
||||
'Skull Woods - Big Key Chest': (0xe99e, 0x186172, False, 'in Skull Woods'),
|
||||
'Skull Woods - Bridge Room': (0xe9fe, 0x1861d2, False, 'near Mothula'),
|
||||
'Skull Woods - Boss': (0x180155, 0x186343, False, 'with Mothula'),
|
||||
'Ice Palace - Compass Chest': (0xe9d4, 0x1861a8, False, 'in Ice Palace'),
|
||||
'Ice Palace - Freezor Chest': (0xe995, 0x186169, False, 'in Ice Palace'),
|
||||
'Ice Palace - Big Chest': (0xe9aa, 0x18617e, False, 'in Ice Palace'),
|
||||
'Ice Palace - Iced T Room': (0xe9e3, 0x1861b7, False, 'in Ice Palace'),
|
||||
'Ice Palace - Spike Room': (0xe9e0, 0x1861b4, False, 'in Ice Palace'),
|
||||
'Ice Palace - Big Key Chest': (0xe9a4, 0x186178, False, 'in Ice Palace'),
|
||||
'Ice Palace - Map Chest': (0xe9dd, 0x1861b1, False, 'in Ice Palace'),
|
||||
'Ice Palace - Boss': (0x180157, 0x186345, False, 'with Kholdstare'),
|
||||
'Misery Mire - Big Chest': (0xea67, 0x18623b, False, 'in Misery Mire'),
|
||||
'Misery Mire - Map Chest': (0xea6a, 0x18623e, False, 'in Misery Mire'),
|
||||
'Misery Mire - Main Lobby': (0xea5e, 0x186232, False, 'in Misery Mire'),
|
||||
'Misery Mire - Bridge Chest': (0xea61, 0x186235, False, 'in Misery Mire'),
|
||||
'Misery Mire - Spike Chest': (0xe9da, 0x1861ae, False, 'in Misery Mire'),
|
||||
'Misery Mire - Compass Chest': (0xea64, 0x186238, False, 'in Misery Mire'),
|
||||
'Misery Mire - Big Key Chest': (0xea6d, 0x186241, False, 'in Misery Mire'),
|
||||
'Misery Mire - Boss': (0x180158, 0x186346, False, 'with Vitreous'),
|
||||
'Turtle Rock - Compass Chest': (0xea22, 0x1861f6, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Roller Room - Left': (0xea1c, 0x1861f0, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Roller Room - Right': (0xea1f, 0x1861f3, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Chain Chomps': (0xea16, 0x1861ea, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Big Key Chest': (0xea25, 0x1861f9, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Big Chest': (0xea19, 0x1861ed, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Crystaroller Room': (0xea34, 0x186208, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Bottom Left': (0xea31, 0x186205, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Bottom Right': (0xea2e, 0x186202, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Top Left': (0xea2b, 0x1861ff, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Top Right': (0xea28, 0x1861fc, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Boss': (0x180159, 0x186347, False, 'with Trinexx'),
|
||||
'Palace of Darkness - Shooter Room': (0xea5b, 0x18622f, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - The Arena - Bridge': (0xea3d, 0x186211, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Stalfos Basement': (0xea49, 0x18621d, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Big Key Chest': (0xea37, 0x18620b, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - The Arena - Ledge': (0xea3a, 0x18620e, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Map Chest': (0xea52, 0x186226, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Compass Chest': (0xea43, 0x186217, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Basement - Left': (0xea4c, 0x186220, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Basement - Right': (0xea4f, 0x186223, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Maze - Top': (0xea55, 0x186229, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Maze - Bottom': (0xea58, 0x18622c, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Big Chest': (0xea40, 0x186214, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Harmless Hellway': (0xea46, 0x18621a, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Boss': (0x180153, 0x186341, False, 'with Helmasaur King'),
|
||||
"Ganons Tower - Bob's Torch": (0x180161, 0x186363, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Hope Room - Left': (0xead9, 0x1862ad, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Hope Room - Right': (0xeadc, 0x1862b0, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Tile Room': (0xeae2, 0x1862b6, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Compass Room - Top Left': (0xeae5, 0x1862b9, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Compass Room - Top Right': (0xeae8, 0x1862bc, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Compass Room - Bottom Left': (0xeaeb, 0x1862bf, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Compass Room - Bottom Right': (0xeaee, 0x1862c2, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - DMs Room - Top Left': (0xeab8, 0x18628c, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - DMs Room - Top Right': (0xeabb, 0x18628f, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - DMs Room - Bottom Left': (0xeabe, 0x186292, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - DMs Room - Bottom Right': (0xeac1, 0x186295, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Map Chest': (0xead3, 0x1862a7, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Firesnake Room': (0xead0, 0x1862a4, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Randomizer Room - Top Left': (0xeac4, 0x186298, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Randomizer Room - Top Right': (0xeac7, 0x18629b, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Randomizer Room - Bottom Left': (0xeaca, 0x18629e, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Randomizer Room - Bottom Right': (0xeacd, 0x1862a1, False, "in Ganon's Tower"),
|
||||
"Ganons Tower - Bob's Chest": (0xeadf, 0x1862b3, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Big Chest': (0xead6, 0x1862aa, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Big Key Room - Left': (0xeaf4, 0x1862c8, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Big Key Room - Right': (0xeaf7, 0x1862cb, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Big Key Chest': (0xeaf1, 0x1862c5, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Mini Helmasaur Room - Left': (0xeafd, 0x1862d1, False, "atop Ganon's Tower"),
|
||||
'Ganons Tower - Mini Helmasaur Room - Right': (0xeb00, 0x1862d4, False, "atop Ganon's Tower"),
|
||||
'Ganons Tower - Pre-Moldorm Chest': (0xeb03, 0x1862d7, False, "atop Ganon's Tower"),
|
||||
'Ganons Tower - Validation Chest': (0xeb06, 0x1862da, False, "atop Ganon's Tower"),
|
||||
'Ganon': (None, None, False, 'from me'),
|
||||
'Agahnim 1': (None, None, False, 'from Ganon\'s wizardry form'),
|
||||
'Agahnim 2': (None, None, False, 'from Ganon\'s wizardry form'),
|
||||
'Floodgate': (None, None, False, None),
|
||||
'Frog': (None, None, False, None),
|
||||
'Missing Smith': (None, None, False, None),
|
||||
'Dark Blacksmith Ruins': (None, None, False, None),
|
||||
'Eastern Palace - Prize': ([0x1209D, 0x53EF8, 0x53EF9, 0x180052, 0x18007C, 0xC6FE], None, True, 'Eastern Palace'),
|
||||
'Desert Palace - Prize': ([0x1209E, 0x53F1C, 0x53F1D, 0x180053, 0x180078, 0xC6FF], None, True, 'Desert Palace'),
|
||||
'Tower of Hera - Prize': ([0x120A5, 0x53F0A, 0x53F0B, 0x18005A, 0x18007A, 0xC706], None, True, 'Tower of Hera'),
|
||||
'Palace of Darkness - Prize': ([0x120A1, 0x53F00, 0x53F01, 0x180056, 0x18007D, 0xC702], None, True, 'Palace of Darkness'),
|
||||
'Swamp Palace - Prize': ([0x120A0, 0x53F6C, 0x53F6D, 0x180055, 0x180071, 0xC701], None, True, 'Swamp Palace'),
|
||||
'Thieves\' Town - Prize': ([0x120A6, 0x53F36, 0x53F37, 0x18005B, 0x180077, 0xC707], None, True, 'Thieves\' Town'),
|
||||
'Skull Woods - Prize': ([0x120A3, 0x53F12, 0x53F13, 0x180058, 0x18007B, 0xC704], None, True, 'Skull Woods'),
|
||||
'Ice Palace - Prize': ([0x120A4, 0x53F5A, 0x53F5B, 0x180059, 0x180073, 0xC705], None, True, 'Ice Palace'),
|
||||
'Misery Mire - Prize': ([0x120A2, 0x53F48, 0x53F49, 0x180057, 0x180075, 0xC703], None, True, 'Misery Mire'),
|
||||
'Turtle Rock - Prize': ([0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], None, True, 'Turtle Rock')}
|
||||
|
|
83
ItemList.py
83
ItemList.py
|
@ -51,8 +51,8 @@ difficulties = {
|
|||
progressivearmor = ['Progressive Armor'] * 2,
|
||||
basicarmor = ['Blue Mail', 'Red Mail'],
|
||||
swordless = ['Rupees (20)'] * 4,
|
||||
progressivesword = ['Progressive Sword'] * 3,
|
||||
basicsword = ['Master Sword', 'Tempered Sword', 'Golden Sword'],
|
||||
progressivesword = ['Progressive Sword'] * 4,
|
||||
basicsword = ['Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'],
|
||||
basicbow = ['Bow', 'Silver Arrows'],
|
||||
timedohko = ['Green Clock'] * 25,
|
||||
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
|
||||
|
@ -78,8 +78,8 @@ difficulties = {
|
|||
progressivearmor = ['Progressive Armor'] * 2,
|
||||
basicarmor = ['Progressive Armor'] * 2, # neither will count
|
||||
swordless = ['Rupees (20)'] * 4,
|
||||
progressivesword = ['Progressive Sword'] * 3,
|
||||
basicsword = ['Master Sword', 'Master Sword', 'Tempered Sword'],
|
||||
progressivesword = ['Progressive Sword'] * 4,
|
||||
basicsword = ['Fighter Sword', 'Master Sword', 'Master Sword', 'Tempered Sword'],
|
||||
basicbow = ['Bow'] * 2,
|
||||
timedohko = ['Green Clock'] * 25,
|
||||
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
|
||||
|
@ -105,8 +105,8 @@ difficulties = {
|
|||
progressivearmor = ['Progressive Armor'] * 2, # neither will count
|
||||
basicarmor = ['Progressive Armor'] * 2, # neither will count
|
||||
swordless = ['Rupees (20)'] * 4,
|
||||
progressivesword = ['Progressive Sword'] * 3,
|
||||
basicsword = ['Fighter Sword', 'Master Sword', 'Master Sword'],
|
||||
progressivesword = ['Progressive Sword'] * 4,
|
||||
basicsword = ['Fighter Sword', 'Fighter Sword', 'Master Sword', 'Master Sword'],
|
||||
basicbow = ['Bow'] * 2,
|
||||
timedohko = ['Green Clock'] * 20 + ['Red Clock'] * 5,
|
||||
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
|
||||
|
@ -269,7 +269,7 @@ take_any_locations = [
|
|||
'Bonk Fairy (Dark)', 'Lake Hylia Healer Fairy', 'Swamp Healer Fairy', 'Desert Healer Fairy',
|
||||
'Dark Lake Hylia Healer Fairy', 'Dark Lake Hylia Ledge Healer Fairy', 'Dark Desert Healer Fairy',
|
||||
'Dark Death Mountain Healer Fairy', 'Long Fairy Cave', 'Good Bee Cave', '20 Rupee Cave',
|
||||
'Kakariko Gamble Game', 'Capacity Upgrade', '50 Rupee Cave', 'Lost Woods Gamble', 'Hookshot Fairy',
|
||||
'Kakariko Gamble Game', '50 Rupee Cave', 'Lost Woods Gamble', 'Hookshot Fairy',
|
||||
'Palace of Darkness Hint', 'East Dark World Hint', 'Archery Game', 'Dark Lake Hylia Ledge Hint',
|
||||
'Dark Lake Hylia Ledge Spike Cave', 'Fortune Teller (Dark)', 'Dark Sanctuary Hint', 'Dark Desert Hint']
|
||||
|
||||
|
@ -287,9 +287,8 @@ def set_up_take_anys(world, player):
|
|||
entrance = world.get_region(reg, player).entrances[0]
|
||||
connect_entrance(world, entrance, old_man_take_any, player)
|
||||
entrance.target = 0x58
|
||||
old_man_take_any.shop = Shop(old_man_take_any, 0x0112, ShopType.TakeAny, 0xE2, True)
|
||||
old_man_take_any.shop = Shop(old_man_take_any, 0x0112, ShopType.TakeAny, 0xE2, True, True)
|
||||
world.shops.append(old_man_take_any.shop)
|
||||
old_man_take_any.shop.active = True
|
||||
|
||||
swords = [item for item in world.itempool if item.type == 'Sword' and item.player == player]
|
||||
if swords:
|
||||
|
@ -310,9 +309,8 @@ def set_up_take_anys(world, player):
|
|||
entrance = world.get_region(reg, player).entrances[0]
|
||||
connect_entrance(world, entrance, take_any, player)
|
||||
entrance.target = target
|
||||
take_any.shop = Shop(take_any, room_id, ShopType.TakeAny, 0xE3, True)
|
||||
take_any.shop = Shop(take_any, room_id, ShopType.TakeAny, 0xE3, True, True)
|
||||
world.shops.append(take_any.shop)
|
||||
take_any.shop.active = True
|
||||
take_any.shop.add_inventory(0, 'Blue Potion', 0, 0)
|
||||
take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0)
|
||||
|
||||
|
@ -365,26 +363,19 @@ def fill_prizes(world, attempts=15):
|
|||
|
||||
|
||||
def set_up_shops(world, player):
|
||||
# Changes to basic Shops
|
||||
# TODO: move hard+ mode changes for sheilds here, utilizing the new shops
|
||||
|
||||
for shop in world.shops:
|
||||
shop.active = True
|
||||
|
||||
if world.retro[player]:
|
||||
rss = world.get_region('Red Shield Shop', player).shop
|
||||
rss.active = True
|
||||
rss.add_inventory(2, 'Single Arrow', 80)
|
||||
|
||||
# Randomized changes to Shops
|
||||
if world.retro[player]:
|
||||
for shop in random.sample([s for s in world.shops if s.replaceable and s.region.player == player], 5):
|
||||
shop.active = True
|
||||
if not rss.locked:
|
||||
rss.add_inventory(2, 'Single Arrow', 80)
|
||||
for shop in random.sample([s for s in world.shops if s.custom and not s.locked and s.region.player == player], 5):
|
||||
shop.locked = True
|
||||
shop.add_inventory(0, 'Single Arrow', 80)
|
||||
shop.add_inventory(1, 'Small Key (Universal)', 100)
|
||||
shop.add_inventory(2, 'Bombs (10)', 50)
|
||||
rss.locked = True
|
||||
|
||||
#special shop types
|
||||
|
||||
def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, retro, logic):
|
||||
pool = []
|
||||
|
@ -449,34 +440,17 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r
|
|||
else:
|
||||
pool.extend(diff.basicarmor)
|
||||
|
||||
if swords != 'swordless':
|
||||
if want_progressives():
|
||||
pool.extend(['Progressive Bow'] * 2)
|
||||
else:
|
||||
pool.extend(diff.basicbow)
|
||||
if want_progressives():
|
||||
pool.extend(['Progressive Bow'] * 2)
|
||||
elif swords != 'swordless':
|
||||
pool.extend(diff.basicbow)
|
||||
else:
|
||||
pool.extend(['Bow', 'Silver Arrows'])
|
||||
|
||||
if swords == 'swordless':
|
||||
pool.extend(diff.swordless)
|
||||
if want_progressives():
|
||||
pool.extend(['Progressive Bow'] * 2)
|
||||
else:
|
||||
pool.extend(['Bow', 'Silver Arrows'])
|
||||
elif swords == 'assured':
|
||||
precollected_items.append('Fighter Sword')
|
||||
if want_progressives():
|
||||
pool.extend(diff.progressivesword)
|
||||
pool.extend(['Rupees (100)'])
|
||||
else:
|
||||
pool.extend(diff.basicsword)
|
||||
pool.extend(['Rupees (100)'])
|
||||
elif swords == 'vanilla':
|
||||
swords_to_use = []
|
||||
if want_progressives():
|
||||
swords_to_use.extend(diff.progressivesword)
|
||||
swords_to_use.extend(['Progressive Sword'])
|
||||
else:
|
||||
swords_to_use.extend(diff.basicsword)
|
||||
swords_to_use.extend(['Fighter Sword'])
|
||||
swords_to_use = diff.progressivesword.copy() if want_progressives() else diff.basicsword.copy()
|
||||
random.shuffle(swords_to_use)
|
||||
|
||||
place_item('Link\'s Uncle', swords_to_use.pop())
|
||||
|
@ -487,12 +461,15 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r
|
|||
else:
|
||||
place_item('Master Sword Pedestal', 'Triforce')
|
||||
else:
|
||||
if want_progressives():
|
||||
pool.extend(diff.progressivesword)
|
||||
pool.extend(['Progressive Sword'])
|
||||
else:
|
||||
pool.extend(diff.basicsword)
|
||||
pool.extend(['Fighter Sword'])
|
||||
pool.extend(diff.progressivesword if want_progressives() else diff.basicsword)
|
||||
if swords == 'assured':
|
||||
if want_progressives():
|
||||
precollected_items.append('Progressive Sword')
|
||||
pool.remove('Progressive Sword')
|
||||
else:
|
||||
precollected_items.append('Fighter Sword')
|
||||
pool.remove('Fighter Sword')
|
||||
pool.extend(['Rupees (50)'])
|
||||
|
||||
extraitems = total_items_to_place - len(pool) - len(placed_items)
|
||||
|
||||
|
|
3
Items.py
3
Items.py
|
@ -25,6 +25,7 @@ def ItemFactory(items, player):
|
|||
# Format: Name: (Advancement, Priority, Type, ItemCode, Pedestal Hint Text, Pedestal Credit Text, Sick Kid Credit Text, Zora Credit Text, Witch Credit Text, Flute Boy Credit Text, Hint Text)
|
||||
item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the Bow'),
|
||||
'Progressive Bow': (True, False, None, 0x64, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'),
|
||||
'Progressive Bow (Alt)': (True, False, None, 0x65, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'),
|
||||
'Book of Mudora': (True, False, None, 0x1D, 'This is a\nparadox?!', 'and the story book', 'the scholarly kid', 'moon runes for sale', 'drugs for literacy', 'book-worm boy can read again', 'the Book'),
|
||||
'Hammer': (True, False, None, 0x09, 'stop\nhammer time!', 'and m c hammer', 'hammer-smashing kid', 'm c hammer for sale', 'stop... hammer time', 'stop, hammer time', 'the hammer'),
|
||||
'Hookshot': (True, False, None, 0x0A, 'BOING!!!\nBOING!!!\nBOING!!!', 'and the tickle beam', 'tickle-monster kid', 'tickle beam for sale', 'witch and tickle boy', 'beam boy tickles again', 'the Hookshot'),
|
||||
|
@ -43,8 +44,8 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla
|
|||
'Flippers': (True, False, None, 0x1E, 'fancy a swim?', 'and the toewebs', 'the swimming kid', 'finger webs for sale', 'shrooms let you swim', 'swimming boy swims again', 'the flippers'),
|
||||
'Ice Rod': (True, False, None, 0x08, 'I\'m the cold\nrod. I make\nthings freeze!', 'and the freeze ray', 'the ice-bending kid', 'freeze ray for sale', 'fungus for ice-rod', 'ice-cube boy freezes again', 'the ice rod'),
|
||||
'Titans Mitts': (True, False, None, 0x1C, 'Now you can\nlift heavy\nstuff!', 'and the golden glove', 'body-building kid', 'carry glove for sale', 'fungus for bling-gloves', 'body-building boy has gold again', 'the mitts'),
|
||||
'Ether': (True, False, None, 0x10, 'This magic\ncoin freezes\neverything!', 'and the bolt coin', 'coin-collecting kid', 'bolt coin for sale', 'shrooms for bolt-coin', 'medallion boy sees floor again', 'Ether'),
|
||||
'Bombos': (True, False, None, 0x0F, 'Burn, baby,\nburn! Fear my\nring of fire!', 'and the swirly coin', 'coin-collecting kid', 'swirly coin for sale', 'shrooms for swirly-coin', 'medallion boy melts room again', 'Bombos'),
|
||||
'Ether': (True, False, None, 0x10, 'This magic\ncoin freezes\neverything!', 'and the bolt coin', 'coin-collecting kid', 'bolt coin for sale', 'shrooms for bolt-coin', 'medallion boy sees floor again', 'Ether'),
|
||||
'Quake': (True, False, None, 0x11, 'Maxing out the\nRichter scale\nis what I do!', 'and the wavy coin', 'coin-collecting kid', 'wavy coin for sale', 'shrooms for wavy-coin', 'medallion boy shakes dirt again', 'Quake'),
|
||||
'Bottle': (True, False, None, 0x16, 'Now you can\nstore potions\nand stuff!', 'and the terrarium', 'the terrarium kid', 'terrarium for sale', 'special promotion', 'bottle boy has terrarium again', 'a Bottle'),
|
||||
'Bottle (Red Potion)': (True, False, None, 0x2B, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a Bottle'),
|
||||
|
|
185
Main.py
185
Main.py
|
@ -4,35 +4,33 @@ from itertools import zip_longest
|
|||
import json
|
||||
import logging
|
||||
import os
|
||||
import pickle
|
||||
import random
|
||||
import time
|
||||
import zlib
|
||||
|
||||
from BaseClasses import World, CollectionState, Item, Region, Location, Shop
|
||||
from Regions import create_regions, mark_light_world_regions
|
||||
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, get_race_rom_patches, get_enemizer_patch, apply_rom_settings, Sprite, 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'
|
||||
|
||||
def main(args, seed=None):
|
||||
if args.outputpath:
|
||||
try:
|
||||
os.mkdir(args.outputpath)
|
||||
except OSError:
|
||||
pass
|
||||
os.makedirs(args.outputpath, exist_ok=True)
|
||||
output_path.cached_path = args.outputpath
|
||||
|
||||
start = time.process_time()
|
||||
|
||||
# initialize the world
|
||||
world = World(args.multi, args.shuffle, args.logic, args.mode, args.swords, args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm, args.accessibility, args.shuffleganon, args.quickswap, args.fastmenu, args.disablemusic, args.retro, args.custom, args.customitemarray, args.hints)
|
||||
world = World(args.multi, args.shuffle, args.logic, args.mode, args.swords, args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm, args.accessibility, args.shuffleganon, args.retro, args.custom, args.customitemarray, args.hints)
|
||||
logger = logging.getLogger('')
|
||||
if seed is None:
|
||||
random.seed(None)
|
||||
|
@ -56,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]]
|
||||
|
@ -64,10 +71,16 @@ def main(args, seed=None):
|
|||
if world.mode[player] == 'standard' and world.enemy_shuffle[player] != 'none':
|
||||
world.escape_assist[player].append('bombs') # enemized escape assumes infinite bombs available and will likely be unbeatable without it
|
||||
|
||||
for tok in filter(None, args.startinventory[player].split(',')):
|
||||
item = ItemFactory(tok.strip(), player)
|
||||
if item:
|
||||
world.push_precollected(item)
|
||||
|
||||
if world.mode[player] != 'inverted':
|
||||
create_regions(world, player)
|
||||
else:
|
||||
create_inverted_regions(world, player)
|
||||
create_shops(world, player)
|
||||
create_dungeons(world, player)
|
||||
|
||||
logger.info('Shuffling the World about.')
|
||||
|
@ -129,87 +142,69 @@ def main(args, seed=None):
|
|||
|
||||
logger.info('Patching ROM.')
|
||||
|
||||
if args.sprite is not None:
|
||||
if isinstance(args.sprite, Sprite):
|
||||
sprite = args.sprite
|
||||
else:
|
||||
sprite = Sprite(args.sprite)
|
||||
else:
|
||||
sprite = None
|
||||
|
||||
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:
|
||||
from MultiServer import MultiWorld
|
||||
multidata = MultiWorld()
|
||||
multidata.players = world.players
|
||||
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)
|
||||
|
||||
for player in range(1, world.players + 1):
|
||||
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.shufflepalette[player] or args.shufflepots[player])
|
||||
rom = JsonRom() if args.jsonout or use_enemizer else LocalRom(args.rom)
|
||||
|
||||
local_rom = None
|
||||
if args.jsonout:
|
||||
rom = JsonRom()
|
||||
else:
|
||||
if use_enemizer:
|
||||
local_rom = LocalRom(args.rom)
|
||||
rom = JsonRom()
|
||||
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:
|
||||
rom = LocalRom.fromJsonRom(rom, args.rom, 0x400000)
|
||||
|
||||
if args.race:
|
||||
patch_race_rom(rom)
|
||||
|
||||
rom_names.append((player, team, list(rom.name)))
|
||||
world.spoiler.hashes[(player, team)] = get_hash_string(rom.hash)
|
||||
|
||||
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])
|
||||
|
||||
if args.jsonout:
|
||||
jsonout[f'patch_t{team}_p{player}'] = rom.patches
|
||||
else:
|
||||
rom = LocalRom(args.rom)
|
||||
patch_rom(world, player, rom, use_enemizer)
|
||||
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 '')
|
||||
|
||||
enemizer_patch = []
|
||||
if use_enemizer and (args.enemizercli or not args.jsonout):
|
||||
enemizer_patch = get_enemizer_patch(world, player, rom, args.rom, args.enemizercli, args.shufflepalette[player], args.shufflepots[player])
|
||||
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.rom_names[player] = list(rom.name)
|
||||
for location in world.get_filled_locations(player):
|
||||
if type(location.address) is int:
|
||||
multidata.locations[(location.address, player)] = (location.item.code, location.item.player)
|
||||
|
||||
if args.jsonout:
|
||||
jsonout[f'patch{player}'] = rom.patches
|
||||
if use_enemizer:
|
||||
jsonout[f'enemizer{player}'] = enemizer_patch
|
||||
if args.race:
|
||||
jsonout[f'race{player}'] = get_race_rom_patches(rom)
|
||||
else:
|
||||
if use_enemizer:
|
||||
local_rom.patch_enemizer(rom.patches, os.path.join(os.path.dirname(args.enemizercli), "enemizerBasePatch.json"), enemizer_patch)
|
||||
rom = local_rom
|
||||
|
||||
if args.race:
|
||||
for addr, values in get_race_rom_patches(rom).items():
|
||||
rom.write_bytes(int(addr), values)
|
||||
|
||||
apply_rom_settings(rom, args.heartbeep, args.heartcolor, world.quickswap, world.fastmenu, world.disable_music, sprite, player_names)
|
||||
|
||||
mcsb_name = ''
|
||||
if all([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]]):
|
||||
mcsb_name = '-keysanity'
|
||||
elif [world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]].count(True) == 1:
|
||||
mcsb_name = '-mapshuffle' if world.mapshuffle[player] else '-compassshuffle' if world.compassshuffle[player] else '-keyshuffle' if world.keyshuffle[player] else '-bigkeyshuffle'
|
||||
elif any([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]]):
|
||||
mcsb_name = '-%s%s%s%sshuffle' % (
|
||||
'M' if world.mapshuffle[player] else '', 'C' if world.compassshuffle[player] else '',
|
||||
'S' if world.keyshuffle[player] else '', 'B' if world.bigkeyshuffle[player] else '')
|
||||
|
||||
playername = f"{f'_P{player}' if world.players > 1 else ''}{f'_{player_names[player]}' if player in player_names else ''}"
|
||||
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'))
|
||||
|
||||
with open(output_path('%s_multidata' % outfilebase), 'wb') as f:
|
||||
pickle.dump(multidata, f, pickle.HIGHEST_PROTOCOL)
|
||||
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:
|
||||
jsonout["multidata"] = list(multidata)
|
||||
else:
|
||||
with open(output_path('%s_multidata' % outfilebase), 'wb') as f:
|
||||
f.write(multidata)
|
||||
|
||||
if args.create_spoiler and not args.jsonout:
|
||||
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
|
||||
|
@ -230,7 +225,9 @@ 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.quickswap, world.fastmenu, world.disable_music, world.retro, world.custom, world.customitemarray, world.hints)
|
||||
ret = World(world.players, world.shuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints)
|
||||
ret.teams = world.teams
|
||||
ret.player_names = copy.deepcopy(world.player_names)
|
||||
ret.required_medallions = world.required_medallions.copy()
|
||||
ret.swamp_patch_required = world.swamp_patch_required.copy()
|
||||
ret.ganon_at_pyramid = world.ganon_at_pyramid.copy()
|
||||
|
@ -268,6 +265,7 @@ def copy_world(world):
|
|||
create_regions(ret, player)
|
||||
else:
|
||||
create_inverted_regions(ret, player)
|
||||
create_shops(ret, player)
|
||||
create_dungeons(ret, player)
|
||||
|
||||
copy_dynamic_regions_and_locations(world, ret)
|
||||
|
@ -279,7 +277,6 @@ def copy_world(world):
|
|||
|
||||
for shop in world.shops:
|
||||
copied_shop = ret.get_region(shop.region.name, shop.region.player).shop
|
||||
copied_shop.active = shop.active
|
||||
copied_shop.inventory = copy.copy(shop.inventory)
|
||||
|
||||
# connect copied world
|
||||
|
@ -296,6 +293,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:
|
||||
|
@ -305,9 +303,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):
|
||||
|
@ -325,7 +325,7 @@ def copy_dynamic_regions_and_locations(world, ret):
|
|||
# Note: ideally exits should be copied here, but the current use case (Take anys) do not require this
|
||||
|
||||
if region.shop:
|
||||
new_reg.shop = Shop(new_reg, region.shop.room_id, region.shop.type, region.shop.shopkeeper_config, region.shop.replaceable)
|
||||
new_reg.shop = Shop(new_reg, region.shop.room_id, region.shop.type, region.shop.shopkeeper_config, region.shop.custom, region.shop.locked)
|
||||
ret.shops.append(new_reg.shop)
|
||||
|
||||
for location in world.dynamic_locations:
|
||||
|
@ -391,7 +391,6 @@ def create_playthrough(world):
|
|||
logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player)
|
||||
old_item = location.item
|
||||
location.item = None
|
||||
state.remove(old_item)
|
||||
if world.can_beat_game(state_cache[num]):
|
||||
to_delete.append(location)
|
||||
else:
|
||||
|
@ -402,6 +401,14 @@ def create_playthrough(world):
|
|||
for location in to_delete:
|
||||
sphere.remove(location)
|
||||
|
||||
# second phase, sphere 0
|
||||
for item in [i for i in world.precollected_items if i.advancement]:
|
||||
logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
|
||||
world.precollected_items.remove(item)
|
||||
world.state.remove(item)
|
||||
if not world.can_beat_game():
|
||||
world.push_precollected(item)
|
||||
|
||||
# we are now down to just the required progress items in collection_spheres. Unfortunately
|
||||
# the previous pruning stage could potentially have made certain items dependant on others
|
||||
# in the same or later sphere (because the location had 2 ways to access but the item originally
|
||||
|
@ -453,4 +460,6 @@ def create_playthrough(world):
|
|||
old_world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = get_path(state, world.get_region('Inverted Big Bomb Shop', player))
|
||||
|
||||
# we can finally output our playthrough
|
||||
old_world.spoiler.playthrough = OrderedDict([(str(i + 1), {str(location): str(location.item) for location in sphere}) for i, sphere in enumerate(collection_spheres)])
|
||||
old_world.spoiler.playthrough = OrderedDict([("0", [str(item) for item in world.precollected_items if item.advancement])])
|
||||
for i, sphere in enumerate(collection_spheres):
|
||||
old_world.spoiler.playthrough[str(i + 1)] = {str(location): str(location.item) for location in sphere}
|
||||
|
|
264
MultiClient.py
264
MultiClient.py
|
@ -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 = 'ws://' + 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()
|
||||
current_team = 0
|
||||
print('Connected players:')
|
||||
for name, team, slot in args['players']:
|
||||
print(' Team #1')
|
||||
for team, slot, name in args['players']:
|
||||
if team != current_team:
|
||||
print(' Default team' if not team else ' Team: %s' % team)
|
||||
print(f' Team #{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
|
||||
|
||||
|
|
224
MultiServer.py
224
MultiServer.py
|
@ -4,10 +4,11 @@ import asyncio
|
|||
import functools
|
||||
import json
|
||||
import logging
|
||||
import pickle
|
||||
import re
|
||||
import shlex
|
||||
import urllib.request
|
||||
import websockets
|
||||
import zlib
|
||||
|
||||
import Items
|
||||
import Regions
|
||||
|
@ -22,38 +23,22 @@ class Client:
|
|||
self.slot = None
|
||||
self.send_index = 0
|
||||
|
||||
class MultiWorld:
|
||||
def __init__(self):
|
||||
self.players = None
|
||||
self.rom_names = {}
|
||||
self.locations = {}
|
||||
|
||||
class Context:
|
||||
def __init__(self, host, port, password):
|
||||
self.data_filename = None
|
||||
self.save_filename = None
|
||||
self.disable_save = False
|
||||
self.world = MultiWorld()
|
||||
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.world.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
|
||||
|
@ -69,21 +54,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):
|
||||
|
@ -109,54 +94,54 @@ async def server(websocket, path, ctx : Context):
|
|||
ctx.clients.remove(client)
|
||||
|
||||
async def on_client_connected(ctx : Context, client : Client):
|
||||
await send_msgs(client.socket, [['RoomInfo', get_room_info(ctx)]])
|
||||
await send_msgs(client.socket, [['RoomInfo', {
|
||||
'password': ctx.password is not None,
|
||||
'players': [(client.team, client.slot, client.name) for client in ctx.clients if client.auth]
|
||||
}]])
|
||||
|
||||
async def on_client_disconnected(ctx : Context, client : Client):
|
||||
if client.auth:
|
||||
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:
|
||||
|
@ -167,36 +152,37 @@ 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.world.locations:
|
||||
target_item, target_player = ctx.world.locations[(location, slot)]
|
||||
if (location, slot) in ctx.locations:
|
||||
target_item, target_player = ctx.locations[(location, slot)]
|
||||
if target_player != slot:
|
||||
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:
|
||||
pickle.dump((ctx.world.players, ctx.world.rom_names, ctx.received_items), f, pickle.HIGHEST_PROTOCOL)
|
||||
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:
|
||||
logging.exception(e)
|
||||
|
||||
|
@ -208,50 +194,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.world.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.world.players:
|
||||
errors.add('SlotAlreadyTaken')
|
||||
elif args['slot'] not in range(1, ctx.world.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.world.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))])
|
||||
|
@ -272,7 +238,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():
|
||||
|
@ -281,10 +247,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
|
||||
|
@ -294,7 +266,7 @@ async def console(ctx : Context):
|
|||
while True:
|
||||
input = await aioconsole.ainput()
|
||||
|
||||
command = input.split()
|
||||
command = shlex.split(input)
|
||||
if not command:
|
||||
continue
|
||||
|
||||
|
@ -307,27 +279,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,13 +337,18 @@ async def main():
|
|||
ctx.data_filename = tkinter.filedialog.askopenfilename(filetypes=(("Multiworld data","*multidata"),))
|
||||
|
||||
with open(ctx.data_filename, 'rb') as f:
|
||||
ctx.world = pickle.load(f)
|
||||
jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8"))
|
||||
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.world.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:
|
||||
|
@ -372,8 +356,10 @@ async def main():
|
|||
ctx.save_filename = (ctx.data_filename[:-9] if ctx.data_filename[-9:] == 'multidata' else (ctx.data_filename + '_')) + 'multisave'
|
||||
try:
|
||||
with open(ctx.save_filename, 'rb') as f:
|
||||
players, rom_names, received_items = pickle.load(f)
|
||||
if players != ctx.world.players or rom_names != ctx.world.rom_names:
|
||||
jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8"))
|
||||
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)))
|
||||
|
|
105
Mystery.py
105
Mystery.py
|
@ -8,15 +8,18 @@ from EntranceRandomizer import parse_arguments
|
|||
from Main import main as ERmain
|
||||
|
||||
def parse_yaml(txt):
|
||||
def strip(s):
|
||||
s = s.strip()
|
||||
return '' if not s else s.strip('"') if s[0] == '"' else s.strip("'") if s[0] == "'" else s
|
||||
ret = {}
|
||||
indents = {len(txt) - len(txt.lstrip(' ')): ret}
|
||||
for line in txt.splitlines():
|
||||
if not line:
|
||||
continue
|
||||
name, val = line.split(':', 1)
|
||||
val = val.strip()
|
||||
val = strip(val)
|
||||
spaces = len(name) - len(name.lstrip(' '))
|
||||
name = name.strip()
|
||||
name = strip(name)
|
||||
if val:
|
||||
indents[spaces][name] = val
|
||||
else:
|
||||
|
@ -36,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')
|
||||
|
@ -75,7 +79,8 @@ def main():
|
|||
|
||||
if args.rom:
|
||||
erargs.rom = args.rom
|
||||
erargs.enemizercli = args.enemizercli
|
||||
if args.enemizercli:
|
||||
erargs.enemizercli = args.enemizercli
|
||||
|
||||
settings_cache = {k: (roll_settings(v) if args.samesettings else None) for k, v in weights_cache.items()}
|
||||
|
||||
|
@ -84,7 +89,8 @@ def main():
|
|||
if path:
|
||||
settings = settings_cache[path] if settings_cache[path] else roll_settings(weights_cache[path])
|
||||
for k, v in vars(settings).items():
|
||||
getattr(erargs, k)[player] = v
|
||||
if v is not None:
|
||||
getattr(erargs, k)[player] = v
|
||||
else:
|
||||
raise RuntimeError(f'No weights specified for player {player}')
|
||||
|
||||
|
@ -108,8 +114,14 @@ def get_weights(path):
|
|||
return parse_yaml(yaml)
|
||||
|
||||
def roll_settings(weights):
|
||||
def get_choice(option):
|
||||
return random.choices(list(weights[option].keys()), weights=list(map(int,weights[option].values())))[0].replace('"','').replace("'",'')
|
||||
def get_choice(option, root=weights):
|
||||
if option not in root:
|
||||
return None
|
||||
if type(root[option]) is not dict:
|
||||
return root[option]
|
||||
if not root[option]:
|
||||
return None
|
||||
return random.choices(list(root[option].keys()), weights=list(map(int,root[option].values())))[0]
|
||||
|
||||
ret = argparse.Namespace()
|
||||
|
||||
|
@ -122,84 +134,85 @@ def roll_settings(weights):
|
|||
item_placement = get_choice('item_placement')
|
||||
# not supported in ER
|
||||
|
||||
if {'map_shuffle', 'compass_shuffle', 'smallkey_shuffle', 'bigkey_shuffle'}.issubset(weights.keys()):
|
||||
ret.mapshuffle = get_choice('map_shuffle') == 'on'
|
||||
ret.compassshuffle = get_choice('compass_shuffle') == 'on'
|
||||
ret.keyshuffle = get_choice('smallkey_shuffle') == 'on'
|
||||
ret.bigkeyshuffle = get_choice('bigkey_shuffle') == 'on'
|
||||
else:
|
||||
dungeon_items = get_choice('dungeon_items')
|
||||
ret.mapshuffle = dungeon_items in ['mc', 'mcs', 'full']
|
||||
ret.compassshuffle = dungeon_items in ['mc', 'mcs', 'full']
|
||||
ret.keyshuffle = dungeon_items in ['mcs', 'full']
|
||||
ret.bigkeyshuffle = dungeon_items in ['full']
|
||||
dungeon_items = get_choice('dungeon_items')
|
||||
ret.mapshuffle = get_choice('map_shuffle') == 'on' if 'map_shuffle' in weights else dungeon_items in ['mc', 'mcs', 'full']
|
||||
ret.compassshuffle = get_choice('compass_shuffle') == 'on' if 'compass_shuffle' in weights else dungeon_items in ['mc', 'mcs', 'full']
|
||||
ret.keyshuffle = get_choice('smallkey_shuffle') == 'on' if 'smallkey_shuffle' in weights else dungeon_items in ['mcs', 'full']
|
||||
ret.bigkeyshuffle = get_choice('bigkey_shuffle') == 'on' if 'bigkey_shuffle' in weights else dungeon_items in ['full']
|
||||
|
||||
accessibility = get_choice('accessibility')
|
||||
ret.accessibility = accessibility
|
||||
ret.accessibility = get_choice('accessibility')
|
||||
|
||||
entrance_shuffle = get_choice('entrance_shuffle')
|
||||
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
|
||||
|
||||
goals = get_choice('goals')
|
||||
ret.goal = {'ganon': 'ganon',
|
||||
'fast_ganon': 'crystals',
|
||||
'dungeons': 'dungeons',
|
||||
'pedestal': 'pedestal',
|
||||
'triforce-hunt': 'triforcehunt'
|
||||
}[goals]
|
||||
ret.openpyramid = goals == 'fast_ganon'
|
||||
}[get_choice('goals')]
|
||||
ret.openpyramid = ret.goal == 'fast_ganon'
|
||||
|
||||
tower_open = get_choice('tower_open')
|
||||
ret.crystals_gt = tower_open
|
||||
ret.crystals_gt = get_choice('tower_open')
|
||||
|
||||
ganon_open = get_choice('ganon_open')
|
||||
ret.crystals_ganon = ganon_open
|
||||
ret.crystals_ganon = get_choice('ganon_open')
|
||||
|
||||
world_state = get_choice('world_state')
|
||||
ret.mode = world_state
|
||||
if world_state == 'retro':
|
||||
ret.mode = get_choice('world_state')
|
||||
if ret.mode == 'retro':
|
||||
ret.mode = 'open'
|
||||
ret.retro = True
|
||||
|
||||
hints = get_choice('hints')
|
||||
ret.hints = hints == 'on'
|
||||
ret.hints = get_choice('hints') == 'on'
|
||||
|
||||
weapons = get_choice('weapons')
|
||||
ret.swords = {'randomized': 'random',
|
||||
'assured': 'assured',
|
||||
'vanilla': 'vanilla',
|
||||
'swordless': 'swordless'
|
||||
}[weapons]
|
||||
}[get_choice('weapons')]
|
||||
|
||||
item_pool = get_choice('item_pool')
|
||||
ret.difficulty = item_pool
|
||||
ret.difficulty = get_choice('item_pool')
|
||||
|
||||
item_functionality = get_choice('item_functionality')
|
||||
ret.item_functionality = item_functionality
|
||||
ret.item_functionality = get_choice('item_functionality')
|
||||
|
||||
boss_shuffle = get_choice('boss_shuffle')
|
||||
ret.shufflebosses = {'none': 'none',
|
||||
'simple': 'basic',
|
||||
'full': 'normal',
|
||||
'random': 'chaos'
|
||||
}[boss_shuffle]
|
||||
}[get_choice('boss_shuffle')]
|
||||
|
||||
enemy_shuffle = get_choice('enemy_shuffle')
|
||||
ret.shuffleenemies = {'none': 'none',
|
||||
'shuffled': 'shuffled',
|
||||
'random': 'chaos'
|
||||
}[enemy_shuffle]
|
||||
}[get_choice('enemy_shuffle')]
|
||||
|
||||
enemy_damage = get_choice('enemy_damage')
|
||||
ret.enemy_damage = {'default': 'default',
|
||||
'shuffled': 'shuffled',
|
||||
'random': 'chaos'
|
||||
}[enemy_damage]
|
||||
}[get_choice('enemy_damage')]
|
||||
|
||||
enemy_health = get_choice('enemy_health')
|
||||
ret.enemy_health = enemy_health
|
||||
ret.enemy_health = get_choice('enemy_health')
|
||||
|
||||
ret.beemizer = int(get_choice('beemizer')) if 'beemizer' in weights.keys() else 1 # suck it :)
|
||||
ret.shufflepots = get_choice('pot_shuffle') == 'on'
|
||||
|
||||
ret.beemizer = int(get_choice('beemizer')) if 'beemizer' in weights else 0
|
||||
|
||||
inventoryweights = weights.get('startinventory', {})
|
||||
startitems = []
|
||||
for item in inventoryweights.keys():
|
||||
if get_choice(item, inventoryweights) == 'on':
|
||||
startitems.append(item)
|
||||
ret.startinventory = ','.join(startitems)
|
||||
|
||||
if 'rom' in weights:
|
||||
romweights = weights['rom']
|
||||
ret.sprite = get_choice('sprite', romweights)
|
||||
ret.disablemusic = get_choice('disablemusic', romweights) == 'on'
|
||||
ret.quickswap = get_choice('quickswap', romweights) == 'on'
|
||||
ret.fastmenu = get_choice('menuspeed', romweights)
|
||||
ret.heartcolor = get_choice('heartcolor', romweights)
|
||||
ret.heartbeep = get_choice('heartbeep', romweights)
|
||||
ret.ow_palettes = get_choice('ow_palettes', romweights)
|
||||
ret.uw_palettes = get_choice('uw_palettes', romweights)
|
||||
|
||||
return ret
|
||||
|
||||
|
|
20
Plando.py
20
Plando.py
|
@ -10,7 +10,7 @@ import sys
|
|||
from BaseClasses import World
|
||||
from Regions import create_regions
|
||||
from EntranceShuffle import link_entrances, connect_entrance, connect_two_way, connect_exit
|
||||
from Rom import patch_rom, LocalRom, Sprite, write_string_to_rom, apply_rom_settings
|
||||
from Rom import patch_rom, LocalRom, write_string_to_rom, apply_rom_settings, get_sprite_from_name
|
||||
from Rules import set_rules
|
||||
from Dungeons import create_dungeons
|
||||
from Items import ItemFactory
|
||||
|
@ -23,7 +23,8 @@ def main(args):
|
|||
start_time = time.process_time()
|
||||
|
||||
# initialize the world
|
||||
world = World(1, 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, args.quickswap, args.fastmenu, args.disablemusic, False, False, False, None, False)
|
||||
world = World(1, 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, False, False, False, None, False)
|
||||
world.player_names[1].append("Player 1")
|
||||
logger = logging.getLogger('')
|
||||
|
||||
hasher = hashlib.md5()
|
||||
|
@ -68,15 +69,10 @@ def main(args):
|
|||
|
||||
logger.info('Patching ROM.')
|
||||
|
||||
if args.sprite is not None:
|
||||
sprite = Sprite(args.sprite)
|
||||
else:
|
||||
sprite = None
|
||||
|
||||
rom = LocalRom(args.rom)
|
||||
patch_rom(world, 1, rom)
|
||||
patch_rom(world, rom, 1, 1, False)
|
||||
|
||||
apply_rom_settings(rom, args.heartbeep, args.heartcolor, world.quickswap, world.fastmenu, world.disable_music, sprite)
|
||||
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, args.sprite, args.ow_palettes, args.uw_palettes)
|
||||
|
||||
for textname, texttype, text in text_patches:
|
||||
if texttype == 'text':
|
||||
|
@ -213,6 +209,8 @@ def start():
|
|||
help='Select the rate at which the heart beep sound is played at low health.')
|
||||
parser.add_argument('--heartcolor', default='red', const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow'],
|
||||
help='Select the color of Link\'s heart meter. (default: %(default)s)')
|
||||
parser.add_argument('--ow_palettes', default='default', choices=['default', 'random', 'blackout'])
|
||||
parser.add_argument('--uw_palettes', default='default', choices=['default', 'random', 'blackout'])
|
||||
parser.add_argument('--sprite', help='Path to a sprite sheet to use for Link. Needs to be in binary format and have a length of 0x7000 (28672) bytes.')
|
||||
parser.add_argument('--plando', help='Filled out template to use for setting up the rom.')
|
||||
args = parser.parse_args()
|
||||
|
@ -224,8 +222,8 @@ def start():
|
|||
if not os.path.isfile(args.plando):
|
||||
input('Could not find Plandomizer distribution at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.plando)
|
||||
sys.exit(1)
|
||||
if args.sprite is not None and not os.path.isfile(args.rom):
|
||||
input('Could not find link sprite sheet at given location. \nPress Enter to exit.' % args.sprite)
|
||||
if args.sprite is not None and not os.path.isfile(args.sprite) and not get_sprite_from_name(args.sprite):
|
||||
input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
|
||||
sys.exit(1)
|
||||
|
||||
# set up logger
|
||||
|
|
69
Regions.py
69
Regions.py
|
@ -293,22 +293,9 @@ def create_regions(world, player):
|
|||
create_dw_region(player, 'Pyramid Ledge', None, ['Pyramid Entrance', 'Pyramid Drop'])
|
||||
]
|
||||
|
||||
for region_name, (room_id, shopkeeper, replaceable) in shop_table.items():
|
||||
region = world.get_region(region_name, player)
|
||||
shop = Shop(region, room_id, ShopType.Shop, shopkeeper, replaceable)
|
||||
region.shop = shop
|
||||
world.shops.append(shop)
|
||||
for index, (item, price) in enumerate(default_shop_contents[region_name]):
|
||||
shop.add_inventory(index, item, price)
|
||||
|
||||
region = world.get_region('Capacity Upgrade', player)
|
||||
shop = Shop(region, 0x0115, ShopType.UpgradeShop, 0x04, True)
|
||||
region.shop = shop
|
||||
world.shops.append(shop)
|
||||
shop.add_inventory(0, 'Bomb Upgrade (+5)', 100, 7)
|
||||
shop.add_inventory(1, 'Arrow Upgrade (+5)', 100, 7)
|
||||
world.initialize_regions()
|
||||
|
||||
|
||||
def create_lw_region(player, name, locations=None, exits=None):
|
||||
return _create_region(player, name, RegionType.LightWorld, 'Light World', locations, exits)
|
||||
|
||||
|
@ -364,37 +351,35 @@ def mark_light_world_regions(world, player):
|
|||
seen.add(exit.connected_region)
|
||||
queue.append(exit.connected_region)
|
||||
|
||||
# (room_id, shopkeeper, replaceable)
|
||||
shop_table = {
|
||||
'Cave Shop (Dark Death Mountain)': (0x0112, 0xC1, True),
|
||||
'Red Shield Shop': (0x0110, 0xC1, True),
|
||||
'Dark Lake Hylia Shop': (0x010F, 0xC1, True),
|
||||
'Dark World Lumberjack Shop': (0x010F, 0xC1, True),
|
||||
'Village of Outcasts Shop': (0x010F, 0xC1, True),
|
||||
'Dark World Potion Shop': (0x010F, 0xC1, True),
|
||||
'Light World Death Mountain Shop': (0x00FF, 0xA0, True),
|
||||
'Kakariko Shop': (0x011F, 0xA0, True),
|
||||
'Cave Shop (Lake Hylia)': (0x0112, 0xA0, True),
|
||||
'Potion Shop': (0x0109, 0xFF, False),
|
||||
# Bomb Shop not currently modeled as a shop, due to special nature of items
|
||||
}
|
||||
# region, [item]
|
||||
# slot, item, price, max=0, replacement=None, replacement_price=0
|
||||
# item = (item, price)
|
||||
|
||||
def create_shops(world, player):
|
||||
for region_name, (room_id, type, shopkeeper, custom, locked, inventory) in shop_table.items():
|
||||
if world.mode[player] == 'inverted' and region_name == 'Dark Lake Hylia Shop':
|
||||
locked = True
|
||||
inventory = [('Blue Potion', 160), ('Blue Shield', 50), ('Bombs (10)', 50)]
|
||||
region = world.get_region(region_name, player)
|
||||
shop = Shop(region, room_id, type, shopkeeper, custom, locked)
|
||||
region.shop = shop
|
||||
world.shops.append(shop)
|
||||
for index, item in enumerate(inventory):
|
||||
shop.add_inventory(index, *item)
|
||||
|
||||
# (type, room_id, shopkeeper, custom, locked, [items])
|
||||
# item = (item, price, max=0, replacement=None, replacement_price=0)
|
||||
_basic_shop_defaults = [('Red Potion', 150), ('Small Heart', 10), ('Bombs (10)', 50)]
|
||||
_dark_world_shop_defaults = [('Red Potion', 150), ('Blue Shield', 50), ('Bombs (10)', 50)]
|
||||
default_shop_contents = {
|
||||
'Cave Shop (Dark Death Mountain)': _basic_shop_defaults,
|
||||
'Red Shield Shop': [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)],
|
||||
'Dark Lake Hylia Shop': _dark_world_shop_defaults,
|
||||
'Dark World Lumberjack Shop': _dark_world_shop_defaults,
|
||||
'Village of Outcasts Shop': _dark_world_shop_defaults,
|
||||
'Dark World Potion Shop': _dark_world_shop_defaults,
|
||||
'Light World Death Mountain Shop': _basic_shop_defaults,
|
||||
'Kakariko Shop': _basic_shop_defaults,
|
||||
'Cave Shop (Lake Hylia)': _basic_shop_defaults,
|
||||
'Potion Shop': [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)],
|
||||
shop_table = {
|
||||
'Cave Shop (Dark Death Mountain)': (0x0112, ShopType.Shop, 0xC1, True, False, _basic_shop_defaults),
|
||||
'Red Shield Shop': (0x0110, ShopType.Shop, 0xC1, True, False, [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)]),
|
||||
'Dark Lake Hylia Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
|
||||
'Dark World Lumberjack Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
|
||||
'Village of Outcasts Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
|
||||
'Dark World Potion Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
|
||||
'Light World Death Mountain Shop': (0x00FF, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults),
|
||||
'Kakariko Shop': (0x011F, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults),
|
||||
'Cave Shop (Lake Hylia)': (0x0112, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults),
|
||||
'Potion Shop': (0x0109, ShopType.Shop, 0xFF, False, True, [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)]),
|
||||
'Capacity Upgrade': (0x0115, ShopType.UpgradeShop, 0x04, True, True, [('Bomb Upgrade (+5)', 100, 7), ('Arrow Upgrade (+5)', 100, 7)])
|
||||
}
|
||||
|
||||
location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'),
|
||||
|
|
590
Rom.py
590
Rom.py
|
@ -9,13 +9,13 @@ import struct
|
|||
import sys
|
||||
import subprocess
|
||||
|
||||
from BaseClasses import ShopType, Region, Location, Item
|
||||
from BaseClasses import CollectionState, ShopType, Region, Location
|
||||
from Dungeons import dungeon_music_addresses
|
||||
from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable
|
||||
from Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, BombShop2_texts, junk_texts
|
||||
from Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts, LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names
|
||||
from Utils import output_path, local_path, int16_as_bytes, int32_as_bytes, snes_to_pc
|
||||
from Items import ItemFactory, item_table
|
||||
from Items import ItemFactory
|
||||
from EntranceShuffle import door_addresses
|
||||
|
||||
|
||||
|
@ -25,8 +25,10 @@ JAP10HASH = '03a63945398191337e896e5771f77173'
|
|||
|
||||
class JsonRom(object):
|
||||
|
||||
def __init__(self):
|
||||
self.name = None
|
||||
def __init__(self, name=None, hash=None):
|
||||
self.name = name
|
||||
self.hash = hash
|
||||
self.orig_buffer = None
|
||||
self.patches = {}
|
||||
self.addresses = []
|
||||
|
||||
|
@ -36,8 +38,7 @@ class JsonRom(object):
|
|||
def write_bytes(self, startaddress, values):
|
||||
if not values:
|
||||
return
|
||||
if type(values) is not list:
|
||||
values = list(values)
|
||||
values = list(values)
|
||||
|
||||
pos = bisect.bisect_right(self.addresses, startaddress)
|
||||
intervalstart = self.addresses[pos-1] if pos else None
|
||||
|
@ -70,12 +71,15 @@ class JsonRom(object):
|
|||
|
||||
class LocalRom(object):
|
||||
|
||||
def __init__(self, file, patch=True):
|
||||
self.name = None
|
||||
def __init__(self, file, patch=True, name=None, hash=None):
|
||||
self.name = name
|
||||
self.hash = hash
|
||||
self.orig_buffer = None
|
||||
with open(file, 'rb') as stream:
|
||||
self.buffer = read_rom(stream)
|
||||
if patch:
|
||||
self.patch_base_rom()
|
||||
self.orig_buffer = self.buffer.copy()
|
||||
|
||||
def write_byte(self, address, value):
|
||||
self.buffer[address] = value
|
||||
|
@ -88,6 +92,14 @@ class LocalRom(object):
|
|||
with open(file, 'wb') as outfile:
|
||||
outfile.write(self.buffer)
|
||||
|
||||
@staticmethod
|
||||
def fromJsonRom(rom, file, rom_size = 0x200000):
|
||||
ret = LocalRom(file, True, rom.name, rom.hash)
|
||||
ret.buffer.extend(bytearray([0x00] * (rom_size - len(ret.buffer))))
|
||||
for address, values in rom.patches.items():
|
||||
ret.write_bytes(int(address), values)
|
||||
return ret
|
||||
|
||||
def patch_base_rom(self):
|
||||
# verify correct checksum of baserom
|
||||
basemd5 = hashlib.md5()
|
||||
|
@ -112,24 +124,6 @@ class LocalRom(object):
|
|||
# if RANDOMIZERBASEHASH != patchedmd5.hexdigest():
|
||||
# raise RuntimeError('Provided Base Rom unsuitable for patching. Please provide a JAP(1.0) "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" rom to use as a base.')
|
||||
|
||||
def patch_enemizer(self, rando_patch, base_enemizer_patch_path, enemizer_patch):
|
||||
# extend to 4MB
|
||||
self.buffer.extend(bytearray([0x00] * (0x400000 - len(self.buffer))))
|
||||
|
||||
# apply randomizer patches
|
||||
for address, values in rando_patch.items():
|
||||
self.write_bytes(int(address), values)
|
||||
|
||||
# load base enemizer patches
|
||||
with open(base_enemizer_patch_path, 'r') as f:
|
||||
base_enemizer_patch = json.load(f)
|
||||
for patch in base_enemizer_patch:
|
||||
self.write_bytes(patch["address"], patch["patchData"])
|
||||
|
||||
# apply enemizer patches
|
||||
for patch in enemizer_patch:
|
||||
self.write_bytes(patch["address"], patch["patchData"])
|
||||
|
||||
def write_crc(self):
|
||||
crc = (sum(self.buffer[:0x7FDC] + self.buffer[0x7FE0:]) + 0x01FE) & 0xFFFF
|
||||
inv = crc ^ 0xFFFF
|
||||
|
@ -161,9 +155,10 @@ def read_rom(stream):
|
|||
buffer = buffer[0x200:]
|
||||
return buffer
|
||||
|
||||
def get_enemizer_patch(world, player, rom, baserom_path, enemizercli, shufflepalette, shufflepots):
|
||||
def patch_enemizer(world, player, rom, baserom_path, enemizercli, shufflepots, random_sprite_on_hit):
|
||||
baserom_path = os.path.abspath(baserom_path)
|
||||
basepatch_path = os.path.abspath(local_path('data/base2current.json'))
|
||||
enemizer_basepatch_path = os.path.join(os.path.dirname(enemizercli), "enemizerBasePatch.json")
|
||||
randopatch_path = os.path.abspath(output_path('enemizer_randopatch.json'))
|
||||
options_path = os.path.abspath(output_path('enemizer_options.json'))
|
||||
enemizer_output_path = os.path.abspath(output_path('enemizer_output.json'))
|
||||
|
@ -197,10 +192,10 @@ def get_enemizer_patch(world, player, rom, baserom_path, enemizercli, shufflepal
|
|||
'RandomizeBossDamageMinAmount': 0,
|
||||
'RandomizeBossDamageMaxAmount': 200,
|
||||
'RandomizeBossBehavior': False,
|
||||
'RandomizeDungeonPalettes': shufflepalette,
|
||||
'RandomizeDungeonPalettes': False,
|
||||
'SetBlackoutMode': False,
|
||||
'RandomizeOverworldPalettes': shufflepalette,
|
||||
'RandomizeSpritePalettes': shufflepalette,
|
||||
'RandomizeOverworldPalettes': False,
|
||||
'RandomizeSpritePalettes': False,
|
||||
'SetAdvancedSpritePalettes': False,
|
||||
'PukeMode': False,
|
||||
'NegativeMode': False,
|
||||
|
@ -221,7 +216,7 @@ def get_enemizer_patch(world, player, rom, baserom_path, enemizercli, shufflepal
|
|||
'RandomizeTileTrapPattern': world.enemy_shuffle[player] == 'chaos',
|
||||
'RandomizeTileTrapFloorTile': False,
|
||||
'AllowKillableThief': bool(random.randint(0,1)) if world.enemy_shuffle[player] == 'chaos' else world.enemy_shuffle[player] != 'none',
|
||||
'RandomizeSpriteOnHit': False,
|
||||
'RandomizeSpriteOnHit': random_sprite_on_hit,
|
||||
'DebugMode': False,
|
||||
'DebugForceEnemy': False,
|
||||
'DebugForceEnemyId': 0,
|
||||
|
@ -243,21 +238,14 @@ def get_enemizer_patch(world, player, rom, baserom_path, enemizercli, shufflepal
|
|||
'IcePalace': world.get_dungeon("Ice Palace", player).boss.enemizer_name,
|
||||
'MiseryMire': world.get_dungeon("Misery Mire", player).boss.enemizer_name,
|
||||
'TurtleRock': world.get_dungeon("Turtle Rock", player).boss.enemizer_name,
|
||||
'GanonsTower1': world.get_dungeon('Ganons Tower' if world.mode[player] != 'inverted' else 'Inverted Ganons Tower', player).bosses['bottom'].enemizer_name,
|
||||
'GanonsTower2': world.get_dungeon('Ganons Tower' if world.mode[player] != 'inverted' else 'Inverted Ganons Tower', player).bosses['middle'].enemizer_name,
|
||||
'GanonsTower3': world.get_dungeon('Ganons Tower' if world.mode[player] != 'inverted' else 'Inverted Ganons Tower', player).bosses['top'].enemizer_name,
|
||||
'GanonsTower4': 'Agahnim2',
|
||||
'Ganon': 'Ganon',
|
||||
}
|
||||
}
|
||||
|
||||
if world.mode[player] != 'inverted':
|
||||
options['ManualBosses']['GanonsTower1'] = world.get_dungeon('Ganons Tower', player).bosses['bottom'].enemizer_name
|
||||
options['ManualBosses']['GanonsTower2'] = world.get_dungeon('Ganons Tower', player).bosses['middle'].enemizer_name
|
||||
options['ManualBosses']['GanonsTower3'] = world.get_dungeon('Ganons Tower', player).bosses['top'].enemizer_name
|
||||
else:
|
||||
options['ManualBosses']['GanonsTower1'] = world.get_dungeon('Inverted Ganons Tower', player).bosses['bottom'].enemizer_name
|
||||
options['ManualBosses']['GanonsTower2'] = world.get_dungeon('Inverted Ganons Tower', player).bosses['middle'].enemizer_name
|
||||
options['ManualBosses']['GanonsTower3'] = world.get_dungeon('Inverted Ganons Tower', player).bosses['top'].enemizer_name
|
||||
|
||||
|
||||
rom.write_to_file(randopatch_path)
|
||||
|
||||
with open(options_path, 'w') as f:
|
||||
|
@ -272,19 +260,61 @@ def get_enemizer_patch(world, player, rom, baserom_path, enemizercli, shufflepal
|
|||
'--output', enemizer_output_path],
|
||||
cwd=os.path.dirname(enemizercli), stdout=subprocess.DEVNULL)
|
||||
|
||||
with open(enemizer_basepatch_path, 'r') as f:
|
||||
for patch in json.load(f):
|
||||
rom.write_bytes(patch["address"], patch["patchData"])
|
||||
|
||||
with open(enemizer_output_path, 'r') as f:
|
||||
ret = json.load(f)
|
||||
for patch in json.load(f):
|
||||
rom.write_bytes(patch["address"], patch["patchData"])
|
||||
|
||||
if os.path.exists(randopatch_path):
|
||||
if random_sprite_on_hit:
|
||||
_populate_sprite_table()
|
||||
sprites = list(_sprite_table.values())
|
||||
if sprites:
|
||||
while len(sprites) < 32:
|
||||
sprites.extend(sprites)
|
||||
random.shuffle(sprites)
|
||||
|
||||
for i, path in enumerate(sprites[:32]):
|
||||
sprite = Sprite(path)
|
||||
rom.write_bytes(0x300000 + (i * 0x8000), sprite.sprite)
|
||||
rom.write_bytes(0x307000 + (i * 0x8000), sprite.palette)
|
||||
rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette)
|
||||
|
||||
try:
|
||||
os.remove(randopatch_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if os.path.exists(options_path):
|
||||
try:
|
||||
os.remove(options_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if os.path.exists(enemizer_output_path):
|
||||
try:
|
||||
os.remove(enemizer_output_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return ret
|
||||
_sprite_table = {}
|
||||
def _populate_sprite_table():
|
||||
if not _sprite_table:
|
||||
for dir in [local_path('data/sprites/official'), local_path('data/sprites/unofficial')]:
|
||||
for file in os.listdir(dir):
|
||||
filepath = os.path.join(dir, file)
|
||||
if not os.path.isfile(filepath):
|
||||
continue
|
||||
sprite = Sprite(filepath)
|
||||
if sprite.valid:
|
||||
_sprite_table[sprite.name.lower()] = filepath
|
||||
|
||||
def get_sprite_from_name(name):
|
||||
_populate_sprite_table()
|
||||
name = name.lower()
|
||||
if name in ['random', 'randomonhit']:
|
||||
return Sprite(random.choice(list(_sprite_table.values())))
|
||||
return Sprite(_sprite_table[name]) if name in _sprite_table else None
|
||||
|
||||
class Sprite(object):
|
||||
default_palette = [255, 127, 126, 35, 183, 17, 158, 54, 165, 20, 255, 1, 120, 16, 157,
|
||||
|
@ -444,7 +474,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
|
||||
|
@ -895,30 +925,158 @@ def patch_rom(world, player, rom, enemized):
|
|||
rom.write_byte(0x1800A1, 0x01) # enable overworld screen transition draining for water level inside swamp
|
||||
rom.write_byte(0x180174, 0x01 if world.fix_fake_world[player] else 0x00)
|
||||
rom.write_byte(0x18017E, 0x01) # Fairy fountains only trade in bottles
|
||||
rom.write_byte(0x180034, 0x0A) # starting max bombs
|
||||
rom.write_byte(0x180035, 30) # starting max arrows
|
||||
for x in range(0x183000, 0x18304F):
|
||||
rom.write_byte(x, 0) # Zero the initial equipment array
|
||||
rom.write_byte(0x18302C, 0x18) # starting max health
|
||||
rom.write_byte(0x18302D, 0x18) # starting current health
|
||||
ability_flags = 0x68 # starting abilities, bit array; may be modified by precollected items
|
||||
|
||||
|
||||
# Starting equipment
|
||||
equip = [0] * (0x340 + 0x4F)
|
||||
equip[0x36C] = 0x18
|
||||
equip[0x36D] = 0x18
|
||||
equip[0x379] = 0x68
|
||||
starting_max_bombs = 10
|
||||
starting_max_arrows = 30
|
||||
|
||||
startingstate = CollectionState(world)
|
||||
|
||||
if startingstate.has('Bow', player):
|
||||
equip[0x340] = 1
|
||||
equip[0x38E] |= 0x20 # progressive flag to get the correct hint in all cases
|
||||
if not world.retro[player]:
|
||||
equip[0x38E] |= 0x80
|
||||
if startingstate.has('Silver Arrows', player):
|
||||
equip[0x38E] |= 0x40
|
||||
|
||||
if startingstate.has('Titans Mitts', player):
|
||||
equip[0x354] = 2
|
||||
elif startingstate.has('Power Glove', player):
|
||||
equip[0x354] = 1
|
||||
|
||||
if startingstate.has('Golden Sword', player):
|
||||
equip[0x359] = 4
|
||||
elif startingstate.has('Tempered Sword', player):
|
||||
equip[0x359] = 3
|
||||
elif startingstate.has('Master Sword', player):
|
||||
equip[0x359] = 2
|
||||
elif startingstate.has('Fighter Sword', player):
|
||||
equip[0x359] = 1
|
||||
|
||||
if startingstate.has('Mirror Shield', player):
|
||||
equip[0x35A] = 3
|
||||
elif startingstate.has('Red Shield', player):
|
||||
equip[0x35A] = 2
|
||||
elif startingstate.has('Blue Shield', player):
|
||||
equip[0x35A] = 1
|
||||
|
||||
if startingstate.has('Red Mail', player):
|
||||
equip[0x35B] = 2
|
||||
elif startingstate.has('Blue Mail', player):
|
||||
equip[0x35B] = 1
|
||||
|
||||
if startingstate.has('Magic Upgrade (1/4)', player):
|
||||
equip[0x37B] = 2
|
||||
equip[0x36E] = 0x80
|
||||
elif startingstate.has('Magic Upgrade (1/2)', player):
|
||||
equip[0x37B] = 1
|
||||
equip[0x36E] = 0x80
|
||||
|
||||
for item in world.precollected_items:
|
||||
if item.player != player:
|
||||
continue
|
||||
|
||||
if item.name == 'Fighter Sword':
|
||||
rom.write_byte(0x183000+0x19, 0x01)
|
||||
rom.write_byte(0x0271A6+0x19, 0x01)
|
||||
rom.write_byte(0x180043, 0x01) # special starting sword byte
|
||||
elif item.name == 'Pegasus Boots':
|
||||
rom.write_byte(0x183015, 0x01)
|
||||
ability_flags |= 0b00000100
|
||||
else:
|
||||
raise RuntimeError("Unsupported pre-collected item: {}".format(item))
|
||||
if item.name in ['Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)',
|
||||
'Titans Mitts', 'Power Glove', 'Progressive Glove',
|
||||
'Golden Sword', 'Tempered Sword', 'Master Sword', 'Fighter Sword', 'Progressive Sword',
|
||||
'Mirror Shield', 'Red Shield', 'Blue Shield', 'Progressive Shield',
|
||||
'Red Mail', 'Blue Mail', 'Progressive Armor',
|
||||
'Magic Upgrade (1/4)', 'Magic Upgrade (1/2)']:
|
||||
continue
|
||||
|
||||
# write abilities after ability flags have been determined
|
||||
rom.write_byte(0x183039, ability_flags)
|
||||
set_table = {'Book of Mudora': (0x34E, 1), 'Hammer': (0x34B, 1), 'Bug Catching Net': (0x34D, 1), 'Hookshot': (0x342, 1), 'Magic Mirror': (0x353, 2),
|
||||
'Cape': (0x352, 1), 'Lamp': (0x34A, 1), 'Moon Pearl': (0x357, 1), 'Cane of Somaria': (0x350, 1), 'Cane of Byrna': (0x351, 1),
|
||||
'Fire Rod': (0x345, 1), 'Ice Rod': (0x346, 1), 'Bombos': (0x347, 1), 'Ether': (0x348, 1), 'Quake': (0x349, 1)}
|
||||
or_table = {'Green Pendant': (0x374, 0x04), 'Red Pendant': (0x374, 0x01), 'Blue Pendant': (0x374, 0x02),
|
||||
'Crystal 1': (0x37A, 0x02), 'Crystal 2': (0x37A, 0x10), 'Crystal 3': (0x37A, 0x40), 'Crystal 4': (0x37A, 0x20),
|
||||
'Crystal 5': (0x37A, 0x04), 'Crystal 6': (0x37A, 0x01), 'Crystal 7': (0x37A, 0x08),
|
||||
'Big Key (Eastern Palace)': (0x367, 0x20), 'Compass (Eastern Palace)': (0x365, 0x20), 'Map (Eastern Palace)': (0x369, 0x20),
|
||||
'Big Key (Desert Palace)': (0x367, 0x10), 'Compass (Desert Palace)': (0x365, 0x10), 'Map (Desert Palace)': (0x369, 0x10),
|
||||
'Big Key (Tower of Hera)': (0x366, 0x20), 'Compass (Tower of Hera)': (0x364, 0x20), 'Map (Tower of Hera)': (0x368, 0x20),
|
||||
'Big Key (Escape)': (0x367, 0xC0), 'Compass (Escape)': (0x365, 0xC0), 'Map (Escape)': (0x369, 0xC0),
|
||||
'Big Key (Palace of Darkness)': (0x367, 0x02), 'Compass (Palace of Darkness)': (0x365, 0x02), 'Map (Palace of Darkness)': (0x369, 0x02),
|
||||
'Big Key (Thieves Town)': (0x366, 0x10), 'Compass (Thieves Town)': (0x364, 0x10), 'Map (Thieves Town)': (0x368, 0x10),
|
||||
'Big Key (Skull Woods)': (0x366, 0x80), 'Compass (Skull Woods)': (0x364, 0x80), 'Map (Skull Woods)': (0x368, 0x80),
|
||||
'Big Key (Swamp Palace)': (0x367, 0x04), 'Compass (Swamp Palace)': (0x365, 0x04), 'Map (Swamp Palace)': (0x369, 0x04),
|
||||
'Big Key (Ice Palace)': (0x366, 0x40), 'Compass (Ice Palace)': (0x364, 0x40), 'Map (Ice Palace)': (0x368, 0x40),
|
||||
'Big Key (Misery Mire)': (0x367, 0x01), 'Compass (Misery Mire)': (0x365, 0x01), 'Map (Misery Mire)': (0x369, 0x01),
|
||||
'Big Key (Turtle Rock)': (0x366, 0x08), 'Compass (Turtle Rock)': (0x364, 0x08), 'Map (Turtle Rock)': (0x368, 0x08),
|
||||
'Big Key (Ganons Tower)': (0x366, 0x04), 'Compass (Ganons Tower)': (0x364, 0x04), 'Map (Ganons Tower)': (0x368, 0x04)}
|
||||
set_or_table = {'Flippers': (0x356, 1, 0x379, 0x02),'Pegasus Boots': (0x355, 1, 0x379, 0x04),
|
||||
'Shovel': (0x34C, 1, 0x38C, 0x04), 'Ocarina': (0x34C, 3, 0x38C, 0x01),
|
||||
'Mushroom': (0x344, 1, 0x38C, 0x20 | 0x08), 'Magic Powder': (0x344, 2, 0x38C, 0x10),
|
||||
'Blue Boomerang': (0x341, 1, 0x38C, 0x80), 'Red Boomerang': (0x341, 2, 0x38C, 0x40)}
|
||||
keys = {'Small Key (Eastern Palace)': [0x37E], 'Small Key (Desert Palace)': [0x37F],
|
||||
'Small Key (Tower of Hera)': [0x386],
|
||||
'Small Key (Agahnims Tower)': [0x380], 'Small Key (Palace of Darkness)': [0x382],
|
||||
'Small Key (Thieves Town)': [0x387],
|
||||
'Small Key (Skull Woods)': [0x384], 'Small Key (Swamp Palace)': [0x381],
|
||||
'Small Key (Ice Palace)': [0x385],
|
||||
'Small Key (Misery Mire)': [0x383], 'Small Key (Turtle Rock)': [0x388],
|
||||
'Small Key (Ganons Tower)': [0x389],
|
||||
'Small Key (Universal)': [0x38B], 'Small Key (Escape)': [0x37C, 0x37D]}
|
||||
bottles = {'Bottle': 2, 'Bottle (Red Potion)': 3, 'Bottle (Green Potion)': 4, 'Bottle (Blue Potion)': 5,
|
||||
'Bottle (Fairy)': 6, 'Bottle (Bee)': 7, 'Bottle (Good Bee)': 8}
|
||||
rupees = {'Rupee (1)': 1, 'Rupees (5)': 5, 'Rupees (20)': 20, 'Rupees (50)': 50, 'Rupees (100)': 100, 'Rupees (300)': 300}
|
||||
bomb_caps = {'Bomb Upgrade (+5)': 5, 'Bomb Upgrade (+10)': 10}
|
||||
arrow_caps = {'Arrow Upgrade (+5)': 5, 'Arrow Upgrade (+10)': 10}
|
||||
bombs = {'Single Bomb': 1, 'Bombs (3)': 3, 'Bombs (10)': 10}
|
||||
arrows = {'Single Arrow': 1, 'Arrows (10)': 10}
|
||||
|
||||
if item.name in set_table:
|
||||
equip[set_table[item.name][0]] = set_table[item.name][1]
|
||||
elif item.name in or_table:
|
||||
equip[or_table[item.name][0]] |= or_table[item.name][1]
|
||||
elif item.name in set_or_table:
|
||||
equip[set_or_table[item.name][0]] = set_or_table[item.name][1]
|
||||
equip[set_or_table[item.name][2]] |= set_or_table[item.name][3]
|
||||
elif item.name in keys:
|
||||
for address in keys[item.name]:
|
||||
equip[address] = min(equip[address] + 1, 99)
|
||||
elif item.name in bottles:
|
||||
if equip[0x34F] < world.difficulty_requirements[player].progressive_bottle_limit:
|
||||
equip[0x35C + equip[0x34F]] = bottles[item.name]
|
||||
equip[0x34F] += 1
|
||||
elif item.name in rupees:
|
||||
equip[0x360:0x362] = list(min(equip[0x360] + (equip[0x361] << 8) + rupees[item.name], 9999).to_bytes(2, byteorder='little', signed=False))
|
||||
equip[0x362:0x364] = list(min(equip[0x362] + (equip[0x363] << 8) + rupees[item.name], 9999).to_bytes(2, byteorder='little', signed=False))
|
||||
elif item.name in bomb_caps:
|
||||
starting_max_bombs = min(starting_max_bombs + bomb_caps[item.name], 50)
|
||||
elif item.name in arrow_caps:
|
||||
starting_max_arrows = min(starting_max_arrows + arrow_caps[item.name], 70)
|
||||
elif item.name in bombs:
|
||||
equip[0x343] += bombs[item.name]
|
||||
elif item.name in arrows:
|
||||
if world.retro[player]:
|
||||
equip[0x38E] |= 0x80
|
||||
equip[0x377] = 1
|
||||
else:
|
||||
equip[0x377] += arrows[item.name]
|
||||
elif item.name in ['Piece of Heart', 'Boss Heart Container', 'Sanctuary Heart Container']:
|
||||
if item.name == 'Piece of Heart':
|
||||
equip[0x36B] = (equip[0x36B] + 1) % 4
|
||||
if item.name != 'Piece of Heart' or equip[0x36B] == 0:
|
||||
equip[0x36C] = min(equip[0x36C] + 0x08, 0xA0)
|
||||
equip[0x36D] = min(equip[0x36D] + 0x08, 0xA0)
|
||||
else:
|
||||
raise RuntimeError(f'Unsupported item in starting equipment: {item.name}')
|
||||
|
||||
equip[0x343] = min(equip[0x343], starting_max_bombs)
|
||||
rom.write_byte(0x180034, starting_max_bombs)
|
||||
equip[0x377] = min(equip[0x377], starting_max_arrows)
|
||||
rom.write_byte(0x180035, starting_max_arrows)
|
||||
rom.write_bytes(0x180046, equip[0x360:0x362])
|
||||
if equip[0x359]:
|
||||
rom.write_byte(0x180043, equip[0x359])
|
||||
|
||||
assert equip[:0x340] == [0] * 0x340
|
||||
rom.write_bytes(0x183000, equip[0x340:])
|
||||
rom.write_bytes(0x271A6, equip[0x340:0x340+60])
|
||||
|
||||
rom.write_byte(0x18004A, 0x00 if world.mode[player] != 'inverted' else 0x01) # Inverted mode
|
||||
rom.write_byte(0x18005D, 0x00) # Hammer always breaks barrier
|
||||
|
@ -944,7 +1102,9 @@ def patch_rom(world, player, rom, enemized):
|
|||
|
||||
rom.write_byte(0x18005E, world.crystals_needed_for_gt[player])
|
||||
rom.write_byte(0x18005F, world.crystals_needed_for_ganon[player])
|
||||
rom.write_byte(0x18008A, 0x01 if world.mode[player] == "standard" else 0x00) # block HC upstairs doors in rain state in standard mode
|
||||
|
||||
# block HC upstairs doors in rain state in standard mode
|
||||
rom.write_byte(0x18008A, 0x01 if world.mode[player] == "standard" and world.shuffle[player] != 'vanilla' else 0x00)
|
||||
|
||||
rom.write_byte(0x18016A, 0x10 | ((0x01 if world.keyshuffle[player] else 0x00)
|
||||
| (0x02 if world.compassshuffle[player] else 0x00)
|
||||
|
@ -1067,13 +1227,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)
|
||||
|
@ -1085,6 +1250,7 @@ def patch_rom(world, player, rom, enemized):
|
|||
hashint & 0x1F,
|
||||
]
|
||||
rom.write_bytes(0x180215, code)
|
||||
rom.hash = code
|
||||
|
||||
return rom
|
||||
|
||||
|
@ -1093,16 +1259,14 @@ try:
|
|||
except ImportError:
|
||||
RaceRom = None
|
||||
|
||||
def get_race_rom_patches(rom):
|
||||
patches = {str(0x180213): [0x01, 0x00]} # Tournament Seed
|
||||
def patch_race_rom(rom):
|
||||
rom.write_bytes(0x180213, [0x01, 0x00]) # Tournament Seed
|
||||
|
||||
if 'RaceRom' in sys.modules:
|
||||
RaceRom.encrypt(rom, patches)
|
||||
|
||||
return patches
|
||||
RaceRom.encrypt(rom)
|
||||
|
||||
def write_custom_shops(rom, world, player):
|
||||
shops = [shop for shop in world.shops if shop.replaceable and shop.active and shop.region.player == player]
|
||||
shops = [shop for shop in world.shops if shop.custom and shop.region.player == player]
|
||||
|
||||
shop_data = bytearray()
|
||||
items_data = bytearray()
|
||||
|
@ -1150,7 +1314,9 @@ def hud_format_text(text):
|
|||
return output[:32]
|
||||
|
||||
|
||||
def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite, 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)
|
||||
|
||||
# enable instant item menu
|
||||
if fastmenu == 'instant':
|
||||
|
@ -1176,119 +1342,13 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr
|
|||
|
||||
rom.write_byte(0x18004B, 0x01 if quickswap else 0x00)
|
||||
|
||||
music_volumes = [
|
||||
(0x00, [0xD373B, 0xD375B, 0xD90F8]),
|
||||
(0x14, [0xDA710, 0xDA7A4, 0xDA7BB, 0xDA7D2]),
|
||||
(0x3C, [0xD5954, 0xD653B, 0xDA736, 0xDA752, 0xDA772, 0xDA792]),
|
||||
(0x50, [0xD5B47, 0xD5B5E]),
|
||||
(0x54, [0xD4306]),
|
||||
(0x64,
|
||||
[0xD6878, 0xD6883, 0xD6E48, 0xD6E76, 0xD6EFB, 0xD6F2D, 0xDA211, 0xDA35B, 0xDA37B, 0xDA38E, 0xDA39F, 0xDA5C3,
|
||||
0xDA691, 0xDA6A8, 0xDA6DF]),
|
||||
(0x78,
|
||||
[0xD2349, 0xD3F45, 0xD42EB, 0xD48B9, 0xD48FF, 0xD543F, 0xD5817, 0xD5957, 0xD5ACB, 0xD5AE8, 0xD5B4A, 0xDA5DE,
|
||||
0xDA608, 0xDA635,
|
||||
0xDA662, 0xDA71F, 0xDA7AF, 0xDA7C6, 0xDA7DD]),
|
||||
(0x82, [0xD2F00, 0xDA3D5]),
|
||||
(0xA0,
|
||||
[0xD249C, 0xD24CD, 0xD2C09, 0xD2C53, 0xD2CAF, 0xD2CEB, 0xD2D91, 0xD2EE6, 0xD38ED, 0xD3C91, 0xD3CD3, 0xD3CE8,
|
||||
0xD3F0C,
|
||||
0xD3F82, 0xD405F, 0xD4139, 0xD4198, 0xD41D5, 0xD41F6, 0xD422B, 0xD4270, 0xD42B1, 0xD4334, 0xD4371, 0xD43A6,
|
||||
0xD43DB,
|
||||
0xD441E, 0xD4597, 0xD4B3C, 0xD4BAB, 0xD4C03, 0xD4C53, 0xD4C7F, 0xD4D9C, 0xD5424, 0xD65D2, 0xD664F, 0xD6698,
|
||||
0xD66FF,
|
||||
0xD6985, 0xD6C5C, 0xD6C6F, 0xD6C8E, 0xD6CB4, 0xD6D7D, 0xD827D, 0xD960C, 0xD9828, 0xDA233, 0xDA3A2, 0xDA49E,
|
||||
0xDA72B,
|
||||
0xDA745, 0xDA765, 0xDA785, 0xDABF6, 0xDAC0D, 0xDAEBE, 0xDAFAC]),
|
||||
(0xAA, [0xD9A02, 0xD9BD6]),
|
||||
(0xB4,
|
||||
[0xD21CD, 0xD2279, 0xD2E66, 0xD2E70, 0xD2EAB, 0xD3B97, 0xD3BAC, 0xD3BE8, 0xD3C0D, 0xD3C39, 0xD3C68, 0xD3C9F,
|
||||
0xD3CBC,
|
||||
0xD401E, 0xD4290, 0xD443E, 0xD456F, 0xD47D3, 0xD4D43, 0xD4DCC, 0xD4EBA, 0xD4F0B, 0xD4FE5, 0xD5012, 0xD54BC,
|
||||
0xD54D5,
|
||||
0xD54F0, 0xD5509, 0xD57D8, 0xD59B9, 0xD5A2F, 0xD5AEB, 0xD5E5E, 0xD5FE9, 0xD658F, 0xD674A, 0xD6827, 0xD69D6,
|
||||
0xD69F5,
|
||||
0xD6A05, 0xD6AE9, 0xD6DCF, 0xD6E20, 0xD6ECB, 0xD71D4, 0xD71E6, 0xD7203, 0xD721E, 0xD8724, 0xD8732, 0xD9652,
|
||||
0xD9698,
|
||||
0xD9CBC, 0xD9DC0, 0xD9E49, 0xDAA68, 0xDAA77, 0xDAA88, 0xDAA99, 0xDAF04]),
|
||||
(0x8c,
|
||||
[0xD1D28, 0xD1D41, 0xD1D5C, 0xD1D77, 0xD1EEE, 0xD311D, 0xD31D1, 0xD4148, 0xD5543, 0xD5B6F, 0xD65B3, 0xD6760,
|
||||
0xD6B6B,
|
||||
0xD6DF6, 0xD6E0D, 0xD73A1, 0xD814C, 0xD825D, 0xD82BE, 0xD8340, 0xD8394, 0xD842C, 0xD8796, 0xD8903, 0xD892A,
|
||||
0xD91E8,
|
||||
0xD922B, 0xD92E0, 0xD937E, 0xD93C1, 0xDA958, 0xDA971, 0xDA98C, 0xDA9A7]),
|
||||
(0xC8,
|
||||
[0xD1D92, 0xD1DBD, 0xD1DEB, 0xD1F5D, 0xD1F9F, 0xD1FBD, 0xD1FDC, 0xD1FEA, 0xD20CA, 0xD21BB, 0xD22C9, 0xD2754,
|
||||
0xD284C,
|
||||
0xD2866, 0xD2887, 0xD28A0, 0xD28BA, 0xD28DB, 0xD28F4, 0xD293E, 0xD2BF3, 0xD2C1F, 0xD2C69, 0xD2CA1, 0xD2CC5,
|
||||
0xD2D05,
|
||||
0xD2D73, 0xD2DAF, 0xD2E3D, 0xD2F36, 0xD2F46, 0xD2F6F, 0xD2FCF, 0xD2FDF, 0xD302B, 0xD3086, 0xD3099, 0xD30A5,
|
||||
0xD30CD,
|
||||
0xD30F6, 0xD3154, 0xD3184, 0xD333A, 0xD33D9, 0xD349F, 0xD354A, 0xD35E5, 0xD3624, 0xD363C, 0xD3672, 0xD3691,
|
||||
0xD36B4,
|
||||
0xD36C6, 0xD3724, 0xD3767, 0xD38CB, 0xD3B1D, 0xD3B2F, 0xD3B55, 0xD3B70, 0xD3B81, 0xD3BBF, 0xD3F65, 0xD3FA6,
|
||||
0xD404F,
|
||||
0xD4087, 0xD417A, 0xD41A0, 0xD425C, 0xD4319, 0xD433C, 0xD43EF, 0xD440C, 0xD4452, 0xD4494, 0xD44B5, 0xD4512,
|
||||
0xD45D1,
|
||||
0xD45EF, 0xD4682, 0xD46C3, 0xD483C, 0xD4848, 0xD4855, 0xD4862, 0xD486F, 0xD487C, 0xD4A1C, 0xD4A3B, 0xD4A60,
|
||||
0xD4B27,
|
||||
0xD4C7A, 0xD4D12, 0xD4D81, 0xD4E90, 0xD4ED6, 0xD4EE2, 0xD5005, 0xD502E, 0xD503C, 0xD5081, 0xD51B1, 0xD51C7,
|
||||
0xD51CF,
|
||||
0xD51EF, 0xD520C, 0xD5214, 0xD5231, 0xD5257, 0xD526D, 0xD5275, 0xD52AF, 0xD52BD, 0xD52CD, 0xD52DB, 0xD549C,
|
||||
0xD5801,
|
||||
0xD58A4, 0xD5A68, 0xD5A7F, 0xD5C12, 0xD5D71, 0xD5E10, 0xD5E9A, 0xD5F8B, 0xD5FA4, 0xD651A, 0xD6542, 0xD65ED,
|
||||
0xD661D,
|
||||
0xD66D7, 0xD6776, 0xD68BD, 0xD68E5, 0xD6956, 0xD6973, 0xD69A8, 0xD6A51, 0xD6A86, 0xD6B96, 0xD6C3E, 0xD6D4A,
|
||||
0xD6E9C,
|
||||
0xD6F80, 0xD717E, 0xD7190, 0xD71B9, 0xD811D, 0xD8139, 0xD816B, 0xD818A, 0xD819E, 0xD81BE, 0xD829C, 0xD82E1,
|
||||
0xD8306,
|
||||
0xD830E, 0xD835E, 0xD83AB, 0xD83CA, 0xD83F0, 0xD83F8, 0xD844B, 0xD8479, 0xD849E, 0xD84CB, 0xD84EB, 0xD84F3,
|
||||
0xD854A,
|
||||
0xD8573, 0xD859D, 0xD85B4, 0xD85CE, 0xD862A, 0xD8681, 0xD87E3, 0xD87FF, 0xD887B, 0xD88C6, 0xD88E3, 0xD8944,
|
||||
0xD897B,
|
||||
0xD8C97, 0xD8CA4, 0xD8CB3, 0xD8CC2, 0xD8CD1, 0xD8D01, 0xD917B, 0xD918C, 0xD919A, 0xD91B5, 0xD91D0, 0xD91DD,
|
||||
0xD9220,
|
||||
0xD9273, 0xD9284, 0xD9292, 0xD92AD, 0xD92C8, 0xD92D5, 0xD9311, 0xD9322, 0xD9330, 0xD934B, 0xD9366, 0xD9373,
|
||||
0xD93B6,
|
||||
0xD97A6, 0xD97C2, 0xD97DC, 0xD97FB, 0xD9811, 0xD98FF, 0xD996F, 0xD99A8, 0xD99D5, 0xD9A30, 0xD9A4E, 0xD9A6B,
|
||||
0xD9A88,
|
||||
0xD9AF7, 0xD9B1D, 0xD9B43, 0xD9B7C, 0xD9BA9, 0xD9C84, 0xD9C8D, 0xD9CAC, 0xD9CE8, 0xD9CF3, 0xD9CFD, 0xD9D46,
|
||||
0xDA35E,
|
||||
0xDA37E, 0xDA391, 0xDA478, 0xDA4C3, 0xDA4D7, 0xDA4F6, 0xDA515, 0xDA6E2, 0xDA9C2, 0xDA9ED, 0xDAA1B, 0xDAA57,
|
||||
0xDABAF,
|
||||
0xDABC9, 0xDABE2, 0xDAC28, 0xDAC46, 0xDAC63, 0xDACB8, 0xDACEC, 0xDAD08, 0xDAD25, 0xDAD42, 0xDAD5F, 0xDAE17,
|
||||
0xDAE34,
|
||||
0xDAE51, 0xDAF2E, 0xDAF55, 0xDAF6B, 0xDAF81, 0xDB14F, 0xDB16B, 0xDB180, 0xDB195, 0xDB1AA]),
|
||||
(0xD2, [0xD2B88, 0xD364A, 0xD369F, 0xD3747]),
|
||||
(0xDC,
|
||||
[0xD213F, 0xD2174, 0xD229E, 0xD2426, 0xD4731, 0xD4753, 0xD4774, 0xD4795, 0xD47B6, 0xD4AA5, 0xD4AE4, 0xD4B96,
|
||||
0xD4CA5,
|
||||
0xD5477, 0xD5A3D, 0xD6566, 0xD672C, 0xD67C0, 0xD69B8, 0xD6AB1, 0xD6C05, 0xD6DB3, 0xD71AB, 0xD8E2D, 0xD8F0D,
|
||||
0xD94E0,
|
||||
0xD9544, 0xD95A8, 0xD9982, 0xD9B56, 0xDA694, 0xDA6AB, 0xDAE88, 0xDAEC8, 0xDAEE6, 0xDB1BF]),
|
||||
(0xE6, [0xD210A, 0xD22DC, 0xD2447, 0xD5A4D, 0xD5DDC, 0xDA251, 0xDA26C]),
|
||||
(0xF0, [0xD945E, 0xD967D, 0xD96C2, 0xD9C95, 0xD9EE6, 0xDA5C6]),
|
||||
(0xFA,
|
||||
[0xD2047, 0xD24C2, 0xD24EC, 0xD25A4, 0xD51A8, 0xD51E6, 0xD524E, 0xD529E, 0xD6045, 0xD81DE, 0xD821E, 0xD94AA,
|
||||
0xD9A9E,
|
||||
0xD9AE4, 0xDA289]),
|
||||
(0xFF, [0xD2085, 0xD21C5, 0xD5F28])
|
||||
]
|
||||
for volume, addresses in music_volumes:
|
||||
for address in addresses:
|
||||
rom.write_byte(address, volume if not disable_music else 0x00)
|
||||
rom.write_byte(0x0CFE18, 0x00 if disable_music else rom.orig_buffer[0x0CFE18] if rom.orig_buffer else 0x70)
|
||||
rom.write_byte(0x0CFEC1, 0x00 if disable_music else rom.orig_buffer[0x0CFEC1] if rom.orig_buffer else 0xC0)
|
||||
rom.write_bytes(0x0D0000, [0x00, 0x00] if disable_music else rom.orig_buffer[0x0D0000:0x0D0002] if rom.orig_buffer else [0xDA, 0x58])
|
||||
rom.write_bytes(0x0D00E7, [0xC4, 0x58] if disable_music else rom.orig_buffer[0x0D00E7:0x0D00E9] if rom.orig_buffer else [0xDA, 0x58])
|
||||
|
||||
rom.write_byte(0x18021A, 1 if disable_music else 0x00)
|
||||
|
||||
# restore Mirror sound effect volumes (for existing seeds that lack it)
|
||||
rom.write_byte(0xD3E04, 0xC8)
|
||||
rom.write_byte(0xD3DC6, 0xC8)
|
||||
rom.write_byte(0xD3D6E, 0xC8)
|
||||
rom.write_byte(0xD3D34, 0xC8)
|
||||
rom.write_byte(0xD3D55, 0xC8)
|
||||
rom.write_byte(0xD3E38, 0xC8)
|
||||
rom.write_byte(0xD3DAA, 0xFA)
|
||||
|
||||
# set heart beep rate
|
||||
rom.write_byte(0x180033, {'off': 0x00, 'half': 0x40, 'quarter': 0x80, 'normal': 0x20, 'double': 0x10}[beep])
|
||||
|
||||
|
@ -1311,10 +1371,17 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr
|
|||
if sprite is not None:
|
||||
write_sprite(rom, sprite)
|
||||
|
||||
# set player names
|
||||
for player, name in names.items():
|
||||
if 0 < player <= 64:
|
||||
rom.write_bytes(0x186380 + ((player - 1) * 32), hud_format_text(name))
|
||||
default_ow_palettes(rom)
|
||||
if ow_palettes == 'random':
|
||||
randomize_ow_palettes(rom)
|
||||
elif ow_palettes == 'blackout':
|
||||
blackout_ow_palettes(rom)
|
||||
|
||||
default_uw_palettes(rom)
|
||||
if uw_palettes == 'random':
|
||||
randomize_uw_palettes(rom)
|
||||
elif uw_palettes == 'blackout':
|
||||
blackout_uw_palettes(rom)
|
||||
|
||||
if isinstance(rom, LocalRom):
|
||||
rom.write_crc()
|
||||
|
@ -1327,13 +1394,140 @@ def write_sprite(rom, sprite):
|
|||
rom.write_bytes(0xDD308, sprite.palette)
|
||||
rom.write_bytes(0xDEDF5, sprite.glove_palette)
|
||||
|
||||
def set_color(rom, address, color, shade):
|
||||
r = round(min(color[0], 0xFF) * pow(0.8, shade) * 0x1F / 0xFF)
|
||||
g = round(min(color[1], 0xFF) * pow(0.8, shade) * 0x1F / 0xFF)
|
||||
b = round(min(color[2], 0xFF) * pow(0.8, shade) * 0x1F / 0xFF)
|
||||
|
||||
rom.write_bytes(address, ((b << 10) | (g << 5) | (r << 0)).to_bytes(2, byteorder='little', signed=False))
|
||||
|
||||
def default_ow_palettes(rom):
|
||||
if not rom.orig_buffer:
|
||||
return
|
||||
rom.write_bytes(0xDE604, rom.orig_buffer[0xDE604:0xDEBB4])
|
||||
|
||||
for address in [0x067FB4, 0x067F94, 0x067FC6, 0x067FE6, 0x067FE1, 0x05FEA9, 0x05FEB3]:
|
||||
rom.write_bytes(address, rom.orig_buffer[address:address+2])
|
||||
|
||||
def randomize_ow_palettes(rom):
|
||||
grass, grass2, grass3, dirt, dirt2, water, clouds, dwdirt,\
|
||||
dwgrass, dwwater, dwdmdirt, dwdmgrass, dwdmclouds1, dwdmclouds2 = [[random.randint(60, 215) for _ in range(3)] for _ in range(14)]
|
||||
dwtree = [c + random.randint(-20, 10) for c in dwgrass]
|
||||
treeleaf = [c + random.randint(-20, 10) for c in grass]
|
||||
|
||||
patches = {0x067FB4: (grass, 0), 0x067F94: (grass, 0), 0x067FC6: (grass, 0), 0x067FE6: (grass, 0), 0x067FE1: (grass, 3), 0x05FEA9: (grass, 0), 0x05FEB3: (dwgrass, 1),
|
||||
0x0DD4AC: (grass, 2), 0x0DE6DE: (grass2, 2), 0x0DE6E0: (grass2, 1), 0x0DD4AE: (grass2, 1), 0x0DE9FA: (grass2, 1), 0x0DEA0E: (grass2, 1), 0x0DE9FE: (grass2, 0),
|
||||
0x0DD3D2: (grass2, 2), 0x0DE88C: (grass2, 2), 0x0DE8A8: (grass2, 2), 0x0DE9F8: (grass2, 2), 0x0DEA4E: (grass2, 2), 0x0DEAF6: (grass2, 2), 0x0DEB2E: (grass2, 2), 0x0DEB4A: (grass2, 2),
|
||||
0x0DE892: (grass, 1), 0x0DE886: (grass, 0), 0x0DE6D2: (grass, 0), 0x0DE6FA: (grass, 3), 0x0DE6FC: (grass, 0), 0x0DE6FE: (grass, 0), 0x0DE70A: (grass, 0), 0x0DE708: (grass, 2), 0x0DE70C: (grass, 1),
|
||||
0x0DE6D4: (dirt, 2), 0x0DE6CA: (dirt, 5), 0x0DE6CC: (dirt, 4), 0x0DE6CE: (dirt, 3), 0x0DE6E2: (dirt, 2), 0x0DE6D8: (dirt, 5), 0x0DE6DA: (dirt, 4), 0x0DE6DC: (dirt, 2),
|
||||
0x0DE6F0: (dirt, 2), 0x0DE6E6: (dirt, 5), 0x0DE6E8: (dirt, 4), 0x0DE6EA: (dirt, 2), 0x0DE6EC: (dirt, 4), 0x0DE6EE: (dirt, 2),
|
||||
0x0DE91E: (grass, 0),
|
||||
0x0DE920: (dirt, 2), 0x0DE916: (dirt, 3), 0x0DE934: (dirt, 3),
|
||||
0x0DE92C: (grass, 0), 0x0DE93A: (grass, 0), 0x0DE91C: (grass, 1), 0x0DE92A: (grass, 1), 0x0DEA1C: (grass, 0), 0x0DEA2A: (grass, 0), 0x0DEA30: (grass, 0),
|
||||
0x0DEA2E: (dirt, 5),
|
||||
0x0DE884: (grass, 3), 0x0DE8AE: (grass, 3), 0x0DE8BE: (grass, 3), 0x0DE8E4: (grass, 3), 0x0DE938: (grass, 3), 0x0DE9C4: (grass, 3), 0x0DE6D0: (grass, 4),
|
||||
0x0DE890: (treeleaf, 1), 0x0DE894: (treeleaf, 0),
|
||||
0x0DE924: (water, 3), 0x0DE668: (water, 3), 0x0DE66A: (water, 2), 0x0DE670: (water, 1), 0x0DE918: (water, 1), 0x0DE66C: (water, 0), 0x0DE91A: (water, 0), 0x0DE92E: (water, 1), 0x0DEA1A: (water, 1), 0x0DEA16: (water, 3), 0x0DEA10: (water, 4),
|
||||
0x0DE66E: (dirt, 3), 0x0DE672: (dirt, 2), 0x0DE932: (dirt, 4), 0x0DE936: (dirt, 2), 0x0DE93C: (dirt, 1),
|
||||
0x0DE756: (dirt2, 4), 0x0DE764: (dirt2, 4), 0x0DE772: (dirt2, 4), 0x0DE994: (dirt2, 4), 0x0DE9A2: (dirt2, 4), 0x0DE758: (dirt2, 3), 0x0DE766: (dirt2, 3), 0x0DE774: (dirt2, 3),
|
||||
0x0DE996: (dirt2, 3), 0x0DE9A4: (dirt2, 3), 0x0DE75A: (dirt2, 2), 0x0DE768: (dirt2, 2), 0x0DE776: (dirt2, 2), 0x0DE778: (dirt2, 2), 0x0DE998: (dirt2, 2), 0x0DE9A6: (dirt2, 2),
|
||||
0x0DE9AC: (dirt2, 1), 0x0DE99E: (dirt2, 1), 0x0DE760: (dirt2, 1), 0x0DE77A: (dirt2, 1), 0x0DE77C: (dirt2, 1), 0x0DE798: (dirt2, 1), 0x0DE980: (dirt2, 1),
|
||||
0x0DE75C: (grass3, 2), 0x0DE786: (grass3, 2), 0x0DE794: (grass3, 2), 0x0DE99A: (grass3, 2), 0x0DE75E: (grass3, 1), 0x0DE788: (grass3, 1), 0x0DE796: (grass3, 1), 0x0DE99C: (grass3, 1),
|
||||
0x0DE76A: (clouds, 2), 0x0DE9A8: (clouds, 2), 0x0DE76E: (clouds, 0), 0x0DE9AA: (clouds, 0), 0x0DE8DA: (clouds, 0), 0x0DE8D8: (clouds, 0), 0x0DE8D0: (clouds, 0), 0x0DE98C: (clouds, 2), 0x0DE990: (clouds, 0),
|
||||
0x0DEB34: (dwtree, 4), 0x0DEB30: (dwtree, 3), 0x0DEB32: (dwtree, 1),
|
||||
0x0DE710: (dwdirt, 5), 0x0DE71E: (dwdirt, 5), 0x0DE72C: (dwdirt, 5), 0x0DEAD6: (dwdirt, 5), 0x0DE712: (dwdirt, 4), 0x0DE720: (dwdirt, 4), 0x0DE72E: (dwdirt, 4), 0x0DE660: (dwdirt, 4),
|
||||
0x0DEAD8: (dwdirt, 4), 0x0DEADA: (dwdirt, 3), 0x0DE714: (dwdirt, 3), 0x0DE722: (dwdirt, 3), 0x0DE730: (dwdirt, 3), 0x0DE732: (dwdirt, 3), 0x0DE734: (dwdirt, 2), 0x0DE736: (dwdirt, 2),
|
||||
0x0DE728: (dwdirt, 2), 0x0DE71A: (dwdirt, 2), 0x0DE664: (dwdirt, 2), 0x0DEAE0: (dwdirt, 2),
|
||||
0x0DE716: (dwgrass, 3), 0x0DE740: (dwgrass, 3), 0x0DE74E: (dwgrass, 3), 0x0DEAC0: (dwgrass, 3), 0x0DEACE: (dwgrass, 3), 0x0DEADC: (dwgrass, 3), 0x0DEB24: (dwgrass, 3), 0x0DE752: (dwgrass, 2),
|
||||
0x0DE718: (dwgrass, 1), 0x0DE742: (dwgrass, 1), 0x0DE750: (dwgrass, 1), 0x0DEB26: (dwgrass, 1), 0x0DEAC2: (dwgrass, 1), 0x0DEAD0: (dwgrass, 1), 0x0DEADE: (dwgrass, 1),
|
||||
0x0DE65A: (dwwater, 5), 0x0DE65C: (dwwater, 3), 0x0DEAC8: (dwwater, 3), 0x0DEAD2: (dwwater, 2), 0x0DEABC: (dwwater, 2), 0x0DE662: (dwwater, 2), 0x0DE65E: (dwwater, 1), 0x0DEABE: (dwwater, 1), 0x0DEA98: (dwwater, 2),
|
||||
0x0DE79A: (dwdmdirt, 6), 0x0DE7A8: (dwdmdirt, 6), 0x0DE7B6: (dwdmdirt, 6), 0x0DEB60: (dwdmdirt, 6), 0x0DEB6E: (dwdmdirt, 6), 0x0DE93E: (dwdmdirt, 6), 0x0DE94C: (dwdmdirt, 6), 0x0DEBA6: (dwdmdirt, 6),
|
||||
0x0DE79C: (dwdmdirt, 4), 0x0DE7AA: (dwdmdirt, 4), 0x0DE7B8: (dwdmdirt, 4), 0x0DEB70: (dwdmdirt, 4), 0x0DEBA8: (dwdmdirt, 4), 0x0DEB72: (dwdmdirt, 3), 0x0DEB74: (dwdmdirt, 3), 0x0DE79E: (dwdmdirt, 3), 0x0DE7AC: (dwdmdirt, 3), 0x0DEBAA: (dwdmdirt, 3), 0x0DE7A0: (dwdmdirt, 3),
|
||||
0x0DE7BC: (dwdmgrass, 3),
|
||||
0x0DEBAC: (dwdmdirt, 2), 0x0DE7AE: (dwdmdirt, 2), 0x0DE7C2: (dwdmdirt, 2), 0x0DE7A6: (dwdmdirt, 2), 0x0DEB7A: (dwdmdirt, 2), 0x0DEB6C: (dwdmdirt, 2), 0x0DE7C0: (dwdmdirt, 2),
|
||||
0x0DE7A2: (dwdmgrass, 3), 0x0DE7BE: (dwdmgrass, 3), 0x0DE7CC: (dwdmgrass, 3), 0x0DE7DA: (dwdmgrass, 3), 0x0DEB6A: (dwdmgrass, 3), 0x0DE948: (dwdmgrass, 3), 0x0DE956: (dwdmgrass, 3), 0x0DE964: (dwdmgrass, 3), 0x0DE7CE: (dwdmgrass, 1), 0x0DE7A4: (dwdmgrass, 1), 0x0DEBA2: (dwdmgrass, 1), 0x0DEBB0: (dwdmgrass, 1),
|
||||
0x0DE644: (dwdmclouds1, 2), 0x0DEB84: (dwdmclouds1, 2), 0x0DE648: (dwdmclouds1, 1), 0x0DEB88: (dwdmclouds1, 1),
|
||||
0x0DEBAE: (dwdmclouds2, 2), 0x0DE7B0: (dwdmclouds2, 2), 0x0DE7B4: (dwdmclouds2, 0), 0x0DEB78: (dwdmclouds2, 0), 0x0DEBB2: (dwdmclouds2, 0)
|
||||
}
|
||||
for address, (color, shade) in patches.items():
|
||||
set_color(rom, address, color, shade)
|
||||
|
||||
def blackout_ow_palettes(rom):
|
||||
rom.write_bytes(0xDE604, [0] * 0xC4)
|
||||
for i in range(0xDE6C8, 0xDE86C, 70):
|
||||
rom.write_bytes(i, [0] * 64)
|
||||
rom.write_bytes(i+66, [0] * 4)
|
||||
rom.write_bytes(0xDE86C, [0] * 0x348)
|
||||
|
||||
for address in [0x067FB4, 0x067F94, 0x067FC6, 0x067FE6, 0x067FE1, 0x05FEA9, 0x05FEB3]:
|
||||
rom.write_bytes(address, [0,0])
|
||||
|
||||
def default_uw_palettes(rom):
|
||||
if not rom.orig_buffer:
|
||||
return
|
||||
rom.write_bytes(0xDD734, rom.orig_buffer[0xDD734:0xDE544])
|
||||
|
||||
def randomize_uw_palettes(rom):
|
||||
for dungeon in range(20):
|
||||
wall, pot, chest, floor1, floor2, floor3 = [[random.randint(60, 240) for _ in range(3)] for _ in range(6)]
|
||||
|
||||
for i in range(5):
|
||||
shade = 10 - (i * 2)
|
||||
set_color(rom, 0x0DD734 + (0xB4 * dungeon) + (i * 2), wall, shade)
|
||||
set_color(rom, 0x0DD770 + (0xB4 * dungeon) + (i * 2), wall, shade)
|
||||
set_color(rom, 0x0DD744 + (0xB4 * dungeon) + (i * 2), wall, shade)
|
||||
if dungeon == 0:
|
||||
set_color(rom, 0x0DD7CA + (0xB4 * dungeon) + (i * 2), wall, shade)
|
||||
|
||||
if dungeon == 2:
|
||||
set_color(rom, 0x0DD74E + (0xB4 * dungeon), wall, 3)
|
||||
set_color(rom, 0x0DD750 + (0xB4 * dungeon), wall, 5)
|
||||
set_color(rom, 0x0DD73E + (0xB4 * dungeon), wall, 3)
|
||||
set_color(rom, 0x0DD740 + (0xB4 * dungeon), wall, 5)
|
||||
|
||||
set_color(rom, 0x0DD7E4 + (0xB4 * dungeon), wall, 4)
|
||||
set_color(rom, 0x0DD7E6 + (0xB4 * dungeon), wall, 2)
|
||||
|
||||
set_color(rom, 0xDD7DA + (0xB4 * dungeon), wall, 10)
|
||||
set_color(rom, 0xDD7DC + (0xB4 * dungeon), wall, 8)
|
||||
|
||||
set_color(rom, 0x0DD75A + (0xB4 * dungeon), pot, 7)
|
||||
set_color(rom, 0x0DD75C + (0xB4 * dungeon), pot, 1)
|
||||
set_color(rom, 0x0DD75E + (0xB4 * dungeon), pot, 3)
|
||||
|
||||
set_color(rom, 0x0DD76A + (0xB4 * dungeon), wall, 7)
|
||||
set_color(rom, 0x0DD76C + (0xB4 * dungeon), wall, 2)
|
||||
set_color(rom, 0x0DD76E + (0xB4 * dungeon), wall, 4)
|
||||
|
||||
set_color(rom, 0x0DD7AE + (0xB4 * dungeon), chest, 2)
|
||||
set_color(rom, 0x0DD7B0 + (0xB4 * dungeon), chest, 0)
|
||||
|
||||
for i in range(3):
|
||||
shade = 6 - (i * 2)
|
||||
set_color(rom, 0x0DD764 + (0xB4 * dungeon) + (i * 2), floor1, shade)
|
||||
set_color(rom, 0x0DD782 + (0xB4 * dungeon) + (i * 2), floor1, shade + 3)
|
||||
|
||||
set_color(rom, 0x0DD7A0 + (0xB4 * dungeon) + (i * 2), floor2, shade)
|
||||
set_color(rom, 0x0DD7BE + (0xB4 * dungeon) + (i * 2), floor2, shade + 3)
|
||||
|
||||
set_color(rom, 0x0DD7E2 + (0xB4 * dungeon), floor3, 3)
|
||||
set_color(rom, 0x0DD796 + (0xB4 * dungeon), floor3, 4)
|
||||
|
||||
def blackout_uw_palettes(rom):
|
||||
for i in range(0xDD734, 0xDE544, 180):
|
||||
rom.write_bytes(i, [0] * 38)
|
||||
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()
|
||||
|
||||
|
@ -1351,11 +1545,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.
|
||||
|
@ -2096,3 +2290,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"
|
||||
]
|
||||
|
|
1
Rules.py
1
Rules.py
|
@ -918,7 +918,6 @@ def overworld_glitches_rules(world, player):
|
|||
for spot in mini_moldorm_cave.locations:
|
||||
add_rule(world.get_location(spot, player), superbunny_mirror_sword, 'or')
|
||||
add_rule(world.get_location('Spiral Cave', player), superbunny_mirror_sword, 'or')
|
||||
>>>>>>> c677a875f2a124bd0930467b2952e0587d24839b
|
||||
add_conditional_lamps(world, player)
|
||||
|
||||
|
||||
|
|
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)
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue