Skip to content
RustPluginGenerator

OnVendingShopOpened

OnVendingShopOpened is called after a player's customer view of a vending machine has been opened, not before. Use OnVendingShopOpen if you need to approve or block the opening instead.

Signature
void OnVendingShopOpened(VendingMachine vendingMachine, BasePlayer player)

Parameters

Hook parameters
ParameterTypeDescription
vendingMachineVendingMachineThe vending machine whose customer UI was just opened. Declared as VendingMachine here; the same hook also fires with InvisibleVendingMachine and NPCVendingMachine instances from other call sites, so declare the base type if you want one handler for all of them.
playerBasePlayerThe BasePlayer who opened the shop screen. May be null in theory if the RPC arrived for a disconnected player, so check before using it.

Return value

This hook does not use a return value. The patched call sites ignore whatever your method returns and the original code continues unchanged.

How it works

This hook fires at the end of the customer-open path, after the loot/shop UI is already shown to the player. There are three call sites in the server assembly:

  • VendingMachine.RPC_OpenShop after the OccupiedCheck passes and OpenShop runs, for normal player-placed vending machines.
  • NPCTalking.OnConversationAction after PlayerOpenLoot is called on an InvisibleVendingMachine (NPC vending machines reachable through NPC conversation).
  • TravellingVendor.SV_OpenMenu after NPCVendingMachine.OpenShop for the travelling vendor.

Because the same hook name is invoked with three different machine types, the parameter type you declare controls which call sites reach your method. Declaring VendingMachine catches only the first. Declaring InvisibleVendingMachine or NPCVendingMachine catches the NPC variants. To handle all three, use a base type that all of them derive from, such as BaseEntity, or write separate overloads. The hook is notification-only: the shop is already open when it fires, so it is the right place for analytics, UI overlays, or per-player cooldowns keyed on the machine.

Other signatures

Oxide also calls OnVendingShopOpened from other places in the game with these parameter types. Declare the overload you need, or several:

  • void OnVendingShopOpened(InvisibleVendingMachine vendingMachine, BasePlayer player)
  • void OnVendingShopOpened(NPCVendingMachine vendingMachine, BasePlayer player)

Example plugin: Show a chat summary and log when a shop opens

VendingShopAudit.cs
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Vending Shop Audit", "RustPluginGenerator", "1.0.0")]
    [Description("Logs and summarises vending machine shop opens per machine and player.")]
    public class VendingShopAudit : RustPlugin
    {
        private const string PermUse = "vendingshopaudit.use";

        private class PluginConfig
        {
            [JsonProperty("Notify player in chat")]
            public bool NotifyPlayer { get; set; } = true;

            [JsonProperty("Seconds between chat notifications per player")]
            public float CooldownSeconds { get; set; } = 20f;
        }

        private class AuditData
        {
            public Dictionary<string, int> OpensPerMachine { get; set; } = new Dictionary<string, int>();
            public Dictionary<string, int> OpensPerPlayer { get; set; } = new Dictionary<string, int>();
        }

        private PluginConfig config;
        private AuditData data;
        private readonly Dictionary<ulong, float> lastNotify = new Dictionary<ulong, float>();
        private Timer saveTimer;

        protected override void LoadDefaultConfig() => config = new PluginConfig();

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                config = Config.ReadObject<PluginConfig>();
                if (config == null) throw new Exception();
            }
            catch
            {
                PrintWarning("Configuration file is invalid, using defaults");
                LoadDefaultConfig();
            }
            SaveConfig();
        }

        protected override void SaveConfig() => Config.WriteObject(config);

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                ["Opened"] = "Shop '{0}' at {1} has {2} sell offers.",
                ["Summary"] = "Tracked {0} shop opens across {1} machines.",
                ["NoPermission"] = "You do not have permission to use this command."
            }, this);
        }

        private string Msg(string key, string userId = null, params object[] args) =>
            string.Format(lang.GetMessage(key, this, userId), args);

        private void Init()
        {
            permission.RegisterPermission(PermUse, this);
        }

        private void OnServerInitialized()
        {
            data = Interface.Oxide.DataFileSystem.ReadObject<AuditData>(Name) ?? new AuditData();
            saveTimer = timer.Every(60f, SaveData);
        }

        private void Unload()
        {
            saveTimer?.Destroy();
            SaveData();
        }

        private void SaveData()
        {
            if (data != null)
            {
                Interface.Oxide.DataFileSystem.WriteObject(Name, data);
            }
        }

        private void OnVendingShopOpened(VendingMachine vendingMachine, BasePlayer player)
        {
            if (vendingMachine == null || player == null || !player.IsConnected) return;

            string machineId = vendingMachine.net.ID.Value.ToString();
            data.OpensPerMachine.TryGetValue(machineId, out int machineCount);
            data.OpensPerMachine[machineId] = machineCount + 1;

            string playerId = player.UserIDString;
            data.OpensPerPlayer.TryGetValue(playerId, out int playerCount);
            data.OpensPerPlayer[playerId] = playerCount + 1;

            if (!config.NotifyPlayer || !permission.UserHasPermission(playerId, PermUse)) return;

            float now = Time.realtimeSinceStartup;
            if (lastNotify.TryGetValue(player.userID, out float last) && now - last < config.CooldownSeconds) return;
            lastNotify[player.userID] = now;

            int sellOrders = vendingMachine.sellOrders?.sellOrders?.Count ?? 0;
            Vector3 pos = vendingMachine.transform.position;
            player.ChatMessage(Msg("Opened", playerId, vendingMachine.shopName, $"{pos.x:0},{pos.z:0}", sellOrders));
        }

        [ChatCommand("shopaudit")]
        private void CmdShopAudit(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!player.IsAdmin && !permission.UserHasPermission(player.UserIDString, PermUse))
            {
                player.ChatMessage(Msg("NoPermission", player.UserIDString));
                return;
            }

            int total = 0;
            foreach (var pair in data.OpensPerMachine) total += pair.Value;
            player.ChatMessage(Msg("Summary", player.UserIDString, total, data.OpensPerMachine.Count));
        }
    }
}

The plugin listens to OnVendingShopOpened and, when it fires, confirms the machine and player are valid. It reads the machine's display name via shopName and its position, then sends the player a short chat line with the machine location and the number of sell orders currently configured. A small per-player cooldown stops chat spam if the player toggles the shop repeatedly. The audit lines are buffered and written to oxide/data/VendingShopAudit on a one-minute timer and again in Unload, avoiding disk writes on every open. Permission vendingshopaudit.use gates the chat reply so admins can test without exposing it to all players.

Common use cases

  • Track shop traffic per machine

    Increment a counter keyed by the machine's net.ID.Value each time it fires, then rank machines by opens in an admin command. Because it fires after the UI is shown, the count reflects successful opens only.

  • Attach a per-player cooldown

    Record the timestamp per player to stop players from spamming the customer panel and repeatedly triggering other hooks like OnVendingShopOpen or CanPurchaseItem checks.

  • Open a follow-up CUI panel

    Push your own overlay panel for the player on top of the vending screen, e.g. to show stock of similar machines in range from your cached data.

Pitfalls

  • Hook fires after the shop is open

    If you want to prevent the shop from opening or alter its stock before it shows, use OnVendingShopOpen instead. OnVendingShopOpened cannot block or modify the interaction.

  • Parameter type filters the call sites

    Declaring VendingMachine only receives player-placed machines. NPC machines reach the hook as InvisibleVendingMachine or NPCVendingMachine, and Oxide drops calls whose argument is not an instance of your declared type.

  • Player and machine may be null

    RPC_OpenShop already calls OccupiedCheck first, but hook parameters can still arrive null during a disconnect race. Guard both before touching player.UserIDString or vendingMachine.shopName.

Build a plugin with OnVendingShopOpened

Describe what should happen and get a compiled plugin that uses this hook.

Prompt: Track shop traffic per machine using the OnVendingShopOpened hook, with a config file and a permission
Generate this plugin

Edit the prompt on the next screen. 2 free plugins a month, no card.

Last updated Oct 26, 2026. Found a mistake? Email support@rustplugingenerator.com.

Build a plugin with OnVendingShopOpened

Generate