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 Gjallar v0.1.1
plugins/Gjallar.dll
Decompiled a week agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Splatform; 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("Guys Being Vikings")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.1.1.0")] [assembly: AssemblyInformationalVersion("0.1.1+ffb1466a1e4cc908a7a349b1ed0925b0566d24af")] [assembly: AssemblyProduct("Gjallar")] [assembly: AssemblyTitle("Gjallar")] [assembly: AssemblyVersion("0.1.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 GBV.Gjallar { internal static class Announce { private const string RpcShowMessage = "ShowMessage"; private const string RpcChatMessage = "ChatMessage"; private static readonly Vector3 FarBelow = new Vector3(0f, -10000f, 0f); internal static void ToEveryone(string text) { if (string.IsNullOrEmpty(text)) { return; } ZRoutedRpc instance = ZRoutedRpc.instance; if (instance == null) { Safety.Warn("Announce.NoRpc", "there is no network yet, so '" + text + "' went nowhere."); return; } bool flag = false; if (GjallarPlugin.CentreScreen.Value) { flag |= SendCentre(instance, text); } if (GjallarPlugin.ChatLine.Value) { flag |= SendChat(instance, text); } if (flag) { ManualLogSource log = GjallarPlugin.Log; if (log != null) { log.LogInfo((object)("Gjallar: announced \"" + text + "\" " + Audience(instance) + ".")); } } else { ManualLogSource log2 = GjallarPlugin.Log; if (log2 != null) { log2.LogWarning((object)("Gjallar: \"" + text + "\" was not sent by any route. Both CentreScreen and ChatLine are off, or every route refused.")); } } } private static string Audience(ZRoutedRpc rpc) { try { List<ZNetPeer> list = AccessTools.FieldRefAccess<ZRoutedRpc, List<ZNetPeer>>("m_peers").Invoke(rpc); int num = 0; int num2 = 0; if (list != null) { num = list.Count; for (int i = 0; i < list.Count; i++) { if (list[i] != null && list[i].IsReady()) { num2++; } } } int num3 = 0; int num4 = 0; if ((Object)(object)ZNet.instance != (Object)null) { List<ZNetPeer> peers = ZNet.instance.GetPeers(); if (peers != null) { num3 = peers.Count; for (int j = 0; j < peers.Count; j++) { if (peers[j] != null && peers[j].IsReady()) { num4++; } } } } return "to " + num2 + " of " + num + " routed peer(s) [ZNet sees " + num4 + " of " + num3 + "]"; } catch (Exception ex) { Safety.Failed("Announce.Audience", ex); return "(could not read the peer list)"; } } private static bool SendCentre(ZRoutedRpc rpc, string text) { try { rpc.InvokeRoutedRPC(0L, "ShowMessage", new object[2] { 2, text }); return true; } catch (Exception ex) { Safety.Failed("Announce.SendCentre", ex); return false; } } private static bool SendChat(ZRoutedRpc rpc, string text) { //IL_001d: Unknown result type (might be due to invalid IL or missing references) try { UserInfo val = BorrowedIdentity(); if (val == null) { return false; } rpc.InvokeRoutedRPC(0L, "ChatMessage", new object[4] { FarBelow, 2, val, text }); return true; } catch (Exception ex) { Safety.Failed("Announce.SendChat", ex); return false; } } private static UserInfo BorrowedIdentity() { //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Expected O, but got Unknown ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { return null; } List<PlayerInfo> playerList = instance.GetPlayerList(); if (playerList == null || playerList.Count == 0) { return null; } for (int i = 0; i < playerList.Count; i++) { PlatformUserID id = playerList[i].m_userInfo.m_id; if (((PlatformUserID)(ref id)).IsValid) { return new UserInfo { Name = GjallarPlugin.ChatName.Value, UserId = id }; } } Safety.Warn("Announce.NoIdentity", "nobody connected has a usable platform ID, so restart notices will appear on screen but not in chat. Nothing is broken; the chat route needs to borrow a real player's ID to get past the game's own permission check."); return null; } } internal static class Client { private static bool _checked; private static bool _hasUi; internal static bool HasUi { get { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Invalid comparison between Unknown and I4 if (_checked) { return _hasUi; } _checked = true; try { _hasUi = (int)SystemInfo.graphicsDeviceType != 4; } catch (Exception ex) { _hasUi = false; Safety.Failed("Client.HasUi", ex); } if (!_hasUi) { ManualLogSource log = GjallarPlugin.Log; if (log != null) { log.LogInfo((object)"Gjallar: no graphics device, so this is a dedicated server. Watching for the restart signal."); } } return _hasUi; } } } [HarmonyPatch(typeof(Terminal), "InitTerminal")] public static class Commands { private static bool _registered; [HarmonyPostfix] public static void Postfix() { try { if (!_registered) { _registered = true; Register(); } } catch (Exception ex) { Safety.Failed("Terminal.InitTerminal command registration", ex); } } private static void Register() { Cmd("gjallar_status", "gjallar_status - where the signal file is expected, whether a countdown is running, and which marks are still to come."); Cmd("gjallar_test", "gjallar_test <seconds> [reason] - announce a countdown that ends in <seconds>, without writing a signal file. Nothing is stopped or restarted."); Cmd("gjallar_cancel", "gjallar_cancel - drop the countdown that is running. Announces nothing, and does not stop the restart itself - only whatever schedules it can do that."); } private static void Cmd(string name, string help) { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Expected O, but got Unknown //IL_002a: Unknown result type (might be due to invalid IL or missing references) new ConsoleCommand(name, help, (ConsoleEvent)delegate(ConsoleEventArgs args) { try { Terminal context = args.Context; if (context != null) { context.AddString(Dispatch(name, args.Args)); } } catch (Exception ex) { Safety.Failed(name, ex); Terminal context2 = args.Context; if (context2 != null) { context2.AddString("Gjallar: " + name + " failed. See the log."); } } }, false, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); } private static string Dispatch(string verb, string[] args) { if ((Object)(object)ZNet.instance == (Object)null) { return "Gjallar: load into a world first."; } if (!ZNet.instance.IsServer()) { return "Gjallar: this machine is not the server, so it has no countdown to show. Gjallar runs on the server only, and clients need nothing installed."; } if (!ZNet.instance.LocalPlayerIsAdminOrHost()) { return "Gjallar: you need to be an admin to do that."; } return verb switch { "gjallar_status" => Status(), "gjallar_test" => Test(args), "gjallar_cancel" => Cancel(), _ => "Gjallar: unknown command '" + verb + "'.", }; } private static string Status() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("Gjallar 0.1.1" + (GjallarPlugin.Enabled.Value ? "" : " (DISABLED in the config)")); string text = Watcher.LastPath ?? Watcher.ResolvePath(); stringBuilder.AppendLine(" signal file: " + text); stringBuilder.AppendLine(" marks: " + GjallarPlugin.Marks.Value); stringBuilder.AppendLine(" routes: " + (GjallarPlugin.CentreScreen.Value ? "centre screen" : "") + ((GjallarPlugin.CentreScreen.Value && GjallarPlugin.ChatLine.Value) ? " + " : "") + (GjallarPlugin.ChatLine.Value ? "chat" : "") + ((!GjallarPlugin.CentreScreen.Value && !GjallarPlugin.ChatLine.Value) ? "NONE" : "")); stringBuilder.AppendLine(" clock: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " server local time"); stringBuilder.AppendLine(" daily: " + Schedule.Describe()); DateTime? dateTime = Schedule.Next(); if (dateTime.HasValue) { TimeSpan timeSpan = dateTime.Value - DateTime.Now; stringBuilder.AppendLine(" next: " + dateTime.Value.ToString("yyyy-MM-dd HH:mm:ss") + ", in " + (int)timeSpan.TotalHours + "h " + timeSpan.Minutes + "m (announced, NOT performed - check this against your own timer)"); } if (!Countdown.Armed) { stringBuilder.AppendLine(" nothing armed. Write a signal file, or run gjallar_test 95 Mod update."); return stringBuilder.ToString(); } stringBuilder.AppendLine(" ARMED: " + Math.Round(Countdown.SecondsLeft) + "s left" + (string.IsNullOrEmpty(Countdown.Reason) ? "" : (" (" + Countdown.Reason + ")"))); return stringBuilder.ToString(); } private static string Test(string[] args) { if (args == null || args.Length == 0) { return "Gjallar: give a number of seconds, for example gjallar_test 95 Mod update."; } if (!int.TryParse(args[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) || result < 0) { return "Gjallar: '" + args[0] + "' is not a number of seconds."; } if ((long)result > 86400L) { return "Gjallar: " + result + "s is further ahead than Gjallar will schedule (" + 86400L + "s)."; } string reason = ((args.Length > 1) ? string.Join(" ", args, 1, args.Length - 1) : string.Empty); Countdown.Arm(result, reason); return "Gjallar: armed a test countdown of " + result + "s. Nothing will be stopped."; } private static string Cancel() { if (!Countdown.Armed) { return "Gjallar: nothing is armed."; } float secondsLeft = Countdown.SecondsLeft; Countdown.Disarm("an admin cancelled it"); return "Gjallar: dropped the countdown with " + Math.Round(secondsLeft) + "s left. Players were told nothing, and if a real restart is still scheduled it will still happen - Gjallar only ever announced it."; } } internal static class Countdown { internal const long MaxLeadSeconds = 86400L; internal const long MaxStaleSeconds = 60L; private const double PickupEchoSeconds = 10.0; private const float ZeroLeadSeconds = 2f; private static bool _armed; private static string _reason; private static float _deadline; private static readonly List<int> Remaining = new List<int>(); internal static bool Armed => _armed; internal static string Reason => _reason; internal static float SecondsLeft { get { if (!_armed) { return 0f; } return _deadline - Time.realtimeSinceStartup; } } internal static void Arm(double seconds, string reason) { if (_armed) { ManualLogSource log = GjallarPlugin.Log; if (log != null) { log.LogInfo((object)("Gjallar: a new signal replaced the countdown already running (" + Math.Round(SecondsLeft) + "s left).")); } } _armed = true; _reason = Clean(reason); _deadline = Time.realtimeSinceStartup + (float)seconds; Remaining.Clear(); foreach (int item in Marks()) { if ((double)item < seconds) { Remaining.Add(item); } } ManualLogSource log2 = GjallarPlugin.Log; if (log2 != null) { log2.LogInfo((object)("Gjallar: restart in " + Math.Round(seconds) + "s" + (string.IsNullOrEmpty(_reason) ? "" : (" (" + _reason + ")")) + ". Calling " + Remaining.Count + " mark(s): " + string.Join(", ", Remaining.ToArray()) + ".")); } if (GjallarPlugin.AnnounceOnPickup.Value && !PickupWouldEcho(seconds)) { Announce.ToEveryone(Compose((int)Math.Round(seconds))); } } private static bool PickupWouldEcho(double seconds) { if (Remaining.Count > 0) { return seconds - (double)Remaining[0] <= 10.0; } return false; } internal static void Disarm(string why) { if (_armed) { _armed = false; _reason = null; Remaining.Clear(); ManualLogSource log = GjallarPlugin.Log; if (log != null) { log.LogInfo((object)("Gjallar: countdown dropped because " + why + ".")); } } } internal static void Tick() { if (!_armed) { return; } if (!GjallarPlugin.Enabled.Value) { Disarm("Gjallar was switched off partway through"); return; } float secondsLeft = SecondsLeft; while (Remaining.Count > 0 && secondsLeft <= (float)Remaining[0]) { int seconds = Remaining[0]; Remaining.RemoveAt(0); Announce.ToEveryone(Compose(seconds)); } if (Remaining.Count <= 0 && !(secondsLeft > 2f)) { if (GjallarPlugin.AnnounceAtZero.Value) { Announce.ToEveryone(ComposeZero()); } _armed = false; _reason = null; ManualLogSource log = GjallarPlugin.Log; if (log != null) { log.LogInfo((object)"Gjallar: the countdown is finished. The server stops when its own scheduler stops it; Gjallar has nothing further to do."); } } } private static string Compose(int seconds) { return Fill(GjallarPlugin.Template.Value, seconds.ToString(CultureInfo.InvariantCulture)); } private static string ComposeZero() { return Fill(GjallarPlugin.ZeroTemplate.Value, "0"); } private static string Fill(string template, string seconds) { return (template ?? string.Empty).Replace("{seconds}", seconds).Replace("{reason}", _reason ?? string.Empty).Trim(' ', '-', ':', ',', '–', '—'); } internal static string Clean(string reason) { if (string.IsNullOrEmpty(reason)) { return string.Empty; } string text = reason.Replace('<', ' ').Replace('>', ' ').Replace('$', ' ') .Replace('\r', ' ') .Replace('\n', ' ') .Trim(); if (text.Length > 120) { text = text.Substring(0, 120).TrimEnd() + "..."; } return text; } internal static int LargestMark() { List<int> list = Marks(); if (list.Count <= 0) { return 0; } return list[0]; } private static List<int> Marks() { List<int> list = new List<int>(); string[] array = (GjallarPlugin.Marks.Value ?? string.Empty).Split(new char[3] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries); foreach (string text in array) { if (!int.TryParse(text.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { Safety.Warn("Countdown.BadMark", "'" + text.Trim() + "' in the Marks setting is not a whole number of seconds, so it was ignored. The rest of the marks still work."); } else if (result > 0 && !list.Contains(result)) { list.Add(result); } } if (list.Count == 0) { Safety.Warn("Countdown.NoMarks", "the Marks setting produced no usable countdown marks, so only the pickup and zero announcements will be sent."); } list.Sort((int a, int b) => b.CompareTo(a)); return list; } } [BepInPlugin("gbv.valheim.gjallar", "Gjallar", "0.1.1")] public class GjallarPlugin : BaseUnityPlugin { public const string PluginGuid = "gbv.valheim.gjallar"; public const string PluginName = "Gjallar"; public const string PluginVersion = "0.1.1"; internal static ManualLogSource Log; internal static ConfigEntry<bool> Enabled; internal static ConfigEntry<string> SignalFile; internal static ConfigEntry<float> PollSeconds; internal static ConfigEntry<string> Marks; internal static ConfigEntry<bool> AnnounceOnPickup; internal static ConfigEntry<bool> AnnounceAtZero; internal static ConfigEntry<string> DailyRestarts; internal static ConfigEntry<string> ScheduledReason; internal static ConfigEntry<string> Template; internal static ConfigEntry<string> ZeroTemplate; internal static ConfigEntry<bool> CentreScreen; internal static ConfigEntry<bool> ChatLine; internal static ConfigEntry<string> ChatName; private Harmony _harmony; private bool _sceneWasLoaded; private void Awake() { //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Expected O, but got Unknown //IL_01ea: Unknown result type (might be due to invalid IL or missing references) //IL_01f4: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; Enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Master switch. Turning it off stops Gjallar watching for the signal file and stops it announcing anything, without uninstalling. It has no effect on whether the server actually restarts - Gjallar never had a say in that."); SignalFile = ((BaseUnityPlugin)this).Config.Bind<string>("General", "SignalFile", "", "Full path to the file Gjallar watches for. Leave this EMPTY and it uses restart.signal in the folder BepInEx sits in, which is the game's own folder on a normal install. Empty rather than a written-out path on purpose: an absolute path baked into this file the first time it is written would point at the wrong place the moment the server is moved or reinstalled."); PollSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("General", "PollSeconds", 2f, new ConfigDescription("How often to look for the signal file. Two seconds is plenty - this only paces the check for a NEW signal, and once a countdown is running the marks are timed every frame regardless of this setting.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 30f), Array.Empty<object>())); Marks = ((BaseUnityPlugin)this).Config.Bind<string>("Countdown", "Marks", "90,60,30,10,5", "Seconds remaining at which to announce, separated by commas. Order does not matter and duplicates are ignored. A mark already behind the deadline when the signal arrives is skipped rather than fired late, so a restart signalled with forty-five seconds to go does not open by claiming ninety."); AnnounceOnPickup = ((BaseUnityPlugin)this).Config.Bind<bool>("Countdown", "AnnounceOnPickup", true, "Announce once immediately when the signal is picked up, stating the real time remaining. This is what covers a signal that arrives between two marks - without it, a restart signalled with forty-five seconds left says nothing at all until the thirty second mark."); AnnounceAtZero = ((BaseUnityPlugin)this).Config.Bind<bool>("Countdown", "AnnounceAtZero", true, "Send a final message when the countdown reaches zero. Whether anybody sees it depends on how promptly the server is actually stopped."); DailyRestarts = ((BaseUnityPlugin)this).Config.Bind<string>("Schedule", "DailyRestarts", "", "Times of day the server is expected to restart, as 24-hour HH:MM separated by commas - for example 00:00,12:00 for midnight and midday. Empty means Gjallar only ever announces what a signal file tells it to.\nThese are in the SERVER'S OWN local time, which is also the clock its systemd timer or cron entry keeps, so daylight saving is handled by the machine rather than by arithmetic.\nGJALLAR DOES NOT PERFORM THESE RESTARTS. It only announces them, so these times have to match whatever actually takes the server down. Gjallar prints the next one at startup and in gjallar_status so a disagreement is visible."); ScheduledReason = ((BaseUnityPlugin)this).Config.Bind<string>("Schedule", "ScheduledReason", "Scheduled restart", "The reason announced for a restart that came from DailyRestarts rather than from a signal file."); Template = ((BaseUnityPlugin)this).Config.Bind<string>("Messages", "Template", "Server restarting in {seconds} seconds - {reason}", "The countdown message. {seconds} becomes the number of seconds remaining and {reason} becomes the text after the '|' in the signal file. If the signal carried no reason, {reason} empties and any separator left dangling on the end is trimmed, so the line still reads properly."); ZeroTemplate = ((BaseUnityPlugin)this).Config.Bind<string>("Messages", "ZeroTemplate", "Server restarting now - {reason}", "The message sent when the countdown reaches zero. Same tokens as Template."); CentreScreen = ((BaseUnityPlugin)this).Config.Bind<bool>("Messages", "CentreScreen", true, "Show each announcement in the middle of the screen, the way the game announces a boss or a forced save. This is the route that matters: it needs nothing from the client and it cannot be missed. Each one is also filed in the player's own message log, so it survives being read late."); ChatLine = ((BaseUnityPlugin)this).Config.Bind<bool>("Messages", "ChatLine", false, "Also put each announcement in the chat window, where it stays in the scrollback after the centre message has faded. OFF by default, because in practice it is not the quiet second copy it sounds like: the game renders a shout in yellow capitals across the bottom left, which next to the centre message reads as the same warning shouted twice.\nIt is also best-effort even when switched on. The game will not display a chat message unless it comes from a real platform account and a dedicated server does not have one, so Gjallar borrows the ID of somebody already connected. On an empty server, or if that fails, the centre message is sent on its own and nothing is lost."); ChatName = ((BaseUnityPlugin)this).Config.Bind<string>("Messages", "ChatName", "Server", "The name shown against the chat line."); _harmony = new Harmony("gbv.valheim.gjallar"); _harmony.PatchAll(typeof(GjallarPlugin).Assembly); bool hasUi = Client.HasUi; Log.LogInfo((object)("Gjallar: watching for " + Watcher.ResolvePath() + (hasUi ? " (this process can draw, so it is a client or a listen host)" : ""))); DateTime? dateTime = Schedule.Next(); if (dateTime.HasValue) { Log.LogInfo((object)("Gjallar: next scheduled restart " + dateTime.Value.ToString("yyyy-MM-dd HH:mm:ss") + " server local time, which is now " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + ". Gjallar announces this restart; it does not perform it.")); } Log.LogInfo((object)"Gjallar 0.1.1 loaded."); } private void Update() { bool flag = (Object)(object)ZNetScene.instance != (Object)null; if (_sceneWasLoaded && !flag) { try { Countdown.Disarm("the world unloaded"); Watcher.Reset(); Schedule.Reset(); } catch (Exception ex) { Safety.Failed("world unload cleanup", ex); } } _sceneWasLoaded = flag; try { Watcher.Tick(); } catch (Exception ex2) { Safety.Failed("Watcher.Tick", ex2); } try { Schedule.Tick(); } catch (Exception ex3) { Safety.Failed("Schedule.Tick", ex3); } try { Countdown.Tick(); } catch (Exception ex4) { Safety.Failed("Countdown.Tick", ex4); } } private void OnDestroy() { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } } internal static class Safety { private static readonly HashSet<string> Reported = new HashSet<string>(StringComparer.Ordinal); public static void Failed(string where, Exception ex) { if (Reported.Add(where)) { ManualLogSource log = GjallarPlugin.Log; if (log != null) { log.LogError((object)("Gjallar: " + where + " threw and was ignored, so nothing was announced. This is a bug in Gjallar; the restart itself is unaffected because systemd stops the server, not this mod. It will not be reported again this session.\n" + ex)); } } } public static void Warn(string where, string message) { if (Reported.Add(where)) { ManualLogSource log = GjallarPlugin.Log; if (log != null) { log.LogWarning((object)("Gjallar: " + message)); } } } public static void Forget(string where) { Reported.Remove(where); } } internal static class Schedule { private const float CheckSeconds = 1f; private static float _nextCheck; private static DateTime _armedFor = DateTime.MinValue; private static string _parsedFrom; private static List<TimeSpan> _times = new List<TimeSpan>(); internal static DateTime? Next() { List<TimeSpan> list = Times(); if (list.Count == 0) { return null; } DateTime now = DateTime.Now; DateTime dateTime = DateTime.MaxValue; for (int i = 0; i < list.Count; i++) { DateTime dateTime2 = now.Date.Add(list[i]); if (dateTime2 <= now) { dateTime2 = dateTime2.AddDays(1.0); } if (dateTime2 < dateTime) { dateTime = dateTime2; } } if (!(dateTime == DateTime.MaxValue)) { return dateTime; } return null; } internal static void Reset() { _nextCheck = 0f; _armedFor = DateTime.MinValue; } internal static void Tick() { if (!GjallarPlugin.Enabled.Value || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || Time.realtimeSinceStartup < _nextCheck) { return; } _nextCheck = Time.realtimeSinceStartup + 1f; DateTime? dateTime = Next(); if (!dateTime.HasValue || Countdown.Armed || _armedFor == dateTime.Value) { return; } int num = Countdown.LargestMark(); if (num <= 0) { Safety.Warn("Schedule.NoLead", "restart times are configured but there are no usable countdown marks, so there is no point at which to start announcing. Check the Marks setting."); return; } double totalSeconds = (dateTime.Value - DateTime.Now).TotalSeconds; if (!(totalSeconds > (double)num)) { _armedFor = dateTime.Value; Countdown.Arm(Math.Max(0.0, totalSeconds), GjallarPlugin.ScheduledReason.Value); } } private static List<TimeSpan> Times() { string text = GjallarPlugin.DailyRestarts.Value ?? string.Empty; if (string.Equals(text, _parsedFrom, StringComparison.Ordinal)) { return _times; } _parsedFrom = text; List<TimeSpan> list = new List<TimeSpan>(); string[] array = text.Split(new char[2] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < array.Length; i++) { string text2 = array[i].Trim(); if (text2.Length != 0) { if (!TryParseTimeOfDay(text2, out var time)) { Safety.Warn("Schedule.BadTime." + text2, "'" + text2 + "' in the DailyRestarts setting is not a time of day. Use 24-hour HH:MM, for example 00:00,12:00. The other times still work."); } else if (!list.Contains(time)) { list.Add(time); } } } list.Sort(); _times = list; ManualLogSource log = GjallarPlugin.Log; if (log != null) { log.LogInfo((object)((list.Count == 0) ? "Gjallar: no daily restart times configured." : ("Gjallar: " + list.Count + " daily restart time(s) configured: " + Describe() + " (server local time)."))); } return _times; } private static bool TryParseTimeOfDay(string text, out TimeSpan time) { time = TimeSpan.Zero; string[] array = text.Split(':'); if (array.Length != 2) { return false; } if (!int.TryParse(array[0].Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { return false; } if (!int.TryParse(array[1].Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var result2)) { return false; } if (result < 0 || result > 23) { return false; } if (result2 < 0 || result2 > 59) { return false; } time = new TimeSpan(result, result2, 0); return true; } internal static string Describe() { List<TimeSpan> list = Times(); if (list.Count == 0) { return "none"; } string[] array = new string[list.Count]; for (int i = 0; i < list.Count; i++) { array[i] = list[i].Hours.ToString("00", CultureInfo.InvariantCulture) + ":" + list[i].Minutes.ToString("00", CultureInfo.InvariantCulture); } return string.Join(", ", array); } } internal static class Watcher { private const int FailuresBeforeGivingUp = 5; private const string SignalFileName = "restart.signal"; private const char ByteOrderMark = '\ufeff'; private static float _nextPoll; private static int _failures; private static long _handledDeadline; internal static string LastPath { get; private set; } internal static void Reset() { _nextPoll = 0f; _failures = 0; _handledDeadline = 0L; } internal static string ResolvePath() { string value = GjallarPlugin.SignalFile.Value; if (!string.IsNullOrEmpty(value) && value.Trim().Length > 0) { return value.Trim(); } string text = Paths.BepInExRootPath; try { DirectoryInfo parent = Directory.GetParent(text); if (parent != null) { text = parent.FullName; } } catch (Exception ex) { Safety.Failed("Watcher.ResolvePath", ex); } return Path.Combine(text, "restart.signal"); } internal static void Tick() { if (!GjallarPlugin.Enabled.Value || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || Time.realtimeSinceStartup < _nextPoll) { return; } _nextPoll = Time.realtimeSinceStartup + Mathf.Max(0.5f, GjallarPlugin.PollSeconds.Value); string text = (LastPath = ResolvePath()); if (!FileIsThere(text)) { return; } string text3 = ReadOrNull(text); if (text3 == null) { return; } if (!TryParse(text3, out var deadline, out var reason)) { OnUnparseable(text); } else { if (deadline == _handledDeadline) { return; } _failures = 0; long num = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); long num2 = deadline - num; if (num2 < -60) { ManualLogSource log = GjallarPlugin.Log; if (log != null) { log.LogWarning((object)("Gjallar: the signal at " + text + " is " + -num2 + "s out of date, so it is a leftover from a restart that already happened. Removing it without announcing anything.")); } Handled(text, deadline); } else if (num2 > 86400) { ManualLogSource log2 = GjallarPlugin.Log; if (log2 != null) { log2.LogError((object)("Gjallar: the signal at " + text + " asks for a restart " + num2 + "s from now, which is further ahead than Gjallar will schedule. The usual cause is milliseconds written where the format wants seconds. Removing it without announcing anything.")); } Handled(text, deadline); } else { Handled(text, deadline); Countdown.Arm(Math.Max(0L, num2), reason); Safety.Forget("Watcher.Unparseable"); Safety.Forget("Watcher.DeleteFailed"); Safety.Forget("Announce.NoIdentity"); } } } private static bool FileIsThere(string path) { try { return File.Exists(path); } catch (Exception ex) { Safety.Failed("Watcher.FileIsThere", ex); return false; } } private static string ReadOrNull(string path) { try { return File.ReadAllText(path); } catch (IOException) { return null; } catch (UnauthorizedAccessException ex2) { Safety.Failed("Watcher.ReadOrNull/permissions", ex2); return null; } catch (Exception ex3) { Safety.Failed("Watcher.ReadOrNull", ex3); return null; } } private static void Handled(string path, long deadline) { _handledDeadline = deadline; _failures = 0; try { File.Delete(path); } catch (Exception ex) { Safety.Failed("Watcher.DeleteFailed", ex); ManualLogSource log = GjallarPlugin.Log; if (log != null) { log.LogError((object)("Gjallar: could not delete " + path + ". This countdown will still run, but the file needs to be writable by the account the server runs as, or the next restart signal will be ignored as a duplicate of this one.")); } } } private static void OnUnparseable(string path) { _failures++; if (_failures < 5) { return; } Safety.Warn("Watcher.Unparseable", "the signal at " + path + " has not been readable for " + _failures + " polls, so it is malformed rather than half-written. Expected one line of <unix-epoch-seconds>|<reason>, for example 1790312345|Mod update. Removing it."); _failures = 0; try { File.Delete(path); } catch (Exception ex) { Safety.Failed("Watcher.OnUnparseable delete", ex); } } internal static bool TryParse(string text, out long deadline, out string reason) { deadline = 0L; reason = string.Empty; if (string.IsNullOrEmpty(text)) { return false; } string text2 = null; string[] array = text.Split('\n'); for (int i = 0; i < array.Length; i++) { string text3 = array[i].Trim().Trim('\ufeff').Trim(); if (text3.Length > 0) { text2 = text3; break; } } if (text2 == null) { return false; } int num = text2.IndexOf('|'); string obj = ((num >= 0) ? text2.Substring(0, num) : text2); if (num >= 0) { reason = text2.Substring(num + 1).Trim(); } return long.TryParse(obj.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out deadline); } } }