Decompiled source of FehuNews v420.0.1

FehuNews.dll

Decompiled a day ago
using 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);
		}
	}
}