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 Denis UI Killfeed v1.2.0
plugins/DenisUIKillFeed/DenisUIKillFeed.dll
Decompiled 2 weeks agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Configuration; using ExitGames.Client.Photon; using HarmonyLib; using Microsoft.CodeAnalysis; using Photon.Pun; using Photon.Realtime; using TMPro; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyCompany("DenisUIKillFeed")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.2.0.0")] [assembly: AssemblyInformationalVersion("1.2.0+0d77aba4ef99a87d6be4daeb81b0de1f2e0c0dca")] [assembly: AssemblyProduct("DenisUIKillFeed")] [assembly: AssemblyTitle("DenisUIKillFeed")] [assembly: AssemblyVersion("1.2.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.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 DenisUIKillFeed { internal static class EnemyDeathEvents { public class EnemyDeathEventData { public Enemy Enemy { get; set; } public EnemyParent Parent { get; set; } public string NormalizedName { get; set; } public float TimeOfDeath { get; set; } public int LastHealth { get; set; } } public static UnityEvent<Enemy, EnemyParent, string> OnEnemyDeath { get; } = new UnityEvent<Enemy, EnemyParent, string>(); public static event Action<EnemyDeathEventData> OnEnemyDeathDetailed; internal static void RaiseEnemyDeath(Enemy enemy, EnemyParent parent, string normalizedName, float timeOfDeath, int lastHealth) { try { OnEnemyDeath?.Invoke(enemy, parent, normalizedName); EnemyDeathEvents.OnEnemyDeathDetailed?.Invoke(new EnemyDeathEventData { Enemy = enemy, Parent = parent, NormalizedName = normalizedName, TimeOfDeath = timeOfDeath, LastHealth = lastHealth }); } catch (Exception arg) { Debug.LogError((object)$"[EnemyDeathEvents] Exception: {arg}"); } } } internal static class EnemyFeedTracker { private static float lastFullCheckTime; private const float FULL_CHECK_INTERVAL = 0.15f; internal static Enemy GetEnemyFromParent(EnemyParent enemyParent) { try { object? obj = ReflectionCache.EnemyParentEnemyField?.GetValue(enemyParent); return (Enemy)((obj is Enemy) ? obj : null); } catch { return null; } } internal static bool GetEnemyHasHealth(Enemy enemy) { try { if ((Object)(object)enemy == (Object)null) { return false; } object obj = ReflectionCache.EnemyHasHealthField?.GetValue(enemy); if (obj is bool) { return (bool)obj; } } catch { } return false; } internal static EnemyHealth GetEnemyHealth(Enemy enemy) { if ((Object)(object)enemy == (Object)null) { return null; } try { FieldInfo enemyHealthRefField = ReflectionCache.EnemyHealthRefField; if (enemyHealthRefField != null) { object? value = enemyHealthRefField.GetValue(enemy); return (EnemyHealth)((value is EnemyHealth) ? value : null); } } catch (Exception ex) { Debug.LogWarning((object)("[DenisUIKillFeed] Failed to get Enemy.Health: " + ex.Message)); } return null; } internal static int GetEnemyHealthCurrent(EnemyHealth health) { try { if ((Object)(object)health == (Object)null) { return 0; } object obj = ReflectionCache.EnemyHealthCurrentField?.GetValue(health); if (obj is int) { return (int)obj; } } catch { } return 0; } internal static bool GetEnemyHealthDead(EnemyHealth health) { try { if ((Object)(object)health == (Object)null) { return false; } object obj = ReflectionCache.EnemyHealthDeadField?.GetValue(health); if (obj is bool) { return (bool)obj; } } catch { } return false; } internal static void ObserveEnemiesFromDirector() { try { EnemyDirector instance = EnemyDirector.instance; if (!((Object)(object)instance == (Object)null) && instance.enemiesSpawned != null && ShouldRunFullCheck()) { RunFullEnemyStateCheck(instance); CleanupDespawnedEnemies(instance); } } catch (Exception arg) { Debug.LogError((object)$"[DenisUIKillFeed] ObserveEnemiesFromDirector error: {arg}"); } } private static void CleanupDespawnedEnemies(EnemyDirector director) { List<int> list = new List<int>(); foreach (KeyValuePair<int, ObservedEnemyState> enemyState in Plugin.EnemyStates) { int id = enemyState.Key; ObservedEnemyState value = enemyState.Value; if (!director.enemiesSpawned.Any((EnemyParent ep) => (Object)(object)ep != (Object)null && ((Object)ep).GetInstanceID() == id) && value.LastHealth > 0) { list.Add(id); } } foreach (int item in list) { Plugin.EnemyStates.Remove(item); } } private static bool ShouldRunFullCheck() { if (Time.time - lastFullCheckTime >= 0.15f) { lastFullCheckTime = Time.time; return true; } return false; } private static void RunFullEnemyStateCheck(EnemyDirector director) { //IL_019a: Unknown result type (might be due to invalid IL or missing references) //IL_00fa: Unknown result type (might be due to invalid IL or missing references) //IL_0101: Expected O, but got Unknown foreach (EnemyParent item in director.enemiesSpawned) { if ((Object)(object)item == (Object)null) { continue; } int instanceID = ((Object)item).GetInstanceID(); Enemy enemyFromParent = GetEnemyFromParent(item); EnemyHealth enemyHealth = GetEnemyHealth(enemyFromParent); int num = (((Object)(object)enemyHealth != (Object)null && GetEnemyHasHealth(enemyFromParent)) ? GetEnemyHealthCurrent(enemyHealth) : 0); if (!Plugin.EnemyStates.TryGetValue(instanceID, out var value)) { ObservedEnemyState newState = new ObservedEnemyState { EnemyParent = item, Name = EnemyNameDirectory.Normalize(item.enemyName ?? "Enemy"), LastHealth = num, FirstSeenAt = Time.time }; Plugin.EnemyStates[instanceID] = newState; if (!((Object)(object)enemyHealth != (Object)null)) { continue; } try { object? obj = ReflectionCache.EnemyHealthOnDeathField?.GetValue(enemyHealth); UnityEvent val = (UnityEvent)((obj is UnityEvent) ? obj : null); if (val != null) { UnityAction val2 = (UnityAction)delegate { HandleEnemyDeath(newState); }; val.AddListener(val2); newState.DeathListener = val2; } } catch { } continue; } value.EnemyParent = item; if (num > 0 && value.LastHealth <= 0) { value.LastHealth = num; } if (num <= 0 && value.LastHealth > 0) { if (Plugin.ShowEnemyKillsConfig.Value && Time.time - Plugin.LevelStartTime >= 1f) { Plugin.AddEntry(Plugin.ApplyHostSyncOrCustomName(value.Name) + " killed", new Color(0.96f, 0.82f, 0.28f, 1f)); } EnemyDeathEvents.RaiseEnemyDeath(enemyFromParent, item, value.Name, Time.time, value.LastHealth); } value.LastHealth = num; } } private static void HandleEnemyDeath(ObservedEnemyState state) { } } internal static class EnemyNameDirectory { public static readonly Dictionary<string, string> NameMapping = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { { "Animal", "Animal" }, { "EnemyAnimal", "Animal" }, { "Bang", "Bang" }, { "EnemyBang", "Bang" }, { "Beamer", "Clown" }, { "EnemyBeamer", "Clown" }, { "Clown", "Clown" }, { "BirthdayBoy", "Birthday Boy" }, { "EnemyBirthdayBoy", "Birthday Boy" }, { "Birthday Boy", "Birthday Boy" }, { "BombThrower", "Cleanup Crew" }, { "Bomb Thrower", "Cleanup Crew" }, { "EnemyBombThrower", "Cleanup Crew" }, { "Cleanup Crew", "Cleanup Crew" }, { "Bowtie", "Bowtie" }, { "EnemyBowtie", "Bowtie" }, { "CeilingEye", "Peeper" }, { "Ceiling Eye", "Peeper" }, { "EnemyCeilingEye", "Peeper" }, { "Peeper", "Peeper" }, { "Duck", "Apex Predator" }, { "EnemyDuck", "Apex Predator" }, { "Apex Predator", "Apex Predator" }, { "Elsa", "Elsa" }, { "EnemyElsa", "Elsa" }, { "Floater", "Mentalist" }, { "EnemyFloater", "Mentalist" }, { "Mentalist", "Mentalist" }, { "Gnome", "Gnome" }, { "EnemyGnome", "Gnome" }, { "HeadController", "Headman" }, { "Head", "Headman" }, { "EnemyHeadController", "Headman" }, { "Headman", "Headman" }, { "HeadGrabber", "Headgrab" }, { "Head Grabber", "Headgrab" }, { "EnemyHeadGrabber", "Headgrab" }, { "Headgrab", "Headgrab" }, { "HeartHugger", "Heart Hugger" }, { "EnemyHeartHugger", "Heart Hugger" }, { "Hidden", "Hidden" }, { "EnemyHidden", "Hidden" }, { "Hunter", "Huntsman" }, { "EnemyHunter", "Huntsman" }, { "Huntsman", "Huntsman" }, { "Oogly", "Oogly" }, { "EnemyOogly", "Oogly" }, { "Robe", "Robe" }, { "EnemyRobe", "Robe" }, { "Runner", "Reaper" }, { "EnemyRunner", "Reaper" }, { "Reaper", "Reaper" }, { "Shadow", "Loom" }, { "EnemyShadow", "Loom" }, { "Loom", "Loom" }, { "SlowMouth", "Spewer" }, { "Slow Mouth", "Spewer" }, { "EnemySlowMouth", "Spewer" }, { "Spewer", "Spewer" }, { "SlowWalker", "Trudge" }, { "Slow Walker", "Trudge" }, { "EnemySlowWalker", "Trudge" }, { "Trudge", "Trudge" }, { "Spinny", "Gambit" }, { "EnemySpinny", "Gambit" }, { "Gambit", "Gambit" }, { "ThinMan", "Shadow Child" }, { "Thin Man", "Shadow Child" }, { "EnemyThinMan", "Shadow Child" }, { "Shadow Child", "Shadow Child" }, { "Tick", "Tick" }, { "EnemyTick", "Tick" }, { "Tricycle", "Bella" }, { "EnemyTricycle", "Bella" }, { "Bella", "Bella" }, { "Tumbler", "Chef" }, { "EnemyTumbler", "Chef" }, { "Chef", "Chef" }, { "Upscream", "Upscream" }, { "EnemyUpscream", "Upscream" }, { "ValuableThrower", "Rugrat" }, { "Valuable Thrower", "Rugrat" }, { "EnemyValuableThrower", "Rugrat" }, { "Rugrat", "Rugrat" } }; public static string Normalize(string rawName) { if (string.IsNullOrWhiteSpace(rawName)) { return "Unknown"; } if (NameMapping.TryGetValue(rawName, out var value)) { return value; } Debug.LogWarning((object)("[EnemyNameDirectory] Unknown enemy name: '" + rawName + "' → Unknown")); return "Unknown"; } public static IEnumerable<string> GetAllDisplayNames() { HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (string value in NameMapping.Values) { hashSet.Add(value); } List<string> list = new List<string>(hashSet); list.Sort(); return list; } } internal static class HostSyncManager { [Serializable] internal sealed class SerializedMonsterNames { [SerializeField] public SerializedNamePair[] names = Array.Empty<SerializedNamePair>(); } [Serializable] internal sealed class SerializedNamePair { [SerializeField] public string Key; [SerializeField] public string Value; } internal static readonly Dictionary<string, string> HostMonsterNames = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); private const string HostMonsterNamesPropertyKey = "denis.killfeed.hostnames.v1"; private static string lastKnownHostNamesJson = string.Empty; internal static void PublishHostMonsterNames(Dictionary<string, string> customNames) { //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Expected O, but got Unknown //IL_006b: Expected O, but got Unknown if (!PhotonNetwork.InRoom || !PhotonNetwork.LocalPlayer.IsMasterClient || customNames.Count == 0) { return; } try { string value = JsonUtility.ToJson((object)new SerializedMonsterNames { names = customNames.Select((KeyValuePair<string, string> kvp) => new SerializedNamePair { Key = kvp.Key, Value = kvp.Value }).ToArray() }); Hashtable val = new Hashtable(); ((Dictionary<object, object>)val).Add((object)"denis.killfeed.hostnames.v1", (object)value); Hashtable val2 = val; Room currentRoom = PhotonNetwork.CurrentRoom; if (currentRoom != null) { currentRoom.SetCustomProperties(val2, (Hashtable)null, (WebFlags)null); } } catch (Exception ex) { Debug.LogWarning((object)("[DenisUIKillFeed] Failed to publish host monster names: " + ex.Message)); } } internal static void UpdateHostMonsterNamesFromRoom() { if (!PhotonNetwork.InRoom) { lastKnownHostNamesJson = string.Empty; HostMonsterNames.Clear(); return; } try { Room currentRoom = PhotonNetwork.CurrentRoom; object value; if (((currentRoom != null) ? ((RoomInfo)currentRoom).CustomProperties : null) == null) { lastKnownHostNamesJson = string.Empty; HostMonsterNames.Clear(); } else if (!((Dictionary<object, object>)(object)((RoomInfo)currentRoom).CustomProperties).TryGetValue((object)"denis.killfeed.hostnames.v1", out value) || !(value is string text) || string.IsNullOrWhiteSpace(text)) { lastKnownHostNamesJson = string.Empty; HostMonsterNames.Clear(); } else { if (text == lastKnownHostNamesJson) { return; } lastKnownHostNamesJson = text; HostMonsterNames.Clear(); SerializedMonsterNames serializedMonsterNames = JsonUtility.FromJson<SerializedMonsterNames>(text); if (serializedMonsterNames?.names == null) { return; } SerializedNamePair[] names = serializedMonsterNames.names; foreach (SerializedNamePair serializedNamePair in names) { if (!string.IsNullOrEmpty(serializedNamePair.Key) && !string.IsNullOrEmpty(serializedNamePair.Value)) { HostMonsterNames[serializedNamePair.Key] = serializedNamePair.Value; } } } } catch (Exception ex) { Debug.LogWarning((object)("[DenisUIKillFeed] Failed to parse host monster names: " + ex.Message)); } } } public static class KillFeedAPI { public class EnemyPoolData { public string EnemyName { get; set; } public string InternalName { get; set; } public string ClassName { get; set; } public GameObject GameObject { get; set; } public EnemyParent EnemyParent { get; set; } public int InstanceId { get; set; } } public static event Action<List<EnemyPoolData>> OnEnemyPoolReady; internal static void RaiseEnemyPoolReady(List<EnemyPoolData> enemies) { try { if (enemies == null) { enemies = new List<EnemyPoolData>(); } KillFeedAPI.OnEnemyPoolReady?.Invoke(enemies); } catch (Exception arg) { Debug.LogError((object)$"[KillFeedAPI] Exception: {arg}"); } } public static string GetCustomEnemyName(string rawEnemyName) { return Plugin.GetCustomEnemyName(rawEnemyName); } } internal static class KillfeedConstants { internal const float HudGraceAfterLevelEnds = 8f; internal const float DisconnectConfirmSeconds = 1.25f; internal const float DisconnectStartupGuardSeconds = 4f; internal const float HudSlideSeconds = 0.18f; internal const float EntryFadeInSeconds = 0.16f; internal const float EntryFadeOutSeconds = 0.24f; internal const float EnemyObserveIntervalSeconds = 0.15f; } [HarmonyPatch(typeof(RoundDirector))] internal static class KillFeedHud { private static GameObject cachedGameHud; private static GameObject cachedTaxHaul; [HarmonyPatch("Update")] [HarmonyPostfix] private static void Update_Postfix() { bool flag = SemiFunc.RunIsLevel(); if (flag) { Plugin.LastLevelActiveAt = Time.time; } if (!EnsureHud()) { return; } if (!flag) { if (Time.time - Plugin.LastLevelActiveAt <= 8f) { UpdateKillFeedHud(); return; } SetInactive(); Plugin.ResetObservedState(); return; } PlayerFeedTracker.ObservePlayers(); if (Time.time - Plugin.LastEnemyObserveAt >= 0.15f) { Plugin.LastEnemyObserveAt = Time.time; EnemyFeedTracker.ObserveEnemiesFromDirector(); } UpdateKillFeedHud(); } internal static bool EnsureHud() { //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Expected O, but got Unknown //IL_0127: Unknown result type (might be due to invalid IL or missing references) //IL_0186: Unknown result type (might be due to invalid IL or missing references) //IL_019b: Unknown result type (might be due to invalid IL or missing references) //IL_01b0: Unknown result type (might be due to invalid IL or missing references) //IL_01c5: Unknown result type (might be due to invalid IL or missing references) //IL_01d9: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)cachedGameHud == (Object)null) { cachedGameHud = GameObject.Find("Game Hud"); } if ((Object)(object)cachedTaxHaul == (Object)null) { cachedTaxHaul = GameObject.Find("Tax Haul"); } if ((Object)(object)cachedGameHud == (Object)null || (Object)(object)cachedTaxHaul == (Object)null) { return false; } TMP_Text component = cachedTaxHaul.GetComponent<TMP_Text>(); TMP_FontAsset val = ((component != null) ? component.font : null); if ((Object)(object)val == (Object)null) { return false; } if ((Object)(object)Plugin.FeedInstance == (Object)null) { Plugin.FeedInstance = new GameObject("Denis UI Killfeed HUD"); Plugin.FeedInstance.SetActive(false); Plugin.FeedInstance.transform.SetParent(cachedGameHud.transform, false); Plugin.FeedText = Plugin.FeedInstance.AddComponent<TextMeshProUGUI>(); ((TMP_Text)Plugin.FeedText).font = val; ((TMP_Text)Plugin.FeedText).fontSize = 24f; ((TMP_Text)Plugin.FeedText).enableWordWrapping = false; ((TMP_Text)Plugin.FeedText).alignment = (TextAlignmentOptions)260; ((TMP_Text)Plugin.FeedText).horizontalAlignment = (HorizontalAlignmentOptions)4; ((TMP_Text)Plugin.FeedText).verticalAlignment = (VerticalAlignmentOptions)256; ((Graphic)Plugin.FeedText).raycastTarget = false; ((TMP_Text)Plugin.FeedText).margin = Vector4.zero; ((TMP_Text)Plugin.FeedText).characterSpacing = 0f; ((TMP_Text)Plugin.FeedText).wordSpacing = 0f; ((TMP_Text)Plugin.FeedText).fontStyle = (FontStyles)0; ContentSizeFitter obj = Plugin.FeedInstance.AddComponent<ContentSizeFitter>(); obj.verticalFit = (FitMode)2; obj.horizontalFit = (FitMode)0; RectTransform component2 = Plugin.FeedInstance.GetComponent<RectTransform>(); component2.pivot = new Vector2(1f, 1f); component2.anchorMin = new Vector2(1f, 1f); component2.anchorMax = new Vector2(1f, 1f); component2.sizeDelta = new Vector2(520f, 220f); component2.anchoredPosition = new Vector2(-10f, -250f); } else if ((Object)(object)Plugin.FeedText != (Object)null && (Object)(object)((TMP_Text)Plugin.FeedText).transform.parent != (Object)(object)cachedGameHud.transform) { ((TMP_Text)Plugin.FeedText).transform.SetParent(cachedGameHud.transform, false); } return true; } internal static void UpdateKillFeedHud() { //IL_00e1: Unknown result type (might be due to invalid IL or missing references) Plugin.Entries.RemoveAll((KillFeedEntry e) => Time.time > e.ExpiresAt); if (Plugin.Entries.Count == 0) { Plugin.FeedInstance.SetActive(false); Plugin.LastRenderedFeedText = string.Empty; Plugin.LastRenderedFontSize = -1f; Plugin.HudShowStartedAt = -100000f; return; } bool activeSelf = Plugin.FeedInstance.activeSelf; Plugin.FeedInstance.SetActive(true); if (!activeSelf) { Plugin.HudShowStartedAt = Time.time; } RectTransform component = Plugin.FeedInstance.GetComponent<RectTransform>(); float num = Mathf.Clamp01(Mathf.Max(0f, Time.time - Plugin.HudShowStartedAt) / 0.18f); float num2 = 1f - Mathf.Pow(1f - num, 3f); float num3 = Mathf.Lerp(24f, 0f, num2); component.anchoredPosition = new Vector2(-10f + num3, -250f); int num4 = Mathf.Clamp((Plugin.Entries.Max((KillFeedEntry e) => e.Text.Length) - 1) / 46, 0, 3); float num5 = Mathf.Clamp(Plugin.TextScaleConfig?.Value ?? 1f, 0.7f, 1f); float num6 = Plugin.MobHudSizes[num4] * num5; if (!Mathf.Approximately(Plugin.LastRenderedFontSize, num6)) { ((TMP_Text)Plugin.FeedText).fontSize = num6; ((TMP_Text)Plugin.FeedText).lineSpacing = (0f - num6) * 1.05f; Plugin.LastRenderedFontSize = num6; } StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < Plugin.Entries.Count; i++) { KillFeedEntry killFeedEntry = Plugin.Entries[i]; if (i > 0) { stringBuilder.Append('\n'); } float num7 = Mathf.Max(0f, Time.time - killFeedEntry.CreatedAt); float num8 = Mathf.Max(0f, killFeedEntry.ExpiresAt - Time.time); float num9 = Mathf.Clamp01(num7 / 0.16f); float num10 = Mathf.Clamp01(num8 / 0.24f); string value = Mathf.Clamp(Mathf.RoundToInt(Mathf.Min(num9, num10) * 255f), 0, 255).ToString("X2"); stringBuilder.Append("<color=#").Append(killFeedEntry.Color).Append(value) .Append('>') .Append(killFeedEntry.Text) .Append("</color>"); } string text = stringBuilder.ToString(); if (text != Plugin.LastRenderedFeedText) { ((TMP_Text)Plugin.FeedText).text = text; Plugin.LastRenderedFeedText = text; } } internal static void SetInactive() { if ((Object)(object)Plugin.FeedInstance != (Object)null) { Plugin.FeedInstance.SetActive(false); } } internal static void ClearHudCache() { cachedGameHud = null; cachedTaxHaul = null; } } internal sealed class KillFeedEntry { public string Text; public string Color; public float ExpiresAt; public float CreatedAt; } internal sealed class ObservedPlayerState { public PlayerAvatar Avatar; public string Name; public bool WasDead; public float LastDeathAt; } internal sealed class PendingDisconnectState { public string Name; public float MissingSince; public bool HadRecentDeath; } internal sealed class ObservedEnemyState { public EnemyParent EnemyParent; public string Name; public int LastHealth; public float FirstSeenAt; public UnityAction DeathListener; } internal static class PlayerFeedTracker { [HarmonyPatch(typeof(PlayerAvatar), "PlayerDeathRPC")] internal static class PlayerAvatarDeathPatch { [HarmonyPostfix] private static void Postfix(PlayerAvatar __instance) { //IL_0058: Unknown result type (might be due to invalid IL or missing references) string text = ResolvePlayerName(__instance); string item = SanitizeDisplayName(text); if (!Plugin.PlayersWithDisconnectFeed.Contains(item) && Plugin.ShowPlayerDeathsConfig.Value && Time.time - Plugin.LevelStartTime >= 1f) { Plugin.AddEntry(text + " died", new Color(0.7f, 0.23f, 0.23f, 1f)); } int num = (((Object)(object)__instance != (Object)null) ? ((Object)__instance).GetInstanceID() : 0); if (num != 0 && Plugin.PlayerStates.TryGetValue(num, out var value)) { value.WasDead = true; value.LastDeathAt = Time.time; } } } private static readonly HashSet<int> seenPlayerIds = new HashSet<int>(); internal static string ResolvePlayerName(PlayerAvatar avatar) { if ((Object)(object)avatar == (Object)null) { return "Player"; } try { string raw = ReflectionCache.PlayerNameField?.GetValue(avatar) as string; raw = SanitizeDisplayName(raw); if (!string.IsNullOrWhiteSpace(raw)) { return raw; } } catch { } return SanitizeDisplayName((!string.IsNullOrWhiteSpace(((Object)avatar).name)) ? ((Object)avatar).name : "Player"); } internal static string SanitizeDisplayName(string raw) { if (string.IsNullOrEmpty(raw)) { return raw; } StringBuilder stringBuilder = new StringBuilder(raw.Length); foreach (char c in raw) { if (!char.IsControl(c) && CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.Format) { stringBuilder.Append(c); } } return stringBuilder.ToString().Trim(); } internal static bool IsPlayerDead(PlayerAvatar avatar) { if ((Object)(object)avatar == (Object)null) { return false; } try { object obj = ReflectionCache.PlayerDeadSetField?.GetValue(avatar); if (obj is bool) { return (bool)obj; } } catch { } return false; } internal static void ObservePlayers() { //IL_02f2: Unknown result type (might be due to invalid IL or missing references) seenPlayerIds.Clear(); try { List<PlayerAvatar> list = SemiFunc.PlayerGetAll(); if (list == null) { return; } foreach (PlayerAvatar item in list) { if (!((Object)(object)item == (Object)null)) { int instanceID = ((Object)item).GetInstanceID(); seenPlayerIds.Add(instanceID); string text = ResolvePlayerName(item); bool wasDead = IsPlayerDead(item); if (!Plugin.PlayerStates.TryGetValue(instanceID, out var value)) { value = new ObservedPlayerState { Avatar = item, Name = text, WasDead = wasDead }; Plugin.PlayerStates[instanceID] = value; } else { value.Avatar = item; value.Name = text; } Plugin.PlayerStatesByName[text] = value; Plugin.PendingDisconnects.Remove(text); } } } catch (Exception arg) { Debug.LogError((object)$"[DenisUIKillFeed] ObservePlayers error: {arg}"); } foreach (int item2 in Plugin.PlayerStates.Keys.Where((int id) => !seenPlayerIds.Contains(id)).ToList()) { ObservedPlayerState observedPlayerState = Plugin.PlayerStates[item2]; if (observedPlayerState != null && !string.IsNullOrWhiteSpace(observedPlayerState.Name) && !Plugin.SceneResetInProgress) { bool hadRecentDeath = observedPlayerState.LastDeathAt > 0f && Time.time - observedPlayerState.LastDeathAt <= 2.5f; string text2 = SanitizeDisplayName(observedPlayerState.Name); if (!string.IsNullOrWhiteSpace(text2) && !Plugin.PendingDisconnects.ContainsKey(text2)) { Plugin.PendingDisconnects[text2] = new PendingDisconnectState { Name = text2, MissingSince = Time.time, HadRecentDeath = hadRecentDeath }; } } if (observedPlayerState != null && !string.IsNullOrWhiteSpace(observedPlayerState.Name)) { Plugin.PlayerStatesByName.Remove(observedPlayerState.Name); } Plugin.PlayerStates.Remove(item2); } foreach (string item3 in Plugin.PendingDisconnects.Keys.ToList()) { if (Plugin.PlayerStatesByName.ContainsKey(item3)) { Plugin.PendingDisconnects.Remove(item3); } else { if (Time.time - Plugin.LevelStartTime < 4f) { continue; } PendingDisconnectState pendingDisconnectState = Plugin.PendingDisconnects[item3]; if (Time.time - pendingDisconnectState.MissingSince < 1.25f) { continue; } if (Plugin.RecentlyDisconnectedPlayers.Add(pendingDisconnectState.Name)) { if (Plugin.ShowDisconnectsConfig.Value) { Plugin.AddEntry(pendingDisconnectState.Name + " disconnected", new Color(0.58f, 0.58f, 0.62f, 1f)); } Plugin.PlayersWithDisconnectFeed.Add(pendingDisconnectState.Name); } Plugin.PendingDisconnects.Remove(item3); } } if (Plugin.SceneResetInProgress && seenPlayerIds.Count > 0) { Plugin.SceneResetInProgress = false; } } } [BepInPlugin("denis.repo.denisuikillfeed", "Denis UI Killfeed", "1.1.2")] public class Plugin : BaseUnityPlugin { internal static GameObject FeedInstance; internal static TextMeshProUGUI FeedText; internal static readonly List<KillFeedEntry> Entries = new List<KillFeedEntry>(); internal static readonly Dictionary<int, ObservedPlayerState> PlayerStates = new Dictionary<int, ObservedPlayerState>(); internal static readonly Dictionary<string, ObservedPlayerState> PlayerStatesByName = new Dictionary<string, ObservedPlayerState>(StringComparer.OrdinalIgnoreCase); internal static readonly HashSet<string> RecentlyDisconnectedPlayers = new HashSet<string>(StringComparer.OrdinalIgnoreCase); internal static readonly HashSet<string> PlayersWithDisconnectFeed = new HashSet<string>(StringComparer.OrdinalIgnoreCase); internal static readonly Dictionary<string, PendingDisconnectState> PendingDisconnects = new Dictionary<string, PendingDisconnectState>(StringComparer.OrdinalIgnoreCase); internal static readonly Dictionary<int, ObservedEnemyState> EnemyStates = new Dictionary<int, ObservedEnemyState>(); internal static readonly float[] MobHudSizes = new float[4] { 24f, 18f, 14f, 11f }; internal static bool SceneResetInProgress; internal static float LastLevelActiveAt; internal static float LevelStartTime; internal static float HudShowStartedAt = -100000f; internal static float LastEnemyObserveAt = -100000f; internal static string LastRenderedFeedText = string.Empty; internal static float LastRenderedFontSize = -1f; internal static ConfigEntry<bool> ShowEnemyKillsConfig; internal static ConfigEntry<bool> ShowPlayerDeathsConfig; internal static ConfigEntry<bool> ShowDisconnectsConfig; internal static ConfigEntry<float> TextScaleConfig; internal static ConfigEntry<bool> HostSyncNamesConfig; internal static Dictionary<string, ConfigEntry<string>> MonsterNameConfigs = new Dictionary<string, ConfigEntry<string>>(); private static Dictionary<string, string> cachedCustomNames; private static int lastConfigCheckFrame = -1; private static bool hasPublishedNamesThisLevel = false; private static bool hasPublishedEnemyPoolThisLevel = false; internal static Plugin Instance { get; private set; } internal Harmony Harmony { get; private set; } private void Awake() { //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Expected O, but got Unknown //IL_0119: Unknown result type (might be due to invalid IL or missing references) //IL_0123: Expected O, but got Unknown //IL_014d: Unknown result type (might be due to invalid IL or missing references) //IL_0157: Expected O, but got Unknown Instance = this; ((Component)this).transform.parent = null; ((Object)((Component)this).gameObject).hideFlags = (HideFlags)61; TextScaleConfig = ((BaseUnityPlugin)this).Config.Bind<float>("General", "Text Scale", 1f, new ConfigDescription("Scales the killfeed text size.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.7f, 1f), Array.Empty<object>())); ShowEnemyKillsConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enemy deaths", true, "Show enemy death events in the killfeed."); ShowPlayerDeathsConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Player deaths", true, "Show player death events in the killfeed."); ShowDisconnectsConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Player disconnects", true, "Show player disconnect events in the killfeed."); HostSyncNamesConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("Custom Names", "Host Sync", true, "When enabled, use custom enemy names from the host during multiplayer. Hosts always broadcast their names; clients sync if this is ON."); foreach (string allDisplayName in EnemyNameDirectory.GetAllDisplayNames()) { MonsterNameConfigs[allDisplayName] = ((BaseUnityPlugin)this).Config.Bind<string>("Custom Names", allDisplayName, allDisplayName, new ConfigDescription("Customize how '" + allDisplayName + "' appears in killfeed.", (AcceptableValueBase)null, Array.Empty<object>())); } Harmony = new Harmony(((BaseUnityPlugin)this).Info.Metadata.GUID); Harmony.PatchAll(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Denis UI Killfeed loaded"); } private void Update() { EnemyFeedTracker.ObserveEnemiesFromDirector(); if (!hasPublishedEnemyPoolThisLevel && LevelStartTime > 0f && Time.time - LevelStartTime >= 0.2f) { PublishEnemyPool(); hasPublishedEnemyPoolThisLevel = true; } if (!hasPublishedNamesThisLevel && LevelStartTime > 0f && Time.time - LevelStartTime >= 0.5f) { HostSyncManager.PublishHostMonsterNames(GetCachedCustomMonsterNames()); hasPublishedNamesThisLevel = true; } HostSyncManager.UpdateHostMonsterNamesFromRoom(); } private static Dictionary<string, string> GetCachedCustomMonsterNames() { int frameCount = Time.frameCount; if (frameCount - lastConfigCheckFrame >= 30) { cachedCustomNames = ParseCustomMonsterNames(); lastConfigCheckFrame = frameCount; } return cachedCustomNames ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } private void OnDestroy() { Harmony harmony = Harmony; if (harmony != null) { harmony.UnpatchSelf(); } } internal static Dictionary<string, string> ParseCustomMonsterNames() { Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair<string, ConfigEntry<string>> monsterNameConfig in MonsterNameConfigs) { string key = ((ConfigEntryBase)monsterNameConfig.Value).Definition.Key; string value = monsterNameConfig.Value.Value; if (!string.IsNullOrWhiteSpace(key) && !string.IsNullOrWhiteSpace(value)) { dictionary[key] = value.Trim(); } } return dictionary; } internal static string NormalizeEnemyName(string rawName) { return EnemyNameDirectory.Normalize(rawName); } internal static string ApplyCustomMonsterName(string resolvedName) { if (string.IsNullOrWhiteSpace(resolvedName)) { return resolvedName; } string text = NormalizeEnemyName(resolvedName); if (GetCachedCustomMonsterNames().TryGetValue(text, out var value)) { return value; } return text; } internal static string ApplyHostSyncOrCustomName(string resolvedName) { if (string.IsNullOrWhiteSpace(resolvedName)) { return resolvedName; } string text = NormalizeEnemyName(resolvedName); if (HostSyncNamesConfig.Value && PhotonNetwork.InRoom && !PhotonNetwork.LocalPlayer.IsMasterClient && HostSyncManager.HostMonsterNames.Count > 0 && HostSyncManager.HostMonsterNames.TryGetValue(text, out var value)) { return value; } if (GetCachedCustomMonsterNames().TryGetValue(text, out var value2)) { return value2; } return text; } internal static void AddEntry(string text, Color color) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) string color2 = ColorUtility.ToHtmlStringRGB(color); Entries.Insert(0, new KillFeedEntry { Text = text, Color = color2, ExpiresAt = Time.time + 7f, CreatedAt = Time.time }); if (Entries.Count > 10) { Entries.RemoveRange(10, Entries.Count - 10); } } public static string GetCustomEnemyName(string rawEnemyName) { if (string.IsNullOrWhiteSpace(rawEnemyName)) { return "Unknown"; } string text = ApplyHostSyncOrCustomName(NormalizeEnemyName(rawEnemyName)); if (text == "Unknown") { Debug.LogWarning((object)("[Plugin] GetCustomEnemyName: '" + rawEnemyName + "' → Unknown (not in directory)")); } return text; } private static void PublishEnemyPool() { try { EnemyDirector instance = EnemyDirector.instance; if ((Object)(object)instance == (Object)null || instance.enemiesSpawned == null || instance.enemiesSpawned.Count == 0) { KillFeedAPI.RaiseEnemyPoolReady(new List<KillFeedAPI.EnemyPoolData>()); return; } List<KillFeedAPI.EnemyPoolData> list = new List<KillFeedAPI.EnemyPoolData>(); foreach (EnemyParent item2 in instance.enemiesSpawned) { if (!((Object)(object)item2 == (Object)null)) { try { EnemyFeedTracker.GetEnemyFromParent(item2); string enemyName = ApplyHostSyncOrCustomName(EnemyNameDirectory.Normalize(item2.enemyName ?? "Enemy")); KillFeedAPI.EnemyPoolData item = new KillFeedAPI.EnemyPoolData { EnemyName = enemyName, InternalName = item2.enemyName, ClassName = ((object)item2).GetType().Name, GameObject = ((Component)item2).gameObject, EnemyParent = item2, InstanceId = ((Object)item2).GetInstanceID() }; list.Add(item); } catch (Exception ex) { Debug.LogWarning((object)("[Plugin] Error processing enemy in PublishEnemyPool: " + ex.Message)); } } } KillFeedAPI.RaiseEnemyPoolReady(list); } catch (Exception ex2) { Debug.LogError((object)("[Plugin] Exception in PublishEnemyPool: " + ex2.Message + "\n" + ex2.StackTrace)); KillFeedAPI.RaiseEnemyPoolReady(new List<KillFeedAPI.EnemyPoolData>()); } } internal static void ResetObservedState() { PlayerStates.Clear(); PlayerStatesByName.Clear(); EnemyStates.Clear(); RecentlyDisconnectedPlayers.Clear(); PlayersWithDisconnectFeed.Clear(); PendingDisconnects.Clear(); SceneResetInProgress = true; LevelStartTime = Time.time; HudShowStartedAt = -100000f; LastEnemyObserveAt = -100000f; LastRenderedFeedText = string.Empty; LastRenderedFontSize = -1f; hasPublishedNamesThisLevel = false; hasPublishedEnemyPoolThisLevel = false; KillFeedHud.ClearHudCache(); } } internal static class ReflectionCache { private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; internal static readonly FieldInfo PlayerNameField = typeof(PlayerAvatar).GetField("playerName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static readonly FieldInfo PlayerDeadSetField = typeof(PlayerAvatar).GetField("deadSet", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static readonly FieldInfo EnemyParentEnemyField = typeof(EnemyParent).GetField("Enemy", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? typeof(EnemyParent).GetField("enemy", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static readonly FieldInfo EnemyParentSpawnedField = typeof(EnemyParent).GetField("Spawned", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? typeof(EnemyParent).GetField("spawned", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static readonly FieldInfo EnemyHasHealthField = typeof(Enemy).GetField("HasHealth", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? typeof(Enemy).GetField("hasHealth", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static readonly FieldInfo EnemyHealthRefField = typeof(Enemy).GetField("Health", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? typeof(Enemy).GetField("health", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static readonly FieldInfo EnemyHealthCurrentField = typeof(EnemyHealth).GetField("healthCurrent", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static readonly FieldInfo EnemyHealthDeadField = typeof(EnemyHealth).GetField("dead", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static readonly FieldInfo EnemyHealthOnDeathField = typeof(EnemyHealth).GetField("onDeath", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); } }