using 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 OnEnemyDeath { get; } = new UnityEvent(); public static event Action 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 list = new List(); foreach (KeyValuePair 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 NameMapping = new Dictionary(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 GetAllDisplayNames() { HashSet hashSet = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (string value in NameMapping.Values) { hashSet.Add(value); } List list = new List(hashSet); list.Sort(); return list; } } internal static class HostSyncManager { [Serializable] internal sealed class SerializedMonsterNames { [SerializeField] public SerializedNamePair[] names = Array.Empty(); } [Serializable] internal sealed class SerializedNamePair { [SerializeField] public string Key; [SerializeField] public string Value; } internal static readonly Dictionary HostMonsterNames = new Dictionary(StringComparer.OrdinalIgnoreCase); private const string HostMonsterNamesPropertyKey = "denis.killfeed.hostnames.v1"; private static string lastKnownHostNamesJson = string.Empty; internal static void PublishHostMonsterNames(Dictionary 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 kvp) => new SerializedNamePair { Key = kvp.Key, Value = kvp.Value }).ToArray() }); Hashtable val = new Hashtable(); ((Dictionary)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)((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(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> OnEnemyPoolReady; internal static void RaiseEnemyPoolReady(List enemies) { try { if (enemies == null) { enemies = new List(); } 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_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(); ((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(); obj.verticalFit = (FitMode)2; obj.horizontalFit = (FitMode)0; RectTransform component2 = Plugin.FeedInstance.GetComponent(); 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(); 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("') .Append(killFeedEntry.Text) .Append(""); } 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 seenPlayerIds = new HashSet(); 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 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 Entries = new List(); internal static readonly Dictionary PlayerStates = new Dictionary(); internal static readonly Dictionary PlayerStatesByName = new Dictionary(StringComparer.OrdinalIgnoreCase); internal static readonly HashSet RecentlyDisconnectedPlayers = new HashSet(StringComparer.OrdinalIgnoreCase); internal static readonly HashSet PlayersWithDisconnectFeed = new HashSet(StringComparer.OrdinalIgnoreCase); internal static readonly Dictionary PendingDisconnects = new Dictionary(StringComparer.OrdinalIgnoreCase); internal static readonly Dictionary EnemyStates = new Dictionary(); 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 ShowEnemyKillsConfig; internal static ConfigEntry ShowPlayerDeathsConfig; internal static ConfigEntry ShowDisconnectsConfig; internal static ConfigEntry TextScaleConfig; internal static ConfigEntry HostSyncNamesConfig; internal static Dictionary> MonsterNameConfigs = new Dictionary>(); private static Dictionary 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("General", "Text Scale", 1f, new ConfigDescription("Scales the killfeed text size.", (AcceptableValueBase)(object)new AcceptableValueRange(0.7f, 1f), Array.Empty())); ShowEnemyKillsConfig = ((BaseUnityPlugin)this).Config.Bind("General", "Enemy deaths", true, "Show enemy death events in the killfeed."); ShowPlayerDeathsConfig = ((BaseUnityPlugin)this).Config.Bind("General", "Player deaths", true, "Show player death events in the killfeed."); ShowDisconnectsConfig = ((BaseUnityPlugin)this).Config.Bind("General", "Player disconnects", true, "Show player disconnect events in the killfeed."); HostSyncNamesConfig = ((BaseUnityPlugin)this).Config.Bind("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("Custom Names", allDisplayName, allDisplayName, new ConfigDescription("Customize how '" + allDisplayName + "' appears in killfeed.", (AcceptableValueBase)null, Array.Empty())); } 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 GetCachedCustomMonsterNames() { int frameCount = Time.frameCount; if (frameCount - lastConfigCheckFrame >= 30) { cachedCustomNames = ParseCustomMonsterNames(); lastConfigCheckFrame = frameCount; } return cachedCustomNames ?? new Dictionary(StringComparer.OrdinalIgnoreCase); } private void OnDestroy() { Harmony harmony = Harmony; if (harmony != null) { harmony.UnpatchSelf(); } } internal static Dictionary ParseCustomMonsterNames() { Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair> 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()); return; } List list = new List(); 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()); } } 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); } }