Decompiled source of NerdyGamerTools AutoBroadcaster v1.0.1

NGT.AutoBroadcaster.dll

Decompiled a month ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Threading;
using AutoBroadcast.Config;
using AutoBroadcast.Messaging;
using AutoBroadcast.Models;
using AutoBroadcast.Patches;
using AutoBroadcast.Scheduling;
using BepInEx;
using BepInEx.Configuration;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Splatform;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyCompany("Nerdy Gamer Tools")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyDescription("Server-side scheduled broadcast mod for Valheim.")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("Nerdy Gamer Tools AutoBroadcaster")]
[assembly: AssemblyTitle("NGT.AutoBroadcaster")]
[assembly: AssemblyVersion("1.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.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 AutoBroadcast
{
	[BepInPlugin("NGT_Autobroadcaster", "Nerdy Gamer Tools AutoBroadcaster", "1.0.0")]
	public sealed class AutoBroadcastPlugin : BaseUnityPlugin
	{
		public const string PluginGuid = "NGT_Autobroadcaster";

		public const string PluginName = "Nerdy Gamer Tools AutoBroadcaster";

		public const string PluginVersion = "1.0.0";

		private MessageConfigService? _configService;

		private HourlyMessageScheduler? _scheduler;

		private Harmony? _harmony;

		private void Awake()
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_0010: Expected O, but got Unknown
			_harmony = new Harmony("NGT_Autobroadcaster");
			_harmony.PatchAll(typeof(ZLogFilterPatch).Assembly);
			_configService = new MessageConfigService(((BaseUnityPlugin)this).Config, delegate(string message)
			{
				((BaseUnityPlugin)this).Logger.LogInfo((object)message);
			}, delegate(string message)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)message);
			});
			_configService.Initialize();
			BroadcastDispatcher @object = new BroadcastDispatcher(delegate(string message)
			{
				((BaseUnityPlugin)this).Logger.LogInfo((object)message);
			}, delegate(string message)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)message);
			}, (MonoBehaviour)(object)this);
			_scheduler = new HourlyMessageScheduler(() => (!_configService.IsEnabled) ? Array.Empty<ScheduledMessage>() : _configService.Messages, @object.Dispatch, delegate(string message)
			{
				((BaseUnityPlugin)this).Logger.LogInfo((object)message);
			});
			((MonoBehaviour)this).StartCoroutine(_scheduler.Run());
			((BaseUnityPlugin)this).Logger.LogInfo((object)"Nerdy Gamer Tools AutoBroadcaster initialized. Server-side scheduler is running.");
		}

		private void OnDestroy()
		{
			Harmony? harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
			_harmony = null;
			_configService?.Dispose();
			_configService = null;
		}
	}
}
namespace AutoBroadcast.Scheduling
{
	internal sealed class HourlyMessageScheduler
	{
		[CompilerGenerated]
		private sealed class <Run>d__6 : IEnumerator<object>, IDisposable, IEnumerator
		{
			private int <>1__state;

			private object <>2__current;

			public HourlyMessageScheduler <>4__this;

			object IEnumerator<object>.Current
			{
				[DebuggerHidden]
				get
				{
					return <>2__current;
				}
			}

			object IEnumerator.Current
			{
				[DebuggerHidden]
				get
				{
					return <>2__current;
				}
			}

			[DebuggerHidden]
			public <Run>d__6(int <>1__state)
			{
				this.<>1__state = <>1__state;
			}

			[DebuggerHidden]
			void IDisposable.Dispose()
			{
				<>1__state = -2;
			}

			private bool MoveNext()
			{
				//IL_0036: Unknown result type (might be due to invalid IL or missing references)
				//IL_0040: Expected O, but got Unknown
				int num = <>1__state;
				HourlyMessageScheduler hourlyMessageScheduler = <>4__this;
				if (num != 0)
				{
					if (num != 1)
					{
						return false;
					}
					<>1__state = -1;
					hourlyMessageScheduler.Tick(DateTime.Now);
				}
				else
				{
					<>1__state = -1;
					hourlyMessageScheduler.Tick(DateTime.Now);
				}
				float secondsUntilNextMinute = GetSecondsUntilNextMinute(DateTime.Now);
				<>2__current = (object)new WaitForSecondsRealtime(secondsUntilNextMinute);
				<>1__state = 1;
				return true;
			}

			bool IEnumerator.MoveNext()
			{
				//ILSpy generated this explicit interface implementation from .override directive in MoveNext
				return this.MoveNext();
			}

			[DebuggerHidden]
			void IEnumerator.Reset()
			{
				throw new NotSupportedException();
			}
		}

		private readonly Func<IReadOnlyList<ScheduledMessage>> _getMessages;

		private readonly Action<ScheduledMessage> _dispatch;

		private readonly Action<string> _logInfo;

		private readonly HashSet<string> _firedKeys = new HashSet<string>(StringComparer.Ordinal);

		private DateTime _lastPruneUtc = DateTime.MinValue;

		public HourlyMessageScheduler(Func<IReadOnlyList<ScheduledMessage>> getMessages, Action<ScheduledMessage> dispatch, Action<string> logInfo)
		{
			_getMessages = getMessages;
			_dispatch = dispatch;
			_logInfo = logInfo;
		}

		[IteratorStateMachine(typeof(<Run>d__6))]
		public IEnumerator Run()
		{
			//yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()
			return new <Run>d__6(0)
			{
				<>4__this = this
			};
		}

		private void Tick(DateTime localNow)
		{
			string text = localNow.ToString("yyyyMMddHH");
			IReadOnlyList<ScheduledMessage> readOnlyList = _getMessages();
			for (int i = 0; i < readOnlyList.Count; i++)
			{
				ScheduledMessage scheduledMessage = readOnlyList[i];
				if (scheduledMessage.Minute == localNow.Minute)
				{
					string item = $"{text}:{scheduledMessage.MessageId}:{scheduledMessage.Method}:{scheduledMessage.Minute}";
					if (!_firedKeys.Contains(item))
					{
						_firedKeys.Add(item);
						_dispatch(scheduledMessage);
					}
				}
			}
			PruneOldState(localNow);
		}

		private static float GetSecondsUntilNextMinute(DateTime now)
		{
			float num = (float)(new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, 0).AddMinutes(1.0) - now).TotalSeconds;
			return Mathf.Max(0.25f, num + 0.05f);
		}

		private void PruneOldState(DateTime localNow)
		{
			DateTime utcNow = DateTime.UtcNow;
			if (!(utcNow - _lastPruneUtc < TimeSpan.FromHours(2.0)))
			{
				_lastPruneUtc = utcNow;
				string keepPrefix = localNow.ToString("yyyyMMddHH");
				_firedKeys.RemoveWhere((string key) => !key.StartsWith(keepPrefix, StringComparison.Ordinal));
				_logInfo($"Scheduler state pruned. Active execution keys: {_firedKeys.Count}");
			}
		}
	}
}
namespace AutoBroadcast.Patches
{
	[HarmonyPatch(typeof(ZLog), "Log")]
	internal static class ZLogFilterPatch
	{
		private const string SuppressedSnippet = "Failed to get player info for player Steam_0";

		private static bool Prefix(object o)
		{
			return !ShouldSuppress(o);
		}

		private static bool ShouldSuppress(object o)
		{
			string text = o?.ToString();
			if (!string.IsNullOrEmpty(text))
			{
				return text.IndexOf("Failed to get player info for player Steam_0", StringComparison.OrdinalIgnoreCase) >= 0;
			}
			return false;
		}
	}
}
namespace AutoBroadcast.Models
{
	internal enum DeliveryMethod
	{
		Alert,
		Chat,
		Both
	}
	internal sealed class ScheduledMessage
	{
		public string MessageId { get; }

		public int Minute { get; }

		public DeliveryMethod Method { get; }

		public string Text { get; }

		public int AlertDurationSeconds { get; }

		public ScheduledMessage(string messageId, int minute, DeliveryMethod method, string text, int alertDurationSeconds)
		{
			if (string.IsNullOrWhiteSpace(messageId))
			{
				throw new ArgumentException("Message ID must not be empty.", "messageId");
			}
			if ((minute < 0 || minute > 59) ? true : false)
			{
				throw new ArgumentOutOfRangeException("minute", "Minute must be between 0 and 59.");
			}
			if (string.IsNullOrWhiteSpace(text))
			{
				throw new ArgumentException("Message text must not be empty.", "text");
			}
			if (alertDurationSeconds < 1)
			{
				throw new ArgumentOutOfRangeException("alertDurationSeconds", "Alert duration must be at least 1 second.");
			}
			MessageId = messageId.Trim();
			Minute = minute;
			Method = method;
			Text = text.Trim();
			AlertDurationSeconds = alertDurationSeconds;
		}
	}
}
namespace AutoBroadcast.Messaging
{
	internal sealed class BroadcastDispatcher
	{
		[CompilerGenerated]
		private sealed class <RepeatAlertForDuration>d__11 : IEnumerator<object>, IDisposable, IEnumerator
		{
			private int <>1__state;

			private object <>2__current;

			public int seconds;

			public string text;

			private int <repeats>5__2;

			private int <i>5__3;

			object IEnumerator<object>.Current
			{
				[DebuggerHidden]
				get
				{
					return <>2__current;
				}
			}

			object IEnumerator.Current
			{
				[DebuggerHidden]
				get
				{
					return <>2__current;
				}
			}

			[DebuggerHidden]
			public <RepeatAlertForDuration>d__11(int <>1__state)
			{
				this.<>1__state = <>1__state;
			}

			[DebuggerHidden]
			void IDisposable.Dispose()
			{
				<>1__state = -2;
			}

			private bool MoveNext()
			{
				//IL_0046: Unknown result type (might be due to invalid IL or missing references)
				//IL_0050: Expected O, but got Unknown
				switch (<>1__state)
				{
				default:
					return false;
				case 0:
					<>1__state = -1;
					<repeats>5__2 = Mathf.Max(0, Mathf.CeilToInt((float)seconds / 2f) - 1);
					<i>5__3 = 0;
					break;
				case 1:
					<>1__state = -1;
					BroadcastCenterAlert(text);
					<i>5__3++;
					break;
				}
				if (<i>5__3 < <repeats>5__2)
				{
					<>2__current = (object)new WaitForSecondsRealtime(2f);
					<>1__state = 1;
					return true;
				}
				return false;
			}

			bool IEnumerator.MoveNext()
			{
				//ILSpy generated this explicit interface implementation from .override directive in MoveNext
				return this.MoveNext();
			}

			[DebuggerHidden]
			void IEnumerator.Reset()
			{
				throw new NotSupportedException();
			}
		}

		private readonly Action<string> _logInfo;

		private readonly Action<string> _logWarning;

		private readonly MonoBehaviour _coroutineHost;

		public BroadcastDispatcher(Action<string> logInfo, Action<string> logWarning, MonoBehaviour coroutineHost)
		{
			_logInfo = logInfo;
			_logWarning = logWarning;
			_coroutineHost = coroutineHost;
		}

		public void Dispatch(ScheduledMessage message)
		{
			if (message != null && ZNet.instance != null && ZNet.instance.IsServer())
			{
				switch (message.Method)
				{
				case DeliveryMethod.Alert:
					DispatchAlert(message);
					break;
				case DeliveryMethod.Chat:
					DispatchChat(message);
					break;
				case DeliveryMethod.Both:
					DispatchAlert(message);
					DispatchChat(message);
					break;
				default:
					_logWarning($"Unknown delivery method: {message.Method}");
					break;
				}
			}
		}

		private void DispatchAlert(ScheduledMessage message)
		{
			try
			{
				BroadcastCenterAlert(message.Text);
				if (message.AlertDurationSeconds > 2)
				{
					_coroutineHost.StartCoroutine(RepeatAlertForDuration(message.Text, message.AlertDurationSeconds));
				}
				_logInfo("Broadcasted alert: " + message.Text);
			}
			catch (Exception ex)
			{
				_logWarning("Failed to broadcast alert message: " + ex.Message);
			}
		}

		private void DispatchChat(ScheduledMessage message)
		{
			try
			{
				BroadcastConsoleStyledChatMessage(message.Text, message.Text);
				_logInfo("Broadcasted chat: " + message.Text);
			}
			catch (Exception arg)
			{
				_logWarning($"Failed to broadcast chat message: {arg}");
			}
		}

		private static void BroadcastCenterAlert(string text)
		{
			ZRoutedRpc instance = ZRoutedRpc.instance;
			if (instance != null)
			{
				instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "ShowMessage", new object[2] { 2, text });
			}
		}

		private static void BroadcastConsoleStyledChatMessage(string formattedText, string fallbackText)
		{
			//IL_0035: Unknown result type (might be due to invalid IL or missing references)
			if (ZRoutedRpc.instance == null)
			{
				Debug.Log((object)("[AutoBroadcaster] " + fallbackText));
				return;
			}
			UserInfo val = CreateServerConsoleUserInfo();
			ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "ChatMessage", new object[4]
			{
				Vector3.zero,
				1,
				val,
				formattedText
			});
		}

		private static UserInfo CreateServerConsoleUserInfo()
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_0010: Unknown result type (might be due to invalid IL or missing references)
			//IL_0011: Unknown result type (might be due to invalid IL or missing references)
			//IL_0016: Unknown result type (might be due to invalid IL or missing references)
			//IL_001c: Expected O, but got Unknown
			return new UserInfo
			{
				Name = "Server",
				UserId = CreateServerSenderId()
			};
		}

		private static PlatformUserID CreateServerSenderId()
		{
			//IL_000e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0020: Unknown result type (might be due to invalid IL or missing references)
			//IL_001e: Unknown result type (might be due to invalid IL or missing references)
			PlatformUserID result = default(PlatformUserID);
			if (PlatformUserID.TryParse("Steam_0", ref result))
			{
				return result;
			}
			PlatformUserID result2 = default(PlatformUserID);
			if (PlatformUserID.TryParse("PlayFab_0", ref result2))
			{
				return result2;
			}
			return PlatformUserID.None;
		}

		[IteratorStateMachine(typeof(<RepeatAlertForDuration>d__11))]
		private IEnumerator RepeatAlertForDuration(string text, int seconds)
		{
			//yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()
			return new <RepeatAlertForDuration>d__11(0)
			{
				text = text,
				seconds = seconds
			};
		}
	}
}
namespace AutoBroadcast.Config
{
	internal sealed class MessageConfigService : IDisposable
	{
		private readonly ConfigFile _configFile;

		private readonly Action<string> _logInfo;

		private readonly Action<string> _logWarning;

		private readonly object _sync = new object();

		private FileSystemWatcher? _watcher;

		private Timer? _reloadDebounceTimer;

		private ConfigEntry<bool>? _enableBroadcastingEntry;

		private ConfigEntry<int>? _broadcastCountEntry;

		private volatile ScheduledMessage[] _messages = Array.Empty<ScheduledMessage>();

		private volatile bool _isEnabled = true;

		public IReadOnlyList<ScheduledMessage> Messages => _messages;

		public bool IsEnabled => _isEnabled;

		public MessageConfigService(ConfigFile configFile, Action<string> logInfo, Action<string> logWarning)
		{
			_configFile = configFile;
			_logInfo = logInfo;
			_logWarning = logWarning;
		}

		public void Initialize()
		{
			_enableBroadcastingEntry = _configFile.Bind<bool>("Default Settings", "EnableBroadcasting", true, "Enable or disable scheduled automatic broadcasts.");
			_broadcastCountEntry = _configFile.Bind<int>("Default Settings", "AutoBroadcastCount", 3, "The number of message sections to be loaded from config.");
			LoadMessagesFromConfig();
			ConfigureWatcher();
		}

		public void Dispose()
		{
			lock (_sync)
			{
				_reloadDebounceTimer?.Dispose();
				_reloadDebounceTimer = null;
				if (_watcher != null)
				{
					_watcher.Changed -= OnConfigFileChanged;
					_watcher.Created -= OnConfigFileChanged;
					_watcher.Renamed -= OnConfigFileChanged;
					_watcher.Dispose();
					_watcher = null;
				}
			}
		}

		private void ConfigureWatcher()
		{
			string configFilePath = _configFile.ConfigFilePath;
			string directoryName = Path.GetDirectoryName(configFilePath);
			string fileName = Path.GetFileName(configFilePath);
			if (string.IsNullOrWhiteSpace(directoryName) || string.IsNullOrWhiteSpace(fileName))
			{
				_logWarning("Could not configure config file watcher. Live reload is disabled.");
				return;
			}
			lock (_sync)
			{
				_watcher = new FileSystemWatcher(directoryName, fileName)
				{
					NotifyFilter = (NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.CreationTime),
					IncludeSubdirectories = false,
					EnableRaisingEvents = true
				};
				_watcher.Changed += OnConfigFileChanged;
				_watcher.Created += OnConfigFileChanged;
				_watcher.Renamed += OnConfigFileChanged;
				_reloadDebounceTimer = new Timer(delegate
				{
					ReloadFromDisk();
				}, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
			}
		}

		private void OnConfigFileChanged(object sender, FileSystemEventArgs e)
		{
			lock (_sync)
			{
				_reloadDebounceTimer?.Change(TimeSpan.FromMilliseconds(300.0), Timeout.InfiniteTimeSpan);
			}
		}

		private void ReloadFromDisk()
		{
			try
			{
				_configFile.Reload();
				LoadMessagesFromConfig();
				_logInfo($"Config reloaded. Parsed {_messages.Length} scheduled messages.");
			}
			catch (Exception ex)
			{
				_logWarning("Failed to reload config from disk: " + ex.Message);
			}
		}

		private void LoadMessagesFromConfig()
		{
			if (_broadcastCountEntry == null || _enableBroadcastingEntry == null)
			{
				_messages = Array.Empty<ScheduledMessage>();
				return;
			}
			_isEnabled = _enableBroadcastingEntry.Value;
			int num = Math.Max(0, _broadcastCountEntry.Value);
			List<ScheduledMessage> list = new List<ScheduledMessage>(num);
			for (int i = 1; i <= num; i++)
			{
				string text = $"Message {i:00}";
				if (!TryLoadMessageSection(text, out ScheduledMessage message2, out string error))
				{
					_logWarning("Skipped invalid section '" + text + "': " + error);
				}
				else
				{
					list.Add(message2);
				}
			}
			_messages = list.OrderBy((ScheduledMessage message) => message.Minute).ThenBy<ScheduledMessage, string>((ScheduledMessage message) => message.MessageId, StringComparer.Ordinal).ThenBy((ScheduledMessage message) => message.Method)
				.ToArray();
		}

		private bool TryLoadMessageSection(string sectionName, out ScheduledMessage? message, out string error)
		{
			message = null;
			error = string.Empty;
			ConfigEntry<string> val = _configFile.Bind<string>(sectionName, "MessageText", GetDefaultMessageText(sectionName), "The message to be displayed.");
			ConfigEntry<string> val2 = _configFile.Bind<string>(sectionName, "MessageType", GetDefaultMessageType(sectionName), "The message type (Chat, Alert, or Both).");
			ConfigEntry<int> obj = _configFile.Bind<int>(sectionName, "ScheduledMinuteOfHour", GetDefaultMessageMinute(sectionName), "Minute of each hour to broadcast (0-59).");
			ConfigEntry<int> val3 = _configFile.Bind<int>(sectionName, "MessageTime", 5, "Amount of time the Alert stays on screen (in seconds).");
			int value = obj.Value;
			if ((value < 0 || value > 59) ? true : false)
			{
				error = "Minute must be a number between 0 and 59.";
				return false;
			}
			if (!Enum.TryParse<DeliveryMethod>(val2.Value.Trim(), ignoreCase: true, out var result))
			{
				error = "MessageType must be Alert, Chat, or Both.";
				return false;
			}
			string text = val.Value.Trim();
			if (string.IsNullOrWhiteSpace(text))
			{
				error = "MessageText is empty.";
				return false;
			}
			if (val3.Value < 1)
			{
				error = "MessageTime must be at least 1.";
				return false;
			}
			message = new ScheduledMessage(sectionName, value, result, text, val3.Value);
			return true;
		}

		private static string GetDefaultMessageText(string sectionName)
		{
			return sectionName switch
			{
				"Message 01" => "Welcome to the server! Be respectful and have fun.", 
				"Message 02" => "Join Our Discord To Be Part of The Community.", 
				"Message 03" => "Thank-You For Downloading. Please Edit Your Configs.", 
				_ => "Edit this message.", 
			};
		}

		private static string GetDefaultMessageType(string sectionName)
		{
			return sectionName switch
			{
				"Message 01" => "Alert", 
				"Message 02" => "Chat", 
				"Message 03" => "Both", 
				_ => "Chat", 
			};
		}

		private static int GetDefaultMessageMinute(string sectionName)
		{
			return sectionName switch
			{
				"Message 01" => 15, 
				"Message 02" => 30, 
				"Message 03" => 45, 
				_ => 0, 
			};
		}
	}
}