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,
};
}
}
}