Hello, i have an issue with disabling messages in chat/console, i set them to false but they still appearing
Disable messages in chat
Just to confirm understanding, you have these set to false in your config/json file:
"Show Kills in Console": false,
"Show Kills in Chat": false,
...and Death Notes is still printing death messages in your chat and console?
Terceran
Just to confirm understanding, you have these set to false in your config/json file:
"Show Kills in Console": false,
"Show Kills in Chat": false,
...and Death Notes is still printing death messages in your chat and console?
exactly
Unfortunately, I'm not able to replicate that issue on my end. Those settings correspond to two boolean variables in the code that are correctly and rather simply control whether or not to print to chat/console. I should also note that I'm not the original plugin author. I have made several fixed and improvements to the last published code version and perhaps something I did along the way would help you here. My recommendation is to 1) try my plugin and see if that fixes it, or 2) disable the plugin entirely since you're disabling all the functionality by setting both of those flags to false. Here is my current plugin:
// #define DEBUG
using Newtonsoft.Json;
using Oxide.Core;
using Rust;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using UnityEngine;
namespace Oxide.Plugins
{
using EnemyPrefabs = DeathNotes.RemoteConfiguration<Dictionary<string, string>>;
using WeaponPrefabs = DeathNotes.RemoteConfiguration<Dictionary<string, string>>;
using CombatEntityTypes = DeathNotes.RemoteConfiguration<Dictionary<string, DeathNotes.CombatEntityType>>;
[Info("Death Notes", "LaserHydra/Mevent", "6.3.9")]
[Description("Broadcasts deaths to chat along with detailed information")]
class DeathNotes : RustPlugin
{
#region Fields
private const string WildcardCharacter = "*";
private const string CanSeePermission = "deathnotes.cansee";
//Added by Terceran on 20241230
private const string SuppressPermission = "deathnotes.suppress";
private static DeathNotes _instance;
private PluginConfiguration _configuration;
private readonly EnemyPrefabs _enemyPrefabs = new EnemyPrefabs("EnemyPrefabs");
private readonly WeaponPrefabs _weaponPrefabs = new WeaponPrefabs("WeaponPrefabs");
private readonly CombatEntityTypes _combatEntityTypes = new CombatEntityTypes("CombatEntityTypes");
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>();
//These were added by Terceran on 9/3/2024
private readonly HitInfo patrolHeliHitInfo = new HitInfo();
private readonly HitInfo bradleyHitInfo = new HitInfo();
private uint patrolHeliTaggedEpoch = 0;
private uint bradleyTaggedEpoch = 0;
private readonly Func<PluginConfiguration.DeathMessage, DeathData, bool>[] _messageMatchingStages =
{
(m, d) => MatchesCombatEntityType(d.KillerEntityType, m.KillerType) &&
MatchesCombatEntityType(d.VictimEntityType, m.VictimType) &&
MatchesDamageType(d.DamageType, m.DamageType),
(m, d) => MatchesCombatEntityType(d.KillerEntityType, m.KillerType) &&
MatchesCombatEntityType(d.VictimEntityType, m.VictimType) &&
m.DamageType == WildcardCharacter,
(m, d) => MatchesCombatEntityType(d.KillerEntityType, m.KillerType) &&
m.VictimType == WildcardCharacter &&
MatchesDamageType(d.DamageType, m.DamageType),
(m, d) => m.KillerType == WildcardCharacter &&
MatchesCombatEntityType(d.VictimEntityType, m.VictimType) &&
MatchesDamageType(d.DamageType, m.DamageType),
(m, d) => MatchesCombatEntityType(d.KillerEntityType, m.KillerType) &&
m.VictimType == WildcardCharacter &&
m.DamageType == WildcardCharacter,
(m, d) => m.KillerType == WildcardCharacter &&
MatchesCombatEntityType(d.VictimEntityType, m.VictimType) &&
m.DamageType == WildcardCharacter,
(m, d) => m.KillerType == WildcardCharacter &&
m.VictimType == WildcardCharacter &&
MatchesDamageType(d.DamageType, m.DamageType),
(m, d) => m.KillerType == WildcardCharacter &&
m.VictimType == WildcardCharacter &&
m.DamageType == WildcardCharacter
};
#endregion
#region Hooks
private void Init()
{
_instance = this;
permission.RegisterPermission(CanSeePermission, this);
//Added by Terceran on 20241230
permission.RegisterPermission(SuppressPermission, this);
_configuration = Config.ReadObject<PluginConfiguration>();
_configuration.LoadDefaults();
Config.WriteObject(_configuration);
_enemyPrefabs.Load();
_weaponPrefabs.Load();
_combatEntityTypes.Load();
}
private void Unload()
{
_instance = null;
}
private void OnEntityTakeDamage(BasePlayer victimEntity, HitInfo hitInfo)
{
if (victimEntity == null || hitInfo == null)
return;
// Don't track bleeding
if (victimEntity.lastDamage == DamageType.Bleeding)
return;
var userId = victimEntity.ToPlayer().userID;
_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.
//Added by Terceran on 9/2/2024
private void OnEntityTakeDamage(BradleyAPC victimEntity, HitInfo hitInfo)
{
if (victimEntity == null || hitInfo == null)
return;
if (hitInfo.WeaponPrefab != null && bradleyHitInfo.WeaponPrefab != hitInfo.WeaponPrefab)
bradleyHitInfo.WeaponPrefab = hitInfo.WeaponPrefab;
if (hitInfo.Weapon != null && bradleyHitInfo.Weapon != hitInfo.Weapon)
bradleyHitInfo.Weapon = hitInfo.Weapon;
if (bradleyHitInfo.Initiator == null && hitInfo.Initiator != null)
bradleyHitInfo.Initiator = hitInfo.Initiator;
TimeSpan timeSpan = DateTime.UtcNow - new DateTime(1970,1,1);
uint nowEpoch = (uint)timeSpan.TotalSeconds;
//The Bradley APC has been tagged in the past couple of minutes.
if (nowEpoch - bradleyTaggedEpoch < 120)
{
bradleyTaggedEpoch = nowEpoch;
return;
}
bradleyTaggedEpoch = nowEpoch;
if (hitInfo.Initiator != null)
{
var data = new DeathData
{
VictimEntity = victimEntity,
KillerEntity = victimEntity.lastAttacker ?? hitInfo?.Initiator,
VictimEntityType = GetCombatEntityType(victimEntity),
KillerEntityType = GetCombatEntityType(victimEntity.lastAttacker),
DamageType = victimEntity.lastDamage,
HitInfo = bradleyHitInfo
};
if (hitInfo.Initiator?.ToPlayer()?.displayName != null)
{
bradleyHitInfo.Initiator = hitInfo.Initiator;
string weaponName = GetCustomizedWeaponName(data);
var tagPlayer = hitInfo.Initiator?.ToPlayer();
string message = "<color=#C4FF00>" + tagPlayer?.displayName +
"</color> has tagged the <color=#C4FF00>Bradley APC</color> with their <color=#C4FF00>" +
weaponName + "</color>.";
//Added by Terceran on 20241231
if (_configuration.ShowBradleyTags && !permission.UserHasPermission(tagPlayer?.UserIDString, SuppressPermission))
{
object hookResult = Interface.Call("OnDeathNotice", data.ToDictionary(), message);
if (hookResult?.Equals(false) ?? false)
return;
if (_configuration.ShowInChat && _configuration.ShowBradleyTags)
{
foreach (var player in BasePlayer.activePlayerList)
{
if (_configuration.RequirePermission &&
!permission.UserHasPermission(player.UserIDString, CanSeePermission))
continue;
if (_configuration.MessageRadius != -1 &&
player.Distance(data.VictimEntity) > _configuration.MessageRadius)
continue;
Player.Reply(
player,
_configuration.ChatFormat.Replace("{message}", message),
ulong.Parse(_configuration.ChatIcon)
);
}
}
if (_configuration.ShowInConsole)
Puts(StripRichText(message));
}
}
}
}
//This method tracks Patrol Helicopter (Karen) damage and reports whoever tagged it.
//Added by Terceran on 9/1/2024
private void OnPatrolHelicopterTakeDamage(PatrolHelicopter victimEntity, HitInfo hitInfo)
{
if (victimEntity == null || hitInfo == null) return;
if (hitInfo.WeaponPrefab != null && patrolHeliHitInfo.WeaponPrefab != hitInfo.WeaponPrefab)
patrolHeliHitInfo.WeaponPrefab = hitInfo.WeaponPrefab;
if (hitInfo.Weapon != null && patrolHeliHitInfo.Weapon != hitInfo.Weapon)
patrolHeliHitInfo.Weapon = hitInfo.Weapon;
if (patrolHeliHitInfo.Initiator == null && hitInfo.Initiator != null)
patrolHeliHitInfo.Initiator = hitInfo.Initiator;
TimeSpan timeSpan = DateTime.UtcNow - new DateTime(1970,1,1);
uint nowEpoch = (uint)timeSpan.TotalSeconds;
//The Patrol Helicopter (Karen) has been hit within a couple of minutes.
if (nowEpoch - patrolHeliTaggedEpoch < 120)
{
patrolHeliTaggedEpoch = nowEpoch;
return;
}
patrolHeliTaggedEpoch = nowEpoch;
if (hitInfo.Initiator != null)
{
var data = new DeathData
{
VictimEntity = victimEntity,
KillerEntity = victimEntity.lastAttacker ?? hitInfo?.Initiator,
VictimEntityType = GetCombatEntityType(victimEntity),
KillerEntityType = GetCombatEntityType(victimEntity.lastAttacker),
DamageType = victimEntity.lastDamage,
HitInfo = patrolHeliHitInfo
};
//Comment out the entire below if statement if you do not want to report Patrol Helicopter tagging.
if (hitInfo.Initiator?.ToPlayer().displayName != null)
{
patrolHeliHitInfo.Initiator = hitInfo.Initiator;
string weaponName = GetCustomizedWeaponName(data);
var tagPlayer = hitInfo.Initiator?.ToPlayer();
string message = "<color=#C4FF00>" + tagPlayer?.displayName +
"</color> has tagged the <color=#C4FF00>Patrol Helicopter (Karen)</color> with their <color=#C4FF00>" +
weaponName + "</color>. She's going to complain to the manager!";
//Added by Terceran on 20241231
if (_configuration.ShowPatrolHeliTags && !permission.UserHasPermission(tagPlayer?.UserIDString, SuppressPermission))
{
object hookResult = Interface.Call("OnDeathNotice", data.ToDictionary(), message);
if (hookResult?.Equals(false) ?? false)
return;
if (_configuration.ShowInChat)
{
foreach (var player in BasePlayer.activePlayerList)
{
if (_configuration.RequirePermission &&
!permission.UserHasPermission(player.UserIDString, CanSeePermission))
continue;
if (_configuration.MessageRadius != -1 &&
player.Distance(data.VictimEntity) > _configuration.MessageRadius)
continue;
Player.Reply(
player,
_configuration.ChatFormat.Replace("{message}", message),
ulong.Parse(_configuration.ChatIcon)
);
}
if (_configuration.ShowInConsole)
Puts(StripRichText(message));
}
}
}
}
}
private void OnEntityDeath(BaseCombatEntity victimEntity, HitInfo hitInfo)
{
// Ignore - there is no victim for some reason
if (victimEntity == null)
return;
// Try to avoid error when entity was destroyed
if (victimEntity.gameObject == null)
return;
var data = new DeathData
{
VictimEntity = victimEntity,
KillerEntity = victimEntity.lastAttacker ?? hitInfo?.Initiator,
VictimEntityType = GetCombatEntityType(victimEntity),
KillerEntityType = GetCombatEntityType(victimEntity.lastAttacker),
DamageType = victimEntity.lastDamage,
HitInfo = hitInfo
};
//Added by Terceran on 9/1/2024
if (victimEntity.GetType().Name == "PatrolHelicopter")
data.HitInfo = patrolHeliHitInfo;
// Handle inconsistencies/exceptions
HandleInconsistencies(ref data);
#if DEBUG
LogDebug("[DEATHNOTES DEBUG]");
LogDebug(
$"VictimEntity: {data.VictimEntity?.GetType().Name ?? "NULL"} / {data.VictimEntity?.ShortPrefabName ?? "NULL"} / {data.VictimEntity?.PrefabName ?? "NULL"}");
LogDebug(
$"KillerEntity: {data.KillerEntity?.GetType().Name ?? "NULL"} / {data.KillerEntity?.ShortPrefabName ?? "NULL"} / {data.KillerEntity?.PrefabName ?? "NULL"}");
LogDebug($"VictimEntityType: {data.VictimEntityType}");
LogDebug($"KillerEntityType: {data.KillerEntityType}");
LogDebug($"DamageType: {data.DamageType}");
LogDebug($"Bodypart: {GetCustomizedBodypartName(data.HitInfo)}");
LogDebug($"Weapon: {hitInfo?.WeaponPrefab?.ShortPrefabName ?? "NULL"}");
#endif
// Change entity type for dwellers
RepairEntityTypes(ref data);
// Ignore deaths of other entities
if (data.KillerEntityType == CombatEntityType.Other || data.VictimEntityType == CombatEntityType.Other)
return;
// Ignore deaths which don't involve players or the helicopter which usually does not track a player as killer
if (data.VictimEntityType != CombatEntityType.Player && data.KillerEntityType != CombatEntityType.Player &&
data.VictimEntityType != CombatEntityType.Helicopter)
return;
// Populate the variables in the message
string message = PopulateMessageVariables(
// Find the best matching death message for this death
GetDeathMessage(data),
data
);
if (message == null)
return;
object hookResult = Interface.Call("OnDeathNotice", data.ToDictionary(), message);
if (hookResult?.Equals(false) ?? false)
return;
//Added by Terceran on 20241230
if ((data.KillerEntityType == CombatEntityType.Player && !permission.UserHasPermission(data.KillerEntity?.ToPlayer()?.UserIDString, SuppressPermission)) ||
(data.VictimEntityType == CombatEntityType.Player && !permission.UserHasPermission(data.VictimEntity?.ToPlayer()?.UserIDString, SuppressPermission)))
{
if (_configuration.ShowInChat)
{
foreach (var player in BasePlayer.activePlayerList)
{
if (_configuration.RequirePermission &&
!permission.UserHasPermission(player.UserIDString, CanSeePermission))
continue;
if (_configuration.MessageRadius != -1 &&
player.Distance(data.VictimEntity) > _configuration.MessageRadius)
continue;
Player.Reply(
player,
_configuration.ChatFormat.Replace("{message}", message),
ulong.Parse(_configuration.ChatIcon)
);
}
}
if (_configuration.ShowInConsole)
Puts(StripRichText(message));
}
//Added by Terceran on 9/2/2024
if (victimEntity.GetType().Name == "BradleyAPC")
{
bradleyHitInfo.Initiator = null;
bradleyHitInfo.WeaponPrefab = null;
bradleyHitInfo.Weapon = null;
bradleyTaggedEpoch = 0;
}
else if (victimEntity.GetType().Name == "PatrolHelicopter")
{
patrolHeliHitInfo.Initiator = null;
patrolHeliHitInfo.WeaponPrefab = null;
patrolHeliHitInfo.Weapon = null;
patrolHeliTaggedEpoch = 0;
}
}
private void RepairEntityTypes(ref DeathData data)
{
if (data.VictimEntity != null)
{
string victimPrefabName = data.VictimEntity.ShortPrefabName.ToLower();
if (victimPrefabName.Contains("underwaterdweller"))
{
data.VictimEntityType = CombatEntityType.UnderwaterDweller;
}
if (victimPrefabName.Contains("tunneldweller"))
{
data.VictimEntityType = CombatEntityType.TunnelDweller;
}
}
if (data.KillerEntity != null)
{
string killerPrefabName = data.KillerEntity.ShortPrefabName.ToLower();
if (killerPrefabName.Contains("underwaterdweller"))
{
data.KillerEntityType = CombatEntityType.UnderwaterDweller;
}
if (killerPrefabName.Contains("tunneldweller"))
{
data.KillerEntityType = CombatEntityType.TunnelDweller;
}
}
}
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) =>
hitInfo.Initiator = fireBall;
#endregion
#region Death Messages
private string GetDeathMessage(DeathData data)
{
foreach (var matchingStage in _messageMatchingStages)
{
var match = _configuration.Translations.Messages.Find(m => matchingStage.Invoke(m, data));
if (match != null)
return match.Messages.GetRandom((uint) DateTime.UtcNow.Millisecond);
}
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)
};
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)
{
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)
{
replacements.Add("owner",
covalence.Players.FindPlayerById(data.KillerEntity.OwnerID.ToString())?.Name ?? "unknown owner"
); // TODO: Work on the potential unknown owner case
}
}
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;
if (_combatEntityTypes.Contents != null)
{
if (_combatEntityTypes.Contents.ContainsKey(entity.GetType().Name))
return _combatEntityTypes.Contents[entity.GetType().Name];
if (_combatEntityTypes.Contents.ContainsKey(entity.ShortPrefabName))
return _combatEntityTypes.Contents[entity.ShortPrefabName];
}
//Terceran TEST
//Puts($"TERC TEST1: {entity.GetType().Name}");
//Added by Terceran on 9/1/2024
if (entity.GetType().Name.Equals("PatrolHelicopter"))
return CombatEntityType.Helicopter;
//Added by Terceran on 10/7/2024
if (entity.GetType().Name.Equals("SimpleShark"))
return CombatEntityType.Animal;
//Added by Terceran on 11/7/2024
if (entity.GetType().Name.Equals("Wolf2"))
return CombatEntityType.Animal;
if (entity is BaseOven)
return CombatEntityType.HeatSource;
if (entity is SimpleBuildingBlock)
return CombatEntityType.ExternalWall;
if (entity is BaseAnimalNPC)
return CombatEntityType.Animal;
if (entity is BaseTrap)
return CombatEntityType.Trap;
if (entity is Barricade)
return CombatEntityType.Barricade;
if (entity is IOEntity)
return CombatEntityType.Trap;
if (entity is ScientistNPC)
return CombatEntityType.Scientist;
//Added by Terceran on 20241218
if (entity.GetType().Name.Equals("GingerbreadNPC"))
return CombatEntityType.GingerbreadNPC;
if (entity.GetType().Name.Equals("ZombieNPC"))
return CombatEntityType.ZombieNPC;
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);
//Added CombatEnttiyType.GingerbreadNPC: case statement and changed Scarecrow to ScarecrowNPC by Terceran on 20241218
case CombatEntityType.Murderer:
case CombatEntityType.ScarecrowNPC:
case CombatEntityType.Scientist:
case CombatEntityType.ZombieNPC:
case CombatEntityType.GingerbreadNPC:
var name = entity.ToPlayer()?.displayName;
if (!string.IsNullOrEmpty(name) && name != entity.ToPlayer()?.userID.ToString())
{
return name;
}
if (!_enemyPrefabs.Contents.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 (Karen)";
case CombatEntityType.Bradley:
return "Bradley APC";
case CombatEntityType.Sentry:
return "Sentry";
case CombatEntityType.Fire:
return entity.creatorEntity?.ToPlayer()?.displayName ?? "Fire";
}
if (_enemyPrefabs.Contents.ContainsKey(entity.ShortPrefabName))
return _enemyPrefabs.Contents[entity.ShortPrefabName];
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,
//Changed by Terceran on 20241218
ScarecrowNPC = 16,
TunnelDweller = 17,
UnderwaterDweller = 18,
ZombieNPC = 19,
//Added by Terceran on 20241218
GingerbreadNPC = 20
}
#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 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);
}
// 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 selfinflicted 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.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;
var prefab = deathData.HitInfo.Initiator?.GetComponent<Flame>()?.SourceEntity?.ShortPrefabName ??
deathData.HitInfo.WeaponPrefab?.ShortPrefabName;
if (prefab != null)
{
if (_weaponPrefabs.Contents.ContainsKey(prefab))
return _weaponPrefabs.Contents[prefab];
return prefab;
}
// Vehicles are the only thing we classify as a weapon, while not being classified as such by the game.
// TODO: Having this here kinda sucks, make this better.
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
#if DEBUG
private static void LogDebug(string text)
{
if (BasePlayer.activePlayerList.Count >= 1)
{
BasePlayer.activePlayerList[0].ConsoleMessage($"<color=orange>{text}</color>");
}
}
#endif
private static string GetDistance(float meters, bool useMetric)
{
double value = Math.Round(useMetric ? meters : meters * 3.28f, 1);
string unit = value == 1
? _instance.lang.GetMessage("Distance Unit Singular", _instance)
: _instance.lang.GetMessage("Distance Unit Plural", _instance);
return $"{value} {unit}";
}
private static string ApplyVariableFormat(string text, string variableName)
{
if (_instance._configuration.VariableFormats.ContainsKey(variableName))
{
var format = _instance._configuration.VariableFormats[variableName];
text = format.Replace("{value}", text);
}
return text;
}
private static string InsertPlaceholderValues(string text, Dictionary<string, string> values)
{
foreach (var kvp in values)
{
string value = ApplyVariableFormat(kvp.Value, kvp.Key);
if (string.IsNullOrEmpty(kvp.Value))
{
text = text.Replace($"{{{kvp.Key}}}", string.Empty);
}
else if (_instance._configuration.VariableColors.ContainsKey(kvp.Key))
{
var color = _instance._configuration.VariableColors[kvp.Key];
text = text.Replace($"{{{kvp.Key}}}", $"<color={color}>{value}</color>");
color = null;
}
else
{
text = text.Replace($"{{{kvp.Key}}}", value);
}
}
return text;
}
private static string HumanizePascalCase(string text)
{
if (string.IsNullOrEmpty(text))
return string.Empty;
var sb = new StringBuilder();
foreach (char c in text)
{
if (char.IsUpper(c) && sb.Length != 0 && !char.IsUpper(sb[sb.Length - 1]))
sb.Append(" ");
sb.Append(c);
}
return sb.ToString();
}
private string StripRichText(string text)
{
if (string.IsNullOrEmpty(text))
return string.Empty;
text = _colorTagRegex.Replace(text, string.Empty);
text = _sizeTagRegex.Replace(text, string.Empty);
foreach (var richTextLiteral in _richTextLiterals)
text = text.Replace(richTextLiteral, string.Empty);
return text;
}
private static bool MatchesCombatEntityType(CombatEntityType combatEntityType, string text)
{
if (combatEntityType == CombatEntityType.None && text == "-")
return true;
return combatEntityType.ToString().Equals(text);
}
private static bool MatchesDamageType(DamageType damageType, string text)
{
return damageType.ToString().Equals(text);
}
#endregion
#region Configuration
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["Distance Unit Singular"] = "meter",
["Distance Unit Plural"] = "meters"
}, this);
}
protected override void LoadDefaultConfig() => PrintWarning("Generating new configuration file...");
private sealed class PluginConfiguration
{
[JsonProperty("Translations")] public Translation Translations = new Translation();
[JsonProperty("Variable Formats")] public Dictionary<string, string> VariableFormats =
new Dictionary<string, string>
{
["attachments"] = " ({value})"
};
[JsonProperty("Variable Colors")] public Dictionary<string, string> VariableColors =
new Dictionary<string, string>
{
["killer"] = "#C4FF00",
["victim"] = "#C4FF00",
["weapon"] = "#C4FF00",
["attachments"] = "#C4FF00",
["distance"] = "#C4FF00",
["owner"] = "#C4FF00"
};
[JsonProperty("Chat Format")]
public string ChatFormat = "<color=#838383>[<color=#80D000>DeathNotes</color>] {message}</color>";
[JsonProperty("Chat Icon (SteamID)")] public string ChatIcon = "76561############";
[JsonProperty("Show Kills in Console")]
public bool ShowInConsole = true;
[JsonProperty("Show Kills in Chat")] public bool ShowInChat = true;
//Added by Terceran on 20241231
[JsonProperty("Show Patrol Heli Tags")] public bool ShowPatrolHeliTags = true;
[JsonProperty("Show Bradley APC Tags")] public bool ShowBradleyTags = true;
[JsonProperty("Message Broadcast Radius (in meters)")]
public int MessageRadius = -1;
[JsonProperty("Use Metric Distance")] public bool UseMetricDistance = true;
[JsonProperty("Require Permission (deathnotes.cansee)")]
public bool RequirePermission = false;
public void LoadDefaults()
{
if (Translations.Messages == null)
{
var defaults = new RemoteConfiguration<List<DeathMessage>>("DefaultMessages");
defaults.Load(success =>
{
if (success)
{
Translations.Messages = defaults.Contents;
_instance.Config.WriteObject(this);
}
});
}
}
public class DeathMessage
{
public string KillerType { get; set; }
public string VictimType { get; set; }
public string DamageType { get; set; }
public string[] Messages { get; set; }
protected bool Equals(DeathMessage other) => string.Equals(KillerType, other.KillerType) &&
string.Equals(VictimType, other.VictimType) &&
string.Equals(DamageType, other.DamageType);
}
public class Translation
{
[JsonProperty("Death Messages")] public List<DeathMessage> Messages;
[JsonProperty("Names")] public Dictionary<string, string> Names = new Dictionary<string, string>();
[JsonProperty("Bodyparts")]
public Dictionary<string, string> Bodyparts = new Dictionary<string, string>();
[JsonProperty("Weapons")] public Dictionary<string, string> Weapons = new Dictionary<string, string>();
[JsonProperty("Attachments")]
public Dictionary<string, string> Attachments = new Dictionary<string, string>();
}
}
internal sealed class RemoteConfiguration<T>
{
private const string Host = "http://files.laserhydra.com/config/DeathNotes/v6.3.6/";
private readonly string _file;
public RemoteConfiguration(string file)
{
_file = file;
}
public T Contents { get; private set; }
private string ExactUrl => Host + _file;
public void Load(Action<bool> callback = null)
{
_instance.webrequest.Enqueue(ExactUrl, string.Empty, (code, response) =>
{
try
{
if (!IsSuccessStatusCode(code))
throw new Exception($"Status code indicates failure. Code: {code}");
Contents = JsonConvert.DeserializeObject<T>(response);
callback?.Invoke(true);
Interface.Oxide.DataFileSystem.WriteObject($"{nameof(DeathNotes)}/{_file}", Contents);
}
catch (Exception ex)
{
if (Interface.Oxide.DataFileSystem.ExistsDatafile($"{nameof(DeathNotes)}/{_file}"))
{
Contents = Interface.Oxide.DataFileSystem.ReadObject<T>($"{nameof(DeathNotes)}/{_file}");
_instance.PrintWarning(
$"Could not load remote config '{_file}'. The plugin will be using the previously downloaded file.");
callback?.Invoke(true);
}
else
{
_instance.PrintError(
$"Could not load remote config '{_file}'. The plugin will not work properly. Please check whether you can access {ExactUrl} via your browser. If you can, please check the FAQ on how to solve this.");
_instance.PrintError($"[Code {code}] {ex.GetType().Name}: {ex.Message}");
_instance.PrintError($"Response: {response}");
callback?.Invoke(false);
}
}
}, _instance);
}
private bool IsSuccessStatusCode(int code) => code >= 200 && code < 300;
}
#endregion
}
}