C#:
using Facepunch;
using Facepunch.Math;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using Oxide.Game.Rust;
using Oxide.Game.Rust.Cui;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using UnityEngine;
namespace Oxide.Plugins {
[Info("Loot Defender", "Author Egor Blagov, Maintainer nivex", "2.2.8")]
[Description("Defends loot from other players who dealt less damage than you.")]
internal class LootDefender : RustPlugin {
[PluginReference]
Plugin PersonalHeli, Friends, Clans, RustRewards, HelpfulSupply, ShoppyStock, XLevels, XPerience, SkillTree;
private const string ZEROMEMBERID = "0";
private const ulong RR_EVENT = 8675309;
private const ulong CONVOY_EVENT = 755446;
private const ulong HARBOR_EVENT = 81182151852251420;
private const ulong CARGO_TRAIN_EVENT = 1337422;
private const ulong DEFENDER_SKIN_ID = 3790355587;
private Dictionary<NetworkableId, List<DamageKey>> _apcAttackers = new();
private Dictionary<NetworkableId, List<DamageKey>> _heliAttackers = new();
private Dictionary<NetworkableId, ulong> _locked = new();
private HashSet<NetworkableId> _heliCriticalHits = new();
private List<NetworkableId> _personal = new();
private List<NetworkableId> _boss = new();
private StoredData data = new();
private MonumentInfo launchSite;
private List<MonumentInfo> harbors = new();
private List<ulong> ownerids = new() {
0, CARGO_TRAIN_EVENT, 3566257, 123425345634634
}
;
public enum DamageEntryType {
Bradley, Corpse, Heli, NPC, None
}
public class Lockout {
public double Bradley;
public double Heli;
public bool Any(int current) => Bradley > current || Heli > current;
}
private class StoredData {
public Dictionary<string, Lockout> Lockouts = new();
public Dictionary<string, UiHandler.Info> UI = new();
[JsonConverter(typeof(NetworkableIdConverter<DamageInfo>))]
public Dictionary<NetworkableId, DamageInfo> Damage = new();
[JsonConverter(typeof(NetworkableIdConverter<LockInfo>))]
public Dictionary<NetworkableId, LockInfo> LootLock = new();
public Dictionary<string, HashSet<ulong>> Dinosaurs = new();
internal LootDefender Instance;
internal Configuration config => Instance.config;
internal bool IsTypeEnabled(DamageEntryType damageEntryType) => damageEntryType switch {
DamageEntryType.NPC => config.Npc.IsEnabledWithThreshold, DamageEntryType.Bradley => config.Bradley.IsEnabled, DamageEntryType.Heli => config.Helicopter.IsEnabled, _ => false
}
;
public void EnsureInitialized() {
Dinosaurs ??= new();
Lockouts ??= new();
UI ??= new();
foreach (var info in UI.Values) {
info?.EnsureInitialized();
}
LootLock ??= new();
Damage ??= new();
}
public void ClearEntityOwners() {
foreach (var (id, damageInfo) in Damage) {
ClearEntityOwner(id, damageInfo?.OwnerID ?? 0uL);
}
foreach (var (id, lockInfo) in LootLock) {
ClearEntityOwner(id, lockInfo?.damageInfo?.OwnerID ?? 0uL);
}
}
private static void ClearEntityOwner(NetworkableId id, ulong ownerid) {
if (!ownerid.IsSteamId()) {
return;
}
BaseEntity entity = BaseNetworkable.serverEntities.Find(id) as BaseEntity;
ClearEntityOwner(entity, ownerid);
}
private static void ClearEntityOwner(BaseEntity entity, ulong ownerid) {
if (entity != null && !entity.IsDestroyed && entity.OwnerID == ownerid) {
entity.OwnerID = 0uL;
}
}
public void Sanitize() {
EnsureInitialized();
using var dps = Pool.Get<PooledList<KeyValuePair<NetworkableId, DamageInfo>>>();
dps.AddRange(Damage);
foreach (var (id, damageInfo) in dps) {
if (damageInfo == null) {
Damage.Remove(id);
continue;
}
damageInfo.Instance = Instance;
BaseEntity entity = BaseNetworkable.serverEntities.Find(id) as BaseEntity;
if (damageInfo.damageKeys == null) {
ClearEntityOwner(entity, damageInfo.OwnerID);
Damage.Remove(id);
Instance._locked.Remove(id);
continue;
}
damageInfo.damageKeys.RemoveAll(x => x == null || x.damageEntry == null || !x.userid.IsSteamId());
if (!HasNetworkId(entity) || damageInfo.damageKeys.Count == 0 || !IsTypeEnabled(damageInfo.damageEntryType)) {
if (damageInfo.OwnerID.IsSteamId()) {
ClearEntityOwner(entity, damageInfo.OwnerID);
}
Damage.Remove(id);
Instance._locked.Remove(id);
continue;
}
foreach (var x in damageInfo.damageKeys) {
x.attacker = RelationshipManager.FindByID(x.userid);
}
damageInfo._id = id;
damageInfo._entity = entity;
damageInfo.maxHealth = entity.MaxHealth();
damageInfo._position = entity.transform.position;
if (damageInfo.OwnerID.IsSteamId()) {
entity.OwnerID = damageInfo.OwnerID;
Instance._locked[id] = damageInfo.OwnerID;
}
else damageInfo.OwnerID = 0uL;
damageInfo.Start();
}
using var locks = Pool.Get<PooledList<KeyValuePair<NetworkableId, LockInfo>>>();
locks.AddRange(LootLock);
foreach (var (id, lockInfo) in locks) {
BaseEntity entity = BaseNetworkable.serverEntities.Find(id) as BaseEntity;
if (lockInfo?.damageInfo != null) {
lockInfo.damageInfo.Instance = Instance;
}
if (lockInfo?.damageInfo?.damageKeys != null) {
lockInfo.damageInfo.damageKeys.RemoveAll(x => x == null || x.damageEntry == null || !x.userid.IsSteamId());
}
if (!HasNetworkId(entity) || !TryGetOwner(lockInfo, out ulong ownerid) || !IsTypeEnabled(lockInfo.damageInfo.damageEntryType)) {
Instance.RemoveLootLock(id, entity, lockInfo);
continue;
}
foreach (var x in lockInfo.damageInfo.damageKeys) {
x.attacker = RelationshipManager.FindByID(x.userid);
}
lockInfo.damageInfo._position = entity.transform.position;
entity.OwnerID = ownerid;
Instance.ScheduleLootLock(id, entity, lockInfo);
}
}
}
private class NetworkableIdConverter<TValue> : JsonConverter {
public override bool CanConvert(Type objectType) {
return objectType == typeof(Dictionary<NetworkableId, TValue>);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
if (reader.TokenType == JsonToken.Null) {
return new Dictionary<NetworkableId, TValue>();
}
if (reader.TokenType != JsonToken.StartObject) {
throw new JsonSerializationException($"Expected an object while reading {objectType.Name}, but found {reader.TokenType}.");
}
var dict = existingValue as Dictionary<NetworkableId, TValue> ?? new();
dict.Clear();
while (reader.Read()) {
if (reader.TokenType == JsonToken.EndObject) {
return dict;
}
if (reader.TokenType != JsonToken.PropertyName) {
throw new JsonSerializationException($"Expected a NetworkableId property name, but found {reader.TokenType}.");
}
string key = reader.Value as string;
if (!reader.Read()) {
throw new JsonSerializationException("Unexpected end while reading a NetworkableId dictionary value.");
}
if (!TryParse(key, out ulong value)) {
reader.Skip();
continue;
}
dict[new NetworkableId(value)] = (TValue)serializer.Deserialize(reader, typeof(TValue));
}
throw new JsonSerializationException("Unexpected end while reading a NetworkableId dictionary.");
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
if (value == null) {
writer.WriteNull();
return;
}
var dict = (Dictionary<NetworkableId, TValue>)value;
writer.WriteStartObject();
foreach (var pair in dict) {
writer.WritePropertyName(pair.Key.Value.ToString(CultureInfo.InvariantCulture));
serializer.Serialize(writer, pair.Value);
}
writer.WriteEndObject();
}
}
private class DamageEntry {
public float DamageDealt;
public DateTime Timestamp;
public string MemberId;
public DamageEntry() {
}
public DamageEntry(string memberId) {
Timestamp = DateTime.Now;
MemberId = memberId;
}
public bool IsOutdated(int timeout) => timeout > 0 && DateTime.Now.Subtract(Timestamp).TotalSeconds >= timeout;
}
private class DamageKey {
public ulong userid;
public string name;
public DamageEntry damageEntry;
internal BasePlayer attacker;
public DamageKey() {
}
public DamageKey(BasePlayer attacker) {
this.attacker = attacker;
userid = attacker.userID;
name = attacker.displayName;
}
public BasePlayer GetAttacker() {
if (attacker == null) attacker = RelationshipManager.FindByID(userid);
return attacker;
}
public bool TryGetAttacker(out BasePlayer result) {
result = GetAttacker();
return result != null;
}
}
private class DamageInfo {
public List<DamageKey> damageKeys = new();
internal Dictionary<ulong, BasePlayer> interact = new();
internal List<ulong> participants = new();
public DamageEntryType damageEntryType = DamageEntryType.None;
public string NPCName;
public ulong OwnerID;
public ulong SkinID;
public DateTime start;
internal int _lockTime;
internal BaseEntity _entity;
internal Vector3 _position;
internal Vector3 lastAttackedPosition;
internal NetworkableId _id;
internal float maxHealth;
internal Timer _timer;
internal List<DamageKey> keys = new();
internal List<DamageGroup> damageGroups;
internal LootDefender Instance;
internal Configuration config => Instance.config;
internal float FullDamage {
get {
float damage = 0;
for (int i = 0; i < damageKeys.Count; i++) {
damage += damageKeys[i].damageEntry.DamageDealt;
}
return damage;
}
}
public DamageInfo() {
}
public DamageInfo(LootDefender instance, DamageEntryType damageEntryType, string NPCName, BaseEntity entity, DateTime start) {
Instance = instance;
SkinID = entity.skinID;
_entity = entity;
_id = entity.net.ID;
maxHealth = entity.MaxHealth();
this.damageEntryType = damageEntryType;
this.NPCName = NPCName;
this.start = start;
Start();
}
public void Start() {
DestroyTimer();
_lockTime = Instance.GetLockTime(damageEntryType);
if (_lockTime > 0) {
_timer = Instance.timer.Every(1f, CheckExpiration);
}
}
public void DestroyTimer() {
if (_timer is {
Destroyed : false
}
) {
_timer.Destroy();
}
_timer = null;
}
private void CheckExpiration() {
foreach (var x in damageKeys) {
if (x.damageEntry.IsOutdated(_lockTime)) {
if (x.userid == OwnerID) {
Unlock();
}
keys.Add(x);
}
}
foreach (var x in keys) {
damageKeys.Remove(x);
}
keys.Clear();
if (damageKeys.Count == 0) {
Instance.RemoveDamageInfo(_id, this);
}
}
public void Unlock() {
if (!Instance._locked.Remove(_id, out ulong userid)) {
OwnerID = 0;
return;
}
bool isDestroyed = !HasNetworkId(_entity);
if (!isDestroyed && _entity.OwnerID == userid) // don't reset the owner when another plugin changed ours
{
_entity.OwnerID = 0;
}
OwnerID = 0;
Interface.CallHook("OnUnlockedEntity", _entity, userid, _id.Value, isDestroyed);
string grid = PositionToGrid(_position);
if (damageEntryType == DamageEntryType.Bradley && config.Bradley.Messages.NotifyChat) {
foreach (var target in BasePlayer.activePlayerList) {
CreateMessage(target, "BradleyUnlocked", grid);
}
}
if (damageEntryType == DamageEntryType.Heli && config.Helicopter.Messages.NotifyChat) {
foreach (var target in BasePlayer.activePlayerList) {
CreateMessage(target, "HeliUnlocked", grid);
}
}
if (damageEntryType == DamageEntryType.NPC && config.Npc.Messages.NotifyChat) {
foreach (var target in BasePlayer.activePlayerList) {
CreateMessage(target, "NpcUnlocked", NPCName, grid);
}
}
}
private void Lock(BaseEntity entity, ulong userid) {
Instance._locked[_id] = entity.OwnerID = OwnerID = userid;
_position = entity.transform.position;
Interface.CallHook("OnLockedEntity", entity, OwnerID, _id.Value, _position);
}
public void AddDamage(BaseCombatEntity entity, BasePlayer attacker, DamageEntry entry, float amount) {
entry.DamageDealt += amount;
entry.Timestamp = DateTime.Now;
_position = entity.transform.position;
lastAttackedPosition = attacker.transform.position;
if (SkinID == 0) {
SkinID = entity.skinID;
}
if (damageEntryType == DamageEntryType.NPC && !Instance.CanLockNpc(entity)) {
return;
}
if (entity.OwnerID.IsSteamId()) {
OwnerID = entity.OwnerID;
}
if (OwnerID != 0uL) {
return;
}
float damage = 0f;
var grid = PositionToGrid(entity.transform.position);
if (entry.MemberId != ZEROMEMBERID) {
foreach (var x in damageKeys) {
if (x.damageEntry.MemberId == entry.MemberId) {
damage += x.damageEntry.DamageDealt;
}
}
}
else damage = entry.DamageDealt;
if (config.Helicopter.IsEnabled && entity is PatrolHelicopter) {
if (damage >= maxHealth * config.Helicopter.Threshold && !Instance.HasPermission(attacker, "lootdefender.bypasshelilock")) {
if (config.Helicopter.Messages.NotifyLocked == true) {
foreach (var target in BasePlayer.activePlayerList) {
CreateMessage(target, "Locked Heli", grid, attacker.displayName);
}
}
Lock(entity, attacker.userID);
}
}
else if (config.Bradley.IsEnabled && entity is BradleyAPC) {
if (damage >= maxHealth * config.Bradley.Threshold && !Instance.HasPermission(attacker, "lootdefender.bypassbradleylock")) {
if (config.Bradley.Messages.NotifyLocked == true) {
foreach (var target in BasePlayer.activePlayerList) {
CreateMessage(target, "Locked Bradley", grid, attacker.displayName);
}
}
Lock(entity, attacker.userID);
}
}
else if (config.Npc.Enabled && entity is BasePlayer npc && Instance.CanLockNpc(npc)) {
if (!npc.userID.IsSteamId() && damage >= maxHealth * config.Npc.Threshold && !Instance.HasPermission(attacker, "lootdefender.bypassnpclock")) {
if (config.Npc.Messages.NotifyLocked == true) {
foreach (var target in BasePlayer.activePlayerList) {
CreateMessage(target, "Locked Npc", grid, npc.displayName, attacker.displayName);
}
}
Lock(entity, attacker.userID);
}
}
}
public bool TryGet(ulong userid, out DamageEntry damageEntry) {
foreach (var x in damageKeys) {
if (x.userid == userid) {
damageEntry = x.damageEntry;
return true;
}
}
damageEntry = null;
return false;
}
public DamageEntry Get(BasePlayer attacker) {
if (!TryGet(attacker.userID, out var entry)) {
string id = Instance.GetMemberId(attacker);
damageKeys.Add(new(attacker) {
damageEntry = entry = new(id),
}
);
}
return entry;
}
public bool isKilled;
public void OnKilled(Vector3 position, HitInfo info, float distance) {
if (isKilled) return;
isKilled = true;
SetCanInteract();
DisplayDamageReport();
FindLooters(position, info, distance);
}
private void FindLooters(Vector3 position, HitInfo info, float distance) {
var weapon = info?.Weapon?.GetItem()?.info?.shortname ?? info?.WeaponPrefab?.ShortPrefabName ?? "";
HashSet<ulong> looters = new();
HashSet<ulong> users = new();
foreach (var x in damageKeys) {
if (CanInteract(x.GetAttacker(), x.userid)) {
if (TryGet(x.userid, out var entry) && entry.DamageDealt > 0) {
users.Add(x.userid);
}
looters.Add(x.userid);
}
}
foreach (var userid in users) {
Instance.GiveXpReward(_entity, this, userid, weapon, distance, users.Count);
Instance.GiveRustReward(_entity, this, userid, weapon, users.Count);
Instance.GiveShopReward(_entity, this, userid, weapon, distance, users.Count);
}
if (damageEntryType is DamageEntryType.Bradley or DamageEntryType.Heli) {
Instance.LockoutLooters(looters, position, damageEntryType, SkinID);
}
}
public void DisplayDamageReport() {
if (damageEntryType is DamageEntryType.Bradley or DamageEntryType.Heli) {
foreach (var target in BasePlayer.activePlayerList) {
if (CanDisplayReport(target)) {
Instance.Message(target, GetDamageReport(target.userID));
}
}
}
else if (damageEntryType == DamageEntryType.NPC) {
foreach (var x in damageKeys) {
if (CanDisplayReport(x.GetAttacker())) {
Instance.Message(x.GetAttacker(), GetDamageReport(x.userid));
}
}
}
}
private bool CanDisplayReport(BasePlayer target) {
if (target == null || !target.IsConnected) {
return false;
}
if (damageEntryType == DamageEntryType.Bradley) {
if (config.Bradley.Messages.NotifyKiller && IsParticipant(target)) {
return true;
}
return config.Bradley.Messages.NotifyChat;
}
if (damageEntryType == DamageEntryType.Heli) {
if (config.Helicopter.Messages.NotifyKiller && IsParticipant(target)) {
return true;
}
return config.Helicopter.Messages.NotifyChat;
}
if (damageEntryType == DamageEntryType.NPC) {
if (config.Npc.Messages.NotifyKiller && IsParticipant(target)) {
return true;
}
return config.Npc.Messages.NotifyChat;
}
return false;
}
public void SetCanInteract() {
var damageGroups = GetDamageGroups();
if (damageGroups.Count > 0) {
using var ownerDamageGroups = GetOwnerDamageGroups(damageGroups);
foreach (var damageGroup in damageGroups) {
if (ownerDamageGroups.Contains(damageGroup) || Instance.IsAlly(damageGroup.FirstDamagerDealer.attacker, damageGroup.FirstDamagerDealer.userid, OwnerID)) {
interact[damageGroup.FirstDamagerDealer.userid] = damageGroup.FirstDamagerDealer.GetAttacker();
}
else {
var damage = TryGet(damageGroup.FirstDamagerDealer.userid, out var x) ? x.DamageDealt : 0f;
var ratio = damage > 0 && FullDamage > 0 ? damage / FullDamage : 0;
float threshold = config.Npc.Threshold;
if (damageEntryType == DamageEntryType.Bradley) {
threshold = config.Bradley.Threshold;
}
else if (damageEntryType == DamageEntryType.Heli) {
threshold = config.Helicopter.Threshold;
}
if (OwnerID == 0 && ratio >= threshold) {
OwnerID = damageGroup.FirstDamagerDealer.userid;
interact[damageGroup.FirstDamagerDealer.userid] = damageGroup.FirstDamagerDealer.GetAttacker();
}
else {
participants.Add(damageGroup.FirstDamagerDealer.userid);
}
}
}
}
this.damageGroups = damageGroups;
}
private string Localize(string key, ulong id, params object[] args) => Instance.Localize(key, id.ToString(), args);
private string Localize(string key, string id, params object[] args) => Instance.Localize(key, id, args);
private void CreateMessage(BasePlayer player, string key, params object[] args) => Instance.CreateMessage(player, key, args);
private StringBuilder sb = new();
public string GetDamageReport(ulong userid) {
var nameKey = damageEntryType == DamageEntryType.Bradley ? Localize("BradleyAPC", userid) : damageEntryType == DamageEntryType.Heli ? Localize("Helicopter", userid) : NPCName;
var firstDamageDealer = string.Empty;
sb.Length = 0;
sb.AppendLine($"{Localize("DamageReport", userid, $"<color= {
config.Report.Ok
}
> {
nameKey
}
</color>")}:");
if (damageEntryType is DamageEntryType.Bradley or DamageEntryType.Heli) {
var seconds = Math.Ceiling((DateTime.Now - start).TotalSeconds);
sb.AppendLine($"{Localize("DamageTime", userid, nameKey, seconds)}");
}
if (damageGroups.Count > 0) {
foreach (var damageGroup in damageGroups) {
if (interact.ContainsKey(damageGroup.FirstDamagerDealer.userid)) {
sb.Append($"<color={config.Report.Ok}>√</color> ");
firstDamageDealer = damageGroup.FirstDamagerDealer.name;
}
else {
sb.Append($"<color={config.Report.NotOk}>X</color> ");
}
sb.Append($"{damageGroup.ToReport(Instance, damageGroup.FirstDamagerDealer, this)}\n");
}
if (damageEntryType == DamageEntryType.NPC && damageGroups.Count > 1 && !string.IsNullOrEmpty(firstDamageDealer)) {
sb.Append($" {Localize("FirstLock", userid, firstDamageDealer, config.Npc.Threshold * 100f)}");
}
}
return sb.ToString();
}
public bool IsParticipant(BasePlayer player) {
return participants.Contains(player.userID) || CanInteract(player, player.userID);
}
public bool CanInteract(BasePlayer player, ulong userid) {
if (damageEntryType == DamageEntryType.NPC && !config.Npc.Enabled) {
return true;
}
if (damageGroups == null) {
interact.Clear();
participants.Clear();
SetCanInteract();
}
if (interact.Count == 0 || interact.ContainsKey(userid)) {
return true;
}
if (Instance.IsAlly(player, userid, OwnerID)) {
interact.Add(userid, player);
return true;
}
return false;
}
private PooledList<DamageGroup> GetOwnerDamageGroups(List<DamageGroup> damageGroups) {
var ownerDamageGroups = Pool.Get<PooledList<DamageGroup>>();
if (damageGroups.Count == 0) {
return ownerDamageGroups;
}
foreach (var damageGroup in damageGroups) {
foreach (var userid in damageGroup.Players) {
BasePlayer player = RelationshipManager.FindByID(userid);
if (Instance.IsAlly(player, userid, OwnerID)) {
ownerDamageGroups.Add(damageGroup);
break;
}
}
}
return ownerDamageGroups;
}
private List<DamageGroup> GetDamageGroups() {
List<DamageGroup> damageGroups = new();
foreach (var x in damageKeys) {
damageGroups.Add(new(x));
}
damageGroups.Sort((x, y) => y.TotalDamage.CompareTo(x.TotalDamage));
return damageGroups;
}
}
private class LockInfo {
public DamageInfo damageInfo;
public DateTime ExpiresAt;
internal bool IsLockOutdated => ExpiresAt != default && DateTime.Now >= ExpiresAt;
public LockInfo() {
}
public LockInfo(DamageInfo damageInfo, DateTime expiresAt) {
ExpiresAt = expiresAt;
this.damageInfo = damageInfo;
}
public bool CanInteract(BasePlayer target) => damageInfo.CanInteract(target, target.userID);
public string GetDamageReport(ulong userId) => damageInfo.GetDamageReport(userId);
}
private class DamageGroup {
public float TotalDamage;
public DamageKey FirstDamagerDealer;
private List<ulong> additionalPlayers = new();
internal List<ulong> Players {
get {
List<ulong> players = new() {
FirstDamagerDealer.userid
}
;
foreach (var userid in additionalPlayers) {
if (!players.Contains(userid)) {
players.Add(userid);
}
}
return players;
}
}
public DamageGroup() {
}
public DamageGroup(DamageKey x) {
TotalDamage = x.damageEntry.DamageDealt;
FirstDamagerDealer = x;
if (RelationshipManager.ServerInstance.playerToTeam.TryGetValue(x.userid, out var team)) {
for (int i = 0; i < team.members.Count; i++) {
ulong member = team.members[i];
if (member == x.userid || additionalPlayers.Contains(member)) {
continue;
}
additionalPlayers.Add(member);
}
}
if (x.TryGetAttacker(out BasePlayer attacker) && attacker.clanId != 0 && TryGetClan(attacker, out IClan clan)) {
for (int i = 0; i < clan.Members.Count; i++) {
ulong member = clan.Members[i].SteamId;
if (member == x.userid || additionalPlayers.Contains(member)) {
continue;
}
additionalPlayers.Add(member);
}
if (clan.Creator != x.userid && !additionalPlayers.Contains(clan.Creator)) {
additionalPlayers.Add(clan.Creator);
}
}
}
public string ToReport(LootDefender Instance, DamageKey damageKey, DamageInfo damageInfo) {
var damage = damageInfo.TryGet(damageKey.userid, out var x) ? x.DamageDealt : 0f;
var percent = damage > 0 && damageInfo.FullDamage > 0 ? damage / damageInfo.FullDamage * 100 : 0;
var color = additionalPlayers.Count == 0 ? Instance.config.Report.SinglePlayer : Instance.config.Report.Team;
var damageLine = Instance.Localize("Format", damageKey.userid.ToString(), damage, percent);
return $"<color={color}>{damageKey.name}</color> {damageLine}";
}
}
public string GetMemberId(BasePlayer attacker) {
string id = attacker switch {
{
clanId : not 0
}
=> attacker.clanId.ToString(), {
currentTeam : not 0
}
=> attacker.currentTeam.ToString(), _ => Clans?.Call("GetClanOf", attacker.userID.Get()) as string
}
;
if (string.IsNullOrEmpty(id)) return ZEROMEMBERID;
return id;
}
public void UpdateMemberId(BasePlayer player, string id) {
if (player == null) {
return;
}
if (string.IsNullOrEmpty(id) || id == ZEROMEMBERID) {
id = GetMemberId(player);
}
if (id == ZEROMEMBERID) {
return;
}
allyLookupTimes.Clear();
foreach (DamageInfo damageInfo in data.Damage.Values) {
if (damageInfo.TryGet(player.userID, out DamageEntry damageEntry) && (string.IsNullOrEmpty(damageEntry.MemberId) || damageEntry.MemberId == ZEROMEMBERID)) {
damageEntry.MemberId = id;
}
}
}
#region Hooks
private void OnClanMemberJoined(string tag, ulong joining, List<ulong> members) {
BasePlayer player = RelationshipManager.FindByID(joining);
if (player != null) {
UpdateMemberId(player, tag);
}
}
private void OnTeamCreated(BasePlayer player, RelationshipManager.PlayerTeam team) {
if (player != null) {
UpdateMemberId(player, team.teamID.ToString());
}
}
private void OnTeamAcceptInvite(RelationshipManager.PlayerTeam team, BasePlayer player) {
if (player == null) {
return;
}
player.Invoke(() => {
if (!player.IsDestroyed && player.currentTeam == team?.teamID) {
UpdateMemberId(player, team.teamID.ToString());
}
}
, 0.01f);
}
private object OnTeamLeave(RelationshipManager.PlayerTeam team, BasePlayer player) => HandleTeam(player?.userID ?? 0uL) ? (object)true : null;
private object OnTeamKick(RelationshipManager.PlayerTeam team, BasePlayer player, ulong targetId) => HandleTeam(targetId) ? (object)true : null;
private void OnServerSave() {
timer.Once(15f, SaveData);
}
private void Init() {
UI = new() {
Instance = this
}
;
Unsubscribe(nameof(OnEventTrigger));
Unsubscribe();
if (!string.IsNullOrEmpty(config.Lockout.Command)) AddCovalenceCommand(config.Lockout.Command, nameof(CommandLockouts));
if (!string.IsNullOrEmpty(config.UI.Command)) AddCovalenceCommand(config.UI.Command, nameof(CommandUI));
AddCovalenceCommand("lo", nameof(CommandLootDefender));
AddCovalenceCommand("lootdefender", nameof(CommandLootDefender));
RegisterPermissions();
LoadData();
}
private void OnServerInitialized(bool initial) {
if (initial) {
data.ClearEntityOwners();
data.Damage.Clear();
data.LootLock.Clear();
_locked.Clear();
SaveData();
}
else {
data.Sanitize();
}
if (config.Hackable.Enabled) {
Subscribe(nameof(CanHackCrate));
Subscribe(nameof(OnGuardedCrateEventEnded));
}
if (config.SupplyDrop.Lock) {
if (config.SupplyDrop.LockTime > 0) {
if (config.SupplyDrop.NpcRandomRaids) {
Subscribe(nameof(OnRandomRaidWin));
}
Subscribe(nameof(OnSupplyDropLanded));
}
if (config.SupplyDrop.Excavator) {
Subscribe(nameof(OnExcavatorSuppliesRequested));
}
if (config.SupplyDrop.HelpfulSupply && HelpfulSupply != null) {
Subscribe(nameof(OnEntitySpawned));
}
Subscribe(nameof(OnExplosiveDropped));
Subscribe(nameof(OnExplosiveThrown));
Subscribe(nameof(OnSupplyDropDropped));
Subscribe(nameof(OnCargoPlaneSignaled));
}
if (config.SupplyDrop.DestroyTime > 0f || config.CH47Gibs) {
Subscribe(nameof(OnEntitySpawned));
}
if (config.Npc.BossMonster) {
Unsubscribe(nameof(OnBossSpawn));
Unsubscribe(nameof(OnBossKilled));
}
if (!config.Bradley.LockPersonal) {
Subscribe(nameof(OnPersonalApcSpawned));
}
if (!config.Helicopter.LockPersonal) {
Subscribe(nameof(OnPersonalHeliSpawned));
}
if (config.UI.Bradley.Enabled || config.UI.Heli.Enabled) {
foreach (var player in BasePlayer.activePlayerList) {
UI.ShowLockouts(player);
}
Subscribe(nameof(OnPlayerSleepEnded));
Subscribe(nameof(OnCuiDraggableDrag));
}
if (config.BradleyOrHelicopterIsEnabled) {
Subscribe(nameof(OnCrateSpawned));
}
if (config.BradleyOrHelicopterIsEnabled || config.Hackable.Laptop) {
Subscribe(nameof(OnPlayerAttack));
}
if (config.Lockout.F15) {
Subscribe(nameof(OnEventTrigger));
}
if (config.Helicopter.IsEnabled) {
Subscribe(nameof(OnPatrolHelicopterKill));
}
Subscribe(nameof(OnEntityTakeDamage));
Subscribe(nameof(OnEntityDeath));
Subscribe(nameof(OnEntityKill));
Subscribe(nameof(CanLootEntity));
SetupLaunchSite();
}
private bool IsF15EventActive;
private void OnEventTrigger(TriggeredEventPrefab prefab) {
if (config.Lockout.F15 && !IsF15EventActive && prefab.name == "assets/bundled/prefabs/world/event_f15e.prefab") {
Puts("F15 event has started; bypassing player lockouts!");
IsF15EventActive = true;
}
}
private void Unload() {
UI.DestroyAllLockoutUI();
SaveData();
}
private void OnPlayerSleepEnded(BasePlayer player) => UI.ShowLockouts(player);
private void OnCuiDraggableDrag(BasePlayer player, string name, Vector3 position, CommunityEntity.DraggablePositionSendType dragType) {
if (!HasNetworkId(player)) {
return;
}
UiType uiType = UI.GetUiType(name);
if (uiType == UiType.Invalid) {
return;
}
UiOffsets offsets = UI.GetOffsets(player.UserIDString, uiType, true);
switch (dragType) {
case CommunityEntity.DraggablePositionSendType.NormalizedScreen : offsets.SetAnchor(new Vector2(position.x, 1f - position.y));
break;
case CommunityEntity.DraggablePositionSendType.NormalizedParent : offsets.SetAnchor(new Vector2(position.x, position.y));
break;
case CommunityEntity.DraggablePositionSendType.Relative : case CommunityEntity.DraggablePositionSendType.RelativeAnchor : offsets.Move(new Vector2(position.x, position.y));
break;
default : return;
}
UI.SaveOffsetData();
UI.UpdateLockoutUI(player, uiType);
}
private object OnPlayerAttack(BasePlayer attacker, HitInfo info) {
if (attacker == null || HasPermission(attacker, "lootdefender.bypass.damage") || info == null) {
return null;
}
if (config.Hackable.Laptop && info.HitBone == 242862488 && info.HitEntity is HackableLockedCrate crate && IsDefended(crate)) // laptopcollision
{
info.HitBone = 0;
return null;
}
if (!config.BradleyOrHelicopterIsEnabled) {
return null;
}
if (!info.HitEntity.Is(out ServerGib gibs) || !TryGetNetworkId(gibs, out NetworkableId id)) {
return null;
}
if (!data.LootLock.TryGetValue(id, out var lockInfo)) {
return null;
}
if (!TryGetOwner(lockInfo, out ulong ownerid)) {
RemoveLootLock(id, gibs, lockInfo);
return null;
}
if (gibs.OwnerID != ownerid) {
if (DebugMode) Puts("Restored ownership of '{0}' to {1} [was={2}, now={3}]", gibs.ShortPrefabName, GetPlayerName(ownerid), gibs.OwnerID, ownerid);
gibs.OwnerID = ownerid;
}
if (lockInfo.CanInteract(attacker)) {
return null;
}
if (CanMessage(attacker)) {
CreateMessage(attacker, "CannotMine");
Message(attacker, lockInfo.GetDamageReport(attacker.userID));
}
CancelDamage(info);
return true;
}
private object OnPatrolHelicopterKill(PatrolHelicopter heli, HitInfo info) {
if (info == null || !config.Helicopter.IsEnabled || !TryGetNetworkId(heli, out NetworkableId id) || heli.myAI == null || heli.myAI.isDead || !ShouldHandleHeli(heli, id)) {
return null;
}
if (!TryGetPlayerAttacker(info, out BasePlayer attacker)) {
return null;
}
if (CanTakeDamage(heli, id, info, attacker, DamageEntryType.Heli, _heliAttackers, false) is false) {
return true;
}
_heliCriticalHits.Add(id);
NextTick(() => _heliCriticalHits.Remove(id));
return null;
}
private bool IsDamageBlocked(BaseCombatEntity entity, NetworkableId id, BasePlayer attacker, HitInfo info, DamageEntryType damageEntryType) {
if (!_locked.TryGetValue(id, out ulong ownerid) || HasPermission(attacker, "lootdefender.bypass.damage") || IsAlly(attacker, ownerid) || !BlockDamage(damageEntryType)) {
return false;
}
if (CanMessage(attacker)) {
CreateMessage(attacker, "CannotDamageThis");
}
CancelDamage(info);
return true;
}
private object CanTakeDamage(BaseCombatEntity entity, NetworkableId id, HitInfo info, BasePlayer attacker, DamageEntryType damageEntryType, Dictionary<NetworkableId, List<DamageKey>> source, bool add = true) {
if (info.damageTypes.Total() <= 0f) {
return null;
}
if (IsDamageBlocked(entity, id, attacker, info, damageEntryType)) {
return false;
}
if (UI.GetLockoutTime(damageEntryType) <= 0d) {
return null;
}
if (HasLockout(attacker, damageEntryType, entity.skinID)) {
CancelDamage(info);
return false;
}
if (add) {
AddDamageAttacker(source, id, attacker);
}
return null;
}
private void AddDamageAttacker(Dictionary<NetworkableId, List<DamageKey>> source, NetworkableId id, BasePlayer attacker) {
if (!source.TryGetValue(id, out var attackers)) {
source[id] = attackers = new();
}
for (int i = 0; i < attackers.Count; i++) {
if (attackers[i].userid == attacker.userID) {
return;
}
}
attackers.Add(new(attacker));
}
private object OnEntityTakeDamage(PatrolHelicopter heli, HitInfo info) {
if (info == null || !config.Helicopter.IsEnabled || !TryGetNetworkId(heli, out NetworkableId id) || heli.myAI == null || !ShouldHandleHeli(heli, id)) {
return null;
}
bool criticalHit = _heliCriticalHits.Remove(id);
if (!criticalHit && heli.myAI.isDead) {
return null;
}
if (!TryGetPlayerAttacker(info, out BasePlayer attacker)) {
return null;
}
if (criticalHit) {
if (config.Lockout.Heli > 0d && info.damageTypes.Total() > 0f) {
AddDamageAttacker(_heliAttackers, id, attacker);
}
}
else if (CanTakeDamage(heli, id, info, attacker, DamageEntryType.Heli, _heliAttackers) is false) {
return true;
}
return OnEntityTakeDamageHandler(heli, id, info, attacker, DamageEntryType.Heli, string.Empty);
}
private object OnEntityTakeDamage(BradleyAPC apc, HitInfo info) {
if (info == null || !config.Bradley.IsEnabled || !TryGetNetworkId(apc, out NetworkableId id) || !ShouldHandleBradley(apc, id)) {
return null;
}
if (!TryGetPlayerAttacker(info, out BasePlayer attacker)) {
return null;
}
if (CanTakeDamage(apc, id, info, attacker, DamageEntryType.Bradley, _apcAttackers) is false) {
return true;
}
return OnEntityTakeDamageHandler(apc, id, info, attacker, DamageEntryType.Bradley, string.Empty);
}
private object OnEntityTakeDamage(BasePlayer player, HitInfo info) {
if (info == null || !config.Npc.IsEnabledWithThreshold ||
// ... [Code preview truncated due to length. Full source in attached file] ...