/*▄▄▄▄ ███▄ ▄███▓ ▄████ ▄▄▄██▀▀▀▓█████▄▄▄█████▓ ▓█████▄ ▓██▒▀█▀ ██▒ ██▒ ▀█▒ ▒██ ▓█ ▀▓ ██▒ ▓▒ ▒██▒ ▄██▓██ ▓██░▒██░▄▄▄░ ░██ ▒███ ▒ ▓██░ ▒░ ▒██░█▀ ▒██ ▒██ ░▓█ ██▓▓██▄██▓ ▒▓█ ▄░ ▓██▓ ░ ░▓█ ▀█▓▒██▒ ░██▒░▒▓███▀▒ ▓███▒ ░▒████▒ ▒██▒ ░ ░▒▓███▀▒░ ▒░ ░ ░ ░▒ ▒ ▒▓▒▒░ ░░ ▒░ ░ ▒ ░░ ▒░▒ ░ ░ ░ ░ ░ ░ ▒ ░▒░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ */ using Facepunch; using Rust; using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using UnityEngine; using UnityEngine.AI; using UnityEngine.SceneManagement; namespace Oxide.Plugins { [Info("RandomOilCraters", "bmgjet", "1.2.3")] class RandomOilCraters : RustPlugin { //Vars private Coroutine _coroutine; private Timer _timer; public List CratersSpawns = new List(); public List Crater = new List(); public List NPCs = new List(); private NavMeshHit _navhit; private RaycastHit _hit; private List _prefabs = new List { "rock", "formation", "cliff" }; //Terrain Stuffq //Classes private class Elevation { public float Min { get; set; } public float Max { get; set; } } //Config private PluginConfig config; protected override void LoadDefaultConfig() { Config.WriteObject(GetDefaultConfig(), true); } private PluginConfig GetDefaultConfig() { return new PluginConfig { NumberOfCraters = 3, //Number of craters to maintain MinDistanceBetweenCraters = 400, //Distance between last spawned craters CraterDespawnTimerOut = 1800, //Time to despawn crater point SpawnNPCAtCrater = true, //Spawn NPC at craters NPCHealth = 120, //Set health of NPC CheckerDelay = 300, //How often to check for new craters CraterExplodesOnSpawn = true, //Crater explodes when it spawns FlameBurnTimer = 600, //How long the flame last after explosion FixPumpJacks = true, //Sets pump jacks to static so they mine without resource manager }; } private class PluginConfig { public int NumberOfCraters; public int MinDistanceBetweenCraters; public int CraterDespawnTimerOut; public bool SpawnNPCAtCrater; public int CheckerDelay; public int NPCHealth; public bool CraterExplodesOnSpawn; public bool FixPumpJacks; public int FlameBurnTimer; } //Oxide Hooks private void Init() { config = Config.ReadObject(); } //Load Config private void OnServerInitialized(bool initial) { Startup(initial ? 30 : 1); } //Server Loaded void Unload() //Server Unload { if (_timer != null) { _timer?.Destroy(); } //Destroy spawner timer foreach (var baseEntity in Crater) { if (baseEntity != null && !baseEntity.IsDestroyed) { baseEntity.Kill(); } } //Remove all spawned Craters foreach (var npc in NPCs) { if (npc != null && !npc.IsDestroyed) { npc.Kill(); } } //Remove spawned NPCs if (_coroutine != null) { ServerMgr.Instance.StopCoroutine(_coroutine); } //Stop coroutine if it exsists. } private void OnEntityKill(SurveyCrater crater) { if (Crater.Contains(crater)) //Check if crater spawned by plugin { Crater.Remove(crater); //Remove from list if (config.FixPumpJacks) //Fix pump jack by setting it as static and give it a skin to ID it later { Vector3 pos = crater.transform.position; NextFrame(() => { MiningQuarry quarry = FindQuarry(pos, 1); if (quarry != null && quarry.ShortPrefabName == "mining.pumpjack") { quarry.skinID = 0011223344; quarry.isStatic = true; quarry.UpdateStaticDeposit(); } }); } } } private void OnPlayerDeath(ScientistNPC npc, HitInfo info) { if (NPCs.Contains(npc)) { NPCs.Remove(npc); } } //Remove spawned Heavy NPC from list //Functions private void Startup(int delay) { timer.Once(delay, () => { if (config.FixPumpJacks) //Check if should fix { int fix = 0; foreach (BaseNetworkable bn in BaseNetworkable.serverEntities.entityList.Get().Values) //Scan all server ents { if (bn is MiningQuarry && (bn as MiningQuarry).skinID == 0011223344) //Matches custom skin ID we set { (bn as MiningQuarry).isStatic = true; //Set as static so it can mine with no resource manager layer (bn as MiningQuarry).UpdateStaticDeposit(); fix++; } } Puts("Fixed " + fix.ToString() + " Pumpjacks."); } _coroutine = ServerMgr.Instance.StartCoroutine(FindPosition()); });//Start up coroutine } private bool IsFlatTerrain(Vector3 center, Elevation elevation, float value) { return elevation.Max - elevation.Min <= value && elevation.Max - center.y <= value; } //Check ground is level private MiningQuarry FindQuarry(Vector3 Location, float Radius) //Scan for mining quarry in radius { List list = new List(); Vis.Entities(Location, Radius, list); foreach (MiningQuarry entity in list) { return entity; } return null; } private IEnumerator FindPosition() { int minPos = (int)(World.Size / -2f); int maxPos = (int)(World.Size / 2f); var checks = 0; var _instruction = ConVar.FPS.limit <= 30 ? CoroutineEx.waitForSeconds(0.003f) : null; //Slow down to help server FPS for (float x = minPos; x < maxPos; x += 5) //Scan whole map { for (float z = minPos; z < maxPos; z += 5) { if (++checks >= 512) { checks = 0; yield return _instruction; //Check if should slow down } Vector3 original = new Vector3(x, 0, z); //Get position original.y = TerrainMeta.HeightMap.GetHeight(original) - 0.05f; //Set ground height if (original.y <= 0) { continue; } //Skip below water level if (TerrainMeta.TopologyMap.GetTopology(original, TerrainTopology.LAKE) || TerrainMeta.TopologyMap.GetTopology(original, TerrainTopology.ROAD) || TerrainMeta.TopologyMap.GetTopology(original, TerrainTopology.RAIL) || TerrainMeta.TopologyMap.GetTopology(original, TerrainTopology.ROADSIDE) || TerrainMeta.TopologyMap.GetTopology(original, TerrainTopology.RAILSIDE) || TerrainMeta.TopologyMap.GetTopology(original, TerrainTopology.MONUMENT) || TerrainMeta.TopologyMap.GetTopology(original, TerrainTopology.OCEAN) || TerrainMeta.TopologyMap.GetTopology(original, TerrainTopology.BUILDING) || TerrainMeta.TopologyMap.GetTopology(original, TerrainTopology.RIVER) || TerrainMeta.TopologyMap.GetTopology(original, TerrainTopology.RIVERSIDE)) { continue; } //Skip Topologys if(IsFlatTerrain(original, GetTerrainElevation(original), 4) && SpawnCheckfunction(original, 16, -1) && !BuildingBlocked(original)) { CratersSpawns.Add(original); } } //Is a valid spawn position } Puts("Created " + CratersSpawns.Count.ToString() + " Crater Spawn Points."); CraterSpawner(); _timer = timer.Every(config.CheckerDelay, CraterSpawner); //Start up spawner manager } private void CreateFire(BaseEntity parant) { timer.Once(1, () => { if (parant == null) { return; } //null check //Play mine explosion effect Effect.server.Run("assets/bundled/prefabs/fx/weapons/landmine/landmine_explosion.prefab", parant.PivotPoint(), parant.transform.up, null, true); //Create fireball FireBall fireBallSPAWN = GameManager.server.CreateEntity("assets/bundled/prefabs/oilfireballsmall.prefab", parant.transform.position) as FireBall; fireBallSPAWN.Spawn(); fireBallSPAWN.ignoreNPC = true; //Dont damage the NPC fireBallSPAWN.EnableSaving(false); fireBallSPAWN.SetParent(parant, true, true); Rigidbody rb = fireBallSPAWN.GetComponent(); rb.isKinematic = true; fireBallSPAWN.CancelInvoke(fireBallSPAWN.Extinguish); //Dont auto go out fireBallSPAWN.CancelInvoke(fireBallSPAWN.TryToSpread); //Dont spread fireBallSPAWN.Invoke(fireBallSPAWN.Extinguish, config.FlameBurnTimer); //Set up death timer of fireball }); } private void CraterSpawner() { List pos = CratersSpawns; //Make a copy of spawn points for (int i = pos.Count - 1; i >= 0; i--) { if (Crater.Count >= config.NumberOfCraters) { break; } //At maintained limit Vector3 p = pos.GetRandom(); //Pick point at random pos.Remove(p); //Remove from list bool spawn = true; foreach (var c in Crater) { if (c != null && Vector3.Distance(c.transform.position, p) <= config.MinDistanceBetweenCraters) { spawn = false; break; } } //Check distance between craters if (!spawn || BuildingBlocked(p)) { continue; } //Check if is valid spot SurveyCrater baseent = GameManager.server.CreateEntity("assets/prefabs/tools/surveycharge/survey_crater_oil.prefab", p) as SurveyCrater; //Spawn crater baseent.Spawn(); baseent.InitializeHealth(config.FlameBurnTimer * 8 + 100, config.FlameBurnTimer * 8 + 100); //Set health Crater.Add(baseent); //Add to list NextFrame(() => //Wait one frame { baseent.CancelInvoke(baseent.RemoveMe); //Remove built in kill timer baseent.Invoke(new Action(baseent.RemoveMe), config.CraterDespawnTimerOut); //Set up new kill timer if (config.CraterExplodesOnSpawn) { CreateFire(baseent); } if (config.SpawnNPCAtCrater) { //Spawn NPC var npcpos = FindPointOnNavmesh(baseent.transform.position, 5); //get position var prefabName = StringPool.Get(712785714); //Pistol NPC var prefab = GameManager.server.FindPrefab(prefabName); var go = Facepunch.Instantiate.GameObject(prefab); go.SetActive(false); go.name = prefabName; go.transform.position = npcpos; ScientistNPC npc = go.GetComponent(); SceneManager.MoveGameObjectToScene(go, Rust.Server.EntityScene); npc.userID = (ulong)UnityEngine.Random.Range(0, 10000000); npc.UserIDString = npc.userID.ToString(); npc.startHealth = config.NPCHealth; go.SetActive(true); npc.Spawn(); npc.SendNetworkUpdateImmediate(); NextFrame(() => { NavMeshHit hit; if (NavMesh.SamplePosition(npc.transform.position, out hit, 50, -1)) //Try find navmeshes { npc.Brain.Navigator.CanUseNavMesh = false; //Block NPC move npc.gameObject.layer = 17; npc.Brain.Navigator.Agent.agentTypeID = NavMesh.GetSettingsByIndex(1).agentTypeID; npc.Brain.Navigator.Warp(hit.position); //Move NPC to found navmesh npc.Invoke(() => { npc.Brain.Navigator.CanUseNavMesh = true; //Allow NPC to move again npc.Brain.Navigator.BestRoamPointMaxDistance = 15; //Try set some limits NPC ignores under a lot of conditions npc.Brain.Navigator.MaxRoamDistanceFromHome = 20; npc.Brain?.Events?.Memory.Position.Set(npcpos, 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; } }); npc.Invoke(() => { if (npc != null && !npc.IsDestroyed) { NPCs.Remove(npc); npc.Kill(); } }, config.CraterDespawnTimerOut); //Set up new kill timer NPCManager(npc); //Simple logic for NPC Control NPCs.Add(npc); //Add to list } }); } } 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.GetAreaFromName("HumanNPC"))) //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 NPCManager(ScientistNPC npc) { npc.InvokeRepeating(() => //Create a loop while NPC is alive { if (npc == null) { 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) <= 20) { npc.Brain.Navigator.SetDestination(target.transform.position, BaseNavigator.NavigationSpeed.Fast, 0.25f, 0f); //Set NPC to target return; } if (Vector3.Distance(npc.transform.position, npc.Brain.Events.Memory.Position.Get(4)) > 20) { 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 * 10; //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); } }, 30, 30); } public bool BuildingBlocked(Vector3 pos) //Check area { List list = Facepunch.Pool.Get>(); Vis.Entities(pos, 12f, list, 2097152, QueryTriggerInteraction.Collide); if (list.Count == 0) //Nothing found so must be clear area { Facepunch.Pool.FreeUnmanaged(ref list); return false; } bool flag = false; foreach (var v in list) //Check each entity found { if (v is BuildingBlock || v is MiningQuarry || v is SurveyCrater) //Blocked area { flag = true; break; } } Facepunch.Pool.FreeUnmanaged(ref list); return flag; } public bool SpawnCheckfunction(Vector3 position, float radius, int layers) //Check Colliders of Area { bool flag = true; var colliders = Pool.Get>(); Vis.Colliders(position, radius, colliders, layers, QueryTriggerInteraction.Collide); foreach (var collider in colliders) { //Blocked colliders if (collider.name.Contains("SafeZone", CompareOptions.OrdinalIgnoreCase) || collider.name.Contains("rock_", CompareOptions.OrdinalIgnoreCase) || collider.name.Contains("modding", CompareOptions.OrdinalIgnoreCase) || collider.name.Contains("structures", CompareOptions.OrdinalIgnoreCase) || collider.name.Contains("formation_", CompareOptions.OrdinalIgnoreCase) || collider.name.Contains("Prevent_", CompareOptions.OrdinalIgnoreCase) || collider.name.Contains("PreventBuilding", CompareOptions.OrdinalIgnoreCase) || collider.name.Contains("cliff_", CompareOptions.OrdinalIgnoreCase) || collider.name.Contains("crudeoutput", CompareOptions.OrdinalIgnoreCase) || collider.name.Contains("ice_", CompareOptions.OrdinalIgnoreCase)) { flag = false; break; } //Check entitys var e = collider.ToBaseEntity(); if (e.IsValid()) { if (e is BasePlayer) //Block baseplayers unless flying such as admin { var player = e as BasePlayer; if (!player.IsConnected || player.IsFlying) { continue; } else { flag = false; break; } } if (e is JunkPile) { flag = false; break; } //Blocked by junkpile if (e is BuildingBlock) { flag = false; break; } if (e is MiningQuarry) { flag = false; break; } } } Pool.FreeUnmanaged(ref colliders); return flag; } private List GetCircumferencePositions(Vector3 center, float radius, float next, float y = 0f) { float degree = 0f; float angleInRadians = 2f * Mathf.PI; List positions = new(); while (degree < 360) { float radian = (angleInRadians / 360) * degree; float x = center.x + radius * Mathf.Cos(radian); float z = center.z + radius * Mathf.Sin(radian); Vector3 a = new(x, y, z); positions.Add(y == 0f ? a.WithY(TerrainMeta.HeightMap.GetHeight(a)) : a); degree += next; } return positions; } private Elevation GetTerrainElevation(Vector3 center) { float maxY = -1000; float minY = 1000; foreach (var position in GetCircumferencePositions(center, 16, 16)) { if (position.y > maxY) maxY = position.y; if (position.y < minY) minY = position.y; } return new Elevation { Min = minY, Max = maxY }; } //Chat Command [ChatCommand("showcraters")] //Chat Command private void CMDShow(BasePlayer player, string command, string[] args) { if (!player.IsAdmin) { return; } //Admins only player.ChatMessage("Currently " + Crater.Count + " Active Craters."); foreach (var v in Crater) { player.SendConsoleCommand("ddraw.sphere", 10f, Color.blue, v.transform.position, 10f); } } } }