C#:
/*
* Copyright (c) 2026
* Author Bazz3l
* Discord bazz3l
*
* Fused Recycler cannot be copied, edited and/or (re)distributed without the express permission of the Author.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
using System.Collections.Generic;
using Oxide.Core.Plugins;
using Newtonsoft.Json;
using UnityEngine;
using HarmonyLib;
using System;
namespace Oxide.Plugins;
[Info("Fused Recycler", "Bazz3l", "1.0.8")]
[Description("Recyclers will now require a fuse to recycle items.")]
internal class FusedRecycler : RustPlugin
{
#region Fields
private const string FUSEBOX_PREFAB = "assets/prefabs/io/electric/switches/fusebox/fusebox.prefab";
private const string DENIED_PREFAB = "assets/prefabs/locks/keypad/effects/lock.code.denied.prefab";
private const float INITIALIZE_DELAY_INTERVAL = 30f;
private const uint FUSEBOX_PREFAB_ID = 3622071578;
private readonly Quaternion _localRotation = new(0f, -0.7071068f, 0, 0.7071068f);
private readonly Vector3 _localPosition = new(-0.93f, -0.6f, 0.25f);
private static ConfigData _configData;
private static readonly object ObjTrue = true;
private static readonly object ObjFalse = false;
#endregion
#region Lang
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
{ LangKeys.NO_FUSE, "Please insert a fuse to power the Recycler." }
}, this);
}
private static class LangKeys
{
public const string NO_FUSE = "NoFuse";
}
private void ShowPlayerToast(BasePlayer player, string key, params object[] args)
{
if (player?.net == null) return;
player.ShowToast(0, string.Format(lang.GetMessage(key, this, player.UserIDString), args));
}
#endregion
#region Config
protected override void LoadDefaultConfig() => _configData = ConfigData.DefaultConfig();
protected override void LoadConfig()
{
base.LoadConfig();
try
{
_configData = Config.ReadObject<ConfigData>() ?? throw new JsonException();
_configData.FuseUseAmount = Math.Clamp(_configData.FuseUseAmount, 1f, float.MaxValue);
}
catch(Exception e)
{
LoadDefaultConfig();
PrintWarning("Loaded default config: {0}", e.Message);
}
}
protected override void SaveConfig() => Config.WriteObject(_configData, true);
private class ConfigData
{
[JsonProperty("Enable player placed only recyclers")]
public bool EnablePlayerPlacedOnly;
[JsonProperty("Fuse durability per recycle")]
public float FuseUseAmount;
public string ToJson() => JsonConvert.SerializeObject(this);
public Dictionary<string, object> ToDictionary() => JsonConvert.DeserializeObject<Dictionary<string, object>>(ToJson());
public static ConfigData DefaultConfig()
{
return new ConfigData
{
EnablePlayerPlacedOnly = false,
FuseUseAmount = 1f
};
}
}
#endregion
#region Oxide Hooks
private void OnServerInitialized()
{
PrintWarning("please wait for all recyclers to initialize in {0} seconds.", INITIALIZE_DELAY_INTERVAL);
timer.In(INITIALIZE_DELAY_INTERVAL, () =>
{
ModifyRecyclers();
Subscribe(nameof(OnEntitySpawned));
PrintWarning("all recyclers are now initialized.");
});
}
private void Init() => Unsubscribe(nameof(OnEntitySpawned));
private void Unload() => RevertRecyclers();
private void OnEntitySpawned(Recycler recycler) => TryCreateElectricalRecycler(recycler);
private object? OnRecyclerToggle(Recycler recycler, BasePlayer player)
{
if (recycler.IsOn())
return null;
ItemBasedFlowRestrictor? flowRestrictor = GetChildEntity<ItemBasedFlowRestrictor>(recycler);
if (flowRestrictor == null)
return null;
if (flowRestrictor.HasPassthroughItem())
return null;
ShowPlayerToast(player, LangKeys.NO_FUSE);
Effect.server.Run(DENIED_PREFAB, recycler, posLocal: Vector3.zero, normLocal: Vector3.zero);
return ObjFalse;
}
private object? CanMoveItem(Item item, PlayerInventory playerLoot, ItemContainerId targetContainer, int targetSlot, int amount)
{
ItemContainer? container = item.GetRootContainer();
if (container?.entityOwner is not ItemBasedFlowRestrictor { prefabID: FUSEBOX_PREFAB_ID } flowRestrictor)
return null;
return flowRestrictor.GetParentEntity() is Recycler recycler && recycler.IsOn() ? ObjTrue : null;
}
private void OnItemRemovedFromContainer(ItemContainer container, Item item)
{
if (container?.entityOwner is not ItemBasedFlowRestrictor { prefabID: FUSEBOX_PREFAB_ID } flowRestrictor)
return;
if (flowRestrictor.GetParentEntity() is Recycler recycler && recycler.IsOn())
recycler.StopRecycling();
}
#endregion
#region Fused Recycler
private void ModifyRecyclers()
{
foreach (BaseNetworkable nEntity in BaseNetworkable.serverEntities)
{
if (nEntity is Recycler { IsDestroyed: false } recycler)
TryCreateElectricalRecycler(recycler);
}
}
private void RevertRecyclers()
{
if (Rust.Application.isQuitting)
return;
foreach (BaseNetworkable nEntity in BaseNetworkable.serverEntities)
{
if (nEntity is Recycler { IsDestroyed: false } recycler)
TryRemoveElectricalRecycler(recycler);
}
}
private void AttachFuseBox(Recycler recycler)
{
ItemBasedFlowRestrictor? fuseBox = GameManager.server.CreateEntity(FUSEBOX_PREFAB, recycler.ServerPosition, recycler.ServerRotation) as ItemBasedFlowRestrictor;
if (fuseBox == null)
return;
fuseBox.transform.localPosition = _localPosition;
fuseBox.transform.localRotation = _localRotation;
fuseBox.SetParent(recycler, 0u, false, true);
fuseBox.Spawn();
}
private void TryCreateElectricalRecycler(Recycler recycler)
{
if (_configData.EnablePlayerPlacedOnly && recycler.OwnerID == 0UL)
return;
if (HasChildEntity<ItemBasedFlowRestrictor>(recycler))
return;
AttachFuseBox(recycler);
}
private void TryRemoveElectricalRecycler(Recycler recycler)
{
ItemBasedFlowRestrictor? fuseBox = GetChildEntity<ItemBasedFlowRestrictor>(recycler);
if (fuseBox == null)
return;
DropUtil.DropItems(fuseBox.inventory, recycler.GetDropPosition());
fuseBox.SetParent(null);
fuseBox.KillMessage();
}
private static bool HasChildEntity<T>(Recycler recycler) where T : BaseEntity
{
if (recycler?.children == null)
return false;
foreach (BaseEntity childEntity in recycler.children)
{
if (childEntity is T { prefabID: FUSEBOX_PREFAB_ID })
return true;
}
return false;
}
private static T? GetChildEntity<T>(Recycler recycler) where T : BaseEntity
{
if (recycler?.children == null)
return null;
foreach (BaseEntity childEntity in recycler.children)
{
if (childEntity is T { prefabID: FUSEBOX_PREFAB_ID } entity)
return entity;
}
return null;
}
#endregion
#region Harmony Patches
[AutoPatch]
[HarmonyPatch(typeof(Recycler), nameof(Recycler.RecycleThink))]
public static class Recycler_FuseBox_Consumption_Patch
{
[HarmonyPostfix]
public static void Postfix(Recycler __instance)
{
ItemBasedFlowRestrictor? flowRestrictor = GetChildEntity<ItemBasedFlowRestrictor>(__instance);
if (flowRestrictor == null)
return;
Item slot = flowRestrictor.inventory.GetSlot(0);
if (slot is not { hasCondition: true })
return;
slot.LoseCondition(_configData.FuseUseAmount);
}
}
#endregion
}