forked from GeyserMC/Geyser
Fix capes (#436)
* fix capes * remove gson * clean up * clean up * formatting code * Made the changes fit a bit better into the already existing code * Throw the nullptr before the download complete message, making debugging skins less confusing Co-authored-by: Tim203 <mctim203@gmail.com>
This commit is contained in:
parent
7a9fff1a64
commit
82433f8078
3 changed files with 51 additions and 20 deletions
|
@ -101,6 +101,8 @@ public class PlayerEntity extends LivingEntity {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendPlayer(GeyserSession session) {
|
public void sendPlayer(GeyserSession session) {
|
||||||
|
if(session.getEntityCache().getPlayerEntity(uuid) == null)
|
||||||
|
return;
|
||||||
if (getLastSkinUpdate() == -1) {
|
if (getLastSkinUpdate() == -1) {
|
||||||
if (playerList) {
|
if (playerList) {
|
||||||
PlayerListPacket playerList = new PlayerListPacket();
|
PlayerListPacket playerList = new PlayerListPacket();
|
||||||
|
|
|
@ -25,16 +25,22 @@
|
||||||
|
|
||||||
package org.geysermc.connector.utils;
|
package org.geysermc.connector.utils;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
import org.geysermc.connector.GeyserConnector;
|
import org.geysermc.connector.GeyserConnector;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Base64;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.*;
|
||||||
|
@ -52,6 +58,7 @@ public class SkinProvider {
|
||||||
private static Map<String, Cape> cachedCapes = new ConcurrentHashMap<>();
|
private static Map<String, Cape> cachedCapes = new ConcurrentHashMap<>();
|
||||||
private static Map<String, CompletableFuture<Cape>> requestedCapes = new ConcurrentHashMap<>();
|
private static Map<String, CompletableFuture<Cape>> requestedCapes = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||||
private static final int CACHE_INTERVAL = 8 * 60 * 1000; // 8 minutes
|
private static final int CACHE_INTERVAL = 8 * 60 * 1000; // 8 minutes
|
||||||
|
|
||||||
public static boolean hasSkinCached(UUID uuid) {
|
public static boolean hasSkinCached(UUID uuid) {
|
||||||
|
@ -74,9 +81,10 @@ public class SkinProvider {
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
long time = System.currentTimeMillis();
|
long time = System.currentTimeMillis();
|
||||||
|
|
||||||
|
CapeProvider provider = capeUrl != null ? CapeProvider.MINECRAFT : null;
|
||||||
SkinAndCape skinAndCape = new SkinAndCape(
|
SkinAndCape skinAndCape = new SkinAndCape(
|
||||||
getOrDefault(requestSkin(playerId, skinUrl, false), EMPTY_SKIN, 5),
|
getOrDefault(requestSkin(playerId, skinUrl, false), EMPTY_SKIN, 5),
|
||||||
getOrDefault(requestCape(capeUrl, false), EMPTY_CAPE, 5)
|
getOrDefault(requestCape(capeUrl, provider, false), EMPTY_CAPE, 5)
|
||||||
);
|
);
|
||||||
|
|
||||||
GeyserConnector.getInstance().getLogger().debug("Took " + (System.currentTimeMillis() - time) + "ms for " + playerId);
|
GeyserConnector.getInstance().getLogger().debug("Took " + (System.currentTimeMillis() - time) + "ms for " + playerId);
|
||||||
|
@ -112,11 +120,11 @@ public class SkinProvider {
|
||||||
return future;
|
return future;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CompletableFuture<Cape> requestCape(String capeUrl, boolean newThread) {
|
public static CompletableFuture<Cape> requestCape(String capeUrl, CapeProvider provider, boolean newThread) {
|
||||||
if (capeUrl == null || capeUrl.isEmpty()) return CompletableFuture.completedFuture(EMPTY_CAPE);
|
if (capeUrl == null || capeUrl.isEmpty()) return CompletableFuture.completedFuture(EMPTY_CAPE);
|
||||||
if (requestedCapes.containsKey(capeUrl)) return requestedCapes.get(capeUrl); // already requested
|
if (requestedCapes.containsKey(capeUrl)) return requestedCapes.get(capeUrl); // already requested
|
||||||
|
|
||||||
boolean officialCape = capeUrl.startsWith("https://textures.minecraft.net");
|
boolean officialCape = provider == CapeProvider.MINECRAFT;
|
||||||
boolean validCache = (System.currentTimeMillis() - CACHE_INTERVAL) < cachedCapes.getOrDefault(capeUrl, EMPTY_CAPE).getRequestedOn();
|
boolean validCache = (System.currentTimeMillis() - CACHE_INTERVAL) < cachedCapes.getOrDefault(capeUrl, EMPTY_CAPE).getRequestedOn();
|
||||||
|
|
||||||
if ((cachedCapes.containsKey(capeUrl) && officialCape) || validCache) {
|
if ((cachedCapes.containsKey(capeUrl) && officialCape) || validCache) {
|
||||||
|
@ -126,14 +134,14 @@ public class SkinProvider {
|
||||||
|
|
||||||
CompletableFuture<Cape> future;
|
CompletableFuture<Cape> future;
|
||||||
if (newThread) {
|
if (newThread) {
|
||||||
future = CompletableFuture.supplyAsync(() -> supplyCape(capeUrl), EXECUTOR_SERVICE)
|
future = CompletableFuture.supplyAsync(() -> supplyCape(capeUrl, provider), EXECUTOR_SERVICE)
|
||||||
.whenCompleteAsync((cape, throwable) -> {
|
.whenCompleteAsync((cape, throwable) -> {
|
||||||
cachedCapes.put(capeUrl, cape);
|
cachedCapes.put(capeUrl, cape);
|
||||||
requestedCapes.remove(capeUrl);
|
requestedCapes.remove(capeUrl);
|
||||||
});
|
});
|
||||||
requestedCapes.put(capeUrl, future);
|
requestedCapes.put(capeUrl, future);
|
||||||
} else {
|
} else {
|
||||||
Cape cape = supplyCape(capeUrl); // blocking
|
Cape cape = supplyCape(capeUrl, provider); // blocking
|
||||||
future = CompletableFuture.completedFuture(cape);
|
future = CompletableFuture.completedFuture(cape);
|
||||||
cachedCapes.put(capeUrl, cape);
|
cachedCapes.put(capeUrl, cape);
|
||||||
}
|
}
|
||||||
|
@ -143,9 +151,9 @@ public class SkinProvider {
|
||||||
public static CompletableFuture<Cape> requestUnofficialCape(Cape officialCape, UUID playerId,
|
public static CompletableFuture<Cape> requestUnofficialCape(Cape officialCape, UUID playerId,
|
||||||
String username, boolean newThread) {
|
String username, boolean newThread) {
|
||||||
if (officialCape.isFailed() && ALLOW_THIRD_PARTY_CAPES) {
|
if (officialCape.isFailed() && ALLOW_THIRD_PARTY_CAPES) {
|
||||||
for (UnofficalCape cape : UnofficalCape.VALUES) {
|
for (CapeProvider provider : CapeProvider.VALUES) {
|
||||||
Cape cape1 = getOrDefault(
|
Cape cape1 = getOrDefault(
|
||||||
requestCape(cape.getUrlFor(playerId, username), newThread),
|
requestCape(provider.getUrlFor(playerId, username), provider, newThread),
|
||||||
EMPTY_CAPE, 4
|
EMPTY_CAPE, 4
|
||||||
);
|
);
|
||||||
if (!cape1.isFailed()) {
|
if (!cape1.isFailed()) {
|
||||||
|
@ -159,15 +167,15 @@ public class SkinProvider {
|
||||||
private static Skin supplySkin(UUID uuid, String textureUrl) {
|
private static Skin supplySkin(UUID uuid, String textureUrl) {
|
||||||
byte[] skin = EMPTY_SKIN.getSkinData();
|
byte[] skin = EMPTY_SKIN.getSkinData();
|
||||||
try {
|
try {
|
||||||
skin = requestImage(textureUrl, false);
|
skin = requestImage(textureUrl, null);
|
||||||
} catch (Exception ignored) {} // just ignore I guess
|
} catch (Exception ignored) {} // just ignore I guess
|
||||||
return new Skin(uuid, textureUrl, skin, System.currentTimeMillis(), false);
|
return new Skin(uuid, textureUrl, skin, System.currentTimeMillis(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Cape supplyCape(String capeUrl) {
|
private static Cape supplyCape(String capeUrl, CapeProvider provider) {
|
||||||
byte[] cape = new byte[0];
|
byte[] cape = new byte[0];
|
||||||
try {
|
try {
|
||||||
cape = requestImage(capeUrl, true);
|
cape = requestImage(capeUrl, provider);
|
||||||
} catch (Exception ignored) {} // just ignore I guess
|
} catch (Exception ignored) {} // just ignore I guess
|
||||||
|
|
||||||
String[] urlSection = capeUrl.split("/"); // A real url is expected at this stage
|
String[] urlSection = capeUrl.split("/"); // A real url is expected at this stage
|
||||||
|
@ -181,11 +189,12 @@ public class SkinProvider {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] requestImage(String imageUrl, boolean cape) throws Exception {
|
private static byte[] requestImage(String imageUrl, CapeProvider provider) throws Exception {
|
||||||
BufferedImage image = ImageIO.read(new URL(imageUrl));
|
BufferedImage image = downloadImage(imageUrl, provider);
|
||||||
GeyserConnector.getInstance().getLogger().debug("Downloaded " + imageUrl);
|
GeyserConnector.getInstance().getLogger().debug("Downloaded " + imageUrl);
|
||||||
|
|
||||||
if (cape) {
|
// if the requested image is an cape
|
||||||
|
if (provider != null) {
|
||||||
image = image.getWidth() > 64 ? scale(image) : image;
|
image = image.getWidth() > 64 ? scale(image) : image;
|
||||||
BufferedImage newImage = new BufferedImage(64, 32, BufferedImage.TYPE_INT_RGB);
|
BufferedImage newImage = new BufferedImage(64, 32, BufferedImage.TYPE_INT_RGB);
|
||||||
Graphics g = newImage.createGraphics();
|
Graphics g = newImage.createGraphics();
|
||||||
|
@ -209,7 +218,25 @@ public class SkinProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BufferedImage scale (BufferedImage bufferedImage) {
|
private static BufferedImage downloadImage(String imageUrl, CapeProvider provider) throws IOException {
|
||||||
|
if (provider == CapeProvider.FIVEZIG)
|
||||||
|
return readFiveZigCape(imageUrl);
|
||||||
|
BufferedImage image = ImageIO.read(new URL(imageUrl));
|
||||||
|
if (image == null) throw new NullPointerException();
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BufferedImage readFiveZigCape(String url) throws IOException {
|
||||||
|
JsonNode element = OBJECT_MAPPER.readTree(WebUtils.getBody(url));
|
||||||
|
if (element != null && element.isObject()) {
|
||||||
|
JsonNode capeElement = element.get("d");
|
||||||
|
if (capeElement == null || capeElement.isNull()) return null;
|
||||||
|
return ImageIO.read(new ByteArrayInputStream(Base64.getDecoder().decode(capeElement.textValue())));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BufferedImage scale(BufferedImage bufferedImage) {
|
||||||
BufferedImage resized = new BufferedImage(bufferedImage.getWidth() / 2, bufferedImage.getHeight() / 2, BufferedImage.TYPE_INT_RGB);
|
BufferedImage resized = new BufferedImage(bufferedImage.getWidth() / 2, bufferedImage.getHeight() / 2, BufferedImage.TYPE_INT_RGB);
|
||||||
Graphics2D g2 = resized.createGraphics();
|
Graphics2D g2 = resized.createGraphics();
|
||||||
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||||
|
@ -262,14 +289,16 @@ public class SkinProvider {
|
||||||
* Sorted by 'priority'
|
* Sorted by 'priority'
|
||||||
*/
|
*/
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
@Getter
|
@Getter
|
||||||
public enum UnofficalCape {
|
public enum CapeProvider {
|
||||||
|
MINECRAFT,
|
||||||
OPTIFINE("http://s.optifine.net/capes/%s.png", CapeUrlType.USERNAME),
|
OPTIFINE("http://s.optifine.net/capes/%s.png", CapeUrlType.USERNAME),
|
||||||
LABYMOD("http://capes.labymod.net/capes/%s.png", CapeUrlType.UUID_DASHED),
|
LABYMOD("https://www.labymod.net/page/php/getCapeTexture.php?uuid=%s", CapeUrlType.UUID_DASHED),
|
||||||
FIVEZIG("http://textures.5zig.net/2/%s", CapeUrlType.UUID),
|
FIVEZIG("https://textures.5zigreborn.eu/profile/%s", CapeUrlType.UUID_DASHED),
|
||||||
MINECRAFTCAPES("https://www.minecraftcapes.co.uk/getCape/%s", CapeUrlType.UUID);
|
MINECRAFTCAPES("https://www.minecraftcapes.co.uk/getCape/%s", CapeUrlType.UUID);
|
||||||
|
|
||||||
public static final UnofficalCape[] VALUES = values();
|
public static final CapeProvider[] VALUES = Arrays.copyOfRange(values(), 1, 5);
|
||||||
private String url;
|
private String url;
|
||||||
private CapeUrlType type;
|
private CapeUrlType type;
|
||||||
|
|
||||||
|
|
|
@ -155,7 +155,7 @@ public class SkinUtils {
|
||||||
cape = SkinProvider.getOrDefault(SkinProvider.requestUnofficialCape(
|
cape = SkinProvider.getOrDefault(SkinProvider.requestUnofficialCape(
|
||||||
cape, entity.getUuid(),
|
cape, entity.getUuid(),
|
||||||
entity.getUsername(), false
|
entity.getUsername(), false
|
||||||
), SkinProvider.EMPTY_CAPE, SkinProvider.UnofficalCape.VALUES.length * 3);
|
), SkinProvider.EMPTY_CAPE, SkinProvider.CapeProvider.VALUES.length * 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entity.getLastSkinUpdate() < skin.getRequestedOn()) {
|
if (entity.getLastSkinUpdate() < skin.getRequestedOn()) {
|
||||||
|
|
Loading…
Reference in a new issue