using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; namespace PKHeX.Core { public static partial class Util { private const string TranslationSplitter = " = "; private static readonly Assembly thisAssembly = typeof(Util).GetTypeInfo().Assembly; private static readonly string[] manifestResourceNames = thisAssembly.GetManifestResourceNames(); private static readonly Dictionary resourceNameMap = new Dictionary(); private static readonly Dictionary stringListCache = new Dictionary(); private static readonly object getStringListLoadLock = new object(); #region String Lists /// /// Gets a list of all Pokémon species names. /// /// Language of the Pokémon species names to select (e.g. "en", "fr", "jp", etc.) /// An array of strings whose indexes correspond to the IDs of each Pokémon species name. public static string[] GetSpeciesList(string language) => GetStringList("species", language); /// /// Gets a list of all move names. /// /// Language of the move names to select (e.g. "en", "fr", "jp", etc.) /// An array of strings whose indexes correspond to the IDs of each move name. public static string[] GetMovesList(string language) => GetStringList("moves", language); /// /// Gets a list of all Pokémon ability names. /// /// Language of the Pokémon ability names to select (e.g. "en", "fr", "jp", etc.) /// An array of strings whose indexes correspond to the IDs of each Pokémon ability name. public static string[] GetAbilitiesList(string language) => GetStringList("abilities", language); /// /// Gets a list of all Pokémon nature names. /// /// Language of the Pokémon nature names to select (e.g. "en", "fr", "jp", etc.) /// An array of strings whose indexes correspond to the IDs of each Pokémon nature name. public static string[] GetNaturesList(string language) => GetStringList("natures", language); /// /// Gets a list of all Pokémon form names. /// /// Language of the Pokémon form names to select (e.g. "en", "fr", "jp", etc.) /// An array of strings whose indexes correspond to the IDs of each Pokémon form name. public static string[] GetFormsList(string language) => GetStringList("forms", language); /// /// Gets a list of all Pokémon type names. /// /// Language of the Pokémon type names to select (e.g. "en", "fr", "jp", etc.) /// An array of strings whose indexes correspond to the IDs of each Pokémon type name. public static string[] GetTypesList(string language) => GetStringList("types", language); /// /// Gets a list of all Pokémon characteristic. /// /// Language of the Pokémon characteristic to select (e.g. "en", "fr", "jp", etc.) /// An array of strings whose indexes correspond to the IDs of each Pokémon characteristic. public static string[] GetCharacteristicsList(string language) => GetStringList("character", language); /// /// Gets a list of all items. /// /// Language of the items to select (e.g. "en", "fr", "jp", etc.) /// An array of strings whose indexes correspond to the IDs of each item. public static string[] GetItemsList(string language) => GetStringList("items", language); public static string[][] GetLanguageStrings7(string fileName) { return new[] { Array.Empty(), // 0 - None GetStringList(fileName, "ja"), // 1 GetStringList(fileName, "en"), // 2 GetStringList(fileName, "fr"), // 3 GetStringList(fileName, "it"), // 4 GetStringList(fileName, "de"), // 5 Array.Empty(), // 6 - None GetStringList(fileName, "es"), // 7 }; } public static string[][] GetLanguageStrings8(string fileName) { return new[] { Array.Empty(), // 0 - None GetStringList(fileName, "ja"), // 1 GetStringList(fileName, "en"), // 2 GetStringList(fileName, "fr"), // 3 GetStringList(fileName, "it"), // 4 GetStringList(fileName, "de"), // 5 Array.Empty(), // 6 - None GetStringList(fileName, "es"), // 7 GetStringList(fileName, "ko"), // 8 }; } public static string[][] GetLanguageStrings10(string fileName, string zh2 = "zh") { return new[] { Array.Empty(), // 0 - None GetStringList(fileName, "ja"), // 1 GetStringList(fileName, "en"), // 2 GetStringList(fileName, "fr"), // 3 GetStringList(fileName, "it"), // 4 GetStringList(fileName, "de"), // 5 Array.Empty(), // 6 - None GetStringList(fileName, "es"), // 7 GetStringList(fileName, "ko"), // 8 GetStringList(fileName, "zh"), // 9 GetStringList(fileName, zh2), // 10 }; } #endregion public static string[] GetStringList(string fileName) { if (IsStringListCached(fileName, out var result)) return result; var txt = GetStringResource(fileName); // Fetch File, \n to list. return LoadStringList(fileName, txt); } public static bool IsStringListCached(string fileName, out string[] result) { lock (getStringListLoadLock) // Make sure only one thread can read the cache return stringListCache.TryGetValue(fileName, out result); } public static string[] LoadStringList(string file, string? txt) { if (txt == null) return Array.Empty(); string[] rawlist = txt.Split('\n'); for (int i = 0; i < rawlist.Length; i++) rawlist[i] = rawlist[i].TrimEnd('\r'); lock (getStringListLoadLock) // Make sure only one thread can write to the cache { if (!stringListCache.ContainsKey(file)) // Check cache again in case of race condition stringListCache.Add(file, rawlist); } return (string[])rawlist.Clone(); } public static string[] GetStringList(string fileName, string lang2char, string type = "text") => GetStringList($"{type}_{fileName}_{lang2char}"); public static byte[] GetBinaryResource(string name) { using var resource = thisAssembly.GetManifestResourceStream($"PKHeX.Core.Resources.byte.{name}"); var buffer = new byte[resource.Length]; resource.Read(buffer, 0, (int)resource.Length); return buffer; } public static string? GetStringResource(string name) { if (!resourceNameMap.TryGetValue(name, out var resname)) { bool Match(string x) => x.StartsWith("PKHeX.Core.Resources.text.") && x.EndsWith($"{name}.txt", StringComparison.OrdinalIgnoreCase); resname = Array.Find(manifestResourceNames, Match); if (resname == null) return null; resourceNameMap.Add(name, resname); } using var resource = thisAssembly.GetManifestResourceStream(resname); if (resource == null) return null; using var reader = new StreamReader(resource); return reader.ReadToEnd(); } #region Non-Form Translation /// /// Gets the names of the properties defined in the given input /// /// Enumerable of translation definitions in the form "Property = Value". private static string[] GetProperties(IEnumerable input) { return input.Select(l => l.Substring(0, l.IndexOf(TranslationSplitter, StringComparison.Ordinal))).ToArray(); } private static IEnumerable DumpStrings(Type t) { var props = ReflectUtil.GetPropertiesStartWithPrefix(t, string.Empty); return props.Select(p => $"{p}{TranslationSplitter}{ReflectUtil.GetValue(t, p)}"); } /// /// Gets the current localization in a static class containing language-specific strings /// /// public static string[] GetLocalization(Type t) => DumpStrings(t).ToArray(); /// /// Gets the current localization in a static class containing language-specific strings /// /// /// Existing localization lines (if provided) public static string[] GetLocalization(Type t, string[] existingLines) { var currentLines = GetLocalization(t); var existing = GetProperties(existingLines); var current = GetProperties(currentLines); var result = new string[currentLines.Length]; for (int i = 0; i < current.Length; i++) { int index = Array.IndexOf(existing, current[i]); result[i] = index < 0 ? currentLines[i] : existingLines[index]; } return result; } /// /// Applies localization to a static class containing language-specific strings. /// /// Type of the static class containing the desired strings. /// Lines containing the localized strings private static void SetLocalization(Type t, IReadOnlyCollection lines) { if (lines.Count == 0) return; foreach (var line in lines) { var index = line.IndexOf(TranslationSplitter, StringComparison.Ordinal); if (index < 0) continue; var prop = line.Substring(0, index); var value = line.Substring(index + TranslationSplitter.Length); try { ReflectUtil.SetValue(t, prop, value); } catch (Exception e) { Debug.WriteLine($"Property not present: {prop} || Value written: {value}"); Debug.WriteLine(e.Message); } } } /// /// Applies localization to a static class containing language-specific strings. /// /// Type of the static class containing the desired strings. /// Prefix of the language file to use. Example: if the target is legality_en.txt, should be "legality". /// Culture information private static void SetLocalization(Type t, string languageFilePrefix, string currentCultureCode) { SetLocalization(t, GetStringList($"{languageFilePrefix}_{currentCultureCode}")); } /// /// Applies localization to a static class containing language-specific strings. /// /// Type of the static class containing the desired strings. /// The values used to translate the given static class are retrieved from [TypeName]_[CurrentLangCode2].txt in the resource manager of PKHeX.Core. /// Culture information public static void SetLocalization(Type t, string currentCultureCode) { SetLocalization(t, t.Name, currentCultureCode); } #endregion #region DataSource Providing private static readonly string[] CountryRegionLanguages = {"ja", "en", "fr", "de", "it", "es", "zh", "ko"}; public static List GetCountryRegionList(string textfile, string lang) { string[] inputCSV = GetStringList(textfile); int index = Array.IndexOf(CountryRegionLanguages, lang); return GetCBListCSVSorted(inputCSV, index); } private static List GetCBListCSVSorted(string[] inputCSV, int index = 0) { var list = GetCBListFromCSV(inputCSV, index); list.Sort(Comparer); return list; } public static List GetCSVUnsortedCBList(string textfile) { string[] inputCSV = GetStringList(textfile); return GetCBListFromCSV(inputCSV, 0); } private static List GetCBListFromCSV(IReadOnlyList inputCSV, int index) { var arr = new List(inputCSV.Count - 1); // skip header index++; for (int i = 1; i < inputCSV.Count; i++) { var line = inputCSV[i]; var zeroth = line.IndexOf(','); var val = line.Substring(0, zeroth); var text = StringUtil.GetNthEntry(line, index, zeroth); var item = new ComboItem(text, Convert.ToInt32(val)); arr.Add(item); } return arr; } public static List GetCBList(IReadOnlyList inStrings) { var list = new List(inStrings.Count); for (int i = 0; i < inStrings.Count; i++) list.Add(new ComboItem(inStrings[i], i)); list.Sort(Comparer); return list; } public static List GetCBList(IReadOnlyList inStrings, IReadOnlyList allowed) { var list = new List(allowed.Count + 1) { new ComboItem(inStrings[0], 0) }; foreach (var index in allowed) list.Add(new ComboItem(inStrings[index], index)); list.Sort(Comparer); return list; } public static List GetCBList(IReadOnlyList inStrings, int index, int offset = 0) { var list = new List(); AddCBWithOffset(list, inStrings, offset, index); return list; } public static List GetCBList(IReadOnlyList inStrings, params int[][] allowed) { var count = allowed.Sum(z => z.Length); var list = new List(count); foreach (var arr in allowed) AddCB(list, inStrings, arr); return list; } public static void AddCBWithOffset(List list, IReadOnlyList inStrings, int offset, int index) { var item = new ComboItem(inStrings[index - offset], index); list.Add(item); } public static void AddCBWithOffset(List cbList, IReadOnlyList inStrings, int offset, params int[] allowed) { int beginCount = cbList.Count; foreach (var index in allowed) { var item = new ComboItem(inStrings[index - offset], index); cbList.Add(item); } cbList.Sort(beginCount, allowed.Length, Comparer); } public static void AddCB(List cbList, IReadOnlyList inStrings, int[] allowed) { int beginCount = cbList.Count; foreach (var index in allowed) { var item = new ComboItem(inStrings[index], index); cbList.Add(item); } cbList.Sort(beginCount, allowed.Length, Comparer); } public static List GetVariedCBListBall(string[] inStrings, int[] stringNum, int[] stringVal) { const int forcedTop = 3; // 3 Balls are preferentially first var list = new List(forcedTop + stringNum.Length) { new ComboItem(inStrings[4], (int)Ball.Poke), new ComboItem(inStrings[3], (int)Ball.Great), new ComboItem(inStrings[2], (int)Ball.Ultra), }; for (int i = 0; i < stringNum.Length; i++) { int index = stringNum[i]; var val = stringVal[i]; var txt = inStrings[index]; list.Add(new ComboItem(txt, val)); } list.Sort(forcedTop, stringNum.Length, Comparer); return list ; } private static readonly FunctorComparer Comparer = new FunctorComparer((a, b) => string.CompareOrdinal(a.Text, b.Text)); private sealed class FunctorComparer : IComparer { private readonly Comparison Comparison; public FunctorComparer(Comparison comparison) => Comparison = comparison; public int Compare(T x, T y) => Comparison(x, y); } #endregion } }