Skip to content
RustPluginGenerator

How to make a Rust furnace splitter plugin (Oxide/Carbon)

Automatically spreads ore evenly across the free input slots when a player moves items into a furnace. Uses CanMoveItem to pre-fill the other slots and lets the vanilla move continue.

Loading one stack of ore into a furnace fills a single slot and leaves the rest empty, which slows smelting and makes throughput uneven. A furnace splitter watches item moves into furnace input containers and, instead of stacking everything into one slot, distributes the moved amount across the free slots so the furnace burns from multiple slots at once.

This plugin hooks CanMoveItem, which Oxide calls when a player moves an item between containers. It only acts when the destination is a base oven's input container, the shortname is on the whitelist, and the moving player has the feature enabled. The plugin creates matching stacks with ItemManager.CreateByName, moves them into the free slots and reduces the originally moved stack so the total stays the same. Each player toggles the behavior with /fs; the choice is persisted to oxide/data so it survives restarts.

The plugin code

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

namespace Oxide.Plugins
{
    [Info("Furnace Splitter", "RustPluginGenerator", "1.0.0")]
    [Description("Splits ore evenly across the free input slots of a furnace.")]
    public class FurnaceSplitter : RustPlugin
    {
        private const string PermUse = "furnacesplitter.use";
        private const string DataFile = "FurnaceSplitter";

        private PluginConfig config;
        private Dictionary<ulong, bool> prefs;

        private class PluginConfig
        {
            [JsonProperty("Max Stack Size")]
            public int MaxStackSize = 100;

            [JsonProperty("Splittable Shortnames")]
            public string[] SplittableShortnames =
            {
                "hq.metal.ore",
                "metal.ore",
                "sulfur.ore",
                "stones"
            };
        }

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

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                config = Config.ReadObject<PluginConfig>();
                if (config == null) throw new Exception();
                if (config.SplittableShortnames == null) config.SplittableShortnames = new string[0];
                if (config.MaxStackSize < 1) config.MaxStackSize = 1;
            }
            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>
            {
                ["Enabled"] = "Furnace splitting is now <color=#7fbf5f>enabled</color>.",
                ["Disabled"] = "Furnace splitting is now <color=#bf5f5f>disabled</color>.",
                ["NoPermission"] = "You do not have permission to use this command."
            }, this);
        }

        private void Init()
        {
            permission.RegisterPermission(PermUse, this);
            prefs = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<ulong, bool>>(DataFile)
                    ?? new Dictionary<ulong, bool>();
        }

        private void Unload()
        {
            Interface.Oxide.DataFileSystem.WriteObject(DataFile, prefs);
        }

        [ChatCommand("fs")]
        private void CmdToggle(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermUse))
            {
                SendReply(player, Msg("NoPermission", player.UserIDString));
                return;
            }

            ulong id = player.userID.Get();
            bool current;
            prefs.TryGetValue(id, out current);
            bool next = !current;
            prefs[id] = next;
            Interface.Oxide.DataFileSystem.WriteObject(DataFile, prefs);
            SendReply(player, Msg(next ? "Enabled" : "Disabled", player.UserIDString));
        }

        private object CanMoveItem(Item item, PlayerInventory playerInventory, ItemContainerId itemContainerId, int num, int num2, ItemMoveModifier itemMoveModifier)
        {
            if (item == null || item.info == null || playerInventory == null) return null;

            BasePlayer player = playerInventory.baseEntity;
            if (player == null || player.IsNpc) return null;
            if (!IsSplittable(item.info.shortname)) return null;
            if (!IsEnabled(player.userID.Get())) return null;

            ItemContainer target = playerInventory.FindContainer(itemContainerId);
            if (target == null) return null;

            BaseOven oven = target.entityOwner as BaseOven;
            if (oven == null || oven.inventory != target) return null;

            Split(item, target);
            return null;
        }

        private void Split(Item source, ItemContainer target)
        {
            List<int> freeSlots = new List<int>();
            for (int i = 0; i < target.capacity; i++)
            {
                if (target.GetSlot(i) == null) freeSlots.Add(i);
            }

            if (freeSlots.Count == 0) return;

            int perSlot = source.amount / (freeSlots.Count + 1);
            if (perSlot < 1) return;
            if (perSlot > config.MaxStackSize) perSlot = config.MaxStackSize;

            foreach (int slot in freeSlots)
            {
                if (source.amount <= perSlot) break;

                Item created = ItemManager.CreateByName(source.info.shortname, perSlot, source.skin);
                if (created == null) continue;
                created.condition = source.condition;
                created.maxCondition = source.maxCondition;

                if (!created.MoveToContainer(target, slot))
                {
                    created.Remove();
                    continue;
                }

                source.amount -= perSlot;
            }

            source.MarkDirty();
        }

        private bool IsSplittable(string shortname)
        {
            if (string.IsNullOrEmpty(shortname)) return false;
            foreach (string entry in config.SplittableShortnames)
            {
                if (string.Equals(entry, shortname, StringComparison.OrdinalIgnoreCase)) return true;
            }
            return false;
        }

        private bool IsEnabled(ulong userId)
        {
            bool state;
            if (!prefs.TryGetValue(userId, out state)) return true;
            return state;
        }

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

Hooks used: CanMoveItem

Config file

Created automatically on first load at oxide/config/FurnaceSplitter.json.

FurnaceSplitter.json
{
  "Max Stack Size": 100,
  "Splittable Shortnames": [
    "hq.metal.ore",
    "metal.ore",
    "sulfur.ore",
    "stones"
  ]
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/fsToggles automatic furnace splitting on or off for the calling player and reports the new state in chat.Everyone
Permissions
PermissionGrants
furnacesplitter.useGrants access to the /fs toggle. Players without it cannot change their setting.

How the code works

  1. Listening for item moves

    The plugin implements object CanMoveItem(Item item, PlayerInventory playerInventory, ItemContainerId itemContainerId, int num, int num2, ItemMoveModifier itemMoveModifier). Oxide calls it whenever a move into a container is attempted; returning null lets vanilla behavior proceed unchanged. The method null-checks item, item.info and playerInventory, then reads playerInventory.baseEntity as the BasePlayer owner and bails out when it is null or IsNpc returns true.

  2. Whitelist and player toggle

    IsSplittable compares the moved item's info.shortname against the Splittable Shortnames config array with StringComparison.OrdinalIgnoreCase. IsEnabled reads the caller's userID.Get() from the prefs dictionary and returns true when the player has no stored entry, so the feature is on by default. When either check fails the hook returns null and the normal move happens.

  3. Recognising a furnace input

    The target container is resolved with playerInventory.FindContainer(itemContainerId). The plugin then casts target.entityOwner with as BaseOven and requires oven.inventory == target, so only the oven's own input container is touched and unrelated entity containers are ignored. If no oven is found the move goes through untouched.

  4. Spreading the amount

    Split scans slots from 0 to target.capacity - 1 and records every index where target.GetSlot(i) is null. With the free slot count known it divides source.amount by freeSlots.Count + 1, clamps the result between 1 and Max Stack Size, then for each free slot creates an item with ItemManager.CreateByName, copies condition and maxCondition, and calls MoveToContainer(target, slot). Each successful move subtracts from source.amount; the source is marked dirty afterwards.

  5. Config values

    LoadConfig reads a PluginConfig with [JsonProperty] fields Max Stack Size (default 100) and Splittable Shortnames (default hq.metal.ore, metal.ore, sulfur.ore, stones). Invalid JSON falls back to LoadDefaultConfig with a warning. Max Stack Size caps how many units go into each new stack; the shortname array controls which items are handled, so wood and charcoal keep their normal behavior.

  6. Per-player toggle and persistence

    The [ChatCommand("fs")] handler checks permission.UserHasPermission against furnacesplitter.use, flips the player's entry in the prefs dictionary and writes it with Interface.Oxide.DataFileSystem.WriteObject. Init loads the dictionary from oxide/data/FurnaceSplitter.json, creating an empty one on first run, and Unload writes it back. Both chat replies come from lang keys Enabled and Disabled.

Install it on your server

  1. Save FurnaceSplitter.cs

    Keep the file name exactly as the class name. Oxide matches them.
  2. Upload it to the plugins folder

    Oxide: oxide/plugins/. Carbon: carbon/plugins/. The server compiles and loads it within a few seconds, no restart needed.
  3. Edit the config if you want

    On first load the default config is written to oxide/config/FurnaceSplitter.json (Carbon: carbon/configs/FurnaceSplitter.json). After editing, reload it:oxide.reload FurnaceSplitter c.reload FurnaceSplitter # Carbon
  4. Grant the permission

    Give it to every player, or swap default for a group such as vip:oxide.grant group default furnacesplitter.use c.grant group default furnacesplitter.use # Carbon

Variations

Each one opens the generator with a ready prompt you can tweak.

  • Skip running furnaces

    Only split ore when the furnace is off so mid-burn top-ups keep their stacked slot.

    Generate this version
  • Split for fuel too

    Extend the whitelist to wood and low grade fuel so fuel burns more evenly.

    Generate this version
  • Team-wide toggle

    Apply the player's preference to every teammate using the same setting.

    Generate this version

FAQ

Does it work with large furnaces and small furnaces?

Yes. The plugin matches the destination container's owner as BaseOven, which covers the small furnace, the large furnace and the stone fireplace since they all use the same oven component and inventory.

Why does the ore still end up in one stack sometimes?

If the destination has only one free slot, the shortname is not on the whitelist, or the player toggled the feature off, the code does nothing and the move is handled normally. Check Splittable Shortnames in oxide/config/FurnaceSplitter.json and toggle with /fs.

How does it install on Oxide or Carbon?

Drop FurnaceSplitter.cs into oxide/plugins and let the server reload. Carbon loads the same file from its plugins folder. The permission furnacesplitter.use is registered in Init, so grant it with oxide.grant group default furnacesplitter.use if you restrict access.

Does the tooltip of the moved stack stay accurate?

Yes. Item.MarkDirty() is called after the source stack's amount changes so the client receives an updated item and the tooltip matches the reduced count.

Will this conflict with other inventory plugins?

Only if another plugin also edits the target container inside CanMoveItem. Free slots are recalculated on every call, so the splitter stays correct across multiple sequential moves even when another plugin adds items in the same pass.

Customize it for your server

Change the reward, add a cooldown, restrict it to VIPs. Start from this prompt and edit anything.

Prompt: Make a Rust furnace splitter plugin that hooks CanMoveItem, divides ore into free slots of a BaseOven, has a /fs toggle using the furnacesplitter.use permission, whitelists hq.metal.ore, metal.ore, sulfur.ore and stones, and persists player prefs.
Customize for my server

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

Last updated Oct 26, 2026.

Customize furnace splitter

Customize