Skip to content

Instantly share code, notes, and snippets.

@aadnk
Last active November 6, 2024 14:27
Show Gist options
  • Save aadnk/0502e32369f203daaba9 to your computer and use it in GitHub Desktop.
Save aadnk/0502e32369f203daaba9 to your computer and use it in GitHub Desktop.
Change the display name and skin of any player. Credit to @bigteddy98, @ferrybig and @lenis0012 for writing the original version.
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));
}
}
@IllusionTheDev
Copy link

This could be updated to use GSON

@nathanfranke
Copy link

InputSupplier does not seem to exist anymore.

@devpeaqe
Copy link

that class is not really good.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment