using Newtonsoft.Json; using Oxide; using Oxide.Core; using Oxide.Core.Plugins; using System; using System.Collections.Generic; using System.Linq; using UnityEngine; namespace Oxide.Plugins { [Info("SimplePatrolSignal", "Yac Vaguer", "1.1.0")] [Description("Call a Patrol Helicopter to your location using a special supply signal.")] public class SimplePatrolSignal : RustPlugin { [PluginReference("NoEscape")] private Plugin NoEscape; private const string HELI_PREFAB = "assets/prefabs/npc/patrol helicopter/patrolhelicopter.prefab"; private const string PermissionUse = "simplepatrolsignal.use"; private const string PermissionVIP = "simplepatrolsignal.vip"; private const string PermissionAdmin = "simplepatrolsignal.admin"; private StoredData storedData; private PatrolHelicopter patrol; private ConfigurationManager config; private bool isActive = false; private bool originalUseDangerZones; private bool originalMonumentCrash; private Vector3 patrolZone; private Timer reconsiderTimer; private List processedContainers = new List(); #region Initialization private void Init() { permission.RegisterPermission(PermissionUse, this); permission.RegisterPermission(PermissionVIP, this); permission.RegisterPermission(PermissionAdmin, this); LoadDefaultMessages(); } private void OnServerInitialized() { LoadConfigValues(); LoadData(); CleanupExpiredCooldowns(); } private void Unload() { SaveData(); DestroyPatrol(); } #endregion #region Localization private void LoadDefaultMessages() { lang.RegisterMessages(new Dictionary { ["NotAllowed"] = "You are not allowed to use this command.", ["HeliSignalActive"] = "Another Patrol Heli Signal is already active.", ["PatrolCalled"] = "A patrol helicopter is on its way to your location!", ["DestroyingPatrol"] = "The patrol helicopter is leaving the area.", ["ReceivedHeliSignal"] = "You have received a Patrol Heli Signal.", ["CooldownActive"] = "You must wait {0} minutes before using another Patrol Heli Signal.", ["VIPCooldownActive"] = "[VIP] You must wait {0} minutes before using another Patrol Heli Signal.", ["CooldownReset"] = "Your cooldown has been reset.", ["CooldownResetTarget"] = "You've reset the cooldown for {0}.", ["NoActiveHeli"] = "There is no active patrol helicopter.", ["HeliDespawned"] = "Patrol helicopter has been despawned.", ["InvalidPlayer"] = "Player not found.", ["RaidBlocked"] = "You cannot call a patrol helicopter during a raid block.", ["NoEscapeBlocked"] = "You cannot call a patrol helicopter during combat block." }, this); } private string GetMessage(string key, string userId = null) => lang.GetMessage(key, this, userId); #endregion #region Data Management private class StoredData { public Dictionary Cooldowns = new Dictionary(); } private void SaveData() => Interface.Oxide.DataFileSystem.WriteObject(Name, storedData); private void OnServerSave() { SaveData(); } private void LoadData() { try { storedData = Interface.Oxide.DataFileSystem.ReadObject(Name) ?? new StoredData(); double currentTime = CurrentTime(); storedData.Cooldowns = storedData.Cooldowns .Where(kvp => (currentTime - kvp.Value) < config.Signal.CooldownSeconds) .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); } catch { storedData = new StoredData(); } } #endregion #region Commands [ChatCommand("helisignal")] private void CmdHeliSignalChat(BasePlayer player, string command, string[] args) { if (args.Length == 0) { GiveHeliSignal(player); return; } if (!permission.UserHasPermission(player.UserIDString, PermissionAdmin)) { player.ChatMessage(GetMessage("NotAllowed", player.UserIDString)); return; } switch (args[0].ToLower()) { case "reset": if (args.Length > 1) { var target = BasePlayer.Find(args[1]); if (target == null) { player.ChatMessage(GetMessage("InvalidPlayer", player.UserIDString)); return; } ResetCooldown(target); player.ChatMessage(string.Format(GetMessage("CooldownResetTarget", player.UserIDString), target.displayName)); } else { ResetCooldown(player); player.ChatMessage(GetMessage("CooldownReset", player.UserIDString)); } break; case "despawn": if (!isActive) { player.ChatMessage(GetMessage("NoActiveHeli", player.UserIDString)); return; } DestroyPatrol(); player.ChatMessage(GetMessage("HeliDespawned", player.UserIDString)); break; } } private void ResetCooldown(BasePlayer player) { if (storedData?.Cooldowns == null) { PrintError("storedData or storedData.Cooldowns is null, cannot reset cooldown!"); return; } storedData.Cooldowns.Remove(player.userID); SaveData(); } private void CleanupExpiredCooldowns() { double currentTime = CurrentTime(); var expiredCooldowns = storedData.Cooldowns .Where(kvp => (currentTime - kvp.Value) >= config.Signal.CooldownSeconds) .Select(kvp => kvp.Key) .ToList(); foreach (var userId in expiredCooldowns) { storedData.Cooldowns.Remove(userId); } if (expiredCooldowns.Count > 0) SaveData(); } [ConsoleCommand("helisignal")] private void CmdHeliSignalConsole(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player != null) { GiveHeliSignal(player); } } private void GiveHeliSignal(BasePlayer player) { if (!HasPermission(player)) { player.ChatMessage(GetMessage("NotAllowed", player.UserIDString)); return; } var supplySignal = CreateHeliSignalItem(); if (supplySignal != null) { player.GiveItem(supplySignal); player.ChatMessage(GetMessage("ReceivedHeliSignal", player.UserIDString)); } } private bool HasPermission(BasePlayer player) { return permission.UserHasPermission(player.UserIDString, PermissionUse) || player.IsAdmin; } #endregion #region Cooldown Management private bool IsOnCooldown(BasePlayer player) { if (!storedData.Cooldowns.TryGetValue(player.userID, out double lastUsage)) return false; double timeSinceLastUse = CurrentTime() - lastUsage; float cooldownTime = GetPlayerCooldownTime(player); return timeSinceLastUse < cooldownTime; } private double GetRemainingCooldown(BasePlayer player) { if (!storedData.Cooldowns.TryGetValue(player.userID, out double lastUsage)) return 0; double timeSinceLastUse = CurrentTime() - lastUsage; float cooldownTime = GetPlayerCooldownTime(player); double remainingTime = cooldownTime - timeSinceLastUse; return remainingTime > 0 ? remainingTime : 0; } private float GetPlayerCooldownTime(BasePlayer player) { return HasVIPPermission(player) ? config.Signal.VIPCooldownSeconds : config.Signal.CooldownSeconds; } private void SetCooldown(BasePlayer player) { storedData.Cooldowns[player.userID] = CurrentTime(); SaveData(); } private double CurrentTime() => System.DateTime.UtcNow.Subtract(System.DateTime.UnixEpoch).TotalSeconds; private bool HasVIPPermission(BasePlayer player) { return permission.UserHasPermission(player.UserIDString, PermissionVIP); } #endregion #region Event Hooks private bool IsRaidBlocked(BasePlayer player) { if (NoEscape == null || !NoEscape.IsLoaded) { PrintWarning("It seems that the NoEscape Plugin is not loaded and you are trying to use it"); return false; } return (bool)(NoEscape.Call("IsRaidBlocked", player) ?? false); } private bool IsNoEscapeActive(BasePlayer player) { if (NoEscape == null || !NoEscape.IsLoaded) { PrintWarning("It seems that the NoEscape Plugin is not loaded and you are trying to use it"); return false; } return (bool)(NoEscape.Call("IsCombatBlocked", player) ?? false); } private void OnExplosiveThrown(BasePlayer player, BaseEntity entity) { if (entity is SupplySignal signal && signal.skinID == config.Signal.SkinId) { if (config.BlockDuringRaid && IsRaidBlocked(player)) { player.ChatMessage(GetMessage("RaidBlocked", player.UserIDString)); ReturnSignal(player, signal); return; } if (config.BlockDuringNoEscape && IsNoEscapeActive(player)) { player.ChatMessage(GetMessage("NoEscapeBlocked", player.UserIDString)); ReturnSignal(player, signal); return; } if (IsOnCooldown(player)) { double remainingSeconds = Math.Ceiling(GetRemainingCooldown(player) / 60.0); string messageKey = HasVIPPermission(player) ? "VIPCooldownActive" : "CooldownActive"; player.ChatMessage(string.Format(GetMessage(messageKey, player.UserIDString), remainingSeconds)); ReturnSignal(player, signal); return; } signal.CancelInvoke(signal.Explode); signal.Invoke(signal.KillMessage, 30f); timer.Once(config.Signal.Warmup, () => { CallPatrolHelicopter(player); SetCooldown(player); signal.Kill(); }); } } private void ReturnSignal(BasePlayer player, SupplySignal signal) { var returnedSignal = CreateHeliSignalItem(); if (returnedSignal != null) { player.GiveItem(returnedSignal); } signal.Kill(); } private object CanLootEntity(BasePlayer player, LootContainer container) { if (container == null || !config.LootSettings.Enabled) return null; if (processedContainers.Contains(container)) return null; processedContainers.Add(container); string containerName = container.ShortPrefabName; float dropChance; if (config.LootSettings.Containers.TryGetValue(containerName, out dropChance)) { if (RollChance(dropChance)) { var item = CreateHeliSignalItem(); if (item != null) { container.inventory.capacity++; container.inventorySlots++; item.MoveToContainer(container.inventory); } } } return null; } private void OnEntityKill(LootContainer container) { if (container != null) { processedContainers.Remove(container); } } #endregion #region Patrol Helicopter Logic private void CallPatrolHelicopter(BasePlayer player) { if (isActive) { player.ChatMessage(GetMessage("HeliSignalActive", player.UserIDString)); GiveHeliSignal(player); return; } isActive = true; patrolZone = player.transform.position; SpawnPatrolHelicopter(); player.ChatMessage(GetMessage("PatrolCalled", player.UserIDString)); } private void SpawnPatrolHelicopter() { originalUseDangerZones = PatrolHelicopterAI.use_danger_zones; originalMonumentCrash = PatrolHelicopterAI.monument_crash; Vector3 spawnPosition = patrolZone + (Vector3.forward * 500f); spawnPosition.y = 60f; patrol = GameManager.server.CreateEntity(HELI_PREFAB, spawnPosition) as PatrolHelicopter; if (patrol == null) { PrintError("Failed to create Patrol Helicopter entity."); isActive = false; return; } patrol.enableSaving = false; patrol.Spawn(); patrol.InitializeHealth(config.Patrol.Health, config.Patrol.Health); if (patrol.weakspots != null) { if (patrol.weakspots.Length > 0) { patrol.weakspots[0].maxHealth = config.Patrol.MainRotorHealth; patrol.weakspots[0].health = config.Patrol.MainRotorHealth; } if (patrol.weakspots.Length > 1) { patrol.weakspots[1].maxHealth = config.Patrol.TailRotorHealth; patrol.weakspots[1].health = config.Patrol.TailRotorHealth; } } patrol.myAI.timeBetweenRockets = config.Patrol.TimeBeforeRocket; patrol.maxCratesToSpawn = config.Patrol.CrateAmount; PatrolHelicopterAI.use_danger_zones = false; PatrolHelicopterAI.monument_crash = false; patrol.myAI.hasInterestZone = true; patrol.myAI.interestZoneOrigin = patrolZone; patrol.myAI.ExitCurrentState(); reconsiderTimer = timer.Repeat(10f, 0, ReconsiderPosition); timer.Once(config.Patrol.Duration, DestroyPatrol); } private void ReconsiderPosition() { if (patrol == null || patrol.IsDestroyed) { DestroyPatrol(); return; } if (patrol.myAI.leftGun.HasTarget() || patrol.myAI.rightGun.HasTarget()) { return; } patrol.myAI.State_Move_Enter(patrolZone); } private void DestroyPatrol() { Puts(GetMessage("DestroyingPatrol")); if (reconsiderTimer != null && !reconsiderTimer.Destroyed) { reconsiderTimer.Destroy(); } if (patrol != null && !patrol.IsDestroyed) { patrol.myAI.Retire(); patrol.Kill(); patrol = null; } PatrolHelicopterAI.use_danger_zones = originalUseDangerZones; PatrolHelicopterAI.monument_crash = originalMonumentCrash; isActive = false; } #endregion #region Helper Methods private Item CreateHeliSignalItem() { var item = ItemManager.CreateByName("supply.signal", 1, config.Signal.SkinId); if (item != null) { item.name = config.Signal.DisplayName; } return item; } private bool RollChance(float chance) { return UnityEngine.Random.Range(0f, 100f) <= chance; } #endregion #region Configuration private class ConfigurationManager { [JsonProperty("Version")] public VersionConfig version { get; set; } = new VersionConfig(); public class VersionConfig { [JsonProperty("Major")] public int major { get; set; } = 1; [JsonProperty("Minor")] public int minor { get; set; } = 0; [JsonProperty("Patch")] public int patch { get; set; } = 0; } [JsonProperty("Supply Signal Settings")] public SupplySignalSettings Signal { get; set; } = new SupplySignalSettings(); [JsonProperty("Patrol Helicopter Settings")] public PatrolSettings Patrol { get; set; } = new PatrolSettings(); [JsonProperty("Loot Settings")] public LootSettings LootSettings { get; set; } = new LootSettings(); [JsonProperty("Block During Raid")] public bool BlockDuringRaid { get; set; } = true; [JsonProperty("Block During Combat block")] public bool BlockDuringNoEscape { get; set; } = true; } private class SupplySignalSettings { [JsonProperty("Skin ID")] public ulong SkinId { get; set; } = 3332447426; [JsonProperty("Display Name")] public string DisplayName { get; set; } = "Patrol Heli Signal"; [JsonProperty("Warmup Time Before Patrol Arrival (seconds)")] public float Warmup { get; set; } = 5f; [JsonProperty("Default Cooldown Time (seconds)")] public float CooldownSeconds { get; set; } = 3600f; [JsonProperty("VIP Cooldown Time (seconds)")] public float VIPCooldownSeconds { get; set; } = 1800f; } private class PatrolSettings { [JsonProperty("Patrol Duration (seconds)")] public float Duration { get; set; } = 1800f; [JsonProperty("Helicopter Health")] public float Health { get; set; } = 10000f; [JsonProperty("Main Rotor Health")] public float MainRotorHealth { get; set; } = 900f; [JsonProperty("Tail Rotor Health")] public float TailRotorHealth { get; set; } = 500f; [JsonProperty("Number of Crates to Spawn")] public int CrateAmount { get; set; } = 6; [JsonProperty("Time Before Firing Rockets (seconds)")] public float TimeBeforeRocket { get; set; } = 0.25f; } private class LootSettings { [JsonProperty("Enable Loot Drops")] public bool Enabled { get; set; } = true; [JsonProperty("Loot Containers and Drop Chances")] public Dictionary Containers { get; set; } = new Dictionary { { "crate_normal", 5f }, { "crate_normal_2", 5f }, { "crate_elite", 10f }, { "heli_crate", 15f }, { "bradley_crate", 15f } }; } protected override void LoadDefaultConfig() { config = new ConfigurationManager(); SaveConfig(); Puts("Default configuration file created."); } private void LoadConfigValues() { base.LoadConfig(); config = Config.ReadObject(); VersionNumber serverVersion = new VersionNumber(config.version.major, config.version.minor, config.version.patch); if (serverVersion < Version) { ConciliateConfiguration(serverVersion); } } private void ConciliateConfiguration(VersionNumber serverVersion) { config.Signal.CooldownSeconds = 3600f; config.Signal.VIPCooldownSeconds = 1800f; config.BlockDuringRaid = true; config.BlockDuringNoEscape = true; PrintWarning("Merging new configuration keys into existing config..."); config.version.major = Version.Major; config.version.minor = Version.Minor; config.version.patch = Version.Patch; SaveConfig(); } private void SaveConfig() { Config.WriteObject(config, true); } #endregion } }