Decompiled source of Skald VikingKillFeed v1.0.0

BepInEx\plugins\Skald_VikingKillFeed\Skald_VikingKillFeed.dll

Decompiled 11 hours ago
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<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);
		}
	}
}