C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
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 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>;
namespace Oxide.Plugins {
[Info("Custom Vending Setup", "WhiteThunder", "2.8.0")]
[Description("Allows editing orders at NPC vending machines.")]
internal class CustomVendingSetup : CovalencePlugin {
#region Fields
[PluginReference]
private Plugin BagOfHolding, InstantBuy, MonumentFinder, VendingInStock;
private SavedData _pluginData;
private Configuration _pluginConfig;
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;
// Going over 7 causes offers to get cut off regardless of resolution.private const int MaxVendingOffers = 7;
private const int ShopNameNoteSlot = 29;
private const int ContainerCapacity = 30;
private const int MaxItemRows = ContainerCapacity / ItemsPerRow;
private readonly object True = true;
private readonly object False = false;
private DataProviderRegistry _dataProviderRegistry = new DataProviderRegistry();
private ComponentTracker<NPCVendingMachine, VendingMachineComponent> _componentTracker = new ComponentTracker<NPCVendingMachine, VendingMachineComponent>();
private ComponentFactory<NPCVendingMachine, VendingMachineComponent> _componentFactory;
private MonumentFinderAdapter _monumentFinderAdapter;
private VendingMachineManager _vendingMachineManager;
private BagOfHoldingLimitManager _bagOfHoldingLimitManager;
private DynamicHookSubscriber<BaseVendingController> _inaccessibleVendingMachines;
private ItemDefinition _noteItemDefinition;
private bool _isServerInitialized;
private bool _performingInstantRestock;
public CustomVendingSetup() {
_monumentFinderAdapter = new MonumentFinderAdapter(this);
_componentFactory = new ComponentFactory<NPCVendingMachine, VendingMachineComponent>(this, _componentTracker);
_vendingMachineManager = new VendingMachineManager(this, _componentFactory, _dataProviderRegistry);
_bagOfHoldingLimitManager = new BagOfHoldingLimitManager(this);
_inaccessibleVendingMachines = new DynamicHookSubscriber<BaseVendingController>(this, nameof(CanAccessVendingMachine));
}
#endregion
#region Hooks
private void Init() {
_pluginConfig.Init();
_pluginData = SavedData.Load();
permission.RegisterPermission(PermissionUse, this);
Unsubscribe(nameof(OnEntitySpawned));
_inaccessibleVendingMachines.UnsubscribeAll();
}
private void OnServerInitialized() {
_isServerInitialized = true;
if (MonumentFinder == null) {
LogError("MonumentFinder is not loaded, get it at http://umod.org.");
}
else {
// 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);
}
}
}
);
}
_bagOfHoldingLimitManager.OnServerInitialized();
Subscribe(nameof(OnEntitySpawned));
_noteItemDefinition = ItemManager.FindItemDefinition("note");
}
private void Unload() {
_vendingMachineManager.ResetAll();
}
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;
}
}
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);
}
if (_pluginConfig.ShopUISettings.EnableSkinOverlays && controller.Profile?.Offers != null) {
component.ShowShopUI(player);
}
}
private object OnVendingTransaction(NPCVendingMachine vendingMachine, BasePlayer player, int sellOrderIndex, int numberOfTransactions, ItemContainer targetContainer) {
// Only override transaction logic if the vending machine is customized by this plugin.var vendingProfile = _vendingMachineManager.GetController(vendingMachine)?.Profile;
if (vendingProfile?.Offers == null) {
return null;
}
var offer = vendingProfile.GetOfferForSellOrderIndex(sellOrderIndex);
if (offer == null) {
return null;
}
// Get all item stacks in the vending machine that match the sold item.var sellableItems = Facepunch.Pool.GetList<Item>();
var amountAvailable = 0;
var sellItemSpec = offer.SellItem.GetItemSpec().Value;
sellItemSpec.FindAllInContainer(vendingMachine.inventory, sellableItems, ref amountAvailable, MatchOptions.Skin);
if (sellableItems.Count == 0) {
Facepunch.Pool.FreeList(ref sellableItems);
return False;
}
// Verify the vending machine has sufficient stock.numberOfTransactions = Mathf.Clamp(numberOfTransactions, 1, sellableItems[0].hasCondition ? 1 : 1000000);
var amountRequested = offer.SellItem.Amount * numberOfTransactions;
if (amountRequested > amountAvailable) {
Facepunch.Pool.FreeList(ref sellableItems);
return False;
}
// Get all item stacks in the player inventory that match the currency item.var currencyItems = Facepunch.Pool.GetList<Item>();
var currencyAvailable = 0;
var currencyItemSpec = offer.CurrencyItem.GetItemSpec().Value;
var currencyMatchOptions = MatchOptions.Empty;
if (currencyItemSpec.Skin != 0) {
// Require skin to match if not default skin.currencyMatchOptions |= MatchOptions.Skin;
}
currencyItemSpec.FindAllInInventory(player.inventory, currencyItems, ref currencyAvailable, currencyMatchOptions);
if (currencyItems.Count == 0) {
Facepunch.Pool.FreeList(ref sellableItems);
Facepunch.Pool.FreeList(ref currencyItems);
return False;
}
// Verify the player has enough currency.var currencyRequired = offer.CurrencyItem.Amount * numberOfTransactions;
if (currencyAvailable < currencyRequired) {
Facepunch.Pool.FreeList(ref sellableItems);
Facepunch.Pool.FreeList(ref currencyItems);
return False;
}
var marketTerminal = targetContainer?.entityOwner as MarketTerminal;
// Temporarily allow the vending machine internal storage to accept items.// Currency items are temporarily added to the vending machine internal storage before deleting.vendingMachine.transactionActive = true;
var currencyToTake = currencyRequired;
foreach (var currencyItem in currencyItems) {
var useableAmount = (currencyItem.contents?.itemList?.Count ?? 0) > 0 ? currencyItem.amount - 1 : currencyItem.amount;
if (useableAmount <= 0) continue;
var amountToTake = Mathf.Min(currencyToTake, useableAmount);
currencyToTake -= amountToTake;
var itemToTake = currencyItem.amount > amountToTake ? currencyItemSpec.Split(currencyItem, amountToTake) : currencyItem;
// Only take ammo from the original currency item, never the split item.if (itemToTake == currencyItem)
{
GiveWeaponAmmo(currencyItem, player);
}
// 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.if (Interface.CallHook("OnTakeCurrencyItem", vendingMachine, itemToTake) == null)
{
itemToTake.RemoveFromContainer();
itemToTake.Remove();
}
marketTerminal?._onCurrencyRemovedCached?.Invoke(player, itemToTake);
if (currencyToTake <= 0) break;
}
vendingMachine.transactionActive = false;
Facepunch.Pool.FreeList(ref currencyItems);
// Perform instant restock if VendingInStock is loaded, since we are replacing its behavior.if (offer.RefillDelay == 0)
{
sellableItems[0].amount += amountRequested;
_performingInstantRestock = true;
}
var maxStackSize = _pluginConfig.GetItemMaxStackSize(sellableItems[0]);
var amountToGive = amountRequested;
foreach (var sellableItem in sellableItems) {
while (amountToGive > maxStackSize && sellableItem.amount > maxStackSize) {
var itemToGive1 = sellItemSpec.Split(sellableItem, maxStackSize);
amountToGive -= itemToGive1.amount;
object canPurchaseHookResult1 = CallHookCanPurchaseItem(player, itemToGive1, marketTerminal?._onItemPurchasedCached, vendingMachine, targetContainer);
if (canPurchaseHookResult1 is bool) {
Facepunch.Pool.FreeList(ref sellableItems);
return canPurchaseHookResult1;
}
GiveSoldItem(vendingMachine, itemToGive1, player, marketTerminal, targetContainer);
}
var itemToGive2 = sellableItem.amount > amountToGive ? sellItemSpec.Split(sellableItem, amountToGive) : sellableItem;
amountToGive -= itemToGive2.amount;
object canPurchaseHookResult2 = CallHookCanPurchaseItem(player, itemToGive2, marketTerminal?._onItemPurchasedCached, vendingMachine, targetContainer);
if (canPurchaseHookResult2 is bool) {
Facepunch.Pool.FreeList(ref sellableItems);
return canPurchaseHookResult2;
}
GiveSoldItem(vendingMachine, itemToGive2, player, marketTerminal, targetContainer);
if (amountToGive <= 0) {
break;
}
}
_performingInstantRestock = false;
Facepunch.Pool.FreeList(ref sellableItems);
vendingMachine.UpdateEmptyFlag();
if (InstantBuy == null) {
var lootContainer = player.inventory.loot.containers.FirstOrDefault();
if (lootContainer != null && lootContainer.entityOwner == vendingMachine) {
OnVendingShopOpened(vendingMachine, player);
}
}
return True;
}
private void OnBuyVendingItem(NPCVendingMachine vendingMachine, BasePlayer player, int sellOrderID, int amount) {
if (InstantBuy == null) {
_componentTracker.GetComponent(vendingMachine)?.RemoveUI(player);
}
}
// This hook is exposed by plugin: Vending In Stock (VendingInStock).private object CanVendingStockRefill(NPCVendingMachine vendingMachine, Item soldItem, BasePlayer player)
{
if (!IsCustomized(vendingMachine)) {
return null;
}
// If refill delay is 0, the item has already been restocked.if (_performingInstantRestock)
{
return False;
}
var itemSpec = ItemSpec.FromItem(soldItem);
// Override VendingInStock behavior to prevent creating new items in the container.// This also ensures additional item attributes are preserved.var existingItem = itemSpec.FirstInContainer(vendingMachine.inventory, MatchOptions.Skin);
if (existingItem != null) {
existingItem.amount += soldItem.amount;
existingItem.MarkDirty();
return False;
}
var newItem = itemSpec.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;
}
#endregion
#region API
private bool API_IsCustomized(NPCVendingMachine vendingMachine) {
return IsCustomized(vendingMachine);
}
private void API_RefreshDataProvider(NPCVendingMachine vendingMachine) {
_vendingMachineManager.HandleVendingMachineKilled(vendingMachine);
_vendingMachineManager.HandleVendingMachineSpawned(vendingMachine);
}
// Undocumented.Intended for MonumentAddons migration to become a Data Provider.private JObject API_MigrateVendingProfile(NPCVendingMachine vendingMachine)
{
var location = MonumentRelativePosition.FromVendingMachine(_monumentFinderAdapter, vendingMachine);
if (location == null) {
// This can happen if a vending machine was moved outside a monument's bounds.return null;
}
var vendingProfile = _pluginData.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;
}
_pluginData.VendingProfiles.Remove(vendingProfile);
_pluginData.Save();
return jObject;
}
#endregion
#region Dependencies
private class MonumentAdapter {
public string PrefabName => (string)_monumentInfo["PrefabName"];
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) => ((Func<Vector3, Vector3>)_monumentInfo["InverseTransformPoint"]).Invoke(worldPosition);
public bool IsInBounds(Vector3 position) => ((Func<Vector3, bool>)_monumentInfo["IsInBounds"]).Invoke(position);
}
private class MonumentFinderAdapter {
private CustomVendingSetup _plugin;
private Plugin _monumentFinder => _plugin.MonumentFinder;
private bool _isLoaded => _monumentFinder != null;
public MonumentFinderAdapter(CustomVendingSetup plugin) {
_plugin = plugin;
}
public MonumentAdapter GetMonumentAdapter(Vector3 position) {
var dictResult = _monumentFinder?.Call("API_GetClosest", position) as Dictionary<string, object>;
if (dictResult == null) 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 bool) || (bool)result == false) {
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);
}
}
#endregion
#region Exposed Hooks
private static bool SetupVendingMachineWasBlocked(NPCVendingMachine vendingMachine) {
object hookResult = Interface.CallHook("OnCustomVendingSetup", vendingMachine);
return hookResult is bool && (bool)hookResult == false;
}
private static object CallHookCanPurchaseItem(BasePlayer player, Item item, Action<BasePlayer, Item> onItemPurchased, NPCVendingMachine vendingMachine, ItemContainer targetContainer) {
return Interface.CallHook("CanPurchaseItem", player, item, onItemPurchased, vendingMachine, targetContainer);
}
private static Dictionary<string, object> CallHookDataProvider(NPCVendingMachine vendingMachine) {
return Interface.CallHook("OnCustomVendingSetupDataProvider", vendingMachine) as Dictionary<string, object>;
}
#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;
NPCVendingMachine vendingMachine;
BaseVendingController vendingController;
if (!PassesUICommandChecks(player, args, out vendingMachine, out 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();
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 AreVectorsClose(Vector3 a, Vector3 b, float xZTolerance = 0.001f, 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 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 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 StorageContainer CreateOrdersContainer(CustomVendingSetup plugin, BasePlayer player, VendingOffer[] vendingOffers, string shopName) {
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);
settingsItem.text = $"{refillMaxLabel}: {offer?.RefillMax ?? VendingOffer.DefaultRefillMax}" + $"\n{refillDelayLabel}: {offer?.RefillDelay ?? VendingOffer.DefaultRefillDelay}" + $"\n{refillAmountLabel}: {offer?.RefillAmount ?? VendingOffer.DefaultRefillAmount}";
var destinationSlot = OrderIndexToSlot(orderIndex);
if (!settingsItem.MoveToContainer(container, destinationSlot + 2)) settingsItem.Remove();
}
var generalSettingsItem = ItemManager.Create(plugin._noteItemDefinition);
if (generalSettingsItem != null) {
generalSettingsItem.text = shopName;
if (!generalSettingsItem.MoveToContainer(container, ShopNameNoteSlot)) generalSettingsItem.Remove();
}
return containerEntity;
}
private static void GiveWeaponAmmo(Item item, BasePlayer player) {
var heldEntity = item.GetHeldEntity();
if (heldEntity == null) 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(NPCVendingMachine vendingMachine, Item item, BasePlayer player, MarketTerminal marketTerminal, ItemContainer 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);
}
marketTerminal?._onItemPurchasedCached?.Invoke(player, item);
}
private bool PassesUICommandChecks(IPlayer player, string[] args, out NPCVendingMachine vendingMachine, out BaseVendingController controller) {
vendingMachine = null;
controller = null;
if (player.IsServer || !player.HasPermission(PermissionUse)) return false;
uint vendingMachineId;
if (args.Length == 0 || !uint.TryParse(args[0], out vendingMachineId)) return false;
vendingMachine = BaseNetworkable.serverEntities.Find(vendingMachineId) as NPCVendingMachine;
if (vendingMachine == null) return false;
controller = _vendingMachineManager.GetController(vendingMachine);
if (controller == null) return false;
return true;
}
private void OpenVendingMachineDelayed(BasePlayer player, NPCVendingMachine vendingMachine, float delay = 0.1f) {
timer.Once(delay, () => {
if (player == null || vendingMachine == null || vendingMachine.IsDestroyed) return;
OpenVendingMachine(player, vendingMachine);
}
);
}
private bool IsCustomized(NPCVendingMachine vendingMachine) => _vendingMachineManager.GetController(vendingMachine)?.Profile?.Offers != null;
#endregion
#region UI
private static class UIConstants {
public const string EditButtonColor = "0.451 0.553 0.271 1";
public const string EditButtonTextColor = "0.659 0.918 0.2 1";
public const string ResetButtonColor = "0.9 0.5 0.2 1";
public const string ResetButtonTextColor = "1 0.9 0.7 1";
public const string SaveButtonColor = EditButtonColor;
public const string SaveButtonTextColor = EditButtonTextColor;
public const string CancelButtonColor = "0.4 0.4 0.4 1";
public const string CancelButtonTextColor = "0.71 0.71 0.71 1";
public const float PanelWidth = 380.5f;
public const float HeaderHeight = 23;
public const float ItemSpacing = 4;
public const float ItemBoxSize = 58;
public const int ButtonHorizontalSpacing = 6;
public const int ButtonHeight = 32;
public const int ButtonWidth = 80;
public const string TexturedBackgroundSprite = "assets/content/ui/ui.background.tiletex.psd";
public const string BroadcastIcon = "assets/icons/broadcast.png";
public const string DroneIcon = "assets/icons/drone.png";
public const string IconMaterial = "assets/icons/iconmaterial.mat";
public const string GreyOutMaterial = "assets/icons/greyout.mat";
public const string AnchorMin = "0.5 0";
public const string AnchorMax = "0.5 0";
}
private class EditFormState {
public static EditFormState FromVendingMachine(BaseVendingController vendingController, NPCVendingMachine vendingMachine) {
return new EditFormState {
Broadcast = vendingController.Profile?.Broadcast ?? vendingMachine.IsBroadcasting(), DroneAccessible = vendingController.Profile?.DroneAccessible ?? true,
}
;
}
public bool Broadcast;
public bool DroneAccessible;
}
private static class ContainerUIRenderer {
public const string UIName = "CustomVendingSetup.ContainerUI";
public const string TipUIName = "CustomVendingSetup.ContainerUI.Tip";
public const string BroadcastUIName = "CustomVendingSetup.ContainerUI.Broadcast";
public const string DroneUIName = "CustomVendingSetup.ContainerUI.Drone";
public static string RenderContainerUI(CustomVendingSetup plugin, BasePlayer player, NPCVendingMachine vendingMachine, EditFormState uiState) {
var offsetX = 192;
var offsetY = 141;
var cuiElements = new CuiElementContainer {
{
new CuiPanel {
RectTransform = {
AnchorMin = UIConstants.AnchorMin, AnchorMax = UIConstants.AnchorMax, OffsetMin = $"{offsetX} {offsetY + MaxItemRows * (UIConstants.ItemBoxSize + UIConstants.ItemSpacing)}", OffsetMax = $"{offsetX} {offsetY + MaxItemRows * (UIConstants.ItemBoxSize + UIConstants.ItemSpacing)}",
}
,
}
, "Hud.Menu", UIName
}
}
;
var saveButtonText = plugin.GetMessage(player, Lang.ButtonSave);
var cancelButtonText = plugin.GetMessage(player, Lang.ButtonCancel);
var vendingMachineId = vendingMachine.net.ID;
AddButton(cuiElements, vendingMachineId, saveButtonText, UICommands.Save, UIConstants.PanelWidth - UIConstants.ButtonWidth - UIConstants.ButtonHorizontalSpacing, UIConstants.SaveButtonColor, UIConstants.SaveButtonTextColor );
AddButton(cuiElements, vendingMachineId, cancelButtonText, UICommands.Cancel, UIConstants.PanelWidth, UIConstants.CancelButtonColor, UIConstants.CancelButtonTextColor );
AddBroadcastButton(cuiElements, vendingMachine, uiState);
AddDroneButton(cuiElements, vendingMachine, uiState);
var headerOffset = -6;
cuiElements.Add(new CuiElement {
Components = {
new CuiRawImageComponent {
Color = UIConstants.CancelButtonColor, Sprite = UIConstants.TexturedBackgroundSprite,
}
, new CuiRectTransformComponent {
AnchorMin = UIConstants.AnchorMin, AnchorMax = UIConstants.AnchorMax, OffsetMin = $"0 {headerOffset - UIConstants.HeaderHeight}", OffsetMax = $"{UIConstants.PanelWidth} {headerOffset}",
}
}
, Parent = UIName, Name = TipUIName,
}
);
var forSaleText = plugin.GetMessage(player, Lang.InfoForSale);
var costText = plugin.GetMessage(player, Lang.InfoCost);
var settingsText = plugin.GetMessage(player, Lang.InfoSettings);
AddHeaderLabel(cuiElements, 0, forSaleText);
AddHeaderLabel(cuiElements, 1, costText);
AddHeaderLabel(cuiElements, 2, settingsText);
AddHeaderLabel(cuiElements, 3, forSaleText);
AddHeaderLabel(cuiElements, 4, costText);
AddHeaderLabel(cuiElements, 5, settingsText);
return CuiHelper.ToJson(cuiElements);
}
private static void AddHeaderLabel(CuiElementContainer cuiElements, int index, string text) {
float xMin = 6 + index * (UIConstants.ItemBoxSize + UIConstants.ItemSpacing);
float xMax = xMin + UIConstants.ItemBoxSize;
cuiElements.Add(new CuiLabel {
Text = {
Text = text, Color = UIConstants.CancelButtonTextColor, Align = TextAnchor.MiddleCenter, FontSize = 13,
}
, RectTransform = {
AnchorMin = "0 0", AnchorMax = "0 0", OffsetMin = $"{xMin} 0", OffsetMax = $"{xMax} {UIConstants.HeaderHeight}",
}
}
, TipUIName );
}
private static void AddBroadcastButton(CuiElementContainer cuiElements, NPCVendingMachine vendingMachine, EditFormState uiState) {
var iconSize = UIConstants.ButtonHeight;
var xMax = UIConstants.PanelWidth - 2 * (UIConstants.ButtonWidth + UIConstants.ButtonHorizontalSpacing);
var xMin = xMax - iconSize;
cuiElements.Add(new CuiElement {
Parent = UIName, Name = BroadcastUIName, Components = {
new CuiButtonComponent {
Color = "0 0 0 0", Command = $"customvendingsetup.ui {vendingMachine.net.ID} {UICommands.ToggleBroadcast}",
}
, new CuiRectTransformComponent {
AnchorMin = "0 0", AnchorMax = "0 0", OffsetMin = $"{xMin} 0", OffsetMax = $"{xMax} {UIConstants.ButtonHeight}",
}
,
}
,
}
);
cuiElements.Add(new CuiElement {
Parent = BroadcastUIName, Components = {
new CuiRawImageComponent {
Color = uiState.Broadcast ? UIConstants.SaveButtonTextColor : UIConstants.CancelButtonTextColor, Sprite = UIConstants.BroadcastIcon, Material = UIConstants.IconMaterial,
}
, new CuiRectTransformComponent {
AnchorMin = "0 0", AnchorMax = "0 0", OffsetMin = "0 0", OffsetMax = $"{iconSize} {iconSize}",
}
,
}
,
}
);
}
private static void AddDroneButton(CuiElementContainer cuiElements, NPCVendingMachine vendingMachine, EditFormState uiState) {
var iconSize = UIConstants.ButtonHeight;
var xMax = - UIConstants.ButtonHorizontalSpacing;
var xMin = xMax - iconSize;
var droneAccessible = uiState.Broadcast && uiState.DroneAccessible;
cuiElements.Add(new CuiElement {
Parent = BroadcastUIName, Name = DroneUIName, Components = {
new CuiButtonComponent {
Color = "0 0 0 0", Command = $"customvendingsetup.ui {vendingMachine.net.ID} {UICommands.ToggleDroneAccessible}",
}
, new CuiRectTransformComponent {
AnchorMin = "0 0", AnchorMax = "0 0", OffsetMin = $"{xMin} 0", OffsetMax = $"{xMax} {UIConstants.ButtonHeight}",
}
,
}
,
}
);
cuiElements.Add(new CuiElement {
Parent = DroneUIName, Components = {
new CuiImageComponent {
Color = droneAccessible ? UIConstants.SaveButtonTextColor : UIConstants.CancelButtonTextColor, Sprite = UIConstants.DroneIcon, Material = UIConstants.GreyOutMaterial,
}
, new CuiRectTransformComponent {
AnchorMin = "0 0", AnchorMax = "0 0", OffsetMin = "0 0", OffsetMax = $"{iconSize} {iconSize}",
}
,
}
,
}
);
}
public static string RenderBroadcastUI(BasePlayer player, NPCVendingMachine vendingMachine, EditFormState uiState) {
var cuiElements = new CuiElementContainer();
AddBroadcastButton(cuiElements, vendingMachine, uiState);
AddDroneButton(cuiElements, vendingMachine, uiState);
return CuiHelper.ToJson(cuiElements);
}
private static void AddButton(CuiElementContainer cuiElements, uint vendingMachineId, string text, string subCommand, float xMax, string color, string textColor) {
var xMin = xMax - UIConstants.ButtonWidth;
cuiElements.Add(new CuiButton {
Text = {
Text = text, Color = textColor, Align = TextAnchor.MiddleCenter, FontSize = 18,
}
, Button = {
Color = color, FadeIn = 0.1f, Command = $"customvendingsetup.ui {vendingMachineId} {subCommand}",
}
, RectTransform = {
AnchorMin = "0 0", AnchorMax = "0 0", OffsetMin = $"{xMin} 0", OffsetMax = $"{xMax} {UIConstants.ButtonHeight}",
}
,
}
, UIName );
}
}
private static class AdminUIRenderer {
public const string UIName = "CustomVendingSetup.AdminUI";
public static string RenderAdminUI(CustomVendingSetup plugin, BasePlayer player, NPCVendingMachine vendingMachine, VendingProfile profile) {
var numSellOrders = vendingMachine.sellOrders?.sellOrders.Count ?? 0;
var offsetY = 136 + 74 * numSellOrders;
var offsetX = 192;
var cuiElements = new CuiElementContainer {
{
new CuiPanel {
RectTransform = {
AnchorMin = UIConstants.AnchorMin, AnchorMax = UIConstants.AnchorMax, OffsetMin = $"{offsetX} {offsetY}", OffsetMax = $"{offsetX} {offsetY}",
}
,
}
, "Overlay", UIName
}
}
;
var buttonIndex = 0;
var vendingMachineId = vendingMachine.net.ID;
if (profile != null) {
var resetButtonText = plugin.GetMessage(player, Lang.ButtonReset);
AddVendingButton(cuiElements, vendingMachineId, resetButtonText, UICommands.Reset, buttonIndex, UIConstants.ResetButtonColor, UIConstants.ResetButtonTextColor);
buttonIndex++;
}
var editButtonText = plugin.GetMessage(player, Lang.ButtonEdit);
AddVendingButton(cuiElements, vendingMachineId, editButtonText, UICommands.Edit, buttonIndex, UIConstants.SaveButtonColor, UIConstants.SaveButtonTextColor);
return CuiHelper.ToJson(cuiElements);
}
private static float GetButtonOffset(int reverseButtonIndex) {
return UIConstants.PanelWidth - reverseButtonIndex * (UIConstants.ButtonWidth + UIConstants.ButtonHorizontalSpacing);
}
private static void AddVendingButton(CuiElementContainer cuiElements, uint vendingMachineId, string text, string subCommand, int reverseButtonIndex, string color, string textColor) {
var xMax = GetButtonOffset(reverseButtonIndex);
var xMin = xMax - UIConstants.ButtonWidth;
cuiElements.Add(new CuiButton {
Text = {
Text = text, Color = textColor, Align = TextAnchor.MiddleCenter, FontSize = 18,
}
, Button = {
Color = color, FadeIn = 0.1f, Command = $"customvendingsetup.ui {vendingMachineId} {subCommand}",
}
, RectTransform = {
AnchorMin = "0 0", AnchorMax = "0 0", OffsetMin = $"{xMin} 0", OffsetMax = $"{xMax} {UIConstants.ButtonHeight}",
}
,
}
, UIName );
}
}
private static class ShopUIRenderer {
public const string UIName = "CustomVendingSetup.ShopUI";
private const float OffsetXItem = 210;
private const float OffsetXCurrency = 352;
private const float OverlaySize = 60;
private const float IconSize = 50;
private const float PaddingLeft = 5.5f;
private const float PaddingBottom = 8;
public static string RenderShopUI(VendingProfile vendingProfile) {
va
// ... [Code preview truncated due to length. Full source in attached file] ...