C#:
// #define ENABLE_TESTS
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core.Libraries.Covalence;
using Oxide.Game.Rust.Cui;
using System;
using System.Collections.Generic;
using System.Linq;
using Oxide.Core;
using Oxide.Core.Libraries;
using UnityEngine;
#if ENABLE_TESTS
using System.Collections;
using System.Reflection;
using Oxide.Core.Plugins;
#endif
namespace Oxide.Plugins {
[Info("Recycle Manager", "WhiteThunder", "2.0.3")]
[Description("Allows customizing recycler speed, input, and output")]
internal class RecycleManager : CovalencePlugin {
#region Fields
private Configuration _config;
private const string PermissionAdmin = "recyclemanager.admin";
private const int ScrapItemId = -932201673;
private const float RecycleEfficiency = 0.5f;
private readonly object True = true;
private readonly object False = false;
private const int NumInputSlots = 6;
private const int NumOutputSlots = 6;
private readonly RecycleComponentManager _recycleComponentManager = new RecycleComponentManager();
private readonly RecycleEditManager _recycleEditManager;
private readonly float[] _recycleTime = new float[1];
private bool IsEditUIEnabled => !_config.UsingDefaults && _config.EditUISettings.Enabled;
#if ENABLE_TESTS
private readonly RecycleManagerTests _testRunner;
#endif
public RecycleManager() {
#if ENABLE_TESTS
_testRunner = new RecycleManagerTests(this);
#endif
_recycleEditManager = new RecycleEditManager(this);
}
#endregion
#region Hooks
private void Init() {
_config.Init(this);
_recycleComponentManager.Init(this);
permission.RegisterPermission(PermissionAdmin, this);
if (!_config.RecycleSpeed.Enabled) {
Unsubscribe(nameof(OnRecyclerToggle));
}
if (!IsEditUIEnabled) {
Unsubscribe(nameof(OnLootEntity));
}
}
private void OnServerInitialized() {
#if ENABLE_TESTS
_testRunner.Run();
#endif
if (IsEditUIEnabled) {
foreach (var player in BasePlayer.activePlayerList) {
var container = player.inventory.loot.containers.FirstOrDefault();
if (container == null) continue;
var recycler = container.entityOwner as Recycler;
if ((object)recycler == null) continue;
OnLootEntity(player, recycler);
}
}
}
private void Unload() {
#if ENABLE_TESTS
_testRunner.Interrupt();
#endif
_recycleComponentManager.Unload();
_recycleEditManager.Unload();
}
// This hook is primarily used to determine whether an item can be placed into the recycler input,
// but it's also called when processing each item.private object CanBeRecycled(Item item, Recycler recycler)
{
if (item == null) return null;
if (_config.RestrictedInputItems.IsDisallowed(item)) {
// Defensively return null if vanilla would *disallow* recycling, to avoid hook conflicts.return IsVanillaRecyclable(item) && !RecyclableWasBlocked(item, recycler)
? False : null;
}
if (_config.OverrideOutput.GetBestOverride(item) != null) {
// Defensively return null if vanilla would *allow* recycling, to avoid hook conflicts.return IsVanillaRecyclable(item) || RecyclableWasBlocked(item, recycler)
? null : True;
}
return null;
}
private object CanRecycle(Recycler recycler, Item item) {
return CanBeRecycled(item, recycler);
}
private void OnRecyclerToggle(Recycler recycler, BasePlayer player) {
_recycleComponentManager.HandleRecyclerToggle(recycler, player);
}
private object OnItemRecycle(Item item, Recycler recycler) {
if (RecycleItemWasBlocked(item, recycler)) return null;
var recycleAmount = DetermineConsumptionAmount(recycler, item);
if (recycleAmount <= 0) return False;
var customIngredientList = _config.OverrideOutput.GetBestOverride(item);
if (customIngredientList != null) {
item.UseItem(recycleAmount);
// Overrides already account for standard recycle efficiency, so only calculate based on item condition.if (PopulateOutputWithOverride(recycler, customIngredientList, recycleAmount, DetermineRecycleEfficiency(item)))
{
recycler.StopRecycling();
}
return False;
}
// If the item is not vanilla recyclable, and this plugin doesn't have an override,
// that probably means another plugin is going to handle recycling it.if (!IsVanillaRecyclable(item))
return null;
item.UseItem(recycleAmount);
var outputIsFull = PopulateOutputVanilla(_config, recycler, item, recycleAmount, DetermineRecycleEfficiency(item, recycler.recycleEfficiency));
if (outputIsFull || !recycler.HasRecyclable()) {
recycler.StopRecycling();
}
return False;
}
private void OnLootEntity(BasePlayer player, Recycler recycler) {
if (!recycler.onlyOneUser) return;
if (permission.UserHasPermission(player.UserIDString, PermissionAdmin)) {
var player2 = player;
var recycler2 = recycler;
NextTick(() => {
var lootingContainer = player2.inventory.loot.containers.FirstOrDefault();
if (lootingContainer == null || lootingContainer != recycler2.inventory) return;
_recycleEditManager.HandlePlayerStartedLooting(player2, recycler2);
}
);
}
}
#endregion
#region Exposed Hooks
private static class ExposedHooks {
public static object OnRecycleManagerItemRecyclable(Item item, Recycler recycler) {
return Interface.CallHook("OnRecycleManagerItemRecyclable", item, recycler);
}
public static object OnRecycleManagerSpeed(Recycler recycler, BasePlayer player, float[] recycleTime) {
return Interface.CallHook("OnRecycleManagerSpeed", recycler, player, recycleTime);
}
public static object OnRecycleManagerRecycle(Item item, Recycler recycler) {
return Interface.CallHook("OnRecycleManagerRecycle", item, recycler);
}
}
#endregion
#region Commands
[Command("recyclemanager.add", "recman.add")]
private void CommandAddItem(IPlayer player, string cmd, string[] args) {
if (!VerifyHasPermission(player, PermissionAdmin) || !VerifyConfigLoaded(player)) return;
ItemDefinition itemDefinition;
if (!VerifyValidItemIdOrShortName(player, args.ElementAtOrDefault(0), out itemDefinition, cmd)) return;
if (!_config.OverrideOutput.AddOverride(this, itemDefinition)) {
ReplyToPlayer(player, LangEntry.AddExists, itemDefinition.shortname);
return;
}
SaveConfig();
ReplyToPlayer(player, LangEntry.AddSuccess, itemDefinition.shortname);
}
[Command("recyclemanager.reset", "recman.reset")]
private void CommandResetItem(IPlayer player, string cmd, string[] args) {
if (!VerifyHasPermission(player, PermissionAdmin) || !VerifyConfigLoaded(player)) return;
ItemDefinition itemDefinition;
if (!VerifyValidItemIdOrShortName(player, args.ElementAtOrDefault(0), out itemDefinition, cmd)) return;
_config.OverrideOutput.ResetOverride(this, itemDefinition);
SaveConfig();
ReplyToPlayer(player, LangEntry.ResetSuccess, itemDefinition.shortname);
}
[Command("recyclemanager.ui")]
private void CommandEdit(IPlayer player, string cmd, string[] args) {
if (player.IsServer || !player.HasPermission(PermissionAdmin)) return;
var basePlayer = player.Object as BasePlayer;
_recycleEditManager.GetController(basePlayer)?.HandleUICommand(basePlayer, args);
}
#endregion
#region Helper Methods - Instance
private bool VerifyHasPermission(IPlayer player, string perm) {
if (player.HasPermission(perm)) return true;
ReplyToPlayer(player, LangEntry.ErrorNoPermission);
return false;
}
private bool VerifyConfigLoaded(IPlayer player) {
if (!_config.UsingDefaults) return true;
ReplyToPlayer(player, LangEntry.ErrorConfig);
return false;
}
private bool VerifyValidItemIdOrShortName(IPlayer player, string itemArg, out ItemDefinition itemDefinition, string command) {
if (itemArg == null) {
itemDefinition = (player.Object as BasePlayer)?.GetActiveItem()?.info;
if (itemDefinition != null) return true;
ReplyToPlayer(player, LangEntry.ItemSyntax, command);
return false;
}
int itemId;
if (int.TryParse(itemArg, out itemId)) {
itemDefinition = ItemManager.FindItemDefinition(itemId);
if (itemDefinition != null) return true;
}
itemDefinition = ItemManager.FindItemDefinition(itemArg);
if (itemDefinition != null) return true;
ReplyToPlayer(player, LangEntry.ErrorInvalidItem, itemArg);
return false;
}
private bool TryDetermineRecycleTime(Recycler recycler, BasePlayer player, out float recycleTime) {
_recycleTime[0] = _config.RecycleSpeed.DefaultRecycleTime * _config.RecycleSpeed.GetTimeMultiplierForPlayer(player);
if (_config.RecycleSpeed.SafeZoneTimeMultiplier != 1 && player.InSafeZone()) {
_recycleTime[0] *= _config.RecycleSpeed.SafeZoneTimeMultiplier;
}
var hookResult = ExposedHooks.OnRecycleManagerSpeed(recycler, player, _recycleTime);
if (hookResult is bool && !(bool)hookResult) {
recycleTime = 0;
return false;
}
recycleTime = Math.Max(0, _recycleTime[0]);
return true;
}
private object CallCanBeRecycled(Item item, Recycler recycler) {
Unsubscribe(nameof(CanBeRecycled));
var hookResult = Interface.CallHook(nameof(CanBeRecycled), item, recycler);
Subscribe(nameof(CanBeRecycled));
return hookResult;
}
private IngredientInfo[] GetVanillaOutput(ItemDefinition itemDefinition) {
if (itemDefinition.Blueprint?.ingredients == null) return new IngredientInfo[0];
var ingredientList = new List<IngredientInfo>();
if (itemDefinition.Blueprint.scrapFromRecycle > 0) {
var ingredientInfo = new IngredientInfo {
ShortName = "scrap", Amount = itemDefinition.Blueprint.scrapFromRecycle,
}
;
ingredientInfo.Init();
ingredientList.Add(ingredientInfo);
}
foreach (var blueprintIngredient in itemDefinition.Blueprint.ingredients) {
if (blueprintIngredient.itemid == ScrapItemId) continue;
var amount = blueprintIngredient.amount / itemDefinition.Blueprint.amountToCreate * RecycleEfficiency;
if (amount > 1) {
amount = Mathf.CeilToInt(amount);
}
var ingredientInfo = new IngredientInfo {
ShortName = blueprintIngredient.itemDef.shortname, Amount = amount,
}
;
ingredientInfo.Init();
ingredientList.Add(ingredientInfo);
}
return ingredientList.ToArray();
}
private float DetermineRecycleEfficiency(Item item, float recycleEfficiency = 1) {
return item.hasCondition ? Mathf.Clamp01(recycleEfficiency * Mathf.Clamp(item.conditionNormalized * item.maxConditionNormalized, 0.1f, 1f)) : recycleEfficiency;
}
private int DetermineConsumptionAmount(Recycler recycler, Item item) {
var recycleAmount = 1;
if (item.amount > 1) {
var consumeMultiplier = _config.MaxItemsPerRecycle.GetPercent(item) / 100f;
recycleAmount = Mathf.CeilToInt(Mathf.Min(item.amount, (float)item.info.stackable * consumeMultiplier));
// In case the configured multiplier is 0, ensure at least 1 item is recycled.recycleAmount = Math.Max(recycleAmount, 1);
}
// Call standard Oxide hook for compatibility.var amountOverride = Interface.CallHook("OnItemRecycleAmount", item, recycleAmount, recycler);
if (amountOverride is int) {
return (int)amountOverride;
}
return recycleAmount;
}
private bool PopulateOutputWithOverride(Recycler recycler, IngredientInfo[] customIngredientList, int recycleAmount, float recycleEfficiency = 1, bool forEditor = false) {
var outputIsFull = false;
foreach (var ingredientInfo in customIngredientList) {
if (ingredientInfo.ItemDefinition == null) continue;
var ingredientAmount = ingredientInfo.Amount * recycleEfficiency;
if (ingredientAmount <= 0) continue;
var outputAmount = CalculateOutputAmount(recycleAmount, ingredientAmount, forEditor : forEditor);
if (outputAmount <= 0) continue;
if (AddItemToRecyclerOutput(recycler, ingredientInfo.ItemDefinition, outputAmount, ingredientInfo.SkinId, ingredientInfo.DisplayName)) {
outputIsFull = true;
}
}
return outputIsFull;
}
private static bool PopulateOutputVanilla(Configuration config, Recycler recycler, Item item, int recycleAmount, float recycleEfficiency = RecycleEfficiency, bool forEditor = false) {
var outputIsFull = false;
if (item.info.Blueprint.scrapFromRecycle > 0) {
var scrapOutputMultiplier = config.OutputMultipliers.GetOutputMultiplier(ScrapItemId);
var scrapAmount = Mathf.CeilToInt(item.info.Blueprint.scrapFromRecycle * recycleAmount);
scrapAmount = Mathf.CeilToInt(scrapAmount * scrapOutputMultiplier);
if (!forEditor && item.info.stackable == 1 && item.hasCondition) {
scrapAmount = Mathf.CeilToInt(scrapAmount * item.conditionNormalized);
}
if (scrapAmount >= 1) {
var scrapItem = ItemManager.CreateByItemID(ScrapItemId, scrapAmount);
recycler.MoveItemToOutput(scrapItem);
}
}
foreach (var ingredient in item.info.Blueprint.ingredients) {
// Skip scrap since it's handled separately.if (ingredient.itemDef.itemid == ScrapItemId)
continue;
var ingredientAmount = ingredient.amount * recycleEfficiency / item.info.Blueprint.amountToCreate;
if (ingredientAmount <= 0) continue;
var outputAmount = CalculateOutputAmount(recycleAmount, ingredientAmount, config.OutputMultipliers.GetOutputMultiplier(ingredient.itemid), forEditor );
if (outputAmount <= 0) continue;
if (AddItemToRecyclerOutput(recycler, ingredient.itemDef, outputAmount)) {
outputIsFull = true;
}
}
return outputIsFull;
}
#endregion
#region Helper Methods - Static
public static void LogInfo(string message) => Interface.Oxide.LogInfo($"[Recycle Manager] {message}");
public static void LogError(string message) => Interface.Oxide.LogError($"[Recycle Manager] {message}");
public static void LogWarning(string message) => Interface.Oxide.LogWarning($"[Recycle Manager] {message}");
private static void Swap<T>(ref T a, ref T b) {
T temp = a;
a = b;
b = temp;
}
private static bool IsVanillaRecyclable(Item item) {
return item.info.Blueprint != null;
}
private static bool RecyclableWasBlocked(Item item, Recycler recycler) {
var hookResult = ExposedHooks.OnRecycleManagerItemRecyclable(item, recycler);
return hookResult is bool && (bool)hookResult == false;
}
private static bool RecycleItemWasBlocked(Item item, Recycler recycler) {
var hookResult = ExposedHooks.OnRecycleManagerRecycle(item, recycler);
return hookResult is bool && (bool)hookResult == false;
}
private static Item CreateItem(ItemDefinition itemDefinition, int amount, ulong skinId, string displayName = null) {
var item = ItemManager.Create(itemDefinition, amount, skinId);
if (!string.IsNullOrWhiteSpace(displayName)) {
item.name = displayName;
}
return item;
}
private static int CalculateOutputAmountVanillaRandom(int recycleAmount, float ingredientChance) {
var outputAmount = 0;
// Roll a random number for every item to consume.for (var i = 0; i < recycleAmount; i++)
{
if (UnityEngine.Random.Range(0f, 1f) <= ingredientChance) {
outputAmount++;
}
}
return outputAmount;
}
private static int CalculateOutputAmountFast(int recycleAmount, float ingredientAmount) {
// To save on performance, don't generate hundreds/thousands/millions of random numbers.var fractionalOutputAmount = ingredientAmount * recycleAmount;
var integerOutputAmount = (int)fractionalOutputAmount;
// Roll a random number to see if the the remainder should be given.var remainderFractionalOutputAmount = fractionalOutputAmount - integerOutputAmount;
if (remainderFractionalOutputAmount > 0 && UnityEngine.Random.Range(0f, 1f) <= remainderFractionalOutputAmount) {
integerOutputAmount++;
}
return integerOutputAmount;
}
private static int CalculateOutputAmount(int recycleAmount, float ingredientAmount, float outputMultiplier = 1, bool forEditor = false) {
// Adjust using vanilla rounding behavior before applying multipliers, to match user expectations.var adjustedIngredientAmount = ingredientAmount > 1
? Mathf.Ceil(ingredientAmount) : ingredientAmount;
adjustedIngredientAmount *= outputMultiplier;
// Use more optimized RNG for stacks larger than 100, because players probably don't care at that point.var outputAmount = adjustedIngredientAmount < 1 && recycleAmount <= 100
? CalculateOutputAmountVanillaRandom(recycleAmount, adjustedIngredientAmount) : CalculateOutputAmountFast(recycleAmount, adjustedIngredientAmount);
return forEditor ? Math.Max(1, outputAmount) : outputAmount;
}
private static bool AddItemToRecyclerOutput(Recycler recycler, ItemDefinition itemDefinition, int ingredientAmount, ulong skinId = 0, string displayName = null) {
var outputIsFull = false;
var numStacks = Mathf.CeilToInt(ingredientAmount / (float)itemDefinition.stackable);
for (var i = 0; i < numStacks; i++) {
var amountForStack = Math.Min(ingredientAmount, itemDefinition.stackable);
var outputItem = CreateItem(itemDefinition, amountForStack, skinId, displayName);
if (!recycler.MoveItemToOutput(outputItem)) {
outputIsFull = true;
}
ingredientAmount -= amountForStack;
if (ingredientAmount <= 0) break;
}
return outputIsFull;
}
#endregion
#region UI
private enum IdentificationType {
Item, Skin, DisplayName,
}
private enum OutputType {
NotRecyclable, Default, Custom,
}
private enum UICommand {
Edit, Reset, Save, Cancel, InputPercentage, ChangeIdentificationType, ChangeOutputType,
}
[Flags]
private enum LayoutOptions {
AnchorBottom = 1 << 0, AnchorRight = 1 << 1, Vertical = 1 << 2,
}
private class LayoutProvider {
public const string AnchorBottomLeft = "0 0";
public const string AnchorBottomRight = "1 0";
public const string AnchorTopLeft = "0 1";
public const string AnchorTopRight = "1 1";
public static LayoutProvider Once(float width = 0, float height = 0) => _reusable.WithOffset().WithDimensions(width, height).WithOptions(0).WithSpacing(0);
private static LayoutProvider _reusable = new LayoutProvider(0, 0);
private LayoutOptions _options;
private string _anchor;
private float _x, _y;
private float _xSpacing, _ySpacing;
private float _width, _height;
private bool _isVertical => (_options & LayoutOptions.Vertical) != 0;
private bool _isLeftToRight => (_options & LayoutOptions.AnchorRight) == 0;
private bool _isTopToBottom => (_options & LayoutOptions.AnchorBottom) == 0;
private int _xSign => _isLeftToRight ? 1 : -1;
private int _ySign => _isTopToBottom ? -1 : 1;
private float XMin, XMax, YMin, YMax;
public string AnchorMin => _anchor;
public string AnchorMax => _anchor;
public string OffsetMin => $"{XMin.ToString()} {YMin.ToString()}";
public string OffsetMax => $"{XMax.ToString()} {YMax.ToString()}";
public LayoutProvider(float width, float height) {
WithDimensions(width, height);
WithOptions(0);
}
public LayoutProvider WithDimensions(float width, float height) {
_width = width;
_height = height;
return this;
}
public LayoutProvider WithOptions(LayoutOptions options) {
_options = options;
_anchor = DetermineAnchor();
return this;
}
public LayoutProvider WithSpacing(float x, float y = float.MaxValue) {
_xSpacing = x;
_ySpacing = y != float.MaxValue ? y : x;
return this;
}
public LayoutProvider WithOffset(float x = 0, float y = 0) {
_x = x * _xSign;
_y = y * _ySign;
return this;
}
public LayoutProvider Next() {
XMin = _x + _xSpacing * _xSign;
YMin = _y + _ySpacing * _ySign;
XMax = XMin + _width * _xSign;
YMax = YMin + _height * _ySign;
if (_isVertical) {
_y = YMax;
}
else {
_x = XMax;
}
if (YMin > YMax) {
Swap(ref YMin, ref YMax);
}
if (XMin > XMax) {
Swap(ref XMin, ref XMax);
}
return this;
}
public CuiRectTransformComponent GetRectTransform() {
return new CuiRectTransformComponent {
AnchorMin = _anchor, AnchorMax = _anchor, OffsetMin = OffsetMin, OffsetMax = OffsetMax,
}
;
}
private string DetermineAnchor() {
return _isTopToBottom ? _isLeftToRight ? AnchorTopLeft : AnchorTopRight : _isLeftToRight ? AnchorBottomLeft : AnchorBottomRight;
}
}
private class ButtonColor {
public readonly string Color;
public readonly string TextColor;
public ButtonColor(string color, string textColor) {
Color = color;
TextColor = textColor;
}
}
private class ButtonColorScheme {
public ButtonColor Active;
public ButtonColor Enabled;
public ButtonColor Disabled;
public ButtonColor Get(bool active = false, bool enabled = false) {
return active ? Active : enabled ? Enabled : Disabled;
}
}
private class CuiInputFieldComponentHud : CuiInputFieldComponent {
[JsonProperty("hudMenuInput", DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool HudMenuInput {
get;
set;
}
}
private class CuiElementRecreate : CuiElement {
[JsonProperty("destroyUi", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string DestroyUi {
get;
set;
}
}
private static class EditUI {
private const string UIName = "RecycleManager.UI";
private const string EditPanelUIName = "RecycleManager.UI.EditPanel";
private const string PercentagePanelUIName = "RecycleManager.UI.EditPanel.Percentages";
private const string AnchorMin = "0.5 0";
private const string AnchorMax = "0.5 0";
private const float PanelWidth = 380.5f;
private const float PanelHeight = 93f;
private const float HeaderHeight = 21;
private const int ItemPaddingLeft = 6;
private const int ItemSize = 58;
private const int ItemSpacing = 4;
private const string BaseUICommand = "recyclemanager.ui";
private const string TextColor = "0.8 0.8 0.8 1";
private const string BackgroundColor = "0.25 0.25 0.25 1";
private static ButtonColorScheme DefaultButtonColorScheme = new ButtonColorScheme {
Active = new ButtonColor("0.25 0.5 0.75 1", "0.75 0.85 1 1"), Enabled = new ButtonColor("0.4 0.4 0.4 1", "0.71 0.71 0.71 1"), Disabled = new ButtonColor("0.4 0.4 0.4 0.5", "0.71 0.71 0.71 0.5"),
}
;
private static ButtonColorScheme SaveButtonColorScheme = new ButtonColorScheme {
Enabled = new ButtonColor("0.451 0.553 0.271 1", "0.659 0.918 0.2 1"), Disabled = new ButtonColor("0.451 0.553 0.271 0.5", "0.659 0.918 0.2 0.5"),
}
;
private static ButtonColorScheme ResetButtonColorScheme = new ButtonColorScheme {
Enabled = new ButtonColor("0.9 0.5 0.2 1", "1 0.9 0.7 1"), Disabled = new ButtonColor("0.9 0.5 0.2 0.25", "1 0.9 0.7 0.25"),
}
;
public static void DestroyUI(BasePlayer player) {
CuiHelper.DestroyUi(player, UIName);
}
public static void DrawUI(RecycleManager plugin, BasePlayer player, EditState state) {
if (state == null) {
DrawDefaultUI(plugin, player);
}
else {
DrawEditUI(plugin, player, state);
}
}
private static void DrawDefaultUI(RecycleManager plugin, BasePlayer player) {
var elements = CreateContainer();
AddEditButton(elements, plugin, player);
CuiHelper.AddUi(player, elements);
}
private static void DrawEditUI(RecycleManager plugin, BasePlayer player, EditState state) {
var elements = CreateContainer();
AddEditPanel(elements, plugin, player, state);
CuiHelper.AddUi(player, elements);
}
private static CuiElementContainer CreateContainer() {
var offsetY = 108f;
var offsetX = 192f;
return new CuiElementContainer {
{
new CuiElementRecreate {
Parent = "Hud.Menu", Name = UIName, DestroyUi = UIName, Components = {
new CuiRectTransformComponent {
AnchorMin = AnchorMin, AnchorMax = AnchorMax, OffsetMin = $"{offsetX} {offsetY}", OffsetMax = $"{offsetX} {offsetY}",
}
}
}
}
}
;
}
private static void AddEditButton(CuiElementContainer elements, RecycleManager plugin, BasePlayer player) {
var buttonWidth = 80f;
var offsetX = PanelWidth - buttonWidth;
var offsetY = 266f;
elements.Add(new CuiButton {
Text = {
Text = plugin.GetMessage(player.UserIDString, LangEntry.UIButtonAdmin), Color = SaveButtonColorScheme.Enabled.TextColor, Align = TextAnchor.MiddleCenter, FontSize = 15,
}
, Button = {
Color = SaveButtonColorScheme.Enabled.Color, Command = $"{BaseUICommand} {UICommand.Edit}", FadeIn = 0.1f,
}
, RectTransform = {
AnchorMin = "0 0", AnchorMax = "0 0", OffsetMin = $"{offsetX} {offsetY}", OffsetMax = $"{offsetX + buttonWidth} {offsetY + HeaderHeight}",
}
,
}
, UIName);
}
private static void AddEditHeader(CuiElementContainer elements, RecycleManager plugin, BasePlayer player) {
var offsetY = 266f;
elements.Add(new CuiElement {
Parent = EditPanelUIName, Components = {
new CuiRawImageComponent {
Color = BackgroundColor, Sprite = "assets/content/ui/ui.background.tiletex.psd",
}
, new CuiRectTransformComponent {
AnchorMin = "0 0", AnchorMax = "0 0", OffsetMin = $"0 {offsetY}", OffsetMax = $"{PanelWidth} {offsetY + HeaderHeight}",
}
,
}
,
}
);
elements.Add(new CuiElement {
Parent = EditPanelUIName, Components = {
new CuiTextComponent {
Text = plugin.GetMessage(player.UserIDString, LangEntry.UIHeader), Align = TextAnchor.MiddleLeft, FontSize = 14, Color = TextColor,
}
, new CuiRectTransformComponent {
AnchorMin = "0 0", AnchorMax = "0 0", OffsetMin = $"5 {offsetY}", OffsetMax = $"{5 + PanelWidth} {offsetY + HeaderHeight}",
}
,
}
}
);
var buttonWidth = 80f;
var offsetX = PanelWidth - buttonWidth;
elements.Add(new CuiButton {
Text = {
Text = plugin.GetMessage(player.UserIDString, LangEntry.UIButtonClose), Color = DefaultButtonColorScheme.Enabled.TextColor, Align = TextAnchor.MiddleCenter, FontSize = 15,
}
, Button = {
Color = DefaultButtonColorScheme.Enabled.Color, Command = $"{BaseUICommand} {UICommand.Cancel}",
}
, RectTransform = {
AnchorMin = "0 0", AnchorMax = "0 0", OffsetMin = $"{offsetX} {offsetY}", OffsetMax = $"{offsetX + buttonWidth} {offsetY + HeaderHeight}",
}
,
}
, UIName);
}
private static void AddEditPanel(CuiElementContainer elements, RecycleManager plugin, BasePlayer player, EditState state) {
elements.Add(new CuiElement {
Parent = UIName, Name = EditPanelUIName, Components = {
new CuiRawImageComponent {
Color = BackgroundColor, Sprite = "assets/content/ui/ui.background.tiletex.psd",
}
, new CuiRectTransformComponent {
AnchorMin = AnchorMin, AnchorMax = AnchorMax, OffsetMin = "0 0", OffsetMax = $"{PanelWidth} {PanelHeight}",
}
,
}
,
}
);
AddEditHeader(elements, plugin, player);
if (state.BlockedByAnotherPlugin) {
elements.Add(new CuiElement {
Parent = EditPanelUIName, Components = {
new CuiTextComponent {
Text = plugin.GetMessage(player.UserIDString, LangEntry.UIItemBlocked), Align = TextAnchor.MiddleCenter, FontSize = 12, Color = TextColor,
}
, new CuiRectTransformComponent {
AnchorMin = "0 0", AnchorMax = "1 1",
}
,
}
}
);
return;
}
AddPercentageControllers(elements, state);
if (state.InputItem == null) {
elements.Add(new CuiElement {
Parent = EditPanelUIName, Components = {
new CuiTextComponent {
Text = plugin.GetMessage(player.UserIDString, LangEntry.UIEmptyState), Align = TextAnchor.MiddleCenter, FontSize = 12, Color = TextColor,
}
, new CuiRectTransformComponent {
AnchorMin = "0 0", AnchorMax = "1 1",
}
,
}
}
);
return;
}
AddItemIdentificationControls(elements, plugin, player, state);
AddItemAllowedControls(elements, plugin, player, state);
AddPrimaryControls(elements, plugin, player, state);
}
private static void AddPercentageControllers(CuiElementContainer elements, EditState state) {
var offsetY = 169;
elements.Add(new CuiElement {
Name = PercentagePanelUIName, Parent = EditPanelUIName, Components = {
new CuiRawImageComponent {
Color = BackgroundColor, Sprite = "assets/content/ui/ui.background.tiletex.psd",
}
, new CuiRectTransformComponent {
AnchorMin = "0 0", AnchorMax = "0 0", OffsetMin = $"0 {offsetY}", OffsetMax = $"{PanelWidth} {offsetY + HeaderHeight}",
}
,
}
,
}
);
if (state.InputItem == null) return;
for (var i = 0; i < 6; i++) {
if (state.Chances[i] <= 0) continue;
var offsetX = ItemPaddingLeft + i * (ItemSize + ItemSpacing);
elements.Add(new CuiElement {
Parent = PercentagePanelUIName, Components = {
new CuiInputFieldComponentHud {
Align = TextAnchor.MiddleCenter, HudMenuInput = true, Text = $"{state.Chances[i]:0.##}%", Color = TextColor, CharsLimit = 6, Command = $"{BaseUICommand} {UICommand.InputPercentage} {i}",
}
, new CuiRectTransformComponent {
AnchorMin = "0 0", AnchorMax = "0 0", OffsetMin = $"{offsetX} 0", OffsetMax = $"{offsetX + ItemSize} {HeaderHeight}",
}
,
}
,
}
);
}
}
private static void AddButton(CuiElementContainer elements, LayoutProvider layoutProvider, ButtonColorScheme buttonColorScheme, string parent, string text, string command, bool active = false, bool enabled = true) {
var buttonColor = buttonColorScheme.Get(active, enabled);
elements.Add(new CuiButton {
Text = {
Text = text, Color = buttonColor.TextColor, Align = TextAnchor.MiddleCenter, FontSize = 12,
}
, Button = {
Color = buttonColor.Color, Command = enabled && !active ? command : null,
}
, RectTransform = {
AnchorMin = layoutProvider.AnchorMin, AnchorMax = layoutProvider.AnchorMax, OffsetMin = layoutProvider.OffsetMin, OffsetMax = layoutProvider.OffsetMax,
}
}
, parent);
}
private static void AddItemIdentificationControls(CuiElementContainer elements, RecycleManager plugin, BasePlayer player, EditState state) {
var spacingX = 5;
var spacingY = 2;
var paddingY = 5;
var numElements = 4;
var columnWidth = PanelWidth / 3f;
var elementHeight = (PanelHeight - 2 * paddingY - (numElements - 1) * spacingY) / numElements;
var layoutProvider = LayoutProvider.Once(columnWidth - 2 * spacingX, elementHeight).WithOffset(0, paddingY - spacingY).WithSpacing(spacingX, spacingY).WithOptions(LayoutOptions.Vertical);
elements.Add(new CuiElement {
Parent = EditPanelUIName, Components = {
new CuiTextComponent {
Text = plugin.GetMessage(player.UserIDString, LangEntry.UILabelConfigureBy), Align = TextAnchor.MiddleCenter, FontSize = 12, Color = TextColor,
}
, layoutProvider.Next().GetRectTransform(),
}
}
);
AddButton(elements, layoutProvider.Next(), DefaultButtonColorScheme, EditPanelUIName, plugin.GetMessage(player.UserIDString, LangEntry.UIButtonItem), $"{BaseUICommand} {UICommand.ChangeIdentificationType} {IdentificationType.Item}", state.IdentificationType == IdentificationType.Item, state.InputItem.skin != 0);
AddButton(elements, layoutProvider.Next(), DefaultButtonColorScheme, EditPanelUIName, plugin.GetMessage(player.UserIDString, LangEntry.UIButtonSkin), $"{BaseUICommand} {UICommand.ChangeIdentificationType} {IdentificationType.Skin}", state.IdentificationType == IdentificationType.Skin, state.InputItem.skin != 0);
AddButton(elements, layoutProvider.Next(), DefaultButtonColorScheme, EditPanelUIName, plugin.GetMessage(player.UserIDString, LangEntry.UIButtonDisplayName), $"{BaseUICommand} {UICommand.ChangeIdentificationType} {IdentificationType.DisplayName}", state.IdentificationType == IdentificationType.DisplayName, !string.IsNullOrWhiteSpace(state.InputItem.name));
}
private static void AddItemAllowedControls(CuiElementContainer elements, RecycleManager plugin, BasePlayer player, EditState state) {
var spacingX = 5;
var spacingY = 2;
var paddingY = 5;
var numElements = 4;
var columnWidth = PanelWidth / 3f;
var elementHeight = (PanelHeight - 2 * paddingY - (numElements - 1) * spacingY) / numElements;
var layoutProvider = LayoutProvider.Once(columnWidth - 2 * spacingX, elementHeight).WithOffset(columnWidth, paddingY - spacingY).WithSpacing(spacingX, spacingY).WithOptions(LayoutOptions.Vertical);
elements.Add(new CuiElement {
Parent = EditPanelUIName, Components = {
new CuiTextComponent {
Text = plugin.GetMessage(player.UserIDString, LangEntry.UILabelOutput), Align = TextAnchor.MiddleCenter, FontSize = 12, Color = TextColor,
}
, layoutProvider.Next().GetRectTransform(),
}
}
);
AddButton(elements, layoutProvider.Next(), DefaultButtonColorScheme, EditPanelUIName, plugin.GetMessage(player.UserIDString, LangEntry.UIButtonNotRecyclable), $"{BaseUICommand} {UICommand.ChangeOutputType} {OutputType.NotRecyclable}", state.OutputType == OutputType.NotRecyclable);
AddButton(elements, layoutProvider.Next(), DefaultButtonColorScheme, EditPanelUIName, plugin.GetMessage(player.UserIDString, LangEntry.UIButtonDefaultOutput), $"{BaseUICommand} {UICommand.ChangeOutputType} {OutputType.Default}", state.OutputType == OutputType.Default, state.IdentificationType != IdentificationType.Item || IsVanillaRecyclable(state.InputItem));
AddButton(elements, layoutProvider.Next(), DefaultButtonColorScheme, EditPanelUIName, plugin.GetMessage(player.UserIDString, LangEntry.UIButtonCustomOutput), $"{BaseUICommand} {UICommand.ChangeOutputType} {OutputType.Custom}", state.OutputType == OutputType.Custom);
}
private static void AddPrimaryControls(CuiElementContainer elements, RecycleManager plugin, BasePlayer player, EditState state) {
var spacingX = 5;
var spacingY = 2;
var paddingY = 5;
var numElements = 4;
var columnWidth = PanelWidth / 3f;
var elementHeight = (PanelHeight - 2 * paddingY - (numElements - 1) * spacingY) / numElements;
var layoutProvider = LayoutProvider.Once(columnWidth - 2 * spacingX, elementHeight).WithOffset(2 * columnWidth, paddingY - spacingY).WithSpacing(spacingX, spacingY).WithOptions(LayoutOptions.Vertical);
elements.Add(new CuiElement {
Parent = EditPanelUIName, Components = {
new CuiTextComponent {
Text = plugin.GetMessage(player.UserIDString, LangEntry.UILabelActions), Align = TextAnchor.MiddleCenter, FontSize = 12, Color = TextColor,
}
, layoutProvider.Next().GetRectTransform(),
}
}
);
AddButton(elements, layoutProvider.Next(), SaveButtonColorScheme, EditPanelUIName, plugin.GetMessage(player.UserIDString, LangEntry.UIButtonSave), $"{BaseUICommand} {UICommand.Save}", enabled : state.CanSave);
AddButton(elements, layoutProvider.Next(), ResetButtonColorScheme, EditPanelUIName, plugin.GetMessage(player.UserIDString, LangEntry.UIButtonReset), $"{BaseUICommand} {UICommand.Reset}", enabled : state.CanReset);
AddButton(elements, layoutProvider.Next(), DefaultButtonColorScheme, EditPanelUIName, plugin.GetMessage(player.UserIDString, LangEntry.UIButtonCancel), $"{BaseUICommand} {UICommand.Cancel}");
}
}
#endregion
#region Edit Controller
private class EditState {
public Item InputItem;
public IdentificationType IdentificationType;
public OutputType OutputType;
public float[] Chances = new float[NumOutputSlots];
public bool BlockedByAnotherPlugin;
public bool CanSave;
public bool CanReset;
}
private class EditController : FacepunchBehaviour {
public static EditController AddToRecycler(RecycleManager plugin, RecycleEditManager recycleEditManager, Recycler recycler) {
var component = recycler.gameObject.AddComponent<EditController>();
component._plugin = plugin;
component._recycleEditManager = recycleEditManager;
component._recycler = recycler;
return component;
}
private RecycleManager _plugin;
private RecycleEditManager _recycleEditManager;
private Recycler _recycler;
private BasePlayer _player;
private EditState _editState;
private Func<Item, int, bool> _originalCanAcceptItem;
private Action _onDirtyDelayed;
private bool _pauseAutoChangeOutput;
private Configuration _config => _plugin._config;
private EditController() {
_onDirtyDelayed = OnDirtyDelayed;
}
public void StartViewing(BasePlayer player) {
_player = player;
DrawUI();
}
public void DestroyImmediate() {
DestroyImmediate(this);
}
private IngredientInfo[] GetSavedIngredients() {
return GetOutput(_editState.IdentificationType, _editState.OutputType);
}
private bool CanSave() {
if (_editState.InputItem == null) return false;
switch (_editState.OutputType) {
case OutputType.NotRecyclable : {
if (IsDisallowed()) return false;
if (GetOverride() != null) return true;
return _editState.IdentificationType == IdentificationType.Item && IsVanillaRecyclable(_editState.InputItem);
}
case OutputType.Default : {
if (IsDisallowed()) return true;
if (GetOverride() != null) return true;
return false;
}
case OutputType.Custom : return GetOverride() == null || !GetOutputIngredients().SequenceEqual(GetSavedIngredients());
default : return true;
}
}
private bool CanReset() {
if (_editState.InputItem == null) return false;
return IsDisallowed() || GetOverride() != null;
}
private void DrawUI() {
if (_editState != null) {
_editState.CanSave = CanSave();
_editState.CanReset = CanReset();
}
EditUI.DrawUI(_plugin, _player, _editState);
}
private List<IngredientInfo> GetOutputIngredients() {
var customIngredientList = new List<IngredientInfo>();
for (var i = NumInputSlots; i < Num
// ... [Code preview truncated due to length. Full source in attached file] ...