using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using GraveyardKeeper2LocalizationAPI.Api; using HarmonyLib; using LazyBearTechnology; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("GraveyardKeeper2LocalizationAPI")] [assembly: AssemblyDescription("Graveyard Keeper 2 Localization API mod for Graveyard Keeper 2 by Ice Box Studio")] [assembly: AssemblyCompany("Ice Box Studio")] [assembly: AssemblyProduct("GraveyardKeeper2LocalizationAPI")] [assembly: AssemblyCopyright("Copyright © 2026 Ice Box Studio All rights reserved.")] [assembly: ComVisible(false)] [assembly: Guid("b1a645ec-4739-4dbd-86a4-2861a957efb5")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyVersion("1.0.0.0")] namespace GraveyardKeeper2LocalizationAPI { [BepInPlugin("IceBoxStudio.GraveyardKeeper2.LocalizationAPI", "GraveyardKeeper2LocalizationAPI", "1.0.0")] public sealed class GraveyardKeeper2LocalizationAPI : BaseUnityPlugin { internal static ManualLogSource Log { get; private set; } private void Awake() { Log = ((BaseUnityPlugin)this).Logger; LocalizationApi.LoadSavedLanguage(); GameSettings.OnLanguageChanged += OnLanguageChanged; Log.LogInfo((object)"GraveyardKeeper2LocalizationAPI 1.0.0 initialized."); } private static void OnLanguageChanged() { LocalizationApi.NotifyLanguageChanged(LLBase.CurrentLang); } } public static class PluginInfo { public const string PLUGIN_GUID = "IceBoxStudio.GraveyardKeeper2.LocalizationAPI"; public const string PLUGIN_NAME = "GraveyardKeeper2LocalizationAPI"; public const string PLUGIN_VERSION = "1.0.0"; } } namespace GraveyardKeeper2LocalizationAPI.Api { internal sealed class ConfigSectionTag { private readonly ModLocalizer _localizer; private readonly string _key; public string Section { get; } public string DisplayName => _localizer.GetLocalizedText(_key); public int? Order { get; } internal ConfigSectionTag(ModLocalizer localizer, string section, string key, int order) { _localizer = localizer; _key = key; Section = section; Order = order; } internal bool TryGetDisplayName(out string displayName) { displayName = _localizer.GetLocalizedText(_key); return !string.Equals(displayName, _key, StringComparison.Ordinal); } } internal sealed class ConfigEntryTag { private readonly ModLocalizer _localizer; private readonly string _key; public string DisplayName => _localizer.GetLocalizedText(_key + ".name"); public string Description => _localizer.GetLocalizedText(_key + ".description"); public int? Order { get; } public double? SliderStep { get; } internal ConfigEntryTag(ModLocalizer localizer, string key, int order, double? sliderStep = null) { _localizer = localizer; _key = key; Order = order; SliderStep = sliderStep; } internal bool TryGetText(string propertyName, out string text) { string text2 = (string.Equals(propertyName, "DisplayName", StringComparison.Ordinal) ? ".name" : ".description"); string text3 = _key + text2; text = _localizer.GetLocalizedText(text3); return !string.Equals(text, text3, StringComparison.Ordinal); } internal bool TryGetDisplayValue(object value, out string displayValue) { displayValue = null; string text = NormalizeValue(value); if (text.Length == 0) { return false; } string text2 = _key + "." + text; string localizedText = _localizer.GetLocalizedText(text2); if (string.Equals(localizedText, text2, StringComparison.Ordinal)) { return false; } displayValue = localizedText; return true; } private static string NormalizeValue(object value) { string text = Convert.ToString(value, CultureInfo.InvariantCulture); if (string.IsNullOrWhiteSpace(text)) { return string.Empty; } StringBuilder stringBuilder = new StringBuilder(text.Length + 4); for (int i = 0; i < text.Length; i++) { char c = text[i]; if (!char.IsLetterOrDigit(c)) { if (stringBuilder.Length > 0 && stringBuilder[stringBuilder.Length - 1] != '_') { stringBuilder.Append('_'); } continue; } if (char.IsUpper(c) && i > 0 && char.IsLower(text[i - 1]) && stringBuilder.Length > 0 && stringBuilder[stringBuilder.Length - 1] != '_') { stringBuilder.Append('_'); } stringBuilder.Append(char.ToLowerInvariant(c)); } return stringBuilder.ToString().Trim(new char[1] { '_' }); } } public static class LocalizationApi { public const string DefaultLanguage = "en"; private static readonly Dictionary Localizers = new Dictionary(StringComparer.OrdinalIgnoreCase); private static readonly HashSet GameLanguages = new HashSet(new string[11] { "en", "de", "fr", "pt-br", "es", "ru", "pl", "ja", "zh_cn", "ko", "tr" }, StringComparer.OrdinalIgnoreCase); private static string _currentLanguage = "en"; public static string CurrentLanguage => _currentLanguage; public static event Action LanguageChanged; public static ModLocalizer For(string pluginGuid) { if (string.IsNullOrWhiteSpace(pluginGuid)) { throw new ArgumentException("Plugin GUID cannot be empty.", "pluginGuid"); } if (!Localizers.TryGetValue(pluginGuid, out var value)) { value = new ModLocalizer(); Localizers.Add(pluginGuid, value); } return value; } public static bool TryGetCurrentGameLanguage(out string language) { language = CurrentLanguage; return true; } internal static void LoadSavedLanguage() { try { string text = PlayerPrefs.GetString("settings"); if (!string.IsNullOrWhiteSpace(text)) { string text2 = (string)JObject.Parse(text)["language"]; if (!string.IsNullOrWhiteSpace(text2) && GameLanguages.Contains(text2)) { _currentLanguage = text2; } } } catch { } } public static string GetConfigDescription(ConfigEntryBase entry) { string entryTagText = GetEntryTagText(entry, "Description"); if (!string.IsNullOrWhiteSpace(entryTagText)) { return entryTagText; } if (entry != null) { return entry.Description.Description; } return null; } public static string GetConfigDisplayName(ConfigEntryBase entry) { string entryTagText = GetEntryTagText(entry, "DisplayName"); if (!string.IsNullOrWhiteSpace(entryTagText)) { return entryTagText; } if (entry != null) { return entry.Definition.Key; } return null; } public static string GetConfigSection(string section, ConfigFile configFile) { ConfigEntryBase[] obj = ((configFile != null) ? configFile.GetConfigEntries() : null) ?? Array.Empty(); string text = null; ConfigEntryBase[] array = obj; foreach (ConfigEntryBase val in array) { if (!string.Equals(val.Definition.Section, section, StringComparison.Ordinal)) { continue; } ConfigDescription description = val.Description; object[] array2 = ((description != null) ? description.Tags : null) ?? Array.Empty(); foreach (object obj2 in array2) { if (!string.Equals(ReadString(obj2, "Section"), section, StringComparison.Ordinal)) { continue; } if (obj2 is ConfigSectionTag configSectionTag) { if (configSectionTag.TryGetDisplayName(out var displayName)) { text = displayName; } continue; } string text2 = ReadString(obj2, "DisplayName"); if (!string.IsNullOrWhiteSpace(text2)) { text = text2; } } } if (!string.IsNullOrWhiteSpace(text)) { return text; } return section; } public static bool TryGetConfigDisplayValue(ConfigEntryBase entry, object value, out string displayValue) { displayValue = null; if (entry == null || value == null) { return false; } ConfigDescription description = entry.Description; object[] array = ((description != null) ? description.Tags : null) ?? Array.Empty(); for (int i = 0; i < array.Length; i++) { if (array[i] is ConfigEntryTag configEntryTag && configEntryTag.TryGetDisplayValue(value, out displayValue)) { return true; } } return false; } internal static void NotifyLanguageChanged(string code) { string obj = (_currentLanguage = GetLanguage(code)); Action languageChanged = LocalizationApi.LanguageChanged; if (languageChanged == null) { return; } Delegate[] invocationList = languageChanged.GetInvocationList(); foreach (Delegate obj2 in invocationList) { try { ((Action)obj2)(obj); } catch (Exception ex) { ManualLogSource log = GraveyardKeeper2LocalizationAPI.Log; if (log != null) { log.LogError((object)("Language change callback failed: " + ex)); } } } } private static string GetLanguage(string code) { if (string.IsNullOrEmpty(code) || !GameLanguages.Contains(code)) { return "en"; } return code; } private static string GetEntryTagText(ConfigEntryBase entry, string propertyName) { if (entry == null) { return null; } string result = null; ConfigDescription description = entry.Description; object[] array = ((description != null) ? description.Tags : null) ?? Array.Empty(); foreach (object obj in array) { if (ReadProperty(obj, "Section") != null) { continue; } if (obj is ConfigEntryTag configEntryTag) { if (configEntryTag.TryGetText(propertyName, out var text)) { result = text; } continue; } string text2 = ReadString(obj, propertyName); if (!string.IsNullOrWhiteSpace(text2)) { result = text2; } } return result; } private static string ReadString(object value, string propertyName) { return ReadProperty(value, propertyName) as string; } private static object ReadProperty(object value, string propertyName) { try { if (value == null) { return null; } return AccessTools.FirstProperty(value.GetType(), (Func)((PropertyInfo property) => string.Equals(property.Name, propertyName, StringComparison.Ordinal)))?.GetValue(value, null); } catch { return null; } } } public sealed class ModLocalizer { private readonly Dictionary> _localizations = new Dictionary>(StringComparer.OrdinalIgnoreCase); internal ModLocalizer() { } public void RegisterJson(string jsonPath) { RegisterJson(jsonPath, requireDefaultLanguage: true); } public void RegisterJson(string jsonPath, bool requireDefaultLanguage) { if (string.IsNullOrWhiteSpace(jsonPath)) { throw new ArgumentException("Localization file path cannot be empty.", "jsonPath"); } if (!File.Exists(jsonPath)) { throw new FileNotFoundException("Localization file not found.", jsonPath); } Dictionary> dictionary = JsonConvert.DeserializeObject>>(File.ReadAllText(jsonPath)); if (dictionary == null) { throw new InvalidDataException("Localization JSON must contain an object."); } if (requireDefaultLanguage && !dictionary.ContainsKey("en")) { throw new InvalidDataException("Localization file must contain a en section."); } _localizations.Clear(); foreach (KeyValuePair> item in dictionary) { if (!string.IsNullOrWhiteSpace(item.Key) && item.Value != null) { _localizations[item.Key] = item.Value; } } } public string GetLocalizedText(string key, params object[] args) { if (string.IsNullOrEmpty(key)) { return string.Empty; } string currentLanguage = LocalizationApi.CurrentLanguage; if (TryGetText(currentLanguage, key, out var text)) { return Format(text, args); } if (!string.Equals(currentLanguage, "en", StringComparison.OrdinalIgnoreCase) && TryGetText("en", key, out text)) { return Format(text, args); } return key; } public ConfigDescription Config(string key, int entryOrder, string section, string sectionKey, int sectionOrder, AcceptableValueBase acceptableValues = null) { return CreateConfig(key, entryOrder, section, sectionKey, sectionOrder, acceptableValues, null); } public ConfigDescription Config(string key, int entryOrder, string section, string sectionKey, int sectionOrder, double sliderStep) { return CreateConfig(key, entryOrder, section, sectionKey, sectionOrder, null, sliderStep); } public ConfigDescription Config(string key, int entryOrder, string section, string sectionKey, int sectionOrder, AcceptableValueBase acceptableValues, double sliderStep) { return CreateConfig(key, entryOrder, section, sectionKey, sectionOrder, acceptableValues, sliderStep); } private ConfigDescription CreateConfig(string key, int entryOrder, string section, string sectionKey, int sectionOrder, AcceptableValueBase acceptableValues, double? sliderStep) { //IL_00d8: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Expected O, but got Unknown if (string.IsNullOrWhiteSpace(key)) { throw new ArgumentException("Localization key cannot be empty.", "key"); } if (string.IsNullOrWhiteSpace(section)) { throw new ArgumentException("Config section cannot be empty.", "section"); } if (string.IsNullOrWhiteSpace(sectionKey)) { throw new ArgumentException("Section localization key cannot be empty.", "sectionKey"); } if (sliderStep.HasValue && (sliderStep.Value <= 0.0 || double.IsNaN(sliderStep.Value) || double.IsInfinity(sliderStep.Value))) { throw new ArgumentOutOfRangeException("sliderStep"); } string text = key + ".description"; string text2 = GetLocalizedText(text); if (string.Equals(text2, text, StringComparison.Ordinal)) { text2 = string.Empty; } return new ConfigDescription(text2, acceptableValues, new object[2] { new ConfigSectionTag(this, section, sectionKey, sectionOrder), new ConfigEntryTag(this, key, entryOrder, sliderStep) }); } private bool TryGetText(string language, string key, out string text) { text = null; if (_localizations.TryGetValue(language, out var value)) { return value.TryGetValue(key, out text); } return false; } private static string Format(string text, object[] args) { if (args == null || args.Length == 0) { return text; } try { return string.Format(text, args); } catch (FormatException) { return text; } } } }