Иконка ресурса

FREE SimplePatrolSignal : 2.2.2 2.2.2

Нет прав для скачивания
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] ...

Автор
15FPS
Скачиваний
0
Просмотры
1
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок
Сверху