Иконка ресурса

FREE VehicleStorage : 3.5.3 3.5.3

Нет прав для скачивания
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] ...

Автор
15FPS
Скачиваний
0
Просмотры
1
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок
Сверху