C#:
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using Rust;
using Facepunch;
using Steamworks;
using System;
using System.Collections.Generic;
using System.Collections;
using System.Reflection;
using UnityEngine;
#region Changelogs and ToDo
/**********************************************************************
*
* 2.0.0 :
*
* - Major rework of the entire plugin
* - Added custom tiers through cfg
* - Added spawn probability
* - Optimised economics handouts
* - Optimised ServerRewards handouts
* - Re-added SimpleKillFeed message
* - Reworked the crate lootinjections and checks
* - The amount of unique items are now consistant
* - Custom item names are supported
* - DLC/Skin Check built in
* - Removed Linq dependency
*
* 2.0.1 :
*
* - Fixed issue with custom naming.
* - Some code cleanup.
* - New lang file entries (remove current lang files and let the plugin generate a fresh one.
* - Reward messages have been added
* - Adressed a message always to be in english rather then the assigned language
*
* 2.0.2 :
* - Adressed boxes being empty when skiping plugin lootsystem and added extra checks.
* - Added string GetBradleyTier(BradleyAPC bradley) to the API
*
* 2.0.3 :
* - Bradleys should only attack Players now.
* - Added support for QueuedPopups plugin
* - Fix for vanilla or other plugin loot system
*
* 2.0.4 :
* - Added several QueuedPopups settings to the cfg.
* - Exposed and added several API's
* - Added console commands for spawning a tiered road bradley (adding player id to the bradley)
* Usefull when adding a purchasable bradley to a GUI shop by command.
* - Added chat command to spawn a tiered bradley on a road near the admin
* - 3 new lang file entries (spawn commands info)
* - Added admin permission (for spawning road bradleys)
* - Readded and fixed RoadBradley, Satdish/harbor Events, Convoy, ArmoredTrain bradleys to get updated or not
* - Readded Notify and UINotify message support
* - Added a catch inside the chat command when for some reason the cfg is not working or tiers where not generated.
*
* 2.0.5 :
* - Optimised and changed the CanUpdateToBradleyTier hook into an object.
* - Fixed Alphaloot call which has been brought to my attention.
* - Added SkillTree rewards.
* - Added XPerience rewards.
* - Added lang file entries for the SkillTree and XPerience plugins.
*
* 2.0.6 :
* - Fixed Popup spams on spawns Thx to Kap for reporting the issue.
* - Added cfg - Annouce spawned to chat
* - Added cfg - Annouce destroyed to chat
*
* API List :
*
* object CanUpdateToBradleyTier(BradleyAPC bradley)
* void OnBradleyTiersSpawned(BradleyAPC apc , string tierName)
* void OnTieredBradleyDeath(BradleyAPC bradley , HitInfo info , string tierName)
* object IsTieredBradley(BradleyAPC bradley)
*
* string GetBradleyTier(BradleyAPC bradley)
* ulong GetBradleyPurchaserId(BradleyAPC bradley)
* bool IsPurchasedBradley(BradleyAPC bradley)
* Dictionary<NetworkableId , ulong> GetAllPurchasedBradleys()
* Dictionary<string , object> GetBradleyTierData()
* void OnBradleyTierPurchased(player , bradley , tier.Name)
*
**********************************************************************/
#endregion
namespace Oxide.Plugins
{
[Info("Bradley Tiers" , "Krungh Crow" , "2.0.6")]
[Description("Bradley with configurable difficulties")]
class BradleyTiers : RustPlugin
{
[PluginReference]//Other plugins
Plugin AlphaLoot, Economics, QueuedPopups, GUIAnnouncements, Notify, UINotify, ServerRewards, SimpleKillFeed, SkillTree, XPerience;
[PluginReference]//Bradley plugins
Plugin ArmoredTrain, Convoy, HarborEvent, RoadBadley, SatDishEvent;
#region Variables
ulong Chaticon = 0;
const string Use_Perm = "BradleyTiers.use";
const string Admin_Perm = "BradleyTiers.admin";
string Prefix;
string BTVersion;
Dictionary<NetworkableId , BradleyAPC> bradleyPool = new Dictionary<NetworkableId , BradleyAPC>();
private readonly object _bradleyLock = new object();
private readonly object _cooldownsLock = new object();
private readonly HashSet<NetworkableId> _processedBradleyCrates = new HashSet<NetworkableId>();
private readonly HashSet<NetworkableId> _skipInitTier = new HashSet<NetworkableId>();
private readonly Dictionary<ulong , PendingBuy> _pendingBuys = new Dictionary<ulong , PendingBuy>();
private readonly Dictionary<NetworkableId , ulong> _purchasedBradleys = new Dictionary<NetworkableId , ulong>();
private readonly object _purchasedLock = new object();
private bool Debug;
#endregion
#region Configuration
void Init()
{
if (!LoadConfigVariables())
{
Puts("Config file issue detected. Please delete file, or check syntax and fix.");
return;
}
permission.RegisterPermission(Use_Perm , this);
permission.RegisterPermission(Admin_Perm , this);
Prefix = configData.Prefix;
Chaticon = configData.Chaticon;
Debug = configData.Debug;
BTVersion = this.Version.ToString();
Debugmsg($"Debug mode enabled for {this.Name} v{BTVersion}");
Debugmsg("Starting DLC skin definition check.");
CheckDefsUpdated(false);
timer.Every(1800f , () => CheckDefsUpdated(false));
foreach (var entity in BaseNetworkable.serverEntities)
{
BradleyAPC bradley = entity as BradleyAPC;
if (bradley == null) continue;
if (!bradleyPool.ContainsKey(bradley.net.ID))
{
foreach (var tier in configData.Tiers.Values)
{
if (bradley.name.Contains(tier.Name))
{
bradley._maxHealth = tier.BradleyHealth;
bradley.health = tier.BradleyHealth;
bradley.bulletDamage = tier.BradleyBDamage;
bradley.viewDistance = tier.BradleyMaxFireRange;
bradley.searchRange = tier.BradleyMaxFireRange;
bradley.throttle = tier.BradleySpeed;
bradley.leftThrottle = bradley.throttle;
bradley.rightThrottle = bradley.throttle;
bradley.maxCratesToSpawn = tier.BradleyCratesAmount;
bradley.name = $"{tier.Name} Bradley";
bradleyPool[bradley.net.ID] = bradley;
Debugmsg($"Added existing {tier.Name} BradleyApc[{bradley.net.ID}] to the pool.");
break;
}
}
}
}
}
private ConfigData configData;
class ConfigData
{
[JsonProperty(PropertyName = "Prefix")]
public string Prefix = "[Bradley Tiers] ";
[JsonProperty(PropertyName = "Chat Steam64ID")]
public ulong Chaticon = 76561199183246772;
[JsonProperty(PropertyName = "Debug")]
public bool Debug = true;
[JsonProperty(PropertyName = "QueuedPopups (when installed)")]
public Popups Popup = new Popups();
[JsonProperty(PropertyName = "Notifications")]
public Notifications Notes = new Notifications();
[JsonProperty(PropertyName = "Other Plugins")]
public OtherPlugins OPlugins = new OtherPlugins();
[JsonProperty(PropertyName = "Loot Settings")]
public SettingsLoot Loot = new SettingsLoot();
[JsonProperty(PropertyName = "Tiers")]
public Dictionary<string , TierSettings> Tiers = new Dictionary<string , TierSettings>();
}
class Popups
{
[JsonProperty(PropertyName = "Background Color (rgba)")]
public string Color = "0 0 0.65 0.6";
[JsonProperty(PropertyName = "Height (Pixels)")]
public float? Height = 34;
}
class OtherPlugins
{
[JsonProperty(PropertyName = "Include ArmoredTrain Bradley")]
public bool TrainUse = false;
[JsonProperty(PropertyName = "Include Convoy Bradley")]
public bool ConvoyUse = false;
[JsonProperty(PropertyName = "Include RoadBradley")]
public bool RoadBradleyUse = false;
[JsonProperty(PropertyName = "Include SatDish/Harbor Event Bradley")]
public bool SatdishUse = false;
}
class Notifications
{
[JsonProperty(PropertyName = "Annouce spawned to chat")]
public bool ChatSpawnUse = true;
[JsonProperty(PropertyName = "Annouce destroyed to chat")]
public bool ChatDestroyUse = true;
[JsonProperty(PropertyName = "Use GUIAnnouncement")]
public bool GUIAUse = false;
[JsonProperty(PropertyName = "Use SimpleKillFeed")]
public bool SKFUse = true;
[JsonProperty(PropertyName = "Use Notify")]
public bool UseNotify = false;
[JsonProperty(PropertyName = "Notify profile ID")]
public int PiD = 0;
}
class TierSettings
{
[JsonProperty(PropertyName = "Name")]
public string Name;
[JsonProperty(PropertyName = "BradleyHealth")]
public float BradleyHealth = 1000;
[JsonProperty(PropertyName = "BradleyBDamage")]
public float BradleyBDamage = 1.0f;
[JsonProperty(PropertyName = "BradleyMaxFireRange")]
public float BradleyMaxFireRange = 100.0f;
[JsonProperty(PropertyName = "BradleySpeed")]
public float BradleySpeed = 10.0f;
[JsonProperty(PropertyName = "BradleyCratesAmount")]
public int BradleyCratesAmount = 3;
[JsonProperty(PropertyName = "Color")]
public string Color = "green";
[JsonProperty(PropertyName = "SpawnChance")]
public float SpawnChance = 25.0f;
[JsonProperty(PropertyName = "EconomicsReward")]
public double EconomicsReward = 0;
[JsonProperty(PropertyName = "ServerRewards")]
public int ServerRewards = 0;
[JsonProperty(PropertyName = "SkillTree Points")]
public int SkillTreePoints = 0;
[JsonProperty(PropertyName = "Xperience XP")]
public double XperienceXP = 0d;
[JsonProperty(PropertyName = "AlphaLoot Profile")]
public string AlphaLootProfile { get; set; } = "";
[JsonProperty(PropertyName = "Spawn Min Amount Items")]
public int MinAmount { get; set; } = 2;
[JsonProperty(PropertyName = "Spawn Max Amount Items")]
public int MaxAmount { get; set; } = 6;
[JsonProperty(PropertyName = "Loot Table" , ObjectCreationHandling = ObjectCreationHandling.Replace)]
public List<BradleyItem> Loot { get; set; } = DefaultLoot;
}
class SettingsLoot
{
[JsonProperty(PropertyName = "Use lootsystem (internal)")]
public bool UseLoot = true;
[JsonProperty(PropertyName = "Use Random Skins (internal)")]
public bool RandomSkins { get; set; } = true;
[JsonProperty(PropertyName = "Use AlphaLoot")]
public bool UseAlphaLoot { get; set; } = false;
}
private bool LoadConfigVariables()
{
try
{
configData = Config.ReadObject<ConfigData>();
}
catch (Exception ex)
{
Puts($"[Error] Failed to load config: {ex.Message}");
return false;
}
SaveConf();
return true;
}
protected override void LoadDefaultConfig()
{
Puts("Fresh install detected. Creating a new config file.");
configData = new ConfigData();
configData.Tiers.Add("0" , new TierSettings
{
Name = "Easy" ,
BradleyHealth = 1000 ,
BradleyBDamage = 1.0f ,
BradleyMaxFireRange = 100.0f ,
BradleySpeed = 10.0f ,
BradleyCratesAmount = 3 ,
Color = "green" ,
SpawnChance = 25.0f ,
EconomicsReward = 100 ,
ServerRewards = 10 ,
SkillTreePoints = 2 ,
XperienceXP = 25d ,
AlphaLootProfile = ""
});
configData.Tiers.Add("1" , new TierSettings
{
Name = "Medium" ,
BradleyHealth = 2000 ,
BradleyBDamage = 1.0f ,
BradleyMaxFireRange = 100.0f ,
BradleySpeed = 10.0f ,
BradleyCratesAmount = 3 ,
Color = "purple" ,
SpawnChance = 25.0f ,
EconomicsReward = 200 ,
ServerRewards = 20 ,
SkillTreePoints = 4 ,
XperienceXP = 25d ,
AlphaLootProfile = ""
});
configData.Tiers.Add("2" , new TierSettings
{
Name = "Hard" ,
BradleyHealth = 3000 ,
BradleyBDamage = 1.0f ,
BradleyMaxFireRange = 100.0f ,
BradleySpeed = 10.0f ,
BradleyCratesAmount = 3 ,
Color = "red" ,
SpawnChance = 25.0f ,
EconomicsReward = 300 ,
ServerRewards = 30 ,
SkillTreePoints = 6 ,
XperienceXP = 25d ,
AlphaLootProfile = ""
});
configData.Tiers.Add("3" , new TierSettings
{
Name = "Nightmare" ,
BradleyHealth = 4000 ,
BradleyBDamage = 1.0f ,
BradleyMaxFireRange = 100.0f ,
BradleySpeed = 10.0f ,
BradleyCratesAmount = 3 ,
Color = "yellow" ,
SpawnChance = 25.0f ,
EconomicsReward = 400 ,
ServerRewards = 40 ,
SkillTreePoints = 10 ,
XperienceXP = 25d ,
AlphaLootProfile = ""
});
SaveConf();
}
void SaveConf() => Config.WriteObject(configData , true);
#endregion
#region LanguageAPI
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string , string>
{
["BradleyInitiate"] = "Careful! This is a {tierName} Bradley APC!" ,
["InvalidInput"] = "<color=red>Please enter a valid command!</color>" ,
["Info"] = "<color=green>Bradley Tiers v{0}</color>\nWe have the following Tiers :\n" ,
["KillNotification"] = "<color=orange>{0}</color> Destroyed a {1} BradleyApc" ,
["NoPermission"] = "<color=green>You do not have permission to use that command!</color>" ,
["NoRoadPoint"] = "<color=red>No valid road point found within range.</color>" ,
["SpawnMessage"] = "A {0} Bradley has spawned around {1}" ,
["KillRewardMain"] = "You destroyed a {0} BradleyApc and received: {1}" ,
["RewardEcon"] = "<color=green>${0} Economics</color>" ,
["RewardSR"] = "<color=orange>{0} RP</color>" ,
["RewardSkillPoints"] = "<color=orange>{0} skill points</color>" ,
["RewardXperience"] = "<color=orange>{0} exp</color>" ,
["RewardSeparator"] = " and " ,
["NoRewards"] = "no extra rewards.",
["SpawnHeader"] = "Spawn a Bradley on the nearest road <color=orange>(Admin permission)</color>\n" ,
["SpawnCommands"] = "/bt spawn {0}",
["ConfigNotLoaded"] = "<color=red>Config not loaded. Check server console for errors.</color>"
} , this);
}
#endregion
#region Commands
[ChatCommand("bt")]
private void BTCmd(BasePlayer player , string command , string[] args)
{
if (args.Length == 0)
return;
if (configData == null || configData.Tiers == null)
{
Player.Message(player , Prefix + msg("ConfigNotLoaded" , player.UserIDString) , Chaticon);
return;
}
string sub = args[0].ToLower();
if (sub == "info")
{
if (!HasPerm(player , Use_Perm))
return;
var messageBuilder = new System.Text.StringBuilder();
string originalInfoMessage = string.Format(
lang.GetMessage("Info" , this , player.UserIDString) ,
BTVersion
);
messageBuilder.AppendLine(originalInfoMessage);
// Tier list
foreach (var tierEntry in configData.Tiers)
{
string name = tierEntry.Value.Name;
string color = tierEntry.Value.Color;
messageBuilder.AppendLine(
$"<color={color}>{name}</color> - <color=white>{tierEntry.Value.BradleyHealth}</color> HP"
);
}
if (HasPerm(player , Admin_Perm))
{
// Spawn header
messageBuilder.AppendLine();
messageBuilder.AppendLine(
lang.GetMessage("SpawnHeader" , this , player.UserIDString)
);
// Spawn command lines per tier
foreach (var tierEntry in configData.Tiers)
{
string name = tierEntry.Value.Name.ToLower();
messageBuilder.AppendLine(
string.Format(
lang.GetMessage("SpawnCommands" , this , player.UserIDString) ,
name
)
);
}
}
Player.Message(player , messageBuilder.ToString() , Chaticon);
return;
}
if (sub == "spawn")
{
if (!HasPerm(player , Admin_Perm))
return;
HandleBuyBradley(player , args);
return;
}
}
[ConsoleCommand("bt.spawn")]
private void CCmdBuyId(ConsoleSystem.Arg arg)
{
// bt.spawn <steamId> <tiername>
if (arg.Args == null || arg.Args.Length < 2) return;
var idStr = arg.GetString(0);
var tierArg = arg.GetString(1);
if (string.IsNullOrEmpty(idStr) || string.IsNullOrEmpty(tierArg)) return;
if (!ulong.TryParse(idStr , out var userId)) return;
var player = BasePlayer.FindByID(userId) ?? BasePlayer.FindSleeping(userId);
if (player == null) return;
HandleBuyBradley(player , new[] { "spawn" , tierArg });
}
#endregion
#region Hooks
void Unload()
{
bradleyPool?.Clear();
CoolDowns?.Clear();
bradleyPool = null;
CoolDowns = null;
_processedBradleyCrates?.Clear();
_dlcSkins?.Clear();
_wsToDefId?.Clear();
_idToSkin?.Clear();
_dlcSkins = null;
_skipInitTier?.Clear();
lock (_purchasedLock)
{
_purchasedBradleys.Clear();
}
_roadBradleys?.Clear();
}
private void OnBradleyApcInitialize(BradleyAPC bradley)
{
if (bradley == null) return;
float now = Time.realtimeSinceStartup;
foreach (var kvp in _pendingBuys)
{
var p = kvp.Value;
if (p == null) continue;
if (now - p.createdAt > 3f) continue;
if (Vector3.Distance(bradley.transform.position , p.spawnPos) <= 25f)
return;
}
var hook = Interface.CallHook("CanUpdateToBradleyTier" , bradley);
if (hook is bool b && b == false) return;
NextTick(() =>
{
if (bradley == null || bradley.IsDestroyed) return;
if (!CanUseAPC(bradley))
return;
var tier = RollRandomTier();
if (tier == null)
return;
ApplyBradleyTier(bradley , tier , announceToAll: true , sendHook: true);
});
}
private TierSettings RollRandomTier()
{
float totalSpawnChance = 0f;
foreach (var t in configData.Tiers.Values)
totalSpawnChance += t.SpawnChance;
if (totalSpawnChance <= 0f)
return null;
float roll = UnityEngine.Random.Range(0f , totalSpawnChance);
float cumulative = 0f;
foreach (var t in configData.Tiers.Values)
{
cumulative += t.SpawnChance;
if (roll <= cumulative)
return t;
}
foreach (var t in configData.Tiers.Values)
return t;
return null;
}
private void ApplyBradleyTier(BradleyAPC bradley , TierSettings tier , bool announceToAll , bool sendHook)
{
if (bradley == null || bradley.IsDestroyed || tier == null)
return;
var dropPosition = bradley.transform.position;
string grid = GetGrid(dropPosition);
timer.Once(0.5f , () =>
{
if (bradley == null || bradley.IsDestroyed)
return;
bradley._maxHealth = tier.BradleyHealth;
bradley.health = tier.BradleyHealth;
bradley.bulletDamage = tier.BradleyBDamage;
bradley.viewDistance = tier.BradleyMaxFireRange;
bradley.searchRange = tier.BradleyMaxFireRange;
bradley.throttle = tier.BradleySpeed;
bradley.leftThrottle = bradley.throttle;
bradley.rightThrottle = bradley.throttle;
bradley.maxCratesToSpawn = tier.BradleyCratesAmount;
bradley.name = $"{tier.Name}";
bradleyPool[bradley.net.ID] = bradley;
if (sendHook)
Interface.CallHook("OnBradleyTiersSpawned" , bradley , tier.Name);
bradley.SendNetworkUpdate();
if (!announceToAll)
return;
var bradleyType = $"<color={tier.Color}>{tier.Name}</color>";
var gridColor = $"<color={tier.Color}>{grid}</color>";
string rawMsg = lang.GetMessage("SpawnMessage" , this);
string broadcastMsg = string.Format(rawMsg , bradleyType , gridColor);
foreach (BasePlayer player in BasePlayer.activePlayerList)
{
if (QueuedPopups != null)
{
QueuedPopups.Call("AddPopup" ,
player ,
broadcastMsg ,
2 ,
"assets/prefabs/npc/m2bradley/BradleyAvatar.png" ,
configData.Popup.Color ,
null ,
null ,
configData.Popup.Height
);
}
else
{
if (configData.Notes.ChatSpawnUse)
Player.Message(player , Prefix + broadcastMsg , Chaticon);
}
}
SendPopups(broadcastMsg);
Puts(tier.Name.ToLower() != "vanilla"
? $"Upgraded a BradleyApc to [{tier.Name}] Tier"
: "Allowed a Vanilla BradleyApc to spawn");
});
}
private void OnEntityDeath(BaseCombatEntity entity , HitInfo info)
{
BradleyAPC bradley = entity as BradleyAPC;
if (bradley == null) return;
if (bradleyPool.ContainsKey(bradley.net.ID))
{
var tierName = bradley.name;
var killer = info?.InitiatorPlayer?.displayName ?? "unknown";
var killerID = info?.InitiatorPlayer?.UserIDString ?? "unknown";
Debugmsg($"Tiered Bradley '{tierName}' got destroyed, removing from pool.");
var message = string.Format(msg("KillNotification") , killer , tierName);
Debugmsg($"{tierName} BradleyAPC at {GetGrid(bradley.transform.position)} was destroyed by {killer}");
if (configData.Notes.ChatDestroyUse) Server.Broadcast(Prefix + message , Chaticon);
SendPopups(message);
TierSettings tierSettings = null;
foreach (var t in configData.Tiers.Values)
{
if (t.Name == tierName)
{
tierSettings = t;
break;
}
}
if (killerID != "unknown" && tierSettings != null)
{
BasePlayer killerPlayer = BasePlayer.FindByID(ulong.Parse(killerID));
if (killerPlayer != null && killerPlayer.IsConnected)
{
List<string> rewardParts = new List<string>();
if (Economics != null && tierSettings.EconomicsReward > 0)
rewardParts.Add(string.Format(lang.GetMessage("RewardEcon" , this , killerID) , tierSettings.EconomicsReward));
if (ServerRewards != null && tierSettings.ServerRewards > 0)
rewardParts.Add(string.Format(lang.GetMessage("RewardSR" , this , killerID) , tierSettings.ServerRewards));
if (SkillTree != null && tierSettings.SkillTreePoints > 0)
rewardParts.Add(string.Format(lang.GetMessage("RewardSkillPoints" , this , killerID) , tierSettings.SkillTreePoints));
if (XPerience != null && tierSettings.XperienceXP > 0d)
rewardParts.Add(string.Format(lang.GetMessage("RewardXperience" , this , killerID) , tierSettings.XperienceXP));
string rewardSummary = rewardParts.Count > 0
? string.Join(lang.GetMessage("RewardSeparator" , this , killerID) , rewardParts)
: lang.GetMessage("NoRewards" , this , killerID);
string mainMsg = string.Format(lang.GetMessage("KillRewardMain" , this , killerID) , tierSettings.Name , rewardSummary);
PrintToChat(killerPlayer , Prefix + mainMsg);
GiveTierRewards(killerPlayer , tierSettings);
}
}
Interface.CallHook("OnTieredBradleyDeath" , bradley , info , tierName);
lock (_purchasedLock)
{
_purchasedBradleys.Remove(bradley.net.ID);
}
bradleyPool.Remove(bradley.net.ID);
}
}
private void OnEntityKill(BaseNetworkable entity)
{
var bradley = entity as BradleyAPC;
if (bradley == null) return;
_roadBradleys.Remove(bradley.net.ID);
lock (_purchasedLock)
{
_purchasedBradleys.Remove(bradley.net.ID);
}
}
void OnEntityTakeDamage(BradleyAPC bradley , HitInfo info)
{
if (!ContainsBradley(bradley.net.ID))
{
Debugmsg($"OnEntityTakeDamage: Bradley {bradley.net.ID} is not registered as tiered.");
return;
}
if (!TryGetBradley(bradley.net.ID , out var tieredBradley))
{
Debugmsg($"OnEntityTakeDamage: Failed to retrieve tiered Bradley {bradley.net.ID} from pool.");
return;
}
Debugmsg($"Tiered Bradley '{tieredBradley.name}' took damage.");
var attacker = info?.InitiatorPlayer;
if (attacker == null)
{
Debugmsg("OnEntityTakeDamage: attacker is null or not a player.");
return;
}
string playerID = attacker.UserIDString;
if (TryStartCooldown(playerID , 30f))
{
string chatMessage = msg("BradleyInitiate" , attacker.UserIDString);
chatMessage = chatMessage.Replace("{tierName}" , tieredBradley.name);
Player.Message(attacker , Prefix + chatMessage , Chaticon);
QueuedPopups?.Call("AddPopup" , attacker , chatMessage , 2 , "assets/prefabs/npc/m2bradley/BradleyAvatar.png" , configData.Popup.Color , null , null , configData.Popup.Height);
Debugmsg($"Sent chat message to {attacker.displayName}: {chatMessage}");
}
else
{
Debugmsg($"{attacker.displayName} is still on cooldown for messages.");
}
}
object CanBradleyApcTarget(BradleyAPC bradley , BaseEntity target)
{
if (bradleyPool.ContainsKey(bradley.net.ID))
{
if (!(target is BasePlayer))
{
return false;
}
BasePlayer player = target as BasePlayer;
if (player != null && player.IsNpc)
{
return false;
}
}
return null;
}
#endregion
#region Timers
Dictionary<string , float> CoolDowns = new Dictionary<string , float>();
private bool TryStartCooldown(string playerID , float durationSeconds)
{
float lastUsed;
float currentTime = Time.realtimeSinceStartup;
if (CoolDowns.TryGetValue(playerID , out lastUsed))
{
if (currentTime < lastUsed + durationSeconds)
return false;
}
CoolDowns[playerID] = currentTime;
return true;
}
public class CooldownTimer
{
public Timer timer;
public float start;
public float countdown;
}
#endregion
#region API
object IsTieredBradley(BradleyAPC bradley)
{
return bradleyPool.ContainsKey(bradley.net.ID);
}
string GetBradleyTier(BradleyAPC bradley)
{
if (bradley == null || !bradleyPool.ContainsKey(bradley.net.ID))
return "Vanilla";
return bradley.name;
}
ulong GetBradleyPurchaserId(BradleyAPC bradley)
{
if (bradley == null) return 0;
lock (_purchasedLock)
{
ulong buyer;
return _purchasedBradleys.TryGetValue(bradley.net.ID , out buyer) ? buyer : 0;
}
}
bool IsPurchasedBradley(BradleyAPC bradley)
{
if (bradley == null) return false;
lock (_purchasedLock)
{
return _purchasedBradleys.ContainsKey(bradley.net.ID);
}
}
Dictionary<NetworkableId , ulong> GetAllPurchasedBradleys()
{
lock (_purchasedLock)
{
return new Dictionary<NetworkableId , ulong>(_purchasedBradleys);
}
}
#endregion
#region API CFG
public class BradleyTierInfo
{
public string Key;
public string Name;
public float BradleyHealth;
public float BradleyBDamage;
public float BradleyMaxFireRange;
public float BradleySpeed;
public int BradleyCratesAmount;
public string Color;
public float SpawnChance;
public double EconomicsReward;
public int ServerRewards;
}
private BradleyTierInfo ToDto(string key , TierSettings t)
{
return new BradleyTierInfo
{
Key = key ,
Name = t.Name ,
BradleyHealth = t.BradleyHealth ,
BradleyBDamage = t.BradleyBDamage ,
BradleyMaxFireRange = t.BradleyMaxFireRange ,
BradleySpeed = t.BradleySpeed ,
BradleyCratesAmount = t.BradleyCratesAmount ,
Color = t.Color ,
SpawnChance = t.SpawnChance ,
EconomicsReward = t.EconomicsReward ,
ServerRewards = t.ServerRewards
};
}
Dictionary<string , object> GetBradleyTierData()
{
var result = new Dictionary<string , object>();
foreach (var kv in configData.Tiers)
result[kv.Key] = ToDto(kv.Key , kv.Value);
return result;
}
#endregion
#region Helpers
private readonly HashSet<ulong> _satDishClaimed = new HashSet<ulong>();
private static ulong GetNetId(BaseNetworkable ent)
{
return ent?.net?.ID.Value ?? 0UL;
}
private void CacheSatDishClaimNextTick(BradleyAPC bradley)
{
if (bradley == null || bradley.net == null) return;
NextTick(() =>
{
var res = Interface.CallHook("CanBradleyTiersEdit" , bradley);
if (res != null)
{
var id = GetNetId(bradley);
if (id != 0UL)
{
_satDishClaimed.Add(id);
Debugmsg($"Cached SatDish claim for {bradley} netID={id} res={res}");
}
}
});
}
private bool IsSatDishBradleyCached(BradleyAPC bradley)
{
var id = GetNetId(bradley);
return id != 0UL && _satDishClaimed.Contains(id);
}
private void OnRoadBradleySpawned(BasePlayer owner , BradleyAPC apc , string profile , string targetRoad)
{
if (apc == null || apc.net == null) return;
_roadBradleys.Add(apc.net.ID);
Debugmsg($"RoadBradley detected: owner={owner?.UserIDString ?? "null"} apc={apc} id={apc.net.ID} profile={profile} road={targetRoad}");
}
private readonly HashSet<NetworkableId> _roadBradleys = new HashSet<NetworkableId>();
private bool CanUseAPC(BradleyAPC bradley)
{
Debugmsg($"[CanUseAPC] called for {bradley} id={bradley?.net?.ID}");
CacheSatDishClaimNextTick(bradley);
bool isSatDishOrHarbor =
Interface.CallHook("CanBradleyTiersEdit" , bradley) != null ||
IsSatDishBradleyCached(bradley);
if (isSatDishOrHarbor)
{
Debugmsg($"Detected SatDish/Harbor Event Bradley {bradley}");
if (!configData.OPlugins.SatdishUse)
{
Debugmsg($"Blocked Tiered APC for SatDish/Harbor Event {bradley}");
return false;
}
return true;
}
var convoyHook = Interface.CallHook("IsConvoyVehicle" , (BaseEntity)bradley);
if (convoyHook is bool isConvoy && isConvoy)
{
Debugmsg($"Detected Convoy Bradley {bradley}");
if (!configData.OPlugins.ConvoyUse)
{
Debugmsg($"Blocked Tiered APC for Convoy {bradley}");
return false;
}
return true;
}
ulong idUlong = bradley?.net?.ID.Value ?? 0UL;
uint idUint = (uint)idUlong;
var trainHook = Interface.CallHook("IsTrainBradley" , idUint);
if (!(trainHook is bool trainBool && trainBool))
trainHook = Interface.CallHook("IsTrainBradley" , idUlong);
if (trainHook is bool isTrain && isTrain)
{
Debugmsg($"Detected ArmoredTrain Bradley {bradley}");
if (!configData.OPlugins.TrainUse)
{
Debugmsg($"Blocked Tiered APC for ArmoredTrain {bradley}");
return false;
}
return true;
}
if (_roadBradleys.Contains(bradley.net.ID))
{
Debugmsg($"Detected RoadBradley {bradley}");
if (!configData.OPlugins.RoadBradleyUse) return false;
return true;
}
if (Interface.CallHook("IsBradleyDrop" , bradley) != null)
{
Debugmsg($"Blocked Tiered APC for BradleyDrops {bradley}");
return false;
}
return true;
}
void OnTieredBradleyDeath(BradleyAPC bradley , HitInfo info , string tierName)
{
var killer = info?.InitiatorPlayer?.displayName ?? "unknown";
Vector3 position = bradley.transform.position;
int count = 0;
string grid = GetGrid(position);
timer.Once(1f , () =>
{
float distance = 10f;
List<LootContainer> crates = new List<LootContainer>();
Vis.Entities<LootContainer>(position , distance , crates);
TierSettings tierSettings = null;
foreach (var t in configData.Tiers.Values)
{
if (t.Name == tierName)
{
tierSettings = t;
break;
}
}
foreach (LootContainer lootContainer in crates)
{
if (lootContainer == null) continue;
if (!lootContainer.PrefabName.Contains("bradley_crate"))
continue;
if (_processedBradleyCrates.Contains(lootContainer.net.ID))
continue;
bool allowAlphaLoot =
configData.Loot.UseAlphaLoot &&
tierSettings != null &&
!string.IsNullOrEmpty(tierSettings.AlphaLootProfile);
bool allowInternalLoot =
configData.Loot.UseLoot &&
tierSettings != null &&
tierSettings.Loot != null &&
tierSettings.Loot.Count > 0;
if (!allowAlphaLoot && !allowInternalLoot)
{
Debugmsg($"Loot disabled for {tierName}. Leaving crate {lootContainer.net.ID} untouched for other plugins.");
continue;
}
lootContainer.inventory.Clear();
count++;
if (allowAlphaLoot)
{
AlphaLoot?.Call("PopulateLoot" , lootContainer.inventory , tierSettings.AlphaLootProfile);
Debugmsg($"Populated crate {lootContainer.net.ID} with AlphaLoot profile: {tierSettings.AlphaLootProfile}");
}
else
{
SpawnLoot(lootContainer.inventory , tierSettings.Loot , tierSettings.MinAmount , tierSettings.MaxAmount);
Debugmsg($"Populated crate {lootContainer.net.ID} with internal loot.");
}
_processedBradleyCrates.Add(lootContainer.net.ID);
}
Debugmsg($"{count} bradley crates populated with {tierName} loot. Others left untouched/vanilla.");
});
}
private bool TryGetBradley(NetworkableId id , out BradleyAPC bradley)
{
lock (_bradleyLock)
{
return bradleyPool.TryGetValue(id , out bradley);
}
}
private bool ContainsBradley(NetworkableId id)
{
lock (_bradleyLock)
{
return bradleyPool.ContainsKey(id);
}
}
bool HasPerm(BasePlayer player , string perm) => permission.UserHasPermission(player.UserIDString , perm);
private static string GetGrid(Vector3? position)
{
if (position.HasValue)
{
return MapHelper.PositionToString(position.Value);
}
else
{
return "Unknown";
}
}
#endregion
#region Exp and Reward systems
private void GiveTierRewards(BasePlayer player , TierSettings tier)
{
if (player == null || !player.IsConnected) return;
if (tier == null) return;
string killerID = player.UserIDString;
if (tier.EconomicsReward > 0)
{
Economics?.Call("Deposit" , killerID , tier.EconomicsReward);
Debugmsg($"[Rewards] Economics Deposit {tier.EconomicsReward} -> {player.displayName} ({killerID})");
}
if (tier.ServerRewards > 0)
{
ServerRewards?.Call("AddPoints" , killerID , tier.ServerRewards);
Debugmsg($"[Rewards] ServerRewards AddPoints {tier.ServerRewards} -> {player.displayName} ({killerID})");
}
int skillPoints = tier.SkillTreePoints;
if (skillPoints > 0)
{
var hookResult = Interface.CallHook("OnBradleyTiersGiveSkillPoints" , player , skillPoints);
if (hookResult is int modified)
skillPoints = modified;
if (skillPoints > 0)
{
SkillTree?.Call("GiveSkillPoints" , player , skillPoints);
Debugmsg($"[Rewards] SkillTree GiveSkillPoints {skillPoints} -> {player.displayName}");
}
else Debugmsg($"[Rewards] SkillTree blocked/modified to 0 for {player.displayName}");
}
double xp = tier.XperienceXP;
if (xp > 0d)
{
XPerience?.Call("GiveXPBasic" , player , xp);
Debugmsg($"[Rewards] Xperience GiveXPBasic {xp} -> {player.displayName} (plugin={(XPerience != null ? "loaded" : "null")})");
}
}
#endregion
#region Lootsystem
private static List<BradleyItem> DefaultLoot
{
get
{
return new List<BradleyItem>
{
new BradleyItem { probability = 1f, shortname = "ammo.pistol", name = "", amountMin = 128, amount = 256, skin = 0 },
new BradleyItem { probability = 1f, shortname = "ammo.pistol.fire", name = "", amountMin = 128, amount = 256, skin = 0 },
new BradleyItem { probability = 1f, shortname = "ammo.rifle.explosive", name = "", amountMin = 128, amount = 256, skin = 0 },
new BradleyItem { probability = 1f, shortname = "ammo.rifle.hv", name = "", amountMin = 128, amount = 256, skin = 0 },
new BradleyItem { probability = 1f, shortname = "ammo.rifle.incendiary", name = "", amountMin = 128, amount = 256, skin = 0 },
new BradleyItem { probability = 1f, shortname = "ammo.shotgun", name = "", amountMin = 128, amount = 256, skin = 0 },
new BradleyItem { probability = 1f, shortname = "ammo.shotgun", name = "", amountMin = 128, amount = 256, skin = 0 },
new BradleyItem { probability = 1f, shortname = "explosive.timed", name = "", amountMin = 1, amount = 5, skin = 0 },
new BradleyItem { probability = 1f, shortname = "explosives", name = "", amountMin = 200, amount = 200, skin = 0 },
new BradleyItem { probability = 1f, shortname = "pistol.m92", name = "", amountMin = 1, amount = 1, skin = 0 },
new BradleyItem { probability = 1f, shortname = "shotgun.spas12", name = "", amountMin = 1, amount = 1, skin = 0 },
new BradleyItem { probability = 1f, shortname = "pickaxe", name = "", amountMin = 1, amount = 1, skin = 0 },
new BradleyItem { probability = 1f, shortname = "hatchet", name = "", amountMin = 1, amount = 1, skin = 0 },
new BradleyItem { probability = 1f, shortname = "can.beans", name = "", amountMin = 3, amount = 5, skin = 0 },
new BradleyItem { probability = 1f, shortname = "can.tuna", name = "", amountMin = 3, amount = 5, skin = 0 },
new BradleyItem { probability = 1f, shortname = "black.raspberries", name = "", amountMin = 3, amount = 5, skin = 0 },
new BradleyItem { probability = 0.6f, shortname = "hmlmg", name = "", amountMin = 1, amount = 1, skin = 0 },
new BradleyItem { probability = 0.6f, shortname = "supply.signal", name = "", amountMin = 1, amount = 1, skin = 0 },
};
}
}
public class BradleyItem
{
public float probability { get; set; } = 1f;
public string shortname { get; set; }
public string name { get; set; } = "";
public ulong skin { get; set; }
public int amountMin { get; set; }
public int amount { get; set; }
}
#region DlcCheckerIntegration
private readonly Hash<ulong , int> _wsToDefId = new Hash<ulong , int>();
private readonly Hash<int , ItemSkinDirectory.Skin> _idToSkin = new Hash<int , ItemSkinDirectory.Skin>();
private float _wst;
private bool _isInitialized;
private int _lastSteamDefCount = 0;
private List<ulong> _dlcSkins = new List<ulong>();
private System.R
// ... [Code preview truncated due to length. Full source in attached file] ...