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.2.1")]
[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 ClassicRecycleEfficiency = 0.5f;
private const float VanillaMaxItemsInStackFraction = 0.1f;
private readonly object True = true;
private readonly object False = false;
private const int NumInputSlots = 6;
private const int NumOutputSlots = 6;
private readonly RecyclerComponentManager _recyclerComponentManager = new();
private readonly RecycleEditManager _recycleEditManager;
private readonly float[] _recycleTime = new float[1];
private readonly float[] _recycleEfficiency = 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);
_recyclerComponentManager.Init(this);
permission.RegisterPermission(PermissionAdmin, this);
if (!_config.Speed.Enabled && !_config.Efficiency.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
_recyclerComponentManager.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)
{
_recyclerComponentManager.HandleRecyclerToggle(recycler, player);
}
private object OnItemRecycle(Item item, Recycler recycler)
{
if (RecycleItemWasBlocked(item, recycler))
return null;
var maxItemsInStackFraction = _config.MaxItemsPerRecycle.GetPercent(item) / 100f;
var recycleAmount = DetermineConsumptionAmount(recycler, item, maxItemsInStackFraction);
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, DetermineItemRecycleEfficiency(item, 1f)))
{
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;
// Issue: Last known player will always be null if speed and efficiency are both disabled, since toggle hook
// is not subscribed to improve performance.
var lastKnownPlayer = _recyclerComponentManager.EnsureRecyclerComponent(recycler).Player;
var recycleEfficiency = DetermineItemRecycleEfficiency(item, GetRecyclerEfficiency(recycler, lastKnownPlayer, out var vanillaEfficiency));
if (recycleEfficiency == vanillaEfficiency
&& maxItemsInStackFraction == VanillaMaxItemsInStackFraction
&& !IsOutputCustomized(item.info.Blueprint))
return null;
item.UseItem(recycleAmount);
var outputIsFull = PopulateOutputVanilla(_config, recycler, item, recycleAmount, 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 void OnRecycleManagerEfficiency(Recycler recycler, BasePlayer player, float[] recyclerEfficiency)
{
Interface.CallHook("OnRecycleManagerEfficiency", recycler, player, recyclerEfficiency);
}
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;
if (!VerifyValidItemIdOrShortName(player, args.ElementAtOrDefault(0), out var 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;
if (!VerifyValidItemIdOrShortName(player, args.ElementAtOrDefault(0), out var 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 Utilities
#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;
}
if (int.TryParse(itemArg, out var 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 float GetRecyclerEfficiency(Recycler recycler, BasePlayer player, out float vanillaEfficiency)
{
vanillaEfficiency = DetermineVanillaRecycleEfficiency(recycler);
_recycleEfficiency[0] = _config.Efficiency.Enabled
? _config.Efficiency.GetRecyclerEfficiency(recycler)
: vanillaEfficiency;
ExposedHooks.OnRecycleManagerEfficiency(recycler, player, _recycleEfficiency);
return Mathf.Max(0, _recycleEfficiency[0]);
}
private bool TryDetermineRecycleTime(Recycler recycler, BasePlayer player, out float recycleTime)
{
_recycleTime[0] = _config.Speed.GetBaseRecycleTime(recycler)
* _config.Speed.GetTimeMultiplierForPlayer(player);
if (ExposedHooks.OnRecycleManagerSpeed(recycler, player, _recycleTime) is false)
{
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 bool IsOutputCustomized(ItemBlueprint blueprint)
{
if (blueprint.scrapFromRecycle > 0 && _config.OutputMultipliers.GetOutputMultiplier(ScrapItemId) != 1)
return true;
foreach (var ingredient in blueprint.ingredients)
{
// Skip scrap since it's handled separately.
if (ingredient.itemDef.itemid == ScrapItemId)
continue;
if (_config.OutputMultipliers.GetOutputMultiplier(ingredient.itemid) != 1)
return true;
}
return false;
}
#endregion
#region Helper Methods - Static
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)
{
(a, b) = (b, a);
}
private static bool IsVanillaRecyclable(Item item)
{
return item.info.Blueprint != null;
}
private static bool RecyclableWasBlocked(Item item, Recycler recycler)
{
return ExposedHooks.OnRecycleManagerItemRecyclable(item, recycler) is false;
}
private static bool RecycleItemWasBlocked(Item item, Recycler recycler)
{
return ExposedHooks.OnRecycleManagerRecycle(item, recycler) is 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 adjustedIngredientChance)
{
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) <= adjustedIngredientChance)
{
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 outputAmountDecimal = ingredientAmount * recycleAmount;
var outputAmountInt = (int)outputAmountDecimal;
// Roll a random number to see if the the remainder should be given.
var remainderFractionalOutputAmount = outputAmountDecimal - outputAmountInt;
if (remainderFractionalOutputAmount > 0 && UnityEngine.Random.Range(0f, 1f) <= remainderFractionalOutputAmount)
{
outputAmountInt++;
}
return outputAmountInt;
}
private static int CalculateOutputAmountRandom(int recycleAmount, float ingredientAmount, float recycleEfficiency, float outputMultiplier)
{
var adjustedIngredientAmount = ingredientAmount * outputMultiplier;
if (adjustedIngredientAmount <= 1 && recycleAmount <= 100)
return CalculateOutputAmountVanillaRandom(recycleAmount, adjustedIngredientAmount * recycleEfficiency);
return CalculateOutputAmountFast(recycleAmount, adjustedIngredientAmount * recycleEfficiency);
}
private static int CalculateOutputAmountNoRandom(int recycleAmount, float ingredientAmount, float recycleEfficiency, float outputMultiplier)
{
var adjustedIngredientAmount = Mathf.CeilToInt(ingredientAmount * recycleEfficiency) * outputMultiplier;
return CalculateOutputAmountFast(recycleAmount, adjustedIngredientAmount);
}
private static int CalculateOutputAmount(int recycleAmount, float ingredientAmount, float recycleEfficiency, float outputMultiplier = 1)
{
if (ingredientAmount <= 1)
return CalculateOutputAmountRandom(recycleAmount, ingredientAmount, recycleEfficiency, outputMultiplier);
return CalculateOutputAmountNoRandom(recycleAmount, ingredientAmount, recycleEfficiency, outputMultiplier);
}
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;
}
private static float DetermineVanillaRecycleEfficiency(Recycler recycler)
{
return recycler.lastFetchedEfficiency;
}
private static IngredientInfo[] GetVanillaOutput(ItemDefinition itemDefinition)
{
if (itemDefinition.Blueprint?.ingredients == null)
return Array.Empty<IngredientInfo>();
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 * ClassicRecycleEfficiency;
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 static float DetermineItemRecycleEfficiency(Item item, float recyclerEfficiency)
{
return item.hasCondition
? Mathf.Clamp01(recyclerEfficiency * Mathf.Clamp(item.conditionNormalized * item.maxConditionNormalized, 0.1f, 1f))
: recyclerEfficiency;
}
private static int DetermineConsumptionAmount(Recycler recycler, Item item, float maxItemsInStackFraction)
{
var recycleAmount = 1;
if (item.amount > 1)
{
recycleAmount = Mathf.CeilToInt(Mathf.Min(item.amount, item.MaxStackable() * maxItemsInStackFraction));
// 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.
if (Interface.CallHook("OnItemRecycleAmount", item, recycleAmount, recycler) is int overrideAmount)
return overrideAmount;
return recycleAmount;
}
private static 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;
if (ingredientAmount <= 0)
continue;
var outputAmount = CalculateOutputAmount(recycleAmount, ingredientAmount, recycleEfficiency);
if (outputAmount <= 0)
continue;
if (forEditor && outputAmount < 1)
{
outputAmount = 1;
}
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)
{
var outputIsFull = false;
if (item.info.Blueprint.scrapFromRecycle > 0)
{
var scrapOutputMultiplier = config.OutputMultipliers.GetOutputMultiplier(ScrapItemId);
var scrapAmountDecimal = item.info.Blueprint.scrapFromRecycle * (float)recycleAmount * scrapOutputMultiplier;
if (item.MaxStackable() == 1 && item.hasCondition)
{
scrapAmountDecimal *= item.conditionNormalized;
}
scrapAmountDecimal *= recycleEfficiency / ClassicRecycleEfficiency;
var scrapAmountInt = Mathf.FloorToInt(scrapAmountDecimal);
var scrapRemainderForThisCycle = scrapAmountDecimal - scrapAmountInt;
if (scrapRemainderForThisCycle > 0)
{
var recyclerScrapRemainder = recycler.scrapRemainder;
recyclerScrapRemainder += scrapRemainderForThisCycle;
var scrapRemainderToOutput = Mathf.FloorToInt(recyclerScrapRemainder);
if (scrapRemainderToOutput > 0)
{
recyclerScrapRemainder -= scrapRemainderToOutput;
scrapAmountInt += scrapRemainderToOutput;
}
recycler.scrapRemainder = recyclerScrapRemainder;
}
if (scrapAmountInt >= 1)
{
var scrapItem = ItemManager.CreateByItemID(ScrapItemId, scrapAmountInt);
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 / item.info.Blueprint.amountToCreate;
if (ingredientAmount <= 0)
continue;
var outputAmount = CalculateOutputAmount(
recycleAmount,
ingredientAmount,
recycleEfficiency,
config.OutputMultipliers.GetOutputMultiplier(ingredient.itemid)
);
if (outputAmount <= 0)
continue;
if (AddItemToRecyclerOutput(recycler, ingredient.itemDef, outputAmount))
{
outputIsFull = true;
}
}
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)
{
return _reusable.WithOffset().WithDimensions(width, height).WithOptions(0).WithSpacing(0);
}
private static LayoutProvider _reusable = new(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()
{
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()
{
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()
{
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 = 109.5f;
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}
// ... [Code preview truncated due to length. Full source in attached file] ...