Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of ValheimAutoTranslator v0.1.4
BepInEx/plugins/ValheimAutoTranslator.dll
Decompiled a day agousing System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using System.Threading; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using TMPro; using UnityEngine; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyVersion("0.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace ValheimAutoTranslator { public static class LlmClient { public struct ProbeResult { public bool Success; public int HttpCode; public string Error; public int? RetryAfterSeconds; public string ResponsePreview; } public static Dictionary<string, string> TranslateBatch(GATSettings s, string context, Dictionary<string, string> items, bool isRetry = false, Dictionary<string, string> retryHints = null) { if (items == null || items.Count == 0) { return new Dictionary<string, string>(); } Dictionary<string, string> dictionary = new Dictionary<string, string>(items.Count, StringComparer.Ordinal); Dictionary<string, Dictionary<int, string>> dictionary2 = new Dictionary<string, Dictionary<int, string>>(items.Count, StringComparer.Ordinal); foreach (KeyValuePair<string, string> item in items) { dictionary[item.Key] = PlaceholderGuard.MaskPlaceholders(item.Value, out var map); if (map.Count > 0) { dictionary2[item.Key] = map; } } float temperature = (isRetry ? 0.3f : 0f); Dictionary<string, Dictionary<int, string>> requiredMarkers = (isRetry ? dictionary2 : null); string body = BuildRequestBody(s, context, dictionary, temperature, requiredMarkers, retryHints); for (int i = 1; i <= 3; i++) { try { string text = Post(s, body); if (text == null) { return null; } string text2 = MiniJson.ExtractStringField(text, "content"); if (string.IsNullOrEmpty(text2)) { GATLog.Warn("Пустой content в ответе. Сырой ответ: " + Trim(text, 800)); return null; } Dictionary<string, string> dictionary3 = MiniJson.ParseFlatObject(text2); if (dictionary3 == null || dictionary3.Count == 0) { GATLog.Warn("Не удалось разобрать JSON модели: " + Trim(text2, 800)); return null; } Dictionary<string, string> dictionary4 = new Dictionary<string, string>(dictionary3.Count, StringComparer.Ordinal); foreach (KeyValuePair<string, string> item2 in dictionary3) { string text3 = PlaceholderGuard.NormalizeFullwidth(item2.Value); if (dictionary2.TryGetValue(item2.Key, out var value) && value.Count > 0) { if (!PlaceholderGuard.ValidateMarkers(text3, value, out var reason) && s.verboseLogging) { GATLog.Warn("ValidateMarkers fail (" + reason + ") key=" + item2.Key); } text3 = PlaceholderGuard.UnmaskPlaceholders(text3, value); } dictionary4[item2.Key] = text3; } return dictionary4; } catch (WebException ex) { int num = 0; string text4 = ex.Message; if (ex.Response is HttpWebResponse httpWebResponse) { num = (int)httpWebResponse.StatusCode; try { using StreamReader streamReader = new StreamReader(httpWebResponse.GetResponseStream(), Encoding.UTF8); text4 = Trim(streamReader.ReadToEnd(), 500); } catch { } } bool flag = num == 429 || num == 408 || num >= 500 || num == 0; GATLog.Warn("HTTP " + num + " (попытка " + i + "/3): " + text4); if (!flag || i == 3) { return null; } Thread.Sleep(1500 * i * i); } catch (Exception ex2) { GATLog.Warn("Ошибка запроса (попытка " + i + "/3): " + ex2.Message); if (i == 3) { return null; } Thread.Sleep(1500 * i); } } return null; } private static string BuildRequestBody(GATSettings s, string context, Dictionary<string, string> items, float temperature = 0f, Dictionary<string, Dictionary<int, string>> requiredMarkers = null, Dictionary<string, string> retryHints = null) { StringBuilder stringBuilder = new StringBuilder(2048); stringBuilder.Append('{'); stringBuilder.Append("\"model\":\"").Append(MiniJson.Escape(s.model)).Append("\","); stringBuilder.Append("\"temperature\":").Append(temperature.ToString("0.0", CultureInfo.InvariantCulture)).Append(","); stringBuilder.Append("\"stream\":false,"); if (s.sendReasoningEffortNone) { stringBuilder.Append("\"reasoning_effort\":\"none\","); } if (s.requestJsonObject) { stringBuilder.Append("\"response_format\":{\"type\":\"json_object\"},"); } stringBuilder.Append("\"messages\":["); stringBuilder.Append("{\"role\":\"system\",\"content\":\"").Append(MiniJson.Escape("Ты переводчик модов игры Valheim на русский язык. Ответь только JSON-объектом {\"id\":\"перевод\"}. Переводи только значения, ключи не меняй. Сохраняй все маркеры ⟦1⟧, ⟦2⟧ ровно по одному разу, а также HTML/TMP-теги, переносы строк и регистр. Не добавляй пояснения. Кириллические фрагменты исходника сохраняй дословно. Для коротких элементов интерфейса используй краткий естественный перевод. Имена боссов: Eikthyr — Эйктюр, The Elder — Древний, Bonemass — Масса костей, Moder — Моудер, Yagluth — Яглут.")).Append("\"},"); stringBuilder.Append("{\"role\":\"user\",\"content\":\"").Append(MiniJson.Escape(Prompt.BuildUserMessage(context, items, requiredMarkers, retryHints))).Append("\"}"); stringBuilder.Append("]}"); return stringBuilder.ToString(); } private static string Post(GATSettings s, string body, int timeoutOverrideSeconds = 0) { HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(s.endpoint); httpWebRequest.Method = "POST"; httpWebRequest.ContentType = "application/json; charset=utf-8"; httpWebRequest.Timeout = ((timeoutOverrideSeconds > 0) ? timeoutOverrideSeconds : Math.Max(15, s.timeoutSeconds)) * 1000; httpWebRequest.ReadWriteTimeout = httpWebRequest.Timeout; httpWebRequest.KeepAlive = true; httpWebRequest.Proxy = null; if (!string.IsNullOrEmpty(s.apiKey)) { httpWebRequest.Headers["Authorization"] = "Bearer " + s.apiKey; } byte[] bytes = Encoding.UTF8.GetBytes(body); httpWebRequest.ContentLength = bytes.Length; using (Stream stream = httpWebRequest.GetRequestStream()) { stream.Write(bytes, 0, bytes.Length); } using HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse(); using StreamReader streamReader = new StreamReader(httpWebResponse.GetResponseStream(), Encoding.UTF8); return streamReader.ReadToEnd(); } public static int? ParseRetryAfterSeconds(string value, DateTime utcNow) { if (string.IsNullOrEmpty(value)) { return null; } if (int.TryParse(value.Trim(), out var result) && result > 0 && result <= 1800) { return result; } return null; } public static ProbeResult Probe(GATSettings s) { ProbeResult result = new ProbeResult { Success = false }; string body = BuildProbeRequestBody(s); try { string json = Post(s, body, 20); result.HttpCode = 200; string text = MiniJson.ExtractStringField(json, "content"); if (string.IsNullOrEmpty(text)) { result.Error = "Empty content"; } else { result.Success = true; result.ResponsePreview = Trim(text, 100); } } catch (WebException ex) { if (ex.Response is HttpWebResponse httpWebResponse) { result.HttpCode = (int)httpWebResponse.StatusCode; if (result.HttpCode == 429) { string value = httpWebResponse.Headers["Retry-After"]; result.RetryAfterSeconds = ParseRetryAfterSeconds(value, DateTime.UtcNow); } try { using StreamReader streamReader = new StreamReader(httpWebResponse.GetResponseStream(), Encoding.UTF8); result.Error = Trim(streamReader.ReadToEnd(), 500); } catch { result.Error = ex.Message; } } else { result.Error = ex.Message; } } catch (Exception ex2) { result.Error = ex2.Message; } return result; } private static string BuildProbeRequestBody(GATSettings s) { StringBuilder stringBuilder = new StringBuilder(256); stringBuilder.Append('{'); stringBuilder.Append("\"model\":\"").Append(MiniJson.Escape(s.model)).Append("\","); stringBuilder.Append("\"stream\":false,"); if (s.sendReasoningEffortNone) { stringBuilder.Append("\"reasoning_effort\":\"none\","); } stringBuilder.Append("\"messages\":[{\"role\":\"user\",\"content\":\"Reply only OK\"}]}"); return stringBuilder.ToString(); } public static string SelfTest(GATSettings s) { Dictionary<string, string> dictionary = new Dictionary<string, string> { { "1", "Root Wand" }, { "2", "YOU ARE NOT WORTHY! DEFEAT $1!" }, { "3", "<color=#FF0000>Critical</color> failure" }, { "4", "Rootweave Chest" }, { "5", "钢制长剑" }, { "6", "$1被击倒了。" }, { "7", "根の杖" }, { "8", "Stahllangschwert" }, { "9", "Attack <color=#FF0000>$1</color> now" } }; Dictionary<string, string> dictionary2 = TranslateBatch(s, "label", dictionary); if (dictionary2 == null) { return "ОШИБКА: нет ответа. Смотри BepInEx/LogOutput.log."; } StringBuilder stringBuilder = new StringBuilder(); foreach (KeyValuePair<string, string> item in dictionary) { dictionary2.TryGetValue(item.Key, out var value); string reason = null; bool flag = value != null && PlaceholderGuard.Validate(item.Value, value, out reason); if (!flag) { reason = reason ?? "нет ключа в ответе"; } stringBuilder.Append(flag ? "[OK] " : "[FAIL] ").Append(value ?? "(null)"); if (!flag) { stringBuilder.Append(" <- ").Append(reason); } stringBuilder.Append('\n'); } return stringBuilder.ToString(); } private static string Trim(string s, int max) { if (string.IsNullOrEmpty(s)) { return string.Empty; } if (s.Length > max) { return s.Substring(0, max) + "..."; } return s; } } public static class MiniJson { public static string Escape(string s) { if (string.IsNullOrEmpty(s)) { return string.Empty; } StringBuilder stringBuilder = new StringBuilder(s.Length + 16); foreach (char c in s) { switch (c) { case '"': stringBuilder.Append("\\\""); continue; case '\\': stringBuilder.Append("\\\\"); continue; case '\n': stringBuilder.Append("\\n"); continue; case '\r': stringBuilder.Append("\\r"); continue; case '\t': stringBuilder.Append("\\t"); continue; case '\b': stringBuilder.Append("\\b"); continue; case '\f': stringBuilder.Append("\\f"); continue; } if (c < ' ') { StringBuilder stringBuilder2 = stringBuilder.Append("\\u"); int num = c; stringBuilder2.Append(num.ToString("x4")); } else { stringBuilder.Append(c); } } return stringBuilder.ToString(); } public static string Unescape(string s) { if (string.IsNullOrEmpty(s)) { return string.Empty; } StringBuilder stringBuilder = new StringBuilder(s.Length); for (int i = 0; i < s.Length; i++) { char c = s[i]; if (c != '\\') { stringBuilder.Append(c); continue; } if (++i >= s.Length) { break; } switch (s[i]) { case '"': stringBuilder.Append('"'); break; case '\\': stringBuilder.Append('\\'); break; case '/': stringBuilder.Append('/'); break; case 'n': stringBuilder.Append('\n'); break; case 'r': stringBuilder.Append('\r'); break; case 't': stringBuilder.Append('\t'); break; case 'b': stringBuilder.Append('\b'); break; case 'f': stringBuilder.Append('\f'); break; case 'u': { if (i + 4 < s.Length && int.TryParse(s.Substring(i + 1, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var result)) { stringBuilder.Append((char)result); i += 4; } break; } default: stringBuilder.Append(s[i]); break; } } return stringBuilder.ToString(); } public static string ExtractStringField(string json, string fieldName) { if (string.IsNullOrEmpty(json)) { return null; } string text = "\"" + fieldName + "\""; int startIndex = 0; int i; while (true) { startIndex = json.IndexOf(text, startIndex, StringComparison.Ordinal); if (startIndex < 0) { return null; } for (i = startIndex + text.Length; i < json.Length && (json[i] == ' ' || json[i] == '\t'); i++) { } if (i >= json.Length || json[i] != ':') { startIndex += text.Length; continue; } for (i++; i < json.Length && (json[i] == ' ' || json[i] == '\t'); i++) { } if (i >= json.Length) { return null; } if (json[i] == '"') { break; } startIndex += text.Length; } i++; int num = i; bool flag = false; while (i < json.Length) { if (json[i] == '\\') { i += 2; continue; } if (json[i] == '"') { flag = true; break; } i++; } if (!flag || i > json.Length) { return null; } return Unescape(json.Substring(num, i - num)); } public static Dictionary<string, string> ParseFlatObject(string json) { Dictionary<string, string> dictionary = new Dictionary<string, string>(); if (string.IsNullOrEmpty(json)) { return dictionary; } int num = json.IndexOf('{'); int num2 = json.LastIndexOf('}'); if (num < 0 || num2 <= num) { return dictionary; } string text = json.Substring(num + 1, num2 - num - 1); int i = 0; while (i < text.Length) { for (; i < text.Length && text[i] != '"'; i++) { } if (i >= text.Length) { break; } i++; string text2 = ReadRawString(text, ref i); if (text2 == null) { break; } for (; i < text.Length && text[i] != ':'; i++) { } if (i >= text.Length) { break; } for (i++; i < text.Length && text[i] != '"'; i++) { } if (i >= text.Length) { break; } i++; string text3 = ReadRawString(text, ref i); if (text3 == null) { break; } dictionary[Unescape(text2)] = Unescape(text3); } return dictionary; } private static string ReadRawString(string s, ref int i) { int num = i; bool flag = false; while (i < s.Length) { if (s[i] == '\\') { i += 2; continue; } if (s[i] == '"') { flag = true; break; } i++; } if (!flag) { i = s.Length; return null; } string result = s.Substring(num, i - num); i++; return result; } } public static class PlaceholderGuard { public struct PlaceholderMatch { public int Index; public int Length; public string Value; } public enum ScriptKind { None, Cyrillic, Latin, Cjk, Other } public static readonly Regex Ph = new Regex("\\[\\[[^\\]]*\\]\\]|\\{[^{}]*\\}|<[^<>]+>|\\[[^\\[\\]]+\\]|\\$[A-Za-z_][A-Za-z_0-9]*|\\$[0-9]+|\\\\n", RegexOptions.Compiled); private static readonly HashSet<string> LatinStopList = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "kg", "cm", "mm", "ms", "px", "hp", "mp", "xp", "fps", "tps", "hz", "ml", "kb", "mb", "gb", "id", "ui" }; private static readonly Regex TagRegex = new Regex("⟦[^⟧]*⟧|<[^>]*>|\\{[^{}]*\\}|\\[\\[[^\\]]*\\]\\]|\\[[^\\]]*\\]|\\$[A-Za-z_][A-Za-z_0-9]*|\\$[0-9]+", RegexOptions.Compiled); private static readonly HashSet<string> CommonServiceWords = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "Изготавливает", "Делает", "Создает", "Создаёт", "Постройка", "Любые", "Патроны", "Требуется", "Каждый", "Каждая", "Каждое", "Снаряды", "Рецепт", "Сборка", "для", "в", "во", "и", "с", "со", "на", "от", "из", "по", "к", "ко", "у", "о", "об", "за", "до", "при", "над", "под", "а", "но", "или", "не", "же", "ли", "что", "как", "это" }; public const string MarkerLeft = "⟦"; public const string MarkerRight = "⟧"; public static List<PlaceholderMatch> GetPlaceholderMatches(string s) { List<PlaceholderMatch> list = new List<PlaceholderMatch>(); if (string.IsNullOrEmpty(s)) { return list; } if (s.IndexOf("{lookup:", StringComparison.OrdinalIgnoreCase) < 0) { foreach (Match item in Ph.Matches(s)) { list.Add(new PlaceholderMatch { Index = item.Index, Length = item.Length, Value = item.Value }); } return list; } int num = 0; while (num < s.Length) { int num2 = s.IndexOf("{lookup:", num, StringComparison.OrdinalIgnoreCase); if (num2 == num) { int num3 = 1; int i; for (i = num + 8; i < s.Length; i++) { if (num3 <= 0) { break; } if (s[i] == '{') { num3++; } else if (s[i] == '}') { num3--; } } if (num3 == 0) { int length = i - num; list.Add(new PlaceholderMatch { Index = num, Length = length, Value = s.Substring(num, length) }); num = i; } else { num += 8; } continue; } Match match2 = Ph.Match(s, num); if (match2.Success && (num2 < 0 || match2.Index <= num2)) { list.Add(new PlaceholderMatch { Index = match2.Index, Length = match2.Length, Value = match2.Value }); num = match2.Index + match2.Length; } else if (num2 >= 0) { num = num2; } else { if (!match2.Success) { break; } num = match2.Index; } } return list; } private static ScriptKind ClassifyChar(char c) { if (c == '×' || c == '÷') { return ScriptKind.None; } if (c >= 'Ѐ' && c <= 'ԯ') { return ScriptKind.Cyrillic; } if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) { return ScriptKind.Latin; } if (c >= 'À' && c <= 'ɏ') { return ScriptKind.Latin; } if (c >= '\u3040' && c <= 'ヿ') { return ScriptKind.Cjk; } if (c >= '㐀' && c <= '䶿') { return ScriptKind.Cjk; } if (c >= '一' && c <= '鿿') { return ScriptKind.Cjk; } if (c >= '豈' && c <= '\ufaff') { return ScriptKind.Cjk; } if (c >= '가' && c <= '\ud7af') { return ScriptKind.Cjk; } if (c >= 'ᄀ' && c <= 'ᇿ') { return ScriptKind.Cjk; } if (c >= '\u3130' && c <= '\u318f') { return ScriptKind.Cjk; } if (c >= 'Ͱ' && c <= 'Ͽ') { return ScriptKind.Other; } if (c >= '\u0590' && c <= '\u05ff') { return ScriptKind.Other; } if (c >= '\u0600' && c <= 'ۿ') { return ScriptKind.Other; } if (c >= 'ݐ' && c <= 'ݿ') { return ScriptKind.Other; } if (c >= '\u0900' && c <= 'ॿ') { return ScriptKind.Other; } if (c >= '\u0e00' && c <= '\u0e7f') { return ScriptKind.Other; } return ScriptKind.None; } public static void CountScripts(string s, out int cyr, out int lat, out int cjk, out int other) { cyr = 0; lat = 0; cjk = 0; other = 0; if (string.IsNullOrEmpty(s)) { return; } string text = StripTagsAndPlaceholders(s); for (int i = 0; i < text.Length; i++) { switch (ClassifyChar(text[i])) { case ScriptKind.Cyrillic: cyr++; break; case ScriptKind.Latin: lat++; break; case ScriptKind.Cjk: cjk++; break; case ScriptKind.Other: other++; break; } } } public static ScriptKind DetectScript(string s) { CountScripts(s, out var cyr, out var lat, out var cjk, out var other); if (cjk > 0 && cjk * 3 >= lat) { return ScriptKind.Cjk; } int num = Math.Max(cyr, Math.Max(lat, Math.Max(cjk, other))); if (num == 0) { return ScriptKind.None; } if (num == cyr) { return ScriptKind.Cyrillic; } if (num == lat) { return ScriptKind.Latin; } if (num == cjk) { return ScriptKind.Cjk; } return ScriptKind.Other; } public static List<string> Placeholders(string s) { List<string> list = new List<string>(); if (string.IsNullOrEmpty(s)) { return list; } foreach (PlaceholderMatch placeholderMatch in GetPlaceholderMatches(s)) { string text = placeholderMatch.Value; if (text.StartsWith("{") && text.EndsWith("}") && text.Contains("?")) { int num = text.IndexOf('?'); text = text.Substring(0, num + 1); } list.Add(text); } list.Sort(StringComparer.Ordinal); return list; } public static bool NeedsTranslation(string text) { if (string.IsNullOrEmpty(text)) { return false; } if (text.Length > 4000) { return false; } string text2 = StripTagsAndPlaceholders(text); CountScripts(text2, out var cyr, out var lat, out var cjk, out var other); if (lat + cjk + other == 0) { return false; } if (cyr == 0) { if (cjk > 0 || other > 0) { return true; } if (lat < 2) { return false; } if (text2.IndexOf(' ') < 0 && text2.Length < 3) { return false; } return true; } if (cjk >= 2 || other >= 4) { return true; } int num = 0; int num2 = 0; int num3 = 0; int num4 = 0; int startIndex = 0; bool flag = false; for (int i = 0; i < text2.Length; i++) { char c = text2[i]; if (ClassifyChar(c) == ScriptKind.Latin) { if (num4 == 0) { startIndex = i; flag = char.IsLower(c); } num4++; } else { if (num4 <= 0) { continue; } string item = text2.Substring(startIndex, num4); if (num4 >= 3 && !LatinStopList.Contains(item)) { num3 += num4; num++; if (flag) { num2++; } } num4 = 0; } } if (num4 > 0) { string item2 = text2.Substring(startIndex, num4); if (num4 >= 3 && !LatinStopList.Contains(item2)) { num3 += num4; num++; if (flag) { num2++; } } } if (num2 >= 1) { return true; } if (num >= 3 && num3 >= 12) { return true; } return false; } public static bool ShouldTranslate(string src) { return NeedsTranslation(src); } public static string StripTagsAndPlaceholders(string s) { if (string.IsNullOrEmpty(s)) { return string.Empty; } if (s.IndexOf("{lookup:", StringComparison.OrdinalIgnoreCase) < 0) { return TagRegex.Replace(s, " "); } List<PlaceholderMatch> placeholderMatches = GetPlaceholderMatches(s); if (placeholderMatches.Count == 0) { return TagRegex.Replace(s, " "); } StringBuilder stringBuilder = new StringBuilder(s.Length); int num = 0; foreach (PlaceholderMatch item in placeholderMatches) { stringBuilder.Append(s, num, item.Index - num).Append(' '); num = item.Index + item.Length; } stringBuilder.Append(s, num, s.Length - num); return TagRegex.Replace(stringBuilder.ToString(), " "); } private static bool IsAtStartOfSentence(string s, int index) { if (index == 0) { return true; } int num = index - 1; while (num >= 0 && char.IsWhiteSpace(s[num])) { num--; } if (num < 0) { return true; } char c = s[num]; if (c != '.' && c != '!' && c != '?' && c != ':' && c != ';' && c != '\r') { return c == '\n'; } return true; } public static Dictionary<string, int> ExtractCyrillicFragments(string s) { Dictionary<string, int> dictionary = new Dictionary<string, int>(StringComparer.Ordinal); if (string.IsNullOrEmpty(s)) { return dictionary; } foreach (Match item in Regex.Matches(s, "[\\u0400-\\u052F]+(?:\\s+[\\u0400-\\u052F]+)+")) { string value = item.Value; int startIndex = item.Index; string[] array = value.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); foreach (string text in array) { int num = s.IndexOf(text, startIndex, StringComparison.Ordinal); if (num >= 0) { startIndex = num + text.Length; if (text.Length >= 4 && char.IsUpper(text[0]) && !CommonServiceWords.Contains(text) && !IsAtStartOfSentence(s, num)) { dictionary.TryGetValue(text, out var value2); dictionary[text] = value2 + 1; } } } } foreach (Match item2 in Regex.Matches(s, "[\\u0400-\\u052F]{6,}")) { string value3 = item2.Value; if (char.IsUpper(value3[0]) && !CommonServiceWords.Contains(value3) && !IsAtStartOfSentence(s, item2.Index) && !dictionary.ContainsKey(value3)) { dictionary.TryGetValue(value3, out var value4); dictionary[value3] = value4 + 1; } } return dictionary; } public static int CountOccurrencesOrdinal(string text, string fragment) { if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(fragment)) { return 0; } int num = 0; int startIndex = 0; while ((startIndex = text.IndexOf(fragment, startIndex, StringComparison.Ordinal)) != -1) { num++; startIndex += fragment.Length; } return num; } public static bool Validate(string src, string dst, out string reason) { reason = null; if (string.IsNullOrWhiteSpace(dst)) { reason = "пусто"; return false; } if (string.Equals(src, dst, StringComparison.Ordinal)) { reason = "модель вернула исходную строку без перевода"; return false; } int num = CountOccurrences(src, "->"); if (num > 0 && CountOccurrences(dst, "->") != num) { reason = "утерян или изменен синтаксис грамматики (->)"; return false; } List<string> list = Placeholders(src); List<string> list2 = Placeholders(dst); if (!list.SequenceEqual<string>(list2, StringComparer.Ordinal)) { reason = "плейсхолдеры не совпадают: [" + string.Join(", ", list) + "] -> [" + string.Join(", ", list2) + "]"; return false; } Dictionary<string, int> dictionary = ExtractCyrillicFragments(src); if (dictionary.Count > 0) { foreach (KeyValuePair<string, int> item in dictionary) { string key = item.Key; int value = item.Value; string fragment = key; if (key.Length >= 5) { fragment = key.Substring(0, key.Length - 2); } if (CountOccurrencesOrdinal(dst, fragment) < value) { reason = "русский фрагмент исходника потерян"; return false; } } } ScriptKind num2 = DetectScript(src); double num3 = ((num2 == ScriptKind.Cjk) ? 9.0 : 3.5); int num4 = ((num2 == ScriptKind.Cjk) ? 60 : 30); if ((double)dst.Length > (double)src.Length * num3 + (double)num4) { reason = "слишком длинно, модель начала объяснять"; return false; } if (dst.Contains("```")) { reason = "markdown в ответе"; return false; } if (dst.StartsWith("Перевод:", StringComparison.Ordinal)) { reason = "префикс-болтовня"; return false; } if (dst.Contains("⟦") || dst.Contains("⟧")) { reason = "в ответе остались нераспознанные маркеры"; return false; } CountScripts(dst, out var cyr, out var lat, out var cjk, out var other); if (cyr == 0 && lat + cjk + other > 0) { reason = "в ответе нет кириллицы, строка не переведена"; return false; } if (cjk > 0) { reason = "в ответе остались иероглифы"; return false; } int num5 = CountOccurrences(dst, "<color"); int num6 = CountOccurrences(dst, "</color>"); if (num5 != num6) { reason = "незакрытый тег color"; return false; } return true; } public static string NormalizeFullwidth(string s) { if (string.IsNullOrEmpty(s)) { return s; } bool flag = false; for (int i = 0; i < s.Length; i++) { switch (s[i]) { case '\u3000': case '!': case '"': case '#': case '$': case '%': case '&': case ''': case '(': case ')': case '*': case '+': case ',': case '-': case '.': case '/': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case ':': case ';': case '<': case '=': case '>': case '?': case '@': case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G': case 'H': case 'I': case 'J': case 'K': case 'L': case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T': case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': case '[': case '\': case ']': case '\uff3e': case '\uff3f': case '\uff40': case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': case 'o': case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': case '{': case '|': case '}': case '~': break; default: continue; } flag = true; break; } if (!flag) { return s; } StringBuilder stringBuilder = new StringBuilder(s.Length); foreach (char c in s) { switch (c) { case '\u3000': stringBuilder.Append(' '); break; case '!': case '"': case '#': case '$': case '%': case '&': case ''': case '(': case ')': case '*': case '+': case ',': case '-': case '.': case '/': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case ':': case ';': case '<': case '=': case '>': case '?': case '@': case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G': case 'H': case 'I': case 'J': case 'K': case 'L': case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T': case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': case '[': case '\': case ']': case '\uff3e': case '\uff3f': case '\uff40': case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': case 'o': case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': case '{': case '|': case '}': case '~': stringBuilder.Append((char)(c - 65248)); break; default: stringBuilder.Append(c); break; } } return stringBuilder.ToString(); } private static int CountOccurrences(string s, string sub) { int num = 0; int startIndex = 0; while ((startIndex = s.IndexOf(sub, startIndex, StringComparison.OrdinalIgnoreCase)) >= 0) { num++; startIndex += sub.Length; } return num; } public static string MaskPlaceholders(string src, out Dictionary<int, string> map) { map = new Dictionary<int, string>(); if (string.IsNullOrEmpty(src)) { return src; } List<PlaceholderMatch> placeholderMatches = GetPlaceholderMatches(src); if (placeholderMatches.Count == 0) { return src; } StringBuilder stringBuilder = new StringBuilder(src.Length); int num = 1; int num2 = 0; foreach (PlaceholderMatch item in placeholderMatches) { string value = item.Value; if (value.StartsWith("{") && value.EndsWith("}") && value.Contains("?")) { stringBuilder.Append(src, num2, item.Index + item.Length - num2); num2 = item.Index + item.Length; continue; } stringBuilder.Append(src, num2, item.Index - num2); stringBuilder.Append("⟦").Append(num).Append("⟧"); map[num] = value; num++; num2 = item.Index + item.Length; } stringBuilder.Append(src, num2, src.Length - num2); return stringBuilder.ToString(); } public static string UnmaskPlaceholders(string translated, Dictionary<int, string> map) { if (string.IsNullOrEmpty(translated) || map == null || map.Count == 0) { return translated; } StringBuilder stringBuilder = new StringBuilder(translated.Length + 64); int num = 0; while (num < translated.Length) { int num2 = translated.IndexOf("⟦", num, StringComparison.Ordinal); if (num2 < 0) { stringBuilder.Append(translated, num, translated.Length - num); break; } stringBuilder.Append(translated, num, num2 - num); int num3 = translated.IndexOf("⟧", num2 + "⟦".Length, StringComparison.Ordinal); if (num3 < 0) { stringBuilder.Append(translated, num2, translated.Length - num2); break; } if (int.TryParse(translated.Substring(num2 + "⟦".Length, num3 - num2 - "⟦".Length), out var result) && map.TryGetValue(result, out var value)) { stringBuilder.Append(value); } else { stringBuilder.Append(translated, num2, num3 + "⟧".Length - num2); } num = num3 + "⟧".Length; } return stringBuilder.ToString(); } public static bool ValidateMarkers(string masked, Dictionary<int, string> map, out string reason) { reason = null; if (map == null || map.Count == 0) { return true; } HashSet<int> hashSet = new HashSet<int>(); int num = 0; while (num < masked.Length) { int num2 = masked.IndexOf("⟦", num, StringComparison.Ordinal); if (num2 < 0) { break; } int num3 = masked.IndexOf("⟧", num2 + "⟦".Length, StringComparison.Ordinal); if (num3 < 0) { break; } if (int.TryParse(masked.Substring(num2 + "⟦".Length, num3 - num2 - "⟦".Length), out var result)) { hashSet.Add(result); } num = num3 + "⟧".Length; } List<int> list = new List<int>(); List<int> list2 = new List<int>(); foreach (int key in map.Keys) { if (!hashSet.Contains(key)) { list.Add(key); } } foreach (int item in hashSet) { if (!map.ContainsKey(item)) { list2.Add(item); } } if (list.Count == 0 && list2.Count == 0) { return true; } StringBuilder stringBuilder = new StringBuilder("маркеры не совпадают:"); if (list.Count > 0) { stringBuilder.Append(" утеряны "); list.Sort(); foreach (int item2 in list) { stringBuilder.Append("⟦").Append(item2).Append("⟧") .Append(' '); } } if (list2.Count > 0) { stringBuilder.Append(" лишние "); list2.Sort(); foreach (int item3 in list2) { stringBuilder.Append("⟦").Append(item3).Append("⟧") .Append(' '); } } reason = stringBuilder.ToString(); return false; } public static string BuildRetryHint(Dictionary<int, string> map) { if (map == null || map.Count == 0) { return ""; } StringBuilder stringBuilder = new StringBuilder("ОБЯЗАТЕЛЬНЫЕ маркеры (перенеси все в перевод без изменений): "); foreach (KeyValuePair<int, string> item in map) { stringBuilder.Append("⟦").Append(item.Key).Append("⟧") .Append(' '); } return stringBuilder.ToString().TrimEnd(); } } public static class Prompt { public const string PromptVersion = "valheim-2"; public const string System = "Ты переводчик модов игры Valheim на русский язык. Ответь только JSON-объектом {\"id\":\"перевод\"}. Переводи только значения, ключи не меняй. Сохраняй все маркеры ⟦1⟧, ⟦2⟧ ровно по одному разу, а также HTML/TMP-теги, переносы строк и регистр. Не добавляй пояснения. Кириллические фрагменты исходника сохраняй дословно. Для коротких элементов интерфейса используй краткий естественный перевод. Имена боссов: Eikthyr — Эйктюр, The Elder — Древний, Bonemass — Масса костей, Moder — Моудер, Yagluth — Яглут."; public static string BuildUserMessage(string context, Dictionary<string, string> items, Dictionary<string, Dictionary<int, string>> requiredMarkers = null, Dictionary<string, string> retryHints = null) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("{\"context\":\"").Append(MiniJson.Escape(context)).Append("\",\"items\":{"); bool flag = true; foreach (KeyValuePair<string, string> item in items) { if (!flag) { stringBuilder.Append(','); } flag = false; stringBuilder.Append('"').Append(MiniJson.Escape(item.Key)).Append("\":\"") .Append(MiniJson.Escape(item.Value)) .Append('"'); } stringBuilder.Append('}'); if (requiredMarkers != null && requiredMarkers.Count > 0) { stringBuilder.Append(",\"required_markers\":{"); flag = true; foreach (KeyValuePair<string, Dictionary<int, string>> requiredMarker in requiredMarkers) { if (!flag) { stringBuilder.Append(','); } flag = false; stringBuilder.Append('"').Append(MiniJson.Escape(requiredMarker.Key)).Append("\":\""); foreach (int key in requiredMarker.Value.Keys) { stringBuilder.Append('⟦').Append(key).Append('⟧') .Append(' '); } stringBuilder.Append('"'); } stringBuilder.Append('}'); } if (retryHints != null && retryHints.Count > 0) { stringBuilder.Append(",\"retry_hints\":{"); flag = true; foreach (KeyValuePair<string, string> retryHint in retryHints) { if (!flag) { stringBuilder.Append(','); } flag = false; stringBuilder.Append('"').Append(MiniJson.Escape(retryHint.Key)).Append("\":\"") .Append(MiniJson.Escape(retryHint.Value)) .Append('"'); } stringBuilder.Append('}'); } stringBuilder.Append('}'); return stringBuilder.ToString(); } } public sealed class GATSettings { public string endpoint = "http://127.0.0.1:8317/v1/chat/completions"; public string model = "gemini-3.8-flash-high"; public string apiKey = ""; public int timeoutSeconds = 120; public int batchSize = 25; public int maxConcurrent = 2; public bool sendReasoningEffortNone = true; public bool requestJsonObject = true; public bool verboseLogging = true; public static GATSettings Load(ConfigFile config) { GATSettings gATSettings = new GATSettings(); gATSettings.endpoint = config.Bind<string>("Connection", "Endpoint", gATSettings.endpoint, "OpenAI-compatible chat completions URL").Value; gATSettings.model = config.Bind<string>("Connection", "Model", gATSettings.model, "Model name").Value; gATSettings.apiKey = config.Bind<string>("Connection", "ApiKey", gATSettings.apiKey, "Optional API key").Value; gATSettings.timeoutSeconds = config.Bind<int>("Connection", "TimeoutSeconds", gATSettings.timeoutSeconds, "Request timeout").Value; gATSettings.batchSize = config.Bind<int>("Worker", "BatchSize", gATSettings.batchSize, "Lines per batch").Value; gATSettings.maxConcurrent = config.Bind<int>("Worker", "MaxConcurrent", gATSettings.maxConcurrent, "Worker count").Value; gATSettings.sendReasoningEffortNone = config.Bind<bool>("Connection", "SendReasoningEffortNone", gATSettings.sendReasoningEffortNone, "Send reasoning_effort=none").Value; gATSettings.requestJsonObject = config.Bind<bool>("Connection", "RequestJsonObject", gATSettings.requestJsonObject, "Request JSON response").Value; gATSettings.verboseLogging = config.Bind<bool>("Diagnostics", "VerboseLogging", gATSettings.verboseLogging, "Log batches and errors").Value; return gATSettings; } } public static class TranslateWorker { private sealed class Job { public string Context; public string Source; public string Key; public int Attempts; public string LastFailure; } private static readonly ConcurrentQueue<Job> Queue = new ConcurrentQueue<Job>(); private static readonly ConcurrentDictionary<string, byte> Pending = new ConcurrentDictionary<string, byte>(StringComparer.Ordinal); private static readonly ConcurrentDictionary<string, long> Rejected = new ConcurrentDictionary<string, long>(StringComparer.Ordinal); private static readonly AutoResetEvent Wake = new AutoResetEvent(initialState: false); private static Thread[] threads; private static volatile bool running; private static GATSettings settings; private static long nextNetworkTryUtcTicks; private static int networkFailureStreak; private const int MaxPending = 5000; private const int MaxRejected = 5000; public static void Start(GATSettings value) { if (!running) { settings = value; running = true; int num = Math.Max(1, Math.Min(4, settings.maxConcurrent)); threads = new Thread[num]; for (int i = 0; i < num; i++) { threads[i] = new Thread(Work); threads[i].IsBackground = true; threads[i].Name = "ValheimAutoTranslator-" + i; threads[i].Start(); } } } public static void Stop() { running = false; Wake.Set(); Thread[] array = threads; if (array == null) { return; } Thread[] array2 = array; foreach (Thread thread in array2) { if (thread != null && thread.IsAlive) { thread.Join(3000); } } } public static void Request(string context, string source) { if (!running || !PlaceholderGuard.NeedsTranslation(source) || TranslationCache.TryGet(context, source, out var _)) { return; } string key = context + "\t" + source; if (Rejected.TryGetValue(key, out var value)) { if (DateTime.UtcNow.Ticks - value < TimeSpan.FromMinutes(10.0).Ticks) { return; } Rejected.TryRemove(key, out var _); } if (Pending.Count < 5000 && Pending.TryAdd(key, 0)) { Queue.Enqueue(new Job { Context = context, Source = source, Key = key }); Wake.Set(); } } private static void Work() { while (running) { long num = Interlocked.Read(in nextNetworkTryUtcTicks) - DateTime.UtcNow.Ticks; if (num > 0) { Wake.WaitOne((int)Math.Min(1000L, Math.Max(1L, num / 10000))); continue; } if (!Queue.TryDequeue(out var result)) { Wake.WaitOne(500); continue; } List<Job> list = new List<Job> { result }; int num2 = Math.Max(1, Math.Min(80, settings.batchSize)); Job result2; while (list.Count < num2 && Queue.TryPeek(out result2) && !(result2.Context != result.Context) && Queue.TryDequeue(out result2)) { list.Add(result2); } try { Process(list); } catch (Exception ex) { GATLog.Warn("Worker error: " + ex); foreach (Job item in list) { Retry(item); } } } } private static void Process(List<Job> jobs) { Dictionary<string, string> dictionary = new Dictionary<string, string>(); for (int i = 0; i < jobs.Count; i++) { dictionary[i.ToString()] = jobs[i].Source; } bool isRetry = false; Dictionary<string, string> dictionary2 = new Dictionary<string, string>(); for (int j = 0; j < jobs.Count; j++) { if (jobs[j].Attempts != 0) { isRetry = true; PlaceholderGuard.MaskPlaceholders(jobs[j].Source, out var map); string text = PlaceholderGuard.BuildRetryHint(map); if (jobs[j].LastFailure == "модель вернула исходную строку без перевода") { text += " Предыдущий ответ повторил исходную строку. Переведи английские слова на русский; не возвращай оригинал."; } dictionary2[j.ToString()] = text; } } Dictionary<string, string> dictionary3 = LlmClient.TranslateBatch(settings, jobs[0].Context, dictionary, isRetry, dictionary2); if (dictionary3 == null) { int num = Interlocked.Increment(ref networkFailureStreak); int num2 = Math.Min(300, 15 * (1 << Math.Min(4, num - 1))); Interlocked.Exchange(ref nextNetworkTryUtcTicks, DateTime.UtcNow.AddSeconds(num2).Ticks); foreach (Job job2 in jobs) { Queue.Enqueue(job2); } GATLog.Warn("Translation endpoint unavailable. Retrying in " + num2 + " seconds."); return; } Interlocked.Exchange(ref networkFailureStreak, 0); Interlocked.Exchange(ref nextNetworkTryUtcTicks, 0L); int num3 = 0; for (int k = 0; k < jobs.Count; k++) { Job job = jobs[k]; string reason = null; string value; bool flag = dictionary3.TryGetValue(k.ToString(), out value); if (flag && PlaceholderGuard.Validate(job.Source, value, out reason)) { if (!TranslationCache.Put(job.Context, job.Source, value)) { Retry(job, "cache did not accept validated translation"); continue; } Pending.TryRemove(job.Key, out var _); ValheimPlugin.ScheduleRefresh(); num3++; } else { Retry(job, flag ? reason : "missing response id"); } } if (settings.verboseLogging) { GATLog.Msg("Batch " + jobs[0].Context + ": " + num3 + "/" + jobs.Count); } } private static void Retry(Job job, string reason = "worker error") { job.LastFailure = reason; job.Attempts++; if (!running || job.Attempts >= 3) { Pending.TryRemove(job.Key, out var _); if (Rejected.Count >= 5000) { Rejected.Clear(); } Rejected[job.Key] = DateTime.UtcNow.Ticks; if (settings != null && settings.verboseLogging) { GATLog.Warn("Rejected " + job.Context + " (length " + job.Source.Length + "): " + reason); } } else { if (settings != null && settings.verboseLogging) { GATLog.Warn("Retry " + job.Context + " (length " + job.Source.Length + ", " + job.Attempts + "/3): " + reason); } Queue.Enqueue(job); Wake.Set(); } } } public static class TranslationCache { private static readonly ConcurrentDictionary<string, string> Items = new ConcurrentDictionary<string, string>(StringComparer.Ordinal); private static readonly object FileLock = new object(); private static string path; public static string PathOnDisk => path; public static int Count => Items.Count; private static string Key(string context, string source) { return context + "\t" + source; } public static bool TryGet(string context, string source, out string result) { return Items.TryGetValue(Key(context, source), out result); } public static void Load(string model) { Items.Clear(); string path = Path.Combine(Paths.ConfigPath, "ValheimAutoTranslator"); Directory.CreateDirectory(path); string text = Regex.Replace(model ?? "default", "[^a-zA-Z0-9._-]", "_"); if (text.Length > 80) { text = text.Substring(0, 80); } TranslationCache.path = Path.Combine(path, "cache-valheim-2-" + text + ".tsv"); if (!File.Exists(TranslationCache.path)) { return; } foreach (string item in File.ReadLines(TranslationCache.path, Encoding.UTF8)) { string[] array = item.Split(new char[1] { '\t' }, 3); if (array.Length != 3) { continue; } try { string context = Decode(array[0]); string text2 = Decode(array[1]); string text3 = Decode(array[2]); if (text2.Length > 0 && text3.Length > 0) { Items[Key(context, text2)] = text3; } } catch (FormatException) { } } } public static bool Put(string context, string source, string result) { if (string.IsNullOrEmpty(source) || string.IsNullOrEmpty(result) || source == result) { return false; } string key = Key(context, source); string contents = Encode(context) + "\t" + Encode(source) + "\t" + Encode(result) + Environment.NewLine; lock (FileLock) { if (Items.ContainsKey(key)) { return true; } File.AppendAllText(path, contents, Encoding.UTF8); Items[key] = result; return true; } } private static string Encode(string value) { return Convert.ToBase64String(Encoding.UTF8.GetBytes(value ?? "")); } private static string Decode(string value) { return Encoding.UTF8.GetString(Convert.FromBase64String(value)); } } internal static class UiHarvest { private sealed class Entry { public WeakReference Control; public string Source; public string Rendered; public bool IsTmp; } private const int MaxTracked = 5000; private static readonly Dictionary<int, Entry> Entries = new Dictionary<int, Entry>(); [ThreadStatic] private static bool applying; internal static void OnSet(Object control, ref string text, bool isTmp) { if (applying || !ValheimPlugin.IsRussian(Localization.instance) || control == (Object)null) { return; } int instanceID = control.GetInstanceID(); if (string.IsNullOrEmpty(text) || !PlaceholderGuard.NeedsTranslation(text)) { Entries.Remove(instanceID); return; } string source = text; if (TranslationCache.TryGet("ui", source, out var result)) { text = result; } else { TranslateWorker.Request("ui", source); } if (Entries.Count >= 5000 && !Entries.ContainsKey(instanceID)) { PruneDead(); } if (Entries.Count < 5000 || Entries.ContainsKey(instanceID)) { Entries[instanceID] = new Entry { Control = new WeakReference(control), Source = source, Rendered = text, IsTmp = isTmp }; } } private static void PruneDead() { List<int> list = new List<int>(); foreach (KeyValuePair<int, Entry> entry in Entries) { object? target = entry.Value.Control.Target; if ((Object)((target is Object) ? target : null) == (Object)null) { list.Add(entry.Key); } } foreach (int item in list) { Entries.Remove(item); } } internal static void Refresh() { //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00c7: Unknown result type (might be due to invalid IL or missing references) List<int> list = new List<int>(); applying = true; try { foreach (KeyValuePair<int, Entry> entry in Entries) { Entry value = entry.Value; object? target = value.Control.Target; Object val = (Object)((target is Object) ? target : null); if (val == (Object)null) { list.Add(entry.Key); continue; } string text = ((!value.IsTmp) ? ((Text)val).text : ((TMP_Text)val).text); string result; if (text != value.Rendered) { list.Add(entry.Key); } else if (TranslationCache.TryGet("ui", value.Source, out result) && !(result == text)) { if (value.IsTmp) { ((TMP_Text)val).text = result; } else { ((Text)val).text = result; } value.Rendered = result; } } foreach (int item in list) { Entries.Remove(item); } } finally { applying = false; } } } [HarmonyPatch(typeof(TMP_Text), "set_text", new Type[] { typeof(string) })] internal static class PatchTmpText { private static void Prefix(TMP_Text __instance, ref string value) { UiHarvest.OnSet((Object)(object)__instance, ref value, isTmp: true); } } [HarmonyPatch(typeof(Text), "set_text", new Type[] { typeof(string) })] internal static class PatchUnityText { private static void Prefix(Text __instance, ref string value) { UiHarvest.OnSet((Object)(object)__instance, ref value, isTmp: false); } } [BepInPlugin("ntxfloy.valheimautotranslator", "Valheim Auto Translator", "0.1.4")] public sealed class ValheimPlugin : BaseUnityPlugin { private Harmony harmony; private static int refreshNeeded; private static volatile bool languageKnown; private static volatile bool russianLanguage; private static float nextRefresh; internal static ManualLogSource Log; private void Awake() { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; try { GATSettings gATSettings = GATSettings.Load(((BaseUnityPlugin)this).Config); TranslationCache.Load(gATSettings.model); TranslateWorker.Start(gATSettings); harmony = new Harmony("ntxfloy.valheimautotranslator"); harmony.PatchAll(typeof(ValheimPlugin).Assembly); ((BaseUnityPlugin)this).Logger.LogInfo((object)("Ready. Cached translations: " + TranslationCache.Count + "; " + TranslationCache.PathOnDisk)); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("Initialization failed: " + ex)); } } private void OnDestroy() { try { if (harmony != null) { harmony.UnpatchSelf(); } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)ex); } try { TranslateWorker.Stop(); } catch (Exception ex2) { ((BaseUnityPlugin)this).Logger.LogWarning((object)ex2); } } internal static bool IsRussian(Localization localization) { if (languageKnown) { return russianLanguage; } if (localization == null) { return false; } SetLanguageState(localization.GetSelectedLanguage()); return russianLanguage; } internal static void SetLanguageState(string language) { russianLanguage = !string.IsNullOrEmpty(language) && (language.IndexOf("Russian", StringComparison.OrdinalIgnoreCase) >= 0 || language.IndexOf("Рус", StringComparison.OrdinalIgnoreCase) >= 0); languageKnown = true; } internal static string Get(string context, string source) { if (string.IsNullOrEmpty(source)) { return source; } if (TranslationCache.TryGet(context, source, out var result)) { return result; } TranslateWorker.Request(context, source); return source; } internal static void ScheduleRefresh() { Interlocked.Exchange(ref refreshNeeded, 1); } private void Update() { if (Time.realtimeSinceStartup < nextRefresh || Interlocked.CompareExchange(ref refreshNeeded, 0, 1) != 1) { return; } nextRefresh = Time.realtimeSinceStartup + 1f; try { if (IsRussian(Localization.instance)) { UiHarvest.Refresh(); } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Refresh failed: " + ex)); } } } internal static class GATLog { public static void Msg(string message) { if (ValheimPlugin.Log != null) { ValheimPlugin.Log.LogInfo((object)message); } } public static void Warn(string message) { if (ValheimPlugin.Log != null) { ValheimPlugin.Log.LogWarning((object)message); } } public static void Err(string message) { if (ValheimPlugin.Log != null) { ValheimPlugin.Log.LogError((object)message); } } } [HarmonyPatch(typeof(Localization), "Localize", new Type[] { typeof(string) })] internal static class PatchLocalizeString { private static void Prefix(Localization __instance, ref string text) { if (ValheimPlugin.IsRussian(__instance)) { text = ValheimPlugin.Get("template", text); } } } [HarmonyPatch(typeof(Localization), "SetLanguage", new Type[] { typeof(string) })] internal static class PatchSetLanguage { private static void Postfix(string language) { ValheimPlugin.SetLanguageState(language); ValheimPlugin.ScheduleRefresh(); } } [HarmonyPatch(typeof(Localization), "Translate")] internal static class PatchTranslate { private static void Postfix(Localization __instance, ref string __result) { if (ValheimPlugin.IsRussian(__instance) && !string.IsNullOrEmpty(__result) && (__result[0] != '[' || __result[__result.Length - 1] != ']')) { __result = ValheimPlugin.Get("keyed", __result); } } } [HarmonyPatch(typeof(MessageHud), "ShowMessage")] internal static class PatchShowMessage { private static void Prefix(ref string text) { if (ValheimPlugin.IsRussian(Localization.instance)) { text = ValheimPlugin.Get("message", text); } } } }