using System; using System.Collections.Generic; using Newtonsoft.Json; using UnityEngine; using System.Collections; using System.Linq; using System.Reflection; using HarmonyLib; using Oxide.Core.Plugins; using Rust; namespace Oxide.Plugins { [Info("ScientistHorse", "tofurahie", "1.1.7")] internal class ScientistHorse : RustPlugin { #region Static private Configuration _config; private List Riders = new(); private List positions = new(); private const int scanHeight = 100; private static ScientistHorse _ins; private static int getBlockMask => LayerMask.GetMask("Construction", "Prevent Building", "Water"); private static bool MaskIsBlocked(int mask) => getBlockMask == (getBlockMask | (1 << mask)); #region Classes private class RiderInWorld { public RidableHorse horse; public BasePlayer npc; public void Kill() { if (horse != null && !horse.IsDestroyed) horse.Kill(); if (npc != null && !npc.IsDestroyed) npc.Kill(); } } private class Rider { [JsonProperty(PropertyName = "Prefab of NPC")] public string NPCPrefab; [JsonProperty(PropertyName = "Weapon [shortname]")] public string Weapon; [JsonProperty(PropertyName = "List of armor for NPC [shortname - skinID]", ObjectCreationHandling = ObjectCreationHandling.Replace)] public Dictionary Clothes; [JsonProperty(PropertyName = "Horse armor")] public string horseArmor; [JsonProperty(PropertyName = "Scan radius for Rider(Find player)")] public int scanRadius; } private class Configuration { [JsonProperty(PropertyName = "Bonus damage to NPCs on horseback")] public float bonusDamage = 6; [JsonProperty(PropertyName = "Maximum Riders population")] public int maxPopulation = 10; [JsonProperty(PropertyName = "Kill horse after NPC death")] public bool DeathTogether = true; [JsonProperty(PropertyName = "NPC Rider won't attack player untill he get hit from player")] public bool WaitForHit = true; [JsonProperty(PropertyName = "Rider types", ObjectCreationHandling = ObjectCreationHandling.Replace)] public List Riders = new() { new() { NPCPrefab = "assets/rust.ai/agents/npcplayer/humannpc/scientist/scientistnpc_full_lr300.prefab", horseArmor = "horse.armor.roadsign", scanRadius = 100, Clothes = new () { ["hazmatsuit"] = 0, } } }; } #endregion #endregion #region Config protected override void LoadConfig() { base.LoadConfig(); try { _config = Config.ReadObject(); if (_config == null) throw new Exception(); SaveConfig(); } catch { PrintError("Your configuration file contains an error. Using default configuration values."); LoadDefaultConfig(); } } protected override void SaveConfig() => Config.WriteObject(_config); protected override void LoadDefaultConfig() => _config = new Configuration(); #endregion #region OxideHooks private void OnServerInitialized() { _ins = this; GeneratePositions(); while (Riders.Count < _config.maxPopulation) SpawnRider(_config.Riders.GetRandom(), positions.GetRandom()); timer.Every(60, () => { while (Riders.Count < _config.maxPopulation) SpawnRider(_config.Riders.GetRandom(), positions.GetRandom()); }); } private void OnEntityTakeDamage(ScientistNPC player, HitInfo info) { if (player == null || info == null || player.userID.IsSteamId() || !player.isMounted) return; info.damageTypes.ScaleAll(_config.bonusDamage); } private RiderInWorld GetRider(ulong attakerNetID) => Riders.FirstOrDefault(x => x.npc.net.ID.Value == attakerNetID); private void OnEntityTakeDamage(BasePlayer player, HitInfo info) { if (player == null || info == null || info.InitiatorPlayer == null || !_config.WaitForHit || !info.InitiatorPlayer.userID.IsSteamId() || player.userID.IsSteamId()) return; var rider = GetRider(player.net.ID.Value); if (rider == null) return; rider.horse.GetComponent().Attakers.Add(info.InitiatorPlayer); } private object OnNpcTarget(ScientistNPC npc, BasePlayer target) { if (target == null || npc?.net?.ID.Value == null || !_config.WaitForHit) return null; var rider = GetRider(npc.net.ID.Value); if (rider == null || rider.horse == null || !rider.horse.HasComponent() || rider.horse.GetComponent().Attakers.Contains(target)) return null; return false; } private void OnEntityTakeDamage(RidableHorse horse, HitInfo info) { if (horse != null && Riders.FirstOrDefault(x => x.horse != null && x.horse.net.ID.Value == horse.net.ID.Value) != null && info.damageTypes.Has(DamageType.Decay)) { info.damageTypes.ScaleAll(0); if (_config.WaitForHit && info.InitiatorPlayer != null && info.InitiatorPlayer.userID.IsSteamId()) horse.GetComponent().Attakers.Add(info.InitiatorPlayer); } } private void OnEntityDeath(BasePlayer player, HitInfo info) { if (player == null || player.userID.IsSteamId()) return; foreach (var check in Riders.ToArray()) { if (check == null || check.npc.userID != player.userID) continue; if (_config.DeathTogether) check.horse?.Kill(); else check.horse?.GetComponent()?.Kill(); Riders.Remove(check); } } private void Unload() { foreach (var check in Riders) check.Kill(); _ins = null; } private object OnVehicleDecayReplace(RidableHorse horse) { if (horse == null || Riders.FirstOrDefault(x => x.horse != null && x.horse.net.ID.Value == horse.net.ID.Value) == null) return null; return false; } #endregion #region Commands [ChatCommand("spawnrider")] private void cmdChatspawnrider(BasePlayer player, string command, string[] args) { if (!player.IsAdmin) return; SpawnRider(_config.Riders.GetRandom(), player.transform.position); } #endregion #region Functions private void GeneratePositions() { positions.Clear(); var generationSuccess = 0; var islandSize = ConVar.Server.worldsize / 2; for (var i = 0; i < 500; i++) { if (generationSuccess >= 500) break; var x = Core.Random.Range(-islandSize, islandSize); var z = Core.Random.Range(-islandSize, islandSize); var original = new Vector3(x, scanHeight, z); var position = GetClosestValidPosition(original); if (position == new Vector3()) continue; positions.Add(position); generationSuccess++; } } private Vector3 GetClosestValidPosition(Vector3 original) { var target = original - new Vector3(0, 200, 0); RaycastHit hitInfo; if (Physics.Linecast(original, target, out hitInfo) == false) return new Vector3(); var position = hitInfo.point; var collider = hitInfo.collider; var colliderLayer = collider?.gameObject.layer ?? 4; if (collider == null) return new Vector3(); if (MaskIsBlocked(colliderLayer) || colliderLayer != 23 || TerrainMeta.HeightMap.GetHeight(hitInfo.point) <= 0) return new Vector3(); return IsValidPosition(position) == false ? new Vector3() : position; } private Vector3 GetValidSpawnPoint() { for (var i = 0; i < 25; i++) { var number = Core.Random.Range(0, positions.Count); var position = positions.ElementAt(number); if (IsValidPosition(position)) return position; positions.Remove(position); } return new Vector3(); } private static bool IsValidPosition(Vector3 position) { var entities = new List(); Vis.Entities(position, 25, entities); return entities.Count == 0; } private void SpawnRider(Rider rider, Vector3 position) { var npc = (ScientistNPC) GameManager.server.CreateEntity(rider.NPCPrefab, position); npc.enableSaving = false; npc.Spawn(); npc.GetComponent().CanUseNavMesh = false; npc.inventory.containerWear.Clear(); foreach (var check in rider.Clothes) ItemManager.CreateByName(check.Key, 1, check.Value).MoveToContainer(npc.inventory.containerWear); if (!string.IsNullOrEmpty(rider.Weapon) && !rider.Weapon.Contains("bow") && ItemManager.FindItemDefinition(rider.Weapon) != null) { npc.inventory.containerBelt.Clear(); npc.GiveItem(ItemManager.CreateByName(rider.Weapon, 1, 0)); } var horse = (RidableHorse) GameManager.server.CreateEntity("assets/content/vehicles/horse/ridablehorse.prefab", position); horse.enableSaving = false; horse.skinID = 119; horse.Spawn(); ItemManager.CreateByName(rider.horseArmor).MoveToContainer(horse.equipmentInventory); horse.EquipmentUpdate(); (horse.children.FirstOrDefault(x => x.PrefabName.Contains("saddle")) as BaseMountable)?.MountPlayer(npc); var comp = horse.gameObject.AddComponent(); comp._rider = rider; comp.npc = npc; comp.load = true; Riders.Add(new RiderInWorld{npc = npc, horse = horse}); } [AutoPatch] [HarmonyPatch(typeof(RidableHorse), "RagdollHorse")] private class PatchRagdoll { public static bool Prefix(RidableHorse __instance) { if (__instance.skinID == 119) return false; return true; } } private class RiderAI : FacepunchBehaviour { private BasePlayer _target; private RidableHorse _horse; public Rider _rider; public bool load = false; public ScientistNPC npc; public FieldInfo throttleInput, steerInput; public float LastTargetTime; public HashSet Attakers = new(); private void Start() { _horse = GetComponent(); throttleInput = typeof(RidableHorse).GetField("throttleInput", BindingFlags.NonPublic | BindingFlags.Instance); steerInput = typeof(RidableHorse).GetField("steerInput", BindingFlags.NonPublic | BindingFlags.Instance); StartCoroutine(FindPath()); } private IEnumerator FindPath() { while (load) yield return new WaitForSeconds(Think()); } private float Think() { if (_horse == null || _horse.IsDead() || npc == null || npc.IsDead()) { load = false; Destroy(this); return 0; } if (_target == null || (_ins._config.WaitForHit && Time.time - LastTargetTime > 25 && !Attakers.Contains(_target))) { FindTarget(); if (_target == null) FindClosestTarget(); if (_target != null) return 0.5f; StopMove(); StopRotate(); return 5; } if (_target == null || _target.IsSleeping() || !_target.IsConnected || _target.IsDead() || _target.IsSwimming() || _target.InSafeZone()) { _target = null; return 5; } if (Vector2.Distance(_target.transform.position, npc.transform.position) >= 15f) { _target = null; return 0.5f; } _horse.currentStamina = _horse.maxStamina; RotateToTarget(); MoveToTarget(); return 0.5f; } private void MoveToTarget() { var distance = GetDistance(); if (distance > _rider.scanRadius) { _target = null; return; } if (distance > distance * 0.85) { StartRun(); return; } StartWalk(); } private void RotateToTarget() { var angle = GetAngle(); if (angle > 0) { if (angle < 35) { StopRotate(); return; } RotateRight(); return; } if (angle > -35) { StopRotate(); return; } RotateLeft(); } private float GetDistance() => Vector3.Distance(_target.transform.position, transform.position); private float GetAngle() => Vector3.SignedAngle(_target.transform.position - transform.position, transform.forward, Vector3.up); private void StartRun() { _horse.currentGait = RidableHorse.GaitType.Canter; throttleInput.SetValue(_horse, 1f); } private void StartWalk() { _horse.currentGait = RidableHorse.GaitType.Walk; throttleInput.SetValue(_horse, 1f); } private void StopMove() => throttleInput.SetValue(_horse, 0f); private void RotateLeft() => steerInput.SetValue(_horse, 1f); private void RotateRight() => steerInput.SetValue(_horse, -1f); private void StopRotate() => steerInput.SetValue(_horse, 0f); private void FindTarget() { var players = Facepunch.Pool.Get>(); Vis.Entities(_horse.transform.position, _rider.scanRadius, players); foreach (var check in players.ToArray()) if (check.IsSleeping() || !check.userID.IsSteamId() || !check.IsConnected || check.IsDead() || check.IsSwimming() || check.InSafeZone() ||!check.IsAdmin) players.Remove(check); if (players.Count > 0) { _target = players.GetRandom(); LastTargetTime = Time.time; } Facepunch.Pool.FreeUnmanaged(ref players); } private void FindClosestTarget() { BasePlayer player = null; foreach (var check in BasePlayer.activePlayerList) { if (check.IsSleeping() || !check.userID.IsSteamId() || !check.IsConnected || check.IsDead() || check.IsSwimming() || check.InSafeZone() || !check.IsAdmin || player != null && Vector2.Distance(npc.transform.position, check.transform.position) >= Vector2.Distance(npc.transform.position, player.transform.position)) continue; player = check; } _target = player; } public void Kill() => Destroy(this); private void OnDestroy() { load = false; StopCoroutine(FindPath()); } } #endregion } }