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] ...