C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
using Facepunch;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Libraries;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using Rust;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using System.Text.RegularExpressions;
using System.Reflection;
using Time = UnityEngine.Time;
using Random = UnityEngine.Random;
/* Changelog
1.2.23
- Added: Check during plugin load for duplicate skin ID & profile permission/shortname
- Fixed: "Failed to run a 11.00 timer" error after heli dies in certain cases
- Fixed: KeyNotFoundException error during "ProcessRewards"
1.2.22
- Fixed: Hook conflict with PreventLooting.1.2.21
- Fixed: "ArgumentNullException: Value cannot be null.Parameter name: key" error.- Fixed: Wave heli signals still sending further helis after retire command used
1.2.20
- Fixed: Entity is NULL but is still in saveList spam
- Fixed: Failed to execute OnFrame callback error when throwing Heli Signals
- Fixed: Players getting rewards if heli retires
- Fixed: NullReferenceException error when signal thrown while using custom map with custom monuments
- Added: Support for Better NPC to enable/disable crate guards
1.2.19
- Fixed: Buy command not working using ServerRewards or Economics
- Fixed: Heli going into never ending Strafe loop
- Added: Config option to specify minimum time bweteen strafe attacks
- Added: Config option to specify Terrain Pushback force (increase if helis glitch under map)
- Added: Config option to specify Obstacle Pushback force (increase it helis glitch through structures)
1.2.18
- Fixed: Custom item name being incorrectly assigned causing stacking conflicts
- Fixed: Active helicopter number reported for some players incorrectly
- Fixed: Rewards not being processed if heli is destroyed mid air without death spiral
- Added: Config option to penalize players rewards who do majority damage with controlled auto turrets
- Added: Config option for Heli to target player controlled auto turrets with optional cooldown option
- Added: Player controlled turret damage added to damage report
- Updated: Code efficiency improvements
1.2.17
- Fixed: OnHelicopterStrafeEnter error console spam
- Fixed: Helis exploding mid air when both rotors are destroyed under certain conditions
- Added: Option to disable monument crash for heli (NOTE: Sets a server convar).1.2.16
- Fixed: For May Rust update
1.2.15
- Fixed: Heli spawning under terrain under certain conditions (causing 'Infinity or NaN' error spam)
1.2.14
- Fixed: OrbitStrafe chance setting not working correctly
- Fixed: OrbitStrafe rockets not hitting the correct position
- Fixed: MapScaleDistance config option now spawns helis the distance from the player, not map center
- Fixed: Rewards not being issued in some cases unless the heli was hit while crashing
- Fixed: Retire command not working in some cases
- Changed: Retire command is now less spammy in chat (better if retiring multiple helis)
1.2.13
- Fixed: OrbitStrafe rockets fired too high above target intermitently
1.2.12
- Fixed: Heli never entering strafe mode if constantly attacked
- Fixed: OnEntityTakeDamage NRE message
1.2.11
- Fixed: Heli not firing rockets/strafing when Allow Flee option was set to false
- Added: Wave signals can now call multi heli profiles (new wave not called until all previous helis destroyed)
1.2.10
- Fixed: Heli fleeing when AllowFlee was set to false.New config options (NOTE: Sets global server ConVar)
- Added: Config option to change the damage percent received before helicopter flees a heavy attack
- Added: Config option to specify how many rockets fired during orbit strafe
- Added: Minimum & maximum variance config options for number of rockets fired during orbit strafe
1.2.9
- Fixed: For Rust force wipe changes
- Fixed: Certain chat messages only showing to the calling player instead of the team/chat
- Added: Config option to specify chance of Heli rocket strafe being an Orbit Strafe type
- Added: Config option to prevent Heli Fleeing when being attacked
1.2.8
- Fixed: Plugin failing to load on server start/restart
1.2.7
- Fixed: Wave Helis are now working as they should
- Fixed: Allow in monuments config option now works properly
- Fixed: Occasional NRE OnEntityKill & OnEntityTakeDamage
1.2.6
- Fixed: Error where the calling player cannot damage the heli unless in a clan
1.2.5
- Fixed: Memory leak leading to poor performance/stuttering in certain cases (Thanks Death for the help)
- Updated: Code cleanup and edits for better performance
- Added: Option to award XP via SkillTree instead of XPerience
- Added: Option to award XP including players XP boosts
1.2.4
- Fixed: Hook conflict with NpcSpawn
- Fixed: OnEntityTakeDamage NullReferenceException error
- Fixed: Occasional bug where killed heli regains HP and is then invincible
1.2.3
- Fixed: Compatibility with Carbon (Helis now fire rockets on Carbon servers)
- Fixed: Heli not firing rockets in some circumstances
- Fixed: NRE console error message
- Fixed: Heli flying away and not returning when it should
- Fixed: Heli signals not enabled for buying showing up in the buy list
1.2.2
- Fixed: Wave signals not being purchasable via buy command
- Added: Separate skin IDs in the config for default wave signals (prevents item name stacking bug)
1.2.1
- Added: Config option to always block damage to a list of specified entities
- Added: Config option to give XP as a reward (requires XPerience plugin)
- Added: Config option to specify number of helis spawned with a signal - default 1 (new skins available on plugin page)
- Added: Config option to reward with a custom item (shortname, skin, name)
- Added: Config option to also give damage report if helicopter retires
- Added: Config option for new Heli Wave Signal (Calls helis in waves, one after the other)
- Added: Config option to display damage report for vanilla patrol helicopter
- Added: Config option for display and owner for vanilla patrol helicopter for damage report
*/
namespace Oxide.Plugins {
[Info("Heli Signals", "ZEODE", "1.2.23")]
[Description("Call Patrol Helicopters to your location with custom supply signals.")]
public class HeliSignals : RustPlugin {
#region Plugin References
[PluginReference]
Plugin Friends, Clans, FancyDrop, RewardPlugin, ServerRewards, Economics, Vanish, NoEscape, BotReSpawn, BetterNPC, DynamicPVP, XPerience, SkillTree, NpcSpawn;
#endregion Plugin References
#region Constants
private static HeliSignals Instance;
private static System.Random random = new System.Random();
private const string permAdmin = "helisignals.admin";
private const string permBuy = "helisignals.buy";
private const string permBypasscooldown = "helisignals.bypasscooldown";
private const string heliPrefab = "assets/prefabs/npc/patrol helicopter/patrolhelicopter.prefab";
private const string hackableCrate = "assets/prefabs/deployable/chinooklockedcrate/codelockedhackablecrate.prefab";
private const string shockSound = "assets/prefabs/locks/keypad/effects/lock.code.shock.prefab";
private const string deniedSound = "assets/prefabs/locks/keypad/effects/lock.code.denied.prefab";
private const string defaultWebhookUrl = "https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks";
private const int supplySignalId = 1397052267;
private const int scrapId = -932201673;
// Default config item skins
// Single
private const ulong easySkinID = 2920175997;
private const ulong medSkinID = 2920176079;
private const ulong hardSkinID = 2920176050;
private const ulong eliteSkinID = 2920176024;
private const ulong expertSkinID = 3099117081;
private const ulong nightmareSkinID = 3099117372;
// Multi
private const ulong easyMultiSkinID = 3083234542;
private const ulong medMultiSkinID = 3083234833;
private const ulong hardMultiSkinID = 3083234755;
private const ulong eliteMultiSkinID = 3083234647;
private const ulong elxpertMultiSkinID = 3099124338;
private const ulong nightmareMultiSkinID = 3099124426;
// Wave
private const ulong normalWaveSkinID = 3104667036;
private const ulong hardWaveSkinID = 3104666951;
// Default config item names
// Single
private const string easyHeli = "Heli Signal (Easy)";
private const string medHeli = "Heli Signal (Medium)";
private const string hardHeli = "Heli Signal (Hard)";
private const string eliteHeli = "Heli Signal (Elite)";
// Multi
private const string easyMulti = "Multi Heli (Easy)";
private const string medMulti = "Multi Heli (Medium)";
private const string hardMulti = "Multi Heli (Hard)";
private const string eliteMulti = "Multi Heli (Elite)";
// Wave
private const string normalWave = "Heli Wave Signal (Normal)";
private const string hardWave = "Heli Wave Signal (Hard)";
#endregion Constants
#region Language
protected override void LoadDefaultMessages() {
lang.RegisterMessages(new Dictionary<string, string> {
["SyntaxPlayer"]
= "<color=red>Invalid syntax</color>, use:\n\n<color=green>/hsgive <type> <SteamID/PlayerName> <amount></color>", ["SyntaxConsole"] = "Invalid syntax, use: hsgive <type> <SteamID/PlayerName> <amount>", ["ClearSyntaxPlayer"] = "<color=red>Invalid syntax</color>, use:\n\n<color=green>/hsclearcd</color> (clears all cooldowns)\n\nOr\n\n<color=green>/hsclearcd <SteamID/PlayerName></color>", ["ClearSyntaxConsole"] = "Invalid syntax, use: \"hsclearcd\" (clears all cooldowns) or \"hsclearcd <SteamID/PlayerName>\"", ["Receive"] = "You received <color=orange>{0}</color> x <color=orange>{1}</color>!", ["PlayerReceive"] = "Player {0} ({1}) received {2} x {3}!", ["Permission"] = "You do not have permission to use <color=orange>{0}</color>!", ["RaidBlocked"] = "You cannot use <color=orange>{0}</color> while raid blocked!", ["CombatBlocked"] = "You cannot use <color=orange>{0}</color> while combat blocked!", ["CooldownTime"] = "Cooldown active! You can call another in {0}!", ["TeamCooldownTime"] = "Team cooldown active! You can call another in {0}!", ["GlobalLimit"] = "Global limit of {0} active helicopters is reached, please try again later", ["PlayerLimit"] = "Player limit of {0} active helicopters is reached, please try again later", ["NotAdmin"] = "You do not have permission to use that command!", ["PlayerNotFound"] = "Can't find a player with the name or ID: {0}", ["InGameOnly"] = "Error: This command is only for use in game!", ["PlayerDead"] = "Player with name or ID {0} is dead, try again when they have respawned", ["InNamedMonument"] = "You cannot call <color=orange>{0}</color> in or near <color=red>{1}</color>, signal refunded, check inventory.", ["InSafeZone"] = "<color=orange>{0}</color> was thrown in a <color=green>Safe Zone</color> and was refunded, check inventory.", ["InvalidDrop"] = "Signal type \"{0}\" not recognised, please check and try again!", ["CannotLoot"] = "You cannot loot this because it is not yours!", ["CannotHack"] = "You cannot hack this because it is not yours!", ["CannotHarvest"] = "You cannot harvest this because it is not yours!", ["BuyCmdSyntax"] = "Buy Command Usage (prefix / for chat):\n\n{0}{1}", ["NoBuy"] = "Buy Command for <color=orange>{0}</color> Is Not Enabled!", ["BuyPermission"] = "You do not have permission to buy Heli Signal \"<color=orange>{0}</color>\".", ["PriceList"] = "Heli Signal Prices:\n\n{0}", ["HeliKilledTime"] = "<color=orange>{0}</color> killed by <color=green>{1}</color> in grid <color=green>{2}</color> (Time Taken: {3})", ["HeliCalled"] = "<color=green>{0}</color> just called in <color=orange>{1}</color> to their location in grid <color=green>{2}</color>", ["PointsGiven"] = "<color=green>{0} {1}</color> received for destroying <color=orange>{2}</color>!", ["XPGiven"] = "<color=green>{0} XP</color> received for destroying <color=orange>{1}</color>!", ["ScrapGiven"] = "<color=green>{0} Scrap</color> received for destroying <color=orange>{1}</color>!", ["CustomGiven"] = "<color=green>{0} {1}</color> received for destroying <color=orange>{2}</color>!", ["CannotDamage"] = "You <color=red>Cannot</color> damage this <color=orange>{0}</color>!", ["NoTurret"] = "You <color=red>Cannot</color> damage this <color=orange>{0}</color> with remote turrets!", ["TooFarAway"] = "You are <color=red>Too Far</color> away to engage this <color=orange>{0}</color> ({1} m)", ["CantAfford"] = "You <color=red>Cannot</color> afford this! Cost: <color=orange>{0}</color> Required: <color=orange>{1}</color>", ["FullInventory"] = "<color=green>{0}</color> <color=red>NOT</color> purchased - no inventory space.You have not been charged.", ["PlayerFull"] = "<color=green>{0}</color> <color=red>NOT</color> given to {1} - No inventory space.", ["HeliReportTitle"] = "There are currently <color=orange>{0}</color> active Helicopters", ["HeliReportItem"] = "<size=9><color=orange>{0}/{1}</color> - <color=green>{2}</color> (Owner: <color=orange>{3}</color> Grid: <color=orange>{4}</color> Health: <color=orange>{5}</color> Rotors: <color=orange>{6}/{7}</color> State: <color=orange>{8}</color>)\n</size>", ["HeliReportTitleCon"] = "There are currently {0} active Helicopters", ["HeliReportItemCon"] = "{0}/{1} - {2} (Owner: {3} Grid: {4} Health: {5} Rotors: {6}/{7} State: {8})\n", ["HeliReportList"] = "{0}", ["RetireHeli"] = "<color=orange>{0}</color> is retiring, you were warned! <color=red>{1}</color>/<color=red>{2}</color>", ["RetireWarn"] = "<color=red>Damage Blocked!</color> You may only attack from a base with TC auth.If you continue, the <color=orange>{0}</color> will retire.Warning Level: <color=red>{1}</color>/<color=red>{2}</color>", ["DmgReport2"] = "<color=orange>Damage</color> / <color=orange>Rotors</color> / <color=orange>Turret Damage</color>\n\n{0}\n{1}", ["DmgReportOwner2"] = "<size=11>Type: <color=orange>{0}</color> Owner: <color=green>{1}</color> Status: <color=red>{2}</color>\n</size>", ["DmgReportIndex2"] = "<size=11>{0}.<color=green>{1}</color> -> {2} HP / <color=green>{3}%</color> / <color=red>{4}%</color>\n</size>", ["DiscordCall"] = "**{0}** just called a **{1}** to their location in grid **{2}**", ["DiscordKill"] = "**{0}** just took down a **{1}** in grid **{2}**", ["DiscordRetire"] = "**{0}** called by **{1}** just retired from grid **{2}**", ["RetiredHelis"] = "You have retired ALL your (your teams) called Patrol Helicopters", ["NoRetiredHelis"] = "You have no active Patrol Helicopters to retire.", ["CooldownsCleared"] = "All player cooldowns have been cleared!", ["PlayerCooldownCleared"] = "Cooldown cleared for player {0} ({1})", ["PlayerNoCooldown"] = "No active cooldown for player {0} ({1})", ["NoWaveProfiles"] = "There are no wave heli profiles set up for <color=orange>{0}</color>, please report to an Admin.", ["WaveProfileError"] = "There is an error in the plugin config <color=orange>{0}</color>, please report to an Admin.", ["NextHeliCalled"] = "<color=green>{0}</color> destroyed! Stand by, a <color=red>{1}</color> is now on route to your location!", ["WaveFinished"] = "<color=green>{0}</color> destroyed! <color=red>Hostile forces have no more airbourne assets to send to your location.</color> Well Done!", ["HeliRetired"] = "<color=green>{0}</color> is low on fuel and is breaking off engagement to return to base.", ["WaveRetired"] = "<color=green>{0}</color> finished operations in the area and is retiring.Hostile airbourne forces are returning to base."
}
, this);
}
private string Lang(string messageKey, string playerId, params object[] args) {
return string.Format(lang.GetMessage(messageKey, this, playerId), args);
}
private void Message(IPlayer player, string messageKey, params object[] args) {
if (player == null || !player.IsConnected) return;
var message = Lang(messageKey, player.Id, args);
if (config.options.usePrefix && config.options.chatPrefix != string.Empty) {
if (player.IsServer) {
Regex regex = new Regex(@"(\[.*?\])");
Match match = regex.Match(config.options.chatPrefix);
player.Reply($"{match}: {message}");
}
else {
player.Reply($"{config.options.chatPrefix}: {message}");
}
}
else {
player.Reply(message);
}
}
private void Message(BasePlayer player, string messageKey, params object[] args) {
if (player == null || player is NPCPlayer || !player.IPlayer.IsConnected) return;
var message = Lang(messageKey, player.UserIDString, args);
Player.Message(player, message, config.options.usePrefix ? config.options.chatPrefix : null, config.options.chatIcon);
}
#endregion Language
#region Oxide Hooks
private void Init() {
permission.RegisterPermission(permAdmin, this);
permission.RegisterPermission(permBuy, this);
permission.RegisterPermission(permBypasscooldown, this);
config.rewards.RegisterPermissions(permission, this);
config.heli.RegisterPermissions(permission, this);
AddCovalenceCommand(config.options.reportCommand, nameof(CmdReport));
AddCovalenceCommand(config.purchasing.buyCommand, nameof(CmdBuySignal));
AddCovalenceCommand(config.heli.retireCommand, nameof(CmdRetireHeli));
AddCovalenceCommand("hsgive", nameof(CmdGiveSignal));
AddCovalenceCommand("hsclearcd", nameof(CmdClearCooldown));
}
private void OnServerInitialized() {
Instance = this;
CheckRewardPlugins();
NextTick(()=> {
LoadProfileCache();
if (!config.options.useStacking) {
Unsubscribe(nameof(CanStackItem));
Unsubscribe(nameof(CanCombineDroppedItem));
}
if (config.options.noVanillaHeli) PrintWarning($"INFO: Vanilla patrol Helicopter server event is disabled");
foreach (var monument in TerrainMeta.Path.Monuments) {
if (config.heli.blockedMonuments.Contains(monument.name)) Monuments.Add(monument);
}
if (Monuments[0] == null) {
PrintWarning($"WARNING: No monument info found.Config options relating to 'Allow Players to Call Helis at Monuments' will not function.");
}
if (config.heli.allowFlee) {
RunSilentCommand("patrolhelicopterai.use_danger_zones", "True");
RunSilentCommand("patrolhelicopterai.flee_damage_percentage", $"{config.heli.fleePercent}");
}
else if (!config.heli.allowFlee) {
RunSilentCommand("patrolhelicopterai.use_danger_zones", "False");
RunSilentCommand("patrolhelicopterai.flee_damage_percentage", "0.35");
}
if (config.heli.canMonumentCrash) RunSilentCommand("patrolhelicopterai.monument_crash", "True");
else if (!config.heli.canMonumentCrash) RunSilentCommand("patrolhelicopterai.monument_crash", "False");
}
);
}
private void Unload() {
foreach (var netId in HeliSignalData.Keys) {
var heli = BaseNetworkable.serverEntities.Find(new NetworkableId(netId)) as PatrolHelicopter;
if (heli != null) heli.Kill();
}
if (config.options.noVanillaHeli) PrintWarning($"INFO: Vanilla patrol Helicopter server event has been re-enabled");
HeliSignalData.Clear();
PlayerCooldowns.Clear();
TierCooldowns.Clear();
LockedCrates.Clear();
HeliProfiles.Clear();
WaveProfiles.Clear();
Monuments.Clear();
HeliProfileCache.Clear();
RewardPlugin = null;
Instance = null;
}
private object OnEventTrigger(TriggeredEventPrefab eventPrefab) {
if (eventPrefab.name.Contains("event_helicopter") && config.options.noVanillaHeli) return true;
return null;
}
// Re-Adds supply signal names to items added via kits/loadouts by plugins
// which don't specify a custom item display name.private void OnItemAddedToContainer(ItemContainer container, Item item)
{
if (item == null) return;
if (item.info?.itemid == supplySignalId) NextTick(()=> CheckAndFixSignal(item));
}
private object OnExplosiveThrown(BasePlayer player, SupplySignal entity, ThrownWeapon item) {
var signal = item.GetItem();
if (signal == null) return null;
if (HeliProfileCache.ContainsKey(signal.skin)) {
entity.EntityToCreate = null;
entity.CancelInvoke(entity.Explode);
entity.skinID = signal.skin;
HeliSignalThrown(player, entity, signal);
}
return null;
}
private void OnExplosiveDropped(BasePlayer player, SupplySignal entity, ThrownWeapon item) => OnExplosiveThrown(player, entity, item);
private object CanStackItem(Item item, Item targetItem) {
if (item == null || targetItem == null) return null;
if (HeliProfileCache.ContainsKey(item.skin)) {
// Only act on Heli Signal supply drops
if (item.info.itemid == targetItem.info.itemid && item.skin != targetItem.skin) return false;
}
return null;
}
private object CanCombineDroppedItem(DroppedItem droppedItem, DroppedItem targetItem) {
if (droppedItem == null || targetItem == null) return null;
if (HeliProfileCache.ContainsKey(droppedItem.item.skin)) {
// Only act on Heli Signal supply drops
if (droppedItem.item.info.itemid == targetItem.item.info.itemid && droppedItem.item.skin != targetItem.item.skin) return true;
}
return null;
}
private void OnEntitySpawned(BaseNetworkable entity) {
if (entity.ShortPrefabName.Contains("rocket_heli")) {
NextTick(()=> {
if (HeliSignalData.Count == 0 || entity == null) return;
var rocketEnt = entity as BaseEntity;
if (rocketEnt == null) return;
var heliList = Pool.Get<List<PatrolHelicopter>>();
Vis.Entities(rocketEnt.transform.position, 20f, heliList);
var ent = heliList[0];
if (ent == null) {
Pool.FreeUnmanaged(ref heliList);
return;
}
else if (ent.IsDestroyed) {
Pool.FreeUnmanaged(ref heliList);
return;
}
PatrolHelicopter heli = ent as PatrolHelicopter;
var heliProfile = heli?._name;
if (string.IsNullOrEmpty(heliProfile)) {
Pool.FreeUnmanaged(ref heliList);
return;
}
if (!config.heli.heliConfig.ContainsKey(heliProfile)) {
Pool.FreeUnmanaged(ref heliList);
return;
}
rocketEnt.creatorEntity = heli;
rocketEnt.OwnerID = heli.OwnerID;
SetDamageScale((rocketEnt as TimedExplosive), config.heli.heliConfig[heliProfile].RocketDamageScale);
Pool.FreeUnmanaged(ref heliList);
}
);
}
else if (entity.ShortPrefabName.Equals("napalm")) {
// Set the owner ID & name of fire/napalm from heli rockets to stop NRE which occurs in
// OnEntityTakeDamage from leftover napalm fire after heli is destroyed.NextTick(()=>
{
BaseEntity baseEnt = entity as BaseEntity;
if (baseEnt.creatorEntity != null) {
baseEnt.OwnerID = baseEnt.creatorEntity.OwnerID;
baseEnt._name = baseEnt.creatorEntity._name;
}
}
);
}
}
private object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info) {
var Initiator = info?.Initiator;
if (Initiator == null || entity == null) return null;
var ownerId = entity.OwnerID;
if (ownerId == null) return null;
if (Initiator is PatrolHelicopter) {
PatrolHelicopter heli = Initiator as PatrolHelicopter;
if (heli == null) return null;
else if (!HeliSignalData.ContainsKey(heli.net.ID.Value)) return null;
string heliProfile = heli?._name;
if (heliProfile == null) return null;
if (heliProfile.Contains("patrolhelicopter")) return null;
var heliOwnerId = heli.OwnerID;
if (heliOwnerId == null) return null;
if (config.heli.heliConfig.TryGetValue(heliProfile, out HeliData? heliData) && heliData != null) {
if (entity is BasePlayer) {
BasePlayer player = entity as BasePlayer;
if (player == null) return null;
if (heliData.BlockPlayerDamage && !IsOwnerOrFriend(heliOwnerId, player.userID)) {
info?.damageTypes.ScaleAll(0);
return true;
}
else if (info.damageTypes.GetMajorityDamageType() == DamageType.Bullet) {
float hitProb = (float)random.Next(1, 100);
if (heliData.BulletAccuracy < hitProb) {
info?.damageTypes.ScaleAll(0);
return true;
}
}
}
else if (ownerId.IsSteamId()) {
if (heliData.BlockProtectedList && config.heli.protectedPrefabs.Contains(entity.name)) {
info?.damageTypes.ScaleAll(0);
return true;
}
else if (heliData.BlockOtherDamage && !IsOwnerOrFriend(heliOwnerId, ownerId)) {
info?.damageTypes.ScaleAll(0);
return true;
}
else if (heliData.BlockOwnerDamage && IsOwnerOrFriend(heliOwnerId, ownerId)) {
info?.damageTypes.ScaleAll(0);
return true;
}
}
}
}
else if (Initiator.ShortPrefabName.Equals("napalm")) {
string heliProfile = Initiator?._name;
if (heliProfile == null) return null;
if (!config.heli.heliConfig.TryGetValue(heliProfile, out HeliData? heliData) || heliData == null) return null;
var heliOwner = Initiator.OwnerID;
if (heliOwner == null) return null;
if (entity is BasePlayer) {
BasePlayer player = entity as BasePlayer;
if (player == null) return null;
if (heliData.BlockPlayerDamage && !IsOwnerOrFriend(heliOwner, player.userID)) {
info?.damageTypes.ScaleAll(0);
return true;
}
}
else if (ownerId.IsSteamId()) {
if (heliData.BlockProtectedList && config.heli.protectedPrefabs.Contains(entity.name)) {
info?.damageTypes.ScaleAll(0);
return true;
}
else if (heliData.BlockOtherDamage && !IsOwnerOrFriend(heliOwner, ownerId)) {
info?.damageTypes.ScaleAll(0);
return true;
}
else if (heliData.BlockOwnerDamage && IsOwnerOrFriend(heliOwner, ownerId)) {
info?.damageTypes.ScaleAll(0);
return true;
}
}
}
return null;
}
private object OnPatrolHelicopterTakeDamage (PatrolHelicopter heli, HitInfo info) {
var Initiator = info?.Initiator;
if (heli == null || Initiator == null || Initiator == heli) return null;
var heliProfile = heli._name;
bool isVanilla = false;
if (heliProfile == null) {
if (heli.skinID == 0) isVanilla = true;
else return null;
}
else if (heliProfile != null) {
if (heliProfile.Contains("patrolhelicopter") && heli.skinID == 0) isVanilla = true;
else if (!config.heli.heliConfig.ContainsKey(heliProfile)) return null;
}
var heliAI = heli.myAI;
var heliId = heli.net.ID.Value;
var ownerId = heli.OwnerID;
ulong attackerId = 0;
BasePlayer attacker = Initiator as BasePlayer;
AutoTurret turret = Initiator as AutoTurret;
if (Initiator is BasePlayer) {
attacker = Initiator as BasePlayer;
if (attacker == null) return null;
attackerId = attacker.userID;
}
else if (Initiator is AutoTurret) {
turret = Initiator as AutoTurret;
if (turret == null) return null;
ulong turretPlayerId = turret.ControllingViewerId.GetValueOrDefault().SteamId;
if (turretPlayerId == null) return null;
attacker = BasePlayer.FindByID(turretPlayerId);
if (attacker == null) return null;
attackerId = attacker.userID;
}
else {
return null;
}
if (attacker == null || attackerId == null) return null;
if (isVanilla && (heliAI.isRetiring || heliAI.isDead)) return null;
HeliComponent heliComp = heli?.GetComponent<HeliComponent>();
if (!isVanilla && heliComp == null) return null;
if (!HeliSignalData.ContainsKey(heliId)) {
HeliSignalData.Add(heliId, new HeliStats());
HeliSignalData[heliId].OwnerID = ownerId;
HeliSignalData[heliId].OwnerName = isVanilla ? config.announce.vanillaOwner : heliComp.owner.displayName;
HeliSignalData[heliId].Attackers.Add(attackerId, new AttackersStats());
HeliSignalData[heliId].Attackers[attackerId].Name = attacker.displayName;
}
else if (!HeliSignalData[heliId].Attackers.ContainsKey(attackerId)) {
HeliSignalData[heliId].Attackers.Add(attackerId, new AttackersStats());
HeliSignalData[heliId].Attackers[attackerId].Name = attacker.displayName;
}
if (!isVanilla) {
if (heliComp.isDying) {
info?.damageTypes.ScaleAll(0);
return true;
}
var heliDisplayName = config.heli.heliConfig[heliProfile].HeliName;
if (config.heli.heliConfig.ContainsKey(heliProfile)) {
if (Initiator is AutoTurret) {
if (!config.heli.allowTurretDamage) {
info?.damageTypes.ScaleAll(0);
ComputerStation station = attacker?.GetMounted() as ComputerStation;
if (station != null) {
timer.Once(0.25f, ()=> Effect.server.Run(shockSound, station.transform.position));
timer.Once(0.5f, ()=> Effect.server.Run(deniedSound, station.transform.position));
attacker.EnsureDismounted();
Message(attacker, "NoTurret", heliDisplayName);
}
return true;
}
HeliSignalData[heliId].LastTurretAttacker = turret;
HeliSignalData[heliId].TurretDamage += info.damageTypes.Total();
HeliSignalData[heliId].Attackers[attackerId].TurretDamage += info.damageTypes.Total();
if (config.heli.heliTargetTurret && (Time.realtimeSinceStartup - heliComp.turretCooldown > config.heli.turretCooldown)) {
if (!heliComp.isStrafingTurret && (HeliSignalData[heliId].TurretDamage > HeliSignalData[heliId].PlayerDamage)) {
heliComp.turretCooldown = Time.realtimeSinceStartup;
heliComp.isStrafing = true;
heliComp.isStrafingTurret = true;
heliAI.ExitCurrentState();
heliAI.State_Strafe_Enter(attacker, false);
}
}
}
else {
HeliSignalData[heliId].PlayerDamage += info.damageTypes.Total();
}
}
if (config.heli.heliConfig[heliProfile].OwnerDamage && !IsOwnerOrFriend(attackerId, ownerId)) {
info?.damageTypes.ScaleAll(0);
Message(attacker, "CannotDamage", heliDisplayName);
return true;
}
var dist = Vector3.Distance(heli.transform.position, attacker.transform.position);
var maxDist = config.heli.maxHitDistance;
if (maxDist > 0 && dist > maxDist) {
info?.damageTypes.ScaleAll(0);
Message(attacker, "TooFarAway", heliDisplayName, maxDist);
return true;
}
if (config.heli.RetireWarning && attacker.IsBuildingBlocked() && IsOwnerOrFriend(attackerId, ownerId)) {
HeliSignalData[heliId].WarningLevel++;
if (!heliComp.isRetiring && HeliSignalData[heliId].WarningLevel >= config.heli.WarningThreshold) {
heliComp.isRetiring = true;
info?.damageTypes.ScaleAll(0);
RetireHeli(heliAI, heliId);
Message(attacker, "RetireHeli", heliDisplayName, HeliSignalData[heliId].WarningLevel, config.heli.WarningThreshold);
return true;
}
info?.damageTypes.ScaleAll(0);
if (!heliComp.isRetiring) Message(attacker, "RetireWarn", heliDisplayName, HeliSignalData[heliId].WarningLevel, config.heli.WarningThreshold);
return true;
}
}
if (HeliSignalData[heliId].FirstHitTime == 0) HeliSignalData[heliId].FirstHitTime = Time.realtimeSinceStartup;
HeliSignalData[heliId].LastAttacker = attacker;
HeliSignalData[heliId].Attackers[attackerId].DamageDealt += (info.damageTypes.Total() > heli._health) ? heli._health : info.damageTypes.Total();
HeliSignalData[heliId].Attackers[attackerId].TotalHits++;
if (info?.HitMaterial == 2306822461) HeliSignalData[heliId].Attackers[attackerId].RotorHits++;
return null;
}
private object OnPatrolHelicopterKill(PatrolHelicopter heli, HitInfo info) {
HeliComponent heliComp = heli?.GetComponent<HeliComponent>();
if (heliComp == null) return null;
var heliProfile = heliComp.heliProfile;
if (config.heli.heliConfig.ContainsKey(heliProfile)) {
if (info.damageTypes.Total() >= heli.health && !heli.myAI.isDead) {
// Prevent heli blowing up in mid air before crash spiral under certain conditions
heli.health = config.heli.heliConfig[heliProfile].Health;
heli.myAI.CriticalDamage();
return true;
}
}
return null;
}
private object OnEntityKill(PatrolHelicopter heli) {
if (!HeliSignalData.ContainsKey(heli.net.ID.Value) || heli.myAI.isRetiring) return null;
var skinId = heli.skinID;
var heliProfile = heli._name;
bool isVanilla = false;
if ((heliProfile == null && skinId == 0) || (heliProfile.Contains("patrolhelicopter") && skinId == 0)) isVanilla = true;
else if (heliProfile == null && skinId != 0) return null;
var position = heli.transform.position;
var ownerId = heli.OwnerID;
var heliId = heli.net.ID.Value;
var heliDisplayName = isVanilla ? config.announce.vanillaName : config.heli.heliConfig[heliProfile].HeliName;
var gridPos = PositionToGrid(position);
if (gridPos == null) gridPos = "Unknown";
BasePlayer lastAttacker = HeliSignalData[heliId].LastAttacker;
if (lastAttacker == null) return null;
if (config.announce.killChat) {
if (!isVanilla || (isVanilla && config.announce.killVanilla)) {
TimeSpan timeSpan = TimeSpan.FromSeconds(Time.realtimeSinceStartup - HeliSignalData[heliId].FirstHitTime);
string time = timeSpan.ToString(@"hh\:mm\:ss");
var message = string.Format(lang.GetMessage("HeliKilledTime", this, lastAttacker.UserIDString), heliDisplayName, lastAttacker.displayName, gridPos, time);
AnnounceToChat(lastAttacker, message);
}
}
if (config.announce.reportChat) {
if (!isVanilla || (isVanilla && config.announce.reportVanilla)) {
var heliOwnerName = config.announce.vanillaOwner;
var heliComp = heli.GetComponent<HeliComponent>();
if (heliComp != null) heliOwnerName = heliComp.owner.displayName;
string topReport = string.Empty;
string ownerReport = string.Format(lang.GetMessage("DmgReportOwner2", this, lastAttacker.UserIDString), heliDisplayName, heliOwnerName, "Killed");
int count = 1;
foreach (var key in HeliSignalData[heliId].Attackers.Keys.OrderByDescending(x => HeliSignalData[heliId].Attackers[x].DamageDealt)) {
if (count >= config.announce.maxReported) break;
string playerName = HeliSignalData[heliId].Attackers[key].Name;
float damageDealt = HeliSignalData[heliId].Attackers[key].DamageDealt;
int totalHits = HeliSignalData[heliId].Attackers[key].TotalHits;
int rotorHits = HeliSignalData[heliId].Attackers[key].RotorHits;
float turretDamage = HeliSignalData[heliId].Attackers[key].TurretDamage;
double rotorAccuracy = ((double)rotorHits / (double)totalHits) * 100;
double damageRatio = ((double)HeliSignalData[heliId].Attackers[key].TurretDamage / (double)HeliSignalData[heliId].Attackers[key].DamageDealt) * 100;
topReport += string.Format(lang.GetMessage("DmgReportIndex2", this, lastAttacker.UserIDString), count, playerName, Math.Round(damageDealt, 2), Math.Round(rotorAccuracy, 2), Math.Round(damageRatio, 2));
count++;
}
var dmgReport = string.Format(lang.GetMessage("DmgReport2", this, lastAttacker.UserIDString), ownerReport, topReport);
AnnounceToChat(lastAttacker, dmgReport);
}
}
if (!isVanilla) {
if (config.discord.sendHeliKill) SendToDiscord(string.Format(lang.GetMessage("DiscordKill", this, lastAttacker.UserIDString), lastAttacker.displayName, heliProfile, gridPos));
if (config.heli.heliConfig[heliProfile].LockedCratesToSpawn > 0) SpawnLockedCrates(ownerId, skinId, heliProfile, position);
NextTick(() => {
var ents = Pool.Get<List<BaseEntity>>();
Vis.Entities(position, 15f, ents);
foreach (var ent in ents) {
if ((ent is ServerGib) || (ent is LockedByEntCrate) || (ent is FireBall)) {
ent.OwnerID = ownerId;
ent.skinID = skinId;
ent._name = heliProfile;
if ((ent is ServerGib) || (ent is LockedByEntCrate)) {
// COMMENT: Adding box collider & changing rb values to help stop
// gibs & crates falling through map on occasion
BoxCollider box = ent.gameObject.AddComponent<BoxCollider>();
if (box != null) box.size = new Vector3(0.6f, 0.6f, 0.6f);
Rigidbody rigidbody = ent.gameObject.GetComponent<Rigidbody>();
if (rigidbody) {
rigidbody.drag = 0.5f;
rigidbody.angularDrag = 0.5f;
rigidbody.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
}
}
ProcessHeliEnt(ent);
}
}
Pool.FreeUnmanaged(ref ents);
}
);
if (BotReSpawn && BotReSpawn.IsLoaded) {
var botReSpawnProfile = config.heli.heliConfig[heliProfile].BotReSpawnProfile;
if (botReSpawnProfile != string.Empty) BotReSpawn?.Call("AddGroupSpawn", position, botReSpawnProfile, $"{botReSpawnProfile}Group", 0);
}
}
timer.Once(15f, () => {
if (HeliSignalData.ContainsKey(heliId)) HeliSignalData.Remove(heliId);
}
);
return null;
}
private object CanHelicopterStrafeTarget(PatrolHelicopterAI heliAI, BasePlayer player) {
PatrolHelicopter heli = heliAI?.helicopterBase;
string heliProfile = heli._name;
if (heliProfile == null) return null;
if (!config.heli.heliConfig.ContainsKey(heliProfile)) return null;
if (!config.heli.heliConfig[heliProfile].TargetOtherPlayers && !IsOwnerOrFriend(player.userID, heli.OwnerID)) {
heliAI.ExitCurrentState();
for (int i = 0; i < heliAI._targetList.Count; i++) {
if (heliAI._targetList[i].ply == player) {
heliAI.ClearAimTarget();
heliAI._targetList.Remove(heliAI._targetList[i]);
continue;
}
}
return false;
}
return null;
}
private object CanHelicopterStrafe(PatrolHelicopterAI heliAI) {
HeliComponent heliComp = heliAI?.helicopterBase.GetComponent<HeliComponent>();
if (heliComp != null) {
if (heliComp.isRetiring || heliComp.isDying) return false;
else if (heliComp.isStrafing) return false;
else if (Time.realtimeSinceStartup - heliAI.lastStrafeTime < config.heli.heliConfig[heliComp.heliProfile].StrafeCooldown) return false;
else return true;
}
return null;
}
private object OnHelicopterStrafeEnter(PatrolHelicopterAI heliAI, Vector3 strafePosition, BasePlayer strafeTarget) {
HeliComponent heliComp = heliAI?.helicopterBase.GetComponent<HeliComponent>();
if (heliComp != null) {
if (heliComp.isStrafingTurret) {
var heliId = heliComp.heliId;
if (heliId == null) return null;
if (HeliSignalData[heliId].LastTurretAttacker == null) return null;
Vector3 turretPos = HeliSignalData[heliId].LastTurretAttacker.transform.position;
heliAI.strafe_target_position = turretPos;
heliAI.lastStrafeTime = Time.realtimeSinceStartup;
heliAI._currentState = PatrolHelicopterAI.aiState.STRAFE;
heliAI.numRocketsLeft = config.heli.heliConfig[heliComp.heliProfile].MaxHeliRockets + 1;
heliAI.lastRocketTime = 0f;
Vector3 randomOffset = heliAI.GetRandomOffset(turretPos, 175f, 192.5f, turretPos.y, turretPos.y + 60f);
heliAI.SetTargetDestination(randomOffset, 10f, 60f);
heliAI.SetIdealRotation(heliAI.GetYawRotationTo(randomOffset), -1f);
heliAI.puttingDistance = true;
heliComp.isStrafing = true;
HeliSignalData[heliId].TurretDamage = 0f;
HeliSignalData[heliId].PlayerDamage = 0f;
HeliSignalData[heliId].LastTurretAttacker = null;
return true;
}
NextTick(()=> {
heliAI.numRocketsLeft = config.heli.heliConfig[heliComp.heliProfile].MaxHeliRockets + 1;
heliComp.UpdateSerializeableFields(heliAI.numRocketsLeft);
}
);
}
return null;
}
private object CanHelicopterUseNapalm(PatrolHelicopterAI heliAI) {
HeliComponent heliComp = heliAI?.helicopterBase.GetComponent<HeliComponent>();
if (heliComp != null) {
var heliProfile = heliComp.heliProfile;
if (heliProfile == null) return null;
if (heliComp.isRetiring || heliComp.isDying) return false;
return true;
}
return null;
}
private object OnHelicopterTarget(HelicopterTurret turret, BasePlayer player) {
PatrolHelicopter heli = turret._heliAI?.helicopterBase;
PatrolHelicopterAI heliAI = turret?._heliAI;
if (heli == null || heliAI == null) return null;
string heliProfile = heli._name;
if (heliProfile == null) return null;
if (config.heli.heliConfig.ContainsKey(heliProfile)) return CanCustomHeliTarget(heliAI, player, heliProfile);
return null;
}
private object CanHelicopterTarget(PatrolHelicopterA
// ... [Code preview truncated due to length. Full source in attached file] ...