Иконка ресурса

FREE DamageControl : 3.0.2 3.0.2

Нет прав для скачивания
C#:
using System;
using System.Collections.Generic;
using System.Linq;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Configuration;
using Oxide.Core.Plugins;
using UnityEngine;
using Rust;

namespace Oxide.Plugins
{
    [Info("DamageControl", "MSpeedie", "3.0.2")]
    [Description("Group-based damage control with distinct NPCs, animals, and Bradley APC settings")]
    class DamageControl : CovalencePlugin
    {
        #region Constants

        private const string PermAdmin = "damagecontrol.admin";
        private const int DamageTypeCount = (int)DamageType.LAST;
        private const int HoursPerDay = 24;

        // Lowest valid individual SteamID64. Real players are >= this value; // server-created fake players / bots are assigned IDs below it.
        private const ulong SteamIdBase = 76561197960265728UL;

        #endregion

        #region Data Structures

        private class EntityGroup
        {
            public string Key { get; set; }
            public string DisplayName { get; set; }
            public string TimeCategory { get; set; }
            public Func<BaseCombatEntity, bool> Matcher { get; set; }
            public Dictionary<string, float> DamageMultipliers { get; set; }
            public HashSet<string> KnownPrefabs { get; set; }

            // False for groups whose classification depends on per-instance state (i.e. real player vs. plugin bot vs. horde member all sharing the "player" prefab). Such groups must never be cached by prefab name.
            public bool Cacheable { get; set; } = true;
        }

        #endregion

        #region Fields

        private Dictionary<string, Dictionary<string, object>> _config;
        private readonly DynamicConfigFile _dataFile = Interface.Oxide.DataFileSystem.GetFile("DamageControl_Overrides");

        // Keyed lookup (for commands) and ordered list (for deterministic matching priority).
        private Dictionary<string, EntityGroup> _entityGroups = new Dictionary<string, EntityGroup>();
        private readonly List<EntityGroup> _orderedGroups = new List<EntityGroup>();

        private Dictionary<string, float[]> _timeMultipliers = new Dictionary<string, float[]>();
        private Dictionary<string, float> _prefabOverrides = new Dictionary<string, float>();

        private readonly Dictionary<string, EntityGroup> _entityCache = new Dictionary<string, EntityGroup>();
        private readonly HashSet<string> _unknownEntities = new HashSet<string>();
        private readonly Dictionary<string, int> _missingCounts = new Dictionary<string, int>();

        private List<string> _damageTypes;
        private string[] _damageTypeNames;
        private readonly HashSet<string> _deployableList = new HashSet<string>();

        // Reusable buffer so the per-hit bot/zombie component scans don't allocate.
        private readonly List<Component> _componentBuffer = new List<Component>();

        private bool _EnforceDecayDamage = true;
        private bool _HeliBypass = false;

        // In-game hour, refreshed every 30s by a timer so the per-hit path
        // does a single field read instead of a TOD_Sky property chain.
        private int _cachedHour = 0;

        // Prefab-name bases for the standalone zombie agent (Zombie : BaseAnimalNPC),
        // covering "zombie", "zombie.corpse", and numbered variants.
        private static readonly string[] ZombieBases = { "zombie" };

        #endregion

        #region Initialization

        private void Init()
        {
            try
            {
                if (!permission.PermissionExists(PermAdmin))
                    permission.RegisterPermission(PermAdmin, this);

                InitializeDamageTypes();
                BuildEntityGroups();

                LoadConfiguration();
                LoadPrefabOverrides();

                UpdateCachedHour();                 // prime before first hit
                timer.Every(30f, UpdateCachedHour); // refresh every 30 seconds

                Puts($"DamageControl initialized with {_entityGroups.Count} entity groups");
                Puts($"Heli Bypass: {(_HeliBypass ? "Enabled" : "Disabled")}");
            }
            catch (Exception ex)
            {
                LogError($"Initialization error: {ex.Message}");
            }
        }

        // ItemManager is not reliably populated during Init() on a cold server
        // boot, which would leave _deployableList empty until the next reload.
        // Building it here (also called on hot-reload) fixes that.
        private void OnServerInitialized()
        {
            BuildDeployableList();
        }

        private void InitializeDamageTypes()
        {
            _damageTypes = Enum.GetValues(typeof(DamageType))
                .Cast<DamageType>()
                .Where(dt => dt != DamageType.LAST)
                .Select(dt => dt.ToString())
                .ToList();

            _damageTypeNames = new string[DamageTypeCount];
            for (int i = 0; i < DamageTypeCount; i++)
            {
                _damageTypeNames[i] = ((DamageType)i).ToString();
            }
        }

        private void BuildEntityGroups()
        {
            _entityGroups.Clear();
            _orderedGroups.Clear();

            // ===== PLAYERS =====
            // Not cacheable: shares the "player" prefab with bots / horde members.
            AddGroup("player", "Players", "Player",
                e => e is BasePlayer && !IsNPC(e) && !IsBot(e) && !IsZombie(e) && !IsScarecrow(e),
                cacheable: false);

            // ===== NPCS (Distinct - specific first) =====
            AddGroup("excavatorscientist", "Excavator Scientists", "NPC",
                e => (e is HumanNPC || e is NPCPlayer || e is Rust.Ai.Gen2.BaseNPC2 || IsNPC(e) || IsBot(e)) &&
                     !IsZombie(e) && !IsScarecrow(e) &&
                     (e.ShortPrefabName.Contains("excavator") || e.PrefabName.Contains("excavator")) &&
                     (e.ShortPrefabName.Contains("scientist") || e.PrefabName.Contains("scientist")));

            AddGroup("excavatorguard", "Excavator Guards", "NPC",
                e => (e is HumanNPC || e is NPCPlayer || e is Rust.Ai.Gen2.BaseNPC2 || IsNPC(e) || IsBot(e)) &&
                     !IsZombie(e) && !IsScarecrow(e) &&
                     (e.ShortPrefabName.Contains("excavator") || e.PrefabName.Contains("excavator")) &&
                     (e.ShortPrefabName.Contains("guard") || e.PrefabName.Contains("guard")));

            AddGroup("tunneldweller", "Tunnel Dwellers", "NPC",
                e => e.ShortPrefabName.Contains("tunneldweller") || e.PrefabName.Contains("tunneldweller"));

            AddGroup("banditguard", "Bandit Camp Guards", "NPC",
                e => e.ShortPrefabName.Contains("banditguard") || e.PrefabName.Contains("banditguard") ||
                     e.ShortPrefabName.Contains("bandit") || e.PrefabName.Contains("bandit"));

            AddGroup("outpostscientist", "Outpost Scientists", "NPC",
                e => e.ShortPrefabName.Contains("outpost") && (e.ShortPrefabName.Contains("scientist") || e.PrefabName.Contains("scientist")));

            AddGroup("militaryscientist", "Military Tunnel Scientists", "NPC",
                e => e.ShortPrefabName.Contains("military") && (e.ShortPrefabName.Contains("scientist") || e.PrefabName.Contains("scientist")));

            AddGroup("junkpilescientist", "Junkpile Scientists", "NPC",
                e => e.ShortPrefabName.Contains("junkpile") && (e.ShortPrefabName.Contains("scientist") || e.PrefabName.Contains("scientist")));

            AddGroup("underwaterdweller", "Underwater Dwellers", "NPC",
                e => e.ShortPrefabName.Contains("underwater") && e.ShortPrefabName.Contains("dweller"));

            // Not cacheable: horde members can be BasePlayers sharing the "player" prefab.
            // Core "zombie" agent + its corpse (by name), plus plugin horde zombies
            // (HordeMember on a BasePlayer) and the Zombie type via IsZombie. Many
            // modded servers run zombies year-round. Scarecrow-based zombies stay under
            // the "scarecrow" group; murderers under "murderer".
            // Non-cacheable: the HordeMember branch can ride the shared "player" prefab.
            AddGroup("zombie", "Zombies", "NPC",
                e => (MatchesFamily(e.ShortPrefabName, ZombieBases) || IsZombie(e))
                     && !IsScarecrow(e) && e.ShortPrefabName != "murderer",
                cacheable: false);

            AddGroup("murderer", "Murderers", "NPC",
                e => e.ShortPrefabName == "murderer");

            AddGroup("scarecrow", "Scarecrows", "NPC",
                e => IsScarecrow(e));

            // Generic scientist catch-all (last)
            AddGroup("scientist", "Scientists", "NPC",
                e => (e is HumanNPC || e is NPCPlayer || e is Rust.Ai.Gen2.BaseNPC2 || IsNPC(e) || IsBot(e)) &&
                     !IsZombie(e) && !IsScarecrow(e) &&
                     !e.ShortPrefabName.Contains("excavator") && !e.PrefabName.Contains("excavator") &&
                     !e.ShortPrefabName.Contains("tunneldweller") && !e.PrefabName.Contains("tunneldweller") &&
                     !e.ShortPrefabName.Contains("bandit") && !e.PrefabName.Contains("bandit") &&
                     !e.ShortPrefabName.Contains("outpost") && !e.PrefabName.Contains("outpost") &&
                     !e.ShortPrefabName.Contains("military") && !e.PrefabName.Contains("military") &&
                     !e.ShortPrefabName.Contains("junkpile") && !e.PrefabName.Contains("junkpile") &&
                     !e.ShortPrefabName.Contains("underwater") && !e.PrefabName.Contains("underwater") &&
                     (e.ShortPrefabName.Contains("scientist") || e.PrefabName.Contains("scientist")));

            // ===== ANIMALS (exact short-prefab match) =====
            // Exact names verified against current prefab list (Jungle, May 2025).
            // Avoids the polarbear/bear substring hazard and the shark/simpleshark
            // trap, and makes tiger/panther match at all. Add new animals by name;
            // until then an unknown one is logged under /dc missing.
            // Family match covers the live agent, its corpse (".corpse"), and gen2
            // numbered prefabs (e.g. "wolf2", "wolf2.corpse") without the substring
            // hazards of Contains. Shark's live agent is "simpleshark" but its corpse
            // is "shark.corpse", so both bases are listed.
            AddGroupFamily("polarbear", "Polar Bears", "Animal", "polarbear");
            AddGroupFamily("bear",      "Bears",       "Animal", "bear");
            AddGroupFamily("wolf",      "Wolves",      "Animal", "wolf");
            AddGroupFamily("boar",      "Boars",       "Animal", "boar");
            AddGroupFamily("chicken",   "Chickens",    "Animal", "chicken");
            AddGroupFamily("stag",      "Stags/Deer",  "Animal", "stag");
            AddGroupFamily("shark",     "Sharks",      "Animal", "simpleshark", "shark");
            AddGroupFamily("crocodile", "Crocodiles",  "Animal", "crocodile");
            // Jungle big cats are two distinct agents; kept under the existing
            // "bigcat" config key so any tuned multipliers carry over.
            AddGroupFamily("bigcat",    "Big Cats (Tiger/Panther)", "Animal", "tiger", "panther");
            AddGroupFamily("snake",     "Snakes",      "Animal", "snake"); // live "snake.entity" + "snake.corpse"

            // Horses left on fuzzy match: the ridable-horse prefab stem has changed
            // across versions (testridablehorse -> ridablehorse -> ...), so an exact
            // list is a maintenance liability. Pin exact names if your build is stable.
            AddGroup("ridablehorse", "Ridable Horses", "Animal",
                e => IsAnimal(e) && e.ShortPrefabName.Contains("ridablehorse"));

            AddGroup("horse", "Horses", "Animal",
                e => IsAnimal(e) && e.ShortPrefabName.Contains("horse"));

            // ===== SPECIFIC NPC VEHICLES (MUST BE BEFORE GENERIC VEHICLES) =====
            AddGroup("apc", "APC/Bradley", "Bradley",
                e => e is BradleyAPC);

            // Staging heli refactor: PatrolHelicopter is now a standalone
            // BaseCombatEntity (no longer derived from BaseHelicopter), so match it
            // by its own type. BaseHelicopter now covers CH47 and the player copters.
            AddGroup("patrolheli", "Patrol Helicopter", "Heli",
                e => e is PatrolHelicopter);

            AddGroup("chinook", "Chinook/CH47", "Chinook",
                e => e is CH47Helicopter);

            AddGroup("train", "Train", "Train",
                e => e is TrainCar);

            // ===== GENERIC VEHICLES (Grouped by type - AFTER specific NPC vehicles) =====
            // Player-piloted rotorcraft (minicopter, scrap heli, attack heli) now
            // derive from PlayerHelicopter : BaseHelicopter. Catch those by type;
            // name-match the rest (hot air balloon, plane) but not the AI helis.
            AddGroup("airvehicle", "Air Vehicles", "Vehicle",
                e => e is PlayerHelicopter ||
                     (IsAirVehicle(e) && !(e is BaseHelicopter) && !(e is PatrolHelicopter)));

            AddGroup("landvehicle", "Land Vehicles", "Vehicle",
                e => IsLandVehicle(e));

            // Type-based boat/sub match keeps this robust to new hulls (e.g. the
            // Naval-update PlayerBoat : BaseBoat) without a per-prefab entry.
            AddGroup("watervehicle", "Water Vehicles", "Vehicle",
                e => e is BaseBoat || e is BaseSubmarine || IsWaterVehicle(e));

            // Removed dead "npcvehicle" group: patrolheli / chinook / train already
            // claim PatrolHelicopter / CH47Helicopter / TrainCar earlier in the
            // ordered list, so this group could never match.

            // ===== BUILDINGS (Grouped - specific first) =====
            AddGroup("toolcupboard", "Tool Cupboards", "Building",
                e => e.ShortPrefabName.Contains("cupboard.tool"));

            AddGroup("highexternal", "High External Walls/Gates", "Building",
                e => e.ShortPrefabName.Contains("external"));

            AddGroup("door", "Doors/Hatches", "Building",
                e => e is Door || e.ShortPrefabName.Contains("hatch"));

            AddGroup("buildingpart", "Building Parts", "Building",
                e => e is BuildingBlock && !(e is Door) && !e.ShortPrefabName.Contains("external"));

            AddGroup("building", "All Buildings", "Building",
                e => e is BuildingBlock || e is Door);

            // ===== DEPLOYABLES (Grouped - specific first) =====
            AddGroup("trap", "Traps/Barricades", "Building",
                e => e is Barricade || e.ShortPrefabName.Contains("trap") ||
                     e.ShortPrefabName.Contains("spikes") || e.ShortPrefabName.Contains("landmine") ||
                     e.ShortPrefabName.Contains("beartrap") || e.ShortPrefabName.Contains("barricade"));

            AddGroup("turret", "Turrets", "Building",
                e => e.ShortPrefabName.Contains("turret"));

            AddGroup("furnace", "Furnaces/Campfires", "Building",
                e => e is BaseOven || e.ShortPrefabName.Contains("furnace") ||
                     e.ShortPrefabName.Contains("campfire") || e.ShortPrefabName.Contains("lantern"));

            AddGroup("workbench", "Workbenches", "Building",
                e => e.ShortPrefabName.Contains("workbench") || e.ShortPrefabName.Contains("repairbench"));

            AddGroup("storage", "Storage/Vending/Recycler", "Building",
                e => e.ShortPrefabName.Contains("vending") || e.ShortPrefabName.Contains("recycler"));

            AddGroup("resource", "Resource Gatherers", "Building",
                e => e.ShortPrefabName.Contains("quarry") || e.ShortPrefabName.Contains("pumpjack"));

            // Excavator
            AddGroup("excavator", "Excavator", "Building",
                e => e.ShortPrefabName.Contains("excavator") || e.PrefabName.Contains("excavator"));

            AddGroup("deployable", "All Deployables", "Building",
                e => IsDeployable(e) || IsKnownDeployable(e));

            // ===== LOOT (Grouped - specific first) =====
            AddGroup("barrel", "Barrels", "Other",
                e => e is LootContainer && e.ShortPrefabName.Contains("barrel"));

            AddGroup("crate", "Crates/Loot Boxes", "Other",
                e => e is LootContainer && (e.ShortPrefabName.Contains("crate") || e.ShortPrefabName.Contains("loot")));

            AddGroup("trashpile", "Trash Piles", "Other",
                e => e is LootContainer && (e.ShortPrefabName.Contains("trash") || e.ShortPrefabName.Contains("junk")));

            AddGroup("loot", "All Loot Containers", "Other",
                e => e is LootContainer || IsLootContainer(e));

            // ===== MISC =====
            AddGroup("samsite", "SAM Sites", "Other",
                e => e.ShortPrefabName.Contains("sam_site") || e.ShortPrefabName.Contains("sam_static"));

            AddGroup("supplydrop", "Supply Drops", "Other",
                e => e.ShortPrefabName.Contains("supplydrop") || e.ShortPrefabName.Contains("supply_drop"));
        }

        private void AddGroup(string key, string displayName, string timeCategory,
                              Func<BaseCombatEntity, bool> matcher, bool cacheable = true)
        {
            var group = new EntityGroup
            {
                Key = key,
                DisplayName = displayName,
                TimeCategory = timeCategory,
                Matcher = matcher,
                DamageMultipliers = new Dictionary<string, float>(),
                KnownPrefabs = new HashSet<string>(),
                Cacheable = cacheable
            };

            // Guard against accidental duplicate keys so the ordered list stays in sync.
            if (_entityGroups.ContainsKey(key))
                _orderedGroups.RemoveAll(g => g.Key == key);

            _entityGroups[key] = group;
            _orderedGroups.Add(group);
        }

        // Exact short-prefab-name match, for stable well-known entities.
        // Avoids substring false-positives (rug.bear), the polarbear/bear
        // ordering hazard, and the "shark" != "simpleshark" trap. Cacheable:
        // prefab -> group is deterministic for these.
        // Family match for stable, well-known entities. A prefab matches a base
        // name when it equals the base, or the base is followed by '.' (variants
        // and corpses: "wolf.corpse") or a digit (gen2 prefabs: "wolf2",
        // "wolf2.corpse"). The boundary check keeps "polarbear"/"beartrap" out of
        // the "bear" family and preserves exact-match precision. Cacheable.
        private static bool MatchesFamily(string shortName, string[] bases)
        {
            if (string.IsNullOrEmpty(shortName)) return false;
            foreach (var b in bases)
            {
                if (shortName.Length < b.Length) continue;
                if (!shortName.StartsWith(b, StringComparison.OrdinalIgnoreCase)) continue;
                if (shortName.Length == b.Length) return true;          // "wolf"
                char c = shortName[b.Length];
                // boundary: '.' (corpse/variant), digit (gen2: "wolf2"),
                // '_' (tutorial variants: "bear_tutorial"). Verified against a full
                // staging prefab dump: no false positives (beartrap, chickencoop,
                // horsesaddle, shark_unused-excluded-by-intent all stay correct).
                if (c == '.' || c == '_' || char.IsDigit(c)) return true;
            }
            return false;
        }

        private void AddGroupFamily(string key, string displayName, string timeCategory,
                                    params string[] bases)
        {
            AddGroup(key, displayName, timeCategory, e => MatchesFamily(e.ShortPrefabName, bases), cacheable: true);
        }

        private bool IsAnimal(BaseCombatEntity entity)
        {
            string prefab = entity.ShortPrefabName.ToLower();
            return entity.PrefabName.Contains("assets/rust.ai/") ||
                   prefab.Contains("bear") || prefab.Contains("wolf") ||
                   prefab.Contains("boar") || prefab.Contains("chicken") ||
                   prefab.Contains("horse") || prefab.Contains("stag") ||
                   prefab.Contains("shark") || prefab.Contains("snake") ||
                   prefab.Contains("bigcat") || prefab.Contains("crocodile") ||
                   prefab.Contains("polarbear");
        }

        private bool IsAirVehicle(BaseCombatEntity entity)
        {
            string prefab = entity.ShortPrefabName.ToLower();
            return prefab.Contains("minicopter") || prefab.Contains("scraptransport") ||
                   prefab.Contains("attackhelicopter") || prefab.Contains("hotairballoon") ||
                   prefab.Contains("plane");
        }

        private bool IsLandVehicle(BaseCombatEntity entity)
        {
            string prefab = entity.ShortPrefabName.ToLower();
            return prefab.Contains("snowmobile") || prefab.Contains("tomaha") ||
                   prefab.Contains("motorbike") || prefab.Contains("pedalbike") ||
                   prefab.Contains("pedaltrike") || prefab.Contains("sedan") ||
                   prefab.Contains("module") || prefab.Contains("batteringram") ||
                   prefab.Contains("ballista") || prefab.Contains("catapult") ||
                   prefab.Contains("siegetower") || prefab.Contains("dpv");
        }

        private bool IsWaterVehicle(BaseCombatEntity entity)
        {
            string prefab = entity.ShortPrefabName.ToLower();
            return prefab.Contains("rowboat") || prefab.Contains("motor_rowboat") ||
                   prefab.Contains("sailboat") || prefab.Contains("rhib") ||
                   prefab.Contains("kayak") || prefab.Contains("tugboat") ||
                   prefab.Contains("submarine") || prefab.Contains("ptboat");
        }

        private bool IsScarecrow(BaseCombatEntity entity)
        {
            return entity.ShortPrefabName.Contains("scarecrow");
        }

        private bool IsKnownDeployable(BaseCombatEntity entity)
        {
            string prefab = entity.ShortPrefabName.ToLower();
            return prefab.Contains("quarry") || prefab.Contains("pumpjack") ||
                   prefab.Contains("sleepingbag") || prefab.Contains("bed") ||
                   prefab.Contains("planter") || prefab.Contains("refinery");
        }

        private bool IsLootContainer(BaseCombatEntity entity)
        {
            string prefab = entity.ShortPrefabName.ToLower();
            return prefab.Contains("trash") || prefab.Contains("junk") ||
                   prefab.Contains("foodbox") || prefab.Contains("giftbox");
        }

        #endregion

        #region Configuration

        private void LoadConfiguration()
        {
            _entityCache.Clear();
            _unknownEntities.Clear();

            _EnforceDecayDamage = GetConfigBool("Settings", "DecayDamageOverride", true);
            _HeliBypass = GetConfigBool("Settings", "HeliBypass", false);

            foreach (var group in _entityGroups.Values)
            {
                group.DamageMultipliers.Clear();

                foreach (var damageType in _damageTypes)
                {
                    float value = GetConfigFloat(
                        $"{group.Key}_Multipliers",
                        damageType,
                        1.0f);

                    group.DamageMultipliers[damageType] = value;
                }
            }

            _timeMultipliers.Clear();
            var timeCategories = new[] { "Global", "Player", "NPC", "Animal", "Vehicle",
                                        "Bradley", "Heli", "Chinook", "Train", "Building", "Other" };

            foreach (var category in timeCategories)
            {
                var multipliers = new float[HoursPerDay];
                for (int hour = 0; hour < HoursPerDay; hour++)
                {
                    multipliers[hour] = GetConfigFloat(
                        $"{category}_Time_Multipliers",
                        hour.ToString(),
                        1.0f);
                }
                _timeMultipliers[category] = multipliers;
            }

            SaveConfig();
        }

        private void BuildDefaultConfig()
        {
            if (_config == null)
                _config = new Dictionary<string, Dictionary<string, object>>();

            foreach (var group in _entityGroups.Values)
            {
                var sectionName = $"{group.Key}_Multipliers";
                if (!_config.ContainsKey(sectionName))
                {
                    _config[sectionName] = new Dictionary<string, object>();
                }

                var section = _config[sectionName];
                foreach (var damageType in _damageTypes)
                {
                    if (!section.ContainsKey(damageType))
                    {
                        section[damageType] = 1.0f;
                    }
                }
            }

            var timeCategories = new[] { "Global", "Player", "NPC", "Animal", "Vehicle",
                                        "Bradley", "Heli", "Chinook", "Train", "Building", "Other" };

            foreach (var category in timeCategories)
            {
                var sectionName = $"{category}_Time_Multipliers";
                if (!_config.ContainsKey(sectionName))
                {
                    _config[sectionName] = new Dictionary<string, object>();
                }

                var section = _config[sectionName];
                for (int hour = 0; hour < HoursPerDay; hour++)
                {
                    if (!section.ContainsKey(hour.ToString()))
                    {
                        section[hour.ToString()] = 1.0f;
                    }
                }
            }

            if (!_config.ContainsKey("Settings"))
            {
                _config["Settings"] = new Dictionary<string, object>();
            }

            if (!_config["Settings"].ContainsKey("DecayDamageOverride"))
            {
                _config["Settings"]["DecayDamageOverride"] = true;
            }

            if (!_config["Settings"].ContainsKey("HeliBypass"))
            {
                _config["Settings"]["HeliBypass"] = false;
            }
        }

        private void LoadPrefabOverrides()
        {
            try
            {
                _prefabOverrides = _dataFile.ReadObject<Dictionary<string, float>>() ??
                                   new Dictionary<string, float>();

                // Normalize existing keys so lookups (which lower-case) always match.
                _prefabOverrides = _prefabOverrides.ToDictionary(
                    kvp => kvp.Key.ToLowerInvariant(),
                    kvp => kvp.Value);
            }
            catch (Exception ex)
            {
                LogWarning($"Error loading prefab overrides, starting fresh: {ex.Message}");
                _prefabOverrides = new Dictionary<string, float>();
                _dataFile.WriteObject(_prefabOverrides, true);
            }
        }

        protected override void LoadDefaultConfig()
        {
            _config = new Dictionary<string, Dictionary<string, object>>();
        }

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                _config = Config.ReadObject<Dictionary<string, Dictionary<string, object>>>();
            }
            catch
            {
                _config = null;
            }

            if (_config == null)
            {
                LogWarning($"Configuration file {Name}.json is missing or invalid; creating a fresh one.");
                _config = new Dictionary<string, Dictionary<string, object>>();
                BuildDefaultConfig();
                SaveConfig();
            }
        }

        protected override void SaveConfig()
        {
            Config.WriteObject(_config, true);
        }

        private float GetConfigFloat(string category, string key, float defaultValue)
        {
            return Convert.ToSingle(GetConfigValue(category, key, defaultValue));
        }

        private bool GetConfigBool(string category, string key, bool defaultValue)
        {
            return Convert.ToBoolean(GetConfigValue(category, key, defaultValue));
        }

        private object GetConfigValue(string category, string key, object defaultValue)
        {
            if (_config == null)
                _config = new Dictionary<string, Dictionary<string, object>>();

            if (!_config.TryGetValue(category, out var data))
            {
                data = new Dictionary<string, object>();
                _config[category] = data;
            }

            if (data.TryGetValue(key, out var value))
                return value;

            data[key] = defaultValue;
            return defaultValue;
        }

        private void SetConfigValue(string category, string key, object value)
        {
            if (_config == null)
                _config = new Dictionary<string, Dictionary<string, object>>();

            if (!_config.TryGetValue(category, out var data))
            {
                data = new Dictionary<string, object>();
                _config[category] = data;
            }

            data[key] = value;
        }

        private void BuildDeployableList()
        {
            try
            {
                _deployableList.Clear();

                foreach (var itemDef in ItemManager.GetItemDefinitions())
                {
                    var mod = itemDef.GetComponent<ItemModDeployable>();
                    if (mod == null) continue;

                    string name = itemDef.name;
                    int lastIndex = name.LastIndexOf(".item");
                    if (lastIndex > 0)
                        name = name.Substring(0, lastIndex);

                    string normalized = name.Replace("_", ".").ToLower();
                    _deployableList.Add(normalized);
                    _deployableList.Add(normalized + ".deployed");
                }
            }
            catch (Exception ex)
            {
                LogError($"Error building deployable list: {ex.Message}");
            }
        }

        #endregion

        #region Damage Processing

        private void OnEntityTakeDamage(BaseCombatEntity entity, HitInfo hitInfo)
        {
            if (entity == null || hitInfo == null || string.IsNullOrEmpty(entity.ShortPrefabName))
                return;

            try
            {
                ProcessDamage(entity, hitInfo);
            }
            catch (Exception ex)
            {
                LogError($"Error processing damage for {entity.ShortPrefabName}: {ex.Message}");
            }
        }

        private EntityGroup GetEntityGroup(BaseCombatEntity entity)
        {
            if (entity == null)
                return null;

            string prefab = entity.ShortPrefabName;

            if (_entityCache.TryGetValue(prefab, out var cached))
                return cached;

            if (_unknownEntities.Contains(prefab))
                return null;

            // Ordered iteration guarantees "specific-before-generic" priority,
            // which a Dictionary's enumeration order does NOT.
            foreach (var group in _orderedGroups)
            {
                if (group.Matcher(entity))
                {
                    group.KnownPrefabs.Add(prefab); // informational only
                    if (group.Cacheable)
                        _entityCache[prefab] = group;
                    return group;
                }
            }

            // Only negatively cache prefabs with a stable prefab -> type mapping.
            // The "player" prefab is reused by real players, plugin bots, and horde
            // members, so an unmatched BasePlayer must never blacklist the prefab
            // (that would silently stop scaling real players' damage).
            if (!(entity is BasePlayer))
                _unknownEntities.Add(prefab);

            return null;
        }

        private void ProcessDamage(BaseCombatEntity entity, HitInfo hitInfo)
        {
            // Skip if no damage is being dealt
            if (hitInfo.damageTypes.Total() <= 0)
                return;

            EntityGroup matchingGroup = GetEntityGroup(entity);

            if (matchingGroup == null)
            {
                string key = entity.ShortPrefabName;

                if (_missingCounts.ContainsKey(key))
                {
                    _missingCounts[key]++;
                }
                else
                {
                    _missingCounts[key] = 1;
                    Puts($"DamageControl unmatched entity: {entity.ShortPrefabName} : {entity.PrefabName}");
                }
                return;
            }

            // Check if this is helicopter damage
            bool isHeliDamage = IsHeliDamage(hitInfo);

            // Read the in-game hour directly (cheap) and clamp defensively so a
            // Cycle.Hour of exactly 24.0 can never index out of the 0..23 arrays.
            // Cached in-game hour (refreshed every 30s). Already clamped to
            // 0..23 at refresh time, so it is safe to index with.
            int currentHour = _cachedHour;

            float globalTimeMultiplier = _timeMultipliers["Global"][currentHour];
            float categoryTimeMultiplier = 1.0f;

            if (!string.IsNullOrEmpty(matchingGroup.TimeCategory) &&
                _timeMultipliers.TryGetValue(matchingGroup.TimeCategory, out var timeArray))
            {
                categoryTimeMultiplier = timeArray[currentHour];
            }

            float totalModifier = globalTimeMultiplier * categoryTimeMultiplier;

            // Apply prefab override if exists (keys are stored lower-cased)
            if (_prefabOverrides.TryGetValue(entity.ShortPrefabName.ToLowerInvariant(), out float prefabModifier))
            {
                totalModifier *= prefabModifier;
            }

            // Apply heli bypass if enabled
            if (_HeliBypass && isHeliDamage)
            {
                if (totalModifier < 1.0f)
                {
                    totalModifier = 1.0f;
                }
            }

            for (int i = 0; i < DamageTypeCount; i++)
            {
                var damageType = (DamageType)i;
                string damageTypeName = _damageTypeNames[i];

                // Skip damage types with zero damage
                float currentDamage = hitInfo.damageTypes.Get(damageType);
                if (currentDamage <= 0)
                    continue;

                float damageTypeMultiplier = 1.0f;
                if (matchingGroup.DamageMultipliers.TryGetValue(damageTypeName, out float dtMult))
                {
                    damageTypeMultiplier = dtMult;
                }

                // Apply heli bypass to damage type multiplier as well
                if (_HeliBypass && isHeliDamage && damageTypeMultiplier < 1.0f)
                {
                    damageTypeMultiplier = 1.0f;
                }

                float finalMultiplier = totalModifier * damageTypeMultiplier;

                // Decay protection
                if (_EnforceDecayDamage &&
                    (damageType == DamageType.Decay || damageType == DamageType.Generic) &&
                    hitInfo.Initiator == null)
                {
                    finalMultiplier = 1.0f;
                }

                hitInfo.damageTypes.Scale(damageType, finalMultiplier);
            }
        }

        // allow Patrol Heli and CH47 to damage things (requested feature)
        private bool IsHeliDamage(HitInfo hitInfo)
        {
            if (hitInfo == null)
                return false;

            // The bypass targets the AI patrol helicopter, which is now a
            // standalone PatrolHelicopter (no longer a BaseHelicopter).
            if (hitInfo.Initiator != null && hitInfo.Initiator is PatrolHelicopter)
                return true;

            if (hitInfo.WeaponPrefab != null &&
                hitInfo.WeaponPrefab.ShortPrefabName.StartsWith("rocket_heli"))
                return true;

            return false;
        }

        // caching to reduce load
        private void UpdateCachedHour()
        {
            int hour = GetCurrentHour();
            if (hour < 0) hour = 0;
            else if (hour >= HoursPerDay) hour = HoursPerDay - 1;
            _cachedHour = hour;
        }

        private int GetCurrentHour()
        {
            return TOD_Sky.Instance != null ?
                   (int)Math.Floor(TOD_Sky.Instance.Cycle.Hour) :
                   DateTime.Now.Hour;
        }

        private bool IsNPC(BaseCombatEntity entity)
        {
            var player = entity as BasePlayer;
            if (player == null) return false;

            // UserIDString is stable across Rust versions (unlike the userID field,
            // whose type has changed). Real players have a SteamID64 >= SteamIdBase;
            // server-spawned fake players get lower IDs.
            if (ulong.TryParse(player.UserIDString, out ulong id))
                return id < SteamIdBase;

            return false;
        }

        private bool IsBot(BaseCombatEntity entity)
        {
            var player = entity as BasePlayer;
            if (player == null) return false;

            // GetComponents(list) reuses the buffer (no array alloc), and
            // GetType().Name avoids the per-component string build ToString() does.
            player.GetComponents(_componentBuffer);
            for (int i = 0; i < _componentBuffer.Count; i++)
            {
                var comp = _componentBuffer[i];
                if (comp != null && comp.GetType().Name.Contains("BotData"))
                    return true;
            }
            return false;
        }

        private bool IsZombie(BaseCombatEntity entity)
        {
            // Standalone zombie agent (catches it even under a renamed prefab).
            if (entity is Zombie)
                return true;

            if (entity.ShortPrefabName == "murderer")
                return true;

            var player = entity as BasePlayer;
            if (player == null) return false;

            player.GetComponents(_componentBuffer);
            for (int i = 0; i < _componentBuffer.Count; i++)
            {
                var comp = _componentBuffer[i];
                if (comp != null && comp.GetType().Name.Contains("HordeMember"))
                    return true;
            }
            return false;
        }

        private bool IsDeployable(BaseCombatEntity entity)
        {
            return _deployableList.Contains(entity.ShortPrefabName.Replace("_", "."));
        }

        #endregion

        #region Commands

        [Command("dc")]
        private void DamageControlCommand(IPlayer iplayer, string command, string[] args)
        {
            if (!IsAllowed(iplayer))
            {
                iplayer.Reply("You do not have permission to use this command.");
                return;
            }

            if (args == null || args.Length == 0)
            {
                ShowHelp(iplayer);
                return;
            }

            string action = args[0].ToLower();

            switch (action)
            {
                case "help":
                    ShowHelp(iplayer);
                    break;

                case "list":
                    if (args.Length >= 2)
                        ListGroupSettings(iplayer, args[1]);
                    else
                        ListAllGroups(iplayer);
                    break;

                case "set":
                    if (args.Length == 4)
                        SetGroupSetting(iplayer, args[1], args[2], args[3]);
                    else
                        iplayer.Reply("Usage: /dc set <group> <damageType> <value>");
                    break;

                case "sethour":
                    if (args.Length == 4)
                        SetHourlySetting(iplayer, args[1], args[2], args[3]);
                    else
                        iplayer.Reply("Usage: /dc sethour <timeCategory> <hour> <value>");
                    break;

                case "setprefab":
                    if (args.Length == 3)
                        SetPrefabOverride(iplayer, args[1], args[2]);
                    else
                        iplayer.Reply("Usage: /dc setprefab <prefab> <value>");
                    break;

                case "heli":
                    ToggleHeliBypass(iplayer);
                    break;

                case "missing":
                    ShowMissingEntities(iplayer);
                    break;

                case "reload":
                    LoadConfiguration();
                    LoadPrefabOverrides();
                    iplayer.Reply("Configuration reloaded.");
                    break;

                default:
                    ShowHelp(iplayer);
                    break;
            }
        }

        private void ShowHelp(IPlayer iplayer)
        {
            iplayer.Reply("DamageControl Commands:");
            iplayer.Reply("/dc list - List all groups");
            iplayer.Reply("/dc list <group> - List multipliers for group");
            iplayer.Reply("/dc set <group> <damageType> <value> - Set group multiplier");
            iplayer.Reply("/dc sethour <category> <hour> <value> - Set hourly multiplier");
            iplayer.Reply("/dc setprefab <prefab> <value> - Set prefab multiplier");
            iplayer.Reply("/dc heli - Toggle heli bypass mode");
            iplayer.Reply("/dc missing - Show unmatched entities");
            iplayer.Reply("/dc reload - Reload configuration");
        }

        private void ToggleHeliBypass(IPlayer iplayer)
        {
            _HeliBypass = !_HeliBypass;
            SetConfigValue("Settings", "HeliBypass", _HeliBypass);
            SaveConfig();

            iplayer.Reply($"Heli bypass: {(_HeliBypass ? "Enabled" : "Disabled")}");
        }

        private void ListAllGroups(IPlayer iplayer)
        {
            iplayer.Reply($"Entity Groups ({_entityGroups.Count}):");
            foreach (var group in _entityGroups.Values.OrderBy(g => g.Key))
            {
                string prefabCount = group.KnownPrefabs.Count > 0 ? $" [{group.KnownPrefabs.Count} prefabs]" : "";
                iplayer.Reply($"  {group.Key} - {group.DisplayName}{prefabCount}");
            }
        }

        private void ListGroupSettings(IPlayer iplayer, string groupKey)
        {
            var normalizedKey = NormalizeString(groupKey);

            if (_entityGroups.TryGetValue(normalizedKey, out var group))
            {
                iplayer.Reply($"Damage multipliers for {group.DisplayName}:");
                foreach (var kvp in group.DamageMultipliers)
                {
                    iplayer.Reply($"  {kvp.Key}: {kvp.Value:F2}");
                }
            }
            else
            {
                iplayer.Reply($"Unknown group: {groupKey}");
            }
        }

        private void SetGroupSetting(IPlayer iplayer, string groupKey, string damageType, string value)
        {
            var normalizedGroup = NormalizeString(groupKey);
            var normalizedDamage = NormalizeString(damageType);

            if (!_entityGroups.TryGetValue(normalizedGroup, out var group))
            {
                iplayer.Reply($"Unknown group: {groupKey}");
                return;
            }

            if (!float.TryParse(value, out float multiplier) || multiplier < 0)
            {
                iplayer.Reply("Invalid value. Must be a positive number.");
                return;
            }

            var matchingDamageType = group.DamageMultipliers.Keys
                .FirstOrDefault(dt => NormalizeString(dt) == normalizedDamage);

            if (matchingDamageType == null)
            {
                iplayer.Reply($"Unknown damage type: {damageType}");
                return;
            }

            group.DamageMultipliers[matchingDamageType] = multiplier;
            SetConfigValue($"{normalizedGroup}_Multipliers", matchingDamageType, multiplier);
            SaveConfig();

            iplayer.Reply($"Set {group.DisplayName} {matchingDamageType} multiplier to {multiplier:F2}");
        }

        private void SetHourlySetting(IPlayer iplayer, string timeCategory, string hourStr, string value)
        {
            var normalizedCategory = NormalizeString(timeCategory);

            if (!int.TryParse(hourStr, out int hour) || hour < 0 || hour >= HoursPerDay)
            {
                iplayer.Reply($"Invalid hour. Must be 0-{HoursPerDay - 1}.");
                return;
            }

            if (!float.TryParse(value, out float multiplier) || multiplier < 0)
            {
                iplayer.Reply("Invalid value. Must be a positive number.");
                return;
            }

            var matchingCategory = _timeMultipliers.Keys
                .FirstOrDefault(cat => NormalizeString(cat) == normalizedCategory);

            if (matchingCategory == null)
            {
                iplayer.Reply($"Unknown time category. Available: {string.Join(", ", _timeMultipliers.Keys)}");
                return;
            }

            _timeMultipliers[matchingCategory][hour] = multiplier;
            SetConfigValue($"{matchingCategory}_Time_Multipliers", hour.ToString(), multiplier);
            SaveConfig();

            iplayer.Reply($"Set {matchingCategory} hour {hour} multiplier to {multiplier:F2}");
        }

        private void SetPrefabOverride(IPlayer iplayer, string prefabName, string value)
        {
            if (!float.TryParse(value, out float multiplier) || multiplier < 0)
            {
                iplayer.Reply("Invalid value. Must be a positive number.");
                return;
            }

            prefabName = prefabName.ToLowerInvariant();

            _prefabOverrides[prefabName] = multiplier;

            try
            {
                _dataFile.WriteObject(_prefabOverrides, true);
                iplayer.Reply($"Set prefab override for {prefabName} to {multiplier:F2}");
            }
            catch (Exception ex)
            {
                LogError($"
// ... [Code preview truncated due to length. Full source in attached file] ...

Автор
15FPS
Скачиваний
0
Просмотры
1
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок
Сверху