using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Xml; using System.Xml.Linq; using RimWorld; using Verse; namespace WulaFallenEmpire.Utils { public static class DefInjectedExportUtility { private sealed class InjectionValue { public string Key; public bool IsCollection; public List Values; } public static void ExportDefInjectedTemplateFromDefs(ModContentPack content) { try { if (content?.ModMetaData == null) { Messages.Message("Export failed: Mod content metadata not found.", MessageTypeDefOf.RejectInput); return; } string outRoot = Path.Combine( GenFilePaths.SaveDataFolderPath, "WulaFallenEmpire_DefInjectedExport", DateTime.Now.ToString("yyyyMMWULA_HHmmss")); string outDefInjected = Path.Combine(outRoot, "English", "DefInjected"); Directory.CreateDirectory(outDefInjected); string outTsvPath = Path.Combine(outRoot, "worklist.tsv"); var entriesByFolder = new Dictionary>(StringComparer.OrdinalIgnoreCase); var seenKeys = new HashSet(StringComparer.OrdinalIgnoreCase); List defTypes = GenTypes.AllSubclassesNonAbstract(typeof(Def)).ToList(); defTypes.Sort((a, b) => string.Compare(a.FullName, b.FullName, StringComparison.OrdinalIgnoreCase)); foreach (Type defType in defTypes) { DefInjectionUtility.ForEachPossibleDefInjection( defType, (string suggestedPath, string normalizedPath, bool isCollection, string currentValue, IEnumerable currentValueCollection, bool translationAllowed, bool fullListTranslationAllowed, FieldInfo fieldInfo, Def def) => { if (!translationAllowed) { return; } if (string.IsNullOrWhiteSpace(suggestedPath)) { return; } if (suggestedPath.IndexOf(".modContentPack.", StringComparison.OrdinalIgnoreCase) >= 0) { return; } if (!isCollection && string.Equals(fieldInfo?.Name, "defName", StringComparison.OrdinalIgnoreCase)) { return; } List values; bool collectionOut; if (isCollection) { values = currentValueCollection?.Where(v => KeepValue(suggestedPath, fieldInfo, v)).ToList() ?? new List(); if (values.Count == 0) { return; } collectionOut = true; } else { if (!KeepValue(suggestedPath, fieldInfo, currentValue)) { return; } values = new List { currentValue }; collectionOut = false; } if (!seenKeys.Add(suggestedPath)) { return; } string folderName = GetDefInjectedFolderName(def?.GetType() ?? defType); if (!entriesByFolder.TryGetValue(folderName, out Dictionary folderEntries)) { folderEntries = new Dictionary(StringComparer.OrdinalIgnoreCase); entriesByFolder.Add(folderName, folderEntries); } folderEntries.Add( suggestedPath, new InjectionValue { Key = suggestedPath, IsCollection = collectionOut, Values = values }); }, content.ModMetaData); } WriteDefInjectedXmlOutputs(entriesByFolder, outDefInjected); WriteWorklistTsv(entriesByFolder, outTsvPath); Messages.Message($"DefInjected export written to: {outRoot}", MessageTypeDefOf.TaskCompletion); } catch (Exception ex) { Log.Error($"[WulaFallenEmpire] DefInjected export failed: {ex}"); Messages.Message("DefInjected export failed (see log).", MessageTypeDefOf.RejectInput); } } private static void WriteDefInjectedXmlOutputs( Dictionary> entriesByFolder, string outDefInjectedRoot) { foreach (KeyValuePair> folderPair in entriesByFolder) { string folder = folderPair.Key; Dictionary entries = folderPair.Value; if (entries.Count == 0) { continue; } string safeFolder = SanitizePathSegment(folder); string folderPath = Path.Combine(outDefInjectedRoot, safeFolder); Directory.CreateDirectory(folderPath); WriteXmlUtf8Indented( BuildLanguageDataDocument(entries, todoMode: false), Path.Combine(folderPath, "Auto_CN.xml")); WriteXmlUtf8Indented( BuildLanguageDataDocument(entries, todoMode: true), Path.Combine(folderPath, "Auto_TODO.xml")); } } private static XDocument BuildLanguageDataDocument( Dictionary entries, bool todoMode) { var languageData = new XElement("LanguageData"); foreach (InjectionValue entry in entries.Values.OrderBy(e => e.Key, StringComparer.OrdinalIgnoreCase)) { if (entry.IsCollection) { var element = new XElement(entry.Key); foreach (string value in entry.Values) { element.Add(new XElement("li", todoMode ? ToTodoListItem(value) : (value ?? string.Empty))); } languageData.Add(element); } else { string value = entry.Values.FirstOrDefault() ?? string.Empty; languageData.Add(new XElement(entry.Key, todoMode ? "TODO" : value)); } } return new XDocument(new XDeclaration("1.0", "utf-8", null), languageData); } private static void WriteWorklistTsv( Dictionary> entriesByFolder, string outTsvPath) { var lines = new List { "Folder\tKey\tCNSourceType\tCNSource" }; foreach (KeyValuePair> folderPair in entriesByFolder) { string folder = folderPair.Key; foreach (InjectionValue entry in folderPair.Value.Values.OrderBy(e => e.Key, StringComparer.OrdinalIgnoreCase)) { if (entry.IsCollection) { string joined = string.Join("\\n", entry.Values.Select(v => v ?? string.Empty)); lines.Add($"{EscapeTsv(folder)}\t{EscapeTsv(entry.Key)}\tlist\t{EscapeTsv(joined)}"); } else { string value = entry.Values.FirstOrDefault() ?? string.Empty; lines.Add($"{EscapeTsv(folder)}\t{EscapeTsv(entry.Key)}\ttext\t{EscapeTsv(value)}"); } } } string dir = Path.GetDirectoryName(outTsvPath); if (!string.IsNullOrEmpty(dir)) { Directory.CreateDirectory(dir); } File.WriteAllLines(outTsvPath, lines, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true)); } private static string GetDefInjectedFolderName(Type defType) { if (defType == null) { return "UnknownDefType"; } string ns = defType.Namespace ?? string.Empty; if (ns == "Verse" || ns == "RimWorld") { return defType.Name; } return defType.FullName ?? defType.Name; } private static string SanitizePathSegment(string segment) { if (string.IsNullOrEmpty(segment)) { return "_"; } foreach (char c in Path.GetInvalidFileNameChars()) { segment = segment.Replace(c, '_'); } return segment; } private static string ToTodoListItem(string original) { string s = original ?? string.Empty; int idx = s.IndexOf("->", StringComparison.Ordinal); if (idx >= 0) { return s.Substring(0, idx) + "->TODO"; } return "TODO"; } private static bool KeepValue(string key, FieldInfo fieldInfo, string value) { if (string.IsNullOrWhiteSpace(value)) { return false; } if (!string.IsNullOrEmpty(key)) { if (key.IndexOf(".defName", StringComparison.OrdinalIgnoreCase) >= 0) { return false; } if (key.IndexOf(".fileName", StringComparison.OrdinalIgnoreCase) >= 0) { return false; } } if (LooksLikeFilePath(value)) { return false; } if (LooksLikeAssetPath(value)) { return false; } string fieldName = fieldInfo?.Name ?? string.Empty; if (fieldName.IndexOf("label", StringComparison.OrdinalIgnoreCase) >= 0) { return true; } if (fieldName.IndexOf("description", StringComparison.OrdinalIgnoreCase) >= 0) { return true; } if (fieldName.IndexOf("title", StringComparison.OrdinalIgnoreCase) >= 0) { return true; } if (fieldName.IndexOf("text", StringComparison.OrdinalIgnoreCase) >= 0) { return true; } for (int i = 0; i < value.Length; i++) { char c = value[i]; if (c >= 0x4E00 && c <= 0x9FFF) { return true; } } if (value.IndexOfAny(new[] { ' ', '\n', '\r', '\t' }) >= 0) { return true; } if (value.IndexOfAny(new[] { ',', '。', '?', '!', ':', ';', '、', '(', ')', '《', '》', '“', '”', '"', '\'', ':', ';', ',', '.', '!', '?' }) >= 0) { return true; } return false; } private static bool LooksLikeFilePath(string value) { if (string.IsNullOrEmpty(value)) { return false; } if (value.Length >= 3 && char.IsLetter(value[0]) && value[1] == ':' && (value[2] == '\\' || value[2] == '/')) { return true; } if (value.Contains("\\\\") || value.Contains(":\\")) { return true; } return false; } private static bool LooksLikeAssetPath(string value) { if (string.IsNullOrEmpty(value)) { return false; } if (value.IndexOf('/') < 0 && value.IndexOf('\\') < 0) { return false; } for (int i = 0; i < value.Length; i++) { char c = value[i]; if (c >= 0x4E00 && c <= 0x9FFF) { return false; } } for (int i = 0; i < value.Length; i++) { char c = value[i]; if (char.IsWhiteSpace(c)) { return false; } } return true; } private static void WriteXmlUtf8Indented(XDocument doc, string path) { var settings = new XmlWriterSettings { Indent = true, IndentChars = " ", NewLineChars = "\n", NewLineHandling = NewLineHandling.Replace, OmitXmlDeclaration = false, Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false) }; using XmlWriter writer = XmlWriter.Create(path, settings); doc.Save(writer); } private static string EscapeTsv(string value) { if (value == null) { return string.Empty; } if (value.IndexOfAny(new[] { '\t', '\n', '\r', '"' }) >= 0) { return "\"" + value.Replace("\"", "\"\"") + "\""; } return value; } } }