using BepInEx; using BepInEx.Configuration; using HarmonyLib; using MxValheim.EventSystem; using MxValheim.KillFeed; using MxValheim.Patch; using MxValheim.Patch.HUD; using Newtonsoft.Json; using Splatform; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Threading; using System.Xml; using TMPro; using UnityEngine; using UnityEngine.UI; using static MxValheim.Patch.Doors_Patch; [BepInPlugin(ModGUID, ModName, ModVersion)] public class MxValheimMod : BaseUnityPlugin { public static MxValheimMod Instance; // Singleton reference public static KillFeed_Patch kfp = new KillFeed_Patch(); public static Doors_Patch dpatch = new Doors_Patch(); private const string ModGUID = "ovh.mxdev.mxvalheim"; private const string ModName = "MxValheim"; private const string ModVersion = "1.6.2"; public static ConfigEntry Config_Locked; public static ConfigEntry Config_OreMultiplier; public static ConfigEntry Config_rangeMultiplier; public static ConfigEntry Config_bowDrawSpeedBonusPerLevel; public static ConfigEntry Config_rainDamage; public static ConfigEntry Config_boatSpeed; public static ConfigEntry Config_autoDoorCloseEnabled; public static ConfigEntry Config_autoDoorClose; public static string modPath = Path.Combine(Paths.PluginPath, "MxValheim"); public static string internalConfigsPath = Path.Combine(modPath, "Configs"); public static string internalDataPath = Path.Combine(modPath, "Data"); public static string internalDataEventSystem = Path.Combine(internalDataPath, "EventSystem.json"); public static string WeightConfigPath => Path.Combine(internalConfigsPath, "items_weight.json"); public static string AutoDoorConfigPath => Path.Combine(internalConfigsPath, "auto_doors.json"); public static DoorRoot CachedConfig; public static Dictionary WeightSettings = new Dictionary(); public static bool initExpBar = true; // Data structures public class KillData { public string attackerName; public string weaponPrefabName; public int victimLevel; public bool isBoss; } public static GameObject _hudRoot; public static Text _killText; public static Image _weaponIconSlot; public static Image _victimIconSlot; public static Image _panelImage; public static Outline _border; public static Color borderColor = Color.white; public static CanvasGroup _canvasGroup; public static float _fadeDuration = 0.5f; public static float _displayTimer; public static readonly Queue _msgQueue = new Queue(); public static readonly Queue _borderColQueue = new Queue(); public static readonly Queue _iconQueue = new Queue(); public static readonly Queue _victimIconQueue = new Queue(); public static readonly Dictionary _activeTrackers = new Dictionary(); public static readonly Queue _doorQueueNview = new Queue(); public static readonly Queue _doorQueueDoor = new Queue(); public static readonly Queue _doorQueueName = new Queue(); public static float _doorTimer; public static readonly Queue _serverEvent = new Queue(); void Awake() { Instance = this; Config_OreMultiplier = Config.Bind("General", "OreMultiplier", 3, "How many items should drop for every 1 ore/scrap found."); Config_rangeMultiplier = Config.Bind("General", "CraftingRangeMultiplier", 2.0f, "Multiplier for the workbench build/crafting range. Default is 2x."); Config_bowDrawSpeedBonusPerLevel = Config.Bind("General", "BowDrawSpeedBonusPercentPerLevel", 1.0f, "Shorten the bow draw speed by this percent for every bow upgrade level."); Config_rainDamage = Config.Bind("General", "RainDamage", true, "Set to true to stop rain damage, false to return to vanilla behavior."); Config_boatSpeed = Config.Bind("General", "BoatSpeedMultiplier", 2.0f, "Your boat/raft will move without wind at a speed multiplied by this value."); Config_autoDoorCloseEnabled = Config.Bind("General", "AutoDoorCloseEnabled", true, "Your doors will auto close if enabled. See AutoDoorCloseTimer for the desired time."); Config_autoDoorClose = Config.Bind("General", "AutoDoorCloseTimer", 5.0f, "Your doors will auto close after the specified timer duration."); LoadLocalization(); LoadJsonConfig(); LoadDoorConfig(); _doorTimer = MxValheimMod.Config_autoDoorClose.Value; Harmony harmony = new Harmony(ModGUID); harmony.PatchAll(); } [HarmonyPatch(typeof(Localization), nameof(Localization.SetupLanguage))] public static class Localization_SetupLanguage_Patch { public static void Postfix() { LoadLocalization(); } } // --- TEST COMMAND: Type 'testkill' in F5 console --- [HarmonyPatch(typeof(Terminal), nameof(Terminal.InputText))] public static class ConsoleInputPatch { static void Postfix(Terminal __instance) { string text = __instance.m_input.text; if (text.ToLower() == "listicons") { } } } [HarmonyPatch(typeof(Game), nameof(Game.Start))] public static class GameStartPatch { static void Postfix() { if (ZRoutedRpc.instance != null && kfp != null) { // We use the explicit 'Method' delegate to avoid the ArgumentException ZRoutedRpc.instance.Register("RPC_MxKillMsg", new RoutedMethod.Method(kfp.OnReceiveKillMsg)); ZRoutedRpc.instance.Register("RPC_ServerCreateUser", Profiles.ServerReceiveCreateUser); ZRoutedRpc.instance.Register("RPC_RequestProfile", Profiles.ServerHandleProfileRequest); ZRoutedRpc.instance.Register("RPC_ClientReceiveProfile", Profiles.LoadProfile); ZRoutedRpc.instance.Register("RPC_ServerSetExperience", Profiles.ServerSetExperience); ZRoutedRpc.instance.Register("RPC_ServerSetLevel", Profiles.ServerSetLevel); } } } void Update() { // Only run if we are actually in the game world if (Player.m_localPlayer != null) { string pid = GetUserPlayerID(Player.m_localPlayer).ToString(); Experience.UpdateProgressBar(); } // Only run if we are actually in the game world if (Player.m_localPlayer != null) { if (Splash.m_canvasObject == null) { Splash spl = new Splash(); spl.CreateOverlay(); } if (Splash.m_textComponent != null) { Splash.m_textComponent.text = $"MxValheim {MxValheimMod.ModVersion}"; } if (Clock.m_canvasObject == null) { Clock clk = new Clock(); clk.CreateOverlay(); if (initExpBar) { string pid = GetUserPlayerID(Player.m_localPlayer).ToString(); Experience.UpdateProgressBar(); initExpBar = false; } } if (Clock.m_textComponent != null) { Clock clk = new Clock(); string timeString = clk.GetFormattedTime(); int day = EnvMan.instance.GetDay(); string pid = GetUserPlayerID(Player.m_localPlayer).ToString(); Clock.m_textComponent.text = $"{Localization.instance.Localize("$clock_string_rank")} {Profiles.serverLevel} | {Localization.instance.Localize("$clock_string_day")} {day} | {timeString}"; } } // Door Logic if (_doorQueueNview.Count > 0) { if (_doorTimer > 0) { _doorTimer -= Time.deltaTime; } if (_doorTimer <= 0) { dpatch.CloseNextDoor(_doorQueueDoor.Dequeue(), _doorQueueNview.Dequeue(), _doorQueueName.Dequeue()); _doorTimer = MxValheimMod.Config_autoDoorClose.Value; } } // Door Logic ///////////// // KillFeed Logic ZNet zn = new ZNet(); if (zn.IsDedicated() || Player.m_localPlayer == null) return; if (_msgQueue == null || _msgQueue.Count == 0 && _displayTimer <= 0) return; // Logic to trigger next message if (_msgQueue.Count > 0 && _displayTimer <= 0) { borderColor = new Color(0.141f, 0.141f, 0.153f); kfp.ShowNextMessage(_msgQueue.Dequeue(), _iconQueue.Dequeue(), _victimIconQueue.Dequeue(), _borderColQueue.Dequeue()); } if (_displayTimer > 0) { _displayTimer -= Time.deltaTime; if (_canvasGroup != null && _hudRoot != null) { RectTransform pRect = _panelImage.GetComponent(); // FADE AND SLIDE LOGIC if (_displayTimer > 4.5f) { // Fading in (first 0.5s) _canvasGroup.alpha = (5f - _displayTimer) * 2; } else if (_displayTimer < 1f) { // Fading out (last 1s) _canvasGroup.alpha = _displayTimer; } else { _canvasGroup.alpha = 1f; pRect.anchoredPosition = new Vector2(0, -40); } } if (_displayTimer <= 0 && _hudRoot != null) _hudRoot.SetActive(false); } // KillFeed Logic ///////////////// } public static long GetUserPlayerID(Player player) { return player.GetPlayerID(); } public static void LoadLocalization() { if (Localization.instance == null) return; string modPath = Path.Combine(Paths.PluginPath, "MxValheim"); string translationsPath = Path.Combine(modPath, "Translations"); string lang = Localization.instance.GetSelectedLanguage(); string filePath = Path.Combine(translationsPath, $"{lang}.json"); if (!File.Exists(filePath)) { filePath = Path.Combine(translationsPath, "English.json"); } if (File.Exists(filePath)) { try { string json = File.ReadAllText(filePath); var dict = JsonConvert.DeserializeObject>(json); // Get the method via Reflection to bypass "Inaccessible" errors MethodInfo addWordMethod = typeof(Localization).GetMethod("AddWord", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (addWordMethod != null) { foreach (var entry in dict) { // Parameters: (instance to run on, array of arguments) addWordMethod.Invoke(Localization.instance, new object[] { entry.Key, entry.Value }); } Debug.Log($"[MxValheim] Successfully injected {dict.Count} strings for {lang}."); } else { Debug.LogError("[MxValheim] Critical Error: Could not find AddWord method in game code."); } } catch (Exception e) { Debug.LogError($"[MxValheim] Error loading JSON: {e.Message}"); } } } private bool LoadJsonConfig() { try { if (!File.Exists(WeightConfigPath)) { WeightSettings = new Dictionary { { "Wood", 1.0f }, { "Stone", 1.0f } }; File.WriteAllText(WeightConfigPath, JsonConvert.SerializeObject(WeightSettings, Newtonsoft.Json.Formatting.Indented)); return true; } string json = File.ReadAllText(WeightConfigPath); WeightSettings = JsonConvert.DeserializeObject>(json); Logger.LogInfo($"Successfully parsed {WeightSettings.Count} items."); return true; } catch (Exception ex) { Logger.LogWarning($"Could not read JSON (might be busy): {ex.Message}"); return false; } } public void LoadDoorConfig() { if (System.IO.File.Exists(AutoDoorConfigPath)) { string json = System.IO.File.ReadAllText(AutoDoorConfigPath); CachedConfig = JsonConvert.DeserializeObject(json); } } internal static void ShowKillMessage(string v) { throw new NotImplementedException(); } }