C#:
// -----------------------------------------------------------------------
// Copyright (c) Yac Vaguer. All rights reserved.
// Author: Yac Vaguer
// This is proprietary software. Unauthorized copying, distribution, or modification is strictly prohibited.
// Licensed for use only on servers owned by the purchaser.
// -----------------------------------------------------------------------
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace Oxide.Plugins {
[Info("SimplePatrolSignal", "Yac Vaguer", "2.2.2")]
[Description("Call a Patrol Helicopter to your location using a special supply signal.")]
public class SimplePatrolSignal : RustPlugin {
[PluginReference]
private Plugin LootDefender;
[PluginReference]
private Plugin AlphaLoot, CustomLoot, Loottable;
[PluginReference]
private Plugin PveMode, SimplePVE, SimplePVESphere;
[PluginReference]
private Plugin ZoneManager;
[PluginReference]
private Plugin NpcSpawn, BetterNpc;
private const string HELI_PREFAB = "assets/prefabs/npc/patrol helicopter/patrolhelicopter.prefab";
private const string PermissionGive = "simplepatrolsignal.give";
private const string PermissionVip = "simplepatrolsignal.vip";
private const string PermissionNoCooldown = "simplepatrolsignal.nocooldown";
private const string PermissionDespawn = "simplepatrolsignal.despawn";
private const string PermissionAdmin = "simplepatrolsignal.admin";
private const string CommandHeliSignal = "helisignal";
private const string CommandGiveHeliSignal = "givehelisignal";
private const string CooldownDataFile = "SimplePatrolSignal/cooldowns";
private const string ActivePatrolDataFile = "SimplePatrolSignal/active_patrols";
private const string LeaderboardDataFile = "SimplePatrolSignal/leaderboard";
private const string CommandPatrolTop = "patroltop";
private const string CommandPatrolStats = "patrolstats";
private const string CommandHeliHud = "helihud";
private const string HudPrefsDataFile = "SimplePatrolSignal/hud_prefs";
private ConfigurationManager _config;
private HashSet<NetworkableId> _processedContainers = new HashSet<NetworkableId>();
private Dictionary<ulong, long> _cooldowns = new Dictionary<ulong, long>();
private Dictionary<NetworkableId, PatrolInstance> _activePatrols = new Dictionary<NetworkableId, PatrolInstance>();
private HashSet<NetworkableId> _ownedCrates = new HashSet<NetworkableId>();
private Dictionary<ulong, LeaderboardEntry> _leaderboard = new Dictionary<ulong, LeaderboardEntry>();
private HashSet<string> _loggedBadPrefabs = new HashSet<string>();
private bool _debugMode;
private HashSet<string> _trackedCratePrefabs = new HashSet<string>();
private float _interestZoneRadiusSq;
private float _leashRadiusSq;
private HashSet<BasePlayer> _priorityScratch = new HashSet<BasePlayer>();
private HashSet<BasePlayer> _seenScratch = new HashSet<BasePlayer>();
private Dictionary<ulong, bool> _hudPrefs = new Dictionary<ulong, bool>();
private const string HudPanelName = "SimplePatrolSignal.HeliHud";
private Timer _hudTickTimer;
private HashSet<ulong> _hudPanelOpen = new HashSet<ulong>();
private List<CrashCleanupRecord> _recentCrashSites = new List<CrashCleanupRecord>();
private const float CrashCleanupRadius = 30f;
private const float CrashCleanupWindow = 10f;
private class CrashCleanupRecord {
public PatrolHelicopter Heli;
public Vector3 LastKnownPos;
public float ExpiresAtRealtime;
}
private class PatrolInstance {
public PatrolHelicopter Helicopter;
public ulong CallerId;
public Vector3 Zone;
public Timer DurationTimer;
public Timer ReconsiderTimer;
public Timer DebugMonitorTimer;
public float DebugLastMonitorHealth = -1f;
public Dictionary<ulong, float> AttackerDamage = new Dictionary<ulong, float>();
public bool CrashHandled;
public float LastObservedHealth = -1f;
public bool LoggedHealthRegen;
public PatrolTier Tier;
public System.DateTime StartedAt;
public System.DateTime ExpiresAt;
public bool HasArrived;
public bool Disposed;
public Timer LeashTimer;
public BaseEntity DebugMapMarker;
public string PveZoneName;
public string PvpZoneId;
public List<BaseEntity> Reinforcements = new List<BaseEntity>();
}
private class PersistedPatrol {
public ulong CallerId;
public ulong HeliNetId;
public float ZoneX;
public float ZoneY;
public float ZoneZ;
public long ExpiresAtTicks;
public ulong TierSkinId;
}
private class LeaderboardEntry {
public ulong UserId {
get;
set;
}
public string DisplayName {
get;
set;
}
public int HelisKilled {
get;
set;
}
public float TotalDamageDealt {
get;
set;
}
public int CratesLooted {
get;
set;
}
public long LastKillAtTicks {
get;
set;
}
}
#region Initialization
private void Init() {
permission.RegisterPermission(PermissionGive, this);
permission.RegisterPermission(PermissionVip, this);
permission.RegisterPermission(PermissionNoCooldown, this);
permission.RegisterPermission(PermissionDespawn, this);
permission.RegisterPermission(PermissionAdmin, this);
LoadDefaultMessages();
LoadConfigValues();
LoadHudPrefs();
}
private bool _terrainHookCalled = false;
private void OnTerrainInitialized() {
try {
if (_config?.Patrol?.BlockVanillaHeliEvent == true) {
ToggleVanillaHeliEvent(false);
}
}
finally {
_terrainHookCalled = true;
}
}
private void OnServerInitialized() {
LoadCooldowns();
LoadLeaderboard();
ReattachActivePatrols();
if (!_terrainHookCalled && _config?.Patrol?.BlockVanillaHeliEvent == true) {
ToggleVanillaHeliEvent(false);
}
if (_config.ShowReviewReminder) {
Puts("If you are enjoying this plugin remember to write a review https://codefling.com/plugins/simplepatrolsignal");
}
StartHudTickIfNeeded();
}
private void OnPlayerDisconnected(BasePlayer player, string reason) {
if (player == null) return;
_hudPanelOpen.Remove(player.userID);
}
private void OnNewSave(string filename) {
if (_config?.Patrol?.BlockVanillaHeliEvent == true) {
ToggleVanillaHeliEvent(false);
}
}
private void Unload() {
SaveCooldowns();
SaveActivePatrols();
SaveLeaderboard();
SaveHudPrefs();
var instances = _activePatrols.Values.ToList();
foreach (var instance in instances) {
DetachPatrol(instance);
}
_activePatrols.Clear();
StopHudTick();
ToggleVanillaHeliEvent(true);
}
private const uint VanillaHeliEventPrefabId = 3029415845;
private void ToggleVanillaHeliEvent(bool active) {
var prefabs = UnityEngine.Object.FindObjectsOfType<TriggeredEventPrefab>();
if (prefabs == null) return;
for (int i = 0; i < prefabs.Length; i++) {
var eve = prefabs[i];
if (eve?.targetPrefab?.resourceID == VanillaHeliEventPrefabId) {
eve.gameObject.SetActive(active);
Puts(active ? "Vanilla patrol heli event re-enabled." : "Vanilla patrol heli event suppressed (use the signal to call patrols).");
return;
}
}
}
private void DetachPatrol(PatrolInstance instance) {
if (instance == null) return;
if (instance.ReconsiderTimer != null && !instance.ReconsiderTimer.Destroyed) {
instance.ReconsiderTimer.Destroy();
}
if (instance.DurationTimer != null && !instance.DurationTimer.Destroyed) {
instance.DurationTimer.Destroy();
}
if (instance.DebugMonitorTimer != null && !instance.DebugMonitorTimer.Destroyed) {
instance.DebugMonitorTimer.Destroy();
}
if (instance.LeashTimer != null && !instance.LeashTimer.Destroyed) {
instance.LeashTimer.Destroy();
}
if (instance.DebugMapMarker != null && !instance.DebugMapMarker.IsDestroyed) {
instance.DebugMapMarker.Kill();
instance.DebugMapMarker = null;
}
}
private void ReattachActivePatrols() {
List<PersistedPatrol> snapshot = null;
try {
snapshot = Interface.Oxide.DataFileSystem.ReadObject<List<PersistedPatrol>>(ActivePatrolDataFile);
}
catch {
return;
}
if (snapshot == null || snapshot.Count == 0) return;
int recovered = 0;
int dropped = 0;
var nowTicks = System.DateTime.UtcNow.Ticks;
foreach (var p in snapshot) {
var heli = BaseNetworkable.serverEntities.Find(new NetworkableId(p.HeliNetId)) as PatrolHelicopter;
if (heli == null || heli.IsDestroyed || heli.myAI == null) {
dropped++;
continue;
}
if (p.ExpiresAtTicks <= nowTicks) {
if (heli.myAI != null) heli.myAI.Retire();
heli.Kill();
dropped++;
continue;
}
var tier = ResolveTier(p.TierSkinId);
var zone = new Vector3(p.ZoneX, p.ZoneY, p.ZoneZ);
var instance = new PatrolInstance {
Helicopter = heli, CallerId = p.CallerId, Zone = zone, Tier = tier, ExpiresAt = System.DateTime.FromBinary(p.ExpiresAtTicks),
}
;
float remainingSeconds = (float)(instance.ExpiresAt - System.DateTime.UtcNow).TotalSeconds;
instance.DurationTimer = timer.Once(remainingSeconds, () => DestroyPatrol(instance));
instance.ReconsiderTimer = timer.Repeat(_config.Patrol.ReconsiderInterval, 0, () => ReconsiderPosition(instance));
instance.LeashTimer = timer.Repeat(1f, 0, () => CheckLeash(instance));
_activePatrols[heli.net.ID] = instance;
StartDebugHealthMonitor(instance);
SpawnDebugMapMarker(instance);
recovered++;
}
if (recovered > 0 || dropped > 0) {
Puts($"Reattach: {recovered} active patrol(s) restored, {dropped} dropped (gone or expired).");
}
try {
Interface.Oxide.DataFileSystem.WriteObject(ActivePatrolDataFile, new List<PersistedPatrol>());
}
catch {
}
}
private void SaveActivePatrols() {
if (_activePatrols.Count == 0) {
Interface.Oxide.DataFileSystem.WriteObject(ActivePatrolDataFile, new List<PersistedPatrol>());
return;
}
var snapshot = new List<PersistedPatrol>();
foreach (var kv in _activePatrols) {
var inst = kv.Value;
if (inst.Helicopter == null || inst.Helicopter.IsDestroyed) continue;
ulong tierSkin = 0;
if (inst.Tier != null && _config.Patrol.Tiers != null) {
foreach (var tkv in _config.Patrol.Tiers) {
if (tkv.Value == inst.Tier) {
tierSkin = tkv.Key;
break;
}
}
}
snapshot.Add(new PersistedPatrol {
CallerId = inst.CallerId, HeliNetId = kv.Key.Value, ZoneX = inst.Zone.x, ZoneY = inst.Zone.y, ZoneZ = inst.Zone.z, ExpiresAtTicks = inst.ExpiresAt.Ticks, TierSkinId = tierSkin,
}
);
}
try {
Interface.Oxide.DataFileSystem.WriteObject(ActivePatrolDataFile, snapshot);
}
catch (System.Exception ex) {
PrintWarning($"Failed to persist active patrols: {ex.Message}");
}
}
private void LoadCooldowns() {
try {
var loaded = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<ulong, long>>(CooldownDataFile);
if (loaded == null) return;
long nowTicks = System.DateTime.UtcNow.Ticks;
foreach (var kv in loaded) {
if (kv.Value > nowTicks) {
_cooldowns[kv.Key] = kv.Value;
}
}
}
catch {
}
}
private void SaveCooldowns() {
if (_cooldowns.Count == 0) return;
try {
Interface.Oxide.DataFileSystem.WriteObject(CooldownDataFile, _cooldowns);
}
catch (System.Exception ex) {
PrintWarning($"Failed to persist cooldowns: {ex.Message}");
}
}
private void LoadLeaderboard() {
if (!_config.Leaderboard.Enabled) return;
try {
var loaded = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<ulong, LeaderboardEntry>>(LeaderboardDataFile);
if (loaded != null) {
_leaderboard = loaded;
}
}
catch {
}
}
private void SaveLeaderboard() {
if (!_config.Leaderboard.Enabled) return;
try {
Interface.Oxide.DataFileSystem.WriteObject(LeaderboardDataFile, _leaderboard);
}
catch (System.Exception ex) {
PrintWarning($"Failed to persist leaderboard: {ex.Message}");
}
}
private void LoadHudPrefs() {
try {
var loaded = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<ulong, bool>>(HudPrefsDataFile);
if (loaded != null) _hudPrefs = loaded;
}
catch {
}
}
private void SaveHudPrefs() {
try {
Interface.Oxide.DataFileSystem.WriteObject(HudPrefsDataFile, _hudPrefs);
}
catch (System.Exception ex) {
PrintWarning($"Failed to persist HUD prefs: {ex.Message}");
}
}
private LeaderboardEntry GetOrCreateEntry(BasePlayer player) {
if (player == null) return null;
if (!_leaderboard.TryGetValue(player.userID, out var entry)) {
entry = new LeaderboardEntry {
UserId = player.userID
}
;
_leaderboard[player.userID] = entry;
}
entry.DisplayName = player.displayName;
return entry;
}
#endregion
#region Localization
private static class Lang {
public const string NotAllowed = "NotAllowed";
public const string HeliSignalActive = "HeliSignalActive";
public const string PatrolLimitReached = "PatrolLimitReached";
public const string PatrolCalled = "PatrolCalled";
public const string DestroyingPatrol = "DestroyingPatrol";
public const string ReceivedHeliSignal = "ReceivedHeliSignal";
public const string CooldownActive = "CooldownActive";
public const string BlockedInSafeZone = "BlockedInSafeZone";
public const string GiveUsage = "GiveUsage";
public const string GiveTargetNotFound = "GiveTargetNotFound";
public const string GiveTargetMultiple = "GiveTargetMultiple";
public const string GiveSuccess = "GiveSuccess";
public const string GiveReceived = "GiveReceived";
public const string WarmupToast = "WarmupToast";
public const string IncomingNearby = "IncomingNearby";
public const string DespawnSuccess = "DespawnSuccess";
public const string DespawnNoActivePatrol = "DespawnNoActivePatrol";
public const string DespawnNotYours = "DespawnNotYours";
public const string DespawnTargetNotFound = "DespawnTargetNotFound";
public const string DespawnNoPermission = "DespawnNoPermission";
public const string UnknownTier = "UnknownTier";
public const string TierListing = "TierListing";
public const string SpecBroadcast = "SpecBroadcast";
public const string HudMasterDisabled = "HudMasterDisabled";
public const string HudToggledOn = "HudToggledOn";
public const string HudToggledOff = "HudToggledOff";
}
private void LoadDefaultMessages() {
lang.RegisterMessages(new Dictionary<string, string> {
[Lang.NotAllowed]
= "You are not allowed to use this command.", [Lang.HeliSignalActive] = "You already have an active patrol helicopter.", [Lang.PatrolLimitReached] = "The server has reached the maximum number of active patrol helicopters. Try again later.", [Lang.PatrolCalled] = "A patrol helicopter is on its way to your location!", [Lang.DestroyingPatrol] = "The patrol helicopter is leaving the area.", [Lang.ReceivedHeliSignal] = "You have received a Patrol Heli Signal.", [Lang.CooldownActive] = "You must wait {0} seconds before calling another patrol.", [Lang.BlockedInSafeZone] = "You cannot call a patrol helicopter inside a safe zone.", [Lang.GiveUsage] = "Usage: givehelisignal <player> [amount] [tier]", [Lang.UnknownTier] = "Unknown tier '{0}'. Available: {1}", [Lang.TierListing] = "base, {0}", [Lang.GiveTargetNotFound] = "No player matched '{0}'.", [Lang.GiveTargetMultiple] = "Multiple players matched '{0}' - be more specific.", [Lang.GiveSuccess] = "Gave {0} patrol heli signal(s) to {1}.", [Lang.GiveReceived] = "{0} gave you {1} patrol heli signal(s).", [Lang.WarmupToast] = "Patrol helicopter inbound in {0} seconds...", [Lang.IncomingNearby] = "You hear a patrol helicopter inbound nearby. Take cover or join the fight.", [Lang.DespawnSuccess] = "Your patrol helicopter has been despawned.", [Lang.DespawnNoActivePatrol] = "You have no active patrol helicopter to despawn.", [Lang.DespawnNotYours] = "Only admins can despawn another player's patrol helicopter.", [Lang.DespawnTargetNotFound] = "No active patrol helicopter found for that player.", [Lang.DespawnNoPermission] = "You don't have permission to despawn your patrol helicopter.", [Lang.SpecBroadcast] = "Incoming {0}: {1} HP | rotor {2} / tail {3} | {4} crates | bullet dmg {5} | gun range {6}m | duration {7}s", [Lang.HudMasterDisabled] = "The heli health HUD is disabled on this server.", [Lang.HudToggledOn] = "Heli health HUD is now ON for you.", [Lang.HudToggledOff] = "Heli health HUD is now OFF for you.",
}
, this);
}
private string GetMessage(string key, string userId = null) => lang.GetMessage(key, this, userId);
#endregion
#region Commands
[ChatCommand(CommandHeliSignal)]
private void CmdHeliSignalChat(BasePlayer player, string command, string[] args) {
if (args != null && args.Length >= 1 && args[0].Equals("despawn", System.StringComparison.OrdinalIgnoreCase)) {
DespawnPatrolForCaller(player, args.Length >= 2 ? args[1] : null);
return;
}
string tierArg = args != null && args.Length >= 1 ? args[0] : null;
GiveHeliSignal(player, tierArg);
}
[ConsoleCommand(CommandHeliSignal)]
private void CmdHeliSignalConsole(ConsoleSystem.Arg arg) {
var args = arg.Args?.Select(a => a.ToString()).ToArray() ?? System.Array.Empty<string>();
if (args.Length >= 1 && args[0].Equals("despawn", System.StringComparison.OrdinalIgnoreCase)) {
var caller = arg.Player();
string targetArg = args.Length >= 2 ? args[1] : null;
DespawnPatrolForCaller(caller, targetArg);
return;
}
var player = arg.Player();
if (player != null) {
string tierArg = args.Length >= 1 ? args[0] : null;
GiveHeliSignal(player, tierArg);
}
}
private void DespawnPatrolForCaller(BasePlayer caller, string targetArg) {
ulong targetId = 0;
bool isServerConsole = caller == null;
if (!string.IsNullOrEmpty(targetArg)) {
if (!isServerConsole && !permission.UserHasPermission(caller.UserIDString, PermissionAdmin)) {
caller.ChatMessage(GetMessage(Lang.DespawnNotYours, caller.UserIDString));
return;
}
if (!ulong.TryParse(targetArg, out targetId)) {
var match = BasePlayer.Find(targetArg) ?? BasePlayer.FindSleeping(targetArg);
if (match != null) targetId = match.userID;
}
if (targetId == 0) {
if (caller != null) caller.ChatMessage(GetMessage(Lang.DespawnTargetNotFound, caller.UserIDString));
else Puts(GetMessage(Lang.DespawnTargetNotFound));
return;
}
}
else {
if (caller == null) {
Puts("Usage: helisignal despawn <steamid>");
return;
}
if (!permission.UserHasPermission(caller.UserIDString, PermissionDespawn)) {
caller.ChatMessage(GetMessage(Lang.DespawnNoPermission, caller.UserIDString));
return;
}
targetId = caller.userID;
}
PatrolInstance match2 = null;
foreach (var kv in _activePatrols) {
if (kv.Value.CallerId == targetId) {
match2 = kv.Value;
break;
}
}
if (match2 == null) {
if (caller != null) caller.ChatMessage(GetMessage(Lang.DespawnNoActivePatrol, caller.UserIDString));
else Puts(GetMessage(Lang.DespawnNoActivePatrol));
return;
}
DestroyPatrol(match2);
if (caller != null) caller.ChatMessage(GetMessage(Lang.DespawnSuccess, caller.UserIDString));
else Puts(GetMessage(Lang.DespawnSuccess));
}
[ChatCommand(CommandHeliHud)]
private void CmdHeliHudChat(BasePlayer player, string command, string[] args) {
ToggleHeliHudForPlayer(player);
}
[ConsoleCommand(CommandHeliHud)]
private void CmdHeliHudConsole(ConsoleSystem.Arg arg) {
var player = arg?.Player();
if (player == null) return;
ToggleHeliHudForPlayer(player);
}
private void ToggleHeliHudForPlayer(BasePlayer player) {
if (player == null) return;
if (_config?.HealthHud == null || !_config.HealthHud.Enabled) {
player.ChatMessage(GetMessage(Lang.HudMasterDisabled, player.UserIDString));
return;
}
bool defaultVisible = _config.HealthHud.DefaultVisible;
bool current = _hudPrefs.TryGetValue(player.userID, out var v) ? v : defaultVisible;
bool next = !current;
_hudPrefs[player.userID] = next;
SaveHudPrefs();
player.ChatMessage(GetMessage(next ? Lang.HudToggledOn : Lang.HudToggledOff, player.UserIDString));
}
[ChatCommand(CommandPatrolTop)]
private void CmdPatrolTopChat(BasePlayer player, string command, string[] args) {
if (player == null) return;
if (!_config.Leaderboard.Enabled) return;
string mode = (args != null && args.Length > 0) ? args[0].ToLowerInvariant() : "kills";
var sorted = SortLeaderboard(mode);
int n = Mathf.Min(_config.Leaderboard.TopN, sorted.Count);
var lines = new List<string>(n + 2);
lines.Add($"<color=#ffd700>Patrol Heli Leaderboard ({mode})</color>");
for (int i = 0; i < n; i++) {
var e = sorted[i];
string display = string.IsNullOrEmpty(e.DisplayName) ? e.UserId.ToString() : e.DisplayName;
lines.Add($"{i + 1}. {display} - kills: {e.HelisKilled}, dmg: {e.TotalDamageDealt:F0}, crates: {e.CratesLooted}");
}
if (n == 0) lines.Add("(no entries yet)");
player.ChatMessage(string.Join("\n", lines));
}
[ChatCommand(CommandPatrolStats)]
private void CmdPatrolStatsChat(BasePlayer player, string command, string[] args) {
if (player == null) return;
if (!_config.Leaderboard.Enabled) return;
ulong target = player.userID;
if (args != null && args.Length > 0) {
var found = FindPlayerByNameOrId(args[0], out _);
if (found != null) target = found.userID;
}
if (!_leaderboard.TryGetValue(target, out var e)) {
player.ChatMessage("No patrol heli stats yet for that player.");
return;
}
string display = string.IsNullOrEmpty(e.DisplayName) ? e.UserId.ToString() : e.DisplayName;
player.ChatMessage($"<color=#ffd700>{display}</color>\n" + $" Helis killed: {e.HelisKilled}\n" + $" Total damage: {e.TotalDamageDealt:F0}\n" + $" Crates looted: {e.CratesLooted}");
}
[ConsoleCommand("patrol.leaderboard.reset")]
private void CmdLeaderboardReset(ConsoleSystem.Arg arg) {
var caller = arg.Player();
if (caller != null && !permission.UserHasPermission(caller.UserIDString, PermissionAdmin)) {
caller.ChatMessage(GetMessage(Lang.NotAllowed, caller.UserIDString));
return;
}
int previous = _leaderboard.Count;
_leaderboard.Clear();
SaveLeaderboard();
string msg = $"SimplePatrolSignal leaderboard reset ({previous} entries cleared).";
if (caller != null) caller.ChatMessage(msg);
Puts(msg);
}
private List<LeaderboardEntry> SortLeaderboard(string mode) {
var query = _leaderboard.Values.Where(e => e != null);
switch (mode) {
case "damage" : query = query.OrderByDescending(e => e.TotalDamageDealt);
break;
case "crates" : query = query.OrderByDescending(e => e.CratesLooted);
break;
default : query = query.OrderByDescending(e => e.HelisKilled);
break;
}
return query.ToList();
}
[ChatCommand(CommandGiveHeliSignal)]
private void CmdGiveHeliSignalChat(BasePlayer player, string command, string[] args) {
if (!permission.UserHasPermission(player.UserIDString, PermissionGive)) {
player.ChatMessage(GetMessage(Lang.NotAllowed, player.UserIDString));
return;
}
GiveHeliSignalToTarget(player, args);
}
[ConsoleCommand(CommandGiveHeliSignal)]
private void CmdGiveHeliSignalConsole(ConsoleSystem.Arg arg) {
var caller = arg.Player();
if (caller != null && !permission.UserHasPermission(caller.UserIDString, PermissionGive)) {
caller.ChatMessage(GetMessage(Lang.NotAllowed, caller.UserIDString));
return;
}
GiveHeliSignalToTarget(caller, arg.Args?.Select(a => a.ToString()).ToArray() ?? System.Array.Empty<string>());
}
private void GiveHeliSignalToTarget(BasePlayer caller, string[] args) {
string callerId = caller?.UserIDString;
if (args == null || args.Length < 1) {
SendReplyToCaller(caller, GetMessage(Lang.GiveUsage, callerId));
return;
}
int amount = 1;
string tierArg = null;
if (args.Length >= 2) {
if (int.TryParse(args[1], out int parsed) && parsed > 0) {
amount = parsed;
if (args.Length >= 3) tierArg = args[2];
}
else {
tierArg = args[1];
}
}
ulong? forcedSkin = null;
if (tierArg != null) {
if (!TryResolveTierSkin(tierArg, out var resolved)) {
SendReplyToCaller(caller, string.Format(GetMessage(Lang.UnknownTier, callerId), tierArg, ListTierCodes()));
return;
}
forcedSkin = resolved;
}
BasePlayer target = FindPlayerByNameOrId(args[0], out string matchError);
if (target == null) {
SendReplyToCaller(caller, string.Format(GetMessage(matchError, callerId), args[0]));
return;
}
for (int i = 0; i < amount; i++) {
var item = CreateHeliSignalItem(forcedSkin);
if (item != null) {
target.GiveItem(item);
}
}
SendReplyToCaller(caller, string.Format(GetMessage(Lang.GiveSuccess, callerId), amount, target.displayName));
target.ChatMessage(string.Format(GetMessage(Lang.GiveReceived, target.UserIDString), caller?.displayName ?? "Server", amount));
}
private BasePlayer FindPlayerByNameOrId(string query, out string errorKey) {
errorKey = null;
if (ulong.TryParse(query, out ulong steamId)) {
var byId = BasePlayer.FindByID(steamId) ?? BasePlayer.FindSleeping(steamId);
if (byId != null) return byId;
}
var matches = BasePlayer.activePlayerList .Where(p => p.displayName != null && p.displayName.IndexOf(query, System.StringComparison.OrdinalIgnoreCase) >= 0) .ToList();
if (matches.Count == 0) {
errorKey = Lang.GiveTargetNotFound;
return null;
}
if (matches.Count > 1) {
errorKey = Lang.GiveTargetMultiple;
return null;
}
return matches[0];
}
private void SendReplyToCaller(BasePlayer caller, string message) {
if (caller != null) {
caller.ChatMessage(message);
}
else {
Puts(message);
}
}
private void GiveHeliSignal(BasePlayer player, string tierArg = null) {
if (!HasPermission(player)) {
player.ChatMessage(GetMessage(Lang.NotAllowed, player.UserIDString));
return;
}
ulong? forcedSkin = null;
if (!string.IsNullOrWhiteSpace(tierArg)) {
if (!TryResolveTierSkin(tierArg, out var resolved)) {
player.ChatMessage(string.Format(GetMessage(Lang.UnknownTier, player.UserIDString), tierArg, ListTierCodes()));
return;
}
forcedSkin = resolved;
}
var supplySignal = CreateHeliSignalItem(forcedSkin);
if (supplySignal != null) {
player.GiveItem(supplySignal);
player.ChatMessage(GetMessage(Lang.ReceivedHeliSignal, player.UserIDString));
}
}
private bool HasPermission(BasePlayer player) {
return permission.UserHasPermission(player.UserIDString, PermissionGive);
}
#endregion
#region Event Hooks
private void OnExplosiveThrown(BasePlayer player, BaseEntity entity) {
HandleHeliSignalDeploy(player, entity);
}
private void OnExplosiveDropped(BasePlayer player, BaseEntity entity, ThrownWeapon item) {
HandleHeliSignalDeploy(player, entity);
}
private void HandleHeliSignalDeploy(BasePlayer player, BaseEntity entity) {
if (player == null) return;
if (!(entity is SupplySignal signal) || !IsHeliSignalSkin(signal.skinID)) return;
ulong thrownSkin = signal.skinID;
signal.CancelInvoke(signal.Explode);
signal.CancelInvoke(signal.KillMessage);
SpawnDropMarker(signal.transform.position);
BroadcastSpecToCaller(player, thrownSkin);
ShowWarmupToast(player, _config.Signal.Warmup);
StartApproachEffect(player);
timer.Once(_config.Signal.Warmup, () => {
if (signal != null && !signal.IsDestroyed) {
signal.Kill();
}
CallPatrolHelicopter(player, thrownSkin);
}
);
}
private void SpawnDropMarker(Vector3 position) {
var settings = _config.Signal.DropMarker;
if (settings == null || !settings.Enabled) return;
SpawnEntitySafe(settings.SmokePrefab, position);
}
private void StartApproachEffect(BasePlayer caller) {
if (caller == null) return;
var settings = _config.Signal.ApproachEffect;
if (settings == null || !settings.Enabled) return;
Vector3 origin = caller.transform.position;
void Play() => RunEffectSafe(settings.EffectPrefab, origin);
Play();
if (settings.RepeatInterval > 0f) {
int totalTicks = Mathf.FloorToInt(_config.Signal.Warmup / settings.RepeatInterval);
if (totalTicks > 1) {
timer.Repeat(settings.RepeatInterval, totalTicks - 1, Play);
}
}
if (settings.NotifyNearbyPlayers && settings.HearingRadius > 0f) {
NotifyPlayersInRadius(origin, settings.HearingRadius, caller);
}
}
private bool RunEffectSafe(string prefab, Vector3 origin) {
if (string.IsNullOrEmpty(prefab)) return false;
if (StringPool.Get(prefab) == 0) {
if (_loggedBadPrefabs.Add(prefab)) {
PrintWarning($"Effect prefab \"{prefab}\" is not in StringPool, skipping. Pick a valid prefab path or set it to \"\" to disable.");
}
return false;
}
Effect.server.Run(prefab, origin);
return true;
}
private BaseEntity SpawnEntitySafe(string prefab, Vector3 position) {
if (string.IsNullOrEmpty(prefab)) return null;
var entity = GameManager.server.CreateEntity(prefab, position);
if (entity == null) {
if (_loggedBadPrefabs.Add(prefab)) {
PrintWarning($"Entity prefab \"{prefab}\" could not be created, skipping. Pick a valid prefab path or set it to \"\" to disable.");
}
return null;
}
entity.Spawn();
return entity;
}
private void NotifyPlayersInRadius(Vector3 origin, float radius, BasePlayer caller) {
float sq = radius * radius;
foreach (var player in BasePlayer.activePlayerList) {
if (player == null || player == caller) continue;
if ((player.transform.position - origin).sqrMagnitude > sq) continue;
player.ChatMessage(GetMessage(Lang.IncomingNearby, player.UserIDString));
}
}
private void BroadcastSpecToCaller(BasePlayer caller, ulong thrownSkin) {
if (caller == null || _config?.Signal == null || !_config.Signal.SpecBroadcastOnThrow) return;
PatrolTier tier = null;
_config.Patrol?.Tiers?.TryGetValue(thrownSkin, out tier);
string label = ResolveSignalDisplayName(thrownSkin);
float health = tier?.Health ?? _config.Patrol.Health;
float rotor = tier?.MainRotorHealth ?? _config.Patrol.MainRotorHealth;
float tail = tier?.TailRotorHealth ?? _config.Patrol.TailRotorHealth;
int crates = tier?.CrateAmount ?? _config.Patrol.CrateAmount;
float bullet = tier?.BulletDamage ?? _config.Patrol.BulletDamage;
float gunRange = tier?.GunMaxTargetRange?? _config.Patrol.GunMaxTargetRange;
float duration = tier?.Duration ?? _config.Patrol.Duration;
string msg = string.Format(GetMessage(Lang.SpecBroadcast, caller.UserIDString), label, Mathf.RoundToInt(health), Mathf.RoundToInt(rotor), Mathf.RoundToInt(tail), crates, bullet, Mathf.RoundToInt(gunRange), Mathf.RoundToInt(duration));
caller.ChatMessage(msg);
}
private void ShowWarmupToast(BasePlayer player, float warmupSeconds) {
if (player == null || !_config.Signal.ShowWarmupToast) return;
int seconds = Mathf.Max(1, Mathf.CeilToInt(warmupSeconds));
string format = GetMessage(Lang.WarmupToast, player.UserIDString);
player.SendConsoleCommand("gametip.showgametip", string.Format(format, seconds));
timer.Once(warmupSeconds + 0.1f, () => {
if (player != null && player.IsConnected) {
player.SendConsoleCommand("gametip.hidegametip");
}
}
);
}
private object CanLootEntity(BasePlayer player, LootContainer container) {
if (container == null || !_config.LootSettings.Enabled) return null;
var containerId = container.net?.ID ?? default;
if (!_processedContainers.Add(containerId)) return null;
string containerName = container.ShortPrefabName;
if (_config.LootSettings.Containers.TryGetValue(containerName, out var rule) && rule != null) {
if (RollChance(rule.DropChance)) {
int amount = UnityEngine.Random.Range(rule.MinAmount, rule.MaxAmount + 1);
for (int i = 0; i < amount; i++) {
var item = CreateHeliSignalItem();
if (item == null) continue;
if (!item.MoveToContainer(container.inventory)) {
container.inventory.capacity++;
container.inventorySlots++;
if (!item.MoveToContainer(container.inventory)) {
item.Remove();
}
}
}
}
}
return null;
}
private void OnEntityKill(LootContainer container) {
if (container == null || container.net == null) return;
_processedContainers.Remove(container.net.ID);
_ownedCrates.Remove(container.net.ID);
}
private void OnEntityDeath(PatrolHelicopter heli, HitInfo info) {
if (heli == null || heli.net == null) return;
if (!_activePatrols.TryGetValue(heli.net.ID, out var instance)) return;
TrackCrashCleanup(heli);
DestroyPatrol(instance);
}
private void TrackCrashCleanup(PatrolHelicopter heli) {
var settings = _config?.Patrol?.CrashSite;
if (settings == null) return;
if (!settings.DespawnWreckGibs && !settings.DisableCrashFire) return;
_recentCrashSites.Add(new CrashCleanupRecord {
Heli = heli, LastKnownPos = heli.transform.position, ExpiresAtRealtime = Time.realtimeSinceStartup + CrashCleanupWindow,
}
);
}
private bool IsNearTrackedCrash(Vector3 pos) {
if (_recentCrashSites.Count == 0) return false;
float now = Time.realtimeSinceStartup;
float radiusSq = CrashCleanupRadius * CrashCleanupRadius;
bool hit = false;
for (int i = _recentCrashSites.Count - 1; i >= 0; i--) {
var rec = _recentCrashSites[i];
if (rec.ExpiresAtRealtime <= now) {
_recentCrashSites.RemoveAt(i);
continue;
}
Vector3 refPos = rec.LastKnownPos;
if (rec.Heli != null && !rec.Heli.IsDestroyed) {
refPos = rec.Heli.transform.position;
rec.LastKnownPos = refPos;
}
if (!hit && (refPos - pos).sqrMagnitude <= radiusSq) hit = true;
}
return hit;
}
private void OnEntitySpawned(ServerGib gib) {
if (gib == null || gib.IsDestroyed) return;
var settings = _config?.Patrol?.CrashSite;
if (settings == null || !settings.DespawnWreckGibs) return;
string shortName = gib.ShortPrefabName;
if (string.IsNullOrEmpty(shortName) || shortName.IndexOf("patrolhelicopter", System.StringComparison.OrdinalIgnoreCase) < 0) return;
if (!IsNearTrackedCrash(gib.transform.position)) return;
if (_debugMode) LogDebug("[crash-cleanup] killing wreck gib {0} at {1}", shortName, gib.transform.position);
gib.Kill();
}
private void OnEntitySpawned(FireBall fire) {
if (fire == null || fire.IsDestroyed) return;
var settings = _config?.Patrol?.CrashSite;
if (settings == null || !settings.DisableCrashFire) return;
if (!IsNearTrackedCrash(fire.transform.position)) return;
if (_debugMode) LogDebug("[crash-cleanup] killing crash fire {0} at {1}", fire.ShortPrefabName, fire.transform.position);
fire.Kill();
}
private void OnEntityTakeDamage(PatrolHelicopter heli, HitInfo info) {
if (heli == null || heli.net == null || info == null) return;
if (!_activePatrols.TryGetValue(heli.net.ID, out var instance)) {
if (_debugMode) LogDebug("[damage] {0} take-damage but not in _activePatrols (skipping attribution)", DescribeHeli(heli));
return;
}
if (heli.myAI != null && heli.myAI._currentState == PatrolHelicopterAI.aiState.DEATH) return;
float damage = info.damageTypes != null ? info.damageTypes.Total() : 0f;
if (_debugMode) {
float beforeHealth = heli.health;
string attackerName = info.InitiatorPlayer?.displayName ?? info.Initiator?.ShortPrefabName ?? "<unknown>";
string boneName = info.boneName ?? "<no-bone>";
LogDebug("[damage] {0} hit by {1} bone={2} dmg={3:F1} health={4:F0} -> expected {5:F0}", DescribeHeli(heli), attackerName, boneName, damage, beforeHealth, beforeHealth - damage);
}
var attacker = info.InitiatorPlayer;
if (attacker == null || attacker.userID == 0 || attacker.IsNpc) {
if (_debugMode) LogDebug("[damage] {0} attacker is null/NPC - no attribution", DescribeHeli(heli));
return;
}
if (damage <= 0f) {
if (_debugMode) LogDebug("[damage] {0} damage <= 0 - skipping attribution", DescribeHeli(heli));
return;
}
if (!instance.AttackerDamage.TryGetValue(attacker.userID, out float total)) total = 0f;
instance.AttackerDamage[attacker.userID] = total + damage;
if (_config.Leaderboard.Enabled) {
var entry = GetOrCreateEntry(attacker);
if (entry != null) {
entry.TotalDamageDealt += damage;
}
}
if (!instance.CrashHandled && damage >= heli.health) {
if (_debugMode) LogDebug("[damage] {0} LETHAL hit (dmg {1:F1} >= health {2:F0}), running ApplyCrashSite", DescribeHeli(heli), damage, heli.health);
ApplyCrashSite(instance, heli, info);
}
}
private void ApplyCrashSite(PatrolInstance instance, PatrolHelicopter heli, HitInfo info) {
var settings = _config.Patrol.CrashSite;
if (settings == null) {
if (_debugMode) LogDebug("[crash] {0} CrashSite settings null - bailing", DescribeHeli(heli));
return;
}
string mode = (settings.Mode ?? "Vanilla").Trim();
if (_debugMode) LogDebug("[crash] {0} ApplyCrashSite mode={1}", DescribeHeli(heli), mode);
if (mode.Equals("Vanilla", System.StringComparison.OrdinalIgnoreCase)) return;
instance.CrashHandled = true;
Vector3 currentPos = heli.transform.position;
if (settings.SkipInSafeZone && IsPositionInSafeZone(currentPos)) {
if (_debugMode) LogDebug("[crash] {0} skipping - inside safe zone", DescribeHeli(heli));
return;
}
Vector3? target = null;
if (mode.Equals("ShootdownLocation", System.StringComparison.OrdinalIgnoreCase)) {
if (_debugMode) LogDebug("[crash] {0} ShootdownLocation - no teleport, vanilla spiral from current pos", DescribeHeli(heli));
return;
}
else if (mode.Equals("NearestMonument", System.StringComparison.OrdinalIgnoreCase)) {
target = FindNearestMonumentCrashPoint(currentPos, settings);
}
else {
Puts($"CrashSite: unknown Mode '{settings.Mode}', falling back to Vanilla.");
return;
}
if (target == null) {
if (_debugMode) LogDebug("[crash] {0} no target picked - bailing", DescribeHeli(heli));
return;
}
if (_debugMode) LogDebug("[crash] {0} teleport from {1} to {2}", DescribeHeli(heli), currentPos, target.Value);
heli.transform.position = target.Value;
var
// ... [Code preview truncated due to length. Full source in attached file] ...