C#:
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Core.Libraries.Covalence;
using Oxide.Game.Rust.Cui;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Drone Storage", "WhiteThunder", "1.3.4")]
[Description("Allows players to deploy a small stash to RC drones.")]
internal class DroneStorage : CovalencePlugin
{
#region Fields
[PluginReference]
private readonly Plugin DroneSettings;
private Configuration _config;
private const string PermissionDeploy = "dronestorage.deploy";
private const string PermissionDeployFree = "dronestorage.deploy.free";
private const string PermissionAutoDeploy = "dronestorage.autodeploy";
private const string PermissionLockable = "dronestorage.lockable";
private const string PermissionViewItems = "dronestorage.viewitems";
private const string PermissionDropItems = "dronestorage.dropitems";
private const string PermissionToggleLock = "dronestorage.togglelock";
private const string PermissionCapacityPrefix = "dronestorage.capacity";
// HAB storage is the best since it has an accurate collider, decent rendering distance and is a StorageContainer.
private const string StoragePrefab = "assets/prefabs/deployable/hot air balloon/subents/hab_storage.prefab";
private const string StorageDeployEffectPrefab = "assets/prefabs/deployable/small stash/effects/small-stash-deploy.prefab";
private const string DropBagPrefab = "assets/prefabs/misc/item drop/item_drop.prefab";
private const string LockEffectPrefab = "assets/prefabs/locks/keypad/effects/lock.code.lock.prefab";
private const string UnlockEffectPrefab = "assets/prefabs/locks/keypad/effects/lock.code.unlock.prefab";
private const int StashItemId = -369760990;
private const BaseEntity.Slot StorageSlot = BaseEntity.Slot.UpperModifier;
private const string ResizableLootPanelName = "generic_resizable";
private static readonly Vector3 StorageLockPosition = new Vector3(0, 0, 0.21f);
private static readonly Quaternion StorageLockRotation = Quaternion.Euler(0, 90, 0);
private static readonly Vector3 StorageLocalPosition = new Vector3(0, 0.12f, 0);
private static readonly Quaternion StorageLocalRotation = Quaternion.Euler(-90, 0, 0);
private static readonly Vector3 StorageDropForwardLocation = new Vector3(0, 0, 0.7f);
private static readonly Quaternion StorageDropRotation = Quaternion.Euler(90, 0, 0);
private readonly object True = true;
private readonly object False = false;
private readonly Func<BasePlayer, Item, int, bool> StashItemFilter;
private readonly Dictionary<string, object> _removeInfo = new Dictionary<string, object>();
private readonly Dictionary<string, object> _refundInfo = new Dictionary<string, object>
{
["stash.small"] = new Dictionary<string, object>
{
["Amount"] = 1,
},
};
private readonly object[] NoteInvTakeOneArguments = { StashItemId, -1 };
private readonly StorageCapacityManager _storageCapacityManager;
private readonly DynamicHookHashSet<StorageContainer> _droneStorageTracker;
private readonly DynamicHookHashSet<ulong> _remoteStashViewerTracker;
private readonly HashSet<BasePlayer> _uiViewers = new HashSet<BasePlayer>();
public DroneStorage()
{
StashItemFilter = CanStashAcceptItem;
_storageCapacityManager = new StorageCapacityManager(this);
_droneStorageTracker = new DynamicHookHashSet<StorageContainer>(this,
nameof(OnEntityDeath),
nameof(OnEntityTakeDamage),
nameof(OnBookmarkControlStarted),
nameof(OnBookmarkControlEnded),
nameof(CanPickupEntity),
nameof(OnItemDeployed)
);
_remoteStashViewerTracker = new DynamicHookHashSet<ulong>(this,
nameof(CanMoveItem),
nameof(OnItemAction)
);
}
#endregion
#region Hooks
private void Init()
{
permission.RegisterPermission(PermissionDeploy, this);
permission.RegisterPermission(PermissionDeployFree, this);
permission.RegisterPermission(PermissionAutoDeploy, this);
permission.RegisterPermission(PermissionLockable, this);
permission.RegisterPermission(PermissionViewItems, this);
permission.RegisterPermission(PermissionDropItems, this);
permission.RegisterPermission(PermissionToggleLock, this);
_storageCapacityManager.Init();
_droneStorageTracker.Unsubscribe();
_remoteStashViewerTracker.Unsubscribe();
}
private void Unload()
{
foreach (var player in _uiViewers)
{
UI.DestroyForPlayer(player);
}
foreach (var entity in BaseNetworkable.serverEntities)
{
var drone = entity as Drone;
if (drone == null || !IsDroneEligible(drone))
continue;
var stash = GetDroneStorage(drone);
if (stash == null)
continue;
DroneStorageComponent.RemoveFromStorage(stash);
}
}
private void OnServerInitialized()
{
foreach (var drone in BaseNetworkable.serverEntities.OfType<Drone>().ToArray())
{
if (!IsDroneEligible(drone))
continue;
RefreshStorage(drone);
}
foreach (var player in BasePlayer.activePlayerList)
{
ComputerStation computerStation;
var drone = RCUtils.GetControlledEntity<Drone>(player, out computerStation);
if (drone == null)
continue;
var storage = GetDroneStorage(drone);
if (storage == null)
continue;
if (storage.inventory != null && storage.inventory == player.inventory?.loot?.containers?.FirstOrDefault())
{
_remoteStashViewerTracker.Add(player.userID);
}
OnBookmarkControlStarted(computerStation, player, string.Empty, drone);
}
}
private void OnEntityBuilt(Planner planner, GameObject go)
{
if (planner == null || go == null)
return;
var drone = go.ToBaseEntity() as Drone;
if (drone == null || !IsDroneEligible(drone))
return;
var player = planner.GetOwnerPlayer();
if (player == null)
return;
var drone2 = drone;
var player2 = player;
NextTick(() =>
{
// Delay this check to allow time for other plugins to deploy an entity to this slot.
if (drone2 == null || player2 == null || drone2.GetSlot(StorageSlot) != null)
return;
var capacity = _storageCapacityManager.DetermineCapacityForUser(drone2.OwnerID);
if (capacity <= 0)
return;
if (permission.UserHasPermission(player2.UserIDString, PermissionAutoDeploy))
{
TryDeployStorage(drone2, capacity);
}
else if (permission.UserHasPermission(player2.UserIDString, PermissionDeploy)
&& UnityEngine.Random.Range(0, 100) < _config.TipChance)
{
ChatMessage(player2, Lang.TipDeployCommand);
}
});
}
private void OnEntityDeath(Drone drone)
{
var storage = GetDroneStorage(drone);
if (storage == null)
return;
DropItems(drone, storage);
}
private object OnEntityTakeDamage(StorageContainer storage, HitInfo info)
{
Drone drone;
if (!IsDroneStorage(storage, out drone))
return null;
drone.Hurt(info);
return True;
}
private void OnBookmarkControlStarted(ComputerStation computerStation, BasePlayer player, string bookmarkName, Drone drone)
{
if (!RCUtils.HasController(drone, player))
return;
if (_uiViewers.Contains(player))
return;
var storage = GetDroneStorage(drone);
if (storage == null)
return;
UI.CreateForPlayer(this, player, storage);
_uiViewers.Add(player);
}
private void OnBookmarkControlEnded(ComputerStation station, BasePlayer player, Drone drone)
{
if (!_uiViewers.Contains(player))
return;
UI.DestroyForPlayer(player);
_uiViewers.Remove(player);
if (_remoteStashViewerTracker.Contains(player.userID))
{
player.EndLooting();
}
}
private object CanPickupEntity(BasePlayer player, Drone drone)
{
if (CanPickupInternal(player, drone))
return null;
ChatMessage(player, Lang.ErrorCannotPickupDroneWithItems);
return False;
}
private void OnItemDeployed(Deployer deployer, StorageContainer storage, BaseLock baseLock)
{
if (!IsDroneStorage(storage))
return;
baseLock.transform.localPosition = StorageLockPosition;
baseLock.transform.localRotation = StorageLockRotation;
baseLock.SendNetworkUpdateImmediate();
}
// Prevent the drone controller from moving items while remotely viewing a drone stash.
private object CanMoveItem(Item item, PlayerInventory playerInventory)
{
if (item.parent == null)
return null;
var player = playerInventory.baseEntity;
if (player == null)
return null;
var drone = RCUtils.GetControlledEntity<Drone>(player);
if (drone == null)
return null;
// For simplicity, block all item moves while the player is looting a drone stash.
var storage = playerInventory.loot.entitySource as StorageContainer;
if (storage != null && IsDroneStorage(storage))
return False;
return null;
}
// Prevent the drone controller from dropping items (or any item action) while remotely viewing a drone stash.
private object OnItemAction(Item item, string text, BasePlayer player)
{
var drone = RCUtils.GetControlledEntity<Drone>(player);
if (drone == null)
return null;
var storage = GetDroneStorage(drone);
if (storage != null && storage == player.inventory.loot.entitySource)
return False;
return null;
}
// This hook is exposed by plugin: Remover Tool (RemoverTool).
private Dictionary<string, object> OnRemovableEntityInfo(StorageContainer storage, BasePlayer player)
{
if (!IsDroneStorage(storage))
return null;
_removeInfo["DisplayName"] = GetMessage(player, Lang.InfoStashName);
if (storage.pickup.enabled)
{
_removeInfo["Refund"] = _refundInfo;
}
else
{
_removeInfo.Remove("Refund");
}
return _removeInfo;
}
// 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.ErrorCannotPickupDroneWithItems);
}
// This hook is exposed by plugin: Drone Settings (DroneSettings).
private string OnDroneTypeDetermine(Drone drone)
{
return GetDroneStorage(drone) != null ? Name : null;
}
#endregion
#region Commands
[Command("dronestash")]
private void DroneStashCommand(IPlayer player)
{
if (player.IsServer)
return;
if (!player.HasPermission(PermissionDeploy))
{
ReplyToPlayer(player, Lang.ErrorNoPermission);
return;
}
var basePlayer = player.Object as BasePlayer;
var drone = GetLookEntity(basePlayer, 3) as Drone;
if (drone == null || !IsDroneEligible(drone))
{
ReplyToPlayer(player, Lang.ErrorNoDroneFound);
return;
}
if (!basePlayer.CanBuild() || !basePlayer.CanBuild(drone.WorldSpaceBounds()))
{
ReplyToPlayer(player, Lang.ErrorBuildingBlocked);
return;
}
var allowedCapacity = _storageCapacityManager.DetermineCapacityForUser(basePlayer.userID);
if (allowedCapacity <= 0)
{
ReplyToPlayer(player, Lang.ErrorNoPermission);
return;
}
if (GetDroneStorage(drone) != null)
{
ReplyToPlayer(player, Lang.ErrorAlreadyHasStorage);
return;
}
if (drone.GetSlot(StorageSlot) != null)
{
ReplyToPlayer(player, Lang.ErrorIncompatibleAttachment);
return;
}
var isFree = player.HasPermission(PermissionDeployFree);
if (!isFree && basePlayer.inventory.FindItemByItemID(StashItemId) == null)
{
ReplyToPlayer(player, Lang.ErrorNoStashItem);
return;
}
if (TryDeployStorage(drone, allowedCapacity, allowRefund: !isFree, deployer: basePlayer) == null)
{
ReplyToPlayer(player, Lang.ErrorDeployFailed);
}
else if (!isFree)
{
basePlayer.inventory.Take(null, StashItemId, 1);
basePlayer.Command("note.inv", NoteInvTakeOneArguments);
}
}
[Command("dronestorage.ui.viewitems")]
private void UICommandViewItems(IPlayer player)
{
BasePlayer basePlayer;
Drone drone;
StorageContainer storage;
if (!TryGetControlledStorage(player, PermissionViewItems, out basePlayer, out drone, out storage)
|| !RCUtils.HasController(drone, basePlayer))
return;
if (basePlayer.inventory.loot.IsLooting() && basePlayer.inventory.loot.entitySource == storage)
{
basePlayer.EndLooting();
return;
}
var baseLock = GetLock(storage);
var isLocked = baseLock != null && baseLock.IsLocked();
// Temporarily unlock the container so that the player can view the contents without authorization.
if (isLocked)
{
baseLock.SetFlagLocal(BaseEntity.Flags.Locked, false, recursive: false);
}
// Temporarily remove the stash owner, to bypass plugins such as PreventLooting.
var ownerId = storage.OwnerID;
storage.OwnerID = 0;
if (storage.PlayerOpenLoot(basePlayer, storage.panelName, doPositionChecks: false))
{
_remoteStashViewerTracker.Add(basePlayer.userID);
}
storage.OwnerID = ownerId;
if (isLocked)
{
baseLock.SetFlagLocal(BaseEntity.Flags.Locked, true, recursive: false);
}
}
[Command("dronestorage.ui.dropitems")]
private void UICommandDropItems(IPlayer player)
{
BasePlayer basePlayer;
Drone drone;
StorageContainer storage;
if (!TryGetControlledStorage(player, PermissionDropItems, out basePlayer, out drone, out storage)
|| !RCUtils.HasController(drone, basePlayer))
return;
DropItems(drone, storage, basePlayer);
}
[Command("dronestorage.ui.togglelock")]
private void UICommandLockStorage(IPlayer player)
{
BasePlayer basePlayer;
Drone drone;
StorageContainer storage;
if (!TryGetControlledStorage(player, PermissionToggleLock, out basePlayer, out drone, out storage)
|| !RCUtils.HasController(drone, basePlayer))
return;
if (!_uiViewers.Contains(basePlayer))
return;
var baseLock = GetLock(storage);
if (baseLock == null)
return;
var wasLocked = baseLock.IsLocked();
using var flagsScope = baseLock.StartSetFlags(BaseEntity.FlagsUpdateMode.SendNetworkUpdate_Flags);
flagsScope.Set(BaseEntity.Flags.Locked, !wasLocked);
Effect.server.Run(wasLocked ? UnlockEffectPrefab : LockEffectPrefab, baseLock, 0, Vector3.zero, Vector3.zero);
UI.CreateForPlayer(this, basePlayer, storage);
}
#endregion
#region UI
private static class UI
{
private const string Name = "DroneStorage";
private static float GetButtonOffsetX(DroneStorage plugin, int index, int totalButtons)
{
var buttonSettings = plugin._config.UISettings.Buttons;
var panelWidth = buttonSettings.Width * totalButtons + buttonSettings.Spacing * (totalButtons - 1);
var offsetXMin = -panelWidth / 2 + (buttonSettings.Width + buttonSettings.Spacing) * index;
return offsetXMin;
}
public static void CreateForPlayer(DroneStorage plugin, BasePlayer player, StorageContainer storage)
{
var baseLock = GetLock(storage);
var iPlayer = player.IPlayer;
var showViewItemsButton = iPlayer.HasPermission(PermissionViewItems);
var showDropItemsButton = iPlayer.HasPermission(PermissionDropItems);
var showToggleLockButton = baseLock != null && iPlayer.HasPermission(PermissionToggleLock);
var totalButtons = Convert.ToInt32(showViewItemsButton)
+ Convert.ToInt32(showDropItemsButton)
+ Convert.ToInt32(showToggleLockButton);
if (totalButtons == 0)
return;
var config = plugin._config;
var cuiElements = new CuiElementContainer
{
new CuiElement
{
Parent = "Overlay",
Name = Name,
DestroyUi = Name,
Components =
{
new CuiRectTransformComponent
{
AnchorMin = config.UISettings.AnchorMin,
AnchorMax = config.UISettings.AnchorMax,
OffsetMin = config.UISettings.OffsetMin,
OffsetMax = config.UISettings.OffsetMax,
},
}
}
};
var currentButtonIndex = 0;
var buttonSettings = config.UISettings.Buttons;
if (showViewItemsButton)
{
var offsetXMin = GetButtonOffsetX(plugin, currentButtonIndex++, totalButtons);
cuiElements.Add(
new CuiButton
{
Text =
{
Text = plugin.GetMessage(player.UserIDString, Lang.UIButtonViewItems),
Align = TextAnchor.MiddleCenter,
Color = buttonSettings.ViewButtonTextColor,
FontSize = buttonSettings.TextSize
},
Button =
{
Color = buttonSettings.ViewButtonColor,
Command = "dronestorage.ui.viewitems",
},
RectTransform =
{
AnchorMin = "0 0",
AnchorMax = "0 0",
OffsetMin = $"{offsetXMin.ToString()} 0",
OffsetMax = $"{(offsetXMin + buttonSettings.Width).ToString()} {buttonSettings.Height.ToString()}"
}
},
Name
);
}
if (showDropItemsButton)
{
var offsetXMin = GetButtonOffsetX(plugin, currentButtonIndex++, totalButtons);
cuiElements.Add(
new CuiButton
{
Text =
{
Text = plugin.GetMessage(player.UserIDString, Lang.UIButtonDropItems),
Align = TextAnchor.MiddleCenter,
Color = buttonSettings.DropButtonTextColor,
FontSize = buttonSettings.TextSize
},
Button =
{
Color = buttonSettings.DropButtonColor,
Command = "dronestorage.ui.dropitems",
},
RectTransform =
{
AnchorMin = "0 0",
AnchorMax = "0 0",
OffsetMin = $"{offsetXMin.ToString()} 0",
OffsetMax = $"{(offsetXMin + buttonSettings.Width).ToString()} {buttonSettings.Height.ToString()}"
}
},
Name
);
}
if (showToggleLockButton)
{
var isLocked = baseLock.IsLocked();
var offsetXMin = GetButtonOffsetX(plugin, currentButtonIndex++, totalButtons);
cuiElements.Add(new CuiButton
{
Text =
{
Text = plugin.GetMessage(player.UserIDString, isLocked ? Lang.UIButtonUnlockStorage : Lang.UIButtonLockStorage),
Align = TextAnchor.MiddleCenter,
Color = isLocked ? buttonSettings.UnlockButtonTextColor : buttonSettings.LockButtonTextColor,
FontSize = buttonSettings.TextSize
},
Button =
{
Color = isLocked ? buttonSettings.UnlockButtonColor : buttonSettings.LockButtonColor,
Command = "dronestorage.ui.togglelock",
},
RectTransform =
{
AnchorMin = "0 0",
AnchorMax = "0 0",
OffsetMin = $"{offsetXMin.ToString()} 0",
OffsetMax = $"{(offsetXMin + buttonSettings.Width).ToString()} {buttonSettings.Height.ToString()}"
}
}, Name);
}
CuiHelper.AddUi(player, cuiElements);
}
public static void DestroyForPlayer(BasePlayer player)
{
CuiHelper.DestroyUi(player, Name);
}
}
#endregion
#region Helpers
private static class RCUtils
{
public static bool IsRCDrone(Drone drone)
{
return !(drone is DeliveryDrone);
}
public static bool HasController(IRemoteControllable controllable, BasePlayer player)
{
return controllable.ControllingViewerId?.SteamId == player.userID;
}
public static T GetControlledEntity<T>(BasePlayer player, out ComputerStation station) where T : class
{
station = player.GetMounted() as ComputerStation;
if ((object)station == null)
return null;
return station.currentlyControllingEnt.Get(serverside: true) as T;
}
public static T GetControlledEntity<T>(BasePlayer player) where T : class
{
ComputerStation station;
return GetControlledEntity<T>(player, out station);
}
}
private static bool DeployStorageWasBlocked(Drone drone, BasePlayer deployer)
{
var hookResult = Interface.CallHook("OnDroneStorageDeploy", drone, deployer);
return hookResult is bool && (bool)hookResult == false;
}
private static bool DropStorageWasBlocked(Drone drone, StorageContainer storage, BasePlayer pilot)
{
var hookResult = Interface.CallHook("OnDroneStorageDrop", drone, storage, pilot);
return hookResult is bool && (bool)hookResult == false;
}
private void RefreshDroneSettingsProfile(Drone drone)
{
DroneSettings?.Call("API_RefreshDroneProfile", drone);
}
private static bool IsDroneEligible(Drone drone)
{
return drone.skinID == 0 && RCUtils.IsRCDrone(drone);
}
private static bool IsDroneStorage(StorageContainer storage, out Drone drone)
{
drone = storage.GetParentEntity() as Drone;
if (drone == null)
return false;
return storage.PrefabName == StoragePrefab;
}
private static bool IsDroneStorage(StorageContainer storage)
{
Drone drone;
return IsDroneStorage(storage, out drone);
}
private static bool CanPickupInternal(BasePlayer player, Drone drone)
{
if (!IsDroneEligible(drone))
return true;
var storage = GetDroneStorage(drone);
if (storage == null)
return true;
// Prevent drone pickup while it has a non-empty storage (the storage must be emptied first).
if (storage != null && !storage.inventory.IsEmpty())
return false;
return true;
}
private static StorageContainer GetDroneStorage(Drone drone)
{
return drone.GetSlot(StorageSlot) as StorageContainer;
}
private static BaseLock GetLock(StorageContainer storage)
{
return storage.GetSlot(BaseEntity.Slot.Lock) as BaseLock;
}
private static void DropItems(Drone drone, StorageContainer storage, BasePlayer pilot = null)
{
var itemList = storage.inventory.itemList;
if (itemList == null || itemList.Count <= 0)
return;
if (DropStorageWasBlocked(drone, storage, pilot))
return;
var dropPosition = pilot == null
? drone.transform.position
: drone.transform.TransformPoint(StorageDropForwardLocation);
if (pilot != null)
{
pilot.EndLooting();
}
Effect.server.Run(StorageDeployEffectPrefab, storage.transform.position);
var dropContainer = storage.inventory.Drop(DropBagPrefab, dropPosition, storage.transform.rotation * StorageDropRotation, 0);
Interface.Call("OnDroneStorageDropped", drone, storage, dropContainer, pilot);
}
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 bool CanStashAcceptItem(BasePlayer player, Item item, int position)
{
if (_config.DisallowedItems != null
&& _config.DisallowedItems.Contains(item.info.shortname))
return false;
if (item.skin != 0
&& _config.DisallowedSkins != null
&& _config.DisallowedSkins.Contains(item.skin))
return false;
return true;
}
private bool TryGetControlledStorage(IPlayer player, string perm, out BasePlayer basePlayer, out Drone drone, out StorageContainer storage)
{
basePlayer = null;
drone = null;
storage = null;
if (player.IsServer || !player.HasPermission(perm))
return false;
basePlayer = player.Object as BasePlayer;
drone = RCUtils.GetControlledEntity<Drone>(basePlayer);
if (drone == null)
return false;
storage = GetDroneStorage(drone);
return storage != null;
}
private void SetupDroneStorage(Drone drone, StorageContainer storage, int capacity)
{
if (!_config.AssignStorageOwnership)
{
storage.OwnerID = 0;
}
else if (storage.OwnerID == 0)
{
storage.OwnerID = drone.OwnerID;
}
var storageOwnerId = _config.AssignStorageOwnership ? storage.OwnerID : drone.OwnerID;
storage.isLockable = storageOwnerId != 0 && permission.UserHasPermission(storageOwnerId.ToString(), PermissionLockable);
storage.inventory.canAcceptItem = StashItemFilter;
// Damage will be processed by the drone.
storage.baseProtection = null;
storage.inventory.capacity = capacity;
storage.panelName = ResizableLootPanelName;
DroneStorageComponent.AddToStorage(this, drone, storage);
RefreshDroneSettingsProfile(drone);
}
private StorageContainer TryDeployStorage(Drone drone, int capacity, bool allowRefund = false, BasePlayer deployer = null)
{
if (DeployStorageWasBlocked(drone, deployer))
return null;
var storage = GameManager.server.CreateEntity(StoragePrefab, StorageLocalPosition, StorageLocalRotation) as StorageContainer;
if (storage == null)
return null;
if (_config.AssignStorageOwnership)
{
storage.OwnerID = deployer?.userID ?? drone.OwnerID;
}
storage.SetParent(drone);
storage.Spawn();
SetupDroneStorage(drone, storage, capacity);
drone.SetSlot(StorageSlot, storage);
// This flag is used to remember whether the stash should be refundable.
// This information is lost on restart but that's a minor concern.
storage.pickup.enabled = allowRefund;
Effect.server.Run(StorageDeployEffectPrefab, storage.transform.position);
Interface.CallHook("OnDroneStorageDeployed", drone, storage, deployer);
return storage;
}
private void RefreshStorage(Drone drone)
{
var storage = GetDroneStorage(drone);
if (storage == null)
return;
// Possibly increase capacity, but do not decrease it because that could hide items.
var capacity = Math.Max(storage.inventory.capacity, _storageCapacityManager.DetermineCapacityForUser(drone.OwnerID));
SetupDroneStorage(drone, storage, capacity);
}
#endregion
#region Drone Storage Component
private class DroneStorageComponent : MonoBehaviour
{
public static void AddToStorage(DroneStorage plugin, Drone drone, StorageContainer storageContainer)
{
var component = storageContainer.gameObject.AddComponent<DroneStorageComponent>();
component._plugin = plugin;
component._drone = drone;
component._storage = storageContainer;
plugin._droneStorageTracker.Add(storageContainer);
}
public static void RemoveFromStorage(StorageContainer storageContainer)
{
DestroyImmediate(storageContainer.GetComponent<DroneStorageComponent>());
}
private DroneStorage _plugin;
private Drone _drone;
private StorageContainer _storage;
// Called via `entity.SendMessage("PlayerStoppedLooting", player)` in PlayerLoot.Clear().
private void PlayerStoppedLooting(BasePlayer looter)
{
_plugin._remoteStashViewerTracker.Remove(looter.userID);
}
private void OnDestroy()
{
if (_drone != null && !_drone.IsDestroyed)
{
_drone.Invoke(() => _plugin.RefreshDroneSettingsProfile(_drone), 0);
}
_plugin._droneStorageTracker.Remove(_storage);
}
}
#endregion
#region Dynamic Hooks
private class HookCollection
{
public bool IsSubscribed { get; private set; } = true;
private readonly DroneStorage _plugin;
private readonly string[] _hookNames;
private readonly Func<bool> _shouldSubscribe;
public HookCollection(DroneStorage plugin, Func<bool> shouldSubscribe, params string[] hookNames)
{
_plugin = plugin;
_hookNames = hookNames;
_shouldSubscribe = shouldSubscribe;
}
public void Subscribe()
{
foreach (var hookName in _hookNames)
{
_plugin.Subscribe(hookName);
}
IsSubscribed = true;
}
public void Unsubscribe()
{
foreach (var hookName in _hookNames)
{
_plugin.Unsubscribe(hookName);
}
IsSubscribed = false;
}
public void Refresh()
{
if (_shouldSubscribe())
{
if (!IsSubscribed)
{
Subscribe();
}
}
else if (IsSubscribed)
{
Unsubscribe();
}
}
}
private class DynamicHookHashSet<T> : HashSet<T>
{
private readonly HookCollection _hookCollection;
public DynamicHookHashSet(DroneStorage plugin, params string[] hookNames)
{
_hookCollection = new HookCollection(plugin, () => Count > 0, hookNames);
}
public new bool Add(T item)
{
var result = base.Add(item);
if (result)
{
_hookCollection.Refresh();
}
return result;
}
public new bool Remove(T item)
{
var result = base.Remove(item);
if (result)
{
_hookCollection.Refresh();
}
return result;
}
public void Unsubscribe() => _hookCollection.Unsubscribe();
}
#endregion
#region Configuration
private class StorageCapacityManager
{
private class StorageSize
{
public readonly int Capacity;
public readonly string Permission;
public StorageSize(int capacity, string permission)
{
Capacity = capacity;
Permission = permission;
}
}
private readonly DroneStorage _plugin;
private StorageSize[] _sortedStorageSizes;
public StorageCapacityManager(DroneStorage plugin)
{
_plugin = plugin;
}
public void Init()
{
var storageSizeList = new List<StorageSize>();
foreach (var capacity in new HashSet<int>(_plugin._config.CapacityAmounts).OrderBy(capacity => capacity))
{
var storageSize = new StorageSize(capacity, $"{PermissionCapacityPrefix}.{capacity.ToString()}");
_plugin.permission.RegisterPermission(storageSize.Permission, _plugin);
storageSizeList.Add(storageSize);
}
_sortedStorageSizes = storageSizeList.ToArray();
}
public int DetermineCapacityForUser(ulong userId)
{
if (userId == 0)
return 0;
var userIdString = userId.ToString();
for (var i = _sortedStorageSizes.Length - 1; i >= 0; i--)
{
var storageSize = _sortedStorageSizes[i];
if (_plugin.permission.UserHasPermission(userIdString, storageSize.Permission))
return storageSize.Capacity;
}
return 0;
}
}
private class Configuration : BaseConfiguration
{
[JsonProperty("TipChance")]
public int TipChance = 25;
[JsonProperty("AssignStorageOwnership")]
public bool AssignStorageOwnership = true;
[JsonProperty("CapacityAmounts")]
public int[] CapacityAmounts = { 6, 12, 18, 24, 30, 36, 42, 48 };
[JsonProperty("DisallowedItems")]
public string[] DisallowedItems = new string[0];
[JsonProperty("DisallowedSkins")]
public ulong[] DisallowedSkins = new ulong[0];
[JsonProperty("UISettings")]
public UISettings UISettings = new UISettings();
}
private class UISettings
{
[JsonProperty("AnchorMin")]
public string AnchorMin = "0.5 1";
[JsonProperty("AnchorMax")]
public string AnchorMax = "0.5 1";
[JsonProperty("OffsetMin")]
public string OffsetMin = "0 -75";
[JsonProperty("OffsetMax")]
public string OffsetMax = "0 -75";
[JsonProperty("Buttons")]
public UIButtons Buttons = new UIButtons();
}
private class UIButtons
{
[JsonProperty("Spacing")]
public int Spacing = 25;
[JsonProperty("Width")]
public int Width = 85;
[JsonProperty("Height")]
public int Height = 26;
[JsonProperty("TextSize")]
public int TextSize = 13;
[JsonProperty("ViewButtonColor")]
public string ViewButtonColor = "0.44 0.54 0.26 1";
[JsonProperty("ViewButtonTextColor")]
public string ViewButtonTextColor = "0.97 0.92 0.88 1";
[JsonProperty("DropButtonColor")]
public string DropButtonColor = "0.77 0.24 0.16 1";
[JsonProperty("DropButtonTextColor")]
public string DropButtonTextColor = "0.97 0.92 0.88 1";
[JsonProperty("LockButtonColor")]
public string LockButtonColor = "0.8 0.4 0 1";
[JsonProperty("LockButtonTextColor")]
public string LockButtonTextColor = "0.97 0.92 0.88 1";
[JsonProperty("UnlockButtonColor")]
public string UnlockButtonColor = "0.8 0.4 0 1";
[JsonProperty("UnlockButtonTextColor")]
public string UnlockButtonTextColor = "0.97 0.92 0.88 1";
}
private Configuration GetDefaultConfig() => new Configuration();
#endregion
#region Configuration Helpers
private class BaseConfiguration
{
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(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, object> currentWithDefaults, Dictionary<string, object> currentRaw)
{
var 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() => _config = GetDefaultConfig();
protected override void LoadConfig()
{
base.LoadConfig();
try
{
_config = Config.ReadObject<Configuration>();
if (_config == null)
{
throw new JsonException();
}
if (MaybeUpdateConfig(_config))
{
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(_config, true);
}
#endregion
#region Localization
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 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 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.UserIDString, messageName), args));
private class Lang
{
public const string UIButtonViewItems = "UI.Button.ViewItems";
public const string UIButtonDropItems = "UI.Button.DropItems";
public const string UIButtonLockStorage = "UI.Button.LockStorage";
public const string UIButtonUnlockStorage = "UI.Button.UnlockStorage";
public const string TipDeployCommand = "Tip.DeployCommand";
public const string InfoStashName = "Info.StashName";
public const string ErrorNoPermission = "Error.NoPermission";
public const string ErrorBuildingBlocked = "Error.BuildingBlocked";
public const string ErrorNoDroneFound = "Error.NoDroneFound";
public const string ErrorNoStashItem = "Error.NoStashItem";
public const string ErrorAlreadyHasStorage = "Error.AlreadyHasStorage";
public const string ErrorIncompatibleAttachment = "Error.IncompatibleAttachment";
public const string ErrorDeployFailed = "Error.DeployFailed";
public const string ErrorCannotPickupDroneWithItems = "Error.CannotPickupDroneWithItems";
}
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
[Lang.UIButtonViewItems] = "View Items",
[Lang.UIButtonDropItems] = "Drop Items",
[Lang.UIButtonLockStorage] = "Lock",
[Lang.UIButtonUnlockStorage] = "Unlock",
[Lang.TipDeployCommand] = "Tip: Look at the drone and run <color=yellow>/dronestash</color> to deploy a stash.",
[Lang.InfoStashName] = "Drone Stash",
[Lang.ErrorNoPermission] = "You don't have permission to do that.",
[Lang.ErrorBuildingBlocked] = "Error: Cannot do that while building blocked.",
[Lang.ErrorNoDroneFound] = "Error: No drone found.",
[Lang.ErrorNoStashItem] = "Error: You need a stash to do that.",
[Lang.ErrorAlreadyHasStorage] = "Error: That drone already has a stash.",
[Lang.ErrorIncompatibleAttachment] = "Error: That drone has an incompatible attachment.",
[Lang.ErrorDeployFailed] = "Error: Failed to deploy stash.",
[Lang.ErrorCannotPickupDroneWithItems] = "Cannot pick up that drone while its s
// ... [Code preview truncated due to length. Full source in attached file] ...