From 0310e528677f7817156dfdb322945deb3bec38f2 Mon Sep 17 00:00:00 2001 From: mikx Date: Wed, 4 Feb 2026 14:55:59 -0500 Subject: [PATCH] (1.5.0) Kill Feed + Auto Door Swamp Fix --- MxValheim/KillFeed/Chat.cs | 47 ++++++ MxValheim/KillFeed/Patch.cs | 307 ++++++++++++++++++++++++++++++++++++ MxValheim/MxValheim.cs | 108 ++++++++++++- MxValheim/MxValheim.csproj | 20 +++ MxValheim/Patch/Doors.cs | 7 +- README.md | 1 + 6 files changed, 483 insertions(+), 7 deletions(-) create mode 100644 MxValheim/KillFeed/Chat.cs create mode 100644 MxValheim/KillFeed/Patch.cs diff --git a/MxValheim/KillFeed/Chat.cs b/MxValheim/KillFeed/Chat.cs new file mode 100644 index 0000000..84b2bcd --- /dev/null +++ b/MxValheim/KillFeed/Chat.cs @@ -0,0 +1,47 @@ +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace MxValheim.KillFeed +{ + internal class Chat_Patch + { + [HarmonyPatch(typeof(Chat), nameof(Chat.Awake))] + class ChatLimitPatch + { + static void Postfix(Chat __instance) + { + // 1. Update the UI component (The most important part for typing) + if (__instance.m_input != null) + { + __instance.m_input.characterLimit = 1000; + } + + // 2. Try to find the internal length field (it might be m_maxLength or m_maxMessageLength) + try + { + var field = AccessTools.Field(typeof(Chat), "m_maxMessageLength") + ?? AccessTools.Field(typeof(Chat), "m_maxLength"); + + if (field != null) + { + field.SetValue(__instance, 1000); + UnityEngine.Debug.Log($"KillFeed: Set {field.Name} to 1000."); + } + else + { + UnityEngine.Debug.LogWarning("KillFeed: Could not find a length field in Chat. Logic might use InputField limit only."); + } + } + catch (Exception e) + { + UnityEngine.Debug.LogWarning($"KillFeed: Non-critical error setting chat limit: {e.Message}"); + } + } + } + } +} diff --git a/MxValheim/KillFeed/Patch.cs b/MxValheim/KillFeed/Patch.cs new file mode 100644 index 0000000..561b076 --- /dev/null +++ b/MxValheim/KillFeed/Patch.cs @@ -0,0 +1,307 @@ +using BepInEx; +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Threading; +using UnityEngine; +using UnityEngine.UI; +using static MxValheimMod; + +namespace MxValheim.KillFeed +{ + public class KillFeed_Patch + { + public static void SendKillToAll(string attacker, string victim, string weaponPrefab, int type) + { + if (ZRoutedRpc.instance != null) + { + ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "RPC_MxKillMsg", attacker, victim, weaponPrefab, type); + } + } + + public void OnReceiveKillMsg(long sender, string attacker, string victim, string weaponPrefab, int type) + { + ZNet zn = new ZNet(); + if (zn.IsDedicated()) return; + + string finalMsg = (type == 1) ? $"☠ {victim} a été tué par {attacker}" : + (type == 2) ? $"{attacker} a tué {victim}" : + $"{attacker} a tué {victim}"; + + Sprite weaponIcon = null; + + // Only check ObjectDB if we are actually in a world + if (ObjectDB.instance != null && !string.IsNullOrEmpty(weaponPrefab)) + { + GameObject itemObj = ObjectDB.instance.GetItemPrefab(weaponPrefab); + if (itemObj != null) + { + ItemDrop itemDrop = itemObj.GetComponent(); + if (itemDrop != null && itemDrop.m_itemData.m_shared.m_icons.Length > 0) + { + weaponIcon = itemDrop.m_itemData.m_shared.m_icons[0]; + } + } + } + lock (_msgQueue) + { + _msgQueue.Enqueue(finalMsg); + _iconQueue.Enqueue(weaponIcon); + } + } + + [HarmonyPatch(typeof(Character), nameof(Character.Damage))] + public static class DamageTracker + { + static void Prefix(Character __instance, HitData hit) + { + if (__instance == null || hit == null) return; + + Character attacker = hit.GetAttacker(); + if (attacker != null) + { + string weaponName = "Unknown"; + + // If the attacker is a player, get their current weapon + if (attacker.IsPlayer()) + { + Player player = attacker as Player; + if (player != null && player.GetCurrentWeapon() != null) + { + weaponName = player.GetCurrentWeapon().m_dropPrefab?.name ?? "Hands"; + } + } + else + { + // If it's a monster, we just use their hover name (e.g., "Troll") + weaponName = attacker.GetHoverName(); + } + + // Store the data for the Postfix to use + _activeTrackers[__instance] = new KillData + { + attackerName = attacker.GetHoverName(), + weaponPrefabName = weaponName + }; + } + } + } + + [HarmonyPatch(typeof(Character), nameof(Character.ApplyDamage))] + public static class DeathNotifier + { + // The parameter names MUST match the game's: hit, showDamageText, triggerEffects, mod + static void Postfix(Character __instance, HitData hit, bool showDamageText, bool triggerEffects, HitData.DamageModifier mod) + { + // Check if the character is dead or just died + if (__instance.GetHealth() <= 0f) + { + ZNetView nview = __instance.GetComponent(); + + // This check should now pass because ApplyDamage happens before the object is invalidated + if (nview == null || !nview.IsValid() || !nview.IsOwner()) return; + + _activeTrackers.TryGetValue(__instance, out KillData data); + + string attackerName = data?.attackerName ?? hit.GetAttacker()?.GetHoverName() ?? "The World"; + string weaponName = data?.weaponPrefabName ?? ""; + string victimName = __instance.GetHoverName(); + + int type = 0; + if (__instance.IsPlayer()) type = 1; + else if (attackerName == Player.m_localPlayer?.GetHoverName()) type = 2; + + if (ZRoutedRpc.instance != null) + { + ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "RPC_MxKillMsg", + attackerName, victimName, weaponName, type); + } + + _activeTrackers.Remove(__instance); + } + } + } + + public void ShowNextMessage(string msg, Sprite icon) + { + if (_hudRoot == null) CreateCustomHud(); + + // Re-check for font here just in case it wasn't loaded during Awake + if (_killText.font.name != "AveriaSerifLibre-Bold") + { + foreach (Font f in Resources.FindObjectsOfTypeAll()) + { + if (f.name == "AveriaSerifLibre-Bold") + { + _killText.font = f; + break; + } + } + } + + _killText.text = msg; + _weaponIconSlot.sprite = icon; + _weaponIconSlot.gameObject.SetActive(icon != null); + _hudRoot.SetActive(true); + _canvasGroup.alpha = 1f; + _displayTimer = 5.0f; + } + + private void CreateCustomHud() + { + GameObject oldRoot = GameObject.Find("MxKillFeed_Root"); + if (oldRoot != null) UnityEngine.Object.Destroy(oldRoot); + + _hudRoot = new GameObject("MxKillFeed_Root"); + UnityEngine.Object.DontDestroyOnLoad(_hudRoot); + + Canvas c = _hudRoot.AddComponent(); + c.renderMode = RenderMode.ScreenSpaceOverlay; + c.sortingOrder = 10000; + + CanvasScaler scaler = _hudRoot.AddComponent(); + scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; + scaler.referenceResolution = new Vector2(1920, 1080); + + _canvasGroup = _hudRoot.AddComponent(); + _canvasGroup.alpha = 0f; // Start invisible for the fade-in + + // 1. Smaller, sleeker Panel + GameObject panel = new GameObject("Background", typeof(RectTransform), typeof(Image)); + panel.transform.SetParent(_hudRoot.transform, false); + _panelImage = panel.GetComponent(); + _panelImage.color = new Color(0, 0, 0, 0.6f); // More transparent/sleek + + // Try to find the "sunken" wood panel style if shout_field isn't your vibe + foreach (Sprite s in Resources.FindObjectsOfTypeAll()) + { + if (s.name == "item_background") + { // This gives a nice square framed look + _panelImage.sprite = s; + _panelImage.type = Image.Type.Sliced; + break; + } + } + + RectTransform pRect = panel.GetComponent(); + pRect.anchorMin = pRect.anchorMax = pRect.pivot = new Vector2(0.5f, 1f); + pRect.anchoredPosition = new Vector2(0, -30); // Closer to top + pRect.sizeDelta = new Vector2(400, 35); // Shorter and narrower + pRect.localScale = Vector3.one; + + // 2. Compact Icon + GameObject iconObj = new GameObject("Icon", typeof(RectTransform), typeof(Image)); + iconObj.transform.SetParent(panel.transform, false); + _weaponIconSlot = iconObj.GetComponent(); + _weaponIconSlot.preserveAspect = true; + + RectTransform iRect = iconObj.GetComponent(); + iRect.anchorMin = iRect.anchorMax = iRect.pivot = new Vector2(0, 0.5f); + iRect.anchoredPosition = new Vector2(8, 0); + iRect.sizeDelta = new Vector2(25, 25); // Smaller icon + + // 3. Elegant Text + GameObject textObj = new GameObject("Text", typeof(RectTransform), typeof(Text)); + textObj.transform.SetParent(panel.transform, false); + _killText = textObj.GetComponent(); + + // Attempt to find Valheim's specific font, fallback to Arial if not found + Font valheimFont = null; + foreach (Font f in Resources.FindObjectsOfTypeAll()) + { + if (f.name == "AveriaSerifLibre-Bold") + { + valheimFont = f; + break; + } + } + + // Corrected fallback for older Unity versions used in Valheim + if (valheimFont == null) + { + valheimFont = Resources.GetBuiltinResource("Arial.ttf"); + } + + _killText.font = valheimFont; + _killText.fontSize = 18; + _killText.alignment = TextAnchor.MiddleLeft; + _killText.supportRichText = true; + _killText.color = Color.white; + + RectTransform tRect = textObj.GetComponent(); + tRect.anchorMin = Vector2.zero; + tRect.anchorMax = Vector2.one; + tRect.offsetMin = new Vector2(40, 0); + tRect.offsetMax = new Vector2(-10, 0); + + _hudRoot.SetActive(false); + } + + /* + public void CreateCustomHud() + { + _hudRoot = new GameObject("MxKillFeed_Root"); + UnityEngine.Object.DontDestroyOnLoad(_hudRoot); + + Canvas c = _hudRoot.AddComponent(); + c.renderMode = RenderMode.ScreenSpaceOverlay; + c.sortingOrder = 10000; + + // Add a Scaler to ensure it looks right on all resolutions + CanvasScaler scaler = _hudRoot.AddComponent(); + scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; + scaler.referenceResolution = new Vector2(1920, 1080); + + _canvasGroup = _hudRoot.AddComponent(); + + GameObject panel = new GameObject("Background", typeof(RectTransform), typeof(Image)); + panel.transform.SetParent(_hudRoot.transform, false); + _panelImage = panel.GetComponent(); + + // Fallback: Set a solid color first in case the sprite search fails + _panelImage.color = new Color(0, 0, 0, 0.8f); + + foreach (Sprite s in Resources.FindObjectsOfTypeAll()) + { + if (s.name == "shout_field") + { + _panelImage.sprite = s; + _panelImage.type = Image.Type.Sliced; + break; + } + } + + RectTransform pRect = panel.GetComponent(); + pRect.anchorMin = pRect.anchorMax = pRect.pivot = new Vector2(0.5f, 0.95f); + pRect.anchoredPosition = Vector2.zero; // Center top + pRect.sizeDelta = new Vector2(550, 40); + pRect.localScale = Vector3.one; // FORCE SCALE TO 1 + + // Icon + GameObject iconObj = new GameObject("Icon", typeof(RectTransform), typeof(Image)); + iconObj.transform.SetParent(panel.transform, false); + _weaponIconSlot = iconObj.GetComponent(); + _weaponIconSlot.preserveAspect = true; + RectTransform iRect = iconObj.GetComponent(); + iRect.anchorMin = iRect.anchorMax = iRect.pivot = new Vector2(0, 0.5f); + iRect.anchoredPosition = new Vector2(10, 0); + iRect.sizeDelta = new Vector2(30, 30); + + // Text + GameObject textObj = new GameObject("Text", typeof(RectTransform), typeof(Text)); + textObj.transform.SetParent(panel.transform, false); + _killText = textObj.GetComponent(); + _killText.font = Resources.GetBuiltinResource("Arial.ttf"); + _killText.fontSize = 18; + _killText.alignment = TextAnchor.MiddleLeft; + _killText.supportRichText = true; + textObj.AddComponent().effectColor = Color.black; + + RectTransform tRect = textObj.GetComponent(); + tRect.anchorMin = Vector2.zero; tRect.anchorMax = Vector2.one; + tRect.offsetMin = new Vector2(50, 0); tRect.offsetMax = new Vector2(-10, 0); + + _hudRoot.SetActive(false); + }*/ + } +} \ No newline at end of file diff --git a/MxValheim/MxValheim.cs b/MxValheim/MxValheim.cs index 7934b8d..af2ca11 100644 --- a/MxValheim/MxValheim.cs +++ b/MxValheim/MxValheim.cs @@ -1,21 +1,25 @@ using BepInEx; using BepInEx.Configuration; using HarmonyLib; +using MxValheim.KillFeed; +using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; +using System.Threading; using System.Xml; using UnityEngine; -using Newtonsoft.Json; +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.4.0"; + private const string ModVersion = "1.5.0"; public static ConfigEntry Config_Locked; public static ConfigEntry Config_OreMultiplier; @@ -29,6 +33,24 @@ public class MxValheimMod : BaseUnityPlugin private static string WeightConfigPath => Path.Combine(Paths.ConfigPath, "mxvalheim.custom_weights.json"); public static Dictionary WeightSettings = new Dictionary(); + // Data structures + public class KillData + { + public string attackerName; + public string weaponPrefabName; + } + + public static GameObject _hudRoot; + public static Text _killText; + public static Image _weaponIconSlot; + public static Image _panelImage; + public static CanvasGroup _canvasGroup; + public static float _displayTimer; + + public static readonly Queue _msgQueue = new Queue(); + public static readonly Queue _iconQueue = new Queue(); + public static readonly Dictionary _activeTrackers = new Dictionary(); + void Awake() { Instance = this; @@ -47,12 +69,81 @@ public class MxValheimMod : BaseUnityPlugin harmony.PatchAll(); } - private void Update() + [HarmonyPatch(typeof(Game), nameof(Game.Start))] + public static class GameStartPatch { - // Only check while in the game world to save resources - if (ObjectDB.instance == null) return; + 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) + { + kfp.ShowNextMessage(_msgQueue.Dequeue(), _iconQueue.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; + pRect.anchoredPosition = new Vector2(0, -20 - ((5f - _displayTimer) * 20)); // Slide down + } + 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); + } + } + + [HarmonyPatch(typeof(Terminal), nameof(Terminal.InputText))] + public static class ConsoleInputPatch + { + static void Postfix(Terminal __instance) + { + // 2. Get the text safely. Valheim often uses m_input.text + string text = __instance.m_input?.text; + Debug.Log($"MxKillFeed: {text}"); + if (string.IsNullOrEmpty(text)) return; + + if (text.ToLower().Trim() == "testkill") + { + // Trigger the message directly on the local client + KillFeed_Patch kfp = new KillFeed_Patch(); + kfp.OnReceiveKillMsg(0, "Developer", "Testing Dummy", "AxeFlint", 2); + } + } } private bool LoadJsonConfig() @@ -77,4 +168,9 @@ public class MxValheimMod : BaseUnityPlugin return false; } } + + internal static void ShowKillMessage(string v) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/MxValheim/MxValheim.csproj b/MxValheim/MxValheim.csproj index 944b071..9b0c5a3 100644 --- a/MxValheim/MxValheim.csproj +++ b/MxValheim/MxValheim.csproj @@ -48,6 +48,7 @@ E:\SteamLibrary\steamapps\common\Valheim\BepInEx\plugins\Newtonsoft.Json.dll + @@ -67,8 +68,19 @@ False E:\SteamLibrary\steamapps\common\Valheim\Valheim_Data\Managed\UnityEngine.PhysicsModule.dll + + + False + E:\SteamLibrary\steamapps\common\Valheim\Valheim_Data\Managed\UnityEngine.UI.dll + + + False + E:\SteamLibrary\steamapps\common\Valheim\Valheim_Data\Managed\UnityEngine.UIModule.dll + + + @@ -82,4 +94,12 @@ + + +copy /Y "$(TargetDir)$(TargetName).dll" "E:\SteamLibrary\steamapps\common\Valheim\BepInEx\plugins\" +copy /Y "$(TargetDir)$(TargetName).pdb" "E:\SteamLibrary\steamapps\common\Valheim\BepInEx\plugins\" + +copy /Y "$(TargetDir)$(TargetName).dll" "R:\Server\valdev\BepInEx\plugins\" +copy /Y "$(TargetDir)$(TargetName).pdb" "R:\Server\valdev\BepInEx\plugins\" + \ No newline at end of file diff --git a/MxValheim/Patch/Doors.cs b/MxValheim/Patch/Doors.cs index 3f067af..39c209f 100644 --- a/MxValheim/Patch/Doors.cs +++ b/MxValheim/Patch/Doors.cs @@ -1,7 +1,8 @@ using BepInEx; using HarmonyLib; -using UnityEngine; using System.Collections; +using UnityEngine; +using UnityEngine.Diagnostics; namespace MxValheim.Patch { @@ -12,7 +13,11 @@ namespace MxValheim.Patch { static void Postfix(Door __instance, Humanoid character, bool hold, bool alt, ZNetView ___m_nview) { + string prefabName = ___m_nview.GetPrefabName(); + if (hold || alt || ___m_nview == null || !___m_nview.IsValid()) return; + // Prevent closing of swamp iron gate + if (prefabName == "piece_crypt_door") return; // Get state: 0 is closed int state = ___m_nview.GetZDO().GetInt("state"); diff --git a/README.md b/README.md index 724dfc0..554bbc9 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ Official Mx Valheim Mod. ## Features +- Kill Feed with custom UI showing player kill and death. - Tweak individual item(s) weight in "BepInEx\config\mxvalheim.custom_weights.json". - Ore drop multiplier. (Value available in the generated config.) - Workbench crafting range multiplier. (Value available in the generated config.)