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.
void OnItemPainted(PaintedItemStorageEntity paintedItemStorageEntity, Item item, BasePlayer player, byte[] array)Parameters
| Parameter | Type | Description |
|---|---|---|
paintedItemStorageEntity | PaintedItemStorageEntity | The PaintedItemStorageEntity that holds the sign canvas being edited. Its net.ID identifies the sign if you need to look it up later. |
item | Item | The sign item (Item) currently being painted. You can read info.shortname, skin, condition and GetOwnerPlayer() from it. |
player | BasePlayer | The BasePlayer who submitted the paint update. Only connected players send edits, but null-check it anyway. |
array | byte[] | 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
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
arrayis 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.
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