C#:
using System;
using System.Collections.Generic;
using System.Reflection;
using Oxide.Core.Plugins;
namespace Oxide.Plugins
{
[Info("Infinite Vending Stock", "Rustic0, Improvements made by SeesAll", "1.3.4")]
[Description("Keeps stock high for NPC vending machines, including shops managed by CustomVendingSetup.")]
public class InfiniteVendingStock : RustPlugin
{
[PluginReference]
private Plugin CustomVendingSetup;
private const int TargetSalesPerOrder = 100000;
private readonly HashSet<ulong> _queuedRefreshes = new HashSet<ulong>();
private readonly HashSet<ulong> _queuedUiUpdates = new HashSet<ulong>();
private static readonly FieldInfo SellOrderSellSkinIdField = GetSellOrderField("sellSkinId", "itemToSellSkin");
private static readonly FieldInfo SellOrderItemToSellAsBpField = GetSellOrderField("itemToSellAsBP");
private void Unload()
{
_queuedRefreshes.Clear();
_queuedUiUpdates.Clear();
}
private void OnServerInitialized()
{
foreach (BaseNetworkable entity in BaseNetworkable.serverEntities)
{
NPCVendingMachine vendingMachine = entity as NPCVendingMachine;
if (vendingMachine == null)
continue;
ScheduleInitialization(vendingMachine);
}
}
private void OnEntitySpawned(NPCVendingMachine vendingMachine)
{
ScheduleInitialization(vendingMachine);
}
private void OnVendingTransaction(
NPCVendingMachine vendingMachine,
BasePlayer buyer,
int sellOrderId,
int numberOfTransactions,
ItemContainer targetContainer)
{
ApplyInfiniteStock(vendingMachine);
QueueRefresh(vendingMachine);
}
private void OnRefreshVendingStock(VendingMachine vendingMachine, ItemDefinition itemDefinition)
{
NPCVendingMachine npcVendingMachine = vendingMachine as NPCVendingMachine;
if (npcVendingMachine == null)
return;
// Rust invokes this hook immediately before it calculates sellOrder.inStock.
// Replenish synchronously so that calculation sees the increased amount.
ApplyInfiniteStock(npcVendingMachine);
}
private void ScheduleInitialization(NPCVendingMachine vendingMachine)
{
QueueRefresh(vendingMachine);
// NPC inventories may be populated after the entity/server hook during startup.
// These bounded retries avoid a permanent miss without adding a polling timer.
timer.Once(1f, delegate { QueueRefresh(vendingMachine); });
timer.Once(5f, delegate { QueueRefresh(vendingMachine); });
}
private void QueueRefresh(NPCVendingMachine vendingMachine)
{
if (!ShouldManage(vendingMachine))
return;
ulong networkId = GetQueueKey(vendingMachine);
if (!_queuedRefreshes.Add(networkId))
return;
NextTick(delegate()
{
_queuedRefreshes.Remove(networkId);
if (!ShouldManage(vendingMachine))
return;
ApplyInfiniteStock(vendingMachine);
});
}
private bool ApplyInfiniteStock(NPCVendingMachine vendingMachine)
{
if (!ShouldManage(vendingMachine))
return false;
ItemContainer inventory = vendingMachine.inventory;
if (inventory == null || inventory.itemList == null || vendingMachine.sellOrders == null || vendingMachine.sellOrders.sellOrders == null)
return false;
List<Item> itemList = inventory.itemList;
bool isCustomized = IsCustomized(vendingMachine);
int orderCount = vendingMachine.sellOrders.sellOrders.Count;
bool changed = false;
for (int orderIndex = 0; orderIndex < orderCount; orderIndex++)
{
ProtoBuf.VendingMachine.SellOrder sellOrder = vendingMachine.sellOrders.sellOrders[orderIndex];
if (sellOrder == null)
continue;
int itemId = GetSoldItemId(sellOrder);
int amountPerSale = GetSoldAmount(sellOrder);
bool isBlueprint = GetSoldIsBlueprint(sellOrder);
ulong skinId = GetSoldSkin(sellOrder);
if (itemId == 0 || amountPerSale <= 0)
continue;
int desiredAmount = GetDesiredBackingAmount(amountPerSale);
if (desiredAmount <= 0)
continue;
Item matchingItem = FindBackingItem(itemList, itemId, skinId, isBlueprint);
if (matchingItem == null)
{
// CustomVendingSetup offers may carry ammo, capacity, instance data, or
// other attributes that cannot be recreated from a sell order alone.
// Existing items are safe to top up, but never replace a missing custom item.
if (isCustomized)
continue;
matchingItem = CreateBackingItem(inventory, itemId, skinId, isBlueprint, desiredAmount);
if (matchingItem == null)
continue;
changed = true;
}
if (matchingItem.amount < desiredAmount)
{
matchingItem.amount = desiredAmount;
matchingItem.MarkDirty();
changed = true;
}
int availableSales = matchingItem.amount / amountPerSale;
if (sellOrder.inStock != availableSales)
{
sellOrder.inStock = availableSales;
changed = true;
}
}
if (!changed)
return false;
vendingMachine.UpdateEmptyFlag();
QueueUiUpdate(vendingMachine);
return true;
}
private void QueueUiUpdate(NPCVendingMachine vendingMachine)
{
if (!ShouldManage(vendingMachine))
return;
ulong networkId = GetQueueKey(vendingMachine);
if (!_queuedUiUpdates.Add(networkId))
return;
NextTick(delegate()
{
_queuedUiUpdates.Remove(networkId);
if (!ShouldManage(vendingMachine))
return;
vendingMachine.SendNetworkUpdateImmediate();
});
}
private bool ShouldManage(NPCVendingMachine vendingMachine)
{
if (vendingMachine == null || vendingMachine.IsDestroyed)
return false;
if (vendingMachine is RentableShopVendingMachine)
return false;
return true;
}
private bool IsCustomized(NPCVendingMachine vendingMachine)
{
if (CustomVendingSetup == null)
return false;
object isCustomized = CustomVendingSetup.Call("API_IsCustomized", vendingMachine);
return isCustomized is bool && (bool)isCustomized;
}
private static ulong GetNetworkId(NPCVendingMachine vendingMachine)
{
if (vendingMachine == null || vendingMachine.net == null)
return 0UL;
try
{
return Convert.ToUInt64(vendingMachine.net.ID.Value);
}
catch
{
try
{
return Convert.ToUInt64(vendingMachine.net.ID);
}
catch
{
return 0UL;
}
}
}
private static ulong GetQueueKey(NPCVendingMachine vendingMachine)
{
ulong networkId = GetNetworkId(vendingMachine);
if (networkId != 0UL)
return networkId;
return (1UL << 63) | unchecked((uint)vendingMachine.GetInstanceID());
}
private static int GetDesiredBackingAmount(int amountPerSale)
{
if (amountPerSale <= 0)
return 0;
long desired = (long)amountPerSale * TargetSalesPerOrder;
return desired > int.MaxValue ? int.MaxValue : (int)desired;
}
private static Item FindBackingItem(List<Item> itemList, int soldItemId, ulong soldSkinId, bool soldAsBlueprint)
{
int backingItemId = soldAsBlueprint && ItemManager.blueprintBaseDef != null
? ItemManager.blueprintBaseDef.itemid
: soldItemId;
int blueprintTarget = soldAsBlueprint ? soldItemId : 0;
for (int i = 0; i < itemList.Count; i++)
{
Item item = itemList[i];
if (item == null || item.info == null)
continue;
if (item.info.itemid != backingItemId)
continue;
if (item.blueprintTarget != blueprintTarget)
continue;
if (item.skin != soldSkinId)
continue;
return item;
}
return null;
}
private static Item CreateBackingItem(ItemContainer inventory, int soldItemId, ulong soldSkinId, bool soldAsBlueprint, int amount)
{
int backingItemId = soldAsBlueprint && ItemManager.blueprintBaseDef != null
? ItemManager.blueprintBaseDef.itemid
: soldItemId;
if (backingItemId == 0)
return null;
Item item = ItemManager.CreateByItemID(backingItemId, amount, soldAsBlueprint ? 0UL : soldSkinId);
if (item == null)
return null;
if (soldAsBlueprint)
item.blueprintTarget = soldItemId;
if (item.MoveToContainer(inventory))
return item;
item.Remove();
return null;
}
private static int GetSoldItemId(ProtoBuf.VendingMachine.SellOrder sellOrder)
{
return sellOrder == null ? 0 : sellOrder.itemToSellID;
}
private static int GetSoldAmount(ProtoBuf.VendingMachine.SellOrder sellOrder)
{
return sellOrder == null ? 0 : sellOrder.itemToSellAmount;
}
private static ulong GetSoldSkin(ProtoBuf.VendingMachine.SellOrder sellOrder)
{
if (sellOrder == null || SellOrderSellSkinIdField == null)
return 0UL;
object value = SellOrderSellSkinIdField.GetValue(sellOrder);
if (value == null)
return 0UL;
try
{
return Convert.ToUInt64(value);
}
catch
{
return 0UL;
}
}
private static bool GetSoldIsBlueprint(ProtoBuf.VendingMachine.SellOrder sellOrder)
{
if (sellOrder == null)
return false;
if (sellOrder.itemToSellIsBP)
return true;
if (SellOrderItemToSellAsBpField == null)
return false;
object value = SellOrderItemToSellAsBpField.GetValue(sellOrder);
return value is bool && (bool)value;
}
private static FieldInfo GetSellOrderField(params string[] fieldNames)
{
const BindingFlags Flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
Type sellOrderType = typeof(ProtoBuf.VendingMachine.SellOrder);
for (int i = 0; i < fieldNames.Length; i++)
{
FieldInfo field = sellOrderType.GetField(fieldNames[i], Flags);
if (field != null)
return field;
}
return null;
}
}
}