using Oxide.Core.Plugins; using System.Collections.Generic; using System.Reflection; using UnityEngine; using Oxide.Game.Rust.Cui; using System; using System.Reflection.Emit; using Network; using Facepunch.Extend; using ProtoBuf; using SilentOrbit.ProtocolBuffers; #if CARBON using HarmonyLib; #else // Reference: 0Harmony using Harmony; #endif namespace Oxide.Plugins { [Info("CustomItemDefinitions", "0xF [dsc.gg/0xf-plugins]", "1.1.0")] 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 (!HarmonyInstance.HasAnyPatches(HarmonyInstance.Id)) { 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.Entity), "ToProto"), new HarmonyMethod(typeof(Patches), nameof(Patches.Entity_ToProto))); 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(CuiImageComponent), "get_ItemId"), postfix: new HarmonyMethod(typeof(Patches), nameof(Patches.CuiItemIconPatch))); } } void Unload() { HarmonyInstance.UnpatchAll(HarmonyInstance.Id); } void OnPluginUnloaded(Plugin plugin) { if (CustomDefinitions.ContainsKey(plugin)) { UnloadPluginItemDefinitions(plugin); } } 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(); } #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.ItemContainer) || field.FieldType == typeof(ProtoBuf.Item); } } public static class Mutate { 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]; IProto peek = stack.Peek(); if (field.DeclaringType == peek.GetType()) { IProto protoValue = field.GetValue(peek) as IProto; if (protoValue != null) { if (FieldsToMutate.IsValidField(field)) { Mutate.ToClientSide(protoValue); while (i + 1 < FieldsToMutate.validFields.Count && FieldsToMutate.validFields[i + 1]?.DeclaringType != peek.GetType()) { stack.Pop(); peek = stack.Peek(); } } else { stack.Push(protoValue); } } } } } public static void ToClientSide(IProto protoObject) { Type thisProtoType = protoObject.GetType(); if (thisProtoType == typeof(ProtoBuf.ItemContainer)) ToClientSide(protoObject as ProtoBuf.ItemContainer); else if (thisProtoType == typeof(ProtoBuf.Item)) ToClientSide(protoObject as ProtoBuf.Item); } public static void ToClientSide(List protoContainers) { foreach (ProtoBuf.ItemContainer protoContainer in protoContainers) ToClientSide(protoContainer); } public static void ToClientSide(ProtoBuf.ItemContainer protoContainer) { if (protoContainer.contents != null) foreach (ProtoBuf.Item item in protoContainer.contents) 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.itemId == 0 || definition.parentItemId == 0) { PluginInstance.PrintError("Error of incorrect data provided by the plugin \"{0}\": The fields shortname, itemId and parentItemId is required", plugin.Name); return null; } 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) != 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 = null; if ((newItemDefinition = GameObject.Find(definition.itemId.ToString())?.GetComponent()) == 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; 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] = 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); 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 { 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 Entity_ToProto(ProtoBuf.Entity __instance) { Mutate.ToClientSide(__instance); return true; } 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 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 } }