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
}