using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text.RegularExpressions; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using Microsoft.CodeAnalysis; using Skald.Configuration; using Skald.Logic; using Skald.Patches; 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: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.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 Skald { [BepInPlugin("com.bigai.skald_vikingkillfeed", "Skald_VikingKillFeed", "1.0.0")] public class SkaldPlugin : BaseUnityPlugin { public const string PluginGUID = "com.bigai.skald_vikingkillfeed"; public const string PluginName = "Skald_VikingKillFeed"; public const string PluginVersion = "1.0.0"; public static SkaldPlugin 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)" Skald_VikingKillFeed v1.0.0 loading..."); Log.LogInfo((object)"══════════════════════════════════════════"); ModConfig.Initialize(((BaseUnityPlugin)this).Config); Log.LogInfo((object)"[Skald_VikingKillFeed] Server chronicle & Viking killfeed active."); ((MonoBehaviour)this).InvokeRepeating("PollDeaths", 2f, 1f); } catch (Exception arg) { Log.LogError((object)string.Format("[{0}] Failed to initialize: {1}", "Skald_VikingKillFeed", arg)); } } private void PollDeaths() { try { PlayerDeathPatch.PollForDeaths(); } catch (Exception arg) { Log.LogError((object)string.Format("[{0}] Error during health poll: {1}", "Skald_VikingKillFeed", arg)); } } private void OnDestroy() { Log.LogInfo((object)"[Skald_VikingKillFeed] Unloaded."); } } } namespace Skald.Patches { public static class PlayerDeathPatch { private static readonly Dictionary _lastDeathAnnounced = new Dictionary(); private static readonly object _announceLock = new object(); private static readonly Dictionary _lastKnownHealth = new Dictionary(); private static int _pollTick = 0; private const float SwarmRadius = 5f; private const float KillerSearchRadius = 20f; private const int OverwhelmedThreshold = 3; public static void PollForDeaths() { //IL_00bd: Unknown result type (might be due to invalid IL or missing references) //IL_013c: Unknown result type (might be due to invalid IL or missing references) //IL_0141: Unknown result type (might be due to invalid IL or missing references) //IL_0235: Unknown result type (might be due to invalid IL or missing references) //IL_015b: Unknown result type (might be due to invalid IL or missing references) //IL_01b9: Unknown result type (might be due to invalid IL or missing references) //IL_01a5: Unknown result type (might be due to invalid IL or missing references) try { if (!ModConfig.EnableDeathAnnouncements.Value || (Object)(object)ZNet.instance == (Object)null || ZDOMan.instance == null) { return; } List peers = ZNet.instance.GetPeers(); _pollTick++; bool flag = _pollTick % 10 == 0; if (flag) { ManualLogSource log = SkaldPlugin.Log; if (log != null) { log.LogInfo((object)$"[Skald][Diag] ZNet peers={peers?.Count ?? (-1)}"); } } if (peers == null) { return; } List<(string, string, Vector3)> list = new List<(string, string, Vector3)>(); foreach (ZNetPeer item2 in peers) { if (item2 == null || ((ZDOID)(ref item2.m_characterID)).IsNone()) { continue; } ZDO zDO = ZDOMan.instance.GetZDO(item2.m_characterID); if (zDO == null) { continue; } float num = zDO.GetFloat(ZDOVars.s_health, -1f); if (num < 0f) { continue; } string text = zDO.GetString(ZDOVars.s_playerName, string.Empty); if (string.IsNullOrWhiteSpace(text)) { text = item2.m_playerName; } if (flag) { ManualLogSource log2 = SkaldPlugin.Log; if (log2 != null) { log2.LogInfo((object)$"[Skald][Diag] '{text}' health={num:0.0}"); } } ZDOID characterID = item2.m_characterID; lock (_announceLock) { if (_lastKnownHealth.TryGetValue(characterID, out var value) && value > 0.01f && num <= 0.01f) { string item = text; ZRpc rpc = item2.m_rpc; object obj; if (rpc == null) { obj = null; } else { ISocket socket = rpc.GetSocket(); obj = ((socket != null) ? socket.GetHostName() : null); } if (obj == null) { obj = string.Empty; } list.Add((item, (string)obj, zDO.GetPosition())); } _lastKnownHealth[characterID] = num; } } foreach (var item3 in list) { ManualLogSource log3 = SkaldPlugin.Log; if (log3 != null) { log3.LogInfo((object)("[Skald][Hook] ZDO health poll detected death for " + item3.Item1 + ".")); } ProcessDeathByName(item3.Item1, item3.Item2, item3.Item3); } } catch (Exception arg) { ManualLogSource log4 = SkaldPlugin.Log; if (log4 != null) { log4.LogError((object)$"[Skald] Error polling for deaths: {arg}"); } } } public static void ProcessDeathByName(string victimName, string victimSteamId, Vector3 position) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) if (!ModConfig.EnableDeathAnnouncements.Value) { ManualLogSource log = SkaldPlugin.Log; if (log != null) { log.LogInfo((object)"[Skald][Hook] ProcessDeathByName skipped - EnableDeathAnnouncements is disabled in config."); } } else { ExecuteAnnouncement(victimName, victimSteamId, position); } } private static List<(string name, float dist)> FindNearbyEnemies(Vector3 position, float maxRadius) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) List<(string, float)> list = new List<(string, float)>(); try { if (ZDOMan.instance == null || (Object)(object)ZNetScene.instance == (Object)null) { return list; } Vector2i zone = ZoneSystem.GetZone(position); List list2 = new List(); List list3 = new List(); ZDOMan.instance.FindSectorObjects(zone, 1, 0, list2, list3); ManualLogSource log = SkaldPlugin.Log; if (log != null) { log.LogInfo((object)$"[Skald][Diag] Enemy scan at {position}: {list2.Count} ZDO(s) in range of sector."); } float num2 = default(float); foreach (ZDO item in list2) { if (item == null) { continue; } float num = Vector3.Distance(item.GetPosition(), position); if (num > maxRadius) { continue; } GameObject prefab = ZNetScene.instance.GetPrefab(item.GetPrefab()); if (!((Object)(object)prefab == (Object)null) && !((Object)(object)prefab.GetComponent() != (Object)null) && !((Object)(object)prefab.GetComponent() == (Object)null)) { bool flag = item.GetFloat(ZDOVars.s_health, ref num2); ManualLogSource log2 = SkaldPlugin.Log; if (log2 != null) { log2.LogInfo((object)$"[Skald][Diag] candidate '{((Object)prefab).name}' dist={num:0.0} hasHealthField={flag} health={num2:0.0}"); } if (!flag || !(num2 <= 0f)) { list.Add((((Object)prefab).name, num)); } } } } catch (Exception arg) { ManualLogSource log3 = SkaldPlugin.Log; if (log3 != null) { log3.LogError((object)$"[Skald] Error scanning nearby enemies: {arg}"); } } return list; } private static void ExecuteAnnouncement(string victimName, string victimSteamId, Vector3 position) { //IL_01b5: Unknown result type (might be due to invalid IL or missing references) //IL_0203: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_01f1: Unknown result type (might be due to invalid IL or missing references) //IL_012c: Unknown result type (might be due to invalid IL or missing references) //IL_0132: Unknown result type (might be due to invalid IL or missing references) //IL_013e: Unknown result type (might be due to invalid IL or missing references) //IL_0143: Unknown result type (might be due to invalid IL or missing references) //IL_0167: Unknown result type (might be due to invalid IL or missing references) //IL_016c: Unknown result type (might be due to invalid IL or missing references) lock (_announceLock) { if (_lastDeathAnnounced.TryGetValue(victimName, out var value) && (DateTime.UtcNow - value).TotalSeconds < 3.0) { ManualLogSource log = SkaldPlugin.Log; if (log != null) { log.LogInfo((object)("[Skald][Hook] Death for " + victimName + " debounced (duplicate event within 3s).")); } return; } _lastDeathAnnounced[victimName] = DateTime.UtcNow; } List<(string, float)> list = FindNearbyEnemies(position, 20f); DeathCategory category; string killerName; if (list.Count<(string, float)>(((string name, float dist) e) => e.dist <= 5f) > 3) { category = DeathCategory.Overwhelmed; killerName = "a horde of enemies"; } else if (list.Count > 0) { (category, killerName) = DeathAnalyzer.ClassifyByEnemyName(list.OrderBy<(string, float), float>(((string name, float dist) e) => e.dist).First().Item1); } else { category = DeathCategory.Generic; killerName = "The Fates"; } string biome = "Wilds"; try { if (WorldGenerator.instance != null) { biome = ((object)WorldGenerator.instance.GetBiome(position.x, position.z, 0.02f, false)/*cast due to .constrained prefix*/).ToString(); } else if ((Object)(object)EnvMan.instance != (Object)null) { biome = ((object)EnvMan.instance.GetCurrentBiome()/*cast due to .constrained prefix*/).ToString(); } } catch { } string text = DeathAnalyzer.FormatDeathMessage(category, victimName, killerName, biome); ChronicleRegistry.RecordDeath(new DeathEventRecord { VictimName = victimName, VictimSteamId = victimSteamId, KillerName = killerName, Category = category, Biome = biome, Position = position, FormattedMessage = text, Timestamp = DateTime.UtcNow }); if (ModConfig.LogToConsole.Value) { ManualLogSource log2 = SkaldPlugin.Log; if (log2 != null) { log2.LogInfo((object)$"[Skald] \ud83d\udc80 {text} (Pos: {position})"); } } BroadcastHelper.BroadcastDeathMessage(text, position); } } } namespace Skald.Logic { public static class BroadcastHelper { public static void BroadcastDeathMessage(string announcement, Vector3 position) { //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_0103: Expected O, but got Unknown //IL_011a: Unknown result type (might be due to invalid IL or missing references) //IL_01e7: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Unknown result type (might be due to invalid IL or missing references) if (string.IsNullOrWhiteSpace(announcement)) { return; } string text = announcement.Trim(); try { if (ZRoutedRpc.instance != null) { ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "ShowMessage", new object[2] { 2, text }); } } catch (Exception ex) { ManualLogSource log = SkaldPlugin.Log; if (log != null) { log.LogWarning((object)("[BroadcastHelper] ZRoutedRpc ShowMessage broadcast failed: " + ex.Message)); } } try { if (ZRoutedRpc.instance != null) { try { ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "ChatMessage", new object[4] { position, 2, "Skald", text }); } catch { } try { ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "ChatMessage", new object[5] { position, 2, "Skald", text, string.Empty }); } catch { } try { UserInfo val = new UserInfo { Name = "Skald" }; ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "ChatMessage", new object[4] { position, 2, val, text }); } catch { } } } catch (Exception ex2) { ManualLogSource log2 = SkaldPlugin.Log; if (log2 != null) { log2.LogWarning((object)("[BroadcastHelper] ZRoutedRpc ChatMessage broadcast failed: " + ex2.Message)); } } try { if ((Object)(object)ZNet.instance != (Object)null) { List peers = ZNet.instance.GetPeers(); if (peers != null) { foreach (ZNetPeer item in peers) { if (item?.m_rpc != null) { try { item.m_rpc.Invoke("ShowMessage", new object[2] { 2, text }); } catch { } try { item.m_rpc.Invoke("ChatMessage", new object[4] { position, 2, "Skald", text }); } catch { } } } } } } catch (Exception ex3) { ManualLogSource log3 = SkaldPlugin.Log; if (log3 != null) { log3.LogWarning((object)("[BroadcastHelper] Direct peer broadcast failed: " + ex3.Message)); } } try { if ((Object)(object)MessageHud.instance != (Object)null) { MessageHud.instance.ShowMessage((MessageType)2, text, 0, (Sprite)null, false); } if ((Object)(object)Chat.instance != (Object)null) { ((Terminal)Chat.instance).AddString(text); } } catch (Exception ex4) { ManualLogSource log4 = SkaldPlugin.Log; if (log4 != null) { log4.LogWarning((object)("[BroadcastHelper] Local client UI display failed: " + ex4.Message)); } } } } public enum DeathCategory { Boss, Monster, Overwhelmed, Generic } public class DeathEventRecord { public string Id { get; set; } = Guid.NewGuid().ToString(); public string VictimName { get; set; } = string.Empty; public string VictimSteamId { get; set; } = string.Empty; public string KillerName { get; set; } = string.Empty; public DeathCategory Category { get; set; } public string Biome { get; set; } = string.Empty; public Vector3 Position { get; set; } public string FormattedMessage { get; set; } = string.Empty; public DateTime Timestamp { get; set; } = DateTime.UtcNow; } public static class ChronicleRegistry { private static readonly List _history = new List(); private static readonly object _lock = new object(); private const int MaxHistoryCount = 200; public static void RecordDeath(DeathEventRecord record) { lock (_lock) { _history.Add(record); if (_history.Count > 200) { _history.RemoveAt(0); } } try { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { string name = assembly.GetName().Name; if (!(name == "Bifrostheim") && !(name == "ValheimPortal")) { continue; } Type type = assembly.GetType("ValheimPortal.Systems.Web.WebApiRouter"); Type type2 = assembly.GetType("ValheimPortal.Systems.Web.SkaldDeathRecordDto"); if (!(type != null) || !(type2 != null)) { continue; } FieldInfo field = type.GetField("_skaldChronicle", BindingFlags.Static | BindingFlags.NonPublic); if (field != null) { object value = field.GetValue(null); if (value != null) { object obj = Activator.CreateInstance(type2); type2.GetProperty("id")?.SetValue(obj, record.Id, null); type2.GetProperty("victimName")?.SetValue(obj, record.VictimName, null); type2.GetProperty("victimSteamId")?.SetValue(obj, record.VictimSteamId, null); type2.GetProperty("killerName")?.SetValue(obj, record.KillerName, null); type2.GetProperty("category")?.SetValue(obj, record.Category.ToString(), null); type2.GetProperty("biome")?.SetValue(obj, record.Biome, null); type2.GetProperty("formattedMessage")?.SetValue(obj, record.FormattedMessage, null); type2.GetProperty("timestamp")?.SetValue(obj, record.Timestamp.ToString("yyyy-MM-dd HH:mm:ss"), null); value.GetType().GetMethod("Insert")?.Invoke(value, new object[2] { 0, obj }); } } } } catch { } } public static List GetRecentDeaths(int count = 50) { lock (_lock) { List list = new List(_history); if (list.Count > count) { return list.GetRange(list.Count - count, count); } return list; } } } public static class DeathAnalyzer { private static readonly Random _rng = new Random(); private static readonly string[] BossNames = new string[10] { "Eikthyr", "Elder", "The Elder", "Bonemass", "Moder", "Yagluth", "The Queen", "Queen", "Fader", "Fader, Lord of the Ashlands" }; public static string FormatDeathMessage(DeathCategory category, string victimName, string killerName, string biome) { string[] templates = ModConfig.GetTemplates(category switch { DeathCategory.Boss => ModConfig.BossDeathMessages.Value, DeathCategory.Monster => ModConfig.MonsterDeathMessages.Value, DeathCategory.Overwhelmed => ModConfig.OverwhelmedMessages.Value, _ => ModConfig.GenericDeathMessages.Value, }); return templates[_rng.Next(templates.Length)].Replace("{victim}", victimName).Replace("{killer}", killerName).Replace("{biome}", biome); } public static (DeathCategory category, string killerName) ClassifyByEnemyName(string name) { string text = CleanEntityName(name); if (!IsBoss(text)) { return (category: DeathCategory.Monster, killerName: text); } return (category: DeathCategory.Boss, killerName: text); } private static bool IsBoss(string name) { string[] bossNames = BossNames; foreach (string value in bossNames) { if (name.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0) { return true; } } return false; } public static string CleanEntityName(string raw) { if (string.IsNullOrWhiteSpace(raw)) { return "Creature"; } if (raw.StartsWith("$")) { raw = raw.Substring(1); } raw = Regex.Replace(raw, "^enemy_", "", RegexOptions.IgnoreCase); raw = Regex.Replace(raw, "\\(Clone\\)$", ""); return raw.Trim(); } } } namespace Skald.Configuration { public static class ModConfig { public static ConfigEntry EnableDeathAnnouncements { get; private set; } public static ConfigEntry EnableBossDefeatAnnouncements { get; private set; } public static ConfigEntry IncludeBiomeInMessage { get; private set; } public static ConfigEntry LogToConsole { get; private set; } public static ConfigEntry MonsterDeathMessages { get; private set; } public static ConfigEntry BossDeathMessages { get; private set; } public static ConfigEntry OverwhelmedMessages { get; private set; } public static ConfigEntry GenericDeathMessages { get; private set; } public static void Initialize(ConfigFile config) { EnableDeathAnnouncements = config.Bind("1 - General", "EnableDeathAnnouncements", true, "Enable in-game global announcements when a player dies."); EnableBossDefeatAnnouncements = config.Bind("1 - General", "EnableBossDefeatAnnouncements", true, "Broadcast server-wide when a legendary boss is summoned or defeated."); IncludeBiomeInMessage = config.Bind("1 - General", "IncludeBiomeInMessage", true, "Include the biome name (e.g. Meadows, Swamp, Mistlands) in the announcement."); LogToConsole = config.Bind("1 - General", "LogToConsole", true, "Output all death records and chronicles to the server console log."); MonsterDeathMessages = config.Bind("2 - Templates", "MonsterDeathMessages", "{victim} was slain by a {killer} in the {biome};{victim} was torn apart by a {killer};{victim} fell before the wrath of a {killer} in the {biome};A {killer} claimed the soul of {victim}", "Message templates for monster deaths. Semicolon-separated. Tokens: {victim}, {killer}, {biome}"); BossDeathMessages = config.Bind("2 - Templates", "BossDeathMessages", "{victim} was annihilated by the mythical {killer}!;The legendary {killer} crushed {victim} into dust;{victim} dared challenge {killer} and was destroyed", "Message templates for boss deaths. Semicolon-separated. Tokens: {victim}, {killer}, {biome}"); OverwhelmedMessages = config.Bind("2 - Templates", "OverwhelmedMessages", "{victim} was defeated in glorious battle against a horde in the {biome};{victim} fell fighting valiantly against overwhelming odds;{victim} made a last stand against a swarm of enemies and was overwhelmed", "Message templates for deaths surrounded by multiple enemies. Semicolon-separated. Tokens: {victim}, {biome}"); GenericDeathMessages = config.Bind("2 - Templates", "GenericDeathMessages", "{victim} has departed for the halls of Valhalla;The Norns have cut the thread of {victim}'s life;{victim} died in the {biome}", "Fallback generic death templates. Semicolon-separated. Tokens: {victim}, {biome}"); } public static string[] GetTemplates(string configValue) { if (string.IsNullOrWhiteSpace(configValue)) { return new string[1] { "{victim} has died in the {biome}." }; } return configValue.Split(new char[1] { ';' }, StringSplitOptions.RemoveEmptyEntries); } } }