C#:
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<ulong> _disabledPlayers = new HashSet<ulong>();
private List<KillEntry> _killEntries = new List<KillEntry>();
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<Configuration>();
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("<color=").Append(_config.Colors.VictimColor).Append(">").Append(entry.VictimName).Append("</color> ");
_sb.Append("<color=").Append(_config.Colors.WeaponColor).Append(">").Append(entry.WeaponName).Append("</color>");
}
else
{
_sb.Append("<color=").Append(_config.Colors.KillerColor).Append(">").Append(entry.KillerName).Append("</color> ");
_sb.Append("<color=").Append(_config.Colors.TextColor).Append(">killed</color> ");
_sb.Append("<color=").Append(_config.Colors.VictimColor).Append(">").Append(entry.VictimName).Append("</color> ");
_sb.Append("<color=").Append(_config.Colors.TextColor).Append(">with</color> ");
_sb.Append("<color=").Append(_config.Colors.WeaponColor).Append(">").Append(entry.WeaponName).Append("</color>");
if (entry.Distance > 0)
_sb.Append(" at <color=").Append(_config.Colors.DistanceColor).Append(">").Append(entry.Distance.ToString("F0")).Append("m</color>");
}
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";
}
}
}
}