C#:
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using Rust;
using Rust.Ai.Gen2;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Death Notes", "Terceran/Mr. Blue", "6.5.5")]
[Description("Broadcasts detailed death information")]
class DeathNotes : RustPlugin
{
[PluginReference]
private Plugin UINotify;
[PluginReference]
private Plugin Notify;
#region Fields
private const string WildcardCharacter = "*";
private const string CanSeePermission = "deathnotes.cansee";
private const string CantSeePermission = "deathnotes.cantsee";
private const string SuppressPermission = "deathnotes.suppress";
private const string TeamOnlyPermission = "deathnotes.seeteamonly";
private static DeathNotes _instance;
private PluginConfiguration _configuration;
private readonly Dictionary<string, string> _enemyPrefabs = new()
{
["spikes.floor"] = "Wooden Floor Spike Cluster",
["spikes_static"] = "Wooden Floor Spike Cluster",
["barricade.woodwire"] = "Barbed Wooden Barricade",
["barricade.metal"] = "Metal Barricade",
["wall.external.high.wood"] = "High External Wooden Wall",
["wall.external.high.stone"] = "High External Stone Wall",
["gates.external.high.stone"] = "High External Stone Gate",
["campfire"] = "Campfire",
["skull_fire_pit"] = "Skull Fire Pit",
["heavyscientist"] = "Heavy Scientist"
};
private readonly Dictionary<string, string> _weaponPrefabs = new()
{
["rocket_basic"] = "Rocket",
["rocket_hv"] = "High Velocity Rocket",
["rocket_fire"] = "Incendiary Rocket",
["grenade.f1.deployed"] = "F1 Grenade",
["grenade.beancan.deployed"] = "Beancan Grenade",
["survey_charge.deployed"] = "Survey Charge",
["explosive.satchel.deployed"] = "Satchel Charge",
["explosive.timed.deployed"] = "Timed Explosive Charge",
["rock.entity"] = "Rock",
["longsword.entity"] = "Longsword",
["mace.entity"] = "Mace",
["spear_stone.entity"] = "Stone Spear",
["spear_wooden.entity"] = "Wooden Spear",
["machete.weapon"] = "Machete",
["knife_bone.entity"] = "Bone Knife",
["bone_club.entity"] = "Bone Club",
["salvaged_cleaver.entity"] = "Salvaged Cleaver",
["salvaged_sword.entity"] = "Salvaged Sword",
["candy_cane.entity"] = "Candy Cane Club",
["flamethrower.entity"] = "Flame Thrower",
["snowball.entity"] = "Snowball",
["combat.knife.entity"] = "Combat Knife"
};
private readonly Dictionary<string, CombatEntityType> _combatEntityTypes = new()
{
["GunTrap"] = CombatEntityType.Trap,
["FlameTurret"] = CombatEntityType.Turret,
["AutoTurret"] = CombatEntityType.Turret,
["BaseHelicopter"] = CombatEntityType.Helicopter,
["BradleyAPC"] = CombatEntityType.Bradley,
["BasePlayer"] = CombatEntityType.Player,
["NPCMurderer"] = CombatEntityType.Murderer,
["CodeLock"] = CombatEntityType.Lock,
["Scientist"] = CombatEntityType.Scientist,
["ScientistNPC"] = CombatEntityType.Scientist,
["HTNPlayer"] = CombatEntityType.Scientist,
["NPCAutoTurret"] = CombatEntityType.Sentry,
["FireBall"] = CombatEntityType.Fire,
["scarecrow"] = CombatEntityType.ScarecrowNPC,
["ScientistNPCNew"] = CombatEntityType.Scientist,
["tunneldweller"] = CombatEntityType.TunnelDweller,
["underwaterdweller"] = CombatEntityType.UnderwaterDweller
};
private readonly Regex _colorTagRegex =
new Regex(@"<color=.{0,7}>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private readonly Regex _sizeTagRegex =
new Regex(@"<size=\d*>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private readonly List<string> _richTextLiterals = new List<string>
{
"</color>", "</size>", "<b>", "</b>", "<i>", "</i>"
};
private readonly Dictionary<ulong, AttackInfo> _previousAttack = new Dictionary<ulong, AttackInfo>();
private Dictionary<ulong, HitInfo> _patrolHeliTagTracker = new Dictionary<ulong, HitInfo>();
private HashSet<ulong> _bradleyApcTagTracker = new HashSet<ulong>();
private void LogOutput(string text)
{
//Puts(text);
string logText = DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss") + " : " + text;
LogToFile("DeathNotes-log", logText, this);
}
private readonly Func<PluginConfiguration.DeathMessage, DeathData, bool>[] _messageMatchingStages =
{
(m, d) => m.Enabled == true && MatchesCombatEntityType(d.KillerEntityType, m.KillerType) &&
MatchesCombatEntityType(d.VictimEntityType, m.VictimType) &&
MatchesDamageType(d.DamageType, m.DamageType),
(m, d) => m.Enabled == true && MatchesCombatEntityType(d.KillerEntityType, m.KillerType) &&
MatchesCombatEntityType(d.VictimEntityType, m.VictimType) &&
m.DamageType == WildcardCharacter,
(m, d) => m.Enabled == true && MatchesCombatEntityType(d.KillerEntityType, m.KillerType) &&
m.VictimType == WildcardCharacter &&
MatchesDamageType(d.DamageType, m.DamageType),
(m, d) => m.Enabled == true && m.KillerType == WildcardCharacter &&
MatchesCombatEntityType(d.VictimEntityType, m.VictimType) &&
MatchesDamageType(d.DamageType, m.DamageType),
(m, d) => m.Enabled == true && MatchesCombatEntityType(d.KillerEntityType, m.KillerType) &&
m.VictimType == WildcardCharacter &&
m.DamageType == WildcardCharacter,
(m, d) => m.Enabled == true && m.KillerType == WildcardCharacter &&
MatchesCombatEntityType(d.VictimEntityType, m.VictimType) &&
m.DamageType == WildcardCharacter,
(m, d) => m.Enabled == true && m.KillerType == WildcardCharacter &&
m.VictimType == WildcardCharacter &&
MatchesDamageType(d.DamageType, m.DamageType),
(m, d) => m.Enabled == true && m.KillerType == WildcardCharacter &&
m.VictimType == WildcardCharacter &&
m.DamageType == WildcardCharacter
};
#endregion
#region Hooks
private void Init()
{
_instance = this;
permission.RegisterPermission(CanSeePermission, this);
permission.RegisterPermission(CantSeePermission, this);
permission.RegisterPermission(SuppressPermission, this);
permission.RegisterPermission(TeamOnlyPermission, this);
_configuration = Config.ReadObject<PluginConfiguration>();
_configuration.LoadDefaults();
Config.WriteObject(_configuration);
string tmpPerm;
foreach (string perm in _configuration.PatrolHeliDisplayPerms)
{
tmpPerm = perm.Trim().ToLower();
if (!perm.StartsWith("deathnotes."))
continue;
permission.RegisterPermission(perm, this);
}
foreach (string perm in _configuration.BradleyDisplayPerms)
{
tmpPerm = perm.Trim().ToLower();
if (!perm.StartsWith("deathnotes."))
continue;
permission.RegisterPermission(perm, this);
}
foreach (var msg in _configuration.Translations.Messages)
{
foreach (string perm in msg.DisplayPerms)
{
tmpPerm = perm.Trim().ToLower();
if (!perm.StartsWith("deathnotes."))
continue;
permission.RegisterPermission(perm, this);
}
}
LogOutput("Initializing DeathNotes plugin");
}
private void OnPluginLoaded(Plugin plugin)
{
if (plugin.Name == "Notify")
Notify = plugin;
else if (plugin.Name == "UINotify")
UINotify = plugin;
}
private void OnPluginUnloaded(Plugin plugin)
{
if (plugin.Name == "Notify")
Notify = null;
else if (plugin.Name == "UINotify")
UINotify = null;
}
private void Unload()
{
LogOutput("Unloading DeathNotes plugin");
_instance = null;
}
private void OnEntityTakeDamage(BasePlayer victimEntity, HitInfo hitInfo)
{
if (victimEntity == null || hitInfo == null)
return;
BaseEntity attacker = victimEntity.lastAttacker ?? hitInfo?.Initiator;
var userId = victimEntity.ToPlayer().userID;
AttackInfo currentAttack;
if (_previousAttack.TryGetValue(userId, out currentAttack))
{
if (attacker != null)
currentAttack.Attacker = attacker;
if (hitInfo != null)
currentAttack.HitInfo = hitInfo;
if (victimEntity.lastDamage != null)
currentAttack.DamageType = victimEntity.lastDamage;
}
else
{
_previousAttack[userId] = new AttackInfo
{
HitInfo = hitInfo,
Attacker = victimEntity.lastAttacker ?? hitInfo?.Initiator,
DamageType = victimEntity.lastDamage
};
}
}
//This method tracks Bradley APC damage and reports whoever tagged it.
private void OnEntityTakeDamage(BradleyAPC victimEntity, HitInfo hitInfo) => HandleEntityDamage(victimEntity, hitInfo);
//This method tracks Patrol Helicopter damage and reports whoever tagged it.
private void OnPatrolHelicopterTakeDamage(PatrolHelicopter victimEntity, HitInfo hitInfo) => HandleEntityDamage(victimEntity, hitInfo);
private void HandleEntityDamage( BaseCombatEntity victimEntity, HitInfo hitInfo )
{
if (victimEntity == null || hitInfo == null)
return;
bool isPatrolHeli = victimEntity is PatrolHelicopter;
HitInfo storedHitInfo;
ulong netID = victimEntity.net.ID.Value;
if (isPatrolHeli)
{
bool wasHeliIdFound = _patrolHeliTagTracker.TryGetValue(netID, out storedHitInfo);
if (!wasHeliIdFound)
{
storedHitInfo = new HitInfo();
_patrolHeliTagTracker.Add(netID, storedHitInfo);
}
if (hitInfo.WeaponPrefab != null)
storedHitInfo.WeaponPrefab = hitInfo.WeaponPrefab;
if (hitInfo.Weapon != null)
storedHitInfo.Weapon = hitInfo.Weapon;
if (wasHeliIdFound || (victimEntity.lastAttackedTime != float.NegativeInfinity))
return;
if (storedHitInfo.Initiator == null && hitInfo.Initiator != null)
storedHitInfo.Initiator = hitInfo.Initiator;
}
else //If it's a Bradley APC
{
if ((victimEntity.lastAttackedTime != float.NegativeInfinity) || (_bradleyApcTagTracker.Contains(netID)))
return;
_bradleyApcTagTracker.Add(netID);
storedHitInfo = hitInfo;
}
var attackerPlayer = hitInfo.Initiator?.ToPlayer();
if( attackerPlayer?.displayName == null )
{
return;
}
storedHitInfo.Initiator = hitInfo.Initiator;
if( (isPatrolHeli ? !ShowPatrolHeliTags() : !ShowBradleyTags()) || permission.UserHasPermission( attackerPlayer.UserIDString, SuppressPermission ) )
{
return;
}
var data = new DeathData
{
VictimEntity = victimEntity,
KillerEntity = victimEntity.lastAttacker ?? hitInfo.Initiator,
VictimEntityType = GetCombatEntityType(victimEntity),
KillerEntityType = GetCombatEntityType(victimEntity.lastAttacker),
DamageType = victimEntity.lastDamage,
HitInfo = storedHitInfo
};
if (data.KillerEntity != null)
data.KillerEntityType = CombatEntityType.Player;
string message = PopulateMessageVariables(isPatrolHeli ? _configuration.PatrolHeliTagMessage : _configuration.BradleyTagMessage, data);
object hookResult = false;
try
{
hookResult = Interface.Call("OnDeathNotice", data.ToDictionary(), message);
}
catch (NullReferenceException)
{
return;
}
if (hookResult?.Equals(false) ?? false)
return;
//Prints to console
if (isPatrolHeli ? _configuration.ShowPatrolHeliTagsInConsole : _configuration.ShowBradleyTagsInConsole)
Puts(StripRichText(message));
//Prints to chat, the Notify plugin, and/or the UINotify plugin
foreach (var player in BasePlayer.activePlayerList)
{
if ((_configuration.RequirePermission &&
!permission.UserHasPermission(player.UserIDString, CanSeePermission)) ||
permission.UserHasPermission(player.UserIDString, CantSeePermission) ||
(permission.UserHasPermission(player.UserIDString, TeamOnlyPermission) && !ArePlayersOnSameTeam(player, data)))
continue;
if (isPatrolHeli ? !PlayersPassesPermsCheck(player.UserIDString, _configuration.PatrolHeliDisplayPerms) : !PlayersPassesPermsCheck(player.UserIDString, _configuration.BradleyDisplayPerms))
continue;
if (_configuration.MessageRadius != -1 &&
player.Distance(data.VictimEntity) > _configuration.MessageRadius)
continue;
if (isPatrolHeli ? _configuration.ShowPatrolHeliTagsInChat : _configuration.ShowBradleyTagsInChat)
{
Player.Reply(
player,
_configuration.ChatFormat.Replace("{message}", message),
ulong.Parse(_configuration.ChatIcon)
);
}
if ((isPatrolHeli ? _configuration.ShowPatrolHeliTagsInNotify : _configuration.ShowBradleyTagsInNotify) && Notify != null)
Notify.Call("SendNotify", player, _configuration.NotifyMessageType, _configuration.ChatFormat.Replace("{message}", message));
if ((isPatrolHeli ? _configuration.ShowPatrolHeliTagsInUINotify : _configuration.ShowBradleyTagsInUINotify) && UINotify != null)
UINotify.Call("SendNotify", player, _configuration.UINotifyMessageType, _configuration.ChatFormat.Replace("{message}", message));
}
}
//This method tracks when Patrol Helicopters are killed or despawns naturally, and cleans them from the tag trackers.
private void OnEntityKill(PatrolHelicopter entity)
{
_patrolHeliTagTracker.Remove(entity.net.ID.Value);
}
//This method tracks when Bradley APCs are killed or despawns naturally, and cleans them from the tag trackers.
private void OnEntityKill(BradleyAPC entity)
{
_bradleyApcTagTracker.Remove(entity.net.ID.Value);
}
private void OnEntityDeath(BaseCombatEntity victimEntity, HitInfo hitInfo)
{
//There is no victim information for some reason.
// Try to avoid error when entity was destroyed
//Note: If someone is wounded and dies, there will be null hitInfo
if ((victimEntity == null) || (victimEntity.gameObject == null))
return;
//Snakes produce two death messages, one when it is killed and one when it is skinned.
//The below is to prevent a death note related to skinning.
if ((victimEntity is SnakeHazard) && (((SnakeHazard)victimEntity).IsCorpse))
return;
//Some corpses produce two death messages, one when it is killed and one when it is skinned.
//The below is to prevent a death note related to skinning.
if (victimEntity is BaseCorpse)
return;
if (hitInfo == null)
{
hitInfo = new HitInfo();
hitInfo.Initiator = null;
}
//Attempts to get the NetID. This was occasionally resulting in a NullReferenceException, so we're being careful.
ulong netID = 0;
try
{
netID = victimEntity.net.ID.Value;
}
catch (Exception e)
{
LogOutput($"Exception in OnEntityDeath while retrieving victimEntity.net: {e.ToString()}");
netID = 0;
}
if ((victimEntity is PatrolHelicopter || victimEntity is BradleyAPC) && netID == 0)
return;
DeathData data = new DeathData
{
VictimEntity = victimEntity,
KillerEntity = victimEntity.lastAttacker ?? hitInfo?.Initiator,
VictimEntityType = GetCombatEntityType(victimEntity),
KillerEntityType = GetCombatEntityType(victimEntity.lastAttacker),
DamageType = victimEntity.lastDamage,
HitInfo = hitInfo
};
if (victimEntity is PatrolHelicopter && _patrolHeliTagTracker.TryGetValue( netID, out var value ))
data.HitInfo = value;
// Handle inconsistencies/exceptions
HandleInconsistencies(ref data);
if (data.VictimEntityType == CombatEntityType.Player)
_previousAttack.Remove(data.VictimEntity.ToPlayer().userID);
if (_configuration.DebugMode)
{
LogOutput("[DEATHNOTES DEBUG]");
LogOutput(
$"\tKillerEntity: {data.KillerEntity?.GetType().Name ?? "NULL"} / {data.KillerEntity?.ShortPrefabName ?? "NULL"} / {data.KillerEntity?.PrefabName ?? "NULL"}");
if (data.KillerEntity != null)
LogOutput(
$"\tKiller's Owner: {covalence.Players.FindPlayerById(data.KillerEntity?.OwnerID.ToString())?.Name}");
LogOutput(
$"\tVictimEntity: {data.VictimEntity?.GetType().Name ?? "NULL"} / {data.VictimEntity?.ShortPrefabName ?? "NULL"} / {data.VictimEntity?.PrefabName ?? "NULL"}");
LogOutput($"\tInitiator: {data.HitInfo?.Initiator?.ShortPrefabName}");
LogOutput($"\tLastAttacker: {data.VictimEntity?.lastAttacker?.ShortPrefabName}");
LogOutput($"\tKillerEntityType: {data.KillerEntityType}");
LogOutput($"\tVictimEntityType: {data.VictimEntityType}");
LogOutput($"\tDamageType: {data.DamageType}");
LogOutput($"\tBodypart: {GetCustomizedBodypartName(data.HitInfo)}");
LogOutput($"\tWeapon: {hitInfo?.WeaponPrefab?.ShortPrefabName ?? "NULL"}");
if (data.HitInfo != null)
LogOutput($"\tHitPositionWorld: {GetPositionString(data.HitInfo.HitPositionWorld)}");
}
// Change entity type for dwellers
RepairEntityTypes(ref data);
// Need do this before we cancel out of the method, as we need to track all these entities dying. Even if it's not a player killing them.
if (victimEntity is PatrolHelicopter)
_patrolHeliTagTracker.Remove(netID);
else if (victimEntity is BradleyAPC)
_bradleyApcTagTracker.Remove(netID);
// Ignore deaths of other entities
if (data.KillerEntityType == CombatEntityType.Other || data.VictimEntityType == CombatEntityType.Other)
return;
PluginConfiguration.DeathMessage deathMessage = GetDeathMessage(data);
// Populate the variables in the message
string message = PopulateMessageVariables(
// Find the best matching death message for this death
GetDeathMessageString(deathMessage),
data
);
if ((message == null) || (deathMessage == null))
return;
object hookResult = false;
try
{
hookResult = Interface.Call("OnDeathNotice", data.ToDictionary(), message);
if (hookResult?.Equals(false) ?? false)
return;
}
catch (Exception e)
{
LogOutput($"Exception in OnEntityDeath while calling OnDeathNotice hook: {e.ToString()}");
return;
}
if (
(deathMessage.ShowKillsInChat == true || deathMessage.ShowKillsInConsole == true || deathMessage.ShowKillsInNotify == true || deathMessage.ShowKillsInUINotify == true) &&
(data.KillerEntityType != CombatEntityType.Player || !permission.UserHasPermission(data.KillerEntity?.ToPlayer()?.UserIDString, SuppressPermission)) &&
(data.VictimEntityType != CombatEntityType.Player || !permission.UserHasPermission(data.VictimEntity?.ToPlayer()?.UserIDString, SuppressPermission))
)
{
//Prints to console
if (deathMessage.ShowKillsInConsole == true)
Puts(StripRichText(message));
//Prints to chat, the Notify plugin, and/or the UINotify plugin
foreach (var player in BasePlayer.activePlayerList)
{
if ((_configuration.RequirePermission &&
!permission.UserHasPermission(player.UserIDString, CanSeePermission)) ||
permission.UserHasPermission(player.UserIDString, CantSeePermission) ||
(permission.UserHasPermission(player.UserIDString, TeamOnlyPermission) && !ArePlayersOnSameTeam(player, data)))
continue;
if (!PlayersPassesPermsCheck(player.UserIDString, deathMessage.DisplayPerms))
continue;
if (_configuration.MessageRadius != -1 &&
player.Distance(data.VictimEntity) > _configuration.MessageRadius)
continue;
if (deathMessage.ShowKillsInChat == true)
{
Player.Reply(
player,
_configuration.ChatFormat.Replace("{message}", message),
ulong.Parse(_configuration.ChatIcon)
);
}
if ((deathMessage.ShowKillsInNotify == true) && Notify != null)
Notify.Call("SendNotify", player, _configuration.NotifyMessageType, _configuration.ChatFormat.Replace("{message}", message));
if ((deathMessage.ShowKillsInUINotify == true) && UINotify != null)
UINotify.Call("SendNotify", player, _configuration.UINotifyMessageType, _configuration.ChatFormat.Replace("{message}", message));
}
}
}
private void RepairEntityTypes(ref DeathData data)
{
if (data.VictimEntity != null)
{
string victimPrefabName = data.VictimEntity.ShortPrefabName.ToLower();
if (victimPrefabName.Contains("corpse"))
{
data.VictimEntityType = CombatEntityType.Other;
}
else if (victimPrefabName.Contains("underwaterdweller"))
{
data.VictimEntityType = CombatEntityType.UnderwaterDweller;
}
else if (victimPrefabName.Contains("tunneldweller"))
{
data.VictimEntityType = CombatEntityType.TunnelDweller;
}
else if (victimPrefabName.Contains("bradleyapc"))
{
data.VictimEntityType = CombatEntityType.Bradley;
}
else if (victimPrefabName.Contains("shark"))
{
data.VictimEntityType = CombatEntityType.Shark;
}
}
if (data.KillerEntity != null)
{
string killerPrefabName = data.KillerEntity.ShortPrefabName.ToLower();
if (killerPrefabName.Contains("corpse"))
{
data.KillerEntityType = CombatEntityType.Other;
}
else if (killerPrefabName.Contains("underwaterdweller"))
{
data.KillerEntityType = CombatEntityType.UnderwaterDweller;
}
else if (killerPrefabName.StartsWith("excavator"))
{
data.KillerEntityType = CombatEntityType.ExcavatorArm;
}
else if (killerPrefabName.StartsWith("carshredder"))
{
data.KillerEntityType = CombatEntityType.CarShredder;
}
else if (killerPrefabName.Contains("tunneldweller"))
{
data.KillerEntityType = CombatEntityType.TunnelDweller;
}
else if (killerPrefabName.Contains("bradleyapc"))
{
data.KillerEntityType = CombatEntityType.Bradley;
}
else if (killerPrefabName.Contains("shark"))
{
data.KillerEntityType = CombatEntityType.Shark;
}
else if (killerPrefabName.Contains("cactus"))
{
data.KillerEntityType = CombatEntityType.Cactus;
}
else if (killerPrefabName.Contains("locomotive"))
{
data.KillerEntityType = CombatEntityType.Train;
}
else if (killerPrefabName.Contains("workcart"))
{
data.KillerEntityType = CombatEntityType.Train;
}
else if (killerPrefabName.Contains("train"))
{
data.KillerEntityType = CombatEntityType.Train;
}
}
}
private void OnFlameThrowerBurn(FlameThrower flameThrower, BaseEntity baseEntity)
{
if (flameThrower == null || baseEntity == null) return;
var flame = baseEntity.gameObject.AddComponent<Flame>();
flame.Source = Flame.FlameSource.Flamethrower;
flame.SourceEntity = flameThrower;
flame.Initiator = flameThrower.GetOwnerPlayer();
}
private void OnFlameExplosion(FlameExplosive explosive, BaseEntity baseEntity)
{
if (explosive == null || baseEntity == null) return;
var flame = baseEntity.gameObject.AddComponent<Flame>();
flame.Source = Flame.FlameSource.IncendiaryProjectile;
flame.SourceEntity = explosive;
flame.Initiator = explosive.creatorEntity;
}
private void OnFireBallSpread(FireBall fireBall, BaseEntity newFire)
{
if (fireBall == null) return;
var flame = fireBall.GetComponent<Flame>();
if (flame == null) return;
var newFlame = newFire.gameObject.AddComponent<Flame>();
newFlame.Source = flame.Source;
newFlame.SourceEntity = flame.SourceEntity;
newFlame.Initiator = flame.Initiator;
}
private void OnFireBallDamage(FireBall fireBall, BaseCombatEntity target, HitInfo hitInfo)
{
if (hitInfo.Initiator == null)
hitInfo.Initiator = fireBall;
}
#endregion
#region Death Messages
private PluginConfiguration.DeathMessage GetDeathMessage(DeathData data)
{
foreach (var matchingStage in _messageMatchingStages)
{
var match = _configuration.Translations.Messages.Find(m => matchingStage.Invoke(m, data));
if (match != null)
return match;
}
return null;
}
private string GetDeathMessageString(PluginConfiguration.DeathMessage deathMessage)
{
if (deathMessage != null)
return deathMessage.Messages.GetRandom((uint) DateTime.UtcNow.Millisecond);
else return null;
}
private string PopulateMessageVariables(string message, DeathData data)
{
if (string.IsNullOrEmpty(message))
return null;
var replacements = new Dictionary<string, string>
{
["victim"] = GetCustomizedEntityName(data.VictimEntity, data.VictimEntityType),
["position"] = GetPositionString(data.HitInfo.HitPositionWorld),
["mapgrid"] = MapHelper.GridToString(MapHelper.PositionToGrid(data.HitInfo.HitPositionWorld))
};
if (data.KillerEntityType != CombatEntityType.None)
{
replacements.Add("killer", GetCustomizedEntityName(data.KillerEntity, data.KillerEntityType));
replacements.Add("bodypart", GetCustomizedBodypartName(data.HitInfo));
if (data.KillerEntity != null)
{
var distance = data.KillerEntity.Distance(data.VictimEntity);
replacements.Add("distance", GetDistance(distance, _configuration.UseMetricDistance));
}
if ((data.KillerEntityType == CombatEntityType.Player) || (data.KillerEntityType == CombatEntityType.CustomNPC) || (data.KillerEntityType == CombatEntityType.Drone))
{
replacements.Add("hp", data.KillerEntity.Health().ToString("#0.#"));
replacements.Add("weapon", GetCustomizedWeaponName(data));
replacements.Add("attachments", string.Join(", ", GetCustomizedAttachmentNames(data.HitInfo)));
}
else if (data.KillerEntityType == CombatEntityType.Turret
|| data.KillerEntityType == CombatEntityType.Lock
|| data.KillerEntityType == CombatEntityType.Trap
|| data.KillerEntityType == CombatEntityType.SAM)
{
replacements.Add("owner",
covalence.Players.FindPlayerById(data.KillerEntity.OwnerID.ToString())?.Name ?? "Unknown Owner"
);
}
}
message = InsertPlaceholderValues(message, replacements);
replacements = null;
return message;
}
private struct DeathData
{
public CombatEntityType VictimEntityType { get; set; }
[JsonIgnore] public BaseCombatEntity VictimEntity { get; set; }
public CombatEntityType KillerEntityType { get; set; }
[JsonIgnore] public BaseEntity KillerEntity { get; set; }
public DamageType DamageType { get; set; }
[JsonIgnore] public HitInfo HitInfo { get; set; }
public Dictionary<string, object> ToDictionary() => new Dictionary<string, object>
{
["VictimEntityType"] = VictimEntityType,
["VictimEntity"] = VictimEntity,
["KillerEntityType"] = KillerEntityType,
["KillerEntity"] = KillerEntity,
["DamageType"] = DamageType,
["HitInfo"] = HitInfo
};
}
#endregion
#region Entity Identification
private CombatEntityType GetCombatEntityType(BaseEntity entity)
{
if (entity == null)
return CombatEntityType.None;
string entityTypeName = entity.GetType().Name;
if (entityTypeName != "ScientistNPC" && _combatEntityTypes.TryGetValue( entityTypeName, out var entityType ))
return entityType;
if (_combatEntityTypes.TryGetValue( entity.ShortPrefabName, out var type ))
return type;
//For those plugins that do not correctly type ZombieNPCs as Zombies.
if (entityTypeName.StartsWith("Zombie"))
return CombatEntityType.ZombieNPC;
//For a plugin that defined a CustomPet entity name, we redefine it as a CustonNPC.
if (entityTypeName.StartsWith("CustomPet"))
return CombatEntityType.CustomNPC;
switch (entity)
{
case ScientistNPC:
case ScientistNPC2:
{
if (entity.ShortPrefabName.EndsWith("heavy"))
return CombatEntityType.HeavyScientist;
return CombatEntityType.Scientist;
}
case Zombie:
return CombatEntityType.ZombieNPC;
case PatrolHelicopter:
return CombatEntityType.Helicopter;
case BaseAnimalNPC:
case Wolf2:
case Panther:
case Tiger:
case Crocodile:
case SnakeHazard:
case RidableHorse:
case FarmableAnimal:
return CombatEntityType.Animal;
case SamSite:
return CombatEntityType.SAM;
case BaseOven:
return CombatEntityType.HeatSource;
case SimpleBuildingBlock:
return CombatEntityType.ExternalWall;
case Barricade:
return CombatEntityType.Barricade;
case BaseTrap:
case IOEntity:
case GunTrap:
return CombatEntityType.Trap;
case GingerbreadNPC:
return CombatEntityType.GingerbreadNPC;
case ScarecrowNPC:
return CombatEntityType.ScarecrowNPC;
case Minicopter:
return CombatEntityType.Minicopter;
case ScrapTransportHelicopter:
return CombatEntityType.ScrapTransportHelicopter;
case AttackHelicopter:
return CombatEntityType.AttackHelicopter;
case BeeSwarmAI:
return CombatEntityType.BeeSwarm;
case BaseFishNPC:
if (entity.ShortPrefabName.ToLower().Contains("shark"))
return CombatEntityType.Shark;
else return CombatEntityType.Animal;
case SimpleShark:
return CombatEntityType.Shark;
case NPCShopKeeper:
return CombatEntityType.NPCShopKeeper;
case ExcavatorArm:
return CombatEntityType.ExcavatorArm;
case LargeShredder:
return CombatEntityType.CarShredder;
case Drone:
return CombatEntityType.Drone;
case ResourceEntity:
{
if (entity.ShortPrefabName.EndsWith("cactus"))
return CombatEntityType.Cactus;
else return CombatEntityType.Other;
}
case TrainEngine:
case TrainCar:
return CombatEntityType.Train;
case MagnetCrane:
return CombatEntityType.MagnetCrane;
case ElevatorLift:
return CombatEntityType.Elevator;
case HotAirBalloon:
return CombatEntityType.HotAirBalloon;
case BatteringRam:
return CombatEntityType.BatteringRam;
case Tugboat:
return CombatEntityType.Tugboat;
case CargoShip:
return CombatEntityType.CargoShip;
case DigitSendCodeLock:
return CombatEntityType.Lock;
default:
return CombatEntityType.Other;
}
}
private string GetCustomizedEntityName(BaseEntity entity, CombatEntityType combatEntityType)
{
var name = GetEntityName(entity, combatEntityType);
if (string.IsNullOrEmpty(name))
return null;
// Don't load player names into config
if (combatEntityType == CombatEntityType.Player)
return name;
if (!_configuration.Translations.Names.ContainsKey(name))
{
_configuration.Translations.Names.Add(name, name);
Config.WriteObject(_configuration);
}
return _configuration.Translations.Names[name];
}
private string GetEntityName(BaseEntity entity, CombatEntityType combatEntityType)
{
// Entity may be null for helicopter or bradley, see HandleExceptions(...)
if (entity == null &&
combatEntityType != CombatEntityType.Helicopter &&
combatEntityType != CombatEntityType.Bradley)
return null;
switch (combatEntityType)
{
case CombatEntityType.Player:
return StripRichText(entity.ToPlayer().displayName);
case CombatEntityType.Murderer:
case CombatEntityType.ScarecrowNPC:
case CombatEntityType.Scientist:
case CombatEntityType.HeavyScientist:
case CombatEntityType.ZombieNPC:
case CombatEntityType.GingerbreadNPC:
case CombatEntityType.NPCShopKeeper:
var name = entity.ToPlayer()?.displayName;
if ((combatEntityType == CombatEntityType.HeavyScientist) && (name == "Scientist"))
name = "Heavy Scientist";
if (!string.IsNullOrEmpty(name) && name != entity.ToPlayer()?.userID.ToString())
{
return name;
}
if (!_enemyPrefabs.ContainsKey(entity.ShortPrefabName))
{
return combatEntityType.ToString();
}
break;
case CombatEntityType.TunnelDweller:
return "Tunnel Dweller";
case CombatEntityType.UnderwaterDweller:
return "Underwater Dweller";
case CombatEntityType.Helicopter:
return "Patrol Helicopter";
case CombatEntityType.Bradley:
return "Bradley APC";
case CombatEntityType.Sentry:
return "Sentry";
case CombatEntityType.Fire:
return entity.creatorEntity?.ToPlayer()?.displayName ?? "Fire";
case CombatEntityType.Minicopter:
return "Minicopter";
//NOTE: Scrappy reporting can't work because a death results in two death notices. One is where the Scrappy is both the killer and the victim,
//and the other is where it shows the player as having committed suicide, even though they did not. There's no apparent way to tie
//the two together.
case CombatEntityType.ScrapTransportHelicopter:
return "Scrap Transport Helicopter";
case CombatEntityType.AttackHelicopter:
return "AttackHelicopter";
case CombatEntityType.BeeSwarm:
return "BeeSwarm";
case CombatEntityType.Shark:
return "Shark";
case CombatEntityType.ExcavatorArm:
return "Giant Excavator Arm";
case CombatEntityType.CarShredder:
return "Car Shredder";
case CombatEntityType.Drone:
return "Drone";
case CombatEntityType.Cactus:
return "Cactus";
case CombatEntityType.Train:
return "Train";
case CombatEntityType.MagnetCrane:
return "Magnet Crane";
case CombatEntityType.Elevator:
return "Elevator";
case CombatEntityType.HotAirBalloon:
return "Hot Air Balloon";
case CombatEntityType.BatteringRam:
return "Battering Ram";
case CombatEntityType.Tugboat:
return "Tugboat";
case CombatEntityType.CargoShip:
return "CargoShip";
case CombatEntityType.CustomNPC:
if (entity != null && entity is BasePlayer)
return entity.ToPlayer().displayName;
else return "CustomNPC";
}
if (_enemyPrefabs.TryGetValue( entity.ShortPrefabName, out string entityName ))
return entityName;
return HumanizePascalCase(entity.GetType().Name);
}
internal enum CombatEntityType
{
Helicopter = 0,
Bradley = 1,
Animal = 2,
Murderer = 3,
Scientist = 4,
Player = 5,
Trap = 6,
Turret = 7,
Barricade = 8,
ExternalWall = 9,
HeatSource = 10,
Fire = 11,
Lock = 12,
Sentry = 13,
Other = 14,
None = 15,
ScarecrowNPC = 16,
TunnelDweller = 17,
UnderwaterDweller = 18,
ZombieNPC = 19,
GingerbreadNPC = 20,
HeavyScientist = 21,
Minicopter = 22,
ScrapTransportHelicopter = 23,
AttackHelicopter = 24,
BeeSwarm = 25,
SAM = 26,
Shark = 27,
NPCShopKeeper = 28,
ExcavatorArm = 29,
Drone = 30,
CarShredder = 31,
Cactus = 32,
Train = 33,
MagnetCrane = 34,
Elevator = 35,
HotAirBalloon = 36,
BatteringRam = 37,
Tugboat = 38,
CargoShip = 39,
CustomNPC = 40
}
#endregion
#region Workarounds and Inconsistency Handling
private void HandleInconsistencies(ref DeathData data)
{
// Deaths of other entity types are not of interest and might cause errors
if (data.VictimEntityType == CombatEntityType.Other)
return;
if (data.KillerEntity is FireBall)
{
data.DamageType = DamageType.Heat;
if (data.KillerEntity.ShortPrefabName.StartsWith("flameturret"))
data.KillerEntityType = CombatEntityType.Turret;
}
// If the killer entity is null, but a weapon is given, we might be able to fall back to the parent entity of that weapon
// Notably for the auto turret after the changes it has had
if (data.KillerEntity == null && data.HitInfo?.Weapon != null)
{
data.KillerEntity = data.HitInfo.Weapon.GetParentEntity();
data.KillerEntityType = GetCombatEntityType(data.KillerEntity);
}
//If the killer is a SAM Site
if (data.KillerEntity == null && data.HitInfo != null && data.HitInfo.Initiator != null &&
data.HitInfo.Initiator.ShortPrefabName.StartsWith("sam_site_turret_deployed"))
{
data.KillerEntity = data.VictimEntity.lastAttacker ?? data.HitInfo.Initiator;
// data.KillerEntity = data.HitInfo.Initiator;
data.KillerEntityType = CombatEntityType.SAM;
}
// Get previous attacker when bleeding out
if (data.VictimEntityType == CombatEntityType.Player &&
(data.DamageType == DamageType.Bleeding || data.HitInfo == null))
{
var userId = data.VictimEntity.ToPlayer().userID;
if (_previousAttack.ContainsKey(userId))
{
var attack = _previousAttack[userId];
data.KillerEntity = attack.Attacker;
data.KillerEntityType = GetCombatEntityType(data.KillerEntity);
// Restore previous hitInfo for weapon determination
if (attack.HitInfo != null)
data.HitInfo = attack.HitInfo;
// Use previous damagetype if this is a self inflicted death,
// so falling to death etc. is also shown when wounded and bleeding out
if (data.KillerEntity == null || data.KillerEntity == data.VictimEntity)
data.DamageType = attack.DamageType;
else
data.DamageType = DamageType.Bleeding;
}
}
if (data.KillerEntityType != CombatEntityType.None && data.KillerEntity != null)
{
// Workaround for deaths caused by flamethrower or rocket fire
var flame = data.KillerEntity.gameObject.GetComponent<Flame>();
if (flame != null && flame.Initiator != null)
{
data.KillerEntity = flame.Initiator;
data.KillerEntityType = CombatEntityType.Player;
return;
}
}
// Bradley kill with main cannon
if (data.HitInfo?.WeaponPrefab?.ShortPrefabName == "maincannonshell")
{
data.KillerEntityType = CombatEntityType.Bradley;
return;
}
if (data.HitInfo?.WeaponPrefab?.ShortPrefabName?.StartsWith("rocket_heli") ?? false)
{
data.KillerEntityType = CombatEntityType.Helicopter;
return;
}
// Vehicle Kills
if ((data.KillerEntityType == CombatEntityType.Player
&& data.DamageType == DamageType.Generic
&& data.KillerEntity.ToPlayer().isMounted) ||
(data.KillerEntityType == CombatEntityType.Train &&
data.DamageType == DamageType.Generic &&
data.VictimEntityType == CombatEntityType.Player))
{
data.DamageType = DamageType.Collision;
return;
}
}
private struct AttackInfo
{
public HitInfo HitInfo { get; set; }
public DamageType DamageType { get; set; }
public BaseEntity Attacker { get; set; }
}
private class Flame : MonoBehaviour
{
public FlameSource Source { get; set; }
public BaseEntity SourceEntity { get; set; }
public BaseEntity Initiator { get; set; }
public enum FlameSource
{
Flamethrower,
IncendiaryProjectile
}
}
#endregion
#region Weapons
private string GetCustomizedWeaponName(DeathData deathData)
{
var name = GetWeaponName(deathData);
if (string.IsNullOrEmpty(name))
return null;
if (!_configuration.Translations.Weapons.ContainsKey(name))
{
_configuration.Translations.Weapons.Add(name, name);
Config.WriteObject(_configuration);
}
return _configuration.Translations.Weapons[name];
}
private string GetWeaponName(DeathData deathData)
{
if (deathData.HitInfo == null)
return null;
Item item = deathData.HitInfo.Weapon?.GetItem();
/*var parentEntity = hitInfo.Weapon?.GetParentEntity();
Item item = null;
if (parentEntity is BasePlayer)
{
(parentEntity as BasePlayer).inventory.FindItemUID(hitInfo.Weapon.ownerItemUID);
}
else if (parentEntity is ContainerIOEntity)
{
(parentEntity as ContainerIOEntity).inventory.FindItemByUID(hitInfo.Weapon.ownerItemUID);
}*/
if (item != null)
return item.info.displayName.english;
string prefab = null;
try {
prefab = deathData.HitInfo.Initiator?.GetComponent<Flame>()?.SourceEntity?.ShortPrefabName ??
deathData.HitInfo.WeaponPrefab?.ShortPrefabName;
}
catch (Exception e)
{
LogOutput($"Exception in GetWeaponName: {e.ToString()}");
prefab = null;
}
if (prefab != null)
{
if (_weaponPrefabs.TryGetValue( prefab, out string weaponName ))
return weaponName;
return prefab;
}
//Added by Terceran 20250215. Ballistas are a special case because they are considered a vehicle and
//not a weapon.
if (deathData.HitInfo.ProjectilePrefab?.name?.StartsWith("ballista") ?? false)
{
return "Ballista";
}
// Vehicles are the only thing we classify as a weapon, while not being classified as such by the game.
if (deathData.DamageType == DamageType.Collision)
{
return "Vehicle";
}
return null;
}
private string[] GetCustomizedAttachmentNames(HitInfo info)
{
var items = info?.Weapon?.GetItem()?.contents?.itemList;
if (items == null)
{
return Array.Empty<string>();
}
return items.Select(i => GetCustomizedAttachmentName(i.info.displayName.english)).ToArray();
}
private string GetCustomizedAttachmentName(string name)
{
if (!_configuration.Translations.Attachments.ContainsKey(name))
{
_configuration.Translations.Attachments.Add(name, name);
Config.WriteObject(_configuration);
}
return _configuration.Translations.Attachments[name];
}
#endregion
#region Bodyparts
private string GetCustomizedBodypartName(HitInfo hitInfo)
{
var name = GetBodypartName(hitInfo);
if (string.IsNullOrEmpty(name))
return null;
if (!_configuration.Translations.Bodyparts.ContainsKey(name))
{
_configuration.Translations.Bodyparts.Add(name, name);
Config.WriteObject(_configuration);
}
return _configuration.Translations.Bodyparts[name];
}
private string GetBodypartName(HitInfo hitInfo)
{
var hitArea = hitInfo?.boneArea ?? (HitArea) (-1);
return (int) hitArea == -1 ? "Body" : hitArea.ToString();
}
#endregion
#region Helper
public bool ShowPatrolHeliTags()
{
return (_configuration.ShowPatrolHeliTagsInConsole || _configuration.ShowPatrolHeliTagsInChat || _configuration.ShowPatrolHeliTagsInNotify || _configuration.ShowPatrolHeliTagsInUINotify);
}
public bool ShowBradleyTags()
{
return (_configuration.ShowBradleyTagsInConsole || _configuration.ShowBradleyTagsInChat || _configuration.ShowBradleyTagsInNotify || _configuration.ShowBradleyTagsInUINotify);
}
[ChatCommand("dn")]
private void CmdDeathNotes(BasePlayer player, string command, string[] args)
{
if (_configuration.CanPlayerUseDNCommand == false)
return;
if (args.Length <= 0)
{
bool isHidden = permission.UserHasPermission(player.UserIDString, CantSeePermission);
if (isHidden)
{
permission.RevokeUserPermission(player.UserIDString, CantSeePermission);
Player.Message(player, $"<color=#80D000>Death Notes</color> have been <color=#00FF00>ENABLED</color>");
}
else
{
permission.GrantUserPermission(player.UserIDString, CantSeePermission, this);
Player.Message(player, $"<color=#80D000>Death Notes</color> have been <color=#FF0000>DISABLED</color>");
}
}
else
{
string arg = args[0].ToLower();
if (args[0] == "teamonly")
{
bool isTeamOnly = permission.UserHasPermission(player.UserIDString, TeamOnlyPermission);
if (isTeamOnly)
{
permission.RevokeUserPermission(player.UserIDString, TeamOnlyPermission);
Player.Message(player, $"<color=#80D000>Death Notes Team Only</color> view has been <color=#FF0000>DISABLED</color>");
}
else {
permission.GrantUserPermission(player.UserIDString, TeamOnlyPermission, this);
Player.Message(player, $"<color=#80D000>Death Notes Team Only</color> view has been <color=#00FF00>ENABLED</color>");
}
}
}
}
//Useful for debugging
private static void LogDebug(string text)
{
if (_instance._configuration.DebugMode)
{
if (BasePlayer.activePlayerList.Count >= 1)
{
BasePlayer.activePlayerList[0].ConsoleMessage($"<color=orange>{text}</color>");
}
}
}
//Checks to see if the player has the specified Death Message permissions
private bool PlayersPassesPermsCheck(string playerIDString, HashSet<string> displayPerms)
{
if (displayPerms == null || displayPerms.Count <= 0)
return true;
string tmpPerm;
foreach (string perm in displayPerms)
{
tmpPerm = perm.Trim().ToLower();
if (!tmpPerm.StartsWith("deathnotes.") || tmpPerm.Equals("deathnotes.cansee") || tmpPerm.Equals("deathnotes.cantsee") || tmpPerm.Equals("deathnotes.suppress") || tmpPerm.Equals("deathnotes.seeteamonly"))
continue;
if (permission.UserHasPermission(playerIDString, tmpPerm))
return true;
}
return false;
}
//Indicates if either the attacker or the victim in the DeathData parameter are on the same team as the supplied player
private bool ArePlayersOnSameTeam(BasePlayer player, DeathData data)
{
if (player == null)
return false;
if ((data.KillerEntity is BasePlayer killer && killer != null) && (killer.userID == player.userID || (killer.currentTeam != 0 && player.currentTeam == killer.currentTeam)))
return true;
if ((data.VictimEntity is BasePlayer victim && victim != null) && ((victim.userID == player.userID) || (victim.currentTeam != 0 && player.currentTeam == victim.currentTeam)))
return true;
return false;
}
//Primarily for Rustcord interoperability, this indicates whether or not the death was PVP caused, or not.
private bool IsDeathPVP(int inVictimType, int inKillerType)
{
CombatEntityType victimType = (CombatEntityType)inVictimType;
CombatEntityType killerType = (CombatEntityType)inKillerType;
return ((victimType == CombatEntityType.Player && (killerType == CombatEntityType.Player ||
killerType == CombatEntityType.Trap || killerType == CombatEntityType.Turret ||
killerType == CombatEntityType.Barricade ||
killerType == CombatEntityType.ExternalWall ||
killerType == CombatEntityType.HeatSource ||
killerType == CombatEntityType.Cactus ||
killerType == CombatEntityType.Fire || killerType == CombatEntityType.Lock ||
killerType == CombatEntityType.Other || killerType == CombatEntityType.None ||
killerType =
// ... [Code preview truncated due to length. Full source in attached file] ...