C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
// #define DEBUG_READONLY
using HarmonyLib;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Core.Libraries.Covalence;
using Oxide.Game.Rust.Cui;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text.RegularExpressions;
using ProtoBuf;
using UnityEngine;
using VLB;
using static ProtoBuf.VendingMachine;
using static VendingMachine;
using CustomGetDataCallback = System.Func<Newtonsoft.Json.Linq.JObject>;
using CustomSaveDataCallback = System.Action<Newtonsoft.Json.Linq.JObject>;
using CustomGetSkinCallback = System.Func<ulong>;
using CustomSetSkinCallback = System.Action<ulong>;
using Pool = Facepunch.Pool;
using Time = UnityEngine.Time;
namespace Oxide.Plugins {
[Info("Custom Vending Setup", "WhiteThunder", "2.14.7")]
[Description("Allows editing orders at NPC vending machines.")]
internal class CustomVendingSetup : CovalencePlugin {
#region Fields
[PluginReference]
private readonly Plugin BagOfHolding, Economics, ItemRetriever, MonumentFinder, ServerRewards;
private static CustomVendingSetup _plugin;
private SavedPrefabRelativeData _prefabRelativeData;
private SavedMapData _mapData;
private SavedSalesData _salesData;
private Configuration _config;
private const string PermissionUse = "customvendingsetup.use";
private const string StoragePrefab = "assets/prefabs/deployable/large wood storage/box.wooden.large.prefab";
private const int ItemsPerRow = 6;
private const int InventorySize = 24;
// Going over 7 causes offers to get cut off regardless of resolution.private const int MaxVendingOffers = 7;
private const int GeneralSettingsNoteSlot = 29;
private const int ContainerCapacity = 30;
private const int MaxItemRows = ContainerCapacity / ItemsPerRow;
private const int BlueprintItemId = -996920608;
private const float MinCurrencyCondition = 0.5f;
private const ulong NpcVendingMachineSkinId = 861142659;
private static readonly Regex KeyValueRegex = new(@"^([^:]+):(.+(?:\n[^:\n]+$)*)", RegexOptions.Compiled | RegexOptions.Multiline);
private readonly object True = true;
private readonly object False = false;
private ItemRetrieverAdapter _itemRetrieverAdapter;
private PluginDataProviderRegistry _dataProviderRegistry = new();
private ComponentTracker<NPCVendingMachine, VendingMachineComponent> _componentTracker = new();
private ComponentFactory<NPCVendingMachine, VendingMachineComponent> _componentFactory;
private MonumentFinderAdapter _monumentFinderAdapter;
private VendingMachineManager _vendingMachineManager;
private BagOfHoldingLimitManager _bagOfHoldingLimitManager;
private DynamicHookSubscriber<VendingController> _inaccessibleVendingMachines;
private DynamicHookSubscriber<BasePlayer> _playersNeedingFakeInventory;
private PaymentProviderResolver _paymentProviderResolver;
private ItemDefinition _noteItemDefinition;
private bool _isServerInitialized;
private bool _performingInstantRestock;
private VendingItem _itemBeingSold;
private Dictionary<string, object> _itemRetrieverQuery = new();
private List<Item> _reusableItemList = new();
private object[] _objectArray1 = new object[1];
private object[] _objectArray2 = new object[2];
public CustomVendingSetup() {
_monumentFinderAdapter = new MonumentFinderAdapter(this);
_itemRetrieverAdapter = new ItemRetrieverAdapter(this);
_componentFactory = new ComponentFactory<NPCVendingMachine, VendingMachineComponent>(this, _componentTracker);
_vendingMachineManager = new VendingMachineManager(this, _componentFactory, _dataProviderRegistry);
_bagOfHoldingLimitManager = new BagOfHoldingLimitManager(this);
_paymentProviderResolver = new PaymentProviderResolver(this);
_inaccessibleVendingMachines = new DynamicHookSubscriber<VendingController>(this, nameof(CanAccessVendingMachine));
_playersNeedingFakeInventory = new DynamicHookSubscriber<BasePlayer>(this, nameof(OnEntitySaved), nameof(OnInventoryNetworkUpdate));
}
#endregion
#region Harmony Patches
[AutoPatch]
[HarmonyPatch(typeof(VendingMachineMapMarker), nameof(VendingMachineMapMarker.Save), typeof(BaseNetworkable.SaveInfo))]
private static class Patch_VendingMachineMapMarker_Save {
[HarmonyPostfix]
private static void Postfix(VendingMachineMapMarker __instance, BaseNetworkable.SaveInfo info) {
if (_plugin == null || info.forDisk || __instance.server_vendingMachine is not NPCVendingMachine vendingMachine || !_plugin.IsCustomized(vendingMachine)) return;
if (vendingMachine.IsLocalized && vendingMachine.shopName != vendingMachine.Phrase.english) {
// Fix issue where the shop name as seen in the map "Store" menu (after clicking on the marker)
// shows the localized name instead of the customized name.info.msg.vendingMachine.translationToken = "";
}
// Disassociate the vending machine from the map marker.This fixes an issue where knowledge of the
// vending machine somehow overrides the map marker name, even when no translation token is sent.info.msg.vendingMachine.networkID = new NetworkableId(0);
}
}
#endregion
#region Hooks
private void Init() {
_plugin = this;
_config.Init();
_prefabRelativeData = SavedPrefabRelativeData.Load();
_salesData = SavedSalesData.Load();
permission.RegisterPermission(PermissionUse, this);
Unsubscribe(nameof(OnEntitySpawned));
_inaccessibleVendingMachines.UnsubscribeAll();
_playersNeedingFakeInventory.UnsubscribeAll();
}
private void OnServerInitialized() {
_isServerInitialized = true;
_mapData = SavedMapData.Load();
if (MonumentFinder == null) {
LogWarning("MonumentFinder is not loaded, so you won't be able to save vending machine customizations relative to monuments.");
}
// Delay to allow Monument Finder to register monuments via its `OnServerInitialized()` hook.NextTick(() =>
{
_vendingMachineManager.SetupAll();
foreach (var player in BasePlayer.activePlayerList) {
var container = player.inventory.loot.containers.FirstOrDefault();
if (container == null) continue;
var vendingMachine = container.entityOwner as NPCVendingMachine;
if (vendingMachine != null) {
OnVendingShopOpened(vendingMachine, player);
}
}
}
);
if (ItemRetriever != null) {
_itemRetrieverAdapter.HandleItemRetrieverLoaded();
}
_bagOfHoldingLimitManager.OnServerInitialized();
Subscribe(nameof(OnEntitySpawned));
_noteItemDefinition = ItemManager.FindItemDefinition("note");
}
private void Unload() {
if (NPCVendingMachine.DynamicPricingEnabled) {
_vendingMachineManager.SaveAllSalesData();
}
_vendingMachineManager.ResetAll();
ObjectCache.Clear<int>();
ObjectCache.Clear<float>();
ObjectCache.Clear<ulong>();
_plugin = null;
}
private void OnNewSave() {
if (NPCVendingMachine.DynamicPricingEnabled) {
_salesData.Reset();
}
}
private void OnServerSave() {
if (NPCVendingMachine.DynamicPricingEnabled) {
_vendingMachineManager.SaveAllSalesData();
}
}
private void OnPluginLoaded(Plugin plugin) {
switch (plugin.Name) {
case nameof(MonumentFinder) : {
// Check whether initialized to detect only late (re)loads.// Note: We are not dynamically subscribing to OnPluginLoaded since that interferes with the PluginReference attribute.if (_isServerInitialized)
{
// Delay to ensure MonumentFinder's `OnServerInitialized` method is called.NextTick(_vendingMachineManager.SetupAll);
}
return;
}
case nameof(BagOfHolding) : _bagOfHoldingLimitManager.HandleBagOfHoldingLoadedChanged();
return;
case nameof(ItemRetriever) : _itemRetrieverAdapter.HandleItemRetrieverLoaded();
return;
}
}
private void OnPluginUnloaded(Plugin plugin) {
switch (plugin.Name) {
case nameof(ItemRetriever) : _itemRetrieverAdapter.HandleItemRetrieverUnloaded();
return;
}
}
private void OnEntitySpawned(NPCVendingMachine vendingMachine) {
// Delay to give other plugins a chance to save a reference so they can block setup.NextTick(() =>
{
if (vendingMachine == null || vendingMachine.IsDestroyed) return;
_vendingMachineManager.HandleVendingMachineSpawned(vendingMachine);
}
);
}
private void OnEntityKill(NPCVendingMachine vendingMachine) {
_vendingMachineManager.HandleVendingMachineKilled(vendingMachine);
}
private void OnVendingShopOpened(NPCVendingMachine vendingMachine, BasePlayer player) {
var controller = _vendingMachineManager.GetController(vendingMachine);
if (controller == null) return;
var component = _componentTracker.GetComponent(vendingMachine);
if (component == null) return;
if (permission.UserHasPermission(player.UserIDString, PermissionUse)) {
component.ShowAdminUI(player);
}
var profile = controller.Profile;
if (profile?.Offers == null) return;
if (_config.ShopUISettings.EnableSkinOverlays) {
component.ShowShopUI(player);
}
if ((_config.Economics.EnabledAndValid && profile.HasPaymentProviderCurrency(_config.Economics)) || (_config.ServerRewards.EnabledAndValid && profile.HasPaymentProviderCurrency(_config.ServerRewards))) {
// Make sure OnEntitySaved/OnInventoryNetworkUpdate are subscribed to modify network updates._playersNeedingFakeInventory.Add(player);
// Mark inventory dirty to send a network update, which will be modified by hooks.player.inventory.containerMain.MarkDirty();
}
}
private object OnVendingTransaction(NPCVendingMachine vendingMachine, BasePlayer player, int sellOrderIndex, int numberOfTransactions, ItemContainer targetContainer) {
var vendingProfile = _vendingMachineManager.GetController(vendingMachine)?.Profile;
if (vendingProfile?.Offers == null) {
// Don't override the transaction logic because the vending machine is not customized by this plugin.return null;
}
var component = _componentTracker.GetComponent(vendingMachine);
if (component == null) return null;
var offer = vendingProfile.GetOfferForSellOrderIndex(sellOrderIndex);
if (offer == null) {
// Something is wrong.No valid offer exists at the specified index.return null;
}
numberOfTransactions = Mathf.Clamp(numberOfTransactions, 1, HasCondition(offer.SellItem.ItemDefinition) ? 1 : 1000000);
var sellAmount = offer.SellItem.Amount * numberOfTransactions;
var sellOrder = vendingMachine.sellOrders.sellOrders[sellOrderIndex];
if (offer.SellItem.ItemDefinition == NPCVendingMachine.ScrapItem && sellOrder.receivedQuantityMultiplier != 1f) {
// Modify the amount of scrap received according to dynamic pricing.sellAmount = GetTotalPriceForOrder(sellAmount, sellOrder.receivedQuantityMultiplier);
}
var sellItemQuery = ItemQuery.FromSellItem(offer.SellItem);
if (ItemUtils.SumContainerItems(vendingMachine.inventory, ref sellItemQuery) < sellAmount) {
// The vending machine has insufficient stock.return False;
}
var currencyAmount = GetTotalPriceForOrder(offer.CurrencyItem.Amount, sellOrder.priceMultiplier) * numberOfTransactions;
var currencyProvider = _paymentProviderResolver.Resolve(offer.CurrencyItem);
if (currencyProvider.GetBalance(player) < currencyAmount) {
// The player has insufficient currency.return False;
}
_reusableItemList.Clear();
currencyProvider.TakeBalance(player, currencyAmount, _reusableItemList);
var onMarketplaceItemPurchase = (targetContainer?.entityOwner as MarketTerminal)?._onItemPurchasedCached;
// Note: The list will be empty if Economics or Server Rewards currency were used.foreach (var currencyItem in _reusableItemList)
{
MaybeGiveWeaponAmmo(currencyItem, player);
// Show a notice on the marketplace UI that the item was taken.onMarketplaceItemPurchase?.Invoke(player, currencyItem);
// Instead of calling `vendingMachine.TakeCurrencyItem(itemToTake)`, just remove the item.// This fixes an "issue" where the item would go into the vending machine storage if there was a matching stack.// Note: The "OnTakeCurrencyItem" hook is not called because Item Retriever always takes the items.currencyItem.RemoveFromContainer();
currencyItem.Remove();
}
_reusableItemList.Clear();
if (offer.RefillDelay <= 0) {
// Don't change the stock amount.Instead, we will just leave the items in the vending machine.// The "CanVendingStockRefill" hook will use this flag to skip all logic._performingInstantRestock = true;
}
else {
// The "CanVendingStockRefill" hook may use this to add stock._itemBeingSold = offer.SellItem;
}
_paymentProviderResolver.Resolve(offer.SellItem).AddBalance(player, sellAmount, new TransactionContext {
VendingMachine = vendingMachine, SellItem = offer.SellItem, TargetContainer = targetContainer, OnMarketplaceItemPurchase = onMarketplaceItemPurchase,
}
);
vendingMachine.RecordSale(sellOrderIndex, sellAmount, offer.CurrencyItem.Amount * numberOfTransactions);
// These can now be unset since the "CanVendingStockRefill" hook can no longer be called after this point._performingInstantRestock = false;
_itemBeingSold = null;
if (offer.RefillDelay > 0) {
// Remove stock only after the items have been given to the player,
// so that max stack size can be determined by an item in stock.ItemUtils.TakeContainerItems(vendingMachine.inventory, ref sellItemQuery, sellAmount);
}
vendingMachine.UpdateEmptyFlag();
// Reopen the UI if it was closed due to a transaction delay.if (!component.HasUI(player) && IsLootingVendingMachine(player, vendingMachine))
{
OnVendingShopOpened(vendingMachine, player);
}
if (offer.CustomSettings?.Count > 0) {
ExposedHooks.OnCustomVendingSetupTransactionWithCustomSettings(vendingMachine, offer.CustomSettings);
}
return True;
}
private void OnBuyVendingItem(NPCVendingMachine vendingMachine, BasePlayer player, int sellOrderID, int amount) {
if (!IsCustomized(vendingMachine)) return;
var component = _componentTracker.GetComponent(vendingMachine);
if (component == null) return;
ScheduleRemoveUI(vendingMachine, player, component);
}
private object OnNpcGiveSoldItem(NPCVendingMachine vendingMachine, Item item, BasePlayer player) {
if (!IsCustomized(vendingMachine)) return null;
ExposedHooks.OnCustomVendingSetupGiveSoldItem(vendingMachine, item, player);
item.SetItemOwnership(player, ItemOwnershipPhrases.VendorSale);
// Simply give the item, without splitting it, since stack size logic has already been taken into account.player.GiveItem(item);
return False;
}
// This hook is exposed by plugin: Vending In Stock (VendingInStock).private object CanVendingStockRefill(NPCVendingMachine vendingMachine, Item soldItem, BasePlayer player)
{
if (!IsCustomized(vendingMachine)) {
// Allow VendingInStock to restock the item.return null;
}
if (_performingInstantRestock) {
// Don't restock the item, since it was never removed from the vending machine in the first place.return False;
}
// Override VendingInStock behavior to prevent creating new items in the container.// This also ensures additional item attributes are preserved.var itemQuery = ItemQuery.FromSellItem(_itemBeingSold);
var existingItem = ItemUtils.FindFirstContainerItem(vendingMachine.inventory, ref itemQuery);
if (existingItem != null) {
existingItem.amount += soldItem.amount;
existingItem.MarkDirty();
return False;
}
if (_itemBeingSold == null) {
// Something is wrong.The "CanPurchaseItem" hook was not called via this plugin.return null;
}
var newItem = _itemBeingSold.Create(soldItem.amount);
vendingMachine.transactionActive = true;
if (!newItem.MoveToContainer(vendingMachine.inventory, allowStack : false)) {
newItem.Remove();
}
vendingMachine.transactionActive = false;
return False;
}
private object CanAccessVendingMachine(DeliveryDroneConfig deliveryDroneConfig, NPCVendingMachine vendingMachine) {
if (!vendingMachine.IsBroadcasting()) return null;
var controller = _vendingMachineManager.GetController(vendingMachine);
if (controller == null) return null;
if (_inaccessibleVendingMachines.Contains(controller)) return False;
return null;
}
private void OnEntitySaved(BasePlayer player, BaseNetworkable.SaveInfo saveInfo) {
AddCurrencyToContainerSnapshot(player, saveInfo.msg.basePlayer.inventory.invMain);
}
private void OnInventoryNetworkUpdate(PlayerInventory inventory, ItemContainer container, ProtoBuf.UpdateItemContainer updatedItemContainer, PlayerInventory.Type inventoryType) {
if (inventoryType != PlayerInventory.Type.Main) return;
AddCurrencyToContainerSnapshot(inventory.baseEntity, updatedItemContainer.container[0]);
}
#endregion
#region API
[HookMethod(nameof(API_IsCustomized))]
public object API_IsCustomized(NPCVendingMachine vendingMachine) {
return IsCustomized(vendingMachine) ? True : False;
}
[HookMethod(nameof(API_RefreshDataProvider))]
public void API_RefreshDataProvider(NPCVendingMachine vendingMachine) {
_vendingMachineManager.RefreshDataProvider(vendingMachine);
}
// Undocumented.Intended for MonumentAddons migration to become a Data Provider.[HookMethod(nameof(API_MigrateVendingProfile))]
public JObject API_MigrateVendingProfile(NPCVendingMachine vendingMachine) {
if (PrefabRelativePosition.FromVendingMachine(_monumentFinderAdapter, vendingMachine) is not {
}
location) {
// This can happen if a vending machine was moved outside a monument's bounds.return null;
}
var vendingProfile = _prefabRelativeData.FindProfile(location);
if (vendingProfile == null) {
return null;
}
JObject jObject;
try {
jObject = JObject.FromObject(vendingProfile);
}
catch (Exception e) {
LogError($"Unable to migrate vending profile\n{e}");
return null;
}
_prefabRelativeData.VendingProfiles.Remove(vendingProfile);
_prefabRelativeData.Save();
return jObject;
}
#endregion
#region Dependencies
private class MonumentAdapter {
public string PrefabName => (string)_monumentInfo["PrefabName"];
public string ShortName => (string)_monumentInfo["ShortName"];
public string Alias => (string)_monumentInfo["Alias"];
public Vector3 Position => (Vector3)_monumentInfo["Position"];
private Dictionary<string, object> _monumentInfo;
public MonumentAdapter(Dictionary<string, object> monumentInfo) {
_monumentInfo = monumentInfo;
}
public Vector3 InverseTransformPoint(Vector3 worldPosition) {
return ((Func<Vector3, Vector3>)_monumentInfo["InverseTransformPoint"]).Invoke(worldPosition);
}
public bool IsInBounds(Vector3 position) {
return ((Func<Vector3, bool>)_monumentInfo["IsInBounds"]).Invoke(position);
}
}
private class MonumentFinderAdapter {
private CustomVendingSetup _plugin;
private Plugin _monumentFinder => _plugin.MonumentFinder;
public MonumentFinderAdapter(CustomVendingSetup plugin) {
_plugin = plugin;
}
public MonumentAdapter GetMonumentAdapter(Vector3 position) {
if (_monumentFinder?.Call("API_GetClosest", position) is not Dictionary<string, object> dictResult) return null;
var monument = new MonumentAdapter(dictResult);
return monument.IsInBounds(position) ? monument : null;
}
public MonumentAdapter GetMonumentAdapter(BaseEntity entity) {
return GetMonumentAdapter(entity.transform.position);
}
}
private class BagOfHoldingLimitManager {
private class CustomLimitProfile {
[JsonProperty("Max total bags")]
public int MaxTotalBags = -1;
}
private CustomVendingSetup _plugin;
private object _limitProfile;
public BagOfHoldingLimitManager(CustomVendingSetup plugin) {
_plugin = plugin;
}
public void OnServerInitialized() {
HandleBagOfHoldingLoadedChanged();
}
public void HandleBagOfHoldingLoadedChanged() {
if (_plugin.BagOfHolding == null) return;
_limitProfile = _plugin.BagOfHolding.Call("API_CreateLimitProfile", JsonConvert.SerializeObject(new CustomLimitProfile()));
if (_limitProfile == null) {
LogError("Failed to create limit profile.");
}
}
public void SetLimitProfile(ItemContainer container) {
if (_limitProfile == null || _plugin.BagOfHolding == null) return;
var result = _plugin.BagOfHolding.Call("API_SetLimitProfile", container, _limitProfile);
if (result is not true) {
LogError("Failed to set limit profile for vending container");
}
}
public void RemoveLimitProfile(ItemContainer container) {
if (_limitProfile == null || _plugin.BagOfHolding == null) return;
_plugin.BagOfHolding.Call("API_RemoveLimitProfile", container);
}
}
private class ItemRetrieverApi {
public Func<BasePlayer, Dictionary<string, object>, int> SumPlayerItems {
get;
}
public Func<BasePlayer, Dictionary<string, object>, int, List<Item>, int> TakePlayerItems {
get;
}
public ItemRetrieverApi(Dictionary<string, object> apiDict) {
SumPlayerItems = apiDict[nameof(SumPlayerItems)] as Func<BasePlayer, Dictionary<string, object>, int>;
TakePlayerItems = apiDict[nameof(TakePlayerItems)] as Func<BasePlayer, Dictionary<string, object>, int, List<Item>, int>;
}
}
private class ItemRetrieverAdapter {
public ItemRetrieverApi Api {
get;
private set;
}
private CustomVendingSetup _plugin;
private Plugin ItemRetriever => _plugin.ItemRetriever;
public ItemRetrieverAdapter(CustomVendingSetup plugin) {
_plugin = plugin;
}
public void HandleItemRetrieverLoaded() {
Api = new ItemRetrieverApi(ItemRetriever.Call("API_GetApi") as Dictionary<string, object>);
}
public void HandleItemRetrieverUnloaded() {
Api = null;
}
}
#endregion
#region Exposed Hooks
private static class ExposedHooks {
public static object OnCustomVendingSetup(NPCVendingMachine vendingMachine) {
return Interface.CallHook("OnCustomVendingSetup", vendingMachine);
}
public static object CanPurchaseItem(BasePlayer player, Item item, Action<BasePlayer, Item> onItemPurchased, NPCVendingMachine vendingMachine, ItemContainer targetContainer) {
return Interface.CallHook("CanPurchaseItem", player, item, onItemPurchased, vendingMachine, targetContainer);
}
public static Dictionary<string, object> OnCustomVendingSetupDataProvider(NPCVendingMachine vendingMachine) {
return Interface.CallHook("OnCustomVendingSetupDataProvider", vendingMachine) as Dictionary<string, object>;
}
public static void OnCustomVendingSetupOfferSettingsParse(CaseInsensitiveDictionary<string> localizedSettings, CaseInsensitiveDictionary<object> customSettings) {
Interface.CallHook("OnCustomVendingSetupOfferSettingsParse", localizedSettings, customSettings);
}
public static void OnCustomVendingSetupOfferSettingsDisplay(CaseInsensitiveDictionary<object> customSettings, CaseInsensitiveDictionary<string> localizedSettings) {
Interface.CallHook("OnCustomVendingSetupOfferSettingsDisplay", customSettings, localizedSettings);
}
public static void OnCustomVendingSetupTransactionWithCustomSettings(NPCVendingMachine vendingMachine, CaseInsensitiveDictionary<object> customSettings) {
Interface.CallHook("OnCustomVendingSetupTransactionWithCustomSettings", vendingMachine, customSettings);
}
public static void OnCustomVendingSetupGiveSoldItem(NPCVendingMachine vendingMachine, Item item, BasePlayer player) {
Interface.CallHook("OnCustomVendingSetupGiveSoldItem", vendingMachine, item, player);
}
}
#endregion
#region Commands
private static class UICommands {
public const string Edit = "edit";
public const string Reset = "reset";
public const string Save = "save";
public const string Cancel = "cancel";
public const string ToggleBroadcast = "togglebroadcast";
public const string ToggleDroneAccessible = "toggledroneaccessible";
}
[Command("customvendingsetup.ui")]
private void CommandUI(IPlayer player, string cmd, string[] args) {
if (args.Length < 2) return;
if (!PassesUICommandChecks(player, args, out var vendingMachine, out var vendingController)) return;
var basePlayer = player.Object as BasePlayer;
var subCommand = args[1];
switch (subCommand) {
case UICommands.Edit : if (vendingController.EditController != null) {
basePlayer.EndLooting();
ChatMessage(basePlayer, Lang.ErrorCurrentlyBeingEdited, vendingController.EditController.EditorPlayer.displayName);
return;
}
vendingController.StartEditing(basePlayer, vendingMachine);
break;
case UICommands.Reset : vendingController.HandleReset();
// Allow Map data provider to be replaced with a Monument data provider.if (vendingController.DataProvider is MapDataProvider)
{
_vendingMachineManager.RefreshDataProvider(vendingMachine);
}
vendingMachine.FullUpdate();
basePlayer.EndLooting();
basePlayer.inventory.loot.SendImmediate();
OpenVendingMachineDelayed(basePlayer, vendingMachine);
break;
case UICommands.ToggleBroadcast : vendingController.EditController?.ToggleBroadcast();
break;
case UICommands.ToggleDroneAccessible : vendingController.EditController?.ToggleDroneAccessible();
break;
case UICommands.Save : vendingController.HandleSave(vendingMachine);
vendingMachine.FullUpdate();
OpenVendingMachine(basePlayer, vendingMachine);
break;
case UICommands.Cancel : OpenVendingMachine(basePlayer, vendingMachine);
break;
}
}
#endregion
#region Helper Methods
public static void LogInfo(string message) => Interface.Oxide.LogInfo($"[Custom Vending Setup] {message}");
public static void LogError(string message) => Interface.Oxide.LogError($"[Custom Vending Setup] {message}");
public static void LogWarning(string message) => Interface.Oxide.LogWarning($"[Custom Vending Setup] {message}");
private static bool IsLootingVendingMachine(BasePlayer player, NPCVendingMachine vendingMachine) {
return player.inventory.loot.containers.FirstOrDefault()?.entityOwner == vendingMachine;
}
private static bool AreVectorsClose(Vector3 a, Vector3 b, float xZTolerance = 0.01f, float yTolerance = 10) {
// Allow a generous amount of vertical distance given that plugins may snap entities to terrain.return Math.Abs(a.y - b.y) < yTolerance
&& Math.Abs(a.x - b.x) < xZTolerance && Math.Abs(a.z - b.z) < xZTolerance;
}
private static bool HasCondition(ItemDefinition itemDefinition) {
return itemDefinition.condition is {
enabled : true, max : > 0
}
;
}
private static bool HasItemMod<T>(ItemDefinition itemDefinition, out T itemModOfType) where T : ItemMod {
foreach (var itemMod in itemDefinition.itemMods) {
itemModOfType = itemMod as T;
if (itemModOfType is not null) return true;
}
itemModOfType = null;
return false;
}
private static void OpenVendingMachine(BasePlayer player, NPCVendingMachine vendingMachine) {
if (vendingMachine.OccupiedCheck(player) && Interface.CallHook("OnVendingShopOpen", vendingMachine, player) == null) {
vendingMachine.SendSellOrders(player);
vendingMachine.PlayerOpenLoot(player, vendingMachine.customerPanel);
Interface.CallHook(nameof(OnVendingShopOpened), vendingMachine, player);
}
}
private static bool CanVendingMachineBeSkinned(NPCVendingMachine vendingMachine) {
return vendingMachine is not InvisibleVendingMachine && vendingMachine.GetParentEntity() is not TravellingVendor;
}
private static bool CanVendingMachineBroadcast(NPCVendingMachine vendingMachine) {
return vendingMachine.GetParentEntity() is not TravellingVendor;
}
private static VendingOffer[] GetOffersFromVendingMachine(NPCVendingMachine vendingMachine) {
var vanillaOffers = vendingMachine.sellOrders.sellOrders;
var offers = new VendingOffer[vanillaOffers.Count];
for (var i = 0; i < offers.Length; i++) {
var manifestEntry = vendingMachine.vendingOrders.orders.ElementAtOrDefault(i);
offers[i] = VendingOffer.FromVanillaSellOrder(vanillaOffers[i], manifestEntry);
}
return offers;
}
private static VendingOffer[] GetOffersFromContainer(CustomVendingSetup plugin, BasePlayer player, ItemContainer container) {
var offers = new List<VendingOffer>();
for (var columnIndex = 0; columnIndex < 2; columnIndex++) {
for (var rowIndex = 0; rowIndex < MaxItemRows; rowIndex++) {
var sellItemSlot = rowIndex * ItemsPerRow + columnIndex * 3;
var sellItem = container.GetSlot(sellItemSlot);
var currencyItem = container.GetSlot(sellItemSlot + 1);
var settingsItem = container.GetSlot(sellItemSlot + 2);
if (sellItem == null || currencyItem == null) continue;
offers.Add(VendingOffer.FromItems(plugin, player, sellItem, currencyItem, settingsItem));
}
}
return offers.ToArray();
}
private static StorageContainer CreateContainerEntity(string prefabPath) {
var entity = GameManager.server.CreateEntity(prefabPath);
if (entity == null) return null;
var container = entity as StorageContainer;
if (container == null) {
UnityEngine.Object.Destroy(entity);
return null;
}
UnityEngine.Object.DestroyImmediate(container.GetComponent<DestroyOnGroundMissing>());
UnityEngine.Object.DestroyImmediate(container.GetComponent<GroundWatch>());
container.limitNetworking = true;
container.EnableSaving(false);
container.Spawn();
return container;
}
private static int OrderIndexToSlot(int orderIndex) {
if (orderIndex < MaxItemRows) return orderIndex * ItemsPerRow;
return (orderIndex % MaxItemRows) * ItemsPerRow + 3;
}
private static string CreateNoteContents(Dictionary<string, string> settingsMap) {
var lines = new List<string>();
foreach (var entry in settingsMap) {
lines.Add($"{entry.Key}: {entry.Value}");
}
return string.Join("\n", lines);
}
private static StorageContainer CreateOrdersContainer(CustomVendingSetup plugin, NPCVendingMachine vendingMachine, BasePlayer player, VendingOffer[] vendingOffers) {
var containerEntity = CreateContainerEntity(StoragePrefab);
var container = containerEntity.inventory;
container.allowedContents = ItemContainer.ContentsType.Generic;
container.capacity = ContainerCapacity;
plugin._bagOfHoldingLimitManager.SetLimitProfile(container);
for (var orderIndex = 0; orderIndex < vendingOffers.Length && orderIndex < 9; orderIndex++) {
var offer = vendingOffers[orderIndex];
var sellItem = offer.SellItem.Create();
if (sellItem == null) continue;
var currencyItem = offer.CurrencyItem.Create();
if (currencyItem == null) {
sellItem.Remove();
continue;
}
var destinationSlot = OrderIndexToSlot(orderIndex);
if (!sellItem.MoveToContainer(container, destinationSlot)) sellItem.Remove();
if (!currencyItem.MoveToContainer(container, destinationSlot + 1)) currencyItem.Remove();
}
// Add 7 note items, so the user doesn't have to make them.for (var orderIndex = 0; orderIndex < 7; orderIndex++)
{
var offer = vendingOffers.Length > orderIndex ? vendingOffers[orderIndex] : null;
var settingsItem = ItemManager.Create(plugin._noteItemDefinition);
if (settingsItem == null) continue;
var refillMaxLabel = plugin.GetMessage(player, Lang.SettingsRefillMax);
var refillDelayLabel = plugin.GetMessage(player, Lang.SettingsRefillDelay);
var refillAmountLabel = plugin.GetMessage(player, Lang.SettingsRefillAmount);
var settingsMap = new CaseInsensitiveDictionary<string> {
[refillMaxLabel]
= (offer?.RefillMax ?? VendingOffer.DefaultRefillMax).ToString(), [refillDelayLabel] = (offer?.RefillDelay ?? VendingOffer.DefaultRefillDelay).ToString(), [refillAmountLabel] = (offer?.RefillAmount ?? VendingOffer.DefaultRefillAmount).ToString(),
}
;
// Allow other plugins to parse the custom settings and display localized options.ExposedHooks.OnCustomVendingSetupOfferSettingsDisplay(
offer?.CustomSettings ?? new CaseInsensitiveDictionary<object>(), settingsMap);
settingsItem.text = CreateNoteContents(settingsMap);
var destinationSlot = OrderIndexToSlot(orderIndex);
if (!settingsItem.MoveToContainer(container, destinationSlot + 2)) {
settingsItem.Remove();
}
}
var generalSettingsItem = ItemManager.Create(plugin._noteItemDefinition);
if (generalSettingsItem != null) {
var settingsMap = new CaseInsensitiveDictionary<string>();
if (NPCVendingMachine.DynamicPricingEnabled) {
var dynamicPricingLabel = plugin.GetMessage(player, Lang.SettingsBypassDynamicPricing);
settingsMap[dynamicPricingLabel] = vendingMachine.BypassDynamicPricing.ToString();
}
if (CanVendingMachineBeSkinned(vendingMachine)) {
var skinIdLabel = plugin.GetMessage(player, Lang.SettingsSkinId);
settingsMap[skinIdLabel] = vendingMachine.skinID.ToString();
}
var shopNameLabel = plugin.GetMessage(player, Lang.SettingsShopName);
settingsMap[shopNameLabel] = vendingMachine.shopName;
generalSettingsItem.text = CreateNoteContents(settingsMap);
if (!generalSettingsItem.MoveToContainer(container, GeneralSettingsNoteSlot)) {
generalSettingsItem.Remove();
}
}
return containerEntity;
}
private static void MaybeGiveWeaponAmmo(Item item, BasePlayer player) {
var heldEntity = item.GetHeldEntity();
if (heldEntity == null) return;
if (heldEntity.creationFrame == Time.frameCount) {
// The item was probably split off another item, so don't refund its ammo.return;
}
var baseProjectile = heldEntity as BaseProjectile;
if ((object)baseProjectile != null) {
var ammoType = baseProjectile.primaryMagazine?.ammoType;
if (ammoType != null && baseProjectile.primaryMagazine.contents > 0) {
var ammoItem = ItemManager.Create(ammoType, baseProjectile.primaryMagazine.contents);
if (ammoItem != null) {
player.GiveItem(ammoItem);
}
}
return;
}
var flameThrower = heldEntity as FlameThrower;
if ((object)flameThrower != null) {
if (flameThrower.fuelType != null && flameThrower.ammo > 0) {
var ammoItem = ItemManager.Create(flameThrower.fuelType, flameThrower.ammo);
if (ammoItem != null) {
player.GiveItem(ammoItem);
}
}
}
}
private static void GiveSoldItem(Item item, BasePlayer player, ref TransactionContext transaction) {
var vendingMachine = transaction.VendingMachine;
var targetContainer = transaction.TargetContainer;
// Unset the placeholder flag to allow Enchanted Items to transform the artifact.item.SetFlag(Item.Flag.Placeholder, false);
if (targetContainer == null) {
vendingMachine.GiveSoldItem(item, player);
}
else if (!item.MoveToContainer(targetContainer)) {
item.Drop(targetContainer.dropPosition, targetContainer.dropVelocity);
}
transaction.OnMarketplaceItemPurchase?.Invoke(player, item);
}
private static int GetHighestUsedSlot(ProtoBuf.ItemContainer containerData) {
var highestUsedSlot = -1;
foreach (var item in containerData.contents) {
if (item.slot > highestUsedSlot) {
highestUsedSlot = item.slot;
}
}
return highestUsedSlot;
}
private static void AddItemForNetwork(ProtoBuf.ItemContainer containerData, int slot, int itemId, int amount, ItemId uid) {
var itemData = Pool.Get<ProtoBuf.Item>();
itemData.slot = slot;
itemData.itemid = itemId;
itemData.amount = amount;
itemData.UID = uid;
containerData.contents.Add(itemData);
}
private static CaseInsensitiveDictionary<string> ParseSettings(string text) {
var dict = new CaseInsensitiveDictionary<string>();
if (string.IsNullOrEmpty(text)) return dict;
foreach (Match match in KeyValueRegex.Matches(text)) {
dict[match.Groups[1].Value.Trim()] = match.Groups[2].Value.Trim();
}
return dict;
}
private object CallPlugin<T1>(Plugin plugin, string methodName, T1 arg1) {
_objectArray1[0] = ObjectCache.Get(arg1);
return plugin.Call(methodName, _objectArray1);
}
private object CallPlugin<T1, T2>(Plugin plugin, string methodName, T1 arg1, T2 arg2) {
_objectArray2[0] = ObjectCache.Get(arg1);
_objectArray2[1] = ObjectCache.Get(arg2);
return plugin.Call(methodName, _objectArray2);
}
private void ScheduleRemoveUI(NPCVendingMachine vendingMachine, BasePlayer player, VendingMachineComponent component) {
component.Invoke(() => {
if (vendingMachine == null || vendingMachine.IsDestroyed) return;
if (IsLootingVendingMachine(player, vendingMachine) && !vendingMachine.IsInvoking(vendingMachine.CompletePendingOrder)) return;
// Remove the UI because the player stopped viewing the vending machine or the transaction is pending.component.RemoveUI(player);
}
, 0);
}
private void AddCurrencyToContainerSnapshot(BasePlayer player, ProtoBuf.ItemContainer containerData) {
if (containerData == null || containerData.slots < InventorySize || !_playersNeedingFakeInventory.Contains(player)) return;
var lootingContainer = player.inventory.loot.containers.FirstOrDefault();
var vendingMachine = lootingContainer?.entityOwner as NPCVendingMachine;
if ((object)vendingMachine == null) return;
var profile = _componentTracker.GetComponent(vendingMachine)?.Profile;
if (profile == null) return;
var nextInvisibleSlot = Math.Max(containerData.slots, GetHighestUsedSlot(containerData) + 1);
if (_config.Economics.EnabledAndValid && profile.HasPaymentProviderCurrency(_config.Economics)) {
AddItemForNetwork(containerData, slot : nextInvisibleSlot, itemId : _config.Economics.ItemDefinition.itemid, amount : _paymentProviderResolver.EconomicsPaymentProvider.GetBalance(player), uid : new ItemId(ulong.MaxValue - (ulong)nextInvisibleSlot) );
nextInvisibleSlot++;
}
if (_config.ServerRewards.EnabledAndValid && profile.HasPaymentProviderCurrency(_config.ServerRewards)) {
AddItemForNetwork(containerData, slot : nextInvisibleSlot, itemId : _config.ServerRewards.ItemDefinition.itemid, amount : _paymentProviderResolver.ServerRewardsPaymentProvider.GetBalance(player), uid : new ItemId(ulong.MaxValue - (ulong)nextInvisibleSlot) );
nextInvisibleSlot++;
}
containerData.slots = nextInvisibleSlot;
}
private Dictionary<string, object> SetupItemRetrieverQuery(ref ItemQuery itemQuery) {
_itemRetrieverQuery.Clear();
_itemRetrieverQuery["MinCondition"] = ObjectCache.Get(MinCurrencyCondition);
_itemRetrieverQuery["RequireEmpty"] = True;
if (itemQuery.BlueprintId != 0) {
_itemRetrieverQuery["BlueprintId"] = ObjectCache.Get(itemQuery.BlueprintId);
}
if (itemQuery.DataInt != 0) {
_itemRetrieverQuery["DataInt"] = ObjectCache.Get(itemQuery.DataInt);
}
if (itemQuery.ItemId != 0) {
_itemRetrieverQuery["ItemId"] = ObjectCache.Get(itemQuery.ItemId);
}
if (itemQuery.SkinId.HasValue) {
_itemRetrieverQuery["SkinId"] = ObjectCache.Get(itemQuery.SkinId.Value);
}
return _itemRetrieverQuery;
}
private int SumPlayerItems(BasePlayer player, ref ItemQuery itemQuery) {
return _itemRetrieverAdapter?.Api?.SumPlayerItems.Invoke(player, SetupItemRetrieverQuery(ref itemQuery)) ?? ItemUtils.SumPlayerItems(player, ref itemQuery);
}
private int TakePlayerItems(BasePlayer player, ref ItemQuery itemQuery, int amount, List<Item> collect = null) {
return _itemRetrieverAdapter?.Api?.TakePlayerItems.Invoke(player, SetupItemRetrieverQuery(ref itemQuery), amount, collect) ?? ItemUtils.TakePlayerItems(player, ref itemQuery, amount, collect);
}
private bool PassesUICommandChecks(IPlayer player, string[] args, out NPCVendingMachine vendingMachine, out VendingController controller) {
vendingMachine = null;
controller = null;
if (player.IsServer || !player.HasPermission(PermissionUse)) return false;
if (args.Length == 0 || !ulong.TryParse(args[0], out var vendingMachineId)) return false;
vendingMachine = BaseNetworkable.serverEntities.Find(new NetworkableId(vendingMachineId)) as
// ... [Code preview truncated due to length. Full source in attached file] ...