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> hoppers = new Dictionary>(); private LayerMask hopperLayerMask; private Dictionary commandCooldowns = new Dictionary(); private Dictionary hopperCache = new Dictionary(); private List spheres = new List(); 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 AllowedContainerTypes { get; set; } [JsonProperty(PropertyName = "Item types allowed")] public Dictionary ItemTypeSettings { get; set; } } private class StoredData { public Dictionary> SavedHoppers = new Dictionary>(); } 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 { "woodbox_deployed", "box.wooden.large", "furnace", "storage_barrel_c", "storage_barrel_b" }, ItemTypeSettings = new Dictionary { { "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() ?? 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("UsableHoppers") ?? new StoredData(); hoppers = storedData.SavedHoppers ?? new Dictionary>(); } #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 - 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(); } 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 "); 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 toRemove = Pool.Get>(); //List toRemove = Pool.GetList(); 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 nearbyEntities = Pool.Get>(); //List nearbyEntities = Pool.GetList(); 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() .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; } } }