private static void ProcessMacroEntities(ExpansionSettings settings, Logger logger) { // Default to .map, if no extension was specified (unless there's an extensionless file that matches): var inputPath = settings.InputPath; if (!Path.HasExtension(settings.InputPath) && !File.Exists(settings.InputPath)) { inputPath = Path.ChangeExtension(settings.InputPath, ".map"); } if (settings.OutputPath == null) { settings.OutputPath = inputPath; } if (settings.Directory == null) { settings.Directory = Path.GetDirectoryName(settings.InputPath); } logger.Info($"Starting to expand macros in '{settings.InputPath}'."); var expandedMap = MacroExpander.ExpandMacros(inputPath, settings, logger); // TODO: Create a backup if the target file already exists! -- how many backups to make? -- make a setting for this behavior? using (var file = File.Create(settings.OutputPath)) { logger.Info($"Finished macro expansion. Saving to '{settings.OutputPath}'."); MapFormat.Save(expandedMap, file); } }
static int Main(string[] args) { var settings = new ExpansionSettings(); var commandLineParser = GetCommandLineParser(settings); try { // NOTE: The -repl argument enables a different mode, so required arguments are no longer required, // but the cmd-line parser doesn't support that, so here's a quick hacky workaround: if (args.Any(arg => arg == "-repl")) { RunMScriptREPL(); return(0); } commandLineParser.Parse(args); using (var logger = new Logger(new StreamWriter(Console.OpenStandardOutput()), settings.LogLevel)) { if (settings.LogLevel != LogLevel.Off) { ShowToolInfo(); Console.WriteLine("----- BEGIN MESS -----"); Console.WriteLine($"Command line: {Environment.CommandLine}"); Console.WriteLine($"Arguments: {string.Join(" ", Environment.GetCommandLineArgs())}"); } try { ProcessMacroEntities(settings, logger); return(0); } catch (Exception ex) { logger.Error("Failed to process macro entities", ex); // TODO: Show more error details here? return(-1); } } } catch (Exception ex) { Console.WriteLine($"Failed to parse command line arguments: {ex.GetType().Name}: '{ex.Message}'."); ShowHelp(commandLineParser); return(-1); } }
/// <summary> /// Returns a command-line parser that will fill the given settings object when it parses command-line arguments. /// </summary> private static CommandLine GetCommandLineParser(ExpansionSettings settings) { return(new CommandLine() .Option( "-repl", () => { }, // This argument is taken care of in Main. $"Enables the interactive MScript interpreter mode. This starts a REPL (read-evaluate-print loop). All other arguments will be ignored.") .Option( "-dir", s => { settings.Directory = Path.GetFullPath(s); }, $"The directory to use for resolving relative template map paths. If not specified, the input map file directory will be used.") .Option( "-fgd", s => { settings.GameDataPaths = s.Split(';').Select(Path.GetFullPath).ToArray(); }, $"The .fgd file(s) that contains entity rewrite directives. Multiple paths must be separated by semicolons.") .Option( "-vars", s => { settings.Variables = ParseVariables(s); }, $"These variables are used when evaluating expressions in the given map's properties and entities. Input format is \"name1 = expression; name2: expression; ...\".") .Option( "-maxrecursion", s => { settings.RecursionLimit = Math.Max(1, int.Parse(s)); }, $"Limits recursion depth (templates that insert other templates). This protects against accidentally triggering infinite recursion. Default value is {settings.RecursionLimit}.") .Option( "-maxinstances", s => { settings.InstanceLimit = Math.Max(1, int.Parse(s)); }, $"Limits the total number of instantiations. This protects against acidentally triggering an excessive amount of instantiation. Default value is {settings.InstanceLimit}.") .Option( "-log", s => { settings.LogLevel = (LogLevel)Enum.Parse(typeof(LogLevel), s, true); }, $"Sets the log level. Valid options are: {string.Join(", ", Enum.GetValues(typeof(LogLevel)).OfType<LogLevel>().Select(level => level.ToString().ToLowerInvariant()))}. Default value is {settings.LogLevel.ToString().ToLowerInvariant()}.") .Argument( s => { settings.InputPath = Path.GetFullPath(s); }, "Input map file.") .OptionalArgument( s => { settings.OutputPath = Path.GetFullPath(s); }, "Output map file. If not specified, the input map file will be overwritten.")); }
/// <summary> /// Fills the given settings object by reading settings from the 'mess.config' file. /// Does nothing if the config file does not exist, but will throw an exception if the config file is not structured correctly. /// </summary> public static void ReadSettings(string path, ExpansionSettings settings) { if (!File.Exists(path)) { return; } var evaluationContext = Evaluation.DefaultContext(); evaluationContext.Bind("EXE_DIR", Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)); string currentSegment = null; var fgdPaths = new List <string>(); var lines = File.ReadLines(path, Encoding.UTF8).ToArray(); for (int i = 0; i < lines.Length; i++) { try { var line = lines[i].Trim(); if (line.StartsWith("//") || line == "") { continue; } var nameValueSeparatorIndex = line.IndexOf(':'); if (nameValueSeparatorIndex == -1) { switch (currentSegment) { case "rewrite-fgds": settings.GameDataPaths.Add(Path.GetFullPath(Evaluation.EvaluateInterpolatedString(ReadString(line), evaluationContext))); break; case "variables": // TODO: This approach does not support comments at the end of a line! var assignments = Parser.ParseAssignments(Tokenizer.Tokenize(line + ";")); foreach (var assignment in assignments) { settings.Variables[assignment.Identifier] = Evaluator.Evaluate(assignment.Value, evaluationContext); } break; default: Console.WriteLine($"Warning: config line #{i + 1} in '{path}' is formatted incorrectly and will be skipped."); break; } } else { currentSegment = null; var name = line.Substring(0, nameValueSeparatorIndex).Trim(); var rest = RemoveTrailingComments(line.Substring(nameValueSeparatorIndex + 1)); switch (name) { case "template-directory": settings.Directory = Path.GetFullPath(Evaluation.EvaluateInterpolatedString(ReadString(rest), evaluationContext)); break; case "max-recursion": settings.RecursionLimit = ReadInteger(rest); break; case "max-instances": settings.InstanceLimit = ReadInteger(rest); break; case "log-level": settings.LogLevel = (LogLevel)Enum.Parse(typeof(LogLevel), ReadString(rest), true); break; case "rewrite-fgds": case "variables": currentSegment = name; break; case "inverted-pitch-predicate": settings.InvertedPitchPredicate = ReadString(rest); break; default: Console.WriteLine($"Unknown setting on config line #{i + 1}: '{name}'."); break; } } } catch (Exception ex) { Console.WriteLine($"Failed to read config line #{i + 1} in '{path}': {ex.GetType().Name}: '{ex.Message}'."); continue; } } string ReadString(string part) => part; int ReadInteger(string part) => int.Parse(part); string RemoveTrailingComments(string part) { var commentStartIndex = part.IndexOf("//"); if (commentStartIndex != -1) { part = part.Substring(0, commentStartIndex); } return(part.Trim()); } }
static int Main(string[] args) { var stopwatch = Stopwatch.StartNew(); var settings = new ExpansionSettings(); var commandLineParser = GetCommandLineParser(settings); try { if (args.Length == 0) { Console.WriteLine("MESS requires at least one argument (the input map file)."); Console.WriteLine(""); ShowHelp(commandLineParser); return(-1); } // NOTE: The -repl argument enables a different mode, so required arguments are no longer required, // but the cmd-line parser doesn't support that, so here's a quick hacky workaround: if (args.Any(arg => arg == "-repl")) { RunMScriptREPL(); return(0); } ConfigFile.ReadSettings(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "mess.config"), settings); commandLineParser.Parse(args); using (var logger = new MultiLogger(new ConsoleLogger(settings.LogLevel), new FileLogger(settings.InputPath + ".mess.log", settings.LogLevel))) { logger.Minimal($"MESS v{Assembly.GetExecutingAssembly().GetName().Version}: Macro Entity Substitution System"); logger.Minimal("----- BEGIN MESS -----"); logger.Minimal($"Command line: {Environment.CommandLine}"); logger.Minimal($"Arguments: {string.Join(" ", Environment.GetCommandLineArgs())}"); logger.Minimal(""); try { ProcessMacroEntities(settings, logger); return(0); } catch (Exception ex) { logger.Error("Failed to process macro entities", ex); var innerException = ex.InnerException; while (innerException != null) { logger.Error("Inner exception:", innerException); innerException = innerException.InnerException; } // TODO: Show more error details here? return(-1); } finally { // TODO: Log a small summary as well? Number of templates/instances/etc? logger.Minimal(""); logger.Minimal($"Finished in {stopwatch.ElapsedMilliseconds / 1000f:0.##} seconds."); logger.Minimal(""); logger.Minimal("----- END MESS -----"); } } } catch (Exception ex) { Console.WriteLine($"Failed to read config file or to parse command line arguments: {ex.GetType().Name}: '{ex.Message}'."); ShowHelp(commandLineParser); return(-1); } }