OnFireworkDesignChanged
OnFireworkDesignChanged is called after a player submits a firework pattern through PatternFirework.ServerSetFireworkDesign, once the new design has been assigned to the entity but before the network update is sent.
void OnFireworkDesignChanged(PatternFirework patternFirework, ProtoBuf.PatternFirework.Design design, BasePlayer player)Parameters
| Parameter | Type | Description |
|---|---|---|
patternFirework | PatternFirework | The PatternFirework entity that just received the new design. Use it for its net.ID, OwnerID, prefab name and position, or to trigger network updates. |
design | ProtoBuf.PatternFirework.Design | The ProtoBuf.PatternFirework.Design that was applied. Its stars list holds the star placements, and editedBy holds the Steam ID of the player whose edit created the design. |
player | BasePlayer | The BasePlayer whose client sent the ServerSetFireworkDesign RPC, i.e. the player who submitted the design. Compare with patternFirework.OwnerID to detect edits by non-owners. |
Return value
The return value is ignored. Returning a value does nothing, and no value can block or alter the design being applied, since the game has already assigned the new design and only sends the network update after the hook returns.
How it works
The hook fires inside PatternFirework.ServerSetFireworkDesign. That method is the server side of the client RPC a player sends when they finish editing a pattern firework: the server copies rpc.player.userID into design.editedBy, logs the UGC creation to Azure analytics, disposes the old design, assigns the new one to Design, and only then calls the hook. Immediately afterwards the entity is pushed to clients with a network update.
Because the hook runs after Design = design, the pattern firework already holds the new design when your code sees it, but players have not yet received the update. All three parameters are populated: the entity, the applied design (with its star list and editedBy), and the submitting player. Return value is ignored.
Declaring a more specific type for player, such as a subclass, would make Oxide skip calls where the argument is not that type, so keep BasePlayer. The same applies to patternFirework: narrow it only if you truly need a derived type. Nothing stops a client from sending the RPC for an entity it does not own, so validate ownership in your own code if it matters.
Example plugin: Log and alert on firework designs edited by non-owners
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Firework Design Guard", "RustPluginGenerator", "1.0.0")]
[Description("Audits pattern firework design submissions and alerts admins about edits by non-owners")]
public class FireworkDesignGuard : RustPlugin
{
private const string PermAdmin = "fireworkdesignguard.admin";
private PluginConfig config;
private StoredData data;
private class PluginConfig
{
[JsonProperty("Alert admins in chat")]
public bool AlertAdmins { get; set; } = true;
[JsonProperty("Max warnings per player per session")]
public int MaxWarningsPerPlayer { get; set; } = 3;
}
private class StoredData
{
[JsonProperty("Submissions per player")]
public Dictionary<string, int> Submissions { get; set; } = new Dictionary<string, int>();
[JsonProperty("Non-owner edit attempts")]
public List<string> NonOwnerEdits { get; set; } = new List<string>();
}
private readonly Dictionary<ulong, int> warningsThisSession = new Dictionary<ulong, 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>
{
["AdminAlert"] = "{0} ({1}) submitted a firework design for an entity they do not own ({2}).",
["AdminInfo"] = "Use /fwguard to list the most recent non-owner edits."
}, 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(PermAdmin, this);
LoadData();
}
private void LoadData()
{
try
{
data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name);
}
catch
{
data = null;
}
if (data == null) data = new StoredData();
}
private void OnFireworkDesignChanged(PatternFirework patternFirework, ProtoBuf.PatternFirework.Design design, BasePlayer player)
{
if (patternFirework == null || design == null || player == null) return;
if (player.IsNpc) return;
string id = player.UserIDString;
if (!data.Submissions.ContainsKey(id)) data.Submissions[id] = 0;
data.Submissions[id]++;
if (patternFirework.OwnerID == player.userID.Get()) return;
string entry = string.Format("{0} {1} net={2} stars={3}", DateTime.UtcNow.ToString("u"), id, patternFirework.net.ID.Value, design.stars?.Count ?? 0);
data.NonOwnerEdits.Add(entry);
if (data.NonOwnerEdits.Count > 200) data.NonOwnerEdits.RemoveAt(0);
if (config.AlertAdmins)
{
int warned;
warningsThisSession.TryGetValue(player.userID.Get(), out warned);
if (warned < config.MaxWarningsPerPlayer)
{
warningsThisSession[player.userID.Get()] = warned + 1;
string message = Msg("AdminAlert", null, player.displayName, id, patternFirework.ShortPrefabName);
foreach (var admin in BasePlayer.activePlayerList)
{
if (admin != null && !admin.IsNpc && permission.UserHasPermission(admin.UserIDString, PermAdmin))
SendReply(admin, message);
}
}
}
Effect.server.Run("assets/bundled/prefabs/fx/weapons/landmine/landmine_explosion.prefab", patternFirework.transform.position);
}
[ChatCommand("fwguard")]
private void CmdGuard(BasePlayer player, string command, string[] args)
{
if (player == null || !permission.UserHasPermission(player.UserIDString, PermAdmin)) return;
if (data.NonOwnerEdits.Count == 0)
{
SendReply(player, "No non-owner firework edits recorded.");
return;
}
int start = Math.Max(0, data.NonOwnerEdits.Count - 10);
for (int i = start; i < data.NonOwnerEdits.Count; i++)
SendReply(player, data.NonOwnerEdits[i]);
}
private void Unload()
{
Interface.Oxide.DataFileSystem.WriteObject(Name, data);
}
}
}The plugin listens to OnFireworkDesignChanged and compares the submitting player's ID with the pattern firework's OwnerID. When they differ, it records the attempt in a persisted data file (keyed by the submitter's Steam ID and entity net ID), warns online admins in chat, and plays a short effect at the firework. Every submission, legitimate or not, increments a per-player counter in the data file so admins can review activity later. The hook itself does no printing to the server console, keeping high-frequency edits quiet. Config holds the admin alert toggle and language strings live in the lang file.
Common use cases
Audit trail for pattern edits
Persist the submitting player, entity net ID and star count of every accepted design to oxide/data so admins can trace who edited which firework.
Block unauthorized design submissions
Detect submissions where the sender is not the firework's owner and react in your own code, for example by reverting the design in a delayed timer or removing the firework.
Reward/event integrations
Call into an economics or event plugin when a player submits a design, for example granting a small currency reward for the first design submitted each session.
Pitfalls
Firing on every save, not just the first
The RPC can be sent repeatedly as a player keeps editing, so treat every call as a fresh submission and rate-limit any chat feedback or rewards.
OwnerID does not prove the sender owns the firework
The hook fires regardless of whether the sender owns the entity; do not assume the player parameter matches patternFirework.OwnerID without checking.
Design can be replaced right after
The game assigns the new design before calling the hook, but a later call can replace it again. If you store the design object, copy the values you need rather than holding the reference.
Build a plugin with OnFireworkDesignChanged
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 OnFireworkDesignChanged
Generate