using System; using System.Collections.Generic; using System.Text; using Newtonsoft.Json; using Oxide.Core; using Oxide.Game.Rust.Cui; using UnityEngine; // ____ _ _ _ // / ___|(_) __ _(_) | ___ // \___ \| |/ _` | | |/ _ \ // ___) | | (_| | | | (_) | // |____/|_|\__, |_|_|\___/ // |___/ // ☽✧✵✧✵✧✵✧✵✧✵✧✵✧☾ // ✦ RUST PLUGINS ✦ // ✦ https://sigilo.dev ✦ // ☽✧✵✧✵✧✵✧✵✧✵✧✵✧☾ namespace Oxide.Plugins { [Info("KillFeed", "Sigilo", "1.3.0")] [Description("Displays kill notifications at the top right of the screen")] class KillFeed : RustPlugin { private Configuration _config; private HashSet _disabledPlayers = new HashSet(); private List _killEntries = new List(); private const string UIPanel = "KillFeed.Panel"; private Timer _cleanupTimer; private bool _uiNeedsRefresh; private float _lastRefreshTime; private const float RefreshCooldown = 0.5f; private StringBuilder _sb = new StringBuilder(256); private string _cachedUIJson; private bool _uiCacheValid; private class Configuration { [JsonProperty("Show Player Kills")] public bool ShowPlayerKills = true; [JsonProperty("Show Suicides")] public bool ShowSuicides = true; [JsonProperty("Show Environmental Deaths")] public bool ShowEnvironmentalDeaths = true; [JsonProperty("Show Players Killed by Animals")] public bool ShowPlayersKilledByAnimals = false; [JsonProperty("Show Players Killed by NPCs")] public bool ShowPlayersKilledByNPCs = false; [JsonProperty("Show Players Killing Animals")] public bool ShowPlayersKillingAnimals = false; [JsonProperty("Show Players Killing NPCs")] public bool ShowPlayersKillingNPCs = false; [JsonProperty("Display Duration (seconds)")] public float DisplayDuration = 10f; [JsonProperty("Max Visible Entries")] public int MaxEntries = 5; [JsonProperty("Font Size")] public int FontSize = 14; [JsonProperty("Colors")] public ColorConfig Colors = new ColorConfig(); [JsonProperty("Position")] public PositionConfig Position = new PositionConfig(); } private class ColorConfig { [JsonProperty("Killer Name")] public string KillerColor = "#6BA8E5"; [JsonProperty("Victim Name")] public string VictimColor = "#E56B6B"; [JsonProperty("Weapon Name")] public string WeaponColor = "#D4A340"; [JsonProperty("Distance")] public string DistanceColor = "#8A96A5"; [JsonProperty("Text")] public string TextColor = "#C8CED5"; [JsonProperty("Background")] public string BackgroundColor = "0 0 0 0"; } private class PositionConfig { [JsonProperty("Anchor Min")] public string AnchorMin = "0.60 0.85"; [JsonProperty("Anchor Max")] public string AnchorMax = "0.995 0.99"; } protected override void LoadConfig() { base.LoadConfig(); try { _config = Config.ReadObject(); if (_config == null) throw new JsonException(); } catch { LoadDefaultConfig(); } SaveConfig(); } protected override void LoadDefaultConfig() => _config = new Configuration(); protected override void SaveConfig() => Config.WriteObject(_config); private void OnServerInitialized() { _cleanupTimer = timer.Every(2f, CleanupAndRefresh); } private void Unload() { _cleanupTimer?.Destroy(); foreach (var player in BasePlayer.activePlayerList) CuiHelper.DestroyUi(player, UIPanel); } private void OnPlayerDeath(BasePlayer victim, HitInfo info) { if (victim == null || victim.IsNpc) return; ProcessPlayerDeath(victim, info); } private object OnEntityDeath(BaseCombatEntity entity, HitInfo info) { if (entity is BasePlayer) return null; if (!_config.ShowPlayersKillingAnimals && !_config.ShowPlayersKillingNPCs) return null; var attacker = info?.InitiatorPlayer; if (attacker == null || attacker.IsNpc) return null; var victimNpc = entity as BaseNpc; if (victimNpc == null) return null; bool victimIsAnimal = IsAnimal(victimNpc.ShortPrefabName); if (victimIsAnimal) { if (!_config.ShowPlayersKillingAnimals) return null; } else { if (!_config.ShowPlayersKillingNPCs) return null; } string victimName = GetNPCName(victimNpc.ShortPrefabName); string killerName = attacker.displayName; string weaponName = GetWeaponName(info); float distance = Vector3.Distance(attacker.transform.position, entity.transform.position); AddKillEntry(new KillEntry { KillerName = killerName, VictimName = victimName, WeaponName = weaponName, Distance = distance, Timestamp = Time.realtimeSinceStartup, IsSuicide = false, IsEnvironmental = false }); return null; } private void ProcessPlayerDeath(BasePlayer victim, HitInfo info) { var attacker = info?.InitiatorPlayer; var attackerNpc = info?.Initiator as BaseNpc; bool attackerIsRealPlayer = attacker != null && !attacker.IsNpc; bool attackerIsAnimal = attackerNpc != null && IsAnimal(attackerNpc.ShortPrefabName); bool isWildBees = IsWildBeeAttack(info); bool isBeeGrenade = IsBeeGrenadeAttack(info); var damageType = info?.damageTypes.GetMajorityDamageType() ?? Rust.DamageType.Generic; bool isSuicide = damageType == Rust.DamageType.Suicide; bool isEnvironmental = !isSuicide && IsEnvironmentalDamage(damageType) && (attacker == null || attacker == victim) && attackerNpc == null && !isWildBees; if (isSuicide) { if (!_config.ShowSuicides) return; } else if (isEnvironmental) { if (!_config.ShowEnvironmentalDeaths) return; } else if (isBeeGrenade) { if (!_config.ShowPlayerKills) return; } else if (isWildBees) { if (!_config.ShowPlayersKilledByAnimals) return; } else if (attackerIsRealPlayer && attacker != victim) { if (!_config.ShowPlayerKills) return; } else if (attackerIsAnimal) { if (!_config.ShowPlayersKilledByAnimals) return; } else if (attacker != null && attacker.IsNpc) { if (!_config.ShowPlayersKilledByNPCs) return; } else if (attackerNpc != null && !attackerIsAnimal) { if (!_config.ShowPlayersKilledByNPCs) return; } else if (!isSuicide && !isEnvironmental) { return; } string victimName = victim.displayName; if (string.IsNullOrEmpty(victimName)) return; bool noKillerNeeded = isSuicide || isEnvironmental; string killerName; string weaponName; if (noKillerNeeded) { killerName = null; weaponName = GetDeathReason(info, isSuicide); } else if (isBeeGrenade) { killerName = attacker.displayName; weaponName = "Bee Grenade"; } else if (isWildBees) { killerName = "Bees"; weaponName = "Stings"; } else { killerName = GetKillerName(info); weaponName = GetWeaponName(info); } if (!noKillerNeeded && string.IsNullOrEmpty(killerName)) return; float distance = 0f; if (!noKillerNeeded && info?.Initiator != null) distance = Vector3.Distance(info.Initiator.transform.position, victim.transform.position); var killEntry = new KillEntry { KillerName = killerName, VictimName = victimName, WeaponName = weaponName, Distance = distance, Timestamp = Time.realtimeSinceStartup, IsSuicide = isSuicide, IsEnvironmental = isEnvironmental }; AddKillEntry(killEntry); } private void AddKillEntry(KillEntry killEntry) { if (_killEntries.Count >= _config.MaxEntries) _killEntries.RemoveAt(0); _killEntries.Add(killEntry); _uiNeedsRefresh = true; _uiCacheValid = false; float now = Time.realtimeSinceStartup; if (now - _lastRefreshTime >= RefreshCooldown) { _lastRefreshTime = now; RefreshAllUI(); _uiNeedsRefresh = false; } } private bool IsEnvironmentalDamage(Rust.DamageType damageType) { switch (damageType) { case Rust.DamageType.Bleeding: case Rust.DamageType.Cold: case Rust.DamageType.Drowned: case Rust.DamageType.Fall: case Rust.DamageType.Heat: case Rust.DamageType.Hunger: case Rust.DamageType.Poison: case Rust.DamageType.Radiation: case Rust.DamageType.Thirst: return true; default: return false; } } private bool IsWildBeeAttack(HitInfo info) { if (info == null || info.Initiator == null) return false; if (info.InitiatorPlayer != null && !info.InitiatorPlayer.IsNpc) return false; string prefabName = info.Initiator.ShortPrefabName; return prefabName == "beeswarm" || prefabName == "bee_swarm"; } private bool IsBeeGrenadeAttack(HitInfo info) { if (info == null || info.WeaponPrefab == null) return false; if (info.InitiatorPlayer == null || info.InitiatorPlayer.IsNpc) return false; string weaponName = info.WeaponPrefab.ShortPrefabName; return weaponName == "grenade.bee" || weaponName == "grenade_bee"; } private void CleanupAndRefresh() { if (_killEntries.Count == 0) { if (_uiNeedsRefresh) { RefreshAllUI(); _uiNeedsRefresh = false; } return; } float currentTime = Time.realtimeSinceStartup; int removed = 0; for (int i = _killEntries.Count - 1; i >= 0; i--) { if (currentTime - _killEntries[i].Timestamp >= _config.DisplayDuration) { _killEntries.RemoveAt(i); removed++; } } if (removed > 0) _uiCacheValid = false; if (removed > 0 || _uiNeedsRefresh) { RefreshAllUI(); _uiNeedsRefresh = false; } } private void RefreshAllUI() { var players = BasePlayer.activePlayerList; int count = players.Count; if (!_uiCacheValid && _killEntries.Count > 0) { _cachedUIJson = BuildUIJson(); _uiCacheValid = true; } for (int i = 0; i < count; i++) { var player = players[i]; if (player == null || !player.IsConnected) continue; if (_disabledPlayers.Contains(player.userID)) continue; CuiHelper.DestroyUi(player, UIPanel); if (_killEntries.Count > 0 && _cachedUIJson != null) CuiHelper.AddUi(player, _cachedUIJson); } } private string BuildUIJson() { var container = new CuiElementContainer(); container.Add(new CuiPanel { Image = { Color = _config.Colors.BackgroundColor }, RectTransform = { AnchorMin = _config.Position.AnchorMin, AnchorMax = _config.Position.AnchorMax }, CursorEnabled = false }, "Hud.Menu", UIPanel); float yOffset = 0.95f; float entryHeight = 0.15f; int entryCount = _killEntries.Count; int maxEntries = _config.MaxEntries; for (int i = entryCount - 1; i >= 0 && (entryCount - 1 - i) < maxEntries; i--) { int index = entryCount - 1 - i; var entry = _killEntries[i]; _sb.Clear(); if (entry.IsSuicide || entry.IsEnvironmental) { _sb.Append("").Append(entry.VictimName).Append(" "); _sb.Append("").Append(entry.WeaponName).Append(""); } else { _sb.Append("").Append(entry.KillerName).Append(" "); _sb.Append("killed "); _sb.Append("").Append(entry.VictimName).Append(" "); _sb.Append("with "); _sb.Append("").Append(entry.WeaponName).Append(""); if (entry.Distance > 0) _sb.Append(" at ").Append(entry.Distance.ToString("F0")).Append("m"); } float yMin = yOffset - (index + 1) * entryHeight; float yMax = yOffset - index * entryHeight; container.Add(new CuiLabel { Text = { Text = _sb.ToString(), FontSize = _config.FontSize, Align = TextAnchor.UpperRight, Color = "1 1 1 1" }, RectTransform = { AnchorMin = $"0.01 {yMin}", AnchorMax = $"0.99 {yMax}" } }, UIPanel); } return container.ToJson(); } [ChatCommand("killfeed")] private void KillFeedCommand(BasePlayer player, string command, string[] args) { if (args.Length == 0) { SendReply(player, "KillFeed Commands:\n/killfeed toggle - Enable/disable kill feed\n/killfeed status - Check current status"); return; } switch (args[0].ToLower()) { case "toggle": if (_disabledPlayers.Contains(player.userID)) { _disabledPlayers.Remove(player.userID); SendReply(player, "KillFeed enabled!"); if (_killEntries.Count > 0) { if (!_uiCacheValid) { _cachedUIJson = BuildUIJson(); _uiCacheValid = true; } if (_cachedUIJson != null) CuiHelper.AddUi(player, _cachedUIJson); } } else { _disabledPlayers.Add(player.userID); SendReply(player, "KillFeed disabled!"); CuiHelper.DestroyUi(player, UIPanel); } break; case "status": SendReply(player, $"KillFeed is currently {(_disabledPlayers.Contains(player.userID) ? "disabled" : "enabled")}"); break; default: SendReply(player, "Unknown command. Use /killfeed for help."); break; } } private class KillEntry { public string KillerName; public string VictimName; public string WeaponName; public float Distance; public float Timestamp; public bool IsSuicide; public bool IsEnvironmental; } private string GetKillerName(HitInfo info) { if (info.InitiatorPlayer != null) { if (!info.InitiatorPlayer.IsNpc) return info.InitiatorPlayer.displayName; string name = info.InitiatorPlayer.displayName; if (name.IndexOf("scientist", StringComparison.OrdinalIgnoreCase) >= 0) return "Scientist"; if (name.IndexOf("tunnel", StringComparison.OrdinalIgnoreCase) >= 0) return "Tunnel Dweller"; if (name.IndexOf("bandit", StringComparison.OrdinalIgnoreCase) >= 0) return "Bandit Guard"; return name; } if (info.Initiator is BaseNpc npc) return GetNPCName(npc.ShortPrefabName); return info.Initiator != null ? info.Initiator.ShortPrefabName : "Unknown"; } private string GetVictimName(BaseCombatEntity entity, BasePlayer player) { if (player != null) { if (!player.IsNpc) return player.displayName; string name = player.displayName; if (name.IndexOf("scientist", StringComparison.OrdinalIgnoreCase) >= 0) return "Scientist"; if (name.IndexOf("tunnel", StringComparison.OrdinalIgnoreCase) >= 0) return "Tunnel Dweller"; if (name.IndexOf("bandit", StringComparison.OrdinalIgnoreCase) >= 0) return "Bandit Guard"; return name; } if (entity is BaseNpc npc) return GetNPCName(npc.ShortPrefabName); return entity.ShortPrefabName; } private string GetNPCName(string shortName) { switch (shortName) { case "bear": return "Bear"; case "polarbear": return "Polar Bear"; case "boar": return "Boar"; case "chicken": return "Chicken"; case "horse": return "Horse"; case "stag": return "Stag"; case "wolf": return "Wolf"; case "tiger": return "Tiger"; case "panther": return "Panther"; case "crocodile": return "Crocodile"; case "snake": return "Snake"; case "simpleshark": return "Shark"; case "zombie": return "Zombie"; case "beeswarm": return "Bees"; case "bee_swarm": return "Bees"; default: return shortName; } } private bool IsAnimal(string shortName) { switch (shortName) { case "bear": case "polarbear": case "boar": case "chicken": case "horse": case "stag": case "wolf": case "tiger": case "panther": case "crocodile": case "snake": case "simpleshark": case "beeswarm": case "bee_swarm": return true; default: return false; } } private bool IsAnimalEntity(BaseCombatEntity entity) { if (entity is BaseNpc npc) return IsAnimal(npc.ShortPrefabName); return false; } private string GetWeaponName(HitInfo info) { if (info.Weapon != null) { var item = info.Weapon.GetItem(); if (item != null) return item.info.displayName.english; } if (info.WeaponPrefab != null) { string shortName = info.WeaponPrefab.ShortPrefabName; return shortName.Replace(".entity", "").Replace("_", " ").Replace(".", " "); } return GetDamageTypeName(info.damageTypes.GetMajorityDamageType()); } private string GetDeathReason(HitInfo info, bool isSuicide) { if (isSuicide) return "Suicided"; var damageType = info.damageTypes.GetMajorityDamageType(); switch (damageType) { case Rust.DamageType.Bleeding: return "Bled out"; case Rust.DamageType.Cold: return "Froze to death"; case Rust.DamageType.Drowned: return "Drowned"; case Rust.DamageType.Fall: return "Fell to their death"; case Rust.DamageType.Heat: return "Burned to death"; case Rust.DamageType.Hunger: return "Starved to death"; case Rust.DamageType.Poison: return "Was poisoned"; case Rust.DamageType.Radiation: return "Died from radiation"; case Rust.DamageType.Thirst: return "Died of thirst"; default: return "Died"; } } private string GetDamageTypeName(Rust.DamageType damageType) { switch (damageType) { case Rust.DamageType.Bite: return "Bite"; case Rust.DamageType.Bleeding: return "Bleeding"; case Rust.DamageType.Blunt: return "Blunt"; case Rust.DamageType.Bullet: return "Bullet"; case Rust.DamageType.Cold: return "Cold"; case Rust.DamageType.Drowned: return "Drowning"; case Rust.DamageType.ElectricShock: return "Electric Shock"; case Rust.DamageType.Explosion: return "Explosion"; case Rust.DamageType.Fall: return "Fall"; case Rust.DamageType.Heat: return "Heat"; case Rust.DamageType.Hunger: return "Hunger"; case Rust.DamageType.Poison: return "Poison"; case Rust.DamageType.Radiation: return "Radiation"; case Rust.DamageType.Slash: return "Slash"; case Rust.DamageType.Stab: return "Stab"; case Rust.DamageType.Suicide: return "Suicide"; case Rust.DamageType.Thirst: return "Thirst"; default: return "Unknown"; } } } }