-
-
Save bicrypt/e228785736c1df507a3280a0bccc5e8b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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