Decompiled source of ValheimAutoTranslator v0.1.4

BepInEx/plugins/ValheimAutoTranslator.dll

Decompiled a day ago
using 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);
			}
		}
	}
}