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 FehuNews v420.0.1
FehuNews.dll
Decompiled a day agousing System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using FehuNews.Admin; using FehuNews.Announcements; using FehuNews.Configuration; using FehuNews.Core; using FehuNews.Monitoring; using FehuNews.Remote; using FehuNews.Sinks; using HarmonyLib; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyVersion("0.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.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace FehuNews { [BepInPlugin("forteca.fehunews", "FehuNews", "1.0.0")] public sealed class FehuNewsPlugin : BaseUnityPlugin { private Harmony _harmony; private FehuNewsConfig _config; private AnnouncementScheduler _announcements; private PlayerMonitor _players; private DayMonitor _days; private DiscordWebhookSink _discord; private RemoteAnnouncementServer _remoteAnnouncements; private FehuNewsCommands _commands; internal static FehuNewsPlugin Instance { get; private set; } internal IFehuEventPublisher Events { get; private set; } private void Awake() { //IL_0123: Unknown result type (might be due to invalid IL or missing references) //IL_012d: Expected O, but got Unknown Instance = this; _config = new FehuNewsConfig(((BaseUnityPlugin)this).Config); FehuEventManager fehuEventManager = new FehuEventManager(_config, ((BaseUnityPlugin)this).Logger); fehuEventManager.AddSink(new LoggingSink(((BaseUnityPlugin)this).Logger)); fehuEventManager.AddSink(new InGameNotificationSink(_config)); _discord = new DiscordWebhookSink(_config, ((BaseUnityPlugin)this).Logger); fehuEventManager.AddSink(_discord); Events = fehuEventManager; _announcements = new AnnouncementScheduler(_config, Events, ((BaseUnityPlugin)this).Logger); _players = new PlayerMonitor(_config, Events, ((BaseUnityPlugin)this).Logger); _days = new DayMonitor(_config, Events, ((BaseUnityPlugin)this).Logger); _remoteAnnouncements = new RemoteAnnouncementServer(_config, Events, ((BaseUnityPlugin)this).Logger); _commands = new FehuNewsCommands(_config, Events, _announcements, _remoteAnnouncements, ((BaseUnityPlugin)this).Logger); _commands.Register(); _harmony = new Harmony("forteca.fehunews"); _harmony.PatchAll(); ValidateStartupConfiguration(); _remoteAnnouncements.Start(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"FehuNews 1.0.0 loaded as a server-side event framework."); } private void Update() { (Events as FehuEventManager)?.DrainQueuedEvents(); _announcements?.Tick(); _days?.Tick(); } private void OnDestroy() { ((BaseUnityPlugin)this).Logger.LogInfo((object)"FehuNews is unloading."); _remoteAnnouncements?.Dispose(); Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } _discord?.Dispose(); (Events as FehuEventManager)?.Shutdown(); if ((Object)(object)Instance == (Object)(object)this) { Instance = null; } } internal void RefreshPlayers() { _players?.RefreshFromZNet(); } private void ValidateStartupConfiguration() { if (_config.EnableWebhook.Value && string.IsNullOrWhiteSpace(_config.WebhookUrl.Value)) { ((BaseUnityPlugin)this).Logger.LogWarning((object)"Discord webhook is enabled but WebhookUrl is empty."); } if (_config.EnableRemoteAnnouncements.Value && string.IsNullOrWhiteSpace(_config.RemoteAuthToken.Value)) { ((BaseUnityPlugin)this).Logger.LogWarning((object)"Remote announcements are enabled but RemoteAuthToken is empty; listener will not start."); } if (_config.Messages.Count == 0) { ((BaseUnityPlugin)this).Logger.LogWarning((object)"No announcement messages are configured."); } } } internal static class PluginInfo { public const string Guid = "forteca.fehunews"; public const string Name = "FehuNews"; public const string Version = "1.0.0"; } } namespace FehuNews.Sinks { internal sealed class DiscordWebhookSink : IFehuEventSink, IDisposable { private readonly FehuNewsConfig _config; private readonly ManualLogSource _logger; private readonly HttpClient _httpClient; private readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1); public DiscordWebhookSink(FehuNewsConfig config, ManualLogSource logger) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Expected O, but got Unknown _config = config; _logger = logger; _httpClient = new HttpClient(); } public async Task PublishAsync(FehuEvent fehuEvent) { if (!_config.EnableWebhook.Value || string.IsNullOrWhiteSpace(_config.WebhookUrl.Value) || !IsEnabled(fehuEvent.Type)) { return; } await _sendLock.WaitAsync().ConfigureAwait(continueOnCapturedContext: false); try { string payload = (_config.EmbedMode.Value ? BuildEmbedPayload(fehuEvent) : BuildPlainPayload(fehuEvent)); int attempts = Math.Max(1, _config.WebhookRetryCount.Value + 1); for (int attempt = 1; attempt <= attempts; attempt++) { using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(_config.WebhookTimeoutSeconds.Value))) { StringContent content = new StringContent(payload, Encoding.UTF8, "application/json"); try { int num; _ = num - 1; _ = 1; try { _logger.LogDebug((object)$"Sending Discord webhook for {fehuEvent.Type}, attempt {attempt}/{attempts}."); HttpResponseMessage response = await _httpClient.PostAsync(_config.WebhookUrl.Value, (HttpContent)(object)content, cts.Token).ConfigureAwait(continueOnCapturedContext: false); if (response.IsSuccessStatusCode) { _logger.LogDebug((object)$"Discord webhook delivered for {fehuEvent.Type}."); break; } string text = await response.Content.ReadAsStringAsync().ConfigureAwait(continueOnCapturedContext: false); _logger.LogWarning((object)$"Discord webhook failed for {fehuEvent.Type}: {(int)response.StatusCode} {response.ReasonPhrase} {text}"); } catch (Exception ex) { _logger.LogWarning((object)$"Discord webhook request failed for {fehuEvent.Type} attempt {attempt}/{attempts}: {ex.Message}"); } } finally { ((IDisposable)content)?.Dispose(); } } if (attempt < attempts) { await Task.Delay(TimeSpan.FromMilliseconds(500 * attempt)).ConfigureAwait(continueOnCapturedContext: false); } } } finally { _sendLock.Release(); } } public void Dispose() { _sendLock.Dispose(); ((HttpMessageInvoker)_httpClient).Dispose(); } private bool IsEnabled(FehuEventType type) { switch (type) { case FehuEventType.PlayerJoined: case FehuEventType.FirstTimePlayerJoined: return _config.SendJoinEvents.Value; case FehuEventType.PlayerLeft: return _config.SendLeaveEvents.Value; case FehuEventType.PlayerDied: return _config.SendDeathEvents.Value; case FehuEventType.BossSummoned: case FehuEventType.BossDefeated: return _config.SendBossEvents.Value; case FehuEventType.NewDayStarted: case FehuEventType.DayMilestone: return _config.SendDayEvents.Value; case FehuEventType.ScheduledAnnouncement: case FehuEventType.ManualAdminAnnouncement: return _config.SendAnnouncementEvents.Value; case FehuEventType.RemoteAnnouncement: return _config.SendRemoteAnnouncementEvents.Value; case FehuEventType.ServerStarted: case FehuEventType.ServerStopping: case FehuEventType.WorldLoaded: case FehuEventType.WorldSaved: return _config.SendServerEvents.Value; case FehuEventType.TerritoryCaptured: case FehuEventType.TerritoryLost: case FehuEventType.ClanCreated: case FehuEventType.ClanDisbanded: case FehuEventType.ClanWarDeclared: case FehuEventType.AchievementUnlocked: case FehuEventType.EconomyTransaction: case FehuEventType.MarketPurchase: return true; default: return false; } } private static string BuildPlainPayload(FehuEvent fehuEvent) { return "{\"content\":\"" + JsonEscape("[Voice of Odin] " + fehuEvent.Title + "\n" + fehuEvent.Message) + "\"}"; } private static string BuildEmbedPayload(FehuEvent fehuEvent) { return "{\"username\":\"FehuNews\",\"embeds\":[{\"title\":\"" + JsonEscape("Voice of Odin") + "\",\"description\":\"" + JsonEscape(fehuEvent.Message) + "\",\"color\":12087123,\"footer\":{\"text\":\"" + JsonEscape(fehuEvent.Title + " | " + fehuEvent.Context.Source) + "\"},\"timestamp\":\"" + fehuEvent.CreatedUtc.ToString("o") + "\"}]}"; } private static string JsonEscape(string value) { if (value == null) { return string.Empty; } return value.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\r", "\\r") .Replace("\n", "\\n"); } } internal sealed class InGameNotificationSink : IFehuEventSink { private readonly FehuNewsConfig _config; public InGameNotificationSink(FehuNewsConfig config) { _config = config; } public Task PublishAsync(FehuEvent fehuEvent) { if (!IsEnabled(fehuEvent) || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || ZRoutedRpc.instance == null) { return Task.CompletedTask; } ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "ShowMessage", new object[2] { 2, fehuEvent.Message }); return Task.CompletedTask; } private bool IsEnabled(FehuEvent fehuEvent) { switch (fehuEvent.Type) { case FehuEventType.ScheduledAnnouncement: return _config.EnableAnnouncements.Value; case FehuEventType.ManualAdminAnnouncement: case FehuEventType.RemoteAnnouncement: return true; case FehuEventType.PlayerJoined: case FehuEventType.FirstTimePlayerJoined: return _config.EnableJoinMessages.Value; case FehuEventType.PlayerLeft: return _config.EnableLeaveMessages.Value; case FehuEventType.PlayerDied: return _config.EnableDeathMessages.Value; case FehuEventType.BossSummoned: case FehuEventType.BossDefeated: return _config.EnableBossMessages.Value; case FehuEventType.NewDayStarted: case FehuEventType.DayMilestone: return _config.EnableDayMessages.Value; case FehuEventType.ServerStarted: case FehuEventType.ServerStopping: case FehuEventType.WorldLoaded: case FehuEventType.WorldSaved: return _config.EnableServerMessages.Value; default: return true; } } } internal sealed class LoggingSink : IFehuEventSink { private readonly ManualLogSource _logger; public LoggingSink(ManualLogSource logger) { _logger = logger; } public Task PublishAsync(FehuEvent fehuEvent) { _logger.LogInfo((object)$"Event logged: {fehuEvent.Type} | {fehuEvent.Message}"); return Task.CompletedTask; } } } namespace FehuNews.Remote { internal sealed class RemoteAnnouncementServer : IDisposable { private readonly FehuNewsConfig _config; private readonly IFehuEventPublisher _events; private readonly ManualLogSource _logger; private readonly object _lock = new object(); private HttpListener _listener; private CancellationTokenSource _cts; private Task _listenTask; private DateTime _lastAcceptedUtc = DateTime.MinValue; public bool IsRunning { get { if (_listener != null) { return _listener.IsListening; } return false; } } public RemoteAnnouncementServer(FehuNewsConfig config, IFehuEventPublisher events, ManualLogSource logger) { _config = config; _events = events; _logger = logger; } public void Start() { if (!_config.EnableRemoteAnnouncements.Value) { _logger.LogInfo((object)"Remote announcements disabled."); return; } if (string.IsNullOrWhiteSpace(_config.RemoteAuthToken.Value)) { _logger.LogWarning((object)"Remote announcements enabled but RemoteAuthToken is empty; listener will not start."); return; } lock (_lock) { if (IsRunning) { return; } string text = $"http://{_config.RemoteBindAddress.Value}:{_config.RemotePort.Value}/"; _listener = new HttpListener(); _listener.Prefixes.Add(text); _cts = new CancellationTokenSource(); try { _listener.Start(); _listenTask = Task.Run(() => ListenLoopAsync(_cts.Token)); _logger.LogInfo((object)("Remote announcement listener started at " + text + "announcement.")); } catch (Exception ex) { _logger.LogWarning((object)("Failed to start remote announcement listener at " + text + ": " + ex.Message)); Stop(); } } } public void Restart() { Stop(); Start(); } public void Stop() { lock (_lock) { try { _cts?.Cancel(); _listener?.Stop(); _listener?.Close(); } catch (Exception ex) { _logger.LogWarning((object)("Remote announcement listener stop failed: " + ex.Message)); } finally { _listener = null; _cts?.Dispose(); _cts = null; _listenTask = null; } } } public void Dispose() { Stop(); } private async Task ListenLoopAsync(CancellationToken token) { while (!token.IsCancellationRequested) { HttpListenerContext context = null; try { context = await _listener.GetContextAsync().ConfigureAwait(continueOnCapturedContext: false); Task.Run(() => HandleRequestAsync(context, token), token); } catch (ObjectDisposedException) { break; } catch (HttpListenerException) { break; } catch (Exception ex3) { _logger.LogWarning((object)("Remote announcement listener error: " + ex3.Message)); context?.Response.Close(); } } } private async Task HandleRequestAsync(HttpListenerContext context, CancellationToken token) { try { if (context.Request.HttpMethod != "POST" || context.Request.Url == null || context.Request.Url.AbsolutePath.TrimEnd(new char[1] { '/' }) != "/announcement") { await WriteResponseAsync(context.Response, 404, "Not Found").ConfigureAwait(continueOnCapturedContext: false); return; } if (!IsAuthorized(context.Request)) { _logger.LogWarning((object)$"Rejected unauthorized remote announcement from {context.Request.RemoteEndPoint}."); await WriteResponseAsync(context.Response, 401, "Unauthorized").ConfigureAwait(continueOnCapturedContext: false); return; } if (!TryRateLimit()) { _logger.LogWarning((object)$"Rate limited remote announcement from {context.Request.RemoteEndPoint}."); await WriteResponseAsync(context.Response, 429, "Rate limited").ConfigureAwait(continueOnCapturedContext: false); return; } string body; using (StreamReader reader = new StreamReader(context.Request.InputStream, context.Request.ContentEncoding ?? Encoding.UTF8)) { body = await reader.ReadToEndAsync().ConfigureAwait(continueOnCapturedContext: false); } string text = ExtractMessage(body); if (string.IsNullOrWhiteSpace(text)) { await WriteResponseAsync(context.Response, 400, "Missing message").ConfigureAwait(continueOnCapturedContext: false); return; } if (text.Length > _config.RemoteMaxMessageLength.Value) { await WriteResponseAsync(context.Response, 400, "Message too long").ConfigureAwait(continueOnCapturedContext: false); return; } _logger.LogInfo((object)$"Accepted remote announcement from {context.Request.RemoteEndPoint}: {text}"); bool flag = _events.Publish(new FehuEvent(FehuEventType.RemoteAnnouncement, "Remote Announcement", text, "remote:" + text, null, new FehuEventContext("RemoteAnnouncement"))); await WriteResponseAsync(context.Response, flag ? 202 : 409, flag ? "Queued" : "Suppressed").ConfigureAwait(continueOnCapturedContext: false); } catch (Exception ex) { _logger.LogWarning((object)("Remote announcement request failed: " + ex.Message)); if (context.Response.OutputStream.CanWrite) { await WriteResponseAsync(context.Response, 500, "Internal Server Error").ConfigureAwait(continueOnCapturedContext: false); } } } private bool IsAuthorized(HttpListenerRequest request) { string value = _config.RemoteAuthToken.Value; string text = request.Headers["Authorization"] ?? string.Empty; string text2 = request.Headers["X-FehuNews-Token"] ?? string.Empty; if (text.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) { return ConstantTimeEquals(text.Substring("Bearer ".Length).Trim(), value); } return ConstantTimeEquals(text2.Trim(), value); } private bool TryRateLimit() { lock (_lock) { DateTime utcNow = DateTime.UtcNow; if (utcNow - _lastAcceptedUtc < TimeSpan.FromSeconds(_config.RemoteRateLimitSeconds.Value)) { return false; } _lastAcceptedUtc = utcNow; return true; } } private static string ExtractMessage(string body) { if (string.IsNullOrWhiteSpace(body)) { return string.Empty; } Match match = Regex.Match(body, "\"message\"\\s*:\\s*\"(?<message>(?:\\\\.|[^\"])*)\"", RegexOptions.IgnoreCase); if (match.Success) { return Regex.Unescape(match.Groups["message"].Value).Trim(); } return body.Trim(); } private static async Task WriteResponseAsync(HttpListenerResponse response, int statusCode, string message) { byte[] bytes = Encoding.UTF8.GetBytes(message); response.StatusCode = statusCode; response.ContentType = "text/plain; charset=utf-8"; response.ContentLength64 = bytes.Length; await response.OutputStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(continueOnCapturedContext: false); response.Close(); } private static bool ConstantTimeEquals(string left, string right) { if (left == null || right == null) { return false; } int num = left.Length ^ right.Length; int num2 = Math.Min(left.Length, right.Length); for (int i = 0; i < num2; i++) { num |= left[i] ^ right[i]; } return num == 0; } } } namespace FehuNews.Patches { [HarmonyPatch(typeof(Character), "Start")] internal static class CharacterStartPatch { private static void Postfix(Character __instance) { //IL_003f: 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) if (IsServerCharacter(__instance) && __instance.IsBoss()) { string hoverName = __instance.GetHoverName(); FehuNewsPlugin instance = FehuNewsPlugin.Instance; if (instance != null) { IFehuEventPublisher events = instance.Events; string message = hoverName + " has been summoned."; ZDOID zDOID = __instance.GetZDOID(); events.Publish(new FehuEvent(FehuEventType.BossSummoned, "Boss Summoned", message, "boss-summoned:" + ((object)(ZDOID)(ref zDOID)).ToString())); } } } private static bool IsServerCharacter(Character character) { if ((Object)(object)character != (Object)null && (Object)(object)ZNet.instance != (Object)null) { return ZNet.instance.IsServer(); } return false; } } [HarmonyPatch(typeof(Character), "OnDeath")] internal static class CharacterDeathPatch { private static void Prefix(Character __instance) { //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)__instance == (Object)null || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } ZDOID zDOID; if (__instance.IsPlayer()) { string hoverName = __instance.GetHoverName(); FehuNewsPlugin instance = FehuNewsPlugin.Instance; if (instance != null) { IFehuEventPublisher events = instance.Events; string message = hoverName + " has fallen."; zDOID = __instance.GetZDOID(); events.Publish(new FehuEvent(FehuEventType.PlayerDied, "Player Died", message, "player-death:" + ((object)(ZDOID)(ref zDOID)).ToString())); } } else if (__instance.IsBoss()) { string hoverName2 = __instance.GetHoverName(); FehuNewsPlugin instance2 = FehuNewsPlugin.Instance; if (instance2 != null) { IFehuEventPublisher events2 = instance2.Events; string message2 = hoverName2 + " has been defeated."; zDOID = __instance.GetZDOID(); events2.Publish(new FehuEvent(FehuEventType.BossDefeated, "Boss Defeated", message2, "boss-defeated:" + ((object)(ZDOID)(ref zDOID)).ToString())); } } } } [HarmonyPatch(typeof(ZNet), "Start")] internal static class ZNetStartPatch { private static void Postfix(ZNet __instance) { if ((Object)(object)__instance != (Object)null && __instance.IsServer()) { FehuNewsPlugin.Instance?.Events.Publish(new FehuEvent(FehuEventType.ServerStarted, "Server Started", "The realm is awake.", "server-started")); } } } [HarmonyPatch(typeof(ZNet), "WorldSetup")] internal static class ZNetWorldSetupPatch { private static void Postfix(ZNet __instance) { if ((Object)(object)__instance != (Object)null && __instance.IsServer()) { FehuNewsPlugin.Instance?.Events.Publish(new FehuEvent(FehuEventType.WorldLoaded, "World Loaded", "World " + __instance.GetWorldName() + " has loaded.", "world-loaded:" + __instance.GetWorldName())); } } } [HarmonyPatch(typeof(ZNet), "OnDestroy")] internal static class ZNetOnDestroyPatch { private static void Prefix(ZNet __instance) { if ((Object)(object)__instance != (Object)null && __instance.IsServer()) { FehuNewsPlugin.Instance?.Events.Publish(new FehuEvent(FehuEventType.ServerStopping, "Server Stopping", "The realm is going quiet.", "server-stopping")); } } } [HarmonyPatch(typeof(ZNet), "UpdatePlayerList")] internal static class ZNetUpdatePlayerListPatch { private static void Postfix(ZNet __instance) { if ((Object)(object)__instance != (Object)null && __instance.IsServer()) { FehuNewsPlugin.Instance?.RefreshPlayers(); } } } [HarmonyPatch(typeof(ZNet), "SaveWorld")] internal static class ZNetSaveWorldPatch { private static void Postfix(ZNet __instance) { if ((Object)(object)__instance != (Object)null && __instance.IsServer()) { FehuNewsPlugin.Instance?.Events.Publish(new FehuEvent(FehuEventType.WorldSaved, "World Saved", "The world has been saved.", "world-saved")); } } } } namespace FehuNews.Monitoring { internal sealed class DayMonitor { private readonly FehuNewsConfig _config; private readonly IFehuEventPublisher _events; private readonly ManualLogSource _logger; private int _lastDay = -1; public DayMonitor(FehuNewsConfig config, IFehuEventPublisher events, ManualLogSource logger) { _config = config; _events = events; _logger = logger; } public void Tick() { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || (Object)(object)EnvMan.instance == (Object)null) { return; } int day = EnvMan.instance.GetDay(); if (day > 0 && day != _lastDay) { _lastDay = day; _logger.LogInfo((object)$"Detected new world day: {day}"); _events.Publish(new FehuEvent(FehuEventType.NewDayStarted, "New Day Started", $"Day {day} has dawned in the realm.", "day:" + day)); if (_config.Milestones.Contains(day)) { _events.Publish(new FehuEvent(FehuEventType.DayMilestone, "Day Milestone", $"The realm has endured {day} days.", "day-milestone:" + day)); } } } } internal sealed class PlayerMonitor { private readonly FehuNewsConfig _config; private readonly IFehuEventPublisher _events; private readonly ManualLogSource _logger; private readonly Dictionary<string, string> _knownOnlinePlayers = new Dictionary<string, string>(StringComparer.Ordinal); public PlayerMonitor(FehuNewsConfig config, IFehuEventPublisher events, ManualLogSource logger) { _config = config; _events = events; _logger = logger; } public void RefreshFromZNet() { //IL_0039: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.Ordinal); foreach (PlayerInfo player in ZNet.instance.GetPlayerList()) { object obj = player; string fieldValue = GetFieldValue(obj, "m_characterID"); if (!string.IsNullOrWhiteSpace(fieldValue) && !fieldValue.EndsWith(":0", StringComparison.Ordinal) && !(fieldValue == "0") && !dictionary.ContainsKey(fieldValue)) { dictionary.Add(fieldValue, GetPlayerName(obj)); } } foreach (KeyValuePair<string, string> item in dictionary) { if (!_knownOnlinePlayers.ContainsKey(item.Key)) { bool flag = !_config.KnownPlayerIds.Contains(item.Key); _knownOnlinePlayers[item.Key] = item.Value; _config.RememberPlayer(item.Key); FehuEventType type = (flag ? FehuEventType.FirstTimePlayerJoined : FehuEventType.PlayerJoined); string message = (flag ? ("The ravens announce that " + item.Value + " has entered the realm for the first time.") : ("The ravens announce that " + item.Value + " has entered the realm.")); _logger.LogInfo((object)$"Detected player join: {item.Value} ({item.Key}), firstTime={flag}"); _events.Publish(new FehuEvent(type, flag ? "First Time Player Joined" : "Player Joined", message, "join:" + item.Key)); } } KeyValuePair<string, string>[] array = _knownOnlinePlayers.ToArray(); for (int i = 0; i < array.Length; i++) { KeyValuePair<string, string> keyValuePair = array[i]; if (!dictionary.ContainsKey(keyValuePair.Key)) { _knownOnlinePlayers.Remove(keyValuePair.Key); _logger.LogInfo((object)("Detected player leave: " + keyValuePair.Value + " (" + keyValuePair.Key + ")")); _events.Publish(new FehuEvent(FehuEventType.PlayerLeft, "Player Left", keyValuePair.Value + " has left the realm.", "leave:" + keyValuePair.Key)); } } } private static string GetPlayerName(object player) { string fieldValue = GetFieldValue(player, "m_name"); if (!string.IsNullOrWhiteSpace(fieldValue)) { return fieldValue; } string fieldValue2 = GetFieldValue(player, "m_serverAssignedDisplayName"); if (!string.IsNullOrWhiteSpace(fieldValue2)) { return fieldValue2; } return "Unknown Viking"; } private static string GetFieldValue(object instance, string fieldName) { if (instance == null) { return string.Empty; } return (instance.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.Public)?.GetValue(instance))?.ToString() ?? string.Empty; } } } namespace FehuNews.Core { public enum FehuEventType { ServerStarted, ServerStopping, WorldLoaded, WorldSaved, PlayerJoined, PlayerLeft, FirstTimePlayerJoined, PlayerDied, ScheduledAnnouncement, ManualAdminAnnouncement, NewDayStarted, DayMilestone, BossSummoned, BossDefeated, RemoteAnnouncement, TerritoryCaptured, TerritoryLost, ClanCreated, ClanDisbanded, ClanWarDeclared, AchievementUnlocked, EconomyTransaction, MarketPurchase } public sealed class FehuEventContext { public string Source { get; } public string ActorName { get; } public string ActorId { get; } public string TargetName { get; } public string TargetId { get; } public IReadOnlyDictionary<string, string> Metadata { get; } public FehuEventContext(string source = null, string actorName = null, string actorId = null, string targetName = null, string targetId = null, IReadOnlyDictionary<string, string> metadata = null) { Source = (string.IsNullOrWhiteSpace(source) ? "FehuNews" : source); ActorName = actorName ?? string.Empty; ActorId = actorId ?? string.Empty; TargetName = targetName ?? string.Empty; TargetId = targetId ?? string.Empty; Metadata = metadata ?? new Dictionary<string, string>(); } public static FehuEventContext Server(string source = null) { return new FehuEventContext(source ?? "Server"); } } public sealed class FehuEvent { public FehuEventType Type { get; } public string Title { get; } public string Message { get; } public string DedupeKey { get; } public IReadOnlyDictionary<string, string> Data { get; } public FehuEventContext Context { get; } public DateTime CreatedUtc { get; } public FehuEvent(FehuEventType type, string title, string message, string dedupeKey = null, IReadOnlyDictionary<string, string> data = null, FehuEventContext context = null) { Type = type; Title = (string.IsNullOrWhiteSpace(title) ? type.ToString() : title.Trim()); Message = (string.IsNullOrWhiteSpace(message) ? string.Empty : message.Trim()); DedupeKey = dedupeKey ?? (type.ToString() + ":" + message); Data = data ?? new Dictionary<string, string>(); Context = context ?? FehuEventContext.Server(); CreatedUtc = DateTime.UtcNow; } } public sealed class FehuEventManager : IFehuEventPublisher { private static readonly object CurrentLock = new object(); private static FehuEventManager _current; private readonly object _lock = new object(); private readonly List<IFehuEventSink> _sinks = new List<IFehuEventSink>(); private readonly List<IFehuEventListener> _listeners = new List<IFehuEventListener>(); private readonly Dictionary<string, DateTime> _recentEvents = new Dictionary<string, DateTime>(StringComparer.Ordinal); private readonly Queue<FehuEvent> _pendingEvents = new Queue<FehuEvent>(); private readonly FehuNewsConfig _config; private readonly ManualLogSource _logger; private readonly int _mainThreadId; internal FehuEventManager(FehuNewsConfig config, ManualLogSource logger) { _config = config; _logger = logger; _mainThreadId = Thread.CurrentThread.ManagedThreadId; lock (CurrentLock) { _current = this; } } public static bool Publish(FehuEvent fehuEvent) { FehuEventManager current; lock (CurrentLock) { current = _current; } return current?.PublishInternal(fehuEvent) ?? false; } public static bool RegisterListener(IFehuEventListener listener) { FehuEventManager current; lock (CurrentLock) { current = _current; } if (current == null || listener == null) { return false; } current.AddListener(listener); return true; } public static bool UnregisterListener(IFehuEventListener listener) { FehuEventManager current; lock (CurrentLock) { current = _current; } if (current == null || listener == null) { return false; } current.RemoveListener(listener); return true; } bool IFehuEventPublisher.Publish(FehuEvent fehuEvent) { return PublishInternal(fehuEvent); } internal void AddSink(IFehuEventSink sink) { if (sink == null) { return; } lock (_lock) { _sinks.Add(sink); } } internal void AddListener(IFehuEventListener listener) { lock (_lock) { if (!_listeners.Contains(listener)) { _listeners.Add(listener); } } } internal void RemoveListener(IFehuEventListener listener) { lock (_lock) { _listeners.Remove(listener); } } internal void DrainQueuedEvents(int maxEvents = 25) { for (int i = 0; i < maxEvents; i++) { FehuEvent fehuEvent; lock (_lock) { if (_pendingEvents.Count == 0) { break; } fehuEvent = _pendingEvents.Dequeue(); } PublishOnMainThread(fehuEvent); } } internal void Shutdown() { lock (CurrentLock) { if (_current == this) { _current = null; } } lock (_lock) { _sinks.Clear(); _listeners.Clear(); _pendingEvents.Clear(); _recentEvents.Clear(); } } private bool PublishInternal(FehuEvent fehuEvent) { if (fehuEvent == null || string.IsNullOrWhiteSpace(fehuEvent.Message)) { _logger.LogWarning((object)"Rejected empty FehuNews event."); return false; } if (Thread.CurrentThread.ManagedThreadId != _mainThreadId) { lock (_lock) { if (_pendingEvents.Count >= _config.EventQueueLimit.Value) { _logger.LogWarning((object)$"Rejected event {fehuEvent.Type}: event queue limit reached."); return false; } _pendingEvents.Enqueue(fehuEvent); return true; } } return PublishOnMainThread(fehuEvent); } private bool PublishOnMainThread(FehuEvent fehuEvent) { TimeSpan cooldown = GetCooldown(fehuEvent.Type); if (IsDuplicate(fehuEvent.DedupeKey, cooldown)) { _logger.LogDebug((object)$"Suppressed duplicate event {fehuEvent.Type}: {fehuEvent.DedupeKey}"); return false; } IFehuEventSink[] array; IFehuEventListener[] array2; lock (_lock) { array = _sinks.ToArray(); array2 = _listeners.ToArray(); } _logger.LogInfo((object)$"Dispatching event {fehuEvent.Type}: {fehuEvent.Message}"); IFehuEventListener[] array3 = array2; foreach (IFehuEventListener fehuEventListener in array3) { try { fehuEventListener.OnFehuEvent(fehuEvent); } catch (Exception ex) { _logger.LogWarning((object)$"Event listener {fehuEventListener.GetType().Name} failed for {fehuEvent.Type}: {ex.Message}"); } } IFehuEventSink[] array4 = array; foreach (IFehuEventSink sink in array4) { PublishToSinkAsync(sink, fehuEvent); } return true; } private async Task PublishToSinkAsync(IFehuEventSink sink, FehuEvent fehuEvent) { try { await sink.PublishAsync(fehuEvent).ConfigureAwait(continueOnCapturedContext: false); } catch (Exception arg) { _logger.LogWarning((object)$"Event sink {sink.GetType().Name} failed for {fehuEvent.Type}: {arg}"); } } private bool IsDuplicate(string key, TimeSpan cooldown) { DateTime utcNow = DateTime.UtcNow; PruneRecentEvents(utcNow); lock (_lock) { if (_recentEvents.TryGetValue(key, out var value) && utcNow - value < cooldown) { return true; } _recentEvents[key] = utcNow; return false; } } private void PruneRecentEvents(DateTime now) { lock (_lock) { if (_recentEvents.Count >= 512) { string[] array = (from pair in _recentEvents where now - pair.Value > TimeSpan.FromMinutes(30.0) select pair.Key).ToArray(); foreach (string key in array) { _recentEvents.Remove(key); } } } } private TimeSpan GetCooldown(FehuEventType type) { switch (type) { case FehuEventType.PlayerJoined: case FehuEventType.PlayerLeft: case FehuEventType.FirstTimePlayerJoined: return TimeSpan.FromSeconds(_config.PlayerEventCooldownSeconds.Value); case FehuEventType.PlayerDied: return TimeSpan.FromSeconds(_config.DeathEventCooldownSeconds.Value); case FehuEventType.WorldSaved: return TimeSpan.FromSeconds(_config.SaveEventCooldownSeconds.Value); case FehuEventType.BossSummoned: case FehuEventType.BossDefeated: return TimeSpan.FromSeconds(_config.BossEventCooldownSeconds.Value); case FehuEventType.ScheduledAnnouncement: case FehuEventType.ManualAdminAnnouncement: case FehuEventType.RemoteAnnouncement: return TimeSpan.FromSeconds(_config.AnnouncementCooldownSeconds.Value); default: return TimeSpan.FromSeconds(_config.DefaultEventCooldownSeconds.Value); } } } public interface IFehuEventListener { void OnFehuEvent(FehuEvent fehuEvent); } public interface IFehuEventPublisher { bool Publish(FehuEvent fehuEvent); } public interface IFehuEventSink { Task PublishAsync(FehuEvent fehuEvent); } } namespace FehuNews.Configuration { internal sealed class FehuNewsConfig { private readonly ConfigFile _configFile; public readonly ConfigEntry<bool> EnableAnnouncements; public readonly ConfigEntry<int> AnnouncementIntervalMinutes; public readonly ConfigEntry<string> AnnouncementMessages; public readonly ConfigEntry<bool> EnableWebhook; public readonly ConfigEntry<string> WebhookUrl; public readonly ConfigEntry<int> WebhookTimeoutSeconds; public readonly ConfigEntry<int> WebhookRetryCount; public readonly ConfigEntry<bool> EnableJoinMessages; public readonly ConfigEntry<bool> EnableLeaveMessages; public readonly ConfigEntry<bool> EnableDeathMessages; public readonly ConfigEntry<bool> EnableBossMessages; public readonly ConfigEntry<bool> EnableDayMessages; public readonly ConfigEntry<bool> EnableServerMessages; public readonly ConfigEntry<string> DayMilestones; public readonly ConfigEntry<bool> EmbedMode; public readonly ConfigEntry<bool> SendJoinEvents; public readonly ConfigEntry<bool> SendLeaveEvents; public readonly ConfigEntry<bool> SendDeathEvents; public readonly ConfigEntry<bool> SendBossEvents; public readonly ConfigEntry<bool> SendDayEvents; public readonly ConfigEntry<bool> SendAnnouncementEvents; public readonly ConfigEntry<bool> SendServerEvents; public readonly ConfigEntry<bool> SendRemoteAnnouncementEvents; public readonly ConfigEntry<bool> EnableRemoteAnnouncements; public readonly ConfigEntry<string> RemoteBindAddress; public readonly ConfigEntry<int> RemotePort; public readonly ConfigEntry<string> RemoteAuthToken; public readonly ConfigEntry<int> RemoteRateLimitSeconds; public readonly ConfigEntry<int> RemoteMaxMessageLength; public readonly ConfigEntry<int> DefaultEventCooldownSeconds; public readonly ConfigEntry<int> PlayerEventCooldownSeconds; public readonly ConfigEntry<int> DeathEventCooldownSeconds; public readonly ConfigEntry<int> BossEventCooldownSeconds; public readonly ConfigEntry<int> SaveEventCooldownSeconds; public readonly ConfigEntry<int> AnnouncementCooldownSeconds; public readonly ConfigEntry<int> EventQueueLimit; private readonly ConfigEntry<string> _knownPlayerIds; public IReadOnlyList<string> Messages => ParseMessages(AnnouncementMessages.Value); public ISet<int> Milestones { get { int result; return new HashSet<int>(from value in ParseStrings(DayMilestones.Value) select int.TryParse(value, out result) ? result : 0 into day where day > 0 select day); } } public ISet<string> KnownPlayerIds => new HashSet<string>(ParseStrings(_knownPlayerIds.Value), StringComparer.Ordinal); public FehuNewsConfig(ConfigFile configFile) { //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Expected O, but got Unknown //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_00fc: Expected O, but got Unknown //IL_011f: Unknown result type (might be due to invalid IL or missing references) //IL_0129: Expected O, but got Unknown //IL_0318: Unknown result type (might be due to invalid IL or missing references) //IL_0322: Expected O, but got Unknown //IL_0369: Unknown result type (might be due to invalid IL or missing references) //IL_0373: Expected O, but got Unknown //IL_039e: Unknown result type (might be due to invalid IL or missing references) //IL_03a8: Expected O, but got Unknown //IL_03cf: Unknown result type (might be due to invalid IL or missing references) //IL_03d9: Expected O, but got Unknown //IL_0401: Unknown result type (might be due to invalid IL or missing references) //IL_040b: Expected O, but got Unknown //IL_0433: Unknown result type (might be due to invalid IL or missing references) //IL_043d: Expected O, but got Unknown //IL_0465: Unknown result type (might be due to invalid IL or missing references) //IL_046f: Expected O, but got Unknown //IL_0497: Unknown result type (might be due to invalid IL or missing references) //IL_04a1: Expected O, but got Unknown //IL_04c9: Unknown result type (might be due to invalid IL or missing references) //IL_04d3: Expected O, but got Unknown //IL_04ff: Unknown result type (might be due to invalid IL or missing references) //IL_0509: Expected O, but got Unknown _configFile = configFile; EnableAnnouncements = configFile.Bind<bool>("Announcements", "EnableAnnouncements", true, "Enable scheduled server announcements."); AnnouncementIntervalMinutes = configFile.Bind<int>("Announcements", "AnnouncementIntervalMinutes", 10, new ConfigDescription("Minutes between scheduled announcements.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 1440), Array.Empty<object>())); AnnouncementMessages = configFile.Bind<string>("Announcements", "AnnouncementMessages", "Welcome to the server!|Respect other players and their builds.|Have fun, Vikings!", "Messages separated by |, ;, real new lines, or literal \\n sequences."); EnableWebhook = configFile.Bind<bool>("Discord", "EnableWebhook", false, "Send FehuNews events to Discord."); WebhookUrl = configFile.Bind<string>("Discord", "WebhookUrl", string.Empty, "Discord webhook URL."); EmbedMode = configFile.Bind<bool>("Discord", "EmbedMode", true, "Send Viking themed Discord embeds instead of plain text."); WebhookTimeoutSeconds = configFile.Bind<int>("Discord", "WebhookTimeoutSeconds", 8, new ConfigDescription("Timeout for Discord webhook requests.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 60), Array.Empty<object>())); WebhookRetryCount = configFile.Bind<int>("Discord", "WebhookRetryCount", 2, new ConfigDescription("Number of retry attempts after a failed Discord webhook request.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 5), Array.Empty<object>())); SendJoinEvents = configFile.Bind<bool>("Discord Events", "SendJoinEvents", true, "Send player join events to Discord."); SendLeaveEvents = configFile.Bind<bool>("Discord Events", "SendLeaveEvents", true, "Send player leave events to Discord."); SendDeathEvents = configFile.Bind<bool>("Discord Events", "SendDeathEvents", true, "Send player death events to Discord."); SendBossEvents = configFile.Bind<bool>("Discord Events", "SendBossEvents", true, "Send boss events to Discord."); SendDayEvents = configFile.Bind<bool>("Discord Events", "SendDayEvents", true, "Send day events to Discord."); SendAnnouncementEvents = configFile.Bind<bool>("Discord Events", "SendAnnouncementEvents", true, "Send announcement events to Discord."); SendServerEvents = configFile.Bind<bool>("Discord Events", "SendServerEvents", true, "Send server lifecycle events to Discord."); SendRemoteAnnouncementEvents = configFile.Bind<bool>("Discord Events", "SendRemoteAnnouncementEvents", true, "Send remote announcement events to Discord."); EnableJoinMessages = configFile.Bind<bool>("In-Game Messages", "EnableJoinMessages", true, "Show join messages in game."); EnableLeaveMessages = configFile.Bind<bool>("In-Game Messages", "EnableLeaveMessages", true, "Show leave messages in game."); EnableDeathMessages = configFile.Bind<bool>("In-Game Messages", "EnableDeathMessages", true, "Show death messages in game."); EnableBossMessages = configFile.Bind<bool>("In-Game Messages", "EnableBossMessages", true, "Show boss messages in game."); EnableDayMessages = configFile.Bind<bool>("In-Game Messages", "EnableDayMessages", true, "Show day and milestone messages in game."); EnableServerMessages = configFile.Bind<bool>("In-Game Messages", "EnableServerMessages", true, "Show server lifecycle messages in game."); EnableRemoteAnnouncements = configFile.Bind<bool>("Remote Announcements", "EnableRemoteAnnouncements", false, "Enable HTTP POST /announcement for authenticated remote announcements."); RemoteBindAddress = configFile.Bind<string>("Remote Announcements", "RemoteBindAddress", "127.0.0.1", "HTTP bind address. Use 127.0.0.1 behind a reverse proxy/VPN for production."); RemotePort = configFile.Bind<int>("Remote Announcements", "RemotePort", 8095, new ConfigDescription("HTTP listener port for remote announcements.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 65535), Array.Empty<object>())); RemoteAuthToken = configFile.Bind<string>("Remote Announcements", "RemoteAuthToken", string.Empty, "Bearer token required for remote announcements. Leave empty to disable listener startup."); RemoteRateLimitSeconds = configFile.Bind<int>("Remote Announcements", "RemoteRateLimitSeconds", 5, new ConfigDescription("Minimum seconds between accepted remote announcements.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 3600), Array.Empty<object>())); RemoteMaxMessageLength = configFile.Bind<int>("Remote Announcements", "RemoteMaxMessageLength", 240, new ConfigDescription("Maximum accepted remote announcement message length.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 2000), Array.Empty<object>())); DefaultEventCooldownSeconds = configFile.Bind<int>("Anti-Spam", "DefaultEventCooldownSeconds", 5, new ConfigDescription("Default duplicate event suppression cooldown.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 3600), Array.Empty<object>())); PlayerEventCooldownSeconds = configFile.Bind<int>("Anti-Spam", "PlayerEventCooldownSeconds", 15, new ConfigDescription("Duplicate join/leave suppression cooldown.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 3600), Array.Empty<object>())); DeathEventCooldownSeconds = configFile.Bind<int>("Anti-Spam", "DeathEventCooldownSeconds", 20, new ConfigDescription("Duplicate player death suppression cooldown.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 3600), Array.Empty<object>())); BossEventCooldownSeconds = configFile.Bind<int>("Anti-Spam", "BossEventCooldownSeconds", 45, new ConfigDescription("Duplicate boss event suppression cooldown.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 3600), Array.Empty<object>())); SaveEventCooldownSeconds = configFile.Bind<int>("Anti-Spam", "SaveEventCooldownSeconds", 20, new ConfigDescription("Duplicate world save suppression cooldown.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 3600), Array.Empty<object>())); AnnouncementCooldownSeconds = configFile.Bind<int>("Anti-Spam", "AnnouncementCooldownSeconds", 10, new ConfigDescription("Duplicate announcement suppression cooldown.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 3600), Array.Empty<object>())); EventQueueLimit = configFile.Bind<int>("Anti-Spam", "EventQueueLimit", 256, new ConfigDescription("Maximum queued cross-thread events.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(10, 5000), Array.Empty<object>())); DayMilestones = configFile.Bind<string>("World", "DayMilestones", "100|500|1000", "World day milestones separated by |, ;, commas, spaces, or new lines."); _knownPlayerIds = configFile.Bind<string>("State", "KnownPlayerIds", string.Empty, "Internal FehuNews state for first-time join detection. Do not edit unless resetting history."); } public void RememberPlayer(string playerId) { if (string.IsNullOrWhiteSpace(playerId)) { return; } HashSet<string> hashSet = new HashSet<string>(KnownPlayerIds, StringComparer.Ordinal); if (hashSet.Add(playerId)) { _knownPlayerIds.Value = string.Join("|", hashSet.OrderBy<string, string>((string value) => value, StringComparer.Ordinal)); _configFile.Save(); } } public void Reload() { _configFile.Reload(); } private static IReadOnlyList<string> ParseStrings(string raw) { if (string.IsNullOrWhiteSpace(raw)) { return Array.Empty<string>(); } return (from value in raw.Replace("\\n", "\n").Split(new char[7] { '|', ';', ',', '\r', '\n', '\t', ' ' }, StringSplitOptions.RemoveEmptyEntries) select value.Trim() into value where !string.IsNullOrWhiteSpace(value) select value).ToArray(); } private static IReadOnlyList<string> ParseMessages(string raw) { if (string.IsNullOrWhiteSpace(raw)) { return Array.Empty<string>(); } return (from value in raw.Replace("\\n", "\n").Split(new char[4] { '|', ';', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) select value.Trim() into value where !string.IsNullOrWhiteSpace(value) select value).ToArray(); } } } namespace FehuNews.Announcements { internal sealed class AnnouncementScheduler { private readonly FehuNewsConfig _config; private readonly IFehuEventPublisher _events; private readonly ManualLogSource _logger; private float _nextAnnouncementTime; private int _nextMessageIndex; public AnnouncementScheduler(FehuNewsConfig config, IFehuEventPublisher events, ManualLogSource logger) { _config = config; _events = events; _logger = logger; ResetTimer(); } public void ResetTimer() { _nextAnnouncementTime = Time.time + (float)_config.AnnouncementIntervalMinutes.Value * 60f; _logger.LogDebug((object)"Announcement timer reset."); } public void Tick() { if (!_config.EnableAnnouncements.Value || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || Time.time < _nextAnnouncementTime) { return; } IReadOnlyList<string> messages = _config.Messages; if (messages.Count == 0) { ResetTimer(); return; } if (_nextMessageIndex >= messages.Count) { _nextMessageIndex = 0; } string text = messages[_nextMessageIndex++]; _logger.LogInfo((object)("Scheduled announcement selected: " + text)); _events.Publish(new FehuEvent(FehuEventType.ScheduledAnnouncement, "Scheduled Announcement", text, "announcement:" + text)); ResetTimer(); } } } namespace FehuNews.Admin { internal sealed class FehuNewsCommands { private readonly FehuNewsConfig _config; private readonly IFehuEventPublisher _events; private readonly AnnouncementScheduler _announcements; private readonly RemoteAnnouncementServer _remote; private readonly ManualLogSource _logger; public FehuNewsCommands(FehuNewsConfig config, IFehuEventPublisher events, AnnouncementScheduler announcements, RemoteAnnouncementServer remote, ManualLogSource logger) { _config = config; _events = events; _announcements = announcements; _remote = remote; _logger = logger; } public void Register() { Type type = FindType("ConsoleCommand"); Type type2 = FindType("ConsoleEvent"); if (type == null || type2 == null) { _logger.LogWarning((object)"Could not register fehunews command because Valheim console command types were not found."); return; } MethodInfo method = GetType().GetMethod("HandleCommand", BindingFlags.Instance | BindingFlags.NonPublic); Delegate @delegate = Delegate.CreateDelegate(type2, this, method); ConstructorInfo constructorInfo = FindCommandConstructor(type, type2); if (constructorInfo == null) { _logger.LogWarning((object)"Could not register fehunews command because a compatible ConsoleCommand constructor was not found."); return; } object command = constructorInfo.Invoke(new object[3] { "fehunews", "FehuNews admin commands: help, send <message>, reload, status, testwebhook", @delegate }); SetCommandFlag(command, "OnlyAdmin", value: true); SetCommandFlag(command, "OnlyServer", value: true); _logger.LogInfo((object)"Registered admin command: fehunews"); } private void HandleCommand(object args) { string propertyOrField = GetPropertyOrField(args, "FullLine"); string[] array = propertyOrField.Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (array.Length < 2) { ReplyHelp(args); return; } switch (array[1].ToLowerInvariant()) { case "help": ReplyHelp(args); break; case "send": { string text = ((propertyOrField.Length > "fehunews send ".Length) ? propertyOrField.Substring("fehunews send ".Length).Trim() : string.Empty); if (string.IsNullOrWhiteSpace(text)) { Reply(args, "Usage: fehunews send <message>"); break; } _logger.LogInfo((object)("Manual admin announcement: " + text)); _events.Publish(new FehuEvent(FehuEventType.ManualAdminAnnouncement, "Manual Admin Announcement", text, "manual:" + text)); Reply(args, "FehuNews announcement sent."); break; } case "reload": _config.Reload(); _announcements.ResetTimer(); _remote.Restart(); _logger.LogInfo((object)"FehuNews configuration reloaded by admin command."); Reply(args, "FehuNews configuration reloaded."); break; case "status": Reply(args, $"FehuNews status: announcements={_config.EnableAnnouncements.Value}, interval={_config.AnnouncementIntervalMinutes.Value}m, messages={_config.Messages.Count}, webhook={_config.EnableWebhook.Value}, remote={_remote.IsRunning}"); break; case "testwebhook": if (!_config.EnableWebhook.Value || string.IsNullOrWhiteSpace(_config.WebhookUrl.Value)) { Reply(args, "Webhook is not enabled or WebhookUrl is empty."); break; } _logger.LogInfo((object)"Discord webhook test requested by admin command."); _events.Publish(new FehuEvent(FehuEventType.ManualAdminAnnouncement, "Webhook Test", "FehuNews webhook test: the ravens can reach Discord.", "testwebhook:" + DateTime.UtcNow.Ticks, null, new FehuEventContext("AdminCommand"))); Reply(args, "FehuNews webhook test queued."); break; default: Reply(args, "Unknown FehuNews command. Use: fehunews help"); break; } } private static void ReplyHelp(object args) { Reply(args, "FehuNews commands: fehunews send <message> | fehunews reload | fehunews status | fehunews testwebhook | fehunews help"); } private static void Reply(object args, string message) { object memberValue = GetMemberValue(args, "Context"); (memberValue?.GetType().GetMethod("AddString", new Type[1] { typeof(string) }))?.Invoke(memberValue, new object[1] { message }); } private static void SetCommandFlag(object command, string name, bool value) { command.GetType().GetField(name, BindingFlags.Instance | BindingFlags.Public)?.SetValue(command, value); } private static Type FindType(string typeName) { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); for (int i = 0; i < assemblies.Length; i++) { Type type = assemblies[i].GetType(typeName, throwOnError: false); if (type != null) { return type; } } return null; } private static ConstructorInfo FindCommandConstructor(Type commandType, Type eventType) { ConstructorInfo[] constructors = commandType.GetConstructors(BindingFlags.Instance | BindingFlags.Public); foreach (ConstructorInfo constructorInfo in constructors) { ParameterInfo[] parameters = constructorInfo.GetParameters(); if (parameters.Length == 3 && parameters[0].ParameterType == typeof(string) && parameters[1].ParameterType == typeof(string) && parameters[2].ParameterType == eventType) { return constructorInfo; } } return null; } private static string GetPropertyOrField(object instance, string name) { return GetMemberValue(instance, name)?.ToString() ?? string.Empty; } private static object GetMemberValue(object instance, string name) { if (instance == null) { return null; } Type type = instance.GetType(); PropertyInfo property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public); if (property != null) { return property.GetValue(instance, null); } return type.GetField(name, BindingFlags.Instance | BindingFlags.Public)?.GetValue(instance); } } }