[Class] Change the skin of a player without changing the name, yes it's possible! [NO SPOUT]

Discussion in 'Resources' started by bigteddy98, May 28, 2014.

Thread Status:
Not open for further replies.
  1. Offline

    marwzoor

    Ooh okey :) Great resource btw!

    Are you really building against 1.7.9?

    EDIT by Moderator: merged posts, please use the edit button instead of double posting.
     
    Last edited by a moderator: Jun 8, 2016
    bigteddy98 likes this.
  2. Offline

    BungeeTheCookie

    Yes. Why?
     
  3. Offline

    bigteddy98

    Because builds lower than 1.7.9 don't support this system :D
     
  4. Offline

    marwzoor

  5. Offline

    bigteddy98

    And also the getProperties() method, it doesn't seem to exist in builds lower than 1.7.9, it is new in 1.7.9.
     
  6. Offline

    codename_B

    This is a great feature, and if anyone starts using this to SELL CAPES we've been 100% guaranteed by Mojang that this feature will be gone, along with it breaking all sorts of cool things (and NPCs) all over again.

    Please don't abuse this.
     
    iiHeroo, mcserverdev, glen3b and 4 others like this.
  7. Offline

    rsod

    So it can only change skin to someone's else skin, but not to custom skin? Useless then.
     
  8. Offline

    BungeeTheCookie

    I am using 1.7.9.... Are you saying this does not support 1.7.9??
     
  9. Offline

    Comphenix

    It's relatively trivial to also add name changing in the same process:
    Code:java
    1. public class ExampleMod extends JavaPlugin {
    2. private PlayerDisplayModifier factory;
    3.  
    4. @Override
    5. public void onEnable() {
    6. factory = new PlayerDisplayModifier(this);
    7. factory.changeDisplay("aadnk", "Dinnerbone", "Any name here");
    8. }
    9. }

    Screenshot:
    [​IMG]

    I also added profile caching (invalidates after 4 hours, with a maximum of 500 stored profiles), the ability to set a player by name alone; and better integration with ProtocolLib to allow other plugins to modify the profile as well (cancelling is not a good idea).

    This is accomplished by using the built in asynchronous packet listener, with a configurable number of worker threads that processes these spawn packets (4 by default). I also use WrappedGameProfile instead, which would allow us to enable version independence with ProtocolLib 3.4.0. The problem is that this method is currently only supported by the 1.7.9 client, and it's highly likely it will be removed in 1.8.0.

    Finally, I also fixed a bug in your original version - by cancelling and sending a spawn entity packet later, you also prevent the ENTITY_EQUIPMENT (for armor and weapon), ENTITY_EFFECT (potion effect) and other packets like them to be disregarded by the client. The end result is that your version doesn't support armor initially.

    You can get the PlayerDisplayModifier class down below or on Gist. Most of the credit still goes to bigteddy98, @ferrybig and @lenis0012 for writing the original version. :)

    Code:
    package com.comphenix.example;
     
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.net.URL;
    import java.net.URLConnection;
    import java.util.Map;
    import java.util.UUID;
    import java.util.concurrent.ConcurrentMap;
    import java.util.concurrent.TimeUnit;
     
    // For ProtocolLib 3.3.1 and lower
    import net.minecraft.util.com.mojang.authlib.GameProfile;
    import net.minecraft.util.com.mojang.authlib.properties.Property;
     
    // For ProtocolLib 3.4.0
    //import com.comphenix.protocol.wrappers.WrappedSignedProperty;
     
    import org.bukkit.Bukkit;
    import org.bukkit.entity.Player;
    import org.bukkit.plugin.Plugin;
    import org.json.simple.JSONArray;
    import org.json.simple.JSONObject;
    import org.json.simple.parser.JSONParser;
     
    import com.comphenix.protocol.PacketType;
    import com.comphenix.protocol.ProtocolLibrary;
    import com.comphenix.protocol.ProtocolManager;
    import com.comphenix.protocol.events.ListenerPriority;
    import com.comphenix.protocol.events.PacketAdapter;
    import com.comphenix.protocol.events.PacketEvent;
    import com.comphenix.protocol.reflect.StructureModifier;
    import com.comphenix.protocol.wrappers.WrappedGameProfile;
     
    import com.google.common.base.Charsets;
    import com.google.common.base.Objects;
    import com.google.common.cache.Cache;
    import com.google.common.cache.CacheBuilder;
    import com.google.common.cache.CacheLoader;
    import com.google.common.collect.Maps;
    import com.google.common.io.CharStreams;
    import com.google.common.io.InputSupplier;
     
    public class PlayerDisplayModifier {
        private static final String PROFILE_URL = "https://sessionserver.mojang.com/session/minecraft/profile/";
        private static final int WORKER_THREADS = 4;
       
        private ProtocolManager protocolManager;
        private ConcurrentMap<String, String> skinNames = Maps.newConcurrentMap();
        private ConcurrentMap<String, String> displayNames = Maps.newConcurrentMap();
     
        private Cache<String, String> profileCache = CacheBuilder.newBuilder().
            maximumSize(500).
            expireAfterWrite(4, TimeUnit.HOURS).
            build(new CacheLoader<String, String>() {
                public String load(String name) throws Exception {
                    return getProfileJson(name);
                };
            });
       
        public PlayerDisplayModifier(Plugin plugin) {
            protocolManager = ProtocolLibrary.getProtocolManager();
            protocolManager.getAsynchronousManager().registerAsyncHandler(
              new PacketAdapter(plugin, ListenerPriority.NORMAL,
                    // Packet we are modifying
                    PacketType.Play.Server.NAMED_ENTITY_SPAWN,
                   
                    // Packets that must be sent AFTER the entity spawn packet
                    PacketType.Play.Server.ENTITY_EFFECT,
                    PacketType.Play.Server.ENTITY_EQUIPMENT,
                    PacketType.Play.Server.ENTITY_METADATA,
                    PacketType.Play.Server.UPDATE_ATTRIBUTES,
                    PacketType.Play.Server.ATTACH_ENTITY,
                    PacketType.Play.Server.BED) {
                 
                // This will be executed on an asynchronous thread
                @Override
                public void onPacketSending(PacketEvent event) {
                    // We only care about the entity spawn packet
                    if (event.getPacketType() != PacketType.Play.Server.NAMED_ENTITY_SPAWN) {
                        return;
                    }
                   
                    Player toDisplay = (Player) event.getPacket().getEntityModifier(event).read(0);
                    String skinName = skinNames.get(toDisplay.getName());
                    String displayName = displayNames.get(toDisplay.getName());
                   
                    if (skinName == null && displayName == null) {
                        return;
                    }
                    StructureModifier<WrappedGameProfile> profiles = event.getPacket().getGameProfiles();
                    WrappedGameProfile original = profiles.read(0);
                    WrappedGameProfile result = new WrappedGameProfile(
                        extractUUID(original.getName()),
                        displayName != null ? displayName : original.getName()
                    );
                   
                    updateSkin(result, skinName != null ? skinName : result.getName());
                    profiles.write(0, result);
                }
            }).start(WORKER_THREADS);
        }
     
        @SuppressWarnings("deprecation")
        private UUID extractUUID(final String playerName) {
            return Bukkit.getOfflinePlayer(playerName).getUniqueId();
        }
       
        // This will be cached by Guava
        private String getProfileJson(String name) throws IOException {
            final URL url = new URL(PROFILE_URL + extractUUID(name).toString().replace("-", ""));
            final URLConnection uc = url.openConnection();
     
            return CharStreams.toString(new InputSupplier<InputStreamReader>() {
                @Override
                public InputStreamReader getInput() throws IOException {
                    return new InputStreamReader(uc.getInputStream(), Charsets.UTF_8);
                }
            });
        }
       
        private void updateSkin(WrappedGameProfile profile, String skinOwner) {
            try {
                JSONObject json = (JSONObject) new JSONParser().parse(profileCache.get(skinOwner));
                JSONArray properties = (JSONArray) json.get("properties");
               
                for (int i = 0; i < properties.size(); i++) {
                    JSONObject property = (JSONObject) properties.get(i);
                    String name = (String) property.get("name");
                    String value = (String) property.get("value");
                    String signature = (String) property.get("signature"); // May be NULL
                   
                    // Uncomment for ProtocolLib 3.4.0
                    //profile.getProperties().put(name, new WrappedSignedProperty(name, value, signature));
                    ((GameProfile)profile.getHandle()).getProperties().put(name, new Property(name, value, signature));
                }
            } catch (Exception e) {
                // ProtocolLib will throttle the number of exceptions printed to the console log
                throw new RuntimeException("Cannot fetch profile for " + skinOwner, e);
            }
        }
       
        public void changeDisplay(String string, String toSkin) {
            changeDisplay(string, toSkin, null);
        }
     
        public void changeDisplay(Player player, String toSkin, String toName) {
            if (updateMap(skinNames, player.getName(), toSkin) |
                updateMap(displayNames, player.getName(), toName)) {
                refreshPlayer(player);
            }
        }
     
        public void changeDisplay(String playerName, String toSkin, String toName) {
            if (updateMap(skinNames, playerName, toSkin) |
                updateMap(displayNames, playerName, toName)) {
                refreshPlayer(playerName);
            }
        }
       
        public void removeChanges(Player player) {
            changeDisplay(player.getName(), null, null);
        }
     
        public void removeChanges(String playerName) {
            changeDisplay(playerName, null, null);
        }
       
        /**
        * Update the map with the new key-value pair.
        * @param map - the map.
        * @param key - the key of the pair.
        * @param value - the new value, or NULL to remove the pair.
        * @return TRUE if the map was updated, FALSE otherwise.
        */
        private <T, U> boolean updateMap(Map<T, U> map, T key, U value) {
            if (value == null) {
                return map.remove(key) != null;
            } else {
                return !Objects.equal(value, map.put(key, value));
            }
        }
       
        @SuppressWarnings("deprecation")
        private void refreshPlayer(String name) {
            Player player = Bukkit.getPlayer(name);
           
            if (player != null) {
                refreshPlayer(player);
            }
        }
       
        private void refreshPlayer(Player player) {
            // Refresh the displayed entity
            protocolManager.updateEntity(player, protocolManager.getEntityTrackers(player));
        }
    }
    
    EDIT: Bugfix.
     
  10. Offline

    bigteddy98

    Please use the newest version as dependency, download from here.

    I do fully agree with you, but you can only do this by changing the skin too, right? I think servers won't do this because of two problems:

    1. Players can't see their own capes, which actually is the most cool of a cape.

    2. You will have to change the skin to someone else who does have a cape. So I personally think it's ugly to see a server with hunderds of people having the same skin.

    But indeed guys, please do NOT abuse this awesome feature, Mojang will remove it without a problem...

    That. Is. Awesome. :D

    Great job. This learns me a lot about ProtocolLib too, it does have many more features than I thought it had.

    Am I allowed to add this as V1.3 in the main post? That would be pretty awesome, because you also fixed the armour bug.

    EDIT by Moderator: merged posts, please use the edit button instead of double posting.
     
    Last edited by a moderator: Jun 8, 2016
  11. Offline

    Skyost

    Comphenix I do not understand that :
    Code:
        private void refreshPlayer(String name) {
            for(Player player : Bukkit.getOnlinePlayers()) {
                if(player.getName().equals(name)) {
                    refreshPlayer(player);
                    return;
                }
            }
        }
    Why not using Bukkit.getPlayer(...) ? Because it is deprecated ?

    By the way, I am getting the following error when using changeDisplay(player, "skin") :
    Show Spoiler

    Code:
    org.bukkit.command.CommandException: Unhandled exception executing command 'bsa' in plugin BSA v0.2
        at org.bukkit.command.PluginCommand.execute(PluginCommand.java:46) ~[craftbukkit.jar:git-Bukkit-1.7.9-R0.1-b3084jnks]
        at org.bukkit.command.SimpleCommandMap.dispatch(SimpleCommandMap.java:180) ~[craftbukkit.jar:git-Bukkit-1.7.9-R0.1-b3084jnks]
        at org.bukkit.craftbukkit.v1_7_R3.CraftServer.dispatchCommand(CraftServer.java:701) ~[craftbukkit.jar:git-Bukkit-1.7.9-R0.1-b3084jnks]
        at net.minecraft.server.v1_7_R3.PlayerConnection.handleCommand(PlayerConnection.java:956) [craftbukkit.jar:git-Bukkit-1.7.9-R0.1-b3084jnks]
        at net.minecraft.server.v1_7_R3.PlayerConnection.a(PlayerConnection.java:817) [craftbukkit.jar:git-Bukkit-1.7.9-R0.1-b3084jnks]
        at net.minecraft.server.v1_7_R3.PacketPlayInChat.a(PacketPlayInChat.java:28) [craftbukkit.jar:git-Bukkit-1.7.9-R0.1-b3084jnks]
        at net.minecraft.server.v1_7_R3.PacketPlayInChat.handle(PacketPlayInChat.java:47) [craftbukkit.jar:git-Bukkit-1.7.9-R0.1-b3084jnks]
        at net.minecraft.server.v1_7_R3.NetworkManager.a(NetworkManager.java:157) [craftbukkit.jar:git-Bukkit-1.7.9-R0.1-b3084jnks]
        at net.minecraft.server.v1_7_R3.ServerConnection.c(SourceFile:134) [craftbukkit.jar:git-Bukkit-1.7.9-R0.1-b3084jnks]
        at net.minecraft.server.v1_7_R3.MinecraftServer.v(MinecraftServer.java:667) [craftbukkit.jar:git-Bukkit-1.7.9-R0.1-b3084jnks]
        at net.minecraft.server.v1_7_R3.DedicatedServer.v(DedicatedServer.java:260) [craftbukkit.jar:git-Bukkit-1.7.9-R0.1-b3084jnks]
        at net.minecraft.server.v1_7_R3.MinecraftServer.u(MinecraftServer.java:558) [craftbukkit.jar:git-Bukkit-1.7.9-R0.1-b3084jnks]
        at net.minecraft.server.v1_7_R3.MinecraftServer.run(MinecraftServer.java:469) [craftbukkit.jar:git-Bukkit-1.7.9-R0.1-b3084jnks]
        at net.minecraft.server.v1_7_R3.ThreadServerApplication.run(SourceFile:628) [craftbukkit.jar:git-Bukkit-1.7.9-R0.1-b3084jnks]
    Caused by: java.lang.NullPointerException
        at java.util.concurrent.ConcurrentHashMap.put(Unknown Source) ~[?:1.7.0_55]
        at com.comphenix.example.PlayerDisplayModifier.changeDisplay(PlayerDisplayModifier.java:138) ~[?:?]
        at com.comphenix.example.PlayerDisplayModifier.changeDisplay(PlayerDisplayModifier.java:134) ~[?:?]
        at fr.skyost.bsa.Agent.<init>(Agent.java:28) ~[?:?]
        at fr.skyost.bsa.BSA.addPlayer(BSA.java:88) ~[?:?]
        at fr.skyost.bsa.commands.BSACommand.onCommand(BSACommand.java:39) ~[?:?]
        at org.bukkit.command.PluginCommand.execute(PluginCommand.java:44) ~[craftbukkit.jar:git-Bukkit-1.7.9-R0.1-b3084jnks]
        ... 13 more
    The error line is :
    if(!Objects.equal(player.getName(), this.skinNames.put(player.getName(), toSkin)) | (!Objects.equal(player.getName(), this.displayNames.put(player.getName(), toName))))
     
  12. Offline

    bigteddy98

    Yep I think so, I can't think of another reason.
     
    Skyost likes this.
  13. Offline

    Comphenix

    Thanks - yeah, you do end up with a few features after 1 1/2 years of development. :p

    Of course. :)

    That was the idea, yeah. But on second thought, I suppose it's unlikely it will ever get removed. Bukkit only deprecated it to call attention to the player UUID issue. So, it's probably fine if you replace it with Bukkit.getPlayer(String).
     
  14. Offline

    Skyost

    Comphenix Okay, thanks for the reply (I have edited my post below to post an Exception, can you look it please ?) ;)
    EDIT : Fixed by canching changeDisplay(player, toSkin, null); to changeDisplay(player, toSkin, player.getName());
     
  15. Offline

    Comphenix

    Oh, sorry, must have forgotten to test that method after I switched to ConcurrentMap.

    I fixed it by creating a generic updateMap() that treats NULL as an instruction to remove a key-value pair, instead of adding it. You can get the updated version on Gist.
     
    Skyost likes this.
  16. Offline

    lenis0012

    Actua;y i was the one who came up with manipulating skin info
     
  17. Offline

    bigteddy98

    Yep that's why I added you to the credit list too ;)

    Thanks, I also updated the code in the main post.

    BTW, why do you use the SwordPvP servers in your class? It's just a proxy which makes your requests slower. Does it have any advantages?

    EDIT by Moderator: merged posts, please use the edit button instead of double posting.
     
    Last edited by a moderator: Jun 8, 2016
  18. Offline

    lenis0012

    I use swordpvp because i found it when i was messing around with their url's

    I could have easily used mojang for it

    you can't really "Sell capes"

    Reason is that the texture info is encoded in base64.
    You can decode this and modify the cape url.
    HOWEVER
    It has to be confirmed by a signature.
    If the signature does not match the value, it will not work.

    EDIT by Moderator: merged posts, please use the edit button instead of double posting.
     
    Last edited by a moderator: Jun 8, 2016
  19. Offline

    bigteddy98

    In your code you're checking if the signature is not equal to null, so it can be null, right? But when will it be null?
     
  20. Offline

    Comphenix

    You can't set it to NULL, thought, as PacketPlayOutSpawnNamedEntity can't serialize NULL fields in GameProfile's property map. I tried setting it to an empty string, but that only disabled the skin altogether (to steve).
     
  21. Offline

    bigteddy98

    Ah okay that's good to know, but I actually ment the other method, which only requeres a name and a value:
    Code:java
    1. if (signature != null) {
    2. profile.getProperties().put(name, new Property(name, value, signature));
    3. } else {
    4. profile.getProperties().put(name, new Property(value, name));
    5. }


    I will check it out myself now ;)

    EDIT: The client is being disconnected, so it doesn't work.
     
  22. Offline

    lenis0012

    bigteddy98 I did that because in the future i asumme more properties will be added.
    Property has 2 constructors you see.
    So it could be that in the future there are values without signatures
     
  23. Offline

    bigteddy98

    aah okay thanks
     
  24. For some reason I can't set my own skin to a player's skin who's online. :/ It doesn't throw an error... just switches to Steve skin for around a second, and then switches back to my skin.
     
  25. Offline

    bigteddy98

    What version of the class are you using? If you're using 1.2, try 1.3. If you're using 1.3, try 1.2. So we know if it's a error in our code ;)
     
  26. bigteddy98 likes this.
  27. Offline

    sipsi133

    Does this work with craftbukkit 1.7.2?
     
  28. Offline

    bigteddy98

    Nope it's a new feature in 1.7.9 builds and probably higher.
     
  29. Offline

    LCastr0

    Code:
    Unhandled exception occured in onAsyncPacket() for
    CoolWool
    java.lang.NoSuchMethodError: net.minecraft.util.com.mojang.authlib.GameProfile.g
    etProperties()Lnet/minecraft/util/com/mojang/authlib/properties/PropertyMap;
            at me.LCastr0.CoolWool.SkinChangeFactory.updateSkin(SkinChangeFactory.ja
    va:143) ~[CoolWool.jar:?]
            at me.LCastr0.CoolWool.SkinChangeFactory.access$4(SkinChangeFactory.java
    :130) ~[CoolWool.jar:?]
            at me.LCastr0.CoolWool.SkinChangeFactory$2.onPacketSending(SkinChangeFac
    tory.java:99) ~[CoolWool.jar:?]
            at com.comphenix.protocol.async.AsyncListenerHandler.processPacket(Async
    ListenerHandler.java:601) [ProtocolLib-3.4.0-SNAPSHOT.jar:?]
            at com.comphenix.protocol.async.AsyncListenerHandler.listenerLoop(AsyncL
    istenerHandler.java:557) [ProtocolLib-3.4.0-SNAPSHOT.jar:?]
            at com.comphenix.protocol.async.AsyncListenerHandler.access$100(AsyncLis
    tenerHandler.java:46) [ProtocolLib-3.4.0-SNAPSHOT.jar:?]
            at com.comphenix.protocol.async.AsyncListenerHandler$1.run(AsyncListener
    Handler.java:192) [ProtocolLib-3.4.0-SNAPSHOT.jar:?]
            at com.comphenix.protocol.async.AsyncListenerHandler$2.run(AsyncListener
    Handler.java:262) [ProtocolLib-3.4.0-SNAPSHOT.jar:?]
            at org.bukkit.craftbukkit.v1_7_R1.scheduler.CraftTask.run(CraftTask.java
    :53) [craftbukkit-172.jar:git-Bukkit-1.7.2-R0.3-2-g85f5776-b3022jnks]
            at org.bukkit.craftbukkit.v1_7_R1.scheduler.CraftAsyncTask.run(CraftAsyn
    cTask.java:53) [craftbukkit-172.jar:git-Bukkit-1.7.2-R0.3-2-g85f5776-b3022jnks]
            at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) [?:
    1.7.0_17]
            at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) [?
    :1.7.0_17]
            at java.lang.Thread.run(Unknown Source) [?:1.7.0_17]
    Help? ;-;
     
  30. Offline

    bigteddy98

    I guess you're not running a 1.7.9 build?
     
Thread Status:
Not open for further replies.

Share This Page