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 System.Reflection;
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.1.0")]
[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 static readonly FieldInfo ScrapRemainderField = typeof(Recycler).GetField("scrapRemainder", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
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
private readonly struct ReflectionAdapter<T> {
private readonly FieldInfo _fieldInfo;
private readonly object _object;
public ReflectionAdapter(object obj, FieldInfo fieldInfo) {
_object = obj;
_fieldInfo = fieldInfo;
}
public T Value {
get => _fieldInfo == null ? default : (T)_fieldInfo.GetValue(_object);
set => _fieldInfo?.SetValue(_object, value);
}
public static implicit operator T(ReflectionAdapter<T> adapter) {
return adapter.Value;
}
}
#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.DefaultRecycleTime * _config.Speed.GetTimeMultiplierForPlayer(player);
if (_config.Speed.SafeZoneTimeMultiplier != 1 && player.InSafeZone()) {
_recycleTime[0] *= _config.Speed.SafeZoneTimeMultiplier;
}
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.IsSafezoneRecycler() ? recycler.safezoneRecycleEfficiency : recycler.radtownRecycleEfficiency;
}
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 scrapRemainderAdapter = new ReflectionAdapter<float>(recycler, ScrapRemainderField);
var recyclerScrapRemainder = scrapRemainderAdapter.Value;
recyclerScrapRemainder += scrapRemainderForThisCycle;
var scrapRemainderToOutput = Mathf.FloorToInt(recyclerScrapRemainder);
if (scrapRemainderToOutput > 0) {
recyclerScrapRemainder -= scrapRemainderToOutput;
scrapAmountInt += scrapRemainderToOutput;
}
scrapRemainderAdapter.Value = 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} {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(),
}
,
}
// ... [Code preview truncated due to length. Full source in attached file] ...