Skin and skull fixes (#1923)

* Skin and skull fixes

- Handle the occasional greater-than-128-px skin
- Remove unused Jackson dependency
- Update used Jackson dependency
- Handle skin downloading on another thread

* Other small touchups

* Flush after rescaling
This commit is contained in:
Camotoy 2021-02-15 16:36:47 -05:00 committed by GitHub
parent f2550de633
commit 93a74d669d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 58 additions and 83 deletions

View file

@ -20,13 +20,7 @@
<dependency> <dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId> <groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId> <artifactId>jackson-dataformat-yaml</artifactId>
<version>2.9.8</version> <version>2.10.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.9.8</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency> <dependency>

View file

@ -77,7 +77,8 @@ public class GeyserConnector {
.enable(JsonParser.Feature.IGNORE_UNDEFINED) .enable(JsonParser.Feature.IGNORE_UNDEFINED)
.enable(JsonParser.Feature.ALLOW_COMMENTS) .enable(JsonParser.Feature.ALLOW_COMMENTS)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES); .enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES)
.enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES);
public static final String NAME = "Geyser"; public static final String NAME = "Geyser";
public static final String GIT_VERSION = "DEV"; // A fallback for running in IDEs public static final String GIT_VERSION = "DEV"; // A fallback for running in IDEs

View file

@ -33,6 +33,7 @@ import com.nukkitx.protocol.bedrock.packet.PlayerListPacket;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.common.AuthType;
import org.geysermc.connector.entity.player.PlayerEntity; import org.geysermc.connector.entity.player.PlayerEntity;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.session.auth.BedrockClientData; import org.geysermc.connector.network.session.auth.BedrockClientData;
@ -163,7 +164,7 @@ public class SkinManager {
geometry = SkinProvider.SkinGeometry.getEars(data.isAlex()); geometry = SkinProvider.SkinGeometry.getEars(data.isAlex());
// Store the skin and geometry for the ears // Store the skin and geometry for the ears
SkinProvider.storeEarSkin(entity.getUuid(), skin); SkinProvider.storeEarSkin(skin);
SkinProvider.storeEarGeometry(entity.getUuid(), data.isAlex()); SkinProvider.storeEarGeometry(entity.getUuid(), data.isAlex());
} }
} }
@ -267,7 +268,10 @@ public class SkinManager {
return new GameProfileData(skinUrl, capeUrl, isAlex); return new GameProfileData(skinUrl, capeUrl, isAlex);
} catch (Exception exception) { } catch (Exception exception) {
GeyserConnector.getInstance().getLogger().debug("Something went wrong while processing skin for " + profile.getName() + ": " + exception.getMessage()); GeyserConnector.getInstance().getLogger().debug("Something went wrong while processing skin for " + profile.getName());
if (GeyserConnector.getInstance().getConfig().isDebugMode()) {
exception.printStackTrace();
}
return loadBedrockOrOfflineSkin(profile); return loadBedrockOrOfflineSkin(profile);
} }
} }
@ -282,7 +286,7 @@ public class SkinManager {
String skinUrl = isAlex ? SkinProvider.EMPTY_SKIN_ALEX.getTextureUrl() : SkinProvider.EMPTY_SKIN.getTextureUrl(); String skinUrl = isAlex ? SkinProvider.EMPTY_SKIN_ALEX.getTextureUrl() : SkinProvider.EMPTY_SKIN.getTextureUrl();
String capeUrl = SkinProvider.EMPTY_CAPE.getTextureUrl(); String capeUrl = SkinProvider.EMPTY_CAPE.getTextureUrl();
if ("steve".equals(skinUrl) || "alex".equals(skinUrl)) { if (("steve".equals(skinUrl) || "alex".equals(skinUrl)) && GeyserConnector.getInstance().getAuthType() != AuthType.ONLINE) {
GeyserSession session = GeyserConnector.getInstance().getPlayerByUuid(profile.getId()); GeyserSession session = GeyserConnector.getInstance().getPlayerByUuid(profile.getId());
if (session != null) { if (session != null) {

View file

@ -79,13 +79,12 @@ public class SkinProvider {
.build(); .build();
private static final Map<String, CompletableFuture<Cape>> requestedCapes = new ConcurrentHashMap<>(); private static final Map<String, CompletableFuture<Cape>> requestedCapes = new ConcurrentHashMap<>();
public static final SkinGeometry EMPTY_GEOMETRY = SkinProvider.SkinGeometry.getLegacy(false);
private static final Map<UUID, SkinGeometry> cachedGeometry = new ConcurrentHashMap<>(); private static final Map<UUID, SkinGeometry> cachedGeometry = new ConcurrentHashMap<>();
public static final boolean ALLOW_THIRD_PARTY_EARS = GeyserConnector.getInstance().getConfig().isAllowThirdPartyEars(); public static final boolean ALLOW_THIRD_PARTY_EARS = GeyserConnector.getInstance().getConfig().isAllowThirdPartyEars();
public static String EARS_GEOMETRY; public static final String EARS_GEOMETRY;
public static String EARS_GEOMETRY_SLIM; public static final String EARS_GEOMETRY_SLIM;
public static SkinGeometry SKULL_GEOMETRY; public static final SkinGeometry SKULL_GEOMETRY;
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@ -229,15 +228,15 @@ public class SkinProvider {
return CompletableFuture.completedFuture(officialCape); return CompletableFuture.completedFuture(officialCape);
} }
public static CompletableFuture<Skin> requestEars(String earsUrl, EarsProvider provider, boolean newThread, Skin skin) { public static CompletableFuture<Skin> requestEars(String earsUrl, boolean newThread, Skin skin) {
if (earsUrl == null || earsUrl.isEmpty()) return CompletableFuture.completedFuture(skin); if (earsUrl == null || earsUrl.isEmpty()) return CompletableFuture.completedFuture(skin);
CompletableFuture<Skin> future; CompletableFuture<Skin> future;
if (newThread) { if (newThread) {
future = CompletableFuture.supplyAsync(() -> supplyEars(skin, earsUrl, provider), EXECUTOR_SERVICE) future = CompletableFuture.supplyAsync(() -> supplyEars(skin, earsUrl), EXECUTOR_SERVICE)
.whenCompleteAsync((outSkin, throwable) -> { }); .whenCompleteAsync((outSkin, throwable) -> { });
} else { } else {
Skin ears = supplyEars(skin, earsUrl, provider); // blocking Skin ears = supplyEars(skin, earsUrl); // blocking
future = CompletableFuture.completedFuture(ears); future = CompletableFuture.completedFuture(ears);
} }
return future; return future;
@ -255,7 +254,7 @@ public class SkinProvider {
public static CompletableFuture<Skin> requestUnofficialEars(Skin officialSkin, UUID playerId, String username, boolean newThread) { public static CompletableFuture<Skin> requestUnofficialEars(Skin officialSkin, UUID playerId, String username, boolean newThread) {
for (EarsProvider provider : EarsProvider.VALUES) { for (EarsProvider provider : EarsProvider.VALUES) {
Skin skin1 = getOrDefault( Skin skin1 = getOrDefault(
requestEars(provider.getUrlFor(playerId, username), provider, newThread, officialSkin), requestEars(provider.getUrlFor(playerId, username), newThread, officialSkin),
officialSkin, 4 officialSkin, 4
); );
if (skin1.isEars()) { if (skin1.isEars()) {
@ -295,12 +294,11 @@ public class SkinProvider {
} }
/** /**
* Stores the ajusted skin with the ear texture to the cache * Stores the adjusted skin with the ear texture to the cache
* *
* @param playerID The UUID to cache it against
* @param skin The skin to cache * @param skin The skin to cache
*/ */
public static void storeEarSkin(UUID playerID, Skin skin) { public static void storeEarSkin(Skin skin) {
cachedSkins.put(skin.getTextureUrl(), skin); cachedSkins.put(skin.getTextureUrl(), skin);
} }
@ -324,7 +322,7 @@ public class SkinProvider {
} }
private static Cape supplyCape(String capeUrl, CapeProvider provider) { private static Cape supplyCape(String capeUrl, CapeProvider provider) {
byte[] cape = new byte[0]; byte[] cape = EMPTY_CAPE.getCapeData();
try { try {
cape = requestImage(capeUrl, provider); cape = requestImage(capeUrl, provider);
} catch (Exception ignored) {} // just ignore I guess } catch (Exception ignored) {} // just ignore I guess
@ -334,7 +332,7 @@ public class SkinProvider {
return new Cape( return new Cape(
capeUrl, capeUrl,
urlSection[urlSection.length - 1], // get the texture id and use it as cape id urlSection[urlSection.length - 1], // get the texture id and use it as cape id
cape.length > 0 ? cape : EMPTY_CAPE.getCapeData(), cape,
System.currentTimeMillis(), System.currentTimeMillis(),
cape.length == 0 cape.length == 0
); );
@ -345,10 +343,9 @@ public class SkinProvider {
* *
* @param existingSkin The players current skin * @param existingSkin The players current skin
* @param earsUrl The URL to get the ears texture from * @param earsUrl The URL to get the ears texture from
* @param provider The ears texture provider
* @return The updated skin with ears * @return The updated skin with ears
*/ */
private static Skin supplyEars(Skin existingSkin, String earsUrl, EarsProvider provider) { private static Skin supplyEars(Skin existingSkin, String earsUrl) {
try { try {
// Get the ears texture // Get the ears texture
BufferedImage ears = ImageIO.read(new URL(earsUrl)); BufferedImage ears = ImageIO.read(new URL(earsUrl));
@ -415,14 +412,15 @@ public class SkinProvider {
// if the requested image is a cape // if the requested image is a cape
if (provider != null) { if (provider != null) {
while(image.getWidth() > 64) { if (image.getWidth() > 64) {
image = scale(image); image = scale(image, 64, 32);
}
} else {
// Very rarely, skins can be larger than Minecraft's default.
// Bedrock will not render anything above a width of 128.
if (image.getWidth() > 128) {
image = scale(image, 128, image.getHeight() / (image.getWidth() / 128));
} }
BufferedImage newImage = new BufferedImage(64, 32, BufferedImage.TYPE_INT_ARGB);
Graphics g = newImage.createGraphics();
g.drawImage(image, 0, 0, image.getWidth(), image.getHeight(), null);
g.dispose();
image = newImage;
} }
byte[] data = bufferedImageToImageData(image); byte[] data = bufferedImageToImageData(image);
@ -506,12 +504,13 @@ public class SkinProvider {
return null; return null;
} }
private static BufferedImage scale(BufferedImage bufferedImage) { private static BufferedImage scale(BufferedImage bufferedImage, int newWidth, int newHeight) {
BufferedImage resized = new BufferedImage(bufferedImage.getWidth() / 2, bufferedImage.getHeight() / 2, BufferedImage.TYPE_INT_ARGB); BufferedImage resized = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB);
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);
g2.drawImage(bufferedImage, 0, 0, bufferedImage.getWidth() / 2, bufferedImage.getHeight() / 2, null); g2.drawImage(bufferedImage, 0, 0, newWidth, newHeight, null);
g2.dispose(); g2.dispose();
bufferedImage.flush();
return resized; return resized;
} }
@ -579,17 +578,17 @@ public class SkinProvider {
@AllArgsConstructor @AllArgsConstructor
@Getter @Getter
public static class SkinAndCape { public static class SkinAndCape {
private Skin skin; private final Skin skin;
private Cape cape; private final Cape cape;
} }
@AllArgsConstructor @AllArgsConstructor
@Getter @Getter
public static class Skin { public static class Skin {
private UUID skinOwner; private UUID skinOwner;
private String textureUrl; private final String textureUrl;
private byte[] skinData; private final byte[] skinData;
private long requestedOn; private final long requestedOn;
private boolean updated; private boolean updated;
private boolean ears; private boolean ears;
@ -603,19 +602,19 @@ public class SkinProvider {
@AllArgsConstructor @AllArgsConstructor
@Getter @Getter
public static class Cape { public static class Cape {
private String textureUrl; private final String textureUrl;
private String capeId; private final String capeId;
private byte[] capeData; private final byte[] capeData;
private long requestedOn; private final long requestedOn;
private boolean failed; private final boolean failed;
} }
@AllArgsConstructor @AllArgsConstructor
@Getter @Getter
public static class SkinGeometry { public static class SkinGeometry {
private String geometryName; private final String geometryName;
private String geometryData; private final String geometryData;
private boolean failed; private final boolean failed;
/** /**
* Generate generic geometry * Generate generic geometry

View file

@ -27,65 +27,42 @@ package org.geysermc.connector.skin;
import com.nukkitx.protocol.bedrock.data.skin.ImageData; import com.nukkitx.protocol.bedrock.data.skin.ImageData;
import com.nukkitx.protocol.bedrock.data.skin.SerializedSkin; import com.nukkitx.protocol.bedrock.data.skin.SerializedSkin;
import com.nukkitx.protocol.bedrock.packet.PlayerListPacket; import com.nukkitx.protocol.bedrock.packet.PlayerSkinPacket;
import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.entity.player.PlayerEntity; import org.geysermc.connector.entity.player.PlayerEntity;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.utils.LanguageUtils; import org.geysermc.connector.utils.LanguageUtils;
import java.util.Collections; import java.util.Collections;
import java.util.UUID;
import java.util.function.Consumer; import java.util.function.Consumer;
public class SkullSkinManager extends SkinManager { public class SkullSkinManager extends SkinManager {
public static PlayerListPacket.Entry buildSkullEntryManually(UUID uuid, String username, long geyserId, public static SerializedSkin buildSkullEntryManually(String skinId, byte[] skinData) {
String skinId, byte[] skinData) {
// Prevents https://cdn.discordapp.com/attachments/613194828359925800/779458146191147008/unknown.png // Prevents https://cdn.discordapp.com/attachments/613194828359925800/779458146191147008/unknown.png
skinId = skinId + "_skull"; skinId = skinId + "_skull";
SerializedSkin serializedSkin = SerializedSkin.of( return SerializedSkin.of(
skinId, SkinProvider.SKULL_GEOMETRY.getGeometryName(), ImageData.of(skinData), Collections.emptyList(), skinId, SkinProvider.SKULL_GEOMETRY.getGeometryName(), ImageData.of(skinData), Collections.emptyList(),
ImageData.of(SkinProvider.EMPTY_CAPE.getCapeData()), SkinProvider.SKULL_GEOMETRY.getGeometryData(), ImageData.of(SkinProvider.EMPTY_CAPE.getCapeData()), SkinProvider.SKULL_GEOMETRY.getGeometryData(),
"", true, false, false, SkinProvider.EMPTY_CAPE.getCapeId(), skinId "", true, false, false, SkinProvider.EMPTY_CAPE.getCapeId(), skinId
); );
PlayerListPacket.Entry entry = new PlayerListPacket.Entry(uuid);
entry.setName(username);
entry.setEntityId(geyserId);
entry.setSkin(serializedSkin);
entry.setXuid("");
entry.setPlatformChatId("");
entry.setTeacher(false);
entry.setTrustedSkin(true);
return entry;
} }
public static void requestAndHandleSkin(PlayerEntity entity, GeyserSession session, public static void requestAndHandleSkin(PlayerEntity entity, GeyserSession session,
Consumer<SkinProvider.Skin> skinConsumer) { Consumer<SkinProvider.Skin> skinConsumer) {
GameProfileData data = GameProfileData.from(entity.getProfile()); GameProfileData data = GameProfileData.from(entity.getProfile());
SkinProvider.requestSkin(entity.getUuid(), data.getSkinUrl(), false) SkinProvider.requestSkin(entity.getUuid(), data.getSkinUrl(), true)
.whenCompleteAsync((skin, throwable) -> { .whenCompleteAsync((skin, throwable) -> {
try { try {
if (session.getUpstream().isInitialized()) { if (session.getUpstream().isInitialized()) {
PlayerListPacket.Entry updatedEntry = buildSkullEntryManually( PlayerSkinPacket packet = new PlayerSkinPacket();
entity.getUuid(), packet.setUuid(entity.getUuid());
entity.getUsername(), packet.setOldSkinName("");
entity.getGeyserId(), packet.setNewSkinName(skin.getTextureUrl());
skin.getTextureUrl(), packet.setSkin(buildSkullEntryManually(skin.getTextureUrl(), skin.getSkinData()));
skin.getSkinData() packet.setTrustedSkin(true);
); session.sendUpstreamPacket(packet);
PlayerListPacket playerAddPacket = new PlayerListPacket();
playerAddPacket.setAction(PlayerListPacket.Action.ADD);
playerAddPacket.getEntries().add(updatedEntry);
session.sendUpstreamPacket(playerAddPacket);
// It's a skull. We don't want them in the player list.
PlayerListPacket playerRemovePacket = new PlayerListPacket();
playerRemovePacket.setAction(PlayerListPacket.Action.REMOVE);
playerRemovePacket.getEntries().add(updatedEntry);
session.sendUpstreamPacket(playerRemovePacket);
} }
} catch (Exception e) { } catch (Exception e) {
GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e); GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e);