Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Skin and skull fixes #1923

Merged
merged 3 commits into from
Feb 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions connector/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,7 @@
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>2.9.8</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.9.8</version>
<version>2.10.2</version>
<scope>compile</scope>
</dependency>
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ public class GeyserConnector {
.enable(JsonParser.Feature.IGNORE_UNDEFINED)
.enable(JsonParser.Feature.ALLOW_COMMENTS)
.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 GIT_VERSION = "DEV"; // A fallback for running in IDEs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.common.AuthType;
import org.geysermc.connector.entity.player.PlayerEntity;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.session.auth.BedrockClientData;
Expand Down Expand Up @@ -163,7 +164,7 @@ public static void requestAndHandleSkinAndCape(PlayerEntity entity, GeyserSessio
geometry = SkinProvider.SkinGeometry.getEars(data.isAlex());

// Store the skin and geometry for the ears
SkinProvider.storeEarSkin(entity.getUuid(), skin);
SkinProvider.storeEarSkin(skin);
SkinProvider.storeEarGeometry(entity.getUuid(), data.isAlex());
}
}
Expand Down Expand Up @@ -267,7 +268,10 @@ public static GameProfileData from(GameProfile profile) {

return new GameProfileData(skinUrl, capeUrl, isAlex);
} 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);
}
}
Expand All @@ -282,7 +286,7 @@ private static GameProfileData loadBedrockOrOfflineSkin(GameProfile profile) {

String skinUrl = isAlex ? SkinProvider.EMPTY_SKIN_ALEX.getTextureUrl() : SkinProvider.EMPTY_SKIN.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());

if (session != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,12 @@ public class SkinProvider {
.build();
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<>();

public static final boolean ALLOW_THIRD_PARTY_EARS = GeyserConnector.getInstance().getConfig().isAllowThirdPartyEars();
public static String EARS_GEOMETRY;
public static String EARS_GEOMETRY_SLIM;
public static SkinGeometry SKULL_GEOMETRY;
public static final String EARS_GEOMETRY;
public static final String EARS_GEOMETRY_SLIM;
public static final SkinGeometry SKULL_GEOMETRY;

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

Expand Down Expand Up @@ -229,15 +228,15 @@ public static CompletableFuture<Cape> requestUnofficialCape(Cape officialCape, U
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);

CompletableFuture<Skin> future;
if (newThread) {
future = CompletableFuture.supplyAsync(() -> supplyEars(skin, earsUrl, provider), EXECUTOR_SERVICE)
future = CompletableFuture.supplyAsync(() -> supplyEars(skin, earsUrl), EXECUTOR_SERVICE)
.whenCompleteAsync((outSkin, throwable) -> { });
} else {
Skin ears = supplyEars(skin, earsUrl, provider); // blocking
Skin ears = supplyEars(skin, earsUrl); // blocking
future = CompletableFuture.completedFuture(ears);
}
return future;
Expand All @@ -255,7 +254,7 @@ public static CompletableFuture<Skin> requestEars(String earsUrl, EarsProvider p
public static CompletableFuture<Skin> requestUnofficialEars(Skin officialSkin, UUID playerId, String username, boolean newThread) {
for (EarsProvider provider : EarsProvider.VALUES) {
Skin skin1 = getOrDefault(
requestEars(provider.getUrlFor(playerId, username), provider, newThread, officialSkin),
requestEars(provider.getUrlFor(playerId, username), newThread, officialSkin),
officialSkin, 4
);
if (skin1.isEars()) {
Expand Down Expand Up @@ -295,12 +294,11 @@ public static void storeBedrockGeometry(UUID playerID, byte[] geometryName, byte
}

/**
* 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
*/
public static void storeEarSkin(UUID playerID, Skin skin) {
public static void storeEarSkin(Skin skin) {
cachedSkins.put(skin.getTextureUrl(), skin);
}

Expand All @@ -324,7 +322,7 @@ private static Skin supplySkin(UUID uuid, String textureUrl) {
}

private static Cape supplyCape(String capeUrl, CapeProvider provider) {
byte[] cape = new byte[0];
byte[] cape = EMPTY_CAPE.getCapeData();
try {
cape = requestImage(capeUrl, provider);
} catch (Exception ignored) {} // just ignore I guess
Expand All @@ -334,7 +332,7 @@ private static Cape supplyCape(String capeUrl, CapeProvider provider) {
return new Cape(
capeUrl,
urlSection[urlSection.length - 1], // get the texture id and use it as cape id
cape.length > 0 ? cape : EMPTY_CAPE.getCapeData(),
cape,
System.currentTimeMillis(),
cape.length == 0
);
Expand All @@ -345,10 +343,9 @@ private static Cape supplyCape(String capeUrl, CapeProvider provider) {
*
* @param existingSkin The players current skin
* @param earsUrl The URL to get the ears texture from
* @param provider The ears texture provider
* @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 {
// Get the ears texture
BufferedImage ears = ImageIO.read(new URL(earsUrl));
Expand Down Expand Up @@ -415,14 +412,15 @@ private static byte[] requestImage(String imageUrl, CapeProvider provider) throw

// if the requested image is a cape
if (provider != null) {
while(image.getWidth() > 64) {
image = scale(image);
if (image.getWidth() > 64) {
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);
Expand Down Expand Up @@ -506,12 +504,13 @@ private static BufferedImage readFiveZigCape(String url) throws IOException {
return null;
}

private static BufferedImage scale(BufferedImage bufferedImage) {
BufferedImage resized = new BufferedImage(bufferedImage.getWidth() / 2, bufferedImage.getHeight() / 2, BufferedImage.TYPE_INT_ARGB);
private static BufferedImage scale(BufferedImage bufferedImage, int newWidth, int newHeight) {
BufferedImage resized = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = resized.createGraphics();
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();
bufferedImage.flush();
return resized;
}

Expand Down Expand Up @@ -579,17 +578,17 @@ public static <T> T getOrDefault(CompletableFuture<T> future, T defaultValue, in
@AllArgsConstructor
@Getter
public static class SkinAndCape {
private Skin skin;
private Cape cape;
private final Skin skin;
private final Cape cape;
}

@AllArgsConstructor
@Getter
public static class Skin {
private UUID skinOwner;
private String textureUrl;
private byte[] skinData;
private long requestedOn;
private final String textureUrl;
private final byte[] skinData;
private final long requestedOn;
private boolean updated;
private boolean ears;

Expand All @@ -603,19 +602,19 @@ private Skin(long requestedOn, String textureUrl, byte[] skinData) {
@AllArgsConstructor
@Getter
public static class Cape {
private String textureUrl;
private String capeId;
private byte[] capeData;
private long requestedOn;
private boolean failed;
private final String textureUrl;
private final String capeId;
private final byte[] capeData;
private final long requestedOn;
private final boolean failed;
}

@AllArgsConstructor
@Getter
public static class SkinGeometry {
private String geometryName;
private String geometryData;
private boolean failed;
private final String geometryName;
private final String geometryData;
private final boolean failed;

/**
* Generate generic geometry
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,65 +27,42 @@

import com.nukkitx.protocol.bedrock.data.skin.ImageData;
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.entity.player.PlayerEntity;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.utils.LanguageUtils;

import java.util.Collections;
import java.util.UUID;
import java.util.function.Consumer;

public class SkullSkinManager extends SkinManager {

public static PlayerListPacket.Entry buildSkullEntryManually(UUID uuid, String username, long geyserId,
String skinId, byte[] skinData) {
public static SerializedSkin buildSkullEntryManually(String skinId, byte[] skinData) {
// Prevents https://cdn.discordapp.com/attachments/613194828359925800/779458146191147008/unknown.png
skinId = skinId + "_skull";
SerializedSkin serializedSkin = SerializedSkin.of(
return SerializedSkin.of(
skinId, SkinProvider.SKULL_GEOMETRY.getGeometryName(), ImageData.of(skinData), Collections.emptyList(),
ImageData.of(SkinProvider.EMPTY_CAPE.getCapeData()), SkinProvider.SKULL_GEOMETRY.getGeometryData(),
"", 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,
Consumer<SkinProvider.Skin> skinConsumer) {
GameProfileData data = GameProfileData.from(entity.getProfile());

SkinProvider.requestSkin(entity.getUuid(), data.getSkinUrl(), false)
SkinProvider.requestSkin(entity.getUuid(), data.getSkinUrl(), true)
.whenCompleteAsync((skin, throwable) -> {
try {
if (session.getUpstream().isInitialized()) {
PlayerListPacket.Entry updatedEntry = buildSkullEntryManually(
entity.getUuid(),
entity.getUsername(),
entity.getGeyserId(),
skin.getTextureUrl(),
skin.getSkinData()
);

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);
PlayerSkinPacket packet = new PlayerSkinPacket();
packet.setUuid(entity.getUuid());
packet.setOldSkinName("");
packet.setNewSkinName(skin.getTextureUrl());
packet.setSkin(buildSkullEntryManually(skin.getTextureUrl(), skin.getSkinData()));
packet.setTrustedSkin(true);
session.sendUpstreamPacket(packet);
}
} catch (Exception e) {
GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e);
Expand Down