C#:
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Libraries;
using Oxide.Core.Plugins;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.ComponentModel;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Vehicle Storage", "WhiteThunder", "3.5.3")]
[Description("Allows adding storage containers to vehicles and increasing built-in storage capacity.")]
internal class VehicleStorage : CovalencePlugin
{
#region Fields
[PluginReference]
private Plugin CargoTrainEvent;
private Configuration _config;
private const string BasePermissionPrefix = "vehiclestorage";
private const string RhibStoragePrefab = "assets/content/vehicles/boats/rhib/subents/rhib_storage.prefab";
private const string HabStoragePrefab = "assets/prefabs/deployable/hot air balloon/subents/hab_storage.prefab";
private const string StashDeployEffectPrefab = "assets/prefabs/deployable/small stash/effects/small-stash-deploy.prefab";
private const string BoxDeployEffectPrefab = "assets/prefabs/deployable/woodenbox/effects/wooden-box-deploy.prefab";
private const string ResizableLootPanelName = "generic_resizable";
private const int MaxCapacity = 48;
private readonly VehicleTracker _vehicleTracker = new();
private readonly HashSet<DroppedItemContainer> _droppedItemContainers = new();
private readonly ReskinEventManager _reskinEventManager = new();
private readonly ContainerBoundsManager _containerBoundsManager = new();
private readonly VehicleStorageManager _vehicleStorageManager;
public VehicleStorage()
{
_vehicleStorageManager = new VehicleStorageManager(_vehicleTracker);
}
#endregion
#region Hooks
private void Init()
{
Unsubscribe(nameof(OnEntitySpawned));
}
private void OnServerInitialized()
{
_config.Init(this);
_containerBoundsManager.OnServerInitialized();
foreach (var networkable in BaseNetworkable.serverEntities)
{
var entity = networkable as BaseCombatEntity;
if (entity == null)
continue;
var vehicleConfig = _config.GetVehicleConfig(entity);
if (vehicleConfig == null)
continue;
_vehicleTracker.AddVehicle(vehicleConfig, entity);
var vehicleProfile = vehicleConfig.GetProfileForVehicle(permission, entity);
if (vehicleProfile == null)
continue;
var defaultContainer = vehicleConfig.GetDefaultContainer(entity);
var vehicleData = _vehicleStorageManager.GetOrCreateVehicleData(entity, defaultContainer?.inventory.capacity ?? -1);
DiscoverExistingContainers(entity, vehicleConfig, vehicleData);
if (AlterStorageWasBlocked(entity))
continue;
ApplyVehicleProfile(entity, vehicleConfig, vehicleProfile, vehicleData);
}
Subscribe(nameof(OnEntitySpawned));
}
private void Unload()
{
_vehicleStorageManager.Unload();
}
private void OnEntitySpawned(BaseCombatEntity entity)
{
var vehicleConfig = _config.GetVehicleConfig(entity);
if (vehicleConfig == null)
return;
_vehicleTracker.AddVehicle(vehicleConfig, entity);
var entity2 = entity;
var vehicleConfig2 = vehicleConfig;
// Wait 2 ticks to give Vehicle Vendor Options an opportunity to set ownership.
NextTick(() => NextTick(() =>
{
if (entity2 == null)
return;
RefreshVehicleStorage(entity2, vehicleConfig2);
}));
}
private void OnEntityKill(BaseCombatEntity entity)
{
if (entity is DroppedItemContainer droppedContainer)
{
_droppedItemContainers.Remove(droppedContainer);
return;
}
var vehicleConfig = _config.GetVehicleConfig(entity);
if (vehicleConfig?.ContainerPresets == null)
return;
_vehicleStorageManager.ForgetVehicle(entity);
if (!_vehicleTracker.RemoveVehicle(vehicleConfig, entity))
return;
foreach (var child in entity.children)
{
var container = child as StorageContainer;
if (container == null)
continue;
if (!vehicleConfig.ContainerPresets.ContainsKey(container.name))
continue;
if (DropItems(container) is { } droppedItemContainer)
{
_droppedItemContainers.Add(droppedItemContainer);
}
}
}
// Allow dropped item containers to be looted (by anyone) if they were dropped by this plugin.
// Note: This approach is a little bit hacky, might be better to harmony patch OnStartBeingLooted.
// This won't work if the plugin is reloaded (or the server is restarted) while the dropped item container is
// still around, but that's probably an acceptable edge case (rare).
private void CanLootEntity(BasePlayer player, DroppedItemContainer droppedItemContainer)
{
if (!_droppedItemContainers.Contains(droppedItemContainer)
|| player.userID == droppedItemContainer.playerSteamID
|| !player.InSafeZone() && !droppedItemContainer.InSafeZone())
return;
droppedItemContainer.playerSteamID = player.userID;
}
private void OnUserPermissionGranted(string userId, string perm)
{
if (perm.StartsWith(BasePermissionPrefix))
{
HandlePermissionChanged(userId);
}
}
private void OnUserPermissionRevoked(string userId, string perm)
{
OnUserPermissionGranted(userId, perm);
}
private void OnGroupPermissionGranted(string group, string perm)
{
if (perm.StartsWith(BasePermissionPrefix))
{
HandlePermissionChanged();
}
}
private void OnGroupPermissionRevoked(string group, string perm)
{
OnGroupPermissionGranted(group, perm);
}
private void OnUserGroupAdded(string userId, string groupName)
{
var permList = permission.GetGroupPermissions(groupName, parents: true);
foreach (var perm in permList)
{
if (perm.StartsWith(BasePermissionPrefix))
{
HandlePermissionChanged(userId);
return;
}
}
}
private void OnUserGroupRemoved(string userId, string groupName)
{
OnUserGroupAdded(userId, groupName);
}
private void OnRidableAnimalClaimed(RidableHorse horse, BasePlayer player)
{
RefreshVehicleStorage(horse);
}
private void OnEntityReskin(Snowmobile snowmobile, ItemSkinDirectory.Skin skin, BasePlayer player)
{
var vehicleConfig = _config.GetVehicleConfig(snowmobile);
if (vehicleConfig?.ContainerPresets == null)
return;
var reskinEvent = _reskinEventManager.GetEvent()
.WithParent(snowmobile)
.WithVehicleConfig(vehicleConfig);
for (var i = snowmobile.children.Count - 1; i >= 0; i--)
{
var container = snowmobile.children[i] as StorageContainer;
if (container == null || container.IsDestroyed)
continue;
if (!vehicleConfig.ContainerPresets.ContainsKey(container.name))
continue;
reskinEvent.AddContainer(container);
// Unparent the container to prevent it from being destroyed.
// It will later be parented to the newly spawned entity.
container.SetParent(null);
}
if (reskinEvent.Containers.Count == 0)
{
_reskinEventManager.CancelEvent(reskinEvent);
}
else
{
_reskinEventManager.RecordEvent(reskinEvent);
// In case another plugin blocks the pre-hook, reparent or kill the containers.
NextTick(_reskinEventManager.CleanupAction);
}
}
private void OnEntityReskinned(Snowmobile snowmobile, ItemSkinDirectory.Skin skin, BasePlayer player)
{
var transform = snowmobile.transform;
var reskinEvent = _reskinEventManager.FindEvent(transform.position);
if (reskinEvent == null)
return;
var newVehicleConfig = _config.GetVehicleConfig(snowmobile);
if (newVehicleConfig?.ContainerPresets == null)
{
// New vehicle has no container presets, so kill the containers.
foreach (var container in reskinEvent.Containers)
{
container.DropItems();
container.Kill();
}
return;
}
foreach (var container in reskinEvent.Containers)
{
var containerName = container.name;
var newContainerPreset = newVehicleConfig.FindContainerPreset(containerName);
if (newContainerPreset == null || container.PrefabName != newContainerPreset.Prefab)
{
container.DropItems();
container.Kill();
continue;
}
newContainerPreset.MoveContainerToParent(container, snowmobile);
}
_reskinEventManager.CompleteEvent(reskinEvent);
}
// Compatibility with plugin: Claim Vehicle Ownership
private void OnVehicleOwnershipChanged(BaseEntity vehicle) => RefreshVehicleStorage(vehicle);
#endregion
#region Dependencies
private bool IsCargoTrain(BaseEntity entity)
{
var workcart = entity as TrainEngine;
if (workcart == null)
return false;
return CargoTrainEvent?.Call("IsTrainSpecial", workcart.net.ID) is true;
}
#endregion
#region API
private void API_RefreshVehicleStorage(BaseEntity vehicle)
{
RefreshVehicleStorage(vehicle);
}
#endregion
#region Exposed Hooks
private bool AlterStorageWasBlocked(BaseEntity vehicle)
{
if (Interface.CallHook("OnVehicleStorageUpdate", vehicle) is false)
return true;
if (IsCargoTrain(vehicle))
return true;
return false;
}
private bool SpawnStorageWasBlocked(BaseEntity vehicle)
{
return Interface.CallHook("OnVehicleStorageSpawn", vehicle) is false;
}
private void CallHookVehicleStorageSpawned(BaseEntity vehicle, StorageContainer container)
{
Interface.CallHook("OnVehicleStorageSpawned", vehicle, container);
}
#endregion
#region Helper Methods
public static void LogError(string message) => Interface.Oxide.LogError($"[Vehicle Storage] {message}");
public static void LogWarning(string message) => Interface.Oxide.LogWarning($"[Vehicle Storage] {message}");
// Copy of StorageContainer.DropItems, but returns the dropped item container if not dropped individually.
// The `initiator` logic was also removed since it's only relevant to loot containers (e.g., loot barrels).
public static DroppedItemContainer DropItems(IItemContainerEntity containerEntity)
{
var itemContainer = containerEntity.inventory;
if (itemContainer?.itemList == null || itemContainer.itemList.Count == 0 || !containerEntity.DropsLoot)
return null;
if (containerEntity.ShouldDropItemsIndividually() || (itemContainer.itemList.Count == 1 && !containerEntity.DropFloats))
{
DropUtil.DropItems(itemContainer, containerEntity.GetDropPosition());
return null;
}
var prefab = containerEntity.DropFloats ? "assets/prefabs/misc/item drop/item_drop_buoyant.prefab" : "assets/prefabs/misc/item drop/item_drop.prefab";
return itemContainer.Drop(prefab, containerEntity.GetDropPosition(), containerEntity.Transform.rotation, containerEntity.DestroyLootPercent);
}
private static int GetHighestUsedSlot(ItemContainer container)
{
var highestUsedSlot = -1;
foreach (var item in container.itemList)
{
if (item.position > highestUsedSlot)
{
highestUsedSlot = item.position;
}
}
return highestUsedSlot;
}
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 void MaybeAdjustCapacity(StorageContainer container, int capacity)
{
container.panelName = ResizableLootPanelName;
if (capacity == -1)
return;
// Allow capacity reduction, but in a safe and slightly lazy way (don't move any items).
var containerInventory = container.inventory;
containerInventory.capacity = containerInventory.capacity < capacity
? capacity
: Mathf.Max(capacity, GetHighestUsedSlot(containerInventory));
}
private void SetupStorage(BaseEntity vehicle, StorageContainer container, ContainerPreset preset)
{
if (container.IsFullySpawned())
{
MoveOverlappingDismountPositions(vehicle, container);
}
container.name = preset.Name;
container.pickup.enabled = false;
container.dropsLoot = true;
RemoveProblemComponents(container);
}
private void MoveOverlappingDismountPositions(BaseEntity vehicle, StorageContainer container)
{
var vehicleEntity = vehicle as BaseVehicle;
if (vehicleEntity == null)
return;
var containerTransform = container.transform;
var containerLocalPosition = containerTransform.localPosition;
var containerBounds = _containerBoundsManager.GetBounds(container);
var containerWorldBounds = new OBB(containerTransform.position, containerTransform.lossyScale, containerTransform.rotation, containerBounds);
foreach (var dismountTransform in vehicleEntity.dismountPositions)
{
if (containerWorldBounds.Contains(dismountTransform.position))
{
var newDismountLocalPosition = dismountTransform.localPosition;
newDismountLocalPosition.y = containerLocalPosition.y + containerBounds.center.y + containerBounds.extents.y + 0.4f;
dismountTransform.localPosition = newDismountLocalPosition;
}
}
}
private StorageContainer SpawnStorage(BaseEntity vehicle, ContainerPreset preset, int capacity)
{
if (SpawnStorageWasBlocked(vehicle))
return null;
Vector3 position;
Quaternion rotation;
var parentAfterSpawn = vehicle is HotAirBalloon && preset.Prefab == HabStoragePrefab;
if (parentAfterSpawn)
{
var vehicleTransform = vehicle.transform;
position = vehicleTransform.TransformPoint(preset.Position);
rotation = vehicleTransform.rotation * preset.Rotation;
}
else
{
position = preset.Position;
rotation = preset.Rotation;
}
var createdEntity = GameManager.server.CreateEntity(preset.Prefab, position, rotation);
if (createdEntity == null)
return null;
var container = createdEntity as StorageContainer;
if (container == null)
{
UnityEngine.Object.Destroy(createdEntity);
return null;
}
container.name = preset.Name;
SetupStorage(vehicle, container, preset);
if (parentAfterSpawn)
{
container.Spawn();
container.SetParent(vehicle, preset.ParentBone, worldPositionStays: true, sendImmediate: true);
}
else
{
container.SetParent(vehicle, preset.ParentBone);
container.Spawn();
}
MoveOverlappingDismountPositions(vehicle, container);
MaybeAdjustCapacity(container, capacity);
CallHookVehicleStorageSpawned(vehicle, container);
var deployEffect = preset.Prefab == RhibStoragePrefab
? BoxDeployEffectPrefab
: preset.Prefab == HabStoragePrefab
? StashDeployEffectPrefab
: null;
if (deployEffect != null)
{
Effect.server.Run(deployEffect, container.transform.position);
}
return container;
}
private StorageContainer FindStorageContainerForPreset(BaseEntity entity, ContainerPreset preset)
{
foreach (var child in entity.children)
{
var container = child as StorageContainer;
if (container == null)
continue;
if (container.PrefabName == preset.Prefab
&& (container.transform.localPosition == preset.Position || container.name == preset.Name))
return container;
}
return null;
}
private void AddOrUpdateExtraContainers(BaseEntity vehicle, VehicleProfile vehicleProfile, ICollection<StorageContainer> containersAddedOrUpdated)
{
foreach (var (containerPreset, capacity) in vehicleProfile.ValidAdditionalStorage)
{
var container = FindStorageContainerForPreset(vehicle, containerPreset);
if (container == null)
{
container = SpawnStorage(vehicle, containerPreset, capacity);
if (container != null)
{
containersAddedOrUpdated.Add(container);
}
continue;
}
containersAddedOrUpdated.Add(container);
var cleaner = VehicleStorageAfterUnloadCleaner.FindComponentByName(container);
if (cleaner != null)
{
UnityEngine.Object.DestroyImmediate(cleaner);
}
// The cleaner component would be lost after server restart, so make sure to allow item input again.
container.inventory.SetFlag(ItemContainer.Flag.NoItemInput, false);
SetupStorage(vehicle, container, containerPreset);
MaybeAdjustCapacity(container, capacity);
var transform = container.transform;
if (transform.localPosition != containerPreset.Position || transform.localRotation != containerPreset.Rotation)
{
transform.localPosition = containerPreset.Position;
transform.localRotation = containerPreset.Rotation;
container.InvalidateNetworkCache();
container.SendNetworkUpdate_Position();
}
}
}
private void DiscoverExistingContainers(BaseEntity vehicle, VehicleConfig vehicleConfig, VehicleData vehicleData)
{
if (vehicleConfig.ContainerPresets == null)
return;
foreach (var containerPreset in vehicleConfig.ContainerPresets.Values)
{
var container = FindStorageContainerForPreset(vehicle, containerPreset);
if (container == null)
continue;
vehicleData.Containers.Add(container);
var cleaner = VehicleStorageAfterUnloadCleaner.FindComponentByName(container);
if (cleaner != null)
{
UnityEngine.Object.DestroyImmediate(cleaner);
}
// The cleaner component would be lost after server restart, so make sure to allow item input again.
container.inventory.SetFlag(ItemContainer.Flag.NoItemInput, false);
}
}
private void ApplyVehicleProfile(BaseEntity vehicle, VehicleConfig vehicleConfig, VehicleProfile vehicleProfile, VehicleData vehicleData)
{
var defaultContainer = vehicleConfig.GetDefaultContainer(vehicle);
if (defaultContainer != null)
{
MaybeAdjustCapacity(defaultContainer, vehicleProfile.BuiltInStorageCapacity);
}
if (vehicleProfile.ValidAdditionalStorage != null)
{
AddOrUpdateExtraContainers(vehicle, vehicleProfile, vehicleData.Containers);
}
if (vehicleConfig.ContainerPresets != null && vehicleData.Containers.Count > 0)
{
_vehicleStorageManager.RemoveUnwantedContainers(vehicleConfig, vehicleProfile, vehicleData);
}
}
private void RefreshVehicleStorage(BaseEntity vehicle, VehicleConfig vehicleConfig)
{
var vehicleProfile = vehicleConfig.GetProfileForVehicle(permission, vehicle);
if (vehicleProfile == null)
return;
if (AlterStorageWasBlocked(vehicle))
return;
var defaultContainer = vehicleConfig.GetDefaultContainer(vehicle);
var vehicleData = _vehicleStorageManager.GetOrCreateVehicleData(vehicle, defaultContainer?.inventory.capacity ?? -1);
ApplyVehicleProfile(vehicle, vehicleConfig, vehicleProfile, vehicleData);
}
private void RefreshVehicleStorage(BaseEntity vehicle)
{
if (vehicle is ModularCar)
{
foreach (var child in vehicle.children)
{
if (child is BaseVehicleModule module)
{
RefreshVehicleStorage(module);
}
}
return;
}
var vehicleConfig = _config.GetVehicleConfig(vehicle);
if (vehicleConfig == null)
return;
RefreshVehicleStorage(vehicle, vehicleConfig);
}
private void HandlePermissionChanged(string userIdString = "")
{
foreach (var (vehicleConfig, vehicleList) in _vehicleTracker.AllSupportedVehicles)
{
foreach (var vehicle in vehicleList)
{
var ownerId = vehicleConfig.GetOwnerId(vehicle);
if (ownerId == 0)
{
// Unowned vehicles cannot be affected by permissions so there's nothing to do.
continue;
}
if (userIdString != string.Empty && userIdString != vehicle.OwnerID.ToString())
{
// Permissions changed for a specific player, but they don't own the vehicle, so nothing to do.
continue;
}
RefreshVehicleStorage(vehicle, vehicleConfig);
}
}
}
#endregion
#region Container Cleaner
// This component is attached to non-empty storage containers when the plugin is unloaded, to kill the container
// when it becomes empty. If the plugin loads again, and still cares about the container, this component will be
// discovered and removed to restore normal functionality.
// CAUTION: Don't rename this class, as it is discovered by name via reflection.
private class VehicleStorageAfterUnloadCleaner : FacepunchBehaviour
{
public static VehicleStorageAfterUnloadCleaner AddToStorageEntity(StorageContainer container)
{
if (container.GetComponent<VehicleStorageAfterUnloadCleaner>() != null)
return null;
var component = container.gameObject.AddComponent<VehicleStorageAfterUnloadCleaner>();
component._storageEntity = container;
component._handleItemAddedRemoved = component.OnItemAddedOrRemoved;
var containerInventory = container.inventory;
containerInventory.onItemAddedRemoved += component._handleItemAddedRemoved;
return component;
}
// Find VehicleStorageUnloadedAutoCleaner component from a potentially different instance of the plugin.
public static FacepunchBehaviour FindComponentByName(StorageContainer container)
{
return container.GetComponents<FacepunchBehaviour>().FirstOrDefault(IsCleanerComponent);
}
private static bool IsCleanerComponent(UnityEngine.Component component)
{
return component.GetType().Name == nameof(VehicleStorageAfterUnloadCleaner);
}
private StorageContainer _storageEntity;
private Action<Item, bool, BasePlayer> _handleItemAddedRemoved;
private bool IsContainerKillEligible()
{
return _storageEntity != null
&& !_storageEntity.IsDestroyed
&& _storageEntity.inventory != null
&& _storageEntity.inventory.IsEmpty();
}
private void OnItemAddedOrRemoved(Item item, bool added, BasePlayer sourcePlayer)
{
// We only care about removal events.
if (added)
return;
// Check if this component was destroyed, as a contingency in case the event handler wasn't removed.
// This should not happen, but who knows, another plugin could save and restore the event handler.
if (this == null)
return;
if (!IsContainerKillEligible())
return;
StartCoroutine(KillOnNextTick(_storageEntity));
}
private IEnumerator KillOnNextTick(BaseEntity entity)
{
yield return null;
if (!IsContainerKillEligible())
yield break;
entity.Kill();
}
private void OnDestroy()
{
var containerInventory = _storageEntity?.inventory;
if (containerInventory == null)
return;
containerInventory.onItemAddedRemoved -= _handleItemAddedRemoved;
}
}
#endregion
#region Container Bounds Manager
private class ContainerBoundsManager
{
private readonly Dictionary<string, string> _boundsReplacements = new()
{
// The RHIB storage bounds are small, so use the normal wooden box bounds.
[RhibStoragePrefab] = "assets/prefabs/deployable/woodenbox/woodbox_deployed.prefab",
};
private readonly Dictionary<uint, Bounds> _boundsReplacementsByPrefabId = new();
public void OnServerInitialized()
{
foreach (var (destinationPrefab, sourcePrefab) in _boundsReplacements)
{
var destinationEntity = GameManager.server.FindPrefab(destinationPrefab)?.GetComponent<BaseEntity>();
if (destinationEntity == null)
{
LogWarning($"Prefab not found: {destinationPrefab}");
continue;
}
var sourceEntity = GameManager.server.FindPrefab(sourcePrefab)?.GetComponent<BaseEntity>();
if (sourceEntity == null)
{
LogWarning($"Prefab not found: {sourcePrefab}");
continue;
}
_boundsReplacementsByPrefabId[destinationEntity.prefabID] = sourceEntity.bounds;
}
}
public Bounds GetBounds(BaseEntity entity)
{
return _boundsReplacementsByPrefabId.TryGetValue(entity.prefabID, out var bounds)
? bounds
: entity.bounds;
}
}
#endregion
#region Supported Vehicle Tracker
private class VehicleTracker
{
public readonly Dictionary<VehicleConfig, HashSet<BaseEntity>> AllSupportedVehicles = new();
public void AddVehicle(VehicleConfig vehicleConfig, BaseEntity vehicle)
{
var vehicles = GetVehicles(vehicleConfig);
if (vehicles == null)
{
vehicles = new HashSet<BaseEntity>();
AllSupportedVehicles[vehicleConfig] = vehicles;
}
vehicles.Add(vehicle);
}
public bool RemoveVehicle(VehicleConfig vehicleConfig, BaseEntity vehicle)
{
return GetVehicles(vehicleConfig)?.Remove(vehicle) ?? false;
}
private HashSet<BaseEntity> GetVehicles(VehicleConfig vehicleConfig)
{
return AllSupportedVehicles.GetValueOrDefault(vehicleConfig);
}
}
#endregion
#region Vehicle Storage Manager
private class VehicleData
{
public HashSet<StorageContainer> Containers = new();
public int InitialDefaultContainerCapacity;
public VehicleData(int initialDefaultContainerCapacity)
{
InitialDefaultContainerCapacity = initialDefaultContainerCapacity;
}
}
private class VehicleStorageManager
{
private readonly VehicleTracker _vehicleTracker;
private readonly Dictionary<BaseEntity, VehicleData> _vehicleDataByEntity = new();
private readonly List<StorageContainer> _containersToRemoveReusable = new();
public VehicleStorageManager(VehicleTracker vehicleTracker)
{
_vehicleTracker = vehicleTracker;
}
public VehicleData GetOrCreateVehicleData(BaseEntity vehicle, int initialDefaultContainerCapacity)
{
if (!_vehicleDataByEntity.TryGetValue(vehicle, out var vehicleData))
{
vehicleData = new VehicleData(initialDefaultContainerCapacity);
_vehicleDataByEntity[vehicle] = vehicleData;
}
return vehicleData;
}
public void ForgetVehicle(BaseEntity entity)
{
_vehicleDataByEntity.Remove(entity);
}
// If the container is empty, add it to the list for removal.
// Otherwise, add the cleaner component and disable item input.
private void TryRemoveStorage(StorageContainer container, List<StorageContainer> containersToRemove)
{
var containerInventory = container.inventory;
if (containerInventory == null || containerInventory.IsEmpty())
{
containersToRemove.Add(container);
}
else
{
VehicleStorageAfterUnloadCleaner.AddToStorageEntity(container);
container.inventory.SetFlag(ItemContainer.Flag.NoItemInput, true);
}
}
public void Unload()
{
_containersToRemoveReusable.Clear();
// Remove empty containers and restore default container capacities when unloading.
foreach (var (vehicleConfig, vehicleList) in _vehicleTracker.AllSupportedVehicles)
{
foreach (var vehicle in vehicleList)
{
if (!_vehicleDataByEntity.TryGetValue(vehicle, out var vehicleData))
{
// No customization was applied to this vehicle, so nothing to do.
continue;
}
// Restore default container capacity to initial value (best effort, no item loss).
if (vehicleData.InitialDefaultContainerCapacity > 0)
{
var defaultContainer = vehicleConfig.GetDefaultContainer(vehicle);
if (defaultContainer != null)
{
MaybeAdjustCapacity(defaultContainer, vehicleData.InitialDefaultContainerCapacity);
}
}
foreach (var container in vehicleData.Containers)
{
if (container == null || container.IsDestroyed)
continue;
TryRemoveStorage(container, _containersToRemoveReusable);
}
}
}
foreach (var container in _containersToRemoveReusable)
{
if (container == null || container.IsDestroyed)
continue;
container.Kill();
}
_containersToRemoveReusable.Clear();
}
public void RemoveUnwantedContainers(VehicleConfig vehicleConfig, VehicleProfile vehicleProfile, VehicleData vehicleData)
{
_containersToRemoveReusable.Clear();
foreach (var container in vehicleData.Containers)
{
if (container == null || container.IsDestroyed)
continue;
var containerName = container.name;
if (!DoesProfileWantContainer(vehicleProfile, containerName))
{
// This container is not part of the current profile, so remove it.
TryRemoveStorage(container, _containersToRemoveReusable);
}
}
foreach (var container in _containersToRemoveReusable)
{
if (container == null || container.IsDestroyed)
continue;
vehicleData.Containers.Remove(container);
container.Kill();
}
_containersToRemoveReusable.Clear();
}
private bool DoesProfileWantContainer(VehicleProfile vehicleProfile, string containerName)
{
if (vehicleProfile.ValidAdditionalStorage == null)
return false;
foreach (var preset in vehicleProfile.ValidAdditionalStorage.Keys)
{
if (preset.Name == containerName)
return true;
}
return false;
}
}
#endregion
#region Reskin Manager
private class ReskinEvent
{
public BaseEntity Parent;
public VehicleConfig VehicleConfig;
public List<StorageContainer> Containers = new();
public Vector3 Position;
public ReskinEvent WithParent(BaseEntity parent)
{
Parent = parent;
Position = parent?.transform.position ?? Vector3.zero;
return this;
}
public ReskinEvent WithVehicleConfig(VehicleConfig vehicleConfig)
{
VehicleConfig = vehicleConfig;
return this;
}
public void AddContainer(StorageContainer container)
{
Containers.Add(container);
}
public void Reset()
{
Parent = null;
Position = Vector3.zero;
Containers.Clear();
}
}
private class ReskinEventManager
{
// Pool only a single reskin event since usually there will be at most a single event per frame.
private ReskinEvent _pooledReskinEvent;
// Keep track of all reskin events happening in a frame, in case there are multiple.
private List<ReskinEvent> _reskinEvents = new();
public readonly Action CleanupAction;
public ReskinEventManager()
{
CleanupAction = CleanupEvents;
}
public ReskinEvent GetEvent()
{
_pooledReskinEvent ??= new ReskinEvent();
return ReferenceEquals(_pooledReskinEvent.Parent, null)
? _pooledReskinEvent
: new ReskinEvent();
}
public void RecordEvent(ReskinEvent reskinEvent)
{
_reskinEvents.Add(reskinEvent);
}
public void CancelEvent(ReskinEvent reskinEvent)
{
reskinEvent.Reset();
}
public ReskinEvent FindEvent(Vector3 position)
{
foreach (var reskinEvent in _reskinEvents)
{
if (reskinEvent.Position == position)
return reskinEvent;
}
return null;
}
public void CompleteEvent(ReskinEvent reskinEvent)
{
reskinEvent.Reset();
_reskinEvents.Remove(reskinEvent);
}
private void CleanupEvents()
{
if (_reskinEvents.Count == 0)
return;
foreach (var reskinEvent in _reskinEvents)
{
if (reskinEvent.Parent == null || reskinEvent.Parent.IsDestroyed)
{
// The post event wasn't called, and the original parent is gone, so kill the containers.
foreach (var container in reskinEvent.Containers)
{
if (container == null || container.IsDestroyed)
continue;
container.DropItems();
container.Kill();
}
continue;
}
// The reskin event must have been blocked, so reparent the containers to the original parent.
foreach (var container in reskinEvent.Containers)
{
var containerPreset = reskinEvent.VehicleConfig.FindContainerPreset(container.name);
if (containerPreset != null)
{
containerPreset.MoveContainerToParent(container, reskinEvent.Parent);
}
else
{
container.SetParent(reskinEvent.Parent);
}
}
}
_pooledReskinEvent.Reset();
_reskinEvents.Clear();
}
}
#endregion
#region Configuration
private class ContainerPreset
{
[JsonProperty("Prefab")]
public string Prefab;
[JsonProperty("Position")]
public Vector3 Position;
[JsonProperty("RotationAngles", DefaultValueHandling = DefaultValueHandling.Ignore)]
public Vector3 RotationAngles;
[JsonProperty("ParentBone", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string ParentBone;
[JsonIgnore]
public Quaternion Rotation => Quaternion.Euler(RotationAngles);
[JsonIgnore]
public string Name;
public void MoveContainerToParent(StorageContainer container, BaseEntity parent)
{
var transform = container.transform;
transform.localPosition = Position;
transform.localRotation = Rotation;
container.SetParent(parent, ParentBone);
container.SendNetworkUpdate_Position();
}
}
private class VehicleProfile
{
[JsonProperty("PermissionSuffix", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string PermissionSuffix;
[JsonProperty("BuiltInStorageCapacity", DefaultValueHandling = DefaultValueHandling.Ignore)]
[DefaultValue(-1)]
public int BuiltInStorageCapacity = -1;
[JsonProperty("AdditionalStorage", DefaultValueHandling = DefaultValueHandling.Ignore)]
public Dictionary<string, int> AdditionalStorage;
[JsonIgnore]
public Dictionary<ContainerPreset, int> ValidAdditionalStorage { get; private set; }
[JsonIgnore]
public string Permission { get; private set; }
public void Init(VehicleStorage pluginInstance, VehicleConfig vehicleConfig)
{
if (PermissionSuffix != null)
{
Permission = $"{BasePermissionPrefix}.{vehicleConfig.VehicleType}.{PermissionSuffix}";
pluginInstance.permission.RegisterPermission(Permission, pluginInstance);
}
if (AdditionalStorage != null && AdditionalStorage.Count != 0)
{
ValidAdditionalStorage = new Dictionary<ContainerPreset, int>(AdditionalStorage.Count);
foreach (var (presetName, capacity) in AdditionalStorage)
{
if (!vehicleConfig.ContainerPresets.TryGetValue(presetName, out var preset))
{
LogError($"Storage preset {vehicleConfig.VehicleType} -> \"{presetName}\" does not exist.");
continue;
}
if (string.IsNullOrEmpty(preset.Prefab))
{
LogError($"Missing prefab for preset {vehicleConfig.VehicleType} -> \"{presetName}\".");
continue;
}
preset.Name = presetName;
ValidAdditionalStorage[preset] = capacity;
}
}
}
}
private abstract class VehicleConfig
{
[JsonProperty("DefaultProfile")]
public VehicleProfile DefaultProfile = new();
[JsonProperty("ProfilesRequiringPermission")]
public VehicleProfile[] ProfilesRequiringPermission;
[JsonProperty("ContainerPresets", DefaultValueHandling = DefaultValueHandling.Ignore, ObjectCreationHandling = ObjectCreationHandling.Replace)]
public Dictionary<string, ContainerPreset> ContainerPresets;
[JsonIgnore]
public abstract string VehicleType { get; }
[JsonIgnore]
public abstract string PrefabPath { get; }
public virtual StorageContainer GetDefaultContainer(BaseEntity entity) => null;
public virtual ulong GetOwnerId(BaseEntity entity) => entity.OwnerID;
public void Init(VehicleStorage pluginInstance)
{
if (ProfilesRequiringPermission == null)
return;
DefaultProfile.Init(pluginInstance, this);
foreach (var profile in ProfilesRequiringPermission)
{
profile.Init(pluginInstance, this);
}
}
public VehicleProfile GetProfileForVehicle(Permission permissionSystem, BaseEntity vehicle)
{
var ownerId = GetOwnerId(vehicle);
if (ownerId == 0 || (ProfilesRequiringPermission?.Length ?? 0) == 0)
return DefaultProfile;
var ownerIdString = ownerId.ToString();
for (var i = ProfilesRequiringPermission.Length - 1; i >= 0; i--)
{
var profile = ProfilesRequiringPermission[i];
if (profile.Permission != null && permissionSystem.UserHasPermission(ownerIdString, profile.Permission))
return profile;
}
return DefaultProfile;
}
public ContainerPreset FindContainerPreset(string name)
{
return ContainerPresets.GetValueOrDefault(name);
}
}
private class AttackHelicopterConfig : VehicleConfig
{
public override string VehicleType => "attackhelicopter";
public override string PrefabPath => "assets/content/vehicles/attackhelicopter/attackhelicopter.entity.prefab";
}
private class ChinookConfig : VehicleConfig
{
public override string VehicleType => "chinook";
public override string PrefabPath => "assets/prefabs/npc/ch47/ch47.entity.prefab";
}
private class DuoSubmarineConfig : SoloSubmarineConfig
{
public override string VehicleType => "duosub";
public override string PrefabPath => "assets/content/vehicles/submarine/submarineduo.entity.prefab";
}
private class HotAirBalloonConfig : VehicleConfig
{
public override string VehicleType => "hotairballoon";
public override string PrefabPath => "assets/prefabs/deployable/hot air balloon/hotairballoon.prefab";
public override StorageContainer GetDefaultContainer(BaseEntity entity) =>
(entity as HotAirBalloon)?.storageUnitInstance.Get(serverside: true);
}
private class KayakConfig : VehicleConfig
{
public override string VehicleType => "kayak";
public override string PrefabPath => "assets/content/vehicles/boats/kayak/kayak.prefab";
}
private class LocomotiveConfig : VehicleConfig
{
public override string VehicleType => "locomotive";
public override string PrefabPath => "assets/content/vehicles/trains/locomotive/locomotive.entity.prefab";
}
private class MagnetCraneConfig : VehicleConfi
// ... [Code preview truncated due to length. Full source in attached file] ...