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

FREE ItemPuller : 2.0.0 2.0.0

Нет прав для скачивания
C#:
// #define DEBUG_CONTAINER_CHANGES
// #define DEBUG_COROUTINE
// #define DEBUG_CACHE_BUILDING_PRIVILEGE
// #define DEBUG_CACHE_CONTAINERS

using System;
using System.Collections;
using System.Collections.Generic;
using Oxide.Core;
using Oxide.Core.Plugins;
using UnityEngine;

using CanUseContainerDelegate = System.Func<Oxide.Core.Plugins.Plugin, BasePlayer, ItemContainer, bool>;
using AddContainerDelegate = System.Action<Oxide.Core.Plugins.Plugin, BasePlayer, IItemContainerEntity, ItemContainer, System.Func<Oxide.Core.Plugins.Plugin, BasePlayer, ItemContainer, bool>>;
using RemoveContainerDelegate = System.Action<Oxide.Core.Plugins.Plugin, BasePlayer, ItemContainer>;

namespace Oxide.Plugins;

[Info("Item Puller", "WhiteThunder", "2.0.0")]
[Description("Gives you the ability to pull items from containers")]
internal class ItemPuller : CovalencePlugin
{
    #region Fields

    [PluginReference]
    private Plugin ItemRetriever;

    private const string permUse = "itempuller.use";
    private ItemRetrieverApi _itemRetrieverApi;
    private readonly BuildingPrivilegeProvider _buildingPrivilegeProvider;
    private readonly ContainerProvider _containerProvider;
    private readonly PlayerStateManager _playerStateManager;
    private const int InitialCacheSize = 32;

    // TODO: Make these configurable.
    private const float CacheDuration = 5;
    private const float PositionTolerance = 10;
    private const float MinCheckIntervalSeconds = 5f;

    public ItemPuller()
    {
        _buildingPrivilegeProvider = new BuildingPrivilegeProvider(CacheDuration, PositionTolerance);
        _containerProvider = new ContainerProvider(CacheDuration);
        _playerStateManager = new PlayerStateManager(this, _buildingPrivilegeProvider, _containerProvider, MinCheckIntervalSeconds);
    }

    #endregion

    #region Hooks

    private void Init()
    {
        LogWarning("Item Puller 2.0 is in pre-release. Do not expect a polished experience.");
        permission.RegisterPermission(permUse, this);
    }

    private void OnServerInitialized()
    {
        timer.Every(300, () =>
        {
            _buildingPrivilegeProvider.ClearExpired();
            _containerProvider.ClearExpired();
            _playerStateManager.ClearInvalid();
        });

        _playerStateManager.OnServerInitialized();

        if (ItemRetriever == null)
        {
            LogError($"{nameof(ItemRetriever)} is not installed. This plugin will not function until {nameof(ItemRetriever)} loads.");
        }
        else
        {
            HandleItemRetrieverLoaded();
        }
    }

    private void Unload()
    {
        _buildingPrivilegeProvider.Unload();
        _containerProvider.Unload();
        _playerStateManager.Unload();
    }

    private void OnPluginLoaded(Plugin plugin)
    {
        if (plugin.Name == nameof(ItemRetriever))
        {
            HandleItemRetrieverLoaded();
        }
    }

    private void OnPluginUnloaded(Plugin plugin)
    {
        if (plugin.Name == nameof(ItemRetriever))
        {
            HandleItemRetrieverUnloaded();
        }
    }

    #endregion

    #region Item Retriever

    private void CacheItemRetrieverApi()
    {
        _itemRetrieverApi = new ItemRetrieverApi(
            this,
            (_, player, container) => _playerStateManager.CanUseContainer(player, container),
            ItemRetriever.Call("API_GetApi") as Dictionary<string, object>
        );

        if (_itemRetrieverApi == null)
        {
            LogError("Error getting ItemRetriever API.");
        }
    }

    private class ItemRetrieverApi
    {
        private readonly ItemPuller _plugin;
        private readonly List<ItemContainer> _tempContainerList = new();
        private AddContainerDelegate _addContainer { get; }
        private RemoveContainerDelegate _removeContainer { get; }
        private CanUseContainerDelegate _canUseContainer;

        public ItemRetrieverApi(ItemPuller plugin, CanUseContainerDelegate canUseContainer, Dictionary<string, object> apiDict)
        {
            _plugin = plugin;
            _canUseContainer = canUseContainer;
            _addContainer = apiDict[nameof(AddContainer)] as AddContainerDelegate;
            _removeContainer = apiDict[nameof(RemoveContainer)] as RemoveContainerDelegate;
        }

        public void AddContainer(BasePlayer player, IItemContainerEntity containerEntity)
        {
            _tempContainerList.Clear();
            containerEntity.GetAllInventories(_tempContainerList);

            foreach (var container in _tempContainerList)
            {
                _addContainer(_plugin, player, containerEntity, container, _canUseContainer);
                #if DEBUG_CONTAINER_CHANGES
                LogWarning($"Added container [{(containerEntity as BaseEntity).ShortPrefabName}] for player [{player.displayName}]");
                #endif
            }
        }

        public void RemoveContainer(BasePlayer player, IItemContainerEntity containerEntity)
        {
            _tempContainerList.Clear();
            containerEntity.GetAllInventories(_tempContainerList);

            foreach (var container in _tempContainerList)
            {
                _removeContainer(_plugin, player, container);
                #if DEBUG_CONTAINER_CHANGES
                LogWarning($"Removed container [{(containerEntity as BaseEntity).ShortPrefabName}] for player [{player.displayName}]");
                #endif
            }
        }
    }

    #endregion

    #region Building Privilege Provider

    private class BuildingPrivilegeProvider
    {
        public struct CacheEntry
        {
            public readonly Vector3 LastPosition;
            public readonly BuildingPrivlidge BuildingPrivilege;

            public CacheEntry(Vector3 lastPosition, BuildingPrivlidge buildingPrivilege)
            {
                LastPosition = lastPosition;
                BuildingPrivilege = buildingPrivilege;
            }
        }

        private readonly Cache<BasePlayer, CacheEntry> _cache;

        public BuildingPrivilegeProvider(float cacheDuration, float positionTolerance)
        {
            var positionToleranceSquared = positionTolerance * positionTolerance;

            _cache = new Cache<BasePlayer, CacheEntry>(
                loader: player =>
                {
                    var nearestBuildingPrivilege = player.GetNearestBuildingPrivilege(true);
                    #if DEBUG_CACHE_BUILDING_PRIVILEGE
                    LogWarning($"Building privilege cache populated for player [{player.displayName}]: [{(nearestBuildingPrivilege is null ? "null" : "object")}]");
                    #endif
                    return new CacheEntry(player.transform.position, nearestBuildingPrivilege);
                },
                cacheDuration: cacheDuration,
                initialSize: InitialCacheSize,
                isValid: (player, entry) =>
                {
                    var isValid = (player.transform.position - entry.LastPosition).sqrMagnitude <= positionToleranceSquared;
                    #if DEBUG_CACHE_BUILDING_PRIVILEGE
                    LogWarning($"Building privilege cache is [{(isValid ? "valid" : "invalid")}] for player [{player.displayName}]");
                    #endif
                    return isValid;
                }
            );
        }

        public void ClearExpired()
        {
            _cache.ClearExpired();
        }

        public void Unload()
        {
            _cache.Clear();
        }

        public BuildingPrivlidge GetBuildingPrivilege(BasePlayer player)
        {
            return _cache.Get(player).BuildingPrivilege;
        }
    }

    #endregion

    #region Tracked Coroutines

    private class TrackedCoroutine : IEnumerator
    {
        private readonly Plugin _plugin;
        private IEnumerator _inner;
        private TrackedCoroutine _innerTracked;

        public TrackedCoroutine(Plugin plugin, IEnumerator inner = null)
        {
            _plugin = plugin;
            _inner = inner;
        }

        public object Current
        {
            get
            {
                return _inner.Current switch
                {
                    TrackedCoroutine or WaitForSeconds or WaitForSecondsRealtime or WaitForEndOfFrame or WaitForFixedUpdate => _inner.Current,
                    IEnumerator enumerator => GetTrackedCoroutine(enumerator),
                    _ => _inner.Current,
                };
            }
        }

        public bool MoveNext()
        {
            bool result;
            _plugin.TrackStart();

            try
            {
                result = _inner.MoveNext();
            }
            finally
            {
                _plugin.TrackEnd();
            }

            return result;
        }

        public void Reset()
        {
            throw new NotImplementedException();
        }

        public TrackedCoroutine WithEnumerator(IEnumerator inner)
        {
            _inner = inner;
            return this;
        }

        private TrackedCoroutine GetTrackedCoroutine(IEnumerator enumerator)
        {
            _innerTracked ??= new TrackedCoroutine(_plugin);
            return _innerTracked.WithEnumerator(enumerator);
        }
    }

    #endregion

    #region Container Provider

    private class ContainerProvider
    {
        private Cache<BuildingPrivlidge, List<IItemContainerEntity>> _cache;

        public ContainerProvider(float cacheDuration)
        {
            _cache = new Cache<BuildingPrivlidge, List<IItemContainerEntity>>(
                loader: buildingPrivilege =>
                {
                    var list = Facepunch.Pool.Get<List<IItemContainerEntity>>();
                    PopulateContainerList(buildingPrivilege, list);
                    #if DEBUG_CACHE_CONTAINERS
                    LogWarning($"Building privilege cache populated with [{list.Count}] containers.");
                    #endif
                    return list;
                },
                cacheDuration: cacheDuration,
                initialSize: InitialCacheSize,
                onRemove: (_, list) =>
                {
                    #if DEBUG_CACHE_CONTAINERS
                    LogWarning($"Released list to pool for building privilege cache with [{list.Count}] containers.");
                    #endif
                    Facepunch.Pool.FreeUnmanaged(ref list);
                }
            );
        }

        public void ClearExpired()
        {
            _cache.ClearExpired();
            #if DEBUG_CACHE_CONTAINERS
            LogWarning($"Cleared expired building privilege cache.");
            #endif
        }

        public void Unload()
        {
            _cache.Clear();
        }

        public List<IItemContainerEntity> GetContainers(BuildingPrivlidge buildingPrivilege)
        {
            return _cache.Get(buildingPrivilege);
        }

        private void PopulateContainerList(BuildingPrivlidge buildingPrivilege, List<IItemContainerEntity> containerEntityList)
        {
            foreach (var decayEntity in buildingPrivilege.GetBuilding().decayEntities)
            {
                if (decayEntity is not IItemContainerEntity containerEntity)
                    continue;

                containerEntityList.Add(containerEntity);
            }
        }
    }

    #endregion

    #region Player State

    private class PlayerStateManager
    {
        private readonly struct PlayerState
        {
            public readonly List<IItemContainerEntity> ContainerEntityList;

            public PlayerState()
            {
                ContainerEntityList = Facepunch.Pool.Get<List<IItemContainerEntity>>();
            }

            public void Dispose()
            {
                var containerList = ContainerEntityList;
                Facepunch.Pool.FreeUnmanaged(ref containerList);
            }
        }

        private class EmptyComponent : MonoBehaviour {}

        private readonly ItemPuller _plugin;
        private readonly BuildingPrivilegeProvider _buildingPrivilegeProvider;
        private readonly ContainerProvider _containerProvider;
        private readonly float _waitSeconds;

        private readonly Queue<BasePlayer> _queuedPlayers = new(InitialCacheSize);
        private readonly Dictionary<BasePlayer, PlayerState> _playerStates = new(InitialCacheSize);
        private readonly TrackedCoroutine _trackedCoroutine;

        private EmptyComponent _component;

        public PlayerStateManager(ItemPuller plugin, BuildingPrivilegeProvider buildingPrivilegeProvider, ContainerProvider containerProvider, float waitSeconds)
        {
            _plugin = plugin;
            _buildingPrivilegeProvider = buildingPrivilegeProvider;
            _containerProvider = containerProvider;
            _waitSeconds = waitSeconds;
            _trackedCoroutine = new TrackedCoroutine(_plugin);
        }

        public void OnServerInitialized()
        {
            _component = new GameObject().AddComponent<EmptyComponent>();
        }

        public void HandleItemRetrieverLoaded()
        {
            StartRoutine();
        }

        public void HandleItemRetrieverUnloaded()
        {
            StopRoutine();
        }

        public void Unload()
        {
            if (_component != null)
            {
                _component.StopAllCoroutines();
                UnityEngine.Object.Destroy(_component.gameObject);
            }

            foreach (var playerState in _playerStates.Values)
            {
                playerState.Dispose();
            }
        }

        public void ClearInvalid()
        {
            List<BasePlayer> keysToRemove = null;

            foreach (var (player, state) in _playerStates)
            {
                if (player == null || !player.IsConnected)
                {
                    foreach (var containerEntity in state.ContainerEntityList)
                    {
                        _plugin._itemRetrieverApi.RemoveContainer(player, containerEntity);
                    }

                    state.Dispose();
                    keysToRemove ??= Facepunch.Pool.Get<List<BasePlayer>>();
                    keysToRemove.Add(player);
                }
            }

            if (keysToRemove != null)
            {
                foreach (var key in keysToRemove)
                {
                    _playerStates.Remove(key);
                }

                Facepunch.Pool.FreeUnmanaged(ref keysToRemove);
            }
        }

        public bool CanUseContainer(BasePlayer player, ItemContainer container)
        {
            if (container.entityOwner is not IItemContainerEntity containerEntity)
                return false;

            var buildingPrivilege = _buildingPrivilegeProvider.GetBuildingPrivilege(player);
            if (buildingPrivilege == null || !buildingPrivilege.IsAuthed(player))
                return false;

            return _containerProvider.GetContainers(buildingPrivilege)?.Contains(containerEntity) ?? false;
        }

        // private PlayerState EnsurePlayerState(BasePlayer player)
        // {
        //     if (!_playerStates.TryGetValue(player, out var state))
        //     {
        //         state = new PlayerState();
        //         _playerStates[player] = state;
        //     }
        //
        //     return state;
        // }

        private void StartRoutine()
        {
            if (_component != null)
            {
                _component.StartCoroutine(_trackedCoroutine.WithEnumerator(Routine()));
            }
        }

        private void StopRoutine()
        {
            if (_component != null)
            {
                _component.StopAllCoroutines();
            }
        }

        private IEnumerator Routine()
        {
            var timeSinceQueueStarted = new TimeSince();
            var listComparer = new ListComparer<IItemContainerEntity>();
            var containersToAdd = new List<IItemContainerEntity>();
            var containersToRemove = new List<IItemContainerEntity>();

            while (true)
            {
                if (_queuedPlayers.Count == 0)
                {
                    timeSinceQueueStarted = 0;

                    foreach (var player in BasePlayer.activePlayerList)
                    {
                        _queuedPlayers.Enqueue(player);
                    }

                    yield return null;
                }

                while (_queuedPlayers.Count > 0)
                {
                    var player = _queuedPlayers.Dequeue();
                    if (!CanPlayerInteract(player))
                        continue;

                    var hasState = _playerStates.TryGetValue(player, out var playerState);
                    if (!hasState)
                    {
                        playerState = new PlayerState();
                    }

                    var newContainers = GetContainers(player);
                    listComparer.CompareLists(playerState.ContainerEntityList, newContainers, containersToAdd, containersToRemove);

                    foreach (var containerEntity in containersToAdd)
                    {
                        _plugin._itemRetrieverApi.AddContainer(player, containerEntity);
                    }

                    foreach (var containerEntity in containersToRemove)
                    {
                        _plugin._itemRetrieverApi.RemoveContainer(player, containerEntity);
                    }

                    if (containersToAdd.Count > 0 || containersToRemove.Count > 0)
                    {
                        playerState.ContainerEntityList.Clear();
                        if (newContainers?.Count > 0)
                        {
                            playerState.ContainerEntityList.AddRange(newContainers);
                        }

                        if (playerState.ContainerEntityList.Count == 0)
                        {
                            _playerStates.Remove(player);
                            playerState.Dispose();
                        }
                        else if (!hasState)
                        {
                            _playerStates[player] = playerState;
                        }
                    }

                    yield return null;
                }

                var seconds = Mathf.Max(_waitSeconds - timeSinceQueueStarted, 0f);
                #if DEBUG_COROUTINE
                LogWarning($"Waiting for [{seconds}] seconds before processing players again.");
                #endif
                yield return CoroutineEx.waitForSecondsRealtime(seconds);
            }
        }

        private List<IItemContainerEntity> GetContainers(BasePlayer player)
        {
            // TODO: Optimize permission lookup
            // TODO: Check player configuration
            if (!_plugin.permission.UserHasPermission(player.UserIDString, permUse))
                return null;

            var buildingPrivilege = _buildingPrivilegeProvider.GetBuildingPrivilege(player);
            if (buildingPrivilege == null || !buildingPrivilege.IsAuthed(player))
                return null;

            return _containerProvider.GetContainers(buildingPrivilege);
        }
    }

    private class ListComparer<T>
    {
        private readonly HashSet<T> set1 = new();
        private readonly HashSet<T> set2 = new();

        public void CompareLists(List<T> list1, List<T> list2, List<T> itemsToAdd, List<T> itemsToRemove)
        {
            set1.Clear();
            set2.Clear();

            itemsToAdd.Clear();
            itemsToRemove.Clear();

            var isList1Empty = list1 == null || list1.Count == 0;
            var isList2Empty = list2 == null || list2.Count == 0;

            if (isList1Empty && isList2Empty)
                return;

            if (isList1Empty)
            {
                itemsToAdd.AddRange(list2);
                return;
            }

            if (isList2Empty)
            {
                itemsToRemove.AddRange(list1);
                return;
            }

            foreach (var item in list1)
            {
                set1.Add(item);
            }

            foreach (var item in list2)
            {
                set2.Add(item);
            }

            foreach (var item in set2)
            {
                if (!set1.Contains(item))
                {
                    itemsToAdd.Add(item);
                }
            }

            foreach (var item in set1)
            {
                if (!set2.Contains(item))
                {
                    itemsToRemove.Add(item);
                }
            }
        }
    }

    #endregion

    #region Cache

    private class Cache<TKey, TValue>
    {
        private struct CacheEntry
        {
            public TValue Value;
            public RealTimeSince TimeSinceCached;
        }

        private Func<TKey, TValue> Loader;
        private Func<TKey, TValue, bool> _isValid;
        private Action<TKey, TValue> _onRemove;
        private float _cacheDuration;
        private Dictionary<TKey, CacheEntry> _cache;

        public Cache(
            Func<TKey, TValue> loader,
            float cacheDuration,
            int initialSize = InitialCacheSize,
            Func<TKey, TValue, bool> isValid = null,
            Action<TKey, TValue> onRemove = null
        )
        {
            Loader = loader;
            _cacheDuration = cacheDuration;
            _cache = new Dictionary<TKey, CacheEntry>(initialSize);
            _isValid = isValid;
            _onRemove = onRemove;
        }

        public TValue Get(TKey key)
        {
            if (!_cache.TryGetValue(key, out var entry))
            {
                entry = new CacheEntry
                {
                    Value = Loader(key),
                    TimeSinceCached = 0,
                };
                _cache[key] = entry;
            }
            else if (!IsValid(key, entry))
            {
                _onRemove?.Invoke(key, entry.Value);
                entry = new CacheEntry
                {
                    Value = Loader(key),
                    TimeSinceCached = 0,
                };
                _cache[key] = entry;
            }

            return entry.Value;
        }

        public void Clear()
        {
            foreach (var (key, entry) in _cache)
            {
                _onRemove?.Invoke(key, entry.Value);
            }

            _cache.Clear();
        }

        public void ClearExpired()
        {
            List<TKey> keysToRemove = null;

            foreach (var (key, value) in _cache)
            {
                if (IsExpired(value))
                {
                    keysToRemove ??= Facepunch.Pool.Get<List<TKey>>();
                    keysToRemove.Add(key);
                }
            }

            if (keysToRemove != null)
            {
                foreach (var key in keysToRemove)
                {
                    _cache.Remove(key);
                }

                Facepunch.Pool.FreeUnmanaged(ref keysToRemove);
            }
        }

        private bool IsExpired(CacheEntry cacheEntry)
        {
            return cacheEntry.TimeSinceCached > _cacheDuration;
        }

        private bool IsValid(TKey key, CacheEntry cacheEntry)
        {
            if (IsExpired(cacheEntry))
                return false;

            return _isValid == null || _isValid(key, cacheEntry.Value);
        }
    }

    #endregion

    #region Helpers

    public static void LogWarning(string message) => Interface.Oxide.LogWarning($"[Item Puller] {message}");
    public static void LogError(string message) => Interface.Oxide.LogError($"[Item Puller] {message}");

    private static void MarkInventoryDirty(BasePlayer player)
    {
        player.inventory?.containerMain?.MarkDirty();
    }

    private static void UpdatePlayerInventories()
    {
        foreach (var player in BasePlayer.activePlayerList)
        {
            player.Invoke(() => MarkInventoryDirty(player), UnityEngine.Random.Range(0f, 5f));
        }
    }

    private static bool CanPlayerInteract(BasePlayer player)
    {
        return player != null
               && player.IsConnected
               && !player.IsRestrained
               && !player.IsSleeping()
               && !player.IsWounded()
               && !player.IsIncapacitated()
               && !player.IsDead()
               && !player.IsSpectating();
    }

    private void HandleItemRetrieverLoaded()
    {
        CacheItemRetrieverApi();
        _playerStateManager.HandleItemRetrieverLoaded();
    }

    private void HandleItemRetrieverUnloaded()
    {
        _itemRetrieverApi = null;
        UpdatePlayerInventories();
        _playerStateManager.HandleItemRetrieverUnloaded();
    }

    #endregion
}

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