using BepInEx; using BepInEx.Configuration; using HarmonyLib; using MxValheim.KillFeed; using Newtonsoft.Json; 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; [BepInPlugin(ModGUID, ModName, ModVersion)] public class MxValheimMod : BaseUnityPlugin { public static MxValheimMod Instance; // Singleton reference public static KillFeed_Patch kfp = new KillFeed_Patch(); private const string ModGUID = "ovh.mxdev.mxvalheim"; private const string ModName = "MxValheim"; private const string ModVersion = "1.5.5"; 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"); private static string WeightConfigPath => Path.Combine(internalConfigsPath, "items_weight.json"); public static Dictionary WeightSettings = new Dictionary(); // 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(); 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(); 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") { var spriteAsset = Resources.FindObjectsOfTypeAll().FirstOrDefault(x => x.name == "icons"); ; if (spriteAsset != null) { Debug.Log($"--- Listing all sprites in {spriteAsset.name} ---"); for (int i = 0; i < spriteAsset.spriteCharacterTable.Count; i++) { var sprite = spriteAsset.spriteCharacterTable[i]; Debug.Log($"Index: {i} | Name: {sprite.name}"); } } } } } [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)); Debug.Log("MxValheimMod: RPC Registered successfully with explicit delegate."); } } } void Update() { // Use the game's native check for dedicated servers 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); } } 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; } } internal static void ShowKillMessage(string v) { throw new NotImplementedException(); } }