Dec 5, 2024 Update - PhoneControllerFixed

Failed to compile: 'PhoneController' does not contain a definition for 'PositionToGridCoord' | Line: 565, Pos: 51

Ā 

Rust changes
There's several changes that will break plugins, such as PhoneController.PositionToGridCoord being removed in favor of MapHelper.PositionToGrid and https://umod.org/plugins/loading-messages no longer being a thing because they blocked customizing the text.

Same to me

sure clear will get this fix soon . got to keep in mind timezones

The below is a temporary patched version for anyone who needs it.
As always, review any code thoroughly before running it on any system.

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Raid Tracker", "Clearshot", "2.1.1")]
    [Description("Track raids by explosives, weapons, and ammo with detailed on-screen visuals")]
    class RaidTracker : CovalencePlugin
    {
        private bool _dev = false; // force certain config options
        private bool _debug = false; // respect config, detailed output
        private float _debugDrawDuration = 15f;
        private static RaidTracker _instance;
        private PluginConfig _config;
        private bool _isConfigValid = true;
        private StringBuilder _sb = new StringBuilder();
        private Game.Rust.Libraries.Player _rustPlayer = Interface.Oxide.GetLibrary<Game.Rust.Libraries.Player>("Player");

        private DiscordWebhookManager _discordWebhookManager = new DiscordWebhookManager();
        private readonly int _collisionLayerMask = LayerMask.GetMask("Construction", "Deployed");
        private Dictionary<Vector3, ulong> _MLRSRocketOwners = new Dictionary<Vector3, ulong>();
        private Dictionary<ulong, bool> _verboseMode = new Dictionary<ulong, bool>();
        private Dictionary<ulong, string[]> _lastViewCommand = new Dictionary<ulong, string[]>();
        private Dictionary<string, DecayEntityIgnoreOptions> _decayEntityIgnoreList = new Dictionary<string, DecayEntityIgnoreOptions>();
        private Dictionary<string, string> _prefabToItem = new Dictionary<string, string>();
        private Dictionary<string, string> _buildingBlockPrettyNames = new Dictionary<string, string>();
        private Dictionary<ulong, float> _notificationCooldown = new Dictionary<ulong, float>();

        private bool _wipeData;
        private List<RaidEvent> _raidEventLog = new List<RaidEvent>();
        private string _raidEventLogFilename;
        private int _raidEventLogCount;

        private string[] _ignoredTimedExplosives = new string[] {
            "firecrackers.deployed",
            "flare.deployed",
            "maincannonshell",
            "rocket_heli",
            "rocket_heli_napalm"
        };

        private string[] _uniqueHexColors = new string[] {
            "#01FFFE", "#FFA6FE", "#FFDB66", "#006401", "#010067",
            "#95003A", "#007DB5", "#FF00F6", "#FFEEE8", "#774D00",
            "#90FB92", "#0076FF", "#D5FF00", "#FF937E", "#6A826C",
            "#FF029D", "#FE8900", "#7A4782", "#7E2DD2", "#85A900",
            "#FF0056", "#A42400", "#00AE7E", "#683D3B", "#BDC6FF",
            "#263400", "#BDD393", "#00B917", "#9E008E", "#001544",
            "#C28C9F", "#FF74A3", "#01D0FF", "#004754", "#E56FFE",
            "#788231", "#0E4CA1", "#91D0CB", "#BE9970", "#968AE8",
            "#BB8800", "#43002C", "#DEFF74", "#00FFC6", "#FFE502",
            "#620E00", "#008F9C", "#98FF52", "#7544B1", "#B500FF",
            "#00FF78", "#FF6E41", "#005F39", "#6B6882", "#5FAD4E",
            "#A75740", "#A5FFD2", "#FFB167", "#009BFF", "#E85EBE",
            "#00FF00", "#0000FF", "#FF0000", "#000000"
        };
        private List<Color> _uniqueColors = new List<Color>();
        private Dictionary<ulong, Color> _teamColors = new Dictionary<ulong, Color>();
        private int _currentTeamColorIdx = 0;

        [PluginReference]
        Plugin AbandonedBases, Clans, RaidableBases;

        private const string PERM_WIPE = "raidtracker.wipe";
        private const string PERM_PX = "raidtracker.px";

        #region Hooks

        private void Init()
        {
            permission.RegisterPermission(PERM_WIPE, this);
            permission.RegisterPermission(PERM_PX, this);

            Unsubscribe(nameof(OnPlayerDisconnected));
            Unsubscribe(nameof(OnMlrsFired));
            Unsubscribe(nameof(OnMlrsFiringEnded));
            Unsubscribe(nameof(OnEntitySpawned));
            Unsubscribe(nameof(OnEntityDeath));
            Unsubscribe(nameof(OnServerSave));
        }

        private void OnServerInitialized()
        {
            _debug = _debug || _config.debug;
            if (_dev)
            {
                _debug = true;
                Puts("[DEV] Dev mode enabled!");
            }

            PrintDebug("Debug mode enabled!");

            _raidEventLogFilename = $"{Name}\\RaidEventLog";
            _raidEventLog = Interface.Oxide.DataFileSystem.ReadObject<List<RaidEvent>>(_raidEventLogFilename);
            _raidEventLogCount = _raidEventLog.Count;

            foreach (string hexColor in _uniqueHexColors)
            {
                Color color;
                if (ColorUtility.TryParseHtmlString(hexColor, out color))
                    _uniqueColors.Add(color);
            }

            foreach (var item in ItemManager.itemList.OrderBy(x => x.shortname))
            {
                ItemModEntity itemModEnt = item.GetComponent<ItemModEntity>();
                if (itemModEnt != null)
                {
                    var gameObjRef = itemModEnt.entityPrefab;
                    if (string.IsNullOrEmpty(gameObjRef.guid)) continue;

                    AddPrefabToItem(gameObjRef.resourcePath, item.shortname, nameof(ItemModEntity));
                }

                ItemModDeployable itemModDeploy = item.GetComponent<ItemModDeployable>();
                if (itemModDeploy != null)
                {
                    var gameObjRef = itemModDeploy.entityPrefab;
                    if (string.IsNullOrEmpty(gameObjRef.guid)) continue;

                    AddPrefabToItem(gameObjRef.resourcePath, item.shortname, nameof(ItemModDeployable));
                }

                ItemModProjectile itemModProj = item.GetComponent<ItemModProjectile>();
                if (itemModProj != null)
                {
                    var gameObjRef = itemModProj.projectileObject;
                    if (string.IsNullOrEmpty(gameObjRef.guid)) continue;

                    AddPrefabToItem(gameObjRef.resourcePath, item.shortname, nameof(ItemModProjectile));
                }
            }

            if (_wipeData && _config.deleteDataOnWipe)
            {
                Puts($"Wipe detected! Removing {_raidEventLog.Count} raid events.");
                _wipeData = false;
                _raidEventLog = new List<RaidEvent>();
                SaveRaidEventLog();
            }

            if (_raidEventLog.Count > 0)
            {
                int removed = 0;
                DateTime currentDateTime = DateTime.Now;
                for (int i = _raidEventLog.Count - 1; i >= 0; i--)
                {
                    RaidEvent raidEvent = _raidEventLog[i];
                    if (currentDateTime.Subtract(raidEvent.timestamp).TotalDays >= _config.daysBeforeDelete)
                    {
                        _raidEventLog.RemoveAt(i);
                        removed++;
                    }
                }

                if (removed > 0)
                {
                    Puts($"Removed {removed} raid events older than {_config.daysBeforeDelete} days");
                    SaveRaidEventLog();
                }
            }

            var saveList = false;
            var decayEntityListFilename = $"{Name}\\DecayEntityIgnoreList";
            if (!Interface.Oxide.DataFileSystem.ExistsDatafile(decayEntityListFilename))
            {
                Puts($"Generating DecayEntityIgnoreList, any items enabled in this list will be ignored by the plugin");
                Puts($"Saving DecayEntityIgnoreList to /oxide/data/{Utility.CleanPath(decayEntityListFilename)}.json");
            }

            _decayEntityIgnoreList = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<string, DecayEntityIgnoreOptions>>(decayEntityListFilename);

            Dictionary<BuildingGrade.Enum, string> buildingGradeNames = new Dictionary<BuildingGrade.Enum, string> {
                { BuildingGrade.Enum.Twigs, "Twig" },
                { BuildingGrade.Enum.TopTier, "HQM" }
            };

            foreach (var prefab in GameManifest.Current.entities)
            {
                var gameObj = GameManager.server.FindPrefab(prefab);
                if (gameObj == null) continue;

                var thrownWep = gameObj.GetComponent<ThrownWeapon>();
                if (thrownWep != null)
                {
                    var itemShortname = GetItemFromPrefabShortname(thrownWep.ShortPrefabName); // get item shortname from held entity
                    AddPrefabToItem(thrownWep.prefabToThrow.resourcePath, itemShortname, nameof(ThrownWeapon)); // assign deployed entity to same item shortname

                    PrintDebug($"  {thrownWep.ShortPrefabName}[{thrownWep.GetType()}] -> {GetPrefabShortname(thrownWep.prefabToThrow.resourcePath)} -> {itemShortname}");
                }

                var ent = gameObj.GetComponent<DecayEntity>();
                if (ent != null)
                {
                    var itemShortname = GetItemFromPrefabShortname(ent.ShortPrefabName);
                    if (!_decayEntityIgnoreList.ContainsKey(ent.PrefabName))
                    {
                        var item = ItemManager.FindItemDefinition(itemShortname);
                        var itemName = item != null ? $"{item?.displayName?.english ?? "Unknown"} ({itemShortname})" : "";
                        _decayEntityIgnoreList[ent.PrefabName] = new DecayEntityIgnoreOptions {
                            name = itemName,
                            ignore = false,
                            ignoreDiscord = false
                        };
                        LogToSingleFile("decay_entity_log", $"added {ent.PrefabName} {(!string.IsNullOrEmpty(itemName) ? $"[ITEM: {itemName}]" : "")}");
                        saveList = true;
                    }

                    if (ent is BuildingBlock)
                    {
                        PrintDebug($"BuildingBlock - {ent.ShortPrefabName}");

                        Type gradeType = typeof(BuildingGrade.Enum);
                        foreach(var grade in Enum.GetNames(gradeType))
                        {
                            var e = (BuildingGrade.Enum)Enum.Parse(gradeType, grade);
                            if (e == BuildingGrade.Enum.None || e == BuildingGrade.Enum.Count) continue;

                            var buildingBlockItemShortname = $"{ent.ShortPrefabName}.{grade.ToLower()}";
                            var gradeName = buildingGradeNames.ContainsKey(e) ? buildingGradeNames[e] : grade;
                            var prettyName = $"{gradeName} {ent.prefabAttribute.Find<Construction>(StringPool.Get(ent.PrefabName))?.info.name?.english ?? "Unknown"}";
                            _buildingBlockPrettyNames[buildingBlockItemShortname] = prettyName;

                            PrintDebug($"   BuildingGrade[{grade}] - {prettyName}[{buildingBlockItemShortname}]");
                        }
                    }
                }
            }

            if (saveList)
            {
                var sorted = _decayEntityIgnoreList
                    .OrderBy(x => string.IsNullOrWhiteSpace(x.Value.name))
                    .ThenBy(x => x.Value.name)
                    .ToDictionary(x => x.Key, x => x.Value);

                Interface.Oxide.DataFileSystem.WriteObject(decayEntityListFilename, sorted);
            }

            var saveCfg = false;
            foreach (var trackerList in _config.trackers)
            {
                foreach (var weaponCfg in trackerList.Value)
                {
                    if (!string.IsNullOrEmpty(weaponCfg.Value.name)) continue;

                    var shortname = GetItemFromPrefabShortname(weaponCfg.Key);
                    weaponCfg.Value.name = GetPrettyItemName(shortname);
                    saveCfg = true;
                }
            }

            // Don't overwrite the config if invalid since the user will lose their config!
            if (_isConfigValid && saveCfg)
                SaveConfig();

            Subscribe(nameof(OnPlayerDisconnected));
            Subscribe(nameof(OnMlrsFired));
            Subscribe(nameof(OnMlrsFiringEnded));
            Subscribe(nameof(OnEntitySpawned));
            Subscribe(nameof(OnEntityDeath));
            Subscribe(nameof(OnServerSave));
        }

        private void Unload()
        {
            var trackers = UnityEngine.Object.FindObjectsOfType<ExplosiveTracker>();
            if (trackers != null && trackers.Length > 0)
            {
                Puts($"Destroying {trackers.Length} explosive trackers");
                foreach (var t in trackers)
                {
                    t.logEvent = false;
                    UnityEngine.Object.Destroy(t);
                }
            }

            _instance = null;
            SaveRaidEventLog();
        }

        private void OnPlayerDisconnected(BasePlayer pl, string reason)
        {
            _verboseMode.Remove(pl.userID);
            _lastViewCommand.Remove(pl.userID);
            _notificationCooldown.Remove(pl.userID);
        }

        private void OnMlrsFired(MLRS mlrs, BasePlayer driver)
        {
            _MLRSRocketOwners[mlrs.transform.position] = driver.userID;
        }

        private void OnMlrsFiringEnded(MLRS mlrs)
        {
            _MLRSRocketOwners.Remove(mlrs.transform.position);
        }

        private void OnEntitySpawned(TimedExplosive ent)
        {
            if (ent == null || _ignoredTimedExplosives.Contains(ent.ShortPrefabName)) return;

            var trackerCategory = "entity_collision";
            if (!AddOrFindWeaponConfig(trackerCategory, ent.ShortPrefabName).enabled) return;

            if (ent is MLRSRocket && _MLRSRocketOwners.Count > 0) // fix MLRS rocket creatorEntity being null when there is no driver in the MLRS truck
            {
                var mlrsOwnerID = _MLRSRocketOwners.First(x => Vector3.Distance(x.Key, ent.transform.position) < 25f).Value;
                var mlrsOwner = BasePlayer.FindByID(mlrsOwnerID);
                if (mlrsOwner != null)
                {
                    ent.creatorEntity = mlrsOwner;
                    ent.OwnerID = mlrsOwner.userID;
                }
            }

            var tracker = ent.gameObject.AddComponent<ExplosiveTracker>();
            if (tracker != null)
            {
                tracker.Init(trackerCategory);
            }

            if (_debug)
            {
                var pl = ent.creatorEntity as BasePlayer;
                PrintDebug($"Added explosive tracker to {ent.ShortPrefabName} spawned by {pl?.displayName ?? "Unknown"}[{pl?.userID ?? 0}]");
            }
        }

        private void OnEntityDeath(DecayEntity entity, HitInfo info)
        {
            if (entity == null || info == null)
                return;

            var initiator = info.Initiator;
            if (initiator is FireBall) // Log fireballs spawned from inc ammo, fire arrows, etc
            {
                var fireballTrackerCategory = "entity_death_fire";
                var fireballWeaponCfg = AddOrFindWeaponConfig(fireballTrackerCategory, initiator.ShortPrefabName);
                if (!fireballWeaponCfg.enabled || IsDecayEntityIgnored(entity))
                    return;

                var creator = initiator.creatorEntity as BasePlayer;
                RaidEvent raidEventFireball = new RaidEvent {
                    attackerName = creator?.displayName ?? fireballWeaponCfg.name,
                    attackerSteamID = creator?.userID ?? 0,
                    attackerTeamID = creator?.Team?.teamID ?? 0,
                    victimSteamID = entity.OwnerID,
                    weapon = $"{initiator.ShortPrefabName}[{fireballTrackerCategory}]",
                    hitEntity = $"EVENT.BURNT {GetDecayEntityShortname(entity)}",
                    startPos = entity.transform.position + new Vector3(0, .2f, 0),
                    endPos = entity.transform.position,
                    timestamp = DateTime.Now
                };
                _raidEventLog.Add(raidEventFireball);
                raidEventFireball.Notify(entity);

                PrintDebug($"OnEntityDeath ({initiator.ShortPrefabName}) - WeaponPrefab: {info?.WeaponPrefab?.ShortPrefabName ?? "NULL"}, ProjectilePrefab: {info?.ProjectilePrefab?.name ?? "NULL"}");
                PrintDebug($"{initiator.ShortPrefabName} ({initiator.creatorEntity}) BURNT {GetDecayEntityShortname(entity)}[{entity?.net?.ID.Value ?? 0}]");
                return;
            }

            var attacker = info.InitiatorPlayer;
            if (attacker == null || IsDecayEntityOrAttackerIgnored(entity, attacker))
                return;

            if (info?.WeaponPrefab == null && info?.damageTypes?.GetMajorityDamageType() == Rust.DamageType.Heat) // Log fires spawned from flame throwers or other weapons
            {
                var fireTrackerCategory = "entity_death_fire";
                var fireWeapon = "fire_damage";
                var fireWeaponCfg = AddOrFindWeaponConfig(fireTrackerCategory, fireWeapon);
                if (!fireWeaponCfg.enabled)
                    return;

                RaidEvent raidEventFire = new RaidEvent {
                    attackerName = attacker?.displayName ?? "Unknown",
                    attackerSteamID = attacker?.userID ?? 0,
                    attackerTeamID = attacker?.Team?.teamID ?? 0,
                    victimSteamID = entity.OwnerID,
                    weapon = $"{fireWeapon}[{fireTrackerCategory}]",
                    hitEntity = $"EVENT.BURNT {GetDecayEntityShortname(entity)}",
                    startPos = entity.transform.position + new Vector3(0, .2f, 0),
                    endPos = entity.transform.position,
                    timestamp = DateTime.Now
                };
                _raidEventLog.Add(raidEventFire);
                raidEventFire.Notify(entity);

                PrintDebug($"OnEntityDeath ({fireWeapon}) - WeaponPrefab: {info?.WeaponPrefab?.ShortPrefabName ?? "NULL"}, ProjectilePrefab: {info?.ProjectilePrefab?.name ?? "NULL"}");
                PrintDebug($"{fireWeapon} BURNT {GetDecayEntityShortname(entity)}[{entity?.net?.ID.Value ?? 0}]");
                return;
            }

            var weaponPrefabShortname = info?.WeaponPrefab?.ShortPrefabName;
            var projectilePrefabShortname = info?.ProjectilePrefab?.name;
            var projectileItemShortname = projectilePrefabShortname != null ? GetItemFromPrefabShortname(projectilePrefabShortname) : null;

            var heldEntity = attacker.GetHeldEntity();
            if (heldEntity is AttackEntity)
            {
                if (info.WeaponPrefab == null)
                {
                    weaponPrefabShortname = heldEntity.ShortPrefabName;

                    PrintDebug($"OnEntityDeath - WeaponPrefab is NULL! Using HeldEntity: {heldEntity.ShortPrefabName ?? "NULL"}");
                }

                var projectile = heldEntity?.GetComponent<BaseProjectile>();
                var heldProjectileItemShortname = projectile?.primaryMagazine?.ammoType?.shortname ?? null;
                if ((info.WeaponPrefab == null && info.ProjectilePrefab == null)
                    || (projectileItemShortname != null && heldProjectileItemShortname != null && projectileItemShortname != heldProjectileItemShortname)) // certain projectiles from HitInfo do not match the projectile in the players gun Ex: ammo.pistol.hv, rifle.ammmo.hv, ammo.shotgun
                {
                    if (_debug)
                    {
                        if (projectileItemShortname != heldProjectileItemShortname)
                            PrintDebug($"OnEntityDeath - ProjectileItemShortname ({projectileItemShortname ?? "NULL"}) != HeldProjectileItemShortname ({heldProjectileItemShortname ?? "NULL"})! Using HeldProjectileItemShortname: {heldProjectileItemShortname ?? "NULL"}");
                        else
                            PrintDebug($"OnEntityDeath - WeaponPrefab + ProjectilePrefab are NULL! Using HeldEntityProjectile: {projectileItemShortname ?? "NULL"}");
                    }

                    projectileItemShortname = heldProjectileItemShortname;
                }
            }

            var weaponTrackerCategory = "entity_death_weapon";
            var weaponItemShortname = weaponPrefabShortname != null ? GetItemFromPrefabShortname(weaponPrefabShortname) : null;
            var weaponEnabled = weaponItemShortname != null ? AddOrFindWeaponConfig(weaponTrackerCategory, weaponItemShortname).enabled : false;

            var projectileTrackerCategory = "entity_death_ammo";
            var projectileEnabled = projectileItemShortname != null && weaponItemShortname != projectileItemShortname ? AddOrFindWeaponConfig(projectileTrackerCategory, projectileItemShortname).enabled : false;

            PrintDebug($"OnEntityDeath - WeaponPrefab: {info?.WeaponPrefab?.ShortPrefabName ?? "NULL"} ({weaponItemShortname ?? "NULL"}), ProjectilePrefab: {info?.ProjectilePrefab?.name ?? "NULL"} ({projectileItemShortname ?? "NULL"})");

            if (!weaponEnabled && !projectileEnabled) return;

            string weapon;
            if (weaponEnabled && projectileItemShortname != null && weaponItemShortname != projectileItemShortname)
                weapon = $"{weaponItemShortname}[{weaponTrackerCategory}];{projectileItemShortname}[{projectileTrackerCategory}]";
            else if (projectileEnabled && weaponItemShortname != null && weaponItemShortname != projectileItemShortname)
                weapon = $"{projectileItemShortname}[{projectileTrackerCategory}];{weaponItemShortname}[{weaponTrackerCategory}]";
            else if (projectileEnabled)
                weapon = $"{projectileItemShortname}[{projectileTrackerCategory}]";
            else
                weapon = $"{weaponItemShortname}[{weaponTrackerCategory}]";

            var startPos = attacker.transform.position;
                startPos.y += attacker.GetHeight() - .5f;
            var endPos = info.HitPositionWorld != Vector3.zero && info.HitPositionWorld != entity.transform.position ? info.HitPositionWorld : entity.WorldSpaceBounds().ToBounds().center;

            RaidEvent raidEvent = new RaidEvent {
                attackerName = attacker?.displayName ?? "Unknown",
                attackerSteamID = attacker?.userID ?? 0,
                attackerTeamID = attacker?.Team?.teamID ?? 0,
                victimSteamID = entity.OwnerID,
                weapon = weapon,
                hitEntity = $"EVENT.DESTROYED {GetDecayEntityShortname(entity)}",
                startPos = startPos,
                endPos = endPos,
                timestamp = DateTime.Now
            };
            _raidEventLog.Add(raidEvent);
            raidEvent.Notify(entity);

            PrintDebug($"{weapon} DESTROYED {GetDecayEntityShortname(entity)}[{entity?.net?.ID.Value ?? 0}]");
        }

        private void OnServerSave()
        {
            SaveRaidEventLog();
        }

        private void OnNewSave(string filename)
        {
            _wipeData = true;
        }

        #endregion

        #region Commands

        [Command("x")]
        private void ViewExplosionsCommand(IPlayer player, string command, string[] args)
        {
            BasePlayer pl = player.Object as BasePlayer;
            if (pl == null || !pl.IsAdmin) return;

            if (args.Length > 0 && (args[0].ToLower() == "v" || args[0].ToLower() == "extra"))
            {
                _verboseMode[pl.userID] = _verboseMode.ContainsKey(pl.userID) ? !_verboseMode[pl.userID] : true;
                SendChatMsg(pl, lang.GetMessage(_verboseMode[pl.userID] ? "ViewEventsCommand.ExtraModeEnabled" : "ViewEventsCommand.ExtraModeDisabled", this, pl.UserIDString));
                return;
            }

            float radius = _config.searchRadius > 0f ? _config.searchRadius : 50f;
            string filterType = args.Length > 0 ? args[0].ToLower() : "";
            string filter = "";
            bool verbose = _verboseMode.ContainsKey(pl.userID) && _verboseMode[pl.userID];

            Vector3 pos = pl.transform.position;
                    pos.y += pl.GetHeight() - .5f;

            float tempRadius;
            IEnumerable<IGrouping<RaidFilter, RaidEvent>> groupedRaidsNearMe;
            switch (filterType)
            {
                case "l":
                case "last":
                    string[] lastArgs;
                    if (_lastViewCommand.TryGetValue(pl.userID, out lastArgs))
                        ViewExplosionsCommand(player, command, lastArgs);
                    return;
                case "help":
                    _sb.Clear();
                    _sb.AppendLine(lang.GetMessage("ViewEventsCommand.HelpHeader", this, pl.UserIDString));
                    _sb.AppendLine(lang.GetMessage("ViewEventsCommand.HelpDefault", this, pl.UserIDString));
                    _sb.AppendLine(lang.GetMessage("ViewEventsCommand.HelpExtraMode", this, pl.UserIDString));
                    _sb.AppendLine(lang.GetMessage("ViewEventsCommand.HelpWipe", this, pl.UserIDString));
                    _sb.AppendLine(lang.GetMessage("ViewEventsCommand.HelpLast", this, pl.UserIDString));
                    _sb.AppendLine(lang.GetMessage("ViewEventsCommand.HelpFilter", this, pl.UserIDString));
                    _sb.Append("<indent=6>");
                    _sb.AppendLine(lang.GetMessage("ViewEventsCommand.HelpFilterTime", this, pl.UserIDString));
                    _sb.AppendLine(lang.GetMessage("ViewEventsCommand.HelpFilterWeapon", this, pl.UserIDString));
                    _sb.AppendLine(lang.GetMessage("ViewEventsCommand.HelpFilterEntity", this, pl.UserIDString));
                    _sb.AppendLine(lang.GetMessage("ViewEventsCommand.HelpFilterTeam", this, pl.UserIDString));
                    _sb.AppendLine(lang.GetMessage("ViewEventsCommand.HelpFilterPlayer", this, pl.UserIDString));
                    _sb.Append("<indent=0>");
                    _sb.AppendLine(lang.GetMessage("ViewEventsCommand.HelpPrintRaidEvent", this, pl.UserIDString));
                    SendChatMsg(pl, _sb.ToString(), "");

                    return;
                case "wipe":
                    if (!permission.UserHasPermission(pl.UserIDString, PERM_WIPE))
                    {
                        SendChatMsg(pl, lang.GetMessage("ViewEventsCommand.WipePermission", this, pl.UserIDString));
                        return;
                    }

                    if (args.Length > 1 && float.TryParse(args[1], out tempRadius))
                        radius = tempRadius;

                    var raidEventsToWipe = FindRaidEventsInSphere(pos, radius).ToList();
                    if (raidEventsToWipe.Count == 0)
                    {
                        SendChatMsg(pl, string.Format(lang.GetMessage("ViewEventsCommand.NotFoundRadius", this, pl.UserIDString), radius));
                        return;
                    }

                    foreach(var raidEvent in raidEventsToWipe)
                    {
                        int idx = raidEvent.GetIndex();
                        if (idx > -1)
                            _raidEventLog.RemoveAt(idx);
                    }

                    var playerPos = pl.transform.position;
					Vector2i gridVector = MapHelper.PositionToGrid(playerPos);
					string gridPos = $"{gridVector.x}, {gridVector.y}"; // Converting Vector2i to string
					
                    var filename = $"{Name}\\WipedRaidEvents\\{string.Format("{0:yyyy-MM-dd}", DateTime.Now)}\\{pl.userID}\\{gridPos}_{string.Format("{0:h-mm-tt}", DateTime.Now)}";
                    LogToFile("wiped_raid_events", $"{pl.displayName}[{pl.userID}] wiped {raidEventsToWipe.Count} raid events in {gridPos} ({FormatPosition(playerPos)})", this);
                    Interface.Oxide.DataFileSystem.WriteObject(filename, raidEventsToWipe);
                    SaveRaidEventLog();

                    SendChatMsg(pl, string.Format(lang.GetMessage("ViewEventsCommand.WipedRaidEventsRadius", this, pl.UserIDString), raidEventsToWipe.Count, radius, gridPos));
                    return;
                case "time":
                    filter = args.Length > 1 ? args[1].ToLower() : "";

                    if (args.Length > 2 && float.TryParse(args[2], out tempRadius))
                        radius = tempRadius;

                    double hours;
                    if (!double.TryParse(filter, out hours))
                        hours = 24;

                    groupedRaidsNearMe = FindRaidEventsInSphere(pos, radius)
                        .Where(x => DateTime.Now.Subtract(x.timestamp).TotalHours <= hours)
                        .GroupBy(x => new RaidFilter { filter = $"{x.attackerName}[{x.attackerSteamID}]", filterType = filterType });
                    break;
                case "weapon":
                    filter = args.Length > 1 ? args[1].ToLower() : "";

                    if (args.Length > 2 && float.TryParse(args[2], out tempRadius))
                        radius = tempRadius;

                    groupedRaidsNearMe = FindRaidEventsInSphere(pos, radius)
                        .Where(x => x.weapon.Contains(filter) || FindWeaponConfig(x.GetTrackerCategory(), x.GetPrimaryWeaponShortname()).name.ToLower().Contains(filter))
                        .GroupBy(x => new RaidFilter { filter = FindWeaponConfig(x.GetTrackerCategory(), x.GetPrimaryWeaponShortname()).name, filterType = filterType });
                    break;
                case "entity":
                    filter = args.Length > 1 ? args[1].ToLower() : "";

                    if (args.Length > 2 && float.TryParse(args[2], out tempRadius))
                        radius = tempRadius;

                    groupedRaidsNearMe = FindRaidEventsInSphere(pos, radius)
                        .Where(x => x.hitEntity.Contains(filter))
                        .GroupBy(x => new RaidFilter { filter = x.GetHitEntityShortname(), filterType = filterType });
                    break;
                case "team":
                    filter = args.Length > 1 ? args[1].ToLower() : "";

                    if (args.Length > 2 && float.TryParse(args[2], out tempRadius))
                        radius = tempRadius;

                    IEnumerable<RaidEvent> tempRaidEvents = FindRaidEventsInSphere(pos, radius);
                    if (!string.IsNullOrEmpty(filter))
                        tempRaidEvents = tempRaidEvents.Where(x => x.attackerTeamID.ToString() == filter);

                    groupedRaidsNearMe = tempRaidEvents.GroupBy(x => {
                        var team = RelationshipManager.ServerInstance.FindTeam(x.attackerTeamID);
                        return new RaidFilter { filter = team != null ? $"{team.GetLeader()?.displayName ?? "UNKNOWN LEADER"}'s Team (ID: {x.attackerTeamID})" : x.attackerTeamID.ToString(), filterType = filterType };
                    });
                    break;
                case "player":
                    filter = args.Length > 1 ? args[1].ToLower() : "";

                    if (args.Length > 2 && float.TryParse(args[2], out tempRadius))
                        radius = tempRadius;

                    groupedRaidsNearMe = FindRaidEventsInSphere(pos, radius)
                        .Where(x => x.attackerSteamID.ToString() == filter || x.attackerName.ToLower().Contains(filter))
                        .GroupBy(x => new RaidFilter { filter = $"{x.attackerName}[{x.attackerSteamID}]", filterType = filterType });
                    break;
                default:
                    if (args.Length > 0 && float.TryParse(args[0], out tempRadius))
                        radius = tempRadius;

                    groupedRaidsNearMe = FindRaidEventsInSphere(pos, radius)
                        .GroupBy(x => new RaidFilter { filter = $"{x.attackerName}[{x.attackerSteamID}]", filterType = "default" });
                    break;
            }

            DrawRaidEvents(pl, groupedRaidsNearMe, filterType, filter, radius);
            _lastViewCommand[pl.userID] = args;
        }

        [Command("px")]
        private void PlayerViewExplosionsCommand(IPlayer player, string command, string[] args)
        {
            BasePlayer pl = player.Object as BasePlayer;
            if (pl == null) return;

            bool isAdmin = player.IsAdmin;
            if (!isAdmin && !permission.UserHasPermission(pl.UserIDString, PERM_PX))
            {
                SendChatMsg(pl, string.Format(lang.GetMessage("ViewEventsCommand.NoPermission", this, pl.UserIDString), $"/{command}"));
                return;
            }

            try
            {
                if (!isAdmin)
                {
                    pl.SetPlayerFlag(BasePlayer.PlayerFlags.IsAdmin, true);
                    pl.SendNetworkUpdateImmediate();
                }

                float radius = _config.searchRadius > 0f ? _config.searchRadius : 50f;
                float tempRadius;
                if (args.Length > 0 && float.TryParse(args[0], out tempRadius))
                    radius = Mathf.Clamp(tempRadius, 0f, 100f);

                Vector3 pos = pl.transform.position;
                        pos.y += pl.GetHeight() - .5f;

                var groupedRaidsNearMe = FindRaidEventsInSphere(pos, radius)
                    .Where(x => x.victimSteamID == pl.userID || (pl.Team != null && pl.Team.members.Contains(x.victimSteamID)) || FindWeaponConfig(x.GetTrackerCategory(), x.GetPrimaryWeaponShortname()).alwaysLog)
                    .Where(x => DateTime.Now.Subtract(x.timestamp).TotalMinutes > _config.playerViewExplosionsCommand.ignoreRaidEventsLessThanMinutes)
                    .GroupBy(x => new RaidFilter { filter = $"{x.attackerSteamID.GetHashCode()}", filterType = "victim" });

                DrawRaidEvents(pl, groupedRaidsNearMe, "victim", pl.UserIDString, radius, _config.playerViewExplosionsCommand.drawAttackerName);
            }
            catch (Exception ex)
            {
                LogToSingleFile("px_error_log", $"error drawing player explosions for {pl.userID}[{pl?.displayName ?? "Unknown"}]\n\nException: {ex.Message}");
            }

            if (!isAdmin)
            {
                pl.SetPlayerFlag(BasePlayer.PlayerFlags.IsAdmin, false);
                pl.SendNetworkUpdateImmediate();
            }
        }

        [Command("re")]
        private void RaidEventDetailsCommand(IPlayer player, string command, string[] args)
        {
            BasePlayer pl = player.Object as BasePlayer;
            if (pl == null || !pl.IsAdmin) return;

            int index = -1;
            int i;
            if (args.Length > 0 && int.TryParse(args[0], out i))
                index = i;

            if (index < _raidEventLog.Count && _raidEventLog[index] != null)
            {
                RaidEvent raidEvent = _raidEventLog[index];
                SendChatMsg(pl, $"\n\nRaid Event {index}:\n{JsonConvert.SerializeObject(raidEvent, Formatting.Indented)}");
            }
            else
                SendChatMsg(pl, $"Raid event {index} not found!");
        }

        [Command("rt.debug")]
        private void DebugCommand(IPlayer player, string command, string[] args)
        {
            if (!player.IsServer) return;

            _debug = !_debug;
            Puts($"debug: {_debug}");
        }

        [Command("rt.weapon_colors")]
        private void WeaponColorsCommand(IPlayer player, string command, string[] args)
        {
            BasePlayer pl = player.Object as BasePlayer;
            if (pl == null || !pl.IsAdmin) return;

            _sb.Clear();
            var category = args.Length > 0 && _config.trackers.ContainsKey(args[0].ToLower()) ? args[0].ToLower() : _config.trackers.Keys.First();
            foreach (var weaponCfg in _config.trackers[category])
            {
                var val = weaponCfg.Value;
                _sb.AppendLine($"<color={val.hexColor}>• {val.name} ({category})</color> - Enabled: {val.enabled}");
            }

            SendChatMsg(pl, $"\n\n{_sb}");
        }

        #endregion

        #region Helpers

        private void DrawRaidEvents(BasePlayer pl, IEnumerable<IGrouping<RaidFilter, RaidEvent>> groupedRaidsNearMe, string filterType, string filter, float radius, bool drawAttackerName = true)
        {
            bool verbose = _verboseMode.ContainsKey(pl.userID) && _verboseMode[pl.userID];
            int raidCount = groupedRaidsNearMe.Sum(x => x.Count());
            _sb.Clear();
            _sb.AppendLine(string.Format(lang.GetMessage("ViewEventsCommand.Header", this, pl.UserIDString), raidCount, radius));

            if (!string.IsNullOrEmpty(filterType))
                _sb.AppendLine(string.Format(lang.GetMessage("ViewEventsCommand.Filter", this, pl.UserIDString), filterType, filter));

            var groupCloseNames = new Dictionary<ulong, List<Vector3>>();
            foreach (var grouping in groupedRaidsNearMe)
            {
                var groupingColor = GetRandomColor();
                var groupingColorHex = GetHexColor(groupingColor);
                foreach (RaidEvent raidEvent in grouping)
                {
                    if (raidEvent.attackerTeamID > 0 && !_teamColors.ContainsKey(raidEvent.attackerTeamID))
                    {
                        _teamColors[raidEvent.attackerTeamID] = _uniqueColors[_currentTeamColorIdx];
                        _currentTeamColorIdx = _currentTeamColorIdx < _uniqueColors.Count() ? _currentTeamColorIdx : -1;
                        _currentTeamColorIdx++;
                    }

                    var teamColor = raidEvent.attackerTeamID > 0 ? _teamColors[raidEvent.attackerTeamID] : GetRandomColor();
                    var teamColorHex = GetHexColor(teamColor);
                    var weaponCfg = FindWeaponConfig(raidEvent.GetTrackerCategory(), raidEvent.GetPrimaryWeaponShortname());
                    var weaponColor = weaponCfg.GetWeaponColor();

                    if (filterType == "weapon")
                    {
                        groupingColor = weaponColor;
                        groupingColorHex = GetHexColor(groupingColor);
                    }
                    else if (filterType == "team")
                    {
                        groupingColor = teamColor;
                        groupingColorHex = GetHexColor(groupingColor);
                    }

                    string startText, endText;
                    var startPos = raidEvent.startPos;
                    var endPos = raidEvent.endPos;

                    if (weaponCfg.shortArrow)
                        startPos = raidEvent.endPos + new Vector3(0, .2f, 0);

                    string attackerTeam = raidEvent.attackerTeamID > 0 ? string.Format(lang.GetMessage("ViewEventsCommand.Team", this, pl.UserIDString), teamColorHex, raidEvent.attackerTeamID) : "";
                    if (verbose)
                    {
                        startText = string.Format(lang.GetMessage("ViewEventsCommand.StartTextExtra", this, pl.UserIDString), drawAttackerName ? raidEvent.attackerName : "X", raidEvent.attackerSteamID, attackerTeam);
                        endText = string.Format(lang.GetMessage("ViewEventsCommand.EndTextExtra", this, pl.UserIDString), groupingColorHex, raidEvent.GetIndex(), raidEvent.GetWeaponName(), raidEvent.hitEntity.Replace(raidEvent.GetEventType(), raidEvent.GetPrettyEventType()));
                    }
                    else
                    {
                        startText = string.Format(lang.GetMessage("ViewEventsCommand.StartText", this, pl.UserIDString), drawAttackerName ? raidEvent.attackerName : "X", attackerTeam);
                        endText = string.Format(lang.GetMessage("ViewEventsCommand.EndText", this, pl.UserIDString), groupingColorHex, raidEvent.GetWeaponName());
                    }

                    if (raidEvent.hitEntity.Contains("EVENT.ATTACHED"))
                        Box(pl, endPos, .05f, groupingColor, _config.drawDuration);
                    else if (raidEvent.hitEntity.Contains("EVENT.HIT"))
                        Sphere(pl, endPos, .05f, groupingColor, _config.drawDuration);

                    if (!groupCloseNames.ContainsKey(raidEvent.attackerSteamID))
                        groupCloseNames[raidEvent.attackerSteamID] = new List<Vector3>();

                    if (!groupCloseNames[raidEvent.attackerSteamID].Any(x => Vector3.Distance(x, startPos) < .1f))
                    {
                        groupCloseNames[raidEvent.attackerSteamID].Add(startPos);
                        Text(pl, startPos, startText, groupingColor, _config.drawDuration);
                    }

                    Arrow(pl, startPos, endPos, .05f, groupingColor, _config.drawDuration);
                    Text(pl, endPos + new Vector3(0, .05f, 0), endText, weaponColor, _config.drawDuration);
                }
                _sb.AppendLine(string.Format(lang.GetMessage("ViewEventsCommand.GroupingCount", this, pl.UserIDString), groupingColorHex, grouping.Count(), grouping.Key.filterType, grouping.Key.filter));

                var weaponCounts = grouping
                    .GroupBy(x => new { weapon = x.GetPrimaryWeaponShortname(), trackerCategory = x.GetTrackerCategory() })
                    .Select(x => new { data = x.Key, count = x.Count() });

                _sb.Append("<indent=6>");
                foreach (var x in weaponCounts)
                {
                    var weaponCfg = FindWeaponConfig(x.data.trackerCategory, x.data.weapon);
                    _sb.Append(string.Format(lang.GetMessage("ViewEventsCommand.WeaponCount", this, pl.UserIDString), weaponCfg.hexColor, x.count, weaponCfg.name, x.data.trackerCategory));
                }
                _sb.Append("<indent=0>");
                _sb.AppendLine($"\n");
            }

            if (raidCount < 1)
                _sb.AppendLine(lang.GetMessage("ViewEventsCommand.NotFound", this, pl.UserIDString));

            SendChatMsg(pl, _sb.ToString().TrimEnd(), "");
        }

        private void LogToSingleFile(string filename, string text) =>
            LogToFile(filename, string.Format("[{0:yyyy-MM-dd HH:mm:ss}] {1}", DateTime.Now, text), this, false);

        private void PrintDebug(string msg)
        {
            if (_debug) Puts($"[DEBUG] {msg}");
        }

        private void SendChatMsg(BasePlayer pl, string msg, string prefix = null)
        {
            var p = prefix != null ? prefix : lang.GetMessage("ChatPrefix", this, pl.UserIDString);
            _rustPlayer.Message(pl, msg, p, _config.chatIconID, Array.Empty<object>());

            if (_config.printToClientConsole)
                pl.ConsoleMessage($"{(!string.IsNullOrEmpty(p) ? $"{p} " : "")}{msg}");
        }

        public void Arrow(BasePlayer player, Vector3 from, Vector3 to, float headSize, Color color, float duration) =>
            player.SendConsoleCommand("ddraw.arrow", duration, color, from, to, headSize);

        public void Sphere(BasePlayer player, Vector3 pos, float radius, Color color, float duration) =>
            player.SendConsoleCommand("ddraw.sphere", duration, color, pos, radius);

        public void Box(BasePlayer player, Vector3 pos, float size, Color color, float duration) =>
            player.SendConsoleCommand("ddraw.box", duration, color, pos, size);

        public void Text(BasePlayer player, Vector3 pos, string text, Color color, float duration) =>
            player.SendConsoleCommand("ddraw.text", duration, color, pos, text);

        private void AddPrefabToItem(string prefab, string itemShortname, string prefabSource)
        {
            var prefabShortname = GetPrefabShortname(prefab);
            if (_prefabToItem.ContainsKey(prefabShortname)) return;

            _prefabToItem[prefabShortname] = itemShortname;
            PrintDebug($"prefabToItem - {prefabSource}: {prefabShortname} -> {itemShortname}");
        }

        private string GetItemFromPrefabShortname(string prefabShortname) => 
            _prefabToItem.ContainsKey(prefabShortname) ? _prefabToItem[prefabShortname] : prefabShortname;

        private string GetPrefabShortname(string prefab) =>
            prefab.Substring(prefab.LastIndexOf('/') + 1).Replace(".prefab", "");

        private string GetPrettyItemName(string itemShortname)
        {
            string buildingBlockName;
            if (_buildingBlockPrettyNames.TryGetValue(itemShortname, out buildingBlockName))
                return buildingBlockName;

            return ItemManager.FindItemDefinition(itemShortname)?.displayName?.english ?? itemShortname;
        }

        private IEnumerable<RaidEvent> FindRaidEventsInSphere(Vector3 pos, float r) =>
            _raidEventLog.Where(x => Vector3.Distance(pos, x.startPos) < r || Vector3.Distance(pos, x.endPos) < r);

        private Color GetRandomColor() => UnityEngine.Random.ColorHSV(0f, 1f, .4f, .8f, .5f, 1f);

        private string GetHexColor(Color color) => $"#{ColorUtility.ToHtmlStringRGB(color)}";

        private PluginConfig.WeaponConfig AddOrFindWeaponConfig(string category, string shortname)
        {
            if (!_config.trackers.ContainsKey(category))
                _config.trackers[category] = new SortedDictionary<string, PluginConfig.WeaponConfig>();

            if (!_config.trackers[category].ContainsKey(shortname))
            {
                var weaponCfg = new PluginConfig.WeaponConfig {
                    enabled = _config.enableNewTrackers,
                    name = GetPrettyItemName(GetItemFromPrefabShortname(shortname)),
                    hexColor = GetHexColor(GetRandomColor())
                };
                _config.trackers[category][shortname] = weaponCfg;

                LogToSingleFile("weapon_config_log", $"added {weaponCfg.name} ({category} / {shortname}), enabled: {weaponCfg.enabled}");
                SaveConfig();
            }

            return FindWeaponConfig(category, shortname);
        }

        private PluginConfig.WeaponConfig FindWeaponConfig(string category, string shortname)
        {
            PluginConfig.WeaponConfig weaponCfg;
            if (_config.trackers.ContainsKey(category) && _config.trackers[category].TryGetValue(shortname, out weaponCfg))
            {
                if (_dev)
                {
                    return new PluginConfig.WeaponConfig {
                        enabled = true,
                        name = weaponCfg.name,
                        hexColor = weaponCfg.hexColor,
                        alwaysLog = weaponCfg.alwaysLog,
                        shortArrow = weaponCfg.shortArrow,
                        discordIcon = weaponCfg.discordIcon,
                        notifyConsole = true,
                        notifyAdmin = true,
                        notifyDiscord = true,
                        logToFile = true
                    };
                }

                PluginConfig.WeaponConfig globalWeaponCfg = null;
                if (_config.trackers.ContainsKey("_global") && _config.trackers["_global"].ContainsKey("*"))
                    globalWeaponCfg = _config.trackers["_global"]["*"];
                    
                if ((globalWeaponCfg == null || !globalWeaponCfg.enabled) && _config.trackers[category].ContainsKey("*"))
                    globalWeaponCfg = _config.trackers[category]["*"];

                if (!weaponCfg.enabled && globalWeaponCfg.enabled) {
                    return new PluginConfig.WeaponConfig {
                        enabled = globalWeaponCfg.enabled,
                        name = weaponCfg.name,
                        hexColor = weaponCfg.hexColor,
                        alwaysLog = weaponCfg.alwaysLog,
                        shortArrow = weaponCfg.shortArrow,
                        discordIcon = weaponCfg.discordIcon,
                        notifyConsole = globalWeaponCfg.notifyConsole,
                        notifyAdmin = globalWeaponCfg.notifyAdmin,
                        notifyDiscord = globalWeaponCfg.notifyDiscord,
                        logToFile = globalWeaponCfg.logToFile
                    };
                }
                return weaponCfg;
            }
            throw new Exception($"THIS SHOULD NEVER HAPPEN! Unable to find weapon config [{category} / {shortname}]");
        }

        private string GetDecayEntityShortname(DecayEntity entity)
        {
            var buildingBlock = entity as BuildingBlock;
            return $"{entity.ShortPrefabName}{(buildingBlock != null ? $".{buildingBlock.grade.ToString().ToLower()}" : "")}";
        }

        private bool IsDecayEntityIgnored(DecayEntity entity)
        {
            if (entity is LootContainer || entity.OwnerID == 0)
                return true;

            if (!_dev && _decayEntityIgnoreList.ContainsKey(entity.PrefabName) && _decayEntityIgnoreList[entity.PrefabName].ignore)
                return true;

            var buildingBlock = entity as BuildingBlock;
            bool ignoreGrade;
            if (!_dev && buildingBlock != null && _config.ignoreBuildingGrades.TryGetValue(buildingBlock.grade, out ignoreGrade) && ignoreGrade)
                return true;

            if (Convert.ToBoolean(RaidableBases?.Call("EventTerritory", entity.transform.position)))
                return true;

            if (Convert.ToBoolean(AbandonedBases?.Call("EventTerritory", entity.transform.position)))
                return true;

            return false;
        }

        private bool IsDecayEntityOrAttackerIgnored(DecayEntity entity, BasePlayer attacker)
        {
            if (IsDecayEntityIgnored(entity))
                return true;

            if (attacker != null && attacker.IsAdmin)
                return false;

            if (!_dev && _config.ignoreSameOwner && attacker != null && attacker.userID == entity.OwnerID)
                return true;

            if (!_dev && _config.ignoreTeamMember && attacker != null && attacker.Team != null && attacker.Team.members.Contains(entity.OwnerID))
                return true;

            if (!_dev && _config.ignoreClanMemberOrAlly && attacker != null && Convert.ToBoolean(Clans?.Call("IsMemberOrAlly", attacker.UserIDString, entity.OwnerID.ToString())))
                return true;

            return false;
        }

        private string FormatPosition(Vector3 pos)
        {
            return string.Format("{0:F1},{1:F1},{2:F1}", new object[] {
                pos.x,
                pos.y,
                pos.z
            });
        }

        private string StringReplaceKeys(string str, Dictionary<string, string> kv)
        {
            foreach (var x in kv)
                str = str.Replace($"{{{x.Key}}}", x.Value);
            return str;
        }

        private void SaveRaidEventLog()
        {
            if (_raidEventLog.Count == _raidEventLogCount) return;

            Interface.Oxide.DataFileSystem.WriteObject(_raidEventLogFilename, _raidEventLog);
            _raidEventLogCount = _raidEventLog.Count;
        }

        #endregion

        #region Config

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string> {
                ["ChatPrefix"] = $"<color=#00a7fe>[{Title}]</color>",
                ["RaidEvent.Message"] = "{attackerName}[{attackerSteamID}] is raiding {victimName}[{victimSteamID}] ~ {weaponName} -> {hitEntity} @ {gridPos} (teleportpos {teleportPos})",
                ["RaidEvent.PrettyMessage"] = "<color=#f5646c>{attackerName}[{attackerSteamID}]</color> is raiding <color=#52bf6f>{victimName}[{victimSteamID}]</color> ~ <color={weaponColor}>{weaponName}</color> {raidEventType} <color=#00a7fe>{entityItemName}</color> ({entityShortname}) @ <color=#00a7fe>{gridPos}</color>",
                ["ViewEventsCommand.HelpHeader"] = $"<size=16><color=#00a7fe>{Title}</color> Help</size>\n",
                ["ViewEventsCommand.HelpDefault"] = "<size=12><color=#00a7fe>/x <radius></color> - Show all raid events within X radius (default 50m)</size>",
                ["ViewEventsCommand.HelpExtraMode"] = "<size=12><color=#00a7fe>/x extra</color> - Toggle extra info mode</size>",
                ["ViewEventsCommand.HelpWipe"] = "<size=12><color=#00a7fe>/x wipe <radius></color> - Wipe all raid events within <radius></size>",
                ["ViewEventsCommand.HelpLast"] = "<size=12><color=#00a7fe>/x last</color> - Re-run last command</size>",
                ["ViewEventsCommand.HelpFilter"] = "<size=12><color=#00a7fe>/x <filterType> <filter> <radius></color></size>",
                ["ViewEventsCommand.HelpFilterTime"] = "<size=12><color=#00a7fe>/x time <hrs> <radius></color> - Show all raid events near by over the past <hrs> within <radius></size>",
                ["ViewEventsCommand.HelpFilterWeapon"] = "<size=12><color=#00a7fe>/x weapon <partial name or item name> <radius></color> - Show all raid events within <radius> filtered by weapon</size>",
                ["ViewEventsCommand.HelpFilterEntity"] = "<size=12><color=#00a7fe>/x entity <partial entity shortname> <radius></color> - Show all raid events within <radius> filtered by entity</size>",
                ["ViewEventsCommand.HelpFilterTeam"] = "<size=12><color=#00a7fe>/x team <team id> <radius></color> - Show all raid events within <radius> filtered by team</size>",
                ["ViewEventsCommand.HelpFilterPlayer"] = "<size=12><color=#00a7fe>/x player <steam id or partial name> <radius></color> - Show all raid events within <radius> filtered by player</size>",
                ["ViewEventsCommand.HelpPrintRaidEvent"] = "<size=12><color=#00a7fe>/re <event id></color> - Print info about a raid event by event id</size>",
                ["ViewEventsCommand.ExtraModeEnabled"] = "<color=#52bf6f>Extra info mode enabled</color>",
                ["ViewEventsCommand.ExtraModeDisabled"] = "<color=#f5646c>Extra info mode disabled</color>",
                ["ViewEventsCommand.WipePermission"] = "You do not have permission to wipe raid events!",
                ["ViewEventsCommand.NotFoundRadius"] = "No raid events found within <color=#00a7fe>{0}m</color>!",
                ["ViewEventsCommand.WipedRaidEventsRadius"] = "Wiped <color=#00a7fe>{0}</color> raid events within <color=#00a7fe>{1}m</color> at <color=#00a7fe>{2}</color>",
                ["ViewEventsCommand.Header"] = $"<size=16><color=#00a7fe>{Title}</color> ~ {{0}} raid event(s) within {{1}}m</size>\n",
                ["ViewEventsCommand.Filter"] = "<color=#00a7fe>filter:</color> [{0}, {1}]\n",
                ["ViewEventsCommand.Team"] = "<color={0}> T:{1}</color>",
                ["ViewEventsCommand.StartTextExtra"] = "<size=12>{0}[{1}]{2}</size>",
                ["ViewEventsCommand.EndTextExtra"] = "<size=12><color={0}>X</color> (RE:{1}) {2} <color={0}>~</color> {3}</size>",
                ["ViewEventsCommand.StartText"] = "<size=12>{0}{1}</size>",
                ["ViewEventsCommand.EndText"] = "<size=12><color={0}>X</color> {1}</size>",
                ["ViewEventsCommand.GroupingCount"] = "<color={0}>{1} raid event(s) [{2}, {3}]</color>",
                ["ViewEventsCommand.WeaponCount"] = "<color={0}>• {1}x {2} <size=10>({3})</size> </color>",
                ["ViewEventsCommand.NotFound"] = "no raid events found!",
                ["ViewEventsCommand.NoPermission"] = "You do not have permission to use <color=#00a7fe>{0}</color>!"
            }, this);
        }

        private PluginConfig GetDefaultConfig()
        {
            return new PluginConfig();
        }

        protected override void LoadDefaultConfig()
        {
            Log($"Loading default configuration");
            _config = GetDefaultConfig();
        }

        protected override void LoadConfig()
        {
            _instance = this;
            base.LoadConfig();
            try
            {
                _config = Config.ReadObject<PluginConfig>();
                if (_config == null)
                {
                    throw new JsonException();
                }

                if (MaybeUpdateConfig(_config))
                {
                    var backupConfigFilename = $"{Name}\\ConfigBackup\\{Name}_{DateTime.Now:yyyy-M-dd_HH-mm-ss}";
                    LogWarning("Configuration appears to be outdated; updating and saving");
                    LogWarning($"Saving configuration backup to /oxide/data/{Utility.CleanPath(backupConfigFilename)}.json");
                    Interface.Oxide.DataFileSystem.WriteObject(backupConfigFilename, _config);
                    SaveConfig();
                }
                else
                    Log("Configuration is up to date");

                var webhookURL = _config.discord.webhookURL;
                Puts($"Discord webhook {(!string.IsNullOrEmpty(webhookURL) && !webhookURL.Contains("Intro-to-Webhooks") ? "enabled" : "disabled")}!");
            }
            catch
            {
                LogWarning($"Configuration file {Name}.json is invalid; using defaults");
                _isConfigValid = false;
                LoadDefaultConfig();
            }
        }

        protected override void SaveConfig()
        {
            Log($"Configuration changes saved to {Name}.json");
            Config.WriteObject(_config, true);
        }

        private class PluginConfig : SerializableConfiguration
        {
            public bool debug = false;
            public ulong chatIconID = 76561############;
            public bool deleteDataOnWipe = true;
            public float daysBeforeDelete = 7f;
            public float searchRadius = 50f;
            public float drawDuration = 30f;

            [JsonProperty(ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public Dictionary<BuildingGrade.Enum, bool> ignoreBuildingGrades = new Dictionary<BuildingGrade.Enum, bool> {
                { BuildingGrade.Enum.Twigs, true },
                { BuildingGrade.Enum.Wood, false },
                { BuildingGrade.Enum.Stone, false },
                { BuildingGrade.Enum.Metal, false },
                { BuildingGrade.Enum.TopTier, false }
            };

            public bool ignoreSameOwner = true;
            public bool ignoreTeamMember = true;
            public bool ignoreClanMemberOrAlly = true;
            public bool enableNewTrackers = true;
            public bool printToClientConsole = true;
            public PlayerViewExplosionsCommand playerViewExplosionsCommand = new PlayerViewExplosionsCommand {
                drawAttackerName = false,
                ignoreRaidEventsLessThanMinutes = 30f
            };
            public NotificationCooldown notificationCooldown = new NotificationCooldown {
                enabled = false,
                cooldown = 300f
            };

            public DiscordConfig discord = new DiscordConfig {
                webhookURL = "https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks",
                simpleMessage = new DiscordSimpleMessage {
                    enabled = false,
                    message = "{attackerName}[{attackerSteamID}] is raiding {victimName}[{victimSteamID}] ~ {weaponName} -> {raidEventType} {entityItemName} ({entityShortname}) @ {gridPos} (teleportpos {teleportPos})"
                },
                embed = new DiscordEmbed {
                    title = "{attackerName} is raiding {victimName} @ {gridPos}",
                    thumbnail = new DiscordEmbedThumbnail {
                        url = "https://www.rustedit.io/images/imagelibrary/{weaponItemShortname}.png"
                    },
                    fields = new List<DiscordEmbedField> {
                        {
                            new DiscordEmbedField {
                                name = "Weapon",
                                value = "{weaponName} ({raidTrackerCategory} / {weaponShortname})",
                                inline = false
                            }
                        },
                        {
                            new DiscordEmbedField {
                                name = "Entity",
                                value = "{raidEventType} {entityItemName} ({entityShortname})",
                                inline = false
                            }
                        },
                        {
                            new DiscordEmbedField {
                                name = "Attacker",
                                value = "{attackerName} \n[Steam Profile](https://steamcommunity.com/profiles/{attackerSteamID}) ({attackerSteamID})\n[SteamID.uk](https://steamid.uk/profile/{attackerSteamID})\n\n**Attacker Team**\n{attackerTeamName}",
                                inline = true
                            }
                        },
                        {
                            new DiscordEmbedField {
                                name = "Victim",
                                value = "{victimName} \n[Steam Profile](https://steamcommunity.com/profiles/{victimSteamID}) ({victimSteamID})\n[SteamID.uk](https://steamid.uk/profile/{victimSteamID})\n\n**Victim Team**\n{victimTeamName}",
                                inline = true
                            }
                        },
                        {
                            new DiscordEmbedField {
                                name = "Location",
                                value = "{gridPos} - teleportpos {teleportPos}",
                                inline = false
                            }
                        }
                    },
                    footer = new DiscordEmbedFooter {
                        text = $"{_instance.Title} {{{0}}} by {_instance.Author}",
                        icon_url = "https://i.imgur.com/DluJ5X5.png"
                    }
                }
            };

            [JsonProperty(ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public SortedDictionary<string, SortedDictionary<string, We

Seems to be cut off at the bottom.

Sorry, yeah must be a character limit - give this one a go https://pastebin.com/7S3RB8Xw

I've also just done a PR on the authors Github for the same.

Awesome...thank you !

Appears to work, the plugin compiles now. Yay! Thank you!
It was fairly easy to narrow necessary edits of the original plugin file by checking for all of the occurrences of "PhoneController.PositionToGridCoord" and then only copy changed lines around that respective occurrence. Seemed more sane to me than just copy/paste everything. Thanks for providing that pastebin, helped a lot! šŸ˜€

It's loading fine, but not reportind Raid's in Discord, nor in Game as well.

IĀ  just tested the fix myself. Raid Tracker with the proposed fix is working fine - my raid attempt was sent to Discord just fine. I don't broadcast ingame hence didn't check that.

Locked automatically