internal static int Export(CompileOptions options) { YarnSpinnerConsole.CheckFileList(options.files, YarnSpinnerConsole.ALLOWED_EXTENSIONS); foreach (var file in options.files) { var dialogue = YarnSpinnerConsole.CreateDialogueForUtilities(); // Load and compile the program try { // First, we need to ensure that this file compiles. dialogue.LoadFile(file); } catch { YarnSpinnerConsole.Warn(string.Format("Skipping file {0} due to compilation errors.", file)); continue; } // Convert the program into BSON var compiledProgram = dialogue.GetCompiledProgram(options.format); var outputPath = System.IO.Path.ChangeExtension(file, "yarn.bytes"); try { System.IO.File.WriteAllBytes(outputPath, compiledProgram); } catch (Exception e) { YarnSpinnerConsole.Error(string.Format("Error writing {0}: {1}", outputPath, e.Message)); } } return(0); }
static int ConvertToYarn(ConvertFormatOptions options) { foreach (var file in options.files) { if (Loader.GetFormatFromFileName(file) == NodeFormat.Text) { YarnSpinnerConsole.Warn(string.Format("Not converting file {0}, because its name implies it's already in Yarn format", file)); continue; } ConvertNodesInFile(options, file, "yarn.txt", ConvertNodesToYarnText); } return(0); }
static int ConvertToJSON(ConvertFormatOptions options) { foreach (var file in options.files) { if (Loader.GetFormatFromFileName(file) == NodeFormat.JSON) { YarnSpinnerConsole.Warn(string.Format("Not converting file {0}, because its name implies it's already in JSON format", file)); continue; } ConvertNodesInFile(options, file, "json", (IEnumerable <Loader.NodeInfo> nodes) => JsonConvert.SerializeObject(nodes, Formatting.Indented)); } return(0); }
static internal int ConvertFormat(ConvertFormatOptions options) { YarnSpinnerConsole.CheckFileList(options.files, YarnSpinnerConsole.ALLOWED_EXTENSIONS); if (options.convertToYarn) { return(ConvertToYarn(options)); } var processName = System.IO.Path.GetFileName(Environment.GetCommandLineArgs()[0]); YarnSpinnerConsole.Error(string.Format(CultureInfo.CurrentCulture, "You must specify a destination format. Run '{0} help convert' to learn more.", processName)); return(1); }
static void ConvertNodesInFile(ConvertFormatOptions options, string file, string fileExtension, ConvertNodesToText convert) { var d = new Dialogue(null); var text = File.ReadAllText(file); IEnumerable <Loader.NodeInfo> nodes; try { nodes = d.loader.GetNodesFromText(text, Loader.GetFormatFromFileName(file)); } catch (FormatException e) { YarnSpinnerConsole.Error(e.Message); return; } var serialisedText = convert(nodes); var destinationDirectory = options.outputDirectory; if (destinationDirectory == null) { destinationDirectory = Path.GetDirectoryName(file); } var fileName = Path.GetFileName(file); // ChangeExtension thinks that the file "Foo.yarn.txt" has the extension "txt", so // to simplify things, just lop that extension off right away if it's there fileName = fileName.Replace(".yarn.txt", ""); // change the filename's extension fileName = Path.ChangeExtension(fileName, fileExtension); // figure out where we're writing this file var destinationFilePath = Path.Combine(destinationDirectory, fileName); File.WriteAllText(destinationFilePath, serialisedText); if (options.verbose) { YarnSpinnerConsole.Note("Wrote " + destinationFilePath); } }
internal static Dialogue CreateDialogueForUtilities() { // Note that we're passing in with a null library - this means // that all function checking will be disabled, and missing funcs // will not cause a compile error. If a func IS missing at runtime, // THAT will throw an exception. // We do this because this tool has no idea about any of the custom // functions that you might be using. Dialogue d = new Dialogue(null); // Debug logging goes to Note d.LogDebugMessage = message => YarnSpinnerConsole.Note(message); // When erroring, call Warn, not Error, which terminates the program d.LogErrorMessage = message => YarnSpinnerConsole.Warn(message); return(d); }
static internal int GenerateTables(GenerateTableOptions options) { YarnSpinnerConsole.CheckFileList(options.files, YarnSpinnerConsole.ALLOWED_EXTENSIONS); if (options.verbose && options.onlyUseTag != null) { YarnSpinnerConsole.Note(string.Format(CultureInfo.CurrentCulture, "Only using lines from nodes tagged \"{0}\"", options.onlyUseTag)); } bool linesWereUntagged = false; foreach (var file in options.files) { var dialogue = YarnSpinnerConsole.CreateDialogueForUtilities(); dialogue.LoadFile(file); var stringTable = dialogue.GetStringTable(); var emittedStringTable = new Dictionary <string, string> (); var anyLinesAreUntagged = false; foreach (var entry in stringTable) { // If options.onlyUseTag is set, we skip all lines in nodes that // don't have that tag. if (options.onlyUseTag != null) { // Find the tags for the node that this string is in LineInfo stringInfo; try { stringInfo = dialogue.program.lineInfo[entry.Key]; } catch (KeyNotFoundException) { YarnSpinnerConsole.Error(string.Format(CultureInfo.CurrentCulture, "{0}: lineInfo table does not contain an entry for line {1} (\"{2}\")", file, entry.Key, entry.Value)); return(1); } Node node; try { node = dialogue.program.nodes[stringInfo.nodeName]; } catch (KeyNotFoundException) { YarnSpinnerConsole.Error(string.Format(CultureInfo.CurrentCulture, "{0}: Line {1}'s lineInfo claims that the line originates in node {2}, but this node is not present in this program.", file, entry.Key, stringInfo.nodeName)); return(1); } var tags = node.tags; // If the tags don't include the one we're looking for, // skip this line if (tags.FindIndex(i => i == options.onlyUseTag) == -1) { continue; } } if (entry.Key.StartsWith("line:", StringComparison.InvariantCulture) == false) { anyLinesAreUntagged = true; } else { emittedStringTable [entry.Key] = entry.Value; } } if (anyLinesAreUntagged) { YarnSpinnerConsole.Warn(string.Format(CultureInfo.CurrentCulture, "Untagged lines in {0}", file)); linesWereUntagged = true; } // Generate the CSV using (var w = new System.IO.StringWriter()) { using (var csv = new CsvWriter(w)) { csv.WriteHeader <LocalisedLine>(); foreach (var entry in emittedStringTable) { var l = new LocalisedLine(); l.LineCode = entry.Key; l.LineText = entry.Value; l.Comment = ""; csv.WriteRecord(l); } var dir = System.IO.Path.GetDirectoryName(file); var fileName = System.IO.Path.GetFileNameWithoutExtension(file); fileName += "_lines.csv"; var filePath = System.IO.Path.Combine(dir, fileName); System.IO.File.WriteAllText(filePath, w.ToString()); if (options.verbose) { YarnSpinnerConsole.Note("Wrote " + filePath); } } } } if (linesWereUntagged) { YarnSpinnerConsole.Warn("Some lines were not tagged, so they weren't added to the " + "string file. Use this tool's 'generate' action to add them."); } return(0); }
static internal int AddLines(AddLabelsOptions options) { YarnSpinnerConsole.CheckFileList(options.files, YarnSpinnerConsole.ALLOWED_EXTENSIONS); var existingKeys = new List <string>(); foreach (var file in options.files) { NodeFormat fileFormat; try { fileFormat = Loader.GetFormatFromFileName(file); } catch (FormatException) { fileFormat = NodeFormat.Unknown; } switch (fileFormat) { case NodeFormat.JSON: break; case NodeFormat.Text: break; default: YarnSpinnerConsole.Warn(string.Format(CultureInfo.CurrentCulture, "Skipping file {0}, which is in unsupported format '{1}'", file, fileFormat)); continue; } Dialogue d = YarnSpinnerConsole.CreateDialogueForUtilities(); try { // First, we need to ensure that this file compiles. d.LoadFile(file); } #pragma warning disable CA1031 // Do not catch general exception types catch { YarnSpinnerConsole.Warn(string.Format(CultureInfo.CurrentCulture, "Skipping file {0} due to compilation errors.", file)); continue; } #pragma warning restore CA1031 // Do not catch general exception types Dictionary <string, LineInfo> linesWithNoTag = new Dictionary <string, LineInfo>(); // Filter the string info table to exclude lines that have a line code foreach (var entry in d.GetStringInfoTable()) { if (entry.Key.StartsWith("line:", StringComparison.InvariantCulture) == false) { linesWithNoTag[entry.Key] = entry.Value; } } if (linesWithNoTag.Count == 0) { var message = string.Format(CultureInfo.CurrentCulture, "{0} had no untagged lines. Either they're all tagged already, or it has no localisable text.", file); YarnSpinnerConsole.Note(message); continue; } // We also need the raw NodeInfo structures contained within this file. // These contain the source code to what we just compiled. Loader.NodeInfo[] nodeInfoList; var nodeFormat = Loader.GetFormatFromFileName(file); using (var reader = new System.IO.StreamReader(file)) { nodeInfoList = d.loader.GetNodesFromText(reader.ReadToEnd(), nodeFormat); } // Convert this list into an easier-to-index dictionary var lineInfo = new Dictionary <string, Loader.NodeInfo>(); foreach (var node in nodeInfoList) { lineInfo[node.title] = node; } // Make a list of line codes that we already know about. // This list will be updated as we tag lines, to prevent collisions. existingKeys.AddRange(lineInfo.Keys); bool anyNodesModified = false; // We now have a list of all strings that do not have a string tag. // Add a new tag to these lines. foreach (var line in linesWithNoTag) { // TODO: There's quite a bit of redundant work done here in each loop. // We're unzipping and re-combining the node for EACH line. Would be better // to do that only once. // Get the node that this line is in. var nodeInfo = lineInfo[line.Value.nodeName]; // Is this line contained within a rawText node? if (nodeInfo.tagsList.FindIndex(i => i == "rawText") != -1) { // We don't need to add a tag to it - genstrings will export // the whole thing for us. continue; } // If we have a tag to consider, and this node doesn't have that tag, // continue if (options.onlyUseTag != null) { if (nodeInfo.tagsList.FindIndex(i => i == options.onlyUseTag) == -1) { continue; } } // Split this node's source by newlines var lines = nodeInfo.body.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.None); // Get the original line var existingLine = lines[line.Value.lineNumber - 1]; // Generate a new tag for this line var newTag = GenerateString(existingKeys); // Remember that we've used this tag, to prevent it from being re-used existingKeys.Add(newTag); if (options.verbose) { YarnSpinnerConsole.Note(string.Format(CultureInfo.CurrentCulture, "Tagged line with ID \"{0}\" in node {1}: {2}", newTag, nodeInfo.title, existingLine)); } // Re-write the line. var newLine = string.Format(CultureInfo.InvariantCulture, "{0} #{1}", existingLine, newTag); lines[line.Value.lineNumber - 1] = newLine; // Pack this all back up into a single string nodeInfo.body = string.Join("\n", lines); // Put the updated node in the node collection. lineInfo[line.Value.nodeName] = nodeInfo; anyNodesModified = true; } // If we didn't end up changing anything, don't modify the file if (anyNodesModified == false) { continue; } // All the nodes have been updated; save this back to disk. // Are we doing a dry run? if (options.dryRun) { // Then bail out at this point, before we start // modifying files YarnSpinnerConsole.Note("Would have written to file " + file); continue; } var format = Loader.GetFormatFromFileName(file); switch (format) { case NodeFormat.JSON: break; case NodeFormat.Text: break; default: throw new ArgumentOutOfRangeException(); } // Convert the nodes into the correct format var savedData = FileFormatConverter.ConvertNodes(lineInfo.Values, fileFormat); // Write the file! using (var writer = new System.IO.StreamWriter(file)) { writer.Write(savedData); } } return(0); }
static string ConvertNodesToYarnText(IEnumerable <Loader.NodeInfo> nodes) { var sb = new System.Text.StringBuilder(); var properties = typeof(Loader.NodeInfo).GetProperties(); foreach (var node in nodes) { foreach (var property in properties) { // ignore the body attribute if (property.Name == "body") { continue; } // piggy-back off the JsonIgnoreAttribute to sense items that should not be serialised if (property.GetCustomAttributes(typeof(JsonIgnoreAttribute), false).Length > 0) { continue; } var field = property.Name; string value; var propertyType = property.PropertyType; if (propertyType.IsAssignableFrom(typeof(string))) { value = (string)property.GetValue(node, null); // avoid storing nulls when we could store the empty string instead if (value == null) { value = ""; } } else if (propertyType.IsAssignableFrom(typeof(int))) { value = ((int)property.GetValue(node, null)).ToString(); } else if (propertyType.IsAssignableFrom(typeof(Loader.NodeInfo.Position))) { var position = (Loader.NodeInfo.Position)property.GetValue(node, null); value = string.Format("{0},{1}", position.x, position.y); } else { YarnSpinnerConsole.Error(string.Format("Internal error: Node {0}'s property {1} has unsupported type {2}", node.title, property.Name, propertyType.FullName)); // will never be run, but prevents the compiler being mean about us not returning a value throw new Exception(); } var header = string.Format("{0}: {1}", field, value); sb.AppendLine(header); } // now write the body sb.AppendLine("---"); sb.AppendLine(node.body); sb.AppendLine("==="); } return(sb.ToString()); }