From 48445db5610b144c9b8738a84532ff5b27d8a143 Mon Sep 17 00:00:00 2001 From: mikx Date: Thu, 5 Feb 2026 07:15:19 -0500 Subject: [PATCH] (1.5.2) KillFeed Tweaks/Fix/Format --- MxValheim/KillFeed/Chat.cs | 47 ------ MxValheim/KillFeed/NPCIcon.cs | 65 ++++++++ MxValheim/KillFeed/Patch.cs | 305 ++++++++++++++++++++++++---------- MxValheim/MxValheim.cs | 37 ++--- MxValheim/MxValheim.csproj | 3 +- 5 files changed, 302 insertions(+), 155 deletions(-) delete mode 100644 MxValheim/KillFeed/Chat.cs create mode 100644 MxValheim/KillFeed/NPCIcon.cs diff --git a/MxValheim/KillFeed/Chat.cs b/MxValheim/KillFeed/Chat.cs deleted file mode 100644 index 84b2bcd..0000000 --- a/MxValheim/KillFeed/Chat.cs +++ /dev/null @@ -1,47 +0,0 @@ -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/NPCIcon.cs b/MxValheim/KillFeed/NPCIcon.cs new file mode 100644 index 0000000..03b87a9 --- /dev/null +++ b/MxValheim/KillFeed/NPCIcon.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MxValheim.KillFeed +{ + internal class NPCIcon + { + Dictionary valheimTrophies = new Dictionary + { + // Meadows + { "Boar", "TrophyBoar" }, + { "Deer", "TrophyDeer" }, + { "Neck", "TrophyNeck" }, + { "Eikthyr", "TrophyEikthyr" }, + + // Black Forest + { "Greydwarf", "TrophyGreydwarf" }, + { "Greydwarf_Shaman", "TrophyGreydwarfShaman" }, + { "Greydwarf_Elite", "TrophyGreydwarfBrute" }, + { "Skeleton", "TrophySkeleton" }, + { "Skeleton_Poison", "TrophySkeletonPoison" }, + { "Ghost", "TrophyGhost" }, + { "Troll", "TrophyTroll" }, + { "gd_king", "TrophyTheElder" }, + + // Swamp + { "Blob", "TrophyBlob" }, + { "Draugr", "TrophyDraugr" }, + { "Draugr_Elite", "TrophyDraugrElite" }, + { "Leech", "TrophyLeech" }, + { "Surtling", "TrophySurtling" }, + { "Wraith", "TrophyWraith" }, + { "Abomination", "TrophyAbomination" }, + { "Bonemass", "TrophyBonemass" }, + + // Mountains + { "Wolf", "TrophyWolf" }, + { "Fenring", "TrophyFenring" }, + { "StoneGolem", "TrophyStoneGolem" }, + { "Drake", "TrophyDrake" }, + { "Ulv", "TrophyUlv" }, + { "Cultist", "TrophyCultist" }, + { "Dragon", "TrophyModer" }, + + // Plains + { "Lox", "TrophyLox" }, + { "Deathsquito", "TrophyDeathsquito" }, + { "Goblin", "TrophyGoblin" }, + { "GoblinBrute", "TrophyGoblinBrute" }, + { "GoblinShaman", "TrophyGoblinShaman" }, + { "Growth", "TrophyGrowth" }, + { "GoblinKing", "TrophyGoblinKing" }, + + // Mistlands + { "Seeker", "TrophySeeker" }, + { "SeekerBrute", "TrophySeekerBrute" }, + { "Gjall", "TrophyGjall" }, + { "Tick", "TrophyTick" }, + { "SeekerQueen", "TrophySeekerQueen" } + }; + } +} diff --git a/MxValheim/KillFeed/Patch.cs b/MxValheim/KillFeed/Patch.cs index a533911..64f37c1 100644 --- a/MxValheim/KillFeed/Patch.cs +++ b/MxValheim/KillFeed/Patch.cs @@ -2,8 +2,10 @@ using HarmonyLib; using System; using System.Collections.Generic; +using System.Reflection.Emit; using System.Threading; using UnityEngine; +using UnityEngine.Diagnostics; using UnityEngine.UI; using static MxValheimMod; @@ -11,28 +13,43 @@ namespace MxValheim.KillFeed { public class KillFeed_Patch { - public static void SendKillToAll(string attacker, string victim, string weaponPrefab, int type) + public static void SendKillToAll(string attacker, string victim, string victimInternalName, string weaponPrefab, int encodedType) { if (ZRoutedRpc.instance != null) { - ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "RPC_MxKillMsg", attacker, victim, weaponPrefab, type); + ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "RPC_MxKillMsg", attacker, victim, victimInternalName, weaponPrefab, encodedType); } } - public void OnReceiveKillMsg(long sender, string attacker, string victim, string weaponPrefab, int type) + public void OnReceiveKillMsg(long sender, string attacker, string victim, string victimInternalName, string weaponPrefab, int encodedType) { ZNet zn = new ZNet(); if (zn.IsDedicated()) return; + float distance = (encodedType / 1000) / 10.0f; + int remainder = encodedType % 1000; + bool isBoss = (remainder >= 100); + int level = (remainder % 100) / 10; + int type = remainder % 10; + + Sprite victimIcon = GetCreatureIcon(victimInternalName); + + GameObject prefab = ZNetScene.instance.GetPrefab(victimInternalName); + string localizedVictim = victimInternalName; + if (prefab != null) + { + Character c = prefab.GetComponent(); + if (c != null) localizedVictim = c.m_name; + } + if (attacker == "The World") return; - string finalMsg = (type == 1) ? $"☠ {victim} a été tué par {attacker}" : - (type == 2) ? $"{attacker} a tué {victim}" : - $"{attacker} a tué {victim}"; + string finalMsg = (type == 1) ? $"{victim.ToUpper()} a été tué par {attacker.ToUpper()} à {distance:F1}m de distance." : + (type == 2) ? $"{attacker.ToUpper()} a tué {victim.ToUpper()} à {distance:F1}m de distance." : + $"{attacker.ToUpper()} a tué {victim.ToUpper()} à {distance:F1}m de distance."; 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); @@ -43,88 +60,38 @@ namespace MxValheim.KillFeed { weaponIcon = itemDrop.m_itemData.m_shared.m_icons[0]; } + } else + { } } + + if (type == 1) + { // Player + borderColor = new Color(0.545f, 0f, 0f); + } + else if (isBoss) + { // Boss + borderColor = new Color(0.545f, 0.361f, 0f); + } + else if (level >= 2) + { // 1-Star + + borderColor = new Color(0.525f, 0.545f, 0f); + } + else if (level < 2) + { + borderColor = new Color(0.141f, 0.141f, 0.153f); + } + lock (_msgQueue) { _msgQueue.Enqueue(finalMsg); + _borderColQueue.Enqueue(borderColor); _iconQueue.Enqueue(weaponIcon); + _victimIconQueue.Enqueue(victimIcon); } } - [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) + public void ShowNextMessage(string msg, Sprite weaponIcon, Sprite victimIcon, Color borderColor) { if (_hudRoot == null) CreateCustomHud(); @@ -142,13 +109,145 @@ namespace MxValheim.KillFeed } _killText.text = msg; - _weaponIconSlot.sprite = icon; - _weaponIconSlot.gameObject.SetActive(icon != null); + _border.effectColor = borderColor; + _border.enabled = true; + // Weapon icon (Left) + _weaponIconSlot.sprite = weaponIcon; + _weaponIconSlot.enabled = (weaponIcon != null); + // Victim icon (Right) + if (_victimIconSlot != null) + { + _victimIconSlot.sprite = victimIcon; + _victimIconSlot.enabled = (victimIcon != null); + } + _weaponIconSlot.gameObject.SetActive(weaponIcon != null); + _victimIconSlot.gameObject.SetActive(victimIcon != null); + _panelImage.rectTransform.localScale = (borderColor != Color.white) ? new Vector3(1.1f, 1.1f, 1.1f) : Vector3.one; _hudRoot.SetActive(true); _canvasGroup.alpha = 1f; _displayTimer = 5.0f; } + private Sprite GetCreatureIcon(string creatureName) + { + if (ObjectDB.instance == null) return null; + + // Clean the name (remove "(Clone)" if it somehow slipped through) + string cleanName = creatureName.Replace("(Clone)", "").Trim(); + + // List of common trophies that don't follow the exact pattern if needed + // But for most (Boar, Greyling, Neck, Deer), this works: + string trophyName = "Trophy" + cleanName; + + GameObject trophyObj = ObjectDB.instance.GetItemPrefab(trophyName); + if (trophyObj != null) + { + ItemDrop item = trophyObj.GetComponent(); + return item?.m_itemData?.m_shared?.m_icons[0]; + } + + return null; + } + + [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 weaponId = ""; + if (attacker is Player p) + { + // IMPORTANT: Use .m_dropPrefab.name to get "AxeStone" + // Do NOT use .m_shared.m_name which returns "$item_axe_stone" + ItemDrop.ItemData currentWeapon = p.GetCurrentWeapon(); + if (currentWeapon != null && currentWeapon.m_dropPrefab != null) + { + weaponId = currentWeapon.m_dropPrefab.name; + } + } + _activeTrackers[__instance] = new KillData + { + attackerName = attacker.GetHoverName(), + weaponPrefabName = weaponId + }; + } + } + } + + [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(); + + // Prevent inter-npc kill + if (!__instance.IsPlayer() && !hit.GetAttacker().IsPlayer()) return; + + // This check should now pass because ApplyDamage happens before the object is invalidated + if (nview == null || !nview.IsValid() || !nview.IsOwner()) return; + + float distance = 0f; + Character attacker = hit.GetAttacker(); + if (attacker != null) + { + Debug.Log($"KillFeed:ApplyDamage Attacker is {attacker} {attacker.transform.position}/{__instance.transform.position}"); + distance = Vector3.Distance(attacker.transform.position, __instance.transform.position); + } + + // Use Utils.GetPrefabName to get "Boar" instead of "Sanglier" or "Boar(Clone)" + string internalName = __instance.gameObject.name; + int cloneIndex = internalName.IndexOf("(Clone)"); + if (cloneIndex != -1) + { + internalName = internalName.Substring(0, cloneIndex); + } + + _activeTrackers.TryGetValue(__instance, out KillData data); + + string attackerName = data?.attackerName ?? hit.GetAttacker()?.GetHoverName() ?? "The World"; + string weaponName = ""; + if (data != null && !string.IsNullOrEmpty(data.weaponPrefabName)) + { + weaponName = data.weaponPrefabName; + } + else if (hit.GetAttacker() is Player p) + { + // Fallback: If tracker missed it, get current player's weapon + weaponName = p.GetCurrentWeapon()?.m_dropPrefab?.name ?? ""; + } + string victimName = __instance.GetHoverName(); + + int type = 0; + if (__instance.IsPlayer()) type = 1; + else if (attackerName == Player.m_localPlayer?.GetHoverName()) type = 2; + + int level = __instance.GetLevel(); + bool isBoss = __instance.IsBoss(); + + int distInt = (int)(Math.Min(distance, 999f) * 10); + int encodedType = type + (level * 10) + (isBoss ? 100 : 0) + (distInt * 1000); + + if (ZRoutedRpc.instance != null) + { + ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "RPC_MxKillMsg", + attackerName, victimName, internalName, weaponName, encodedType); + } + + _activeTrackers.Remove(__instance); + } + } + } + private void CreateCustomHud() { GameObject oldRoot = GameObject.Find("MxKillFeed_Root"); @@ -174,6 +273,21 @@ namespace MxValheim.KillFeed _panelImage = panel.GetComponent(); _panelImage.color = new Color(0, 0, 0, 0.6f); // More transparent/sleek + // 1. Add Layout Group to handle the horizontal row + HorizontalLayoutGroup layout = panel.AddComponent(); + layout.childAlignment = TextAnchor.MiddleCenter; + layout.spacing = 10f; + layout.padding = new RectOffset(15, 15, 5, 5); // Internal margins + layout.childControlWidth = true; // Let the layout control widths + layout.childControlHeight = true; + layout.childForceExpandWidth = false; + + // 2. Add the Content Size Fitter (The "Stretcher") + ContentSizeFitter fitter = panel.AddComponent(); + fitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize; + fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; + + // Try to find the "sunken" wood panel style if shout_field isn't your vibe foreach (Sprite s in Resources.FindObjectsOfTypeAll()) { @@ -196,6 +310,9 @@ namespace MxValheim.KillFeed iconObj.transform.SetParent(panel.transform, false); _weaponIconSlot = iconObj.GetComponent(); _weaponIconSlot.preserveAspect = true; + LayoutElement weaponLayout = iconObj.AddComponent(); + weaponLayout.preferredWidth = 24; + weaponLayout.preferredHeight = 24; RectTransform iRect = iconObj.GetComponent(); iRect.anchorMin = iRect.anchorMax = iRect.pivot = new Vector2(0, 0.5f); @@ -206,6 +323,8 @@ namespace MxValheim.KillFeed GameObject textObj = new GameObject("Text", typeof(RectTransform), typeof(Text)); textObj.transform.SetParent(panel.transform, false); _killText = textObj.GetComponent(); + // Important: Text needs to have its "Horizontal Overflow" set to Wrap or Overflow + _killText.horizontalOverflow = HorizontalWrapMode.Overflow; // Attempt to find Valheim's specific font, fallback to Arial if not found Font valheimFont = null; @@ -225,16 +344,34 @@ namespace MxValheim.KillFeed } _killText.font = valheimFont; - _killText.fontSize = 18; + _killText.fontSize = 12; _killText.alignment = TextAnchor.MiddleLeft; _killText.supportRichText = true; _killText.color = Color.white; + // 4. Victim Portrait Slot + GameObject victimObj = new GameObject("VictimIcon", typeof(RectTransform), typeof(Image)); + victimObj.transform.SetParent(panel.transform, false); + _victimIconSlot = victimObj.GetComponent(); + _victimIconSlot.preserveAspect = true; + LayoutElement victimLayout = victimObj.AddComponent(); + victimLayout.preferredWidth = 24; + victimLayout.preferredHeight = 24; + + RectTransform vRect = victimObj.GetComponent(); + vRect.anchorMin = vRect.anchorMax = vRect.pivot = new Vector2(1, 0.5f); + vRect.anchoredPosition = new Vector2(-8, 0); + vRect.sizeDelta = new Vector2(30, 30); + RectTransform tRect = textObj.GetComponent(); tRect.anchorMin = Vector2.zero; tRect.anchorMax = Vector2.one; tRect.offsetMin = new Vector2(40, 0); - tRect.offsetMax = new Vector2(-10, 0); + tRect.offsetMax = new Vector2(-40, 0); + + _border = panel.AddComponent(); + _border.effectDistance = new Vector2(2, 2); + _border.enabled = false; // Off by default _hudRoot.SetActive(false); } diff --git a/MxValheim/MxValheim.cs b/MxValheim/MxValheim.cs index 7fe5463..0e2b83b 100644 --- a/MxValheim/MxValheim.cs +++ b/MxValheim/MxValheim.cs @@ -19,7 +19,7 @@ public class MxValheimMod : BaseUnityPlugin private const string ModGUID = "ovh.mxdev.mxvalheim"; private const string ModName = "MxValheim"; - private const string ModVersion = "1.5.1"; + private const string ModVersion = "1.5.2"; public static ConfigEntry Config_Locked; public static ConfigEntry Config_OreMultiplier; @@ -38,17 +38,25 @@ public class MxValheimMod : BaseUnityPlugin { 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() @@ -77,8 +85,8 @@ public class MxValheimMod : BaseUnityPlugin 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_MxKillMsg", + new RoutedMethod.Method(kfp.OnReceiveKillMsg)); Debug.Log("MxValheimMod: RPC Registered successfully with explicit delegate."); } @@ -90,12 +98,14 @@ public class MxValheimMod : BaseUnityPlugin // 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()); + borderColor = new Color(0.141f, 0.141f, 0.153f); + kfp.ShowNextMessage(_msgQueue.Dequeue(), _iconQueue.Dequeue(), _victimIconQueue.Dequeue(), _borderColQueue.Dequeue()); } if (_displayTimer > 0) @@ -110,7 +120,6 @@ public class MxValheimMod : BaseUnityPlugin 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) @@ -125,25 +134,7 @@ public class MxValheimMod : BaseUnityPlugin 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() diff --git a/MxValheim/MxValheim.csproj b/MxValheim/MxValheim.csproj index 9b0c5a3..a87dc39 100644 --- a/MxValheim/MxValheim.csproj +++ b/MxValheim/MxValheim.csproj @@ -51,6 +51,7 @@ + @@ -79,7 +80,7 @@ - +