C#:
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Rust.Modular;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Oxide.Core.Plugins;
namespace Oxide.Plugins
{
[Info("Bomb Trucks", "WhiteThunder", "0.9.2")]
[Description("Allow players to spawn bomb trucks.")]
internal class BombTrucks : CovalencePlugin
{
#region Fields
[PluginReference]
private readonly Plugin NoEngineParts, NoEscape, SpawnModularCar;
private const int RfReservedRangeMin = 4760;
private const int RfReservedRangeMax = 4790;
private const string DefaultTruckConfigName = "default";
private const string PermissionSpawnFormat = "bombtrucks.spawn.{0}";
private const string PermissionGiveBombTruck = "bombtrucks.give";
private const string PermissionFreeDetonator = "bombtrucks.freedetonator";
private const string PrefabExplosiveRocket = "assets/prefabs/ammo/rocket/rocket_basic.prefab";
private const string PrefabRfReceiver = "assets/prefabs/deployable/playerioents/gates/rfreceiver/rfreceiver.prefab";
private const int DetonatorItemId = 596469572;
private const int InvalidFrequency = -1;
private readonly object False = false;
private readonly Vector3 RfReceiverPosition = new(0, -0.1f, 0);
private readonly Quaternion RfReceiverRotation = Quaternion.Euler(0, 180, 0);
private readonly RFReceiverManager _receiverManager;
private readonly BombTruckTracker _bombTruckTracker;
private readonly Dictionary<ulong, float> _dismountTimeByPlayer = new();
private StoredData _pluginData;
private Configuration _pluginConfig;
private ProtectionProperties _immortalProtection;
private bool _pluginUnloaded;
public BombTrucks()
{
_receiverManager = new RFReceiverManager(this);
_bombTruckTracker = new BombTruckTracker(this);
}
#endregion
#region Hooks
private void Init()
{
_pluginData = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name);
foreach (var truckConfig in _pluginConfig.BombTrucks)
{
permission.RegisterPermission(GetSpawnPermission(truckConfig.Name), this);
}
permission.RegisterPermission(PermissionGiveBombTruck, this);
permission.RegisterPermission(PermissionFreeDetonator, this);
}
private void OnServerInitialized()
{
_immortalProtection = ScriptableObject.CreateInstance<ProtectionProperties>();
_immortalProtection.name = "BombTrucksProtection";
_immortalProtection.Add(1);
VerifyDependencies();
CleanStaleTruckData();
InitializeBombTrucks();
}
private void Unload()
{
UnityEngine.Object.Destroy(_immortalProtection);
_bombTruckTracker.Unload();
// This is used to signal coroutines to stop early (simpler than keeping track of them).
_pluginUnloaded = true;
}
private void OnNewSave()
{
ClearData();
}
private void OnEntityDeath(ModularCar car)
{
if (IsBombTruck(car) && car.OwnerID != 0)
{
DetonateBombTruck(car);
}
}
private void OnEntityMounted(ModularCarSeat seat, BasePlayer player)
{
var car = (seat.GetParentEntity() as BaseVehicleModule)?.GetParentEntity() as ModularCar;
if (car == null || !IsBombTruck(car))
return;
if (car.GetFuelSystem() is EntityFuelSystem fuelSystem)
{
var fuelContainer = fuelSystem.GetFuelContainer();
fuelContainer.inventory.AddItem(fuelContainer.allowedItem, fuelContainer.allowedItem.stackable);
}
}
private void OnEntityDismounted(BaseMountable mountable, BasePlayer player)
{
_dismountTimeByPlayer[player.userID] = Time.time;
}
private object CanLootEntity(BasePlayer player, ModularCarGarage carLift)
{
if (!carLift.PlatformIsOccupied)
return null;
var car = carLift.carOccupant;
if (car != null && IsBombTruck(car))
{
ChatMessage(player, "Lift.Edit.Error");
return False;
}
return null;
}
// This hook is exposed by the deprecated plugin Modular Car Code Locks (CarCodeLocks).
private object CanDeployCarCodeLock(ModularCar car, BasePlayer player)
{
return CanLockVehicle(car, player);
}
private object CanDeployVehicleCodeLock(ModularCar car, BasePlayer player)
{
return CanLockVehicle(car, player);
}
private object CanDeployVehicleKeyLock(ModularCar car, BasePlayer player)
{
return CanLockVehicle(car, player);
}
private object CanLockVehicle(ModularCar car, BasePlayer player)
{
if (!IsBombTruck(car))
return null;
if (player != null)
{
player.ChatMessage(GetMessage(player.IPlayer, "Lock.Deploy.Error"));
}
return False;
}
private void OnRfBroadcasterAdded(IRFObject obj, int frequency)
{
_receiverManager.DetonateFrequency(frequency);
}
private void OnRfListenerAdded(IRFObject obj, int frequency)
{
var receiver = obj as RFReceiver;
if (receiver != null)
{
var frequency2 = frequency;
var receiver2 = receiver;
// Need to delay checking for the car since the receiver is spawned unparented to mitigate rendering bug.
NextTick(() =>
{
var car = GetReceiverCar(receiver2);
if (car == null || !IsBombTruck(car))
return;
_receiverManager.AddReceiver(frequency2, receiver2);
});
}
}
private void OnRfListenerRemoved(IRFObject obj, int frequency)
{
var receiver = obj as RFReceiver;
if (receiver == null)
return;
var car = GetReceiverCar(receiver);
if (car == null || !IsBombTruck(car))
return;
_receiverManager.RemoveReceiver(frequency, receiver);
}
// This hook is exposed by Claim Vehicle Ownership (ClaimVehicle).
private object OnVehicleUnclaim(BasePlayer player, ModularCar car)
{
if (car == null || !IsBombTruck(car))
return null;
ChatMessage(player, "Unclaim.Error");
return False;
}
// This hook is exposed by Modular Car Turrets (CarTurrets).
private object OnCarAutoTurretDeploy(BaseVehicleModule module, BasePlayer player, bool automatedDeployment)
{
if (module == null)
return null;
var car = module.Vehicle as ModularCar;
if (car == null || !IsBombTruck(car))
return null;
if (player != null && !automatedDeployment)
{
ChatMessage(player, "AutoTurret.Deploy.Error");
}
return False;
}
// This hook is exposed by No Engine Parts (NoEngineParts).
private object OnEngineLoadoutOverride(EngineStorage engineStorage)
{
var car = engineStorage.GetEngineModule()?.Vehicle as ModularCar;
if (car == null || !IsBombTruck(car))
return null;
return False;
}
// This hook is exposed by Engine Parts Durability (EnginePartsDurability).
private object OnEngineDamageMultiplierChange(EngineStorage engineStorage, float desiredMultiplier)
{
var car = engineStorage.GetEngineModule()?.Vehicle as ModularCar;
if (car == null || !IsBombTruck(car))
return null;
return False;
}
// This hook is exposed by Auto Engine Parts (AutoEngineParts).
private object OnEngineStorageFill(EngineStorage engineStorage, int enginePartsTier)
{
var car = engineStorage.GetEngineModule()?.Vehicle as ModularCar;
if (car == null || !IsBombTruck(car))
return null;
return False;
}
#endregion
#region Commands
[Command("bombtruck", "bt", "boomer")]
private void SpawnBombTruckCommand(IPlayer player, string cmd, string[] args)
{
if (player.IsServer)
return;
var basePlayer = player.Object as BasePlayer;
if (!basePlayer.CanInteract())
return;
if (args.Length == 0)
{
SubCommand_SpawnBombTruck(player, args);
return;
}
switch (args[0].ToLower())
{
case "help":
SubCommand_Help(player, args.Skip(1).ToArray());
return;
default:
SubCommand_SpawnBombTruck(player, args);
return;
}
}
private void SubCommand_Help(IPlayer player, string[] args)
{
var allowedTruckConfigs = _pluginConfig.BombTrucks
.Where(config => permission.UserHasPermission(player.Id, GetSpawnPermission(config.Name)))
.OrderBy(truckConfig => truckConfig.Name, Comparer<string>.Create(SortTruckNames))
.ToList();
if (allowedTruckConfigs.Count == 0)
{
ReplyToPlayer(player, "Generic.Error.NoPermission");
return;
}
var messages = new List<string> { GetMessage(player, "Command.Help") };
messages.AddRange(allowedTruckConfigs.Select(truckConfig =>
{
var message = (truckConfig.Name == DefaultTruckConfigName) ?
GetMessage(player, "Command.Help.Spawn.Default") :
GetMessage(player, "Command.Help.Spawn.Named", truckConfig.Name);
var limitUsage = GetPlayerData(player.Id).GetTruckCount(truckConfig.Name);
message += " - " + GetMessage(player, "Command.Help.LimitUsage", limitUsage, truckConfig.SpawnLimit);
var remainingCooldown = GetPlayerRemainingCooldownSeconds(player.Id, truckConfig);
if (remainingCooldown > 0)
message += " - " + GetMessage(player, "Command.Help.RemainingCooldown", FormatTime(remainingCooldown));
return message;
}));
player.Reply(string.Join("\n", messages));
}
private void SubCommand_SpawnBombTruck(IPlayer player, string[] args)
{
var truckName = DefaultTruckConfigName;
if (args.Length > 0)
truckName = args[0];
var basePlayer = player.Object as BasePlayer;
if (!VerifyTruckConfigDefined(player, truckName, out var truckConfig) ||
!VerifyHasPermission(player, GetSpawnPermission(truckName)) ||
!VerifyOffCooldown(player, truckConfig) ||
!VerifyBelowTruckLimit(player, truckConfig) ||
!VerifyNotInSafeZone(player, basePlayer) ||
!VerifyNotBuildingBlocked(player, basePlayer) ||
!VerifyNotMounted(player, basePlayer) ||
!VerifyNotRecentlyDismounted(player, basePlayer) ||
!VerifyOnGround(player, basePlayer) ||
!VerifyNotParented(player, basePlayer) ||
!VerifyNotRaidOrCombatBlocked(basePlayer) ||
SpawnWasBlocked(basePlayer))
return;
SpawnBombTruck(basePlayer, truckConfig, shouldTrack: true);
}
[Command("givebombtruck")]
private void GiveBombTruckCommand(IPlayer player, string cmd, string[] args)
{
if (!player.IsServer && !VerifyHasPermission(player, PermissionGiveBombTruck))
return;
if (args.Length < 1)
{
ReplyToPlayer(player, "Command.Give.Error.Syntax");
return;
}
var playerNameOrIdArg = args[0];
var truckName = DefaultTruckConfigName;
if (args.Length > 1)
{
truckName = args[1];
}
var targetPlayer = BasePlayer.Find(playerNameOrIdArg);
if (targetPlayer == null)
{
ReplyToPlayer(player, "Command.Give.Error.PlayerNotFound", playerNameOrIdArg);
return;
}
if (!VerifyTruckConfigDefined(player, truckName, out var truckConfig))
return;
SpawnBombTruck(targetPlayer, truckConfig, shouldTrack: false);
}
#endregion
#region Helper Methods - Command Checks
private static bool SpawnWasBlocked(BasePlayer player)
{
return Interface.CallHook("CanSpawnBombTruck", player) is false;
}
private bool VerifyNotRaidOrCombatBlocked(BasePlayer player)
{
if (!_pluginConfig.NoEscapeSettings.CanSpawnWhileRaidBlocked && IsRaidBlocked(player))
{
ChatMessage(player, "Command.Spawn.Error.RaidBlocked");
return false;
}
if (!_pluginConfig.NoEscapeSettings.CanSpawnWhileCombatBlocked && IsCombatBlocked(player))
{
ChatMessage(player, "Command.Spawn.Error.CombatBlocked");
return false;
}
return true;
}
private bool IsRaidBlocked(BasePlayer player)
{
return NoEscape != null && (bool)NoEscape.Call("IsRaidBlocked", player);
}
private bool IsCombatBlocked(BasePlayer player)
{
return NoEscape != null && (bool)NoEscape.Call("IsCombatBlocked", player);
}
private bool VerifyHasPermission(IPlayer player, string perm)
{
if (player.HasPermission(perm))
return true;
ReplyToPlayer(player, "Generic.Error.NoPermission");
return false;
}
private bool VerifyTruckConfigDefined(IPlayer player, string truckName, out TruckConfig truckConfig)
{
truckConfig = GetTruckConfig(truckName);
if (truckConfig == null)
{
ReplyToPlayer(player, "Command.Spawn.Error.NotFound", truckName);
return false;
}
return true;
}
private bool VerifyNotBuildingBlocked(IPlayer player, BasePlayer basePlayer)
{
if (basePlayer.IsBuildingBlocked())
{
ReplyToPlayer(player, "Generic.Error.BuildingBlocked");
return false;
}
return true;
}
private bool VerifyNotInSafeZone(IPlayer player, BasePlayer basePlayer)
{
if (!basePlayer.InSafeZone())
return true;
ReplyToPlayer(player, "Command.Spawn.Error.Generic");
return false;
}
private bool VerifyOffCooldown(IPlayer player, TruckConfig truckConfig)
{
var secondsRemaining = GetPlayerRemainingCooldownSeconds(player.Id, truckConfig);
if (secondsRemaining > 0)
{
ReplyToPlayer(player, "Generic.Error.Cooldown", FormatTime(secondsRemaining));
return false;
}
return true;
}
private bool VerifyNotMounted(IPlayer player, BasePlayer basePlayer)
{
if (basePlayer.isMounted)
{
ReplyToPlayer(player, "Command.Spawn.Error.Mounted");
return false;
}
return true;
}
private bool VerifyNotRecentlyDismounted(IPlayer player, BasePlayer basePlayer)
{
if (_dismountTimeByPlayer.TryGetValue(basePlayer.userID, out var dismountTime) && dismountTime + 0.2f > Time.time)
{
ReplyToPlayer(player, "Command.Spawn.Error.Generic");
return false;
}
return true;
}
private bool VerifyOnGround(IPlayer player, BasePlayer basePlayer)
{
if (!basePlayer.IsOnGround())
{
ReplyToPlayer(player, "Command.Spawn.Error.NotOnGround");
return false;
}
return true;
}
private bool VerifyNotParented(IPlayer player, BasePlayer basePlayer)
{
if (basePlayer.HasParent())
{
ReplyToPlayer(player, "Command.Spawn.Error.Generic");
return false;
}
return true;
}
private bool VerifyBelowTruckLimit(IPlayer player, TruckConfig truckConfig)
{
if (GetPlayerData(player.Id).GetTruckCount(truckConfig.Name) >= truckConfig.SpawnLimit)
{
ReplyToPlayer(player, "Command.Spawn.Error.TooManyOfType", truckConfig.SpawnLimit);
return false;
}
return true;
}
#endregion
#region Helper Methods - Misc
private static void DisableEnginePartDamage(ModularCar car)
{
foreach (var module in car.AttachedModuleEntities)
{
var engineStorage = GetEngineStorage(module);
if (engineStorage == null)
continue;
engineStorage.internalDamageMultiplier = 0;
}
}
private static EngineStorage GetEngineStorage(BaseVehicleModule module)
{
var engineModule = module as VehicleModuleEngine;
if (engineModule == null)
return null;
return engineModule.GetContainer() as EngineStorage;
}
private static string GetSpawnPermission(string truckName)
{
return string.Format(PermissionSpawnFormat, truckName);
}
private static int SortTruckNames(string a, string b)
{
return a.ToLower() == DefaultTruckConfigName
? -1 : b.ToLower() == DefaultTruckConfigName
? 1 : string.Compare(a, b, StringComparison.Ordinal);
}
private static ModularCar GetReceiverCar(RFReceiver receiver)
{
return (receiver.GetParentEntity() as VehicleModuleSeating)?.Vehicle as ModularCar;
}
private static RFReceiver GetBombTruckReceiver(ModularCar car)
{
var driverModule = FindFirstDriverModule(car);
if (driverModule == null)
return null;
return GetChildOfType<RFReceiver>(driverModule);
}
private static T GetChildOfType<T>(BaseEntity entity) where T : BaseEntity
{
foreach (var child in entity.children)
{
var childOfType = child as T;
if (childOfType != null)
return childOfType;
}
return null;
}
private static void RemoveProblemComponents(BaseEntity entity)
{
foreach (var meshCollider in entity.GetComponentsInChildren<MeshCollider>())
{
UnityEngine.Object.DestroyImmediate(meshCollider);
}
UnityEngine.Object.DestroyImmediate(entity.GetComponent<DestroyOnGroundMissing>());
UnityEngine.Object.DestroyImmediate(entity.GetComponent<GroundWatch>());
}
private static VehicleModuleSeating FindFirstDriverModule(ModularCar car)
{
for (var socketIndex = 0; socketIndex < car.TotalSockets; socketIndex++)
{
if (car.TryGetModuleAt(socketIndex, out var module))
{
var seatingModule = module as VehicleModuleSeating;
if (seatingModule != null && seatingModule.HasADriverSeat())
return seatingModule;
}
}
return null;
}
private static bool HasExistingDetonator(ItemContainer container, out int frequency)
{
var hasDetonator = false;
frequency = InvalidFrequency;
for (var slot = 0; slot < container.capacity; slot++)
{
var item = container.GetSlot(slot);
if (item == null || item.info.itemid != DetonatorItemId)
continue;
hasDetonator = true;
frequency = item.instanceData?.dataInt ?? InvalidFrequency;
if (frequency != InvalidFrequency)
{
// Only exit early if the detonator has a valid frequency.
// Otherwise, keep searching for detonators with a valid frequency.
return true;
}
}
return hasDetonator;
}
private static bool HasExistingDetonator(BasePlayer player, out int frequency)
{
var hasDetonator = false;
frequency = -1;
var activeDetonator = player.GetActiveItem();
if (activeDetonator != null && activeDetonator.info.itemid == DetonatorItemId)
{
frequency = activeDetonator.instanceData?.dataInt ?? -1;
hasDetonator = true;
}
// Only exit early if one of the belt detonators had a valid frequency.
// Otherwise, keep searching for a detonator with a valid frequency.
if (hasDetonator && frequency != InvalidFrequency)
return true;
if (HasExistingDetonator(player.inventory.containerBelt, out frequency))
{
hasDetonator = true;
}
if (hasDetonator && frequency != InvalidFrequency)
return true;
if (HasExistingDetonator(player.inventory.containerMain, out frequency))
{
hasDetonator = true;
}
return hasDetonator;
}
private static Item CreateRFTransmitter(int frequency)
{
var detonatorItem = ItemManager.CreateByItemID(DetonatorItemId);
if (detonatorItem == null)
return null;
detonatorItem.instanceData ??= new ProtoBuf.Item.InstanceData { ShouldPool = false };
detonatorItem.instanceData.dataInt = frequency;
var detonator = detonatorItem.GetHeldEntity() as Detonator;
if (detonator != null)
{
detonator.frequency = frequency;
}
return detonatorItem;
}
private static int GenerateRandomFrequency()
{
var frequency = Core.Random.Range(RFManager.minFreq, RFManager.maxFreq);
return frequency is >= RfReservedRangeMin and <= RfReservedRangeMax
? RfReservedRangeMin - 1
: frequency;
}
private static string FormatTime(double seconds)
{
return TimeSpan.FromSeconds(seconds).ToString("g");
}
private bool VerifyDependencies()
{
if (SpawnModularCar == null)
{
LogError("SpawnModularCar is not loaded, get it at https://umod.org");
return false;
}
var requiredVersion = new VersionNumber(5, 0, 1);
if (SpawnModularCar.Version < requiredVersion)
{
LogError($"SpawnModularCar {requiredVersion} or newer is required, get it at https://umod.org");
return false;
}
return true;
}
private void SetupReceiver(RFReceiver receiver)
{
receiver.pickup.enabled = false;
receiver.baseProtection = _immortalProtection;
RemoveProblemComponents(receiver);
}
private void InitializeBombTrucks()
{
foreach (var entity in BaseNetworkable.serverEntities)
{
var car = entity as ModularCar;
if (car == null || !IsBombTruck(car))
continue;
_bombTruckTracker.TrackBombTruck(car);
DisableEnginePartDamage(car);
var receiver = GetBombTruckReceiver(car);
if (receiver != null)
{
_receiverManager.AddReceiver(receiver.GetFrequency(), receiver);
SetupReceiver(receiver);
}
}
}
private bool IsBombTruck(ModularCar car)
{
foreach (var item in _pluginData.PlayerData)
{
foreach (var data in item.Value.BombTrucks)
{
if (data.ID == car.net.ID.Value)
return true;
}
}
return false;
}
private ModularCar SpawnBombTruck(BasePlayer player, TruckConfig truckConfig, bool shouldTrack = false)
{
if (!VerifyDependencies())
return null;
var car = SpawnModularCar.Call("API_SpawnPreset", new Dictionary<string, object>
{
["EnginePartsTier"] = truckConfig.EnginePartsTier,
["FuelAmount"] = -1,
["Modules"] = truckConfig.Modules
}, player) as ModularCar;
if (car == null)
return null;
_bombTruckTracker.TrackBombTruck(car);
if (car.GetFuelSystem() is EntityFuelSystem fuelSystem)
{
using var flagsScope = fuelSystem.GetFuelContainer().StartSetFlags(BaseEntity.FlagsUpdateMode.SendNetworkUpdate_Flags);
flagsScope.Set(BaseEntity.Flags.Locked, true);
}
foreach (var module in car.AttachedModuleEntities)
{
var engineStorage = GetEngineStorage(module);
if (engineStorage != null)
{
engineStorage.inventory.SetLocked(true);
engineStorage.internalDamageMultiplier = 0;
}
}
var message = GetMessage(player.IPlayer, "Command.Spawn.Success");
if (truckConfig.AttachRFReceiver)
{
var hasDetonator = HasExistingDetonator(player, out var frequency);
var receiver = AttachRFReceiver(car, frequency);
if (receiver != null)
{
// Get the current frequency of the RF receiver, in case a new one was generated.
frequency = receiver.GetFrequency();
if (!hasDetonator && permission.UserHasPermission(player.UserIDString, PermissionFreeDetonator))
{
var detonatorItem = CreateRFTransmitter(frequency);
if (detonatorItem != null)
player.GiveItem(detonatorItem);
}
message += " " + GetMessage(player.IPlayer, "Command.Spawn.Success.Frequency", frequency);
}
}
player.IPlayer.Reply(message);
if (shouldTrack)
{
UpdatePlayerCooldown(player.UserIDString, truckConfig.Name);
}
GetPlayerData(player.UserIDString).BombTrucks.Add(new PlayerTruckData
{
Name = truckConfig.Name,
ID = car.net.ID.Value,
Tracked = shouldTrack
});
SaveData();
if (NoEngineParts != null)
{
// Refresh engine stats on next tick to override NoEngineParts.
// This has to be done on the next tick after the bomb truck id has been registered.
NextTick(() =>
{
if (car == null || car.IsDestroyed)
return;
foreach (var module in car.AttachedModuleEntities)
{
var engineStorage = GetEngineStorage(module);
if (engineStorage != null)
{
engineStorage.RefreshLoadoutData();
}
}
});
}
return car;
}
private RFReceiver AttachRFReceiver(ModularCar car, int frequency = -1)
{
var module = FindFirstDriverModule(car);
if (module == null)
return null;
var receiver = GameManager.server.CreateEntity(PrefabRfReceiver, module.transform.TransformPoint(RfReceiverPosition), module.transform.rotation * RfReceiverRotation) as RFReceiver;
if (receiver == null)
return null;
if (frequency == -1)
frequency = GenerateRandomFrequency();
SetupReceiver(receiver);
receiver.Spawn();
receiver.SetParent(module, worldPositionStays: true);
RFManager.ChangeFrequency(receiver.frequency, frequency, receiver, true);
receiver.frequency = frequency;
return receiver;
}
private double GetPlayerRemainingCooldownSeconds(string userID, TruckConfig truckConfig)
{
var playerCooldowns = GetPlayerData(userID).Cooldowns;
if (!playerCooldowns.TryGetValue(truckConfig.Name, out var lastUsed))
return 0;
var cooldownDuration = truckConfig.CooldownSeconds;
var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
return lastUsed + cooldownDuration - currentTime;
}
private void UpdatePlayerCooldown(string userID, string truckName)
{
GetPlayerData(userID).UpdateCooldown(truckName, DateTimeOffset.UtcNow.ToUnixTimeSeconds());
}
#endregion
#region Unity Components
private class BombTruckComponent : FacepunchBehaviour
{
public static BombTruckComponent AddToCar(BombTruckTracker tracker, ModularCar car)
{
var component = car.gameObject.AddComponent<BombTruckComponent>();
component._tracker = tracker;
component.Car = car;
component.NetId = car.net.ID;
component.OwnerId = car.OwnerID;
return component;
}
public ModularCar Car { get; private set; }
public NetworkableId NetId { get; private set; }
public ulong OwnerId { get; private set; }
private BombTruckTracker _tracker;
private void OnDestroy()
{
_tracker.HandleBombTruckDestroyed(this);
}
}
private class BombTruckTracker
{
private BombTrucks _plugin;
private HashSet<Component> _bombTruckComponents = new();
public BombTruckTracker(BombTrucks plugin)
{
_plugin = plugin;
}
public void TrackBombTruck(ModularCar car)
{
_bombTruckComponents.Add(BombTruckComponent.AddToCar(this, car));
}
public void HandleBombTruckDestroyed(BombTruckComponent component)
{
_bombTruckComponents.Remove(component);
if (component.Car == null || component.Car.IsDestroyed)
{
_plugin.GetPlayerData(component.OwnerId.ToString()).RemoveTruck(component.NetId.Value);
_plugin.SaveData();
}
}
public void Unload()
{
foreach (var component in _bombTruckComponents.ToArray())
{
UnityEngine.Object.DestroyImmediate(component);
}
}
}
#endregion
#region RF Receiver Manager
private class RFReceiverManager
{
private BombTrucks _plugin;
private readonly Dictionary<int, List<RFReceiver>> _receivers = new();
public RFReceiverManager(BombTrucks plugin)
{
_plugin = plugin;
}
public void AddReceiver(int frequency, RFReceiver receiver)
{
if (_receivers.TryGetValue(frequency, out var receiverList))
{
receiverList.Add(receiver);
}
else
{
_receivers.Add(frequency, new List<RFReceiver> { receiver });
}
}
public void RemoveReceiver(int frequency, RFReceiver receiver)
{
if (_receivers.TryGetValue(frequency, out var receiverList))
{
receiverList.Remove(receiver);
}
}
public void DetonateFrequency(int frequency)
{
if (_receivers.TryGetValue(frequency, out var receiverList))
{
for (var i = receiverList.Count - 1; i >= 0; i--)
{
var receiver = receiverList[i];
var car = GetReceiverCar(receiver);
if (car != null)
{
_plugin.DetonateBombTruck(car);
}
}
}
}
}
#endregion
#region Explosions
private static void FireRocket(BasePlayer attackerPlayer, ulong attackerId, string rocketPrefab, Vector3 origin, Vector3 direction, float time, float damageRadiusMult = 1.0f, float damageMult = 1.0f)
{
var rocket = GameManager.server.CreateEntity(rocketPrefab, origin) as TimedExplosive;
var rocketProjectile = rocket.GetComponent<ServerProjectile>();
rocketProjectile.gravityModifier = 0;
rocket.explosionRadius *= damageRadiusMult;
rocket.timerAmountMin = time;
rocket.timerAmountMax = time;
for (var i = 0; i < rocket.damageTypes.Count; i++)
{
rocket.damageTypes[i].amount *= damageMult;
}
rocket.creatorEntity = attackerPlayer;
rocket.OwnerID = attackerId;
rocket.SendMessage("InitializeVelocity", direction);
rocket.Spawn();
}
private void DetonateBombTruck(ModularCar car)
{
var playerConfig = GetPlayerData(car.OwnerID.ToString());
var netID = car.net.ID.Value;
var truckName = playerConfig.FindTruck(netID)?.Name;
if (truckName == null)
{
LogError("Unable to determine truck name.");
return;
}
var truckConfig = GetTruckConfig(truckName);
if (truckConfig == null)
{
LogError("Unable to detonate '{0}' truck because its configuration is missing.", truckName);
return;
}
// Remove the engine parts.
foreach (var module in car.AttachedModuleEntities)
{
var engineStorage = GetEngineStorage(module);
if (engineStorage != null)
{
engineStorage.inventory.Kill();
}
}
var attackerPlayer = _pluginConfig.AttributeDamageToBombTruckOwner ? BasePlayer.FindByID(car.OwnerID) : null;
var attackerId = _pluginConfig.AttributeDamageToBombTruckOwner ? car.OwnerID : 0;
var carPosition = car.CenterPoint();
car.Kill();
DetonateExplosion(attackerPlayer, attackerId, truckConfig.ExplosionSpec, carPosition);
}
private void DetonateExplosion(BasePlayer attackerPlayer, ulong attackerId, ExplosionSpec spec, Vector3 origin)
{
ServerMgr.Instance.StartCoroutine(ExplosionCoroutine(attackerPlayer, attackerId, spec, origin));
}
private IEnumerator ExplosionCoroutine(BasePlayer attackerPlayer, ulong attackerId, ExplosionSpec spec, Vector3 origin)
{
var rocketTravelTime = 0.3f;
var totalTime = spec.Radius / spec.Speed;
var numExplosions = (int)Math.Ceiling(spec.DensityCoefficient * Math.Pow(spec.Radius, spec.DensityExponent));
float timeElapsed = 0;
double prevDistance = 0;
FireRocket(attackerPlayer, attackerId, PrefabExplosiveRocket, origin, Vector3.forward, 0, spec.BlastRadiusMult, spec.DamageMult);
for (var i = 1; i <= numExplosions; i++)
{
if (_pluginUnloaded)
yield break;
var timeFraction = timeElapsed / totalTime;
var stepDistance = spec.Radius * timeFraction;
var stepStartDistance = prevDistance;
var stepEndDistance = stepDistance;
var rocketDistance = Core.Random.Range(stepStartDistance, stepEndDistance);
var rocketSpeed = rocketDistance / rocketTravelTime;
var rocketVector = MakeRandomDomeVector();
// Skip over some space to reduce the frequency of rockets colliding with each other.
var skipDistance = rocketVector;
rocketVector *= Convert.ToSingle(rocketSpeed);
FireRocket(attackerPlayer, attackerId, PrefabExplosiveRocket, origin + skipDistance, rocketVector + skipDistance, rocketTravelTime, spec.BlastRadiusMult, spec.DamageMult);
var timeToNext = Convert.ToSingle(Math.Pow(i / spec.DensityCoefficient, 1.0 / spec.DensityExponent) / spec.Speed - timeElapsed);
yield return CoroutineEx.waitForSeconds(timeToNext);
prevDistance = stepDistance;
timeElapsed += timeToNext;
}
}
private Vector3 MakeRandomDomeVector()
{
return new Vector3(Core.Random.Range(-1f, 1f), Core.Random.Range(0, 1f), Core.Random.Range(-1f, 1f)).normalized;
}
#endregion
#region Data Management
private PlayerData GetPlayerData(string userID)
{
if (!_pluginData.PlayerData.ContainsKey(userID))
{
_pluginData.PlayerData.Add(userID, new PlayerData());
}
return _pluginData.PlayerData[userID];
}
private void CleanStaleTruckData()
{
var cleanedCount = 0;
// Clean up any stale truck IDs in case of a data file desync.
foreach (var playerData in _pluginData.PlayerData.Values)
{
cleanedCount += playerData.BombTrucks.RemoveAll(truckData =>
BaseNetworkable.serverEntities.Find(new NetworkableId(truckData.ID)) as ModularCar == null);
}
if (cleanedCount > 0)
{
SaveData();
}
}
private void SaveData()
{
Interface.Oxide.DataFileSystem.WriteObject(Name, _pluginData);
}
private void ClearData()
{
Interface.Oxide.DataFileSystem.WriteObject(Name, new StoredData());
}
private class StoredData
{
[JsonProperty("PlayerData")]
public Dictionary<string, PlayerData> PlayerData = new();
}
private class PlayerData
{
[JsonProperty("BombTrucks")]
public List<PlayerTruckData> BombTrucks = new();
[JsonProperty("Cooldowns")]
public Dictionary<string, long> Cooldowns = new();
public void UpdateCooldown(string truckName, long time)
{
Cooldowns[truckName] = time;
}
public int GetTruckCount(string truckName)
{
return BombTrucks.Count(truckData => truckData.Tracked && truckData.Name == truckName);
}
public PlayerTruckData FindTruck(ulong netID)
{
return BombTrucks.FirstOrDefault(truckData => truckData.ID == netID);
}
public void RemoveTruck(ulong netID)
{
BombTrucks.RemoveAll(truckData => truckData.ID == netID);
}
}
private class PlayerTruckData
{
[JsonProperty("ID")]
public ulong ID;
[JsonProperty("Name")]
public string Name;
[JsonProperty("Tracked")]
public bool Tracked = true;
}
#endregion
#region Configuration
private TruckConfig GetTruckConfig(string truckName)
{
return _pluginConfig.BombTrucks.FirstOrDefault(truckConfig =>
string.Equals(truckConfig.Name, truckName, StringComparison.CurrentCultureIgnoreCase));
}
private class Configuration : BaseConfiguration
{
[JsonProperty("BombTrucks")]
public TruckConfig[] BombTrucks = Array.Empty<TruckConfig>();
[JsonProperty("AttributeDamageToBombTruckOwner")]
public bool AttributeDamageToBombTruckOwner;
[JsonProperty("NoEscapeSettings")]
public NoEscapeSettings NoEscapeSettings = new();
}
private class TruckConfig
{
[JsonProperty("Name")]
public string Name;
[JsonProperty("CooldownSeconds")]
public long CooldownSeconds;
[JsonProperty("SpawnLimitPerPlayer")]
public int SpawnLimit = 1;
[JsonProperty("AttachRFReceiver")]
public bool AttachRFReceiver = true;
private int _enginePartsTier = 1;
[JsonProperty("EnginePartsTier")]
public int EnginePartsTier
{
get => _enginePartsTier;
set => _enginePartsTier = Math.Min(Math.Max(value, 1), 3);
}
[JsonProperty("Modules")]
public object[] Modules =
{
"vehicle.1mod.cockpit.with.engine",
"vehicle.2mod.fuel.tank"
};
[JsonProperty("ExplosionSettings")]
public ExplosionSpec ExplosionSpec = new();
}
private Configuration GetDefaultConfig()
{
return new Configuration
{
BombTrucks = new[]
{
new TruckConfig
{
Name = DefaultTruckConfigName,
EnginePartsTier = 3,
CooldownSeconds = 3600,
SpawnLimit = 3,
ExplosionSpec = new ExplosionSpec
{
BlastRadiusMult = 1,
DamageMult = 4.0f,
DensityCoefficient = 1,
DensityExponent = Math.Round(1.8f * 100) / 100,
Radius = 5,
Speed = 10,
}
},
new TruckConfig
{
Name = "Nuke",
EnginePartsTier = 1,
CooldownSeconds = 10800,
SpawnLimit = 1,
Modules = new object[]
{
"vehicle.1mod.engine",
"vehicle.1mod.cockpit.armored",
"vehicle.2mod.fuel.tank"
},
ExplosionSpec = new ExplosionSpec
{
BlastRadiusMult = 1,
DamageMult = 6.0f,
DensityCoefficient = 1,
DensityExponent = Math.Round(1.6f * 100) / 100,
Radius = 15,
Speed = 10,
}
}
}
};
}
private class ExplosionSpec
{
private double _speed = 10;
private double _densityCoefficient = 1;
private double _densityExponent = 2;
[JsonProperty("Radius")]
public double Radius = 10;
[JsonProperty("DensityCoefficient")]
public double DensityCoefficient
{
get => _densityCoefficient;
set => _densityCoefficient = Math.Max(value, 0.01);
}
[JsonProperty("DensityExponent")]
public double DensityExponent
{
get => _densityExponent;
set => _densityExponent = Math.Min(Math.Max(value, 1), 3);
}
[JsonProperty("Speed")]
public double Speed
{
get => _speed;
set => _speed = Math.Max(value, 0.1);
}
[JsonProperty("BlastRadiusMult")]
public float BlastRadiusMult = 1;
[JsonProperty("DamageMult")]
public float DamageMult = 1;
}
private class NoEscapeSettings
{
[JsonProperty("CanSpawnWhileRaidBlocked")]
public bool CanSpawnWhileRaidBlocked = true;
[JsonProperty("CanSpawnWhileCombatBlocked")]
public bool CanSpawnWhileCombatBlocked = true;
}
#endregion
#region Configuration Helpers
private class BaseConfiguration
{
public string ToJson()
{
return JsonConvert.SerializeObject(this);
}
public Dictionary<string, object> ToDictionary()
{
return JsonHelper.Deserialize(ToJson()) as Dictionary<string, object>;
}
}
private static class JsonHelper
{
public static object Deserialize(string json)
{
return ToObject(JToken.Parse(json));
}
private static object ToObject(JToken token)
{
switch (token.Type)
{
case JTokenType.Object:
return token.Children<JProperty>()
.ToDictionary(prop => prop.Name,
prop => ToObject(prop.Value));
case JTokenType.Array:
return token.Select(ToObject).ToList();
default:
return ((JValue)token).Value;
}
}
}
private bool MaybeUpdateConfig(BaseConfiguration config)
{
var currentWithDefaults = config.ToDictionary();
var currentRaw = Config.ToDictionary(x => x.Key, x => x.Value);
return MaybeUpdateConfigDict(currentWithDefaults, currentRaw);
}
private bool MaybeUpdateConfigDict(Dictionary<string, ob
// ... [Code preview truncated due to length. Full source in attached file] ...