Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of Bigfrost ServerPortal v1.0.0
BepInEx\plugins\Bigfrost_ServerPortal\Bigfrost_ServerPortal.dll
Decompiled 10 hours ago
The result has been truncated due to the large size, download it to view full contents!
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.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; 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.Web; using HarmonyLib; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("TestRunner")] [assembly: AssemblyConfiguration("Debug")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+aaed364b2d1fe22de3733e0597dd5e084c8c9366")] [assembly: AssemblyProduct("TestRunner")] [assembly: AssemblyTitle("TestRunner")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.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 TestRunner { internal class Program { private static void Main(string[] args) { AppDomain.CurrentDomain.AssemblyResolve += (object sender, ResolveEventArgs resolveArgs) => new AssemblyName(resolveArgs.Name).Name.Equals("netstandard", StringComparison.OrdinalIgnoreCase) ? typeof(object).Assembly : null; string text = Path.Combine(Path.GetTempPath(), "test_bepinex_config_" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(text); Environment.SetEnvironmentVariable("BEPINEX_CONFIG_PATH", text); Console.WriteLine("Testing ConfigSyncManager with test directory: " + text); Console.WriteLine("\n[1] Testing Valgrind Save & Load..."); ConfigSyncManager.SaveValgrindConfig(new ValgrindConfigDto { calculationMode = "TieredBrackets", useTopNSkillsOnly = true, topNSkillsCount = 7, resetAccumulatorOnDeath = false, enableDebugLogging = true, earlyGameLossPercent = 10f, midGameLossPercent = 6f, lateGameLossPercent = 3f, endgameLossPercent = 1.5f, curveMaxLossPercent = 10f, curveMinLossPercent = 1.5f }); string path = Path.Combine(text, "com.bigai.valgrind.cfg"); if (!File.Exists(path)) { throw new Exception("com.bigai.valgrind.cfg was not created!"); } Console.WriteLine("Config file content:\n" + File.ReadAllText(path)); ValgrindConfigDto valgrindConfigDto = ConfigSyncManager.LoadValgrindConfig(); if (valgrindConfigDto.calculationMode != "TieredBrackets" || !valgrindConfigDto.useTopNSkillsOnly || valgrindConfigDto.topNSkillsCount != 7 || Math.Abs(valgrindConfigDto.earlyGameLossPercent - 10f) > 0.01f) { throw new Exception($"Valgrind mismatch: mode={valgrindConfigDto.calculationMode}, topN={valgrindConfigDto.useTopNSkillsOnly}, count={valgrindConfigDto.topNSkillsCount}, early={valgrindConfigDto.earlyGameLossPercent}"); } Console.WriteLine("✓ Valgrind PASSED"); Console.WriteLine("\n[2] Testing Dagr & Nott Save & Load..."); ConfigSyncManager.SaveDagrNottConfig(new DagrNottConfigDto { dawnMultiplier = 0.9f, dayMultiplier = 0.5f, duskMultiplier = 0.9f, nightMultiplier = 0.3f, logPhaseTransitions = true }); string path2 = Path.Combine(text, "com.bigai.dagrnott_customdaycycle.cfg"); if (!File.Exists(path2)) { throw new Exception("com.bigai.dagrnott_customdaycycle.cfg was not created!"); } Console.WriteLine("Config file content:\n" + File.ReadAllText(path2)); DagrNottConfigDto dagrNottConfigDto = ConfigSyncManager.LoadDagrNottConfig(); Console.WriteLine($"Loaded Dagr: Dawn={dagrNottConfigDto.dawnMultiplier}x (~{dagrNottConfigDto.dawnMinutes}m), Day={dagrNottConfigDto.dayMultiplier}x (~{dagrNottConfigDto.dayMinutes}m), Dusk={dagrNottConfigDto.duskMultiplier}x (~{dagrNottConfigDto.duskMinutes}m), Night={dagrNottConfigDto.nightMultiplier}x (~{dagrNottConfigDto.nightMinutes}m) | Total: ~{dagrNottConfigDto.totalMinutes}m"); if (Math.Abs(dagrNottConfigDto.dawnMultiplier - 0.9f) > 0.01f || Math.Abs(dagrNottConfigDto.dayMultiplier - 0.5f) > 0.01f || Math.Abs(dagrNottConfigDto.nightMultiplier - 0.3f) > 0.01f || dagrNottConfigDto.totalMinutes != 60f) { throw new Exception($"Dagr & Nott mismatch: dawn={dagrNottConfigDto.dawnMultiplier}, day={dagrNottConfigDto.dayMultiplier}, night={dagrNottConfigDto.nightMultiplier}, total={dagrNottConfigDto.totalMinutes}"); } Console.WriteLine("✓ Dagr & Nott PASSED"); Console.WriteLine("\n[3] Testing Skald Save & Load..."); ConfigSyncManager.SaveSkaldConfig(new SkaldConfigDto { enabled = true, enableBosses = true, monsterTemplates = "{victim} fell to {killer};A foul {killer} destroyed {victim}", bossTemplates = "{victim} was crushed by legendary {killer}", overwhelmedMessages = "{victim} was swarmed by enemies", genericDeathMessages = "{victim} died in the {biome}" }); string path3 = Path.Combine(text, "com.bigai.skald_vikingkillfeed.cfg"); if (!File.Exists(path3)) { throw new Exception("com.bigai.skald_vikingkillfeed.cfg was not created!"); } Console.WriteLine("Config file content:\n" + File.ReadAllText(path3)); SkaldConfigDto skaldConfigDto = ConfigSyncManager.LoadSkaldConfig(); if (!skaldConfigDto.enabled || skaldConfigDto.overwhelmedMessages != "{victim} was swarmed by enemies" || skaldConfigDto.monsterTemplates != "{victim} fell to {killer};A foul {killer} destroyed {victim}") { throw new Exception($"Skald mismatch: enabled={skaldConfigDto.enabled}, overwhelmed={skaldConfigDto.overwhelmedMessages}"); } Console.WriteLine("✓ Skald PASSED"); Console.WriteLine("\n[4] Testing Njörðr Save & Load..."); ConfigSyncManager.SaveNjororConfig(new NjororConfigDto { enableFairWinds = true, headwindMitigationPercent = 85f, stormFrequencyMultiplier = 1.75f, alwaysTailwindInOcean = true }); string path4 = Path.Combine(text, "com.bigai.njoror_fairwinds.cfg"); if (!File.Exists(path4)) { throw new Exception("com.bigai.njoror_fairwinds.cfg was not created!"); } Console.WriteLine("Config file content:\n" + File.ReadAllText(path4)); NjororConfigDto njororConfigDto = ConfigSyncManager.LoadNjororConfig(); if (Math.Abs(njororConfigDto.headwindMitigationPercent - 85f) > 0.01f || Math.Abs(njororConfigDto.stormFrequencyMultiplier - 1.75f) > 0.01f || !njororConfigDto.alwaysTailwindInOcean) { throw new Exception($"Njörðr mismatch: headwindMitigation={njororConfigDto.headwindMitigationPercent}, stormFreq={njororConfigDto.stormFrequencyMultiplier}"); } Console.WriteLine("✓ Njörðr PASSED"); Console.WriteLine("\n[6] Testing CharactersVault Load, Parse, Unbind & Wipe..."); string text2 = Path.Combine(text, "CharacterVault"); Directory.CreateDirectory(text2); string path5 = Path.Combine(text2, "bindings.json"); string contents = "{\n \"Steam_76561198132796198\": {\n \"characterName\": \"Ragnar Lothbrok\",\n \"created\": \"2026-08-10\",\n \"lastLogin\": \"2026-08-28 20:00\",\n \"status\": \"Bound\"\n },\n \"Steam_456\": \"Lagertha\"\n}"; File.WriteAllText(path5, contents); List<Dictionary<string, object>> list = ConfigSyncManager.LoadCharacterVaultBindings(); Console.WriteLine($"Loaded {list.Count} bindings."); foreach (Dictionary<string, object> item in list) { Console.WriteLine(string.Format(" SteamId: {0}, Character: {1}, Created: {2}, LastLogin: {3}, Status: {4}", item["steamId"], item["characterName"], item["created"], item["lastLogin"], item["status"])); } Dictionary<string, object> dictionary = list.Find((Dictionary<string, object> b) => b["steamId"].ToString() == "Steam_76561198132796198"); if (dictionary == null || dictionary["characterName"].ToString() != "Ragnar Lothbrok") { throw new Exception(string.Format("CharacterVault object parsing failed! Expected 'Ragnar Lothbrok', got '{0}'", dictionary?["characterName"])); } if (dictionary["characterName"].ToString().Contains("Dictionary")) { throw new Exception("CharacterVault characterName contains raw dictionary type string!"); } if (dictionary["created"].ToString() != "2026-08-10" || dictionary["lastLogin"].ToString() != "2026-08-28 20:00") { throw new Exception(string.Format("CharacterVault timestamps mismatch! Expected 2026-08-10 / 2026-08-28 20:00, got {0} / {1}", dictionary["created"], dictionary["lastLogin"])); } Dictionary<string, object> dictionary2 = list.Find((Dictionary<string, object> b) => b["steamId"].ToString() == "Steam_456"); if (dictionary2 == null || dictionary2["characterName"].ToString() != "Lagertha") { throw new Exception(string.Format("CharacterVault string parsing failed! Expected 'Lagertha', got '{0}'", dictionary2?["characterName"])); } if (dictionary2["created"].ToString() != "—" || dictionary2["lastLogin"].ToString() != "—") { throw new Exception(string.Format("CharacterVault missing timestamps should default to '—', got {0} / {1}", dictionary2["created"], dictionary2["lastLogin"])); } string text3 = Path.Combine(text2, "characters"); Directory.CreateDirectory(text3); string path6 = Path.Combine(text3, "76561198999999999.fch"); File.WriteAllText(path6, "DUMMY_CHARACTER_DATA"); DateTime creationTimeUtc = new DateTime(2025, 4, 15, 10, 0, 0, DateTimeKind.Utc); DateTime lastWriteTimeUtc = new DateTime(2026, 1, 20, 14, 30, 0, DateTimeKind.Utc); File.SetCreationTimeUtc(path6, creationTimeUtc); File.SetLastWriteTimeUtc(path6, lastWriteTimeUtc); string contents2 = "{\n \"Steam_76561198999999999\": \"Floki\"\n}"; File.WriteAllText(path5, contents2); Dictionary<string, object> dictionary3 = ConfigSyncManager.LoadCharacterVaultBindings().Find((Dictionary<string, object> b) => b["steamId"].ToString() == "Steam_76561198999999999"); if (dictionary3 == null || dictionary3["created"].ToString() != "2025-04-15" || dictionary3["lastLogin"].ToString() != "2026-01-20 14:30") { throw new Exception(string.Format("CharacterVault file timestamp detection failed! Got created={0}, lastLogin={1}", dictionary3?["created"], dictionary3?["lastLogin"])); } ConfigSyncManager.UnbindCharacter("76561198999999999"); if (ConfigSyncManager.LoadCharacterVaultBindings().Exists((Dictionary<string, object> b) => b["steamId"].ToString() == "Steam_76561198999999999")) { throw new Exception("CharacterVault unbind with prefix mismatch failed!"); } ConfigSyncManager.WipeCharacters(); string text4 = File.ReadAllText(path5); Console.WriteLine("After wipe JSON: " + text4); if (text4.Trim() != "{}") { throw new Exception("CharacterVault wipe failed!"); } Console.WriteLine("✓ CharactersVault PASSED"); Console.WriteLine("\n[7] Testing INI Preservation with Comments..."); string path7 = Path.Combine(text, "com.bigai.valgrind.cfg"); string contents3 = "## Settings file was created by plugin Valgrind\n\n[1 - General]\n\n## Custom comment\nCalculationMode = TieredBrackets\nUseTopNSkillsOnly = false\n\n[2 - Tiered Brackets]\nEarlyGameLossPercent = 8.0\n"; File.WriteAllText(path7, contents3); ConfigSyncManager.SaveValgrindConfig(new ValgrindConfigDto { calculationMode = "ContinuousCurve", earlyGameLossPercent = 12f }); string text5 = File.ReadAllText(path7); Console.WriteLine("Preserved INI content:\n" + text5); if (!text5.Contains("## Custom comment") || !text5.Contains("CalculationMode = ContinuousCurve") || !text5.Contains("EarlyGameLossPercent = 12.0")) { throw new Exception("INI preservation failed to preserve comments or update keys!"); } Console.WriteLine("✓ INI Preservation PASSED"); Console.WriteLine("\n========================================================"); Console.WriteLine(">>> ALL CONFIG PERSISTENCE TESTS PASSED WITH 100% SUCCESS <<<"); Console.WriteLine("========================================================"); } } } namespace Bifrostheim { [BepInPlugin("com.bigai.bigfrost_serverportal", "Bigfrost_ServerPortal", "1.0.0")] public class BifrostheimPlugin : BaseUnityPlugin { public const string PluginGUID = "com.bigai.bigfrost_serverportal"; public const string PluginName = "Bigfrost_ServerPortal"; public const string PluginVersion = "1.0.0"; public static ConfigEntry<bool> EnableWebPortal; public static ConfigEntry<int> WebPortalPort; public static ConfigEntry<string> WebAdminPassword; public static ConfigEntry<bool> VerboseLogging; public static ConfigEntry<string> LifecycleRestartMode; public static ConfigEntry<string> LifecycleScriptPath; public static ConfigEntry<bool> DailyRestartEnabled; public static ConfigEntry<string> DailyRestartTime; public static BifrostheimPlugin Instance { get; private set; } public static ManualLogSource Log { get; private set; } private void Awake() { Instance = this; Log = ((BaseUnityPlugin)this).Logger; try { Log.LogInfo((object)"══════════════════════════════════════════"); Log.LogInfo((object)" Bigfrost_ServerPortal v1.0.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", "admin", "Password required for administrative actions in the web portal."); VerboseLogging = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "VerboseLogging", false, "Enable verbose logging in BepInEx console."); 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)."); MainThreadDispatcher.Initialize(); ConfigSyncManager.OnlinePlayerChecker = ZNetHelper.IsPlayerOnline; Logger.Listeners.Add((ILogListener)(object)new BepInExLogListener()); 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() { WebApiRouter.TickLifecycle(); } private void OnDestroy() { WebPortalServer.Stop(); Log.LogInfo((object)"[Bigfrost_ServerPortal] Unloaded."); } } } namespace Bifrostheim.Systems.Web { public static class WebApiRouter { private static readonly DateTime StartTime = DateTime.UtcNow; private static readonly List<ConsoleLogEntry> LogsBuffer = new List<ConsoleLogEntry>(); private static readonly object LogLock = new object(); private static ScheduledRestartState _scheduledRestart = new ScheduledRestartState(); private static DailyRestartState _dailyRestart = new DailyRestartState(); private static LifecycleConfigState _lifecycleConfig = new LifecycleConfigState(); 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 List<SkaldDeathRecordDto> _skaldChronicle = new List<SkaldDeathRecordDto>(); public static void RecordPendingChange(string module, string moduleName) { lock (_pendingLock) { PendingConfigChange pendingConfigChange = _pendingChanges.FirstOrDefault((PendingConfigChange c) => c.module.Equals(module, StringComparison.OrdinalIgnoreCase)); if (pendingConfigChange != null) { pendingConfigChange.timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss"); return; } _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.Add(new ConsoleLogEntry { time = DateTime.Now.ToString("HH:mm:ss"), source = source, text = text, level = level }); if (LogsBuffer.Count > 300) { LogsBuffer.RemoveAt(0); } } } 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"; response.AddHeader("Access-Control-Allow-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/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 (Exception ex) { BifrostheimPlugin.Log.LogError((object)$"[WebApiRouter] Error handling '{path}': {ex}"); await SendJsonAsync(response, 500, new { error = ex.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 "admin"; } return text; } public static bool IsAuthorized(HttpListenerRequest request) { string configuredAdminPassword = GetConfiguredAdminPassword(); 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) && string.Equals(text, configuredAdminPassword, StringComparison.Ordinal)) { return true; } string text2 = request.Headers["Authorization"]; if (!string.IsNullOrEmpty(text2)) { if (text2.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) { if (string.Equals(text2.Substring(7).Trim(), configuredAdminPassword, StringComparison.Ordinal)) { return true; } } else if (string.Equals(text2.Trim(), configuredAdminPassword, StringComparison.Ordinal)) { 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.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) { string text = SimpleJson.DeserializeObject<AuthRequest>(await ReadBodyAsync(request))?.password ?? string.Empty; string configuredAdminPassword = GetConfiguredAdminPassword(); if (string.Equals(configuredAdminPassword, "none", StringComparison.OrdinalIgnoreCase) || string.Equals(configuredAdminPassword, "open", StringComparison.OrdinalIgnoreCase)) { await SendJsonAsync(response, 200, new { success = true, token = text, message = "Authentication successful (open access)." }); } else if (string.Equals(text, configuredAdminPassword, StringComparison.Ordinal)) { AddLog("info", "AUTH", "Admin login successful from " + clientIp + "."); await SendJsonAsync(response, 200, new { success = true, token = text, message = "Authentication successful." }); } else { AddLog("warn", "AUTH", "Failed admin login attempt from " + clientIp + "."); 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(); } }); float num = 1f / Mathf.Max(Time.unscaledDeltaTime, 0.0001f); long num2 = GC.GetTotalMemory(forceFullCollection: false) / 1048576; ServerTelemetryDto data = new ServerTelemetryDto { uptime = uptimeStr, uptimeSeconds = (long)uptimeSpan.TotalSeconds, onlineCount = onlineCount, maxPlayers = maxPlayers, fps = (int)Math.Round(num), tickRate = "50 Hz", activeZdos = activeZdos, memoryMb = (int)num2 }; await SendJsonAsync(response, 200, data); } private static async Task HandleGetPlayers(HttpListenerResponse response) { List<object> playerList = new List<object>(); await MainThreadDispatcher.EnqueueAsync(delegate { //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0053: 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_0066: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)ZNet.instance != (Object)null) { 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); 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 + "'"); 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; } await MainThreadDispatcher.EnqueueAsync(delegate { if ((Object)(object)ZNet.instance != (Object)null) { ZNet.instance.Ban(name); } }); AddLog("warn", "BAN", "Banned player '" + name + "' (Reason: " + reason + ")"); await SendJsonAsync(response, 200, new { success = true, message = "Banned player '" + name + "'" }); } 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; } AddLog("cmd", "ADMIN", "> " + cmd); await MainThreadDispatcher.EnqueueAsync(delegate { if ((Object)(object)Console.instance != (Object)null) { ((Terminal)Console.instance).TryRunCommand(cmd, false, false); } }); await SendJsonAsync(response, 200, new { success = true, output = "Command '" + cmd + "' executed." }); } 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(); } bool enabled = BifrostheimPlugin.DailyRestartEnabled?.Value ?? _dailyRestart.enabled; string time = BifrostheimPlugin.DailyRestartTime?.Value ?? _dailyRestart.time; string mode = BifrostheimPlugin.LifecycleRestartMode?.Value ?? _lifecycleConfig.mode; string scriptPath = BifrostheimPlugin.LifecycleScriptPath?.Value ?? _lifecycleConfig.scriptPath; var data = new { scheduledRestart = (_scheduledRestart.active ? new { active = true, targetTimestamp = _scheduledRestart.targetTimestamp, totalMinutes = _scheduledRestart.totalMinutes, remainingSeconds = Math.Max(0, (int)((_scheduledRestart.targetTimestamp - DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()) / 1000)), reason = _scheduledRestart.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(); _scheduledRestart = new ScheduledRestartState { active = true, minutes = minutes, totalMinutes = minutes, targetTimestamp = targetTimestamp, reason = reason }; _restartWarningsSent.Clear(); _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) { _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)); _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 { } 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) { _lifecycleConfig = lifecycleConfigState; if (BifrostheimPlugin.LifecycleRestartMode != null) { BifrostheimPlugin.LifecycleRestartMode.Value = lifecycleConfigState.mode; } if (BifrostheimPlugin.LifecycleScriptPath != null) { BifrostheimPlugin.LifecycleScriptPath.Value = lifecycleConfigState.scriptPath; } try { BifrostheimPlugin instance = BifrostheimPlugin.Instance; if (instance != null) { ConfigFile config = ((BaseUnityPlugin)instance).Config; if (config != null) { config.Save(); } } } catch { } } 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)."); await SendJsonAsync(response, 200, new { success = true, lifecycleConfig = lifecycleConfigState2 }); } public static void TickLifecycle() { if ((DateTime.UtcNow - _lastLifecycleTick).TotalSeconds < 1.0) { return; } _lastLifecycleTick = DateTime.UtcNow; try { 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 && num2 > num3 - 3 && !_restartWarningsSent.Contains(num3)) { _restartWarningsSent.Add(num3); string text = ((num3 >= 60) ? string.Format("{0} minute{1}", num3 / 60, (num3 / 60 > 1) ? "s" : "") : $"{num3} seconds"); ZNetHelper.BroadcastServerMessage("⚠\ufe0f SERVER RESTART in " + text + "! Reason: " + _scheduledRestart.reason + ". Please find shelter."); AddLog("warn", "RESTART", "Broadcast in-game warning: " + text + " remaining."); } } if (num2 <= 0) { _isExecutingRestart = true; ExecuteServerRestartSequence(); } } bool num4 = BifrostheimPlugin.DailyRestartEnabled?.Value ?? _dailyRestart.enabled; string text2 = BifrostheimPlugin.DailyRestartTime?.Value ?? _dailyRestart.time; if (num4 && !string.IsNullOrWhiteSpace(text2) && (_scheduledRestart == null || !_scheduledRestart.active)) { string text3 = DateTime.UtcNow.ToString("yyyy-MM-dd"); if (DateTime.Now.ToString("HH:mm") == text2 && _lastDailyRestartDate != text3) { _lastDailyRestartDate = text3; 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(); AddLog("warn", "RESTART", $"Daily restart triggered automatically for {num5}m countdown."); ZNetHelper.BroadcastServerMessage($"⚠\ufe0f AUTOMATED DAILY RESTART scheduled in {num5} minutes. World will be saved."); } } } 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..."); 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 log5 = BifrostheimPlugin.Log; if (log5 != null) { log5.LogInfo((object)"[WebApiRouter] World save completed before restart."); } } } catch (Exception ex2) { ManualLogSource log6 = BifrostheimPlugin.Log; if (log6 != null) { log6.LogError((object)("[WebApiRouter] Error saving world before restart: " + ex2.Message)); } } }); ClearPendingChanges(); WebPortalServer.Stop(); string obj = BifrostheimPlugin.LifecycleRestartMode?.Value ?? _lifecycleConfig.mode; string text = BifrostheimPlugin.LifecycleScriptPath?.Value ?? _lifecycleConfig.scriptPath; if (obj.Equals("SpawnProcess", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(text)) { try { ManualLogSource log = BifrostheimPlugin.Log; if (log != null) { log.LogInfo((object)("[WebApiRouter] Spawning external restart process: '" + text + "'")); } Process.Start(new ProcessStartInfo { FileName = text, UseShellExecute = true }); } catch (Exception ex) { ManualLogSource log2 = BifrostheimPlugin.Log; if (log2 != null) { log2.LogError((object)("[WebApiRouter] Failed to spawn external restart script: " + ex.Message)); } } } try { Process.Start(new ProcessStartInfo { FileName = "supervisorctl", Arguments = "shutdown", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }); ManualLogSource log3 = BifrostheimPlugin.Log; if (log3 != null) { log3.LogInfo((object)"[WebApiRouter] Dispatched 'supervisorctl shutdown' to trigger Docker container restart."); } } catch { } try { Process.Start(new ProcessStartInfo { FileName = "kill", Arguments = "-15 1", UseShellExecute = false, CreateNoWindow = true }); } catch { } await MainThreadDispatcher.EnqueueAsync(delegate { ManualLogSource log5 = BifrostheimPlugin.Log; if (log5 != null) { log5.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 log4 = BifrostheimPlugin.Log; if (log4 != null) { log4.LogError((object)$"[WebApiRouter] Error during restart execution: {arg}"); } _isExecutingRestart = false; } }); } private static async Task HandleGetCharacterBindings(HttpListenerResponse response) { List<Dictionary<string, object>> data = ConfigSyncManager.LoadCharacterVaultBindings(); 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 { } await SendJsonAsync(response, 200, _skaldChronicle); } 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") }; _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) { using StreamReader reader = new StreamReader(request.InputStream, request.ContentEncoding); return await reader.ReadToEndAsync(); } private static async Task SendJsonAsync(HttpListenerResponse response, int statusCode, object data) { string s = SimpleJson.SerializeObject(data); 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 static class WebPortalServer { private static HttpListener? _listener; private static CancellationTokenSource? _cts; private static byte[]? _embeddedIndexHtmlBytes; private static bool _isRunning; public static string AdminPassword { get; private set; } = string.Empty; public static void Start(int port, string password) { if (_isRunning) { return; } 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 { } continue; } break; } if (!flag || _listener == null) { BifrostheimPlugin.Log.LogError((object)$"[WebPortalServer] Failed to bind HTTP listener on port {port}."); } else { _isRunning = true; LoadEmbeddedAssets(); while (!_cts.Token.IsCancellationRequested && _listener.IsListening) { try { ProcessRequestAsync(await _listener.GetContextAsync()); } catch (HttpListenerException) when (_cts.Token.IsCancellationRequested) { break; } catch (Exception ex3) { if (!_cts.Token.IsCancellationRequested) { BifrostheimPlugin.Log.LogWarning((object)("[WebPortalServer] Request accept error: " + ex3.Message)); } } } } } catch (Exception arg) { BifrostheimPlugin.Log.LogError((object)$"[WebPortalServer] Server error: {arg}"); } finally { _isRunning = false; } }); } public static void Stop() { if (!_isRunning) { return; } 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)); } finally { _isRunning = false; } } 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)) { context.Response.AddHeader("Access-Control-Allow-Origin", "*"); 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.StatusCode = 204; context.Response.Close(); } else if (text.StartsWith("/api/", StringComparison.OrdinalIgnoreCase)) { await WebApiRouter.HandleApiRequestAsync(context, text, clientIp); } else { await ServeStaticSpaAsync(context.Response); } } catch (Exception arg) { BifrostheimPlugin.Log.LogError((object)$"[WebPortalServer] Error processing request: {arg}"); try { context.Response.StatusCode = 500; context.Response.Close(); } catch { } } } 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.LogWarning((object)"[WebPortalServer] Embedded index.html not found in assembly resources. Using fallback UI."); } catch (Exception ex) { BifrostheimPlugin.Log.LogError((object)("[WebPortalServer] Error loading embedded assets: " + ex.Message)); } } private static byte[] GetDefaultFallbackHtml() { string s = "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>Bifröstheim Server Portal</title><style>body{background:#030712;color:#f3f4f6;font-family:sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;} .card{background:#111827;padding:2rem;border-radius:12px;border:1px solid #374151;text-align:center;max-width:480px;}</style></head><body><div class=\"card\"><h1 style=\"color:#f97316;\">Bifröstheim</h1><p>Embedded web portal bundle not found in assembly.</p><p>API is active at <code>/api/modules/installed</code></p></div></body></html>"; return Encoding.UTF8.GetBytes(s); } private static string GetClientIp(HttpListenerRequest request) { string text = request.Headers["X-Forwarded-For"]; if (!string.IsNullOrWhiteSpace(text)) { string[] array = text.Split(new char[1] { ',' }); if (array.Length != 0 && !string.IsNullOrWhiteSpace(array[0])) { return array[0].Trim(); } } return request.RemoteEndPoint?.Address?.ToString() ?? "127.0.0.1"; } } } namespace Bifrostheim.Helpers { public class BepInExLogListener : ILogListener, IDisposable { public void LogEvent(object sender, LogEventArgs eventArgs) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) if (eventArgs == null || eventArgs.Data == null) { return; } string level; if ((eventArgs.Level & 3) != 0) { level = "error"; } else if ((eventArgs.Level & 4) != 0) { level = "warn"; } else if ((eventArgs.Level & 0x18) != 0) { level = "info"; } else { if ((eventArgs.Level & 0x20) == 0) { return; } ConfigEntry<bool> verboseLogging = BifrostheimPlugin.VerboseLogging; if (verboseLogging == null || !verboseLogging.Value) { return; } level = "info"; } ILogSource source = eventArgs.Source; string text = ((source != null) ? source.SourceName : null) ?? "Server"; string text2 = eventArgs.Data.ToString() ?? string.Empty; if (!string.IsNullOrWhiteSpace(text2) && (!text.Equals("Bifrostheim", StringComparison.OrdinalIgnoreCase) || !text2.Contains("[WebPortalServer]"))) { WebApiRouter.AddLog(level, text, text2); } } public void Dispose() { } } public static class ConfigSyncManager { public static Func<string, string, bool>? OnlinePlayerChecker { get; set; } public static string GetConfigDirectory() { string environmentVariable = Environment.GetEnvironmentVariable("BEPINEX_CONFIG_PATH"); if (!string.IsNullOrWhiteSpace(environmentVariable)) { if (!Directory.Exists(environmentVariable)) { Directory.CreateDirectory(environmentVariable); } return environmentVariable; } try { if (!string.IsNullOrWhiteSpace(Paths.ConfigPath)) { if (!Directory.Exists(Paths.ConfigPath)) { Directory.CreateDirectory(Paths.ConfigPath); } return Paths.ConfigPath; } } catch { } try { string text = Path.Combine(Directory.GetCurrentDirectory(), "BepInEx", "config"); if (!Directory.Exists(text)) { Directory.CreateDirectory(text); } return text; } catch { } return AppDomain.CurrentDomain.BaseDirectory; } public static string ResolveConfigFile(string primaryFileName, params string[] alternativeFileNames) { string configDirectory = GetConfigDirectory(); string text = Path.Combine(configDirectory, primaryFileName); if (File.Exists(text)) { return text; } foreach (string path in alternativeFileNames) { string text2 = Path.Combine(configDirectory, path); if (File.Exists(text2)) { return text2; } } return text; } public static Dictionary<string, Dictionary<string, string>> ReadIniFile(string filePath) { Dictionary<string, Dictionary<string, string>> dictionary = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase); if (!File.Exists(filePath)) { return dictionary; } try { string[] array = File.ReadAllLines(filePath, Encoding.UTF8); string key = "General"; if (!dictionary.ContainsKey(key)) { dictionary[key] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } string[] array2 = array; for (int i = 0; i < array2.Length; i++) { string text = array2[i].Trim(); if (string.IsNullOrEmpty(text) || text.StartsWith("#") || text.StartsWith(";")) { continue; } if (text.StartsWith("[") && text.EndsWith("]")) { key = text.Substring(1, text.Length - 2).Trim(); if (!dictionary.ContainsKey(key)) { dictionary[key] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } continue; } int num = text.IndexOf('='); if (num > 0) { string key2 = text.Substring(0, num).Trim(); string value = text.Substring(num + 1).Trim(); dictionary[key][key2] = value; } } } catch (Exception ex) { ManualLogSource log = BifrostheimPlugin.Log; if (log != null) { log.LogError((object)("[ConfigSyncManager] Error reading INI file '" + filePath + "': " + ex.Message)); } } return dictionary; } public static void WriteIniFile(string filePath, Dictionary<string, Dictionary<string, string>> updates, string defaultHeader = "") { try { string directoryName = Path.GetDirectoryName(filePath); if (!string.IsNullOrEmpty(directoryName) && !Directory.Exists(directoryName)) { Directory.CreateDirectory(directoryName); } if (!File.Exists(filePath)) { StringBuilder stringBuilder = new StringBuilder(); if (!string.IsNullOrEmpty(defaultHeader)) { stringBuilder.AppendLine(defaultHeader); stringBuilder.AppendLine(); } foreach (KeyValuePair<string, Dictionary<string, string>> update in updates) { stringBuilder.AppendLine("[" + update.Key + "]"); stringBuilder.AppendLine(); foreach (KeyValuePair<string, string> item in update.Value) { stringBuilder.AppendLine(item.Key + " = " + item.Value); } stringBuilder.AppendLine(); } File.WriteAllText(filePath, stringBuilder.ToString(), Encoding.UTF8); ManualLogSource log = BifrostheimPlugin.Log; if (log != null) { log.LogInfo((object)("[ConfigSyncManager] Created new config file at '" + filePath + "'.")); } return; } List<string> list = File.ReadAllLines(filePath, Encoding.UTF8).ToList(); Dictionary<string, Dictionary<string, string>> dictionary = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair<string, Dictionary<string, string>> update2 in updates) { dictionary[update2.Key] = new Dictionary<string, string>(update2.Value, StringComparer.OrdinalIgnoreCase); } string text = ""; for (int i = 0; i < list.Count; i++) { string text2 = list[i].Trim(); if (text2.StartsWith("[") && text2.EndsWith("]")) { if (!string.IsNullOrEmpty(text) && dictionary.TryGetValue(text, out var value) && value.Count > 0) { foreach (KeyValuePair<string, string> item2 in value.ToList()) { list.Insert(i, item2.Key + " = " + item2.Value); value.Remove(item2.Key); i++; } } text = text2.Substring(1, text2.Length - 2).Trim(); } else { if (text2.StartsWith("#") || text2.StartsWith(";")) { continue; } int num = text2.IndexOf('='); if (num > 0) { string text3 = text2.Substring(0, num).Trim(); if (dictionary.TryGetValue(text, out var value2) && value2.TryGetValue(text3, out var value3)) { list[i] = text3 + " = " + value3; value2.Remove(text3); } } } } if (dictionary.TryGetValue(text, out var value4) && value4.Count > 0) { foreach (KeyValuePair<string, string> item3 in value4) { list.Add(item3.Key + " = " + item3.Value); } dictionary.Remove(text); } foreach (KeyValuePair<string, Dictionary<string, string>> item4 in dictionary) { if (item4.Value.Count == 0) { continue; } list.Add(""); list.Add("[" + item4.Key + "]"); list.Add(""); foreach (KeyValuePair<string, string> item5 in item4.Value) { list.Add(item5.Key + " = " + item5.Value); } } File.WriteAllLines(filePath, list, Encoding.UTF8); ManualLogSource log2 = BifrostheimPlugin.Log; if (log2 != null) { log2.LogInfo((object)("[ConfigSyncManager] Saved configuration updates to '" + filePath + "'.")); } } catch (Exception ex) { ManualLogSource log3 = BifrostheimPlugin.Log; if (log3 != null) { log3.LogError((object)("[ConfigSyncManager] Error writing INI file '" + filePath + "': " + ex.Message)); } } } public static void SyncLivePluginConfig(string[] candidateGuids, Action<ConfigFile> updateAction) { try { if (Chainloader.PluginInfos == null) { return; } foreach (KeyValuePair<string, PluginInfo> pluginInfo in Chainloader.PluginInfos) { string guid = pluginInfo.Key; PluginInfo value = pluginInfo.Value; if (value == null || !candidateGuids.Any((string g) => string.Equals(guid, g, StringComparison.OrdinalIgnoreCase) || guid.IndexOf(g, StringComparison.OrdinalIgnoreCase) >= 0)) { continue; } BaseUnityPlugin instance = value.Instance; ConfigFile val = ((instance != null) ? instance.Config : null); if (val == null) { continue; } updateAction(val); try { val.Save(); ManualLogSource log = BifrostheimPlugin.Log; if (log != null) { log.LogInfo((object)("[ConfigSyncManager] Live-synchronized and saved ConfigFile for loaded plugin '" + guid + "'.")); } } catch (Exception ex) { ManualLogSource log2 = BifrostheimPlugin.Log; if (log2 != null) { log2.LogWarning((object)("[ConfigSyncManager] Failed to call ConfigFile.Save() on plugin '" + guid + "': " + ex.Message)); } } } } catch (Exception ex2) { ManualLogSource log3 = BifrostheimPlugin.Log; if (log3 != null) { log3.LogWarning((object)("[ConfigSyncManager] Live plugin sync error: " + ex2.Message)); } } } private static void TrySetEntryValue(ConfigFile configFile, string keyName, object value) { try { PropertyInfo property = ((object)configFile).GetType().GetProperty("Keys", BindingFlags.Instance | BindingFlags.Public); if (!(property != null) || !(property.GetValue(configFile) is IEnumerable<ConfigDefinition> enumerable)) { return; } foreach (ConfigDefinition item in enumerable) { if (!item.Key.Equals(keyName, StringComparison.OrdinalIgnoreCase)) { continue; } PropertyInfo property2 = ((object)configFile).GetType().GetProperty("Item", new Type[1] { typeof(ConfigDefinition) }); if (property2 != null) { object? value2 = property2.GetValue(configFile, new object[1] { item }); ConfigEntryBase val = (ConfigEntryBase)((value2 is ConfigEntryBase) ? value2 : null); if (val != null) { val.BoxedValue = Convert.ChangeType(value, val.SettingType, CultureInfo.InvariantCulture); break; } } } } catch (Exception ex) { ManualLogSource log = BifrostheimPlugin.Log; if (log != null) { log.LogWarning((object)("[ConfigSyncManager] Failed to set live entry '" + keyName + "': " + ex.Message)); } } } public static ValgrindConfigDto LoadValgrindConfig() { ValgrindConfigDto valgrindConfigDto = new ValgrindConfigDto(); Dictionary<string, Dictionary<string, string>> ini = ReadIniFile(ResolveConfigFile("com.bigai.valgrind.cfg", "valgrind.cfg")); string text = FindValue(ini, "CalculationMode", "calculationMode", "calcMode"); if (!string.IsNullOrWhiteSpace(text)) { string text2 = text.Trim(); if (text2.Equals("TieredBrackets", StringComparison.OrdinalIgnoreCase)) { valgrindConfigDto.calculationMode = "TieredBrackets"; } else if (text2.Equals("ContinuousCurve", StringComparison.OrdinalIgnoreCase)) { valgrindConfigDto.calculationMode = "ContinuousCurve"; } else if (text2.Equals("PerSkill", StringComparison.OrdinalIgnoreCase)) { valgrindConfigDto.calculationMode = "PerSkill"; } else { valgrindConfigDto.calculationMode = text2; } } if (bool.TryParse(FindValue(ini, "UseTopNSkillsOnly", "useTopNSkillsOnly"), out var result)) { valgrindConfigDto.useTopNSkillsOnly = result; } if (int.TryParse(FindValue(ini, "TopNSkillsCount", "topNSkillsCount"), out var result2)) { valgrindConfigDto.topNSkillsCount = Math.Max(1, Math.Min(20, result2)); } if (bool.TryParse(FindValue(ini, "ResetAccumulatorOnDeath", "resetAccumulatorOnDeath"), out var result3)) { valgrindConfigDto.resetAccumulatorOnDeath = result3; } if (bool.TryParse(FindValue(ini, "EnableDebugLogging", "enableDebugLogging"), out var result4)) { valgrindConfigDto.enableDebugLogging = result4; } if (float.TryParse(FindValue(ini, "EarlyGameLossPercent", "earlyGameLossPercent"), NumberStyles.Any, CultureInfo.InvariantCulture, out var result5)) { valgrindConfigDto.earlyGameLossPercent = result5; } if (float.TryParse(FindValue(ini, "MidGameLossPercent", "midGameLossPercent"), NumberStyles.Any, CultureInfo.InvariantCulture, out var result6)) { valgrindConfigDto.midGameLossPercent = result6; } if (float.TryParse(FindValue(ini, "LateGameLossPercent", "lateGameLossPercent"), NumberStyles.Any, CultureInfo.InvariantCulture, out var result7)) { valgrindConfigDto.lateGameLossPercent = result7; } if (float.TryParse(FindValue(ini, "EndgameLossPercent", "endgameLossPercent"), NumberStyles.Any, CultureInfo.InvariantCulture, out var result8)) { valgrindConfigDto.endgameLossPercent = result8; } if (float.TryParse(FindValue(ini, "CurveMaxLossPercent", "curveMaxLossPercent"), NumberStyles.Any, CultureInfo.InvariantCulture, out var result9)) { valgrindConfigDto.curveMaxLossPercent = result9; } if (float.TryParse(FindValue(ini, "CurveMinLossPercent", "curveMinLossPercent"), NumberStyles.Any, CultureInfo.InvariantCulture, out var result10)) { valgrindConfigDto.curveMinLossPercent = result10; } return valgrindConfigDto; } public static void SaveValgrindConfig(ValgrindConfigDto dto) { string filePath = ResolveConfigFile("com.bigai.valgrind.cfg", "valgrind.cfg"); Dictionary<string, Dictionary<string, string>> updates = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase) { ["1 - General"] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { ["CalculationMode"] = dto.calculationMode, ["UseTopNSkillsOnly"] = dto.useTopNSkillsOnly.ToString().ToLowerInvariant(), ["TopNSkillsCount"] = dto.topNSkillsCount.ToString(CultureInfo.InvariantCulture), ["ResetAccumulatorOnDeath"] = dto.resetAccumulatorOnDeath.ToString().ToLowerInvariant(), ["EnableDebugLogging"] = dto.enableDebugLogging.ToString().ToLowerInvariant() }, ["2 - Tiered Brackets"] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { ["EarlyGameLossPercent"] = dto.earlyGameLossPercent.ToString("F1", CultureInfo.InvariantCulture), ["MidGameLossPercent"] = dto.midGameLossPercent.ToString("F1", CultureInfo.InvariantCulture), ["LateGameLossPercent"] = dto.lateGameLossPercent.ToString("F1", CultureInfo.InvariantCulture), ["EndgameLossPercent"] = dto.endgameLossPercent.ToString("F1", CultureInfo.InvariantCulture) }, ["3 - Continuous Curve"] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { ["CurveMaxLossPercent"] = dto.curveMaxLossPercent.ToString("F1", CultureInfo.InvariantCulture), ["CurveMinLossPercent"] = dto.curveMinLossPercent.ToString("F1", CultureInfo.InvariantCulture) } }; WriteIniFile(filePath, updates, "## Settings file was created by plugin Valgrind\n## Plugin GUID: com.bigai.valgrind"); SyncLivePluginConfig(new string[2] { "com.bigai.valgrind", "valgrind" }, delegate(ConfigFile config) { TrySetEntryValue(config, "CalculationMode", dto.calculationMode); TrySetEntryValue(config, "UseTopNSkillsOnly", dto.useTopNSkillsOnly); TrySetEntryValue(config, "TopNSkillsCount", dto.topNSkillsCount); TrySetEntryValue(config, "ResetAccumulatorOnDeath", dto.resetAccumulatorOnDeath); TrySetEntryValue(config, "EnableDebugLogging", dto.enableDebugLogging); TrySetEntryValue(config, "EarlyGameLossPercent", dto.earlyGameLossPercent); TrySetEntryValue(config, "MidGameLossPercent", dto.midGameLossPercent); TrySetEntryValue(config, "LateGameLossPercent", dto.lateGameLossPercent); TrySetEntryValue(config, "EndgameLossPercent", dto.endgameLossPercent); TrySetEntryValue(config, "CurveMaxLossPercent", dto.curveMaxLossPercent); TrySetEntryValue(config, "CurveMinLossPercent", dto.curveMinLossPercent); }); } public static DagrNottConfigDto LoadDagrNottConfig() { DagrNottConfigDto dagrNottConfigDto = new DagrNottConfigDto(); Dictionary<string, Dictionary<string, string>> ini = ReadIniFile(ResolveConfigFile("com.bigai.dagrnott_customdaycycle.cfg", "com.bigai.dagrandnott.cfg", "com.bigai.dagrnott.cfg", "dagrnott_customdaycycle.cfg", "dagrandnott.cfg", "dagrnott.cfg")); string text = FindValue(ini, "DawnMultiplier", "dawnMultiplier"); if (text != null && float.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) { dagrNottConfigDto.dawnMultiplier = (float)Math.Round(Math.Max(0.01f, result), 2); } string text2 = FindValue(ini, "DayMultiplier", "dayMultiplier"); if (text2 != null && float.TryParse(text2, NumberStyles.Any, CultureInfo.InvariantCulture, out var result2)) { dagrNottConfigDto.dayMultiplier = (float)Math.Round(Math.Max(0.01f, result2), 2); } string text3 = FindValue(ini, "DuskMultiplier", "duskMultiplier"); if (text3 != null && float.TryParse(text3, NumberStyles.Any, CultureInfo.InvariantCulture, out var result3)) { dagrNottConfigDto.duskMultiplier = (float)Math.Round(Math.Max(0.01f, result3), 2); } string text4 = FindValue(ini, "NightMultiplier", "nightMultiplier"); if (text4 != null && float.TryParse(text4, NumberStyles.Any, CultureInfo.InvariantCulture, out var result4)) { dagrNottConfigDto.nightMultiplier = (float)Math.Round(Math.Max(0.01f, result4), 2); } string text5 = FindValue(ini, "LogPhaseTransitions", "logPhaseTransitions"); if (text5 != null && bool.TryParse(text5, out var result5)) { dagrNottConfigDto.logPhaseTransitions = result5; } dagrNottConfigDto.dawnMinutes = (float)Math.Round(4.5f / Math.Max(0.001f, dagrNottConfigDto.dawnMultiplier), 1); dagrNottConfigDto.dayMinutes = (float)Math.Round(15f / Math.Max(0.001f, dagrNottConfigDto.dayMultiplier), 1); dagrNottConfigDto.duskMinutes = (float)Math.Round(4.5f / Math.Max(0.001f, dagrNottConfigDto.duskMultiplier), 1); dagrNottConfigDto.nightMinutes = (float)Math.Round(6f / Math.Max(0.001f, dagrNottConfigDto.nightMultiplier), 1); dagrNottConfigDto.totalMinutes = (float)Math.Round(dagrNottConfigDto.dawnMinutes + dagrNottConfigDto.dayMinutes + dagrNottConfigDto.duskMinutes + dagrNottConfigDto.nightMinutes, 1); return dagrNottConfigDto; } public static void SaveDagrNottConfig(DagrNottConfigDto dto) { string filePath = ResolveConfigFile("com.bigai.dagrnott_customdaycycle.cfg", "com.bigai.dagrandnott.cfg", "com.bigai.dagrnott.cfg", "dagrnott_customdaycycle.cfg", "dagrandnott.cfg", "dagrnott.cfg"); Dictionary<string, Dictionary<string, string>> updates = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase) { ["DayCycle"] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { ["DawnMultiplier"] = dto.dawnMultiplier.ToString("F2", CultureInfo.InvariantCulture), ["DayMultiplier"] = dto.dayMultiplier.ToString("F2", CultureInfo.InvariantCulture), ["DuskMultiplier"] = dto.duskMultiplier.ToString("F2", CultureInfo.InvariantCulture), ["NightMultiplier"] = dto.nightMultiplier.ToString("F2", CultureInfo.InvariantCulture) }, ["Logging"] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { ["LogPhaseTransitions"] = dto.logPhaseTransitions.ToString().ToLowerInvariant() } }; WriteIniFile(filePath, updates, "## Settings file was created by plugin DagrNott_CustomDayCycle\n## Plugin GUID: com.bigai.dagrnott_customdaycycle"); SyncLivePluginConfig(new string[6] { "com.bigai.dagrnott_customdaycycle", "com.bigai.dagrandnott", "com.bigai.dagrnott", "dagrnott_customdaycycle", "dagrandnott", "dagrnott" }, delegate(ConfigFile config) { TrySetEntryValue(config, "DawnMultiplier", dto.dawnMultiplier); TrySetEntryValue(config, "DayMultiplier", dto.dayMultiplier); TrySetEntryValue(config, "DuskMultiplier", dto.duskMultiplier); TrySetEntryValue(config, "NightMultiplier", dto.nightMultiplier); TrySetEntryValue(config, "LogPhaseTransitions", dto.logPhaseTransitions); }); } public static SkaldConfigDto LoadSkaldConfig() { SkaldConfigDto skaldConfigDto = new SkaldConfigDto(); Dictionary<string, Dictionary<string, string>> ini = ReadIniFile(ResolveConfigFile("com.bigai.skald_vikingkillfeed.cfg", "com.bigai.skald.cfg", "skald_vikingkillfeed.cfg", "skald.cfg")); if (bool.TryParse(FindValue(ini, "EnableDeathAnnouncements", "Enabled"), out var result)) { skaldConfigDto.enabled = result; } if (bool.TryParse(Fin