806 lines
43 KiB
Python
806 lines
43 KiB
Python
#!/usr/bin/python3
|
|
|
|
from Utils import output_path
|
|
import argparse, os.path, json, sys, shutil, random, copy, requests
|
|
|
|
from .rando.RandoSettings import RandoSettings, GraphSettings
|
|
from .rando.RandoExec import RandoExec
|
|
from .graph.graph_utils import GraphUtils, getAccessPoint
|
|
from .utils.parameters import Controller, easy, medium, hard, harder, hardcore, mania, infinity, text2diff, appDir
|
|
from .rom.rom_patches import RomPatches
|
|
from .rom.rompatcher import RomPatcher
|
|
from .utils.utils import PresetLoader, loadRandoPreset, getDefaultMultiValues, getPresetDir
|
|
from .utils.version import displayedVersion
|
|
from .utils.doorsmanager import DoorsManager
|
|
from .logic.logic import Logic
|
|
from .utils.objectives import Objectives
|
|
from .utils.utils import dumpErrorMsg
|
|
|
|
from .utils import log
|
|
from ..Options import StartLocation
|
|
|
|
# we need to know the logic before doing anything else
|
|
def getLogic():
|
|
# check if --logic is there
|
|
logic = 'vanilla'
|
|
for i, param in enumerate(sys.argv):
|
|
if param == '--logic' and i+1 < len(sys.argv):
|
|
logic = sys.argv[i+1]
|
|
return logic
|
|
Logic.factory(getLogic())
|
|
defaultMultiValues = getDefaultMultiValues()
|
|
speeds = defaultMultiValues['progressionSpeed']
|
|
energyQties = defaultMultiValues['energyQty']
|
|
progDiffs = defaultMultiValues['progressionDifficulty']
|
|
morphPlacements = defaultMultiValues['morphPlacement']
|
|
majorsSplits = defaultMultiValues['majorsSplit']
|
|
gravityBehaviours = defaultMultiValues['gravityBehaviour']
|
|
objectives = defaultMultiValues['objective']
|
|
tourians = defaultMultiValues['tourian']
|
|
areaRandomizations = defaultMultiValues['areaRandomization']
|
|
|
|
def randomMulti(args, param, defaultMultiValues):
|
|
value = args[param]
|
|
|
|
isRandom = False
|
|
if value == "random":
|
|
isRandom = True
|
|
if args[param+"List"] is not None:
|
|
# use provided list
|
|
choices = args[param+"List"].split(',')
|
|
value = random.choice(choices)
|
|
else:
|
|
# use default list
|
|
value = random.choice(defaultMultiValues)
|
|
|
|
return (isRandom, value)
|
|
|
|
def dumpErrorMsgs(outFileName, msgs):
|
|
dumpErrorMsg(outFileName, joinErrorMsgs(msgs))
|
|
|
|
def joinErrorMsgs(msgs):
|
|
return '\n'.join(msgs)
|
|
|
|
def restricted_float(x):
|
|
x = float(x)
|
|
if x < 0.0 or x > 9.0:
|
|
raise argparse.ArgumentTypeError("%r not in range [1.0, 9.0]"%(x,))
|
|
return x
|
|
|
|
def to_pascal_case_with_space(snake_str):
|
|
return snake_str.replace("_", " ").title()
|
|
|
|
class VariaRandomizer:
|
|
|
|
parser = argparse.ArgumentParser(description="Random Metroid Randomizer")
|
|
parser.add_argument('--param', '-p', help="the input parameters",
|
|
default=None, dest='paramsFileName')
|
|
parser.add_argument('--dir',
|
|
help="output directory for ROM and dot files",
|
|
dest='directory', nargs='?', default='.')
|
|
parser.add_argument('--dot',
|
|
help="generate dot file with area graph",
|
|
action='store_true',dest='dot', default=False)
|
|
parser.add_argument('--area', help="area mode",
|
|
dest='area', nargs='?', const=True, choices=["random"]+areaRandomizations, default='off')
|
|
parser.add_argument('--areaList', help="list to choose from when random",
|
|
dest='areaList', nargs='?', default=None)
|
|
parser.add_argument('--areaLayoutBase',
|
|
help="use simple layout patch for area mode", action='store_true',
|
|
dest='areaLayoutBase', default=False)
|
|
parser.add_argument('--escapeRando',
|
|
help="Randomize the escape sequence",
|
|
dest='escapeRando', nargs='?', const=True, default=False)
|
|
parser.add_argument('--noRemoveEscapeEnemies',
|
|
help="Do not remove enemies during escape sequence", action='store_true',
|
|
dest='noRemoveEscapeEnemies', default=False)
|
|
parser.add_argument('--bosses', help="randomize bosses",
|
|
dest='bosses', nargs='?', const=True, default=False)
|
|
parser.add_argument('--minimizer', help="minimizer mode: area and boss mixed together. arg is number of non boss locations",
|
|
dest='minimizerN', nargs='?', const=35, default=None,
|
|
choices=[str(i) for i in range(30,101)]+["random"])
|
|
parser.add_argument('--startLocation', help="Name of the Access Point to start from",
|
|
dest='startLocation', nargs='?', default="Landing Site",
|
|
choices=['random'] + GraphUtils.getStartAccessPointNames())
|
|
parser.add_argument('--startLocationList', help="list to choose from when random",
|
|
dest='startLocationList', nargs='?', default=None)
|
|
parser.add_argument('--debug', '-d', help="activate debug logging", dest='debug',
|
|
action='store_true')
|
|
parser.add_argument('--maxDifficulty', '-t',
|
|
help="the maximum difficulty generated seed will be for given parameters",
|
|
dest='maxDifficulty', nargs='?', default=None,
|
|
choices=['easy', 'medium', 'hard', 'harder', 'hardcore', 'mania', 'random'])
|
|
parser.add_argument('--minDifficulty',
|
|
help="the minimum difficulty generated seed will be for given parameters (speedrun prog speed required)",
|
|
dest='minDifficulty', nargs='?', default=None,
|
|
choices=['easy', 'medium', 'hard', 'harder', 'hardcore', 'mania'])
|
|
parser.add_argument('--seed', '-s', help="randomization seed to use", dest='seed',
|
|
nargs='?', default=0, type=int)
|
|
parser.add_argument('--rom', '-r',
|
|
help="the vanilla ROM",
|
|
dest='rom', nargs='?', default=None)
|
|
parser.add_argument('--output',
|
|
help="to choose the name of the generated json (for the webservice)",
|
|
dest='output', nargs='?', default=None)
|
|
parser.add_argument('--preset',
|
|
help="the name of the preset (for the webservice)",
|
|
dest='preset', nargs='?', default=None)
|
|
parser.add_argument('--patch', '-c',
|
|
help="optional patches to add",
|
|
dest='patches', nargs='?', default=[], action='append',
|
|
choices=['itemsounds.ips', 'random_music.ips',
|
|
'fast_doors.ips', 'elevators_speed.ips',
|
|
'spinjumprestart.ips', 'rando_speed.ips', 'No_Music', 'AimAnyButton.ips',
|
|
'max_ammo_display.ips', 'supermetroid_msu1.ips', 'Infinite_Space_Jump',
|
|
'refill_before_save.ips', 'relaxed_round_robin_cf.ips'])
|
|
parser.add_argument('--missileQty', '-m',
|
|
help="quantity of missiles",
|
|
dest='missileQty', nargs='?', default=3,
|
|
type=restricted_float)
|
|
parser.add_argument('--superQty', '-q',
|
|
help="quantity of super missiles",
|
|
dest='superQty', nargs='?', default=2,
|
|
type=restricted_float)
|
|
parser.add_argument('--powerBombQty', '-w',
|
|
help="quantity of power bombs",
|
|
dest='powerBombQty', nargs='?', default=1,
|
|
type=restricted_float)
|
|
parser.add_argument('--minorQty', '-n',
|
|
help="quantity of minors",
|
|
dest='minorQty', nargs='?', default=100,
|
|
choices=[str(i) for i in range(0,101)])
|
|
parser.add_argument('--energyQty', '-g',
|
|
help="quantity of ETanks/Reserve Tanks",
|
|
dest='energyQty', nargs='?', default='vanilla',
|
|
choices=energyQties + ['random'])
|
|
parser.add_argument('--energyQtyList', help="list to choose from when random",
|
|
dest='energyQtyList', nargs='?', default=None)
|
|
parser.add_argument('--strictMinors',
|
|
help="minors quantities values will be strictly followed instead of being probabilities",
|
|
dest='strictMinors', nargs='?', const=True, default=False)
|
|
parser.add_argument('--majorsSplit',
|
|
help="how to split majors/minors: Full, FullWithHUD, Major, Chozo, Scavenger",
|
|
dest='majorsSplit', nargs='?', choices=majorsSplits + ['random'], default='Full')
|
|
parser.add_argument('--majorsSplitList', help="list to choose from when random",
|
|
dest='majorsSplitList', nargs='?', default=None)
|
|
parser.add_argument('--scavNumLocs',
|
|
help="For Scavenger split, number of major locations in the mandatory route",
|
|
dest='scavNumLocs', nargs='?', default=10,
|
|
choices=["0"]+[str(i) for i in range(4,17)])
|
|
parser.add_argument('--scavRandomized',
|
|
help="For Scavenger split, decide whether mandatory major locs will have non-vanilla items",
|
|
dest='scavRandomized', nargs='?', const=True, default=False)
|
|
parser.add_argument('--suitsRestriction',
|
|
help="no suits in early game",
|
|
dest='suitsRestriction', nargs='?', const=True, default=False)
|
|
parser.add_argument('--morphPlacement',
|
|
help="morph placement",
|
|
dest='morphPlacement', nargs='?', default='early',
|
|
choices=morphPlacements + ['random'])
|
|
parser.add_argument('--morphPlacementList', help="list to choose from when random",
|
|
dest='morphPlacementList', nargs='?', default=None)
|
|
parser.add_argument('--hideItems', help="Like in dessy's rando hide half of the items",
|
|
dest="hideItems", nargs='?', const=True, default=False)
|
|
parser.add_argument('--progressionSpeed', '-i',
|
|
help="progression speed, from " + str(speeds) + ". 'random' picks a random speed from these. Pick a random speed from a subset using comma-separated values, like 'slow,medium,fast'.",
|
|
dest='progressionSpeed', nargs='?', default='medium', choices=speeds+['random'])
|
|
parser.add_argument('--progressionSpeedList', help="list to choose from when random",
|
|
dest='progressionSpeedList', nargs='?', default=None)
|
|
parser.add_argument('--progressionDifficulty',
|
|
help="",
|
|
dest='progressionDifficulty', nargs='?', default='normal',
|
|
choices=progDiffs + ['random'])
|
|
parser.add_argument('--progressionDifficultyList', help="list to choose from when random",
|
|
dest='progressionDifficultyList', nargs='?', default=None)
|
|
parser.add_argument('--superFun',
|
|
help="randomly remove major items from the pool for maximum enjoyment",
|
|
dest='superFun', nargs='?', default=[], action='append',
|
|
choices=['Movement', 'Combat', 'Suits', 'MovementRandom', 'CombatRandom', 'SuitsRandom'])
|
|
parser.add_argument('--animals',
|
|
help="randomly change the save the animals room",
|
|
dest='animals', action='store_true', default=False)
|
|
parser.add_argument('--nolayout',
|
|
help="do not include total randomizer layout patches",
|
|
dest='noLayout', action='store_true', default=False)
|
|
parser.add_argument('--gravityBehaviour',
|
|
help="varia/gravity suits behaviour",
|
|
dest='gravityBehaviour', nargs='?', default='Balanced', choices=gravityBehaviours+['random'])
|
|
parser.add_argument('--gravityBehaviourList', help="list to choose from when random",
|
|
dest='gravityBehaviourList', nargs='?', default=None)
|
|
parser.add_argument('--nerfedCharge',
|
|
help="apply nerfed charge patch",
|
|
dest='nerfedCharge', action='store_true', default=False)
|
|
parser.add_argument('--novariatweaks',
|
|
help="do not include VARIA randomizer tweaks",
|
|
dest='noVariaTweaks', action='store_true', default=False)
|
|
parser.add_argument('--controls',
|
|
help="specify controls, comma-separated, in that order: Shoot,Jump,Dash,ItemSelect,ItemCancel,AngleUp,AngleDown. Possible values: A,B,X,Y,L,R,Select,None",
|
|
dest='controls')
|
|
parser.add_argument('--moonwalk',
|
|
help="Enables moonwalk by default",
|
|
dest='moonWalk', action='store_true', default=False)
|
|
parser.add_argument('--runtime',
|
|
help="Maximum runtime limit in seconds. If 0 or negative, no runtime limit. Default is 30.",
|
|
dest='runtimeLimit_s', nargs='?', default=30, type=int)
|
|
parser.add_argument('--race', help="Race mode magic number, between 1 and 65535", dest='raceMagic',
|
|
type=int)
|
|
parser.add_argument('--vcr', help="Generate VCR output file", dest='vcr', action='store_true')
|
|
parser.add_argument('--ext_stats', help="dump extended stats SQL", nargs='?', default=None, dest='extStatsFilename')
|
|
parser.add_argument('--randoPreset', help="rando preset file", dest="randoPreset", nargs='?', default=None)
|
|
parser.add_argument('--fakeRandoPreset', help="for prog speed stats", dest="fakeRandoPreset", nargs='?', default=None)
|
|
parser.add_argument('--plandoRando', help="json string with already placed items/locs", dest="plandoRando",
|
|
nargs='?', default=None)
|
|
parser.add_argument('--jm,', help="display data used by jm for its stats", dest='jm', action='store_true', default=False)
|
|
parser.add_argument('--doorsColorsRando', help='randomize color of colored doors', dest='doorsColorsRando',
|
|
nargs='?', const=True, default=False)
|
|
parser.add_argument('--allowGreyDoors', help='add grey color in doors colors pool', dest='allowGreyDoors',
|
|
nargs='?', const=True, default=False)
|
|
parser.add_argument('--logic', help='logic to use', dest='logic', nargs='?', default="varia", choices=["varia", "rotation"])
|
|
parser.add_argument('--hud', help='Enable VARIA hud', dest='hud',
|
|
nargs='?', const=True, default=False)
|
|
parser.add_argument('--objective',
|
|
help="objectives to open G4",
|
|
dest='objective', nargs='?', default=[], action='append',
|
|
choices=Objectives.getAllGoals()+["random"]+[str(i) for i in range(6)])
|
|
parser.add_argument('--objectiveList', help="list to choose from when random",
|
|
dest='objectiveList', nargs='?', default=None)
|
|
parser.add_argument('--tourian', help="Tourian mode",
|
|
dest='tourian', nargs='?', default='Vanilla',
|
|
choices=tourians+['random'])
|
|
parser.add_argument('--tourianList', help="list to choose from when random",
|
|
dest='tourianList', nargs='?', default=None)
|
|
|
|
def __init__(self, world, rom, player):
|
|
# parse args
|
|
self.args = copy.deepcopy(VariaRandomizer.parser.parse_args(["--logic", "varia"])) #dummy custom args to skip parsing _sys.argv while still get default values
|
|
self.player = player
|
|
args = self.args
|
|
args.rom = rom
|
|
# args.startLocation = to_pascal_case_with_space(world.startLocation[player].current_key)
|
|
|
|
if args.output is None and args.rom is None:
|
|
raise Exception("Need --output or --rom parameter")
|
|
|
|
elif args.output is not None and args.rom is not None:
|
|
raise Exception("Can't have both --output and --rom parameters")
|
|
|
|
if args.plandoRando is not None and args.output is None:
|
|
raise Exception("plandoRando param requires output param")
|
|
|
|
log.init(args.debug)
|
|
logger = log.get('Rando')
|
|
|
|
Logic.factory(args.logic)
|
|
|
|
# service to force an argument value and notify it
|
|
argDict = vars(args)
|
|
self.forcedArgs = {}
|
|
self.optErrMsgs = [ ]
|
|
optErrMsgs = self.optErrMsgs
|
|
def forceArg(arg, value, msg, altValue=None, webArg=None, webValue=None):
|
|
okValues = [value]
|
|
if altValue is not None:
|
|
okValues.append(altValue)
|
|
|
|
if argDict[arg] not in okValues:
|
|
argDict[arg] = value
|
|
self.forcedArgs[webArg if webArg is not None else arg] = webValue if webValue is not None else value
|
|
# print(msg)
|
|
# optErrMsgs.append(msg)
|
|
|
|
preset = loadRandoPreset(world, self.player, args)
|
|
# use the skill preset from the rando preset
|
|
if preset is not None and preset != 'custom' and preset != 'varia_custom' and args.paramsFileName is None:
|
|
args.paramsFileName = "/".join((appDir, getPresetDir(preset), preset+".json"))
|
|
|
|
# if diff preset given, load it
|
|
if args.paramsFileName is not None:
|
|
PresetLoader.factory(args.paramsFileName).load(self.player)
|
|
preset = os.path.splitext(os.path.basename(args.paramsFileName))[0]
|
|
|
|
if args.preset is not None:
|
|
preset = args.preset
|
|
else:
|
|
if preset == 'custom':
|
|
PresetLoader.factory(world.custom_preset[player].value).load(self.player)
|
|
elif preset == 'varia_custom':
|
|
if len(world.varia_custom_preset[player].value) == 0:
|
|
raise Exception("varia_custom was chosen but varia_custom_preset is missing.")
|
|
url = 'https://randommetroidsolver.pythonanywhere.com/presetWebService'
|
|
preset_name = next(iter(world.varia_custom_preset[player].value))
|
|
payload = '{{"preset": "{}"}}'.format(preset_name)
|
|
headers = {'content-type': 'application/json', 'Accept-Charset': 'UTF-8'}
|
|
response = requests.post(url, data=payload, headers=headers)
|
|
if response.ok:
|
|
PresetLoader.factory(json.loads(response.text)).load(self.player)
|
|
else:
|
|
raise Exception("Got error {} {} {} from trying to fetch varia custom preset named {}".format(response.status_code, response.reason, response.text, preset_name))
|
|
else:
|
|
preset = 'default'
|
|
PresetLoader.factory("/".join((appDir, getPresetDir('casual'), 'casual.json'))).load(self.player)
|
|
|
|
|
|
|
|
logger.debug("preset: {}".format(preset))
|
|
|
|
# if no seed given, choose one
|
|
if args.seed == 0:
|
|
self.seed = random.randrange(sys.maxsize)
|
|
else:
|
|
self.seed = args.seed
|
|
logger.debug("seed: {}".format(self.seed))
|
|
|
|
if args.raceMagic is not None:
|
|
if args.raceMagic <= 0 or args.raceMagic >= 0x10000:
|
|
raise Exception("Invalid magic")
|
|
|
|
# if no max diff, set it very high
|
|
if args.maxDifficulty:
|
|
if args.maxDifficulty == 'random':
|
|
diffs = ['easy', 'medium', 'hard', 'harder', 'hardcore', 'mania']
|
|
self.maxDifficulty = text2diff[random.choice(diffs)]
|
|
else:
|
|
self.maxDifficulty = text2diff[args.maxDifficulty]
|
|
else:
|
|
self.maxDifficulty = infinity
|
|
# same as solver, increase max difficulty
|
|
threshold = self.maxDifficulty
|
|
epsilon = 0.001
|
|
if self.maxDifficulty <= easy:
|
|
threshold = medium - epsilon
|
|
elif self.maxDifficulty <= medium:
|
|
threshold = hard - epsilon
|
|
elif self.maxDifficulty <= hard:
|
|
threshold = harder - epsilon
|
|
elif self.maxDifficulty <= harder:
|
|
threshold = hardcore - epsilon
|
|
elif self.maxDifficulty <= hardcore:
|
|
threshold = mania - epsilon
|
|
maxDifficulty = threshold
|
|
logger.debug("maxDifficulty: {}".format(self.maxDifficulty))
|
|
|
|
# handle random parameters with dynamic pool of values
|
|
(_, progSpeed) = randomMulti(args.__dict__, "progressionSpeed", speeds)
|
|
(_, progDiff) = randomMulti(args.__dict__, "progressionDifficulty", progDiffs)
|
|
(majorsSplitRandom, args.majorsSplit) = randomMulti(args.__dict__, "majorsSplit", majorsSplits)
|
|
(_, self.gravityBehaviour) = randomMulti(args.__dict__, "gravityBehaviour", gravityBehaviours)
|
|
(_, args.tourian) = randomMulti(args.__dict__, "tourian", tourians)
|
|
(areaRandom, args.area) = randomMulti(args.__dict__, "area", areaRandomizations)
|
|
areaRandomization = args.area in ['light', 'full']
|
|
lightArea = args.area == 'light'
|
|
|
|
if args.minDifficulty:
|
|
minDifficulty = text2diff[args.minDifficulty]
|
|
if progSpeed != "speedrun":
|
|
optErrMsgs.append("Minimum difficulty setting ignored, as prog speed is not speedrun")
|
|
else:
|
|
minDifficulty = 0
|
|
|
|
if areaRandomization == True and args.bosses == True and args.minimizerN is not None:
|
|
forceArg('majorsSplit', 'Full', "'Majors Split' forced to Full", altValue='FullWithHUD')
|
|
if args.minimizerN == "random":
|
|
self.minimizerN = random.randint(30, 60)
|
|
logger.debug("minimizerN: {}".format(self.minimizerN))
|
|
else:
|
|
self.minimizerN = int(args.minimizerN)
|
|
else:
|
|
self.minimizerN = None
|
|
|
|
doorsColorsRandom = False
|
|
if args.doorsColorsRando == 'random':
|
|
doorsColorsRandom = True
|
|
args.doorsColorsRando = bool(random.getrandbits(1))
|
|
logger.debug("doorsColorsRando: {}".format(args.doorsColorsRando))
|
|
|
|
bossesRandom = False
|
|
if args.bosses == 'random':
|
|
bossesRandom = True
|
|
args.bosses = bool(random.getrandbits(1))
|
|
logger.debug("bosses: {}".format(args.bosses))
|
|
|
|
if args.escapeRando == 'random':
|
|
args.escapeRando = bool(random.getrandbits(1))
|
|
logger.debug("escapeRando: {}".format(args.escapeRando))
|
|
|
|
if args.suitsRestriction != False and self.minimizerN is not None:
|
|
forceArg('suitsRestriction', False, "'Suits restriction' forced to off", webValue='off')
|
|
|
|
if args.suitsRestriction == 'random':
|
|
if args.morphPlacement == 'late' and areaRandomization == True:
|
|
forceArg('suitsRestriction', False, "'Suits restriction' forced to off", webValue='off')
|
|
else:
|
|
args.suitsRestriction = bool(random.getrandbits(1))
|
|
logger.debug("suitsRestriction: {}".format(args.suitsRestriction))
|
|
|
|
if args.hideItems == 'random':
|
|
args.hideItems = bool(random.getrandbits(1))
|
|
|
|
if args.morphPlacement == 'random':
|
|
if args.morphPlacementList is not None:
|
|
morphPlacements = args.morphPlacementList.split(',')
|
|
args.morphPlacement = random.choice(morphPlacements)
|
|
# Scavenger Hunt constraints
|
|
if args.majorsSplit == 'Scavenger':
|
|
forceArg('progressionSpeed', 'speedrun', "'Progression speed' forced to speedrun")
|
|
progSpeed = "speedrun"
|
|
forceArg('hud', True, "'VARIA HUD' forced to on", webValue='on')
|
|
if not GraphUtils.isStandardStart(args.startLocation):
|
|
forceArg('startLocation', "Landing Site", "Start Location forced to Landing Site because of Scavenger mode")
|
|
if args.morphPlacement == 'late':
|
|
forceArg('morphPlacement', 'normal', "'Morph Placement' forced to normal instead of late")
|
|
# use escape rando for auto escape trigger
|
|
if args.tourian == 'Disabled':
|
|
forceArg('escapeRando', True, "'Escape randomization' forced to on", webValue='on')
|
|
forceArg('noRemoveEscapeEnemies', True, "Enemies enabled during escape sequence", webArg='removeEscapeEnemies', webValue='off')
|
|
# random fill makes certain options unavailable
|
|
if (progSpeed == 'speedrun' or progSpeed == 'basic') and args.majorsSplit != 'Scavenger':
|
|
forceArg('progressionDifficulty', 'normal', "'Progression difficulty' forced to normal")
|
|
progDiff = args.progressionDifficulty
|
|
logger.debug("progressionDifficulty: {}".format(progDiff))
|
|
|
|
if args.strictMinors == 'random':
|
|
args.strictMinors = bool(random.getrandbits(1))
|
|
|
|
# in plando rando we know that the start ap is ok
|
|
if not GraphUtils.isStandardStart(args.startLocation) and args.plandoRando is None:
|
|
if args.majorsSplit in ['Major', "Chozo"]:
|
|
forceArg('hud', True, "'VARIA HUD' forced to on", webValue='on')
|
|
forceArg('noVariaTweaks', False, "'VARIA tweaks' forced to on", webValue='on')
|
|
forceArg('noLayout', False, "'Anti-softlock layout patches' forced to on", webValue='on')
|
|
forceArg('suitsRestriction', False, "'Suits restriction' forced to off", webValue='off')
|
|
forceArg('areaLayoutBase', False, "'Additional layout patches for easier navigation' forced to on", webValue='on')
|
|
possibleStartAPs, reasons = GraphUtils.getPossibleStartAPs(areaRandomization, self.maxDifficulty, args.morphPlacement, self.player)
|
|
if args.startLocation == 'random':
|
|
if args.startLocationList is not None:
|
|
startLocationList = args.startLocationList.split(',')
|
|
# intersection between user whishes and reality
|
|
possibleStartAPs = sorted(list(set(possibleStartAPs).intersection(set(startLocationList))))
|
|
if len(possibleStartAPs) == 0:
|
|
#optErrMsgs += ["%s : %s" % (apName, cause) for apName, cause in reasons.items() if apName in startLocationList]
|
|
raise Exception("Invalid start locations list with your settings." +
|
|
"%s : %s" % (apName, cause) for apName, cause in reasons.items() if apName in startLocationList)
|
|
#dumpErrorMsgs(args.output, optErrMsgs)
|
|
args.startLocation = random.choice(possibleStartAPs)
|
|
elif args.startLocation not in possibleStartAPs:
|
|
args.startLocation = 'Landing Site'
|
|
world.start_location[player] = StartLocation(StartLocation.default)
|
|
#optErrMsgs.append('Invalid start location: {}. {}'.format(args.startLocation, reasons[args.startLocation]))
|
|
#optErrMsgs.append('Possible start locations with these settings: {}'.format(possibleStartAPs))
|
|
#dumpErrorMsgs(args.output, optErrMsgs)
|
|
ap = getAccessPoint(args.startLocation)
|
|
if 'forcedEarlyMorph' in ap.Start and ap.Start['forcedEarlyMorph'] == True:
|
|
forceArg('morphPlacement', 'early', "'Morph Placement' forced to early for custom start location")
|
|
else:
|
|
if progSpeed == 'speedrun':
|
|
if args.morphPlacement == 'late':
|
|
forceArg('morphPlacement', 'normal', "'Morph Placement' forced to normal instead of late")
|
|
elif (not GraphUtils.isStandardStart(args.startLocation)) and args.morphPlacement != 'normal':
|
|
forceArg('morphPlacement', 'normal', "'Morph Placement' forced to normal for custom start location")
|
|
if args.majorsSplit == 'Chozo' and args.morphPlacement == "late":
|
|
forceArg('morphPlacement', 'normal', "'Morph Placement' forced to normal for Chozo")
|
|
#print("SEED: " + str(self.seed))
|
|
|
|
# fill restrictions dict
|
|
restrictions = { 'Suits' : args.suitsRestriction, 'Morph' : args.morphPlacement, "doors": "normal" if not args.doorsColorsRando else "late" }
|
|
restrictions['MajorMinor'] = 'Full' if args.majorsSplit == 'FullWithHUD' else args.majorsSplit
|
|
if restrictions["MajorMinor"] == "Scavenger":
|
|
scavNumLocs = int(args.scavNumLocs)
|
|
if scavNumLocs == 0:
|
|
scavNumLocs = random.randint(4,16)
|
|
restrictions["ScavengerParams"] = {'numLocs':scavNumLocs, 'vanillaItems':not args.scavRandomized}
|
|
restrictions["EscapeTrigger"] = args.tourian == 'Disabled'
|
|
seedCode = 'X'
|
|
if majorsSplitRandom == False:
|
|
if restrictions['MajorMinor'] == 'Full':
|
|
seedCode = 'FX'
|
|
elif restrictions['MajorMinor'] == 'Chozo':
|
|
seedCode = 'ZX'
|
|
elif restrictions['MajorMinor'] == 'Major':
|
|
seedCode = 'MX'
|
|
elif restrictions['MajorMinor'] == 'Scavenger':
|
|
seedCode = 'SX'
|
|
if args.bosses == True and bossesRandom == False:
|
|
seedCode = 'B'+seedCode
|
|
if args.doorsColorsRando == True and doorsColorsRandom == False:
|
|
seedCode = 'D'+seedCode
|
|
if areaRandomization == True and areaRandom == False:
|
|
seedCode = 'A'+seedCode
|
|
|
|
#fileName = 'VARIA_Randomizer_' + seedCode + str(seed) + '_' + preset
|
|
#if args.progressionSpeed != "random":
|
|
# fileName += "_" + args.progressionSpeed
|
|
self.fileName = output_path
|
|
seedName = self.fileName
|
|
if args.directory != '.':
|
|
self.fileName = args.directory + '/' + self.fileName
|
|
if args.noLayout == True:
|
|
RomPatches.ActivePatches[self.player] = RomPatches.TotalBase.copy()
|
|
else:
|
|
RomPatches.ActivePatches[self.player] = RomPatches.Total.copy()
|
|
RomPatches.ActivePatches[self.player].remove(RomPatches.BlueBrinstarBlueDoor)
|
|
RomPatches.ActivePatches[self.player] += GraphUtils.getGraphPatches(args.startLocation)
|
|
if self.gravityBehaviour != "Balanced":
|
|
RomPatches.ActivePatches[self.player].remove(RomPatches.NoGravityEnvProtection)
|
|
if self.gravityBehaviour == "Progressive":
|
|
RomPatches.ActivePatches[self.player].append(RomPatches.ProgressiveSuits)
|
|
if args.nerfedCharge == True:
|
|
RomPatches.ActivePatches[self.player].append(RomPatches.NerfedCharge)
|
|
if args.noVariaTweaks == False:
|
|
RomPatches.ActivePatches[self.player] += RomPatches.VariaTweaks
|
|
if self.minimizerN is not None:
|
|
RomPatches.ActivePatches[self.player].append(RomPatches.NoGadoras)
|
|
if args.tourian == 'Fast':
|
|
RomPatches.ActivePatches[self.player] += RomPatches.MinimizerTourian
|
|
elif args.tourian == 'Disabled':
|
|
RomPatches.ActivePatches[self.player].append(RomPatches.NoTourian)
|
|
if 'relaxed_round_robin_cf.ips' in args.patches:
|
|
RomPatches.ActivePatches[self.player].append(RomPatches.RoundRobinCF)
|
|
missileQty = float(args.missileQty)
|
|
superQty = float(args.superQty)
|
|
powerBombQty = float(args.powerBombQty)
|
|
minorQty = int(args.minorQty)
|
|
self.energyQty = args.energyQty
|
|
if missileQty < 1:
|
|
missileQty = random.randint(1, 9)
|
|
if superQty < 1:
|
|
superQty = random.randint(1, 9)
|
|
if powerBombQty < 1:
|
|
powerBombQty = random.randint(1, 9)
|
|
if minorQty < 1:
|
|
minorQty = random.randint(25, 100)
|
|
if self.energyQty == 'random':
|
|
if args.energyQtyList is not None:
|
|
energyQties = args.energyQtyList.split(',')
|
|
self.energyQty = random.choice(energyQties)
|
|
if self.energyQty == 'ultra sparse':
|
|
# add nerfed rainbow beam patch
|
|
RomPatches.ActivePatches[self.player].append(RomPatches.NerfedRainbowBeam)
|
|
qty = {'energy': self.energyQty,
|
|
'minors': minorQty,
|
|
'ammo': { 'Missile': missileQty,
|
|
'Super': superQty,
|
|
'PowerBomb': powerBombQty },
|
|
'strictMinors' : args.strictMinors }
|
|
logger.debug("quantities: {}".format(qty))
|
|
|
|
if len(args.superFun) > 0:
|
|
superFun = []
|
|
for fun in args.superFun:
|
|
if fun.find('Random') != -1:
|
|
if bool(random.getrandbits(1)) == True:
|
|
superFun.append(fun[0:fun.find('Random')])
|
|
else:
|
|
superFun.append(fun)
|
|
args.superFun = superFun
|
|
logger.debug("superFun: {}".format(args.superFun))
|
|
|
|
ctrlButton = ["A", "B", "X", "Y", "L", "R", "Select"]
|
|
ctrl = Controller.ControllerDict[self.player]
|
|
self.ctrlDict = { getattr(ctrl, button) : button for button in ctrlButton }
|
|
args.moonWalk = ctrl.Moonwalk
|
|
|
|
plandoSettings = None
|
|
if args.plandoRando is not None:
|
|
plandoRando = json.loads(args.plandoRando)
|
|
forceArg('progressionSpeed', 'speedrun', "'Progression Speed' forced to speedrun")
|
|
progSpeed = 'speedrun'
|
|
forceArg('majorsSplit', 'Full', "'Majors Split' forced to Full")
|
|
forceArg('morphPlacement', 'normal', "'Morph Placement' forced to normal")
|
|
forceArg('progressionDifficulty', 'normal', "'Progression difficulty' forced to normal")
|
|
progDiff = 'normal'
|
|
RomPatches.ActivePatches = plandoRando["patches"]
|
|
DoorsManager.unserialize(plandoRando["doors"])
|
|
plandoSettings = {"locsItems": plandoRando['locsItems'], "forbiddenItems": plandoRando['forbiddenItems']}
|
|
randoSettings = RandoSettings(self.maxDifficulty, progSpeed, progDiff, qty,
|
|
restrictions, args.superFun, args.runtimeLimit_s,
|
|
plandoSettings, minDifficulty)
|
|
|
|
dotFile = None
|
|
if areaRandomization == True:
|
|
if args.dot == True:
|
|
dotFile = args.directory + '/' + seedName + '.dot'
|
|
RomPatches.ActivePatches[self.player] += RomPatches.AreaBaseSet
|
|
if args.areaLayoutBase == False:
|
|
RomPatches.ActivePatches[self.player] += RomPatches.AreaComfortSet
|
|
if args.doorsColorsRando == True:
|
|
RomPatches.ActivePatches[self.player].append(RomPatches.RedDoorsMissileOnly)
|
|
graphSettings = GraphSettings(self.player, args.startLocation, areaRandomization, lightArea, args.bosses,
|
|
args.escapeRando, self.minimizerN, dotFile,
|
|
args.doorsColorsRando, args.allowGreyDoors, args.tourian,
|
|
plandoRando["transitions"] if plandoSettings is not None else None)
|
|
|
|
if plandoSettings is None:
|
|
DoorsManager.setDoorsColor(self.player)
|
|
|
|
self.escapeAttr = None
|
|
if plandoSettings is None:
|
|
self.objectivesManager = Objectives(self.player, args.tourian != 'Disabled', randoSettings)
|
|
addedObjectives = 0
|
|
if args.majorsSplit == "Scavenger":
|
|
self.objectivesManager.setScavengerHunt()
|
|
addedObjectives = 1
|
|
|
|
if not args.objective:
|
|
args.objective = ["nothing"]
|
|
|
|
if args.objective:
|
|
if (args.objectiveRandom):
|
|
availableObjectives = [goal for goal in objectives if goal != "collect 100% items"] if "random" in args.objectiveList else args.objectiveList
|
|
self.objectivesManager.setRandom(args.nbObjective, availableObjectives)
|
|
else:
|
|
maxActiveGoals = Objectives.maxActiveGoals - addedObjectives
|
|
if len(args.objective) > maxActiveGoals:
|
|
args.objective = args.objective[0:maxActiveGoals]
|
|
for goal in args.objective:
|
|
self.objectivesManager.addGoal(goal)
|
|
self.objectivesManager.expandGoals()
|
|
else:
|
|
if not (args.majorsSplit == "Scavenger" and args.tourian == 'Disabled'):
|
|
self.objectivesManager.setVanilla()
|
|
if len(self.objectivesManager.activeGoals) == 0:
|
|
self.objectivesManager.addGoal('nothing')
|
|
if any(goal for goal in self.objectivesManager.activeGoals if goal.area is not None):
|
|
forceArg('hud', True, "'VARIA HUD' forced to on", webValue='on')
|
|
else:
|
|
args.tourian = plandoRando["tourian"]
|
|
self.objectivesManager = Objectives(self.player, args.tourian != 'Disabled')
|
|
for goal in plandoRando["objectives"]:
|
|
self.objectivesManager.addGoal(goal)
|
|
|
|
# print some parameters for jm's stats
|
|
#if args.jm == True:
|
|
# print("startLocation:{}".format(args.startLocation))
|
|
# print("progressionSpeed:{}".format(progSpeed))
|
|
# print("majorsSplit:{}".format(args.majorsSplit))
|
|
# print("morphPlacement:{}".format(args.morphPlacement))
|
|
# print("gravity:{}".format(gravityBehaviour))
|
|
# print("maxDifficulty:{}".format(maxDifficulty))
|
|
# print("tourian:{}".format(args.tourian))
|
|
# print("objectives:{}".format([g.name for g in Objectives.activeGoals]))
|
|
# print("energyQty:{}".format(energyQty))
|
|
|
|
#try:
|
|
self.randoExec = RandoExec(seedName, args.vcr, randoSettings, graphSettings, self.player)
|
|
self.container = self.randoExec.randomize()
|
|
# if we couldn't find an area layout then the escape graph is not created either
|
|
# and getDoorConnections will crash if random escape is activated.
|
|
stuck = False
|
|
if not stuck or args.vcr == True:
|
|
self.escapeAttr = self.randoExec.areaGraph.EscapeAttributes if args.escapeRando else None
|
|
if self.escapeAttr is not None:
|
|
self.escapeAttr['patches'] = []
|
|
if args.noRemoveEscapeEnemies == True:
|
|
self.escapeAttr['patches'].append("Escape_Rando_Enable_Enemies")
|
|
#except Exception as e:
|
|
# import traceback
|
|
# traceback.print_exc(file=sys.stdout)
|
|
# dumpErrorMsg(args.output, "Error: {}".format(e))
|
|
|
|
if stuck == True:
|
|
#dumpErrorMsg(args.output, self.randoExec.errorMsg)
|
|
raise Exception("Can't generate " + self.fileName + " with the given parameters: {}".format(self.randoExec.errorMsg))
|
|
|
|
def PatchRom(self, outputFilename, customPrePatchApply = None, customPostPatchApply = None):
|
|
args = self.args
|
|
optErrMsgs = self.optErrMsgs
|
|
|
|
# choose on animal patch
|
|
if args.animals == True:
|
|
animalsPatches = ['animal_enemies.ips', 'animals.ips', 'draygonimals.ips', 'escapimals.ips',
|
|
'gameend.ips', 'grey_door_animals.ips', 'low_timer.ips', 'metalimals.ips',
|
|
'phantoonimals.ips', 'ridleyimals.ips']
|
|
if args.escapeRando == False:
|
|
args.patches.append(random.choice(animalsPatches))
|
|
args.patches.append("Escape_Animals_Change_Event")
|
|
else:
|
|
optErrMsgs.append("Ignored animals surprise because of escape randomization")
|
|
# # transform itemLocs in our usual dict(location, item), exclude minors, we'll get them with the solver
|
|
# locsItems = {}
|
|
# for itemLoc in itemLocs:
|
|
# locName = itemLoc.Location.Name
|
|
# itemType = itemLoc.Item.Type
|
|
# if itemType in ['Missile', 'Super', 'PowerBomb']:
|
|
# continue
|
|
# locsItems[locName] = itemType
|
|
# if args.debug == True:
|
|
# for loc in sorted(locsItems.keys()):
|
|
# print('{:>50}: {:>16} '.format(loc, locsItems[loc]))
|
|
|
|
# if plandoSettings is not None:
|
|
# with open(args.output, 'w') as jsonFile:
|
|
# json.dump({"itemLocs": [il.json() for il in itemLocs], "errorMsg": randoExec.errorMsg}, jsonFile)
|
|
|
|
# # generate extended stats
|
|
# if args.extStatsFilename is not None:
|
|
# with open(args.extStatsFilename, 'a') as extStatsFile:
|
|
# skillPreset = os.path.splitext(os.path.basename(args.paramsFileName))[0]
|
|
# if args.fakeRandoPreset is not None:
|
|
# randoPreset = args.fakeRandoPreset
|
|
# else:
|
|
# randoPreset = os.path.splitext(os.path.basename(args.randoPreset))[0]
|
|
# db.DB.dumpExtStatsItems(skillPreset, randoPreset, locsItems, extStatsFile)
|
|
|
|
try:
|
|
if args.hud == True or args.majorsSplit == "FullWithHUD":
|
|
args.patches.append("varia_hud.ips")
|
|
if args.debug == True:
|
|
args.patches.append("Disable_Clear_Save_Boot")
|
|
|
|
patcherSettings = {
|
|
"isPlando": False,
|
|
"majorsSplit": args.majorsSplit,
|
|
"startLocation": args.startLocation,
|
|
"optionalPatches": args.patches,
|
|
"layout": not args.noLayout,
|
|
"suitsMode": args.gravityBehaviour,
|
|
"area": args.area in ['light', 'full'],
|
|
"boss": args.bosses,
|
|
"areaLayout": not args.areaLayoutBase,
|
|
"variaTweaks": not args.noVariaTweaks,
|
|
"nerfedCharge": args.nerfedCharge,
|
|
"nerfedRainbowBeam": args.energyQty == 'ultra sparse',
|
|
"escapeAttr": self.escapeAttr,
|
|
"minimizerN": None, #minimizerN,
|
|
"tourian": args.tourian,
|
|
"doorsColorsRando": args.doorsColorsRando,
|
|
"vanillaObjectives": self.objectivesManager.isVanilla(),
|
|
"ctrlDict": self.ctrlDict,
|
|
"moonWalk": args.moonWalk,
|
|
"seed": self.seed,
|
|
"randoSettings": self.randoExec.randoSettings,
|
|
"doors": self.doors,
|
|
"displayedVersion": displayedVersion,
|
|
#"itemLocs": itemLocs,
|
|
#"progItemLocs": progItemLocs,
|
|
}
|
|
|
|
# args.rom is not None: generate local rom named filename.sfc with args.rom as source
|
|
# args.output is not None: generate local json named args.output
|
|
if args.rom is not None:
|
|
# patch local rom
|
|
romFileName = args.rom
|
|
shutil.copyfile(romFileName, outputFilename)
|
|
romPatcher = RomPatcher(settings=patcherSettings, romFileName=outputFilename, magic=args.raceMagic, player=self.player)
|
|
else:
|
|
romPatcher = RomPatcher(settings=patcherSettings, magic=args.raceMagic)
|
|
|
|
if customPrePatchApply != None:
|
|
customPrePatchApply(romPatcher)
|
|
|
|
romPatcher.patchRom()
|
|
|
|
if customPostPatchApply != None:
|
|
customPostPatchApply(romPatcher)
|
|
|
|
if len(optErrMsgs) > 0:
|
|
#optErrMsgs.append(randoExec.errorMsg)
|
|
msg = joinErrorMsgs(optErrMsgs)
|
|
else:
|
|
#msg = randoExec.errorMsg
|
|
msg = ''
|
|
|
|
if args.rom is None: # web mode
|
|
data = romPatcher.romFile.data
|
|
self.fileName = '{}.sfc'.format(self.fileName)
|
|
data["fileName"] = self.fileName
|
|
# error msg in json to be displayed by the web site
|
|
data["errorMsg"] = msg
|
|
# replaced parameters to update stats in database
|
|
if len(self.forcedArgs) > 0:
|
|
data["forcedArgs"] = self.forcedArgs
|
|
with open(outputFilename, 'w') as jsonFile:
|
|
json.dump(data, jsonFile)
|
|
else: # CLI mode
|
|
if msg != "":
|
|
print(msg)
|
|
except Exception as e:
|
|
import traceback
|
|
traceback.print_exc(file=sys.stdout)
|
|
raise Exception("Error patching {}: ({}: {})".format(outputFilename, type(e).__name__, e))
|
|
#dumpErrorMsg(args.output, msg)
|
|
|
|
# if stuck == True:
|
|
# print("Rom generated for debug purpose: {}".format(self.fileName))
|
|
# else:
|
|
# print("Rom generated: {}".format(self.fileName))
|