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> list = ConfigSyncManager.LoadCharacterVaultBindings(); Console.WriteLine($"Loaded {list.Count} bindings."); foreach (Dictionary 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 dictionary = list.Find((Dictionary 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 dictionary2 = list.Find((Dictionary 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 dictionary3 = ConfigSyncManager.LoadCharacterVaultBindings().Find((Dictionary 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 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 EnableWebPortal; public static ConfigEntry WebPortalPort; public static ConfigEntry WebAdminPassword; public static ConfigEntry VerboseLogging; public static ConfigEntry LifecycleRestartMode; public static ConfigEntry LifecycleScriptPath; public static ConfigEntry DailyRestartEnabled; public static ConfigEntry 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("WebPortal", "EnableWebPortal", true, "Enable the embedded web management portal."); WebPortalPort = ((BaseUnityPlugin)this).Config.Bind("WebPortal", "WebPortalPort", 8080, "Port for the embedded web management portal."); WebAdminPassword = ((BaseUnityPlugin)this).Config.Bind("WebPortal", "WebAdminPassword", "admin", "Password required for administrative actions in the web portal."); VerboseLogging = ((BaseUnityPlugin)this).Config.Bind("General", "VerboseLogging", false, "Enable verbose logging in BepInEx console."); LifecycleRestartMode = ((BaseUnityPlugin)this).Config.Bind("Lifecycle", "RestartMode", "ExitOnly", "Server restart strategy: ExitOnly or SpawnProcess."); LifecycleScriptPath = ((BaseUnityPlugin)this).Config.Bind("Lifecycle", "RestartScriptPath", "./start_server.sh", "Path to external restart script when RestartMode is SpawnProcess."); DailyRestartEnabled = ((BaseUnityPlugin)this).Config.Bind("Lifecycle", "DailyRestartEnabled", false, "Enable automated daily server restart."); DailyRestartTime = ((BaseUnityPlugin)this).Config.Bind("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 LogsBuffer = new List(); 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 _restartWarningsSent = new HashSet(); private static string _lastDailyRestartDate = string.Empty; private static bool _isExecutingRestart = false; private static readonly List _pendingChanges = new List(); private static readonly object _pendingLock = new object(); private static List _skaldChronicle = new List(); 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.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(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 list = new List(); 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 playerList = new List(); 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(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(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 bansList = new List(); 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(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(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 data; lock (LogLock) { data = new List(LogsBuffer); } await SendJsonAsync(response, 200, data); } private static async Task HandleExecCommand(HttpListenerRequest request, HttpListenerResponse response, string clientIp) { string cmd = SimpleJson.DeserializeObject(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(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 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 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(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(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(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> data = ConfigSyncManager.LoadCharacterVaultBindings(); await SendJsonAsync(response, 200, data); } private static async Task HandleUnbindCharacter(HttpListenerRequest request, HttpListenerResponse response, string clientIp) { string text = SimpleJson.DeserializeObject(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(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(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(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 list = new List(); 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(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 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(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(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 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? 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 entries { get; set; } = new List(); } 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 sections { get; set; } = new List(); 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>? 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 = "Bifröstheim Server Portal

Bifröstheim

Embedded web portal bundle not found in assembly.

API is active at /api/modules/installed

"; 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 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? 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> ReadIniFile(string filePath) { Dictionary> dictionary = new Dictionary>(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(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(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> 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> update in updates) { stringBuilder.AppendLine("[" + update.Key + "]"); stringBuilder.AppendLine(); foreach (KeyValuePair 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 list = File.ReadAllLines(filePath, Encoding.UTF8).ToList(); Dictionary> dictionary = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair> update2 in updates) { dictionary[update2.Key] = new Dictionary(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 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 item3 in value4) { list.Add(item3.Key + " = " + item3.Value); } dictionary.Remove(text); } foreach (KeyValuePair> item4 in dictionary) { if (item4.Value.Count == 0) { continue; } list.Add(""); list.Add("[" + item4.Key + "]"); list.Add(""); foreach (KeyValuePair 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 updateAction) { try { if (Chainloader.PluginInfos == null) { return; } foreach (KeyValuePair 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 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> 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> updates = new Dictionary>(StringComparer.OrdinalIgnoreCase) { ["1 - General"] = new Dictionary(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(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(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> 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> updates = new Dictionary>(StringComparer.OrdinalIgnoreCase) { ["DayCycle"] = new Dictionary(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(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> 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(FindValue(ini, "EnableBossDefeatAnnouncements", "EnableBosses"), out var result2)) { skaldConfigDto.enableBosses = result2; } if (bool.TryParse(FindValue(ini, "IncludeBiomeInMessage", "IncludeBiome"), out var result3)) { skaldConfigDto.includeBiome = result3; } if (bool.TryParse(FindValue(ini, "LogToConsole"), out var result4)) { skaldConfigDto.logToConsole = result4; } string text = FindValue(ini, "MonsterDeathMessages", "MonsterTemplates"); if (!string.IsNullOrEmpty(text)) { skaldConfigDto.monsterTemplates = text; } string text2 = FindValue(ini, "BossDeathMessages", "BossTemplates"); if (!string.IsNullOrEmpty(text2)) { skaldConfigDto.bossTemplates = text2; } string text3 = FindValue(ini, "OverwhelmedMessages"); if (!string.IsNullOrEmpty(text3)) { skaldConfigDto.overwhelmedMessages = text3; } string text4 = FindValue(ini, "GenericDeathMessages"); if (!string.IsNullOrEmpty(text4)) { skaldConfigDto.genericDeathMessages = text4; } return skaldConfigDto; } public static void SaveSkaldConfig(SkaldConfigDto dto) { string filePath = ResolveConfigFile("com.bigai.skald_vikingkillfeed.cfg", "com.bigai.skald.cfg", "skald_vikingkillfeed.cfg", "skald.cfg"); Dictionary> updates = new Dictionary>(StringComparer.OrdinalIgnoreCase) { ["1 - General"] = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["EnableDeathAnnouncements"] = dto.enabled.ToString().ToLowerInvariant(), ["EnableBossDefeatAnnouncements"] = dto.enableBosses.ToString().ToLowerInvariant(), ["IncludeBiomeInMessage"] = dto.includeBiome.ToString().ToLowerInvariant(), ["LogToConsole"] = dto.logToConsole.ToString().ToLowerInvariant() }, ["2 - Templates"] = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["MonsterDeathMessages"] = dto.monsterTemplates, ["BossDeathMessages"] = dto.bossTemplates, ["OverwhelmedMessages"] = dto.overwhelmedMessages, ["GenericDeathMessages"] = dto.genericDeathMessages } }; WriteIniFile(filePath, updates, "## Settings file was created by plugin Skald_VikingKillFeed\n## Plugin GUID: com.bigai.skald_vikingkillfeed"); SyncLivePluginConfig(new string[4] { "com.bigai.skald_vikingkillfeed", "com.bigai.skald", "skald_vikingkillfeed", "skald" }, delegate(ConfigFile config) { TrySetEntryValue(config, "EnableDeathAnnouncements", dto.enabled); TrySetEntryValue(config, "EnableBossDefeatAnnouncements", dto.enableBosses); TrySetEntryValue(config, "IncludeBiomeInMessage", dto.includeBiome); TrySetEntryValue(config, "LogToConsole", dto.logToConsole); TrySetEntryValue(config, "MonsterDeathMessages", dto.monsterTemplates); TrySetEntryValue(config, "BossDeathMessages", dto.bossTemplates); TrySetEntryValue(config, "OverwhelmedMessages", dto.overwhelmedMessages); TrySetEntryValue(config, "GenericDeathMessages", dto.genericDeathMessages); }); } public static NjororConfigDto LoadNjororConfig() { NjororConfigDto njororConfigDto = new NjororConfigDto(); Dictionary> ini = ReadIniFile(ResolveConfigFile("com.bigai.njoror_fairwinds.cfg", "com.bigai.njoror.cfg", "njoror_fairwinds.cfg", "njoror.cfg")); if (bool.TryParse(FindValue(ini, "EnableFairWinds"), out var result)) { njororConfigDto.enableFairWinds = result; } if (float.TryParse(FindValue(ini, "HeadwindMitigationPercent", "HeadwindDeflectionChance"), NumberStyles.Any, CultureInfo.InvariantCulture, out var result2)) { njororConfigDto.headwindMitigationPercent = result2; } if (float.TryParse(FindValue(ini, "MinimumWindSpeedMultiplier", "MinWindSpeedMultiplier"), NumberStyles.Any, CultureInfo.InvariantCulture, out var result3)) { njororConfigDto.minWindSpeedMultiplier = result3; } if (bool.TryParse(FindValue(ini, "AlwaysTailwindInOcean"), out var result4)) { njororConfigDto.alwaysTailwindInOcean = result4; } if (bool.TryParse(FindValue(ini, "CheckDeflectOnWindChange"), out var result5)) { njororConfigDto.checkDeflectOnWindChange = result5; } if (int.TryParse(FindValue(ini, "CheckDeflectTimeSeconds"), NumberStyles.Any, CultureInfo.InvariantCulture, out var result6)) { njororConfigDto.checkDeflectTimeSeconds = result6; } if (bool.TryParse(FindValue(ini, "EnableWeatherTuning"), out var result7)) { njororConfigDto.enableWeatherTuning = result7; } if (float.TryParse(FindValue(ini, "StormFrequencyMultiplier"), NumberStyles.Any, CultureInfo.InvariantCulture, out var result8)) { njororConfigDto.stormFrequencyMultiplier = result8; } if (float.TryParse(FindValue(ini, "RainFrequencyMultiplier"), NumberStyles.Any, CultureInfo.InvariantCulture, out var result9)) { njororConfigDto.rainFrequencyMultiplier = result9; } if (float.TryParse(FindValue(ini, "ClearWeatherFrequencyMultiplier", "ClearFrequencyMultiplier"), NumberStyles.Any, CultureInfo.InvariantCulture, out var result10)) { njororConfigDto.clearFrequencyMultiplier = result10; } if (bool.TryParse(FindValue(ini, "EnableSerpentTuning"), out var result11)) { njororConfigDto.enableSerpentTuning = result11; } if (float.TryParse(FindValue(ini, "DaytimeSerpentSpawnChance"), NumberStyles.Any, CultureInfo.InvariantCulture, out var result12)) { njororConfigDto.daytimeSerpentSpawnChance = result12; } if (float.TryParse(FindValue(ini, "NighttimeSerpentSpawnChance"), NumberStyles.Any, CultureInfo.InvariantCulture, out var result13)) { njororConfigDto.nighttimeSerpentSpawnChance = result13; } if (float.TryParse(FindValue(ini, "SerpentSpawnIntervalSeconds"), NumberStyles.Any, CultureInfo.InvariantCulture, out var result14)) { njororConfigDto.serpentSpawnIntervalSeconds = result14; } if (bool.TryParse(FindValue(ini, "AllowCalmWeatherDaySerpents"), out var result15)) { njororConfigDto.allowCalmWeatherDaySerpents = result15; } return njororConfigDto; } public static void SaveNjororConfig(NjororConfigDto dto) { string filePath = ResolveConfigFile("com.bigai.njoror_fairwinds.cfg", "com.bigai.njoror.cfg", "njoror_fairwinds.cfg", "njoror.cfg"); Dictionary> updates = new Dictionary>(StringComparer.OrdinalIgnoreCase) { ["1 - Fair Winds"] = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["EnableFairWinds"] = dto.enableFairWinds.ToString().ToLowerInvariant(), ["HeadwindMitigationPercent"] = dto.headwindMitigationPercent.ToString("F1", CultureInfo.InvariantCulture), ["MinimumWindSpeedMultiplier"] = dto.minWindSpeedMultiplier.ToString("F1", CultureInfo.InvariantCulture), ["AlwaysTailwindInOcean"] = dto.alwaysTailwindInOcean.ToString().ToLowerInvariant(), ["CheckDeflectOnWindChange"] = dto.checkDeflectOnWindChange.ToString().ToLowerInvariant(), ["CheckDeflectTimeSeconds"] = dto.checkDeflectTimeSeconds.ToString(CultureInfo.InvariantCulture) }, ["2 - Weather & Storms"] = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["EnableWeatherTuning"] = dto.enableWeatherTuning.ToString().ToLowerInvariant(), ["StormFrequencyMultiplier"] = dto.stormFrequencyMultiplier.ToString("F2", CultureInfo.InvariantCulture), ["RainFrequencyMultiplier"] = dto.rainFrequencyMultiplier.ToString("F2", CultureInfo.InvariantCulture), ["ClearWeatherFrequencyMultiplier"] = dto.clearFrequencyMultiplier.ToString("F2", CultureInfo.InvariantCulture) }, ["3 - Sea Serpents"] = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["EnableSerpentTuning"] = dto.enableSerpentTuning.ToString().ToLowerInvariant(), ["DaytimeSerpentSpawnChance"] = dto.daytimeSerpentSpawnChance.ToString("F1", CultureInfo.InvariantCulture), ["NighttimeSerpentSpawnChance"] = dto.nighttimeSerpentSpawnChance.ToString("F1", CultureInfo.InvariantCulture), ["SerpentSpawnIntervalSeconds"] = dto.serpentSpawnIntervalSeconds.ToString("F0", CultureInfo.InvariantCulture), ["AllowCalmWeatherDaySerpents"] = dto.allowCalmWeatherDaySerpents.ToString().ToLowerInvariant() } }; WriteIniFile(filePath, updates, "## Settings file was created by plugin Njoror_FairWinds\n## Plugin GUID: com.bigai.njoror_fairwinds"); SyncLivePluginConfig(new string[4] { "com.bigai.njoror_fairwinds", "com.bigai.njoror", "njoror_fairwinds", "njoror" }, delegate(ConfigFile config) { TrySetEntryValue(config, "EnableFairWinds", dto.enableFairWinds); TrySetEntryValue(config, "HeadwindMitigationPercent", dto.headwindMitigationPercent); TrySetEntryValue(config, "MinimumWindSpeedMultiplier", dto.minWindSpeedMultiplier); TrySetEntryValue(config, "AlwaysTailwindInOcean", dto.alwaysTailwindInOcean); TrySetEntryValue(config, "CheckDeflectOnWindChange", dto.checkDeflectOnWindChange); TrySetEntryValue(config, "CheckDeflectTimeSeconds", dto.checkDeflectTimeSeconds); TrySetEntryValue(config, "EnableWeatherTuning", dto.enableWeatherTuning); TrySetEntryValue(config, "StormFrequencyMultiplier", dto.stormFrequencyMultiplier); TrySetEntryValue(config, "RainFrequencyMultiplier", dto.rainFrequencyMultiplier); TrySetEntryValue(config, "ClearWeatherFrequencyMultiplier", dto.clearFrequencyMultiplier); TrySetEntryValue(config, "EnableSerpentTuning", dto.enableSerpentTuning); TrySetEntryValue(config, "DaytimeSerpentSpawnChance", dto.daytimeSerpentSpawnChance); TrySetEntryValue(config, "NighttimeSerpentSpawnChance", dto.nighttimeSerpentSpawnChance); TrySetEntryValue(config, "SerpentSpawnIntervalSeconds", dto.serpentSpawnIntervalSeconds); TrySetEntryValue(config, "AllowCalmWeatherDaySerpents", dto.allowCalmWeatherDaySerpents); }); } public static string GetCharacterVaultBindingsFilePath() { string configDirectory = GetConfigDirectory(); string text = Path.Combine(configDirectory, "CharacterVault", "bindings.json"); if (File.Exists(text)) { return text; } string text2 = Path.Combine(configDirectory, "CharactersVault", "bindings.json"); if (File.Exists(text2)) { return text2; } string text3 = Path.Combine(configDirectory, "bindings.json"); if (File.Exists(text3)) { return text3; } return text; } public static List> LoadCharacterVaultBindings() { try { string characterVaultBindingsFilePath = GetCharacterVaultBindingsFilePath(); if (!File.Exists(characterVaultBindingsFilePath)) { return new List>(); } return ParseCharacterBindings(File.ReadAllText(characterVaultBindingsFilePath, Encoding.UTF8), characterVaultBindingsFilePath); } catch (Exception ex) { ManualLogSource log = BifrostheimPlugin.Log; if (log != null) { log.LogWarning((object)("[ConfigSyncManager] Error loading CharacterVault bindings: " + ex.Message)); } return new List>(); } } public static List> ParseCharacterBindings(string json, string? filePath = null) { List> list = new List>(); if (string.IsNullOrWhiteSpace(json)) { return list; } try { object obj = SimpleJson.Deserialize(json); if (obj == null) { return list; } IDictionary dictionary = obj as IDictionary; if (dictionary != null) { if (dictionary.Count == 1) { KeyValuePair keyValuePair = dictionary.First(); if (keyValuePair.Value is IDictionary dictionary2) { dictionary = dictionary2; } else if (keyValuePair.Value is IList list2) { return ParseListBindings(list2); } } foreach (KeyValuePair item in dictionary) { string key = item.Key; object value = item.Value; string characterName = "Unknown"; string created = null; string lastLogin = null; string value2 = "Bound"; if (value is string text) { characterName = text; } else if (value is IDictionary dict) { ExtractBindingFromDict(dict, key, out characterName, out created, out lastLogin, out string status); if (!string.IsNullOrWhiteSpace(status)) { value2 = status; } } else if (value is IList { Count: >0 } list3) { object obj2 = list3[0]; if (obj2 is string text2) { characterName = text2; } else if (obj2 is IDictionary dict2) { ExtractBindingFromDict(dict2, key, out characterName, out created, out lastLogin, out string status2); if (!string.IsNullOrWhiteSpace(status2)) { value2 = status2; } } } else if (value != null) { characterName = value.ToString() ?? "Unknown"; } if (IsPlayerOnline(key, characterName)) { lastLogin = "Online Now"; value2 = "Online"; } if (string.IsNullOrWhiteSpace(created) || string.IsNullOrWhiteSpace(lastLogin)) { TryGetFileTimestamps(key, characterName, out string created2, out string lastLogin2); if (string.IsNullOrWhiteSpace(created) && !string.IsNullOrWhiteSpace(created2)) { created = created2; } if (string.IsNullOrWhiteSpace(lastLogin) && !string.IsNullOrWhiteSpace(lastLogin2)) { lastLogin = lastLogin2; } } if (string.IsNullOrWhiteSpace(created)) { created = "—"; } if (string.IsNullOrWhiteSpace(lastLogin)) { lastLogin = "—"; } list.Add(new Dictionary { { "steamId", key }, { "characterName", characterName }, { "created", created }, { "lastLogin", lastLogin }, { "status", value2 } }); } } else if (obj is IList list4) { return ParseListBindings(list4); } } catch (Exception ex) { ManualLogSource log = BifrostheimPlugin.Log; if (log != null) { log.LogWarning((object)("[ConfigSyncManager] Failed to parse character bindings JSON: " + ex.Message)); } } return list; } private static List> ParseListBindings(IList list) { List> list2 = new List>(); foreach (object item in list) { if (item is IDictionary dict) { string text = string.Empty; string[] array = new string[11] { "steamId", "steam_id", "steamID", "id", "playerId", "player_id", "userId", "user_id", "key", "account", "account_id" }; foreach (string key in array) { if (TryGetCaseInsensitive(dict, key, out object value) && value != null) { text = value.ToString() ?? string.Empty; if (!string.IsNullOrWhiteSpace(text)) { break; } } } if (string.IsNullOrWhiteSpace(text)) { text = "Unknown"; } ExtractBindingFromDict(dict, text, out string characterName, out string created, out string lastLogin, out string status); string value2 = ((!string.IsNullOrWhiteSpace(status)) ? status : "Bound"); string text2 = created; string text3 = lastLogin; if (IsPlayerOnline(text, characterName)) { text3 = "Online Now"; value2 = "Online"; } if (string.IsNullOrWhiteSpace(text2) || string.IsNullOrWhiteSpace(text3)) { TryGetFileTimestamps(text, characterName, out string created2, out string lastLogin2); if (string.IsNullOrWhiteSpace(text2) && !string.IsNullOrWhiteSpace(created2)) { text2 = created2; } if (string.IsNullOrWhiteSpace(text3) && !string.IsNullOrWhiteSpace(lastLogin2)) { text3 = lastLogin2; } } list2.Add(new Dictionary { { "steamId", text }, { "characterName", characterName }, { "created", (!string.IsNullOrWhiteSpace(text2)) ? text2 : "—" }, { "lastLogin", (!string.IsNullOrWhiteSpace(text3)) ? text3 : "—" }, { "status", value2 } }); } else { if (!(item is string text4)) { continue; } string value3 = "—"; string value4 = "—"; string value5 = "Bound"; if (IsPlayerOnline(text4, text4)) { value4 = "Online Now"; value5 = "Online"; } else { TryGetFileTimestamps(text4, text4, out string created3, out string lastLogin3); if (!string.IsNullOrWhiteSpace(created3)) { value3 = created3; } if (!string.IsNullOrWhiteSpace(lastLogin3)) { value4 = lastLogin3; } } list2.Add(new Dictionary { { "steamId", text4 }, { "characterName", text4 }, { "created", value3 }, { "lastLogin", value4 }, { "status", value5 } }); } } return list2; } private static bool IsPlayerOnline(string steamId, string characterName) { try { return OnlinePlayerChecker != null && OnlinePlayerChecker(steamId, characterName); } catch { } return false; } private static void TryGetFileTimestamps(string steamId, string characterName, out string? created, out string? lastLogin) { created = null; lastLogin = null; try { string configDirectory = GetConfigDirectory(); string text = (steamId.StartsWith("Steam_", StringComparison.OrdinalIgnoreCase) ? steamId.Substring(6) : steamId); string text2 = (steamId.StartsWith("Steam_", StringComparison.OrdinalIgnoreCase) ? steamId : ("Steam_" + steamId)); List list = new List { Path.Combine(configDirectory, "CharactersVault", "characters"), Path.Combine(configDirectory, "CharacterVault", "characters"), Path.Combine(configDirectory, "CharactersVault", "profiles"), Path.Combine(configDirectory, "CharacterVault", "profiles"), Path.Combine(configDirectory, "CharactersVault", "saves"), Path.Combine(configDirectory, "CharacterVault", "saves"), Path.Combine(configDirectory, "CharactersVault", "vault"), Path.Combine(configDirectory, "CharacterVault", "vault"), Path.Combine(configDirectory, "CharactersVault"), Path.Combine(configDirectory, "CharacterVault") }; try { string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); string text3 = Path.Combine(folderPath + "Low", "IronGate", "Valheim", "characters"); if (Directory.Exists(text3)) { list.Add(text3); } string text4 = Path.Combine(folderPath + "Low", "IronGate", "Valheim", "characters_local"); if (Directory.Exists(text4)) { list.Add(text4); } } catch { } List list2 = new List(); if (!string.IsNullOrWhiteSpace(text)) { list2.Add(text + ".fch"); list2.Add(text + ".dat"); list2.Add(text + ".json"); list2.Add(text + ".profile"); list2.Add(text2 + ".fch"); list2.Add(text2 + ".dat"); list2.Add(text2 + ".json"); list2.Add(text2 + ".profile"); } if (!string.IsNullOrWhiteSpace(characterName) && characterName != "Unknown") { list2.Add(characterName + ".fch"); list2.Add(characterName + ".dat"); list2.Add(characterName + ".json"); list2.Add(text + "_" + characterName + ".fch"); list2.Add(characterName + "_" + text + ".fch"); } foreach (string item in list) { if (!Directory.Exists(item)) { continue; } foreach (string item2 in list2) { if (!item2.Equals("bindings.json", StringComparison.OrdinalIgnoreCase)) { string text5 = Path.Combine(item, item2); if (File.Exists(text5)) { FileInfo fileInfo = new FileInfo(text5); created = fileInfo.CreationTimeUtc.ToString("yyyy-MM-dd"); lastLogin = fileInfo.LastWriteTimeUtc.ToString("yyyy-MM-dd HH:mm"); return; } } } } } catch { } } private static void ExtractBindingFromDict(IDictionary dict, string fallbackSteamId, out string characterName, out string? created, out string? lastLogin, out string? status) { characterName = string.Empty; created = null; lastLogin = null; status = null; string[] array = new string[19] { "characterName", "character_name", "charName", "char_name", "playerName", "player_name", "name", "boundCharacter", "bound_character", "character", "player", "profileName", "profile_name", "profile", "vikingName", "viking_name", "hero", "valheimCharacter", "characterId" }; string[] array2 = array; foreach (string key in array2) { if (!TryGetCaseInsensitive(dict, key, out object value) || value == null) { continue; } if (value is string text && !string.IsNullOrWhiteSpace(text)) { characterName = text; break; } if (value is IDictionary dict2) { string[] array3 = array; foreach (string key2 in array3) { if (TryGetCaseInsensitive(dict2, key2, out object value2) && value2 is string text2 && !string.IsNullOrWhiteSpace(text2)) { characterName = text2; break; } } if (!string.IsNullOrEmpty(characterName)) { break; } } else if (!(value is IDictionary) && !(value is IList)) { characterName = value.ToString() ?? string.Empty; if (!string.IsNullOrWhiteSpace(characterName)) { break; } } } if (string.IsNullOrWhiteSpace(characterName)) { foreach (KeyValuePair item in dict) { if (item.Value is string text3 && !string.IsNullOrWhiteSpace(text3) && (!text3.Contains("-") || (text3.Length != 10 && text3.Length < 16)) && !text3.StartsWith("Steam_", StringComparison.OrdinalIgnoreCase)) { characterName = text3; break; } } } if (string.IsNullOrWhiteSpace(characterName)) { characterName = "Unknown"; } array2 = new string[15] { "created", "createdAt", "created_at", "creationDate", "creation_date", "createDate", "create_date", "dateCreated", "date_created", "timestamp", "bindingDate", "binding_date", "boundAt", "bound_at", "date" }; foreach (string key3 in array2) { if (TryGetCaseInsensitive(dict, key3, out object value3) && value3 != null) { created = FormatDateString(value3); if (!string.IsNullOrWhiteSpace(created)) { break; } } } array2 = new string[17] { "lastLogin", "last_login", "lastSeen", "last_seen", "lastConnected", "last_connected", "lastOnline", "last_online", "loginTime", "login_time", "updatedAt", "updated_at", "lastActive", "last_active", "modified", "lastModified", "last_modified" }; foreach (string key4 in array2) { if (TryGetCaseInsensitive(dict, key4, out object value4) && value4 != null) { lastLogin = FormatDateTimeString(value4); if (!string.IsNullOrWhiteSpace(lastLogin)) { break; } } } array2 = new string[8] { "status", "state", "active", "isActive", "is_active", "bound", "isBound", "is_bound" }; foreach (string key5 in array2) { if (TryGetCaseInsensitive(dict, key5, out object value5) && value5 != null) { if (value5 is bool flag) { status = (flag ? "Active" : "Inactive"); } else if (value5 is string text4 && !string.IsNullOrWhiteSpace(text4)) { status = text4; } if (!string.IsNullOrWhiteSpace(status)) { break; } } } } private static bool TryGetCaseInsensitive(IDictionary dict, string key, out object? value) { if (dict.TryGetValue(key, out value)) { return true; } foreach (KeyValuePair item in dict) { if (string.Equals(item.Key, key, StringComparison.OrdinalIgnoreCase)) { value = item.Value; return true; } } value = null; return false; } private static string? FormatDateString(object val) { if (val is DateTime dateTime) { return dateTime.ToString("yyyy-MM-dd"); } if ((val is long || val is int || val is double) ? true : false) { double num = Convert.ToDouble(val, CultureInfo.InvariantCulture); if (num > 1000000000000.0) { return DateTimeOffset.FromUnixTimeMilliseconds((long)num).UtcDateTime.ToString("yyyy-MM-dd"); } if (num > 1000000000.0) { return DateTimeOffset.FromUnixTimeSeconds((long)num).UtcDateTime.ToString("yyyy-MM-dd"); } } if (val is string text && !string.IsNullOrWhiteSpace(text)) { if (DateTime.TryParse(text, CultureInfo.InvariantCulture, DateTimeStyles.None, out var result)) { return result.ToString("yyyy-MM-dd"); } return text; } return val.ToString(); } private static string? FormatDateTimeString(object val) { if (val is DateTime dateTime) { return dateTime.ToString("yyyy-MM-dd HH:mm"); } if ((val is long || val is int || val is double) ? true : false) { double num = Convert.ToDouble(val, CultureInfo.InvariantCulture); if (num > 1000000000000.0) { return DateTimeOffset.FromUnixTimeMilliseconds((long)num).UtcDateTime.ToString("yyyy-MM-dd HH:mm"); } if (num > 1000000000.0) { return DateTimeOffset.FromUnixTimeSeconds((long)num).UtcDateTime.ToString("yyyy-MM-dd HH:mm"); } } if (val is string text && !string.IsNullOrWhiteSpace(text)) { if (DateTime.TryParse(text, CultureInfo.InvariantCulture, DateTimeStyles.None, out var result)) { return result.ToString("yyyy-MM-dd HH:mm"); } return text; } return val.ToString(); } public static bool UnbindCharacter(string steamId) { try { string characterVaultBindingsFilePath = GetCharacterVaultBindingsFilePath(); if (!File.Exists(characterVaultBindingsFilePath)) { return true; } object obj = SimpleJson.Deserialize(File.ReadAllText(characterVaultBindingsFilePath, Encoding.UTF8)); if (obj == null) { return false; } bool flag = false; if (obj is IDictionary dictionary) { IDictionary dictionary2 = dictionary; if (dictionary.Count == 1 && dictionary.First().Value is IDictionary dictionary3) { dictionary2 = dictionary3; } List list = new List(); foreach (string key2 in dictionary2.Keys) { if (IsSteamIdMatch(key2, steamId)) { list.Add(key2); } } foreach (string item in list) { dictionary2.Remove(item); flag = true; } } else if (obj is IList list2) { for (int num = list2.Count - 1; num >= 0; num--) { object obj2 = list2[num]; if (obj2 is IDictionary dict) { string[] array = new string[9] { "steamId", "steam_id", "steamID", "id", "playerId", "player_id", "userId", "key", "account" }; foreach (string key in array) { if (TryGetCaseInsensitive(dict, key, out object value) && value != null && IsSteamIdMatch(value.ToString() ?? "", steamId)) { list2.RemoveAt(num); flag = true; break; } } } else if (obj2 is string a && IsSteamIdMatch(a, steamId)) { list2.RemoveAt(num); flag = true; } } } if (flag) { string contents = SimpleJson.SerializeObject(obj); File.WriteAllText(characterVaultBindingsFilePath, contents, Encoding.UTF8); ManualLogSource log = BifrostheimPlugin.Log; if (log != null) { log.LogInfo((object)("[ConfigSyncManager] Removed CharacterVault binding for '" + steamId + "'.")); } return true; } } catch (Exception ex) { ManualLogSource log2 = BifrostheimPlugin.Log; if (log2 != null) { log2.LogError((object)("[ConfigSyncManager] Failed to unbind character '" + steamId + "': " + ex.Message)); } } return false; } public static bool IsSteamIdMatch(string a, string b) { if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b)) { return false; } if (string.Equals(a.Trim(), b.Trim(), StringComparison.OrdinalIgnoreCase)) { return true; } return string.Equals(Clean(a), Clean(b), StringComparison.OrdinalIgnoreCase); static string Clean(string s) { if (!s.Trim().StartsWith("Steam_", StringComparison.OrdinalIgnoreCase)) { return s.Trim(); } return s.Trim().Substring(6); } } public static bool WipeCharacters() { try { string characterVaultBindingsFilePath = GetCharacterVaultBindingsFilePath(); string directoryName = Path.GetDirectoryName(characterVaultBindingsFilePath); if (!string.IsNullOrEmpty(directoryName) && !Directory.Exists(directoryName)) { Directory.CreateDirectory(directoryName); } File.WriteAllText(characterVaultBindingsFilePath, "{}", Encoding.UTF8); ManualLogSource log = BifrostheimPlugin.Log; if (log != null) { log.LogInfo((object)"[ConfigSyncManager] Wiped all CharacterVault bindings."); } return true; } catch (Exception ex) { ManualLogSource log2 = BifrostheimPlugin.Log; if (log2 != null) { log2.LogError((object)("[ConfigSyncManager] Failed to wipe CharacterVault bindings: " + ex.Message)); } } return false; } public static List ScanOtherModConfigFiles() { List list = new List(); try { string configDirectory = GetConfigDirectory(); if (!Directory.Exists(configDirectory)) { return list; } string[] files = Directory.GetFiles(configDirectory, "*.cfg", SearchOption.TopDirectoryOnly); foreach (string text in files) { try { string fileName = Path.GetFileName(text); FileInfo fileInfo = new FileInfo(text); bool isFirstParty = IsFirstPartyModFile(fileName); OtherModSummaryDto otherModSummaryDto = new OtherModSummaryDto { fileName = fileName, filePath = fileName, displayName = CleanDisplayName(fileName), fileSizeBytes = fileInfo.Length, lastModified = fileInfo.LastWriteTime.ToString("yyyy-MM-dd HH:mm"), isFirstParty = isFirstParty }; CountSectionsAndSettings(text, out var sectionCount, out var settingCount); otherModSummaryDto.sectionCount = sectionCount; otherModSummaryDto.settingCount = settingCount; MatchLoadedPluginSummary(fileName, otherModSummaryDto); list.Add(otherModSummaryDto); } catch (Exception ex) { ManualLogSource log = BifrostheimPlugin.Log; if (log != null) { log.LogWarning((object)("[ConfigSyncManager] Error scanning mod config '" + text + "': " + ex.Message)); } } } } catch (Exception ex2) { ManualLogSource log2 = BifrostheimPlugin.Log; if (log2 != null) { log2.LogError((object)("[ConfigSyncManager] Failed to scan config directory: " + ex2.Message)); } } return (from m in list orderby m.isFirstParty, m.displayName select m).ToList(); } private static bool MatchPluginToConfigFile(string fileName, PluginInfo info, string guid) { if (info == null) { return false; } if (string.Equals(fileName, "BepInEx.cfg", StringComparison.OrdinalIgnoreCase)) { return false; } BaseUnityPlugin instance = info.Instance; object obj; if (instance == null) { obj = null; } else { ConfigFile config = instance.Config; obj = ((config != null) ? config.ConfigFilePath : null); } string text = (string)obj; if (!string.IsNullOrEmpty(text) && string.Equals(Path.GetFileName(text), fileName, StringComparison.OrdinalIgnoreCase)) { return true; } string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName); if (string.Equals(guid, fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)) { return true; } string text2 = guid.Replace(".", "_").Replace("-", "_"); string text3 = fileNameWithoutExtension.Replace(".", "_").Replace("-", "_"); if (string.Equals(text2, text3, StringComparison.OrdinalIgnoreCase)) { return true; } if (!new string[8] { "bepinex", "plugin", "plugins", "valheim", "mod", "com", "org", "net" }.Contains(text3.ToLowerInvariant()) && (text2.EndsWith("_" + text3, StringComparison.OrdinalIgnoreCase) || text2.EndsWith("." + fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase))) { return true; } return false; } private static void MatchLoadedPluginSummary(string fileName, OtherModSummaryDto summary) { if (string.Equals(fileName, "BepInEx.cfg", StringComparison.OrdinalIgnoreCase)) { summary.displayName = "BepInEx Core Framework"; summary.pluginGuid = "BepInEx"; summary.pluginName = "BepInEx Core"; summary.isLoadedInGame = true; return; } try { if (Chainloader.PluginInfos == null) { return; } foreach (KeyValuePair pluginInfo in Chainloader.PluginInfos) { PluginInfo value = pluginInfo.Value; if (value == null) { continue; } string key = pluginInfo.Key; if (MatchPluginToConfigFile(fileName, value, key)) { summary.pluginGuid = key; BepInPlugin metadata = value.Metadata; summary.pluginName = ((metadata != null) ? metadata.Name : null) ?? summary.displayName; BepInPlugin metadata2 = value.Metadata; summary.pluginVersion = ((metadata2 == null) ? null : metadata2.Version?.ToString()) ?? ""; summary.isLoadedInGame = true; if (!string.IsNullOrWhiteSpace(summary.pluginName)) { summary.displayName = summary.pluginName; } break; } } } catch { } } public static OtherModConfigDetailDto ParseModConfigFile(string fileName) { string fileName2 = Path.GetFileName(fileName); OtherModConfigDetailDto otherModConfigDetailDto = new OtherModConfigDetailDto { fileName = fileName2, displayName = CleanDisplayName(fileName2), sections = new List() }; string path = Path.Combine(GetConfigDirectory(), fileName2); if (!File.Exists(path)) { return otherModConfigDetailDto; } try { otherModConfigDetailDto.lastModified = File.GetLastWriteTime(path).ToString("yyyy-MM-dd HH:mm:ss"); otherModConfigDetailDto.rawContent = File.ReadAllText(path, Encoding.UTF8); MatchLoadedPluginDetail(fileName2, otherModConfigDetailDto); string[] array = File.ReadAllLines(path, Encoding.UTF8); OtherModSectionDto value = new OtherModSectionDto { name = "General", entries = new List() }; List list = new List { value }; Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["General"] = value }; List descriptionLines = new List(); string currentSettingType = null; string currentDefaultValue = null; string currentAcceptableRange = null; List currentAcceptableValues = null; string[] array2 = array; for (int i = 0; i < array2.Length; i++) { string text = array2[i].Trim(); if (string.IsNullOrEmpty(text)) { continue; } if (text.StartsWith("##")) { string text2 = text.Substring(2).Trim(); if (!string.IsNullOrEmpty(text2)) { descriptionLines.Add(text2); } } else if (text.StartsWith("# Setting type:", StringComparison.OrdinalIgnoreCase)) { currentSettingType = text.Substring("# Setting type:".Length).Trim(); } else if (text.StartsWith("# Default value:", StringComparison.OrdinalIgnoreCase)) { currentDefaultValue = text.Substring("# Default value:".Length).Trim(); } else if (text.StartsWith("# Acceptable value range:", StringComparison.OrdinalIgnoreCase)) { currentAcceptableRange = text.Substring("# Acceptable value range:".Length).Trim(); } else if (text.StartsWith("# Acceptable values:", StringComparison.OrdinalIgnoreCase)) { string text3 = text.Substring("# Acceptable values:".Length).Trim(); currentAcceptableValues = (from s in text3.Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries) select s.Trim() into s where !string.IsNullOrEmpty(s) select s).ToList(); } else { if (text.StartsWith("#") || text.StartsWith(";")) { continue; } if (text.StartsWith("[") && text.EndsWith("]")) { string text4 = text.Substring(1, text.Length - 2).Trim(); if (!dictionary.TryGetValue(text4, out value)) { value = (dictionary[text4] = new OtherModSectionDto { name = text4, entries = new List() }); list.Add(value); } ResetEntryMetadata(); continue; } int num = text.IndexOf('='); if (num > 0) { string key = text.Substring(0, num).Trim(); string text5 = text.Substring(num + 1).Trim(); string valueType = currentSettingType ?? InferValueType(text5); float? minRange = null; float? maxRange = null; if (!string.IsNullOrWhiteSpace(currentAcceptableRange)) { TryParseRange(currentAcceptableRange, out minRange, out maxRange); } OtherModConfigEntryDto item = new OtherModConfigEntryDto { key = key, value = text5, defaultValue = currentDefaultValue, valueType = valueType, description = string.Join(" ", descriptionLines), acceptableValues = currentAcceptableValues, minRange = minRange, maxRange = maxRange }; value.entries.Add(item); ResetEntryMetadata(); } } } otherModConfigDetailDto.sections = list.Where((OtherModSectionDto s) => s.entries.Count > 0).ToList(); void ResetEntryMetadata() { descriptionLines.Clear(); currentSettingType = null; currentDefaultValue = null; currentAcceptableRange = null; currentAcceptableValues = null; } } catch (Exception ex) { ManualLogSource log = BifrostheimPlugin.Log; if (log != null) { log.LogError((object)("[ConfigSyncManager] Error parsing config file '" + fileName + "': " + ex.Message)); } } return otherModConfigDetailDto; } private static void MatchLoadedPluginDetail(string fileName, OtherModConfigDetailDto detail) { if (string.Equals(fileName, "BepInEx.cfg", StringComparison.OrdinalIgnoreCase)) { detail.displayName = "BepInEx Core Framework"; detail.pluginGuid = "BepInEx"; detail.pluginName = "BepInEx Core"; detail.isLoadedInGame = true; return; } try { if (Chainloader.PluginInfos == null) { return; } foreach (KeyValuePair pluginInfo in Chainloader.PluginInfos) { PluginInfo value = pluginInfo.Value; if (value == null) { continue; } string key = pluginInfo.Key; if (MatchPluginToConfigFile(fileName, value, key)) { detail.pluginGuid = key; BepInPlugin metadata = value.Metadata; detail.pluginName = ((metadata != null) ? metadata.Name : null) ?? detail.displayName; BepInPlugin metadata2 = value.Metadata; detail.pluginVersion = ((metadata2 == null) ? null : metadata2.Version?.ToString()) ?? ""; detail.isLoadedInGame = true; if (!string.IsNullOrWhiteSpace(detail.pluginName)) { detail.displayName = detail.pluginName; } break; } } } catch { } } public static OtherModConfigDetailDto SaveOtherModConfig(SaveOtherModConfigRequest req) { string configDirectory = GetConfigDirectory(); string fileName = Path.GetFileName(req.fileName); string text = Path.Combine(configDirectory, fileName); if (req.saveRaw && !string.IsNullOrEmpty(req.rawContent)) { File.WriteAllText(text, req.rawContent, Encoding.UTF8); ManualLogSource log = BifrostheimPlugin.Log; if (log != null) { log.LogInfo((object)("[ConfigSyncManager] Saved raw content to '" + fileName + "'.")); } } else if (req.updates != null) { WriteIniFile(text, req.updates); } TrySyncOtherModInMemory(fileName, req.updates); return ParseModConfigFile(fileName); } public static OtherModConfigDetailDto ResetOtherModConfigDefaults(string fileName) { string configDirectory = GetConfigDirectory(); string fileName2 = Path.GetFileName(fileName); string text = Path.Combine(configDirectory, fileName2); if (!File.Exists(text)) { return new OtherModConfigDetailDto { fileName = fileName2 }; } OtherModConfigDetailDto otherModConfigDetailDto = ParseModConfigFile(fileName2); Dictionary> dictionary = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (OtherModSectionDto section in otherModConfigDetailDto.sections) { Dictionary dictionary2 = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (OtherModConfigEntryDto entry in section.entries) { if (!string.IsNullOrEmpty(entry.defaultValue)) { dictionary2[entry.key] = entry.defaultValue; } } if (dictionary2.Count > 0) { dictionary[section.name] = dictionary2; } } if (dictionary.Count > 0) { WriteIniFile(text, dictionary); TrySyncOtherModInMemory(fileName2, dictionary); } return ParseModConfigFile(fileName2); } private static void TrySyncOtherModInMemory(string fileName, Dictionary>? updates) { try { if (Chainloader.PluginInfos == null) { return; } foreach (KeyValuePair pluginInfo in Chainloader.PluginInfos) { PluginInfo value = pluginInfo.Value; object obj; if (value == null) { obj = null; } else { BaseUnityPlugin instance = value.Instance; obj = ((instance != null) ? instance.Config : null); } if (obj == null) { continue; } string key = pluginInfo.Key; if (!MatchPluginToConfigFile(fileName, value, key)) { continue; } ConfigFile config = value.Instance.Config; if (updates != null) { foreach (KeyValuePair> update in updates) { foreach (KeyValuePair item in update.Value) { TrySetEntryValue(config, item.Key, item.Value); } } } try { config.Reload(); config.Save(); ManualLogSource log = BifrostheimPlugin.Log; if (log != null) { log.LogInfo((object)("[ConfigSyncManager] Live-reloaded in-memory ConfigFile for plugin '" + pluginInfo.Key + "'.")); } break; } catch (Exception ex) { ManualLogSource log2 = BifrostheimPlugin.Log; if (log2 != null) { log2.LogWarning((object)("[ConfigSyncManager] Config.Reload() warning on '" + pluginInfo.Key + "': " + ex.Message)); } break; } } } catch (Exception ex2) { ManualLogSource log3 = BifrostheimPlugin.Log; if (log3 != null) { log3.LogWarning((object)("[ConfigSyncManager] In-memory sync exception for '" + fileName + "': " + ex2.Message)); } } } private static bool IsFirstPartyModFile(string fileName) { string text = fileName.ToLowerInvariant(); if (!text.Contains("valgrind") && !text.Contains("skald") && !text.Contains("dagrandnott") && !text.Contains("dagrnott") && !text.Contains("njoror") && !text.Contains("charactervault") && !text.Contains("charactersvault") && !text.Contains("bifrostheim")) { return text.Contains("bigfrost"); } return true; } private static void CountSectionsAndSettings(string filePath, out int sectionCount, out int settingCount) { sectionCount = 0; settingCount = 0; try { string[] array = File.ReadAllLines(filePath, Encoding.UTF8); HashSet hashSet = new HashSet(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(";")) { if (text.StartsWith("[") && text.EndsWith("]")) { hashSet.Add(text); } else if (text.Contains("=")) { settingCount++; } } } sectionCount = Math.Max(1, hashSet.Count); } catch { } } private static string InferValueType(string val) { if (string.Equals(val, "true", StringComparison.OrdinalIgnoreCase) || string.Equals(val, "false", StringComparison.OrdinalIgnoreCase)) { return "Boolean"; } if (int.TryParse(val, out var _)) { return "Int32"; } if (float.TryParse(val, NumberStyles.Any, CultureInfo.InvariantCulture, out var _)) { return "Single"; } return "String"; } private static void TryParseRange(string? rangeStr, out float? minRange, out float? maxRange) { minRange = null; maxRange = null; if (string.IsNullOrWhiteSpace(rangeStr)) { return; } try { Match match = Regex.Match(rangeStr, "From\\s+([-\\d\\.]+)\\s+to\\s+([-\\d\\.]+)", RegexOptions.IgnoreCase); if (match.Success) { if (float.TryParse(match.Groups[1].Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) { minRange = result; } if (float.TryParse(match.Groups[2].Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result2)) { maxRange = result2; } } } catch { } } private static string CleanDisplayName(string fileName) { string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName); if (fileNameWithoutExtension.Equals("BepInEx", StringComparison.OrdinalIgnoreCase)) { return "BepInEx Core Framework"; } List list = (from p in fileNameWithoutExtension.Split(new char[2] { '.', '_' }, StringSplitOptions.RemoveEmptyEntries) where !p.Equals("com", StringComparison.OrdinalIgnoreCase) && !p.Equals("org", StringComparison.OrdinalIgnoreCase) && !p.Equals("bepinex", StringComparison.OrdinalIgnoreCase) && !p.Equals("plugins", StringComparison.OrdinalIgnoreCase) && !p.Equals("valheim", StringComparison.OrdinalIgnoreCase) select p).ToList(); if (list.Count > 0) { List list2 = new List(); foreach (string item in list) { list2.Add(SplitCamelCase(item)); } return string.Join(" - ", list2); } return SplitCamelCase(fileNameWithoutExtension); } private static string SplitCamelCase(string input) { if (string.IsNullOrWhiteSpace(input)) { return input; } return Regex.Replace(input, "([a-z])([A-Z])", "$1 $2"); } private static string? FindValue(Dictionary> ini, params string[] candidateKeys) { foreach (Dictionary value2 in ini.Values) { foreach (string key in candidateKeys) { if (value2.TryGetValue(key, out var value)) { return value; } } } return null; } } public class MainThreadDispatcher : MonoBehaviour { private static readonly ConcurrentQueue ExecutionQueue = new ConcurrentQueue(); private static MainThreadDispatcher? _instance; public static void Initialize() { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Expected O, but got Unknown if ((Object)(object)_instance == (Object)null) { GameObject val = new GameObject("Bifrostheim_MainThreadDispatcher"); Object.DontDestroyOnLoad((Object)val); _instance = val.AddComponent(); } } private void Update() { Action result; while (ExecutionQueue.TryDequeue(out result)) { try { result?.Invoke(); } catch (Exception arg) { BifrostheimPlugin.Log.LogError((object)$"[MainThreadDispatcher] Error executing action: {arg}"); } } } public static void Enqueue(Action action) { if (action != null) { ExecutionQueue.Enqueue(action); } } public static Task EnqueueAsync(Action action) { TaskCompletionSource tcs = new TaskCompletionSource(); Enqueue(delegate { try { action(); tcs.SetResult(result: true); } catch (Exception exception) { tcs.SetException(exception); } }); return tcs.Task; } public static Task EnqueueAsync(Func function) { TaskCompletionSource tcs = new TaskCompletionSource(); Enqueue(delegate { try { T result = function(); tcs.SetResult(result); } catch (Exception exception) { tcs.SetException(exception); } }); return tcs.Task; } } public static class SimpleJson { public static string SerializeObject(object? obj, bool prettyPrint = true) { StringBuilder stringBuilder = new StringBuilder(); SerializeValue(obj, stringBuilder, (!prettyPrint) ? (-1) : 0); return stringBuilder.ToString(); } public static T? DeserializeObject(string json) where T : new() { if (string.IsNullOrWhiteSpace(json)) { return default(T); } object obj = Deserialize(json); if (obj == null) { return default(T); } if (obj is T) { return (T)obj; } if (typeof(IDictionary).IsAssignableFrom(typeof(T)) && obj is IDictionary dictionary) { Type[] genericArguments = typeof(T).GetGenericArguments(); if (genericArguments.Length == 2 && genericArguments[0] == typeof(string)) { Type targetType = genericArguments[1]; IDictionary dictionary2 = (IDictionary)Activator.CreateInstance(typeof(T)); foreach (KeyValuePair item in dictionary) { if (item.Value is IDictionary dict) { object value = ConvertDictionaryToObject(dict, targetType); dictionary2.Add(item.Key, value); } else { dictionary2.Add(item.Key, ConvertValue(item.Value, targetType)); } } return (T)dictionary2; } } if (obj is IDictionary dict2) { return (T)ConvertDictionaryToObject(dict2, typeof(T)); } return default(T); } private static object ConvertDictionaryToObject(IDictionary dict, Type targetType) { object obj = Activator.CreateInstance(targetType); PropertyInfo[] properties = targetType.GetProperties(BindingFlags.Instance | BindingFlags.Public); FieldInfo[] fields = targetType.GetFields(BindingFlags.Instance | BindingFlags.Public); PropertyInfo[] array = properties; foreach (PropertyInfo propertyInfo in array) { if (propertyInfo.CanWrite && FindKey(dict, propertyInfo.Name, out object value)) { propertyInfo.SetValue(obj, ConvertValue(value, propertyInfo.PropertyType), null); } } FieldInfo[] array2 = fields; foreach (FieldInfo fieldInfo in array2) { if (FindKey(dict, fieldInfo.Name, out object value2)) { fieldInfo.SetValue(obj, ConvertValue(value2, fieldInfo.FieldType)); } } return obj; } private static bool FindKey(IDictionary dict, string name, out object? value) { if (dict.TryGetValue(name, out value)) { return true; } foreach (KeyValuePair item in dict) { if (string.Equals(item.Key, name, StringComparison.OrdinalIgnoreCase)) { value = item.Value; return true; } } value = null; return false; } private static object? ConvertValue(object? val, Type targetType) { if (val == null) { return null; } if (typeof(IDictionary).IsAssignableFrom(targetType) && val is IDictionary dictionary) { Type[] genericArguments = targetType.GetGenericArguments(); if (genericArguments.Length == 2 && genericArguments[0] == typeof(string)) { Type targetType2 = genericArguments[1]; IDictionary dictionary2 = (IDictionary)Activator.CreateInstance(targetType); { foreach (KeyValuePair item in dictionary) { dictionary2.Add(item.Key, ConvertValue(item.Value, targetType2)); } return dictionary2; } } } if (targetType == typeof(DateTime)) { if (val is string s && DateTime.TryParse(s, null, DateTimeStyles.RoundtripKind, out var result)) { return result; } return default(DateTime); } if (targetType == typeof(bool)) { if (val is bool flag) { return flag; } if (val is string value && bool.TryParse(value, out var result2)) { return result2; } return false; } if (targetType == typeof(int)) { return Convert.ToInt32(val, CultureInfo.InvariantCulture); } if (targetType == typeof(long)) { return Convert.ToInt64(val, CultureInfo.InvariantCulture); } if (targetType == typeof(float)) { return Convert.ToSingle(val, CultureInfo.InvariantCulture); } if (targetType == typeof(double)) { return Convert.ToDouble(val, CultureInfo.InvariantCulture); } if (targetType == typeof(string)) { return val.ToString(); } return val; } private static void SerializeValue(object? value, StringBuilder sb, int indentLevel) { if (value == null) { sb.Append("null"); } else if (value is string str) { sb.Append('"').Append(EscapeString(str)).Append('"'); } else if (value is bool flag) { sb.Append(flag ? "true" : "false"); } else if (value is DateTime dateTime) { sb.Append('"').Append(dateTime.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture)).Append('"'); } else if (value is int || value is long || value is short || value is byte) { sb.Append(Convert.ToString(value, CultureInfo.InvariantCulture)); } else if (value is float num) { sb.Append(num.ToString("R", CultureInfo.InvariantCulture)); } else if (value is double num2) { sb.Append(num2.ToString("R", CultureInfo.InvariantCulture)); } else if (value is IDictionary dict) { SerializeDictionary(dict, sb, indentLevel); } else if (value is IEnumerable list) { SerializeList(list, sb, indentLevel); } else { SerializeObjectFields(value, sb, indentLevel); } } private static void SerializeDictionary(IDictionary dict, StringBuilder sb, int indentLevel) { bool flag = indentLevel >= 0; if (dict.Count == 0) { sb.Append("{}"); return; } sb.Append('{'); if (flag) { sb.Append('\n'); } int num = 0; foreach (DictionaryEntry item in dict) { if (flag) { sb.Append(' ', (indentLevel + 1) * 2); } sb.Append('"').Append(EscapeString(item.Key?.ToString() ?? string.Empty)).Append("\":"); if (flag) { sb.Append(' '); } SerializeValue(item.Value, sb, flag ? (indentLevel + 1) : (-1)); if (++num < dict.Count) { sb.Append(','); } if (flag) { sb.Append('\n'); } } if (flag) { sb.Append(' ', indentLevel * 2); } sb.Append('}'); } private static void SerializeList(IEnumerable list, StringBuilder sb, int indentLevel) { bool flag = indentLevel >= 0; sb.Append('['); bool flag2 = true; foreach (object item in list) { if (!flag2) { sb.Append(flag ? ", " : ","); } SerializeValue(item, sb, flag ? (indentLevel + 1) : (-1)); flag2 = false; } sb.Append(']'); } private static void SerializeObjectFields(object obj, StringBuilder sb, int indentLevel) { bool flag = indentLevel >= 0; Type type = obj.GetType(); PropertyInfo[] properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public); FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public); List<(string, object)> list = new List<(string, object)>(); PropertyInfo[] array = properties; foreach (PropertyInfo propertyInfo in array) { if (propertyInfo.CanRead && propertyInfo.GetIndexParameters().Length == 0) { list.Add((propertyInfo.Name, propertyInfo.GetValue(obj, null))); } } FieldInfo[] array2 = fields; foreach (FieldInfo fieldInfo in array2) { list.Add((fieldInfo.Name, fieldInfo.GetValue(obj))); } if (list.Count == 0) { sb.Append("{}"); return; } sb.Append('{'); if (flag) { sb.Append('\n'); } for (int j = 0; j < list.Count; j++) { if (flag) { sb.Append(' ', (indentLevel + 1) * 2); } sb.Append('"').Append(EscapeString(list[j].Item1)).Append("\":"); if (flag) { sb.Append(' '); } SerializeValue(list[j].Item2, sb, flag ? (indentLevel + 1) : (-1)); if (j < list.Count - 1) { sb.Append(','); } if (flag) { sb.Append('\n'); } } if (flag) { sb.Append(' ', indentLevel * 2); } sb.Append('}'); } private static string EscapeString(string str) { StringBuilder stringBuilder = new StringBuilder(str.Length + 4); foreach (char c in str) { switch (c) { case '"': stringBuilder.Append("\\\""); continue; case '\\': stringBuilder.Append("\\\\"); continue; case '\b': stringBuilder.Append("\\b"); continue; case '\f': stringBuilder.Append("\\f"); continue; case '\n': stringBuilder.Append("\\n"); continue; case '\r': stringBuilder.Append("\\r"); continue; case '\t': stringBuilder.Append("\\t"); continue; } if (c < ' ') { StringBuilder stringBuilder2 = stringBuilder.Append("\\u"); int num = c; stringBuilder2.Append(num.ToString("x4")); } else { stringBuilder.Append(c); } } return stringBuilder.ToString(); } public static object? Deserialize(string json) { int index = 0; return ParseValue(json, ref index); } private static object? ParseValue(string json, ref int index) { SkipWhitespace(json, ref index); if (index >= json.Length) { return null; } char c = json[index]; switch (c) { case '{': return ParseObject(json, ref index); case '[': return ParseArray(json, ref index); case '"': return ParseString(json, ref index); case 'f': case 't': return ParseBool(json, ref index); case 'n': return ParseNull(json, ref index); default: if (char.IsDigit(c) || c == '-') { return ParseNumber(json, ref index); } return null; } } private static Dictionary ParseObject(string json, ref int index) { Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); index++; while (index < json.Length) { SkipWhitespace(json, ref index); if (index >= json.Length) { break; } if (json[index] == '}') { index++; break; } string key = ParseString(json, ref index); SkipWhitespace(json, ref index); if (index < json.Length && json[index] == ':') { index++; } object value = ParseValue(json, ref index); dictionary[key] = value; SkipWhitespace(json, ref index); if (index < json.Length && json[index] == ',') { index++; } } return dictionary; } private static List ParseArray(string json, ref int index) { List list = new List(); index++; while (index < json.Length) { SkipWhitespace(json, ref index); if (index >= json.Length) { break; } if (json[index] == ']') { index++; break; } list.Add(ParseValue(json, ref index)); SkipWhitespace(json, ref index); if (index < json.Length && json[index] == ',') { index++; } } return list; } private static string ParseString(string json, ref int index) { StringBuilder stringBuilder = new StringBuilder(); index++; while (index < json.Length) { char c = json[index++]; if (c == '"') { break; } if (c == '\\' && index < json.Length) { char c2 = json[index++]; switch (c2) { case '"': stringBuilder.Append('"'); break; case '\\': stringBuilder.Append('\\'); break; case '/': stringBuilder.Append('/'); break; case 'b': stringBuilder.Append('\b'); break; case 'f': stringBuilder.Append('\f'); break; case 'n': stringBuilder.Append('\n'); break; case 'r': stringBuilder.Append('\r'); break; case 't': stringBuilder.Append('\t'); break; case 'u': { if (index + 4 <= json.Length && int.TryParse(json.Substring(index, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var result)) { stringBuilder.Append((char)result); index += 4; } break; } default: stringBuilder.Append(c2); break; } } else { stringBuilder.Append(c); } } return stringBuilder.ToString(); } private static bool ParseBool(string json, ref int index) { if (json.Substring(index).StartsWith("true", StringComparison.OrdinalIgnoreCase)) { index += 4; return true; } if (json.Substring(index).StartsWith("false", StringComparison.OrdinalIgnoreCase)) { index += 5; return false; } index++; return false; } private static object? ParseNull(string json, ref int index) { if (json.Substring(index).StartsWith("null", StringComparison.OrdinalIgnoreCase)) { index += 4; } return null; } private static object ParseNumber(string json, ref int index) { int num = index; while (index < json.Length && (char.IsDigit(json[index]) || json[index] == '-' || json[index] == '+' || json[index] == '.' || json[index] == 'e' || json[index] == 'E')) { index++; } string text = json.Substring(num, index - num); long result2; if (text.Contains(".") || text.Contains("e") || text.Contains("E")) { if (double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { return result; } } else if (long.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out result2)) { if (result2 <= int.MaxValue && result2 >= int.MinValue) { return (int)result2; } return result2; } return text; } private static void SkipWhitespace(string json, ref int index) { while (index < json.Length && char.IsWhiteSpace(json[index])) { index++; } } } internal static class ZNetHelper { private static FieldInfo? FiPeers; private static FieldInfo? FiAdminList; private static FieldInfo? FiBannedList; private static FieldInfo? FiServerPlayerLimit; private static MethodInfo? MiListContainsId; static ZNetHelper() { try { FiPeers = typeof(ZNet).GetField("m_peers", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); FiAdminList = typeof(ZNet).GetField("m_adminList", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); FiBannedList = typeof(ZNet).GetField("m_bannedList", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); FiServerPlayerLimit = typeof(ZNet).GetField("m_serverPlayerLimit", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); MiListContainsId = typeof(ZNet).GetMethod("ListContainsId", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); } catch { } } public static List GetPeers() { if ((Object)(object)ZNet.instance == (Object)null || FiPeers == null) { return new List(); } if (!(FiPeers.GetValue(ZNet.instance) is List collection)) { return new List(); } return new List(collection); } public static int GetServerPlayerLimit() { if (FiServerPlayerLimit != null) { object obj = (FiServerPlayerLimit.IsStatic ? FiServerPlayerLimit.GetValue(null) : (((Object)(object)ZNet.instance != (Object)null) ? FiServerPlayerLimit.GetValue(ZNet.instance) : null)); if (obj is int) { return (int)obj; } } return 10; } public static List GetBannedList() { List list = new List(); if ((Object)(object)ZNet.instance == (Object)null) { return list; } try { object obj = ((FiBannedList != null) ? FiBannedList.GetValue(ZNet.instance) : Traverse.Create((object)ZNet.instance).Field("m_bannedList").GetValue()); if (obj != null) { MethodInfo method = obj.GetType().GetMethod("GetList"); if (method != null && method.Invoke(obj, null) is IEnumerable enumerable) { foreach (object item in enumerable) { if (item is string text && !string.IsNullOrWhiteSpace(text)) { list.Add(text.Trim()); } } } } } catch (Exception ex) { ManualLogSource log = BifrostheimPlugin.Log; if (log != null) { log.LogError((object)("[ZNetHelper] GetBannedList error: " + ex.Message)); } } return list; } public static int GetPeerPing(ZNetPeer peer) { if (peer == null) { return 0; } try { if (peer.m_rpc != null) { PropertyInfo property = ((object)peer.m_rpc).GetType().GetProperty("m_ping", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (property != null && property.GetValue(peer.m_rpc) is float num) { return (int)(num * 1000f); } FieldInfo field = ((object)peer.m_rpc).GetType().GetField("m_ping", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field != null && field.GetValue(peer.m_rpc) is float num2) { return (int)(num2 * 1000f); } } } catch { } return 0; } public static ZNetPeer? GetPeerByRpc(ZRpc rpc) { return ((IEnumerable)GetPeers()).FirstOrDefault((Func)((ZNetPeer p) => p.m_rpc == rpc)); } public static ZNetPeer? FindPeerByPlayerId(string playerId) { if (string.IsNullOrWhiteSpace(playerId)) { return null; } return ((IEnumerable)GetPeers()).FirstOrDefault((Func)((ZNetPeer p) => p != null && string.Equals(GetPlayerId(p), playerId, StringComparison.OrdinalIgnoreCase))); } public static bool IsPlayerOnline(string steamId, string characterName) { if ((Object)(object)ZNet.instance == (Object)null) { return false; } try { foreach (ZNetPeer peer in GetPeers()) { if (peer != null) { if (ConfigSyncManager.IsSteamIdMatch(GetPlayerId(peer), steamId)) { return true; } if (!string.IsNullOrWhiteSpace(characterName) && characterName != "Unknown" && string.Equals(peer.m_playerName, characterName, StringComparison.OrdinalIgnoreCase)) { return true; } } } } catch { } return false; } public static (float health, float maxHealth, bool pvp, string zone, int daysSurvived) GetPlayerData(ZNetPeer peer) { //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_00d4: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_00eb: Unknown result type (might be due to invalid IL or missing references) float num = 25f; float num2 = 25f; bool item = false; string item2 = "Meadows"; int item3 = 1; try { if ((Object)(object)EnvMan.instance != (Object)null && (Object)(object)ZNet.instance != (Object)null) { item3 = EnvMan.instance.GetDay(ZNet.instance.GetTimeSeconds()); } else if ((Object)(object)ZNet.instance != (Object)null) { item3 = Math.Max(1, (int)(ZNet.instance.GetTimeSeconds() / 1200.0) + 1); } } catch { } if (peer == null) { return (health: num, maxHealth: num2, pvp: item, zone: item2, daysSurvived: item3); } Vector3 refPos = peer.m_refPos; try { if (WorldGenerator.instance != null) { item2 = ((object)WorldGenerator.instance.GetBiome(refPos.x, refPos.z, 0.02f, false)/*cast due to .constrained prefix*/).ToString(); } } catch { } try { if (ZDOMan.instance != null && peer.m_characterID != ZDOID.None) { ZDO zDO = ZDOMan.instance.GetZDO(peer.m_characterID); if (zDO != null) { num = zDO.GetFloat("health", 25f); num2 = zDO.GetFloat("max_health", 25f); if (num2 < num) { num2 = num; } if (num2 <= 0f) { num2 = 25f; } item = zDO.GetBool("pvp", false); } } } catch { } return (health: num, maxHealth: num2, pvp: item, zone: item2, daysSurvived: item3); } public static string GetPlayerId(ZNetPeer peer) { if (peer == null || peer.m_socket == null) { return string.Empty; } string hostName = peer.m_socket.GetHostName(); if (string.IsNullOrWhiteSpace(hostName)) { return string.Empty; } if (ulong.TryParse(hostName, out var _)) { return "Steam_" + hostName; } return hostName; } public static bool IsValidPlayerId(string? playerId) { if (string.IsNullOrWhiteSpace(playerId) || playerId.Length > 128) { return false; } foreach (char c in playerId) { if (!char.IsLetterOrDigit(c) && c != '_' && c != '-') { return false; } } ulong result; if (!playerId.StartsWith("Steam_", StringComparison.OrdinalIgnoreCase) && !playerId.StartsWith("Xbox_", StringComparison.OrdinalIgnoreCase) && !playerId.StartsWith("PlayFab_", StringComparison.OrdinalIgnoreCase)) { return ulong.TryParse(playerId, out result); } return true; } public static bool IsAdmin(ZNetPeer peer) { if (peer == null) { return false; } string playerId = GetPlayerId(peer); ISocket socket = peer.m_socket; string text = ((socket != null) ? socket.GetHostName() : null) ?? string.Empty; if (!IsAdmin(playerId)) { if (!string.IsNullOrWhiteSpace(text)) { return IsAdmin(text); } return false; } return true; } public static bool IsAdmin(string playerId) { if ((Object)(object)ZNet.instance == (Object)null || string.IsNullOrWhiteSpace(playerId)) { return false; } try { object obj = ((FiAdminList != null) ? FiAdminList.GetValue(ZNet.instance) : Traverse.Create((object)ZNet.instance).Field("m_adminList").GetValue()); if (obj == null) { return false; } HashSet hashSet = new HashSet(StringComparer.OrdinalIgnoreCase) { playerId.Trim() }; ulong result; if (playerId.StartsWith("Steam_", StringComparison.OrdinalIgnoreCase)) { hashSet.Add(playerId.Substring("Steam_".Length).Trim()); } else if (ulong.TryParse(playerId.Trim(), out result)) { hashSet.Add("Steam_" + playerId.Trim()); } if (playerId.StartsWith("Xbox_", StringComparison.OrdinalIgnoreCase)) { hashSet.Add(playerId.Substring("Xbox_".Length).Trim()); } if (playerId.StartsWith("PlayFab_", StringComparison.OrdinalIgnoreCase)) { hashSet.Add(playerId.Substring("PlayFab_".Length).Trim()); } if (MiListContainsId != null) { foreach (string item in hashSet) { if ((bool)(MiListContainsId.Invoke(ZNet.instance, new object[2] { obj, item }) ?? ((object)false))) { return true; } } } MethodInfo method = obj.GetType().GetMethod("Contains", new Type[1] { typeof(string) }); if (method != null) { foreach (string item2 in hashSet) { if ((bool)(method.Invoke(obj, new object[1] { item2 }) ?? ((object)false))) { return true; } } } MethodInfo method2 = obj.GetType().GetMethod("GetList"); if (method2 != null && method2.Invoke(obj, null) is IEnumerable enumerable) { foreach (object item3 in enumerable) { if (item3 is string text && hashSet.Contains(text.Trim())) { return true; } } } return false; } catch (Exception ex) { ManualLogSource log = BifrostheimPlugin.Log; if (log != null) { log.LogError((object)("[ZNetHelper] IsAdmin check failed: " + ex.Message)); } return false; } } public static void BroadcastServerMessage(string message) { //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_01a3: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) if (string.IsNullOrWhiteSpace(message)) { return; } string text = message.Trim(); string text2 = "[SERVER] " + text; try { if (ZRoutedRpc.instance != null) { ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "ShowMessage", new object[2] { 2, text2 }); } } catch (Exception ex) { ManualLogSource log = BifrostheimPlugin.Log; if (log != null) { log.LogWarning((object)("[ZNetHelper] ZRoutedRpc ShowMessage broadcast failed: " + ex.Message)); } } try { if (ZRoutedRpc.instance != null) { try { ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "ChatMessage", new object[4] { Vector3.zero, 2, "Server", text }); } catch { } try { ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "ChatMessage", new object[5] { Vector3.zero, 2, "Server", text, string.Empty }); } catch { } } } catch (Exception ex2) { ManualLogSource log2 = BifrostheimPlugin.Log; if (log2 != null) { log2.LogWarning((object)("[ZNetHelper] ZRoutedRpc ChatMessage broadcast failed: " + ex2.Message)); } } try { if (!((Object)(object)ZNet.instance != (Object)null)) { return; } foreach (ZNetPeer peer in GetPeers()) { if (peer?.m_rpc != null) { try { peer.m_rpc.Invoke("ShowMessage", new object[2] { 2, text2 }); } catch { } try { peer.m_rpc.Invoke("ChatMessage", new object[4] { Vector3.zero, 2, "Server", text }); } catch { } } } } catch (Exception ex3) { ManualLogSource log3 = BifrostheimPlugin.Log; if (log3 != null) { log3.LogWarning((object)("[ZNetHelper] Direct peer broadcast failed: " + ex3.Message)); } } } } }