C#:
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Oxide.Core;
using System;
using UnityEngine;
using System.Diagnostics;
using SilentOrbit.ProtocolBuffers;
using Oxide.Game.Rust.Cui;
using static ConsoleSystem;
namespace Oxide.Plugins
{
[Info("Auto Composter", "D3m3ntor", "1.0.0")]
[Description("Allow composters to collect dungs from the floor and compost it efficiently by splitting.")]
class AutoComposter : RustPlugin
{
#region Variables
public static AutoComposter _instance;
public const string PermissionKey = "autocomposter.use";
public const string PermissionKeyPerf = "autocomposter.performance";
public List<string> playersEnabled = new List<string>();
public List<string> playersDisabled = new List<string>();
public List<TimeSpan> dungDropPerformanceDebug = new List<TimeSpan>();
#endregion
#region Configuration
public static Configuration _config;
public class Configuration
{
[JsonProperty(PropertyName = "Use Only If Permission")]
public bool UseOnlyIfPermission = false;
[JsonProperty(PropertyName = "Turned on for players by default")]
public bool TurnedOnByDefault = false;
[JsonProperty(PropertyName = "Collect in radius")]
public float CollectInRadius = 20;
[JsonProperty(PropertyName = "Frequency of Composters Spliting")]
public float SplitingFreq = 3f;
[JsonProperty(PropertyName = "Composter Pickup sound ON")]
public bool ComposterGrabDungPlaySound = true;
[JsonProperty(PropertyName = "Composter pickup sound prefab")]
public string ComposterGrabDungPrefab = "assets/bundled/prefabs/fx/collect/collect mushroom.prefab";
[JsonProperty(PropertyName = "Debug")]
public bool Debug = false;
}
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["enabled"] = "AutoComposter <color=#7FFF00>enabled</color>.",
["disabled"] = "AutoComposter <color=#ff0000>disabled</color>.",
["turnon"] = "Turn On",
["turnoff"] = "Turn Off",
["autocomposter"] = "AutoComposter",
["info"] = ("* Consolidates fertilizer and non-optimal compostable stacks.\n" +
"* Distributes the best compostable items across available slots.\n" +
"* Collects falling dung within a range of {0}m."),
["noAccess"] = "You do not have an <color=#ff0000>access</color> to this command.",
["perfInDebug"] = "Performance check is avaiable only in <color=#ff0000>debug</color> mode.",
}, this);
}
protected override void LoadConfig()
{
base.LoadConfig();
_config = Config.ReadObject<Configuration>();
if (_config == null)
{
PrintError("Your AutoComposter configuration file contains an error. Using default configuration values.");
LoadDefaultConfig();
SaveConfig();
}
}
protected override void LoadDefaultConfig() => _config = new Configuration();
protected override void SaveConfig() => Config.WriteObject(_config);
private void SaveData()
{
if (_config.TurnedOnByDefault)
{
Interface.Oxide.DataFileSystem.WriteObject(Name + "/PlayersDisabled", playersDisabled);
return;
}
Interface.Oxide.DataFileSystem.WriteObject(Name + "/PlayersEnabled", playersEnabled);
}
private void LoadData()
{
List<string> fileList;
if (_config.TurnedOnByDefault)
{
fileList = Interface.Oxide.DataFileSystem.ReadObject<List<string>>(Name + "/PlayersDisabled");
playersDisabled = fileList ?? new List<string>();
}
else
{
fileList = Interface.Oxide.DataFileSystem.ReadObject<List<string>>(Name + "/PlayersEnabled");
playersEnabled = fileList ?? new List<string>();
}
}
#endregion
#region commands
[ConsoleCommand("autocomposter.toggle")]
void ToggleAutoComposterCommand(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player == null)
return;
ToggleAutocomposter(player);
if (HasRightToAction(player.UserIDString))
CreateUI(player, IsActionOn(player.UserIDString));
}
[ChatCommand("autocomposter")]
private void AutoComposterCmd(BasePlayer player, string command, string[] args)
{
if (player == null)
return;
if (args.Contains("performance"))
{
if (!permission.UserHasPermission(player.UserIDString, PermissionKeyPerf))
{
Message(player, "noAccess");
return;
}
else if(!_config.Debug)
{
Message(player, "perfInDebug");
return;
}
PrintOutPerformanceResults(player);
return;
}
ToggleAutocomposter(player);
}
#endregion
#region Hooks
private void Init()
{
_instance = this;
LoadData();
LoadConfig();
permission.RegisterPermission(PermissionKey, this);
permission.RegisterPermission(PermissionKeyPerf, this);
var composters = UnityEngine.Object.FindObjectsOfType<Composter>();
Array.ForEach(composters, x => OnEntitySpawned(x));
}
private void Unload()
{
SaveData();
var composters = UnityEngine.Object.FindObjectsOfType<Composter>();
for (var i = 0; i < composters.Length; i++)
{
var composter = composters[i];
if (composter == null || composter.IsDestroyed)
continue;
var component = composter.gameObject.GetComponent<SplittingController>();
if (component == null)
continue;
component.Stop();
UnityEngine.Object.Destroy(component);
}
}
private void OnEntitySpawned(BaseNetworkable entity)
{
var composter = entity as Composter;
if (composter == null)
return;
var component = composter.GetComponent<SplittingController>();
if (component == null)
component = composter.gameObject.AddComponent<SplittingController>();
timer.Once(3f, () => component.Start());
}
private void OnLootEntity(BasePlayer player, BaseEntity entity)
{
var composter = entity as Composter;
if (composter == null || player == null)
return;
if (HasRightToAction(player.UserIDString))
CreateUI(player, IsActionOn(player.UserIDString));
var component = composter.gameObject.GetComponent<SplittingController>();
if (component == null)
return;
component.OpenedBy(player.net.ID.Value);
PrintDebug($"Composter has been open by player with id {player.net.ID.ToString()}");
}
private void OnLootEntityEnd(BasePlayer player, BaseCombatEntity entity)
{
var composter = entity as Composter;
if (composter == null || player == null)
return;
DestroyUI(player);
var component = composter.gameObject.GetComponent<SplittingController>();
if (component == null)
return;
component.ClosedBy(player.net.ID.Value);
PrintDebug($"Composter has been closed by player with id {player?.net.ID.ToString()}");
}
void OnItemDropped(Item item, BaseEntity entity)
{
if (item == null || entity == null)
return;
var dung = entity as DroppedItem;
if (dung == null || item.info.shortname != "horsedung")
return;
Stopwatch watch = null;
if (_config.Debug)
watch = Stopwatch.StartNew();
MoveDungToNearbyComposter(item, dung);
SavePerformanceTest(watch, dungDropPerformanceDebug);
}
#endregion
#region Helpers
public void MoveDungToNearbyComposter(Item item, DroppedItem dung)
{
PrintDebug($"Dung item dropped at position {dung.transform.position}.");
List<Composter> composters = new List<Composter>();
Vis.Entities(dung.transform.position, _config.CollectInRadius, composters);
var composterWithMinAmountItems = composters
.OrderBy(x => x.inventory.itemList.Where(z => z.info.shortname == item.info.shortname).Sum(y => y.amount))
.FirstOrDefault();
if (composterWithMinAmountItems == null)
{
PrintDebug("No composters found within radius.");
return;
}
var ownerId = composterWithMinAmountItems.OwnerID.ToString();
if (IsActionEnabledForPlayer(ownerId))
{
var fertilizers = composterWithMinAmountItems.inventory.itemList.Where(x => x.info.shortname == "fertilizer").ToList();
var compostableItems = composterWithMinAmountItems.inventory.itemList.Where(x => x.info.shortname != "fertilizer").ToList();
var component = composterWithMinAmountItems.GetComponent<SplittingController>();
var avaiableSlots = component?.CalculateAvailableSlots(fertilizers, compostableItems, item);
var dungStacks = composterWithMinAmountItems.inventory.itemList
.Where(x => x.info.shortname == item.info.shortname);
var canStackDung = avaiableSlots * item.MaxStackable() >= dungStacks.Sum(x => x.amount) + item.amount;
if (avaiableSlots <= dungStacks.Count() && !canStackDung)
{
PrintDebug($"Composter could not stack dung:{canStackDung}, slots:{avaiableSlots}");
return;
}
Item itemx = ItemManager.Create(item.info, item.amount);
if (itemx.MoveToContainer(composterWithMinAmountItems.inventory))
{
item.RemoveFromWorld();
if (_config.ComposterGrabDungPlaySound)
Effect.server.Run(_config.ComposterGrabDungPrefab, composterWithMinAmountItems.transform.position, Vector3.zero, null, false);
return;
}
itemx.Remove();
}
}
public bool IsActionOn(string playerId)
{
return _config.TurnedOnByDefault ? !playersDisabled.Contains(playerId) : playersEnabled.Contains(playerId);
}
public bool HasRightToAction(string playerId)
{
return !_config.UseOnlyIfPermission || (_config.UseOnlyIfPermission && !permission.UserHasPermission(playerId, PermissionKey));
}
public bool IsActionEnabledForPlayer(string playerId)
{
if (!HasRightToAction(playerId))
return false;
return IsActionOn(playerId);
}
public void ToggleAutocomposter(BasePlayer player)
{
if (_config.UseOnlyIfPermission && !permission.UserHasPermission(player.UserIDString, PermissionKey))
{
Message(player, "noAccess");
return;
}
if (_config.TurnedOnByDefault)
{
if (!playersDisabled.Contains(player.UserIDString))
{
playersDisabled.Add(player.UserIDString);
Message(player, "disabled");
}
else
{
playersDisabled.Remove(player.UserIDString);
Message(player, "enabled");
}
}
else
{
if (!playersEnabled.Contains(player.UserIDString))
{
playersEnabled.Add(player.UserIDString);
Message(player, "enabled");
}
else
{
playersEnabled.Remove(player.UserIDString);
Message(player, "disabled");
}
}
SaveData();
}
public void PrintOutPerformanceResults(BasePlayer player)
{
var splitTests = GameObject.FindObjectsOfType<SplittingController>().Select(x => x.componentPerformanceDebug);
var message = GetStatistics($"Splitting Dung in composters({splitTests.Count()})", splitTests.SelectMany(x => x).ToList());
player.ChatMessage(message);
message = GetStatistics("Dung Drop to composter", dungDropPerformanceDebug);
player.ChatMessage(message);
}
public static string GetStatistics(string name, List<TimeSpan> timeSpans)
{
if (timeSpans == null || timeSpans.Count == 0)
{
return $"No data available for {name}.";
}
// Calculate statistics
var minTime = timeSpans.Min();
var maxTime = timeSpans.Max();
var avgTime = TimeSpan.FromTicks((long)timeSpans.Average(ts => ts.Ticks));
var medianTime = CalculateMedian(timeSpans);
// Build the result string
return $"Statistics of Execution Times for <color=#7FFF00>{name}</color>:\n" +
$"- Count: {timeSpans.Count}\n" +
$"- Min: {minTime.TotalMilliseconds} ms\n" +
$"- Max: {maxTime.TotalMilliseconds} ms\n" +
$"- Average: {avgTime.TotalMilliseconds} ms\n" +
$"- Median: {medianTime.TotalMilliseconds} ms\n";
}
// Helper function to calculate the median of a list of TimeSpan
private static TimeSpan CalculateMedian(List<TimeSpan> times)
{
if (times.Count == 0)
return TimeSpan.Zero;
var sortedTimes = times.OrderBy(t => t).ToList();
int count = sortedTimes.Count;
if (count % 2 == 0)
return sortedTimes[count / 2];
var mid1 = sortedTimes[(count / 2) - 1];
var mid2 = sortedTimes[count / 2];
return TimeSpan.FromTicks((mid1.Ticks + mid2.Ticks) / 2);
}
public static void SavePerformanceTest(Stopwatch stopwatch, List<TimeSpan> performanceList)
{
if (!_config.Debug)
return;
stopwatch.Stop();
performanceList.Add(stopwatch.Elapsed);
}
public string Lang(string key, string id = null, params object[] args) => string.Format(lang.GetMessage(key, this, id), args);
public void Message(BasePlayer player, string key, params object[] args) => player.ChatMessage(Lang(key, player.UserIDString, args));
public static void PrintDebug(string message)
{
if (_config.Debug)
UnityEngine.Debug.Log($"DEBUG AutoComposter > " + message);
}
#endregion
#region UI
void CreateUI(BasePlayer player, bool isOn)
{
DestroyUI(player);
CuiElementContainer container = new CuiElementContainer();
// Main Panel
string mainPanel = container.Add(new CuiPanel
{
RectTransform = { AnchorMin = "0.65 0.36", AnchorMax = "0.947 0.55" },
Image = { Color = "0.1 0.1 0.1 0.8" }
}, "Overlay", "AutocomposterUI");
string contentColor = "0.7 0.7 0.7 1.0";
// Title Box
container.Add(new CuiLabel
{
Text = { Text = Lang("autocomposter", player.UserIDString), Color = contentColor, FontSize = 16, Align = TextAnchor.MiddleCenter },
RectTransform = { AnchorMin = "0 0.8", AnchorMax = "1 1" }
}, mainPanel);
string buttonColor = !isOn
? "0.415 0.5 0.258 0.4"
: "0.8 0.254 0.254 0.4";
string buttonTextColor = !isOn
? "0.607 0.705 0.431"
: "0.705 0.607 0.431";
string buttonText = !isOn
? Lang("turnon", player.UserIDString)
: Lang("turnoff", player.UserIDString);
container.Add(new CuiButton
{
Button = { Command = "autocomposter.toggle", Color = buttonColor },
RectTransform = { AnchorMin = "0.75 0.8", AnchorMax = "0.95 0.95" },
Text = { Text = buttonText, Color = buttonTextColor, FontSize = 14, Align = TextAnchor.MiddleCenter }
}, mainPanel);
// Body Content
container.Add(new CuiLabel
{
Text = { Text = Lang("info", player.UserIDString, _config.CollectInRadius),
FontSize = 12, Align = TextAnchor.MiddleLeft, Color = contentColor },
RectTransform = { AnchorMin = "0.05 0", AnchorMax = "0.95 0.85" }
}, mainPanel);
CuiHelper.AddUi(player, container);
}
void DestroyUI(BasePlayer player)
{
CuiHelper.DestroyUi(player, "AutocomposterUI");
}
#endregion
#region Controller
public class SplittingController : FacepunchBehaviour
{
public List<TimeSpan> componentPerformanceDebug = new List<TimeSpan>();
private List<ulong> _playersHoldOpen = new List<ulong>();
private Composter _composter;
private Composter Composter
{
get
{
if (_composter == null)
_composter = GetComponent<Composter>();
return _composter;
}
}
public void Start()
{
Stop();
Composter.InvokeRepeating(ComposterItemsSplit, _config.SplitingFreq, _config.SplitingFreq);
}
public void Stop()
{
Composter.CancelInvoke(ComposterItemsSplit);
}
public void OpenedBy(ulong playerId)
{
if(!_playersHoldOpen.Contains(playerId))
{
_playersHoldOpen.Add(playerId);
}
}
public void ClosedBy(ulong playerId)
{
if (_playersHoldOpen.Contains(playerId))
{
_playersHoldOpen.Remove(playerId);
}
}
void ComposterItemsSplit()
{
if (Composter.OwnerID == default(ulong) ||
!_instance.IsActionEnabledForPlayer(Composter.OwnerID.ToString()) ||
!Composter.inventory.itemList.Any())
return;
var isComposterOpen = _playersHoldOpen.Any();
if (isComposterOpen)
{
PrintDebug($"Composter at pos {Composter.transform.position} is open, skipping.");
return;
}
Stopwatch stopwatch = null;
if(_config.Debug)
stopwatch = Stopwatch.StartNew();
var fertilizers = Composter.inventory.itemList.Where(item => item.info.shortname == "fertilizer").ToList();
ConsolidateFertilizerStacks(fertilizers);
var compostableItems = Composter.inventory.itemList.Where(item => item.info.shortname != "fertilizer").ToList();
if (!compostableItems.Any())
{
SavePerformanceTest(stopwatch, componentPerformanceDebug);
return;
}
var bestItem = compostableItems
.OrderByDescending(x => x.info.GetComponent<ItemModCompostable>().TotalFertilizerProduced)
.FirstOrDefault();
var didChanges = ConsolidateCompostableItems(compostableItems, bestItem.info.shortname);
if (didChanges)
{
SavePerformanceTest(stopwatch, componentPerformanceDebug);
return;
}
int availableSlots = CalculateAvailableSlots(fertilizers, compostableItems, bestItem);
if (availableSlots <= 0)
{
SavePerformanceTest(stopwatch, componentPerformanceDebug);
return;
}
DistributeBestItem(Composter, bestItem.info, availableSlots);
SavePerformanceTest(stopwatch, componentPerformanceDebug);
}
public int CalculateAvailableSlots(List<Item> fertilizers, List<Item> compostableItems, Item bestItem)
{
var compostablesWithoutBest = compostableItems.Where(x => x.info.shortname != bestItem.info.shortname).ToList();
if (!fertilizers.Any())
return Composter.inventory.capacity - compostablesWithoutBest.Count - 1;
int totalFertilizer = fertilizers.Sum(f => f.amount);
int maxStackSize = fertilizers.FirstOrDefault().MaxStackable();
var fertilizerLeftToEndStacks = (maxStackSize * fertilizers.Count()) - totalFertilizer;
float sumFertilizerProdWithoutBest = 0;
var slotsTakenByNonBest = 0;
foreach (var compostable in compostablesWithoutBest)
{
if (compostable == null)
continue;
var fertiProductionOfItem = compostable.info.GetComponent<ItemModCompostable>().TotalFertilizerProduced;
sumFertilizerProdWithoutBest += fertiProductionOfItem;
slotsTakenByNonBest++;
}
var avaibleSlots = Composter.inventory.capacity - (fertilizers.Count() + slotsTakenByNonBest);
var bestItemProductionOnEmptySlots = avaibleSlots * bestItem.info.GetComponent<ItemModCompostable>().TotalFertilizerProduced;
var totalFertiOnTick = sumFertilizerProdWithoutBest + bestItemProductionOnEmptySlots;
var ticksToFillUpFertiStacks = Math.Ceiling(fertilizerLeftToEndStacks / totalFertiOnTick);
if (ticksToFillUpFertiStacks <= 2)
return avaibleSlots - 1;
return avaibleSlots;
}
void DistributeBestItem(Composter composter, ItemDefinition bestItemInfo, int availableSlots)
{
if (availableSlots <= 0)
return;
var bestItems = composter.inventory.itemList.Where(item => item.info.shortname == bestItemInfo.shortname);
int totalAmountToDistribute = bestItems.Sum(item => item.amount);
if (totalAmountToDistribute <= 0 || bestItems.Count() == availableSlots)
return;
availableSlots = Math.Min(availableSlots, composter.inventory.capacity);
int amountPerSlot = totalAmountToDistribute / availableSlots;
int leftover = totalAmountToDistribute % availableSlots;
for (int i = 0; i < composter.inventory.capacity; i++)
{
var item = composter.inventory.GetSlot(i);
if (item == null || item?.info.shortname == bestItemInfo.shortname)
{
if (totalAmountToDistribute == 0 && item != null)
{
item.Remove();
break;
}
if (availableSlots <= 0 || (leftover == 0 && amountPerSlot == 0))
break;
int amountForThisSlot = amountPerSlot;
if (leftover > 0)
{
amountForThisSlot++;
leftover--;
}
if (item == null)
{
Item newItem = ItemManager.Create(bestItemInfo, amountForThisSlot);
var success = newItem.MoveToContainer(composter.inventory, i, ignoreStackLimit: true);
if (!success)
{
PrintDebug($"Could not move a new stack of {newItem.info.shortname}({amountForThisSlot}) on {i} slot for composter at pos {composter.transform.position}");
continue;
}
item = newItem;
}
else if (item.info.shortname == bestItemInfo.shortname)
{
item.amount = amountForThisSlot;
}
totalAmountToDistribute -= amountForThisSlot;
availableSlots--;
}
}
if (totalAmountToDistribute > 0)
{
Item newItem = ItemManager.Create(bestItemInfo, totalAmountToDistribute);
newItem.MoveToContainer(composter.inventory);
}
composter.inventory.MarkDirty();
}
bool ConsolidateCompostableItems(List<Item> compostableItems, string bestItemName)
{
if (compostableItems.Select(x => x.info.shortname).Count() <= 1)
return false;
var itemsAndSums = new Dictionary<string, int>();
var itemsNotFullStacksAppearedTimes = new Dictionary<string, int>();
for (int i = 0; i < Composter.inventory.capacity; i++)
{
var item = Composter.inventory.GetSlot(i);
if (item == null || item.info.shortname == bestItemName)
continue;
var sum = itemsAndSums.ContainsKey(item.info.shortname) ? itemsAndSums[item.info.shortname] : 0;
itemsAndSums[item.info.shortname] = sum + item.amount;
var appearedTimes = itemsNotFullStacksAppearedTimes.ContainsKey(item.info.shortname) ? itemsNotFullStacksAppearedTimes[item.info.shortname] : 0;
if (item.amount < item.MaxStackable())
itemsNotFullStacksAppearedTimes[item.info.shortname] = appearedTimes + 1;
}
if (!itemsNotFullStacksAppearedTimes.Any(x => x.Value > 1))
return false;
for (int i = 0; i < Composter.inventory.capacity; i++)
{
var item = Composter.inventory.GetSlot(i);
if (item == null || item.info.shortname == bestItemName)
continue;
if (itemsAndSums[item.info.shortname] <= 0)
{
item.Remove();
continue;
}
if (item.amount >= item.MaxStackable())
{
itemsAndSums[item.info.shortname] -= item.MaxStackable();
continue;
}
var newAmount = Math.Min(item.MaxStackable(), itemsAndSums[item.info.shortname]);
item.amount = newAmount;
itemsAndSums[item.info.shortname] -= newAmount;
}
Composter.inventory.MarkDirty();
return true;
}
void ConsolidateFertilizerStacks(List<Item> fertilizers)
{
fertilizers = fertilizers.OrderBy(f => f.amount).ToList();
for (int i = 0; i < fertilizers.Count; i++)
{
Item currentFertilizer = fertilizers[i];
if (currentFertilizer == null || currentFertilizer.amount >= currentFertilizer.MaxStackable())
continue;
for (int j = i + 1; j < fertilizers.Count; j++)
{
Item nextFertilizer = fertilizers[j];
if (nextFertilizer == null || nextFertilizer.amount >= nextFertilizer.MaxStackable())
continue;
int spaceAvailable = currentFertilizer.MaxStackable() - currentFertilizer.amount;
if (spaceAvailable <= 0)
break;
int amountToMove = Math.Min(spaceAvailable, nextFertilizer.amount);
currentFertilizer.amount += amountToMove;
nextFertilizer.amount -= amountToMove;
currentFertilizer.MarkDirty();
nextFertilizer.MarkDirty();
if (nextFertilizer.amount <= 0)
{
nextFertilizer.Remove();
fertilizers[j] = null;
}
}
}
}
}
#endregion
}
}