First commit
This commit is contained in:
parent
5384bdd91d
commit
9d1f489996
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal 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
17
README.md
Normal 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
143
pom.xml
Normal 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>
|
@ -0,0 +1,10 @@
|
|||||||
|
package org.warp.cachedplayerheads;
|
||||||
|
|
||||||
|
import net.skinsrestorer.shared.storage.SkinStorage;
|
||||||
|
|
||||||
|
public class BukkitSkinStorage extends SkinStorage {
|
||||||
|
|
||||||
|
public BukkitSkinStorage() {
|
||||||
|
super(Platform.BUKKIT);
|
||||||
|
}
|
||||||
|
}
|
107
src/main/java/org/warp/cachedplayerheads/BungeeAPI.java
Normal file
107
src/main/java/org/warp/cachedplayerheads/BungeeAPI.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
124
src/main/java/org/warp/cachedplayerheads/EventListener.java
Normal file
124
src/main/java/org/warp/cachedplayerheads/EventListener.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
}
|
209
src/main/java/org/warp/cachedplayerheads/MojangAPI.java
Normal file
209
src/main/java/org/warp/cachedplayerheads/MojangAPI.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
150
src/main/java/org/warp/cachedplayerheads/Playerheads.java
Normal file
150
src/main/java/org/warp/cachedplayerheads/Playerheads.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
107
src/main/java/org/warp/cachedplayerheads/ReflectionHandler.java
Normal file
107
src/main/java/org/warp/cachedplayerheads/ReflectionHandler.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
15
src/main/java/org/warp/cachedplayerheads/StringUtils.java
Normal file
15
src/main/java/org/warp/cachedplayerheads/StringUtils.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
24
src/main/java/org/warp/cachedplayerheads/TabCompleter.java
Normal file
24
src/main/java/org/warp/cachedplayerheads/TabCompleter.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
117
src/main/java/org/warp/cachedplayerheads/bungee/BungeeAPI.java
Normal file
117
src/main/java/org/warp/cachedplayerheads/bungee/BungeeAPI.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
4
src/main/resources/bungee.yml
Normal file
4
src/main/resources/bungee.yml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
name: ${project.name}
|
||||||
|
main: ${project.groupId}.cachedplayerheads.bungee.Playerheads
|
||||||
|
version: 2.1
|
||||||
|
author: Cavallium
|
16
src/main/resources/plugin.yml
Normal file
16
src/main/resources/plugin.yml
Normal 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
29
start.sh
Normal 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
|
Loading…
Reference in New Issue
Block a user