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 HarmonyLib; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using SunkenlandLocalizationAPI.Api; using UnityEngine; using UnityEngine.Localization; using UnityEngine.Localization.Settings; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("SunkenlandLocalizationAPI")] [assembly: AssemblyDescription("Sunkenland Localization API mod for Sunkenland by Ice Box Studio")] [assembly: AssemblyCompany("Ice Box Studio")] [assembly: AssemblyProduct("SunkenlandLocalizationAPI")] [assembly: AssemblyCopyright("Copyright © 2026 Ice Box Studio All rights reserved.")] [assembly: ComVisible(false)] [assembly: Guid("79870812-2aca-43d5-b058-5844d23fb3b4")] [assembly: AssemblyFileVersion("1.2.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyVersion("1.2.0.0")] namespace SunkenlandLocalizationAPI { public static class PluginInfo { public const string PLUGIN_GUID = "IceBoxStudio.Sunkenland.LocalizationAPI"; public const string PLUGIN_NAME = "SunkenlandLocalizationAPI"; public const string PLUGIN_VERSION = "1.2.0"; } [BepInPlugin("IceBoxStudio.Sunkenland.LocalizationAPI", "SunkenlandLocalizationAPI", "1.2.0")] public sealed class SunkenlandLocalizationAPI : BaseUnityPlugin { private Harmony _harmony; internal static ManualLogSource Log { get; private set; } private void Awake() { //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; LocalizationApi.LoadSavedLanguage(); LocalizationSettings.SelectedLocaleChanged += OnLanguageChanged; _harmony = new Harmony("IceBoxStudio.Sunkenland.LocalizationAPI"); _harmony.PatchAll(Assembly.GetExecutingAssembly()); Log.LogInfo((object)"SunkenlandLocalizationAPI 1.2.0 initialized."); } private static void OnLanguageChanged(Locale locale) { LocalizationApi.NotifyLanguageChanged(locale); } } } namespace SunkenlandLocalizationAPI.Patches { [HarmonyPatch(typeof(GameInitialization), "OnBeforeSplashScreen")] internal static class GameInitializationPatch { [HarmonyPostfix] private static void Postfix() { LocalizationApi.NotifyLanguageChanged(LocalizationSettings.SelectedLocale); } } } namespace SunkenlandLocalizationAPI.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[9] { "en", "zh-Hans", "fr", "de", "ja", "ko", "ru", "es", "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 path = Path.Combine(Application.persistentDataPath, "Setting.json"); if (File.Exists(path)) { JToken obj = JObject.Parse(File.ReadAllText(path))["SettingProfile"]; object obj2; if (obj == null) { obj2 = null; } else { JToken obj3 = obj[(object)"value"]; obj2 = ((obj3 != null) ? obj3[(object)"localeCode"] : null); } string text = (string)(JToken)obj2; if (!string.IsNullOrWhiteSpace(text) && GameLanguages.Contains(text)) { _currentLanguage = text; } } } 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(Locale locale) { string obj = (_currentLanguage = GetLanguage(locale)); 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 = SunkenlandLocalizationAPI.Log; if (log != null) { log.LogError((object)("Language change callback failed: " + ex)); } } } } private static string GetLanguage(Locale locale) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) object obj; if (!((Object)(object)locale == (Object)null)) { LocaleIdentifier identifier = locale.Identifier; obj = ((LocaleIdentifier)(ref identifier)).Code; } else { obj = null; } string text = (string)obj; if (string.IsNullOrEmpty(text) || !GameLanguages.Contains(text)) { return "en"; } return text; } 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 { return (value == null) ? null : AccessTools.Property(value.GetType(), propertyName)?.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) { 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 || !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; } } } }