Decompiled source of BetterPortuguese v0.1.0

plugins/BetterPortuguese/BetterPortuguese.dll

Decompiled 3 hours ago
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using BetterPortuguese.Core;
using BetterPortuguese.Runtime;
using HarmonyLib;
using UnityEngine;
using UnityEngine.SceneManagement;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = "")]
[assembly: AssemblyCompany("BetterPortuguese")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.1.0.0")]
[assembly: AssemblyInformationalVersion("0.1.0")]
[assembly: AssemblyProduct("BetterPortuguese")]
[assembly: AssemblyTitle("BetterPortuguese")]
[assembly: AssemblyVersion("0.1.0.0")]
namespace BetterPortuguese
{
	[BepInPlugin("com.gumatos.betterportuguese", "BetterPortuguese", "0.1.0")]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string Guid = "com.gumatos.betterportuguese";

		private static readonly StateQueue States = new StateQueue();

		private static Overlay<LocalizationEntry> overlay;

		private static ISet<string> changedKeys = new HashSet<string>(StringComparer.Ordinal);

		private static OnceLog limited;

		private static ManualLogSource log;

		private static Action poll;

		private static bool firstLookup;

		private static bool refreshPending;

		private static float nextPoll;

		private ConfigEntry<bool> active;

		private ConfigEntry<bool> integrate;

		private ConfigEntry<bool> diagnostic;

		private ConfigEntry<Conjunto> variant;

		private string lastFile;

		private Harmony harmony;

		private void Awake()
		{
			//IL_029a: Unknown result type (might be due to invalid IL or missing references)
			//IL_02a4: Expected O, but got Unknown
			log = ((BaseUnityPlugin)this).Logger;
			limited = new OnceLog(delegate(string s)
			{
				log.LogWarning((object)s);
			});
			try
			{
				bool num = !File.Exists(((BaseUnityPlugin)this).Config.ConfigFilePath);
				((BaseUnityPlugin)this).Config.SaveOnConfigSet = false;
				active = ((BaseUnityPlugin)this).Config.Bind<bool>("Geral", "Ativo", true, "Ativa somente as camadas do BetterPortuguese.");
				variant = ((BaseUnityPlugin)this).Config.Bind<Conjunto>("Geral", "Conjunto", Conjunto.Corrigido, "Corrigido (padrao) ou Original. Original deixa a localizacao vigente do jogo intacta.");
				integrate = ((BaseUnityPlugin)this).Config.Bind<bool>("Geral", "IntegracaoMods", true, "Reserva a camada de textos aprovados de mods. 0.1.0: registro vazio, nenhum adaptador ativo.");
				diagnostic = ((BaseUnityPlugin)this).Config.Bind<bool>("Geral", "Diagnostico", false, "Log de estado e patches, sem log por frame/chave.");
				if (num)
				{
					((BaseUnityPlugin)this).Config.Save();
				}
				((BaseUnityPlugin)this).Config.SaveOnConfigSet = true;
				((BaseUnityPlugin)this).Config.SettingChanged += delegate
				{
					QueueEntries();
				};
				poll = PollFile;
				QueueEntries();
				PollFile();
			}
			catch (Exception ex)
			{
				poll = null;
				States.Enqueue(new Settings(active: false, Conjunto.Corrigido, integrateMods: true, diagnostic: false, valid: false));
				log.LogError((object)("BetterPortuguese: config indisponivel, overrides desativados: " + ex.Message));
			}
			try
			{
				Catalog catalog = Resources.Load(typeof(Plugin).Assembly);
				changedKeys = new HashSet<string>(catalog.Changed.Keys, StringComparer.Ordinal);
				overlay = new Overlay<LocalizationEntry>(catalog, Bindings.Equal, Bindings.Clone, limited);
				log.LogInfo((object)("BetterPortuguese 0.1.0: snapshots completos; " + catalog.Original.Records.Count + " registros; " + catalog.Changed.Count + " chaves corrigidas aprovadas."));
			}
			catch (Exception ex2)
			{
				overlay = null;
				log.LogError((object)("BetterPortuguese: camada base desativada: " + ex2.Message));
			}
			try
			{
				log.LogInfo((object)("BetterPortuguese: registry de mods = " + Resources.LoadMods(typeof(Plugin).Assembly) + "; nenhum adaptador ativo / manuais sem traducao aprovada."));
			}
			catch (Exception ex3)
			{
				log.LogWarning((object)("BetterPortuguese: camada de mods desativada, base independente: " + ex3.Message));
			}
			harmony = new Harmony("com.gumatos.betterportuguese");
			Install("consulta da lista", () => Bindings.ListTarget(), "ListPostfix");
			Install("fila/config/refresh", () => Bindings.Exact(typeof(GUIManager), "Update", typeof(void), isStatic: false, Type.EmptyTypes), "MainThreadPostfix");
			log.LogInfo((object)"BetterPortuguese 0.1.0 carregado.");
		}

		private void Install(string label, Func<MethodInfo> target, string patch)
		{
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			//IL_002e: Expected O, but got Unknown
			try
			{
				MethodInfo methodInfo = target();
				harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(Plugin).GetMethod(patch, BindingFlags.Static | BindingFlags.NonPublic)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
				Patches patchInfo = Harmony.GetPatchInfo((MethodBase)methodInfo);
				int num = 0;
				foreach (Patch postfix in patchInfo.Postfixes)
				{
					if (postfix.owner == "com.gumatos.betterportuguese")
					{
						num++;
					}
				}
				if (num != 1)
				{
					throw new InvalidOperationException("Expected one owned postfix, got " + num);
				}
				log.LogInfo((object)("BetterPortuguese: patch " + label + " registrado (owner=com.gumatos.betterportuguese, postfixes proprios=" + num + ")."));
				foreach (Patch postfix2 in patchInfo.Postfixes)
				{
					if (postfix2.owner != "com.gumatos.betterportuguese")
					{
						limited.Write("Outro postfix no alvo " + methodInfo.Name + ": " + postfix2.owner + "; nao ha garantia contra mudancas posteriores.");
					}
				}
			}
			catch (Exception ex)
			{
				log.LogError((object)("BetterPortuguese: alvo " + label + " indisponivel: " + ex.Message));
				if (patch == "MainThreadPostfix")
				{
					States.Enqueue(new Settings(active: false, Conjunto.Corrigido, integrateMods: true, diagnostic: false, valid: false));
				}
			}
		}

		private void QueueEntries()
		{
			States.Enqueue(new Settings(active.Value, variant.Value, integrate.Value, diagnostic.Value));
		}

		private void PollFile()
		{
			try
			{
				string text = File.ReadAllText(((BaseUnityPlugin)this).Config.ConfigFilePath);
				if (!(text == lastFile))
				{
					lastFile = text;
					Settings settings = ConfigReader.Parse(text, new Settings());
					bool saveOnConfigSet = ((BaseUnityPlugin)this).Config.SaveOnConfigSet;
					try
					{
						((BaseUnityPlugin)this).Config.SaveOnConfigSet = false;
						((BaseUnityPlugin)this).Config.Reload();
					}
					finally
					{
						((BaseUnityPlugin)this).Config.SaveOnConfigSet = saveOnConfigSet;
					}
					States.Enqueue(settings);
				}
			}
			catch (Exception ex)
			{
				States.Enqueue(new Settings(active: false, Conjunto.Corrigido, integrateMods: true, diagnostic: false, valid: false));
				limited.Write("Config invalida/ilegivel; overrides desativados: " + ex.GetType().Name + ": " + ex.Message);
				lastFile = null;
			}
		}

		private static void ListPostfix(string original, LanguageChoice languageChoice, bool fallbackToTranslatorKey, ref List<LocalizationEntry> __result)
		{
			//IL_0033: Unknown result type (might be due to invalid IL or missing references)
			//IL_0036: Invalid comparison between Unknown and I4
			//IL_0038: Unknown result type (might be due to invalid IL or missing references)
			//IL_003f: Invalid comparison between Unknown and I4
			List<LocalizationEntry> list = __result;
			try
			{
				if (!firstLookup)
				{
					firstLookup = true;
					ManualLogSource obj = log;
					if (obj != null)
					{
						obj.LogInfo((object)"BetterPortuguese: gancho da lista vivo; selecao de genero/fallback permanece no jogo.");
					}
				}
				__result = overlay?.Resolve(original, (int)languageChoice == 10, (int)OptionsManager.CurrentLanguage == 10, States.Current, list) ?? list;
			}
			catch (Exception ex)
			{
				__result = list;
				limited?.Write("Postfix preservou retorno: " + ex.GetType().Name);
			}
		}

		private static void MainThreadPostfix()
		{
			//IL_00e6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00eb: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				if (Time.realtimeSinceStartup >= nextPoll)
				{
					nextPoll = Time.realtimeSinceStartup + 1f;
					poll?.Invoke();
				}
				Settings current = States.Current;
				if (States.ApplyPending())
				{
					refreshPending |= RefreshPlan.Needed(current, States.Current);
					Settings current2 = States.Current;
					ManualLogSource obj = log;
					if (obj != null)
					{
						obj.LogInfo((object)("BetterPortuguese: estado aplicado na thread principal: " + ((current2.Valid && current2.Active) ? current2.Variant.ToString() : "desativado") + ", IntegracaoMods=" + current2.IntegrateMods + " (0 adaptadores)."));
					}
				}
				if (!refreshPending)
				{
					return;
				}
				Scene activeScene = SceneManager.GetActiveScene();
				if (!((Scene)(ref activeScene)).IsValid() || (Object)(object)OptionsManager.instance == (Object)null)
				{
					return;
				}
				refreshPending = false;
				int num = UiRefresh.Apply(changedKeys, limited);
				if (States.Current.Diagnostic)
				{
					ManualLogSource obj2 = log;
					if (obj2 != null)
					{
						obj2.LogInfo((object)("BetterPortuguese: refresh dirigido de " + num + " texto(s) aprovado(s); sem LocalizeUI global; tooltips no proximo hover."));
					}
				}
			}
			catch (Exception ex)
			{
				limited?.Write("Refresh/config falhou; retorno do jogo preservado: " + ex.GetType().Name);
			}
		}
	}
}
namespace BetterPortuguese.Runtime
{
	internal static class Bindings
	{
		public static MethodInfo Exact(Type type, string name, Type result, bool isStatic, params Type[] parameters)
		{
			BindingFlags bindingAttr = (BindingFlags)(0x30 | (isStatic ? 8 : 4) | 2);
			MethodInfo method = type.GetMethod(name, bindingAttr, null, parameters, null);
			if (method == null || method.IsStatic != isStatic || method.ReturnType != result || method.ContainsGenericParameters)
			{
				throw new MissingMethodException(type.FullName, name);
			}
			return method;
		}

		public static MethodInfo ListTarget()
		{
			MethodInfo methodInfo = Exact(typeof(OptionsManager), "GetLocalizationEntryList", typeof(List<LocalizationEntry>), true, typeof(string), typeof(LanguageChoice), typeof(bool));
			ParameterInfo[] parameters = methodInfo.GetParameters();
			string[] array = new string[3] { "original", "languageChoice", "fallbackToTranslatorKey" };
			for (int i = 0; i < array.Length; i++)
			{
				if (parameters[i].Name != array[i])
				{
					throw new MissingMethodException("Localization parameter names changed");
				}
			}
			return methodInfo;
		}

		public static bool Equal(LocalizationEntry game, Entry baseline)
		{
			//IL_007c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0087: Invalid comparison between Unknown and I4
			if (game != null && game.Value == baseline.Value && game.Key == baseline.Key && game.TranslatorKey == baseline.TranslatorKey && game.Feminine == baseline.Feminine && game.Masculine == baseline.Masculine && game.Neutral == baseline.Neutral && (int)game.LanguageGender == baseline.LanguageGender && game.CanBeGendered == baseline.CanBeGendered)
			{
				return game.NeedsGenderInfo == baseline.NeedsGenderInfo;
			}
			return false;
		}

		public static LocalizationEntry Clone(Entry entry)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_0011: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0029: Unknown result type (might be due to invalid IL or missing references)
			//IL_0035: Unknown result type (might be due to invalid IL or missing references)
			//IL_0041: Unknown result type (might be due to invalid IL or missing references)
			//IL_004d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0054: Unknown result type (might be due to invalid IL or missing references)
			//IL_0059: Unknown result type (might be due to invalid IL or missing references)
			//IL_0065: Unknown result type (might be due to invalid IL or missing references)
			//IL_0072: Expected O, but got Unknown
			return new LocalizationEntry
			{
				Value = entry.Value,
				Key = entry.Key,
				TranslatorKey = entry.TranslatorKey,
				Feminine = entry.Feminine,
				Masculine = entry.Masculine,
				Neutral = entry.Neutral,
				LanguageGender = (LanguageGender)entry.LanguageGender,
				CanBeGendered = entry.CanBeGendered,
				NeedsGenderInfo = entry.NeedsGenderInfo
			};
		}
	}
	internal static class ConfigReader
	{
		public static Settings Parse(string text, Settings defaults)
		{
			Dictionary<string, string> values = new Dictionary<string, string>(StringComparer.Ordinal);
			string text2 = "";
			string[] array = text.Split('\n');
			for (int i = 0; i < array.Length; i++)
			{
				string text3 = array[i].Trim();
				if (text3.Length == 0 || text3.StartsWith("#") || text3.StartsWith(";"))
				{
					continue;
				}
				if (text3.StartsWith("[") && text3.EndsWith("]"))
				{
					text2 = text3.Substring(1, text3.Length - 2);
				}
				else if (!(text2 != "Geral"))
				{
					int num = text3.IndexOf('=');
					if (num < 0)
					{
						throw new FormatException("Malformed BetterPortuguese config line");
					}
					string text4 = text3.Substring(0, num).Trim();
					if (text4 != "Ativo" && text4 != "Conjunto" && text4 != "IntegracaoMods" && text4 != "Diagnostico")
					{
						throw new FormatException("Unknown BetterPortuguese setting");
					}
					if (values.ContainsKey(text4))
					{
						throw new FormatException("Duplicate BetterPortuguese setting");
					}
					values.Add(text4, text3.Substring(num + 1).Trim());
				}
			}
			Conjunto variant = defaults.Variant;
			if (values.TryGetValue("Conjunto", out var value))
			{
				if (value != "Original" && value != "Corrigido")
				{
					throw new FormatException("Conjunto must be Original or Corrigido");
				}
				variant = ((!(value == "Original")) ? Conjunto.Corrigido : Conjunto.Original);
			}
			return new Settings(Boolean("Ativo", defaults.Active), variant, Boolean("IntegracaoMods", defaults.IntegrateMods), Boolean("Diagnostico", defaults.Diagnostic));
			bool Boolean(string key, bool fallback)
			{
				if (!values.TryGetValue(key, out var value2))
				{
					return fallback;
				}
				if (!bool.TryParse(value2, out var result))
				{
					throw new FormatException("Invalid " + key);
				}
				return result;
			}
		}
	}
	internal static class UiRefresh
	{
		private static FieldInfo originals;

		public static int Apply(ISet<string> changed, OnceLog log)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0007: Invalid comparison between Unknown and I4
			if ((int)OptionsManager.CurrentLanguage != 10 || changed.Count == 0)
			{
				return 0;
			}
			if (originals == null)
			{
				originals = typeof(OptionsManager).GetField("OriginalTextValues", BindingFlags.DeclaredOnly | BindingFlags.Static | BindingFlags.NonPublic);
				if (originals == null || originals.FieldType != typeof(Dictionary<TextOrTMPText, string>))
				{
					originals = null;
					throw new MissingFieldException("OptionsManager.OriginalTextValues dictionary unavailable; reopen UI to refresh.");
				}
			}
			Dictionary<TextOrTMPText, string> dictionary = (Dictionary<TextOrTMPText, string>)originals.GetValue(null);
			if (dictionary == null)
			{
				return 0;
			}
			int num = 0;
			KeyValuePair<TextOrTMPText, string>[] array = RefreshPlan.Select(dictionary, changed);
			for (int i = 0; i < array.Length; i++)
			{
				KeyValuePair<TextOrTMPText, string> keyValuePair = array[i];
				try
				{
					if (keyValuePair.Key != null && !((Object)(object)keyValuePair.Key.GameObject == (Object)null))
					{
						OptionsManager.LocalizeUIText(keyValuePair.Key);
						num++;
					}
				}
				catch (Exception ex)
				{
					log?.Write("Refresh de texto aprovado preservou demais alvos: " + ex.GetType().Name);
				}
			}
			return num;
		}
	}
}
namespace BetterPortuguese.Core
{
	internal sealed class Entry
	{
		internal static readonly string[] Fields = new string[9] { "Value", "Key", "TranslatorKey", "Feminine", "Masculine", "Neutral", "LanguageGender", "CanBeGendered", "NeedsGenderInfo" };

		private readonly IReadOnlyDictionary<string, object> values;

		public string Value => (string)values["Value"];

		public string Key => (string)values["Key"];

		public string TranslatorKey => (string)values["TranslatorKey"];

		public string Feminine => (string)values["Feminine"];

		public string Masculine => (string)values["Masculine"];

		public string Neutral => (string)values["Neutral"];

		public int LanguageGender => JsonShape.Int(values["LanguageGender"]);

		public bool CanBeGendered => (bool)values["CanBeGendered"];

		public bool NeedsGenderInfo => (bool)values["NeedsGenderInfo"];

		public object this[string field] => values[field];

		public Entry(Dictionary<string, object> obj)
		{
			JsonShape.Fields(obj, Fields);
			foreach (string item in Fields.Take(6))
			{
				JsonShape.Text(obj[item], nullable: true);
			}
			int num = JsonShape.Int(obj["LanguageGender"]);
			if (num < 0 || num > 6)
			{
				throw new FormatException("Unknown LanguageGender");
			}
			JsonShape.Bool(obj["CanBeGendered"]);
			JsonShape.Bool(obj["NeedsGenderInfo"]);
			values = new ReadOnlyDictionary<string, object>(new Dictionary<string, object>(obj, StringComparer.Ordinal));
		}

		public bool Same(Entry other)
		{
			if (other != null)
			{
				return Fields.All((string f) => object.Equals(this[f], other[f]));
			}
			return false;
		}

		public static bool SameLists(IReadOnlyList<Entry> a, IReadOnlyList<Entry> b)
		{
			if (a == null || b == null)
			{
				if (a == null)
				{
					return b == null;
				}
				return false;
			}
			if (a.Count != b.Count)
			{
				return false;
			}
			for (int i = 0; i < a.Count; i++)
			{
				if (!a[i].Same(b[i]))
				{
					return false;
				}
			}
			return true;
		}
	}
	internal sealed class Record
	{
		public string Id { get; }

		public string Key { get; }

		public bool PresentInSource { get; }

		public IReadOnlyList<Entry> Entries { get; }

		public Record(Dictionary<string, object> obj)
		{
			JsonShape.Fields(obj, "id", "key", "presentInSource", "entries");
			Id = JsonShape.Text(obj["id"]);
			Key = JsonShape.Text(obj["key"]);
			if (!Regex.IsMatch(Id, "^LANG-[0-9]{5}$"))
			{
				throw new FormatException("Invalid editorial ID");
			}
			PresentInSource = JsonShape.Bool(obj["presentInSource"]);
			Entries = ((obj["entries"] == null) ? null : new ReadOnlyCollection<Entry>((from e in JsonShape.Array(obj["entries"])
				select new Entry(JsonShape.Object(e))).ToList()));
		}
	}
	internal sealed class Snapshot
	{
		private readonly object source;

		private readonly List<object> manual;

		public string Variant { get; }

		public IReadOnlyDictionary<string, Record> Records { get; }

		public int ManualCount => manual.Count;

		private Snapshot(Dictionary<string, object> obj, string expectedVariant)
		{
			JsonShape.Fields(obj, "schemaVersion", "locale", "variant", "source", "records", "manualRecords");
			if (JsonShape.Int(obj["schemaVersion"]) != 1 || JsonShape.Text(obj["locale"]) != "pt-BR" || JsonShape.Text(obj["variant"]) != expectedVariant)
			{
				throw new FormatException("Unsupported snapshot");
			}
			Variant = expectedVariant;
			Dictionary<string, object> dictionary = JsonShape.Object(obj["source"]);
			JsonShape.Fields(dictionary, "taskId", "artifactSha256", "resourcePath", "resourcePathId", "resourceAssetSha256", "assemblySha256", "canonicalMemberSha256");
			string[] array = new string[4] { "artifactSha256", "resourceAssetSha256", "assemblySha256", "canonicalMemberSha256" };
			foreach (string key in array)
			{
				if (!Regex.IsMatch(JsonShape.Text(dictionary[key]), "^[a-f0-9]{64}$"))
				{
					throw new FormatException("Invalid source hash");
				}
			}
			JsonShape.Text(dictionary["taskId"]);
			JsonShape.Text(dictionary["resourcePath"]);
			JsonShape.Int(dictionary["resourcePathId"]);
			source = dictionary;
			Dictionary<string, Record> dictionary2 = new Dictionary<string, Record>(StringComparer.Ordinal);
			HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal);
			string text = null;
			foreach (object item in JsonShape.Array(obj["records"]))
			{
				Record record = new Record(JsonShape.Object(item));
				if (!hashSet.Add(record.Id) || dictionary2.ContainsKey(record.Key))
				{
					throw new FormatException("Duplicate ID/key");
				}
				if (text != null && string.CompareOrdinal(text, record.Id) >= 0)
				{
					throw new FormatException("Records not ordered by ID");
				}
				text = record.Id;
				if (expectedVariant == "Original" && record.PresentInSource != (record.Entries != null))
				{
					throw new FormatException("Presence/entries mismatch");
				}
				dictionary2.Add(record.Key, record);
			}
			Records = new ReadOnlyDictionary<string, Record>(dictionary2);
			manual = JsonShape.Array(obj["manualRecords"]);
			HashSet<string> hashSet2 = new HashSet<string>(StringComparer.Ordinal);
			foreach (object item2 in manual)
			{
				Dictionary<string, object> dictionary3 = JsonShape.Object(item2);
				JsonShape.Fields(dictionary3, "id", "sourceFile", "sourcePathId", "englishText", "originalEntries", "ptBRText");
				if (!hashSet2.Add(JsonShape.Text(dictionary3["id"])))
				{
					throw new FormatException("Duplicate manual ID");
				}
				JsonShape.Text(dictionary3["sourceFile"]);
				JsonShape.Int(dictionary3["sourcePathId"]);
				JsonShape.Text(dictionary3["englishText"]);
				JsonShape.Text(dictionary3["ptBRText"], nullable: true);
				foreach (object item3 in JsonShape.Array(dictionary3["originalEntries"]))
				{
					Dictionary<string, object> dictionary4 = JsonShape.Object(item3);
					JsonShape.Fields(dictionary4, "Language", "Text");
					JsonShape.Int(dictionary4["Language"]);
					JsonShape.Text(dictionary4["Text"], nullable: true);
				}
			}
		}

		public static Snapshot Parse(string json, string variant)
		{
			return new Snapshot(JsonShape.Object(StrictJson.Parse(json)), variant);
		}

		public void ValidateIdentity(Snapshot corrected)
		{
			if (Records.Count != corrected.Records.Count || !JsonShape.Equal(source, corrected.source) || !JsonShape.Equal(manual, corrected.manual))
			{
				throw new FormatException("Snapshot identity/manual drift (manual adapter unsupported)");
			}
			foreach (KeyValuePair<string, Record> record in Records)
			{
				if (!corrected.Records.TryGetValue(record.Key, out var value) || record.Value.Id != value.Id || record.Value.PresentInSource != value.PresentInSource)
				{
					throw new FormatException("Immutable ID/key/presence changed");
				}
			}
		}
	}
	internal sealed class Catalog
	{
		public Snapshot Original { get; }

		public Snapshot Corrected { get; }

		public IReadOnlyDictionary<string, Record> Changed { get; }

		private Catalog(Snapshot original, Snapshot corrected, List<object> diff)
		{
			Original = original;
			Corrected = corrected;
			original.ValidateIdentity(corrected);
			Dictionary<string, Dictionary<string, object>> dictionary = new Dictionary<string, Dictionary<string, object>>(StringComparer.Ordinal);
			Dictionary<string, EntryAddition> dictionary2 = new Dictionary<string, EntryAddition>(StringComparer.Ordinal);
			foreach (object item in diff)
			{
				Dictionary<string, object> dictionary3 = JsonShape.Object(item);
				if (dictionary3.ContainsKey("operation"))
				{
					EntryAddition value = EntryAddition.Parse(dictionary3, original);
					string key = JsonShape.Text(dictionary3["key"]);
					if (dictionary2.ContainsKey(key))
					{
						throw new FormatException("Duplicate addEntry target");
					}
					dictionary2.Add(key, value);
					continue;
				}
				JsonShape.Fields(dictionary3, "id", "key", "entryIndex", "field", "before", "after", "approvalTaskId");
				string key2 = JsonShape.Text(dictionary3["key"]);
				string text = JsonShape.Text(dictionary3["field"]);
				int num = JsonShape.Int(dictionary3["entryIndex"]);
				if (!Regex.IsMatch(JsonShape.Text(dictionary3["approvalTaskId"]), "^t_[a-f0-9]{8}$"))
				{
					throw new FormatException("Approval task missing/invalid");
				}
				if (!new string[4] { "Value", "Feminine", "Masculine", "Neutral" }.Contains(text))
				{
					throw new FormatException("Unsupported diff field/operation");
				}
				if (!original.Records.TryGetValue(key2, out var value2) || JsonShape.Text(dictionary3["id"]) != value2.Id || value2.Entries == null || num < 0 || num >= value2.Entries.Count)
				{
					throw new FormatException("Invalid diff target");
				}
				string text2 = JsonShape.Text(dictionary3["before"], nullable: true);
				string text3 = JsonShape.Text(dictionary3["after"], nullable: true);
				if (text2 == text3 || !object.Equals(value2.Entries[num][text], text2))
				{
					throw new FormatException("Invalid diff before/after");
				}
				string[] array = TokenGuard.Inventory(text2).ToArray();
				string[] array2 = TokenGuard.Inventory(text3).ToArray();
				if (!array.SequenceEqual<string>(array2, StringComparer.Ordinal))
				{
					TokenGuard.RegisterException(JsonShape.Text(dictionary3["approvalTaskId"]), "0.1.0", array, array2);
				}
				string key3 = value2.Id + ":" + num + ":" + text;
				if (dictionary.ContainsKey(key3))
				{
					throw new FormatException("Duplicate diff target");
				}
				dictionary.Add(key3, dictionary3);
			}
			Dictionary<string, Record> dictionary4 = new Dictionary<string, Record>(StringComparer.Ordinal);
			int num2 = 0;
			foreach (KeyValuePair<string, Record> record2 in original.Records)
			{
				Record value3 = record2.Value;
				Record record = corrected.Records[record2.Key];
				if (dictionary2.TryGetValue(record2.Key, out var value4))
				{
					if (record.Entries == null || record.Entries.Count != 1 || !value4.After.Same(record.Entries[0]))
					{
						throw new FormatException("addEntry not exactly reflected by corrected snapshot: " + value3.Id);
					}
					num2++;
					dictionary4.Add(record2.Key, record);
				}
				else
				{
					if (Entry.SameLists(value3.Entries, record.Entries))
					{
						continue;
					}
					if (value3.Entries == null || record.Entries == null || value3.Entries.Count != record.Entries.Count)
					{
						throw new FormatException("List/absence operations require a newer validated loader");
					}
					for (int i = 0; i < value3.Entries.Count; i++)
					{
						string[] fields = Entry.Fields;
						foreach (string text4 in fields)
						{
							if (!object.Equals(value3.Entries[i][text4], record.Entries[i][text4]))
							{
								if (!dictionary.TryGetValue(value3.Id + ":" + i + ":" + text4, out var value5) || !object.Equals(value5["after"], record.Entries[i][text4]))
								{
									throw new FormatException("Unapproved change");
								}
								num2++;
							}
						}
					}
					dictionary4.Add(record2.Key, record);
				}
			}
			if (num2 != dictionary.Count + dictionary2.Count)
			{
				throw new FormatException("Diff not reflected by corrected snapshot");
			}
			Changed = new ReadOnlyDictionary<string, Record>(dictionary4);
		}

		public static Catalog Parse(string original, string corrected, string diff)
		{
			return new Catalog(Snapshot.Parse(original, "Original"), Snapshot.Parse(corrected, "Corrigido"), JsonShape.Array(StrictJson.Parse(diff)));
		}
	}
	internal static class TokenGuard
	{
		private static readonly Regex Tokens = new Regex("\\[\\[[^\\]]*\\]\\]|\\[[^\\]]*\\]|\\{[^}]*\\}|\\*[0-9]+|<[^>]*>", RegexOptions.CultureInvariant);

		private static volatile List<(string taskId, string version, string[] beforeTokens, string[] afterTokens)> validatedExceptions;

		private static readonly object validatedExceptionsLock = new object();

		private static List<(string taskId, string version, string[] beforeTokens, string[] afterTokens)> ValidatedExceptions
		{
			get
			{
				if (validatedExceptions == null)
				{
					lock (validatedExceptionsLock)
					{
						if (validatedExceptions == null)
						{
							validatedExceptions = new List<(string, string, string[], string[])>();
						}
					}
				}
				return validatedExceptions;
			}
		}

		internal static IEnumerable<string> Inventory(string s)
		{
			return (from Match m in Tokens.Matches(s ?? "")
				select m.Value).OrderBy<string, string>((string x) => x, StringComparer.Ordinal);
		}

		public static void Check(string before, string after)
		{
			string[] array = Inventory(before).ToArray();
			string[] second = Inventory(after).ToArray();
			if (array.SequenceEqual<string>(second, StringComparer.Ordinal))
			{
				return;
			}
			foreach (var validatedException in ValidatedExceptions)
			{
				if (validatedException.beforeTokens.SequenceEqual<string>(array, StringComparer.Ordinal) && validatedException.afterTokens.SequenceEqual<string>(second, StringComparer.Ordinal))
				{
					return;
				}
			}
			throw new FormatException("Functional tokens/tags changed; explicit validated exception unsupported in 0.1.0");
		}

		public static void RegisterException(string taskId, string version, string[] beforeTokens, string[] afterTokens)
		{
			if (!Regex.IsMatch(taskId, "^t_[a-f0-9]{8}$"))
			{
				throw new FormatException("Invalid exception taskId");
			}
			if (string.IsNullOrEmpty(version))
			{
				throw new FormatException("Invalid exception version");
			}
			if (beforeTokens == null || afterTokens == null)
			{
				throw new FormatException("Null inventory in exception");
			}
			if (beforeTokens.SequenceEqual<string>(afterTokens, StringComparer.Ordinal))
			{
				throw new FormatException("Exception before/after identical");
			}
			ValidatedExceptions.Add((taskId, version, beforeTokens, afterTokens));
		}
	}
	internal sealed class EntryAddition
	{
		public Entry After { get; }

		private EntryAddition(Entry after)
		{
			After = after;
		}

		public static EntryAddition Parse(Dictionary<string, object> d, Snapshot original)
		{
			JsonShape.Fields(d, "operation", "id", "key", "entryIndex", "before", "after", "approvalTaskId");
			if (JsonShape.Text(d["operation"]) != "addEntry")
			{
				throw new FormatException("Unsupported diff operation");
			}
			string text = JsonShape.Text(d["id"]);
			string text2 = JsonShape.Text(d["key"]);
			string text3 = "addEntry " + text + ": ";
			if (!Regex.IsMatch(JsonShape.Text(d["approvalTaskId"]), "^t_[a-f0-9]{8}$"))
			{
				throw new FormatException(text3 + "Approval task missing/invalid");
			}
			if (!original.Records.TryGetValue(text2, out var value) || value.Id != text)
			{
				throw new FormatException(text3 + "Unknown ID/key");
			}
			if (JsonShape.Int(d["entryIndex"]) != 0)
			{
				throw new FormatException(text3 + "Only entryIndex 0 supported");
			}
			if (value.Entries != null && value.Entries.Count != 0)
			{
				throw new FormatException(text3 + "Entry already exists; populated baseline");
			}
			bool num;
			if (value.Entries != null)
			{
				if (d["before"] == null)
				{
					goto IL_0189;
				}
				num = JsonShape.Array(d["before"]).Count != 0;
			}
			else
			{
				num = d["before"] != null;
			}
			if (!num)
			{
				Entry entry = new Entry(JsonShape.Object(d["after"]));
				if (string.IsNullOrWhiteSpace(entry.Value))
				{
					throw new FormatException(text3 + "Value must be nonblank");
				}
				if (entry.Key != text2 || entry.TranslatorKey != null || entry.Feminine != null || entry.Masculine != null || entry.Neutral != null || entry.LanguageGender != 0 || entry.CanBeGendered || entry.NeedsGenderInfo)
				{
					throw new FormatException(text3 + "Unsupported metadata; expected neutral Value-only entry");
				}
				if (!TokenGuard.Inventory(text2).SequenceEqual<string>(TokenGuard.Inventory(entry.Value), StringComparer.Ordinal))
				{
					throw new FormatException(text3 + "Functional tokens/tags changed against source key");
				}
				return new EntryAddition(entry);
			}
			goto IL_0189;
			IL_0189:
			throw new FormatException(text3 + "Baseline null/empty mismatch");
		}
	}
	public enum Conjunto
	{
		Original,
		Corrigido
	}
	internal sealed class Settings
	{
		public bool Active { get; }

		public Conjunto Variant { get; }

		public bool IntegrateMods { get; }

		public bool Diagnostic { get; }

		public bool Valid { get; }

		public Settings(bool active = true, Conjunto variant = Conjunto.Corrigido, bool integrateMods = true, bool diagnostic = false, bool valid = true)
		{
			Active = active;
			Variant = variant;
			IntegrateMods = integrateMods;
			Diagnostic = diagnostic;
			Valid = valid && Enum.IsDefined(typeof(Conjunto), variant);
		}
	}
	internal sealed class StateQueue
	{
		private Settings current = new Settings(active: false);

		private Settings pending;

		public Settings Current => Volatile.Read(in current);

		public void Enqueue(Settings settings)
		{
			Interlocked.Exchange(ref pending, settings ?? new Settings(active: false, Conjunto.Corrigido, integrateMods: true, diagnostic: false, valid: false));
		}

		public bool ApplyPending()
		{
			Settings settings = Interlocked.Exchange(ref pending, null);
			if (settings == null)
			{
				return false;
			}
			Settings settings2 = Current;
			if (settings2.Active == settings.Active && settings2.Variant == settings.Variant && settings2.IntegrateMods == settings.IntegrateMods && settings2.Diagnostic == settings.Diagnostic && settings2.Valid == settings.Valid)
			{
				return false;
			}
			Interlocked.Exchange(ref current, settings);
			return true;
		}
	}
	internal sealed class OnceLog
	{
		private readonly HashSet<string> seen = new HashSet<string>(StringComparer.Ordinal);

		private readonly Action<string> log;

		private readonly int maximum;

		public int Count
		{
			get
			{
				lock (seen)
				{
					return seen.Count;
				}
			}
		}

		public OnceLog(Action<string> log, int maximum = 128)
		{
			this.log = log;
			this.maximum = maximum;
		}

		public void Write(string reason)
		{
			bool flag;
			lock (seen)
			{
				flag = seen.Count < maximum && seen.Add(reason);
			}
			if (flag)
			{
				try
				{
					log?.Invoke(reason);
				}
				catch
				{
				}
			}
		}
	}
	internal sealed class Overlay<T>
	{
		private readonly Catalog catalog;

		private readonly Func<T, Entry, bool> equal;

		private readonly Func<Entry, T> clone;

		private readonly OnceLog log;

		public Overlay(Catalog catalog, Func<T, Entry, bool> equal, Func<Entry, T> clone, OnceLog log)
		{
			this.catalog = catalog;
			this.equal = equal;
			this.clone = clone;
			this.log = log;
		}

		public List<T> Resolve(string key, bool requestedPortuguese, bool effectivePortuguese, Settings settings, List<T> incoming)
		{
			try
			{
				if (catalog == null || settings == null || !settings.Valid || !settings.Active || settings.Variant != Conjunto.Corrigido || !requestedPortuguese || !effectivePortuguese || key == null || !catalog.Changed.TryGetValue(key, out var value))
				{
					return incoming;
				}
				IReadOnlyList<Entry> entries = catalog.Original.Records[key].Entries;
				bool flag = ((entries == null) ? (incoming == null) : (incoming != null && entries.Count == incoming.Count));
				if (flag && entries != null)
				{
					for (int i = 0; i < entries.Count; i++)
					{
						if (!equal(incoming[i], entries[i]))
						{
							flag = false;
							break;
						}
					}
				}
				if (!flag)
				{
					log?.Write("Conflito de baseline: " + value.Id);
					return incoming;
				}
				if (value.Entries == null)
				{
					return incoming;
				}
				List<T> list = new List<T>(value.Entries.Count);
				foreach (Entry entry in value.Entries)
				{
					list.Add(clone(entry));
				}
				return list;
			}
			catch (Exception ex)
			{
				log?.Write("Overlay preservou retorno: " + ex.GetType().Name);
				return incoming;
			}
		}
	}
	internal static class RefreshPlan
	{
		private static bool Effective(Settings s)
		{
			if (s != null && s.Valid && s.Active)
			{
				return s.Variant == Conjunto.Corrigido;
			}
			return false;
		}

		public static bool Needed(Settings before, Settings after)
		{
			return Effective(before) != Effective(after);
		}

		public static KeyValuePair<T, string>[] Select<T>(IEnumerable<KeyValuePair<T, string>> registered, ISet<string> changed)
		{
			return registered.Where((KeyValuePair<T, string> p) => p.Value != null && changed.Contains(p.Value)).ToArray();
		}
	}
	public static class ResourceHashes
	{
		public static readonly IReadOnlyDictionary<string, string> Expected = new Dictionary<string, string>
		{
			{ "correcoes.diff.json", "78d520a40d0ed3187cb913f0e719fcd9f4e6d1cfa3e272d83625e7d42bbf19e2" },
			{ "manifest.json", "3408218505d8eda9ec2b9ea10b82460d00fc829e6d6f236f801a6455bcd84d8b" },
			{ "pendencias.json", "778e0f9e0d269b15802ab620a18a5ffec1768aeb761428bbd0de75d66fb6dfaf" },
			{ "pt-BR.corrigido.json", "88e9cb7f2e606962664f1dcce9f7083bd0fba7c2387479252c17179e446cfe68" },
			{ "pt-BR.mods.json", "cb7e2437cf1ff68497c2223be77c10f1847ae8314d904943a53ceb6dac1aff65" },
			{ "pt-BR.original.json", "80d48629b6f0d6681badee323ee97201d7c445876e9d9d17a6ae0dadab9f2224" },
			{ "upstream.BP3.manifest.json", "2f9e7d57198881d1c2ec31b3332cdbe7e9162d24b34b5a7665299e61888c870e" }
		};
	}
	internal static class Resources
	{
		public static string Sha256(byte[] bytes)
		{
			using SHA256 sHA = SHA256.Create();
			return BitConverter.ToString(sHA.ComputeHash(bytes)).Replace("-", "").ToLowerInvariant();
		}

		public static string Decode(byte[] bytes)
		{
			return new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true).GetString(bytes);
		}

		public static string Read(Assembly assembly, string name)
		{
			using Stream stream = assembly.GetManifestResourceStream("BetterPortuguese.Data." + name) ?? throw new InvalidDataException("Missing embedded resource " + name);
			using MemoryStream memoryStream = new MemoryStream();
			stream.CopyTo(memoryStream);
			byte[] bytes = memoryStream.ToArray();
			if (!ResourceHashes.Expected.TryGetValue(name, out var value) || Sha256(bytes) != value)
			{
				throw new InvalidDataException("Resource SHA-256 mismatch " + name);
			}
			return Decode(bytes);
		}

		public static Catalog Load(Assembly assembly)
		{
			foreach (KeyValuePair<string, string> item in ResourceHashes.Expected)
			{
				if (item.Key != "pt-BR.mods.json")
				{
					Read(assembly, item.Key);
				}
			}
			return Catalog.Parse(Read(assembly, "pt-BR.original.json"), Read(assembly, "pt-BR.corrigido.json"), Read(assembly, "correcoes.diff.json"));
		}

		public static int LoadMods(Assembly assembly)
		{
			Dictionary<string, object> dictionary = JsonShape.Object(StrictJson.Parse(Read(assembly, "pt-BR.mods.json")));
			JsonShape.Fields(dictionary, "schemaVersion", "locale", "records");
			if (JsonShape.Int(dictionary["schemaVersion"]) != 1 || JsonShape.Text(dictionary["locale"]) != "pt-BR")
			{
				throw new FormatException("Invalid mod registry");
			}
			int count = JsonShape.Array(dictionary["records"]).Count;
			if (count != 0)
			{
				throw new NotSupportedException("Versioned mod adapters require an approved dataset and a newer plugin");
			}
			return count;
		}
	}
	internal sealed class StrictJson
	{
		private readonly string text;

		private int pos;

		private StrictJson(string text)
		{
			this.text = text ?? throw new ArgumentNullException("text");
		}

		public static object Parse(string text)
		{
			StrictJson strictJson = new StrictJson(text);
			object result = strictJson.Value(0);
			strictJson.White();
			if (strictJson.pos != text.Length)
			{
				throw strictJson.Error("Trailing content");
			}
			return result;
		}

		private FormatException Error(string reason)
		{
			return new FormatException(reason + " at JSON offset " + pos);
		}

		private void White()
		{
			while (pos < text.Length && " \t\r\n".IndexOf(text[pos]) >= 0)
			{
				pos++;
			}
		}

		private bool Take(char c)
		{
			White();
			if (pos < text.Length && text[pos] == c)
			{
				pos++;
				return true;
			}
			return false;
		}

		private void Need(char c)
		{
			if (!Take(c))
			{
				throw Error("Expected " + c);
			}
		}

		private object Value(int depth)
		{
			if (depth > 64)
			{
				throw Error("Maximum nesting exceeded");
			}
			White();
			if (pos >= this.text.Length)
			{
				throw Error("Unexpected end");
			}
			switch (this.text[pos])
			{
			case '"':
				return String();
			case '{':
			{
				pos++;
				Dictionary<string, object> dictionary = new Dictionary<string, object>(StringComparer.Ordinal);
				if (Take('}'))
				{
					return dictionary;
				}
				do
				{
					White();
					if (pos >= this.text.Length || this.text[pos] != '"')
					{
						throw Error("Expected member name");
					}
					string text2 = String();
					Need(':');
					object value = Value(depth + 1);
					if (dictionary.ContainsKey(text2))
					{
						throw Error("Duplicate member " + text2);
					}
					dictionary.Add(text2, value);
				}
				while (Take(','));
				Need('}');
				return dictionary;
			}
			case '[':
			{
				pos++;
				List<object> list = new List<object>();
				if (Take(']'))
				{
					return list;
				}
				do
				{
					list.Add(Value(depth + 1));
				}
				while (Take(','));
				Need(']');
				return list;
			}
			default:
			{
				string[] array = new string[3] { "true", "false", "null" };
				foreach (string text in array)
				{
					if (pos + text.Length <= this.text.Length && string.CompareOrdinal(this.text, pos, text, 0, text.Length) == 0)
					{
						pos += text.Length;
						if (!(text == "null"))
						{
							return text == "true";
						}
						return null;
					}
				}
				int num = pos;
				if (this.text[pos] == '-')
				{
					pos++;
				}
				if (pos >= this.text.Length)
				{
					throw Error("Number truncated");
				}
				if (this.text[pos] == '0')
				{
					pos++;
				}
				else
				{
					if (this.text[pos] < '1' || this.text[pos] > '9')
					{
						throw Error("Invalid value");
					}
					Digits();
				}
				if (pos < this.text.Length && this.text[pos] == '.')
				{
					pos++;
					int num2 = pos;
					Digits();
					if (num2 == pos)
					{
						throw Error("Fraction missing");
					}
				}
				if (pos < this.text.Length && (this.text[pos] == 'e' || this.text[pos] == 'E'))
				{
					pos++;
					if (pos < this.text.Length && (this.text[pos] == '+' || this.text[pos] == '-'))
					{
						pos++;
					}
					int num3 = pos;
					Digits();
					if (num3 == pos)
					{
						throw Error("Exponent missing");
					}
				}
				if (!decimal.TryParse(this.text.Substring(num, pos - num), NumberStyles.Float, CultureInfo.InvariantCulture, out var result))
				{
					throw Error("Number out of range");
				}
				return result;
			}
			}
		}

		private void Digits()
		{
			while (pos < text.Length && text[pos] >= '0' && text[pos] <= '9')
			{
				pos++;
			}
		}

		private char Hex()
		{
			if (pos + 4 > text.Length)
			{
				throw Error("Truncated unicode escape");
			}
			if (!ushort.TryParse(text.Substring(pos, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var result))
			{
				throw Error("Invalid unicode escape");
			}
			pos += 4;
			return (char)result;
		}

		private string String()
		{
			Need('"');
			StringBuilder stringBuilder = new StringBuilder();
			while (pos < this.text.Length)
			{
				char c = this.text[pos++];
				if (c == '"')
				{
					string text = stringBuilder.ToString();
					for (int i = 0; i < text.Length; i++)
					{
						if (char.IsHighSurrogate(text[i]))
						{
							if (++i >= text.Length || !char.IsLowSurrogate(text[i]))
							{
								throw Error("Unpaired surrogate");
							}
						}
						else if (char.IsLowSurrogate(text[i]))
						{
							throw Error("Unpaired surrogate");
						}
					}
					return text;
				}
				if (c < ' ')
				{
					throw Error("Control character in string");
				}
				if (c != '\\')
				{
					stringBuilder.Append(c);
					continue;
				}
				if (pos >= this.text.Length)
				{
					throw Error("Truncated escape");
				}
				c = this.text[pos++];
				switch (c)
				{
				case '"':
				case '/':
				case '\\':
					stringBuilder.Append(c);
					break;
				case 'b':
					stringBuilder.Append('\b');
					break;
				case 'f':
					stringBuilder.Append('\f');
					break;
				case 'n':
					stringBuilder.Append('\n');
					break;
				case 'r':
					stringBuilder.Append('\r');
					break;
				case 't':
					stringBuilder.Append('\t');
					break;
				case 'u':
					stringBuilder.Append(Hex());
					break;
				default:
					throw Error("Invalid escape");
				}
			}
			throw Error("Unterminated string");
		}
	}
	internal static class JsonShape
	{
		public static Dictionary<string, object> Object(object value)
		{
			return (value as Dictionary<string, object>) ?? throw new FormatException("Expected object");
		}

		public static List<object> Array(object value)
		{
			return (value as List<object>) ?? throw new FormatException("Expected array");
		}

		public static string Text(object value, bool nullable = false)
		{
			return (value == null && nullable) ? null : ((value as string) ?? throw new FormatException("Expected string"));
		}

		public static bool Bool(object value)
		{
			if (value is bool)
			{
				return (bool)value;
			}
			throw new FormatException("Expected boolean");
		}

		public static int Int(object value)
		{
			if (!(value is decimal num) || !(decimal.Truncate(num) == num) || !(num >= -2147483648m) || !(num <= 2147483647m))
			{
				throw new FormatException("Expected integer");
			}
			return (int)num;
		}

		public static void Fields(Dictionary<string, object> obj, params string[] fields)
		{
			if (obj.Count != fields.Length)
			{
				throw new FormatException("Unexpected/missing fields");
			}
			foreach (string text in fields)
			{
				if (!obj.ContainsKey(text))
				{
					throw new FormatException("Missing field " + text);
				}
			}
		}

		public static bool Equal(object a, object b)
		{
			if (a == b)
			{
				return true;
			}
			if (a is Dictionary<string, object> dictionary && b is Dictionary<string, object> dictionary2)
			{
				if (dictionary.Count != dictionary2.Count)
				{
					return false;
				}
				foreach (KeyValuePair<string, object> item in dictionary)
				{
					if (!dictionary2.TryGetValue(item.Key, out var value) || !Equal(item.Value, value))
					{
						return false;
					}
				}
				return true;
			}
			if (a is List<object> list && b is List<object> list2)
			{
				if (list.Count != list2.Count)
				{
					return false;
				}
				for (int i = 0; i < list.Count; i++)
				{
					if (!Equal(list[i], list2[i]))
					{
						return false;
					}
				}
				return true;
			}
			return object.Equals(a, b);
		}
	}
}