C#:
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Core.Libraries.Covalence;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Drone Boombox", "WhiteThunder", "1.0.3")]
[Description("Allows players to deploy boomboxes onto RC drones.")]
internal class DroneBoombox : CovalencePlugin
{
#region Fields
[PluginReference]
private Plugin DroneSettings;
private static DroneBoombox _pluginInstance;
private Configuration _pluginConfig;
private const string PermissionDeploy = "droneboombox.deploy";
private const string PermissionDeployFree = "droneboombox.deploy.free";
private const string PermissionAutoDeploy = "droneboombox.autodeploy";
private const string BoomboxPrefab = "assets/prefabs/voiceaudio/boombox/boombox.static.prefab";
private const string DeployEffectPrefab = "assets/prefabs/voiceaudio/boombox/effects/boombox-deploy.prefab";
private const int PortableBoomboxItemId = 576509618;
private const int DeployableBoomboxItemId = -1113501606;
private const BaseEntity.Slot BoomboxSlot = BaseEntity.Slot.UpperModifier;
private static readonly Vector3 BoomboxLocalPosition = new Vector3(0, 0.21f, 0.17f);
private static readonly Quaternion BoomboxLocalRotation = Quaternion.Euler(270, 0, 0);
// Subscribe to these hooks while there are boombox drones.
private DynamicHookSubscriber<NetworkableId> _boomboxDroneTracker = new DynamicHookSubscriber<NetworkableId>(
nameof(OnEntityKill),
nameof(OnEntityTakeDamage),
nameof(CanPickupEntity),
nameof(canRemove)
);
#endregion
#region Hooks
private void Init()
{
_pluginInstance = this;
permission.RegisterPermission(PermissionDeploy, this);
permission.RegisterPermission(PermissionDeployFree, this);
permission.RegisterPermission(PermissionAutoDeploy, this);
_boomboxDroneTracker.UnsubscribeAll();
}
private void Unload()
{
_pluginInstance = null;
}
private void OnServerInitialized()
{
foreach (var entity in BaseNetworkable.serverEntities)
{
var drone = entity as Drone;
if (drone == null || !IsDroneEligible(drone))
continue;
var droneBoombox = GetDroneBoombox(drone);
if (droneBoombox == null)
continue;
SetupDroneBoombox(drone, droneBoombox);
}
}
private void OnEntityBuilt(Planner planner, GameObject go)
{
if (planner == null || go == null)
return;
var drone = go.ToBaseEntity() as Drone;
if (drone == null)
return;
var player = planner.GetOwnerPlayer();
if (player == null)
return;
NextTick(() =>
{
// Delay this check to allow time for other plugins to deploy an entity to this slot.
if (drone == null || player == null || drone.GetSlot(BoomboxSlot) != null)
return;
if (permission.UserHasPermission(player.UserIDString, PermissionAutoDeploy))
{
DeployBoombox(drone, player);
}
else if (permission.UserHasPermission(player.UserIDString, PermissionDeploy)
&& UnityEngine.Random.Range(0, 100) < _pluginConfig.TipChance)
{
ChatMessage(player, Lang.TipDeployCommand);
}
});
}
private void OnEntityKill(Drone drone)
{
_boomboxDroneTracker.Remove(drone.net.ID);
}
private void OnEntityKill(DeployableBoomBox boombox)
{
var drone = GetParentDrone(boombox);
if (drone == null)
return;
_boomboxDroneTracker.Remove(drone.net.ID);
drone.Invoke(() => RefreshDroneSettingsProfile(drone), 0);
}
// Redirect damage from the boombox to the drone.
private bool? OnEntityTakeDamage(DeployableBoomBox boombox, HitInfo info)
{
var drone = GetParentDrone(boombox);
if (drone == null)
return null;
drone.Hurt(info);
return true;
}
private bool? CanPickupEntity(BasePlayer player, Drone drone)
{
if (CanPickupInternal(player, drone))
return null;
ChatMessage(player, Lang.ErrorCannotPickupWithCassette);
return false;
}
// This hook is exposed by plugin: Remover Tool (RemoverTool).
private string canRemove(BasePlayer player, Drone drone)
{
if (CanPickupInternal(player, drone))
return null;
return GetMessage(player, Lang.ErrorCannotPickupWithCassette);
}
// This hook is exposed by plugin: Drone Settings (DroneSettings).
private string OnDroneTypeDetermine(Drone drone)
{
return GetDroneBoombox(drone) != null ? Name : null;
}
#endregion
#region API
private DeployableBoomBox API_DeployBoombox(Drone drone, BasePlayer player)
{
if (GetDroneBoombox(drone) != null
|| drone.GetSlot(BoomboxSlot) != null
|| DeployBoomboxWasBlocked(drone, player))
return null;
return DeployBoombox(drone, player);
}
#endregion
#region Commands
[Command("droneboombox", "dronebb")]
private void DroneBoomboxCommand(IPlayer player, string cmd, string[] args)
{
if (player.IsServer)
return;
var basePlayer = player.Object as BasePlayer;
if (!basePlayer.CanInteract())
return;
Drone drone;
if (!VerifyPermission(player, PermissionDeploy)
|| !VerifyDroneFound(player, out drone)
|| !VerifyCanBuild(player, drone)
|| !VerifyDroneHasNoBoombox(player, drone)
|| !VerifyDroneHasSlotVacant(player, drone))
return;
Item boomboxPaymentItem = null;
if (!player.HasPermission(PermissionDeployFree))
{
boomboxPaymentItem = FindPlayerBoomboxItem(basePlayer);
if (boomboxPaymentItem == null)
{
ReplyToPlayer(player, Lang.ErrorNoBoomboxItem);
return;
}
}
if (DeployBoomboxWasBlocked(drone, basePlayer))
return;
if (DeployBoombox(drone, basePlayer) == null)
{
ReplyToPlayer(player, Lang.ErrorDeployFailed);
return;
}
if (boomboxPaymentItem != null)
UseItem(basePlayer, boomboxPaymentItem);
}
#endregion
#region Helper Methods - Command Checks
private bool VerifyPermission(IPlayer player, string perm)
{
if (player.HasPermission(perm))
return true;
ReplyToPlayer(player, Lang.ErrorNoPermission);
return false;
}
private bool VerifyDroneFound(IPlayer player, out Drone drone)
{
var basePlayer = player.Object as BasePlayer;
drone = GetLookEntity(basePlayer, 3) as Drone;
if (drone != null && IsDroneEligible(drone))
return true;
ReplyToPlayer(player, Lang.ErrorNoDroneFound);
return false;
}
private bool VerifyCanBuild(IPlayer player, Drone drone)
{
var basePlayer = player.Object as BasePlayer;
if (basePlayer.CanBuild() && basePlayer.CanBuild(drone.WorldSpaceBounds()))
return true;
ReplyToPlayer(player, Lang.ErrorBuildingBlocked);
return false;
}
private bool VerifyDroneHasNoBoombox(IPlayer player, Drone drone)
{
if (GetDroneBoombox(drone) == null)
return true;
ReplyToPlayer(player, Lang.ErrorAlreadyHasBoombox);
return false;
}
private bool VerifyDroneHasSlotVacant(IPlayer player, Drone drone)
{
if (drone.GetSlot(BoomboxSlot) == null)
return true;
ReplyToPlayer(player, Lang.ErrorIncompatibleAttachment);
return false;
}
#endregion
#region Helper Method
private static bool DeployBoomboxWasBlocked(Drone drone, BasePlayer deployer)
{
object hookResult = Interface.CallHook("OnDroneBoomboxDeploy", drone, deployer);
return hookResult is bool && (bool)hookResult == false;
}
private static bool IsDroneEligible(Drone drone) =>
drone.skinID == 0 && !(drone is DeliveryDrone);
private static BaseEntity GetLookEntity(BasePlayer basePlayer, float maxDistance = 3)
{
RaycastHit hit;
return Physics.Raycast(basePlayer.eyes.HeadRay(), out hit, maxDistance, Physics.DefaultRaycastLayers, QueryTriggerInteraction.Ignore)
? hit.GetEntity()
: null;
}
private static Drone GetParentDrone(BaseEntity entity) =>
entity.GetParentEntity() as Drone;
private static DeployableBoomBox GetDroneBoombox(Drone drone) =>
drone.GetSlot(BoomboxSlot) as DeployableBoomBox;
private static bool CanPickupInternal(BasePlayer player, Drone drone)
{
if (!IsDroneEligible(drone))
return true;
var boombox = GetDroneBoombox(drone);
if (boombox == null)
return true;
// Prevent drone pickup while it has a boombox with a cassette in it (the cassette must be removed first).
if (boombox != null && !boombox.inventory.IsEmpty() && !boombox.inventory.IsLocked())
return false;
return true;
}
private static void RunOnEntityBuilt(Item boomboxItem, DeployableBoomBox boombox) =>
Interface.CallHook("OnEntityBuilt", boomboxItem.GetHeldEntity(), boombox.gameObject);
private static void UseItem(BasePlayer basePlayer, Item item, int amountToConsume = 1)
{
item.UseItem(amountToConsume);
basePlayer.Command("note.inv", item.info.itemid, -amountToConsume);
}
private static Item FindPlayerBoomboxItem(BasePlayer basePlayer) =>
basePlayer.inventory.FindItemByItemID(PortableBoomboxItemId) ??
basePlayer.inventory.FindItemByItemID(DeployableBoomboxItemId);
private void RefreshDroneSettingsProfile(Drone drone)
{
_pluginInstance.DroneSettings?.Call("API_RefreshDroneProfile", drone);
}
private void SetupDroneBoombox(Drone drone, DeployableBoomBox boombox)
{
// Damage will be processed by the drone.
boombox.baseProtection = null;
// These boomboxes should not decay while playing, since that would damage the drone.
// Default is 0.025 for the static boomboxes, even though it's 0 for other deployable boomboxes.
boombox.BoxController.ConditionLossRate = 0;
RefreshDroneSettingsProfile(drone);
_boomboxDroneTracker.Add(drone.net.ID);
}
private DeployableBoomBox DeployBoombox(Drone drone, BasePlayer basePlayer)
{
var boombox = GameManager.server.CreateEntity(BoomboxPrefab, BoomboxLocalPosition, BoomboxLocalRotation) as DeployableBoomBox;
if (boombox == null)
return null;
if (basePlayer != null)
boombox.OwnerID = basePlayer.userID;
boombox.SetParent(drone);
boombox.Spawn();
drone.SetSlot(BoomboxSlot, boombox);
SetupDroneBoombox(drone, boombox);
Effect.server.Run(DeployEffectPrefab, boombox.transform.position);
Interface.CallHook("OnDroneBoomboxDeployed", drone, boombox, basePlayer);
if (basePlayer == null)
return boombox;
// Allow other plugins to detect the boombox being deployed (e.g., to set a default station).
var boomboxItem = FindPlayerBoomboxItem(basePlayer);
if (boomboxItem != null)
{
RunOnEntityBuilt(boomboxItem, boombox);
}
else
{
// Temporarily increase the player inventory capacity to ensure there is enough space.
basePlayer.inventory.containerMain.capacity++;
var temporaryItem = ItemManager.CreateByItemID(DeployableBoomboxItemId);
if (basePlayer.inventory.GiveItem(temporaryItem))
{
RunOnEntityBuilt(temporaryItem, boombox);
temporaryItem.RemoveFromContainer();
}
temporaryItem.Remove();
basePlayer.inventory.containerMain.capacity--;
}
return boombox;
}
#endregion
#region Dynamic Hook Subscriptions
private class DynamicHookSubscriber<T>
{
private HashSet<T> _list = new HashSet<T>();
private string[] _hookNames;
public DynamicHookSubscriber(params string[] hookNames)
{
_hookNames = hookNames;
}
public void Add(T item)
{
if (_list.Add(item) && _list.Count == 1)
SubscribeAll();
}
public void Remove(T item)
{
if (_list.Remove(item) && _list.Count == 0)
UnsubscribeAll();
}
public void SubscribeAll()
{
foreach (var hookName in _hookNames)
_pluginInstance.Subscribe(hookName);
}
public void UnsubscribeAll()
{
foreach (var hookName in _hookNames)
_pluginInstance.Unsubscribe(hookName);
}
}
#endregion
#region Configuration
private class Configuration : SerializableConfiguration
{
[JsonProperty("TipChance")]
public int TipChance = 25;
}
private Configuration GetDefaultConfig() => new Configuration();
#endregion
#region Configuration Boilerplate
private class SerializableConfiguration
{
public string ToJson() => JsonConvert.SerializeObject(this);
public Dictionary<string, object> ToDictionary() => JsonHelper.Deserialize(ToJson()) as Dictionary<string, object>;
}
private static class JsonHelper
{
public static object Deserialize(string json) => 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(SerializableConfiguration config)
{
var currentWithDefaults = config.ToDictionary();
var currentRaw = Config.ToDictionary(x => x.Key, x => x.Value);
return MaybeUpdateConfigDict(currentWithDefaults, currentRaw);
}
private bool MaybeUpdateConfigDict(Dictionary<string, object> currentWithDefaults, Dictionary<string, object> currentRaw)
{
bool changed = false;
foreach (var key in currentWithDefaults.Keys)
{
object currentRawValue;
if (currentRaw.TryGetValue(key, out currentRawValue))
{
var defaultDictValue = currentWithDefaults[key] as Dictionary<string, object>;
var currentDictValue = currentRawValue as Dictionary<string, object>;
if (defaultDictValue != null)
{
if (currentDictValue == null)
{
currentRaw[key] = currentWithDefaults[key];
changed = true;
}
else if (MaybeUpdateConfigDict(defaultDictValue, currentDictValue))
changed = true;
}
}
else
{
currentRaw[key] = currentWithDefaults[key];
changed = true;
}
}
return changed;
}
protected override void LoadDefaultConfig() => _pluginConfig = GetDefaultConfig();
protected override void LoadConfig()
{
base.LoadConfig();
try
{
_pluginConfig = Config.ReadObject<Configuration>();
if (_pluginConfig == null)
{
throw new JsonException();
}
if (MaybeUpdateConfig(_pluginConfig))
{
LogWarning("Configuration appears to be outdated; updating and saving");
SaveConfig();
}
}
catch (Exception e)
{
LogError(e.Message);
LogWarning($"Configuration file {Name}.json is invalid; using defaults");
LoadDefaultConfig();
}
}
protected override void SaveConfig()
{
Log($"Configuration changes saved to {Name}.json");
Config.WriteObject(_pluginConfig, true);
}
#endregion
#region Localization
private void ReplyToPlayer(IPlayer player, string messageName, params object[] args) =>
player.Reply(string.Format(GetMessage(player, messageName), args));
private void ChatMessage(BasePlayer player, string messageName, params object[] args) =>
player.ChatMessage(string.Format(GetMessage(player.IPlayer, messageName), args));
private string GetMessage(IPlayer player, string messageName, params object[] args) =>
GetMessage(player.Id, messageName, args);
private string GetMessage(BasePlayer player, string messageName, params object[] args) =>
GetMessage(player.UserIDString, messageName, args);
private string GetMessage(string playerId, string messageName, params object[] args)
{
var message = lang.GetMessage(messageName, this, playerId);
return args.Length > 0 ? string.Format(message, args) : message;
}
private class Lang
{
public const string TipDeployCommand = "Tip.DeployCommand";
public const string ErrorNoPermission = "Error.NoPermission";
public const string ErrorNoDroneFound = "Error.NoDroneFound";
public const string ErrorBuildingBlocked = "Error.BuildingBlocked";
public const string ErrorNoBoomboxItem = "Error.NoBoomboxItem";
public const string ErrorAlreadyHasBoombox = "Error.AlreadyHasBoombox";
public const string ErrorIncompatibleAttachment = "Error.IncompatibleAttachment";
public const string ErrorDeployFailed = "Error.DeployFailed";
public const string ErrorCannotPickupWithCassette = "Error.CannotPickupWithCassette";
}
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
[Lang.TipDeployCommand] = "Tip: Look at the drone and run <color=yellow>/droneboombox</color> to deploy a boombox.",
[Lang.ErrorNoPermission] = "You don't have permission to do that.",
[Lang.ErrorNoDroneFound] = "Error: No drone found.",
[Lang.ErrorBuildingBlocked] = "Error: Cannot do that while building blocked.",
[Lang.ErrorNoBoomboxItem] = "Error: You need a boombox to do that.",
[Lang.ErrorAlreadyHasBoombox] = "Error: That drone already has a boombox.",
[Lang.ErrorIncompatibleAttachment] = "Error: That drone has an incompatible attachment.",
[Lang.ErrorDeployFailed] = "Error: Failed to deploy boombox.",
[Lang.ErrorCannotPickupWithCassette] = "Cannot pick up that drone while its boombox contains a cassette.",
}, this, "en");
}
#endregion
}
}