Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of Skald VikingKillFeed v1.0.0
BepInEx\plugins\Skald_VikingKillFeed\Skald_VikingKillFeed.dll
Decompiled 11 hours agousing 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<string, DateTime> _lastDeathAnnounced = new Dictionary<string, DateTime>(); private static readonly object _announceLock = new object(); private static readonly Dictionary<ZDOID, float> _lastKnownHealth = new Dictionary<ZDOID, float>(); 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<ZNetPeer> 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<ZDO> list2 = new List<ZDO>(); List<ZDO> list3 = new List<ZDO>(); 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<Player>() != (Object)null) && !((Object)(object)prefab.GetComponent<Character>() == (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<ZNetPeer> 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<DeathEventRecord> _history = new List<DeathEventRecord>(); 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<DeathEventRecord> GetRecentDeaths(int count = 50) { lock (_lock) { List<DeathEventRecord> list = new List<DeathEventRecord>(_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<bool> EnableDeathAnnouncements { get; private set; } public static ConfigEntry<bool> EnableBossDefeatAnnouncements { get; private set; } public static ConfigEntry<bool> IncludeBiomeInMessage { get; private set; } public static ConfigEntry<bool> LogToConsole { get; private set; } public static ConfigEntry<string> MonsterDeathMessages { get; private set; } public static ConfigEntry<string> BossDeathMessages { get; private set; } public static ConfigEntry<string> OverwhelmedMessages { get; private set; } public static ConfigEntry<string> GenericDeathMessages { get; private set; } public static void Initialize(ConfigFile config) { EnableDeathAnnouncements = config.Bind<bool>("1 - General", "EnableDeathAnnouncements", true, "Enable in-game global announcements when a player dies."); EnableBossDefeatAnnouncements = config.Bind<bool>("1 - General", "EnableBossDefeatAnnouncements", true, "Broadcast server-wide when a legendary boss is summoned or defeated."); IncludeBiomeInMessage = config.Bind<bool>("1 - General", "IncludeBiomeInMessage", true, "Include the biome name (e.g. Meadows, Swamp, Mistlands) in the announcement."); LogToConsole = config.Bind<bool>("1 - General", "LogToConsole", true, "Output all death records and chronicles to the server console log."); MonsterDeathMessages = config.Bind<string>("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<string>("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<string>("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<string>("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); } } }