Files
Grinch-AP/Plando.py
Bonta-kun ad278f91d6 Multiworld: clients will now be automatically be identified from the rom name and have their names and teams set by the host, meaning those need to be configured during seed gen
Player names will show up in spoiler log and hint tiles instead of player id
MultiClient: autoreconnect to mw server
2020-01-14 10:42:27 +01:00

237 lines
11 KiB
Python
Executable File

#!/usr/bin/env python3
import argparse
import hashlib
import logging
import os
import random
import time
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, 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
from ItemList import difficulties
from Main import create_playthrough
__version__ = '0.2-dev'
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, False, False, False, None, False)
world.player_names[1].append("Player 1")
logger = logging.getLogger('')
hasher = hashlib.md5()
with open(args.plando, 'rb') as plandofile:
buf = plandofile.read()
hasher.update(buf)
world.seed = int(hasher.hexdigest(), 16) % 1000000000
random.seed(world.seed)
logger.info('ALttP Plandomizer Version %s - Seed: %s\n\n', __version__, args.plando)
world.difficulty_requirements[1] = difficulties[world.difficulty[1]]
create_regions(world, 1)
create_dungeons(world, 1)
link_entrances(world, 1)
logger.info('Calculating Access Rules.')
set_rules(world, 1)
logger.info('Fill the world.')
text_patches = []
fill_world(world, args.plando, text_patches)
if world.get_entrance('Dam', 1).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', 1).connected_region.name != 'Swamp Palace (Entrance)':
world.swamp_patch_required[1] = True
logger.info('Calculating playthrough.')
try:
create_playthrough(world)
except RuntimeError:
if args.ignore_unsolvable:
pass
else:
raise
logger.info('Patching ROM.')
rom = LocalRom(args.rom)
patch_rom(world, rom, 1, 1, False)
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, args.sprite, args.ow_palettes, args.uw_palettes)
for textname, texttype, text in text_patches:
if texttype == 'text':
write_string_to_rom(rom, textname, text)
#elif texttype == 'credit':
# write_credits_string_to_rom(rom, textname, text)
outfilebase = 'Plando_%s_%s' % (os.path.splitext(os.path.basename(args.plando))[0], world.seed)
rom.write_to_file('%s.sfc' % outfilebase)
if args.create_spoiler:
world.spoiler.to_file('%s_Spoiler.txt' % outfilebase)
logger.info('Done. Enjoy.')
logger.debug('Total Time: %s', time.process_time() - start_time)
return world
def fill_world(world, plando, text_patches):
mm_medallion = 'Ether'
tr_medallion = 'Quake'
logger = logging.getLogger('')
with open(plando, 'r') as plandofile:
for line in plandofile.readlines():
if line.startswith('#'):
continue
if ':' in line:
line = line.lstrip()
if line.startswith('!'):
if line.startswith('!mm_medallion'):
_, medallionstr = line.split(':', 1)
mm_medallion = medallionstr.strip()
elif line.startswith('!tr_medallion'):
_, medallionstr = line.split(':', 1)
tr_medallion = medallionstr.strip()
elif line.startswith('!mode'):
_, modestr = line.split(':', 1)
world.mode = {1: modestr.strip()}
elif line.startswith('!logic'):
_, logicstr = line.split(':', 1)
world.logic = {1: logicstr.strip()}
elif line.startswith('!goal'):
_, goalstr = line.split(':', 1)
world.goal = {1: goalstr.strip()}
elif line.startswith('!light_cone_sewers'):
_, sewerstr = line.split(':', 1)
world.sewer_light_cone = {1: sewerstr.strip().lower() == 'true'}
elif line.startswith('!light_cone_lw'):
_, lwconestr = line.split(':', 1)
world.light_world_light_cone = lwconestr.strip().lower() == 'true'
elif line.startswith('!light_cone_dw'):
_, dwconestr = line.split(':', 1)
world.dark_world_light_cone = dwconestr.strip().lower() == 'true'
elif line.startswith('!fix_trock_doors'):
_, trdstr = line.split(':', 1)
world.fix_trock_doors = {1: trdstr.strip().lower() == 'true'}
elif line.startswith('!fix_trock_exit'):
_, trfstr = line.split(':', 1)
world.fix_trock_exit = {1: trfstr.strip().lower() == 'true'}
elif line.startswith('!fix_gtower_exit'):
_, gtfstr = line.split(':', 1)
world.fix_gtower_exit = gtfstr.strip().lower() == 'true'
elif line.startswith('!fix_pod_exit'):
_, podestr = line.split(':', 1)
world.fix_palaceofdarkness_exit = {1: podestr.strip().lower() == 'true'}
elif line.startswith('!fix_skullwoods_exit'):
_, swestr = line.split(':', 1)
world.fix_skullwoods_exit = {1: swestr.strip().lower() == 'true'}
elif line.startswith('!check_beatable_only'):
_, chkbtstr = line.split(':', 1)
world.check_beatable_only = chkbtstr.strip().lower() == 'true'
elif line.startswith('!ganon_death_pyramid_respawn'):
_, gnpstr = line.split(':', 1)
world.ganon_at_pyramid = gnpstr.strip().lower() == 'true'
elif line.startswith('!save_quit_boss'):
_, sqbstr = line.split(':', 1)
world.save_and_quite_from_boss = sqbstr.strip().lower() == 'true'
elif line.startswith('!text_'):
textname, text = line.split(':', 1)
text_patches.append([textname.lstrip('!text_').strip(), 'text', text.strip()])
#temporarilly removed. New credits system not ready to handle this.
#elif line.startswith('!credits_'):
# textname, text = line.split(':', 1)
# text_patches.append([textname.lstrip('!credits_').strip(), 'credits', text.strip()])
continue
locationstr, itemstr = line.split(':', 1)
location = world.get_location(locationstr.strip(), 1)
if location is None:
logger.warning('Unknown location: %s', locationstr)
continue
else:
item = ItemFactory(itemstr.strip(), 1)
if item is not None:
world.push_item(location, item)
if item.smallkey or item.bigkey:
location.event = True
elif '<=>' in line:
entrance, exit = line.split('<=>', 1)
connect_two_way(world, entrance.strip(), exit.strip(), 1)
elif '=>' in line:
entrance, exit = line.split('=>', 1)
connect_entrance(world, entrance.strip(), exit.strip(), 1)
elif '<=' in line:
entrance, exit = line.split('<=', 1)
connect_exit(world, exit.strip(), entrance.strip(), 1)
world.required_medallions[1] = (mm_medallion, tr_medallion)
# set up Agahnim Events
world.get_location('Agahnim 1', 1).event = True
world.get_location('Agahnim 1', 1).item = ItemFactory('Beat Agahnim 1', 1)
world.get_location('Agahnim 2', 1).event = True
world.get_location('Agahnim 2', 1).item = ItemFactory('Beat Agahnim 2', 1)
def start():
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--create_spoiler', help='Output a Spoiler File', action='store_true')
parser.add_argument('--ignore_unsolvable', help='Do not abort if seed is deemed unsolvable.', action='store_true')
parser.add_argument('--rom', 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('--seed', help='Define seed number to generate.', type=int)
parser.add_argument('--fastmenu', default='normal', const='normal', nargs='?', choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
help='''\
Select the rate at which the menu opens and closes.
(default: %(default)s)
''')
parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true')
parser.add_argument('--disablemusic', help='Disables game music.', action='store_true')
parser.add_argument('--heartbeep', default='normal', const='normal', nargs='?', choices=['normal', 'half', 'quarter', 'off'],
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()
# 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)
sys.exit(1)
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.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
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[args.loglevel]
logging.basicConfig(format='%(message)s', level=loglevel)
main(args=args)
if __name__ == '__main__':
start()