Solved Reflection problems with nms field maxStackSize

Discussion in 'Spigot Plugin Development' started by jetp250, Oct 10, 2016.

  1. Okay uh, on the project I'm working on, I need to be able to set any items max stack size to 1. Thus far I've done it with enchantments, but it's just not a good way to go. So instead, I'm going with nms code, setting the items maxStackSize to 1. However, as you know, that requires nms imports, thus making the plugin version-dependant. I just won't allow that to happen :L
    So, what I have thus far (mostly experiencing / playing around):
    Code (Java):
    public String setSize(ItemStack stack) {
            Material material = stack.getType();
            try {
            Class<?> cbclass = getNMSClass("org.bukkit.craftbukkit", "util.CraftMagicNumbers");
            Method m = cbclass.getDeclaredMethod("getItem", Material.class);
            Class<?> result = m.invoke(stack, material).getClass();
            Class<?> realclass = result;
            Bukkit.broadcastMessage(result.getCanonicalName());
            Bukkit.broadcastMessage(result.getCanonicalName().split("\\.").toString() + ", " + result.getCanonicalName().split("\\.").length);
            if (!result.getCanonicalName().split("\\.")[result.getCanonicalName().split("\\.").length-1].toLowerCase().equalsIgnoreCase("item")) {
                Bukkit.broadcastMessage(result.getCanonicalName());
                while(!result.getCanonicalName().split("\\.")[result.getCanonicalName().split("\\.").length-1].toLowerCase().equalsIgnoreCase("item")) {
                    result = result.getSuperclass();
                }
            }
            Field f = result.getDeclaredField("maxStackSize");
            f.setAccessible(true);
            f.set(1, result); //Error on this line
            return result.getCanonicalName();
            } catch (Exception e) {e.printStackTrace();}
            return "";
        }
    It gets all the way to the f.set(1, result) and throws an IllegalArgumentException:
    Code (Text):
    [16:55:27] [Server thread/WARN]: java.lang.IllegalArgumentException: Can not set int field net.minecraft.server.v1_9_R2.Item.maxStackSize to java.lang.Integer
    [16:55:27] [Server thread/WARN]:     at sun.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(Unknown Source)
    [16:55:27] [Server thread/WARN]:     at sun.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(Unknown Source)
    [16:55:27] [Server thread/WARN]:     at sun.reflect.UnsafeFieldAccessorImpl.ensureObj(Unknown Source)
    [16:55:27] [Server thread/WARN]:     at sun.reflect.UnsafeIntegerFieldAccessorImpl.set(Unknown Source)
    [16:55:27] [Server thread/WARN]:     at java.lang.reflect.Field.set(Unknown Source)
    [16:55:27] [Server thread/WARN]:     at me.jetp250.evillagers.VillagerEnchanters.setSize(VillagerEnchanters.java:122)
    [16:55:27] [Server thread/WARN]:     at me.jetp250.evillagers.VillagerEnchanters.onInteract(VillagerEnchanters.java:130)
    [16:55:27] [Server thread/WARN]:     at com.destroystokyo.paper.event.executor.asm.generated.GeneratedEventExecutor1324.execute(Unknown Source)
    [16:55:27] [Server thread/WARN]:     at org.bukkit.plugin.EventExecutor$1.execute(EventExecutor.java:44)
    [16:55:27] [Server thread/WARN]:     at co.aikar.timings.TimedEventExecutor.execute(TimedEventExecutor.java:78)
    [16:55:27] [Server thread/WARN]:     at org.bukkit.plugin.RegisteredListener.callEvent(RegisteredListener.java:62)
    [16:55:27] [Server thread/WARN]:     at org.bukkit.plugin.SimplePluginManager.fireEvent(SimplePluginManager.java:517)
    [16:55:27] [Server thread/WARN]:     at org.bukkit.plugin.SimplePluginManager.callEvent(SimplePluginManager.java:502)
    [16:55:27] [Server thread/WARN]:     at org.bukkit.craftbukkit.v1_9_R2.event.CraftEventFactory.callPlayerInteractEvent(CraftEventFactory.java:231)
    [16:55:27] [Server thread/WARN]:     at org.bukkit.craftbukkit.v1_9_R2.event.CraftEventFactory.callPlayerInteractEvent(CraftEventFactory.java:198)
    [16:55:27] [Server thread/WARN]:     at org.bukkit.craftbukkit.v1_9_R2.event.CraftEventFactory.callPlayerInteractEvent(CraftEventFactory.java:194)
    [16:55:27] [Server thread/WARN]:     at net.minecraft.server.v1_9_R2.PlayerConnection.a(PlayerConnection.java:946)
    [16:55:27] [Server thread/WARN]:     at net.minecraft.server.v1_9_R2.PacketPlayInBlockPlace.a(PacketPlayInBlockPlace.java:27)
    [16:55:27] [Server thread/WARN]:     at net.minecraft.server.v1_9_R2.PacketPlayInBlockPlace.a(PacketPlayInBlockPlace.java:5)
    [16:55:27] [Server thread/WARN]:     at net.minecraft.server.v1_9_R2.PlayerConnectionUtils$1.run(SourceFile:13)
    [16:55:27] [Server thread/WARN]:     at java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
    [16:55:27] [Server thread/WARN]:     at java.util.concurrent.FutureTask.run(Unknown Source)
    [16:55:27] [Server thread/WARN]:     at net.minecraft.server.v1_9_R2.SystemUtils.a(SourceFile:45)
    [16:55:27] [Server thread/WARN]:     at net.minecraft.server.v1_9_R2.MinecraftServer.D(MinecraftServer.java:786)
    [16:55:27] [Server thread/WARN]:     at net.minecraft.server.v1_9_R2.DedicatedServer.D(DedicatedServer.java:403)
    [16:55:27] [Server thread/WARN]:     at net.minecraft.server.v1_9_R2.MinecraftServer.C(MinecraftServer.java:723)
    [16:55:27] [Server thread/WARN]:     at net.minecraft.server.v1_9_R2.MinecraftServer.run(MinecraftServer.java:622)
    [16:55:27] [Server thread/WARN]:     at java.lang.Thread.run(Unknown Source)
    First of all, I thought it'd be
    Code (Java):
    f.set(result, 1);
    but that just threw another IllegalArgumentException, that time saying 'Can not set int field net.minecraft.server.v1_9_R2.Item.maxStackSize to java.lang.Class'
    So uh.. But yeah, it works perfectly until that point. And yes, the 'realclass' isn't used, I tried to change the 'result' to it without any luck.

    Any ideas?
     
  2. Jokes on you, reflection is still somewhat version dependent due to obfuscation!

    The documentation clearly specifies that the first argument must be the object you're setting the field of (which can be null for static fields), and the second parameter is for the value. Moreover, there is an overload for primitives (Field#setInt(Object, int) f.e) which should both be "more correct" (i.e. not allowing null values), and potentially faster(?)

    Your main problem at the moment - while it might seem a bit obscure from the error message - is that you're trying to set a Field that belongs to Item, using an object that isn't an Item (first parameter).
     
  3. The int is boxed into an object (java.lang.Integer) which is then tried to set to an int field, what obviously does not work. So use setInt() to avoid autoboxing.
     
  4. That was a fast reply!

    Anyhow, the 'result' class canonical path returns 'net.minecraft.server.v1_9_R2.Item' just like it should (1_9_R2 in my case) So isn't it an item?
    That's the main reason I tried to use the 'realclass' class instead.

    So, I believe I need the instance of the item, not the class? Wouldn't class.newInstance() create a completely new instance, not related to the item I have? I'm seriously terrible with the reflection.
     
  5. Oh uh? Looking at the source, it's an int. Java boxes it into an Integer? That's new to me.
    And what 'setInt' method were you talking about? .-. can't I, in this case, just ... Ooooh wait
    What a fool I am

    setInt() instead of set()..
    Alright, I'll go experiment a bit, thanks .-.

    EDIT: ... Okay. Got rid of the error. But, absolutely nothing happened. I believe it's because of the .newInstance() I had to use.

    Code (Java):
    public String setSize(ItemStack stack) {
            Material material = stack.getType();
            try {
            Class<?> cbclass = getNMSClass("org.bukkit.craftbukkit", "util.CraftMagicNumbers");
            Method m = cbclass.getDeclaredMethod("getItem", Material.class);
            Class<?> result = m.invoke(stack, material).getClass();
            Class<?> realclass = result;
            Bukkit.broadcastMessage(result.getCanonicalName());
            Bukkit.broadcastMessage(result.getCanonicalName().split("\\.").toString() + ", " + result.getCanonicalName().split("\\.").length);
            if (!result.getCanonicalName().split("\\.")[result.getCanonicalName().split("\\.").length-1].toLowerCase().equalsIgnoreCase("item")) {
                Bukkit.broadcastMessage(result.getCanonicalName());
                while(!result.getCanonicalName().split("\\.")[result.getCanonicalName().split("\\.").length-1].toLowerCase().equalsIgnoreCase("item")) {
                    result = result.getSuperclass();
                }
            }
            Field f = result.getDeclaredField("maxStackSize");
            f.setAccessible(true);
            f.setInt(result.newInstance(), 1);
            return result.getCanonicalName();
            } catch (Exception e) {e.printStackTrace();}
            return "";
        }
     
  6. Assuming it's not a static field, call it with the Item instance you want to change the max stack size for. Wasn't there an Items class with all Item instances as constants?
    Java can unbox it fine (with reflection at least)
     
  7. hm. How would I go about getting the nms instance with reflection? Getting the class was easy, but instance isn't, atleast for me.
     
    1. Get the Items class
    2. Get the right Field object
    3. Field#get(null) should get you your instance
     
    • Like Like x 1
  8. Ahh right, but doesn't that change the field of that item type in general..? Confusion.
    Well, I'll play around with it.

    EDIT: That did the trick! Thank you! I'm not certainly sure how that works though? I would've assumed
    #get(null) returns the instance of that type in general, not from the exact ItemStack I'm changing?

    Anyway, that works with items now. How would I go about all those extra stuff, i.e ItemBlock, and the ones that extend it? Should I use some kind of trickery, and check if a
    net.minecraft.server.version.Item<Item> exists, and if not, assume it's an item?

    Well, I guess I can solve that somehow.

    For anyone interested, here's the final code:
    Code (Java):
    public void setSize(ItemStack stack) {
            Material material = stack.getType();
            try {
            Class<?> cbclass = getNMSClass("org.bukkit.craftbukkit", "util.CraftMagicNumbers");
            Method m = cbclass.getDeclaredMethod("getItem", Material.class);
            Class<?> result = m.invoke(stack, material).getClass();
            if (!result.getCanonicalName().split("\\.")[result.getCanonicalName().split("\\.").length-1].toLowerCase().equalsIgnoreCase("item")) {
                while(!result.getCanonicalName().split("\\.")[result.getCanonicalName().split("\\.").length-1].toLowerCase().equalsIgnoreCase("item")) {
                    result = result.getSuperclass();
                }
            }
            Object myItem = getNMSClass("net.minecraft.server", "Items").getDeclaredField(material.name()).get(null);
            Field f = result.getDeclaredField("maxStackSize");
            f.setAccessible(true);
            f.setInt(myItem, 1);
            } catch (Exception e) {e.printStackTrace();}
        }
    and getNMSClass:
    Code (Java):
        public Class<?> getNMSClass(String path, String name) {
            String version = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3];
            try {
                return Class.forName(path + "." + version + "." + name);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
                return null;
            }
        }
    Marking as solved.

    EDIT 2: Oh well, now that I look at it, it does have a connection to my ItemStacks material field. I guess that's enough.
     
    #9 jetp250, Oct 10, 2016
    Last edited: Oct 10, 2016
  9. all Item fields in Items are static, meaning they don't belong to an instance (hence null works). It shouldn't matter what subtype of Item it is, as the max stack size is always stored in the Item base class.
     
  10. Ah okay, thanks!
    And for others, remember to use Player#updateInventory() so it isn't as glitchy ;)