using Oxide.Core.Plugins; using System.Collections.Generic; using System.Reflection; using UnityEngine; using Oxide.Game.Rust.Cui; using System; using SilentOrbit.ProtocolBuffers; using System.Linq; using System.Diagnostics; using System.IO; #if CARBON using HarmonyLib; #else // Reference: 0Harmony using Harmony; #endif namespace Oxide.Plugins { [Info("CustomItemDefinitions", "0xF [dsc.gg/0xf-plugins]", "1.1.2")] public class CustomItemDefinitions : RustPlugin { #region Consts public const ItemDefinition.Flag CUSTOM_DEFINITION_FLAG = (ItemDefinition.Flag)128; public static readonly ItemDefinition FallbackItemDefinition = ItemManager.FindItemDefinition("coal"); #endregion #region CustomItemDefinition Class public class CustomItemDefinition { public int parentItemId; public string shortname; public int? itemId; public string defaultName; public string defaultDescription; public ulong defaultSkinId; public int? maxStackSize; public ItemCategory? category; public ItemDefinition.Flag flags; public ItemMod[] itemMods; } #endregion #region Variables private static Dictionary> CustomDefinitions = new Dictionary>(); private static CustomItemDefinitions PluginInstance; #if CARBON private static HarmonyLib.Harmony HarmonyInstance = new HarmonyLib.Harmony("com.0xf.customitemdefinitions"); #else private static HarmonyInstance HarmonyInstance = HarmonyInstance.Create("com.0xf.customitemdefinitions"); #endif #endregion #region Hooks void Init() { FieldsToMutate.Initialize(); PluginInstance = this; #if CARBON if (!Harmony.HasAnyPatches(HarmonyInstance.Id)) #else if (!HarmonyInstance.HasAnyPatches(HarmonyInstance.Id)) #endif { #if CARBON HarmonyInstance.Patch(AccessTools.Method(typeof(Carbon.Core.ModLoader), "UninitializePlugin"), prefix: new HarmonyMethod(typeof(Patches), nameof(Patches.UnloadPlugin))); #else HarmonyInstance.Patch(AccessTools.Method(typeof(Oxide.Core.OxideMod), "UnloadPlugin"), prefix: new HarmonyMethod(typeof(Patches), nameof(Patches.UnloadPlugin))); #endif HarmonyInstance.Patch(AccessTools.Method(typeof(Item), "Load"), prefix: new HarmonyMethod(typeof(Patches), nameof(Patches.Item_Load1)), postfix: new HarmonyMethod(typeof(Patches), nameof(Patches.Item_Load2))); HarmonyInstance.Patch(AccessTools.Method(typeof(ProtoBuf.PlayerUpdateLoot), "WriteToStream"), new HarmonyMethod(typeof(Patches), nameof(Patches.PlayerUpdateLoot_WriteToStream))); HarmonyInstance.Patch(AccessTools.Method(typeof(ProtoBuf.UpdateItem), "WriteToStream"), new HarmonyMethod(typeof(Patches), nameof(Patches.UpdateItem_WriteToStream))); HarmonyInstance.Patch(AccessTools.Method(typeof(ProtoBuf.UpdateItemContainer), "WriteToStream"), new HarmonyMethod(typeof(Patches), nameof(Patches.UpdateItemContainer_WriteToStream))); HarmonyInstance.Patch(AccessTools.Method(typeof(ProtoBuf.VendingMachine.SellOrderContainer), "WriteToStream"), new HarmonyMethod(typeof(Patches), nameof(Patches.SellOrderContainer_WriteToStream))); HarmonyInstance.Patch(AccessTools.Method(typeof(CuiImageComponent), "get_ItemId"), postfix: new HarmonyMethod(typeof(Patches), nameof(Patches.CuiItemIconPatch))); } } void Unload() { HarmonyInstance.UnpatchAll(HarmonyInstance.Id); } private static void OnItemDefinitionBroken(Item item, ProtoBuf.Item protoItem) { UnityEngine.Debug.LogWarning("Item has broken definition, the fallback item definition will be applied to it."); item.info = FallbackItemDefinition; item.text = protoItem.itemid.ToString(); } void OnEntitySaved(BaseNetworkable entity, BaseNetworkable.SaveInfo saveInfo) { if (!saveInfo.forDisk) Mutate.ToClientSide(saveInfo.msg); } #endregion #region Classes public static class FieldsToMutate { public static List validFields; public static void Initialize() { validFields = CollectFields(typeof(ProtoBuf.Entity)); } private static List CollectFields(Type type) { List result = new List(); foreach (FieldInfo field in type.GetFields(BindingFlags.Public | BindingFlags.Instance)) { if (IsValidField(field)) { result.Add(field); } else if (typeof(IProto).IsAssignableFrom(field.FieldType)) { List collectedFields = CollectFields(field.FieldType); if (collectedFields.Count > 0) { result.Add(field); result.AddRange(CollectFields(field.FieldType)); } } } return result; } public static bool IsValidField(FieldInfo field) { return field.FieldType == typeof(ProtoBuf.Item) || field.FieldType == typeof(List) || field.FieldType == typeof(List); } } public static class Mutate { private static readonly Dictionary> TypeToMethodMap = new Dictionary> { { typeof(ProtoBuf.Item), obj => ToClientSide((ProtoBuf.Item)obj) }, { typeof(ProtoBuf.ItemContainer), obj => ToClientSide((ProtoBuf.ItemContainer)obj) }, { typeof(ProtoBuf.VendingMachine.SellOrder), obj => ToClientSide((ProtoBuf.VendingMachine.SellOrder)obj) }, { typeof(List), obj => ToClientSide((List)obj) }, { typeof(List), obj => ToClientSide((List)obj) }, { typeof(List), obj => ToClientSide((List)obj) }, }; public static void ToClientSide(ProtoBuf.Entity protoEntity) { if (FieldsToMutate.validFields == null) return; Type protoEntityType = protoEntity.GetType(); Stack stack = new Stack(); stack.Push(protoEntity); for (int i = 0; i < FieldsToMutate.validFields.Count; i++) { FieldInfo field = FieldsToMutate.validFields[i]; object peek = stack.Peek(); if (field.DeclaringType == peek.GetType()) { object fieldValue = field.GetValue(peek); if (fieldValue != null) { if (FieldsToMutate.IsValidField(field)) { Mutate.ToClientSide(fieldValue); while (i + 1 < FieldsToMutate.validFields.Count && FieldsToMutate.validFields[i + 1]?.DeclaringType != peek.GetType()) { stack.Pop(); peek = stack.Peek(); } } else { stack.Push(fieldValue); } } } } } public static void ToClientSide(object obj) { if (TypeToMethodMap.TryGetValue(obj.GetType(), out var method)) { method.Invoke(obj); } else { Console.WriteLine("Unknown type to mutate: " + obj.GetType()); } } public static void ToClientSide(ProtoBuf.VendingMachine.SellOrderContainer sellOrderContainer) { if (sellOrderContainer.sellOrders != null) ToClientSide(sellOrderContainer.sellOrders); } public static void ToClientSide(List sellOrders) { foreach (ProtoBuf.VendingMachine.SellOrder order in sellOrders) ToClientSide(order); } public static void ToClientSide(ProtoBuf.VendingMachine.SellOrder sellOrder) { ItemDefinition itemDefToSell = ItemManager.FindItemDefinition(sellOrder.itemToSellID); if (itemDefToSell != null && itemDefToSell.HasFlag(CUSTOM_DEFINITION_FLAG) && itemDefToSell.Parent != null) sellOrder.itemToSellID = itemDefToSell.Parent.itemid; ItemDefinition currencyDef = ItemManager.FindItemDefinition(sellOrder.currencyID); if (currencyDef != null && currencyDef.HasFlag(CUSTOM_DEFINITION_FLAG) && currencyDef.Parent != null) sellOrder.currencyID = currencyDef.Parent.itemid; } public static void ToClientSide(List containers) { foreach (ProtoBuf.ItemContainer container in containers) ToClientSide(container); } public static void ToClientSide(ProtoBuf.ItemContainer container) { if (container.contents != null) ToClientSide(container.contents); } public static void ToClientSide(List items) { foreach (ProtoBuf.Item item in items) ToClientSide(item); } public static void ToClientSide(ProtoBuf.Item protoItem) { ItemDefinition itemDefinition = ItemManager.FindItemDefinition(protoItem.itemid); if (itemDefinition == null) { protoItem.itemid = FallbackItemDefinition.itemid; return; } if (protoItem.contents != null) ToClientSide(protoItem.contents); if (!itemDefinition.HasFlag(CUSTOM_DEFINITION_FLAG) || itemDefinition.Parent == null) return; protoItem.itemid = itemDefinition.Parent.itemid; DefaultProperties defaultProperties = itemDefinition.GetComponent(); if (protoItem.name == null && defaultProperties.name != null) protoItem.name = defaultProperties.name; if (protoItem.skinid == 0UL && defaultProperties.skinId != 0UL) protoItem.skinid = defaultProperties.skinId; } } #endregion #region Methods public static HashSet RegisterPluginItemDefinitions(IEnumerable definitions, Plugin plugin) { HashSet hashSet = new HashSet(); foreach (var definition in definitions) { ItemDefinition resultItemDefinition = RegisterPluginItemDefinition(definition, plugin); if (resultItemDefinition != null) hashSet.Add(resultItemDefinition); }; return hashSet; } public static ItemDefinition RegisterPluginItemDefinition(CustomItemDefinition definition, Plugin plugin) { HashSet pluginCustomDefinitions = null; if (!CustomDefinitions.TryGetValue(plugin, out pluginCustomDefinitions)) pluginCustomDefinitions = (CustomDefinitions[plugin] = new HashSet()); if (pluginCustomDefinitions.Contains(definition)) { PluginInstance.PrintError("Error by the plugin \"{0}\": The provided CustomItemDefinition is already contained", plugin.Name); return null; } if (string.IsNullOrEmpty(definition.shortname) || definition.parentItemId == 0) { PluginInstance.PrintError("Error of incorrect data provided by the plugin \"{0}\": The fields shortname, parentItemId is required", plugin.Name); return null; } if (!definition.itemId.HasValue) definition.itemId = definition.shortname.GetHashCode(); else if (ItemManager.FindItemDefinition(definition.shortname) != null) { PluginInstance.PrintError("Error of incorrect data provided by the plugin \"{0}\": Shortname must be unique! The provided shortname - \"{1}\"", plugin.Name, definition.shortname); return null; } else if (ItemManager.FindItemDefinition(definition.itemId.Value) != null) { PluginInstance.PrintError("Error of incorrect data provided by the plugin \"{0}\": ItemId must be unique! The provided itemId - \"{1}\"", plugin.Name, definition.itemId); return null; } ItemDefinition parentItemDefinition = ItemManager.FindItemDefinition(definition.parentItemId); if (parentItemDefinition == null) { PluginInstance.PrintError("Error of incorrect data provided by the plugin \"{0}\": ItemDefinition by parentItemId not found! The provided parentItemId - \"{1}\"", plugin.Name, definition.parentItemId); return null; } if (parentItemDefinition.gameObject.scene.isLoaded) { PluginInstance.PrintError("Error by the plugin \"{0}\": You cannot use a custom ItemDefinition as a parent!", plugin.Name); return null; } ItemDefinition newItemDefinition = GameObject.Find(definition.itemId.ToString())?.GetComponent(); if (newItemDefinition == null) { newItemDefinition = CloneItemDefinition(parentItemDefinition); newItemDefinition.name = definition.itemId.ToString(); } if (newItemDefinition == null) { PluginInstance.PrintError("Error by the plugin \"{0}\": Failure to create or search for a new item definition ({1})!", plugin.Name, definition.shortname); return null; } newItemDefinition.shortname = definition.shortname; newItemDefinition.itemid = definition.itemId.Value; if (definition.defaultName != null) newItemDefinition.displayName = definition.defaultName; if (definition.defaultDescription != null) newItemDefinition.displayDescription = definition.defaultDescription; newItemDefinition.Parent = parentItemDefinition; newItemDefinition.flags = CUSTOM_DEFINITION_FLAG; newItemDefinition.flags |= definition.flags; if (definition.category.HasValue) newItemDefinition.category = definition.category.Value; if (definition.maxStackSize.HasValue) newItemDefinition.stackable = definition.maxStackSize.Value; newItemDefinition.itemMods = null; foreach (ItemMod mod in newItemDefinition.GetComponentsInChildren(true)) UnityEngine.Object.DestroyImmediate(mod); DefaultProperties defaultProperties = newItemDefinition.gameObject.AddComponent(); defaultProperties.name = definition.defaultName; defaultProperties.skinId = definition.defaultSkinId; if (definition.itemMods != null) { foreach (ItemMod mod in definition.itemMods) { Component addedMod = newItemDefinition.gameObject.AddComponent(mod.GetType()); foreach (var field in mod.GetType().GetFields((BindingFlags)(-1))) field.SetValue(addedMod, field.GetValue(mod)); } } newItemDefinition.Initialize(ItemManager.itemList); ItemManager.itemDictionary[definition.itemId.Value] = newItemDefinition; ItemManager.itemDictionaryByName[definition.shortname] = newItemDefinition; if (!ItemManager.itemList.Contains(newItemDefinition)) ItemManager.itemList.Add(newItemDefinition); CustomDefinitions[plugin].Add(definition); return newItemDefinition; } public static void UnloadPluginItemDefinitions(Plugin plugin) { if (CustomDefinitions.TryGetValue(plugin, out HashSet definitions)) foreach (var definition in definitions) UnloadCustomItemDefinition(definition); } private static void UnloadCustomItemDefinition(CustomItemDefinition definition) { if (ItemManager.itemDictionaryByName.TryGetValue(definition.shortname, out ItemDefinition itemDefinition)) { itemDefinition.itemMods = null; foreach (ItemMod mod in itemDefinition.GetComponentsInChildren(true)) UnityEngine.Object.DestroyImmediate(mod); itemDefinition.Initialize(ItemManager.itemList); ItemManager.itemDictionary.Remove(definition.itemId.Value); ItemManager.itemDictionaryByName.Remove(definition.shortname); ItemManager.itemList.Remove(itemDefinition); } } private static ItemDefinition CloneItemDefinition(ItemDefinition itemDefinition) { if (itemDefinition == null) return null; GameObject clone = UnityEngine.Object.Instantiate(itemDefinition.gameObject); UnityEngine.Object.DontDestroyOnLoad(clone); return clone.GetComponent(); } private static ItemDefinition CloneItemDefinition(string shortname) => CloneItemDefinition(ItemManager.FindItemDefinition(shortname)); private static ItemDefinition CloneItemDefinition(int itemId) => CloneItemDefinition(ItemManager.FindItemDefinition(itemId)); #endregion #region Default Mods public class DefaultProperties : ItemMod { public string name; public ulong skinId; } #endregion #region Harmony Patches internal static class Patches { #if CARBON internal static void UnloadPlugin(RustPlugin plugin) { if (CustomDefinitions.ContainsKey(plugin)) UnloadPluginItemDefinitions(plugin); } #else internal static void UnloadPlugin(string name) { Plugin plugin = Oxide.Core.Interface.Oxide.RootPluginManager.GetPlugin(name); if (plugin == null || (plugin.IsCorePlugin && !Oxide.Core.Interface.Oxide.IsShuttingDown)) return; if (CustomDefinitions.ContainsKey(plugin)) UnloadPluginItemDefinitions(plugin); } #endif internal static void Item_Load1(Item __instance, ProtoBuf.Item __0) { if (__0.itemid == FallbackItemDefinition.itemid && __0.text != null && int.TryParse(__0.text, out int restoreItemId) && ItemManager.itemDictionary.ContainsKey(restoreItemId)) __0.itemid = restoreItemId; } internal static void Item_Load2(Item __instance, ProtoBuf.Item __0) { if (__instance.info == null) OnItemDefinitionBroken(__instance, __0); } internal static bool PlayerUpdateLoot_WriteToStream(ProtoBuf.PlayerUpdateLoot __instance) { if (__instance.containers != null) Mutate.ToClientSide(__instance.containers); return true; } internal static bool UpdateItem_WriteToStream(ProtoBuf.UpdateItem __instance) { if (__instance.item != null) Mutate.ToClientSide(__instance.item); return true; } internal static bool UpdateItemContainer_WriteToStream(ProtoBuf.UpdateItemContainer __instance) { if (__instance.container != null) Mutate.ToClientSide(__instance.container); return true; } internal static bool SellOrderContainer_WriteToStream(ref ProtoBuf.VendingMachine.SellOrderContainer __instance) { if (__instance.sellOrders != null) { __instance = __instance.Copy(); Mutate.ToClientSide(__instance.sellOrders); } return true; } internal static void CuiItemIconPatch(CuiImageComponent __instance, ref int __result) { ItemDefinition itemDef = ItemManager.FindItemDefinition(__result); if (itemDef == null) return; if (itemDef.HasFlag(CUSTOM_DEFINITION_FLAG)) { __result = itemDef.Parent.itemid; if (__instance.SkinId == 0) __instance.SkinId = itemDef.GetComponent().skinId; } } } #endregion } }