Skip to content
RustPluginGenerator

OnSprayCreate

OnSprayCreate is called inside SprayCan.CreateSpray before the server spawns the spray decal entity, after the player's spray item has already been validated.

Signature
object OnSprayCreate(SprayCan sprayCan, UnityEngine.Vector3 vector, UnityEngine.Quaternion quaternion)

Parameters

Hook parameters
ParameterTypeDescription
sprayCanSprayCanThe SprayCan entity instance that would create the decal; call GetOwnerPlayer() to reach the player, since the hook does not pass them directly.
vectorUnityEngine.Vector3World-space position where the spray decal entity would be spawned, taken from the player's aim raycast.
quaternionUnityEngine.QuaternionWorld rotation the decal entity would be spawned with, aligned to the surface that was hit.

Return value

Returning null (or nothing) lets the original code continue and the spray decal is spawned normally. Returning any non-null object makes CreateSpray return immediately, so no decal entity is created; the value itself is discarded.

How it works

The hook fires from SprayCan.CreateSpray, which runs when a player uses a spray can and the server has already confirmed the player owns the matching spray item. It runs before ItemDefinition.FindSkin and before the SprayDecalEntityRef entity is created with GameManager.server.CreateEntity, so returning non-null prevents the decal from existing at all.

sprayCan is the held SprayCan entity; vector and quaternion are the spawn transform of the decal, which is the surface point the player aimed at. Declaring the first parameter as SprayCan is the only variant that matters here - the injection passes this, so a broader type such as BaseEntity also matches, but you lose nothing by keeping it concrete.

The hook is not per-frame but it does fire on every spray attempt, so keep the body cheap. Since it fires before the decal is created, this is the place to enforce rules that depend on location (building privilege, monuments, custom zones) or to count sprays for a cooldown. If you need the player, call sprayCan.GetOwnerPlayer() and null-check it; the hook does not hand you the player directly.

Example plugin: Block sprays inside building privilege and in custom no-spray zones

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

namespace Oxide.Plugins
{
    [Info("Spray Zone Guard", "RustPluginGenerator", "1.0.0")]
    [Description("Blocks spray decals inside building privilege and configurable sphere zones.")]
    public class SprayZoneGuard : RustPlugin
    {
        private const string PermAdminReload = "sprayzoneguard.admin";

        private PluginConfig config;

        private class Zone
        {
            [JsonProperty("Name")]
            public string Name = "monument";

            [JsonProperty("Center (x y z)")]
            public string Center = "0 0 0";

            [JsonProperty("Radius")]
            public float Radius = 50f;
        }

        private class PluginConfig
        {
            [JsonProperty("Block sprays inside building privilege")]
            public bool BlockInPrivilege = true;

            [JsonProperty("Block sprays inside zones")]
            public bool BlockInZones = true;

            [JsonProperty("Zones")]
            public List<Zone> Zones = new List<Zone>
            {
                new Zone { Name = "spawn", Center = "0 0 0", Radius = 80f }
            };
        }

        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>
            {
                ["DeniedPrivilege"] = "You cannot spray inside a building privilege area.",
                ["DeniedZone"] = "You cannot spray here ({0})."
            }, 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(PermAdminReload, this);
        }

        private object OnSprayCreate(SprayCan sprayCan, Vector3 vector, Quaternion quaternion)
        {
            if (sprayCan == null) return null;
            var player = sprayCan.GetOwnerPlayer();
            if (player == null || player.IsNpc) return null;

            if (config.BlockInPrivilege && player.GetBuildingPrivilege() != null)
            {
                player.ChatMessage(Msg("DeniedPrivilege", player.UserIDString));
                return true;
            }

            if (config.BlockInZones)
            {
                foreach (var zone in config.Zones)
                {
                    if (zone == null) continue;
                    var center = ParseVector(zone.Center);
                    if (Vector3.Distance(center, vector) <= zone.Radius)
                    {
                        player.ChatMessage(Msg("DeniedZone", player.UserIDString, zone.Name));
                        return true;
                    }
                }
            }

            return null;
        }

        private static Vector3 ParseVector(string text)
        {
            if (string.IsNullOrEmpty(text)) return Vector3.zero;
            var parts = text.Split(' ');
            if (parts.Length < 3) return Vector3.zero;
            float x, y, z;
            if (!float.TryParse(parts[0], out x)) x = 0f;
            if (!float.TryParse(parts[1], out y)) y = 0f;
            if (!float.TryParse(parts[2], out z)) z = 0f;
            return new Vector3(x, y, z);
        }
    }
}

The plugin stops sprays in places admins usually want clean: inside any building privilege radius and inside configurable sphere zones (monuments, spawn areas). OnSprayCreate looks up the owning player from the SprayCan entity, then checks the position passed to the hook with GetBuildingPrivilege() on the player and a distance check against each zone in the config. Denied sprays return a non-null object so CreateSpray aborts before the decal entity is spawned, and the player gets a localized chat message. Zone centres are stored as strings in the config and parsed with ParseVector on each check, with a radius per zone.

Common use cases

  • Keep building interiors clean

    Reject sprays whose world position falls inside a tool cupboard radius so raiders and teammates cannot decorate or grief another group's base.

  • Monument and spawn protection

    Define sphere zones around monuments or spawn points and cancel any spray inside them, since the hook gives you the exact spawn position of the decal.

  • Spray accounting and cooldowns

    Count allowed sprays per player using the spawn position for logging, and cancel attempts that come faster than a configured interval.

Pitfalls

  • The spray item is not consumed automatically

    Cancelling here prevents the decal entity only. The server already reached the point where the player's item was found and changed skin, so test whether a blocked spray still uses up the player's spray charge on your build before promising refunds.

  • No player argument

    The hook passes the SprayCan entity, so call GetOwnerPlayer() and null-check it; a spray can can exist without a connected owner in edge cases.

  • World position, not player position

    Privilege and zone checks must use the vector argument, not the player's transform, otherwise sprays at the far edge of a base or across a zone border pass through.

Build a plugin with OnSprayCreate

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

Prompt: Keep building interiors clean using the OnSprayCreate 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 OnSprayCreate

Generate