Skip to content
RustPluginGenerator

OnItemPainted

OnItemPainted is called after a player commits an edit to a sign or other paintable item, once the server has accepted the new canvas data.

Signature
void OnItemPainted(PaintedItemStorageEntity paintedItemStorageEntity, Item item, BasePlayer player, byte[] array)

Parameters

Hook parameters
ParameterTypeDescription
paintedItemStorageEntityPaintedItemStorageEntityThe PaintedItemStorageEntity that holds the sign canvas being edited. Its net.ID identifies the sign if you need to look it up later.
itemItemThe sign item (Item) currently being painted. You can read info.shortname, skin, condition and GetOwnerPlayer() from it.
playerBasePlayerThe BasePlayer who submitted the paint update. Only connected players send edits, but null-check it anyway.
arraybyte[]The raw byte payload of the canvas as received from the client. Treat it as opaque binary; use its length or absence to tell a blank canvas from a drawn one.

Return value

The return value is ignored. Server_UpdateImage returns void and the hook call happens after the canvas has been saved, so nothing you return changes the paint, cancels the edit or alters what the client sees.

How it works

The hook fires inside PaintedItemStorageEntity.Server_UpdateImage, after the server has already validated and stored the incoming canvas and applied any condition loss caused by painting. It runs on every paint commit, not on each brush stroke, so a player dragging a brush and releasing produces one call. By the time you receive it, paintedItemStorageEntity is the live storage entity, item is the sign item, player is the author of the edit, and array is the byte payload just written. Because the hook fires after the write, returning nothing cannot undo the paint; the canvas is already changed. Declaring a narrower parameter type does not help here, since every argument is always the same concrete type. Use this hook to record who painted what, to meter painting by permission or cooldown, or to snapshot canvas sizes for auditing. The call runs on the main thread, so keep work short and avoid spawning entities inside it.

Example plugin: Track sign edits per player with canvas size

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

namespace Oxide.Plugins
{
    [Info("Sign Paint Logger", "RustPluginGenerator", "1.0.0")]
    [Description("Counts and stores sign paint edits per player and reports totals through a chat command")]
    public class SignPaintLogger : RustPlugin
    {
        private const string PermUse = "signpaintlogger.use";
        private const string PermReset = "signpaintlogger.reset";
        private const string DataFile = "SignPaintLogger";

        private PluginConfig config;
        private StoredData data;

        private class PluginConfig
        {
            [JsonProperty("Announce every N paints (0 = off)")]
            public int AnnounceEvery { get; set; } = 10;

            [JsonProperty("Count blank canvases")]
            public bool CountBlank { get; set; } = false;
        }

        private class StoredData
        {
            [JsonProperty("Paint counts by user id")]
            public Dictionary<string, int> Counts { get; set; } = new Dictionary<string, int>();
        }

        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>
            {
                ["Announce"] = "You have painted {0} signs on this server.",
                ["Stats"] = "Your sign paint count: {0}",
                ["Reset"] = "Paint counters cleared.",
                ["NoPerm"] = "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);
            permission.RegisterPermission(PermReset, this);
            LoadData();
        }

        private void LoadData()
        {
            data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(DataFile) ?? new StoredData();
            if (data.Counts == null) data.Counts = new Dictionary<string, int>();
        }

        private void Unload()
        {
            SaveData();
        }

        private void SaveData()
        {
            Interface.Oxide.DataFileSystem.WriteObject(DataFile, data);
        }

        [ChatCommand("signpaint.stats")]
        private void CmdStats(BasePlayer player, string command, string[] args)
        {
            if (player == null || !permission.UserHasPermission(player.UserIDString, PermUse)) return;
            int count;
            data.Counts.TryGetValue(player.UserIDString, out count);
            SendReply(player, Msg("Stats", player.UserIDString, count));
        }

        [ChatCommand("signpaint.reset")]
        private void CmdReset(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!player.IsAdmin && !permission.UserHasPermission(player.UserIDString, PermReset))
            {
                SendReply(player, Msg("NoPerm", player.UserIDString));
                return;
            }
            data.Counts.Clear();
            SaveData();
            SendReply(player, Msg("Reset", player.UserIDString));
        }

        private void OnItemPainted(PaintedItemStorageEntity paintedItemStorageEntity, Item item, BasePlayer player, byte[] array)
        {
            if (item == null || item.info == null || player == null || !player.IsConnected) return;
            if (!config.CountBlank && (array == null || array.Length == 0)) return;

            int count;
            data.Counts.TryGetValue(player.UserIDString, out count);
            count++;
            data.Counts[player.UserIDString] = count;

            if (config.AnnounceEvery > 0 && count % config.AnnounceEvery == 0)
            {
                SendReply(player, Msg("Announce", player.UserIDString, count));
            }
        }
    }
}

The plugin records every accepted sign edit into a per-player counter and prints a short summary to the painter's chat when a configurable threshold is crossed. OnItemPainted reads the item shortname and skin, measures the byte payload length to classify the canvas as blank or drawn, and increments a counter keyed by the player's ID. Counters persist to oxide/data/SignPaintLogger and are written on unload. A signpaint.stats chat command lets anyone with the permission see their own totals, and signpaint.reset (admin-only) clears them. The hook itself only touches dictionaries and one chat message, so it stays cheap enough to run on every edit.

Common use cases

  • Audit who drew on which sign

    Store player ID, item skin and payload size per edit so admins can trace griefing or offensive images back to a specific account and time.

  • Daily paint quota

    Count commits against a per-player limit from config and refuse further painting by removing the sign from the player's hands or warning them through chat.

  • Reward artists

    Grant a small economy payout through a [PluginReference] Economics call when a player paints a non-blank canvas on a sign they own.

Pitfalls

  • Painting is already saved

    The hook runs after the canvas is written, so you cannot reject or roll back the edit from here. To block painting, act earlier by removing the item or gating the player before they open the paint UI.

  • Byte array is not an image

    array is a raw client payload, not a decodable PNG. Use its length and presence only; parsing it requires game internals and is not stable across Rust updates.

  • Frequent calls on busy servers

    Popular signs get repainted often and each commit fires the hook. Keep the body to dictionary updates and avoid Puts, entity spawns or data writes inside it.

Build a plugin with OnItemPainted

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

Prompt: Audit who drew on which sign using the OnItemPainted 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 OnItemPainted

Generate