C#:
using System;
using System.Collections.Generic;
using System.Linq;
using Oxide.Plugins;
using Newtonsoft.Json;
using UnityEngine;
using Oxide.Game.Rust.Cui;
using Oxide.Core;
using Oxide.Core.Libraries;
namespace Oxide.Plugins
{
// --- Main.cs ---
[Info("CompostHelper", "HiroOfCanton", "1.0.207")]
[Description("Smart fertilizer distribution system for Rust composters")]
public class CompostHelper : RustPlugin
{
private ComposterManager _composterManager;
private UIManager _uiManager;
#region Localization
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
[Consts.LANG_NO_PERMISSION] = "You don't have permission to use this command.",
[Consts.LANG_INVALID_COMMAND] = "Invalid spread command",
[Consts.LANG_INVALID_COMPOSTER] = "Invalid composter ID",
[Consts.LANG_COMPOSTER_NOT_FOUND] = "Composter not found or has been destroyed",
[Consts.LANG_TOO_FAR_AWAY] = "You must be closer to the composter to use the Spread button",
[Consts.LANG_DISTRIBUTE_SUCCESS] = "Distributed {0}/{1} fertilizer to {2} planters",
[Consts.LANG_DISTRIBUTE_FAILED] = "Found {0} fertilizer but couldn't distribute - {1}",
[Consts.LANG_FERTILIZER_REMAINING] = "{0} fertilizer stored back in composter",
[Consts.LANG_NO_FERTILIZER] = "{0}",
[Consts.LANG_PROCESS_FAILED] = "Failed to process composter"
}, this);
}
public string GetMessage(string key, BasePlayer player, params object[] args)
{
var message = lang.GetMessage(key, this, player?.UserIDString);
return args.Length > 0 ? string.Format(message, args) : message;
}
#endregion
#region Configuration
protected override void LoadDefaultConfig()
{
PluginConfig.Instance = new PluginConfig();
Puts("Created default configuration");
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
PluginConfig.Instance = Config.ReadObject<PluginConfig>();
if (PluginConfig.Instance == null)
{
LoadDefaultConfig();
}
else if (PluginConfig.Instance.ValidateAndCorrect())
{
SaveConfig();
}
}
catch
{
LoadDefaultConfig();
}
}
protected override void SaveConfig()
{
Config.WriteObject(PluginConfig.Instance);
}
#endregion
#region Plugin Lifecycle
private void Init()
{
permission.RegisterPermission(Consts.PERMISSION_USE, this);
_composterManager = new ComposterManager(new FertilizerDistributor());
_uiManager = new UIManager(this, _composterManager);
StartupDiagnostics.Run(permission);
}
private void Unload()
{
_uiManager.Cleanup();
}
#endregion
#region Oxide Hooks
private void OnLootEntity(BasePlayer player, BaseEntity entity)
{
if (player == null || entity == null) return;
_uiManager.OnLootEntityOpened(player, entity);
}
private void OnLootEntityEnd(BasePlayer player, BaseCombatEntity entity)
{
if (player == null || entity == null) return;
_uiManager.OnLootEntityClosed(player, entity);
}
private void OnPlayerDisconnected(BasePlayer player, string reason)
{
if (player == null) return;
_uiManager.OnPlayerDisconnected(player);
}
#endregion
#region Console Commands
[ConsoleCommand(Consts.CMD_SPREAD)]
private void SpreadCommand(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, Consts.PERMISSION_USE))
{
SendReply(player, GetMessage(Consts.LANG_NO_PERMISSION, player));
return;
}
_uiManager.HandleSpreadCommand(player, arg.Args != null ? arg.Args.Select(a => a.ToString()).ToArray() : Array.Empty<string>());
}
#endregion
public bool HasPermission(BasePlayer player, string perm)
{
return player != null && permission.UserHasPermission(player.UserIDString, perm);
}
}
// --- Configuration/PluginConfig.cs ---
public class PluginConfig
{
public static PluginConfig Instance { get; set; } = new PluginConfig();
[JsonProperty("Maximum Distribution Range")]
public float MaxDistributionRange { get; set; } = 50.0f;
[JsonProperty("Supported Planter Types")]
public string[] SupportedPlanterTypes { get; set; } = new string[]
{
"planter.large.deployed",
"planter.small.deployed",
"planter.triangle.deployed",
"plantpot.single.deployed",
"bathtub.planter.deployed",
"minecart.planter.deployed"
};
[JsonProperty("Fertilizer Item Shortname")]
public string FertilizerItem { get; set; } = "fertilizer";
[JsonProperty("Debug Mode")]
public bool Debug { get; set; } = false;
[JsonIgnore]
private HashSet<string> _planterTypes;
public bool ValidateAndCorrect()
{
bool changed = false;
if (MaxDistributionRange <= 0)
{
MaxDistributionRange = 50.0f;
changed = true;
}
if (SupportedPlanterTypes == null || SupportedPlanterTypes.Length == 0)
{
SupportedPlanterTypes = new[]
{
"planter.large.deployed", "planter.small.deployed", "planter.triangle.deployed",
"plantpot.single.deployed", "bathtub.planter.deployed", "minecart.planter.deployed"
};
changed = true;
}
if (string.IsNullOrEmpty(FertilizerItem))
{
FertilizerItem = "fertilizer";
changed = true;
}
return changed;
}
public bool IsSupportedPlanterType(string shortname)
{
if (string.IsNullOrEmpty(shortname) || SupportedPlanterTypes == null) return false;
if (_planterTypes == null)
_planterTypes = new HashSet<string>(SupportedPlanterTypes, StringComparer.OrdinalIgnoreCase);
return _planterTypes.Contains(shortname);
}
}
// --- Data/PlanterInfo.cs ---
public class PlanterInfo
{
public BaseEntity Entity { get; set; }
public ItemContainer Container { get; set; }
public int FertilizerCount { get; set; }
public PlanterInfo(BaseEntity entity)
{
Entity = entity;
Container = entity.GetComponent<StorageContainer>()?.inventory;
UpdateInfo();
}
public void UpdateInfo()
{
if (Entity == null || Entity.IsDestroyed || Container == null) return;
FertilizerCount = 0;
foreach (var item in Container.itemList)
{
if (item.info.shortname == PluginConfig.Instance.FertilizerItem)
FertilizerCount += item.amount;
}
}
public int RemoveAllFertilizer()
{
if (Container == null || Entity == null || Entity.IsDestroyed) return 0;
int total = 0;
var items = new List<Item>();
foreach (var item in Container.itemList)
{
if (item.info.shortname != PluginConfig.Instance.FertilizerItem) continue;
items.Add(item);
total += item.amount;
}
foreach (var item in items)
{
item.RemoveFromContainer();
item.Remove();
}
if (total > 0) UpdateInfo();
return total;
}
public bool AddFertilizer(int amount = 1)
{
if (Container == null || Entity == null || Entity.IsDestroyed) return false;
var itemDefinition = ItemManager.FindItemDefinition(PluginConfig.Instance.FertilizerItem);
if (itemDefinition == null) return false;
foreach (var existingItem in Container.itemList)
{
if (existingItem.info.shortname != PluginConfig.Instance.FertilizerItem) continue;
if (existingItem.amount >= existingItem.MaxStackable()) continue;
int canAdd = Math.Min(amount, existingItem.MaxStackable() - existingItem.amount);
existingItem.amount += canAdd;
existingItem.MarkDirty();
amount -= canAdd;
if (amount <= 0)
{
UpdateInfo();
return true;
}
}
while (amount > 0)
{
var item = ItemManager.Create(itemDefinition, Math.Min(amount, itemDefinition.stackable));
if (item == null) return false;
bool placed = false;
for (int slot = 0; slot < Container.capacity; slot++)
{
if (Container.GetSlot(slot) != null) continue;
if (item.MoveToContainer(Container, slot))
{
amount -= item.amount;
placed = true;
break;
}
}
if (!placed)
{
item.Remove();
return false;
}
}
UpdateInfo();
return true;
}
}
// --- Managers/ComposterManager.cs ---
public class ComposterManager
{
private readonly FertilizerDistributor _fertilizerDistributor;
public ComposterManager(FertilizerDistributor fertilizerDistributor)
{
_fertilizerDistributor = fertilizerDistributor;
}
public ProcessResult ProcessComposterManually(Composter composter)
{
if (composter == null || composter.IsDestroyed) return null;
var result = new ProcessResult();
var container = composter.inventory;
if (container == null)
{
result.StopReason = "No inventory container";
return result;
}
int total = 0;
var items = new List<Item>(container.itemList);
foreach (var item in items)
{
total += item.amount;
item.Remove();
}
var planters = _fertilizerDistributor.GetPlantersNear(composter.transform.position);
foreach (var planter in planters)
{
total += planter.RemoveAllFertilizer();
}
if (total == 0)
{
result.StopReason = "No fertilizer to distribute";
return result;
}
int remaining = _fertilizerDistributor.DistributeFertilizerAndReturnRemainder(
composter.transform.position, total);
result.FertilizerFound = total;
result.FertilizerDistributed = total - remaining;
if (remaining > 0)
{
AddFertilizerToComposter(composter, remaining);
result.FertilizerRemaining = remaining;
}
foreach (var planter in planters)
{
planter.UpdateInfo();
if (planter.FertilizerCount > 0) result.PlantersReceived++;
}
return result;
}
private void AddFertilizerToComposter(Composter composter, int amount)
{
if (composter == null || composter.inventory == null || amount <= 0) return;
var item = ItemUtils.CreateFertilizerItem(amount);
if (item == null) return;
if (!composter.inventory.Insert(item))
{
item.Remove();
}
}
public class ProcessResult
{
public int FertilizerFound { get; set; }
public int FertilizerDistributed { get; set; }
public int FertilizerRemaining { get; set; }
public int PlantersReceived { get; set; }
public string StopReason { get; set; } = string.Empty;
}
}
// --- Managers/FertilizerDistributor.cs ---
public class FertilizerDistributor
{
public int DistributeFertilizerAndReturnRemainder(Vector3 composterPosition, int totalFertilizer)
{
if (totalFertilizer <= 0) return 0;
var planters = PlanterFinder.FindNearbyPlanters(composterPosition);
if (planters.Count == 0) return totalFertilizer;
int remaining = totalFertilizer;
while (remaining > 0 && planters.Count > 0)
{
bool placed = false;
for (int i = 0; i < planters.Count && remaining > 0; i++)
{
if (planters[i].AddFertilizer(1))
{
remaining--;
placed = true;
}
}
if (!placed) break;
planters.Sort((a, b) => a.FertilizerCount.CompareTo(b.FertilizerCount));
}
return remaining;
}
public List<PlanterInfo> GetPlantersNear(Vector3 position)
{
return PlanterFinder.FindNearbyPlanters(position);
}
}
// --- Managers/PlanterFinder.cs ---
public static class PlanterFinder
{
private static readonly int PlanterLayerMask = LayerMask.GetMask("Deployed", "Default");
public static List<PlanterInfo> FindNearbyPlanters(Vector3 position)
{
var planters = new List<PlanterInfo>();
var seen = new HashSet<ulong>();
foreach (var collider in Physics.OverlapSphere(position, PluginConfig.Instance.MaxDistributionRange, PlanterLayerMask))
{
var entity = collider.GetComponentInParent<BaseEntity>();
if (entity == null || entity.IsDestroyed) continue;
if (!PluginConfig.Instance.IsSupportedPlanterType(entity.ShortPrefabName)) continue;
if (!seen.Add(entity.net.ID.Value)) continue;
planters.Add(new PlanterInfo(entity));
}
return planters;
}
}
// --- Managers/UIManager.cs ---
public class UIManager
{
private readonly CompostHelper _plugin;
private readonly ComposterManager _composterManager;
private readonly Dictionary<ulong, string> _activeUIs = new Dictionary<ulong, string>();
public UIManager(CompostHelper plugin, ComposterManager composterManager)
{
_plugin = plugin;
_composterManager = composterManager;
}
public void OnLootEntityOpened(BasePlayer player, BaseEntity entity)
{
if (entity is not Composter composter) return;
CreateSpreadButton(player, composter);
}
public void OnLootEntityClosed(BasePlayer player, BaseEntity entity)
{
if (entity is not Composter) return;
DestroySpreadButton(player);
}
private void CreateSpreadButton(BasePlayer player, Composter composter)
{
if (!_plugin.HasPermission(player, Consts.PERMISSION_USE)) return;
var elements = new CuiElementContainer();
var button = new CuiButton
{
Button = {
Command = $"{Consts.CMD_SPREAD} {composter.net.ID}",
Color = "0.8 0.4 0.2 0.9"
},
RectTransform = {
AnchorMin = "0.70 0.90",
AnchorMax = "0.95 0.98"
},
Text = {
Text = "SPREAD",
FontSize = 12,
Align = TextAnchor.MiddleCenter,
Color = "1 1 1 1"
}
};
elements.Add(button, "Overlay", Consts.UI_SPREAD_BUTTON);
CuiHelper.AddUi(player, elements);
_activeUIs[player.userID] = Consts.UI_SPREAD_BUTTON;
}
private void DestroySpreadButton(BasePlayer player)
{
if (!_activeUIs.TryGetValue(player.userID, out var uiName)) return;
CuiHelper.DestroyUi(player, uiName);
_activeUIs.Remove(player.userID);
}
public void HandleSpreadCommand(BasePlayer player, string[] args)
{
if (!_plugin.HasPermission(player, Consts.PERMISSION_USE))
{
player.ChatMessage(_plugin.GetMessage(Consts.LANG_NO_PERMISSION, player));
return;
}
if (args.Length < 1)
{
player.ChatMessage(_plugin.GetMessage(Consts.LANG_INVALID_COMMAND, player));
return;
}
if (!ulong.TryParse(args[0], out var netId))
{
player.ChatMessage(_plugin.GetMessage(Consts.LANG_INVALID_COMPOSTER, player));
return;
}
var composter = BaseNetworkable.serverEntities.Find(new NetworkableId(netId)) as Composter;
if (composter == null || composter.IsDestroyed)
{
player.ChatMessage(_plugin.GetMessage(Consts.LANG_COMPOSTER_NOT_FOUND, player));
return;
}
if (Vector3.Distance(player.transform.position, composter.transform.position) > 5f)
{
player.ChatMessage(_plugin.GetMessage(Consts.LANG_TOO_FAR_AWAY, player));
return;
}
var result = _composterManager.ProcessComposterManually(composter);
if (result == null)
{
player.ChatMessage(_plugin.GetMessage(Consts.LANG_PROCESS_FAILED, player));
return;
}
if (result.FertilizerFound == 0)
{
player.ChatMessage(_plugin.GetMessage(Consts.LANG_NO_FERTILIZER, player, result.StopReason));
return;
}
player.ChatMessage(result.FertilizerDistributed > 0
? _plugin.GetMessage(Consts.LANG_DISTRIBUTE_SUCCESS, player, result.FertilizerDistributed, result.FertilizerFound, result.PlantersReceived)
: _plugin.GetMessage(Consts.LANG_DISTRIBUTE_FAILED, player, result.FertilizerFound, result.StopReason));
if (result.FertilizerRemaining > 0)
{
player.ChatMessage(_plugin.GetMessage(Consts.LANG_FERTILIZER_REMAINING, player, result.FertilizerRemaining));
}
}
public void OnPlayerDisconnected(BasePlayer player)
{
_activeUIs.Remove(player.userID);
}
public void Cleanup()
{
foreach (var kvp in _activeUIs)
{
var player = BasePlayer.FindByID(kvp.Key);
if (player != null && !player.IsDestroyed)
{
CuiHelper.DestroyUi(player, kvp.Value);
}
}
_activeUIs.Clear();
}
}
// --- Utils/Consts.cs ---
internal static class Consts
{
public const string LOG_PREFIX = "CompostHelper";
public const string PERMISSION_USE = "composthelper.use";
public const string UI_SPREAD_BUTTON = "ComposterButton";
public const string CMD_SPREAD = "composter.spread";
public const string LANG_NO_PERMISSION = "NoPermission";
public const string LANG_INVALID_COMMAND = "InvalidCommand";
public const string LANG_INVALID_COMPOSTER = "InvalidComposterId";
public const string LANG_COMPOSTER_NOT_FOUND = "ComposterNotFound";
public const string LANG_TOO_FAR_AWAY = "TooFarAway";
public const string LANG_DISTRIBUTE_SUCCESS = "DistributeSuccess";
public const string LANG_DISTRIBUTE_FAILED = "DistributeFailed";
public const string LANG_FERTILIZER_REMAINING = "FertilizerRemaining";
public const string LANG_NO_FERTILIZER = "NoFertilizer";
public const string LANG_PROCESS_FAILED = "ProcessFailed";
}
// --- Utils/ItemUtils.cs ---
public static class ItemUtils
{
public static Item CreateFertilizerItem(int amount = 1)
{
var definition = ItemManager.FindItemDefinition(PluginConfig.Instance.FertilizerItem);
return definition != null ? ItemManager.Create(definition, amount) : null;
}
}
// --- Utils/Log.cs ---
public static class Log
{
public static void Info(string message)
{
if (!PluginConfig.Instance.Debug) return;
Interface.Oxide.LogInfo($"[{Consts.LOG_PREFIX}] {message}");
}
public static void Warning(string message)
{
if (!PluginConfig.Instance.Debug) return;
Interface.Oxide.LogWarning($"[{Consts.LOG_PREFIX}] {message}");
}
public static void Error(string message)
{
Interface.Oxide.LogError($"[{Consts.LOG_PREFIX}] {message}");
}
}
// --- Utils/StartupDiagnostics.cs ---
internal static class StartupDiagnostics
{
public static void Run(Permission permissionSystem)
{
if (!PluginConfig.Instance.Debug) return;
var config = PluginConfig.Instance;
Log.Info("=== CompostHelper Startup ===");
Log.Info($" - Distribution Range: {config.MaxDistributionRange}m");
Log.Info($" - Fertilizer Item: {config.FertilizerItem}");
Log.Info($" - Supported Planters: {config.SupportedPlanterTypes?.Length ?? 0} types");
if (!permissionSystem.PermissionExists(Consts.PERMISSION_USE))
Log.Error($"Permission not registered: {Consts.PERMISSION_USE}");
}
}
}