/// <summary>Formats a config node key/value line.</summary> /// <param name="indentation">The indentation in tabs. Each tab is 8 spaces.</param> /// <param name="key">The key string.</param> /// <param name="value"> /// The value string. It can contain multiple lines separated by a "\n" symbols. /// </param> /// <param name="meta"> /// The meta block of this field. Only the applicable properties will be used. If set to <c>null</c>, then no /// comments/MM logic be dispatched. /// </param> /// <returns>A properly formatted line.</returns> static string MakeConfigNodeLine(int indentation, string key, string value, MetaBlock meta = null) { meta ??= new MetaBlock(); var fieldName = (meta.mmCommand ?? "") + key + (meta.mmArguments ?? ""); var operand = (meta.mmOperator ?? "") + "="; var fieldValue = EscapeValue(value); var comment = (meta.inlineComment != null ? " // " + meta.inlineComment : ""); return(new string('\t', indentation) + fieldName + " " + operand + " " + fieldValue + comment); }
/// <summary>Extracts the strings that look like the localized tags.</summary> /// <remarks> /// This methods looks for the values that are localized in way it's done in the stock parts. /// </remarks> /// <param name="part">The part to extract the items for.</param> /// <param name="config"> /// The config node of the part. It's an extend version with the comments. /// </param> /// <returns>The list of extracted items.</returns> static List <LocItem> EmitItemsForNode(AvailablePart part, ConfigNode config) { var res = new List <LocItem>(); // Go through all the fields to detect if there are localized optional fields. foreach (var field in config.values.Cast <ConfigNode.Value>()) { if (string.IsNullOrEmpty(field.comment)) { continue; } var meta = MetaBlock.MakeFromString(field.comment); var comment = meta.inlineComment ?? ""; var match = Regex.Match(comment, @"^(#[a-zA-Z0-9_-]+)\s*=\s*(.+?)$"); if (!match.Success) { continue; // Not localized. } var locTag = match.Groups[1].Value; var locValue = field.value; if (field.value == locTag) { // In case of the tag is not get resolved, use the default template. DebugEx.Warning( "Field '{0}' in part {1} looks localized, but the tag '{2}' is not found" + " (suggested default value: '{3}')", field.name, part.name, locTag, match.Groups[2].Value); locValue = match.Groups[2].Value; } var item = new LocItem() { groupKey = "Part: " + part.name, fullFilePath = part.configFileFullName, locTag = locTag, locDefaultValue = locValue, }; res.Add(item); } // Scan the nested nodes. foreach (var nestedNode in config.GetNodes()) { res.AddRange(EmitItemsForNode(part, nestedNode)); } return(res); }
/// <summary>Merges localizable values from one config node to another.</summary> /// <remarks> /// The values in the nodes must be in the same order. The <paramref name="toNode"/> is allowed /// to have more values, the extra values will be silently skipped. /// </remarks> /// <param name="toNode">The node to merge value to. It's a regular node from the part prefab.</param> /// <param name="fromNode"> /// The node to merge values from. It must have comments loaded. Note, that the comments are encoded via the /// <see cref="MetaBlock"/>. /// </param> static void MergeLocalizableValues(ConfigNode toNode, ConfigNode fromNode) { for (var i = 0; i < fromNode.values.Count && i < toNode.values.Count; i++) { var fromValue = fromNode.values[i]; var toValue = toNode.values[i]; if (fromValue.name != toValue.name) { DebugEx.Error("Cannot merge config nodes.\nTO:\n{0}\nFROM:\n{1}", toNode, fromNode); return; } var metaBlock = MetaBlock.MakeFromString(fromValue.comment); if (IsLocalizationTag(metaBlock.inlineComment, firstWordOnly: true)) { toValue.value = fromValue.value; toValue.comment = metaBlock.inlineComment; } } for (var i = 0; i < fromNode.nodes.Count && i < toNode.nodes.Count; i++) { MergeLocalizableValues(toNode.nodes[i], fromNode.nodes[i]); } }
/// <summary>Extracts the localization items from the part's config.</summary> /// <param name="part">The part to extract items for.</param> /// <returns>All the localization items for the part.</returns> public static List <LocItem> EmitItemsForPart(AvailablePart part) { var res = new List <LocItem>(); // The part's config in the AvailablePart doesn't have all the fields and lacks the comments. // We do need the comments to resolve the field tag names, so load via a custom method. // In case of the custom loading method fails, use the stock one without the comments support. var config = (ConfigStore.LoadConfigWithComments(part.configFileFullName) ?? ConfigNode.Load(part.configFileFullName)).GetNode("PART"); if (config == null) { DebugEx.Error("Failed to load part's config: partName={0}, configUrl={1}", part.name, part.configFileFullName); return(res); } // Go through the fields we know must be localized. foreach (var fieldName in LocalizablePartFields) { var field = config.values.Cast <ConfigNode.Value>().FirstOrDefault(x => x.name == fieldName); if (field == null) { DebugEx.Warning("Field '{0}' is not found in the part {1} config", fieldName, part.name); continue; } config.values.Remove(field); // Don't handle it down the stream. string locTag = null; string locDefaultValue = null; var meta = MetaBlock.MakeFromString(field.comment); if (LocalizationManager.IsLocalizationTag(meta.inlineComment, firstWordOnly: true)) { var match = Regex.Match(meta.inlineComment, @"^(#[a-zA-Z0-9_-]+)\s*=\s*(.+?)$"); if (match.Success) { locTag = match.Groups[1].Value; if (field.value == locTag) { // Part's tag localization failed, use the default text. locDefaultValue = match.Groups[2].Value; } } else { DebugEx.Warning("Cannot resolve default localization tag in field {0} for part {1}: {2}", fieldName, config.GetValue("name"), meta.inlineComment); } } // ReSharper disable once UseStringInterpolation var fieldOrderStr = string.Format( "\0x00_{0:000}_{1}", // Use zero prefix to place it before anything else. Controller.partFieldsSorting.IndexOf(fieldName, StringComparison.Ordinal), fieldName); var item = new LocItem() { groupKey = "Part: " + part.name, sortKey = fieldOrderStr, fullFilePath = part.configFileFullName, locTag = locTag ?? MakePartFieldLocalizationTag(config.GetValue("name"), fieldName), locDefaultValue = locDefaultValue ?? field.value, }; res.Add(item); } res.AddRange(EmitItemsForNode(part, config)); return(res); }
/// <summary>Recursively collects and serializes the fields in the nodes.</summary> /// <remarks> /// Supports special field name <c>__commentField</c> to output the line comments. If the line /// comment is empty, then only an empty line is output into the result. /// </remarks> /// <param name="res"></param> /// <param name="node"></param> /// <param name="indentation"></param> static void SerializeNode(StringBuilder res, ConfigNode node, int indentation) { var indentSpaces = new string('\t', indentation); var nodeMeta = MetaBlock.MakeFromString(node.comment); foreach (var trailingLine in nodeMeta.trailingLines) { if (trailingLine.isEmptyLine) { res.AppendLine(); } else { res.AppendLine(indentSpaces + "// " + trailingLine.value); } } var nodeText = (nodeMeta.mmCommand ?? "") + node.name + (nodeMeta.mmArguments ?? ""); var openBlockComment = nodeMeta.openBlockComment != null ? " // " + nodeMeta.openBlockComment : ""; var closeBlockComment = nodeMeta.closeBlockComment != null ? " // " + nodeMeta.closeBlockComment : ""; var inlineComment = nodeMeta.inlineComment != null ? " // " + nodeMeta.inlineComment : ""; // Check for an empty block. Write it in a short form. if (node.values.Count == 0 && node.nodes.Count == 0 && openBlockComment == "" && inlineComment == "") { res.AppendLine(indentSpaces + nodeText + " {}" + closeBlockComment); return; } res.AppendLine(indentSpaces + nodeText + inlineComment); res.AppendLine(indentSpaces + "{" + openBlockComment); indentation++; var indentBlock = indentSpaces + '\t'; var fields = node.values.Cast <ConfigNode.Value>(); foreach (var field in fields) { var meta = MetaBlock.MakeFromString(field.comment); foreach (var line in meta.trailingLines) { if (line.isEmptyLine) { res.AppendLine(""); } else { res.AppendLine(indentBlock + "// " + line.value); } } if (meta.isFakeField) { continue; // No actual field is needed. } res.AppendLine(MakeConfigNodeLine(indentation, field.name, field.value, meta: meta)); } if (node.CountNodes > 0) { foreach (var childNode in node.GetNodes()) { SerializeNode(res, childNode, indentation); } } res.AppendLine(indentSpaces + "}" + closeBlockComment); }