C#:
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("CompostSplitter", "xADROCx", "1.7.3")]
[Description("Safely splits inserted compostables across free slots without deleting existing inventory.")]
public class CompostSplitter : RustPlugin
{
private const string PermUse = "compostsplitter.use";
private readonly HashSet<ItemContainer> busy = new HashSet<ItemContainer>();
private readonly Dictionary<ItemContainer, Dictionary<int, long>> observed = new Dictionary<ItemContainer, Dictionary<int, long>>();
private bool unloading;
private void Init()
{
permission.RegisterPermission(PermUse, this);
timer.Every(0.25f, CheckDeposits);
}
private void Unload() { unloading = true; observed.Clear(); busy.Clear(); }
private static bool IsComposter(StorageContainer comp)
{
return comp != null && !comp.IsDestroyed && comp.ShortPrefabName != null &&
comp.ShortPrefabName.Contains("composter");
}
private static bool IsCompostable(Item item)
{
if (item == null || item.info == null) return false;
string name = item.info.shortname;
return name == "horsedung" || name == "plantfiber" || name == "food.scraps" ||
name == "horsepoo" || item.info.GetComponent<ItemModCompostable>() != null;
}
private bool HasAuthorizedLooter(StorageContainer comp)
{
// Read actual loot state so closing one of several viewers cannot disable another.
foreach (BasePlayer player in BasePlayer.activePlayerList)
if (player != null && player.IsConnected && player.inventory != null &&
player.inventory.loot != null && player.inventory.loot.entitySource == comp &&
permission.UserHasPermission(player.UserIDString, PermUse)) return true;
return false;
}
private object CanAcceptItem(ItemContainer container, Item item, int targetPos)
{
StorageContainer comp = container == null ? null : container.entityOwner as StorageContainer;
if (!IsComposter(comp) || !IsCompostable(item) || busy.Contains(container) ||
item.parent == container || !HasAuthorizedLooter(comp)) return null;
for (int i = 0; i < container.capacity; i++)
if (container.GetSlot(i) == null) return null;
// Current Oxide expects this enum, not a bool.
return ItemContainer.CanAcceptResult.CannotAccept;
}
private static Dictionary<int, long> Totals(ItemContainer container)
{
var result = new Dictionary<int, long>();
foreach (Item item in container.itemList)
{
if (!IsCompostable(item)) continue;
long amount;
result.TryGetValue(item.info.itemid, out amount);
result[item.info.itemid] = amount + item.amount;
}
return result;
}
private void OnLootEntity(BasePlayer player, BaseEntity entity)
{
var comp = entity as StorageContainer;
if (!IsComposter(comp) || comp.inventory == null || player == null ||
!permission.UserHasPermission(player.UserIDString, PermUse)) return;
if (!observed.ContainsKey(comp.inventory)) observed[comp.inventory] = Totals(comp.inventory);
}
private void CheckDeposits()
{
if (unloading) return;
var active = new HashSet<ItemContainer>();
foreach (BasePlayer player in BasePlayer.activePlayerList)
{
var comp = player?.inventory?.loot?.entitySource as StorageContainer;
if (!IsComposter(comp) || comp.inventory == null ||
!permission.UserHasPermission(player.UserIDString, PermUse) || !active.Add(comp.inventory)) continue;
ItemContainer container = comp.inventory;
Dictionary<int, long> current = Totals(container);
Dictionary<int, long> previous;
if (!observed.TryGetValue(container, out previous))
{
// Opening/reloading is not a deposit. Establish a baseline without rearranging.
observed[container] = current;
continue;
}
if (!busy.Add(container)) continue;
try
{
foreach (var entry in current)
{
long oldAmount;
previous.TryGetValue(entry.Key, out oldAmount);
if (entry.Value <= oldAmount) continue;
var handled = new List<Item>();
foreach (Item item in new List<Item>(container.itemList))
{
if (item.info.itemid != entry.Key) continue;
bool duplicate = false;
foreach (Item other in handled)
if (other == item || other.CanStack(item)) { duplicate = true; break; }
if (duplicate) continue;
SplitInsertedStack(container, item, comp);
handled.Add(item);
}
}
}
catch (Exception ex) { PrintError("Compost split stopped: " + ex.Message); }
finally
{
busy.Remove(container);
if (!comp.IsDestroyed) observed[container] = Totals(container);
}
}
foreach (ItemContainer container in new List<ItemContainer>(observed.Keys))
if (!active.Contains(container)) observed.Remove(container);
}
private void SplitInsertedStack(ItemContainer container, Item source, StorageContainer comp)
{
if (source.info.stackable <= 1) return;
var stacks = new List<Item>();
var slots = new List<int>();
long total = 0;
// Occupied compatible slots come first, so each original stack keeps at least one.
for (int i = 0; i < container.capacity; i++)
{
Item current = container.GetSlot(i);
if (current != null && (current == source || source.CanStack(current)))
{
stacks.Add(current);
slots.Add(i);
total += current.amount;
}
}
for (int i = 0; i < container.capacity; i++)
{
// Respect the composter's input/output slot rules.
if (container.GetSlot(i) == null &&
container.CanAcceptItem(null, source, i) == ItemContainer.CanAcceptResult.CanAccept)
{
stacks.Add(null);
slots.Add(i);
}
}
int count = (int)Math.Min(total, slots.Count);
if (count <= 1) return;
int each = (int)(total / count);
int extra = (int)(total % count);
var targets = new int[count];
for (int i = 0; i < count; i++) targets[i] = each + (i < extra ? 1 : 0);
for (int receiver = 0; receiver < count; receiver++)
{
for (int donor = 0; donor < count; donor++)
{
Item to = stacks[receiver];
int need = targets[receiver] - (to == null ? 0 : to.amount);
if (need <= 0) break;
Item from = stacks[donor];
if (from == null || from.parent != container) continue;
int surplus = from.amount - targets[donor];
int amount = Math.Min(need, surplus);
if (amount <= 0) continue;
if (to != null)
{
if (to.parent != container || !from.CanStack(to)) return;
// No item destruction or recreation: transfer quantities between compatible stacks.
from.amount -= amount;
to.amount += amount;
from.MarkDirty();
to.MarkDirty();
continue;
}
if (container.GetSlot(slots[receiver]) != null) return;
Item part = from.SplitItem(amount);
if (part == null) return;
bool moved = false;
try { moved = part.MoveToContainer(container, slots[receiver], false); }
finally
{
if (!moved && part.parent == null && part.amount > 0)
{
if (from.parent == container && !comp.IsDestroyed)
{
from.amount += part.amount;
from.MarkDirty();
part.Remove();
}
else part.Drop(comp.transform.position + Vector3.up, Vector3.zero);
}
}
if (!moved) return;
stacks[receiver] = part;
}
}
container.MarkDirty();
}
// Rust saves the real inventory. Legacy snapshots are deliberately never replayed:
// recreating inventory from them can lose items, erase metadata, or resurrect consumed resources.
}
}