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() : _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 d__6 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public HourlyMessageScheduler <>4__this; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public 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> _getMessages; private readonly Action _dispatch; private readonly Action _logInfo; private readonly HashSet _firedKeys = new HashSet(StringComparer.Ordinal); private DateTime _lastPruneUtc = DateTime.MinValue; public HourlyMessageScheduler(Func> getMessages, Action dispatch, Action logInfo) { _getMessages = getMessages; _dispatch = dispatch; _logInfo = logInfo; } [IteratorStateMachine(typeof(d__6))] public IEnumerator Run() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__6(0) { <>4__this = this }; } private void Tick(DateTime localNow) { string text = localNow.ToString("yyyyMMddHH"); IReadOnlyList 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 d__11 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public int seconds; public string text; private int 5__2; private int 5__3; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public 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; 5__2 = Mathf.Max(0, Mathf.CeilToInt((float)seconds / 2f) - 1); 5__3 = 0; break; case 1: <>1__state = -1; BroadcastCenterAlert(text); 5__3++; break; } if (5__3 < 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 _logInfo; private readonly Action _logWarning; private readonly MonoBehaviour _coroutineHost; public BroadcastDispatcher(Action logInfo, Action 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(d__11))] private IEnumerator RepeatAlertForDuration(string text, int seconds) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__11(0) { text = text, seconds = seconds }; } } } namespace AutoBroadcast.Config { internal sealed class MessageConfigService : IDisposable { private readonly ConfigFile _configFile; private readonly Action _logInfo; private readonly Action _logWarning; private readonly object _sync = new object(); private FileSystemWatcher? _watcher; private Timer? _reloadDebounceTimer; private ConfigEntry? _enableBroadcastingEntry; private ConfigEntry? _broadcastCountEntry; private volatile ScheduledMessage[] _messages = Array.Empty(); private volatile bool _isEnabled = true; public IReadOnlyList Messages => _messages; public bool IsEnabled => _isEnabled; public MessageConfigService(ConfigFile configFile, Action logInfo, Action logWarning) { _configFile = configFile; _logInfo = logInfo; _logWarning = logWarning; } public void Initialize() { _enableBroadcastingEntry = _configFile.Bind("Default Settings", "EnableBroadcasting", true, "Enable or disable scheduled automatic broadcasts."); _broadcastCountEntry = _configFile.Bind("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(); return; } _isEnabled = _enableBroadcastingEntry.Value; int num = Math.Max(0, _broadcastCountEntry.Value); List list = new List(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 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 val = _configFile.Bind(sectionName, "MessageText", GetDefaultMessageText(sectionName), "The message to be displayed."); ConfigEntry val2 = _configFile.Bind(sectionName, "MessageType", GetDefaultMessageType(sectionName), "The message type (Chat, Alert, or Both)."); ConfigEntry obj = _configFile.Bind(sectionName, "ScheduledMinuteOfHour", GetDefaultMessageMinute(sectionName), "Minute of each hour to broadcast (0-59)."); ConfigEntry val3 = _configFile.Bind(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(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, }; } } }