Decompiled source of G3A3 Config Editor v1.1.0

G3A3ConfigEditor.dll

Decompiled a day ago
using System;
using System.Collections;
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.Net.Sockets;
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.Bootstrap;
using BepInEx.Configuration;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[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 G3A3ConfigEditor
{
	[BepInPlugin("g3a3.configeditor", "G3A3 Config Editor", "1.1.0")]
	public class EditorPlugin : BaseUnityPlugin
	{
		[HarmonyPatch(typeof(Menu), "Start")]
		private static class PauseMenuPatch
		{
			private static void Postfix(Menu __instance)
			{
				try
				{
					AddButton(__instance.m_settingsButton);
				}
				catch (Exception ex)
				{
					EditorPlugin instance = _instance;
					if (instance != null)
					{
						((BaseUnityPlugin)instance).Logger.LogWarning((object)("Pause menu button: " + ex.Message));
					}
				}
			}
		}

		[HarmonyPatch(typeof(FejdStartup), "Start")]
		private static class MainMenuPatch
		{
			private static readonly FieldInfo MenuButtons = AccessTools.Field(typeof(FejdStartup), "m_menuButtons");

			private static void Postfix(FejdStartup __instance)
			{
				try
				{
					object obj = (MenuButtons?.GetValue(__instance) as Button[])?.FirstOrDefault((Func<Button, bool>)IsSettings);
					if (obj == null)
					{
						GameObject menuList = __instance.m_menuList;
						obj = ((menuList != null) ? ((IEnumerable<Button>)menuList.GetComponentsInChildren<Button>(true)).FirstOrDefault((Func<Button, bool>)IsSettings) : null);
					}
					AddButton((Button)obj);
				}
				catch (Exception ex)
				{
					EditorPlugin instance = _instance;
					if (instance != null)
					{
						((BaseUnityPlugin)instance).Logger.LogWarning((object)("Main menu button: " + ex.Message));
					}
				}
			}

			private static bool IsSettings(Button button)
			{
				if ((Object)(object)button == (Object)null)
				{
					return false;
				}
				for (int i = 0; i < ((UnityEventBase)button.onClick).GetPersistentEventCount(); i++)
				{
					if (((UnityEventBase)button.onClick).GetPersistentMethodName(i) == "OnButtonSettings")
					{
						return true;
					}
				}
				return false;
			}
		}

		[Serializable]
		[CompilerGenerated]
		private sealed class <>c
		{
			public static readonly <>c <>9 = new <>c();

			public static UnityAction <>9__21_0;

			internal void <AddButton>b__21_0()
			{
				_instance.Open(null);
			}
		}

		public const string Guid = "g3a3.configeditor";

		public const string Name = "G3A3 Config Editor";

		public const string Version = "1.1.0";

		private const string ButtonName = "G3A3ConfigEditorButton";

		private static EditorPlugin _instance;

		private readonly ConcurrentQueue<Action> _mainThread = new ConcurrentQueue<Action>();

		private ConfigEntry<KeyboardShortcut> _hotkey;

		private ConfigEntry<bool> _menuButtons;

		private ConfigEntry<int> _port;

		private EditorServer _server;

		private void Awake()
		{
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			//IL_0085: Unknown result type (might be due to invalid IL or missing references)
			//IL_008f: Expected O, but got Unknown
			//IL_00b1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00db: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ef: Expected O, but got Unknown
			//IL_00ea: Unknown result type (might be due to invalid IL or missing references)
			_instance = this;
			_hotkey = ((BaseUnityPlugin)this).Config.Bind<KeyboardShortcut>("General", "Hotkey", new KeyboardShortcut((KeyCode)288, Array.Empty<KeyCode>()), "EN: Key that opens the config editor in your browser. RU: Клавиша, которая открывает редактор конфигов в браузере.");
			_menuButtons = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "MenuButtons", true, "EN: When true, the main menu and the pause menu (Esc) get a Configs button. RU: Если true, в главном меню и в меню паузы (Esc) появляется кнопка «Конфиги».");
			_port = ((BaseUnityPlugin)this).Config.Bind<int>("General", "Port", 47900, new ConfigDescription("EN: First port the editor tries on this computer (127.0.0.1); the next free one up to +20 is used. RU: Первый порт, который редактор пробует на этом компьютере (127.0.0.1); берётся следующий свободный, до +20.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1024, 65000), Array.Empty<object>()));
			if (Application.isBatchMode)
			{
				((BaseUnityPlugin)this).Logger.LogInfo((object)"Dedicated server: the editor stays off.");
				return;
			}
			new Harmony("g3a3.configeditor").PatchAll(typeof(EditorPlugin).Assembly);
			new ConsoleCommand("configeditor", "[G3A3] opens the config editor in your browser", (ConsoleEvent)delegate(ConsoleEventArgs args)
			{
				Open(args.Context);
			}, false, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false);
		}

		private void Update()
		{
			//IL_003b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0040: Unknown result type (might be due to invalid IL or missing references)
			//IL_0043: Unknown result type (might be due to invalid IL or missing references)
			//IL_0050: Unknown result type (might be due to invalid IL or missing references)
			//IL_0055: Unknown result type (might be due to invalid IL or missing references)
			Action result;
			while (_mainThread.TryDequeue(out result))
			{
				try
				{
					result();
				}
				catch (Exception ex)
				{
					((BaseUnityPlugin)this).Logger.LogWarning((object)ex);
				}
			}
			if (_hotkey == null || Application.isBatchMode)
			{
				return;
			}
			KeyboardShortcut value = _hotkey.Value;
			if ((int)((KeyboardShortcut)(ref value)).MainKey != 0)
			{
				value = _hotkey.Value;
				if (((KeyboardShortcut)(ref value)).IsDown() && !Typing())
				{
					Open(null);
				}
			}
		}

		private void OnDestroy()
		{
			_server?.Stop();
		}

		private static bool Typing()
		{
			if (!Console.IsVisible() && !TextInput.IsVisible())
			{
				if ((Object)(object)Chat.instance != (Object)null)
				{
					return Chat.instance.HasFocus();
				}
				return false;
			}
			return true;
		}

		internal void Open(Terminal output)
		{
			try
			{
				if (_server == null)
				{
					string backupRoot = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "G3A3 Config Editor", "backups");
					_server = new EditorServer(Paths.ConfigPath, backupRoot, LoadPage(), Hint(), ApplyInGame, delegate(string message)
					{
						((BaseUnityPlugin)this).Logger.LogInfo((object)message);
					});
				}
				if (!_server.Running && !_server.Start(_port.Value))
				{
					((BaseUnityPlugin)this).Logger.LogError((object)("No free port from " + _port.Value + " to " + (_port.Value + 20) + "."));
					if (output != null)
					{
						output.AddString("Config editor: no free port from " + _port.Value);
					}
				}
				else
				{
					((BaseUnityPlugin)this).Logger.LogInfo((object)("Config editor: " + _server.Url));
					if (output != null)
					{
						output.AddString("Config editor: " + _server.Url);
					}
					Application.OpenURL(_server.Url);
				}
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogError((object)("Config editor could not start: " + ex));
				if (output != null)
				{
					output.AddString("Config editor could not start: " + ex.Message);
				}
			}
		}

		private static string LoadPage()
		{
			using Stream stream = typeof(EditorPlugin).Assembly.GetManifestResourceStream("G3A3ConfigEditor.ConfigEditor.html");
			using StreamReader streamReader = new StreamReader(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
			return streamReader.ReadToEnd();
		}

		private unsafe string Hint()
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_000b: Unknown result type (might be due to invalid IL or missing references)
			//IL_000e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0027: Unknown result type (might be due to invalid IL or missing references)
			//IL_002c: Unknown result type (might be due to invalid IL or missing references)
			KeyboardShortcut value = _hotkey.Value;
			string text;
			if ((int)((KeyboardShortcut)(ref value)).MainKey == 0)
			{
				text = string.Empty;
			}
			else
			{
				value = _hotkey.Value;
				text = " или клавиша " + ((object)(*(KeyboardShortcut*)(&value))/*cast due to .constrained prefix*/).ToString();
			}
			string text2 = text;
			return (_menuButtons.Value ? "кнопка «Конфиги» в меню (Esc)" : "команда configeditor в консоли") + text2;
		}

		private bool ApplyInGame(string path)
		{
			bool applied = false;
			ManualResetEvent done = new ManualResetEvent(initialState: false);
			_mainThread.Enqueue(delegate
			{
				try
				{
					applied = Reload(path);
				}
				finally
				{
					done.Set();
				}
			});
			return done.WaitOne(3000) && applied;
		}

		private static bool Reload(string path)
		{
			bool result = false;
			foreach (ConfigFile item in OpenConfigs())
			{
				if (string.Equals(Path.GetFullPath(item.ConfigFilePath), path, StringComparison.OrdinalIgnoreCase))
				{
					item.Reload();
					result = true;
				}
			}
			return result;
		}

		private static IEnumerable<ConfigFile> OpenConfigs()
		{
			HashSet<ConfigFile> seen = new HashSet<ConfigFile>();
			object? obj = AccessTools.Property(typeof(ConfigFile), "CoreConfig")?.GetValue(null, null);
			ConfigFile val = (ConfigFile)((obj is ConfigFile) ? obj : null);
			if (val != null && seen.Add(val))
			{
				yield return val;
			}
			foreach (PluginInfo item in Chainloader.PluginInfos.Values.ToList())
			{
				ConfigFile val2 = (((Object)(object)item.Instance != (Object)null) ? item.Instance.Config : null);
				if (val2 != null && seen.Add(val2))
				{
					yield return val2;
				}
			}
		}

		private static string Label()
		{
			if (!(((Localization.instance != null) ? Localization.instance.GetSelectedLanguage() : string.Empty) == "Russian"))
			{
				return "Configs";
			}
			return "Конфиги";
		}

		private static void AddButton(Button template)
		{
			//IL_0120: Unknown result type (might be due to invalid IL or missing references)
			//IL_012a: Expected O, but got Unknown
			//IL_0143: Unknown result type (might be due to invalid IL or missing references)
			//IL_0148: Unknown result type (might be due to invalid IL or missing references)
			//IL_014e: Expected O, but got Unknown
			if ((Object)(object)template == (Object)null || (Object)(object)_instance == (Object)null || !_instance._menuButtons.Value)
			{
				return;
			}
			Transform parent = ((Component)template).transform.parent;
			if ((Object)(object)parent == (Object)null || (Object)(object)parent.Find("G3A3ConfigEditorButton") != (Object)null)
			{
				return;
			}
			if ((Object)(object)((Component)parent).GetComponent<LayoutGroup>() == (Object)null)
			{
				((BaseUnityPlugin)_instance).Logger.LogInfo((object)("No layout around the " + ((Object)template).name + " button: no Configs button there, use the hotkey."));
				return;
			}
			GameObject val = Object.Instantiate<GameObject>(((Component)template).gameObject, parent);
			((Object)val).name = "G3A3ConfigEditorButton";
			val.transform.SetSiblingIndex(((Component)template).transform.GetSiblingIndex() + 1);
			Localize[] componentsInChildren = val.GetComponentsInChildren<Localize>(true);
			for (int i = 0; i < componentsInChildren.Length; i++)
			{
				Object.DestroyImmediate((Object)(object)componentsInChildren[i]);
			}
			UIGamePad[] componentsInChildren2 = val.GetComponentsInChildren<UIGamePad>(true);
			for (int i = 0; i < componentsInChildren2.Length; i++)
			{
				Object.DestroyImmediate((Object)(object)componentsInChildren2[i]);
			}
			TMP_Text[] componentsInChildren3 = val.GetComponentsInChildren<TMP_Text>(true);
			for (int i = 0; i < componentsInChildren3.Length; i++)
			{
				componentsInChildren3[i].text = Label();
			}
			Button component = val.GetComponent<Button>();
			component.onClick = new ButtonClickedEvent();
			ButtonClickedEvent onClick = component.onClick;
			object obj = <>c.<>9__21_0;
			if (obj == null)
			{
				UnityAction val2 = delegate
				{
					_instance.Open(null);
				};
				<>c.<>9__21_0 = val2;
				obj = (object)val2;
			}
			((UnityEvent)onClick).AddListener((UnityAction)obj);
			val.SetActive(true);
		}
	}
	internal sealed class EditorServer
	{
		private sealed class Request
		{
			public string Method;

			public string Path;

			public Dictionary<string, string> Query;

			public Dictionary<string, string> Headers;

			public byte[] Body;

			public string Arg(string name)
			{
				if (!Query.TryGetValue(name, out var value))
				{
					return null;
				}
				return value;
			}

			public string Header(string name)
			{
				if (!Headers.TryGetValue(name, out var value))
				{
					return null;
				}
				return value;
			}
		}

		private sealed class Response
		{
			public int Status;

			public string Type;

			public byte[] Body;

			public bool StopAfter;
		}

		private const int MaxHeader = 32768;

		private const int MaxBody = 16777216;

		private const int KeepBackups = 15;

		private const int MaxDepth = 4;

		private static readonly UTF8Encoding Utf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);

		private static readonly byte[] HeaderEnd = new byte[4] { 13, 10, 13, 10 };

		private static readonly Regex PluginLine = new Regex("^## Settings file was created by plugin (.+?)(?: v(\\S+))?\\s*$");

		private readonly string _configDir;

		private readonly string _backupRoot;

		private readonly string _page;

		private readonly string _hint;

		private readonly Func<string, bool> _apply;

		private readonly Action<string> _log;

		private readonly string _token = Guid.NewGuid().ToString("N");

		private TcpListener _listener;

		private volatile bool _running;

		public int Port { get; private set; }

		public bool Running => _running;

		public string Url => "http://127.0.0.1:" + Port + "/";

		public EditorServer(string configDir, string backupRoot, string page, string hint, Func<string, bool> apply, Action<string> log)
		{
			_configDir = Path.GetFullPath(configDir).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
			_backupRoot = backupRoot;
			_page = page;
			_hint = hint;
			_apply = apply;
			_log = log ?? ((Action<string>)delegate
			{
			});
		}

		public bool Start(int firstPort)
		{
			if (_running)
			{
				return true;
			}
			for (int i = firstPort; i <= firstPort + 20 && i <= 65535; i++)
			{
				TcpListener tcpListener = new TcpListener(IPAddress.Loopback, i)
				{
					ExclusiveAddressUse = true
				};
				try
				{
					tcpListener.Start();
				}
				catch (SocketException)
				{
					continue;
				}
				_listener = tcpListener;
				Port = i;
				_running = true;
				Thread thread = new Thread(Loop);
				thread.IsBackground = true;
				thread.Name = "G3A3 Config Editor";
				thread.Start();
				return true;
			}
			return false;
		}

		public void Stop()
		{
			_running = false;
			try
			{
				_listener?.Stop();
			}
			catch (Exception)
			{
			}
		}

		private void Loop()
		{
			while (_running)
			{
				TcpClient client;
				try
				{
					client = _listener.AcceptTcpClient();
				}
				catch (Exception)
				{
					if (_running)
					{
						Thread.Sleep(200);
					}
					continue;
				}
				ThreadPool.QueueUserWorkItem(delegate
				{
					Serve(client);
				});
			}
		}

		private void Serve(TcpClient client)
		{
			bool flag = false;
			using (client)
			{
				try
				{
					client.ReceiveTimeout = 10000;
					client.SendTimeout = 10000;
					NetworkStream stream = client.GetStream();
					Request request = ReadRequest(stream);
					if (request == null)
					{
						return;
					}
					Response response;
					try
					{
						response = ((request.Header("Transfer-Encoding") != null) ? Text(411, "text/plain; charset=utf-8", "length required") : Handle(request));
					}
					catch (Exception ex)
					{
						_log("request failed: " + ex.Message);
						response = Json(500, new Dictionary<string, object> { ["error"] = ex.Message });
					}
					flag = response.StopAfter;
					Write(stream, response);
					Linger(client, stream);
				}
				catch (Exception ex2)
				{
					_log("connection failed: " + ex2.Message);
				}
			}
			if (flag)
			{
				Stop();
			}
		}

		private static Request ReadRequest(Stream stream)
		{
			MemoryStream memoryStream = new MemoryStream();
			byte[] array = new byte[8192];
			int num = -1;
			while (num < 0)
			{
				int num2 = stream.Read(array, 0, array.Length);
				if (num2 <= 0)
				{
					return null;
				}
				memoryStream.Write(array, 0, num2);
				num = IndexOf(memoryStream.GetBuffer(), (int)memoryStream.Length, HeaderEnd);
				if (num < 0 && memoryStream.Length > 32768)
				{
					return null;
				}
			}
			byte[] array2 = memoryStream.ToArray();
			string[] array3 = Encoding.ASCII.GetString(array2, 0, num).Split(new string[1] { "\r\n" }, StringSplitOptions.None);
			string[] array4 = array3[0].Split(new char[1] { ' ' });
			if (array4.Length < 3)
			{
				return null;
			}
			Request request = new Request
			{
				Method = array4[0],
				Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
			};
			for (int i = 1; i < array3.Length; i++)
			{
				int num3 = array3[i].IndexOf(':');
				if (num3 > 0)
				{
					request.Headers[array3[i].Substring(0, num3).Trim()] = array3[i].Substring(num3 + 1).Trim();
				}
			}
			string text = array4[1];
			int num4 = text.IndexOf('?');
			request.Path = Uri.UnescapeDataString((num4 < 0) ? text : text.Substring(0, num4));
			request.Query = ParseQuery((num4 < 0) ? string.Empty : text.Substring(num4 + 1));
			int result = 0;
			string text2 = request.Header("Content-Length");
			if (text2 != null && (!int.TryParse(text2, out result) || result < 0 || result > 16777216))
			{
				return null;
			}
			byte[] array5 = new byte[result];
			int j = Math.Min(result, array2.Length - (num + 4));
			Array.Copy(array2, num + 4, array5, 0, j);
			int num5;
			for (; j < result; j += num5)
			{
				num5 = stream.Read(array5, j, result - j);
				if (num5 <= 0)
				{
					return null;
				}
			}
			request.Body = array5;
			return request;
		}

		private static Dictionary<string, string> ParseQuery(string query)
		{
			Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.Ordinal);
			string[] array = query.Split(new char[1] { '&' });
			foreach (string text in array)
			{
				if (text.Length != 0)
				{
					int num = text.IndexOf('=');
					string text2 = ((num < 0) ? text : text.Substring(0, num));
					string text3 = ((num < 0) ? string.Empty : text.Substring(num + 1));
					dictionary[Uri.UnescapeDataString(text2.Replace('+', ' '))] = Uri.UnescapeDataString(text3.Replace('+', ' '));
				}
			}
			return dictionary;
		}

		private static int IndexOf(byte[] data, int length, byte[] pattern)
		{
			for (int i = 0; i + pattern.Length <= length; i++)
			{
				int j;
				for (j = 0; j < pattern.Length && data[i + j] == pattern[j]; j++)
				{
				}
				if (j == pattern.Length)
				{
					return i;
				}
			}
			return -1;
		}

		private static void Write(Stream stream, Response response)
		{
			string s = "HTTP/1.1 " + response.Status + " " + Reason(response.Status) + "\r\nContent-Type: " + response.Type + "\r\nContent-Length: " + response.Body.Length + "\r\nCache-Control: no-store\r\nX-Content-Type-Options: nosniff\r\nConnection: close\r\n\r\n";
			byte[] bytes = Encoding.ASCII.GetBytes(s);
			stream.Write(bytes, 0, bytes.Length);
			stream.Write(response.Body, 0, response.Body.Length);
			stream.Flush();
		}

		private static void Linger(TcpClient client, NetworkStream stream)
		{
			try
			{
				client.Client.Shutdown(SocketShutdown.Send);
				client.ReceiveTimeout = 500;
				byte[] array = new byte[8192];
				int num;
				for (int i = 0; i < 16777216; i += num)
				{
					num = stream.Read(array, 0, array.Length);
					if (num <= 0)
					{
						break;
					}
				}
			}
			catch (Exception)
			{
			}
		}

		private static string Reason(int status)
		{
			return status switch
			{
				200 => "OK", 
				400 => "Bad Request", 
				403 => "Forbidden", 
				404 => "Not Found", 
				409 => "Conflict", 
				411 => "Length Required", 
				_ => "Internal Server Error", 
			};
		}

		private static Response Text(int status, string type, string text)
		{
			return new Response
			{
				Status = status,
				Type = type,
				Body = Utf8.GetBytes(text)
			};
		}

		private static Response Json(int status, object data)
		{
			return Text(status, "application/json; charset=utf-8", G3A3ConfigEditor.Json.Write(data));
		}

		private static Response Error(int status, string message)
		{
			return Json(status, new Dictionary<string, object> { ["error"] = message });
		}

		private Response Handle(Request request)
		{
			string text = request.Header("Host");
			if (text != "127.0.0.1:" + Port && text != "localhost:" + Port)
			{
				return Text(403, "text/plain; charset=utf-8", "forbidden");
			}
			string path = request.Path;
			if (request.Method == "GET" && (path == "/" || path == "/index.html"))
			{
				return Text(200, "text/html; charset=utf-8", _page.Replace("__G3A3_TOKEN__", _token));
			}
			if (!path.StartsWith("/api/", StringComparison.Ordinal))
			{
				return Text(404, "text/plain; charset=utf-8", "not found");
			}
			string text2 = request.Header("Origin");
			if (request.Header("X-G3A3-Token") != _token || (text2 != null && text2 != "http://127.0.0.1:" + Port && text2 != "http://localhost:" + Port))
			{
				return Error(403, "forbidden");
			}
			if (path == "/api/files" && request.Method == "GET")
			{
				return Files();
			}
			if (path == "/api/status" && request.Method == "GET")
			{
				string text3 = Target(request.Arg("name"));
				return Json(200, new Dictionary<string, object>
				{
					["game"] = false,
					["modified"] = ((text3 != null && File.Exists(text3)) ? Stamp(text3) : string.Empty)
				});
			}
			if (path == "/api/file" && request.Method == "GET")
			{
				return ReadFile(request.Arg("name"));
			}
			if (path == "/api/file" && request.Method == "POST")
			{
				return SaveFile(request);
			}
			if (path == "/api/quit" && request.Method == "POST")
			{
				Response response = Json(200, new Dictionary<string, object> { ["ok"] = true });
				response.StopAfter = true;
				return response;
			}
			return Error(404, "not found");
		}

		private Response Files()
		{
			string configDir = _configDir;
			char directorySeparatorChar = Path.DirectorySeparatorChar;
			string text = configDir + directorySeparatorChar;
			List<object> list = new List<object>();
			foreach (string item in ConfigFiles())
			{
				string[] array = PluginHead(item);
				list.Add(new Dictionary<string, object>
				{
					["name"] = item.Substring(text.Length),
					["size"] = new FileInfo(item).Length,
					["modified"] = Stamp(item),
					["plugin"] = array[0],
					["version"] = array[1]
				});
			}
			string[] array2 = PlaceInfo(_configDir);
			return Json(200, new Dictionary<string, object>
			{
				["dir"] = _configDir,
				["title"] = array2[0],
				["source"] = array2[1],
				["files"] = list,
				["game"] = false,
				["mode"] = "game",
				["hint"] = _hint
			});
		}

		private Response ReadFile(string name)
		{
			string text = Target(name);
			if (text == null || !File.Exists(text))
			{
				return Error(404, "нет такого файла");
			}
			return Json(200, new Dictionary<string, object>
			{
				["name"] = name,
				["text"] = ReadShared(text),
				["modified"] = Stamp(text)
			});
		}

		private Response SaveFile(Request request)
		{
			string text = request.Arg("name");
			string text2 = Target(text);
			if (text2 == null || !File.Exists(text2))
			{
				return Error(404, "нет такого файла");
			}
			string text3 = Utf8.GetString(request.Body);
			if (!text3.Contains("[") || !text3.Contains("="))
			{
				return Error(400, "это не похоже на конфиг BepInEx");
			}
			string text4 = Stamp(text2);
			string text5 = request.Arg("since");
			if (request.Arg("force") != "1" && !string.IsNullOrEmpty(text5) && text5 != text4)
			{
				return Json(409, new Dictionary<string, object>
				{
					["error"] = "файл изменился на диске",
					["modified"] = text4
				});
			}
			string text6 = SaveBackup(text2, text);
			File.WriteAllText(text2, text3, Utf8);
			bool flag = false;
			try
			{
				flag = _apply != null && _apply(text2);
			}
			catch (Exception ex)
			{
				_log("could not apply " + text + " in the game: " + ex.Message);
			}
			_log("saved " + text + (flag ? ", applied in the game" : string.Empty) + " (backup: " + text6 + ")");
			return Json(200, new Dictionary<string, object>
			{
				["ok"] = true,
				["modified"] = Stamp(text2),
				["backup"] = text6,
				["game"] = false,
				["applied"] = flag
			});
		}

		private List<string> ConfigFiles()
		{
			List<string> result = new List<string>();
			Collect(_configDir, 0, result);
			return result;
		}

		private static void Collect(string dir, int depth, List<string> result)
		{
			try
			{
				result.AddRange(from f in Directory.GetFiles(dir, "*.cfg")
					where string.Equals(Path.GetExtension(f), ".cfg", StringComparison.OrdinalIgnoreCase)
					select f);
				if (depth < 4)
				{
					string[] directories = Directory.GetDirectories(dir);
					for (int num = 0; num < directories.Length; num++)
					{
						Collect(directories[num], depth + 1, result);
					}
				}
			}
			catch (Exception)
			{
			}
		}

		private string Target(string name)
		{
			if (string.IsNullOrEmpty(name))
			{
				return null;
			}
			char directorySeparatorChar = Path.DirectorySeparatorChar;
			name = name.Replace('/', directorySeparatorChar).Replace('\\', directorySeparatorChar);
			if (!name.EndsWith(".cfg", StringComparison.OrdinalIgnoreCase) || Path.IsPathRooted(name) || name.IndexOf(':') >= 0 || name.Split(new char[1] { directorySeparatorChar }).Contains("..") || name.IndexOfAny(Path.GetInvalidPathChars()) >= 0)
			{
				return null;
			}
			string text = _configDir + directorySeparatorChar;
			string fullPath;
			try
			{
				fullPath = Path.GetFullPath(text + name);
			}
			catch (Exception)
			{
				return null;
			}
			if (!fullPath.StartsWith(text, StringComparison.OrdinalIgnoreCase))
			{
				return null;
			}
			return fullPath;
		}

		private static string Stamp(string path)
		{
			return File.GetLastWriteTimeUtc(path).Ticks.ToString();
		}

		private static string ReadShared(string path)
		{
			using FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
			using StreamReader streamReader = new StreamReader(stream, Utf8, detectEncodingFromByteOrderMarks: true);
			return streamReader.ReadToEnd();
		}

		private static string[] PluginHead(string path)
		{
			try
			{
				using FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
				using StreamReader streamReader = new StreamReader(stream, Utf8, detectEncodingFromByteOrderMarks: true);
				Match match = PluginLine.Match(streamReader.ReadLine() ?? string.Empty);
				if (match.Success)
				{
					return new string[2]
					{
						match.Groups[1].Value,
						match.Groups[2].Value
					};
				}
			}
			catch (Exception)
			{
			}
			return new string[2]
			{
				string.Empty,
				string.Empty
			};
		}

		private static string[] PlaceInfo(string dir)
		{
			string[] parts = dir.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
			int num = parts.Length;
			if (num >= 3 && Is(num - 1, "config") && Is(num - 2, "BepInEx"))
			{
				string text = parts[num - 3];
				if (num >= 6 && Is(num - 4, "profiles"))
				{
					string text2 = (Is(num - 6, "DataFolder") ? "Thunderstore Mod Manager" : (Is(num - 6, "r2modmanPlus-local") ? "r2modman" : (Is(num - 6, "com.kesomannen.gale") ? "Gale" : "Профили мод-менеджеров")));
					return new string[2]
					{
						parts[num - 5] + " · " + text,
						text2
					};
				}
				if (num >= 5 && Is(num - 4, "common") && Is(num - 5, "steamapps"))
				{
					return new string[2] { text, "Steam" };
				}
				return new string[2] { text, "Папки игр" };
			}
			return new string[2]
			{
				parts[num - 1],
				"Другие папки"
			};
			bool Is(int i, string value)
			{
				if (i >= 0)
				{
					return string.Equals(parts[i], value, StringComparison.OrdinalIgnoreCase);
				}
				return false;
			}
		}

		private string SaveBackup(string path, string name)
		{
			byte[] source;
			using (SHA1 sHA = SHA1.Create())
			{
				source = sHA.ComputeHash(Utf8.GetBytes(_configDir.ToLowerInvariant()));
			}
			string text = Regex.Replace(Regex.Replace(PlaceInfo(_configDir)[0], "\\s*·\\s*", "-"), "[\\\\/:*?\"<>|]", "_");
			string text2 = Path.Combine(_backupRoot, string.Concat(text, "-", string.Concat(from b in source.Take(4)
				select b.ToString("x2"))));
			Directory.CreateDirectory(text2);
			string text3 = name.Replace('/', '\\').Replace("\\", "__");
			string text4 = Path.Combine(text2, text3 + "." + DateTime.Now.ToString("yyyyMMdd-HHmmss") + ".bak");
			File.Copy(path, text4, overwrite: true);
			foreach (string item in Directory.GetFiles(text2, text3 + ".*.bak").OrderByDescending<string, string>((string f) => f, StringComparer.Ordinal).Skip(15))
			{
				File.Delete(item);
			}
			string? fileName = Path.GetFileName(text2);
			char directorySeparatorChar = Path.DirectorySeparatorChar;
			return fileName + directorySeparatorChar + Path.GetFileName(text4);
		}
	}
	internal static class Json
	{
		public static string Write(object value)
		{
			StringBuilder stringBuilder = new StringBuilder();
			Append(stringBuilder, value);
			return stringBuilder.ToString();
		}

		private static void Append(StringBuilder sb, object value)
		{
			if (value != null)
			{
				if (!(value is string text))
				{
					if (!(value is bool flag))
					{
						if (!(value is int) && !(value is long) && !(value is double) && !(value is float))
						{
							if (!(value is IDictionary<string, object> dictionary))
							{
								if (value is IEnumerable enumerable)
								{
									sb.Append('[');
									bool flag2 = true;
									foreach (object item in enumerable)
									{
										if (!flag2)
										{
											sb.Append(',');
										}
										flag2 = false;
										Append(sb, item);
									}
									sb.Append(']');
								}
								else
								{
									AppendString(sb, value.ToString());
								}
								return;
							}
							sb.Append('{');
							bool flag3 = true;
							foreach (KeyValuePair<string, object> item2 in dictionary)
							{
								if (!flag3)
								{
									sb.Append(',');
								}
								flag3 = false;
								AppendString(sb, item2.Key);
								sb.Append(':');
								Append(sb, item2.Value);
							}
							sb.Append('}');
						}
						else
						{
							sb.Append(((IFormattable)value).ToString(null, CultureInfo.InvariantCulture));
						}
					}
					else
					{
						sb.Append(flag ? "true" : "false");
					}
				}
				else
				{
					AppendString(sb, text);
				}
			}
			else
			{
				sb.Append("null");
			}
		}

		private static void AppendString(StringBuilder sb, string text)
		{
			sb.Append('"');
			foreach (char c in text)
			{
				switch (c)
				{
				case '"':
					sb.Append("\\\"");
					continue;
				case '\\':
					sb.Append("\\\\");
					continue;
				case '\n':
					sb.Append("\\n");
					continue;
				case '\r':
					sb.Append("\\r");
					continue;
				case '\t':
					sb.Append("\\t");
					continue;
				}
				if (c < ' ' || c == '\u2028' || c == '\u2029')
				{
					StringBuilder stringBuilder = sb.Append("\\u");
					int num = c;
					stringBuilder.Append(num.ToString("x4"));
				}
				else
				{
					sb.Append(c);
				}
			}
			sb.Append('"');
		}
	}
}