Resource Modifying the type of a Spawn Egg

Discussion in 'Spigot Plugin Development' started by Serializator, May 22, 2016.

  1. Serializator

    Supporter

    Mojang decided to store the type of a spawn egg from the durability of the item to the NBT tag, and this makes it for us a little bit more complicated to set the type of a spawn egg as long as Spigot doesn't have a metadata for it, and that's why I'm writing this tutorial, to make it as simple as possible for you.

    Every ItemStack holds a NBT tag that stores data associated with the ItemStack in kind of a key-value store, we will have to modify this key-value store to set the type of a spawn egg. To do this we will obviously have to create an instance of ItemStack that we can modify.

    Code (Text):
    ItemStack egg = new ItemStack(Material.MONSTER_EGG);
    Now that we have that we can start modifying the NBT tag from the ItemStack, but this NBT tag isn't accessible through the standard Bukkit API, we will have to touch some NMS. To do this we need to get us a NMS copy of the ItemStack, and obviously the method to get that copy is `CraftItemStack#asNMSCopy()`.

    Code (Text):
    Object nmsStack = Class.forName("org.bukkit.craftbukkit." + version + ".inventory.CraftItemStack").getMethod("asNMSCopy", ItemStack.class).invoke(null, stack);
    I know what you think right now, where does the variable named `version` come from? As you see we use reflection to access the necessary methods, this is to make this NBT modification "version independent". Some people argue whether or not this is really the way to go or that we should use other things like NMS to accomplish this, but to keep it as simple as possible I will use reflection for this tutorial. The `version` variable is set to:

    Code (Text):
    Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3];
    Now that that's out of the way we have to get the NBT tag from the NMS copy we previously made from the ItemStack, we do this by invoking `ItemStack#getTag`, and don't forget, the ItemStack I'm referring to in this case is in the "net.minecraft.server" package, also known as the NMS package.

    Code (Text):
    Object nmsCompound = nmsStack.getClass().getMethod("getTag").invoke(nmsStack);
    The tag of an ItemStack can be null, so we will have to do a null check to make sure we have an instance of `NBTTagCompound` and not a variable with a null reference.

    Code (Text):
    if(nmsCompound == null) {
        nmsCompound = Class.forName("net.minecraft.server." + version + ".NBTTagCompound").getConstructor().newInstance();
    }
    Now that we made sure that we have an instance of `NBTTagCompound`, we can start modifying it. The key we're looking for is 'id', but this key is embedded in another `NBTTagCompound` stored in the NBT tag from the ItemStack under the key 'EntityTag', so we first have to get the instance of `NBTTagCompound` stored under 'EntityTag', after that we can modify the 'id' tag to actually set the type of the spawn egg.Z

    I previously said that we have to get the instance of `NBTTagCompound` stored under the 'EntityTag' key, but this key doesn't exist by default, so we have to create the new instance ourselves and associate it with the 'EntityTag' key after we've set the 'id' tag in it.

    Code (Text):
    Object nmsTag = nmsCompound.getClass().getConstructor().newInstance();
    Now that we have the instance of `NBTTagCompound` that will be associated with the 'EntityTag' key, we can set the 'id' key, to do this we will have to do the following.

    Code (Text):
    nmsTag.getClass().getMethod("setString", String.class, String.class).invoke(nmsTag, "id", type.getName());
    You probably saw that I used `EntityType#getName()` in that piece of code, the `type` variable is equal to the `EntityType` enum value you want the type of the spawn egg to be.

    Now that we've set the 'id' key we can set the 'EntityTag' key and finish this tutorial. To set the 'EntityTag' key we will have to do the same thing as before but with a little bit of modifications.

    Code (Text):
    nmsCompound.getClass().getMethod("set", String.class, Class.forName("net.minecraft.server." + version + ".NBTBase")).invoke(nmsCompound, "EntityTag", nmsTag);
    Finally, that's also done, the only thing left to do is set the NBT tag from the ItemStack to the instance of `NBTCompoundTag` that we've just created and get the Bukkit instance of ItemStack from the NMS instance we're using now.

    To set the tag of the ItemStack we use the `ItemStack#setTag()` method.

    Code (Text):
    nmsStack.getClass().getMethod("setTag", nmsCompound.getClass()).invoke(nmsStack, nmsCompound);
    And now, the grand finally, we're going to get our new instance of the Bukkit ItemStack instead of the NMS we're using. But you're probably asking yourself, how do I do that? It's as simple as a single line of code.

    Code (Text):
    ((ItemStack) Class.forName("org.bukkit.craftbukkit." + version + ".inventory.CraftItemStack").getMethod("asBukkitCopy", nmsStack.getClass()).invoke(null, nmsStack));
    And for the copy paste people who don't want to learn anything, here's the complete code. Just know that I don't like you and that I will find you and will teach you!

    Code (Text):
    String version = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3];

    try {
        Object nmsStack = Class.forName("org.bukkit.craftbukkit." + version + ".inventory.CraftItemStack").getMethod("asNMSCopy", ItemStack.class).invoke(null, stack);
        Object nmsCompound = nmsStack.getClass().getMethod("getTag").invoke(nmsStack);

        if(nmsCompound == null) {
            nmsCompound = Class.forName("net.minecraft.server." + version + ".NBTTagCompound").getConstructor().newInstance();
        }

        Object nmsTag = nmsCompound.getClass().getConstructor().newInstance();

        nmsTag.getClass().getMethod("setString", String.class, String.class).invoke(nmsTag, "id", type.getName());
        nmsCompound.getClass().getMethod("set", String.class, Class.forName("net.minecraft.server." + version + ".NBTBase")).invoke(nmsCompound, "EntityTag", nmsTag);
        nmsStack.getClass().getMethod("setTag", nmsCompound.getClass()).invoke(nmsStack, nmsCompound);

        ItemStack result = ((ItemStack) Class.forName("org.bukkit.craftbukkit." + version + ".inventory.CraftItemStack").getMethod("asBukkitCopy", nmsStack.getClass()).invoke(null, nmsStack));
    } catch(InstantiationException | InvocationTargetException | ClassNotFoundException | IllegalAccessException | NoSuchMethodException exception) {
        exception.printStackTrace();
    }
    Hopefully this will provide some help to people and will make their life a little bit easier.
     
    • Useful Useful x 4
    • Like Like x 1
    • Informative Informative x 1
  2. Choco

    Moderator

    Because Mojang just loves to make things harder for developers, don't they :p
     
    • Funny Funny x 2
  3. Spawn eggs can actually hold actual entities now. Not just a type of it.
     
    • Agree Agree x 1
    • Informative Informative x 1
  4. Serializator

    Supporter

    I know that, but don't say they can hold actual entities, because they can't.
    They can hold attributes that are automatically applied to the entity that's spawned by the spawn egg.
     
    • Agree Agree x 1
    • Informative Informative x 1
  5. In the contrary, actually. Lots and lots of stuff keeps getting added and added. IDs used to be useful, but there just gets less and less values available. By moving over to names, you won't have to worry about IDs suddenly changing. This is the same for Block/Item IDs
     
    • Agree Agree x 1
    • Informative Informative x 1
  6. Serializator

    Supporter

    I agree with you, and I like the new NBT system more than the id system used previously, it's just that their is no API implementation for it yet, and that's what makes it annoying.

    Sent from my SM-A510F using Tapatalk
     
    • Agree Agree x 2
    • Like Like x 1
  7. Could you make a method which implements it without reflection? For people using version abstraction.
     
  8. I'll take on this task as soon as I get out of bed :p

    Code (Java):
    public org.bukkit.inventory.ItemStack changeSpawnEgg(org.bukkit.inventory.ItemStack egg, EntityType type)
    {
        Objects.requireNonNull(egg);
        Objects.requireNonNull(type);
        ItemStack nms = CraftItemStack.asNMSCopy(egg);
        NBTTagCompound tag = nms.hasTag() ? nms.getTag() : new NBTTagCompound();
        NBTTagCompound entityTag = tag.hasKeyOfType("EntityTag", tag.getTypeId()) ? tag.getCompound("EntityTag") : new NBTTagCompound();
        entityTag.setString("id", type.getName()); // Ignore the fact that getName() is deprecated. We need it.
        tag.set("EntityTag", entityTag);
        nms.setTag(tag);
        return CraftItemStack.asCraftMirror(nms); // Or #asBukkitCopy - Your decision
    }
    The imported ItemStack is the NMS version, by the way.
     
    #8 Proximyst, Dec 4, 2016
    Last edited: Dec 4, 2016
    • Like Like x 1
    • Useful Useful x 1
  9. Alright, thank you very much.
    Version abstraction is beautiful:
    Code (Java):
    import org.bukkit.Material;
    import org.bukkit.craftbukkit.v1_11_R1.inventory.CraftItemStack;
    import org.bukkit.entity.EntityType;

    import net.minecraft.server.v1_11_R1.NBTTagCompound;
    import net.minecraft.server.v1_11_R1.ItemStack;

    public class V1_11_R1 extends Version
    {

        public V1_11_R1(String version)
        {
            super(version);
        }

        @SuppressWarnings("deprecation")
        @Override
        public org.bukkit.inventory.ItemStack applyEntityToEgg(org.bukkit.inventory.ItemStack stack, EntityType type)
        {
            if (type == null || stack == null || stack.getType() != Material.MONSTER_EGG)
                return stack;
            ItemStack nms = CraftItemStack.asNMSCopy(stack);
            NBTTagCompound tag = nms.hasTag() ? nms.getTag() : new NBTTagCompound();
            NBTTagCompound entityTag = tag.hasKeyOfType("EntityTag", tag.getTypeId()) ? tag.getCompound("EntityTag")
                    : new NBTTagCompound();
            entityTag.setString("id", type.getName());
            tag.set("EntityTag", entityTag);
            nms.setTag(tag);
            return CraftItemStack.asCraftMirror(nms);
        }

    }
     
    #9 FiXed, Dec 4, 2016
    Last edited: Dec 4, 2016
  10. Sweet! It's my turn to contribute to this awesome community! At least for Craftbukkit / Spigot 1.11 you don't have to use NMS.

    You can use this code to CREATE a spawn egg of any monster type. The second function can be used to detect the EntityType of any spawn egg.


    Code (Text):
    import org.bukkit.Material;
    import org.bukkit.entity.EntityType;
    import org.bukkit.inventory.ItemStack;
    import org.bukkit.inventory.meta.ItemMeta;
    import org.bukkit.inventory.meta.SpawnEggMeta;

    public class SpawnEgg {
        public static ItemStack Create(EntityType type, int amount) {
            ItemStack item = new ItemStack(Material.MONSTER_EGG, amount);
            ItemMeta meta = item.getItemMeta();
            ((SpawnEggMeta) meta).setSpawnedType(type);
            item.setItemMeta(meta);
            return item;
        }

        public static EntityType GetType(ItemStack item) {
            EntityType result = null;
            ItemMeta meta = item.getItemMeta();
            if (meta instanceof SpawnEggMeta) {
                result = ((SpawnEggMeta) meta).getSpawnedType();
            }
            return result;
        }
    }
     
     
    #10 NoobDad, Feb 28, 2017
    Last edited: Feb 28, 2017
    • Useful Useful x 1
  11. Necroposting intensifies.
     
  12. This was the first post I found while searching for this. The code I provided is a great solution because it utilizes Spigot's API. Hopefully it will help other developers as well.
     
  13. Your contribution was great, but at the time this was posted (May 22, 2016) this change was only recent, and the SpawnEggMeta didn't exist yet. Since the API is now here, people can use it and if they have an earlier server version without this API they will use this resource. This is another reason why one shouldn't necropost.
     
    • Agree Agree x 1
  14. Serializator

    Supporter

    Even though I like attention and people commenting on something I posted @NoobDad (attention whoring intensifies), I have to agree with @FlyingLlama and @megamichiel, don't necropost, it's really annoying.