Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of Roguenarok Foundation v0.2.0
BepInEx/plugins/Roguenarok-Foundation/Roguenarok.Foundation.dll
Decompiled 13 hours agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Xml; using System.Xml.Linq; using BepInEx; using BepInEx.Configuration; using HarmonyLib; using Jotunn; using Jotunn.Utils; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("Roguenarok.Foundation")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+66087733cbceada476c9502a244f4a1829af82f0")] [assembly: AssemblyProduct("Roguenarok.Foundation")] [assembly: AssemblyTitle("Roguenarok.Foundation")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace Roguenarok.Foundation { [BepInPlugin("local.roguenarok.foundation", "Roguenarok Foundation", "0.2.0")] [BepInDependency("com.jotunn.jotunn", "2.30.2")] [NetworkCompatibility(/*Could not decode attribute arguments.*/)] public sealed class Plugin : BaseUnityPlugin { private sealed class View { public string Epoch; public string RaidId; public string Result; public long Sequence; public long Revision; public RaidPhase Phase; public double Remaining; public bool Admin; public List<Member> Members = new List<Member>(); } public const string Guid = "local.roguenarok.foundation"; public const string Version = "0.2.0"; private const int Protocol = 1; private const string RequestRpc = "Roguenarok.Foundation.Request.v1"; private const string StateRpc = "Roguenarok.Foundation.State.v1"; private const string ReplyRpc = "Roguenarok.Foundation.Reply.v1"; private static Plugin instance; private Harmony harmony; private ConfigEntry<bool> hudEnabled; private ConfigEntry<string> stateDirectory; private RaidStore store; private bool storageBlocked; private long savedRevision = -1L; private ZNet network; private ZRpc serverRpc; private RaidState state; private string epoch; private long sequence; private double nextBroadcast; private double nextSync; private readonly Dictionary<ZRpc, ZNetPeer> registered = new Dictionary<ZRpc, ZNetPeer>(); private readonly Dictionary<ZRpc, double> lastRequest = new Dictionary<ZRpc, double>(); private readonly SnapshotOrder order = new SnapshotOrder(); private View view; private double receivedAt; private string reply = "Ожидание синхронизации с сервером."; private string input = "rogue start 300"; private Rect menuRect = new Rect(35f, 140f, 550f, 380f); internal static bool MenuOpen; private CursorLockMode savedLock; private bool savedVisible; private static double Now => Time.realtimeSinceStartupAsDouble; private double DisplayRemaining { get { if (view != null) { return Math.Max(0.0, view.Remaining - (Now - receivedAt)); } return 0.0; } } private void Awake() { //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Expected O, but got Unknown //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Expected O, but got Unknown //IL_00a5: Unknown result type (might be due to invalid IL or missing references) instance = this; hudEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Interface", "ShowHUD", true, "Show raid status and timer. F6 opens the test panel."); stateDirectory = ((BaseUnityPlugin)this).Config.Bind<string>("Server", "StateDirectory", Path.Combine(Paths.BepInExRootPath, "Roguenarok", "state"), "Server-only checkpoint directory; one subdirectory per world UID."); harmony = new Harmony("local.roguenarok.foundation"); harmony.PatchAll(typeof(Plugin).Assembly); new ConsoleCommand("rogue", "Roguenarok: status | start [10..2700 seconds] | extract | finish | reset", new ConsoleEvent(OnCommand), false, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); ((BaseUnityPlugin)this).Logger.LogInfo((object)string.Format("Roguenarok Foundation {0}: protocol {1}; exact client/server version required. F6 test panel. Server raid checkpoints enabled.", "0.2.0", 1)); } private void OnDestroy() { SaveCheckpoint(); SetMenu(open: false); Harmony obj = harmony; if (obj != null) { obj.UnpatchSelf(); } instance = null; } private void ResetNetwork(ZNet net) { SaveCheckpoint(); SetMenu(open: false); network = net; registered.Clear(); lastRequest.Clear(); serverRpc = null; view = null; order.Reset(); nextSync = (nextBroadcast = 0.0); state = null; store = null; storageBlocked = false; savedRevision = -1L; epoch = null; sequence = 0L; } private void Update() { ZNet val = ZNet.instance; if ((Object)(object)network != (Object)(object)val) { ResetNetwork(val); } if ((Object)(object)val == (Object)null) { SetMenu(open: false); return; } if ((Object)(object)Player.m_localPlayer != (Object)null) { if (Input.GetKeyDown((KeyCode)287)) { SetMenu(!MenuOpen); } if (MenuOpen && Input.GetKeyDown((KeyCode)27)) { SetMenu(open: false); } } else if (MenuOpen) { SetMenu(open: false); } if (val.IsServer()) { UpdateServer(val); } else { UpdateClient(val); } } private void UpdateServer(ZNet net) { if (state == null) { if (net.GetWorldUID() == 0L) { return; } store = new RaidStore(Path.Combine(stateDirectory.Value, net.GetWorldUID().ToString(CultureInfo.InvariantCulture), "raid-state.xml")); epoch = System.Guid.NewGuid().ToString("N"); try { state = store.Load(Now, DateTime.UtcNow, out var source); ((BaseUnityPlugin)this).Logger.LogInfo((object)$"Restored raid checkpoint: source={source}, phase={state.Phase}, id={state.Id}, remaining={state.Remaining(Now):0.0}s; file={store.FilePath}"); SaveCheckpoint(); } catch (Exception ex) { state = new RaidState(); storageBlocked = true; ((BaseUnityPlugin)this).Logger.LogError((object)("Raid checkpoint load failed; raid commands disabled. Files preserved. " + ex)); } ((BaseUnityPlugin)this).Logger.LogInfo((object)$"Server raid controller ready: {state.Phase}; persistent={!storageBlocked}."); } List<ZNetPeer> peers = (from p in net.GetPeers() where !p.m_server && p.m_rpc != null select p).ToList(); foreach (ZNetPeer item in peers) { if (!registered.ContainsKey(item.m_rpc)) { ZNetPeer captured = item; item.m_rpc.Register<ZPackage>("Roguenarok.Foundation.Request.v1", (Action<ZRpc, ZPackage>)delegate(ZRpc rpc, ZPackage package) { ReceiveRequest(captured, rpc, package); }); registered.Add(item.m_rpc, item); } } foreach (ZRpc item2 in registered.Keys.Where((ZRpc r) => !peers.Any((ZNetPeer p) => p.m_rpc == r)).ToList()) { registered.Remove(item2); lastRequest.Remove(item2); } RaidState raidState = state.Copy(); bool flag = !storageBlocked && state.Tick(Now); if (flag && !SaveCheckpoint()) { state = raidState; flag = false; } if (flag) { ((BaseUnityPlugin)this).Logger.LogInfo((object)$"Raid {state.Id}: {state.Phase}; {state.Result}"); } if (Now < nextBroadcast && !flag) { return; } nextBroadcast = Now + 1.0; raidState = state.Copy(); if (!storageBlocked && state.RefreshMembers(OnlineMembers(net)) && !SaveCheckpoint()) { state = raidState; } sequence++; foreach (ZNetPeer item3 in peers.Where((ZNetPeer p) => p.IsReady() && p.m_rpc.IsConnected())) { SendState(item3); } if (!net.IsDedicated()) { view = MakeView(admin: true); receivedAt = Now; } } private IEnumerable<Member> OnlineMembers(ZNet net) { foreach (ZNetPeer item in from p in net.GetPeers() where !p.m_server && p.IsReady() && p.m_rpc.IsConnected() select p) { string hostName = item.m_socket.GetHostName(); if (!string.IsNullOrEmpty(hostName)) { yield return new Member(hostName, CleanName(item.m_playerName)); } } if (!net.IsDedicated() && (Object)(object)Player.m_localPlayer != (Object)null) { yield return new Member("local-host", CleanName(Player.m_localPlayer.GetPlayerName())); } } private static string CleanName(string name) { if (!string.IsNullOrWhiteSpace(name)) { return name.Replace("<", "").Replace(">", "").Replace("\n", " ") .Replace("\r", " ") .Substring(0, Math.Min(48, name.Replace("<", "").Replace(">", "").Replace("\n", " ") .Replace("\r", " ") .Length)); } return "Игрок"; } private void UpdateClient(ZNet net) { ZRpc val = net.GetServerPeer()?.m_rpc; if (val == null || !val.IsConnected()) { if (serverRpc != null) { serverRpc = null; view = null; order.Reset(); reply = "Соединение с сервером потеряно."; SetMenu(open: false); } return; } if (serverRpc != val) { serverRpc = val; view = null; order.Reset(); nextSync = 0.0; val.Register<ZPackage>("Roguenarok.Foundation.State.v1", (Action<ZRpc, ZPackage>)ReceiveState); val.Register<ZPackage>("Roguenarok.Foundation.Reply.v1", (Action<ZRpc, ZPackage>)ReceiveReply); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Server connection bound; requesting raid snapshot."); } if (Now >= nextSync) { nextSync = Now + (double)((view == null) ? 2 : 10); SendRequest("sync", 0); } } private void ReceiveRequest(ZNetPeer peer, ZRpc rpc, ZPackage package) { //IL_012e: Unknown result type (might be due to invalid IL or missing references) //IL_0135: Expected O, but got Unknown if ((Object)(object)network == (Object)null || !network.IsServer() || state == null || network.GetPeer(peer.m_uid) != peer || peer.m_rpc != rpc || !peer.IsReady() || (lastRequest.TryGetValue(rpc, out var value) && Now - value < 0.35)) { return; } lastRequest[rpc] = Now; try { if (package.Size() <= 1024 && package.ReadInt() == 1 && !(package.ReadString() != "0.2.0")) { string text = package.ReadString(); int seconds = package.ReadInt(); bool admin = ZNetExtension.IsAdmin(network, peer.m_uid); string text2 = ExecuteServer(text, seconds, admin); if (text != "sync" && text != "status") { ((BaseUnityPlugin)this).Logger.LogInfo((object)$"Command '{text}' from peer {peer.m_uid}: {text2}"); } sequence++; if (text != "sync") { ZPackage val = new ZPackage(); val.Write(1); val.Write(text2); rpc.Invoke("Roguenarok.Foundation.Reply.v1", new object[1] { val }); } SendState(peer); nextBroadcast = 0.0; } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Rejected malformed request: " + ex.GetType().Name)); } } private string ExecuteServer(string action, int seconds, bool admin) { if (state == null) { return "Контроллер рейда ещё загружается."; } if (storageBlocked) { return "Ошибка хранилища рейда. Команды отключены до восстановления файлов и перезапуска сервера."; } RaidState raidState = state.Copy(); string result = state.Execute(action, seconds, admin, Now, OnlineMembers(network)); if (!SaveCheckpoint()) { state = raidState; return "Не удалось сохранить рейд. Изменение отменено, команды отключены. Проверь журнал сервера."; } return result; } private bool SaveCheckpoint() { if (store == null || state == null || storageBlocked) { return !storageBlocked; } if (savedRevision == state.Revision) { return true; } try { store.Save(state, Now, DateTime.UtcNow); savedRevision = state.Revision; return true; } catch (Exception ex) { storageBlocked = true; ((BaseUnityPlugin)this).Logger.LogError((object)("Raid checkpoint write failed; state changes blocked. " + ex)); return false; } } private View MakeView(bool admin) { return new View { Epoch = epoch, Sequence = sequence, Revision = state.Revision, RaidId = state.Id, Phase = state.Phase, Remaining = state.Remaining(Now), Result = (storageBlocked ? "Ошибка сохранения рейда: управление отключено." : state.Result), Admin = admin, Members = state.Members.Select((Member m) => m.Copy()).ToList() }; } private void SendState(ZNetPeer peer) { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Expected O, but got Unknown View view = MakeView(ZNetExtension.IsAdmin(network, peer.m_uid)); ZPackage val = new ZPackage(); val.Write(1); val.Write("0.2.0"); val.Write(view.Epoch); val.Write(view.Sequence); val.Write(view.Revision); val.Write(view.RaidId); val.Write((int)view.Phase); val.Write(view.Remaining); val.Write(view.Result); val.Write(view.Admin); val.Write(view.Members.Count); foreach (Member member in view.Members) { val.Write(member.Id); val.Write(member.Name); val.Write(member.Connected); } peer.m_rpc.Invoke("Roguenarok.Foundation.State.v1", new object[1] { val }); } private void ReceiveState(ZRpc rpc, ZPackage p) { if (rpc != serverRpc || (Object)(object)network == (Object)null || network.IsServer()) { return; } try { if (p.Size() > 16384 || p.ReadInt() != 1 || p.ReadString() != "0.2.0") { return; } View view = new View { Epoch = p.ReadString(), Sequence = p.ReadLong(), Revision = p.ReadLong(), RaidId = p.ReadString(), Phase = (RaidPhase)p.ReadInt(), Remaining = p.ReadDouble(), Result = p.ReadString(), Admin = p.ReadBool() }; int num = p.ReadInt(); if (!Enum.IsDefined(typeof(RaidPhase), view.Phase) || num < 0 || num > 64 || double.IsNaN(view.Remaining) || double.IsInfinity(view.Remaining) || view.Remaining < 0.0 || view.Remaining > 3000.0) { return; } for (int i = 0; i < num; i++) { view.Members.Add(new Member(p.ReadString(), CleanName(p.ReadString()), p.ReadBool())); } if (order.Accept(view.Epoch, view.Sequence)) { bool num2 = this.view == null; this.view = view; receivedAt = Now; if (num2) { reply = "Синхронизация выполнена."; ((BaseUnityPlugin)this).Logger.LogInfo((object)$"Raid snapshot received: {view.Phase}, revision {view.Revision}, admin={view.Admin}"); } } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Rejected malformed snapshot: " + ex.GetType().Name)); } } private void ReceiveReply(ZRpc rpc, ZPackage p) { if (rpc != serverRpc) { return; } try { if (p.Size() <= 4096 && p.ReadInt() == 1) { reply = p.ReadString(); ((BaseUnityPlugin)this).Logger.LogInfo((object)reply); } } catch { } } private void SendRequest(string action, int seconds) { //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Expected O, but got Unknown if ((Object)(object)network == (Object)null) { reply = "Сначала подключись к серверу."; } else if (network.IsServer()) { reply = ExecuteServer(action, seconds, admin: true); sequence++; nextBroadcast = 0.0; if (!network.IsDedicated()) { view = MakeView(admin: true); receivedAt = Now; } } else if (serverRpc == null || !serverRpc.IsConnected()) { reply = "Нет соединения с сервером."; } else { ZPackage val = new ZPackage(); val.Write(1); val.Write("0.2.0"); val.Write(action); val.Write(seconds); serverRpc.Invoke("Roguenarok.Foundation.Request.v1", new object[1] { val }); } } private void ExecuteLine(string line) { string[] array = line.Trim().Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (array.Length != 0 && array[0].Equals("rogue", StringComparison.OrdinalIgnoreCase)) { array = array.Skip(1).ToArray(); } if (array.Length == 0) { reply = "rogue status | start [10..2700] | extract | finish | reset"; return; } string text = array[0].ToLowerInvariant(); int result = 300; if ((text == "start" && array.Length > 2) || (text != "start" && array.Length > 1) || (array.Length == 2 && !int.TryParse(array[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out result))) { reply = "Неверные аргументы команды."; } else { SendRequest(text, result); } } private void OnCommand(ConsoleEventArgs args) { ExecuteLine(args.FullLine); args.Context.AddString("[Roguenarok] " + reply); } private static string PhaseName(RaidPhase p) { return p switch { RaidPhase.Waiting => "ОЖИДАНИЕ", RaidPhase.Preparing => "ПОДГОТОВКА", RaidPhase.Active => "РЕЙД", RaidPhase.Evacuating => "ЭВАКУАЦИЯ", _ => "ЗАВЕРШЁН", }; } private void SetMenu(bool open) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) if (MenuOpen != open) { if (open) { savedLock = Cursor.lockState; savedVisible = Cursor.visible; } MenuOpen = open; if (!open) { Cursor.lockState = savedLock; Cursor.visible = savedVisible; } } } private void LateUpdate() { if (MenuOpen) { Cursor.lockState = (CursorLockMode)0; Cursor.visible = true; } } private void OnGUI() { //IL_0028: 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_0034: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Expected O, but got Unknown //IL_0181: Unknown result type (might be due to invalid IL or missing references) //IL_018d: Unknown result type (might be due to invalid IL or missing references) //IL_01a1: Expected O, but got Unknown //IL_019c: Unknown result type (might be due to invalid IL or missing references) //IL_01a1: Unknown result type (might be due to invalid IL or missing references) //IL_012d: Unknown result type (might be due to invalid IL or missing references) //IL_015d: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)Player.m_localPlayer == (Object)null) { return; } if (hudEnabled.Value) { GUIStyle val = new GUIStyle(GUI.skin.label) { alignment = (TextAnchor)4, fontSize = 16, richText = false }; string text = ((view == null) ? "ROGUENAROK · синхронизация…" : $"ROGUENAROK · {PhaseName(view.Phase)}\n{TimeSpan.FromSeconds(Math.Ceiling(DisplayRemaining)):mm\\:ss} · {view.Members.Count((Member m) => m.Connected)}/{view.Members.Count} игроков"); if (view != null && Now - receivedAt > 5.0) { text += "\nНет свежих данных сервера"; } GUI.Box(new Rect((float)Screen.width / 2f - 170f, 20f, 340f, 90f), ""); GUI.Label(new Rect((float)Screen.width / 2f - 165f, 23f, 330f, 80f), text + "\nF6 · управление", val); } if (MenuOpen) { menuRect = GUILayout.Window(812941, menuRect, new WindowFunction(DrawMenu), "Roguenarok Foundation 0.2.0 · F6", Array.Empty<GUILayoutOption>()); } } private void DrawMenu(int id) { //IL_029d: Unknown result type (might be due to invalid IL or missing references) GUILayout.Label((view == null) ? "Ожидание сервера" : string.Format("{0} · ревизия {1} · права: {2}", PhaseName(view.Phase), view.Revision, view.Admin ? "администратор" : "игрок"), Array.Empty<GUILayoutOption>()); if (view != null) { GUILayout.Label("ID рейда: " + ((view.RaidId == "") ? "—" : view.RaidId), Array.Empty<GUILayoutOption>()); foreach (Member member in view.Members) { GUILayout.Label(member.Name + (member.Connected ? " · подключён" : " · отключён"), Array.Empty<GUILayoutOption>()); } } if (view != null && !string.IsNullOrEmpty(view.Result)) { GUILayout.Label(view.Result, Array.Empty<GUILayoutOption>()); } GUILayout.Space(8f); GUILayout.Label("Тестовый каркас: мир, инвентарь и награды не изменяются.", Array.Empty<GUILayoutOption>()); GUILayout.Label("Рейд сохраняется. Таймер учитывает время простоя сервера.", Array.Empty<GUILayoutOption>()); GUI.enabled = view != null && view.Admin; GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); if (GUILayout.Button("Рейд 5 минут", Array.Empty<GUILayoutOption>())) { SendRequest("start", 300); } if (GUILayout.Button("Эвакуация", Array.Empty<GUILayoutOption>())) { SendRequest("extract", 0); } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); if (GUILayout.Button("Завершить", Array.Empty<GUILayoutOption>())) { SendRequest("finish", 0); } if (GUILayout.Button("Сбросить", Array.Empty<GUILayoutOption>())) { SendRequest("reset", 0); } GUILayout.EndHorizontal(); GUI.enabled = true; input = GUILayout.TextField(input, 120, Array.Empty<GUILayoutOption>()); if (GUILayout.Button("Выполнить команду", Array.Empty<GUILayoutOption>())) { ExecuteLine(input); } GUILayout.Label(reply, Array.Empty<GUILayoutOption>()); if (GUILayout.Button("Обновить статус", Array.Empty<GUILayoutOption>())) { SendRequest("status", 0); } if (GUILayout.Button("Закрыть", Array.Empty<GUILayoutOption>())) { SetMenu(open: false); } GUI.DragWindow(new Rect(0f, 0f, 10000f, 25f)); } } [HarmonyPatch(typeof(PlayerController), "TakeInput")] internal static class MenuInputPatch { private static bool Prefix(ref bool __result) { if (!Plugin.MenuOpen) { return true; } __result = false; return false; } } [HarmonyPatch(typeof(Player), "SetControls")] internal static class MenuControlPatch { private static void Prefix(Player __instance, object[] __args) { //IL_002d: Unknown result type (might be due to invalid IL or missing references) if (Plugin.MenuOpen && !((Object)(object)__instance != (Object)(object)Player.m_localPlayer)) { for (int i = 0; i < __args.Length; i++) { __args[i] = ((__args[i] is Vector3) ? ((object)Vector3.zero) : ((object)false)); } } } } public enum RaidPhase { Waiting, Preparing, Active, Evacuating, Finished } public sealed class Member { public string Id; public string Name; public bool Connected; public Member(string id, string name, bool connected = true) { Id = id; Name = name; Connected = connected; } public Member Copy() { return new Member(Id, Name, Connected); } } public sealed class RaidState { public string Id = ""; public string Result = ""; public RaidPhase Phase; public long Revision; public double Deadline; public double RaidDeadline; public readonly List<Member> Members = new List<Member>(); public double Remaining(double now) { if (Phase != RaidPhase.Preparing && Phase != RaidPhase.Active && Phase != RaidPhase.Evacuating) { return 0.0; } return Math.Max(0.0, Deadline - now); } public string Execute(string action, int seconds, bool admin, double now, IEnumerable<Member> online) { Tick(now); if (action == "status" || action == "sync") { return "Состояние обновлено."; } if (!admin) { return "Отказ: команда доступна только администратору сервера."; } switch (action) { case "start": { if (Phase != RaidPhase.Waiting) { return "Сначала выполни rogue reset."; } if (seconds < 10 || seconds > 2700) { return "Длительность: от 10 до 2700 секунд."; } List<Member> list = (from m in online group m by m.Id into g select g.First().Copy()).ToList(); if (list.Count == 0 || list.Count > 4) { return "Для тестового рейда нужно от 1 до 4 подключённых игроков."; } Members.Clear(); Members.AddRange(list); Id = Guid.NewGuid().ToString("N"); Result = ""; Phase = RaidPhase.Preparing; Deadline = now + 5.0; RaidDeadline = Deadline + (double)seconds; Revision++; return "Подготовка 5 секунд. Затем начнётся тестовый рейд."; } case "extract": if (Phase != RaidPhase.Active) { return "Эвакуацию можно начать только во время рейда."; } Phase = RaidPhase.Evacuating; Deadline = Math.Min(now + 120.0, RaidDeadline); Revision++; return "Тестовая эвакуация началась. Награды пока не выдаются."; case "finish": if (Phase != RaidPhase.Active && Phase != RaidPhase.Evacuating) { return "Завершать можно только рейд или эвакуацию."; } Finish("Завершён администратором"); return "Тестовый рейд завершён."; case "reset": Phase = RaidPhase.Waiting; Id = ""; Result = ""; Deadline = (RaidDeadline = 0.0); Members.Clear(); Revision++; return "Рейд сброшен. Мир и инвентари не изменены."; default: return "Команды: rogue status | start [секунды] | extract | finish | reset"; } } public bool Tick(double now) { bool result = false; if (Phase == RaidPhase.Preparing && now >= Deadline) { Phase = RaidPhase.Active; Deadline = RaidDeadline; Revision++; result = true; } if (Phase == RaidPhase.Active && now >= Deadline) { Finish("Время рейда истекло"); result = true; } else if (Phase == RaidPhase.Evacuating && now >= Deadline) { Finish("Тестовая эвакуация завершена"); result = true; } return result; } public bool RefreshMembers(IEnumerable<Member> online) { List<Member> list = (from m in online group m by m.Id into g select g.First().Copy() into m orderby m.Id select m).ToList(); if (Phase == RaidPhase.Waiting) { if (Members.Count == list.Count && Members.Zip(list, (Member a, Member b) => a.Id == b.Id && a.Name == b.Name).All((bool x) => x)) { return false; } Members.Clear(); Members.AddRange(list); Revision++; return true; } bool flag = false; foreach (Member member in Members) { bool flag2 = list.Any((Member m) => m.Id == member.Id); if (flag2 != member.Connected) { member.Connected = flag2; flag = true; } } if (flag) { Revision++; } return flag; } public RaidState Copy() { RaidState raidState = new RaidState(); raidState.Id = Id; raidState.Result = Result; raidState.Phase = Phase; raidState.Revision = Revision; raidState.Deadline = Deadline; raidState.RaidDeadline = RaidDeadline; raidState.Members.AddRange(Members.Select((Member m) => m.Copy())); return raidState; } private void Finish(string reason) { Phase = RaidPhase.Finished; Deadline = 0.0; Result = reason; Revision++; } } public sealed class SnapshotOrder { private string epoch; private long sequence = -1L; public void Reset() { epoch = null; sequence = -1L; } public bool Accept(string newEpoch, long newSequence) { if (epoch != null && epoch != newEpoch) { return false; } if (newSequence <= sequence) { return false; } epoch = newEpoch; sequence = newSequence; return true; } } public sealed class RaidStore { private const int Schema = 1; private const long MaxBytes = 65536L; public string FilePath { get; } public RaidStore(string filePath) { FilePath = filePath; } public void Save(RaidState state, double monotonicNow, DateTime utcNow) { Directory.CreateDirectory(Path.GetDirectoryName(FilePath)); XElement xElement = new XElement("RoguenarokRaid", new XAttribute("schema", 1), new XElement("Id", state.Id), new XElement("Phase", state.Phase.ToString()), new XElement("Revision", state.Revision), new XElement("Result", state.Result), new XElement("SavedUtc", utcNow.ToUniversalTime().Ticks), new XElement("DeadlineUtc", DeadlineTicks(state.Deadline, monotonicNow, utcNow)), new XElement("RaidDeadlineUtc", DeadlineTicks(state.RaidDeadline, monotonicNow, utcNow)), new XElement("Members", state.Members.Select((Member m) => new XElement("Member", new XAttribute("id", m.Id), new XAttribute("name", m.Name))))); XDocument xDocument = new XDocument(xElement); string text = FilePath + ".tmp"; using (FileStream fileStream = new FileStream(text, FileMode.Create, FileAccess.Write, FileShare.None)) { xDocument.Save(fileStream); fileStream.Flush(flushToDisk: true); } if (File.Exists(FilePath)) { File.Replace(text, FilePath, FilePath + ".bak"); } else { File.Move(text, FilePath); } } private static long DeadlineTicks(double deadline, double now, DateTime utc) { if (deadline != 0.0) { return utc.ToUniversalTime().AddSeconds(deadline - now).Ticks; } return 0L; } public RaidState Load(double monotonicNow, DateTime utcNow, out string source) { source = "new"; if (!File.Exists(FilePath) && !File.Exists(FilePath + ".bak")) { return new RaidState(); } Exception ex = null; try { RaidState result = Read(FilePath, monotonicNow, utcNow); source = "primary"; return result; } catch (Exception ex2) when (ex2 is IOException || ex2 is XmlException || ex2 is InvalidDataException || ex2 is FormatException || ex2 is ArgumentException || ex2 is OverflowException) { ex = ex2; } try { RaidState result2 = Read(FilePath + ".bak", monotonicNow, utcNow); if (File.Exists(FilePath)) { File.Move(FilePath, FilePath + ".corrupt-" + Guid.NewGuid().ToString("N")); } source = "backup"; return result2; } catch (Exception ex3) when (ex3 is IOException || ex3 is XmlException || ex3 is InvalidDataException || ex3 is FormatException || ex3 is ArgumentException || ex3 is OverflowException) { throw new InvalidDataException("No valid raid checkpoint. Primary: " + ex.GetType().Name + "; backup: " + ex3.GetType().Name, ex3); } } private static RaidState Read(string path, double monotonicNow, DateTime utcNow) { FileInfo fileInfo = new FileInfo(path); if (!fileInfo.Exists || fileInfo.Length > 65536 || fileInfo.Length == 0L) { throw new InvalidDataException("Invalid checkpoint size."); } XDocument xDocument; using (XmlReader reader = XmlReader.Create(path, new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null, MaxCharactersInDocument = 65536L })) { xDocument = XDocument.Load(reader); } XElement root = xDocument.Root; if (root == null || root.Name != "RoguenarokRaid" || (int?)root.Attribute("schema") != 1) { throw new InvalidDataException("Unsupported checkpoint schema."); } if (!Enum.TryParse<RaidPhase>(Text("Phase"), out var result) || !Enum.IsDefined(typeof(RaidPhase), result)) { throw new InvalidDataException("Invalid phase."); } long num = long.Parse(Text("Revision"), CultureInfo.InvariantCulture); long ticks = long.Parse(Text("SavedUtc"), CultureInfo.InvariantCulture); DateTime savedUtc = new DateTime(ticks, DateTimeKind.Utc); RaidState raidState = new RaidState { Id = Text("Id"), Phase = result, Revision = num, Result = Text("Result") }; if (num < 0 || num > 9223372036853775807L || raidState.Result.Length > 512 || raidState.Id.Length > 64) { throw new InvalidDataException("Invalid checkpoint fields."); } if (result != RaidPhase.Waiting && !Guid.TryParseExact(raidState.Id, "N", out var _)) { throw new InvalidDataException("Invalid raid ID."); } if (result == RaidPhase.Waiting && raidState.Id != "") { throw new InvalidDataException("Waiting raid must not have an ID."); } raidState.Deadline = ReadDeadline("DeadlineUtc"); raidState.RaidDeadline = ReadDeadline("RaidDeadlineUtc"); foreach (XElement item in (root.Element("Members") ?? throw new InvalidDataException("Missing members.")).Elements("Member")) { string id = (string?)item.Attribute("id"); string text = (string?)item.Attribute("name"); if (string.IsNullOrWhiteSpace(id) || id.Length > 128 || text == null || text.Length > 48 || raidState.Members.Count >= 64 || raidState.Members.Any((Member m) => m.Id == id)) { throw new InvalidDataException("Invalid participant list."); } raidState.Members.Add(new Member(id, text, connected: false)); } if ((result == RaidPhase.Preparing || result == RaidPhase.Active || result == RaidPhase.Evacuating) && (raidState.Members.Count < 1 || raidState.Members.Count > 4 || Text("DeadlineUtc") == "0" || Text("RaidDeadlineUtc") == "0" || raidState.Deadline > raidState.RaidDeadline)) { throw new InvalidDataException("Invalid active raid."); } raidState.Tick(monotonicNow); return raidState; double ReadDeadline(string key) { long num2 = long.Parse(Text(key), CultureInfo.InvariantCulture); if (num2 == 0L) { return 0.0; } DateTime dateTime = new DateTime(num2, DateTimeKind.Utc); if ((dateTime - savedUtc).TotalSeconds > 3000.0) { throw new InvalidDataException("Checkpoint deadline exceeds maximum raid duration."); } return monotonicNow + (dateTime - utcNow.ToUniversalTime()).TotalSeconds; } string Text(string key) { return root.Element(key)?.Value ?? throw new InvalidDataException("Missing field: " + key); } } } }