Resource Loading player skull from texture

Discussion in 'Spigot Plugin Development' started by ItzJustaGame, Jun 12, 2018.

?

Did you find this useful

  1. Yes

    53.3%
  2. A Bit

    26.7%
  3. Not Really

    6.7%
  4. No

    13.3%
  1. Hello everyone here you go:

    Doing this will load the player skull in under 400ms the first time and 2ms when loaded in the hash map, the store time is currently 1 min but you can change it here
    Code (Java):
    Bukkit.getScheduler().runTaskLater(mainInstance, () -> {
                        map.remove(name);
                        System.out.println("Removing from map.");
                    },20*60);

    //Change 20*60, This is currently 60 seconds as 20 ticks( 1 second * 60) so change 60 to 120 for 2 mins

    First of all, I have used outside resources for this and a lot of help from other threads,

    We need to create a hash map to store the users' texture in, as the Mojang API only allows 1 request per account per minute. We also need to create our constructor,

    Put all this code in a class called HeadLoader, or whatever you want. Just not your main class.
    Code (Java):
        private Main mainInstance;
        public HashMap<String, JsonObject> map = new HashMap<>();
     
        public HeadLoader(Main main) {
            this.mainInstance = main;
        }


    Then we need to create our getFromName function, this will take in the player name of the user you want to find and the command sender and return a String[]. We will also need to add a check to see if the user the player is looking for is in the map.
    I have commented all the code for you, so you can understand.
    Code (Java):
    public String[] getFromName(String name, Player p) {
                try {
                    if(!map.containsKey(name)) { //See if the user is in the map
                     URL url_0 = new URL("https://api.mojang.com/users/profiles/minecraft/" + name); //create the url to get the UUID from the mojang api
                     InputStreamReader reader_0 = new InputStreamReader(url_0.openStream()); //Go the the URL
                     try {//Try and get the users uuid
                     String uuid = new JsonParser().parse(reader_0).getAsJsonObject().get("id").getAsString(); //If fouind uuid parse it to a string from the json api
                     URL url_1 = new URL("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid + "?unsigned=false"); //New url for getting the skin texture
                     InputStreamReader reader_1 = new InputStreamReader(url_1.openStream()); // Open url
                    JsonObject textureProperty = new JsonParser().parse(reader_1).getAsJsonObject().get("properties").getAsJsonArray().get(0).getAsJsonObject(); //Parse the json into properties
                    String texture = textureProperty.get("value").getAsString(); //Get The Skin Texture
                    String signature = textureProperty.get("signature").getAsString(); //Get ther skin signature
                    map.put(name, textureProperty); //Put the skin texture into the map for later use
                    System.out.println("Not in map, created new data");
                    Bukkit.getScheduler().runTaskLater(mainInstance, () -> { //1 Min timer to remove user from map
                        map.remove(name);
                        System.out.println("Removing from map.");
                    },20*60);
         
                    return new String[] {texture, signature};
                     }catch(IllegalStateException e) {//Catch if the user doesnt exist
                         p.sendMessage(ChatColor.translateAlternateColorCodes('&', "&cError: &3Player not found in mojang database."));
                     }
           
                    }else { //This runs if the user is in the map
                        System.out.println("In map, using stored data");
                        JsonObject textureProperty = map.get(name);  //Get texture from map
                        String texture = textureProperty.get("value").getAsString(); //Get texture
                        String signature = textureProperty.get("signature").getAsString(); //Get Signature
                        return new String[] {texture, signature};//Return the texture and signature
                    }
                } catch (IOException e) {//Catch errors
                    System.err.println("Could not get skin data from session servers!");
                    e.printStackTrace();
                    return null;
                }
                return null;
           }

    We then need to make the function to apply the texture.

    I have commented all the code for you, so you can understand.
    Code (Java):
    public void createSkull(int x, int y, int z, String name, Player p) {
                Bukkit.getScheduler().runTaskAsynchronously(mainInstance, () -> {
                    String[] a = getFromName(name, p); //Get the texture
                    try {//Try and add the textures to the profile, This will fail if the user doesnt exist as we returned null, thats why we have the return
                    profile.getProperties().put("textures", new Property("textures", a[0], a[1])); // Apply textures
                                byte[] dec = Base64.getDecoder().decode(a[0]);//Decode
                                String s = new String(dec);

                                s = s.substring(s.indexOf("l\":\"") + 1);
                                s = s.substring(0, s.indexOf("\"}}}"));
                                s = s.substring(s.indexOf("\""));
                                s = s.substring(1);
                                s = s.substring(1);
                                s = s.substring(1); //This needs to be cleaned up. Im sharing this, im not spoon feeding :P

                                ItemStack skull = (Skull.getCustomSkull(s)); //Create Custom Skull
                                SkullMeta sm = (SkullMeta) skull.getItemMeta();
                                sm.setDisplayName("Skull of " + name);
                                skull.setItemMeta(sm);
                                p.getInventory().setItemInHand(skull);
                             
                             
                    }catch(NullPointerException e) {//This means the player doesn't exist. No message is sent because one has already been sent.
                        return;
                    }

                });
                }

    Now we will add the command to run this.

    I have commented all the code for you, so you can understand.

    Code (Java):


            public String cmd1 = "head";//Makes it easier in Main class
            @Override
            public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
                if(!command.getName().equalsIgnoreCase(cmd1)) return false; //Not our command, don't use our method.
                if(!(sender instanceof Player)) {
                    sender.sendMessage("Be a player, then you can use this command.");
                    return true;
                }
                try {//Try this, if no player argument is present run catch
                    Player p = (Player) sender;
                    int x = p.getLocation().getBlockX();
                    int y = p.getLocation().getBlockY();
                    int z = p.getLocation().getBlockZ();
                    createSkull(x,y,z, args[0], p);//x,y,z can be removed for this exmaple
                 
                    return true;
                }catch(ArrayIndexOutOfBoundsException e) {
                    sender.sendMessage(ChatColor.translateAlternateColorCodes('&', "&cError: &3Inncorrect usage.\n&aCorrect Usage: &3/head <playername>"));//Tell user correct format
                    return false;
                }
            }


    Put this in the same class as the rest
    Code (Java):
            public static <T> FieldAccessor<T> getField(Class<?> target, String name, Class<T> fieldType) {
                return getField(target, name, fieldType, 0);
            }
         
            public static <T> FieldAccessor<T> getField(Class<?> target, Class<T> fieldType, int index) {
                return getField(target, null, fieldType, index);
            }

            private static <T> FieldAccessor<T> getField(Class<?> target, String name, Class<T> fieldType, int index) {
                for (final Field field : target.getDeclaredFields()) {
                    if ((name == null || field.getName().equals(name)) && fieldType.isAssignableFrom(field.getType()) && index-- <= 0) {
                        field.setAccessible(true);

                        // A function for retrieving a specific field value
                        return new FieldAccessor<T>() {
                            @SuppressWarnings("unchecked")
                            @Override
                            public T get(Object target) {
                                try {
                                    return (T) field.get(target);
                                } catch (IllegalAccessException e) {
                                    throw new RuntimeException("Cannot access reflection.", e);
                                }
                            }

                            @Override
                            public void set(Object target, Object value) {
                                try {
                                    field.set(target, value);
                                } catch (IllegalAccessException e) {
                                    throw new RuntimeException("Cannot access reflection.", e);
                                }
                            }

                            @Override
                            public boolean hasField(Object target) {
                                // target instanceof DeclaringClass
                                return field.getDeclaringClass().isAssignableFrom(target.getClass());
                            }
                        };
                    }
                }

                // Search in parent classes
                if (target.getSuperclass() != null)
                    return getField(target.getSuperclass(), name, fieldType, index);
                throw new IllegalArgumentException("Cannot find field with type " + fieldType);
            }

         
            public interface FieldAccessor<T> {
                /**
                 * Retrieve the content of a field.
                 *
                 * @param target the target object, or NULL for a static field
                 * @return the value of the field
                 */

                public T get(Object target);

                /**
                 * Set the content of a field.
                 *
                 * @param target the target object, or NULL for a static field
                 * @param value  the new value of the field
                 */

                public void set(Object target, Object value);

                /**
                 * Determine if the given object has this field.
                 *
                 * @param target the object to test
                 * @return TRUE if it does, FALSE otherwise
                 */

                public boolean hasField(Object target);
            }
    Put this in a new class called Skull
    Code (Java):


    import com.mojang.authlib.GameProfile;
    import com.mojang.authlib.properties.Property;
    import com.mojang.authlib.properties.PropertyMap;

    import me.dev.commands.HeadLoader;

    import org.apache.commons.codec.binary.Base64;
    import org.bukkit.Material;
    import org.bukkit.inventory.ItemStack;
    import org.bukkit.inventory.meta.ItemMeta;
    import org.bukkit.inventory.meta.SkullMeta;

    import java.util.UUID;

    public enum Skull {

        ARROW_LEFT("MHF_ArrowLeft"),
        ARROW_RIGHT("MHF_ArrowRight"),
        ARROW_UP("MHF_ArrowUp"),
        ARROW_DOWN("MHF_ArrowDown"),
        QUESTION("MHF_Question"),
        EXCLAMATION("MHF_Exclamation"),
        CAMERA("FHG_Cam"),

        ZOMBIE_PIGMAN("MHF_PigZombie"),
        PIG("MHF_Pig"),
        SHEEP("MHF_Sheep"),
        BLAZE("MHF_Blaze"),
        CHICKEN("MHF_Chicken"),
        COW("MHF_Cow"),
        SLIME("MHF_Slime"),
        SPIDER("MHF_Spider"),
        SQUID("MHF_Squid"),
        VILLAGER("MHF_Villager"),
        OCELOT("MHF_Ocelot"),
        HEROBRINE("MHF_Herobrine"),
        LAVA_SLIME("MHF_LavaSlime"),
        MOOSHROOM("MHF_MushroomCow"),
        GOLEM("MHF_Golem"),
        GHAST("MHF_Ghast"),
        ENDERMAN("MHF_Enderman"),
        CAVE_SPIDER("MHF_CaveSpider"),

        CACTUS("MHF_Cactus"),
        CAKE("MHF_Cake"),
        CHEST("MHF_Chest"),
        MELON("MHF_Melon"),
        LOG("MHF_OakLog"),
        PUMPKIN("MHF_Pumpkin"),
        TNT("MHF_TNT"),
        DYNAMITE("MHF_TNT2");

        private static final Base64 base64 = new Base64();
        private String id;

        private Skull(String id) {
            this.id = id;
        }

        /**
         * Return a skull that has a custom texture specified by url.
         *
         * @param url skin url
         * @return itemstack
         */

        public static ItemStack getCustomSkull(String url) {
            GameProfile profile = new GameProfile(UUID.randomUUID(), null);
            PropertyMap propertyMap = profile.getProperties();
            if (propertyMap == null) {
                throw new IllegalStateException("Profile doesn't contain a property map");
            }
            byte[] encodedData = base64.encode(String.format("{textures:{SKIN:{url:\"%s\"}}}", url).getBytes());
            propertyMap.put("textures", new Property("textures", new String(encodedData)));
            ItemStack head = new ItemStack(Material.SKULL_ITEM, 1, (short) 3);
            ItemMeta headMeta = head.getItemMeta();
            Class<?> headMetaClass = headMeta.getClass();
            HeadLoader.getField(headMetaClass, "profile", GameProfile.class).set(headMeta, profile);
            head.setItemMeta(headMeta);
            return head;
        }

        /**
         * Return a skull of a player.
         *
         * @param name player's name
         * @return itemstack
         */

        public static ItemStack getPlayerSkull(String name) {
            ItemStack itemStack = new ItemStack(Material.SKULL_ITEM, 1, (short) 3);
            SkullMeta meta = (SkullMeta) itemStack.getItemMeta();
            meta.setOwner(name);
            itemStack.setItemMeta(meta);
            return itemStack;
        }

        /**
         * Return the skull's id.
         *
         * @return id
         */

        public String getId() {
            return id;
        }

        /**
         * Return the skull of the enum.
         *
         * @return itemstack
         */

        public ItemStack getSkull() {
            ItemStack itemStack = new ItemStack(Material.SKULL_ITEM, 1, (short) 3);
            SkullMeta meta = (SkullMeta) itemStack.getItemMeta();
            meta.setOwner(id);
            itemStack.setItemMeta(meta);
            return itemStack;
        }

    }
     

    Ok. So now what we do is add it to our main class

    Where you declare your variables put
    Code (Java):
    HeadLoader hl = new HeadLoader(this);
    Then in your onEnable function put
    Code (Java):
    mainInstance = this;
    getCommand(hl.cmd1).setExecutor(hl);
    Then add it to your plugin.yml
    Code (Text):
    commands:
      head:
        aliases: []
        description: Test Textures
    And boom you're done!

    This is my first tutorial/Resource so please leave tips, I'm sorry for any messy code too.

    But thanks

    Pce



    This Code has a timer and a few other parts I have left out, please use the tutorial code, but if you're really lazy this will work too :)
     
    #1 ItzJustaGame, Jun 12, 2018
    Last edited: Jun 12, 2018
    • Useful Useful x 1
  2. It would be handy to wrap this up in a lib, something with a getSkull method that takes a callback, maybe?

    Or, better yet... PR something like that to Spigot. Spigot could really use an async API for fetching skulls, it's kind of crazy that there's no way to do it without pausing the entire server.
     
    • Agree Agree x 1
  3. Would be beneficial to construct a Cache for the textures using Guava's cache builder.
    https://google.github.io/guava/releases/19.0/api/docs/com/google/common/cache/CacheBuilder.html

    The cache entries can be expired after write, at a set interval.
    https://google.github.io/guava/releases/19.0/api/docs/com/google/common/cache/CacheBuilder.html#expireAfterWrite(long, java.util.concurrent.TimeUnit)

    If you continue to use your own map implementation, I would suggest transforming over to a TreeMap with the case insensitive order comparator, to avoid duplicate grabs from the web (i.e Charlie & chArLiE).

    Also, for parsing the JSON format, creating a mapper object for it and serializing & deserializing with GSON library would make it much more maintainable and interpretative.
     
    • Useful Useful x 2