Compare commits

...

1 Commits

Author SHA1 Message Date
Andrea Cavalli 9d1f489996 First commit 2021-04-19 20:53:45 +02:00
18 changed files with 1216 additions and 0 deletions

18
.gitignore vendored Normal file
View File

@ -0,0 +1,18 @@
# Eclipse stuff
/.classpath
/.project
/.settings
/.checkstyle
/bin/
/target/
.idea/
.papermc/
dependency-reduced-pom.xml
CachedPlayerHeads.iml
CoordinatesObfuscator.iml

17
README.md Normal file
View File

@ -0,0 +1,17 @@
CachedPlayerHeads
============
**Spigot plugin that drops player heads when they die, without causing client-side stuttering**
Features
--------
- SkinsRestorer support
- Bungeecord support (just copy it in both spigot and bungeecord plugins folders)
- The only plugin of its kind that doesn't cause client-side stuttering when the heads are loaded
- Older heads remain the same also if a player changes skin.
How it works
------------
When a player is killed by another player in survival mode, the plugin downloads the skin and drops a head with that skin.
The standard player heads plugins cause stuttering because the Minecraft client downloads the skins from the internet synchronously when it loads the chunks: this plugin instead downloads the skin on the server-side asynchronously and it applies the texture data itself on the head, instead of using the player name/uuid.

143
pom.xml Normal file
View File

@ -0,0 +1,143 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.warp</groupId>
<artifactId>CachedPlayerHeads</artifactId>
<version>3.2.0</version>
<name>CachedPlayerHeads</name>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>plugin.yml</include>
<include>bungee.yml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<excludes>
<exclude>plugin.yml</exclude>
<exclude>bungee.yml</exclude>
</excludes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>8</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<artifactSet>
<includes>
<include>*:skullcreator:*</include>
</includes>
</artifactSet>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>bungeecord-repo</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</repository>
<repository>
<id>papermc</id>
<url>https://papermc.io/repo/repository/maven-public/</url>
</repository>
<repository>
<id>dmulloy2-repo</id>
<url>https://repo.dmulloy2.net/content/groups/public/</url>
</repository>
<repository>
<id>spigotmc-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
<repository>
<id>skullcreator-repo</id>
<url>https://dl.bintray.com/deanveloper/SkullCreator</url>
</repository>
<repository>
<id>codemc-snapshots</id>
<url>https://repo.codemc.org/repository/maven-snapshots/</url>
</repository>
<repository>
<id>mc</id>
<url>https://libraries.minecraft.net/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>dev.dbassett</groupId>
<artifactId>skullcreator</artifactId>
<version>3.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.comphenix.protocol</groupId>
<artifactId>ProtocolLib</artifactId>
<version>4.6.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.destroystokyo.paper</groupId>
<artifactId>paper-api</artifactId>
<version>1.16.5-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<!--
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot</artifactId>
<version>1.16.4-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
-->
<dependency>
<groupId>net.skinsrestorer</groupId>
<artifactId>skinsrestorer</artifactId>
<version>14.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.mojang</groupId>
<artifactId>authlib</artifactId>
<version>1.5.21</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-api</artifactId>
<version>1.16-R0.5-SNAPSHOT</version>
<type>jar</type>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-api</artifactId>
<version>1.16-R0.5-SNAPSHOT</version>
<type>javadoc</type>
<scope>provided</scope>
</dependency>
</dependencies>
<url>http://blackspectrum.eu/</url>
</project>

View File

@ -0,0 +1,10 @@
package org.warp.cachedplayerheads;
import net.skinsrestorer.shared.storage.SkinStorage;
public class BukkitSkinStorage extends SkinStorage {
public BukkitSkinStorage() {
super(Platform.BUKKIT);
}
}

View File

@ -0,0 +1,107 @@
package org.warp.cachedplayerheads;
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.bukkit.Bukkit;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.messaging.PluginMessageListener;
import org.jetbrains.annotations.Nullable;
public class BungeeAPI implements PluginMessageListener {
private final Plugin playerheads;
private final ConcurrentHashMap<UUID, CompletableFuture<String>> requests = new ConcurrentHashMap<>();
private static final ScheduledExecutorService executor;
static {
executor = Executors.newSingleThreadScheduledExecutor();
}
public BungeeAPI(Plugin playerheads) {
this.playerheads = playerheads;
playerheads.getServer().getMessenger()
.registerIncomingPluginChannel(playerheads, "cachedplayerheads:channel", this);
playerheads.getServer().getMessenger()
.registerOutgoingPluginChannel(playerheads, "cachedplayerheads:channel");
}
private boolean checkIfBungee() {
@Nullable ConfigurationSection settings = playerheads
.getServer()
.spigot()
.getConfig()
.getConfigurationSection("settings");
if (settings == null || !settings.getBoolean("settings.bungeecord")) {
playerheads.getLogger().severe("This server is not BungeeCord.");
return false;
} else {
playerheads.getLogger().severe("This server is BungeeCord.");
return true;
}
}
public CompletableFuture<String> getPlayerTexture(String playerName) {
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF("PlayerTextureRequest");
UUID reqUUID = UUID.randomUUID();
out.writeLong(reqUUID.getMostSignificantBits());
out.writeLong(reqUUID.getLeastSignificantBits());
out.writeUTF(playerName);
CompletableFuture<String> futureResult = new CompletableFuture<>();
requests.put(reqUUID, futureResult);
try {
System.out.println("Asking player texture. Request: " + reqUUID);
Bukkit.getServer().sendPluginMessage(playerheads, "cachedplayerheads:channel", out.toByteArray());
} catch (Exception ex) {
requests.remove(reqUUID);
throw ex;
}
CompletableFuture<String> timeoutFuture = new CompletableFuture<>();
executor.schedule(() -> {
requests.remove(reqUUID);
timeoutFuture.completeExceptionally(new TimeoutException());
}, 30, TimeUnit.SECONDS);
return CompletableFuture.anyOf(futureResult, timeoutFuture).thenApply(x -> (String) x);
}
@Override
public void onPluginMessageReceived(String channel, Player player, byte[] message) {
if (!channel.equalsIgnoreCase("cachedplayerheads:channel")) {
return;
}
ByteArrayDataInput in = ByteStreams.newDataInput(message);
String subchannel = in.readUTF();
if (subchannel.equals("PlayerTextureResponse")) {
long msb = in.readLong();
long lsb = in.readLong();
boolean hasString = in.readBoolean();
String skinData = hasString ? in.readUTF() : null;
UUID reqUUID = new UUID(msb, lsb);
System.out.println("Received player texture. Request: " + reqUUID);
CompletableFuture<String> req = requests.get(reqUUID);
if (req != null) {
req.complete(skinData);
} else {
System.out.println("Received response for unknown request: " + reqUUID);
}
} else {
System.err.println("Invalid subchannel: " + subchannel);
}
}
}

View File

@ -0,0 +1,78 @@
package org.warp.cachedplayerheads;
import java.util.HashMap;
import java.util.concurrent.CompletableFuture;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
public class CommandExecutor implements org.bukkit.command.CommandExecutor {
private Playerheads plugin;
CommandExecutor(Playerheads plugin) {
this.plugin = plugin;
}
public boolean onCommand(CommandSender sender, Command cmd, String commandLabel, String[] args) {
if (this.plugin.lockdown)
return true;
if (sender instanceof Player && this.plugin.givePermissionOption && !sender.hasPermission("vendettacraft.playerheads.give")) {
sender.sendMessage("[PlayerHeads] You do not have permission to perform this command.");
return true;
}
if (args.length < 3)
return false;
if (args[0].equalsIgnoreCase("give")) {
Player p = Bukkit.getPlayer(args[1]);
if (p == null) {
sender.sendMessage("[PlayerHeads] The <PlayerName> field is not recognised.");
return true;
}
Player pp = Bukkit.getPlayer(args[2]);
if (pp == null) {
ReflectionHandler
.getSkullItem(args[2])
.thenAccept(i -> displayNameFunction(i, p, sender));
return true;
}
ReflectionHandler
.getTexture(pp)
.thenCompose(texture -> {
if (texture != null) {
return ReflectionHandler.getSkullItemFromTexture(pp.getName(), texture);
} else {
sender.sendMessage("[PlayerHeads] The <SkinName/Base64> field is not recognised.");
return CompletableFuture.completedFuture(null);
}
})
.thenAccept(item -> {
if (item != null) {
displayNameFunction(item, p, sender);
}
});
return true;
}
return true;
}
private void givePlayerItem(Player player, ItemStack itemStack) {
ItemMeta meta = itemStack.getItemMeta();
itemStack.setItemMeta(meta);
HashMap<Integer, ItemStack> h = player.getInventory().addItem(itemStack);
Location loc = player.getLocation();
for (ItemStack i : h.values())
loc.getWorld().dropItem(loc, i);
}
private void displayNameFunction(ItemStack i, Player p, CommandSender sender) {
if (i != null) {
givePlayerItem(p, i);
} else {
sender.sendMessage("[PlayerHeads] The SkinName/Base64 is not recognised.");
}
}
}

View File

@ -0,0 +1,124 @@
package org.warp.cachedplayerheads;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.plugin.Plugin;
public class EventListener implements Listener {
private Playerheads plugin;
EventListener(Playerheads plugin) {
plugin.getServer().getPluginManager().registerEvents(this, (Plugin)plugin);
this.plugin = plugin;
}
@EventHandler
public void onDeath(EntityDeathEvent event) {
LivingEntity livingEntity = event.getEntity();
boolean shouldDrop = false;
if (livingEntity instanceof Player) {
if (livingEntity.getKiller() != null) {
if (this.plugin.dropPlayerOption)
shouldDrop = true;
} else if (this.plugin.dropOtherDeathOption) {
shouldDrop = true;
}
if (shouldDrop) {
Player p = ((Player) livingEntity).getPlayer();
if (p != null) {
ReflectionHandler
.getSkullItem(p)
.thenAccept(skullItem -> plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> {
p.getWorld().dropItem(p.getLocation(), skullItem);
}));
}
}
}
}
@EventHandler
public void onLogin(PlayerLoginEvent event) {
/*
Player p = event.getPlayer();
String id = p.getUniqueId().toString();
String base64 = ReflectionHandler.getTexture(p);
if (base64 == null) {
Playerheads.playerDataConfig = (FileConfiguration)YamlConfiguration.loadConfiguration(Playerheads.playerDataFile);
Object obj = Playerheads.playerDataConfig.get(id);
if (obj != null) {
base64 = obj.toString();
}
}
if (base64 != null) {
Playerheads.PlayerBase64.put(id, base64);
Playerheads.refreshPlayerDatafile();
}
}
@EventHandler
public void onExit(PlayerQuitEvent event) {
Playerheads.PlayerBase64.remove(event.getPlayer().getUniqueId().toString());
Playerheads.refreshPlayerDatafile();
}
@EventHandler
public void onHeadPlace(BlockPlaceEvent event) {
ItemStack i = event.getItemInHand();
Block b = event.getBlockPlaced();
if (b.getType() == Material.LEGACY_SKULL && i.getType() == Material.LEGACY_SKULL_ITEM) {
String s = i.getItemMeta().getDisplayName();
if (s == null)
s = "Head";
Playerheads.HeadsPlaced.put(StringUtils.loc2str(b.getLocation()), s);
Playerheads.refreshHeadBlockDataFile();
}
}
@EventHandler
public void onHeadBreak(BlockBreakEvent event) {
Block b = event.getBlock();
if (b.getType() == Material.PLAYER_HEAD || b.getType() == Material.PLAYER_WALL_HEAD)
for (String sloc : Playerheads.HeadsPlaced.keySet()) {
Location loc = StringUtils.str2loc(sloc);
Object blockState = b.getState();
boolean isSkull = blockState instanceof Skull;
if (loc.equals(b.getLocation()) && isSkull) {
ItemStack i = ReflectionHandler.getSkullItem((Skull) blockState);
ItemMeta im = i.getItemMeta();
im.setDisplayName(Playerheads.HeadsPlaced.get(sloc));
i.setItemMeta(im);
if (event.getPlayer().getGameMode() != GameMode.CREATIVE)
loc.getWorld().dropItem(loc, i);
Playerheads.HeadsPlaced.remove(sloc);
Playerheads.refreshHeadBlockDataFile();
event.setCancelled(true);
b.setType(Material.AIR);
}
}
}
@EventHandler
public void onPistonMoveHead(BlockPistonExtendEvent event) {
for (Block b : event.getBlocks()) {
if (b.getType() == Material.LEGACY_SKULL)
for (String sloc : Playerheads.HeadsPlaced.keySet()) {
Location loc = StringUtils.str2loc(sloc);
GameProfile gp = ReflectionHandler.getGameProfile(b);
if (loc.equals(b.getLocation()) && gp != null) {
ItemStack i = ReflectionHandler.getSkull(gp);
ItemMeta im = i.getItemMeta();
im.setDisplayName(Playerheads.HeadsPlaced.get(sloc));
i.setItemMeta(im);
loc.getWorld().dropItem(loc, i);
Playerheads.HeadsPlaced.remove(sloc);
Playerheads.refreshHeadBlockDataFile();
b.setType(Material.AIR, true);
}
}
}*/
}
}

View File

@ -0,0 +1,209 @@
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.warp.cachedplayerheads;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import net.skinsrestorer.shared.exception.SkinRequestException;
import net.skinsrestorer.shared.storage.Locale;
import net.skinsrestorer.shared.storage.SkinStorage;
import net.skinsrestorer.shared.utils.MetricsCounter;
import net.skinsrestorer.shared.utils.Property;
public class MojangAPI {
private static final String UUID_URL = "https://api.minetools.eu/uuid/%name%";
private static final String UUID_URL_MOJANG = "https://api.mojang.com/users/profiles/minecraft/%name%";
private static final String UUID_URL_BACKUP = "https://api.ashcon.app/mojang/v2/user/%name%";
private static final String SKIN_URL = "https://api.minetools.eu/profile/%uuid%";
private static final String SKIN_URL_MOJANG = "https://sessionserver.mojang.com/session/minecraft/profile/%uuid%?unsigned=false";
private static final String SKIN_URL_BACKUP = "https://api.ashcon.app/mojang/v2/user/%uuid%";
private SkinStorage skinStorage;
public MojangAPI() {
}
public Object getSkinProperty(String uuid) {
return this.getSkinProperty(uuid, true);
}
public Object getSkinProperty(String uuid, boolean tryNext) {
try {
String output = this.readURL("https://api.minetools.eu/profile/%uuid%".replace("%uuid%", uuid));
JsonObject obj = (JsonObject)(new Gson()).fromJson(output, JsonObject.class);
Property property = new Property();
if (obj.has("raw")) {
JsonObject raw = obj.getAsJsonObject("raw");
if (raw.has("status") && raw.get("status").getAsString().equalsIgnoreCase("ERR")) {
return this.getSkinPropertyMojang(uuid);
}
if (property.valuesFromJson(raw)) {
return this.getSkinStorage().createProperty("textures", property.getValue(), property.getSignature());
}
}
} catch (Exception var7) {
if (tryNext) {
return this.getSkinPropertyMojang(uuid);
}
}
return null;
}
public Object getSkinPropertyMojang(String uuid) {
return this.getSkinPropertyMojang(uuid, true);
}
public Object getSkinPropertyMojang(String uuid, boolean tryNext) {
if (tryNext) {
System.out.println("Trying Mojang API to get skin property for " + uuid + ".");
}
try {
String output = this.readURL("https://sessionserver.mojang.com/session/minecraft/profile/%uuid%?unsigned=false".replace("%uuid%", uuid));
JsonObject obj = (JsonObject)(new Gson()).fromJson(output, JsonObject.class);
Property property = new Property();
if (obj.has("properties") && property.valuesFromJson(obj)) {
return this.getSkinStorage().createProperty("textures", property.getValue(), property.getSignature());
}
} catch (Exception var6) {
if (tryNext) {
return this.getSkinPropertyBackup(uuid);
}
}
return null;
}
public Object getSkinPropertyBackup(String uuid) {
return this.getSkinPropertyBackup(uuid, true);
}
public Object getSkinPropertyBackup(String uuid, boolean tryNext) {
if (tryNext) {
System.out.println("Trying backup API to get skin property for " + uuid + ".");
}
try {
String output = this.readURL("https://api.ashcon.app/mojang/v2/user/%uuid%".replace("%uuid%", uuid), 10000);
JsonObject obj = (JsonObject)(new Gson()).fromJson(output, JsonObject.class);
JsonObject textures = obj.get("textures").getAsJsonObject();
JsonObject rawTextures = textures.get("raw").getAsJsonObject();
Property property = new Property();
property.setValue(rawTextures.get("value").getAsString());
property.setSignature(rawTextures.get("signature").getAsString());
return this.getSkinStorage().createProperty("textures", property.getValue(), property.getSignature());
} catch (Exception var8) {
System.err.println("Failed to get skin property from backup API. (" + uuid + ")");
return null;
}
}
public String getUUID(String name, boolean tryNext) throws SkinRequestException {
try {
String output = this.readURL("https://api.minetools.eu/uuid/%name%".replace("%name%", name));
JsonObject obj = (JsonObject)(new Gson()).fromJson(output, JsonObject.class);
if (obj.has("status") && obj.get("status").getAsString().equalsIgnoreCase("ERR")) {
return this.getUUIDMojang(name);
} else if (obj.get("id") == null) {
throw new SkinRequestException(Locale.NOT_PREMIUM);
} else {
return obj.get("id").getAsString();
}
} catch (IOException var5) {
return tryNext ? this.getUUIDMojang(name) : null;
}
}
public String getUUIDMojang(String name) throws SkinRequestException {
return this.getUUIDMojang(name, true);
}
public String getUUIDMojang(String name, boolean tryNext) throws SkinRequestException {
if (tryNext) {
System.out.println("Trying Mojang API to get UUID for player " + name + ".");
}
try {
String output = this.readURL("https://api.mojang.com/users/profiles/minecraft/%name%".replace("%name%", name));
if (output.isEmpty()) {
throw new SkinRequestException(Locale.NOT_PREMIUM);
} else {
JsonObject obj = (JsonObject)(new Gson()).fromJson(output, JsonObject.class);
if (obj.has("error")) {
return tryNext ? this.getUUIDBackup(name) : null;
} else {
return obj.get("id").getAsString();
}
}
} catch (IOException var5) {
return tryNext ? this.getUUIDBackup(name) : null;
}
}
public String getUUIDBackup(String name) throws SkinRequestException {
return this.getUUIDBackup(name, true);
}
public String getUUIDBackup(String name, boolean tryNext) throws SkinRequestException {
if (tryNext) {
System.out.println("Trying backup API to get UUID for player " + name + ".");
}
try {
String output = this.readURL("https://api.ashcon.app/mojang/v2/user/%name%".replace("%name%", name), 10000);
JsonObject obj = (JsonObject)(new Gson()).fromJson(output, JsonObject.class);
if (obj.has("code")) {
if (obj.get("error").getAsString().equalsIgnoreCase("Not Found")) {
throw new SkinRequestException(Locale.NOT_PREMIUM);
} else {
throw new SkinRequestException(Locale.ALT_API_FAILED);
}
} else {
return obj.get("uuid").getAsString().replace("-", "");
}
} catch (IOException var5) {
throw new SkinRequestException(Locale.NOT_PREMIUM);
}
}
private String readURL(String url) throws IOException {
return this.readURL(url, 5000);
}
private String readURL(String url, int timeout) throws IOException {
HttpURLConnection con = (HttpURLConnection)(new URL(url)).openConnection();
MetricsCounter.incrAPI(url);
con.setRequestMethod("GET");
con.setRequestProperty("User-Agent", "SkinsRestorer");
con.setConnectTimeout(timeout);
con.setReadTimeout(timeout);
con.setDoOutput(true);
StringBuilder output = new StringBuilder();
BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
String line;
while((line = in.readLine()) != null) {
output.append(line);
}
in.close();
return output.toString();
}
public SkinStorage getSkinStorage() {
return this.skinStorage;
}
public void setSkinStorage(final SkinStorage skinStorage) {
this.skinStorage = skinStorage;
}
}

View File

@ -0,0 +1,150 @@
package org.warp.cachedplayerheads;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import net.skinsrestorer.api.SkinsRestorerAPI;
import net.skinsrestorer.bukkit.SkinsRestorer;
import org.bukkit.command.PluginCommand;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.Nullable;
public final class Playerheads extends JavaPlugin {
private FileConfiguration config = getConfig();
static ConcurrentHashMap<String, String> PlayerBase64 = new ConcurrentHashMap<>();
static ConcurrentHashMap<String, String> HeadsPlaced = new ConcurrentHashMap<>();
// Setting definition
static SkinsRestorer skinsRestorer;
@Nullable
static SkinsRestorerAPI skinsRestorerAPI;
static MojangAPI mojangAPI;
static BungeeAPI bungeeAPI;
static FileConfiguration playerDataConfig;
static File playerDataFile;
private static File headBlockDataFile;
boolean dropPlayerOption;
boolean dropOtherDeathOption;
boolean givePermissionOption;
boolean lockdown;
public void onEnable() {
//Connecting to the main SkinsRestorer API
skinsRestorer = JavaPlugin.getPlugin(SkinsRestorer.class);
// Connecting to Bukkit API for applying the skin
skinsRestorerAPI = SkinsRestorerAPI.getApi();
mojangAPI = new MojangAPI();
mojangAPI.setSkinStorage(new BukkitSkinStorage());
bungeeAPI = new BungeeAPI(this);
PluginCommand cmd = getCommand("playerheads");
cmd.setExecutor(new CommandExecutor(this));
cmd.setTabCompleter(new TabCompleter());
new EventListener(this);
createPlayerData();
createHeadBlockData();
reloadHeadBlockDataToHashMap();
this.config.addDefault("Should player heads drop when a player is killed by another player?", Boolean.valueOf(true));
this.config.addDefault("Should player heads drop whenever a player gets killed? (excluding being killed by players)", Boolean.valueOf(false));
this.config.addDefault("Do players need the give permission to use /playerheads give command?)", Boolean.valueOf(true));
this.config.options().copyDefaults(true);
saveConfig();
this.dropPlayerOption = this.config.getBoolean("Should player heads drop when a player is killed by another player?");
this.dropOtherDeathOption = this.config.getBoolean("Should player heads drop whenever a player gets killed? (excluding being killed by players)");
this.givePermissionOption = this.config.getBoolean("Do players need the give permission to use /playerheads give command?)");
getLogger().info("Player Heads Plugin has been enabled");
}
public void onDisable() {
getLogger().info("Player Heads Plugin has been disabled");
}
private static synchronized void createPlayerData() {
try {
File folder = new File("plugins/CachedPlayerHeads");
if (!folder.exists())
folder.mkdirs();
playerDataFile = new File(folder, "PlayerData.yml");
if (!playerDataFile.exists())
playerDataFile.createNewFile();
} catch (Exception e) {
e.printStackTrace();
}
}
private static synchronized void createHeadBlockData() {
try {
File folder = new File("plugins/CachedPlayerHeads");
if (!folder.exists())
folder.mkdirs();
headBlockDataFile = new File(folder, "HeadBlockData.yml");
if (!headBlockDataFile.exists())
headBlockDataFile.createNewFile();
} catch (Exception e) {
e.printStackTrace();
}
}
static synchronized void refreshPlayerDatafile() {
playerDataFile.delete();
createPlayerData();
playerDataConfig = (FileConfiguration)YamlConfiguration.loadConfiguration(playerDataFile);
for (String s : PlayerBase64.keySet()) {
String gt = PlayerBase64.get(s);
playerDataConfig.set(s, gt);
}
try {
playerDataConfig.save(playerDataFile);
} catch (IOException e) {
e.printStackTrace();
}
}
static synchronized void refreshHeadBlockDataFile() {
headBlockDataFile.delete();
createPlayerData();
YamlConfiguration yamlConfiguration = YamlConfiguration.loadConfiguration(headBlockDataFile);
for (String s : HeadsPlaced.keySet()) {
String gt = HeadsPlaced.get(s);
yamlConfiguration.set(s, gt);
}
try {
yamlConfiguration.save(headBlockDataFile);
} catch (IOException e) {
e.printStackTrace();
}
}
private static synchronized void reloadHeadBlockDataToHashMap() {
YamlConfiguration yamlConfiguration = YamlConfiguration.loadConfiguration(headBlockDataFile);
for (String s : yamlConfiguration.getKeys(false)) {
String gt = yamlConfiguration.getString(s);
HeadsPlaced.put(s, gt);
}
try {
yamlConfiguration.save(headBlockDataFile);
} catch (IOException e) {
e.printStackTrace();
}
}
}

View File

@ -0,0 +1,107 @@
package org.warp.cachedplayerheads;
import com.destroystokyo.paper.profile.PlayerProfile;
import com.mojang.authlib.properties.Property;
import dev.dbassett.skullcreator.SkullCreator;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Function;
import net.skinsrestorer.shared.exception.SkinRequestException;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.SkullMeta;
class ReflectionHandler {
private static final ExecutorService blockingTextures = Executors.newFixedThreadPool(4);
static CompletableFuture<ItemStack> getSkullItem(Player player) {
return ReflectionHandler
.getTexture(player)
.thenComposeAsync(texture -> getSkullItemFromTexture(player.getName(), texture), blockingTextures);
}
static CompletableFuture<ItemStack> getSkullItem(Player skinPlayer, Player namePlayer) {
return ReflectionHandler
.getTexture(skinPlayer)
.thenComposeAsync(texture -> getSkullItemFromTexture(namePlayer.getName(), texture), blockingTextures);
}
public static CompletableFuture<ItemStack> getSkullItem(String playerName) {
return ReflectionHandler
.getTexture(playerName)
.thenComposeAsync(texture -> getSkullItemFromTexture(playerName, texture), blockingTextures);
}
public static CompletableFuture<ItemStack> getSkullItem(String skinPlayerName, String namePlayerName) {
return ReflectionHandler
.getTexture(skinPlayerName)
.thenComposeAsync(texture -> getSkullItemFromTexture(namePlayerName, texture), blockingTextures);
}
public static CompletableFuture<ItemStack> getSkullItemFromTexture(String playerName, String texture) {
return CompletableFuture
.supplyAsync(() -> {
ItemStack item;
if (texture == null) {
// hardcoded steve skin
item = SkullCreator.itemFromBase64("eyJ0aW1lc3RhbXAiOjE1MDA4MzU1ODY5ODcsInByb2ZpbGVJZCI6ImMxZWQ5N2Q0ZDE2NzQyYzI5OGI1ODFiZmRiODhhMjFmIiwicHJvZmlsZU5hbWUiOiJ5b2xvX21hdGlzIiwic2lnbmF0dXJlUmVxdWlyZWQiOnRydWUsInRleHR1cmVzIjp7IlNLSU4iOnsidXJsIjoiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS9jYjNjY2NkOTUzNjVjNWQ4NTY0NGE1MzZlNjliNGJlNThlYmZiZjE2ZjQzY2Y1NjE3ODZiNzRkYTJiOGVlYiJ9fX0");
} else {
item = SkullCreator.itemFromBase64(texture);
}
SkullMeta skullMeta = (SkullMeta) item.getItemMeta();
PlayerProfile playerProfile = skullMeta.getPlayerProfile();
if (playerProfile != null) {
playerProfile.setName(playerName);
skullMeta.setPlayerProfile(playerProfile);
}
item.setItemMeta(skullMeta);
ItemMeta itemMeta = item.getItemMeta();
item.setItemMeta(itemMeta);
return item;
}, blockingTextures);
}
static CompletableFuture<String> getTexture(Player player) {
return getTexture(player.getName());
}
static CompletableFuture<String> getTexture(String playerName) {
try {
Function<String, CompletableFuture<String>> downloader = texture -> CompletableFuture
.supplyAsync(() -> {
if (texture != null) {
return texture;
}
// Try to download online skin as fallback
String uuid = null;
try {
uuid = Playerheads.mojangAPI.getUUID(playerName, true);
} catch (SkinRequestException e) {
return null;
}
Property prop = ((Property) Playerheads.mojangAPI.getSkinProperty(uuid));
if (prop != null) {
return prop.getValue();
}
return null;
}, blockingTextures);
if (Playerheads.skinsRestorerAPI != null) {
return CompletableFuture
.supplyAsync(() ->
SkinsRestorerAPIUtils.getSkinData(Playerheads.skinsRestorerAPI, playerName), blockingTextures)
.thenComposeAsync(downloader, blockingTextures);
} else if (Playerheads.skinsRestorer.isBungeeEnabled()) {
return Playerheads.bungeeAPI.getPlayerTexture(playerName)
.thenComposeAsync(downloader, blockingTextures);
} else {
return downloader.apply(null);
}
} catch (Exception ex) {
ex.printStackTrace();
return CompletableFuture.completedFuture(null);
}
}
}

View File

@ -0,0 +1,27 @@
package org.warp.cachedplayerheads;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import net.skinsrestorer.api.SkinsRestorerAPI;
public class SkinsRestorerAPIUtils {
public static String getSkinData(SkinsRestorerAPI skinsRestorerAPI, String playerName) {
if (skinsRestorerAPI == null) return null;
// Use SkinRestorer's skin username if the player has set it
String transformedPlayerName = skinsRestorerAPI.getSkinName(playerName);
if (transformedPlayerName == null) {
transformedPlayerName = playerName;
}
Object skinData = skinsRestorerAPI.getSkinData(transformedPlayerName);
if (skinData != null) {
try {
Method getValueMethod = skinData.getClass().getMethod("getValue");
return (String) getValueMethod.invoke(skinData);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
return null;
}
}

View File

@ -0,0 +1,15 @@
package org.warp.cachedplayerheads;
import org.bukkit.Bukkit;
import org.bukkit.Location;
class StringUtils {
static Location str2loc(String str) {
String[] str2loc = str.split(":");
return new Location(Bukkit.getServer().getWorld(str2loc[0]), Double.parseDouble(str2loc[1]), Double.parseDouble(str2loc[2]), Double.parseDouble(str2loc[3]));
}
static String loc2str(Location loc) {
return loc.getWorld().getName() + ":" + loc.getBlockX() + ":" + loc.getBlockY() + ":" + loc.getBlockZ();
}
}

View File

@ -0,0 +1,24 @@
package org.warp.cachedplayerheads;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
public class TabCompleter implements org.bukkit.command.TabCompleter {
public List<String> onTabComplete(CommandSender commandSender, Command cmd, String commandLabel, String[] args) {
if (cmd.getName().equalsIgnoreCase("playerheads")) {
int argsLength = args.length;
if (argsLength == 1) {
ArrayList<String> listFinal = new ArrayList<>();
String al = args[0].toLowerCase();
if ("give".startsWith(al))
listFinal.add("give");
return listFinal;
}
if (argsLength > 3)
return new ArrayList<>();
}
return null;
}
}

View File

@ -0,0 +1,117 @@
package org.warp.cachedplayerheads.bungee;
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import java.util.UUID;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.connection.Server;
import net.md_5.bungee.api.event.PluginMessageEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.event.EventHandler;
import net.skinsrestorer.api.SkinsRestorerAPI;
import org.warp.cachedplayerheads.SkinsRestorerAPIUtils;
public class BungeeAPI implements Listener {
private final Plugin playerheads;
private final SkinsRestorerAPI skinsRestorerAPI;
public BungeeAPI(SkinsRestorerAPI skinsRestorerAPI, Plugin playerheads) {
this.skinsRestorerAPI = skinsRestorerAPI;
this.playerheads = playerheads;
playerheads.getProxy().getPluginManager().registerListener(playerheads, this);
}
public void sendSkinData(Sender sender, UUID reqUUID, String skinData) {
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF("PlayerTextureResponse");
out.writeLong(reqUUID.getMostSignificantBits());
out.writeLong(reqUUID.getLeastSignificantBits());
if (skinData == null) {
out.writeBoolean(false);
} else {
out.writeBoolean(true);
out.writeUTF(skinData);
}
// we send the data to the server
// using ServerInfo the packet is being queued if there are no players in the server
// using only the server to send data the packet will be lost if no players are in it
System.out.println("Replying skin request: " + reqUUID);
sender.sendData("cachedplayerheads:channel", out.toByteArray());
}
@EventHandler
public void on(PluginMessageEvent event) {
if (!event.getTag().equals("cachedplayerheads:channel")) {
return;
}
ByteArrayDataInput in = ByteStreams.newDataInput(event.getData());
String subChannel = in.readUTF();
long msb = in.readLong();
long lsb = in.readLong();
UUID reqUUID = new UUID(msb, lsb);
String playerName = in.readUTF();
System.out.println("Received skin request: " + reqUUID);
if (subChannel.equalsIgnoreCase("PlayerTextureRequest")) {
Sender sender;
// the receiver is a server when the proxy talks to a server
if (event.getReceiver() instanceof Server) {
Server receiver = (Server) event.getReceiver();
sender = new ServerSender(receiver);
}
// the receiver is a ProxiedPlayer when a server talks to the proxy
else if (event.getReceiver() instanceof ProxiedPlayer) {
ProxiedPlayer receiver = (ProxiedPlayer) event.getReceiver();
sender = new ProxiedPlayerSender(receiver);
} else {
System.err.println("Invalid receiver type: " + event.getReceiver());
sender = null;
}
if (sender != null) {
playerheads.getProxy().getScheduler().runAsync(playerheads, () -> {
String skinData = SkinsRestorerAPIUtils.getSkinData(skinsRestorerAPI, playerName);
sendSkinData(sender, reqUUID, skinData);
});
}
} else {
System.err.println("Invalid subchannel: " + subChannel);
}
}
private interface Sender {
void sendData(String channel, byte[] data);
}
private static class ServerSender implements Sender {
private final Server server;
public ServerSender(Server server) {
this.server = server;
}
@Override
public void sendData(String channel, byte[] data) {
server.sendData(channel, data);
}
}
private static class ProxiedPlayerSender implements Sender {
private final ProxiedPlayer proxiedPlayer;
public ProxiedPlayerSender(ProxiedPlayer proxiedPlayer) {
this.proxiedPlayer = proxiedPlayer;
}
@Override
public void sendData(String channel, byte[] data) {
proxiedPlayer.getServer().sendData(channel, data);
}
}
}

View File

@ -0,0 +1,21 @@
package org.warp.cachedplayerheads.bungee;
import net.md_5.bungee.api.plugin.Plugin;
import net.skinsrestorer.api.SkinsRestorerAPI;
public class Playerheads extends Plugin {
// Setting definition
static SkinsRestorerAPI skinsRestorerAPI;
private BungeeAPI api;
@Override
public void onEnable() {
skinsRestorerAPI = SkinsRestorerAPI.getApi();
getProxy().registerChannel("cachedplayerheads:channel");
api = new BungeeAPI(skinsRestorerAPI, this);
}
}

View File

@ -0,0 +1,4 @@
name: ${project.name}
main: ${project.groupId}.cachedplayerheads.bungee.Playerheads
version: 2.1
author: Cavallium

View File

@ -0,0 +1,16 @@
name: ${project.name}
main: ${project.groupId}.cachedplayerheads.Playerheads
api-version: 1.16
version: 2.1
author: Cavallium
load: POSTWORLD
depend:
- SkinsRestorer
commands:
playerheads:
description: Allows you to give players, player heads.
usage: "Usage:\n/playerheads give <PlayerName> <SkinName/Base64>"
permissions:
vendettacraft.movableblocks.give:
description: Allows you to give players, player heads via the give command.
default: op

29
start.sh Normal file
View File

@ -0,0 +1,29 @@
WORKSPACE=".papermc"
MC_VERSION="1.16.4"
PAPER_BUILD="latest"
## ============== DO NOT EDIT THE SCRIPT BELOW UNLESS YOU KNOW WHAT YOU ARE DOING ============== ##
#cd || exit # Moving to the user folder or exit if it fails.
[ -d $WORKSPACE ] || mkdir $WORKSPACE
[ -d $WORKSPACE ] || mkdir $WORKSPACE/plugins
cp target/CachedPlayerHeads-*.jar $WORKSPACE/plugins || exit
# Checking the workspace folder availability.
if [ ! -d $WORKSPACE ]; then
# Create the workspace folder.
mkdir $WORKSPACE
fi
cd $WORKSPACE || exit # Moving to the workspace fodler or exit if it fails.
# Check for the paper executable
PAPER_JAR="paper-$MC_VERSION-$PAPER_BUILD.jar"
PAPER_LNK="https://papermc.io/api/v1/paper/$MC_VERSION/$PAPER_BUILD/download"
if [ ! -f $PAPER_JAR ]; then
wget -O $PAPER_JAR $PAPER_LNK
fi
/usr/lib/jvm/java-1.8.0-openjdk-amd64/bin/java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -jar $PAPER_JAR nogui