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; using Network; #if CARBON using HarmonyLib; #else // Reference: 0Harmony using Harmony; #endif namespace Oxide.Plugins { [Info("CustomItemDefinitions", "0xF [dsc.gg/0xf-plugins]", "1.1.3")] 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); } void OnLootNetworkUpdate(PlayerLoot loot) { RepairBench repairBench = loot.entitySource as RepairBench; if (repairBench == null) return; Item repairableItem = repairBench.inventory?.GetSlot(0); if (repairableItem == null) return; ItemDefinition itemDefinition = repairableItem.info; if (!itemDefinition.Blueprint || !itemDefinition.condition.repairable) return; BasePlayer player = loot.baseEntity; if (IsValidCustomItemDefinition(itemDefinition) && player.blueprints.HasUnlocked(itemDefinition)) SendFakeUnlockedBlueprint(player, repairableItem.info.Parent.itemid); else player.SendNetworkUpdateImmediate(); } #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; 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 { UnityEngine.Debug.LogError("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); bool IsBlueprint = itemDefinition == ItemManager.blueprintBaseDef; if (IsBlueprint && protoItem.instanceData != null) { ItemDefinition blueprintItemDef = ItemManager.FindItemDefinition(protoItem.instanceData.blueprintTarget); if (blueprintItemDef == null) return; itemDefinition = blueprintItemDef; } if (!IsValidCustomItemDefinition(itemDefinition)) return; if (!IsBlueprint) { 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; } else if (protoItem.instanceData != null) { protoItem.instanceData = new ProtoBuf.Item.InstanceData() { ShouldPool = true }; protoItem.instanceData.blueprintTarget = itemDefinition.Parent.itemid; } } } #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)); private static bool IsValidCustomItemDefinition(ItemDefinition itemDefinition) { if (itemDefinition == null) return false; return itemDefinition.HasFlag(CUSTOM_DEFINITION_FLAG) && itemDefinition.Parent != null; } private static void SendSnapshotWithUnlockedBlueprint(BasePlayer player, int itemId) { if (player == null || player.net == null || !player.IsConnected) return; Network.Connection connection = player.net.connection; try { NetWrite netWrite = Network.Net.sv.StartWrite(); global::BaseNetworkable.SaveInfo saveInfo = new global::BaseNetworkable.SaveInfo { forConnection = player.net.connection, forDisk = false }; netWrite.PacketID(Message.Type.Entities); netWrite.UInt32(connection.validate.entityUpdates + 1U); using (saveInfo.msg = Facepunch.Pool.Get()) { player.Save(saveInfo); ProtoBuf.PersistantPlayer persistantData = saveInfo.msg.basePlayer.persistantData; if (persistantData != null && persistantData.unlockedItems != null && !persistantData.unlockedItems.Contains(itemId)) persistantData.unlockedItems.Add(itemId); saveInfo.msg.ToProto(netWrite); } netWrite.Send(new SendInfo(player.net.connection)); } catch (Exception ex) { UnityEngine.Debug.LogException(ex); } finally { connection.validate.entityUpdates++; } } private static void SendFakeUnlockedBlueprint(BasePlayer player, int itemId) { SendSnapshotWithUnlockedBlueprint(player, itemId); player.ClientRPCPlayer(null, player, "UnlockedBlueprint", 0); // [Clientside RPC] Update blueprints } #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 } }