C#:
using Oxide.Core;
using Oxide.Core.Plugins;
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using Facepunch;
using Newtonsoft.Json;
namespace Oxide.Plugins
{
[Info("UsableHoppers", "Mattffhh", "1.0.2")]
[Description("A plugin that collects nearby loot into a container.")]
public class UsableHoppers : RustPlugin
{
private ConfigData config;
private StoredData storedData;
private Dictionary<ulong, List<ulong>> hoppers = new Dictionary<ulong, List<ulong>>();
private LayerMask hopperLayerMask;
private Dictionary<ulong, float> commandCooldowns = new Dictionary<ulong, float>();
private Dictionary<ulong, StorageContainer> hopperCache = new Dictionary<ulong, StorageContainer>();
private List<SphereEntity> spheres = new List<SphereEntity>();
private const string adminPermission = "usablehoppers.admin";
#region Configuration and Data
private class ConfigData
{
[JsonProperty(PropertyName = "Hopper radius")]
public float HopperRadius { get; set; }
[JsonProperty(PropertyName = "Max items per tick")]
public int MaxItemsPerTick { get; set; }
[JsonProperty(PropertyName = "How often the hopper collects items")]
public float CollectionIntervalSeconds { get; set; }
[JsonProperty(PropertyName = "Debug mode")]
public bool EnableDebug { get; set; }
[JsonProperty(PropertyName = "Max hoppers per players")]
public int MaxHoppersPerPlayer { get; set; }
[JsonProperty(PropertyName = "Command cooldown")]
public float CommandCooldown { get; set; }
[JsonProperty(PropertyName = "Allowed containers for hoppers")]
public List<string> AllowedContainerTypes { get; set; }
[JsonProperty(PropertyName = "Item types allowed")]
public Dictionary<string, bool> ItemTypeSettings { get; set; }
}
private class StoredData
{
public Dictionary<ulong, List<ulong>> SavedHoppers = new Dictionary<ulong, List<ulong>>();
}
protected override void LoadDefaultConfig()
{
ConfigData defaultConfig = new ConfigData
{
CommandCooldown = 0,
HopperRadius = 3.0f,
MaxItemsPerTick = 10,
CollectionIntervalSeconds = 5.0f,
EnableDebug = false,
MaxHoppersPerPlayer = 5,
AllowedContainerTypes = new List<string> { "woodbox_deployed", "box.wooden.large", "furnace", "storage_barrel_c", "storage_barrel_b" },
ItemTypeSettings = new Dictionary<string, bool>
{
{ "weapon", true },
{ "resources", true },
{ "medical", true },
{ "construction", true },
{ "attire", true },
{ "tool", true },
{ "food", true },
{ "ammunition", true },
{ "traps", true },
{ "misc", true },
{ "component", true },
{ "blueprint", true },
{ "items", true },
{ "electrical", true },
{ "fun", true }
}
};
Config.WriteObject(defaultConfig, true);
}
private void LoadConfigValues()
{
config = Config.ReadObject<ConfigData>() ?? new ConfigData();
if (config.CommandCooldown < 0)
{
Puts("CommandCooldown in config was set to a negative value. Automatically setting the value to 0.");
config.CommandCooldown = 0;
}
}
private void SaveData(bool forceSave = false)
{
if (storedData == null)
{
storedData = new StoredData();
}
storedData.SavedHoppers = hoppers;
if (forceSave)
{
Interface.Oxide.DataFileSystem.WriteObject("UsableHoppers", storedData);
}
else
{
timer.Once(300f, () => Interface.Oxide.DataFileSystem.WriteObject("UsableHoppers", storedData));
}
}
private void LoadData()
{
storedData = Interface.Oxide.DataFileSystem.ReadObject<StoredData>("UsableHoppers") ?? new StoredData();
hoppers = storedData.SavedHoppers ?? new Dictionary<ulong, List<ulong>>();
}
#endregion
private void Init()
{
permission.RegisterPermission(adminPermission, this);
}
private void OnServerInitialized()
{
hopperLayerMask = LayerMask.GetMask("Physics Debris");
LoadConfigValues();
LoadData();
timer.Every(config.CollectionIntervalSeconds, HopperScan);
}
private void OnServerSave()
{
SaveData(true);
}
private void Unload()
{
hopperCache.Clear();
foreach (var sphere in spheres)
{
if (sphere != null && !sphere.IsDestroyed)
{
sphere.Kill();
}
}
SaveData(true);
}
#region Chat Commands
[ChatCommand("uh")]
private void ListCommands(BasePlayer player)
{
if (!permission.UserHasPermission(player.UserIDString, adminPermission))
{
player.ChatMessage("[UsableHoppers] commands: " +
"\nuhset - set the container you are looking at to a hopper. " +
"\nuhremove - remove the hopper from the container you are looking at." +
"\nuhlist - shows a list of all your hoppers." +
"\nuhrange - shows the pickup radius of your hoppers.");
}
else
{
player.ChatMessage("[UsableHoppers] commands: " +
"\nuhset - set the container you are looking at to a hopper. " +
"\nuhremove - remove the hopper from the container you are looking at." +
"\nuhlist - shows a list of all your hoppers." +
"\nuhrange - shows the pickup radius of your hoppers." +
"\nuhclearplayer <playername or steamid> - clears the player's hoppers." +
"\nuhclearall - clear all the player's hoppers.");
}
}
[ChatCommand("uhset")]
private void SetHopperCommand(BasePlayer player)
{
if (IsOnCooldown(player, config.CommandCooldown)) return;
var entity = GetLookEntity(player);
if (entity is StorageContainer container)
{
if (!IsAllowedContainer(container.ShortPrefabName))
{
player.ChatMessage($"You cannot set {container.ShortPrefabName} as a hopper.");
return;
}
if (!hoppers.ContainsKey(player.userID))
{
hoppers[player.userID] = new List<ulong>();
}
if (hoppers[player.userID].Count >= config.MaxHoppersPerPlayer)
{
player.ChatMessage($"You have reached the maximum number of hoppers ({config.MaxHoppersPerPlayer}).");
return;
}
if (IsHopperClaimed(container.net.ID.Value))
{
player.ChatMessage("This container is already marked as a hopper by another player.");
return;
}
hoppers[player.userID].Add(container.net.ID.Value);
player.ChatMessage("This container has been marked as a hopper.");
SaveData();
}
else
{
player.ChatMessage("You are not looking at a valid container!");
}
}
[ChatCommand("uhremove")]
private void RemoveHopperCommand(BasePlayer player)
{
if (IsOnCooldown(player, config.CommandCooldown)) return;
var entity = GetLookEntity(player);
if (entity is StorageContainer container)
{
if (!hoppers.ContainsKey(player.userID) || hoppers[player.userID].Count == 0)
{
player.ChatMessage("You don't have any hoppers to remove.");
return;
}
if (hoppers[player.userID].Contains(container.net.ID.Value))
{
hoppers[player.userID].Remove(container.net.ID.Value);
player.ChatMessage("The hopper you were looking at has been removed.");
SaveData();
}
else
{
player.ChatMessage("This container is not marked as a hopper.");
}
}
else
{
player.ChatMessage("You are not looking at a valid container!");
}
}
[ChatCommand("uhlist")]
private void ShowActiveHoppers(BasePlayer player)
{
if (IsOnCooldown(player, config.CommandCooldown)) return;
if (!hoppers.ContainsKey(player.userID) || hoppers[player.userID].Count == 0)
{
player.ChatMessage("You have no active hoppers.");
return;
}
player.ChatMessage($"You have the following hoppers ({hoppers[player.userID].Count} / {config.MaxHoppersPerPlayer}):");
foreach (var hopperID in hoppers[player.userID])
{
var container = FindChestByID(hopperID);
if (container != null)
{
player.ChatMessage($"Hopper {container.ShortPrefabName} at:{container.transform.position}");
Vector3 spherePosition = container.transform.position + new Vector3(0, 1.0f, 0);
CreateSphereEntity(spherePosition, 0.5f, player);
}
}
}
[ChatCommand("uhrange")]
private void DrawHopperRange(BasePlayer player)
{
if (IsOnCooldown(player, config.CommandCooldown)) return;
if (!hoppers.ContainsKey(player.userID) || hoppers[player.userID].Count == 0)
{
player.ChatMessage("You have no active hoppers.");
return;
}
foreach (var hopperID in hoppers[player.userID])
{
var container = FindChestByID(hopperID);
if (container != null)
{
CreateSphereEntity(container.transform.position, config.HopperRadius, player);
}
}
}
[ChatCommand("uhclearall")]
private void ClearAllHoppersCommand(BasePlayer player)
{
if (!permission.UserHasPermission(player.UserIDString, adminPermission))
{
player.ChatMessage("You don't have permission to use this command.");
return;
}
hoppers.Clear();
SaveData();
player.ChatMessage("All hoppers have been cleared.");
Puts("All hoppers have been cleared by an admin.");
}
[ChatCommand("uhclearplayer")]
private void ClearPlayerHoppersCommand(BasePlayer player, string command, string[] args)
{
if (!permission.UserHasPermission(player.UserIDString, adminPermission))
{
player.ChatMessage("You don't have permission to use this command.");
return;
}
if (args.Length != 1)
{
player.ChatMessage("Usage: /uhclearplayer <playername or steamid>");
return;
}
BasePlayer targetPlayer = FindPlayer(args[0]);
if (targetPlayer == null)
{
player.ChatMessage("Player not found.");
return;
}
if (hoppers.ContainsKey(targetPlayer.userID))
{
hoppers.Remove(targetPlayer.userID);
SaveData();
player.ChatMessage($"All hoppers for player {targetPlayer.displayName} have been cleared.");
Puts($"Admin cleared hoppers for player {targetPlayer.displayName}.");
}
else
{
player.ChatMessage("This player has no hoppers.");
}
}
#endregion
private void OnEntityKill(BaseNetworkable entity)
{
if (entity is StorageContainer container)
{
RemoveHopperByChest(container.net.ID.Value);
}
}
private void RemoveHopperByChest(ulong chestID)
{
if (hopperCache.ContainsKey(chestID))
{
hopperCache.Remove(chestID);
}
foreach (var playerHoppers in hoppers)
{
if (playerHoppers.Value.Contains(chestID))
{
playerHoppers.Value.Remove(chestID);
if (playerHoppers.Value.Count == 0)
{
hoppers.Remove(playerHoppers.Key);
}
SaveData();
break;
}
}
}
private bool IsAllowedContainer(string prefabName)
{
return config.AllowedContainerTypes.Contains(prefabName);
}
private bool IsHopperClaimed(ulong hopperID)
{
foreach (var playerHoppers in hoppers.Values)
{
if (playerHoppers.Contains(hopperID))
{
return true;
}
}
return false;
}
private bool IsOnCooldown(BasePlayer player, float cooldown)
{
if (commandCooldowns.TryGetValue(player.userID, out var lastUsed) && Time.realtimeSinceStartup - lastUsed < cooldown)
{
player.ChatMessage("Please wait before using this command again.");
return true;
}
commandCooldowns[player.userID] = Time.realtimeSinceStartup;
return false;
}
private bool IsValidItem(Item item)
{
var itemCategory = item.info.category.ToString().ToLower();
if (config.EnableDebug) Puts($"Item {item.info.displayName.english} belongs to category {itemCategory}");
if (config.ItemTypeSettings.ContainsKey(itemCategory))
{
if (config.EnableDebug) Puts($"Item {item.info.displayName.english} is categorized as {itemCategory} and is {(config.ItemTypeSettings[itemCategory] ? "valid" : "invalid")}");
return config.ItemTypeSettings[itemCategory];
}
if (config.EnableDebug) Puts($"Item {item.info.displayName.english} does not match any valid categories.");
return false;
}
private void CreateSphereEntity(Vector3 position, float radius, BasePlayer player)
{
var sphere = GameManager.server.CreateEntity("assets/bundled/prefabs/modding/events/twitch/br_sphere.prefab", position) as SphereEntity;
if (sphere != null)
{
sphere.currentRadius = radius * 2 - 0.1f;
sphere.enableSaving = false;
sphere.Spawn();
sphere?.LerpRadiusTo(radius * 2, radius * 0.75f);
sphere.SendNetworkUpdate();
spheres.Add(sphere);
timer.Once(10f, () =>
{
if (sphere != null && !sphere.IsDestroyed)
{
spheres.Remove(sphere);
sphere.Kill();
}
});
}
}
private BasePlayer FindPlayer(string nameOrId)
{
return BasePlayer.activePlayerList.FirstOrDefault(player =>
player.UserIDString == nameOrId || player.displayName.ToLower().Contains(nameOrId.ToLower()));
}
private void HopperScan()
{
List<ulong> toRemove = Pool.Get<List<ulong>>();
//List<ulong> toRemove = Pool.GetList<ulong>(); Deprecated function
foreach (var playerHoppers in hoppers)
{
foreach (var hopperID in playerHoppers.Value.ToList())
{
var container = FindChestByID(hopperID);
if (container == null)
{
Puts($"Removed invalid hopper for player {playerHoppers.Key}");
playerHoppers.Value.Remove(hopperID);
if (playerHoppers.Value.Count == 0)
{
toRemove.Add(playerHoppers.Key);
}
}
else
{
ScanForItems(container);
}
}
}
foreach (var userId in toRemove)
{
hoppers.Remove(userId);
}
Pool.FreeUnmanaged(ref toRemove);
SaveData();
}
private void ScanForItems(StorageContainer container)
{
List<BaseEntity> nearbyEntities = Pool.Get<List<BaseEntity>>();
//List<BaseEntity> nearbyEntities = Pool.GetList<BaseEntity>(); Deprecated function
var hopperPosition = container.transform.position;
Vis.Entities(hopperPosition, config.HopperRadius, nearbyEntities, hopperLayerMask);
if (nearbyEntities.Count == 0)
{
if (config.EnableDebug) Puts("No entities found near hopper at " + hopperPosition);
Pool.FreeUnmanaged(ref nearbyEntities);
return;
}
foreach (var entity in nearbyEntities.Take(config.MaxItemsPerTick))
{
Item item = null;
if (entity is DroppedItem droppedItemEntity)
{
item = droppedItemEntity.item;
}
else if (entity is DroppedItemContainer itemContainerEntity)
{
foreach (var containerItem in itemContainerEntity.inventory.itemList)
{
if (IsValidItem(containerItem) && MoveItemToChest(container, containerItem))
{
timer.Once(0.1f, () =>
{
containerItem.Remove();
});
}
}
continue;
}
if (item != null && item.amount > 0 && IsValidItem(item))
{
if (MoveItemToChest(container, item))
{
timer.Once(0.1f, () =>
{
if (!entity.IsDestroyed)
{
entity.Kill();
}
});
}
}
else
{
if (config.EnableDebug && entity.IsDestroyed)
{
Puts($"Entity {entity.ShortPrefabName} is already destroyed.");
}
}
}
Pool.FreeUnmanaged(ref nearbyEntities);
}
private bool MoveItemToChest(StorageContainer container, Item item)
{
return container.inventory.itemList.Count < container.inventory.capacity && item.MoveToContainer(container.inventory);
}
private StorageContainer FindChestByID(ulong entityID)
{
if (hopperCache.TryGetValue(entityID, out var container) && container != null)
{
return container;
}
container = BaseNetworkable.serverEntities
.OfType<StorageContainer>()
.FirstOrDefault(c => c.net.ID.Value == entityID);
if (container != null)
{
hopperCache[entityID] = container;
}
return container;
}
private BaseEntity GetLookEntity(BasePlayer player, float maxDistance = 5f)
{
Vector3 eyesPos = player.eyes.position;
Vector3 direction = player.eyes.HeadForward();
if (Physics.Raycast(eyesPos, direction, out RaycastHit hit, maxDistance))
{
return hit.GetEntity();
}
return null;
}
}
}