Decompiled source of SansaraCharacters v0.2.1

plugins/SansaraCharacters.dll

Decompiled 7 hours ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Jotunn.Utils;
using Microsoft.CodeAnalysis;
using Sansara.Admin;
using SansaraCharacters.Core;
using Splatform;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.6.2", FrameworkDisplayName = ".NET Framework 4.6.2")]
[assembly: AssemblyCompany("SansaraCharacters")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.2.1.0")]
[assembly: AssemblyInformationalVersion("0.2.1+b3c36db37415dcfd0ba5e58ce65d745e02185986")]
[assembly: AssemblyProduct("SansaraCharacters")]
[assembly: AssemblyTitle("SansaraCharacters")]
[assembly: AssemblyVersion("0.2.1.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 Sansara.Admin
{
	internal static class AdminChannel
	{
		private const float ScanSeconds = 1f;

		private const float DenialLogSeconds = 30f;

		private static ManualLogSource _log;

		private static bool _owner;

		private static bool _announced;

		private static float _nextScan;

		private static readonly HashSet<ZRpc> Registered = new HashSet<ZRpc>();

		private static readonly Dictionary<ZRpc, float> LastDenial = new Dictionary<ZRpc, float>();

		public static void Register(string module, string[] verbs, Func<string, string, string, string> handler, ManualLogSource log)
		{
			_log = log;
			if (AdminRegistry.Shared.Register(module, verbs, handler))
			{
				log.LogWarning((object)("Админ-канал: модуль " + module + " зарегистрирован повторно — обработчик заменён."));
			}
			if (AdminRegistry.Shared.ClaimOwner(typeof(AdminChannel).Assembly.GetName().Name))
			{
				_owner = true;
			}
		}

		public static void Tick()
		{
			if (!_owner || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || Time.realtimeSinceStartup < _nextScan)
			{
				return;
			}
			_nextScan = Time.realtimeSinceStartup + 1f;
			try
			{
				Scan();
			}
			catch (Exception ex)
			{
				_log.LogError((object)("Админ-канал: ошибка: " + ex));
			}
		}

		public static void CheckFile(string guid, string logPrefix, Func<string, string, string, string> handler)
		{
			string path = Path.Combine(Paths.ConfigPath, guid + ".command");
			if (!File.Exists(path))
			{
				return;
			}
			string text;
			try
			{
				text = File.ReadAllText(path).Trim();
				File.Delete(path);
			}
			catch (Exception ex)
			{
				_log.LogWarning((object)(logPrefix + ": файл команды не прочитан: " + ex.Message));
				return;
			}
			_log.LogInfo((object)(logPrefix + ": команда «" + AdminProtocol.ForLog(text) + "»."));
			try
			{
				_log.LogInfo((object)(logPrefix + ": " + handler(text, "файл команд", "").Replace("\n", "; ")));
			}
			catch (Exception ex2)
			{
				_log.LogError((object)(logPrefix + ": ошибка команды: " + ex2));
			}
		}

		private static void Scan()
		{
			if (!_announced)
			{
				_announced = true;
				_log.LogInfo((object)("Админ-канал: владелец " + AdminRegistry.Shared.Owner + ", модули: " + string.Join(", ", AdminRegistry.Shared.Modules().ToArray()) + "."));
			}
			HashSet<ZRpc> live = new HashSet<ZRpc>();
			foreach (ZNetPeer peer in ZNet.instance.GetPeers())
			{
				if (peer != null && peer.m_rpc != null)
				{
					live.Add(peer.m_rpc);
					if (Registered.Add(peer.m_rpc))
					{
						peer.m_rpc.Register<ZPackage>("Sansara_Admin", (Action<ZRpc, ZPackage>)OnRequest);
					}
				}
			}
			Registered.RemoveWhere((ZRpc r) => !live.Contains(r));
			foreach (ZRpc item in new List<ZRpc>(LastDenial.Keys))
			{
				if (!live.Contains(item))
				{
					LastDenial.Remove(item);
				}
			}
		}

		private static void OnRequest(ZRpc rpc, ZPackage pkg)
		{
			//IL_00b7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bd: Expected O, but got Unknown
			int num = 0;
			AdminReply adminReply;
			try
			{
				int protocol = pkg.ReadInt();
				num = pkg.ReadInt();
				string module = pkg.ReadString();
				string line = pkg.ReadString();
				adminReply = Handle(rpc, protocol, module, line);
			}
			catch (Exception ex)
			{
				ZNetPeer val = ZNet.instance.GetPeers().Find((ZNetPeer p) => p.m_rpc == rpc);
				if (val == null || !val.IsReady())
				{
					return;
				}
				LogThrottled(rpc, "Админ-канал: неверный запрос от " + AdminProtocol.ForLog(val.m_playerName) + ": " + AdminProtocol.ForLog(ex.Message));
				adminReply = new AdminReply(4, "неверный запрос");
			}
			if (adminReply != null)
			{
				ZPackage val2 = new ZPackage();
				val2.Write(1);
				val2.Write(num);
				val2.Write(adminReply.Code);
				val2.Write(AdminProtocol.Truncate(adminReply.Text));
				rpc.Invoke("Sansara_AdminReply", new object[1] { val2 });
			}
		}

		private static void LogThrottled(ZRpc rpc, string message)
		{
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			if (!LastDenial.TryGetValue(rpc, out var value) || realtimeSinceStartup - value >= 30f)
			{
				LastDenial[rpc] = realtimeSinceStartup;
				_log.LogWarning((object)message);
			}
		}

		private static AdminReply Handle(ZRpc rpc, int protocol, string module, string line)
		{
			ZNetPeer val = ZNet.instance.GetPeers().Find((ZNetPeer p) => p.m_rpc == rpc);
			if (val == null || !val.IsReady())
			{
				return null;
			}
			AdminReply adminReply = AdminProtocol.Check(protocol, module, line);
			if (adminReply != null)
			{
				return adminReply;
			}
			string text = AdminProtocol.ForLog(val.m_playerName);
			string text2 = AdminProtocol.ForLog(val.m_socket.GetHostName());
			string text3 = AdminProtocol.ForLog(AdminProtocol.Shown(module, line));
			bool flag = ((ZDOID)(ref val.m_characterID)).IsNone();
			if (flag || !ZNet.instance.IsAdmin(val.m_socket.GetHostName()))
			{
				LogThrottled(rpc, "Админ-канал: отказано " + text + " (" + text2 + "): " + text3);
				return new AdminReply(1, flag ? "отказано: персонаж не в мире (ещё не вошёл или погиб)" : "отказано: нужны права администратора");
			}
			Exception error;
			AdminReply adminReply2 = AdminRegistry.Shared.Dispatch(module, line, val.m_playerName, val.m_socket.GetHostName(), out error);
			if (error != null)
			{
				_log.LogError((object)("Админ-канал: " + text + " (" + text2 + ") → " + text3 + ": " + AdminProtocol.ForLog(error.Message) + Environment.NewLine + error.StackTrace));
			}
			else
			{
				_log.LogInfo((object)("Админ-канал: " + text + " (" + text2 + ") → " + text3 + ": " + AdminProtocol.ForLog(AdminProtocol.FirstLine(adminReply2.Text))));
			}
			return adminReply2;
		}
	}
	public sealed class AdminCommand
	{
		public string Verb = "";

		public readonly List<string> Args = new List<string>();

		public string Argument => string.Join(" ", Args);

		public static AdminCommand Parse(string text)
		{
			AdminCommand adminCommand = new AdminCommand();
			List<string> list = Tokens(text);
			if (list.Count == 0)
			{
				return adminCommand;
			}
			adminCommand.Verb = list[0].ToLowerInvariant();
			adminCommand.Args.AddRange(list.GetRange(1, list.Count - 1));
			return adminCommand;
		}

		public static List<string> Tokens(string text)
		{
			List<string> list = new List<string>();
			StringBuilder stringBuilder = new StringBuilder();
			bool flag = false;
			bool flag2 = false;
			string text2 = text ?? "";
			foreach (char c in text2)
			{
				if (c == '"')
				{
					flag = !flag;
					flag2 = true;
				}
				else if (!flag && char.IsWhiteSpace(c))
				{
					if (flag2)
					{
						list.Add(stringBuilder.ToString());
						stringBuilder.Clear();
						flag2 = false;
					}
				}
				else
				{
					stringBuilder.Append(c);
					flag2 = true;
				}
			}
			if (flag2)
			{
				list.Add(stringBuilder.ToString());
			}
			return list;
		}

		public static bool TryId(string argument, out string id)
		{
			string text = (argument ?? "").Trim();
			id = ((text.Length > 1 && text[0] == '@') ? text.Substring(1).Trim() : null);
			if (id == "")
			{
				id = null;
			}
			return id != null;
		}
	}
	public sealed class AdminReply
	{
		public readonly int Code;

		public readonly string Text;

		public AdminReply(int code, string text)
		{
			Code = code;
			Text = text ?? "";
		}
	}
	public static class AdminProtocol
	{
		public const int Version = 1;

		public const string RequestRpc = "Sansara_Admin";

		public const string ReplyRpc = "Sansara_AdminReply";

		public const int MaxLine = 256;

		public const int MaxReply = 4000;

		public const int Done = 0;

		public const int Denied = 1;

		public const int NoModule = 2;

		public const int Failed = 3;

		public const int Invalid = 4;

		public const int MaxLogText = 300;

		public static string Truncate(string text)
		{
			string text2 = text ?? "";
			if (text2.Length > 4000)
			{
				return text2.Substring(0, 3999) + "…";
			}
			return text2;
		}

		public static AdminReply Check(int protocol, string module, string line)
		{
			if (protocol != 1)
			{
				return new AdminReply(4, $"неверный запрос: обновите Sansara Admin (протокол сервера {1})");
			}
			if (module == null || line == null)
			{
				return new AdminReply(4, "неверный запрос");
			}
			if (Shown(module, line).Length > 256)
			{
				return new AdminReply(4, $"строка длиннее {256} символов");
			}
			return null;
		}

		public static string Shown(string module, string line)
		{
			string text = (module ?? "").Trim();
			if (!(text == ""))
			{
				return (text + " " + (line ?? "").Trim()).Trim();
			}
			return "help";
		}

		public static string ForLog(string s)
		{
			char[] array = (s ?? "").ToCharArray();
			for (int i = 0; i < array.Length; i++)
			{
				if (char.IsControl(array[i]))
				{
					array[i] = ' ';
				}
			}
			if (array.Length > 300)
			{
				return new string(array, 0, 300);
			}
			return new string(array);
		}

		public static string FirstLine(string text)
		{
			string text2 = (text ?? "").Replace("\r", "");
			int num = text2.IndexOf('\n');
			if (num >= 0)
			{
				return text2.Substring(0, num);
			}
			return text2;
		}

		public static string HelpLine(string module, IEnumerable<string> verbs)
		{
			return module + ": " + string.Join(", ", verbs);
		}

		public static Dictionary<string, string[]> ParseHelp(string text)
		{
			Dictionary<string, string[]> dictionary = new Dictionary<string, string[]>(StringComparer.Ordinal);
			string[] array = (text ?? "").Replace("\r", "").Split(new char[1] { '\n' });
			foreach (string text2 in array)
			{
				int num = text2.IndexOf(':');
				if (num <= 0)
				{
					continue;
				}
				string text3 = text2.Substring(0, num).Trim();
				if (IsName(text3))
				{
					dictionary[text3] = (from v in text2.Substring(num + 1).Split(new char[1] { ',' })
						select v.Trim()).Where(IsName).ToArray();
				}
			}
			return dictionary;
		}

		public static bool IsName(string s)
		{
			if (!string.IsNullOrEmpty(s))
			{
				return s.All((char c) => c >= 'a' && c <= 'z');
			}
			return false;
		}

		public static string Unknown(string verb, IEnumerable<string> verbs)
		{
			return (string.IsNullOrEmpty(verb) ? "укажите действие" : ("неизвестное действие «" + verb + "»")) + "; есть: " + string.Join(", ", verbs);
		}
	}
	public sealed class AdminRegistry
	{
		public const string SharedKey = "sansara.Admin.v1";

		public static readonly AdminRegistry Shared = new AdminRegistry("sansara.Admin.v1");

		private readonly string _key;

		private Dictionary<string, Tuple<string[], Func<string, string, string, string>>> Table
		{
			get
			{
				object data = AppDomain.CurrentDomain.GetData(_key);
				if (data == null)
				{
					Dictionary<string, Tuple<string[], Func<string, string, string, string>>> dictionary = new Dictionary<string, Tuple<string[], Func<string, string, string, string>>>(StringComparer.Ordinal);
					AppDomain.CurrentDomain.SetData(_key, dictionary);
					return dictionary;
				}
				return (data as Dictionary<string, Tuple<string[], Func<string, string, string, string>>>) ?? throw new InvalidOperationException("данные " + _key + " другого формата");
			}
		}

		public string Owner => (AppDomain.CurrentDomain.GetData(_key + ".owner") as string) ?? "";

		public AdminRegistry(string key)
		{
			_key = key;
		}

		public bool Register(string module, string[] verbs, Func<string, string, string, string> handler)
		{
			string text = (module ?? "").Trim().ToLowerInvariant();
			if (!AdminProtocol.IsName(text))
			{
				throw new ArgumentException("имя модуля — латинские буквы: " + module);
			}
			string[] array = verbs ?? new string[0];
			foreach (string text2 in array)
			{
				if (!AdminProtocol.IsName(text2))
				{
					throw new ArgumentException("глагол — латинские буквы: " + text2);
				}
			}
			if (handler == null)
			{
				throw new ArgumentNullException("handler");
			}
			Dictionary<string, Tuple<string[], Func<string, string, string, string>>> table = Table;
			bool result = table.ContainsKey(text);
			table[text] = Tuple.Create(verbs ?? new string[0], handler);
			return result;
		}

		public bool ClaimOwner(string assembly)
		{
			if (string.IsNullOrEmpty(assembly))
			{
				throw new ArgumentException("пустое имя сборки", "assembly");
			}
			string name = _key + ".owner";
			if (!(AppDomain.CurrentDomain.GetData(name) is string text))
			{
				AppDomain.CurrentDomain.SetData(name, assembly);
				return true;
			}
			return text == assembly;
		}

		public List<string> Modules()
		{
			return Table.Keys.OrderBy<string, string>((string k) => k, StringComparer.Ordinal).ToList();
		}

		public string HelpText()
		{
			Dictionary<string, Tuple<string[], Func<string, string, string, string>>> table = Table;
			if (table.Count != 0)
			{
				return string.Join("\n", from k in Modules()
					select AdminProtocol.HelpLine(k, table[k].Item1));
			}
			return "на сервере нет модулей Sansara Admin";
		}

		public AdminReply Dispatch(string module, string line, string adminName, string adminId, out Exception error)
		{
			error = null;
			try
			{
				string text = (module ?? "").Trim().ToLowerInvariant();
				if (text == "")
				{
					return new AdminReply(0, AdminProtocol.Truncate(HelpText()));
				}
				Dictionary<string, Tuple<string[], Func<string, string, string, string>>> table = Table;
				if (!table.TryGetValue(text, out var value))
				{
					return new AdminReply(2, "модуля " + text + " нет на сервере; есть: " + ((table.Count == 0) ? "ничего" : string.Join(", ", Modules())));
				}
				string text2 = value.Item2((line ?? "").Trim(), adminName ?? "", adminId ?? "");
				return new AdminReply(0, AdminProtocol.Truncate(string.IsNullOrEmpty(text2) ? "готово" : text2));
			}
			catch (Exception ex)
			{
				return new AdminReply(3, AdminProtocol.Truncate("ошибка: " + (error = ex).Message));
			}
		}
	}
	public static class AdminUi
	{
		public const string Verb = "ui";

		public const string Head = "ui=1";

		public const string NoteKey = "note";

		public const string MoreKey = "more";

		private const int Reserve = 16;

		public static string Field(object value)
		{
			char[] array = ((!(value is bool)) ? (Convert.ToString(value, CultureInfo.InvariantCulture) ?? "") : (((bool)value) ? "1" : "0")).ToCharArray();
			for (int i = 0; i < array.Length; i++)
			{
				if (array[i] == ';')
				{
					array[i] = ',';
				}
				else if (char.IsControl(array[i]))
				{
					array[i] = ' ';
				}
			}
			return new string(array).Trim();
		}

		public static string Row(string key, params object[] fields)
		{
			if (!AdminProtocol.IsName(key))
			{
				throw new ArgumentException("ключ строки ui — латинские буквы в нижнем регистре: " + key);
			}
			return key + "=" + string.Join(";", (fields ?? new object[0]).Select(Field));
		}

		public static string Note(string text)
		{
			return Text(new string[1] { Row("note", text) });
		}

		public static string Text(IEnumerable<string> rows)
		{
			List<string> list = (rows ?? Enumerable.Empty<string>()).ToList();
			StringBuilder stringBuilder = new StringBuilder("ui=1");
			for (int i = 0; i < list.Count; i++)
			{
				if (stringBuilder.Length + 1 + list[i].Length > 3984)
				{
					stringBuilder.Append('\n').Append("more").Append('=')
						.Append((list.Count - i).ToString(CultureInfo.InvariantCulture));
					break;
				}
				stringBuilder.Append('\n').Append(list[i]);
			}
			return stringBuilder.ToString();
		}
	}
}
namespace SansaraCharacters
{
	internal static class CharsAdmin
	{
		internal static readonly string[] Verbs = new string[15]
		{
			"status", "help", "ui", "list", "info", "backups", "snapshot", "restore", "strip", "skill",
			"resetskills", "unstuck", "clearpower", "import", "archive"
		};

		private const string HelpText = "Персонаж — имя или @файл (Steam_<id>_<имя>), как в списке; игрока в сети изменения отключают от сервера.\nstatus — сколько персонажей, кто в сети, окно переноса\nlist — все персонажи: файл, в сети ли, когда сохранён, копий\ninfo <персонаж> — вещи, надетое, навыки, еда, сила Форсакена, трофеи\nbackups <персонаж> — резервные копии: дата, вещей, сумма навыков\nsnapshot <персонаж> — снять копию сейчас\nrestore <персонаж> <копия|last> — вернуть персонажа к копии (текущее состояние сначала тоже копируется)\nstrip <персонаж> — очистить, как при переносе: вещи, рецепты, сила, еда (навыки остаются)\nskill <персонаж> <навык> <уровень 0–100> — поставить уровень навыка (Swords, Run, WoodCutting…)\nresetskills <персонаж> — сбросить все навыки\nunstuck <персонаж> — забыть точку выхода: при входе — у кровати или у стартового алтаря\nclearpower <персонаж> — снять силу Форсакена\nimport status | import until <гггг-мм-дд чч:мм> | import close — окно переноса персонажей\narchive <персонаж> — убрать персонажа и его копии в characters_local/archive (вход — как новый)\nui — данные для окна Sansara Admin; ui @<файл> — его копии";

		private static bool _namesLoaded;

		public static string Handle(string line, string admin, string adminId)
		{
			AdminCommand c = AdminCommand.Parse(line);
			if (!ServerSide.Active)
			{
				return "модуль работает только на выделенном сервере";
			}
			try
			{
				return c.Verb switch
				{
					"help" => "Персонаж — имя или @файл (Steam_<id>_<имя>), как в списке; игрока в сети изменения отключают от сервера.\nstatus — сколько персонажей, кто в сети, окно переноса\nlist — все персонажи: файл, в сети ли, когда сохранён, копий\ninfo <персонаж> — вещи, надетое, навыки, еда, сила Форсакена, трофеи\nbackups <персонаж> — резервные копии: дата, вещей, сумма навыков\nsnapshot <персонаж> — снять копию сейчас\nrestore <персонаж> <копия|last> — вернуть персонажа к копии (текущее состояние сначала тоже копируется)\nstrip <персонаж> — очистить, как при переносе: вещи, рецепты, сила, еда (навыки остаются)\nskill <персонаж> <навык> <уровень 0–100> — поставить уровень навыка (Swords, Run, WoodCutting…)\nresetskills <персонаж> — сбросить все навыки\nunstuck <персонаж> — забыть точку выхода: при входе — у кровати или у стартового алтаря\nclearpower <персонаж> — снять силу Форсакена\nimport status | import until <гггг-мм-дд чч:мм> | import close — окно переноса персонажей\narchive <персонаж> — убрать персонажа и его копии в characters_local/archive (вход — как новый)\nui — данные для окна Sansara Admin; ui @<файл> — его копии", 
					"status" => Status(), 
					"ui" => Ui(c), 
					"list" => List(), 
					"info" => WithCharacter(c, 0, Info), 
					"backups" => WithCharacter(c, 0, Backups), 
					"snapshot" => WithCharacter(c, 0, (string f) => Snapshot(f, admin)), 
					"restore" => WithCharacter(c, 1, (string f) => Restore(f, c.Args[c.Args.Count - 1], admin)), 
					"strip" => WithCharacter(c, 0, (string f) => Change(f, admin, "очищен (вещи, рецепты, сила, еда; навыки остались)", delegate
					{
						ServerSide.RewritePlayerData(f, PlayerDataStrip.Strip);
					})), 
					"skill" => WithCharacter(c, 2, (string f) => Skill(f, c.Args[c.Args.Count - 2], c.Args[c.Args.Count - 1], admin)), 
					"resetskills" => WithCharacter(c, 0, (string f) => Change(f, admin, "навыки сброшены", delegate
					{
						ServerSide.RewritePlayerData(f, (byte[] d) => PlayerDataView.WithSkills(d, (List<SkillInfo> _) => new List<SkillInfo>()));
					})), 
					"clearpower" => WithCharacter(c, 0, (string f) => Change(f, admin, "сила Форсакена снята", delegate
					{
						ServerSide.RewritePlayerData(f, (byte[] d) => PlayerDataView.WithGuardianPower(d, ""));
					})), 
					"unstuck" => WithCharacter(c, 0, (string f) => Change(f, admin, "точка выхода в этом мире забыта: при входе — у кровати или у стартового алтаря", delegate
					{
						ServerSide.RewriteWithGame(f, delegate(PlayerProfile p)
						{
							p.ClearLoguoutPoint();
						});
					})), 
					"archive" => WithCharacter(c, 0, (string f) => Archive(f, admin)), 
					"import" => Import(c, admin), 
					_ => AdminProtocol.Unknown(c.Verb, Verbs), 
				};
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)$"Персонажи: {admin}: «{AdminProtocol.ForLog(line)}»: {ex}");
				return "ошибка: " + ex.Message;
			}
		}

		private static List<string> Files()
		{
			if (!Directory.Exists(Transport.CharacterFolder))
			{
				return new List<string>();
			}
			return (from f in Directory.GetFiles(Transport.CharacterFolder, "*.fch").Select(Path.GetFileNameWithoutExtension)
				where !ServerSide.IsTemporary(f)
				select f).OrderBy<string, string>((string f) => f, StringComparer.Ordinal).ToList();
		}

		private static List<string> BackupNames(string file)
		{
			string path = ServerSide.BackupFolder(file);
			if (!Directory.Exists(path))
			{
				return new List<string>();
			}
			return Directory.GetFiles(path, "*.fch").Select(Path.GetFileNameWithoutExtension).OrderBy<string, string>((string n) => n, StringComparer.Ordinal)
				.ToList();
		}

		private static string Saved(string file)
		{
			return File.GetLastWriteTime(ServerSide.PathOf(file)).ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture);
		}

		private static string Status()
		{
			List<string> list = Files();
			int num = list.Count((string f) => ServerSide.OnlinePeer(f) != null);
			return $"персонажей на сервере {list.Count}, в сети {num}, резервных копий {list.Sum((string f) => BackupNames(f).Count)}; " + ImportState();
		}

		private static string ImportState()
		{
			try
			{
				DateTime? until = ImportWindow.ParseUntil(PluginConfig.ImportUntil.Value);
				if (!until.HasValue)
				{
					return "перенос закрыт";
				}
				return ImportWindow.IsOpen(until, DateTime.Now) ? ("перенос открыт до " + until.Value.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture)) : ("перенос закрыт (срок " + until.Value.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture) + " прошёл)");
			}
			catch (FormatException)
			{
				return "перенос закрыт (ImportUntil в cfg не разобран)";
			}
		}

		private static string List()
		{
			List<string> list = Files();
			if (list.Count == 0)
			{
				return "персонажей на сервере нет";
			}
			return string.Join("\n", list.Select((string f) => string.Format("{0} — {1}, сохранён {2}, копий {3}", f, (ServerSide.OnlinePeer(f) != null) ? "в сети" : "не в сети", Saved(f), BackupNames(f).Count)));
		}

		private static string Ui(AdminCommand c)
		{
			List<string> list = new List<string>();
			if (c.Args.Count > 0)
			{
				string error;
				string text = CharacterQuery.Resolve(Files(), c.Argument, out error);
				if (text == null)
				{
					return AdminUi.Note(error);
				}
				foreach (string item in BackupNames(text).AsEnumerable().Reverse())
				{
					PlayerDataView playerDataView = TryView(File.ReadAllBytes(Path.Combine(ServerSide.BackupFolder(text), item + ".fch")));
					list.Add(AdminUi.Row("backup", text, item, playerDataView?.Items.Count ?? (-1), (playerDataView == null) ? (-1) : ((int)playerDataView.Skills.Sum((SkillInfo s) => s.Level))));
				}
				return AdminUi.Text(list);
			}
			foreach (string item2 in Files())
			{
				list.Add(AdminUi.Row("char", item2, CharacterQuery.NameOf(item2), ServerSide.OnlinePeer(item2) != null, Saved(item2), BackupNames(item2).Count));
			}
			return AdminUi.Text(list);
		}

		private static string Info(string file)
		{
			//IL_0002: Unknown result type (might be due to invalid IL or missing references)
			//IL_0008: Expected O, but got Unknown
			PlayerProfile val = new PlayerProfile(file, (FileSource)2);
			if (!val.Load())
			{
				return file + ": файл не читается загрузчиком игры";
			}
			byte[] array = ServerSide.PlayerDataRef.Invoke(val);
			string text = val.GetName() + " (" + file + "), " + ((ServerSide.OnlinePeer(file) != null) ? "в сети — данные на момент последней записи" : "не в сети") + ", " + $"сохранён {Saved(file)}, копий {BackupNames(file).Count}";
			if (array == null)
			{
				return text + "\nданных игрока ещё нет (не появлялся в мире)";
			}
			PlayerDataView playerDataView = PlayerDataView.Parse(array);
			IEnumerable<string> values = from i in playerDataView.Items
				group i by i.PrefabHash into g
				select ItemName(g.Key) + ((g.Sum((ItemInfo i) => i.Stack) > 1) ? (" ×" + g.Sum((ItemInfo i) => i.Stack)) : "") + ((g.Max((ItemInfo i) => i.Quality) > 1) ? (" ур." + g.Max((ItemInfo i) => i.Quality)) : "") + (g.Any((ItemInfo i) => i.Equipped) ? " (надето)" : "");
			IEnumerable<string> values2 = from s in playerDataView.Skills
				where s.Level >= 1f
				orderby s.Level descending
				select SkillName(s.Type) + " " + Math.Floor(s.Level).ToString(CultureInfo.InvariantCulture);
			return text + "\n" + string.Format("здоровье {0:0}/{1:0}, выносливость {2:0}; сила Форсакена: {3}\n", playerDataView.Health, playerDataView.MaxHealth, playerDataView.MaxStamina, (playerDataView.GuardianPower == "") ? "нет" : playerDataView.GuardianPower) + string.Format("вещей {0}: {1}\n", playerDataView.Items.Count, (playerDataView.Items.Count == 0) ? "—" : string.Join(", ", values)) + "навыки: " + ((playerDataView.Skills.Count == 0) ? "—" : string.Join(", ", values2)) + "\nеда: " + ((playerDataView.Foods.Count == 0) ? "—" : string.Join(", ", playerDataView.Foods.Select((KeyValuePair<string, float> f) => $"{ItemName(f.Key)} ({f.Value / 60f:0} мин)"))) + "\n" + $"рецептов {playerDataView.KnownRecipes}, станций {playerDataView.KnownStations}, трофеев {playerDataView.Trophies.Count}, биомов {playerDataView.Biomes.Count}";
		}

		private static string Backups(string file)
		{
			List<string> list = BackupNames(file);
			if (list.Count == 0)
			{
				return file + ": резервных копий нет";
			}
			string folder = ServerSide.BackupFolder(file);
			return $"{file}: копий {list.Count} (новые сверху)\n" + string.Join("\n", list.AsEnumerable().Reverse().Select(delegate(string n)
			{
				byte[] array = File.ReadAllBytes(Path.Combine(folder, n + ".fch"));
				PlayerDataView playerDataView = TryView(array);
				return (playerDataView != null) ? $"{n} — вещей {playerDataView.Items.Count}, сумма навыков {playerDataView.Skills.Sum((SkillInfo s) => s.Level):0}, {array.Length / 1024} КБ" : $"{n} — {array.Length / 1024} КБ, данные не разобраны";
			}));
		}

		private static PlayerDataView TryView(byte[] file)
		{
			try
			{
				byte[] array = ProfileFile.Unframe(file);
				for (int i = 4; i < array.Length - 5; i++)
				{
					int num = array.Length - i - 5;
					if (array[num] == 1 && BitConverter.ToInt32(array, num + 1) == i)
					{
						byte[] array2 = new byte[i];
						Buffer.BlockCopy(array, num + 5, array2, 0, i);
						return PlayerDataView.Parse(array2);
					}
				}
			}
			catch (Exception)
			{
			}
			return null;
		}

		private static void EnsureNames()
		{
			if (!_namesLoaded)
			{
				_namesLoaded = true;
				ItemNames.Load();
			}
		}

		private static string ItemName(int hash)
		{
			EnsureNames();
			ObjectDB instance = ObjectDB.instance;
			GameObject val = ((instance != null) ? instance.GetItemPrefab(hash) : null);
			if (!((Object)(object)val == (Object)null))
			{
				return ItemNames.Of(((Object)val).name);
			}
			return "?" + hash;
		}

		private static string ItemName(string prefab)
		{
			EnsureNames();
			ObjectDB instance = ObjectDB.instance;
			if (!((Object)(object)((instance != null) ? instance.GetItemPrefab(prefab) : null) == (Object)null))
			{
				return ItemNames.Of(prefab);
			}
			return prefab;
		}

		private static string SkillName(int type)
		{
			//IL_002a: Unknown result type (might be due to invalid IL or missing references)
			if (!Enum.IsDefined(typeof(SkillType), type))
			{
				return "#" + type;
			}
			return ((object)(SkillType)type/*cast due to .constrained prefix*/).ToString();
		}

		private static string WithCharacter(AdminCommand c, int extraArgs, Func<string, string> action)
		{
			if (c.Args.Count < 1 + extraArgs)
			{
				return "не хватает аргументов; справка: sansara chars help";
			}
			string query = string.Join(" ", c.Args.Take(c.Args.Count - extraArgs));
			string error;
			string text = CharacterQuery.Resolve(Files(), query, out error);
			if (text != null)
			{
				return action(text);
			}
			return error;
		}

		private static string Change(string file, string admin, string what, Action change)
		{
			bool flag = KickIfOnline(file);
			string text;
			lock (ServerSide.FileLock)
			{
				text = ServerSide.Backup(file, ServerSide.PathOf(file), force: true);
				change();
			}
			Plugin.Log.LogInfo((object)("Персонажи: " + admin + ": " + file + " — " + what + "; копия до изменения " + text));
			return file + ": " + what + "." + (flag ? " Игрок был в сети — отключён." : "") + " Копия до изменения: " + text + ".";
		}

		private static bool KickIfOnline(string file)
		{
			ZNetPeer val = ServerSide.OnlinePeer(file);
			if (val == null)
			{
				return false;
			}
			Plugin.Log.LogInfo((object)("Персонажи: " + file + " в сети — отключаю перед изменением"));
			ServerSide.Kick(val);
			return true;
		}

		private static string Snapshot(string file, string admin)
		{
			string text;
			lock (ServerSide.FileLock)
			{
				text = ServerSide.Backup(file, ServerSide.PathOf(file), force: true);
			}
			Plugin.Log.LogInfo((object)("Персонажи: " + admin + ": " + file + " — копия " + text));
			return file + ": копия " + text + ((ServerSide.OnlinePeer(file) != null) ? " (игрок в сети: копия — последнее записанное сервером состояние)" : "");
		}

		private static string Restore(string file, string which, string admin)
		{
			List<string> list = BackupNames(file);
			if (list.Count == 0)
			{
				return file + ": резервных копий нет";
			}
			string text = (which.Equals("last", StringComparison.OrdinalIgnoreCase) ? list[list.Count - 1] : (list.FirstOrDefault((string n) => n == which) ?? ((list.Count((string n) => n.StartsWith(which, StringComparison.Ordinal)) == 1) ? list.First((string n) => n.StartsWith(which, StringComparison.Ordinal)) : null)));
			if (text == null)
			{
				return file + ": копии «" + which + "» нет; список — sansara chars backups " + file;
			}
			string path = Path.Combine(ServerSide.BackupFolder(file), text + ".fch");
			byte[] bytes = File.ReadAllBytes(path);
			ProfileFile.Unframe(bytes);
			return Change(file, admin, "восстановлен из копии " + text, delegate
			{
				Transport.WriteFileAtomic(ServerSide.PathOf(file), bytes);
			});
		}

		private static string Skill(string file, string skill, string level, string admin)
		{
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0025: Unknown result type (might be due to invalid IL or missing references)
			//IL_002f: Invalid comparison between Unknown and I4
			//IL_00cd: Unknown result type (might be due to invalid IL or missing references)
			if (!Enum.TryParse<SkillType>(skill, ignoreCase: true, out SkillType type) || (int)type == 0 || (int)type == 999)
			{
				return "навык «" + skill + "» не найден; навыки: " + string.Join(", ", from n in Enum.GetNames(typeof(SkillType))
					where n != "None" && n != "All"
					select n);
			}
			if (!float.TryParse(level.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out var value) || value < 0f || value > 100f)
			{
				return "уровень — число от 0 до 100";
			}
			return Change(file, admin, $"навык {type} = {value.ToString(CultureInfo.InvariantCulture)}", delegate
			{
				ServerSide.RewritePlayerData(file, (byte[] d) => PlayerDataView.WithSkillLevel(d, (int)type, value));
			});
		}

		private static string Archive(string file, string admin)
		{
			bool flag = KickIfOnline(file);
			string text = DateTime.Now.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture);
			string text2 = Path.Combine(Path.Combine(Transport.CharacterFolder, "archive"), text + "_" + file);
			lock (ServerSide.FileLock)
			{
				Directory.CreateDirectory(text2);
				File.Move(ServerSide.PathOf(file), Path.Combine(text2, file + ".fch"));
				string text3 = ServerSide.BackupFolder(file);
				if (Directory.Exists(text3))
				{
					Directory.Move(text3, Path.Combine(text2, "backups"));
				}
			}
			Plugin.Log.LogInfo((object)("Персонажи: " + admin + ": " + file + " — в архиве " + text2));
			return file + ": убран в архив characters_local/archive/" + text + "_" + file + "." + (flag ? " Игрок был в сети — отключён." : "") + " При следующем входе этим персонажем — как новый (или перенос, если окно открыто).";
		}

		private static string Import(AdminCommand c, string admin)
		{
			switch ((c.Args.Count > 0) ? c.Args[0].ToLowerInvariant() : "status")
			{
			case "status":
				return ImportState();
			case "close":
				PluginConfig.ImportUntil.Value = "";
				Plugin.Log.LogInfo((object)("Персонажи: " + admin + ": перенос закрыт"));
				return "перенос закрыт (ImportUntil пуст, записано в cfg)";
			case "until":
			{
				string text = string.Join(" ", c.Args.Skip(1));
				DateTime? dateTime;
				try
				{
					dateTime = ImportWindow.ParseUntil(text);
				}
				catch (FormatException)
				{
					return "дата «" + text + "» не разобрана; формат yyyy-MM-dd HH:mm, например 2027-01-31 23:59";
				}
				if (!dateTime.HasValue)
				{
					return "укажите дату: import until 2027-01-31 23:59";
				}
				PluginConfig.ImportUntil.Value = dateTime.Value.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture);
				Plugin.Log.LogInfo((object)("Персонажи: " + admin + ": перенос до " + PluginConfig.ImportUntil.Value));
				return ImportState() + " (записано в cfg)";
			}
			default:
				return "import status | import until <гггг-мм-дд чч:мм> | import close";
			}
		}
	}
	internal static class ClientSide
	{
		internal enum State
		{
			Idle,
			WaitingServer,
			Importing,
			Ready,
			Refused
		}

		private const float FirstSaveDelaySeconds = 5f;

		private static readonly FieldRef<PlayerProfile, IDictionary> WorldDataRef = AccessTools.FieldRefAccess<PlayerProfile, IDictionary>("m_worldData");

		private static readonly Reassembler Reassembler = new Reassembler();

		internal static State Current = State.Idle;

		private static float _waitingSince;

		private static long _sequence;

		private static bool _inventoryDirty;

		private static bool _skillsDirty;

		private static bool _changedSinceFullSave;

		private static bool _firstSaveDue;

		private static float _lastLive;

		private static float _lastFullSave;

		private static float _lastPush = -1E+09f;

		private static float _spawnedAt = -1f;

		private static bool _giveDefaultItems;

		private static bool _importNoticeDue;

		public static void Register(ZNetPeer serverPeer)
		{
			Reset();
			serverPeer.m_rpc.Register<ZPackage>("SansaraCharacters_Load", (Action<ZRpc, ZPackage>)OnLoad);
		}

		public static void SayHello(ZRpc rpc)
		{
			rpc.Invoke("SansaraCharacters_Hello", new object[1] { "0.2.1" });
		}

		public static void OnPeerInfo()
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Invalid comparison between Unknown and I4
			if ((int)ZNet.GetConnectionStatus() == 2 && Current == State.Idle)
			{
				Current = State.WaitingServer;
				_waitingSince = Time.realtimeSinceStartup;
				Plugin.Log.LogInfo((object)"connected, waiting for the server copy of the character");
			}
		}

		public static bool MaySpawn()
		{
			if ((Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer())
			{
				return true;
			}
			if (Current != State.WaitingServer && Current != State.Importing)
			{
				return true;
			}
			if (Time.realtimeSinceStartup - _waitingSince > PluginConfig.ServerCopyTimeoutSeconds.Value)
			{
				Plugin.Log.LogError((object)$"the server did not send the character in {PluginConfig.ServerCopyTimeoutSeconds.Value:0} s");
				Fail(7104);
			}
			return false;
		}

		public static bool MaySaveLocally(PlayerProfile profile)
		{
			if (Current == State.Refused && !((Object)(object)ZNet.instance == (Object)null) && !ZNet.instance.IsServer() && !((Object)(object)Game.instance == (Object)null))
			{
				return profile != Game.instance.GetPlayerProfile();
			}
			return true;
		}

		public static void OnSaved(PlayerProfile profile, bool saved)
		{
			//IL_003f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0045: Invalid comparison between Unknown and I4
			if (!saved || !IsReadyClient() || profile != Game.instance.GetPlayerProfile())
			{
				return;
			}
			ZNetPeer serverPeer = ZNet.instance.GetServerPeer();
			if (Transport.Connected(serverPeer))
			{
				byte[] array = ReadLocalProfile(profile);
				bool flag = Game.instance.IsShuttingDown() || (int)ZNet.GetConnectionStatus() != 2;
				if (flag)
				{
					Transport.SendBlocking(serverPeer, "SansaraCharacters_Save", ++_sequence, array);
				}
				else
				{
					Transport.SendAsync(serverPeer, "SansaraCharacters_Save", ++_sequence, array);
				}
				_lastPush = (_lastFullSave = Time.realtimeSinceStartup);
				_changedSinceFullSave = false;
				_firstSaveDue = false;
				Plugin.Log.LogInfo((object)($"full save sent ({array.Length} B)" + (flag ? ", blocking" : "")));
			}
		}

		public static void OnInventoryChanged(Inventory inventory)
		{
			if ((Object)(object)Player.m_localPlayer != (Object)null && inventory == ((Humanoid)Player.m_localPlayer).GetInventory() && IsReadyClient())
			{
				_inventoryDirty = (_changedSinceFullSave = true);
			}
		}

		public static void OnSkillsChanged(Skills skills)
		{
			if ((Object)(object)Player.m_localPlayer != (Object)null && (Object)(object)skills == (Object)(object)((Character)Player.m_localPlayer).GetSkills() && IsReadyClient())
			{
				_skillsDirty = (_changedSinceFullSave = true);
			}
		}

		public static void Tick()
		{
			//IL_0020: Unknown result type (might be due to invalid IL or missing references)
			//IL_0026: Invalid comparison between Unknown and I4
			if (!IsReadyClient() || (Object)(object)Player.m_localPlayer == (Object)null || Game.instance.IsShuttingDown() || (int)ZNet.GetConnectionStatus() != 2)
			{
				if ((Object)(object)Player.m_localPlayer == (Object)null)
				{
					_spawnedAt = -1f;
				}
				return;
			}
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			if (_spawnedAt < 0f)
			{
				_spawnedAt = realtimeSinceStartup;
			}
			if (_giveDefaultItems)
			{
				_giveDefaultItems = false;
				((Humanoid)Player.m_localPlayer).GiveDefaultItems();
			}
			if (_importNoticeDue && !Game.instance.InIntro(false))
			{
				_importNoticeDue = false;
				((Character)Player.m_localPlayer).Message((MessageType)2, "Персонаж перенесён в новый мир: вещи, рецепты, сила Форсакена и еда сброшены, навыки сохранены.", 0, (Sprite)null, false);
			}
			if (_firstSaveDue && realtimeSinceStartup - _spawnedAt >= 5f)
			{
				_firstSaveDue = false;
				_lastFullSave = realtimeSinceStartup;
				Plugin.Log.LogInfo((object)"new character: first save to the server");
				Game.instance.SavePlayerProfile(true, false);
				return;
			}
			int value = PluginConfig.FullSaveMinutes.Value;
			if (value > 0 && _changedSinceFullSave && realtimeSinceStartup - _lastFullSave >= (float)value * 60f)
			{
				_changedSinceFullSave = false;
				_lastFullSave = realtimeSinceStartup;
				Game.instance.SavePlayerProfile(true, false);
				return;
			}
			float num = realtimeSinceStartup - _lastLive;
			if ((_inventoryDirty && num >= PluginConfig.InventorySendSeconds.Value) || (_skillsDirty && num >= PluginConfig.SkillSendSeconds.Value) || num >= PluginConfig.IdleSendSeconds.Value)
			{
				SendLive(realtimeSinceStartup);
			}
		}

		public static void BeforeSocketClose(ISocket socket)
		{
			if (!((Object)(object)ZNet.instance == (Object)null) && !ZNet.instance.IsServer() && Current != State.Idle && !(Time.realtimeSinceStartup - _lastPush > 30f))
			{
				Stopwatch stopwatch = Stopwatch.StartNew();
				Transport.TryFlush(socket);
				while (socket.GetSendQueueSize() > 0 && stopwatch.ElapsedMilliseconds < 3000)
				{
					Thread.Sleep(20);
					Transport.TryFlush(socket);
				}
				Plugin.Log.LogInfo((object)$"socket close: waited {stopwatch.ElapsedMilliseconds} ms, {socket.GetSendQueueSize()} B left");
			}
		}

		private static void SendLive(float now)
		{
			//IL_0014: Unknown result type (might be due to invalid IL or missing references)
			//IL_001a: Expected O, but got Unknown
			ZNetPeer serverPeer = ZNet.instance.GetServerPeer();
			if (Transport.Connected(serverPeer))
			{
				ZPackage val = new ZPackage();
				Player.m_localPlayer.Save(val);
				Transport.SendAsync(serverPeer, "SansaraCharacters_Live", ++_sequence, val.GetArray());
				_lastLive = (_lastPush = now);
				_inventoryDirty = (_skillsDirty = false);
			}
		}

		private static void OnLoad(ZRpc rpc, ZPackage pkg)
		{
			try
			{
				if (Transport.Receive(Reassembler, "server", pkg, out var payload, out var sequence))
				{
					Apply(rpc, payload, (LoadMode)sequence);
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("server copy: " + ex));
				Fail(7103);
			}
		}

		private static void Apply(ZRpc rpc, byte[] data, LoadMode mode)
		{
			//IL_0131: Unknown result type (might be due to invalid IL or missing references)
			//IL_0138: Expected O, but got Unknown
			//IL_018a: Unknown result type (might be due to invalid IL or missing references)
			//IL_018f: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a8: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ca: Unknown result type (might be due to invalid IL or missing references)
			if (Current != State.WaitingServer && Current != State.Idle && Current != State.Importing)
			{
				Plugin.Log.LogWarning((object)$"server copy ignored in state {Current}");
				return;
			}
			Game instance = Game.instance;
			PlayerProfile val = ((instance != null) ? instance.GetPlayerProfile() : null);
			if (val == null)
			{
				Plugin.Log.LogWarning((object)"server copy arrived without a local profile");
				return;
			}
			if (mode == LoadMode.ImportFailed)
			{
				Plugin.Log.LogInfo((object)"the server could not import this character");
				Fail(7105);
				return;
			}
			int num = WorldDataRef.Invoke(val)?.Count ?? 0;
			switch (Admission.OnServerAnswer(data.Length != 0, mode, val.m_firstSpawn, num))
			{
			case JoinDecision.Import:
				SendImport(rpc, val);
				return;
			case JoinDecision.AcceptNew:
				BecomeReady();
				_firstSaveDue = true;
				Plugin.Log.LogInfo((object)"no server copy, new character accepted");
				return;
			case JoinDecision.RefuseHasProgress:
				Plugin.Log.LogInfo((object)"no server copy and the character has progress elsewhere: refused");
				Fail(7102);
				return;
			}
			string text = "sansara-probe_" + val.GetFilename();
			string path = Transport.CharacterFolder + text + ".fch";
			try
			{
				Directory.CreateDirectory(Transport.CharacterFolder);
				WriteProfile(path, data, (FileSource)2);
				PlayerProfile val2 = new PlayerProfile(text, (FileSource)2);
				if (!val2.Load() || !string.Equals(val2.GetName(), val.GetName(), StringComparison.OrdinalIgnoreCase))
				{
					Plugin.Log.LogError((object)$"server copy refused by the game's loader ({data.Length} B); local file untouched");
					Fail(7103);
					return;
				}
				string path2 = val.GetPath();
				FileSource fileSource = val.m_fileSource;
				if (num > 0)
				{
					KeepOriginal(val);
				}
				WriteProfile(path2 + ".new", data, fileSource);
				FileHelpers.ReplaceOldFile(path2, path2 + ".new", path2 + ".old", (CloudStorageFileGrouping)1, fileSource);
				if (!val.Load())
				{
					Plugin.Log.LogError((object)"server copy written but the profile did not load");
					Fail(7103);
					return;
				}
				BecomeReady();
				if (mode == LoadMode.Imported)
				{
					_giveDefaultItems = (_importNoticeDue = true);
					Traverse.Create((object)Game.instance).Field("m_queuedIntro").SetValue((object)val.m_firstSpawn);
				}
				Plugin.Log.LogInfo((object)($"server copy loaded ({data.Length} B) for '{val.GetName()}'" + ((mode == LoadMode.Imported) ? ", imported" : "")));
			}
			finally
			{
				try
				{
					if (File.Exists(path))
					{
						File.Delete(path);
					}
				}
				catch (Exception)
				{
				}
			}
		}

		private static void SendImport(ZRpc rpc, PlayerProfile profile)
		{
			ZNetPeer peer = ((IEnumerable<ZNetPeer>)ZNet.instance.GetPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer p) => p != null && p.m_rpc == rpc));
			if (!Transport.Connected(peer))
			{
				Plugin.Log.LogError((object)"import offered, but the server connection is gone");
				Fail(7105);
				return;
			}
			byte[] array = ReadLocalProfile(profile);
			Current = State.Importing;
			_waitingSince = Time.realtimeSinceStartup;
			Transport.SendAsync(peer, "SansaraCharacters_Import", ++_sequence, array);
			Plugin.Log.LogInfo((object)$"character with progress sent for import ({array.Length} B)");
		}

		private static void KeepOriginal(PlayerProfile profile)
		{
			//IL_0027: Unknown result type (might be due to invalid IL or missing references)
			//IL_002d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0033: Expected O, but got Unknown
			string text = Transport.CharacterFolder + profile.GetFilename() + ".fch.beforesansara";
			if (File.Exists(text))
			{
				return;
			}
			try
			{
				FileReader val = new FileReader(profile.GetPath(), profile.m_fileSource, (FileHelperType)0);
				byte[] bytes;
				try
				{
					bytes = val.m_binary.ReadBytes((int)val.m_binary.BaseStream.Length);
				}
				finally
				{
					val.Dispose();
				}
				Directory.CreateDirectory(Transport.CharacterFolder);
				File.WriteAllBytes(text, bytes);
				Plugin.Log.LogInfo((object)("kept the original local file as " + text));
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("could not keep the original local file: " + ex.Message));
			}
		}

		private static void BecomeReady()
		{
			Current = State.Ready;
			_lastLive = (_lastFullSave = Time.realtimeSinceStartup);
		}

		private static void Fail(int code)
		{
			Current = State.Refused;
			SetStatus(code);
			try
			{
				Game instance = Game.instance;
				if (instance != null)
				{
					instance.Logout(false, true);
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("logout: " + ex.Message));
			}
			SetStatus(code);
		}

		private static void SetStatus(int code)
		{
			Traverse.Create(typeof(ZNet)).Field("m_connectionStatus").SetValue((object)(ConnectionStatus)code);
		}

		private static bool IsReadyClient()
		{
			if (Current == State.Ready && (Object)(object)ZNet.instance != (Object)null && !ZNet.instance.IsServer())
			{
				return (Object)(object)Game.instance != (Object)null;
			}
			return false;
		}

		private static void Reset()
		{
			Current = State.Idle;
			_sequence = 0L;
			_inventoryDirty = (_skillsDirty = (_changedSinceFullSave = (_firstSaveDue = false)));
			_giveDefaultItems = (_importNoticeDue = false);
			_spawnedAt = -1f;
		}

		private static byte[] ReadLocalProfile(PlayerProfile profile)
		{
			//IL_0007: Unknown result type (might be due to invalid IL or missing references)
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0013: Expected O, but got Unknown
			FileReader val = new FileReader(profile.GetPath(), profile.m_fileSource, (FileHelperType)0);
			try
			{
				int num = val.m_binary.ReadInt32();
				if (num <= 0 || num > 67108864)
				{
					throw new IOException("implausible profile length " + num);
				}
				return val.m_binary.ReadBytes(num);
			}
			finally
			{
				val.Dispose();
			}
		}

		private static void WriteProfile(string path, byte[] data, FileSource source)
		{
			//IL_000a: 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_0011: Expected O, but got Unknown
			//IL_0024: Unknown result type (might be due to invalid IL or missing references)
			//IL_002a: Invalid comparison between Unknown and I4
			//IL_0032: Unknown result type (might be due to invalid IL or missing references)
			byte[] buffer = ProfileFile.Frame(data);
			FileWriter val = new FileWriter(path, (CloudStorageFileGrouping)1, (FileHelperType)0, source);
			val.m_binary.Write(buffer);
			val.Finish();
			if ((int)val.Status != 2)
			{
				throw new IOException($"FileWriter status {val.Status} for {path}");
			}
		}
	}
	internal static class ItemNames
	{
		private static readonly Dictionary<string, string> Russian = new Dictionary<string, string>();

		public static void Load()
		{
			try
			{
				foreach (TextAsset item in Assets())
				{
					ReadColumn(item, "Russian", Russian);
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Персонажи: русская локализация не прочитана: " + ex.Message));
			}
			Plugin.Log.LogInfo((object)$"Персонажи: строк русской локализации {Russian.Count}.");
		}

		public static string Of(string prefab)
		{
			GameObject itemPrefab = ObjectDB.instance.GetItemPrefab(prefab);
			string text = ((itemPrefab == null) ? null : itemPrefab.GetComponent<ItemDrop>()?.m_itemData.m_shared.m_name) ?? prefab;
			string key = text.TrimStart(new char[1] { '$' });
			if (Russian.TryGetValue(key, out var value) && value != "")
			{
				return value;
			}
			if (Localization.instance == null)
			{
				return prefab;
			}
			return Localization.instance.Localize(text);
		}

		public static string Token(string prefab)
		{
			GameObject itemPrefab = ObjectDB.instance.GetItemPrefab(prefab);
			return ((itemPrefab == null) ? null : itemPrefab.GetComponent<ItemDrop>()?.m_itemData.m_shared.m_name) ?? "";
		}

		private static IEnumerable<TextAsset> Assets()
		{
			_ = Localization.instance;
			object obj = AccessTools.Field(typeof(Localization), "m_localizationSettings")?.GetValue(null);
			if (obj == null)
			{
				yield break;
			}
			Traverse val = Traverse.Create(obj);
			if (!((val.Field("Localizations").GetValue() ?? val.Property("Localizations", (object[])null).GetValue()) is IEnumerable enumerable))
			{
				yield break;
			}
			foreach (object item in enumerable)
			{
				TextAsset val2 = (TextAsset)((item is TextAsset) ? item : null);
				if (val2 != null && (Object)(object)val2 != (Object)null)
				{
					yield return val2;
				}
			}
		}

		private static void ReadColumn(TextAsset asset, string language, Dictionary<string, string> into)
		{
			List<List<string>> list = SplitCsv(asset.text);
			if (list.Count == 0)
			{
				return;
			}
			int num = list[0].FindIndex((string h) => StripQuotes(h).Trim() == language);
			if (num < 0)
			{
				return;
			}
			foreach (List<string> item in list.Skip(1))
			{
				if (item.Count != 0 && item[0].Length != 0 && !item[0].StartsWith("//"))
				{
					string text = ((item.Count > num) ? item[num].Trim() : "");
					if (text.Length == 0 && item.Count > 1)
					{
						text = item[1].Trim();
					}
					into[item[0]] = text;
				}
			}
		}

		private static string StripQuotes(string s)
		{
			if (s.Length < 2 || s[0] != '"' || s[s.Length - 1] != '"')
			{
				return s;
			}
			return s.Substring(1, s.Length - 2);
		}

		private static List<List<string>> SplitCsv(string text)
		{
			List<List<string>> list = new List<List<string>>();
			List<string> list2 = new List<string>();
			StringBuilder stringBuilder = new StringBuilder();
			bool flag = false;
			for (int i = 0; i < text.Length; i++)
			{
				char c = text[i];
				switch (c)
				{
				case '"':
					if (flag && i + 1 < text.Length && text[i + 1] == '"')
					{
						stringBuilder.Append('"');
						i++;
					}
					else
					{
						flag = !flag;
					}
					continue;
				case ',':
					if (!flag)
					{
						list2.Add(stringBuilder.ToString());
						stringBuilder.Length = 0;
						continue;
					}
					break;
				}
				if (c == '\n' && !flag)
				{
					list2.Add(stringBuilder.ToString().TrimEnd(new char[1] { '\r' }));
					stringBuilder.Length = 0;
					list.Add(list2);
					list2 = new List<string>();
				}
				else
				{
					stringBuilder.Append(c);
				}
			}
			if (stringBuilder.Length > 0 || list2.Count > 0)
			{
				list2.Add(stringBuilder.ToString());
				list.Add(list2);
			}
			return list;
		}
	}
	[HarmonyPatch(typeof(ZNet), "OnNewConnection")]
	internal static class NewConnectionPatch
	{
		private static void Postfix(ZNet __instance, ZNetPeer peer)
		{
			try
			{
				if (peer?.m_rpc == null)
				{
					return;
				}
				if (__instance.IsServer())
				{
					if (ServerSide.Active)
					{
						ServerSide.Register(peer);
					}
				}
				else
				{
					ClientSide.Register(peer);
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("OnNewConnection: " + ex));
			}
		}
	}
	[HarmonyPatch(typeof(ZNet), "SendPeerInfo")]
	internal static class SendPeerInfoPatch
	{
		private static void Prefix(ZNet __instance, ZRpc rpc)
		{
			try
			{
				if (!__instance.IsServer())
				{
					ClientSide.SayHello(rpc);
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("SendPeerInfo: " + ex));
			}
		}
	}
	[HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")]
	internal static class PeerInfoPatch
	{
		private static bool Prefix(ZNet __instance, ZRpc rpc)
		{
			try
			{
				return !__instance.IsServer() || !ServerSide.Active || ServerSide.MayProceed(rpc);
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("RPC_PeerInfo check: " + ex));
				return false;
			}
		}

		private static void Postfix(ZNet __instance, ZRpc rpc)
		{
			try
			{
				if (__instance.IsServer())
				{
					if (ServerSide.Active)
					{
						ServerSide.OnPeerInfo(__instance, rpc);
					}
				}
				else
				{
					ClientSide.OnPeerInfo();
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("RPC_PeerInfo: " + ex));
			}
		}
	}
	[HarmonyPatch(typeof(ZNet), "Disconnect")]
	internal static class DisconnectPatch
	{
		private static void Prefix(ZNet __instance, ZNetPeer peer)
		{
			try
			{
				if (ServerSide.Active)
				{
					ServerSide.OnDisconnect(peer);
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("Disconnect: " + ex));
			}
		}
	}
	[HarmonyPatch(typeof(ZNet), "SaveWorld")]
	internal static class SaveWorldPatch
	{
		private static void Prefix()
		{
			try
			{
				if (ServerSide.Active)
				{
					ServerSide.OnWorldSave();
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("SaveWorld: " + ex));
			}
		}
	}
	[HarmonyPatch(typeof(Game), "UpdateRespawn")]
	internal static class UpdateRespawnPatch
	{
		private static bool Prefix()
		{
			try
			{
				return ClientSide.MaySpawn();
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("UpdateRespawn: " + ex));
				return true;
			}
		}
	}
	[HarmonyPatch(typeof(PlayerProfile), "Save")]
	internal static class ProfileSavePatch
	{
		private static bool Prefix(PlayerProfile __instance, ref bool __result)
		{
			try
			{
				if (ClientSide.MaySaveLocally(__instance))
				{
					return true;
				}
				Plugin.Log.LogInfo((object)"save skipped: the server refused this character, the local file stays as it was");
				__result = false;
				return false;
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("Save prefix: " + ex));
				return true;
			}
		}

		private static void Postfix(PlayerProfile __instance, bool __result)
		{
			try
			{
				ClientSide.OnSaved(__instance, __result);
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("Save postfix: " + ex));
			}
		}
	}
	[HarmonyPatch(typeof(Inventory), "Changed")]
	internal static class InventoryChangedPatch
	{
		private static void Postfix(Inventory __instance)
		{
			try
			{
				ClientSide.OnInventoryChanged(__instance);
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Inventory.Changed: " + ex.Message));
			}
		}
	}
	[HarmonyPatch]
	internal static class SkillsChangedPatch
	{
		private static IEnumerable<MethodBase> TargetMethods()
		{
			yield return AccessTools.Method(typeof(Skills), "RaiseSkill", (Type[])null, (Type[])null);
			yield return AccessTools.Method(typeof(Skills), "LowerAllSkills", (Type[])null, (Type[])null);
			yield return AccessTools.Method(typeof(Skills), "ResetSkill", (Type[])null, (Type[])null);
		}

		private static void Postfix(Skills __instance)
		{
			try
			{
				ClientSide.OnSkillsChanged(__instance);
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Skills: " + ex.Message));
			}
		}
	}
	[HarmonyPatch(typeof(ZSteamSocket), "Close")]
	internal static class SteamSocketClosePatch
	{
		private static void Prefix(ZSteamSocket __instance)
		{
			try
			{
				ClientSide.BeforeSocketClose((ISocket)(object)__instance);
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("socket close: " + ex.Message));
			}
		}
	}
	[HarmonyPatch(typeof(FejdStartup), "ShowConnectError")]
	internal static class ConnectErrorPatch
	{
		private static void Postfix(FejdStartup __instance)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_000a: Expected I4, but got Unknown
			try
			{
				string text = Texts.ForCode((int)ZNet.GetConnectionStatus());
				if (text != null && !((Object)(object)__instance.m_connectionFailedError == (Object)null))
				{
					__instance.m_connectionFailedError.text = text;
					if ((Object)(object)__instance.m_connectionFailedPanel != (Object)null)
					{
						__instance.m_connectionFailedPanel.SetActive(true);
					}
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("connect error text: " + ex.Message));
			}
		}
	}
	[BepInPlugin("sansara.Characters", "Sansara Characters", "0.2.1")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	[BepInIncompatibility("org.bepinex.plugins.servercharacters")]
	[BepInIncompatibility("reef.characters")]
	[BepInIncompatibility("Landoria.CharacterVault")]
	[BepInIncompatibility("chazman.RunicCharacterVault")]
	[NetworkCompatibility(/*Could not decode attribute arguments.*/)]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string Guid = "sansara.Characters";

		public const string Version = "0.2.1";

		internal static ManualLogSource Log;

		internal static Plugin Instance;

		private void Awake()
		{
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			Instance = this;
			Log = ((BaseUnityPlugin)this).Logger;
			PluginConfig.Bind(((BaseUnityPlugin)this).Config);
			new Harmony("sansara.Characters").PatchAll(typeof(Plugin).Assembly);
			AdminChannel.Register("chars", CharsAdmin.Verbs, CharsAdmin.Handle, Log);
			Log.LogInfo((object)("Sansara Characters 0.2.1 loaded: " + PluginConfig.Describe()));
		}

		private void Update()
		{
			try
			{
				ClientSide.Tick();
				if (ServerSide.Active)
				{
					AdminChannel.Tick();
					AdminChannel.CheckFile("sansara.Characters", "Персонажи", CharsAdmin.Handle);
				}
			}
			catch (Exception ex)
			{
				Log.LogError((object)("client tick: " + ex));
			}
		}
	}
	internal static class PluginConfig
	{
		public static ConfigEntry<int> BackupsToKeep;

		public static ConfigEntry<int> BackupIntervalMinutes;

		public static ConfigEntry<bool> OneCharacterPerAccount;

		public static ConfigEntry<float> ServerCopyTimeoutSeconds;

		public static ConfigEntry<float> InventorySendSeconds;

		public static ConfigEntry<float> SkillSendSeconds;

		public static ConfigEntry<float> IdleSendSeconds;

		public static ConfigEntry<int> FullSaveMinutes;

		public static ConfigEntry<string> ImportUntil;

		public static void Bind(ConfigFile cfg)
		{
			BackupsToKeep = cfg.Bind<int>("Server", "BackupsToKeep", 25, Description("Сколько датированных копий хранить на персонажа в characters_local/backups/<файл>/. Копия снимается перед перезаписью файла сервером, лишние старые удаляются.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 200)));
			BackupIntervalMinutes = cfg.Bind<int>("Server", "BackupIntervalMinutes", 30, Description("Не чаще одной копии на персонажа за столько минут; перезаписи между ними копий не создают. 0 — копия перед каждой перезаписью.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 1440)));
			OneCharacterPerAccount = cfg.Bind<bool>("Server", "OneCharacterPerAccount", false, Description("true — аккаунт Steam, у которого уже есть персонаж на сервере, не может зайти другим (администраторы из adminlist.txt — могут)."));
			ImportUntil = cfg.Bind<string>("Import", "ImportUntil", "", Description("До какого момента (время сервера, формат yyyy-MM-dd HH:mm) сервер принимает персонажей, уже игравших в других мирах: при первом входе такой персонаж очищается — вещи, рецепты, станции, материалы, сила Форсакена и еда сбрасываются, навыки, внешность и трофеи остаются. Пусто — перенос закрыт, принимаются только новые персонажи."));
			ServerCopyTimeoutSeconds = cfg.Bind<float>("Client", "ServerCopyTimeoutSeconds", 30f, Description("Сколько секунд клиент ждёт копию персонажа от сервера, прежде чем отключиться с ошибкой.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 120f)));
			InventorySendSeconds = cfg.Bind<float>("Client", "InventorySendSeconds", 0.25f, Description("Не чаще чем раз в столько секунд клиент отправляет серверу состояние игрока после изменения инвентаря. Чем меньше, тем уже окно для дюпа через вылет.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 5f)));
			SkillSendSeconds = cfg.Bind<float>("Client", "SkillSendSeconds", 5f, Description("То же после роста навыков (навыки растут при каждом ударе, поэтому реже).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 60f)));
			IdleSendSeconds = cfg.Bind<float>("Client", "IdleSendSeconds", 30f, Description("Раз в столько секунд состояние отправляется и без изменений (место игрока, еда).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 600f)));
			FullSaveMinutes = cfg.Bind<int>("Client", "FullSaveMinutes", 10, Description("Раз в столько минут клиент сохраняет персонажа целиком (карта, точка выхода), если что-то менялось. Игра сама сохраняет раз в 30 минут и при выходе. 0 — только сохранения игры.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 60)));
		}

		public static string Describe()
		{
			return $"копий {BackupsToKeep.Value} (не чаще раза в {BackupIntervalMinutes.Value} мин), один персонаж на аккаунт: " + string.Format("{0}, инвентарь каждые {1} с, ", OneCharacterPerAccount.Value ? "да" : "нет", InventorySendSeconds.Value) + $"полное сохранение раз в {FullSaveMinutes.Value} мин, перенос: " + (string.IsNullOrWhiteSpace(ImportUntil.Value) ? "закрыт" : ("до " + ImportUntil.Value));
		}

		private static ConfigDescription Description(string text, AcceptableValueBase range = null)
		{
			//IL_000a: Unknown result type (might be due to invalid IL or missing references)
			//IL_000f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0017: Expected O, but got Unknown
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Expected O, but got Unknown
			return new ConfigDescription(text, range, new object[1] { (object)new ConfigurationManagerAttributes
			{
				IsAdminOnly = true
			} });
		}
	}
	internal static class ServerSide
	{
		private sealed class Session
		{
			public ZNetPeer Peer;

			public string FileName;

			public readonly LiveLedger Ledger = new LiveLedger();

			public byte[] StoredPlayerData;

			public bool StoredKnown;

			public bool ImportOffered;
		}

		private const string IncomingPrefix = "sansara-incoming_";

		internal static readonly FieldRef<PlayerProfile, byte[]> PlayerDataRef = AccessTools.FieldRefAccess<PlayerProfile, byte[]>("m_playerData");

		private static readonly FieldRef<PlayerProfile, bool> CreateBackupRef = AccessTools.FieldRefAccess<PlayerProfile, bool>("m_createBackupBeforeSaving");

		private static readonly Dictionary<ZRpc, Session> Sessions = new Dictionary<ZRpc, Session>();

		private static readonly HashSet<ZRpc> Greeted = new HashSet<ZRpc>();

		private static readonly Reassembler Reassembler = new Reassembler();

		internal static readonly object FileLock = new object();

		public static bool Active
		{
			get
			{
				if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer())
				{
					return ZNet.instance.IsDedicated();
				}
				return false;
			}
		}

		public static void Register(ZNetPeer peer)
		{
			peer.m_rpc.Register<ZPackage>("SansaraCharacters_Save", (Action<ZRpc, ZPackage>)OnSave);
			peer.m_rpc.Register<ZPackage>("SansaraCharacters_Live", (Action<ZRpc, ZPackage>)OnLive);
			peer.m_rpc.Register<string>("SansaraCharacters_Hello", (Action<ZRpc, string>)OnHello);
			peer.m_rpc.Register<ZPackage>("SansaraCharacters_Import", (Action<ZRpc, ZPackage>)OnImport);
		}

		public static bool MayProceed(ZRpc rpc)
		{
			if (Greeted.Contains(rpc))
			{
				return true;
			}
			ISocket socket = rpc.GetSocket();
			string text = ((socket != null) ? socket.GetHostName() : null) ?? "?";
			Plugin.Log.LogWarning((object)("refused " + text + ": no Sansara Characters 0.2.1 on the client"));
			rpc.Invoke("Error", new object[1] { 3 });
			return false;
		}

		private static void OnHello(ZRpc rpc, string version)
		{
			if (VersionsMatch(version, "0.2.1"))
			{
				Greeted.Add(rpc);
				return;
			}
			ManualLogSource log = Plugin.Log;
			ISocket socket = rpc.GetSocket();
			log.LogWarning((object)(((socket != null) ? socket.GetHostName() : null) + ": client has Sansara Characters " + version + ", server 0.2.1"));
		}

		private static bool VersionsMatch(string a, string b)
		{
			string[] array = (a ?? "").Split(new char[1] { '.' });
			string[] array2 = (b ?? "").Split(new char[1] { '.' });
			if (array.Length >= 2 && array2.Length >= 2 && array[0] == array2[0])
			{
				return array[1] == array2[1];
			}
			return false;
		}

		public static void OnPeerInfo(ZNet net, ZRpc rpc)
		{
			ZNetPeer val = ((IEnumerable<ZNetPeer>)net.GetPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer p) => p != null && p.m_rpc == rpc));
			if (val == null || val.m_uid == 0L || string.IsNullOrEmpty(val.m_playerName))
			{
				return;
			}
			string hostName = val.m_socket.GetHostName();
			string text = CharacterFileName.For(hostName, val.m_playerName);
			string path = PathOf(text);
			try
			{
				Directory.CreateDirectory(Transport.CharacterFolder);
				bool flag = File.Exists(path);
				IEnumerable<string> existingFileNames = Directory.GetFiles(Transport.CharacterFolder, "*.fch").Select(Path.GetFileNameWithoutExtension);
				if (Admission.IsSecondCharacter(PluginConfig.OneCharacterPerAccount.Value, net.IsAdmin(hostName), flag, CharacterFileName.Account(hostName), existingFileNames))
				{
					Plugin.Log.LogInfo((object)("refused " + text + ": the account already has a character"));
					rpc.Invoke("Error", new object[1] { 7101 });
					net.Disconnect(val);
					return;
				}
				Session session = new Session
				{
					Peer = val,
					FileName = text
				};
				Sessions[rpc] = session;
				if (!flag)
				{
					session.ImportOffered = ImportOpen();
					LoadMode loadMode = (session.ImportOffered ? LoadMode.ImportAllowed : LoadMode.Normal);
					Transport.SendAsync(val, "SansaraCharacters_Load", (long)loadMode, null);
					Plugin.Log.LogInfo((object)(text + ": no server copy, a new character may join" + (session.ImportOffered ? ", import open" : "")));
				}
				else
				{
					byte[] array = ProfileFile.Unframe(File.ReadAllBytes(path));
					Transport.SendAsync(val, "SansaraCharacters_Load", 0L, array);
					Plugin.Log.LogInfo((object)$"{text}: sent the server copy ({array.Length} B)");
				}
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"join of {text}: {arg}");
			}
		}

		public static void OnDisconnect(ZNetPeer peer)
		{
			if (peer?.m_rpc != null)
			{
				Greeted.Remove(peer.m_rpc);
				if (Sessions.TryGetValue(peer.m_rpc, out var value))
				{
					Flush(value, "disconnect");
					Sessions.Remove(peer.m_rpc);
					Reassembler.Forget(SenderOf(peer.m_rpc));
				}
			}
		}

		public static void OnWorldSave()
		{
			foreach (Session item in Sessions.Values.ToList())
			{
				Flush(item, "world save");
			}
		}

		private static void OnSave(ZRpc rpc, ZPackage pkg)
		{
			try
			{
				if (Sessions.TryGetValue(rpc, out var value) && Transport.Receive(Reassembler, SenderOf(rpc), pkg, out var payload, out var sequence))
				{
					StoreFullSave(value, payload, sequence);
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("full save: " + ex));
			}
		}

		private static void OnImport(ZRpc rpc, ZPackage pkg)
		{
			if (!Sessions.TryGetValue(rpc, out var value))
			{
				return;
			}
			try
			{
				if (Transport.Receive(Reassembler, SenderOf(rpc), pkg, out var payload, out var sequence))
				{
					if (!value.ImportOffered)
					{
						Plugin.Log.LogWarning((object)(value.FileName + ": import not offered on this connection, ignored"));
						return;
					}
					value.ImportOffered = false;
					byte[] payload2 = Import(value, payload, sequence);
					Transport.SendAsync(value.Peer, "SansaraCharacters_Load", 2L, payload2);
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)(value.FileName + ": import failed: " + ex.Message));
				Transport.SendAsync(value.Peer, "SansaraCharacters_Load", 3L, null);
			}
		}

		private static byte[] Import(Session session, byte[] data, long sequence)
		{
			//IL_0066: Unknown result type (might be due to invalid IL or missing references)
			//IL_006d: Expected O, but got Unknown
			if (data.Length == 0)
			{
				throw new InvalidDataException("empty profile");
			}
			lock (FileLock)
			{
				string text = PathOf(session.FileName);
				if (File.Exists(text))
				{
					throw new InvalidOperationException("the server already has a copy");
				}
				string text2 = "sansara-incoming_" + session.FileName;
				string text3 = PathOf(text2);
				try
				{
					File.WriteAllBytes(text3, ProfileFile.Frame(data));
					PlayerProfile val = new PlayerProfile(text2, (FileSource)2);
					if (!val.Load())
					{
						throw new InvalidDataException("refused by the game's loader");
					}
					string text4 = (val.GetName() ?? "").ToLowerInvariant();
					if (text4 != session.Peer.m_playerName.ToLowerInvariant())
					{
						throw new InvalidDataException("profile of another character '" + text4 + "'");
					}
					byte[] array = PlayerDataRef.Invoke(val);
					byte[] array2 = PlayerDataStrip.Strip(array);
					PlayerDataRef.Invoke(val) = array2;
					val.m_firstSpawn = true;
					Traverse.Create((object)val).Field("m_worldData").GetValue<IDictionary>()
						.Clear();
					CreateBackupRef.Invoke(val) = false;
					if (!val.Save())
					{
						throw new IOException("the game's writer failed");
					}
					byte[] result = ProfileFile.Unframe(File.ReadAllBytes(text3));
					Transport.MoveOver(text3, text);
					session.Ledger.OnFullSave(sequence);
					session.StoredPlayerData = array2;
					session.StoredKnown = true;
					Plugin.Log.LogInfo((object)($"{session.FileName}: imported, player data {array.Length} B -> {array2.Length} B " + "(items, recipes, power, food reset; skills kept)"));
					return result;
				}
				finally
				{
					TryDelete(text3);
					TryDelete(text3 + ".old");
					string[] files = Directory.GetFiles(Transport.CharacterFolder, text2 + "_backup*");
					for (int i = 0; i < files.Length; i++)
					{
						TryDelete(files[i]);
					}
				}
			}
		}

		private static bool ImportOpen()
		{
			try
			{
				return ImportWindow.IsOpen(ImportWindow.ParseUntil(PluginConfig.ImportUntil.Value), DateTime.Now);
			}
			catch (FormatException ex)
			{
				Plugin.Log.LogError((object)(ex.Message + "; import stays closed"));
				return false;
			}
		}

		private static void OnLive(ZRpc rpc, ZPackage pkg)
		{
			try
			{
				if (Sessions.TryGetValue(rpc, out var value) && Transport.Receive(Reassembler, SenderOf(rpc), pkg, out var payload, out var sequence))
				{
					byte[] array = payload;
					if (!ProfileFile.IsPlausiblePlayerData(value.StoredPlayerData, array))
					{
						Plugin.Log.LogWarning((object)$"{value.FileName}: implausible live state ({array.Length} B) ignored");
					}
					else
					{
						value.Ledger.OnLive(new LiveSnapshot(sequence, array));
					}
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("live state: " + ex));
			}
		}

		private static void StoreFullSave(Session session, byte[] data, long sequence)
		{
			//IL_003b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0042: Expected O, but got Unknown
			if (data.Length == 0)
			{
				return;
			}
			lock (FileLock)
			{
				string text = "sansara-incoming_" + session.FileName;
				string text2 = PathOf(text);
				try
				{
					File.WriteAllBytes(text2, ProfileFile.Frame(data));
					PlayerProfile val = new PlayerProfile(text, (FileSource)2);
					if (!val.Load())
					{
						Plugin.Log.LogWarning((object)$"{session.FileName}: full save refused by the game's loader ({data.Length} B)");
						return;
					}
					string text3 = (val.GetName() ?? "").ToLowerInvariant();
					if (text3 != session.Peer.m_playerName.ToLowerInvariant())
					{
						Plugin.Log.LogWarning((object)(session.FileName + ": full save of another character '" + text3 + "' refused"));
					}
					else if (session.Ledger.OnFullSave(sequence))
					{
						string text4 = PathOf(session.FileName);
						Backup(session.FileName, text4);
						Transport.MoveOver(text2, text4);
						session.StoredPlayerData = PlayerDataRef.Invoke(val);
						session.StoredKnown = true;
						Plugin.Log.LogInfo((object)$"{session.FileName}: full save stored ({data.Length} B)");
					}
				}
				finally
				{
					TryDelete(text2);
				}
			}
		}

		private static void Flush(Session session, string reason)
		{
			//IL_0074: Unknown result type (might be due to invalid IL or missing references)
			//IL_007b: Expected O, but got Unknown
			LiveSnapshot liveSnapshot = session.Ledger.TakePending();
			if (liveSnapshot == null)
			{
				return;
			}
			lock (FileLock)
			{
				string path = PathOf(session.FileName);
				try
				{
					if (!File.Exists(path))
					{
						Plugin.Log.LogInfo((object)(session.FileName + ": no server file yet, live state dropped (" + reason + ")"));
						session.Ledger.Drop(liveSnapshot);
						return;
					}
					if (!session.StoredKnown)
					{
						PlayerProfile val = new PlayerProfile(session.FileName, (FileSource)2);
						if (!val.Load())
						{
							throw new InvalidDataException("server file refused by the game's loader");
						}
						session.StoredPlayerData = PlayerDataRef.Invoke(val);
						session.StoredKnown = true;
					}
					byte[] data = ProfileFile.ReplacePlayerData(ProfileFile.Unframe(File.ReadAllBytes(path)), session.StoredPlayerData, liveSnapshot.PlayerData);
					Backup(session.FileName, path);
					Transport.WriteFileAtomic(path, ProfileFile.Frame(data));
					session.StoredPlayerData = liveSnapshot.PlayerData;
					session.Ledger.MarkApplied(liveSnapshot);
					Plugin.Log.LogInfo((object)$"{session.FileName}: live state written ({liveSnapshot.PlayerData.Length} B, {reason})");
				}
				catch (Exception ex)
				{
					session.StoredKnown = false;
					session.Ledger.Drop(liveSnapshot);
					Plugin.Log.LogError((object)(session.FileName + ": live state not written (" + reason + "): " + ex.Message));
				}
			}
		}

		internal static string BackupFolder(string fileName)
		{
			return Path.Combine(Path.Combine(Transport.CharacterFolder, "backups"), fileName);
		}

		internal static string Backup(string fileName, string path, bool force = false)
		{
			if (!File.Exists(path))
			{
				return null;
			}
			try
			{
				string text = BackupFolder(fileName);
				Directory.CreateDirectory(text);
				List<string> list = Directory.GetFiles(text, "*.fch").Select(Path.GetFileNameWithoutExtension).ToList();
				if (!force && !BackupPlan.IsDue(list, DateTime.Now, PluginConfig.BackupIntervalMinutes.Value))
				{
					return null;
				}
				string text2 = BackupPlan.NameFor(DateTime.Now, list);
				File.Copy(path, Path.Combine(text, text2 + ".fch"));
				list.Add(text2);
				foreach (string item in BackupPlan.ToDelete(list, PluginConfig.BackupsToKeep.Value))
				{
					TryDelete(Path.Combine(text, item + ".fch"));
				}
				return text2;
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)(fileName + ": backup failed: " + ex.Message));
				if (force)
				{
					throw;
				}
				return null;
			}
		}

		internal static ZNetPeer OnlinePeer(string fileName)
		{
			return Sessions.Values.FirstOrDefault((Session x) => x.FileName == fileName && x.Peer?.m_socket != null && x.Peer.m_socket.IsConnected())?.Peer;
		}

		internal static void Kick(ZNetPeer peer)
		{
			try
			{
				peer.m_rpc.Invoke("Error", new object[1] { 4 });
			}
			catch (Exception)
			{
			}
			ZNet.instance.Disconnect(peer);
		}

		internal static void RewriteWithGame(string fileName, Action<PlayerProfile> change)
		{
			//IL_0024: Unknown result type (might be due to invalid IL or missing references)
			//IL_002a: Expected O, but got Unknown
			string text = PathOf(fileName);
			string text2 = "sansara-incoming_" + fileName;
			string text3 = PathOf(text2);
			try
			{
				File.Copy(text, text3, overwrite: true);
				PlayerProfile val = new PlayerProfile(text2, (FileSource)2);
				if (!val.Load())
				{
					throw new InvalidDataException("refused by the game's loader");
				}
				change(val);
				CreateBackupRef.Invoke(val) = false;
				if (!val.Save())
				{
					throw new IOException("the game's writer failed");
				}
				ProfileFile.Unframe(File.ReadAllBytes(text3));
				Transport.MoveOver(text3, text);
			}
			finally
			{
				TryDelete(text3);
				TryDelete(text3 + ".old");
				string[] files = Directory.GetFiles(Transport.CharacterFolder, text2 + "_backup*");
				for (int i = 0; i < files.Length; i++)
				{
					TryDelete(files[i]);
				}
			}
		}

		internal static void RewritePlayerData(string fileName, Func<byte[], byte[]> change)
		{
			//IL_0008: Unknown result type (might be due to invalid IL or missing references)
			//IL_000e: Expected O, but got Unknown
			string path = PathOf(fileName);
			PlayerProfile val = new PlayerProfile(fileName, (FileSource)2);
			if (!val.Load())
			{
				throw new InvalidDataException("server file refused by the game's loader");
			}
			byte[] array = PlayerDataRef.Invoke(val);
			if (array == null)
			{
				throw new InvalidDataException("the character has no player data yet");
			}
			byte[] profileData = ProfileFile.Unframe(File.ReadAllBytes(path));
			Transport.WriteFileAtomic(path, ProfileFile.Frame(ProfileFile.ReplacePlayerData(profileData, array, change(array))));
		}

		internal static string PathOf(string fileName)
		{
			return Transport.CharacterFolder + fileName + ".fch";
		}

		internal static bool IsTemporary(string fileName)
		{
			return fileName.StartsWith("sansara-incoming_", StringComparison.Ordinal);
		}

		private static string SenderOf(ZRpc rpc)
		{
			return ((object)rpc).GetHashCode().ToString();
		}

		internal static void TryDelete(string path)
		{
			try
			{
				if (File.Exists(path))
				{
					File.Delete(path);
				}
			}
			catch (Exception)
			{
			}
		}
	}
	internal static class Texts
	{
		public const int ErrSecondCharacter = 7101;

		public const int ErrHasProgress = 7102;

		public const int ErrServerCopyBroken = 7103;

		public const int ErrTimeout = 7104;

		public const int ErrImportFailed = 7105;

		public const string Imported = "Персонаж перенесён в новый мир: вещи, рецепты, сила Форсакена и еда сброшены, навыки сохранены.";

		public static string ForCode(int code)
		{
			return code switch
			{
				7101 => "На сервере «Сансара» у вашего аккаунта уже есть персонаж. Войдите им.", 
				7102 => "Этот персонаж уже играл в другом мире, а на сервере «Сансара» его нет, и перенос персонажей закрыт. Создайте нового персонажа для сервера.", 
				7103 => "Не удалось прочитать копию персонажа с сервера. Сообщите администратору; ваш файл не изменён.", 
				7104 => "Сервер не прислал персонажа вовремя. Попробуйте зайти ещё раз.", 
				7105 => "Не удалось перенести персонажа. Зайдите им в любой мир Valheim 1.0.20 (например, в одиночную игру), выйдите и попробуйте снова — или создайте нового персонажа.", 
				_ => null, 
			};
		}
	}
	internal static class Transport
	{
		public const string RpcLoad = "SansaraCharacters_Load";

		public const string RpcSave = "SansaraCharacters_Save";

		public const string RpcLive = "SansaraCharacters_Live";

		public const string RpcHello = "SansaraCharacters_Hello";

		public const string RpcImport = "SansaraCharacters_Import";

		private const int SendQueueLimit = 100000;

		private const float SendTimeoutSeconds = 30f;

		private static long _messageId = DateTime.UtcNow.Ticks;

		public static string CharacterFolder => SaveSystem.GetCharacterFolderPath((FileSource)2);

		public static IEnumerable<bool> Send(ZNetPeer peer, string rpc, long sequence, byte[] payload)
		{
			long id = ++_messageId;
			List<byte[]> parts = ((payload == null || payload.Length == 0) ? new List<byte[]>() : Chunks.Split(Chunks.Deflate(payload)));
			if (parts.Count == 0)
			{
				Invoke(peer, rpc, id, sequence, 0, 0, Array.Empty<byte>());
				yield return true;
				yield break;
			}
			float deadline = Time.realtimeSinceStartup + 30f;
			for (int i = 0; i < parts.Count; i++)
			{
				while (Connected(peer) && peer.m_socket.GetSendQueueSize() > 100000)
				{
					if (Time.realtimeSinceStartup > deadline)
					{
						Plugin.Log.LogWarning((object)$"{rpc}: send queue did not drain in {30f:0} s, stopped at {i}/{parts.Count}");
						yield break;
					}
					yield return false;
				}
				if (!Connected(peer))
				{
					Plugin.Log.LogWarning((object)$"{rpc}: peer gone after {i}/{parts.Count} parts");
					break;
				}
				Invoke(peer, rpc, id, sequence, i, parts.Count, parts[i]);
				yield return true;
			}
		}

		public static void SendAsync(ZNetPeer peer, string rpc, long sequence, byte[] payload)
		{
			((MonoBehaviour)ZNet.instance).StartCoroutine(Run(Send(peer, rpc, sequence, payload)));
		}

		public static void SendBlocking(ZNetPeer peer, string rpc, long sequence, byte[] payload)
		{
			foreach (bool item in Send(peer, rpc, sequence, payload))
			{
				if (!item)
				{
					TryFlush(peer.m_socket);
					Thread.Sleep(10);
				}
			}
			TryFlush(peer.m_socket);
		}

		public static bool Receive(Reassembler reassembler, string sender, ZPackage pkg, out byte[] payload, out long sequence)
		{
			long messageId = pkg.ReadLong();
			sequence = pkg.ReadLong();
			int index = pkg.ReadInt();
			int total = pkg.ReadInt();
			byte[] part = pkg.ReadByteArray();
			byte[] array = reassembler.Add(sender, messageId, index, total, part, Time.realtimeSinceStartup);
			payload = ((array == null) ? null : ((array.Length == 0) ? Array.Empty<byte>() : Chunks.Inflate(array)));
			return payload != null;
		}

		public static bool Connected(ZNetPeer peer)
		{
			if (peer?.m_socket != null)
			{
				return peer.m_socket.IsConnected();
			}
			return false;
		}

		public static void TryFlush(ISocket socket)
		{
			try
			{
				if (socket != null)
				{
					socket.Flush();
				}
			}
			catch (Exception)
			{
			}
		}

		public static void WriteFileAtomic(string path, byte[] bytes)
		{
			string text = path + ".tmp";
			File.WriteAllBytes(text, bytes);
			MoveOver(text, path);
		}

		public static void MoveOver(string source, string target)
		{
			if (!File.Exists(target))
			{
				File.Move(source, target);
				return;
			}
			try
			{
				File.Replace(source, target, null);
			}
			catch (Exception ex) when (ex is PlatformNotSupportedException || ex is NotSupportedException || ex is IOException)
			{
				File.Copy(source, target, overwrite: true);
				File.Delete(source);
			}
		}

		private static void Invoke(ZNetPeer peer, string rpc, long id, long sequence, int index, int total, byte[] part)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Expected O, but got Unknown
			ZPackage val = new ZPackage();
			val.Write(id);
			val.Write(sequence);
			val.Write(index);
			val.Write(total);
			val.Write(part);
			peer.m_rpc.Invoke(rpc, new object[1] { val });
		}

		private static IEnumerator Run(IEnumerable<bool> steps)
		{
			foreach (bool step in steps)
			{
				if (!step)
				{
					yield return null;
				}
			}
		}
	}
}
namespace SansaraCharacters.Core
{
	public enum LoadMode
	{
		Normal,
		ImportAllowed,
		Imported,
		ImportFailed
	}
	public enum JoinDecision
	{
		UseServerCopy,
		AcceptNew,
		Import,
		RefuseHasProgress,
		RefuseSecondCharacter
	}
	public static class Admission
	{
		public static bool IsSecondCharacter(bool oneCharacterPerAccount, bool isAdmin, bool hasServerCopy, string account, IEnumerable<string> existingFileNames)
		{
			if (!oneCharacterPerAccount || isAdmin || hasServerCopy)
			{
				return false;
			}
			return existingFileNames.Any((string n) => CharacterFileName.BelongsTo(n, account));
		}

		public static JoinDecision OnServerAnswer(bool serverSentCopy, LoadMode mode, bool firstSpawn, int worldDataCount)
		{
			if (serverSentCopy)
			{
				return JoinDecision.UseServerCopy;
			}
			if (firstSpawn && worldDataCount == 0)
			{
				return JoinDecision.AcceptNew;
			}
			if (mode != LoadMode.ImportAllowed)
			{
				return JoinDecision.RefuseHasProgress;
			}
			return JoinDecision.Import;
		}
	}
	public static class BackupPlan
	{
		public const string Format = "yyyy-MM-ddTHH-mm-ss";

		public static string NameFor(DateTime now, ICollection<string> existing)
		{
			string text = now.ToString("yyyy-MM-ddTHH-mm-ss", CultureInfo.InvariantCulture);
			if (!existing.Contains(text))
			{
				return text;
			}
			int num = 2;
			while (existing.Contains(text + "-" + num))
			{
				num++;
			}
			return text + "-" + num;
		}

		public static bool IsDue(IEnumerable<string> existing, DateTime now, int intervalMinutes)
		{
			if (intervalMinutes <= 0)
			{
				return true;
			}
			DateTime dateTime = (from t in existing.Select(ParseTime)
				where t.HasValue
				select t.Value).DefaultIfEmpty(DateTime.MinValue).Max();
			if (dateTime == DateTime.MinValue || dateTime > now)
			{
				return true;
			}
			return now - dateTime >= TimeSpan.FromMinutes(intervalMinutes);
		}

		public static List<string> ToDelete(IEnumerable<string> existing, int keep)
		{
			List<string> list = existing.Where((string n) => ParseTime(n).HasValue).OrderBy<string, string>((string n) => n, StringComparer.Ordinal).ToList();
			int num = list.Count - Math.Max(1, keep);
			if (num <= 0)
			{
				return new List<string>();
			}
			return list.Take(num).ToList();
		}

		public static DateTime? ParseTime(string name)
		{
			if (name == null || name.Length < "yyyy-MM-ddTHH-mm-ss".Length)
			{
				return null;
			}
			if (!DateTime.TryParseExact(name.Substring(0, "yyyy-MM-ddTHH-mm-ss".Length), "yyyy-MM-ddTHH-mm-ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out var result))
			{
				return null;
			}
			return result;
		}
	}
	public static class CharacterFileName
	{
		public const string BackupMarker = "_backup_";

		private static readonly char[] Invalid = new char[11]
		{
			'/', '\\', ':', '*', '?', '"', '<', '>', '|', '\0',
			'.'
		};

		public static string Account(string hostName)
		{
			if (string.IsNullOrEmpty(hostName))
			{
				return "Unknown";
			}
			if (!hostName.All(char.IsDigit))
			{
				return Sanitize(hostName);
			}
			return "Steam_" + hostName;
		}

		public static string For(string hostName, string playerName)
		{
			return Account(hostName) + "_" + Sanitize((playerName ?? "").ToLowerInvariant());
		}

		public static bool BelongsTo(string fileName, string account)
		{
			if (fileName != null && fileName.StartsWith(account + "_", StringComparison.Ordinal))
			{
				return fileName.IndexOf("_backup_", StringComparison.Ordinal) < 0;
			}
			return false;
		}

		private static string Sanitize(string value)
		{
			StringBuilder stringBuilder = new StringBuilder(value.Length);
			foreach (char c in value)
			{
				stringBuilder.Append((Array.IndexOf(Invalid, c) >= 0 || char.IsControl(c)) ? '_' : c);
			}
			return stringBuilder.ToString();
		}
	}
	public static class CharacterQuery
	{
		public static string Resolve(IEnumerable<string> fileNames, string query, out string error)
		{
			List<string> source = fileNames.ToList();
			string q = (query ?? "").Trim();
			error = null;
			if (q.StartsWith("@", StringComparison.Ordinal))
			{
				q = q.Substring(1).Trim();
			}
			if (q == "")
			{
				error = "не указан персонаж";
				return null;
			}
			string text = source.FirstOrDefault((string f) => string.Equals(f, q, StringComparison.Ordinal));
			if (text != null)
			{
				return text;
			}
			string lower = q.ToLowerInvariant();
			List<string> list = source.Where((string f) => NameOf(f) == lower).ToList();
			if (list.Count == 1)
			{
				return list[0];
			}
			if (list.Count > 1)
			{
				error = "персонажей с именем «" + q + "» несколько: " + string.Join(", ", list) + "; укажите @файл";
				return null;
			}
			error = "персонаж «" + q + "» не найден";
			return null;
		}

		public static string NameOf(string fileName)
		{
			string[] array = (fileName ?? "").Split(new char[1] { '_' }, 3);
			object obj;
			if (array.Length != 3)
			{
				obj = fileName;
				if (obj == null)
				{
					return "";
				}
			}
			else
			{
				obj = array[2];
			}
			return (string)obj;
		}

		public static string AccountOf(string fileName)
		{
			string[] array = (fileName ?? "").Split(new char[1] { '_' }, 3);
			if (array.Length != 3)
			{
				return "";
			}
			return array[0] + "_" + array[1];
		}
	}
	public static class Chunks
	{
		public const int PartSize = 200000;

		public static byte[] Deflate(byte[] data)
		{
			using MemoryStream memoryStream = new MemoryStream();
			using (DeflateStream deflateStream = new DeflateStream(memoryStream, CompressionLevel.Optimal, leaveOpen: true))
			{
				deflateStream.Write(data, 0, data.Length);
			}
			return memoryStream.ToArray();
		}

		public static byte[] Inflate(byte[] data, int maxLength = 67108864)
		{
			using MemoryStream stream = new MemoryStream(data);
			using DeflateStream deflateStream = new DeflateStream(stream, CompressionMode.Decompress);
			using MemoryStream memoryStream = new MemoryStream();
			byte[] array = new byte[81920];
			int num;
			while ((num = deflateStream.Read(array, 0, array.Length)) > 0)
			{
				if (memoryStream.Length + num > maxLength)
				{
					throw new InvalidDataException("payload too large");
				}
				memoryStream.Write(array, 0, num);
			}
			return memoryStream.ToArray();
		}

		public static List<byte[]> Split(byte[] data, int partSize = 200000)
		{
			List<byte[]> list = new List<byte[]>();
			if (data == null || data.Length == 0)
			{
				return list;
			}
			for (int i = 0; i < data.Length; i += partSize)
			{
				byte[] array = new byte[Math.Min(partSize, data.Length - i)];
				Buffer.BlockCopy(data, i, array, 0, array.Length);
				list.Add(array);
			}
			return list;
		}
	}
	public sealed class Reassembler
	{
		private sealed class Entry
		{
			public readonly SortedDictionary<int, byte[]> Parts = new SortedDictionary<int, byte[]>();

			public int Total;

			public double Expires;
		}

		public const double ExpirySeconds = 60.0;

		public const int MaxParts = 400;

		private readonly Dictionary<string, Entry> _entries = new Dictionary<string, Entry>();

		public int Count => _entries.Count;

		public byte[] Add(string sender, long messageId, int index, int total, byte[] part, double now)
		{
			foreach (string item in (from kv in _entries
				where kv.Value.Expires < now
				select kv.Key).ToList())
			{
				_entries.Remove(item);
			}
			if (total <= 0)
			{
				return Array.Empty<byte>();
			}
			if (total > 400 || index < 0 || index >= total || part == null)
			{
				throw new InvalidDataException("bad part " + index + "/" + total);
			}
			string key = sender + ":" + messageId;
			if (!_entries.TryGetValue(key, out var value))
			{
				value = new Entry
				{
					Total = total,
					Expires = now + 60.0
				};
				_entries[key] = value;
			}
			if (value.Total != total)
			{
				throw new InvalidDataException("part count changed for message " + messageId);
			}
			value.Parts[index] = part;
			if (value.Parts.Count < value.Total)
			{
				return null;
			}
			_entries.Remove(key);
			return value.Parts.Values.SelectMany((byte[] p) => p).ToArray();
		}

		public void Forget(string sender)
		{
			foreach (string item in _entries.Keys.Where((string k) => k.StartsWith(sender + ":", StringComparison.Ordinal)).ToList())
			{
				_entries.Remove(item);
			}
		}
	}
	public static class ImportWindow
	{
		public const string Format = "yyyy-MM-dd HH:mm";

		public static DateTime? ParseUntil(string value)
		{
			if (string.IsNullOrWhiteSpace(value))
			{
				return null;
			}
			if (DateTime.TryParseExact(value.Trim(), "yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture, DateTimeStyles.None, out var result))
			{
				return result;
			}
			throw new FormatException("ImportUntil '" + value + "': expected yyyy-MM-dd HH:mm");
		}

		public static bool IsOpen(DateTime? until, DateTime now)
		{
			if (until.HasValue)
			{
				return now <= until.Value;
			}
			return false;
		}
	}
	public sealed class LiveSnapshot
	{
		public long Sequence { get; }

		public byte[] PlayerData { get; }

		public LiveSnapshot(long sequence, byte[] playerData)
		{
			Sequence = sequence;
			PlayerData = playerData;
		}
	}
	public sealed class LiveLedger
	{
		public long StoredSequence { get; private set; }

		public LiveSnapshot Pending { get; private set; }

		public bool OnFullSave(long sequence)
		{
			if (sequence <= StoredSequence)
			{
				return false;
			}
			StoredSequence = sequence;
			if (Pending != null && Pending.Sequence <= sequence)
			{
				Pending = null;
			}
			return true;
		}

		public bool OnLive(LiveSnapshot snapshot)
		{
			if (snapshot == null || snapshot.Sequence <= StoredSequence)
			{
				return false;
			}
			if (Pending != null && snapshot.Sequence <= Pending.Sequence)
			{
				return false;
			}
			Pending = snapshot;
			return true;
		}

		public LiveSnapshot TakePending()
		{
			return Pending;
		}

		public void MarkApplied(LiveSnapshot snapshot)
		{
			if (snapshot.Sequence > StoredSequence)
			{
				StoredSequence = snapshot.Sequence;
			}
			if (Pending != null && Pending.Sequence <= snapshot.Sequence)
			{
				Pending = null;
			}
		}

		public void Drop(LiveSnapshot snapshot)
		{
			if (Pending == snapshot)
			{
				Pending = null;
			}
		}
	}
	public static class PlayerDataStrip
	{
		public const int SupportedVersion = 34;

		public const int InventoryVersion = 110;

		public const int SkillsVersion = 2;

		public const float BaseHealth = 25f;

		public const float BaseStamina = 75f;

		public const float NoDeath = 999999f;

		public static readonly string[] DroppedCustomData = new string[1] { "eaqs_backup" };

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

		public static byte[] Strip(byte[] playerData)
		{
			if (playerData == null || playerData.Length < 4)
			{
				throw new InvalidDataException("no player data");
			}
			using MemoryStream memoryStream = new MemoryStream(playerData);
			BinaryReader r = new BinaryReader(memoryStream, Utf8);
			try
			{
				using MemoryStream memoryStream2 = new MemoryStream(playerData.Length);
				BinaryWriter w = new BinaryWriter(memoryStream2, Utf8);
				try
				{
					int num = r.ReadInt32();
					if (num != 34)
					{
						throw new InvalidDataException($"player data version {num},