1.16.5 Dynamic Player Head Textures

Discussion in 'Spigot Plugin Development' started by repeater64, Jan 8, 2021.

  1. Hi all,

    I'm wondering if there is a way to dynamically create a texture for a player head item.

    The main question is: Do PLAYER_HEAD itemstacks store the actual texture data, or do they just store a URL to mojang's skin servers?

    If it's the former, does anyone have any idea how I would go about setting the texture of a player head item? I'm guessing it would need reflection as the API doesn't have anything for it.

    If it's the latter, this pretty much ruins my plans. However, I would only need to generate a dynamic texture quite infrequently, so maybe it's a possibility that I could automatically submit a skin to mojang's servers and then quickly save the URL.... I'm aware that wiki.vg has details of the protocol used for communicating with mojang servers, but before I dig into it I feel like it's worth asking if this is even plausable. Also, I'd probably need a Minecraft account that nobody uses, because afaik the only way to get a skin onto mojang's skin servers is to have it be the skin of an actual player, even if it's only for a few seconds.

    Any advice?

    If I haven't explained myself clearly enough, apologies - give me a shout and I'll clarify.

    EDIT: You know how Bukkit has a feature for serialising (and deserialising) ItemStacks into YAML? Well, I tried serializing a custom player head itemstack into YAML, and this is what I got:


    Code (YAML):
    pigHead:
      ==
    : org.bukkit.inventory.ItemStack
      v
    : 2584
      type
    : PLAYER_HEAD
      meta
    :
        ==
    : ItemMeta
        meta-type
    : SKULL
        display-name
    : This is a Pig's Head
        internal
    : H4sIAAAAAAAAAE2Pu07DMBhGfxBIxfAUPEHSKkEdGBBI1FHt0Na52FubusrlT1ulCY2zMTHyLEhM7Oy8DSthY/uOzlk+AkDgalE0iE/VbpOhJkD6tddVnenDBQxq3dZNpQ8EAE4GcB4usdHwoY1nqTi11rGHiaFuz2JhoU/z/Q3dhmZ1T11a9n5y507N+F/r1MvIQTnyUrWdNasytKajOerJ3E7K4JlHHNWDNFwERg6ZzcTsKCPWsk52vpC2ipgtRYoqL468Y8aPVCpLjlwkf43Fcp7xYdDKLmj5I3V4jpk03ngTW7cAl3BK1/2Rs5/27fMLvfSl2L1+X5N3gF+NAu2nCgEAAA==
    Clearly the "internal" section within the meta is where the head data is stored. But, question is, does this represent a URL, or the actual pixels?
     
    #1 repeater64, Jan 8, 2021
    Last edited: Jan 8, 2021
  2. They do not store any URLs, they store the skin texture in Base-64 encoded string. Not sure which character in the string belongs to which pixel on the skin, tho.

    EDIT: I was wrong, check my post below for more information.
     
    #2 davidcubesvk, Jan 8, 2021
    Last edited: Jan 17, 2021
    • Like Like x 1
  3. I did have a suspicion it was this! Well... now I need to somehow work out how to get from a texture to the base64 string.

    Do you, or does anyone, have any idea where to start with this?

    My thoughts are to try looking around in NMS or in craftbukkit. But I'm terrible at reading decompiled code :/
    For finding out the order it stores the pixels in, I can create a test skin with every pixel having a different color... but this doesn't help me unless I know what method they are using to encode. Anyone have any idea what method might be used for encoding the pixels into this string?
     
  4. Another thing I'm wondering.... how would I actually go about setting that "internal" string on an itemstack? My first thought was to get the ItemMeta and use reflection, I kind of expected SkullMeta to have a field called "internal"... but, as a test, I tried printing out the result of getDeclaredFields() and got this:

    Code (Text):
    static final org.bukkit.craftbukkit.v1_16_R3.inventory.CraftMetaItem$ItemMetaKey org.bukkit.craftbukkit.v1_16_R3.inventory.CraftMetaSkull.SKULL_PROFILE
    static final org.bukkit.craftbukkit.v1_16_R3.inventory.CraftMetaItem$ItemMetaKey org.bukkit.craftbukkit.v1_16_R3.inventory.CraftMetaSkull.SKULL_OWNER
    static final int org.bukkit.craftbukkit.v1_16_R3.inventory.CraftMetaSkull.MAX_OWNER_LENGTH
    private com.mojang.authlib.GameProfile org.bukkit.craftbukkit.v1_16_R3.inventory.CraftMetaSkull.profile
    private net.minecraft.server.v1_16_R3.NBTTagCompound org.bukkit.craftbukkit.v1_16_R3.inventory.CraftMetaSkull.serializedProfile
    So I'm not quite sure what's going on here.
     
  5. Okay, so I made some searching. If you decode that Base-64 string, you get a JSON, which contains the timestamp (which you changed your skin at I believe), your UUID, name and textures. There is "SKIN" section, there is an URL to texture server. If you load that URL, you get the texture image. I think that the client downloads it, then analyzes the image and applies the skin? I don't know...
     
  6. Oh, what base64 decoder did you use?? I tried decoding it and got nonsense.

    What this means is that... yeah it isn't actually storing the pixels, but the URL. Oh no, you got me excited!

    But, this has given me an idea. It's just storing a URL, right? It doesn't have to be a mojang skin server URL. I can create my own service especially for this dynamic player head thing!

    Ok, so just tell me what base64 decoder you used. I just plugged it into https://www.base64decode.org/ and got random characters.
     
  7. Ah, just noticed something. I used some website to generate a /give command to give me the player head, and in that command there was a long string. I base64 decoded that and got the JSON that you described. I guess the itemstack serialisation is doing something weird with the way it stores it.

    The current question is - how can I set the URL of a player head ItemStack? Using reflection somehow, but none of the fields in ItemStack (see one of my previous posts) seem to be a URL.
     
  8. I was making the same method for my plugin, here it is: (i would like to explain it somehow but i dont understand it either)
    Code (Java):
        public static ItemStack getSkullFromBase64(String base64) {
            ItemStack skull = new ItemStack(Material.PLAYER_HEAD, 1, (short) 3);
            if (base64 == null || base64.isEmpty())
                return skull;
            SkullMeta skullMeta = (SkullMeta) skull.getItemMeta();
            GameProfile profile = new GameProfile(UUID.randomUUID(), null);
            profile.getProperties().put("textures", new Property("textures", base64));
            Field profileField = null;
            try {
                profileField = skullMeta.getClass().getDeclaredField("profile");
            } catch (NoSuchFieldException | SecurityException e) {
                e.printStackTrace();
            }
            profileField.setAccessible(true);
            try {
                profileField.set(skullMeta, profile);
            } catch (IllegalArgumentException | IllegalAccessException e) {
                e.printStackTrace();
            }
            skull.setItemMeta(skullMeta);
            return skull;
        }
    Note: you can use it like this or you can "understand" how does it work, but this will work on 100% (on 1.16)
    and you need to import Mojang authlib for the GameProfile
     

  9. Thank you so much! Just to clarify, the base64 string that is inputted into the function, what is that? Is that the string containing the URL?

    EDIT: And I do understand it :)

    EDIT 2: A better question to ask might be - do you have a method for generating that base64 string from a raw URL string?
     
  10. with URL it would look like this:
    Code (Java):
        public static ItemStack getSkull(String url) {
            ItemStack skull = new ItemStack(Material.PLAYER_HEAD, 1, (short) 3);
            if (url == null || url.isEmpty())
                return skull;
            SkullMeta skullMeta = (SkullMeta) skull.getItemMeta();
            GameProfile profile = new GameProfile(UUID.randomUUID(), null);
            byte[] encodedData = Base64.getEncoder().encode(String.format("{textures:{SKIN:{url:\"%s\"}}}", url).getBytes());
            profile.getProperties().put("textures", new Property("textures", new String(encodedData)));
            Field profileField = null;
            try {
                profileField = skullMeta.getClass().getDeclaredField("profile");
            } catch (NoSuchFieldException | SecurityException e) {
                e.printStackTrace();
            }
            profileField.setAccessible(true);
            try {
                profileField.set(skullMeta, profile);
            } catch (IllegalArgumentException | IllegalAccessException e) {
                e.printStackTrace();
            }
            skull.setItemMeta(skullMeta);
            return skull;
        }
     
  11. Brilliant, that makes sense. This is exactly what I need.

    Now time to create a web server to handle my dynamic player head creation... this is going to be a nightmare XD
    (but thank you so much for your help, this is EXACTLY what I was looking for)
     
  12. If you don't understand what's done there, I can explain it to you pretty easily, if you want.

    @repeater64 I done a small research into texture setting and packet transfers. On my server I gave myself the head of mine (using /give) and placed it. Then, I reconnected and tried to catch the NBT data in some packet. I was trying to catch more packets, first of all the Chunk Data Packet. Using ProtocolLib I managed to log all the NBT tags from the last (block entity data) field. Nothing was there, so I tried another, Entity Block Data Packet. I got the NBT tags successfully and there was the content you can get from Mojang API. I thought that sending the URLs to all clients to load the textures by themselves is not that effective and so it works another way. But, no...

    And to this, textures are got using an URL, you already know that. However, that does not mean you need any sort of web service. You can simply store all textures made by you (not sure if downloading other textures is legal) locally on the drive and refer to these files using the URL. A discussion on this topic including how to achieve this is here.
     
  13. Hi, first of all, I understand that code perfectly fine, so that's all good.

    Yes, after posting this I realised that I don't actually need a web service, I can just use file://, just as you describe. Thanks for the clarification, but I think I'm good from here!
     
  14. Hi, sorry to bump this thread, but I've actually tried this now and hit an issue.

    When I use that method you provided with a proper mojang skin URL, it works perfectly. However, if I try any other image URL, it just gives me a steve or alex skin.

    My test was to use an image upload service, upload a valid skin file, then get the URL that the service gave me, and pass that into the method - and I just get a steve or alex head.

    Is there any chance that there is some internal check to only allow mojang URLs? Or is there anything else I might have messed up?

    Thanks!
     
  15. The JSON texture value you are encoding is invalid. It should be formatted like this:
    Code (Text):
    {\"textures\":{\"SKIN\":{\"url\":\"%s\"}}}
    Also, I am not sure, but I think I read somewhere that the game client does not accept other URLs than ones pointing to the official texture server. If the JSON above does not work either, that might be the case.
     
  16. byte[] encodedData = Base64.getEncoder().encode(String.format("{textures:{SKIN:{url:\"%s\"}}}", url).getBytes());

    That's the format I'm using, and I know it does work because when I pass in a mojang skin server URL it works perfectly.

    The second part of your answer is what I fear. If you vaguely remember where you found that information it would be great because I'd love to read more about it.
     
  17. Well, an online parser shows me errors when the value keys are not surrounded by quotes. Anyway, I found something about the URL problems: https://www.spigotmc.org/threads/skull-texture.364457/#post-3362403.
     
  18. Sounds like that confirms my fear that I can't do it the way I hoped.

    upload_2021-1-17_16-34-24.png

    That's another quote I found. So this idea is impossible.

    The only way it would be possible would be to have a spare minecraft account that you keep updating the skin of. The protocol for setting the skin of a player is well documented by wiki.vg so it would be possible to do it automatedly (as in, outside of the minecraft launcher). Might consider looking into that, or just finding an alternative to what I'm trying to do.
     
  19. It looks like Mineskin is just a tool to set the skin of a minecraft account. It does it in the exact same way the launcher does it.