using System; using System.Linq; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; using Oxide.Core; using Oxide.Core.Configuration; using Oxide.Core.Libraries; using Oxide.Core.Plugins; using Oxide.Game.Rust; using Oxide.Game.Rust.Cui; using Oxide.Core.Libraries.Covalence; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Time = Oxide.Core.Libraries.Time; using Rust; using Facepunch; using UnityEngine; using UnityEngine.Networking; //using ProtoBuf.Item; /* CHANGE LOG: Version 1.3.2: Added compatibility with the ItemHistory plugin. Version 1.3.3: Added compatibility with the Loottable plugin. Version 1.3.4: Added compatibility with the Economics plugin. Added the ability to set blanket economic reward values for recipes (just money) Version 1.3.5: Bugfix: corrected an issue with Configs not loading correctly. The main cause of the issue turned out to be a typo. Version 1.3.6: Added a flag to set a recipe as a BP or an Item Added a new BlueprintOutput Modified the default config to match the new functionality Version 1.4.0: Added compatibility with Stack-Modifier */ namespace Oxide.Plugins { [Info("HGRecycleAnything", "MijiSK", "1.4.0")] [Description("This plugin allows you to set your own recipes and outputs for recycling.")] public class HGRecycleAnything : RustPlugin { #region Config //////////////////////////////////////////////////////////////////////////// // Config //////////////////////////////////////////////////////////////////////////// public ConfigData configData; [PluginReference] private Plugin ItemHistory, Loottable, Economics, StackModifier; public Hash> validRecipes; private Hash itemPlayerTable; public class Recipe { [JsonProperty("Required Item SkinID", Order = 1)] public ulong requiredSkin { get; set; } = 0ul; [JsonProperty("Required Item Amount", Order = 2)] public int requiredAmount { get; set; } = 1; [JsonProperty("Required Item is BP?", Order = 3)] public bool requiredBP { get; set; } = false; [JsonProperty("Required Item DisplayName", Order = 4)] public string? requiredName { get; set; } [JsonProperty("Process Per Tick", Order = 5)] public int processPerTick { get; set; } [JsonProperty("Outputs", Order = 6)] public List outputs { get; set; } = new List(); } [JsonConverter(typeof(RecipeOutputConverter))] public abstract class RecipeOutput { [JsonProperty("Output Type", Order=1)] public string outputType { get; protected set; } protected RecipeOutput(string outputType) => this.outputType = outputType; } public class ItemOutput: RecipeOutput { [JsonProperty("Item Shortname", Order=2)] public string shortname { get; set; } [JsonProperty("Item Displayname", Order=3)] public string? displayName { get; set; } [JsonProperty("Item SkinID", Order=4)] public ulong? skin { get; set; } = 0ul; [JsonProperty("Min", Order=6)] public int min { get; set; } [JsonProperty("Max", Order=7)] public int max { get; set; } public ItemOutput(): base("item") {} } public class BlueprintOutput: RecipeOutput { [JsonProperty("Item Shortname", Order=2)] public string shortname { get; set; } [JsonProperty("Min", Order=3)] public int min { get; set; } [JsonProperty("Max", Order=4)] public int max { get; set; } public BlueprintOutput(): base("blueprint") {} } public class EconomicsOutput: RecipeOutput { [JsonProperty("Economic Reward", Order=2)] public int economicReward { get; set; } public EconomicsOutput(): base("economics") {} } public class ConfigData { [JsonProperty("Recipes", Order = 1)] public Dictionary> Recipes { get; set; } } protected override void LoadConfig() { // o.reload HGRecycleAnything base.LoadConfig(); // tweak the behaviors of the config loader to keep our param ordering in check. Config.Settings = new JsonSerializerSettings{ Formatting = Newtonsoft.Json.Formatting.Indented, ContractResolver = new OrderedContractResolver(), ReferenceLoopHandling = ReferenceLoopHandling.Ignore, }; configData = Config.ReadObject(); if (configData == null) LoadDefaultConfig(); SaveConfig(); } protected override void LoadDefaultConfig() { configData = new ConfigData(); configData.Recipes = new Dictionary>(); configData.Recipes.Add("pumpkin", new List { new Recipe { requiredAmount = 1, processPerTick = 10, outputs = new List { new ItemOutput { shortname = "seed.pumpkin", min = 1, max = 1 }, }} }); configData.Recipes.Add("corn", new List { new Recipe { requiredAmount = 1, processPerTick = 10, outputs = new List { new ItemOutput { shortname = "seed.corn", min = 1, max = 1 }, }} }); configData.Recipes.Add("potato", new List { new Recipe { requiredAmount = 1, processPerTick = 10, outputs = new List { new ItemOutput { shortname = "seed.potato", min = 1, max = 1 }, }} }); configData.Recipes.Add("blue.berry", new List { new Recipe { requiredAmount = 1, processPerTick = 10, outputs = new List { new ItemOutput { shortname = "seed.blue.berry", min = 1, max = 1 }, }} }); configData.Recipes.Add("green.berry", new List { new Recipe { requiredAmount = 1, processPerTick = 10, outputs = new List { new ItemOutput { shortname = "seed.green.berry", min = 1, max = 1 }, }} }); configData.Recipes.Add("white.berry", new List { new Recipe { requiredAmount = 1, processPerTick = 10, outputs = new List { new ItemOutput { shortname = "seed.white.berry", min = 1, max = 1 }, }} }); configData.Recipes.Add("yellow.berry", new List { new Recipe { requiredAmount = 1, processPerTick = 10, outputs = new List { new ItemOutput { shortname = "seed.yellow.berry", min = 1, max = 1 }, }} }); configData.Recipes.Add("red.berry", new List { new Recipe { requiredAmount = 1, processPerTick = 10, outputs = new List { new ItemOutput { shortname = "seed.red.berry", min = 1, max = 1 }, }} }); configData.Recipes.Add("rifle.ak", new List { new Recipe { requiredAmount = 1, requiredBP = false, processPerTick = 10, outputs = new List { new BlueprintOutput { shortname = "rifle.ak", min = 1, max = 1 }, new ItemOutput { shortname = "rifle.ak", min = 1, max = 1 }, new EconomicsOutput { economicReward = 500 }, }} }); configData.Recipes.Add( "pookie.bear", new List { new Recipe { requiredAmount = 1, processPerTick = 10, outputs = new List { new EconomicsOutput { economicReward = 1000 } } } }); } protected override void SaveConfig() => Config.WriteObject(configData, true); #endregion private void OnServerInitialized() { validRecipes = new Hash>(); itemPlayerTable = new Hash(); if (configData == null) { configData = new ConfigData(); } if (configData?.Recipes == null) { return; } foreach (KeyValuePair> kvRecipeList in configData?.Recipes) { string currentRecipe = kvRecipeList.Key; foreach (Recipe input in kvRecipeList.Value) { bool hasValidOutput = false; if (input.outputs?.Count > 0) { List outputs = input.outputs; for (int i = 0; i < outputs.Count; i++) { if (outputs[i].outputType == "economics") { EconomicsOutput item = (outputs[i] as EconomicsOutput); // honestly so long as the economicReward is set to anything other than null it can be considered valid if (item.economicReward == null) { // otherwise skip this output as it is not a valid recipe continue; } } else if (outputs[i].outputType == "item") { ItemOutput item = (outputs[i] as ItemOutput); if (!(ItemManager.FindItemDefinition(item.shortname) != null)) { // if the shortname doesn't match the rust definition then this is not a valid recipe continue; } // since we know the shortname is valid then we just need to clean up some possible config issues if (item.min < 0) { item.min = 0; } // swap the min and max if the admin is being a dope if (item.max < item.min) { int swap = item.min; item.max = item.min; item.min = swap; } outputs[i] = item; } else if (outputs[i].outputType == "blueprint") { BlueprintOutput item = (outputs[i] as BlueprintOutput); if (!(ItemManager.FindItemDefinition(item.shortname) != null)) { // if the shortname doesn't match the rust definition then this is not a valid recipe continue; } // since we know the shortname is valid then we just need to clean up some possible config issues if (item.min < 0) { item.min = 0; } // swap the min and max if the admin is being a dope if (item.max < item.min) { int swap = item.min; item.max = item.min; item.min = swap; } outputs[i] = item; } else { continue; } hasValidOutput = true; } } if (hasValidOutput) { if (!validRecipes.ContainsKey(currentRecipe)) { validRecipes.Add(currentRecipe, new List()); } validRecipes[currentRecipe].Add(input); } } } Unsubscribe(); } private void Unsubscribe() { if (Loottable || StackModifier) { Unsubscribe(nameof(OnItemSplit)); Unsubscribe(nameof(CanStackItem)); } } private object CanBeRecycled(Item item, Recycler recycler) { if (item == null) return (object) false; object hasValidRecipe = HasValidCustomRecipe(item); if (hasValidRecipe != null) { if (itemPlayerTable == null) { itemPlayerTable = new Hash(); } if (!itemPlayerTable.ContainsKey(item.uid)) { BasePlayer player = item.GetOwnerPlayer(); itemPlayerTable.Add(item.uid, player.userID); } } return hasValidRecipe; } private object CanRecycle(Recycler recycler, Item item) => HasValidCustomRecipe(item); private object HasValidCustomRecipe(Item item) { if (item == null) return null; string shortname = item.info.shortname; if (item.IsBlueprint()) { ItemDefinition targetDef = item.blueprintTargetDef; shortname = targetDef.shortname; } // do we have any recipes that match the current items shortname if (!validRecipes.TryGetValue(shortname, out List inputRecipes)) { return null; } object foundRecipe = null; // check each of the recipes for matching skinIDs and matching required ammounts foreach (Recipe recipe in inputRecipes) { // Check if the item to be recycled is a BP and if the recipe matches it. if (!IsBpOk(item, recipe)) { continue; } // Then we just need to check the item amounts are appropriate for the recipe if (!IsQtyOk(item, recipe)) { continue; } // In terms of the item skin we are trying to catch a couple of cases; // 1) if the recipe has no required skin and the item has a default skin (0ul) // 2) if the recipe has a required skin and the item matches the required skin if (!IsSkinOk(item, recipe)) { continue; } // If the recipe has a 'requied' displayName then check it matches the items name if (!IsDisplayNameOk(item, recipe)) { continue; } foundRecipe = true; break; } return foundRecipe; } // If the recipe has a required name and the items name doesn't match the required name then we fail the check private bool IsDisplayNameOk(Item item, Recipe recipe) { if ((recipe?.requiredName ?? "") != "") { if (item.name != (recipe?.requiredName ?? "")) { return false; } } return true; } // if the recipe has a required skin, and the items skin doesn't match then we fail the check private bool IsSkinOk(Item item, Recipe recipe) { if (recipe?.requiredSkin != item.skin) { return false; } return true; } // if the item has less quantity than the recipe we fail the check private bool IsQtyOk(Item item, Recipe recipe) { if (item.amount < recipe.requiredAmount) { return false; } return true; } // if the item has less quantity than the recipe we fail the check private bool IsBpOk(Item item, Recipe recipe) { if (item.IsBlueprint() != recipe.requiredBP) { return false; } return true; } private Recipe GetValidRecipe(Item item) { if (item == null) return null; // The best valid recipe is basically the most complex matching recipe // does the skin match, y/n, is there enough qty?, y/n, grab the recipe with the highest requiredAmount // cache the shortname in order to use as the recipe key string shortname = item.info.shortname; if (item.IsBlueprint()) { ItemDefinition targetDef = item.blueprintTargetDef; shortname = targetDef.shortname; } // do we have any recipes that match the current items shortname if (!validRecipes.TryGetValue(shortname, out List inputRecipes)) { return null; } List foundRecipes = new List(); // check each of the recipes for matching skinIDs and matching required ammounts foreach (Recipe recipe in inputRecipes) { if (IsBpOk(item, recipe) && IsQtyOk(item, recipe) && IsDisplayNameOk(item, recipe) && IsSkinOk(item, recipe)) { foundRecipes.Add(recipe); } } int highestRequiredAmount = 0; Recipe foundRecipe = null; foreach (Recipe recipe in foundRecipes) { if (recipe.requiredAmount >= highestRequiredAmount) { highestRequiredAmount = recipe.requiredAmount; foundRecipe = recipe; } } foundRecipes.Clear(); foundRecipes = null; return foundRecipe; } object OnItemRecycle(Item item, Recycler recycler) { // Recipe recipe = GetValidRecipe(item); if (recipe != null) { return SimulateRecycle(recycler, item, recipe); } // return null; } private bool SimulateRecycle(Recycler recycler, Item item, Recipe inputRecipe) { // int amountToRecycle = Math.Min(item.amount, inputRecipe.processPerTick); // consume the amount to recycle from the item. item.UseItem(amountToRecycle); // foreach (RecipeOutput output in inputRecipe.outputs) { if (Economics && output is EconomicsOutput) { EconomicsOutput o = output as EconomicsOutput; if (itemPlayerTable.TryGetValue(item.uid, out ulong playerId)) { BasePlayer player = BasePlayer.FindByID(playerId); if (player != null) { Economics?.Call("Deposit", player.UserIDString, (double)o.economicReward * amountToRecycle); } } } else if (output is BlueprintOutput) { BlueprintOutput o = output as BlueprintOutput; // for each allotment (meaning each time the item is recycled) give the output to the player int outputAmount = UnityEngine.Random.Range(o.min * amountToRecycle, (o.max * amountToRecycle) + 1); // if there is a chance of returning zero and we happen to hit that chance just skip to the next recipe output if (outputAmount <= 0 && o.min <= 0) { continue; } else if (outputAmount <= 0) { outputAmount = o.min; } var itemDef = ItemManager.FindItemDefinition(o.shortname); if (itemDef == null) { continue; } // Create a BP and change it to the itemDef item: Item outputItem = ItemManager.CreateByItemID(-996920608, outputAmount, 0); outputItem.blueprintTarget = itemDef.itemid; outputItem.MarkDirty(); // Ensures the item is updated properly. if (!recycler.MoveItemToOutput(outputItem)) { // because this plugin overwrites the default functionality of CanRecycle we need to stop the recycler when its full. recycler.StopRecycling(); } } else // (output is ItemOutput) { ItemOutput o = output as ItemOutput; // for each allotment (meaning each time the item is recycled) give the output to the player int outputAmount = UnityEngine.Random.Range(o.min * amountToRecycle, (o.max * amountToRecycle) + 1); // if there is a chance of returning zero and we happen to hit that chance just skip to the next recipe output if (outputAmount <= 0 && o.min <= 0) { continue; } else if (outputAmount <= 0) { outputAmount = o.min; } var itemDef = ItemManager.FindItemDefinition(o.shortname); if (itemDef == null) { continue; } Item outputItem = ItemManager.Create(itemDef, outputAmount); if (o.skin != 0ul) { outputItem.skin = (ulong) o.skin; } if ((o?.displayName ?? "") != "") { outputItem.name = o.displayName; } if (!recycler.MoveItemToOutput(outputItem)) { // because this plugin overwrites the default functionality of CanRecycle we need to stop the recycler when its full. recycler.StopRecycling(); } } } // Prevent default recycling behavior return true; } // block items with custom names/skins from being stackable private object CanStackItem(Item item, Item targetItem) { if (item.IsBlueprint()) return null; if ((item.name != "" && item.name != targetItem.name) || item.skin != targetItem.skin || item.info.shortname != targetItem.info.shortname || (item.info.shortname == targetItem.info.shortname && item.amount > item.info.stackable) ) return false; return null; } private Item OnItemSplit(Item item, int amount) { // if the item has a custom name lets try to protect it on split if (!item.IsBlueprint() && item.name != "" && item.name != item.info.displayName.english) { item.amount -= amount; Item newItem = ItemManager.CreateByItemID(item.info.itemid); newItem.name = item.name; newItem.skin = item.skin; newItem.amount = amount; newItem.MarkDirty(); item.MarkDirty(); return newItem; } return null; } private void OnItemRemovedFromContainer(ItemContainer container, Item item) { Recycler recycler = container.entityOwner as Recycler; if (recycler == null) return; if (itemPlayerTable == null) return; if (itemPlayerTable.ContainsKey(item.uid)) { itemPlayerTable.Remove(item.uid); } } void OnLootEntity(BasePlayer player, BaseEntity entity) { if (entity is Recycler recycler) { // Access the input inventory ItemContainer inputContainer = recycler.inventory.GetSlot(0)?.parent; if (inputContainer != null) { foreach (Item item in inputContainer.itemList) { if (itemPlayerTable != null && !itemPlayerTable.ContainsKey(item.uid)) { itemPlayerTable.Add(item.uid, player.userID); } } } } return; // Stop after finding the first recycler } public class RecipeOutputConverter : JsonConverter { public override bool CanConvert(Type objectType) { return typeof(RecipeOutput).IsAssignableFrom(objectType); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { JObject jo = JObject.Load(reader); string? outputType = jo["Output Type"]?.ToString().ToLower(); RecipeOutput output = outputType switch { "item" => new ItemOutput(), "economics" => new EconomicsOutput(), "blueprint" => new BlueprintOutput(), _ => throw new Exception($"Unknown Output Type: {outputType}") }; serializer.Populate(jo.CreateReader(), output); return output; } public override bool CanWrite { get { return false; } } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { throw new NotImplementedException(); } } } public class OrderedContractResolver : Newtonsoft.Json.Serialization.DefaultContractResolver { protected override System.Collections.Generic.IList CreateProperties(System.Type type, Newtonsoft.Json.MemberSerialization memberSerialization) { var properties = base.CreateProperties(type, memberSerialization); return properties .OrderBy(p => p.Order ?? int.MaxValue) // Respect the Order property, default to int.MaxValue for unordered properties .ThenBy(p => p.PropertyName) // Secondary sort by property name .ToList(); } } }