Decompiled source of Bigfrost ServerPortal v1.1.0

BepInEx\plugins\Bigfrost_ServerPortal\Bigfrost_ServerPortal.dll

Decompiled 2 months ago
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Security.Cryptography;
using System.Security.Permissions;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using Bifrostheim.Helpers;
using Bifrostheim.Systems.Discord;
using Bifrostheim.Systems.Web;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

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

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

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[CompilerGenerated]
	[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 Bifrostheim
{
	[BepInPlugin("com.bigai.bigfrost_serverportal", "Bigfrost_ServerPortal", "1.1.0")]
	public class BifrostheimPlugin : BaseUnityPlugin
	{
		public const string PluginGUID = "com.bigai.bigfrost_serverportal";

		public const string PluginName = "Bigfrost_ServerPortal";

		public const string PluginVersion = "1.1.0";

		public static ConfigEntry<bool> EnableWebPortal = null;

		public static ConfigEntry<int> WebPortalPort = null;

		internal static ConfigEntry<string> WebAdminPassword = null;

		public static ConfigEntry<bool> VerboseLogging = null;

		public static ConfigEntry<float> PlayerHealthPollInterval = null;

		public static ConfigEntry<string> LifecycleRestartMode = null;

		public static ConfigEntry<string> LifecycleScriptPath = null;

		public static ConfigEntry<bool> DailyRestartEnabled = null;

		public static ConfigEntry<string> DailyRestartTime = null;

		public static ConfigEntry<bool> DiscordEnabled = null;

		public static ConfigEntry<string> DiscordWebhookUrl = null;

		public static ConfigEntry<string> DiscordOverrideChatWebhookUrl = null;

		public static ConfigEntry<string> DiscordOverrideAdminWebhookUrl = null;

		public static ConfigEntry<string> DiscordBotUsername = null;

		public static ConfigEntry<string> DiscordBotAvatarUrl = null;

		public static ConfigEntry<bool> DiscordUseRichEmbeds = null;

		public static ConfigEntry<bool> DiscordNotifyPlayerJoin = null;

		public static ConfigEntry<bool> DiscordNotifyPlayerLeave = null;

		public static ConfigEntry<bool> DiscordNotifyPlayerDeath = null;

		public static ConfigEntry<bool> DiscordNotifyServerLifecycle = null;

		public static ConfigEntry<bool> DiscordNotifyWorldEvents = null;

		public static ConfigEntry<bool> DiscordNotifyBossMilestones = null;

		public static ConfigEntry<bool> DiscordNotifyAdminActions = null;

		public static ConfigEntry<bool> DiscordNotifyChatShouts = null;

		private static float _fpsAccumulator = 60f;

		private static bool _isClient;

		private static BepInExLogListener? _logListener;

		public static BifrostheimPlugin Instance { get; private set; } = null;

		public static ManualLogSource Log { get; private set; } = null;

		public static int CurrentFps { get; private set; } = 60;

		private void Awake()
		{
			//IL_00f4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00fe: Expected O, but got Unknown
			//IL_0383: Unknown result type (might be due to invalid IL or missing references)
			//IL_0389: Invalid comparison between Unknown and I4
			//IL_03a0: Unknown result type (might be due to invalid IL or missing references)
			Instance = this;
			Log = ((BaseUnityPlugin)this).Logger;
			try
			{
				Log.LogInfo((object)"══════════════════════════════════════════");
				Log.LogInfo((object)"  Bigfrost_ServerPortal v1.1.0 loading...");
				Log.LogInfo((object)"══════════════════════════════════════════");
				EnableWebPortal = ((BaseUnityPlugin)this).Config.Bind<bool>("WebPortal", "EnableWebPortal", true, "Enable the embedded web management portal.");
				WebPortalPort = ((BaseUnityPlugin)this).Config.Bind<int>("WebPortal", "WebPortalPort", 8080, "Port for the embedded web management portal.");
				WebAdminPassword = ((BaseUnityPlugin)this).Config.Bind<string>("WebPortal", "WebAdminPassword", string.Empty, "Password required for administrative actions in the web portal.");
				VerboseLogging = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "VerboseLogging", false, "Enable verbose logging in BepInEx console.");
				PlayerHealthPollInterval = ((BaseUnityPlugin)this).Config.Bind<float>("General", "PlayerHealthPollInterval", 1f, new ConfigDescription("Interval in seconds between player health and death monitoring polls. Default is 1.0s.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.2f, 5f), Array.Empty<object>()));
				LifecycleRestartMode = ((BaseUnityPlugin)this).Config.Bind<string>("Lifecycle", "RestartMode", "ExitOnly", "Server restart strategy: ExitOnly or SpawnProcess.");
				LifecycleScriptPath = ((BaseUnityPlugin)this).Config.Bind<string>("Lifecycle", "RestartScriptPath", "./start_server.sh", "Path to external restart script when RestartMode is SpawnProcess.");
				DailyRestartEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Lifecycle", "DailyRestartEnabled", false, "Enable automated daily server restart.");
				DailyRestartTime = ((BaseUnityPlugin)this).Config.Bind<string>("Lifecycle", "DailyRestartTime", "04:00", "Daily restart time in 24h format (HH:mm).");
				DiscordEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("DiscordWebhook", "Enabled", false, "Enable Discord Webhook notifications.");
				DiscordWebhookUrl = ((BaseUnityPlugin)this).Config.Bind<string>("DiscordWebhook", "WebhookUrl", string.Empty, "Primary Discord Webhook URL for server notifications.");
				DiscordOverrideChatWebhookUrl = ((BaseUnityPlugin)this).Config.Bind<string>("DiscordWebhook", "OverrideChatWebhookUrl", string.Empty, "Optional dedicated webhook URL for in-game chat/shout relay.");
				DiscordOverrideAdminWebhookUrl = ((BaseUnityPlugin)this).Config.Bind<string>("DiscordWebhook", "OverrideAdminWebhookUrl", string.Empty, "Optional dedicated webhook URL for admin punishment alerts.");
				DiscordBotUsername = ((BaseUnityPlugin)this).Config.Bind<string>("DiscordWebhook", "BotUsername", "Bifrostheim Herald", "Display name used for Discord webhook messages.");
				DiscordBotAvatarUrl = ((BaseUnityPlugin)this).Config.Bind<string>("DiscordWebhook", "BotAvatarUrl", string.Empty, "Avatar image URL used for Discord webhook messages.");
				DiscordUseRichEmbeds = ((BaseUnityPlugin)this).Config.Bind<bool>("DiscordWebhook", "UseRichEmbeds", true, "Send rich Discord embeds (set to false for minimalist markdown plain text).");
				DiscordNotifyPlayerJoin = ((BaseUnityPlugin)this).Config.Bind<bool>("DiscordWebhook", "NotifyPlayerJoin", true, "Send notification when a player joins.");
				DiscordNotifyPlayerLeave = ((BaseUnityPlugin)this).Config.Bind<bool>("DiscordWebhook", "NotifyPlayerLeave", true, "Send notification when a player leaves.");
				DiscordNotifyPlayerDeath = ((BaseUnityPlugin)this).Config.Bind<bool>("DiscordWebhook", "NotifyPlayerDeath", true, "Send notification when a player dies or is slain in PvP.");
				DiscordNotifyServerLifecycle = ((BaseUnityPlugin)this).Config.Bind<bool>("DiscordWebhook", "NotifyServerLifecycle", true, "Send notification on server startup, shutdown, and restart warnings.");
				DiscordNotifyWorldEvents = ((BaseUnityPlugin)this).Config.Bind<bool>("DiscordWebhook", "NotifyWorldEvents", true, "Send notification when raids start and end.");
				DiscordNotifyBossMilestones = ((BaseUnityPlugin)this).Config.Bind<bool>("DiscordWebhook", "NotifyBossMilestones", true, "Send notification when a Forsaken boss is summoned or defeated.");
				DiscordNotifyAdminActions = ((BaseUnityPlugin)this).Config.Bind<bool>("DiscordWebhook", "NotifyAdminActions", true, "Send notification when admin kicks or bans a player.");
				DiscordNotifyChatShouts = ((BaseUnityPlugin)this).Config.Bind<bool>("DiscordWebhook", "NotifyChatShouts", false, "Relay in-game /s shouts to Discord.");
				if ((int)SystemInfo.graphicsDeviceType != 4)
				{
					_isClient = true;
					Log.LogWarning((object)(string.Format("[{0}] Client environment detected (GraphicsDevice: {1}). ", "Bigfrost_ServerPortal", SystemInfo.graphicsDeviceType) + "Bigfrost_ServerPortal is designed exclusively for Valheim dedicated servers. The embedded web management server will not be started on client instances."));
					return;
				}
				if (string.IsNullOrWhiteSpace(WebAdminPassword.Value) || string.Equals(WebAdminPassword.Value.Trim(), "admin", StringComparison.OrdinalIgnoreCase))
				{
					string value = GenerateRandomPassword();
					WebAdminPassword.Value = value;
					((BaseUnityPlugin)this).Config.Save();
					Log.LogWarning((object)"[Bigfrost_ServerPortal] Generated new secure random admin password. Please check configuration file.");
				}
				Log.LogInfo((object)"[Bigfrost_ServerPortal] Web Portal active. Admin password is configured in BepInEx/config/com.bigai.bigfrost_serverportal.cfg [WebPortal.WebAdminPassword]");
				ConfigSyncManager.OnlinePlayerChecker = ZNetHelper.IsPlayerOnline;
				_logListener = new BepInExLogListener();
				Logger.Listeners.Add((ILogListener)(object)_logListener);
				DiscordWebhookDispatcher.Initialize();
				DiscordEventPatches.ApplyPatches();
				if (EnableWebPortal.Value)
				{
					WebPortalServer.Start(WebPortalPort.Value, WebAdminPassword.Value);
				}
				Log.LogInfo((object)"[Bigfrost_ServerPortal] Initialized successfully.");
			}
			catch (Exception arg)
			{
				Log.LogError((object)string.Format("[{0}] Failed to initialize: {1}", "Bigfrost_ServerPortal", arg));
			}
		}

		private void Update()
		{
			if (!_isClient)
			{
				float unscaledDeltaTime = Time.unscaledDeltaTime;
				if (unscaledDeltaTime > 0.0001f)
				{
					_fpsAccumulator += (1f / unscaledDeltaTime - _fpsAccumulator) * 0.1f;
					CurrentFps = (int)Math.Round(_fpsAccumulator);
				}
				MainThreadDispatcher.PumpQueue();
				WebApiRouter.TickLifecycle();
				DiscordEventPatches.TickPlayerHealthMonitoring(unscaledDeltaTime);
			}
		}

		private void OnDestroy()
		{
			if (!_isClient)
			{
				if (DiscordEnabled != null && DiscordEnabled.Value)
				{
					DiscordWebhookDispatcher.OnServerLifecycle("\ud83d\uded1 Server Shutting Down", "The dedicated server is shutting down. Safe travels!", 15158332);
				}
				DiscordEventPatches.RemovePatches();
				DiscordWebhookDispatcher.Shutdown();
				WebPortalServer.Stop();
				if (_logListener != null)
				{
					Logger.Listeners.Remove((ILogListener)(object)_logListener);
					_logListener.Dispose();
					_logListener = null;
				}
				Log.LogInfo((object)"[Bigfrost_ServerPortal] Unloaded.");
			}
		}

		private static string GenerateRandomPassword(int length = 16)
		{
			int num = 256 / "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789".Length * "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789".Length;
			char[] array = new char[length];
			byte[] array2 = new byte[length * 2];
			int num2 = array2.Length;
			using (RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create())
			{
				for (int i = 0; i < length; i++)
				{
					byte b;
					do
					{
						if (num2 >= array2.Length)
						{
							randomNumberGenerator.GetBytes(array2);
							num2 = 0;
						}
						b = array2[num2++];
					}
					while (b >= num);
					array[i] = "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"[b % "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789".Length];
				}
			}
			return new string(array);
		}
	}
}
namespace Bifrostheim.Systems.Web
{
	public static class WebApiRouter
	{
		private const int MaxRequestBodySizeBytes = 1048576;

		private static readonly HashSet<string> _blockedConsoleCommands = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
		{
			"devcommands", "debugmode", "god", "ghost", "dpspath", "resetwind", "killall", "spawn", "fly", "nocost",
			"raiseskill"
		};

		private static readonly DateTime StartTime = DateTime.UtcNow;

		private static readonly LinkedList<ConsoleLogEntry> LogsBuffer = new LinkedList<ConsoleLogEntry>();

		private static readonly object LogLock = new object();

		private static readonly ConcurrentDictionary<string, (int attempts, DateTime lockoutUntil)> _loginRateLimits = new ConcurrentDictionary<string, (int, DateTime)>(StringComparer.OrdinalIgnoreCase);

		private static readonly ConcurrentDictionary<string, DateTime> _activeSessions = new ConcurrentDictionary<string, DateTime>(StringComparer.Ordinal);

		private static ScheduledRestartState _scheduledRestart = new ScheduledRestartState();

		private static DailyRestartState _dailyRestart = new DailyRestartState();

		private static LifecycleConfigState _lifecycleConfig = new LifecycleConfigState();

		private static readonly object _restartLock = new object();

		private static DateTime _lastLifecycleTick = DateTime.MinValue;

		private static readonly HashSet<int> _restartWarningsSent = new HashSet<int>();

		private static string _lastDailyRestartDate = string.Empty;

		private static bool _isExecutingRestart = false;

		private static readonly List<PendingConfigChange> _pendingChanges = new List<PendingConfigChange>();

		private static readonly object _pendingLock = new object();

		private static readonly List<SkaldDeathRecordDto> _skaldChronicle = new List<SkaldDeathRecordDto>();

		private static readonly object _skaldLock = new object();

		[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
		private static bool ConstantTimeEquals(string? a, string? b)
		{
			if (a == null || b == null)
			{
				return false;
			}
			byte[] bytes = Encoding.UTF8.GetBytes(a);
			byte[] bytes2 = Encoding.UTF8.GetBytes(b);
			using SHA256 sHA = SHA256.Create();
			byte[] array = sHA.ComputeHash(bytes);
			byte[] array2 = sHA.ComputeHash(bytes2);
			int num = 0;
			for (int i = 0; i < array.Length; i++)
			{
				num |= array[i] ^ array2[i];
			}
			return num == 0;
		}

		public static bool IsAllowedOrigin(string? origin, HttpListenerRequest request)
		{
			if (string.IsNullOrWhiteSpace(origin))
			{
				return false;
			}
			if (!Uri.TryCreate(origin, UriKind.Absolute, out Uri result) || result == null)
			{
				return false;
			}
			if (result.IsLoopback || string.Equals(result.Host, "localhost", StringComparison.OrdinalIgnoreCase) || string.Equals(result.Host, "127.0.0.1", StringComparison.OrdinalIgnoreCase) || string.Equals(result.Host, "::1", StringComparison.OrdinalIgnoreCase))
			{
				return true;
			}
			string text = request.Headers["Host"];
			if (!string.IsNullOrWhiteSpace(text))
			{
				string text2 = text;
				int num = text2.IndexOf(':');
				int result2 = -1;
				if (num >= 0)
				{
					int.TryParse(text2.Substring(num + 1), out result2);
					text2 = text2.Substring(0, num);
				}
				if (string.Equals(result.Host, text2, StringComparison.OrdinalIgnoreCase) && (result2 <= 0 || result.Port == result2))
				{
					return true;
				}
			}
			if (request.Url != null && string.Equals(result.Host, request.Url.Host, StringComparison.OrdinalIgnoreCase) && result.Port == request.Url.Port)
			{
				return true;
			}
			return false;
		}

		public static void RecordPendingChange(string module, string moduleName)
		{
			lock (_pendingLock)
			{
				_pendingChanges.RemoveAll((PendingConfigChange c) => c.module.Equals(module, StringComparison.OrdinalIgnoreCase));
				_pendingChanges.Add(new PendingConfigChange
				{
					module = module,
					moduleName = moduleName,
					timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss")
				});
			}
		}

		public static void ClearPendingChanges()
		{
			lock (_pendingLock)
			{
				_pendingChanges.Clear();
			}
		}

		public static void AddLog(string level, string source, string text)
		{
			lock (LogLock)
			{
				LogsBuffer.AddLast(new ConsoleLogEntry
				{
					time = DateTime.Now.ToString("HH:mm:ss"),
					source = source,
					text = text,
					level = level
				});
				while (LogsBuffer.Count > 300)
				{
					LogsBuffer.RemoveFirst();
				}
			}
		}

		public static async Task HandleApiRequestAsync(HttpListenerContext context, string path, string clientIp)
		{
			HttpListenerRequest request = context.Request;
			HttpListenerResponse response = context.Response;
			string text = request.HttpMethod.ToUpperInvariant();
			response.ContentType = "application/json; charset=utf-8";
			string text2 = request.Headers["Origin"];
			if (!string.IsNullOrEmpty(text2) && IsAllowedOrigin(text2, request))
			{
				response.AddHeader("Access-Control-Allow-Origin", text2);
				response.AddHeader("Vary", "Origin");
			}
			try
			{
				if (path.Equals("/api/auth/login", StringComparison.OrdinalIgnoreCase) && text == "POST")
				{
					await HandleAuthLogin(request, response, clientIp);
					return;
				}
				if (path.Equals("/api/auth/verify", StringComparison.OrdinalIgnoreCase) && text == "GET")
				{
					await HandleAuthVerify(request, response);
					return;
				}
				if (path.Equals("/api/auth/status", StringComparison.OrdinalIgnoreCase) && text == "GET")
				{
					await HandleAuthStatus(request, response);
					return;
				}
				if (!IsAuthorized(request))
				{
					AddLog("warn", "AUTH", "Unauthorized " + text + " request to " + path + " from " + clientIp + ".");
					await SendJsonAsync(response, 401, new
					{
						success = false,
						error = "Unauthorized: Admin password required."
					});
					return;
				}
				if (path.Equals("/api/modules/installed", StringComparison.OrdinalIgnoreCase))
				{
					await HandleGetInstalledModules(response);
					return;
				}
				if (path.Equals("/api/server/telemetry", StringComparison.OrdinalIgnoreCase))
				{
					await HandleGetTelemetry(response);
					return;
				}
				if (path.Equals("/api/players", StringComparison.OrdinalIgnoreCase) && text == "GET")
				{
					await HandleGetPlayers(response);
					return;
				}
				if (path.Equals("/api/players/kick", StringComparison.OrdinalIgnoreCase) && text == "POST")
				{
					await HandleKickPlayer(request, response, clientIp);
					return;
				}
				if (path.Equals("/api/players/ban", StringComparison.OrdinalIgnoreCase) && text == "POST")
				{
					await HandleBanPlayer(request, response, clientIp);
					return;
				}
				if (path.Equals("/api/bans", StringComparison.OrdinalIgnoreCase) && text == "GET")
				{
					await HandleGetBans(response);
					return;
				}
				if (path.Equals("/api/bans/unban", StringComparison.OrdinalIgnoreCase) && text == "POST")
				{
					await HandleUnbanPlayer(request, response, clientIp);
					return;
				}
				if (path.Equals("/api/bans/add", StringComparison.OrdinalIgnoreCase) && text == "POST")
				{
					await HandleAddBan(request, response, clientIp);
					return;
				}
				if (path.Equals("/api/console/logs", StringComparison.OrdinalIgnoreCase) && text == "GET")
				{
					await HandleGetLogs(response);
					return;
				}
				if (path.Equals("/api/console/exec", StringComparison.OrdinalIgnoreCase) && text == "POST")
				{
					await HandleExecCommand(request, response, clientIp);
					return;
				}
				if (path.Equals("/api/server/save", StringComparison.OrdinalIgnoreCase) && text == "POST")
				{
					await HandleForceSave(response, clientIp);
					return;
				}
				if (path.Equals("/api/server/broadcast", StringComparison.OrdinalIgnoreCase) && text == "POST")
				{
					await HandleBroadcast(request, response, clientIp);
					return;
				}
				if (path.Equals("/api/server/restart-status", StringComparison.OrdinalIgnoreCase) && text == "GET")
				{
					await HandleGetRestartStatus(response);
					return;
				}
				if (path.Equals("/api/server/schedule-restart", StringComparison.OrdinalIgnoreCase) && text == "POST")
				{
					await HandleScheduleRestart(request, response, clientIp);
					return;
				}
				if (path.Equals("/api/server/cancel-restart", StringComparison.OrdinalIgnoreCase) && text == "POST")
				{
					await HandleCancelRestart(response, clientIp);
					return;
				}
				if (path.Equals("/api/server/daily-restart", StringComparison.OrdinalIgnoreCase) && text == "POST")
				{
					await HandleUpdateDailyRestart(request, response, clientIp);
					return;
				}
				if (path.Equals("/api/server/pending-changes", StringComparison.OrdinalIgnoreCase) && text == "GET")
				{
					await HandleGetPendingChanges(response);
					return;
				}
				if (path.Equals("/api/server/clear-pending-changes", StringComparison.OrdinalIgnoreCase) && text == "POST")
				{
					await HandleClearPendingChanges(response, clientIp);
					return;
				}
				if (path.Equals("/api/server/lifecycle-config", StringComparison.OrdinalIgnoreCase))
				{
					if (text == "GET")
					{
						await HandleGetLifecycleConfig(response);
					}
					else if (text == "POST")
					{
						await HandleSaveLifecycleConfig(request, response, clientIp);
					}
					return;
				}
				if (path.Equals("/api/discord/config", StringComparison.OrdinalIgnoreCase))
				{
					if (text == "GET")
					{
						await HandleGetDiscordConfig(response);
					}
					else if (text == "POST")
					{
						await HandleSaveDiscordConfig(request, response, clientIp);
					}
					return;
				}
				if (path.Equals("/api/discord/test", StringComparison.OrdinalIgnoreCase) && text == "POST")
				{
					await HandleTestDiscordWebhook(request, response, clientIp);
					return;
				}
				if (path.Equals("/api/modules/charactervault/bindings", StringComparison.OrdinalIgnoreCase) && text == "GET")
				{
					await HandleGetCharacterBindings(response);
					return;
				}
				if (path.Equals("/api/modules/charactervault/unbind", StringComparison.OrdinalIgnoreCase) && text == "POST")
				{
					await HandleUnbindCharacter(request, response, clientIp);
					return;
				}
				if (path.Equals("/api/modules/charactervault/wipe", StringComparison.OrdinalIgnoreCase) && text == "POST")
				{
					await HandleWipeCharacters(response, clientIp);
					return;
				}
				if (path.Equals("/api/modules/valgrind/config", StringComparison.OrdinalIgnoreCase))
				{
					if (text == "GET")
					{
						await HandleGetValgrindConfig(response);
					}
					else if (text == "POST")
					{
						await HandleSaveValgrindConfig(request, response, clientIp);
					}
					return;
				}
				if (path.Equals("/api/modules/dagrnott/config", StringComparison.OrdinalIgnoreCase))
				{
					if (text == "GET")
					{
						await HandleGetDagrNottConfig(response);
					}
					else if (text == "POST")
					{
						await HandleSaveDagrNottConfig(request, response, clientIp);
					}
					return;
				}
				if (path.Equals("/api/modules/skald/config", StringComparison.OrdinalIgnoreCase))
				{
					if (text == "GET")
					{
						await HandleGetSkaldConfig(response);
					}
					else if (text == "POST")
					{
						await HandleSaveSkaldConfig(request, response, clientIp);
					}
					return;
				}
				if (path.Equals("/api/modules/skald/chronicle", StringComparison.OrdinalIgnoreCase) && text == "GET")
				{
					await HandleGetSkaldChronicle(response);
					return;
				}
				if (path.Equals("/api/modules/skald/test-death", StringComparison.OrdinalIgnoreCase) && text == "POST")
				{
					await HandleTestDeathAnnouncement(request, response, clientIp);
					return;
				}
				if (path.Equals("/api/modules/njoror/config", StringComparison.OrdinalIgnoreCase))
				{
					if (text == "GET")
					{
						await HandleGetNjororConfig(response);
					}
					else if (text == "POST")
					{
						await HandleSaveNjororConfig(request, response, clientIp);
					}
					return;
				}
				if (path.Equals("/api/other-mods/list", StringComparison.OrdinalIgnoreCase) && text == "GET")
				{
					await HandleGetOtherModsList(response);
					return;
				}
				if (path.Equals("/api/other-mods/config", StringComparison.OrdinalIgnoreCase) && text == "GET")
				{
					await HandleGetOtherModConfig(request, response);
					return;
				}
				if (path.Equals("/api/other-mods/config/save", StringComparison.OrdinalIgnoreCase) && text == "POST")
				{
					await HandleSaveOtherModConfig(request, response, clientIp);
					return;
				}
				if (path.Equals("/api/other-mods/config/reset-defaults", StringComparison.OrdinalIgnoreCase) && text == "POST")
				{
					await HandleResetOtherModDefaults(request, response, clientIp);
					return;
				}
				await SendJsonAsync(response, 404, new
				{
					error = "API endpoint '" + path + "' not found."
				});
			}
			catch (InvalidOperationException ex) when (ex.Message.Contains("maximum allowed size"))
			{
				AddLog("warn", "SECURITY", "Request body size limit exceeded on '" + path + "' from " + clientIp + ": " + ex.Message);
				await SendJsonAsync(response, 413, new
				{
					error = ex.Message
				});
			}
			catch (Exception ex2)
			{
				BifrostheimPlugin.Log.LogError((object)$"[WebApiRouter] Error handling '{path}': {ex2}");
				await SendJsonAsync(response, 500, new
				{
					error = ex2.Message
				});
			}
		}

		private static bool HasPlugin(params string[] candidateGuids)
		{
			Dictionary<string, PluginInfo>.KeyCollection keys = Chainloader.PluginInfos.Keys;
			foreach (string candidate in candidateGuids)
			{
				if (keys.Any((string k) => string.Equals(k, candidate, StringComparison.OrdinalIgnoreCase) || k.IndexOf(candidate, StringComparison.OrdinalIgnoreCase) >= 0))
				{
					return true;
				}
			}
			return false;
		}

		private static string GetConfiguredAdminPassword()
		{
			string text = BifrostheimPlugin.WebAdminPassword?.Value ?? WebPortalServer.AdminPassword;
			if (string.IsNullOrWhiteSpace(text))
			{
				return string.Empty;
			}
			return text;
		}

		public static bool IsAuthorized(HttpListenerRequest request)
		{
			string configuredAdminPassword = GetConfiguredAdminPassword();
			if (string.IsNullOrWhiteSpace(configuredAdminPassword))
			{
				return false;
			}
			if (string.Equals(configuredAdminPassword, "none", StringComparison.OrdinalIgnoreCase) || string.Equals(configuredAdminPassword, "open", StringComparison.OrdinalIgnoreCase))
			{
				return true;
			}
			string text = request.Headers["X-Admin-Password"];
			if (!string.IsNullOrEmpty(text) && ConstantTimeEquals(text, configuredAdminPassword))
			{
				return true;
			}
			string text2 = request.Headers["Authorization"];
			if (!string.IsNullOrEmpty(text2))
			{
				string text3 = (text2.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase) ? text2.Substring(7).Trim() : text2.Trim());
				if (!string.IsNullOrEmpty(text3))
				{
					if (_activeSessions.TryGetValue(text3, out var value))
					{
						if (DateTime.UtcNow < value)
						{
							_activeSessions[text3] = DateTime.UtcNow.AddHours(12.0);
							return true;
						}
						_activeSessions.TryRemove(text3, out var _);
					}
					if (ConstantTimeEquals(text3, configuredAdminPassword))
					{
						return true;
					}
				}
			}
			return false;
		}

		private static async Task HandleAuthVerify(HttpListenerRequest request, HttpListenerResponse response)
		{
			if (IsAuthorized(request))
			{
				await SendJsonAsync(response, 200, new
				{
					authenticated = true,
					message = "Session valid."
				});
			}
			else
			{
				await SendJsonAsync(response, 401, new
				{
					authenticated = false,
					message = "Unauthorized. Admin password required."
				});
			}
		}

		private static async Task HandleAuthStatus(HttpListenerRequest request, HttpListenerResponse response)
		{
			string configuredAdminPassword = GetConfiguredAdminPassword();
			bool flag = !string.IsNullOrWhiteSpace(configuredAdminPassword) && !string.Equals(configuredAdminPassword, "none", StringComparison.OrdinalIgnoreCase) && !string.Equals(configuredAdminPassword, "open", StringComparison.OrdinalIgnoreCase);
			bool authenticated = !flag || IsAuthorized(request);
			await SendJsonAsync(response, 200, new
			{
				required = flag,
				authenticated = authenticated
			});
		}

		private static async Task HandleAuthLogin(HttpListenerRequest request, HttpListenerResponse response, string clientIp)
		{
			if (_loginRateLimits.TryGetValue(clientIp, out (int, DateTime) value) && DateTime.UtcNow < value.Item2)
			{
				int num = Math.Max(1, (int)(value.Item2 - DateTime.UtcNow).TotalSeconds);
				AddLog("warn", "AUTH", $"Blocked login attempt from locked-out address {clientIp} ({num}s remaining).");
				await SendJsonAsync(response, 429, new
				{
					success = false,
					message = $"Too many failed attempts. Try again in {num} seconds."
				});
				return;
			}
			string text = SimpleJson.DeserializeObject<AuthRequest>(await ReadBodyAsync(request))?.password ?? string.Empty;
			string configuredAdminPassword = GetConfiguredAdminPassword();
			if (!string.IsNullOrWhiteSpace(configuredAdminPassword) && (string.Equals(configuredAdminPassword, "none", StringComparison.OrdinalIgnoreCase) || string.Equals(configuredAdminPassword, "open", StringComparison.OrdinalIgnoreCase)))
			{
				string text2 = Guid.NewGuid().ToString("N");
				_activeSessions[text2] = DateTime.UtcNow.AddHours(12.0);
				await SendJsonAsync(response, 200, new
				{
					success = true,
					token = text2,
					message = "Authentication successful (open access)."
				});
				return;
			}
			if (!string.IsNullOrWhiteSpace(configuredAdminPassword) && !string.IsNullOrWhiteSpace(text) && ConstantTimeEquals(text, configuredAdminPassword))
			{
				_loginRateLimits.TryRemove(clientIp, out (int, DateTime) _);
				string text3 = Guid.NewGuid().ToString("N");
				_activeSessions[text3] = DateTime.UtcNow.AddHours(12.0);
				AddLog("info", "AUTH", "Admin login successful from " + clientIp + ".");
				await SendJsonAsync(response, 200, new
				{
					success = true,
					token = text3,
					message = "Authentication successful."
				});
				return;
			}
			int attempts = 1;
			DateTime item = DateTime.MinValue;
			if (_loginRateLimits.TryGetValue(clientIp, out (int, DateTime) value3))
			{
				attempts = value3.Item1 + 1;
			}
			if (attempts >= 5)
			{
				item = DateTime.UtcNow.AddMinutes(5.0);
				AddLog("warn", "AUTH", "Multiple failed admin login attempts from " + clientIp + ". Locked out for 5 minutes.");
			}
			else
			{
				AddLog("warn", "AUTH", $"Failed admin login attempt from {clientIp} (Attempt {attempts}/5).");
			}
			_loginRateLimits[clientIp] = (attempts, item);
			await Task.Delay(1000);
			if (attempts >= 5)
			{
				await SendJsonAsync(response, 429, new
				{
					success = false,
					message = "Too many failed attempts. Locked out for 5 minutes."
				});
			}
			else
			{
				await SendJsonAsync(response, 401, new
				{
					success = false,
					message = "Invalid admin password."
				});
			}
		}

		private static async Task HandleGetInstalledModules(HttpListenerResponse response)
		{
			List<string> list = new List<string>();
			if (HasPlugin("com.charactervault.valheim", "com.bigai.charactervault", "com.bigai.charactersvault", "charactervault"))
			{
				list.Add("charvault");
			}
			if (HasPlugin("com.bigai.valgrind", "valgrind"))
			{
				list.Add("valgrind");
			}
			if (HasPlugin("com.bigai.dagrnott_customdaycycle", "com.bigai.dagrandnott", "com.bigai.dagrnott", "dagrnott_customdaycycle", "dagrandnott", "dagrnott"))
			{
				list.Add("dagrnott");
			}
			if (HasPlugin("com.bigai.skald_vikingkillfeed", "com.bigai.skald", "skald_vikingkillfeed", "skald"))
			{
				list.Add("skald");
			}
			if (HasPlugin("com.bigai.njoror_fairwinds", "com.bigai.njoror", "njoror_fairwinds", "njoror"))
			{
				list.Add("njoror");
			}
			await SendJsonAsync(response, 200, new
			{
				installed = list
			});
		}

		private static async Task HandleGetTelemetry(HttpListenerResponse response)
		{
			TimeSpan uptimeSpan = DateTime.UtcNow - StartTime;
			string uptimeStr = $"{(int)uptimeSpan.TotalHours}h {uptimeSpan.Minutes}m {uptimeSpan.Seconds}s";
			int onlineCount = 0;
			int maxPlayers = 10;
			int activeZdos = 0;
			await MainThreadDispatcher.EnqueueAsync(delegate
			{
				if ((Object)(object)ZNet.instance != (Object)null)
				{
					onlineCount = ZNet.instance.GetNrOfPlayers();
					maxPlayers = ZNetHelper.GetServerPlayerLimit();
				}
				if (ZDOMan.instance != null)
				{
					activeZdos = ZDOMan.instance.NrOfObjects();
				}
			});
			long num;
			try
			{
				using Process process = Process.GetCurrentProcess();
				num = process.WorkingSet64 / 1048576;
			}
			catch (Exception ex)
			{
				ManualLogSource log = BifrostheimPlugin.Log;
				if (log != null)
				{
					log.LogDebug((object)("[WebApiRouter] Failed to query OS working set, falling back to GC total memory: " + ex.Message));
				}
				num = GC.GetTotalMemory(forceFullCollection: false) / 1048576;
			}
			ServerTelemetryDto data = new ServerTelemetryDto
			{
				uptime = uptimeStr,
				uptimeSeconds = (long)uptimeSpan.TotalSeconds,
				onlineCount = onlineCount,
				maxPlayers = maxPlayers,
				fps = BifrostheimPlugin.CurrentFps,
				tickRate = "50 Hz",
				activeZdos = activeZdos,
				memoryMb = (int)num
			};
			await SendJsonAsync(response, 200, data);
		}

		private static async Task HandleGetPlayers(HttpListenerResponse response)
		{
			List<object> playerList = new List<object>();
			await MainThreadDispatcher.EnqueueAsync(delegate
			{
				//IL_0055: Unknown result type (might be due to invalid IL or missing references)
				//IL_005a: Unknown result type (might be due to invalid IL or missing references)
				//IL_0061: Unknown result type (might be due to invalid IL or missing references)
				//IL_006d: Unknown result type (might be due to invalid IL or missing references)
				//IL_0079: Unknown result type (might be due to invalid IL or missing references)
				if ((Object)(object)ZNet.instance != (Object)null)
				{
					int currentWorldDay = ZNetHelper.GetCurrentWorldDay();
					foreach (ZNetPeer peer in ZNetHelper.GetPeers())
					{
						if (peer != null)
						{
							string playerId = ZNetHelper.GetPlayerId(peer);
							string name = peer.m_playerName ?? "Unknown";
							int peerPing = ZNetHelper.GetPeerPing(peer);
							Vector3 refPos = peer.m_refPos;
							string pos = $"{refPos.x:F0}, {refPos.y:F0}, {refPos.z:F0}";
							(float, float, bool, string, int) playerData = ZNetHelper.GetPlayerData(peer, currentWorldDay);
							playerList.Add(new
							{
								id = playerId,
								name = name,
								steamId = playerId,
								ping = $"{peerPing}ms",
								pos = pos,
								zone = playerData.Item4,
								health = (int)Math.Round(playerData.Item1),
								maxHealth = (int)Math.Round(playerData.Item2),
								pvp = playerData.Item3,
								daysSurvived = playerData.Item5
							});
						}
					}
				}
			});
			await SendJsonAsync(response, 200, playerList);
		}

		private static async Task HandleKickPlayer(HttpListenerRequest request, HttpListenerResponse response, string clientIp)
		{
			string name = SimpleJson.DeserializeObject<KickRequest>(await ReadBodyAsync(request))?.name?.Trim() ?? string.Empty;
			if (string.IsNullOrWhiteSpace(name))
			{
				await SendJsonAsync(response, 400, new
				{
					success = false,
					message = "Player name is required."
				});
				return;
			}
			bool kicked = false;
			await MainThreadDispatcher.EnqueueAsync(delegate
			{
				if ((Object)(object)ZNet.instance != (Object)null)
				{
					foreach (ZNetPeer peer in ZNetHelper.GetPeers())
					{
						if (peer != null && (peer.m_playerName.Equals(name, StringComparison.OrdinalIgnoreCase) || ZNetHelper.GetPlayerId(peer).Equals(name, StringComparison.OrdinalIgnoreCase)))
						{
							ZNet.instance.Disconnect(peer);
							kicked = true;
							break;
						}
					}
				}
			});
			if (kicked)
			{
				AddLog("warn", "KICK", "Kicked player '" + name + "'");
				DiscordWebhookDispatcher.OnAdminAction("Kick", name, "Admin", "");
				await SendJsonAsync(response, 200, new
				{
					success = true,
					message = "Kicked player '" + name + "'"
				});
			}
			else
			{
				await SendJsonAsync(response, 404, new
				{
					success = false,
					message = "Player '" + name + "' not found online."
				});
			}
		}

		private static async Task HandleBanPlayer(HttpListenerRequest request, HttpListenerResponse response, string clientIp)
		{
			BanRequest banRequest = SimpleJson.DeserializeObject<BanRequest>(await ReadBodyAsync(request));
			string name = banRequest?.name?.Trim() ?? string.Empty;
			string reason = banRequest?.reason?.Trim() ?? "Banned by administrator";
			if (string.IsNullOrWhiteSpace(name))
			{
				await SendJsonAsync(response, 400, new
				{
					success = false,
					message = "Player name or ID is required."
				});
				return;
			}
			bool banExecuted = false;
			bool wasOnlinePeer = false;
			string targetDisplayName = name;
			await MainThreadDispatcher.EnqueueAsync(delegate
			{
				if ((Object)(object)ZNet.instance != (Object)null)
				{
					List<ZNetPeer> peers = ZNetHelper.GetPeers();
					ZNetPeer val = null;
					foreach (ZNetPeer item in peers)
					{
						if (item != null && (string.Equals(item.m_playerName, name, StringComparison.OrdinalIgnoreCase) || string.Equals(ZNetHelper.GetPlayerId(item), name, StringComparison.OrdinalIgnoreCase)))
						{
							val = item;
							break;
						}
					}
					string text2 = ((val != null) ? ZNetHelper.GetPlayerId(val) : string.Empty);
					if (!string.IsNullOrWhiteSpace(text2))
					{
						ZNet.instance.Ban(text2);
						banExecuted = true;
						targetDisplayName = name + " (" + text2 + ")";
					}
					if (!string.Equals(name, text2, StringComparison.OrdinalIgnoreCase))
					{
						ZNet.instance.Ban(name);
						banExecuted = true;
					}
					if (val != null)
					{
						wasOnlinePeer = true;
						ZNet.instance.Disconnect(val);
					}
				}
			});
			if (banExecuted)
			{
				string text = (wasOnlinePeer ? ("Banned and disconnected player '" + targetDisplayName + "'") : ("Added '" + targetDisplayName + "' to ban list"));
				AddLog("warn", "BAN", text + " (Reason: " + reason + ")");
				DiscordWebhookDispatcher.OnAdminAction("Ban", targetDisplayName, "Admin", reason);
				await SendJsonAsync(response, 200, new
				{
					success = true,
					message = text + "."
				});
			}
			else
			{
				AddLog("warn", "BAN", "Failed ban attempt for '" + name + "': Valheim network instance unavailable.");
				await SendJsonAsync(response, 503, new
				{
					success = false,
					message = "Server networking unavailable; unable to register ban."
				});
			}
		}

		private static async Task HandleGetBans(HttpListenerResponse response)
		{
			List<object> bansList = new List<object>();
			await MainThreadDispatcher.EnqueueAsync(delegate
			{
				if ((Object)(object)ZNet.instance != (Object)null)
				{
					foreach (string banned in ZNetHelper.GetBannedList())
					{
						bansList.Add(new
						{
							id = banned,
							name = banned,
							steamId = banned,
							bannedAt = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm"),
							reason = "Server ban",
							bannedBy = "Administrator"
						});
					}
				}
			});
			await SendJsonAsync(response, 200, bansList);
		}

		private static async Task HandleUnbanPlayer(HttpListenerRequest request, HttpListenerResponse response, string clientIp)
		{
			string steamId = SimpleJson.DeserializeObject<UnbanRequest>(await ReadBodyAsync(request))?.steamId?.Trim() ?? string.Empty;
			if (string.IsNullOrWhiteSpace(steamId))
			{
				await SendJsonAsync(response, 400, new
				{
					success = false,
					message = "Steam ID is required."
				});
				return;
			}
			await MainThreadDispatcher.EnqueueAsync(delegate
			{
				if ((Object)(object)ZNet.instance != (Object)null)
				{
					ZNet.instance.Unban(steamId);
				}
			});
			AddLog("info", "UNBAN", "Unbanned Steam ID '" + steamId + "'");
			await SendJsonAsync(response, 200, new
			{
				success = true,
				message = "Unbanned player '" + steamId + "'"
			});
		}

		private static async Task HandleAddBan(HttpListenerRequest request, HttpListenerResponse response, string clientIp)
		{
			ManualBanRequest req = SimpleJson.DeserializeObject<ManualBanRequest>(await ReadBodyAsync(request));
			string steamId = req?.steamId?.Trim() ?? string.Empty;
			if (string.IsNullOrWhiteSpace(steamId))
			{
				await SendJsonAsync(response, 400, new
				{
					success = false,
					message = "Steam ID is required."
				});
				return;
			}
			await MainThreadDispatcher.EnqueueAsync(delegate
			{
				if ((Object)(object)ZNet.instance != (Object)null)
				{
					ZNet.instance.Ban(steamId);
				}
			});
			AddLog("warn", "BAN", "Added ban for '" + steamId + "' (Reason: " + req?.reason + ")");
			await SendJsonAsync(response, 200, new
			{
				success = true,
				message = "Banned ID '" + steamId + "'"
			});
		}

		private static async Task HandleGetLogs(HttpListenerResponse response)
		{
			List<ConsoleLogEntry> data;
			lock (LogLock)
			{
				data = new List<ConsoleLogEntry>(LogsBuffer);
			}
			await SendJsonAsync(response, 200, data);
		}

		private static async Task HandleExecCommand(HttpListenerRequest request, HttpListenerResponse response, string clientIp)
		{
			string cmd = SimpleJson.DeserializeObject<ExecCommandRequest>(await ReadBodyAsync(request))?.command?.Trim() ?? string.Empty;
			if (string.IsNullOrWhiteSpace(cmd))
			{
				await SendJsonAsync(response, 400, new
				{
					success = false,
					output = "Command is empty."
				});
				return;
			}
			if (cmd.IndexOf('\n') >= 0 || cmd.IndexOf('\r') >= 0)
			{
				AddLog("warn", "SECURITY", "Rejected multi-line console command attempt from " + clientIp + ".");
				await SendJsonAsync(response, 400, new
				{
					success = false,
					output = "Multi-line commands are not allowed."
				});
				return;
			}
			string[] array = cmd.Split(new char[2] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
			string text = ((array.Length != 0) ? array[0] : string.Empty);
			if (_blockedConsoleCommands.Contains(text))
			{
				AddLog("warn", "SECURITY", "Blocked restricted console command '" + text + "' from " + clientIp + ".");
				await SendJsonAsync(response, 403, new
				{
					success = false,
					output = "Command '" + text + "' is restricted from remote execution."
				});
				return;
			}
			AddLog("cmd", "ADMIN", "> " + cmd);
			bool commandDispatched = false;
			await MainThreadDispatcher.EnqueueAsync(delegate
			{
				if ((Object)(object)Console.instance != (Object)null)
				{
					((Terminal)Console.instance).TryRunCommand(cmd, false, false);
					commandDispatched = true;
					return;
				}
				try
				{
					object? obj = typeof(Terminal).GetField("m_terminalInstance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null);
					Terminal val = (Terminal)((obj is Terminal) ? obj : null);
					if ((Object)(object)val != (Object)null)
					{
						val.TryRunCommand(cmd, false, false);
						commandDispatched = true;
					}
				}
				catch
				{
				}
			});
			if (commandDispatched)
			{
				await SendJsonAsync(response, 200, new
				{
					success = true,
					output = "Command '" + cmd + "' dispatched for execution."
				});
			}
			else
			{
				await SendJsonAsync(response, 503, new
				{
					success = false,
					output = "Server terminal console is not initialized on this instance."
				});
			}
		}

		private static async Task HandleForceSave(HttpListenerResponse response, string clientIp)
		{
			await MainThreadDispatcher.EnqueueAsync(delegate
			{
				if ((Object)(object)ZNet.instance != (Object)null)
				{
					ZNet.instance.Save(true, false, false);
				}
			});
			AddLog("info", "SAVE", "World save triggered by administrator.");
			await SendJsonAsync(response, 200, new
			{
				success = true,
				message = "World save triggered successfully."
			});
		}

		private static async Task HandleBroadcast(HttpListenerRequest request, HttpListenerResponse response, string clientIp)
		{
			string message = SimpleJson.DeserializeObject<BroadcastRequest>(await ReadBodyAsync(request))?.message?.Trim() ?? string.Empty;
			if (string.IsNullOrWhiteSpace(message))
			{
				await SendJsonAsync(response, 400, new
				{
					success = false,
					message = "Message is empty."
				});
				return;
			}
			try
			{
				await MainThreadDispatcher.EnqueueAsync(delegate
				{
					ZNetHelper.BroadcastServerMessage(message);
				});
				AddLog("info", "BROADCAST", "Broadcast: '" + message + "'");
				await SendJsonAsync(response, 200, new
				{
					success = true,
					message = "Broadcast sent."
				});
			}
			catch (Exception ex)
			{
				BifrostheimPlugin.Log.LogError((object)$"[WebApiRouter] Broadcast error: {ex}");
				AddLog("error", "BROADCAST", "Broadcast error: " + ex.Message);
				await SendJsonAsync(response, 200, new
				{
					success = false,
					message = "Broadcast attempted: " + ex.Message
				});
			}
		}

		private static async Task HandleGetRestartStatus(HttpListenerResponse response)
		{
			List<PendingConfigChange> pendingChanges;
			lock (_pendingLock)
			{
				pendingChanges = _pendingChanges.ToList();
			}
			ScheduledRestartState scheduledRestartState;
			DailyRestartState dailyRestartState;
			lock (_restartLock)
			{
				scheduledRestartState = new ScheduledRestartState
				{
					active = _scheduledRestart.active,
					minutes = _scheduledRestart.minutes,
					totalMinutes = _scheduledRestart.totalMinutes,
					targetTimestamp = _scheduledRestart.targetTimestamp,
					reason = _scheduledRestart.reason
				};
				dailyRestartState = new DailyRestartState
				{
					enabled = _dailyRestart.enabled,
					time = _dailyRestart.time
				};
			}
			bool enabled = BifrostheimPlugin.DailyRestartEnabled?.Value ?? dailyRestartState.enabled;
			string time = BifrostheimPlugin.DailyRestartTime?.Value ?? dailyRestartState.time;
			string mode = BifrostheimPlugin.LifecycleRestartMode?.Value ?? _lifecycleConfig.mode;
			string scriptPath = BifrostheimPlugin.LifecycleScriptPath?.Value ?? _lifecycleConfig.scriptPath;
			var data = new
			{
				scheduledRestart = (scheduledRestartState.active ? new
				{
					active = true,
					targetTimestamp = scheduledRestartState.targetTimestamp,
					totalMinutes = scheduledRestartState.totalMinutes,
					remainingSeconds = Math.Max(0, (int)((scheduledRestartState.targetTimestamp - DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()) / 1000)),
					reason = scheduledRestartState.reason
				} : null),
				dailyRestart = new { enabled, time },
				lifecycleConfig = new { mode, scriptPath },
				pendingChanges = pendingChanges
			};
			await SendJsonAsync(response, 200, data);
		}

		private static async Task HandleGetPendingChanges(HttpListenerResponse response)
		{
			List<PendingConfigChange> pendingChanges;
			lock (_pendingLock)
			{
				pendingChanges = _pendingChanges.ToList();
			}
			await SendJsonAsync(response, 200, new
			{
				success = true,
				pendingChanges = pendingChanges
			});
		}

		private static async Task HandleClearPendingChanges(HttpListenerResponse response, string clientIp)
		{
			ClearPendingChanges();
			AddLog("info", "RESTART", "Cleared pending restart notifications list.");
			await SendJsonAsync(response, 200, new
			{
				success = true,
				message = "Pending changes cleared."
			});
		}

		private static async Task HandleScheduleRestart(HttpListenerRequest request, HttpListenerResponse response, string clientIp)
		{
			ScheduleRestartRequest scheduleRestartRequest = SimpleJson.DeserializeObject<ScheduleRestartRequest>(await ReadBodyAsync(request));
			int minutes = scheduleRestartRequest?.minutes ?? 5;
			string reason = scheduleRestartRequest?.reason ?? "Scheduled maintenance";
			long targetTimestamp = DateTimeOffset.UtcNow.AddMinutes(minutes).ToUnixTimeMilliseconds();
			lock (_restartLock)
			{
				_scheduledRestart = new ScheduledRestartState
				{
					active = true,
					minutes = minutes,
					totalMinutes = minutes,
					targetTimestamp = targetTimestamp,
					reason = reason
				};
				_restartWarningsSent.Clear();
				int[] obj = new int[7] { 900, 600, 300, 120, 60, 30, 10 };
				int num = minutes * 60;
				int[] array = obj;
				foreach (int num2 in array)
				{
					if (num2 >= num)
					{
						_restartWarningsSent.Add(num2);
					}
				}
				_isExecutingRestart = false;
			}
			AddLog("warn", "RESTART", $"Server restart scheduled in {minutes} minutes (Reason: {reason})");
			await MainThreadDispatcher.EnqueueAsync(delegate
			{
				ZNetHelper.BroadcastServerMessage(string.Format("⚠\ufe0f SERVER RESTART scheduled in {0} minute{1}! Reason: {2}.", minutes, (minutes > 1) ? "s" : "", reason));
			});
			await SendJsonAsync(response, 200, new
			{
				success = true,
				message = $"Restart scheduled in {minutes} minutes.",
				targetTimestamp = targetTimestamp
			});
		}

		private static async Task HandleCancelRestart(HttpListenerResponse response, string clientIp)
		{
			lock (_restartLock)
			{
				_scheduledRestart = new ScheduledRestartState();
				_restartWarningsSent.Clear();
				_isExecutingRestart = false;
			}
			AddLog("info", "RESTART", "Scheduled server restart cancelled.");
			await MainThreadDispatcher.EnqueueAsync(delegate
			{
				ZNetHelper.BroadcastServerMessage("ℹ\ufe0f The scheduled server restart has been CANCELLED by an administrator.");
			});
			await SendJsonAsync(response, 200, new
			{
				success = true,
				message = "Scheduled restart cancelled."
			});
		}

		private static async Task HandleUpdateDailyRestart(HttpListenerRequest request, HttpListenerResponse response, string clientIp)
		{
			DailyRestartRequest dailyRestartRequest = SimpleJson.DeserializeObject<DailyRestartRequest>(await ReadBodyAsync(request));
			lock (_restartLock)
			{
				_dailyRestart = new DailyRestartState
				{
					enabled = (dailyRestartRequest?.enabled ?? false),
					time = (dailyRestartRequest?.time ?? "04:00")
				};
			}
			if (BifrostheimPlugin.DailyRestartEnabled != null)
			{
				BifrostheimPlugin.DailyRestartEnabled.Value = _dailyRestart.enabled;
			}
			if (BifrostheimPlugin.DailyRestartTime != null)
			{
				BifrostheimPlugin.DailyRestartTime.Value = _dailyRestart.time;
			}
			try
			{
				BifrostheimPlugin instance = BifrostheimPlugin.Instance;
				if (instance != null)
				{
					ConfigFile config = ((BaseUnityPlugin)instance).Config;
					if (config != null)
					{
						config.Save();
					}
				}
			}
			catch (Exception ex)
			{
				ManualLogSource log = BifrostheimPlugin.Log;
				if (log != null)
				{
					log.LogWarning((object)("[WebApiRouter] Failed to save daily restart config to disk: " + ex.Message));
				}
			}
			AddLog("info", "RESTART", "Updated daily restart: " + (_dailyRestart.enabled ? ("Enabled at " + _dailyRestart.time) : "Disabled") + " (Saved to config).");
			await SendJsonAsync(response, 200, new
			{
				success = true,
				dailyRestart = _dailyRestart
			});
		}

		private static async Task HandleGetLifecycleConfig(HttpListenerResponse response)
		{
			LifecycleConfigState data = new LifecycleConfigState
			{
				mode = (BifrostheimPlugin.LifecycleRestartMode?.Value ?? _lifecycleConfig.mode),
				scriptPath = (BifrostheimPlugin.LifecycleScriptPath?.Value ?? _lifecycleConfig.scriptPath)
			};
			await SendJsonAsync(response, 200, data);
		}

		private static async Task HandleSaveLifecycleConfig(HttpListenerRequest request, HttpListenerResponse response, string clientIp)
		{
			LifecycleConfigState lifecycleConfigState = SimpleJson.DeserializeObject<LifecycleConfigState>(await ReadBodyAsync(request));
			if (lifecycleConfigState != null && !string.IsNullOrWhiteSpace(lifecycleConfigState.mode))
			{
				string text = (lifecycleConfigState.mode.Equals("SpawnProcess", StringComparison.OrdinalIgnoreCase) ? "SpawnProcess" : "ExitOnly");
				_lifecycleConfig.mode = text;
				if (BifrostheimPlugin.LifecycleRestartMode != null)
				{
					BifrostheimPlugin.LifecycleRestartMode.Value = text;
				}
				try
				{
					BifrostheimPlugin instance = BifrostheimPlugin.Instance;
					if (instance != null)
					{
						ConfigFile config = ((BaseUnityPlugin)instance).Config;
						if (config != null)
						{
							config.Save();
						}
					}
				}
				catch (Exception ex)
				{
					ManualLogSource log = BifrostheimPlugin.Log;
					if (log != null)
					{
						log.LogWarning((object)("[WebApiRouter] Failed to save lifecycle config to disk: " + ex.Message));
					}
				}
			}
			LifecycleConfigState lifecycleConfigState2 = new LifecycleConfigState
			{
				mode = (BifrostheimPlugin.LifecycleRestartMode?.Value ?? _lifecycleConfig.mode),
				scriptPath = (BifrostheimPlugin.LifecycleScriptPath?.Value ?? _lifecycleConfig.scriptPath)
			};
			AddLog("info", "RESTART", "Updated restart strategy: " + lifecycleConfigState2.mode + " (Saved to config). Custom scriptPath is managed locally on host.");
			await SendJsonAsync(response, 200, new
			{
				success = true,
				lifecycleConfig = lifecycleConfigState2
			});
		}

		private static string MaskWebhookUrl(string? url)
		{
			if (string.IsNullOrWhiteSpace(url))
			{
				return string.Empty;
			}
			string text = url.Trim();
			if (text.Length <= 12)
			{
				return new string('*', text.Length);
			}
			int num = text.LastIndexOf('/');
			if (num > 0 && num < text.Length - 4)
			{
				string text2 = text.Substring(0, num + 1);
				string text3 = text.Substring(num + 1);
				if (text3.Length > 8)
				{
					return text2 + new string('*', 8) + text3.Substring(text3.Length - 4);
				}
				return text2 + new string('*', text3.Length);
			}
			return text.Substring(0, 8) + "********" + text.Substring(text.Length - 4);
		}

		private static bool IsMaskedUrl(string? url)
		{
			return url?.Contains("****") ?? false;
		}

		private static async Task HandleGetDiscordConfig(HttpListenerResponse response)
		{
			DiscordConfigDto data = new DiscordConfigDto
			{
				enabled = (BifrostheimPlugin.DiscordEnabled?.Value ?? false),
				webhookUrl = MaskWebhookUrl(BifrostheimPlugin.DiscordWebhookUrl?.Value),
				overrideChatWebhookUrl = MaskWebhookUrl(BifrostheimPlugin.DiscordOverrideChatWebhookUrl?.Value),
				overrideAdminWebhookUrl = MaskWebhookUrl(BifrostheimPlugin.DiscordOverrideAdminWebhookUrl?.Value),
				botUsername = (BifrostheimPlugin.DiscordBotUsername?.Value ?? "Bifrostheim Herald"),
				botAvatarUrl = (BifrostheimPlugin.DiscordBotAvatarUrl?.Value ?? ""),
				useRichEmbeds = (BifrostheimPlugin.DiscordUseRichEmbeds?.Value ?? true),
				notifyPlayerJoin = (BifrostheimPlugin.DiscordNotifyPlayerJoin?.Value ?? true),
				notifyPlayerLeave = (BifrostheimPlugin.DiscordNotifyPlayerLeave?.Value ?? true),
				notifyPlayerDeath = (BifrostheimPlugin.DiscordNotifyPlayerDeath?.Value ?? true),
				notifyServerLifecycle = (BifrostheimPlugin.DiscordNotifyServerLifecycle?.Value ?? true),
				notifyWorldEvents = (BifrostheimPlugin.DiscordNotifyWorldEvents?.Value ?? true),
				notifyBossMilestones = (BifrostheimPlugin.DiscordNotifyBossMilestones?.Value ?? true),
				notifyAdminActions = (BifrostheimPlugin.DiscordNotifyAdminActions?.Value ?? true),
				notifyChatShouts = (BifrostheimPlugin.DiscordNotifyChatShouts?.Value ?? false)
			};
			await SendJsonAsync(response, 200, data);
		}

		private static async Task HandleSaveDiscordConfig(HttpListenerRequest request, HttpListenerResponse response, string clientIp)
		{
			DiscordConfigDto discordConfigDto = SimpleJson.DeserializeObject<DiscordConfigDto>(await ReadBodyAsync(request));
			if (discordConfigDto != null)
			{
				if (!string.IsNullOrWhiteSpace(discordConfigDto.webhookUrl) && !IsMaskedUrl(discordConfigDto.webhookUrl) && !DiscordWebhookDispatcher.IsValidDiscordWebhookUrl(discordConfigDto.webhookUrl))
				{
					await SendJsonAsync(response, 400, new
					{
						success = false,
						message = "Primary Discord Webhook URL is invalid. Must use HTTPS and point to an official Discord webhook endpoint."
					});
					return;
				}
				if (!string.IsNullOrWhiteSpace(discordConfigDto.overrideChatWebhookUrl) && !IsMaskedUrl(discordConfigDto.overrideChatWebhookUrl) && !DiscordWebhookDispatcher.IsValidDiscordWebhookUrl(discordConfigDto.overrideChatWebhookUrl))
				{
					await SendJsonAsync(response, 400, new
					{
						success = false,
						message = "Chat Override Discord Webhook URL is invalid. Must use HTTPS and point to an official Discord webhook endpoint."
					});
					return;
				}
				if (!string.IsNullOrWhiteSpace(discordConfigDto.overrideAdminWebhookUrl) && !IsMaskedUrl(discordConfigDto.overrideAdminWebhookUrl) && !DiscordWebhookDispatcher.IsValidDiscordWebhookUrl(discordConfigDto.overrideAdminWebhookUrl))
				{
					await SendJsonAsync(response, 400, new
					{
						success = false,
						message = "Admin Override Discord Webhook URL is invalid. Must use HTTPS and point to an official Discord webhook endpoint."
					});
					return;
				}
				if (BifrostheimPlugin.DiscordEnabled != null)
				{
					BifrostheimPlugin.DiscordEnabled.Value = discordConfigDto.enabled;
				}
				if (BifrostheimPlugin.DiscordWebhookUrl != null && !IsMaskedUrl(discordConfigDto.webhookUrl))
				{
					BifrostheimPlugin.DiscordWebhookUrl.Value = discordConfigDto.webhookUrl?.Trim() ?? "";
				}
				if (BifrostheimPlugin.DiscordOverrideChatWebhookUrl != null && !IsMaskedUrl(discordConfigDto.overrideChatWebhookUrl))
				{
					BifrostheimPlugin.DiscordOverrideChatWebhookUrl.Value = discordConfigDto.overrideChatWebhookUrl?.Trim() ?? "";
				}
				if (BifrostheimPlugin.DiscordOverrideAdminWebhookUrl != null && !IsMaskedUrl(discordConfigDto.overrideAdminWebhookUrl))
				{
					BifrostheimPlugin.DiscordOverrideAdminWebhookUrl.Value = discordConfigDto.overrideAdminWebhookUrl?.Trim() ?? "";
				}
				if (BifrostheimPlugin.DiscordBotUsername != null)
				{
					BifrostheimPlugin.DiscordBotUsername.Value = (string.IsNullOrWhiteSpace(discordConfigDto.botUsername) ? "Bifrostheim Herald" : discordConfigDto.botUsername.Trim());
				}
				if (BifrostheimPlugin.DiscordBotAvatarUrl != null)
				{
					BifrostheimPlugin.DiscordBotAvatarUrl.Value = discordConfigDto.botAvatarUrl?.Trim() ?? "";
				}
				if (BifrostheimPlugin.DiscordUseRichEmbeds != null)
				{
					BifrostheimPlugin.DiscordUseRichEmbeds.Value = discordConfigDto.useRichEmbeds;
				}
				if (BifrostheimPlugin.DiscordNotifyPlayerJoin != null)
				{
					BifrostheimPlugin.DiscordNotifyPlayerJoin.Value = discordConfigDto.notifyPlayerJoin;
				}
				if (BifrostheimPlugin.DiscordNotifyPlayerLeave != null)
				{
					BifrostheimPlugin.DiscordNotifyPlayerLeave.Value = discordConfigDto.notifyPlayerLeave;
				}
				if (BifrostheimPlugin.DiscordNotifyPlayerDeath != null)
				{
					BifrostheimPlugin.DiscordNotifyPlayerDeath.Value = discordConfigDto.notifyPlayerDeath;
				}
				if (BifrostheimPlugin.DiscordNotifyServerLifecycle != null)
				{
					BifrostheimPlugin.DiscordNotifyServerLifecycle.Value = discordConfigDto.notifyServerLifecycle;
				}
				if (BifrostheimPlugin.DiscordNotifyWorldEvents != null)
				{
					BifrostheimPlugin.DiscordNotifyWorldEvents.Value = discordConfigDto.notifyWorldEvents;
				}
				if (BifrostheimPlugin.DiscordNotifyBossMilestones != null)
				{
					BifrostheimPlugin.DiscordNotifyBossMilestones.Value = discordConfigDto.notifyBossMilestones;
				}
				if (BifrostheimPlugin.DiscordNotifyAdminActions != null)
				{
					BifrostheimPlugin.DiscordNotifyAdminActions.Value = discordConfigDto.notifyAdminActions;
				}
				if (BifrostheimPlugin.DiscordNotifyChatShouts != null)
				{
					BifrostheimPlugin.DiscordNotifyChatShouts.Value = discordConfigDto.notifyChatShouts;
				}
				try
				{
					BifrostheimPlugin instance = BifrostheimPlugin.Instance;
					if (instance != null)
					{
						ConfigFile config = ((BaseUnityPlugin)instance).Config;
						if (config != null)
						{
							config.Save();
						}
					}
				}
				catch (Exception ex)
				{
					ManualLogSource log = BifrostheimPlugin.Log;
					if (log != null)
					{
						log.LogWarning((object)("[WebApiRouter] Failed to save Discord config to disk: " + ex.Message));
					}
				}
				AddLog("info", "DISCORD", "Updated Discord webhook configuration (Saved to disk).");
			}
			DiscordConfigDto config2 = new DiscordConfigDto
			{
				enabled = (BifrostheimPlugin.DiscordEnabled?.Value ?? false),
				webhookUrl = MaskWebhookUrl(BifrostheimPlugin.DiscordWebhookUrl?.Value),
				overrideChatWebhookUrl = MaskWebhookUrl(BifrostheimPlugin.DiscordOverrideChatWebhookUrl?.Value),
				overrideAdminWebhookUrl = MaskWebhookUrl(BifrostheimPlugin.DiscordOverrideAdminWebhookUrl?.Value),
				botUsername = (BifrostheimPlugin.DiscordBotUsername?.Value ?? "Bifrostheim Herald"),
				botAvatarUrl = (BifrostheimPlugin.DiscordBotAvatarUrl?.Value ?? ""),
				useRichEmbeds = (BifrostheimPlugin.DiscordUseRichEmbeds?.Value ?? true),
				notifyPlayerJoin = (BifrostheimPlugin.DiscordNotifyPlayerJoin?.Value ?? true),
				notifyPlayerLeave = (BifrostheimPlugin.DiscordNotifyPlayerLeave?.Value ?? true),
				notifyPlayerDeath = (BifrostheimPlugin.DiscordNotifyPlayerDeath?.Value ?? true),
				notifyServerLifecycle = (BifrostheimPlugin.DiscordNotifyServerLifecycle?.Value ?? true),
				notifyWorldEvents = (BifrostheimPlugin.DiscordNotifyWorldEvents?.Value ?? true),
				notifyBossMilestones = (BifrostheimPlugin.DiscordNotifyBossMilestones?.Value ?? true),
				notifyAdminActions = (BifrostheimPlugin.DiscordNotifyAdminActions?.Value ?? true),
				notifyChatShouts = (BifrostheimPlugin.DiscordNotifyChatShouts?.Value ?? false)
			};
			await SendJsonAsync(response, 200, new
			{
				success = true,
				config = config2
			});
		}

		private static async Task HandleTestDiscordWebhook(HttpListenerRequest request, HttpListenerResponse response, string clientIp)
		{
			DiscordTestRequestDto discordTestRequestDto = SimpleJson.DeserializeObject<DiscordTestRequestDto>(await ReadBodyAsync(request));
			string eventType = ((!string.IsNullOrWhiteSpace(discordTestRequestDto?.eventType)) ? discordTestRequestDto.eventType.Trim() : "server_online");
			string text = ((!string.IsNullOrWhiteSpace(discordTestRequestDto?.webhookUrl)) ? discordTestRequestDto.webhookUrl.Trim() : null);
			if (IsMaskedUrl(text))
			{
				text = null;
			}
			if (!string.IsNullOrWhiteSpace(text) && !DiscordWebhookDispatcher.IsValidDiscordWebhookUrl(text))
			{
				await SendJsonAsync(response, 400, new
				{
					success = false,
					message = "Invalid Discord Webhook URL. URL must use HTTPS and point to an official Discord webhook endpoint."
				});
				return;
			}
			var (flag, text2) = await DiscordWebhookDispatcher.SendImmediateTestAsync(eventType, text);
			AddLog(flag ? "info" : "warn", "DISCORD", "Test webhook '" + eventType + "': " + text2);
			await SendJsonAsync(response, flag ? 200 : 400, new
			{
				success = flag,
				message = text2,
				eventType = eventType
			});
		}

		public static void TickLifecycle()
		{
			if ((DateTime.UtcNow - _lastLifecycleTick).TotalSeconds < 1.0)
			{
				return;
			}
			_lastLifecycleTick = DateTime.UtcNow;
			try
			{
				bool flag = false;
				List<(int, string, string)> list = new List<(int, string, string)>();
				lock (_restartLock)
				{
					if (_scheduledRestart != null && _scheduledRestart.active && !_isExecutingRestart)
					{
						long num = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
						int num2 = Math.Max(0, (int)((_scheduledRestart.targetTimestamp - num) / 1000));
						int[] array = new int[7] { 900, 600, 300, 120, 60, 30, 10 };
						foreach (int num3 in array)
						{
							if (num2 <= num3 && !_restartWarningsSent.Contains(num3))
							{
								_restartWarningsSent.Add(num3);
								string item = ((num3 >= 60) ? string.Format("{0} minute{1}", num3 / 60, (num3 / 60 > 1) ? "s" : "") : $"{num3} seconds");
								list.Add((num3, item, _scheduledRestart.reason));
							}
						}
						if (num2 <= 0)
						{
							_isExecutingRestart = true;
							flag = true;
						}
					}
					bool num4 = BifrostheimPlugin.DailyRestartEnabled?.Value ?? _dailyRestart.enabled;
					string text = BifrostheimPlugin.DailyRestartTime?.Value ?? _dailyRestart.time;
					if (num4 && !string.IsNullOrWhiteSpace(text) && (_scheduledRestart == null || !_scheduledRestart.active))
					{
						string text2 = DateTime.Now.ToString("yyyy-MM-dd");
						if (DateTime.Now.ToString("HH:mm") == text && _lastDailyRestartDate != text2)
						{
							_lastDailyRestartDate = text2;
							int num5 = 5;
							long targetTimestamp = DateTimeOffset.UtcNow.AddMinutes(num5).ToUnixTimeMilliseconds();
							_scheduledRestart = new ScheduledRestartState
							{
								active = true,
								minutes = num5,
								totalMinutes = num5,
								targetTimestamp = targetTimestamp,
								reason = "Automated daily maintenance"
							};
							_restartWarningsSent.Clear();
							list.Add((-1, $"{num5} minutes", "Automated daily maintenance"));
						}
					}
				}
				foreach (var item2 in list)
				{
					if (item2.Item1 == -1)
					{
						AddLog("warn", "RESTART", "Daily restart triggered automatically for " + item2.Item2 + " countdown.");
						ZNetHelper.BroadcastServerMessage("⚠\ufe0f AUTOMATED DAILY RESTART scheduled in " + item2.Item2 + ". World will be saved.");
						continue;
					}
					ZNetHelper.BroadcastServerMessage("⚠\ufe0f SERVER RESTART in " + item2.Item2 + "! Reason: " + item2.Item3 + ". Please find shelter.");
					AddLog("warn", "RESTART", "Broadcast in-game warning: " + item2.Item2 + " remaining.");
					if (item2.Item1 == 600 || item2.Item1 == 300 || item2.Item1 == 60)
					{
						DiscordWebhookDispatcher.OnServerLifecycle("⏳ Scheduled Server Restart Warning", "Server restarting in " + item2.Item2 + "! Reason: " + item2.Item3 + ". Find shelter!", 15105570, isRestartWarning: true);
					}
				}
				DateTime value = DateTime.UtcNow;
				if (value.Second == 0)
				{
					foreach (string item3 in (from kvp in _activeSessions
						where kvp.Value < DateTime.UtcNow
						select kvp.Key).ToList())
					{
						_activeSessions.TryRemove(item3, out value);
					}
					foreach (string item4 in (from kvp in _loginRateLimits
						where DateTime.UtcNow >= kvp.Value.lockoutUntil && kvp.Value.lockoutUntil != DateTime.MinValue
						select kvp.Key).ToList())
					{
						_loginRateLimits.TryRemove(item4, out (int, DateTime) _);
					}
				}
				if (flag)
				{
					ExecuteServerRestartSequence();
				}
			}
			catch (Exception ex)
			{
				ManualLogSource log = BifrostheimPlugin.Log;
				if (log != null)
				{
					log.LogError((object)("[WebApiRouter] Lifecycle tick error: " + ex.Message));
				}
			}
		}

		private static void ExecuteServerRestartSequence()
		{
			AddLog("warn", "RESTART", "Executing server restart sequence: saving world and terminating process...");
			ZNetHelper.BroadcastServerMessage("⚠\ufe0f [SERVER RESTART] Server is restarting NOW. World saving...");
			DiscordWebhookDispatcher.OnServerLifecycle("\ud83d\uded1 Server Restarting Now", "Server restart initiated. The world is being saved and the server is restarting.", 15158332);
			Task.Run(async delegate
			{
				_ = 3;
				try
				{
					await MainThreadDispatcher.EnqueueAsync(delegate
					{
						try
						{
							if ((Object)(object)ZNet.instance != (Object)null)
							{
								ZNet.instance.Save(true, false, false);
								ManualLogSource log7 = BifrostheimPlugin.Log;
								if (log7 != null)
								{
									log7.LogInfo((object)"[WebApiRouter] World save completed before restart.");
								}
								{
									foreach (ZNetPeer peer in ZNetHelper.GetPeers())
									{
										try
										{
											ZNet.instance.Disconnect(peer);
										}
										catch
										{
										}
									}
									return;
								}
							}
						}
						catch (Exception ex3)
						{
							ManualLogSource log8 = BifrostheimPlugin.Log;
							if (log8 != null)
							{
								log8.LogError((object)("[WebApiRouter] Error saving world before restart: " + ex3.Message));
							}
						}
					});
					ClearPendingChanges();
					WebPortalServer.Stop();
					DiscordWebhookDispatcher.Shutdown();
					string obj = BifrostheimPlugin.LifecycleRestartMode?.Value ?? _lifecycleConfig.mode;
					string text = BifrostheimPlugin.LifecycleScriptPath?.Value ?? _lifecycleConfig.scriptPath;
					if (obj.Equals("SpawnProcess", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(text))
					{
						try
						{
							if (File.Exists(text))
							{
								ManualLogSource log = BifrostheimPlugin.Log;
								if (log != null)
								{
									log.LogInfo((object)("[WebApiRouter] Spawning external restart process: '" + text + "'"));
								}
								using (Process.Start(new ProcessStartInfo
								{
									FileName = text,
									UseShellExecute = false
								}))
								{
								}
							}
							else
							{
								ManualLogSource log2 = BifrostheimPlugin.Log;
								if (log2 != null)
								{
									log2.LogWarning((object)("[WebApiRouter] Configured restart script '" + text + "' does not exist on disk. Skipping spawn."));
								}
							}
						}
						catch (Exception ex)
						{
							ManualLogSource log3 = BifrostheimPlugin.Log;
							if (log3 != null)
							{
								log3.LogError((object)("[WebApiRouter] Failed to spawn external restart script: " + ex.Message));
							}
						}
					}
					if (File.Exists("/.dockerenv") || (File.Exists("/proc/1/cgroup") && File.ReadAllText("/proc/1/cgroup").Contains("docker")))
					{
						try
						{
							using (Process.Start(new ProcessStartInfo
							{
								FileName = "supervisorctl",
								Arguments = "shutdown",
								UseShellExecute = false,
								RedirectStandardOutput = true,
								RedirectStandardError = true,
								CreateNoWindow = true
							}))
							{
								ManualLogSource log4 = BifrostheimPlugin.Log;
								if (log4 != null)
								{
									log4.LogInfo((object)"[WebApiRouter] Dispatched 'supervisorctl shutdown' to trigger Docker container restart.");
								}
							}
						}
						catch (Exception ex2)
						{
							ManualLogSource log5 = BifrostheimPlugin.Log;
							if (log5 != null)
							{
								log5.LogDebug((object)("[WebApiRouter] supervisorctl shutdown not available: " + ex2.Message));
							}
						}
					}
					await MainThreadDispatcher.EnqueueAsync(delegate
					{
						ManualLogSource log7 = BifrostheimPlugin.Log;
						if (log7 != null)
						{
							log7.LogInfo((object)"[WebApiRouter] Terminating server via Application.Quit() and Environment.Exit().");
						}
						Application.Quit();
					});
					await Task.Delay(500);
					Environment.Exit(0);
					await Task.Delay(1000);
					Process.GetCurrentProcess().Kill();
				}
				catch (Exception arg)
				{
					ManualLogSource log6 = BifrostheimPlugin.Log;
					if (log6 != null)
					{
						log6.LogError((object)$"[WebApiRouter] Error during restart execution: {arg}");
					}
					_isExecutingRestart = false;
				}
			});
		}

		private static async Task HandleGetCharacterBindings(HttpListenerResponse response)
		{
			HashSet<string> onlineIdentifiers = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			await MainThreadDispatcher.EnqueueAsync(delegate
			{
				if ((Object)(object)ZNet.instance != (Object)null)
				{
					foreach (ZNetPeer peer in ZNetHelper.GetPeers())
					{
						if (peer != null)
						{
							string playerId = ZNetHelper.GetPlayerId(peer);
							if (!string.IsNullOrWhiteSpace(playerId))
							{
								onlineIdentifiers.Add(playerId);
								if (playerId.StartsWith("Steam_", StringComparison.OrdinalIgnoreCase))
								{
									onlineIdentifiers.Add(playerId.Substring(6));
								}
							}
							if (!string.IsNullOrWhiteSpace(peer.m_playerName))
							{
								onlineIdentifiers.Add(peer.m_playerName);
							}
						}
					}
				}
			});
			List<Dictionary<string, object>> data = ConfigSyncManager.LoadCharacterVaultBindings(onlineIdentifiers);
			await SendJsonAsync(response, 200, data);
		}

		private static async Task HandleUnbindCharacter(HttpListenerRequest request, HttpListenerResponse response, string clientIp)
		{
			string text = SimpleJson.DeserializeObject<UnbindRequest>(await ReadBodyAsync(request))?.steamId?.Trim() ?? string.Empty;
			if (!string.IsNullOrWhiteSpace(text))
			{
				ConfigSyncManager.UnbindCharacter(text);
			}
			AddLog("info", "CHARVAULT", "Unbound character binding for '" + text + "'.");
			await SendJsonAsync(response, 200, new
			{
				success = true
			});
		}

		private static async Task HandleWipeCharacters(HttpListenerResponse response, string clientIp)
		{
			ConfigSyncManager.WipeCharacters();
			AddLog("warn", "CHARVAULT", "Triggered character bindings wipe.");
			await SendJsonAsync(response, 200, new
			{
				success = true,
				message = "CharactersVault data wiped successfully."
			});
		}

		private static async Task HandleGetValgrindConfig(HttpListenerResponse response)
		{
			ValgrindConfigDto data = ConfigSyncManager.LoadValgrindConfig();
			await SendJsonAsync(response, 200, data);
		}

		private static async Task HandleSaveValgrindConfig(HttpListenerRequest request, HttpListenerResponse response, string clientIp)
		{
			ValgrindConfigDto valgrindConfigDto = SimpleJson.DeserializeObject<ValgrindConfigDto>(await ReadBodyAsync(request));
			if (valgrindConfigDto != null)
			{
				ConfigSyncManager.SaveValgrindConfig(valgrindConfigDto);
			}
			RecordPendingChange("valgrind", "Valgrind");
			AddLog("info", "VALGRIND", "Updated Valgrind configuration (Mode: " + valgrindConfigDto?.calculationMode + ") - Saved to disk.");
			ValgrindConfigDto config = ConfigSyncManager.LoadValgrindConfig();
			await SendJsonAsync(response, 200, new
			{
				success = true,
				config = config
			});
		}

		private static async Task HandleGetDagrNottConfig(HttpListenerResponse response)
		{
			DagrNottConfigDto data = ConfigSyncManager.LoadDagrNottConfig();
			await SendJsonAsync(response, 200, data);
		}

		private static async Task HandleSaveDagrNottConfig(HttpListenerRequest request, HttpListenerResponse response, string clientIp)
		{
			DagrNottConfigDto dagrNottConfigDto = SimpleJson.DeserializeObject<DagrNottConfigDto>(await ReadBodyAsync(request));
			if (dagrNottConfigDto != null)
			{
				ConfigSyncManager.SaveDagrNottConfig(dagrNottConfigDto);
			}
			RecordPendingChange("dagrnott", "Dagr & Nott");
			AddLog("info", "DAGRNOTT", $"Updated Dagr & Nott cycle (Dawn: {dagrNottConfigDto?.dawnMultiplier:F2}x, Day: {dagrNottConfigDto?.dayMultiplier:F2}x, Dusk: {dagrNottConfigDto?.duskMultiplier:F2}x, Night: {dagrNottConfigDto?.nightMultiplier:F2}x | ~{dagrNottConfigDto?.totalMinutes:F1}m total) - Saved to disk.");
			DagrNottConfigDto config = ConfigSyncManager.LoadDagrNottConfig();
			await SendJsonAsync(response, 200, new
			{
				success = true,
				config = config
			});
		}

		private static async Task HandleGetSkaldConfig(HttpListenerResponse response)
		{
			SkaldConfigDto data = ConfigSyncManager.LoadSkaldConfig();
			await SendJsonAsync(response, 200, data);
		}

		private static async Task HandleSaveSkaldConfig(HttpListenerRequest request, HttpListenerResponse response, string clientIp)
		{
			SkaldConfigDto skaldConfigDto = SimpleJson.DeserializeObject<SkaldConfigDto>(await ReadBodyAsync(request));
			if (skaldConfigDto != null)
			{
				ConfigSyncManager.SaveSkaldConfig(skaldConfigDto);
			}
			RecordPendingChange("skald", "Skald");
			AddLog("info", "SKALD", "Updated Skald Viking chronicle configuration - Saved to disk.");
			SkaldConfigDto config = ConfigSyncManager.LoadSkaldConfig();
			await SendJsonAsync(response, 200, new
			{
				success = true,
				config = config
			});
		}

		private static async Task HandleGetSkaldChronicle(HttpListenerResponse response)
		{
			try
			{
				Type type = null;
				Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
				foreach (Assembly assembly in assemblies)
				{
					if (assembly.GetName().Name == "Skald" || assembly.GetName().Name == "Skald_VikingKillFeed")
					{
						type = assembly.GetType("Skald.Logic.ChronicleRegistry");
						if (type != null)
						{
							break;
						}
					}
				}
				if (type != null)
				{
					MethodInfo method = type.GetMethod("GetRecentDeaths", BindingFlags.Static | BindingFlags.Public);
					if (method != null && method.Invoke(null, new object[1] { 100 }) is IEnumerable enumerable)
					{
						List<SkaldDeathRecordDto> list = new List<SkaldDeathRecordDto>();
						foreach (object item in enumerable)
						{
							if (item != null)
							{
								Type type2 = item.GetType();
								list.Add(new SkaldDeathRecordDto
								{
									id = (type2.GetProperty("Id")?.GetValue(item)?.ToString() ?? Guid.NewGuid().ToString()),
									victimName = (type2.GetProperty("VictimName")?.GetValue(item)?.ToString() ?? ""),
									victimSteamId = (type2.GetProperty("VictimSteamId")?.GetValue(item)?.ToString() ?? ""),
									killerName = (type2.GetProperty("KillerName")?.GetValue(item)?.ToString() ?? ""),
									category = (type2.GetProperty("Category")?.GetValue(item)?.ToString() ?? ""),
									biome = (type2.GetProperty("Biome")?.GetValue(item)?.ToString() ?? ""),
									formattedMessage = (type2.GetProperty("FormattedMessage")?.GetValue(item)?.ToString() ?? ""),
									timestamp = ((type2.GetProperty("Timestamp")?.GetValue(item) is DateTime dateTime) ? dateTime.ToString("yyyy-MM-dd HH:mm:ss") : DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss"))
								});
							}
						}
						if (list.Count > 0)
						{
							list.Reverse();
							await SendJsonAsync(response, 200, list);
							return;
						}
					}
				}
			}
			catch (Exception ex)
			{
				ManualLogSource log = BifrostheimPlugin.Log;
				if (log != null)
				{
					log.LogDebug((object)("[WebApiRouter] Skald live reflection query failed, falling back to local chronicle: " + ex.Message));
				}
			}
			List<SkaldDeathRecordDto> data;
			lock (_skaldLock)
			{
				data = new List<SkaldDeathRecordDto>(_skaldChronicle);
			}
			await SendJsonAsync(response, 200, data);
		}

		private static async Task HandleTestDeathAnnouncement(HttpListenerRequest request, HttpListenerResponse response, string clientIp)
		{
			await ReadBodyAsync(request);
			SkaldDeathRecordDto skaldDeathRecordDto = new SkaldDeathRecordDto
			{
				id = Guid.NewGuid().ToString(),
				victimName = "VikingWarrior",
				victimSteamId = "Steam_76561198000000001",
				killerName = "Troll",
				category = "Monsters",
				biome = "BlackForest",
				formattedMessage = "VikingWarrior was crushed by Troll in Black Forest.",
				timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss")
			};
			lock (_skaldLock)
			{
				_skaldChronicle.Insert(0, skaldDeathRecordDto);
				if (_skaldChronicle.Count > 100)
				{
					_skaldChronicle.RemoveAt(_skaldChronicle.Count - 1);
				}
			}
			await SendJsonAsync(response, 200, new
			{
				success = true,
				record = skaldDeathRecordDto
			});
		}

		private static async Task HandleGetNjororConfig(HttpListenerResponse response)
		{
			NjororConfigDto data = ConfigSyncManager.LoadNjororConfig();
			await SendJsonAsync(response, 200, data);
		}

		private static async Task HandleSaveNjororConfig(HttpListenerRequest request, HttpListenerResponse response, string clientIp)
		{
			NjororConfigDto njororConfigDto = SimpleJson.DeserializeObject<NjororConfigDto>(await ReadBodyAsync(request));
			if (njororConfigDto != null)
			{
				ConfigSyncManager.SaveNjororConfig(njororConfigDto);
			}
			RecordPendingChange("njoror", "Njörðr");
			AddLog("info", "NJOROR", "Updated Njörðr fair winds configuration - Saved to disk.");
			NjororConfigDto config = ConfigSyncManager.LoadNjororConfig();
			await SendJsonAsync(response, 200, new
			{
				success = true,
				config = config
			});
		}

		private static async Task HandleGetOtherModsList(HttpListenerResponse response)
		{
			List<OtherModSummaryDto> mods = ConfigSyncManager.ScanOtherModConfigFiles();
			await SendJsonAsync(response, 200, new { mods });
		}

		private static async Task HandleGetOtherModConfig(HttpListenerRequest request, HttpListenerResponse response)
		{
			string text = request.QueryString["file"];
			if (string.IsNullOrWhiteSpace(text))
			{
				await SendJsonAsync(response, 400, new
				{
					success = false,
					message = "Missing 'file' query parameter."
				});
				return;
			}
			OtherModConfigDetailDto data = ConfigSyncManager.ParseModConfigFile(text);
			await SendJsonAsync(response, 200, data);
		}

		private static async Task HandleSaveOtherModConfig(HttpListenerRequest request, HttpListenerResponse response, string clientIp)
		{
			SaveOtherModConfigRequest saveOtherModConfigRequest = SimpleJson.DeserializeObject<SaveOtherModConfigRequest>(await ReadBodyAsync(request));
			if (saveOtherModConfigRequest == null || string.IsNullOrWhiteSpace(saveOtherModConfigRequest.fileName))
			{
				await SendJsonAsync(response, 400, new
				{
					success = false,
					message = "Invalid save request or missing fileName."
				});
				return;
			}
			OtherModConfigDetailDto otherModConfigDetailDto = ConfigSyncManager.SaveOtherModConfig(saveOtherModConfigRequest);
			string text = ((!string.IsNullOrEmpty(otherModConfigDetailDto.displayName)) ? otherModConfigDetailDto.displayName : saveOtherModConfigRequest.fileName);
			RecordPendingChange(saveOtherModConfigRequest.fileName, text);
			AddLog("info", "CONFIG", "Updated mod config '" + saveOtherModConfigRequest.fileName + "' (" + text + ") - Staged restart pending.");
			await SendJsonAsync(response, 200, new
			{
				success = true,
				config = otherModConfigDetailDto
			});
		}

		private static async Task HandleResetOtherModDefaults(HttpListenerRequest request, HttpListenerResponse response, string clientIp)
		{
			ResetOtherModConfigRequest resetOtherModConfigRequest = SimpleJson.DeserializeObject<ResetOtherModConfigRequest>(await ReadBodyAsync(request));
			if (resetOtherModConfigRequest == null || string.IsNullOrWhiteSpace(resetOtherModConfigRequest.fileName))
			{
				await SendJsonAsync(response, 400, new
				{
					success = false,
					message = "Invalid reset request or missing fileName."
				});
				return;
			}
			OtherModConfigDetailDto otherModConfigDetailDto = ConfigSyncManager.ResetOtherModConfigDefaults(resetOtherModConfigRequest.fileName);
			string text = ((!string.IsNullOrEmpty(otherModConfigDetailDto.displayName)) ? otherModConfigDetailDto.displayName : resetOtherModConfigRequest.fileName);
			RecordPendingChange(resetOtherModConfigRequest.fileName, text);
			AddLog("warn", "CONFIG", "Reset mod config '" + resetOtherModConfigRequest.fileName + "' (" + text + ") to default values.");
			await SendJsonAsync(response, 200, new
			{
				success = true,
				config = otherModConfigDetailDto
			});
		}

		private static async Task<string> ReadBodyAsync(HttpListenerRequest request, int maxBytes = 1048576)
		{
			if (request.ContentLength64 > maxBytes)
			{
				throw new InvalidOperationException($"Request body exceeds maximum allowed size of {maxBytes} bytes.");
			}
			Encoding encoding = request.ContentEncoding ?? Encoding.UTF8;
			using CancellationTokenSource timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(15.0));
			using MemoryStream ms = new MemoryStream();
			byte[] buffer = new byte[8192];
			int totalRead = 0;
			int num;
			while ((num = await request.InputStream.ReadAsync(buffer, 0, buffer.Length, timeoutCts.Token)) > 0)
			{
				totalRead += num;
				if (totalRead > maxBytes)
				{
					throw new InvalidOperationException($"Request body exceeds maximum allowed size of {maxBytes} bytes.");
				}
				ms.Write(buffer, 0, num);
			}
			return encoding.GetString(ms.ToArray());
		}

		private static async Task SendJsonAsync(HttpListenerResponse response, int statusCode, object data)
		{
			string s = SimpleJson.SerializeObject(data, prettyPrint: false);
			byte[] bytes = Encoding.UTF8.GetBytes(s);
			response.StatusCode = statusCode;
			response.ContentType = "application/json; charset=utf-8";
			response.ContentLength64 = bytes.Length;
			using (Stream stream = response.OutputStream)
			{
				await stream.WriteAsync(bytes, 0, bytes.Length);
			}
			response.Close();
		}
	}
	public class ServerTelemetryDto
	{
		public string uptime { get; set; } = "0h 0m 0s";

		public long uptimeSeconds { get; set; }

		public int onlineCount { get; set; }

		public int maxPlayers { get; set; } = 10;

		public int fps { get; set; } = 60;

		public string tickRate { get; set; } = "50 Hz";

		public int activeZdos { get; set; }

		public int memoryMb { get; set; }
	}
	public class ValgrindConfigDto
	{
		public string calculationMode { get; set; } = "TieredBrackets";

		public bool useTopNSkillsOnly { get; set; }

		public int topNSkillsCount { get; set; } = 5;

		public bool resetAccumulatorOnDeath { get; set; } = true;

		public bool enableDebugLogging { get; set; }

		public float earlyGameLossPercent { get; set; } = 8f;

		public float midGameLossPercent { get; set; } = 5f;

		public float lateGameLossPercent { get; set; } = 2.5f;

		public float endgameLossPercent { get; set; } = 1f;

		public float curveMaxLossPercent { get; set; } = 8f;

		public float curveMinLossPercent { get; set; } = 1f;
	}
	public class DagrNottConfigDto
	{
		public float dawnMultiplier { get; set; } = 0.9f;

		public float dayMultiplier { get; set; } = 0.5f;

		public float duskMultiplier { get; set; } = 0.9f;

		public float nightMultiplier { get; set; } = 0.3f;

		public bool logPhaseTransitions { get; set; } = true;

		public float dawnMinutes { get; set; } = 5f;

		public float dayMinutes { get; set; } = 30f;

		public float duskMinutes { get; set; } = 5f;

		public float nightMinutes { get; set; } = 20f;

		public float totalMinutes { get; set; } = 60f;
	}
	public class SkaldConfigDto
	{
		public bool enabled { get; set; } = true;

		public bool enableBosses { get; set; } = true;

		public bool includeBiome { get; set; } = true;

		public bool logToConsole { get; set; } = true;

		public string monsterTemplates { get; set; } = "{victim} was slain by a {killer} in the {biome};{victim} was torn apart by a {killer};A {killer} claimed the soul of {victim}";

		public string bossTemplates { get; set; } = "{victim} was annihilated by the mythical {killer}!;The legendary {killer} crushed {victim} into dust";

		public string overwhelmedMessages { get; set; } = "{victim} was defeated in glorious battle against a horde in the {biome};{victim} fell fighting valiantly against overwhelming odds";

		public string genericDeathMessages { get; set; } = "{victim} has departed for the halls of Valhalla;The Norns have cut the thread of {victim}'s life;{victim} died in the {biome}";
	}
	public class SkaldDeathRecordDto
	{
		public string id { get; set; } = string.Empty;

		public string victimName { get; set; } = string.Empty;

		public string victimSteamId { get; set; } = string.Empty;

		public string killerName { get; set; } = string.Empty;

		public string category { get; set; } = string.Empty;

		public string biome { get; set; } = string.Empty;

		public string formattedMessage { get; set; } = string.Empty;

		public string timestamp { get; set; } = string.Empty;
	}
	public class NjororConfigDto
	{
		public bool enableFairWinds { get; set; } = true;

		public float headwindMitigationPercent { get; set; } = 60f;

		public float minWindSpeedMultiplier { get; set; } = 1f;

		public bool alwaysTailwindInOcean { get; set; }

		public bool checkDeflectOnWindChange { get; set; } = true;

		public int checkDeflectTimeSeconds { get; set; }

		public bool enableWeatherTuning { get; set; } = true;

		public float stormFrequencyMultiplier { get; set; } = 1f;

		public float rainFrequencyMultiplier { get; set; } = 1f;

		public float clearFrequencyMultiplier { get; set; } = 1f;

		public bool enableSerpentTuning { get; set; } = true;

		public float daytimeSerpentSpawnChance { get; set; }

		public float nighttimeSerpentSpawnChance { get; set; } = 5f;

		public float serpentSpawnIntervalSeconds { get; set; } = 1000f;

		public bool allowCalmWeatherDaySerpents { get; set; }
	}
	public class ConsoleLogEntry
	{
		public string time { get; set; } = string.Empty;

		public string source { get; set; } = string.Empty;

		public string text { get; set; } = string.Empty;

		public string level { get; set; } = "info";
	}
	public class AuthRequest
	{
		public string? password { get; set; }
	}
	public class KickRequest
	{
		public string? name { get; set; }
	}
	public class BanRequest
	{
		public string? name { get; set; }

		public string? reason { get; set; }
	}
	public class UnbanRequest
	{
		public string? steamId { get; set; }
	}
	public class UnbindRequest
	{
		public string? steamId { get; set; }

		public string? name { get; set; }
	}
	public class ManualBanRequest
	{
		public string? steamId { get; set; }

		public string? name { get; set; }

		public string? reason { get; set; }
	}
	public class ExecCommandRequest
	{
		public string? command { get; set; }
	}
	public class BroadcastRequest
	{
		public string? message { get; set; }
	}
	public class ScheduleRestartRequest
	{
		public int minutes { get; set; }

		public string? reason { get; set; }
	}
	public class DailyRestartRequest
	{
		public bool enabled { get; set; }

		public string? time { get; set; }
	}
	public class ScheduledRestartState
	{
		public bool active { get; set; }

		public int minutes { get; set; }

		public int totalMinutes { get; set; }

		public long targetTimestamp { get; set; }

		public string reason { get; set; } = string.Empty;
	}
	public class DailyRestartState
	{
		public bool enabled { get; set; }

		public string time { get; set; } = "04:00";
	}
	public class LifecycleConfigState
	{
		public string mode { get; set; } = "ExitOnly";

		public string scriptPath { get; set; } = string.Empty;
	}
	public class PendingConfigChange
	{
		public string module { get; set; } = string.Empty;

		public string moduleName { get; set; } = string.Empty;

		public string timestamp { get; set; } = string.Empty;
	}
	public class OtherModSummaryDto
	{
		public string fileName { get; set; } = string.Empty;

		public string filePath { get; set; } = string.Empty;

		public string displayName { get; set; } = string.Empty;

		public string pluginGuid { get; set; } = string.Empty;

		public string pluginName { get; set; } = string.Empty;

		public string pluginVersion { get; set; } = string.Empty;

		public int sectionCount { get; set; }

		public int settingCount { get; set; }

		public long fileSizeBytes { get; set; }

		public string lastModified { get; set; } = string.Empty;

		public bool isLoadedInGame { get; set; }

		public bool isFirstParty { get; set; }
	}
	public class OtherModConfigEntryDto
	{
		public string key { get; set; } = string.Empty;

		public string value { get; set; } = string.Empty;

		public string? defaultValue { get; set; }

		public string valueType { get; set; } = "String";

		public string description { get; set; } = string.Empty;

		public List<string>? acceptableValues { get; set; }

		public float? minRange { get; set; }

		public float? maxRange { get; set; }
	}
	public class OtherModSectionDto
	{
		public string name { get; set; } = string.Empty;

		public List<OtherModConfigEntryDto> entries { get; set; } = new List<OtherModConfigEntryDto>();
	}
	public class OtherModConfigDetailDto
	{
		public string fileName { get; set; } = string.Empty;

		public string displayName { get; set; } = string.Empty;

		public string pluginGuid { get; set; } = string.Empty;

		public string pluginName { get; set; } = string.Empty;

		public string pluginVersion { get; set; } = string.Empty;

		public bool isLoadedInGame { get; set; }

		public List<OtherModSectionDto> sections { get; set; } = new List<OtherModSectionDto>();

		public string rawContent { get; set; } = string.Empty;

		public string lastModified { get; set; } = string.Empty;
	}
	public class SaveOtherModConfigRequest
	{
		public string fileName { get; set; } = string.Empty;

		public Dictionary<string, Dictionary<string, string>>? updates { get; set; }

		public string? rawContent { get; set; }

		public bool saveRaw { get; set; }
	}
	public class ResetOtherModConfigRequest
	{
		public string fileName { get; set; } = string.Empty;
	}
	public class DiscordConfigDto
	{
		public bool enabled { get; set; }

		public string webhookUrl { get; set; } = string.Empty;

		public string overrideChatWebhookUrl { get; set; } = string.Empty;

		public string overrideAdminWebhookUrl { get; set; } = string.Empty;

		public string botUsername { get; set; } = "Bifrostheim Herald";

		public string botAvatarUrl { get; set; } = string.Empty;

		public bool useRichEmbeds { get; set; } = true;

		public bool notifyPlayerJoin { get; set; } = true;

		public bool notifyPlayerLeave { get; set; } = true;

		public bool notifyPlayerDeath { get; set; } = true;

		public bool notifyServerLifecycle { get; set; } = true;

		public bool notifyWorldEvents { get; set; } = true;

		public bool notifyBossMilestones { get; set; } = true;

		public bool notifyAdminActions { get; set; } = true;

		public bool notifyChatShouts { get; set; }
	}
	public class DiscordTestRequestDto
	{
		public string? eventType { get; set; }

		public string? webhookUrl { get; set; }
	}
	public static class WebPortalServer
	{
		private static HttpListener? _listener;

		private static CancellationTokenSource? _cts;

		private static byte[]? _embeddedIndexHtmlBytes;

		private static bool _isRunning;

		private static readonly object _serverLock = new object();

		internal static string AdminPassword { get; private set; } = string.Empty;

		public static void Start(int port, string password)
		{
			lock (_serverLock)
			{
				if (_isRunning)
				{
					return;
				}
				_isRunning = true;
			}
			AdminPassword = password ?? string.Empty;
			_cts = new CancellationTokenSource();
			Task.Run(async delegate
			{
				try
				{
					string[] obj = new string[4]
					{
						$"http://*:{port}/",
						$"http://+:{port}/",
						$"http://127.0.0.1:{port}/",
						$"http://localhost:{port}/"
					};
					bool flag = false;
					string[] array = obj;
					foreach (string text in array)
					{
						try
						{
							_listener = new HttpListener();
							_listener.Prefixes.Add(text);
							_listener.Start();
							flag = true;
							BifrostheimPlugin.Log.LogInfo((object)("[WebPortalServer] Listening on " + text));
						}
						catch (Exception ex)
						{
							BifrostheimPlugin.Log.LogWarning((object)("[WebPortalServer] Could not bind prefix '" + text + "': " + ex.Message));
							try
							{
								_listener?.Close();
							}
							catch (Exception ex2)
							{
								BifrostheimPlugin.Log.LogDebug((object)("[WebPortalServer] Failed to close listener on prefix bind error: " + ex2.Message));
							}
							continue;
						}
						break;
					}
					if (!flag || _listener == null)
					{
						BifrostheimPlugin.Log.LogError((object)$"[WebPortalServer] Failed to bind HTTP listener on port {port}.");
						lock (_serverLock)
						{
							_isRunning = false;
							return;
						}
					}
					LoadEmbeddedAssets();
					while (!_cts.Token.IsCancellationRequested && _listener.IsListening)
					{
						try
						{
							ProcessRequestAsync(await _listener.GetContextAsync());
						}
						catch (HttpListenerException) when (_cts.Token.IsCancellationRequested)
						{
							break;
						}
						catch (Exception ex4)
						{
							if (!_cts.Token.IsCancellationRequested)
							{
								BifrostheimPlugin.Log.LogWarning((object)("[WebPortalServer] Request accept error: " + ex4.Message));
							}
						}
					}
				}
				catch (Exception arg)
				{
					BifrostheimPlugin.Log.LogError((object)$"[WebPortalServer] Server error: {arg}");
				}
				finally
				{
					lock (_serverLock)
					{
						_isRunning = false;
					}
					try
					{
						if (_cts?.IsCancellationRequested ?? false)
						{
							goto IL_0302;
						}
						HttpListener? listener = _listener;
						if (listener != null && !listener.IsListening)
						{
							goto IL_0302;
						}
						goto end_IL_02d9;
						IL_0302:
						_listener?.Stop();
						_listener?.Close();
						end_IL_02d9:;
					}
					catch (Exception ex5)
					{
						BifrostheimPlugin.Log.LogDebug((object)("[WebPortalServer] Error closing listener on exit: " + ex5.Message));
					}
				}
			});
		}

		public static void Stop()
		{
			lock (_serverLock)
			{
				if (!_isRunning)
				{
					return;
				}
				_isRunning = false;
			}
			try
			{
				_cts?.Cancel();
				_listener?.Stop();
				_listener?.Close();
				BifrostheimPlugin.Log.LogInfo((object)"[WebPortalServer] Web portal server stopped.");
			}
			catch (Exception ex)
			{
				BifrostheimPlugin.Log.LogWarning((object)("[WebPortalServer] Error stopping server: " + ex.Message));
			}
		}

		private static async Task ProcessRequestAsync(HttpListenerContext context)
		{
			_ = 1;
			try
			{
				string clientIp = GetClientIp(context.Request);
				string text = context.Request.Url?.AbsolutePath ?? "/";
				if (context.Request.HttpMethod.Equals("OPTIONS", StringComparison.OrdinalIgnoreCase))
				{
					string text2 = context.Request.Headers["Origin"];
					if (!string.IsNullOrEmpty(text2) && WebApiRouter.IsAllowedOrigin(text2, context.Request))
					{
						context.Response.AddHeader("Access-Control-Allow-Origin", text2);
						context.Response.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
						context.Response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Admin-Password");
						context.Response.AddHeader("Vary", "Origin");
						context.Response.StatusCode = 204;
					}
					else
					{
						context.Response.StatusCode = (string.IsNullOrEmpty(text2) ? 204 : 403);
					}
					context.Response.Close();
				}
				else if (text.StartsWith("/api/", StringComparison.OrdinalIgnoreCase))
				{
					await WebApiRouter.HandleApiRequestAsync(context, text, clientIp);
				}
				else
				{
					await ServeStaticSpaAsync(context.Response);
				}
			}
			catch (HttpListenerException ex) when (ex.ErrorCode == 64 || ex.ErrorCode == 1229)
			{
				BifrostheimPlugin.Log.LogDebug((object)("[WebPortalServer] Client disconnected: " + ex.Message));
			}
			catch (IOException ex2)
			{
				BifrostheimPlugin.Log.LogDebug((object)("[WebPortalServer] IO abort during request processing: " + ex2.Message));
			}
			catch (Exception arg)
			{
				BifrostheimPlugin.Log.LogError((object)$"[WebPortalServer] Error processing request: {arg}");
				try
				{
					context.Response.StatusCode = 500;
					context.Response.Close();
				}
				catch (Exception ex3)
				{
					BifrostheimPlugin.Log.LogDebug((object)("[WebPortalServer] Failed to return 500 response: " + ex3.Message));
				}
			}
		}

		private static async Task ServeStaticSpaAsync(HttpListenerResponse response)
		{
			byte[] array = _embeddedIndexHtmlBytes ?? GetDefaultFallbackHtml();
			response.StatusCode = 200;
			response.ContentType = "text/html; charset=utf-8";
			response.ContentLength64 = array.Length;
			response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
			using (Stream output = response.OutputStream)
			{
				await output.WriteAsync(array, 0, array.Length);
			}
			response.Close();
		}

		private static void LoadEmbeddedAssets()
		{
			try
			{
				Assembly executingAssembly = Assembly.GetExecutingAssembly();
				string name = "Bifrostheim.dist.index.html";
				using (Stream stream = executingAssembly.GetManifestResourceStream(name))
				{
					if (stream != null)
					{
						using (MemoryStream memoryStream = new MemoryStream())
						{
							stream.CopyTo(memoryStream);
							_embeddedIndexHtmlBytes = memoryStream.ToArray();
							BifrostheimPlugin.Log.LogInfo((object)$"[WebPortalServer] Loaded embedded React bundle ({_embeddedIndexHtmlBytes.Length / 1024} KB).");
							return;
						}
					}
				}
				string[] manifestResourceNames = executingAssembly.GetManifestResourceNames();
				foreach (string text in manifestResourceNames)
				{
					if (!text.EndsWith("index.html", StringComparison.OrdinalIgnoreCase))
					{
						continue;
					}
					using Stream stream2 = executingAssembly.GetManifestResourceStream(text);
					if (stream2 != null)
					{
						using (MemoryStream memoryStream2 = new MemoryStream())
						{
							stream2.CopyTo(memoryStream2);
							_embeddedIndexHtmlBytes = memoryStream2.ToArray();
							BifrostheimPlugin.Log.LogInfo((object)$"[WebPortalServer] Loaded embedded resource '{text}' ({_embeddedIndexHtmlBytes.Length / 1024} KB).");
							return;
						}
					}
				}
				BifrostheimPlugin.Log.L