using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Oxide.Core; using System; using System.Collections.Generic; using UnityEngine; using Oxide.Core.Plugins; using Oxide.Plugins.NpcHordeRustEditExtensionMethods; namespace Oxide.Plugins { [Info("NpcHordeRustEdit", "KpucTaJl", "1.0.4")] internal class NpcHordeRustEdit : RustPlugin { #region Data private const bool En = true; public class ItemConfig { [JsonProperty("ShortName")] public string ShortName { get; set; } [JsonProperty(En ? "Minimum" : "Минимальное кол-во")] public int MinAmount { get; set; } [JsonProperty(En ? "Maximum" : "Максимальное кол-во")] public int MaxAmount { get; set; } [JsonProperty(En ? "Chance [0.0-100.0]" : "Шанс выпадения предмета [0.0-100.0]")] public float Chance { get; set; } [JsonProperty(En ? "Is this a blueprint? [true/false]" : "Это чертеж? [true/false]")] public bool IsBluePrint { get; set; } [JsonProperty("SkinID (0 - default)")] public ulong SkinID { get; set; } [JsonProperty(En ? "Name (empty - default)" : "Название (empty - default)")] public string Name { get; set; } } public class LootTableConfig { [JsonProperty(En ? "Minimum numbers of items" : "Минимальное кол-во элементов")] public int Min { get; set; } [JsonProperty(En ? "Maximum numbers of items" : "Максимальное кол-во элементов")] public int Max { get; set; } [JsonProperty(En ? "Use minimum and maximum values? [true/false]" : "Использовать минимальное и максимальное значение? [true/false]")] public bool UseCount { get; set; } [JsonProperty(En ? "List of items" : "Список предметов")] public List Items { get; set; } } public class PrefabConfig { [JsonProperty(En ? "Chance [0.0-100.0]" : "Шанс выпадения [0.0-100.0]")] public float Chance { get; set; } [JsonProperty(En ? "The path to the prefab" : "Путь к prefab-у")] public string PrefabDefinition { get; set; } } public class PrefabLootTableConfig { [JsonProperty(En ? "Minimum numbers of prefabs" : "Минимальное кол-во prefab-ов")] public int Min { get; set; } [JsonProperty(En ? "Maximum numbers of prefabs" : "Максимальное кол-во prefab-ов")] public int Max { get; set; } [JsonProperty(En ? "Use minimum and maximum values? [true/false]" : "Использовать минимальное и максимальное значение? [true/false]")] public bool UseCount { get; set; } [JsonProperty(En ? "List of prefabs" : "Список prefab-ов")] public List Prefabs { get; set; } } public class NpcBelt { [JsonProperty("ShortName")] public string ShortName { get; set; } [JsonProperty("SkinID (0 - default)")] public ulong SkinID { get; set; } [JsonProperty(En ? "Mods" : "Модификации на оружие")] public HashSet Mods { get; set; } [JsonProperty(En ? "Ammo" : "Боеприпасы")] public string Ammo { get; set; } [JsonProperty(En ? "Probability Percent [0.0-100.0]" : "Вероятность [0.0-100.0]")] public float Chance { get; set; } } public class NpcWear { [JsonProperty("ShortName")] public string ShortName { get; set; } [JsonProperty("SkinID (0 - default)")] public ulong SkinID { get; set; } } public class PresetConfig { [JsonProperty(En ? "Minimum" : "Минимальное кол-во")] public int Min { get; set; } [JsonProperty(En ? "Maximum" : "Максимальное кол-во")] public int Max { get; set; } [JsonProperty(En ? "List of locations" : "Список расположений")] public List Positions { get; set; } [JsonProperty(En ? "NPCs setting" : "Настройки NPC")] public NpcConfig Config { get; set; } [JsonProperty(En ? "Which loot table should the plugin use? (0 - default; 1 - own; 2 - AlphaLoot; 3 - CustomLoot; 4 - loot table of the Rust objects; 5 - combine the 1 and 4 methods)" : "Какую таблицу предметов необходимо использовать? (0 - стандартную; 1 - собственную; 2 - AlphaLoot; 3 - CustomLoot; 4 - таблица предметов объектов Rust; 5 - совместить 1 и 4 методы)")] public int TypeLootTable { get; set; } [JsonProperty(En ? "Loot table from prefabs (if the loot table type is 4 or 5)" : "Таблица предметов из prefab-ов (если тип таблицы предметов - 4 или 5)")] public PrefabLootTableConfig PrefabLootTable { get; set; } [JsonProperty(En ? "Own loot table (if the loot table type is 1 or 5)" : "Собственная таблица предметов (если тип таблицы предметов - 1 или 5)")] public LootTableConfig OwnLootTable { get; set; } } public class NpcConfig { [JsonProperty(En ? "Name" : "Название")] public string Name { get; set; } [JsonProperty(En ? "Health" : "Кол-во ХП")] public float Health { get; set; } [JsonProperty(En ? "Chase Range" : "Дальность погони за целью")] public float ChaseRange { get; set; } [JsonProperty(En ? "Attack Range Multiplier" : "Множитель радиуса атаки")] public float AttackRangeMultiplier { get; set; } [JsonProperty(En ? "Sense Range" : "Радиус обнаружения цели")] public float SenseRange { get; set; } [JsonProperty(En ? "Target Memory Duration [sec.]" : "Длительность памяти цели [sec.]")] public float MemoryDuration { get; set; } [JsonProperty(En ? "Scale damage" : "Множитель урона")] public float DamageScale { get; set; } [JsonProperty(En ? "Aim Cone Scale" : "Множитель разброса")] public float AimConeScale { get; set; } [JsonProperty(En ? "Speed" : "Скорость")] public float Speed { get; set; } [JsonProperty(En ? "Disable radio effects? [true/false]" : "Отключать эффекты рации? [true/false]")] public bool DisableRadio { get; set; } [JsonProperty(En ? "Remove a corpse after death? (it is recommended to use the true value to improve performance) [true/false]" : "Удалять труп после смерти? (рекомендуется использовать значение true для повышения производительности) [true/false]")] public bool IsRemoveCorpse { get; set; } [JsonProperty(En ? "Wear items" : "Одежда")] public HashSet WearItems { get; set; } [JsonProperty(En ? "Belt items" : "Быстрые слоты")] public HashSet BeltItems { get; set; } [JsonProperty(En ? "Kit (it is recommended to use the previous 2 settings to improve performance)" : "Kit (рекомендуется использовать предыдущие 2 пункта настройки для повышения производительности)")] public string Kit { get; set; } } public class SensorConfig { [JsonProperty(En ? "Sensor position" : "Позиция сенсора")] public string Position { get; set; } [JsonProperty(En ? "Minimum time between the appearance of new NPCs [sec.]" : "Минимальное время между появлением новых NPC [sec.]")] public float Time { get; set; } [JsonProperty(En ? "Presets setting" : "Настройка пресетов")] public HashSet Npc { get; set; } } public class CustomMapConfig { [JsonProperty(En ? "ID" : "Идентификатор")] public string ID { get; set; } [JsonProperty(En ? "List of sensors" : "Список датчиков")] public HashSet Sensors { get; set; } } private void LoadCustomMapConfigs() { Puts("Loading files on the /oxide/data/NpcHordeRustEdit/ path has started..."); foreach (string name in Interface.Oxide.DataFileSystem.GetFiles("NpcHordeRustEdit/")) { string fileName = name.Split('/').Last().Split('.').First(); CustomMapConfig config = Interface.Oxide.DataFileSystem.ReadObject($"NpcHordeRustEdit/{fileName}"); if (config == null) { PrintError($"File {fileName} is corrupted and cannot be loaded!"); continue; } if (!string.IsNullOrEmpty(config.ID) && !_ids.Any(x => Math.Abs(x - Convert.ToSingle(config.ID)) < 0.001f)) { PrintWarning($"File {fileName} cannot be loaded on the current map!"); continue; } Puts($"File {fileName} has been loaded successfully!"); foreach (SensorConfig sensor in config.Sensors) { foreach (PresetConfig preset in sensor.Npc) { CheckLootTable(preset.OwnLootTable); CheckPrefabLootTable(preset.PrefabLootTable); } _sensors.Add(sensor); } Interface.Oxide.DataFileSystem.WriteObject($"NpcHordeRustEdit/{fileName}", config); } } private readonly HashSet _sensors = new HashSet(); private readonly HashSet _ids = new HashSet(); private void LoadIDs() { foreach (RANDSwitch entity in BaseNetworkable.serverEntities.OfType()) _ids.Add(entity.transform.position.x + entity.transform.position.y + entity.transform.position.z); } #endregion Data #region Oxide Hooks private object OnSensorDetect(HBHFSensor sensor, BasePlayer target) { if (!target.IsPlayer()) return null; SensorConfig config = _sensors.FirstOrDefault(x => Vector3.Distance(x.Position.ToVector3(), sensor.transform.position) < 1f); if (config == null) return null; ulong sensorID = sensor.net.ID.Value; if (_data.Any(x => x.ID == sensorID)) return null; SensorData data = new SensorData { ID = sensorID, Scientists = new HashSet() }; foreach (PresetConfig preset in config.Npc) { int count = UnityEngine.Random.Range(preset.Min, preset.Max + 1); List positions = preset.Positions.Select(x => x.ToVector3()); JObject cfg = GetObjectConfig(preset.Config, sensor.transform.position.ToString()); for (int i = 0; i < count; i++) { Vector3 pos = positions.GetRandom(); positions.Remove(pos); ScientistNPC npc = (ScientistNPC)NpcSpawn.Call("SpawnNpc", pos, cfg); if (npc != null) data.Scientists.Add(npc); } } _data.Add(data); timer.In(config.Time, () => { foreach (ScientistNPC npc in data.Scientists) if (npc.IsExists()) npc.Kill(); _data.Remove(data); }); return null; } private void OnServerInitialized() { LoadIDs(); LoadCustomMapConfigs(); } private void Unload() { foreach (SensorData data in _data) foreach (ScientistNPC npc in data.Scientists) if (npc.IsExists()) npc.Kill(); } #endregion Oxide Hooks #region Helpers [PluginReference] private readonly Plugin NpcSpawn; public class SensorData { public ulong ID; public HashSet Scientists; } private readonly HashSet _data = new HashSet(); private JObject GetObjectConfig(NpcConfig config, string homePos) { float chance = UnityEngine.Random.Range(0.0f, 100.0f); NpcBelt belt = config.BeltItems.WhereToList(x => chance <= x.Chance).GetRandom(); JArray belts = new JArray { new JObject { ["ShortName"] = belt.ShortName, ["Amount"] = 1, ["SkinID"] = belt.SkinID, ["Mods"] = new JArray { belt.Mods }, ["Ammo"] = belt.Ammo }, new JObject { ["ShortName"] = "grenade.beancan", ["Amount"] = 1, ["SkinID"] = 0, ["Mods"] = new JArray(), ["Ammo"] = string.Empty } }; return new JObject { ["Name"] = config.Name, ["WearItems"] = new JArray { config.WearItems.Select(x => new JObject { ["ShortName"] = x.ShortName, ["SkinID"] = x.SkinID }) }, ["BeltItems"] = belts, ["Kit"] = config.Kit, ["Health"] = config.Health, ["RoamRange"] = 10f, ["ChaseRange"] = config.ChaseRange, ["DamageScale"] = config.DamageScale, ["TurretDamageScale"] = 1f, ["AimConeScale"] = config.AimConeScale, ["DisableRadio"] = config.DisableRadio, ["CanRunAwayWater"] = true, ["Speed"] = config.Speed, ["AreaMask"] = 1, ["AgentTypeID"] = -1372625422, ["HomePosition"] = homePos, ["States"] = new JArray { "RoamState", "ChaseState", "CombatState" }, ["Sensory"] = new JObject { ["AttackRangeMultiplier"] = config.AttackRangeMultiplier, ["SenseRange"] = config.SenseRange, ["MemoryDuration"] = config.MemoryDuration, ["CheckVisionCone"] = false, ["VisionCone"] = 135f } }; } #endregion Helpers #region Spawn Loot private void OnCorpsePopulate(ScientistNPC entity, NPCPlayerCorpse corpse) { if (entity == null) return; SensorData data = _data.FirstOrDefault(x => x.Scientists.Contains(entity)); if (data != null) { data.Scientists.Remove(entity); PresetConfig preset = GetPreset(entity.displayName); NextTick(() => { if (corpse == null) return; ItemContainer container = corpse.containers[0]; if (preset.TypeLootTable == 0) { for (int i = container.itemList.Count - 1; i >= 0; i--) { Item item = container.itemList[i]; if (preset.Config.WearItems.Any(x => x.ShortName == item.info.shortname)) { item.RemoveFromContainer(); item.Remove(); } } return; } if (preset.TypeLootTable == 2 || preset.TypeLootTable == 3) { if (preset.Config.IsRemoveCorpse && !corpse.IsDestroyed) corpse.Kill(); return; } container.ClearItemsContainer(); if (preset.TypeLootTable == 4 || preset.TypeLootTable == 5) AddToContainerPrefab(container, preset.PrefabLootTable); if (preset.TypeLootTable == 1 || preset.TypeLootTable == 5) AddToContainerItem(container, preset.OwnLootTable); if (preset.Config.IsRemoveCorpse && !corpse.IsDestroyed) corpse.Kill(); }); } } private object CanPopulateLoot(ScientistNPC entity, NPCPlayerCorpse corpse) { if (entity == null || corpse == null) return null; if (_data.Any(x => x.Scientists.Contains(entity))) { PresetConfig preset = GetPreset(entity.displayName); if (preset.TypeLootTable == 2) return null; else return true; } return null; } private object OnCustomLootNPC(NetworkableId netID) { SensorData data = _data.FirstOrDefault(x => x.Scientists.Any(y => y.IsExists() && y.net.ID.Value == netID.Value)); if (data == null) return null; ScientistNPC entity = data.Scientists.FirstOrDefault(x => x.IsExists() && x.net.ID.Value == netID.Value); if (entity != null) { PresetConfig preset = GetPreset(entity.displayName); if (preset.TypeLootTable == 3) return null; else return true; } return null; } private PresetConfig GetPreset(string name) { foreach (SensorConfig sensor in _sensors) foreach (PresetConfig preset in sensor.Npc) if (preset.Config.Name == name) return preset; return null; } private void AddToContainerPrefab(ItemContainer container, PrefabLootTableConfig lootTable) { HashSet prefabsInContainer = new HashSet(); if (lootTable.UseCount) { int count = UnityEngine.Random.Range(lootTable.Min, lootTable.Max + 1); while (prefabsInContainer.Count < count) { foreach (PrefabConfig prefab in lootTable.Prefabs) { if (prefabsInContainer.Contains(prefab.PrefabDefinition)) continue; if (UnityEngine.Random.Range(0.0f, 100.0f) <= prefab.Chance) { if (_allLootSpawnSlots.ContainsKey(prefab.PrefabDefinition)) { LootContainer.LootSpawnSlot[] lootSpawnSlots = _allLootSpawnSlots[prefab.PrefabDefinition]; foreach (LootContainer.LootSpawnSlot lootSpawnSlot in lootSpawnSlots) for (int j = 0; j < lootSpawnSlot.numberToSpawn; j++) if (UnityEngine.Random.Range(0f, 1f) <= lootSpawnSlot.probability) lootSpawnSlot.definition.SpawnIntoContainer(container); } else _allLootSpawn[prefab.PrefabDefinition].SpawnIntoContainer(container); prefabsInContainer.Add(prefab.PrefabDefinition); if (prefabsInContainer.Count == count) return; } } } } else { foreach (PrefabConfig prefab in lootTable.Prefabs) { if (prefabsInContainer.Contains(prefab.PrefabDefinition)) continue; if (UnityEngine.Random.Range(0.0f, 100.0f) <= prefab.Chance) { if (_allLootSpawnSlots.ContainsKey(prefab.PrefabDefinition)) { LootContainer.LootSpawnSlot[] lootSpawnSlots = _allLootSpawnSlots[prefab.PrefabDefinition]; foreach (LootContainer.LootSpawnSlot lootSpawnSlot in lootSpawnSlots) for (int j = 0; j < lootSpawnSlot.numberToSpawn; j++) if (UnityEngine.Random.Range(0f, 1f) <= lootSpawnSlot.probability) lootSpawnSlot.definition.SpawnIntoContainer(container); } else _allLootSpawn[prefab.PrefabDefinition].SpawnIntoContainer(container); prefabsInContainer.Add(prefab.PrefabDefinition); } } } } private void AddToContainerItem(ItemContainer container, LootTableConfig lootTable) { HashSet indexMove = new HashSet(); if (lootTable.UseCount) { int count = UnityEngine.Random.Range(lootTable.Min, lootTable.Max + 1); while (indexMove.Count < count) { foreach (ItemConfig item in lootTable.Items) { if (indexMove.Contains(lootTable.Items.IndexOf(item))) continue; if (UnityEngine.Random.Range(0.0f, 100.0f) <= item.Chance) { Item newItem = item.IsBluePrint ? ItemManager.CreateByName("blueprintbase") : ItemManager.CreateByName(item.ShortName, UnityEngine.Random.Range(item.MinAmount, item.MaxAmount + 1), item.SkinID); if (newItem == null) { PrintWarning($"Failed to create item! ({item.ShortName})"); continue; } if (item.IsBluePrint) newItem.blueprintTarget = ItemManager.FindItemDefinition(item.ShortName).itemid; if (!string.IsNullOrEmpty(item.Name)) newItem.name = item.Name; if (container.capacity < container.itemList.Count + 1) container.capacity++; if (!newItem.MoveToContainer(container)) newItem.Remove(); else { indexMove.Add(lootTable.Items.IndexOf(item)); if (indexMove.Count == count) return; } } } } } else { foreach (ItemConfig item in lootTable.Items) { if (indexMove.Contains(lootTable.Items.IndexOf(item))) continue; if (UnityEngine.Random.Range(0.0f, 100.0f) <= item.Chance) { Item newItem = item.IsBluePrint ? ItemManager.CreateByName("blueprintbase") : ItemManager.CreateByName(item.ShortName, UnityEngine.Random.Range(item.MinAmount, item.MaxAmount + 1), item.SkinID); if (newItem == null) { PrintWarning($"Failed to create item! ({item.ShortName})"); continue; } if (item.IsBluePrint) newItem.blueprintTarget = ItemManager.FindItemDefinition(item.ShortName).itemid; if (!string.IsNullOrEmpty(item.Name)) newItem.name = item.Name; if (container.capacity < container.itemList.Count + 1) container.capacity++; if (!newItem.MoveToContainer(container)) newItem.Remove(); else indexMove.Add(lootTable.Items.IndexOf(item)); } } } } private static void CheckLootTable(LootTableConfig lootTable) { lootTable.Items = lootTable.Items.OrderBy(x => x.Chance); if (lootTable.Max > lootTable.Items.Count) lootTable.Max = lootTable.Items.Count; if (lootTable.Min > lootTable.Max) lootTable.Min = lootTable.Max; } private void CheckPrefabLootTable(PrefabLootTableConfig lootTable) { HashSet prefabs = new HashSet(); foreach (PrefabConfig prefabConfig in lootTable.Prefabs) { if (prefabs.Any(x => x.PrefabDefinition == prefabConfig.PrefabDefinition)) PrintWarning($"Duplicate prefab removed from loot table! ({prefabConfig.PrefabDefinition})"); else { GameObject gameObject = GameManager.server.FindPrefab(prefabConfig.PrefabDefinition); global::HumanNPC humanNpc = gameObject.GetComponent(); ScarecrowNPC scarecrowNPC = gameObject.GetComponent(); LootContainer lootContainer = gameObject.GetComponent(); if (humanNpc != null && humanNpc.LootSpawnSlots.Length != 0) { if (!_allLootSpawnSlots.ContainsKey(prefabConfig.PrefabDefinition)) _allLootSpawnSlots.Add(prefabConfig.PrefabDefinition, humanNpc.LootSpawnSlots); prefabs.Add(prefabConfig); } else if (scarecrowNPC != null && scarecrowNPC.LootSpawnSlots.Length != 0) { if (!_allLootSpawnSlots.ContainsKey(prefabConfig.PrefabDefinition)) _allLootSpawnSlots.Add(prefabConfig.PrefabDefinition, scarecrowNPC.LootSpawnSlots); prefabs.Add(prefabConfig); } else if (lootContainer != null && lootContainer.LootSpawnSlots.Length != 0) { if (!_allLootSpawnSlots.ContainsKey(prefabConfig.PrefabDefinition)) _allLootSpawnSlots.Add(prefabConfig.PrefabDefinition, lootContainer.LootSpawnSlots); prefabs.Add(prefabConfig); } else if (lootContainer != null && lootContainer.lootDefinition != null) { if (!_allLootSpawn.ContainsKey(prefabConfig.PrefabDefinition)) _allLootSpawn.Add(prefabConfig.PrefabDefinition, lootContainer.lootDefinition); prefabs.Add(prefabConfig); } else PrintWarning($"Unknown prefab removed! ({prefabConfig.PrefabDefinition})"); } } lootTable.Prefabs = prefabs.OrderBy(x => x.Chance); if (lootTable.Max > lootTable.Prefabs.Count) lootTable.Max = lootTable.Prefabs.Count; if (lootTable.Min > lootTable.Max) lootTable.Min = lootTable.Max; } private readonly Dictionary _allLootSpawn = new Dictionary(); private readonly Dictionary _allLootSpawnSlots = new Dictionary(); #endregion Spawn Loot } } namespace Oxide.Plugins.NpcHordeRustEditExtensionMethods { public static class ExtensionMethods { public static bool Any(this IEnumerable source, Func predicate) { using (var enumerator = source.GetEnumerator()) while (enumerator.MoveNext()) if (predicate(enumerator.Current)) return true; return false; } public static List WhereToList(this IEnumerable source, Func predicate) { List result = new List(); using (var enumerator = source.GetEnumerator()) while (enumerator.MoveNext()) if (predicate(enumerator.Current)) result.Add(enumerator.Current); return result; } public static TSource FirstOrDefault(this IEnumerable source, Func predicate) { using (var enumerator = source.GetEnumerator()) while (enumerator.MoveNext()) if (predicate(enumerator.Current)) return enumerator.Current; return default(TSource); } public static HashSet Select(this IEnumerable source, Func predicate) { HashSet result = new HashSet(); using (var enumerator = source.GetEnumerator()) while (enumerator.MoveNext()) result.Add(predicate(enumerator.Current)); return result; } public static List Select(this IList source, Func predicate) { List result = new List(); for (int i = 0; i < source.Count; i++) { TSource element = source[i]; result.Add(predicate(element)); } return result; } public static HashSet OfType(this IEnumerable source) { HashSet result = new HashSet(); using (var enumerator = source.GetEnumerator()) while (enumerator.MoveNext()) if (enumerator.Current is T) result.Add((T)(object)enumerator.Current); return result; } public static List ToList(this IEnumerable source) { List result = new List(); using (var enumerator = source.GetEnumerator()) while (enumerator.MoveNext()) result.Add(enumerator.Current); return result; } public static TSource First(this IList source) => source[0]; public static TSource Last(this IList source) => source[source.Count - 1]; public static List OrderBy(this IEnumerable source, Func predicate) { List result = source.ToList(); for (int i = 0; i < result.Count; i++) { for (int j = 0; j < result.Count - 1; j++) { if (predicate(result[j]) > predicate(result[j + 1])) { TSource z = result[j]; result[j] = result[j + 1]; result[j + 1] = z; } } } return result; } public static bool IsPlayer(this BasePlayer player) => player != null && player.userID.IsSteamId(); public static bool IsExists(this BaseNetworkable entity) => entity != null && !entity.IsDestroyed; public static void ClearItemsContainer(this ItemContainer container) { for (int i = container.itemList.Count - 1; i >= 0; i--) { Item item = container.itemList[i]; item.RemoveFromContainer(); item.Remove(); } } } }