diff --git a/Data/TranslationData.cs b/Data/TranslationData.cs index 4bd2bce..f71952b 100644 --- a/Data/TranslationData.cs +++ b/Data/TranslationData.cs @@ -28,6 +28,11 @@ public void Validate() new TranslationItem() { Value = englishName } } }; + + public void Concat(TranslationData other) + { + Items.AddRange(other.Items); + } } [Serializable] diff --git a/Runtime/LangHelperMain.cs b/Runtime/LangHelperMain.cs index d5f1484..97edc6f 100644 --- a/Runtime/LangHelperMain.cs +++ b/Runtime/LangHelperMain.cs @@ -43,6 +43,56 @@ private static void DrawGUI(UnityModManager.ModEntry entry) { Settings.Draw(entry); + GUILayout.Space(5); + GUILayout.Label("NOTE: Changes made to translation overrides may require a game restart"); + GUILayout.BeginHorizontal(); + + if (GUILayout.Button("Reload Overrides", GUILayout.Width(200))) + { + OverrideManager.ReloadOverrides(true); + } + + if (GUILayout.Button("Open Folder", GUILayout.Width(200))) + { + if (!Directory.Exists(OverrideManager.OverrideDir)) + { + Directory.CreateDirectory(OverrideManager.OverrideDir); + } + System.Diagnostics.Process.Start("explorer.exe", OverrideManager.OverrideDir); + } + + GUILayout.EndHorizontal(); + + foreach (var source in TranslationInjector.Instances) + { + GUILayout.BeginHorizontal(); + + GUILayout.Label(source.Id); + + if (GUILayout.Button("New Override File", GUILayout.Width(200))) + { + OverrideManager.CreateOverrideFile(source.Id); + GuiMessage = $"Created {source.Id} override file"; + } + + GUI.enabled = OverrideManager.DoesOverrideExist(source.Id); + if (GUILayout.Button("Reload", GUILayout.Width(100))) + { + OverrideManager.ReloadOverrideForSource(source.Id); + GuiMessage = $"Reloaded overrides for {source.Id}"; + } + + if (GUILayout.Button("Delete", GUILayout.Width(100))) + { + OverrideManager.DeleteOverrideFile(source.Id); + GuiMessage = $"Deleted {source.Id} override file"; + } + GUI.enabled = true; + + GUILayout.EndHorizontal(); + } + + GUILayout.Space(5); if (!string.IsNullOrEmpty(GuiMessage)) { GUILayout.Label(GuiMessage); @@ -102,4 +152,20 @@ public override void Save(UnityModManager.ModEntry modEntry) Save(this, modEntry); } } + + [HarmonyPatch] + internal static class UMM_AfterLoadPatch + { + [HarmonyTargetMethod] + public static MethodInfo GetTarget() + { + return typeof(UnityModManager).GetNestedType("GameScripts", BindingFlags.NonPublic).GetMethod("OnAfterLoadMods"); + } + + [HarmonyPostfix] + public static void AfterLoaded() + { + OverrideManager.ReloadOverrides(false); + } + } } diff --git a/Runtime/OverrideManager.cs b/Runtime/OverrideManager.cs new file mode 100644 index 0000000..28f16ff --- /dev/null +++ b/Runtime/OverrideManager.cs @@ -0,0 +1,117 @@ +using I2.Loc; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace DVLangHelper.Runtime +{ + internal static class OverrideManager + { + public const string OVERRIDE_DIR_NAME = "override"; + + public static string OverrideDir => Path.Combine(LangHelperMain.Instance.Path, OVERRIDE_DIR_NAME); + public static string OverridePath(string sourceId) => Path.Combine(LangHelperMain.Instance.Path, OVERRIDE_DIR_NAME, $"{sourceId}.csv"); + + public static bool DoesOverrideExist(string sourceId) => File.Exists(OverridePath(sourceId)); + + public static void CreateOverrideFile(string sourceId) + { + static string CsvValue(string value) + { + return value.Contains(' ') ? $"\"{value}\"" : value; + } + + var source = TranslationInjector.Instances.First(x => x.Id == sourceId); + + if (!Directory.Exists(OverrideDir)) + { + Directory.CreateDirectory(OverrideDir); + } + + var sb = new StringBuilder(); + + // header + sb.Append("Key,Description"); + + int langCount = 0; + foreach (var lang in source.Languages) + { + sb.Append(','); + sb.Append(CsvValue(lang.Name)); + langCount += 1; + } + sb.AppendLine(); + + // terms + string termCommas = new string(Enumerable.Repeat(',', langCount).ToArray()); + foreach (var term in source.Terms) + { + sb.Append($"{term.Term},{CsvValue(term.Description)}"); + sb.AppendLine(termCommas); + } + + File.WriteAllText(OverridePath(sourceId), sb.ToString(), Encoding.UTF8); + } + + public static void DeleteOverrideFile(string sourceId) + { + string overridePath = OverridePath(sourceId); + if (File.Exists(overridePath)) + { + try + { + File.Delete(overridePath); + } + catch (Exception ex) + { + LangHelperMain.Error("Failed to delete override file", ex); + } + } + } + + public static void ReloadOverrideForSource(string sourceId) + { + string overrideFilePath = OverridePath(sourceId); + if (File.Exists(overrideFilePath) && + (TranslationInjector.Instances.FirstOrDefault(i => i.Id == sourceId) is TranslationInjector source)) + { + source.AddTranslationsFromCsv(overrideFilePath, true); + LangHelperMain.Log($"Loaded override file for {sourceId}"); + } + } + + public static void ReloadOverrides(bool reloadAll) + { + CoroutineManager.Start(ReloadOverridesWorker(reloadAll)); + } + + private static IEnumerator ReloadOverridesWorker(bool reloadAll) + { + foreach (string overrideFilePath in Directory.EnumerateFiles(OverrideDir, "*.csv")) + { + string sourceId = Path.GetFileNameWithoutExtension(overrideFilePath); + + if (TranslationInjector.Instances.FirstOrDefault(i => i.Id == sourceId) is TranslationInjector source) + { + if (reloadAll) + { + source.Reload(); + } + + while (source.PendingWebRequests) + { + yield return new WaitForEndOfFrame(); + } + + source.AddTranslationsFromCsv(overrideFilePath, true); + LangHelperMain.Log($"Loaded override file for {sourceId}"); + } + } + } + } +} diff --git a/Runtime/TranslationInjector.cs b/Runtime/TranslationInjector.cs index 49d230b..4d9aa53 100644 --- a/Runtime/TranslationInjector.cs +++ b/Runtime/TranslationInjector.cs @@ -22,25 +22,11 @@ public class TranslationInjector public static int ReloadTranslationFiles() { - int totalReloaded = 0; foreach (var instance in _instances) { - foreach (var file in instance._csvFiles) - { - switch (file.Type) - { - case CsvFileInfo.SourceType.Local: - instance.AddTranslationsFromCsv(file.Path); - totalReloaded++; - break; - case CsvFileInfo.SourceType.URL: - instance.AddTranslationsFromWebCsv(file.Path); - totalReloaded++; - break; - } - } + instance.Reload(); } - return totalReloaded; + return _instances.Count; } public readonly string Id; @@ -48,8 +34,15 @@ public static int ReloadTranslationFiles() private readonly LanguageSource _source; private readonly LanguageSourceData _langData; + public IEnumerable Languages => _langData.mLanguages; + public IEnumerable Terms => _langData.mTerms; + + private readonly Dictionary _originalData = new Dictionary(); private readonly List _csvFiles = new List(1); + private readonly List _pendingWebRequests = new List(); + public bool PendingWebRequests => _pendingWebRequests.Count > 0; + public TranslationInjector(string sourceId) { Id = sourceId; @@ -59,28 +52,58 @@ public TranslationInjector(string sourceId) _source = _sourceHolder.AddComponent(); _langData = _source.SourceData; + ResetData(); + + _instances.Add(this); + + if (InjectionStarted) + { + PerformInjection(); + } + } + + public void ResetData() + { + _langData.ClearAllData(); + foreach (DVLanguage language in Enum.GetValues(typeof(DVLanguage))) { _langData.AddLanguage(language.Name(), language.Code()); _langData.EnableLanguage(language.Name(), true); } + } - _instances.Add(this); + public void Reload() + { + ResetData(); - if (InjectionStarted) + foreach (var file in _csvFiles) { - PerformInjection(); + switch (file.Type) + { + case CsvFileInfo.SourceType.Local: + AddTranslationsFromCsv(file.Path); + break; + case CsvFileInfo.SourceType.URL: + AddTranslationsFromWebCsv(file.Path); + break; + } + } + + foreach (var kvp in _originalData) + { + AddTranslations(kvp.Key, kvp.Value, true); } } - public void AddTranslationsFromCsv(string csvPath) + public void AddTranslationsFromCsv(string csvPath, bool isOverride = false) { try { string csvText = LocalizationReader.ReadCSVfile(csvPath, Encoding.UTF8); _langData.Import_CSV(string.Empty, csvText, eSpreadsheetUpdateMode.Merge); - if (!_csvFiles.Any(f => f.Path == csvPath)) + if (!isOverride && !_csvFiles.Any(f => f.Path == csvPath)) { _csvFiles.Add(new CsvFileInfo(CsvFileInfo.SourceType.Local, csvPath)); } @@ -93,6 +116,7 @@ public void AddTranslationsFromCsv(string csvPath) public void AddTranslationsFromWebCsv(string url) { + _pendingWebRequests.Add(url); _source.StartCoroutine(AddWebCsvCoro(url)); } @@ -108,11 +132,13 @@ private IEnumerator AddWebCsvCoro(string url) if (LangHelperMain.Settings.UseCache && File.Exists(cachePath)) { LangHelperMain.Warning($"Failed to fetch web csv translations @ {url}, using cached"); + _pendingWebRequests.Remove(url); AddTranslationsFromCsv(cachePath); yield break; } LangHelperMain.Error($"Failed to fetch web csv translations @ {url}"); + _pendingWebRequests.Remove(url); yield break; } @@ -124,6 +150,7 @@ private IEnumerator AddWebCsvCoro(string url) _csvFiles.Add(new CsvFileInfo(CsvFileInfo.SourceType.URL, url)); } + _pendingWebRequests.Remove(url); LangHelperMain.Log($"Successfully fetched web csv translations from {url}"); if (LangHelperMain.Settings.UseCache) @@ -140,13 +167,15 @@ private IEnumerator AddWebCsvCoro(string url) } } - public void AddTranslations(string key, TranslationData data) + public void AddTranslations(string key, TranslationData data, bool isOverride = false) { - AddTranslations(key, data.Items); + AddTranslations(key, data.Items, isOverride); } - public void AddTranslations(string key, IEnumerable items) + public void AddTranslations(string key, IEnumerable items, bool isOverride = false) { + if (!isOverride) RegisterOriginalData(key, items.ToArray()); + var term = _langData.AddTerm(key); foreach (var item in items) @@ -156,14 +185,31 @@ public void AddTranslations(string key, IEnumerable items) } } - public void AddTranslation(string key, DVLanguage language, string value) + public void AddTranslation(string key, DVLanguage language, string value, bool isOverride = false) { + if (!isOverride) RegisterOriginalData(key, new TranslationItem(language, value)); + var term = _langData.AddTerm(key); int idx = _langData.GetLanguageIndexFromCode(language.Code()); term.SetTranslation(idx, value); } + private void RegisterOriginalData(string key, params TranslationItem[] items) + { + if (!_originalData.TryGetValue(key, out var data)) + { + data = new TranslationData() + { + Items = items.ToList(), + }; + _originalData.Add(key, data); + return; + } + + data.Items.AddRange(items); + } + public static void StartInjection() { foreach (var injector in Instances)