/*▄▄▄ ███▄ ▄███▓ ▄████ ▄▄▄██▀▀▀▓█████▄▄▄█████▓ ▓█████▄ ▓██▒▀█▀ ██▒ ██▒ ▀█▒ ▒██ ▓█ ▀▓ ██▒ ▓▒ ▒██▒ ▄██▓██ ▓██░▒██░▄▄▄░ ░██ ▒███ ▒ ▓██░ ▒░ ▒██░█▀ ▒██ ▒██ ░▓█ ██▓▓██▄██▓ ▒▓█ ▄░ ▓██▓ ░ ░▓█ ▀█▓▒██▒ ░██▒░▒▓███▀▒ ▓███▒ ░▒████▒ ▒██▒ ░ ░▒▓███▀▒░ ▒░ ░ ░ ░▒ ▒ ▒▓▒▒░ ░░ ▒░ ░ ▒ ░░ ▒░▒ ░ ░ ░ ░ ░ ░ ▒ ░▒░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░*/ using Facepunch; using HarmonyLib; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Oxide.Core.Plugins; using ProtoBuf; using Rust; using System; using System.Collections.Generic; using System.Linq; using System.Reflection.Emit; using UnityEngine; using UnityEngine.AI; namespace Oxide.Plugins { [Info("CH47Reinforcements", "bmgjet", "1.2.4")] class CH47Reinforcements : RustPlugin { [PluginReference] private Plugin Kits, NpcSpawn; Dictionary HackTriggers = new Dictionary(); //Found hackable crates on the map private List _prefabs = new List { "rock", "formation", "cliff" }; //Terrain Stuff private RaycastHit _hit; //Terrain Hit private NavMeshHit _navhit; //Navmesh hit private List Raidwalls = new List(); //List of walls spawned by raidableBases private Dictionary Heavys = new Dictionary(); //List of active NPCs spawned by plugin private List SpawnedCH47 = new List(); //List of active CH47 spawned by plugin private List BlockedAreas = new List(); //List of areas to block for edge cases. private float LastEvent = 0; //Cool down timer private List Chase = new List(); //List of active NPCs spawned by plugin #region BetterNPC Config Layout public class NpcBelt { [JsonProperty("ShortName")] public string ShortName { get; set; } [JsonProperty("Amount")] public int Amount { get; set; } [JsonProperty("SkinID (0 - default)")] public ulong SkinID { get; set; } [JsonProperty("Mods")] public List Mods { get; set; } [JsonProperty("Ammo")] public string Ammo { get; set; } } public class NpcWear { [JsonProperty("ShortName")] public string ShortName { get; set; } [JsonProperty("SkinID (0 - default)")] public ulong SkinID { get; set; } } #endregion #region Configuration private Configuration config; private class Configuration { [JsonProperty("OrderID: ")] public string OrderID = ""; [JsonProperty("Force Carbon Fallback")] public bool ForceCarbon = false; [JsonProperty("Breaking raidablebase wall triggers event")] public bool RaidbaseWallBreakTriggersEvent = false; [JsonProperty("NPC Suicide Secs")] public int NPCSuisideSecs = 1200; [JsonProperty("NPC Health")] public float NPCHealth = 300; [JsonProperty("NPC AimCone Scale")] public float aimConeScale = 1; [JsonProperty("NPC Damage Scale")] public float damageScale = 1; [JsonProperty("NPC Ignore Non Vision Max Distance")] public float IgnoreNonVisionMaxDistance = 50; [JsonProperty("NPC Ignore Sneakers Max Distance")] public float IgnoreSneakersMaxDistance = 20; [JsonProperty("NPC Attack Range Multiplier")] public float AttackRangeMultiplier = 2; [JsonProperty("NPC Target Lost Range")] public float TargetLostRange = 40; [JsonProperty("NPC Vision Cone")] public float VisionCone = 0.5f; [JsonProperty("Hight Above Terrain To Target")] public int HeliHeightOffset = 15; [JsonProperty("Extra Height On Heli Spawn Helps Clear Mountains")] public int HeliSpawnHeightOffset = 15; [JsonProperty("Create Intrest Point NPCS Go Back To")] public bool SearchForCrate = true; [JsonProperty("Try Switch Navmesh Agent If NPC Not Moving")] public bool TryNavAgents = true; [JsonProperty("Play Alarm Sound")] public bool UseAlarmSound = true; [JsonProperty("Drop Flare Where CH47 Will Land")] public bool UseFlareSignal = true; [JsonProperty("Despawn Ch47 Timer")] public int Ch47SuisideSecs = 30; [JsonProperty("Distance From Target To Spawn CH47")] public int SpawnDistance = 400; [JsonProperty("Estimated Travel Time (Wont Try Land Until This Secs)")] public int TravelTime = 45; [JsonProperty("How Long Before Extra Hight Removed After Spawn")] public int ExtraHeightTimer = 25; [JsonProperty("Extra Time To Play Alarm Sound Effect Over Vanilla 60 sec")] public int ExtendAlarmSec = 30; [JsonProperty("When MLRS Fired Trigger Event")] public bool MLRSFireTriggersEvent = false; [JsonProperty("When Excavator Started Trigger Event")] public bool ExcivatorTriggersEvent = false; [JsonProperty("When Bradley Killed Trigger Event")] public bool BradleyDeathTriggersEvent = false; [JsonProperty("When Patrol Heli Killed Trigger Event")] public bool AttackHeliDeathTriggersEvent = false; [JsonProperty("Cool Down Between Events (sec)")] public int EventsCooldownSec = 30; [JsonProperty("Block If Close To SafeZone (Distance)")] public int BlockRadius = 150; [JsonProperty("Custom Kit")] public string CustomKit = ""; [JsonProperty("Use Better NPC for Bots")] public bool UseBetterNPC = false; [JsonProperty("[Better NPC] RoamRange")] public float BetterNPCRoamRange = 50; [JsonProperty("[Better NPC] ChaseRange")] public float BetterNPCChaseRange = 150; [JsonProperty("[Better NPC] SenseRange")] public float BetterNPCSenseRange = 60; [JsonProperty("[Better NPC] ListenRange")] public float BetterNPCListenRange = 30; [JsonProperty("[Better NPC] NPC Guns")] public string[] BetterNPCGuns = { "rifle.lr300", "minigun", "lmg.m249", "rifle.m39" }; public string ToJson() => JsonConvert.SerializeObject(this); public Dictionary ToDictionary() => JsonConvert.DeserializeObject>(ToJson()); } protected override void LoadDefaultConfig() { config = new Configuration(); } protected override void LoadConfig() { base.LoadConfig(); try { config = Config.ReadObject(); if (config == null) { throw new JsonException(); } if (!config.ToDictionary().Keys.SequenceEqual(Config.ToDictionary(x => x.Key, x => x.Value).Keys)) { PrintWarning("Configuration appears to be outdated; updating and saving"); SaveConfig(); } } catch { PrintWarning($"Configuration file {Name}.json is invalid; using defaults"); LoadDefaultConfig(); } } protected override void SaveConfig() { PrintWarning($"Configuration changes saved to {Name}.json"); Config.WriteObject(config, true); } #endregion Configuration #region Oxide Hooks //Console Command for APIs [ConsoleCommand("ch47trigger.spawn")] private void Cmd(ConsoleSystem.Arg arg) { RunCMD(arg, false); } [ConsoleCommand("ch47trigger.spawnsilent")] private void Cmd2(ConsoleSystem.Arg arg) { RunCMD(arg, true); } private void Init() { //Remove unneeded hooks if (config.UseBetterNPC) { Unsubscribe("OnEntityTakeDamage"); } if (!config.RaidbaseWallBreakTriggersEvent) { Unsubscribe("OnRaidableBaseDespawned"); Unsubscribe("OnRaidableBaseStarted"); } } private void OnServerInitialized(bool initial) { Startup(initial ? 30 : 1); } //Start up delay if initial startup private void Unload() //Unload what plugin has spawned { foreach (CH47HelicopterAIController ch47 in SpawnedCH47) { if (!IsReallyValid(ch47)) { ch47.Kill(); } } foreach (KeyValuePair kvp in Heavys) { if (kvp.Key != null && !kvp.Key.IsDestroyed) { kvp.Key.Kill(); } } } //MLRS trigger and offsets to create landing pad void OnMlrsFired(MLRS mlrs, BasePlayer player) { if (config.MLRSFireTriggersEvent) { SpawnCH47LandingZone(mlrs.transform.position + (mlrs.transform.forward * 20) + (mlrs.transform.right * 15)); } } //Excavator trigger and offsets to create landing pad void OnExcavatorMiningToggled(ExcavatorArm arm) { if (config.ExcivatorTriggersEvent && arm.IsPowered()) { ExcavatorOutputPile hooper = arm.outputPiles.GetRandom(); SpawnCH47LandingZone(hooper.transform.position + (hooper.transform.right * -20) + (hooper.transform.forward * 15)); } } //RaidableBases Hooks private void OnRaidableBaseStarted(Vector3 pos, int diff, bool pvp, string id, float spawn, float despawn, float load, ulong ownerid, BasePlayer owner, List raiders, List intruders, List entities, string BaseName, System.DateTime spawnDateTime, System.DateTime despawnDateTime, float radius) { foreach (var entity in entities) { if (IsReallyValid(entity) && entity.ShortPrefabName.Contains("external.high")) { Raidwalls.Add(entity); } } } private void OnRaidableBaseDespawned(Vector3 pos) { for (int i = Raidwalls.Count - 1; i >= 0; i--) { if (Raidwalls[i] == null && i < Raidwalls.Count) { Raidwalls.RemoveAt(i); continue; } if (Vector3.Distance(pos, Raidwalls[i].transform.position) <= 100) { Raidwalls.Remove(Raidwalls[i]); } } } //Raidable Base and patrol heli triggers (On heli crash or raidablebase wall broken) private void OnEntityDeath(BaseCombatEntity entity, HitInfo info) { if (config.AttackHeliDeathTriggersEvent && entity is PatrolHelicopter && entity.PrefabName == "assets/prefabs/npc/patrol helicopter/patrolhelicopter.prefab") { SpawnCH47LandingZone(entity.transform.position + (entity.transform.forward * 5) + (entity.transform.right * 10)); //Offset slightly to get out of fire } if (Raidwalls.Contains(entity)) { OnRaidableBaseDespawned(entity.transform.position); SpawnCH47LandingZone(entity.transform.position + (entity.transform.forward * -30)); } } //Bradley killed trigger and offsets private void OnEntityDestroy(BradleyAPC bradley) { if (config.BradleyDeathTriggersEvent) { SpawnCH47LandingZone(bradley.transform.position + (bradley.transform.forward * 5) + (bradley.transform.right * 10)); } } //Trigger from map placed codelockedhackablecrate_oilrig with settings private void OnCrateHack(HackableLockedCrate hackableLockedCrate) { foreach (KeyValuePair kvp in HackTriggers) { if (Vector3.Distance(kvp.Key, hackableLockedCrate.transform.position) <= 1) { SpawnCH47LandingZone(kvp.Value, "", false, true); return; } } } private void OnPlayerDeath(ScientistNPC npc, HitInfo info) { if (Heavys.ContainsKey(npc)) { Heavys.Remove(npc); } } //Remove spawned Heavy NPC from list private void OnEntityDismounted(BaseMountable mount, ScientistNPC npc) //Apply logic to heavys when they leave our spawned CH47 { if (mount.VehicleParent() is CH47Helicopter && SpawnedCH47.Contains(mount.VehicleParent() as CH47Helicopter)) { npc.Invoke(() => { if (!IsReallyValid(npc) && npc.IsHeadUnderwater()) { npc.Kill(); return; } }, 5); //Check NPC isnt in water. if (config.UseBetterNPC) //Better NPC controlls npcs { npc.Invoke(() => //Delay 3 seconds { if (!IsReallyValid(npc)) { return; } //Stop if npc is already destroyed NavMeshHit hit; if (NavMesh.SamplePosition(npc.transform.position, out hit, 50, -1)) //Try find navmeshes { npc.Brain.Navigator.Warp(hit.position); //Move player to found navmesh } }, 3); return; } npc.InitializeHealth(config.NPCHealth, config.NPCHealth); //Change health to config setting npc.Brain.Navigator.CanUseNavMesh = false; //Stop navspam while moving npc.Brain.Navigator.Agent.agentTypeID = NavMesh.GetSettingsByIndex(1).agentTypeID; //Try agent type 1 first (Walks on walkable but not monument navmesh) Heavys.Add(npc, UnityEngine.Time.realtimeSinceStartup + config.NPCSuisideSecs); //List to manage NPCS from despawn 20mins npc.Invoke(() => //Delay 3 seconds { if (!IsReallyValid(npc)) { return; } //Stop if npc is already destroyed Item gun = npc.inventory.FindItemByItemID(703057617); //Check for flamethrower if (gun == null) { gun = npc.inventory.FindItemByItemID(-41440462); } //Check for Spaz shotty if (gun != null) { gun.Remove(); CreateGun(npc, "rifle.lr300", "weapon.mod.lasersight"); } NPCManager(npc); //Method to maintain some control of NPC NavMeshHit hit; if (NavMesh.SamplePosition(npc.transform.position, out hit, 50, -1)) //Try find navmeshes { npc.gameObject.layer = 17; npc.Brain.Navigator.Warp(hit.position); //Move player to found navmesh npc.Invoke(() => { SwitchAgent(npc, npc.transform.position); //Check if NPC should change agent type to walk in monuments (Doesnt walk on walkable) npc.Brain.Navigator.CanUseNavMesh = true; //Allow NPC to move again npc.Brain.SetEnabled(true); npc.Brain.Navigator.BestRoamPointMaxDistance = 15; //Try set some limits NPC ignores under a lot of conditions npc.Brain.Navigator.MaxRoamDistanceFromHome = 30; npc.Brain.Events.Memory.Position.Set(ClosestHackableCrate(npc.transform.position), 4); //Set the home position to return to npc.Brain.Navigator.SetDestination(npc.Brain.Events.Memory.Position.Get(4), BaseNavigator.NavigationSpeed.Fast, 0.25f, 0f); }, 3f); return; } }, 1f); } } private void OnEntityTakeDamage(ScientistNPC npc, HitInfo info) { if(Heavys == null || info == null || info.InitiatorPlayer == null) { return; } if (Heavys.ContainsKey(npc) && !info.InitiatorPlayer.IsNpc) //Check if its a NPC from this plugin { BaseEntity attacker = info.InitiatorPlayer; //Get attacker if (npc != null && npc.Brain != null && attacker != null) { if (npc.Brain.Senses.Memory.Targets.Contains(attacker)) { return; } //Already has this player as target SendNPCAfterPlayer(npc, attacker); } } } #endregion #region Methods private void Startup(int delay) { timer.Once(delay, () => { //Block safezones from triggering foreach (TriggerSafeZone safezone in TriggerSafeZone.allSafeZones) { BlockedAreas.Add(safezone.transform.position); } //Block Oil rigs from triggering foreach (MonumentInfo info in TerrainMeta.Path.Monuments) { if (info.transform.parent?.name.Contains("assets/bundled/prefabs/autospawn/monument/offshore/oilrig_") == true) { BlockedAreas.Add(info.transform.position); } } uint id = StringPool.Get("assets/prefabs/deployable/chinooklockedcrate/codelockedhackablecrate_oilrig.prefab"); //Get prefab id foreach (PrefabData pd in World.Serialization.world.prefabs) //Scan all of maps prefabs { if (pd.id == id) //Only target codelockedhackablecrate_oilrig { if (!pd.category.ToLower().Contains("ch47landingzone=")) { continue; } //Read prefab settings string[] values = pd.category.Split('='); if (values.Length != 4) { continue; } //Check valid settings length float x; float y; float z; if (float.TryParse(values[1], out x) && float.TryParse(values[2], out y) && float.TryParse(values[3].Split(':')[0], out z)) //Try convert settings { HackTriggers.Add(new Vector3(pd.position.x, pd.position.y, pd.position.z), new Vector3(x, y, z)); //Add trigger with settings } } } Puts("Map LandingZones: " + HackTriggers.Count.ToString() + ". Blocked Zones: " + BlockedAreas.Count.ToString() + "."); }); } public void SendNPCAfterPlayer(ScientistNPC npc, BaseEntity target) { List list = Pool.Get>(); Vis.Entities(npc.transform.position, 20, list, -1); foreach (BasePlayer baseplayer in list) { if (!baseplayer.UserIDString.IsSteamId()) { ScientistNPC bot = baseplayer as ScientistNPC; if (bot != null && Heavys.ContainsKey(bot)) { //Adjust targets and settings temp npc.Brain.Senses.Memory.Targets.Clear(); npc.Brain.Senses.Memory.Players.Clear(); npc.Brain.Senses.Memory.Threats.Clear(); npc.Brain.Senses.Memory.Targets.Add(target); npc.Brain.Senses.Memory.Players.Add(target); npc.Brain.Senses.Memory.Threats.Add(target); if (npc.Brain.AttackRangeMultiplier != 10) //Check if settings already been modded { if (!Chase.Contains(bot)) { Chase.Add(npc); timer.Once(30, () => { if (npc != null) { Chase.Remove(npc); } }); } float oldTLR = npc.Brain.TargetLostRange; float oldARM = npc.Brain.AttackRangeMultiplier; float oldAIM = npc.aimConeScale; npc.aimConeScale = 0.5f; npc.Brain.TargetLostRange = 800; npc.Brain.AttackRangeMultiplier = 10; bot.Brain.Navigator.CanUseNavMesh = true; //Allow NPC to move again bot.Brain.SetEnabled(true); npc.Brain.Senses.Memory.Targets.Add(target); bot.Brain.Navigator.SetDestination(target.transform.position, BaseNavigator.NavigationSpeed.Fast); npc.Brain.Events.Memory.Entity.Set(target, 0); npc.Invoke(() => { //Restore default settings and remove targets if (npc != null) { npc.aimConeScale = oldAIM; npc.Brain.TargetLostRange = oldTLR; npc.Brain.AttackRangeMultiplier = oldARM; npc.Brain.Senses.Memory.Targets.Clear(); npc.Brain.Senses.Memory.Players.Clear(); npc.Brain.Senses.Memory.Threats.Clear(); } }, 8f); } } } } Pool.FreeUnmanaged(ref list); } private void CreateGun(ScientistNPC npc, string ItemName, string Attachment) { try //Try catch since some times half way though NPC might die and its quicker then adding null check each line { if (string.IsNullOrEmpty(ItemName)) { return; } Item item = ItemManager.CreateByName(ItemName, 1, 0); if (item == null) { return; } BaseEntity be = item.GetHeldEntity(); BaseEntity we = item.GetWorldEntity(); if (be != null) { be.isSpawned = false; } if (we != null) { we.isSpawned = false; } timer.Once(3f, () => { if (be != null) { be.isSpawned = true; } if (we != null) { we.isSpawned = true; } }); if (!item.MoveToContainer(npc.inventory.containerBelt, -1, false)) { item.Remove(); return; } if (be != null && be is BaseProjectile) { if (!string.IsNullOrEmpty(Attachment)) { Item moditem = ItemManager.CreateByName(Attachment, 1, 0); if (moditem != null && item.contents != null) { BaseEntity bemi = moditem.GetHeldEntity(); BaseEntity wemi = moditem.GetWorldEntity(); if (bemi != null) { bemi.isSpawned = false; } if (wemi != null) { wemi.isSpawned = false; } timer.Once(3f, () => { if (bemi != null) { bemi.isSpawned = true; } if (wemi != null) { wemi.isSpawned = true; } }); if (!moditem.MoveToContainer(item.contents)) { item.contents.Insert(moditem); } } } timer.Once(5f, () => { if (npc != null && item != null) { npc.UpdateActiveItem(item.uid); } }); } } catch { } } private void NPCManager(ScientistNPC npc) { npc.InvokeRepeating(() => //Create a loop while NPC is alive { if (npc == null || Chase.Contains(npc)) { return; } //NPC doesnt exsist so return; BaseEntity target = npc.GetBestTarget(); //Read targets from NPC if (target != null && Vector3.Distance(npc.transform.position, target.transform.position) <= config.IgnoreNonVisionMaxDistance) { npc.Brain.Navigator.SetDestination(target.transform.position, BaseNavigator.NavigationSpeed.Fast); //Set NPC to target return; } if (Heavys.ContainsKey(npc) && Heavys[npc] <= UnityEngine.Time.realtimeSinceStartup) //If NPC age above allowed kill NPC { Heavys.Remove(npc); npc.Kill(); return; } if (Vector3.Distance(npc.transform.position, npc.Brain.Events.Memory.Position.Get(4)) > config.IgnoreNonVisionMaxDistance) { npc.Brain.Navigator.SetDestination(npc.Brain.Events.Memory.Position.Get(4), BaseNavigator.NavigationSpeed.Slow, 0.25f, 0f); } else { //Still close to point of intrest so go some where random Vector3 vector = UnityEngine.Random.insideUnitCircle * 15; //Create circle and select a point in it Vector3 newtarget = npc.transform.position + new Vector3(vector.x, 0, vector.y); //Apply that postion as offset to target npc.Brain.Navigator.SetDestination(newtarget, BaseNavigator.NavigationSpeed.Slow, 0.25f, 0f); } }, 10, 10); } private void SpawnCH47LandingZone(Vector3 pos, string kit = "", bool silent = false, bool bypassCoolDown = false, bool bypassblock = false) //Create Landing Zone { if (LastEvent >= UnityEngine.Time.realtimeSinceStartup && !bypassCoolDown) { return; } //Check cooldown if (!bypassblock) { foreach (Vector3 bzone in BlockedAreas) { if (Vector3.Distance(bzone, pos) <= config.BlockRadius) { return; } } } //Block event from running LastEvent = UnityEngine.Time.realtimeSinceStartup + config.EventsCooldownSec; //Update cooldown //Spawn alarm sound fx IOEntity alarmsound = GameManager.server.CreateEntity("assets/prefabs/io/electric/other/alarmsound.prefab", pos, default(Quaternion), true) as IOEntity; alarmsound.Spawn(); //Set up delayed removal of alarm alarmsound.Invoke(() => { if (IsReallyValid(alarmsound)) { alarmsound.Kill(); } }, 60 + config.ExtendAlarmSec); if (config.UseAlarmSound && !silent) { alarmsound.UpdateFromInput(100, 0); } //Trigger Sound if (config.UseFlareSignal && !silent) //Check if should drop flare { BaseEntity flare = GameManager.server.CreateEntity("assets/prefabs/tools/flareold/flare.deployed.prefab", pos, default(Quaternion), true); flare.Spawn(); } //Create landing zone attached to alarm prefab CH47LandingZone zoone = alarmsound.gameObject.AddComponent(); CH47LandingZone closest = CH47LandingZone.GetClosest(alarmsound.transform.position); //Find closes landing zone Vector3 position = SpawnPoint(closest.transform.position); //Create spawn point for ch47 //Spawn ch47 and set up CH47HelicopterAIController component = GameManager.server.CreateEntity("assets/prefabs/npc/ch47/ch47scientists.entity.prefab", position, default(Quaternion)) as CH47HelicopterAIController; component.SetLandingTarget(closest.transform.position); component.Spawn(); SpawnedCH47.Add(component); component.OwnerID = 123456; //How to tell it apart from other CH47's component.InitializeHealth(99999, 9999); //Give lots of health to help if it clipped into something on spawn component.transform.position = position; component.SetMinHoverHeight(position.y + config.HeliSpawnHeightOffset); //Add some extra hight to the hover component.Invoke(() => { if (config.UseBetterNPC && NpcSpawn != null) { foreach (BaseVehicle.MountPointInfo mountPointInfo in component.mountPoints) //Checks each seat { if (mountPointInfo.mountable && mountPointInfo.mountable.AnyMounted()) //Seat valid and has npc mounted to it { ScientistNPC oldnpc = mountPointInfo.mountable.GetMounted() as ScientistNPC; //Convert mount to NPC if (oldnpc != null) { oldnpc.Kill(); NextFrame(() => { HashSet states = new HashSet { "RoamState", "ChaseState", "CombatState" }; var WearItems = new List { new NpcWear { ShortName = "scientistsuit_heavy", SkinID = 0 } }; var BeltItems = new List { new NpcBelt { ShortName = config.BetterNPCGuns.GetRandom(), Amount = 1, SkinID = 0, Mods = new List { string.Empty }, Ammo = string.Empty }, new NpcBelt { ShortName = "syringe.medical", Amount = 10, SkinID = 0, Mods = new List(), Ammo = string.Empty }, }; JObject objectConfig = new JObject { ["Name"] = RandomUsernames.Get((ulong)UnityEngine.Random.Range(0, 10000000)), ["WearItems"] = new JArray { WearItems.Select(x => new JObject { ["ShortName"] = x.ShortName, ["SkinID"] = x.SkinID }) }, ["BeltItems"] = new JArray { BeltItems.Select(x => new JObject { ["ShortName"] = x.ShortName, ["Amount"] = x.Amount, ["SkinID"] = x.SkinID, ["Mods"] = new JArray { x.Mods }, ["Ammo"] = x.Ammo }) }, ["Kit"] = !string.IsNullOrEmpty(config.CustomKit) ? config.CustomKit : "", ["Health"] = config.NPCHealth, ["RoamRange"] = config.BetterNPCRoamRange, ["ChaseRange"] = config.BetterNPCChaseRange, ["SenseRange"] = config.BetterNPCSenseRange, ["ListenRange"] = config.BetterNPCListenRange, ["AttackRangeMultiplier"] = config.AttackRangeMultiplier, ["CheckVisionCone"] = false, ["VisionCone"] = config.VisionCone, ["HostileTargetsOnly"] = false, ["DamageScale"] = config.damageScale, ["TurretDamageScale"] = 1f, ["AimConeScale"] = config.aimConeScale, ["DisableRadio"] = true, ["CanRunAwayWater"] = false, ["CanSleep"] = false, ["SleepDistance"] = 0, ["Speed"] = 3, ["AreaMask"] = 1, ["AgentTypeID"] = -1372625422, ["HomePosition"] = string.Empty, ["MemoryDuration"] = 60, ["States"] = new JArray { states } }; ScientistNPC npc = (ScientistNPC)NpcSpawn.Call("SpawnNpc", mountPointInfo.mountable.transform.position, objectConfig); if (npc != null) { Heavys.Add(npc, config.NPCSuisideSecs); npc.transform.localPosition = position; npc.SendNetworkUpdateImmediate(); npc.Invoke(() => { Heavys.Remove(npc); npc.Kill(); }, config.NPCSuisideSecs); mountPointInfo.mountable.MountPlayer(npc); } }); } } } return; } if (kit == "") { kit = config.CustomKit; } if (kit != "" && !config.UseBetterNPC) //Check if custom kit in settings { if (Kits == null || !IsKit(kit)) { return; } //Checks kits plugin loaded and valid kit foreach (BaseVehicle.MountPointInfo mountPointInfo in component.mountPoints) //Checks each seat { if (mountPointInfo.mountable && mountPointInfo.mountable.AnyMounted()) //Seat valid and has npc mounted to it { ScientistNPC npc = mountPointInfo.mountable.GetMounted() as ScientistNPC; //Convert mount to NPC if (npc != null) { BotSkin(npc, kit); npc.aimConeScale = config.aimConeScale; npc.damageScale = config.damageScale; if (npc.Brain != null) { npc.Brain.IgnoreNonVisionMaxDistance = config.IgnoreNonVisionMaxDistance; npc.Brain.IgnoreSneakersMaxDistance = config.IgnoreSneakersMaxDistance; npc.Brain.AttackRangeMultiplier = config.AttackRangeMultiplier; npc.Brain.TargetLostRange = config.TargetLostRange; npc.Brain.VisionCone = config.VisionCone; } } } } } }, 10); component.Invoke(() => { if (component != null) { component.SetMinHoverHeight(pos.y); component.InitializeHealth(1000, 1000); } }, config.ExtraHeightTimer); //Remove the extra height component.Invoke(() => { if (component == null) { return; } //Check ch47 still exsists after traveling (Should do in a invoke) LandDismountLeave(component, closest.transform.position, Vector3.zero); }, config.TravelTime); //Start checking if should land } private bool IsKit(string kit) { //Call kit plugin to check if its valid kit var success = Kits?.Call("isKit", kit); if (success == null || !(success is bool)) { return false; } return (bool)success; } private void LandDismountLeave(CH47HelicopterAIController component, Vector3 landingpoint, Vector3 oldpos, int trys = 0) { if (component == null) { return; } if (Vector3.Distance(component.transform.position, landingpoint) < 3f || trys >= 10) //Close to landing point { timer.Once(8f, () => { //Dismount all NPCs foreach (BaseVehicle.MountPointInfo mountPointInfo in component.mountPoints) { if (mountPointInfo.mountable && mountPointInfo.mountable.AnyMounted()) { DismountNPCs(mountPointInfo.mountable); } } //Take off again timer.Once(2f, () => { if (component != null) { component.SetMinHoverHeight(component.transform.position.y + 10); } }); //quarter way till despawn leave landing position timer.Once(config.Ch47SuisideSecs / 4, () => { if (component != null) { component.SetMoveTarget(new Vector3(UnityEngine.Random.Range(-9999, 9999), component.transform.position.y + 10, UnityEngine.Random.Range(-9999, 9999))); } }); //Half way till despawn set retire point somewhere random timer.Once(config.Ch47SuisideSecs / 2, () => { if (component != null) { component.SetMoveTarget(new Vector3(UnityEngine.Random.Range(-9999, 9999), 999, UnityEngine.Random.Range(-9999, 9999))); } }); //Despawn Ch47 timer.Once(config.Ch47SuisideSecs, () => { if (IsReallyValid(component)) { SpawnedCH47.Remove(component); component.Kill(); } }); }); return; } if (Vector3.Distance(component.transform.position, oldpos) < 3f) //If hasnt moved more then 3f attempt ladning { component.SetMinHoverHeight(-100); //Set hover below sea level should bring down to ground trys++; } oldpos = component.transform.position; //Set old point as current point to be used if ch47 is stuck component.Invoke(() => { LandDismountLeave(component, landingpoint, oldpos, trys); }, 2); } public Vector3 SpawnPoint(Vector3 target) //Generate a spawn point for the CH47 { Vector3 org = target; //Copy the landing position and call it org int tries = 25; //Set limits of trys to generate a good position while (true) //Loop { --tries; //Remove one of the trys from the limit target = org; //Reload orignal starting point Vector3 vector = UnityEngine.Random.onUnitSphere * config.SpawnDistance; //Create a sphere and select a point at random on its edge target += new Vector3(vector.x, 0, vector.y); //Apply that difference to the orignal position to create a target for spawning if (Vector3.Distance(target, org) >= config.SpawnDistance - 100 || tries <= 0) //If meets conditions or at trys limit { target.y = TerrainMeta.HeightMap.GetHeight(target) + config.HeliHeightOffset; //Set the spawn height of the heli if (target.y < 40) { target.y = 40; } return target; //Return positon } } } private Vector3 ClosestHackableCrate(Vector3 pos) { if (!config.SearchForCrate) { return pos; } //Return home point //Try to find HackableLockedCrate and set that as point of intrest List list = new List(); Vis.Entities(pos, 80, list, -1, QueryTriggerInteraction.Collide); if (list.Count > 0) { pos = list[0].transform.position; } //Found one return frst position return pos; } private void SwitchAgent(ScientistNPC npc, Vector3 pos) { if (!config.TryNavAgents) { return; } //Navmesh disable since couldnt find anything try { timer.Once(5, () => { if (npc == null) { return; } List list = new List(); Vis.Entities(pos, 80, list, -1, QueryTriggerInteraction.Collide); if (list.Count > 0) { npc.Brain.Navigator.SetDestination(list[0].transform.position, BaseNavigator.NavigationSpeed.Fast, 0.25f, 0f); } //Found one return frst position }); timer.Once(30, () => { //Wait 20 secs if npc hasnt moved from its dismount position switch agent to type 0 for monument navmesh if (npc == null || Vector3.Distance(npc.transform.position, pos) >= 3) { return; } npc.Brain.Navigator.Agent.agentTypeID = NavMesh.GetSettingsByIndex(0).agentTypeID; npc.Brain.Navigator.CanUseNavMesh = true; npc.Brain.Navigator.SetDestination(npc.Brain.Events.Memory.Position.Get(4), BaseNavigator.NavigationSpeed.Fast, 0.25f, 0f); }); timer.Once(60, () => { //Wait 20 secs if npc hasnt moved from its dismount position switch agent to type 0 for monument navmesh if (npc == null || Vector3.Distance(npc.transform.position, pos) >= 3) { return; } npc.Brain.Navigator.Agent.agentTypeID = NavMesh.GetSettingsByIndex(2).agentTypeID; npc.Brain.Navigator.CanUseNavMesh = true; npc.Brain.Navigator.SetDestination(npc.Brain.Events.Memory.Position.Get(4), BaseNavigator.NavigationSpeed.Fast, 0.25f, 0f); }); } catch {/*supress facepunch bug*/ } } public Vector3 FindPointOnNavmesh(Vector3 target, float radius) { int tries = 25; //Set limit of trys to find navmesh while (--tries > 0) { Vector3 vector = UnityEngine.Random.insideUnitCircle * 5; //Create circle and select a point in it Vector3 newtarget = target + new Vector3(vector.x, 0, vector.y); //Apply that postion as offset to target newtarget.y = TerrainMeta.HeightMap.GetHeight(newtarget); //Set terrain height if (NavMesh.SamplePosition(newtarget, out _navhit, radius, NavMesh.AllAreas)) //Check that created position for any navmeshes { var a = _navhit.position; //Store hit point if (GamePhysics.CheckSphere(a, 0.5f, Layers.Mask.Player_Server | Layers.Server.Deployed, QueryTriggerInteraction.Ignore)) { continue; } //Check its layers bool faces = Physics.queriesHitBackfaces; //Check for objects Physics.queriesHitBackfaces = true; //Allow checking back side of objects bool isRockFaceUpwards = Physics.Raycast(a, Vector3.up, out _hit, 30f, Layers.Mask.World | Layers.Mask.Terrain) && ((_hit.collider.IsOnLayer(Layer.Terrain) || _prefabs.Exists(_hit.collider.name.ToLower().Contains))); //Find top of rock Physics.queriesHitBackfaces = faces; //Disable checking back sides if (isRockFaceUpwards) { continue; } //if rock is upside down not a good spawn to run loop again Vector3 b = a + new Vector3(0f, 30f, 0f); //Adjust position Vector3 d = a - b; bool isRockFaceDownwards = System.Array.Exists(Physics.RaycastAll(b, d, d.magnitude, Layers.World), hit => _prefabs.Exists(hit.collider.name.ToLower().Contains)); //Check positions offsets if (isRockFaceDownwards) { continue; } //Bad rock position return _navhit.position; //Return good position } } return target; //Return NPCs dismount position } private void DismountNPCs(BaseMountable m) { var position = FindPointOnNavmesh(m.transform.position, 50f); //Try find a navmesh point BasePlayer npc = m.GetMounted(); //Get reference to npc m.DismountAllPlayers(); //Try dismount all npc NextFrame(() => //Wait one frame for dismounts { if (npc) //If reference to npc valid { //Try move that npc npc.MovePosition(position); npc.Teleport(position); npc.SendNetworkUpdateImmediate(); } }); } void BotSkin(ScientistNPC npc, string Skin) { //Remove Default kit npc.inventory.Strip(); //Try apply the kit Kits?.Call("GiveKit", npc, Skin); //Delay for items to spawn from kit timer.Once(0.5f, () => { EquipWeapon(npc); }); } public void EquipWeapon(ScientistNPC npc) { if (npc == null) { return; } //Check NPC still exsists List list = Pool.Get>(); npc.inventory.GetAllItems(list); foreach (Item item in list) //Loop inventory { var e = item.GetHeldEntity() as HeldEntity; //Get held entity if (IsReallyValid(e)) //Check its valid { if (item.skin != 0) //No standard skin { e.skinID = item.skin; e.SendNetworkUpdate(); //Refresh } var weapon = e as BaseProjectile; //Get guns if (IsReallyValid(weapon)) { weapon.primaryMagazine.contents = weapon.primaryMagazine.capacity; //Reload gun weapon.SendNetworkUpdateImmediate(); } if (e is AttackEntity && item.GetRootContainer() == npc.inventory.containerBelt) { var attackEntity = e as AttackEntity; if (attackEntity.hostile) { npc.UpdateActiveItem(item.uid); if (attackEntity is Chainsaw) { (attackEntity as Chainsaw).ServerNPCStart(); } //Start chainsaw engine npc.damageScale = 1f; attackEntity.TopUpAmmo(); attackEntity.SetHeld(true); } } } item.MarkDirty(); } Pool.FreeUnmanaged(ref list); } private bool IsReallyValid(BaseNetworkable a) { return !((object)a == null || a.IsDestroyed || (object)a.net == null); } //Commands private void RunCMD(ConsoleSystem.Arg arg, bool Silent) { if (arg.IsAdmin && arg.Args?.Length >= 3) //Check there are 3 or more args { float x; float y; float z; if (float.TryParse(arg.Args[0].ToString(), out x) && float.TryParse(arg.Args[1].ToString(), out y) && float.TryParse(arg.Args[2].ToString(), out z)) //Try convert to vector3 { Vector3 pos = new Vector3(x, y, z); if (arg.Args.Length == 4) //If there is 4th arg thats kit name { PrintToConsole("ch47trigger @ " + pos + " with kit " + arg.Args[3] + "."); SpawnCH47LandingZone(pos, arg.Args[3].ToString(), Silent, true, true); } else { PrintToConsole("ch47trigger @ " + pos); //No kit passed used default SpawnCH47LandingZone(pos, "", Silent, true, true); } } } } private void RunChatCMD(BasePlayer player, string[] args, bool Silent) { if (!player.IsAdmin) { return; } //Check is admin if (args.Length == 1) //Is passing a kit arg { SpawnCH47LandingZone(player.transform.position, args[0], Silent, true, true); player.ChatMessage("Triggered CH47 event at your position with kit " + args[0] + "."); return; } SpawnCH47LandingZone(player.transform.position, "", Silent, true, true); player.ChatMessage("Triggered CH47 event at your position"); } //Chat commands [ChatCommand("ch47trigger")] private void CMDData(BasePlayer player, string command, string[] args) { RunChatCMD(player, args, false); } [ChatCommand("ch47trigger.silent")] private void CMDData2(BasePlayer player, string command, string[] args) { RunChatCMD(player, args, true); } #endregion #region Harmony [AutoPatch] [HarmonyPatch(typeof(CH47HelicopterAIController), "CalculateOverrideAltitude")] public static class CalculateOverrideAltitude { [HarmonyPostfix] private static void Postfix(CH47HelicopterAIController __instance) { if (__instance.OwnerID == 123456 && __instance.transform.position.y > 480) { __instance.currentDesiredAltitude = 480; __instance.altOverride = 480; } } } [AutoPatch] [HarmonyPatch(typeof(BaseNavigator), "PlaceOnNavMesh")] public static class PlaceOnNavMesh { private static IEnumerable Transpiler(IEnumerable instructions) { List o = instructions.ToList(); bool nop = false; for (int i = 0; i < o.Count; i++) { if (o[i].ToString() == "call Void set_StuckOffNavmesh(Boolean)") { nop = true; continue; } if (o[i].ToString() == "call Void LogWarning(System.Object, UnityEngine.Object)") { o[i].opcode = OpCodes.Nop; o[i].operand = null; nop = false; } if (nop) { o[i].opcode = OpCodes.Nop; o[i].operand = null; } } return o; } } [AutoPatch] [HarmonyPatch(typeof(BaseNavigator), "Warp", typeof(Vector3))] public static class Warp { [HarmonyPrefix] private static bool Prefix(Vector3 position, BaseNavigator __instance, ref bool __result) { try { __instance.Agent.Warp(position); __instance.Agent.enabled = true; __instance.transform.position = position; __result = true; return false; } catch { } return true; } } #endregion } }