Skip to content

Instantly share code, notes, and snippets.

@bicrypt
Created August 12, 2019 08:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bicrypt/e228785736c1df507a3280a0bccc5e8b to your computer and use it in GitHub Desktop.
Save bicrypt/e228785736c1df507a3280a0bccc5e8b to your computer and use it in GitHub Desktop.
package net.bicrypt.gist;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.wrappers.EnumWrappers.NativeGameMode;
import com.comphenix.protocol.wrappers.EnumWrappers.PlayerInfoAction;
import com.comphenix.protocol.wrappers.PlayerInfoData;
import com.comphenix.protocol.wrappers.WrappedChatComponent;
import com.comphenix.protocol.wrappers.WrappedDataWatcher;
import com.comphenix.protocol.wrappers.WrappedDataWatcher.Registry;
import com.comphenix.protocol.wrappers.WrappedDataWatcher.WrappedDataWatcherObject;
import com.comphenix.protocol.wrappers.WrappedGameProfile;
import java.lang.reflect.InvocationTargetException;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;
import static java.util.Collections.singletonList;
import static com.comphenix.protocol.PacketType.Play.Server.ENTITY_HEAD_ROTATION;
import static com.comphenix.protocol.PacketType.Play.Server.ENTITY_METADATA;
import static com.comphenix.protocol.PacketType.Play.Server.NAMED_ENTITY_SPAWN;
import static com.comphenix.protocol.PacketType.Play.Server.PLAYER_INFO;
@RequiredArgsConstructor
public class FakePlayerGist implements Listener {
private static final ProtocolManager PROTOCOL_MANAGER;
static {
PROTOCOL_MANAGER = ProtocolLibrary.getProtocolManager();
}
private final JavaPlugin plugin;
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
final Player player = event.getPlayer();
final Location location = player.getLocation().clone();
new BukkitRunnable() {
@Override
public void run() {
if (!player.isOnline()) {
cancel();
return;
}
FakePlayer fakePlayer = new FakePlayer(plugin, player, location);
// spawn fake player
fakePlayer.spawn();
}
}.runTaskLaterAsynchronously(plugin, 40L);
}
private static class FakePlayer {
private static final AtomicInteger GENERATOR;
static {
GENERATOR = new AtomicInteger(1);
}
private final int entityId;
private final JavaPlugin plugin;
private final Player player;
private final UUID uniqueId;
private final String username;
private final WrappedGameProfile gameProfile;
private final Location location;
FakePlayer(JavaPlugin plugin, Player player, Location location) {
this.entityId = nextEntityId();
this.plugin = plugin;
this.player = player;
this.uniqueId = player.getUniqueId();
this.username = player.getName();
// use player skin for fake player
this.gameProfile = WrappedGameProfile.fromPlayer(player);
this.location = location;
}
static synchronized int nextEntityId() {
return GENERATOR.getAndIncrement();
}
@SneakyThrows(value = InvocationTargetException.class)
void spawn() {
// add player to tab list
PROTOCOL_MANAGER.sendServerPacket(player, getPlayerInfoPacket(PlayerInfoAction.ADD_PLAYER));
// spawn fake player with skin data
PROTOCOL_MANAGER.sendServerPacket(player, getNamedEntitySpawnPacket());
// update the fake player's metadata (try to send skin parts yet another time)
PROTOCOL_MANAGER.sendServerPacket(player, getEntityMetadataPacket());
// send head rotation packet to ensure the player is facing the intended direction
PROTOCOL_MANAGER.sendServerPacket(player, getHeadRotationPacket());
new BukkitRunnable() {
@SneakyThrows(value = InvocationTargetException.class)
@Override
public void run() {
if (!player.isOnline()) {
cancel();
return;
}
// try to display skin parts for the last time
PROTOCOL_MANAGER.sendServerPacket(player, getEntityMetadataPacket());
// remove fake player from the tab list
PROTOCOL_MANAGER.sendServerPacket(player, getPlayerInfoPacket(PlayerInfoAction.REMOVE_PLAYER));
}
}.runTaskLaterAsynchronously(plugin, 2L);
}
private PacketContainer getPlayerInfoPacket(PlayerInfoAction action) {
PacketContainer container = PROTOCOL_MANAGER.createPacket(PLAYER_INFO);
container.getModifier().writeDefaults();
container.getPlayerInfoAction().write(0, action);
PlayerInfoData infoData = new PlayerInfoData(
gameProfile,
1,
NativeGameMode.NOT_SET,
WrappedChatComponent.fromText(username)
);
container.getPlayerInfoDataLists().write(0, singletonList(infoData));
return container;
}
private PacketContainer getNamedEntitySpawnPacket() {
PacketContainer container = PROTOCOL_MANAGER.createPacket(NAMED_ENTITY_SPAWN);
container.getModifier().writeDefaults();
container.getIntegers().write(0, entityId);
container.getUUIDs().write(0, uniqueId);
container.getDoubles()
.write(0, location.getX())
.write(1, location.getY())
.write(2, location.getZ());
container.getBytes()
.write(0, (byte) (location.getYaw() * 256.0F / 360.0F))
.write(1, (byte) (location.getPitch() * 256.0F / 360.0F));
container.getDataWatcherModifier().write(0, getFakePlayerDataWatcher());
return container;
}
private PacketContainer getEntityMetadataPacket() {
PacketContainer container = PROTOCOL_MANAGER.createPacket(ENTITY_METADATA);
container.getModifier().writeDefaults();
container.getIntegers().write(0, entityId);
container.getWatchableCollectionModifier().write(0, getFakePlayerDataWatcher().getWatchableObjects());
return container;
}
private PacketContainer getHeadRotationPacket() {
PacketContainer container = PROTOCOL_MANAGER.createPacket(ENTITY_HEAD_ROTATION);
container.getModifier().writeDefaults();
container.getIntegers().write(0, entityId);
container.getBytes().write(0, (byte) (location.getYaw() * 256.0F / 360.0F));
return container;
}
private WrappedDataWatcher getFakePlayerDataWatcher() {
WrappedDataWatcher watcher = new WrappedDataWatcher();
/*
* According to https://wiki.vg/Entity_metadata#Player one has to send
* the desired bitmask at index 13 in order to display certain parts of
* the players skin.
*
* In this case we set the value to the sum of all available masks
* (0x01 + 0x02 + 0x04 + 0x08 + 0x10 + 0x20 + 0x40 = 127)
*
* Somehow no skin parts are being displayed!
*
* On a 1.8 paper spigot instance at index 10 (was the index to use back in
* the day) it works perfectly with 127 as value.
*
* Seems to be broken in 1.14 and latest paper spigot build though.
*/
watcher.setObject(new WrappedDataWatcherObject(13, Registry.get(Byte.class)), (byte) 127);
return watcher;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment