From 2c0f3ec84d525b1547763a311c551fddb47bad96 Mon Sep 17 00:00:00 2001 From: OnlyBMan <27742182+OnlyBMan@users.noreply.github.com> Date: Fri, 4 Dec 2020 16:55:24 -0500 Subject: [PATCH] Custom skull block support (#683) Custom skulls are now implemented within the world when placed as a block. This is achieved by placing a fake player entity in the same spot. Co-authored-by: DoctorMacc Co-authored-by: bundabrg Co-authored-by: bundabrg Co-authored-by: Camotoy <20743703+DoctorMacc@users.noreply.github.com> --- .../geysermc/connector/GeyserConnector.java | 10 +- .../configuration/GeyserConfiguration.java | 4 +- .../GeyserJacksonConfiguration.java | 3 + .../connector/entity/player/PlayerEntity.java | 1 - .../entity/player/SkullPlayerEntity.java | 68 ++++++ .../network/session/GeyserSession.java | 7 +- ...SetLocalPlayerAsInitializedTranslator.java | 19 +- .../translators/nbt/PlayerHeadTranslator.java | 72 ++++++ .../player/JavaPlayerListEntryTranslator.java | 7 +- .../spawn/JavaSpawnPlayerTranslator.java | 4 +- .../java/world/JavaUnloadChunkTranslator.java | 14 +- .../world/JavaUpdateTileEntityTranslator.java | 5 + .../world/block/BlockStateValues.java | 30 +++ .../entity/SkullBlockEntityTranslator.java | 213 ++++++++++++++---- .../{utils => skin}/ProvidedSkin.java | 2 +- .../SkinUtils.java => skin/SkinManager.java} | 46 ++-- .../{utils => skin}/SkinProvider.java | 106 ++++++--- .../connector/skin/SkullSkinManager.java | 100 ++++++++ .../geysermc/connector/utils/ChunkUtils.java | 17 +- .../connector/utils/DimensionUtils.java | 1 + .../geysermc/connector/utils/FileUtils.java | 25 +- .../geysermc/connector/utils/WebUtils.java | 13 ++ .../skin/geometry.humanoid.customskull.json | 27 +++ connector/src/main/resources/config.yml | 3 + 24 files changed, 663 insertions(+), 134 deletions(-) create mode 100644 connector/src/main/java/org/geysermc/connector/entity/player/SkullPlayerEntity.java create mode 100644 connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/PlayerHeadTranslator.java rename connector/src/main/java/org/geysermc/connector/{utils => skin}/ProvidedSkin.java (98%) rename connector/src/main/java/org/geysermc/connector/{utils/SkinUtils.java => skin/SkinManager.java} (89%) rename connector/src/main/java/org/geysermc/connector/{utils => skin}/SkinProvider.java (86%) create mode 100644 connector/src/main/java/org/geysermc/connector/skin/SkullSkinManager.java create mode 100644 connector/src/main/resources/bedrock/skin/geometry.humanoid.customskull.json diff --git a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java index dbbe826ad..96c708989 100644 --- a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java +++ b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java @@ -55,6 +55,7 @@ import org.geysermc.connector.network.translators.world.WorldManager; import org.geysermc.connector.network.translators.world.block.BlockTranslator; import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator; import org.geysermc.connector.network.translators.collision.CollisionTranslator; +import org.geysermc.connector.network.translators.world.block.entity.SkullBlockEntityTranslator; import org.geysermc.connector.utils.DimensionUtils; import org.geysermc.connector.utils.LanguageUtils; import org.geysermc.connector.utils.LocaleUtils; @@ -78,7 +79,11 @@ import java.util.concurrent.TimeUnit; @Getter public class GeyserConnector { - public static final ObjectMapper JSON_MAPPER = new ObjectMapper().enable(JsonParser.Feature.IGNORE_UNDEFINED).enable(JsonParser.Feature.ALLOW_COMMENTS).disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + public static final ObjectMapper JSON_MAPPER = new ObjectMapper() + .enable(JsonParser.Feature.IGNORE_UNDEFINED) + .enable(JsonParser.Feature.ALLOW_COMMENTS) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES); public static final String NAME = "Geyser"; public static final String VERSION = "DEV"; // A fallback for running in IDEs @@ -182,6 +187,7 @@ public class GeyserConnector { authType = AuthType.getByName(config.getRemote().getAuthType()); DimensionUtils.changeBedrockNetherId(config.isAboveBedrockNetherBuilding()); // Apply End dimension ID workaround to Nether + SkullBlockEntityTranslator.ALLOW_CUSTOM_SKULLS = config.isAllowCustomSkulls(); // https://github.com/GeyserMC/Geyser/issues/957 RakNetConstants.MAXIMUM_MTU_SIZE = (short) config.getMtu(); @@ -255,7 +261,7 @@ public class GeyserConnector { message += LanguageUtils.getLocaleStringLog("geyser.core.finish.console"); } logger.info(message); - + if (platformType == PlatformType.STANDALONE) { logger.warning(LanguageUtils.getLocaleStringLog("geyser.core.movement_warn")); } diff --git a/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java b/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java index f57f29b8f..963385c92 100644 --- a/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java +++ b/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java @@ -85,6 +85,8 @@ public interface GeyserConfiguration { int getCacheImages(); + boolean isAllowCustomSkulls(); + IMetricsInfo getMetrics(); interface IBedrockConfiguration { @@ -107,7 +109,7 @@ public interface GeyserConfiguration { String getAddress(); int getPort(); - + void setAddress(String address); void setPort(int port); diff --git a/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java b/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java index f3bb82488..0ff808951 100644 --- a/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java +++ b/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java @@ -101,6 +101,9 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration @JsonProperty("cache-images") private int cacheImages = 0; + @JsonProperty("allow-custom-skulls") + private boolean allowCustomSkulls = true; + @JsonProperty("above-bedrock-nether-building") private boolean aboveBedrockNetherBuilding = false; diff --git a/connector/src/main/java/org/geysermc/connector/entity/player/PlayerEntity.java b/connector/src/main/java/org/geysermc/connector/entity/player/PlayerEntity.java index 54ae492af..0e77d33d8 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/player/PlayerEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/player/PlayerEntity.java @@ -65,7 +65,6 @@ public class PlayerEntity extends LivingEntity { private GameProfile profile; private UUID uuid; private String username; - private long lastSkinUpdate = -1; private boolean playerList = true; // Player is in the player list /** diff --git a/connector/src/main/java/org/geysermc/connector/entity/player/SkullPlayerEntity.java b/connector/src/main/java/org/geysermc/connector/entity/player/SkullPlayerEntity.java new file mode 100644 index 000000000..f281d4c0e --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/entity/player/SkullPlayerEntity.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2019-2020 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.connector.entity.player; + +import com.github.steveice10.mc.auth.data.GameProfile; +import com.nukkitx.math.vector.Vector3f; +import com.nukkitx.math.vector.Vector3i; +import com.nukkitx.protocol.bedrock.data.entity.EntityData; +import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; +import lombok.Getter; +import lombok.Setter; +import org.geysermc.connector.network.session.GeyserSession; + +/** + * A wrapper to handle skulls more effectively - skulls have to be treated as entities since there are no + * custom player skulls in Bedrock. + */ +public class SkullPlayerEntity extends PlayerEntity { + + /** + * Stores the block state that the skull is associated with. Used to determine if the block in the skull's position + * has changed + */ + @Getter + @Setter + private int blockState; + + public SkullPlayerEntity(GameProfile gameProfile, long geyserId, Vector3f position, Vector3f rotation) { + super(gameProfile, 0, geyserId, position, Vector3f.ZERO, rotation); + setPlayerList(false); + + //Set bounding box to almost nothing so the skull is able to be broken and not cause entity to cast a shadow + metadata.clear(); + metadata.put(EntityData.SCALE, 1.08f); + metadata.put(EntityData.BOUNDING_BOX_HEIGHT, 0.001f); + metadata.put(EntityData.BOUNDING_BOX_WIDTH, 0.001f); + metadata.getOrCreateFlags().setFlag(EntityFlag.CAN_SHOW_NAME, false); + metadata.getFlags().setFlag(EntityFlag.INVISIBLE, true); // Until the skin is loaded + } + + public void despawnEntity(GeyserSession session, Vector3i position) { + this.despawnEntity(session); + session.getSkullCache().remove(position, this); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java index e3ba75d97..fc73b62ae 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java @@ -67,6 +67,7 @@ import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.command.CommandSender; import org.geysermc.connector.common.AuthType; import org.geysermc.connector.entity.Entity; +import org.geysermc.connector.entity.player.SkullPlayerEntity; import org.geysermc.connector.entity.player.SessionPlayerEntity; import org.geysermc.connector.inventory.PlayerInventory; import org.geysermc.connector.network.translators.chat.MessageTranslator; @@ -80,6 +81,7 @@ import org.geysermc.connector.network.translators.PacketTranslatorRegistry; import org.geysermc.connector.network.translators.collision.CollisionManager; import org.geysermc.connector.network.translators.inventory.EnchantmentInventoryTranslator; import org.geysermc.connector.network.translators.item.ItemRegistry; +import org.geysermc.connector.skin.SkinManager; import org.geysermc.connector.utils.*; import org.geysermc.floodgate.util.BedrockData; import org.geysermc.floodgate.util.EncryptionUtil; @@ -90,6 +92,7 @@ import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.spec.InvalidKeySpecException; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.atomic.AtomicInteger; @@ -121,7 +124,7 @@ public class GeyserSession implements CommandSender { */ private final CollisionManager collisionManager; - @Getter + private final Map skullCache = new ConcurrentHashMap<>(); private final Long2ObjectMap storedMaps = Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>()); /** @@ -531,7 +534,7 @@ public class GeyserSession implements CommandSender { // Check if they are not using a linked account if (connector.getAuthType() == AuthType.OFFLINE || playerEntity.getUuid().getMostSignificantBits() == 0) { - SkinUtils.handleBedrockSkin(playerEntity, clientData); + SkinManager.handleBedrockSkin(playerEntity, clientData); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java index 8c3827f48..eea73fee8 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java @@ -25,13 +25,14 @@ package org.geysermc.connector.network.translators.bedrock; +import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; +import com.nukkitx.protocol.bedrock.packet.SetLocalPlayerAsInitializedPacket; import org.geysermc.connector.entity.player.PlayerEntity; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.Translator; -import org.geysermc.connector.utils.SkinUtils; - -import com.nukkitx.protocol.bedrock.packet.SetLocalPlayerAsInitializedPacket; +import org.geysermc.connector.skin.SkinManager; +import org.geysermc.connector.skin.SkullSkinManager; @Translator(packet = SetLocalPlayerAsInitializedPacket.class) public class BedrockSetLocalPlayerAsInitializedTranslator extends PacketTranslator { @@ -44,10 +45,20 @@ public class BedrockSetLocalPlayerAsInitializedTranslator extends PacketTranslat for (PlayerEntity entity : session.getEntityCache().getEntitiesByType(PlayerEntity.class)) { if (!entity.isValid()) { - SkinUtils.requestAndHandleSkinAndCape(entity, session, null); + SkinManager.requestAndHandleSkinAndCape(entity, session, null); entity.sendPlayer(session); } } + + // Send Skulls + for (PlayerEntity entity : session.getSkullCache().values()) { + entity.spawnEntity(session); + + SkullSkinManager.requestAndHandleSkin(entity, session, (skin) -> { + entity.getMetadata().getFlags().setFlag(EntityFlag.INVISIBLE, false); + entity.updateBedrockMetadata(session); + }); + } } } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/PlayerHeadTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/PlayerHeadTranslator.java new file mode 100644 index 000000000..160b319fa --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/PlayerHeadTranslator.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2019-2020 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.connector.network.translators.item.translators.nbt; + +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.github.steveice10.opennbt.tag.builtin.StringTag; +import com.github.steveice10.opennbt.tag.builtin.Tag; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.ItemRemapper; +import org.geysermc.connector.network.translators.item.ItemEntry; +import org.geysermc.connector.network.translators.item.NbtItemStackTranslator; +import org.geysermc.connector.utils.LocaleUtils; + +@ItemRemapper +public class PlayerHeadTranslator extends NbtItemStackTranslator { + + @Override + public void translateToBedrock(GeyserSession session, CompoundTag itemTag, ItemEntry itemEntry) { + if (!itemTag.contains("display") || !((CompoundTag) itemTag.get("display")).contains("name")) { + if (itemTag.contains("SkullOwner")) { + StringTag name; + Tag skullOwner = itemTag.get("SkullOwner"); + if (skullOwner instanceof StringTag) { + name = (StringTag) skullOwner; + } else { + StringTag skullName; + if (skullOwner instanceof CompoundTag && (skullName = ((CompoundTag) skullOwner).get("Name")) != null) { + name = skullName; + } else { + session.getConnector().getLogger().debug("Not sure how to handle skull head item display. " + itemTag); + return; + } + } + // Add correct name of player skull + // TODO: It's always yellow, even with a custom name. Handle? + String displayName = "\u00a7r\u00a7e" + LocaleUtils.getLocaleString("block.minecraft.player_head.named", session.getLocale()).replace("%s", name.getValue()); + if (!itemTag.contains("display")) { + itemTag.put(new CompoundTag("display")); + } + ((CompoundTag) itemTag.get("display")).put(new StringTag("Name", displayName)); + } + } + } + + @Override + public boolean acceptItem(ItemEntry itemEntry) { + return itemEntry.getJavaIdentifier().equals("minecraft:player_head"); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerListEntryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerListEntryTranslator.java index bc31466b4..798c14187 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerListEntryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerListEntryTranslator.java @@ -30,7 +30,7 @@ import org.geysermc.connector.entity.player.PlayerEntity; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.Translator; -import org.geysermc.connector.utils.SkinUtils; +import org.geysermc.connector.skin.SkinManager; import com.github.steveice10.mc.protocol.data.game.PlayerListEntry; import com.github.steveice10.mc.protocol.data.game.PlayerListEntryAction; @@ -57,7 +57,8 @@ public class JavaPlayerListEntryTranslator extends PacketTranslator + //TODO: playerEntity.setProfile(entry.getProfile()); seems to help with online mode skins but needs more testing to ensure Floodgate skins aren't overwritten + SkinManager.requestAndHandleSkinAndCape(playerEntity, session, skinAndCape -> GeyserConnector.getInstance().getLogger().debug("Loaded Local Bedrock Java Skin Data")); } else { playerEntity = session.getEntityCache().getPlayerEntity(entry.getProfile().getId()); @@ -81,7 +82,7 @@ public class JavaPlayerListEntryTranslator extends PacketTranslator { @@ -62,7 +62,7 @@ public class JavaSpawnPlayerTranslator extends PacketTranslator { @@ -37,5 +39,15 @@ public class JavaUnloadChunkTranslator extends PacketTranslator iterator = session.getSkullCache().keySet().iterator(); + while (iterator.hasNext()) { + Vector3i position = iterator.next(); + if (Math.floor(position.getX() / 16) == packet.getX() && Math.floor(position.getZ() / 16) == packet.getZ()) { + session.getSkullCache().get(position).despawnEntity(session); + iterator.remove(); + } + } } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaUpdateTileEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaUpdateTileEntityTranslator.java index af92d0e21..9f67fb938 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaUpdateTileEntityTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaUpdateTileEntityTranslator.java @@ -36,6 +36,7 @@ import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.Translator; import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator; +import org.geysermc.connector.network.translators.world.block.entity.SkullBlockEntityTranslator; import org.geysermc.connector.utils.BlockEntityUtils; import org.geysermc.connector.utils.ChunkUtils; @@ -64,6 +65,10 @@ public class JavaUpdateTileEntityTranslator extends PacketTranslator= 2 && diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockStateValues.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockStateValues.java index bfd59cc7c..2701f82fd 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockStateValues.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockStateValues.java @@ -47,6 +47,7 @@ public class BlockStateValues { private static final Int2BooleanMap PISTON_VALUES = new Int2BooleanOpenHashMap(); private static final Int2ByteMap SKULL_VARIANTS = new Int2ByteOpenHashMap(); private static final Int2ByteMap SKULL_ROTATIONS = new Int2ByteOpenHashMap(); + private static final Int2IntMap SKULL_WALL_DIRECTIONS = new Int2IntOpenHashMap(); private static final Int2ByteMap SHULKERBOX_DIRECTIONS = new Int2ByteOpenHashMap(); /** @@ -110,6 +111,26 @@ public class BlockStateValues { SKULL_ROTATIONS.put(javaBlockState, (byte) skullRotation.intValue()); } + if (entry.getKey().contains("wall_skull") || entry.getKey().contains("wall_head")) { + String direction = entry.getKey().substring(entry.getKey().lastIndexOf("facing=") + 7); + int rotation = 0; + switch (direction.substring(0, direction.length() - 1)) { + case "north": + rotation = 180; + break; + case "south": + rotation = 0; + break; + case "west": + rotation = 90; + break; + case "east": + rotation = 270; + break; + } + SKULL_WALL_DIRECTIONS.put(javaBlockState, rotation); + } + JsonNode shulkerDirection = entry.getValue().get("shulker_direction"); if (shulkerDirection != null) { BlockStateValues.SHULKERBOX_DIRECTIONS.put(javaBlockState, (byte) shulkerDirection.intValue()); @@ -222,6 +243,15 @@ public class BlockStateValues { return SKULL_ROTATIONS.getOrDefault(state, (byte) -1); } + /** + * Skull rotations are part of the namespaced ID in Java Edition, but part of the block entity tag in Bedrock. + * This gives a integer rotation that Bedrock can use. + * + * @return Skull wall rotation value with the blockstate + */ + public static Int2IntMap getSkullWallDirections() { + return SKULL_WALL_DIRECTIONS; + } /** * Shulker box directions are part of the namespaced ID in Java Edition, but part of the block entity tag in Bedrock. diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SkullBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SkullBlockEntityTranslator.java index c5f479948..5da9c0e0d 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SkullBlockEntityTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SkullBlockEntityTranslator.java @@ -1,50 +1,163 @@ -/* - * Copyright (c) 2019-2020 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Geyser - */ - -package org.geysermc.connector.network.translators.world.block.entity; - -import com.github.steveice10.opennbt.tag.builtin.CompoundTag; -import com.nukkitx.nbt.NbtMapBuilder; -import org.geysermc.connector.network.translators.world.block.BlockStateValues; - -@BlockEntity(name = "Skull", regex = "skull") -public class SkullBlockEntityTranslator extends BlockEntityTranslator implements RequiresBlockState { - @Override - public boolean isBlock(int blockState) { - return BlockStateValues.getSkullVariant(blockState) != -1; - } - - @Override - public void translateTag(NbtMapBuilder builder, CompoundTag tag, int blockState) { - byte skullVariant = BlockStateValues.getSkullVariant(blockState); - float rotation = BlockStateValues.getSkullRotation(blockState) * 22.5f; - // Just in case... - if (skullVariant == -1) { - skullVariant = 0; - } - builder.put("Rotation", rotation); - builder.put("SkullType", skullVariant); - } -} +/* + * Copyright (c) 2019-2020 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.connector.network.translators.world.block.entity; + +import com.github.steveice10.mc.auth.data.GameProfile; +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.github.steveice10.opennbt.tag.builtin.ListTag; +import com.github.steveice10.opennbt.tag.builtin.StringTag; +import com.nukkitx.math.vector.Vector3f; +import com.nukkitx.math.vector.Vector3i; +import com.nukkitx.nbt.NbtMapBuilder; +import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; +import org.geysermc.connector.entity.player.SkullPlayerEntity; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.world.block.BlockStateValues; +import org.geysermc.connector.skin.SkinProvider; +import org.geysermc.connector.skin.SkullSkinManager; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +@BlockEntity(name = "Skull", regex = "skull") +public class SkullBlockEntityTranslator extends BlockEntityTranslator implements RequiresBlockState { + public static boolean ALLOW_CUSTOM_SKULLS; + + @Override + public boolean isBlock(int blockState) { + return BlockStateValues.getSkullVariant(blockState) != -1; + } + + @Override + public void translateTag(NbtMapBuilder builder, CompoundTag tag, int blockState) { + byte skullVariant = BlockStateValues.getSkullVariant(blockState); + float rotation = BlockStateValues.getSkullRotation(blockState) * 22.5f; + // Just in case... + if (skullVariant == -1) { + skullVariant = 0; + } + builder.put("Rotation", rotation); + builder.put("SkullType", skullVariant); + } + + public static CompletableFuture getProfile(CompoundTag tag) { + if (tag.contains("SkullOwner")) { + CompoundTag owner = tag.get("SkullOwner"); + CompoundTag properties = owner.get("Properties"); + if (properties == null) { + return SkinProvider.requestTexturesFromUsername(owner); + } + + ListTag textures = properties.get("textures"); + LinkedHashMap tag1 = (LinkedHashMap) textures.get(0).getValue(); + StringTag texture = (StringTag) tag1.get("Value"); + + List profileProperties = new ArrayList<>(); + + GameProfile gameProfile = new GameProfile(UUID.randomUUID(), ""); + profileProperties.add(new GameProfile.Property("textures", texture.getValue())); + gameProfile.setProperties(profileProperties); + return CompletableFuture.completedFuture(gameProfile); + } + return CompletableFuture.completedFuture(null); + } + + public static void spawnPlayer(GeyserSession session, CompoundTag tag, int blockState) { + int posX = (int) tag.get("x").getValue(); + int posY = (int) tag.get("y").getValue(); + int posZ = (int) tag.get("z").getValue(); + float x = posX + .5f; + float y = posY - .01f; + float z = posZ + .5f; + float rotation; + + byte floorRotation = BlockStateValues.getSkullRotation(blockState); + if (floorRotation == -1) { + // Wall skull + y += 0.25f; + rotation = BlockStateValues.getSkullWallDirections().get(blockState); + switch ((int) rotation) { + case 180: + // North + z += 0.24f; + break; + case 0: + // South + z -= 0.24f; + break; + case 90: + // West + x += 0.24f; + break; + case 270: + // East + x -= 0.24f; + break; + } + } else { + rotation = (180f + (floorRotation * 22.5f)) % 360; + } + + Vector3i blockPosition = Vector3i.from(posX, posY, posZ); + Vector3f entityPosition = Vector3f.from(x, y, z); + Vector3f entityRotation = Vector3f.from(rotation, 0, rotation); + long geyserId = session.getEntityCache().getNextEntityId().incrementAndGet(); + + getProfile(tag).whenComplete((gameProfile, throwable) -> { + if (gameProfile == null) { + session.getConnector().getLogger().debug("Custom skull with invalid SkullOwner tag: " + blockPosition.toString() + " " + tag.toString()); + return; + } + + SkullPlayerEntity existingSkull = session.getSkullCache().get(blockPosition); + if (existingSkull != null) { + // Ensure that two skulls can't spawn on the same point + existingSkull.despawnEntity(session, blockPosition); + } + + SkullPlayerEntity player = new SkullPlayerEntity(gameProfile, geyserId, entityPosition, entityRotation); + player.setBlockState(blockState); + + // Cache entity + session.getSkullCache().put(blockPosition, player); + + // Only send to session if we are initialized, otherwise it will happen then. + if (session.getUpstream().isInitialized()) { + player.spawnEntity(session); + + SkullSkinManager.requestAndHandleSkin(player, session, (skin -> session.getConnector().getGeneralThreadPool().schedule(() -> { + // Delay to minimize split-second "player" pop-in + player.getMetadata().getFlags().setFlag(EntityFlag.INVISIBLE, false); + player.updateBedrockMetadata(session); + }, 250, TimeUnit.MILLISECONDS))); + } + }); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/utils/ProvidedSkin.java b/connector/src/main/java/org/geysermc/connector/skin/ProvidedSkin.java similarity index 98% rename from connector/src/main/java/org/geysermc/connector/utils/ProvidedSkin.java rename to connector/src/main/java/org/geysermc/connector/skin/ProvidedSkin.java index 2c0165d38..abb6476c9 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/ProvidedSkin.java +++ b/connector/src/main/java/org/geysermc/connector/skin/ProvidedSkin.java @@ -23,7 +23,7 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.connector.utils; +package org.geysermc.connector.skin; import lombok.Getter; diff --git a/connector/src/main/java/org/geysermc/connector/utils/SkinUtils.java b/connector/src/main/java/org/geysermc/connector/skin/SkinManager.java similarity index 89% rename from connector/src/main/java/org/geysermc/connector/utils/SkinUtils.java rename to connector/src/main/java/org/geysermc/connector/skin/SkinManager.java index d0179777e..db8f25929 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/SkinUtils.java +++ b/connector/src/main/java/org/geysermc/connector/skin/SkinManager.java @@ -23,10 +23,9 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.connector.utils; +package org.geysermc.connector.skin; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.github.steveice10.mc.auth.data.GameProfile; import com.nukkitx.protocol.bedrock.data.skin.ImageData; import com.nukkitx.protocol.bedrock.data.skin.SerializedSkin; @@ -38,6 +37,7 @@ 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; +import org.geysermc.connector.utils.LanguageUtils; import java.nio.charset.StandardCharsets; import java.util.Base64; @@ -45,12 +45,11 @@ import java.util.Collections; import java.util.UUID; import java.util.function.Consumer; -public class SkinUtils { +public class SkinManager { - public static PlayerListPacket.Entry buildCachedEntry(GeyserSession session, GameProfile profile, long geyserId) { - GameProfileData data = GameProfileData.from(profile); + public static PlayerListPacket.Entry buildCachedEntry(GeyserSession session, PlayerEntity playerEntity) { + GameProfileData data = GameProfileData.from(playerEntity.getProfile()); SkinProvider.Cape cape = SkinProvider.getCachedCape(data.getCapeUrl()); - SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.getLegacy(data.isAlex()); SkinProvider.Skin skin = SkinProvider.getCachedSkin(data.getSkinUrl()); @@ -60,25 +59,24 @@ public class SkinUtils { return buildEntryManually( session, - profile.getId(), - profile.getName(), - geyserId, + playerEntity.getProfile().getId(), + playerEntity.getProfile().getName(), + playerEntity.getGeyserId(), skin.getTextureUrl(), skin.getSkinData(), cape.getCapeId(), cape.getCapeData(), - geometry.getGeometryName(), - geometry.getGeometryData() + geometry ); } public static PlayerListPacket.Entry buildEntryManually(GeyserSession session, UUID uuid, String username, long geyserId, - String skinId, byte[] skinData, - String capeId, byte[] capeData, - String geometryName, String geometryData) { + String skinId, byte[] skinData, + String capeId, byte[] capeData, + SkinProvider.SkinGeometry geometry) { SerializedSkin serializedSkin = SerializedSkin.of( - skinId, geometryName, ImageData.of(skinData), Collections.emptyList(), - ImageData.of(capeData), geometryData, "", true, false, !capeId.equals(SkinProvider.EMPTY_CAPE.getCapeId()), capeId, skinId + skinId, geometry.getGeometryName(), ImageData.of(skinData), Collections.emptyList(), + ImageData.of(capeData), geometry.getGeometryData(), "", true, false, !capeId.equals(SkinProvider.EMPTY_CAPE.getCapeId()), capeId, skinId ); // This attempts to find the xuid of the player so profile images show up for xbox accounts @@ -119,11 +117,11 @@ public class SkinUtils { try { SkinProvider.Skin skin = skinAndCape.getSkin(); SkinProvider.Cape cape = skinAndCape.getCape(); + SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.getLegacy(data.isAlex()); if (cape.isFailed()) { - cape = SkinProvider.getOrDefault(SkinProvider.requestBedrockCape( - entity.getUuid(), false - ), SkinProvider.EMPTY_CAPE, 3); + cape = SkinProvider.getOrDefault(SkinProvider.requestBedrockCape(entity.getUuid()), + SkinProvider.EMPTY_CAPE, 3); } if (cape.isFailed() && SkinProvider.ALLOW_THIRD_PARTY_CAPES) { @@ -133,9 +131,8 @@ public class SkinUtils { ), SkinProvider.EMPTY_CAPE, SkinProvider.CapeProvider.VALUES.length * 3); } - SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.getLegacy(data.isAlex()); geometry = SkinProvider.getOrDefault(SkinProvider.requestBedrockGeometry( - geometry, entity.getUuid(), false + geometry, entity.getUuid() ), geometry, 3); // Not a bedrock player check for ears @@ -165,8 +162,6 @@ public class SkinUtils { } } - entity.setLastSkinUpdate(skin.getRequestedOn()); - if (session.getUpstream().isInitialized()) { PlayerListPacket.Entry updatedEntry = buildEntryManually( session, @@ -177,8 +172,7 @@ public class SkinUtils { skin.getSkinData(), cape.getCapeId(), cape.getCapeData(), - geometry.getGeometryName(), - geometry.getGeometryData() + geometry ); @@ -252,7 +246,7 @@ public class SkinUtils { GameProfile.Property skinProperty = profile.getProperty("textures"); // TODO: Remove try/catch here - JsonNode skinObject = new ObjectMapper().readTree(new String(Base64.getDecoder().decode(skinProperty.getValue()), StandardCharsets.UTF_8)); + JsonNode skinObject = GeyserConnector.JSON_MAPPER.readTree(new String(Base64.getDecoder().decode(skinProperty.getValue()), StandardCharsets.UTF_8)); JsonNode textures = skinObject.get("textures"); JsonNode skinTexture = textures.get("SKIN"); diff --git a/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java b/connector/src/main/java/org/geysermc/connector/skin/SkinProvider.java similarity index 86% rename from connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java rename to connector/src/main/java/org/geysermc/connector/skin/SkinProvider.java index d848d95ed..117198685 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java +++ b/connector/src/main/java/org/geysermc/connector/skin/SkinProvider.java @@ -23,10 +23,14 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.connector.utils; +package org.geysermc.connector.skin; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.steveice10.mc.auth.data.GameProfile; +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.github.steveice10.opennbt.tag.builtin.IntArrayTag; +import com.github.steveice10.opennbt.tag.builtin.Tag; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import lombok.AllArgsConstructor; @@ -34,15 +38,20 @@ import lombok.Getter; import lombok.NoArgsConstructor; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.utils.FileUtils; +import org.geysermc.connector.utils.WebUtils; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; -import java.io.*; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; -import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.List; import java.util.*; import java.util.concurrent.*; @@ -76,40 +85,20 @@ public class SkinProvider { 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; private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); static { /* Load in the normal ears geometry */ - InputStream earsStream = FileUtils.getResource("bedrock/skin/geometry.humanoid.ears.json"); - - StringBuilder earsDataBuilder = new StringBuilder(); - try (Reader reader = new BufferedReader(new InputStreamReader(earsStream, Charset.forName(StandardCharsets.UTF_8.name())))) { - int c = 0; - while ((c = reader.read()) != -1) { - earsDataBuilder.append((char) c); - } - } catch (IOException e) { - throw new AssertionError("Unable to load ears geometry", e); - } - - EARS_GEOMETRY = earsDataBuilder.toString(); - + EARS_GEOMETRY = new String(FileUtils.readAllBytes(FileUtils.getResource("bedrock/skin/geometry.humanoid.ears.json")), StandardCharsets.UTF_8); /* Load in the slim ears geometry */ - earsStream = FileUtils.getResource("bedrock/skin/geometry.humanoid.earsSlim.json"); + EARS_GEOMETRY_SLIM = new String(FileUtils.readAllBytes(FileUtils.getResource("bedrock/skin/geometry.humanoid.earsSlim.json")), StandardCharsets.UTF_8); - earsDataBuilder = new StringBuilder(); - try (Reader reader = new BufferedReader(new InputStreamReader(earsStream, Charset.forName(StandardCharsets.UTF_8.name())))) { - int c = 0; - while ((c = reader.read()) != -1) { - earsDataBuilder.append((char) c); - } - } catch (IOException e) { - throw new AssertionError("Unable to load ears geometry", e); - } - - EARS_GEOMETRY_SLIM = earsDataBuilder.toString(); + /* Load in the custom skull geometry */ + String skullData = new String(FileUtils.readAllBytes(FileUtils.getResource("bedrock/skin/geometry.humanoid.customskull.json")), StandardCharsets.UTF_8); + SKULL_GEOMETRY = new SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.customskull\"}}", skullData, false); // Schedule Daily Image Expiry if we are caching them if (GeyserConnector.getInstance().getConfig().getCacheImages() > 0) { @@ -205,7 +194,6 @@ public class SkinProvider { if (capeUrl == null || capeUrl.isEmpty()) return CompletableFuture.completedFuture(EMPTY_CAPE); if (requestedCapes.containsKey(capeUrl)) return requestedCapes.get(capeUrl); // already requested - boolean officialCape = provider == CapeProvider.MINECRAFT; Cape cachedCape = cachedCapes.getIfPresent(capeUrl); if (cachedCape != null) { return CompletableFuture.completedFuture(cachedCape); @@ -280,7 +268,7 @@ public class SkinProvider { return CompletableFuture.completedFuture(officialSkin); } - public static CompletableFuture requestBedrockCape(UUID playerID, boolean newThread) { + public static CompletableFuture requestBedrockCape(UUID playerID) { Cape bedrockCape = cachedCapes.getIfPresent(playerID.toString() + ".Bedrock"); if (bedrockCape == null) { bedrockCape = EMPTY_CAPE; @@ -288,7 +276,7 @@ public class SkinProvider { return CompletableFuture.completedFuture(bedrockCape); } - public static CompletableFuture requestBedrockGeometry(SkinGeometry currentGeometry, UUID playerID, boolean newThread) { + public static CompletableFuture requestBedrockGeometry(SkinGeometry currentGeometry, UUID playerID) { SkinGeometry bedrockGeometry = cachedGeometry.getOrDefault(playerID, currentGeometry); return CompletableFuture.completedFuture(bedrockGeometry); } @@ -444,6 +432,60 @@ public class SkinProvider { return data; } + /** + * If a skull has a username but no textures, request them. + * @param skullOwner the CompoundTag of the skull with no textures + * @return a completable GameProfile with textures included + */ + public static CompletableFuture requestTexturesFromUsername(CompoundTag skullOwner) { + return CompletableFuture.supplyAsync(() -> { + Tag uuidTag = skullOwner.get("Id"); + String uuidToString = ""; + JsonNode node; + GameProfile gameProfile = new GameProfile(UUID.randomUUID(), ""); + boolean retrieveUuidFromInternet = !(uuidTag instanceof IntArrayTag); // also covers null check + + if (!retrieveUuidFromInternet) { + int[] uuidAsArray = ((IntArrayTag) uuidTag).getValue(); + // thank u viaversion + UUID uuid = new UUID((long) uuidAsArray[0] << 32 | ((long) uuidAsArray[1] & 0xFFFFFFFFL), + (long) uuidAsArray[2] << 32 | ((long) uuidAsArray[3] & 0xFFFFFFFFL)); + retrieveUuidFromInternet = uuid.version() != 4; + uuidToString = uuid.toString().replace("-", ""); + } + + try { + if (retrieveUuidFromInternet) { + // Offline skin, or no present UUID + node = WebUtils.getJson("https://api.mojang.com/users/profiles/minecraft/" + skullOwner.get("Name").getValue()); + JsonNode id = node.get("id"); + if (id == null) { + GeyserConnector.getInstance().getLogger().debug("No UUID found in Mojang response for " + skullOwner.get("Name").getValue()); + return null; + } + uuidToString = id.asText(); + } + + // Get textures from UUID + node = WebUtils.getJson("https://sessionserver.mojang.com/session/minecraft/profile/" + uuidToString); + List profileProperties = new ArrayList<>(); + JsonNode properties = node.get("properties"); + if (properties == null) { + GeyserConnector.getInstance().getLogger().debug("No properties found in Mojang response for " + uuidToString); + return null; + } + profileProperties.add(new GameProfile.Property("textures", node.get("properties").get(0).get("value").asText())); + gameProfile.setProperties(profileProperties); + return gameProfile; + } catch (Exception e) { + if (GeyserConnector.getInstance().getConfig().isDebugMode()) { + e.printStackTrace(); + } + return null; + } + }, EXECUTOR_SERVICE); + } + private static BufferedImage downloadImage(String imageUrl, CapeProvider provider) throws IOException { if (provider == CapeProvider.FIVEZIG) return readFiveZigCape(imageUrl); diff --git a/connector/src/main/java/org/geysermc/connector/skin/SkullSkinManager.java b/connector/src/main/java/org/geysermc/connector/skin/SkullSkinManager.java new file mode 100644 index 000000000..967e1d9b8 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/skin/SkullSkinManager.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2019-2020 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.connector.skin; + +import com.nukkitx.protocol.bedrock.data.skin.ImageData; +import com.nukkitx.protocol.bedrock.data.skin.SerializedSkin; +import com.nukkitx.protocol.bedrock.packet.PlayerListPacket; +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) { + // Prevents https://cdn.discordapp.com/attachments/613194828359925800/779458146191147008/unknown.png + skinId = skinId + "_skull"; + SerializedSkin serializedSkin = 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 skinConsumer) { + GameProfileData data = GameProfileData.from(entity.getProfile()); + + SkinProvider.requestSkin(entity.getUuid(), data.getSkinUrl(), false) + .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); + } + } catch (Exception e) { + GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e); + } + + if (skinConsumer != null) { + skinConsumer.accept(skin); + } + }); + } + +} diff --git a/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java b/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java index 005a4960e..8950601a8 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java @@ -51,12 +51,14 @@ import lombok.experimental.UtilityClass; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.entity.Entity; import org.geysermc.connector.entity.ItemFrameEntity; +import org.geysermc.connector.entity.player.SkullPlayerEntity; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.world.block.BlockStateValues; import org.geysermc.connector.network.translators.world.block.BlockTranslator; import org.geysermc.connector.network.translators.world.block.entity.BedrockOnlyBlockEntity; import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator; import org.geysermc.connector.network.translators.world.block.entity.RequiresBlockState; +import org.geysermc.connector.network.translators.world.block.entity.SkullBlockEntityTranslator; import org.geysermc.connector.network.translators.world.chunk.BlockStorage; import org.geysermc.connector.network.translators.world.chunk.ChunkSection; import org.geysermc.connector.network.translators.world.chunk.bitarray.BitArray; @@ -68,9 +70,7 @@ import java.util.ArrayList; import java.util.BitSet; import java.util.List; -import static org.geysermc.connector.network.translators.world.block.BlockTranslator.JAVA_AIR_ID; -import static org.geysermc.connector.network.translators.world.block.BlockTranslator.BEDROCK_AIR_ID; -import static org.geysermc.connector.network.translators.world.block.BlockTranslator.BEDROCK_WATER_ID; +import static org.geysermc.connector.network.translators.world.block.BlockTranslator.*; @UtilityClass public class ChunkUtils { @@ -293,6 +293,11 @@ public class ChunkUtils { } bedrockBlockEntities[i] = blockEntityTranslator.getBlockEntityTag(tagName, tag, blockState); + + // Check for custom skulls + if (SkullBlockEntityTranslator.ALLOW_CUSTOM_SKULLS && tag.contains("SkullOwner")) { + SkullBlockEntityTranslator.spawnPlayer(session, tag, blockState); + } i++; } @@ -357,6 +362,12 @@ public class ChunkUtils { } } + SkullPlayerEntity skull = session.getSkullCache().get(position); + if (skull != null && blockState != skull.getBlockState()) { + // Skull is gone + skull.despawnEntity(session, position); + } + int blockId = BlockTranslator.getBedrockBlockId(blockState); UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket(); diff --git a/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java b/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java index 9db47d64b..de9bcf884 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java @@ -66,6 +66,7 @@ public class DimensionUtils { session.getEntityCache().removeAllEntities(); session.getItemFrameCache().clear(); + session.getSkullCache().clear(); if (session.getPendingDimSwitches().getAndIncrement() > 0) { ChunkUtils.sendEmptyChunks(session, player.getPosition().toInt(), 3, true); } diff --git a/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java b/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java index d99b645e2..4277f5388 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java @@ -215,14 +215,27 @@ public class FileUtils { * @return The byte array of the file */ public static byte[] readAllBytes(File file) { - int size = (int) file.length(); - byte[] bytes = new byte[size]; try { - BufferedInputStream buf = new BufferedInputStream(new FileInputStream(file)); + return readAllBytes(new FileInputStream(file)); + } catch (IOException e) { + throw new RuntimeException("Cannot read " + file); + } + } + + /** + * @param stream the InputStream to read off of + * @return the byte array of an InputStream + */ + public static byte[] readAllBytes(InputStream stream) { + try { + int size = stream.available(); + byte[] bytes = new byte[size]; + BufferedInputStream buf = new BufferedInputStream(stream); buf.read(bytes, 0, bytes.length); buf.close(); - } catch (IOException ignored) { } - - return bytes; + return bytes; + } catch (IOException e) { + throw new RuntimeException("Error while trying to read input stream!"); + } } } diff --git a/connector/src/main/java/org/geysermc/connector/utils/WebUtils.java b/connector/src/main/java/org/geysermc/connector/utils/WebUtils.java index 94167c3e6..329358402 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/WebUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/WebUtils.java @@ -25,6 +25,7 @@ package org.geysermc.connector.utils; +import com.fasterxml.jackson.databind.JsonNode; import org.geysermc.connector.GeyserConnector; import java.io.*; @@ -57,6 +58,18 @@ public class WebUtils { } } + /** + * Makes a web request to the given URL and returns the body as a {@link JsonNode}. + * + * @param reqURL URL to fetch + * @return the response as JSON + */ + public static JsonNode getJson(String reqURL) throws IOException { + HttpURLConnection con = (HttpURLConnection) new URL(reqURL).openConnection(); + con.setRequestProperty("User-Agent", "Geyser-" + GeyserConnector.getInstance().getPlatformType().toString() + "/" + GeyserConnector.VERSION); + return GeyserConnector.JSON_MAPPER.readTree(con.getInputStream()); + } + /** * Downloads a file from the given URL and saves it to disk * diff --git a/connector/src/main/resources/bedrock/skin/geometry.humanoid.customskull.json b/connector/src/main/resources/bedrock/skin/geometry.humanoid.customskull.json new file mode 100644 index 000000000..88cf65ad2 --- /dev/null +++ b/connector/src/main/resources/bedrock/skin/geometry.humanoid.customskull.json @@ -0,0 +1,27 @@ +{ + "format_version": "1.10.0", + "geometry.humanoid.customskull": { + "texturewidth": 64, + "textureheight": 64, + "visible_bounds_width": 2, + "visible_bounds_height": 1, + "visible_bounds_offset": [0, 0, 0], + "bones": [ + { + "name": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-4, 0, -4], "size": [8, 8, 8], "uv": [0, 0]} + ] + }, + { + "name": "hat", + "parent": "head", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-4, 0, -4], "size": [8, 8, 8], "uv": [32, 0], "inflate": 0.5} + ] + } + ] + } +} \ No newline at end of file diff --git a/connector/src/main/resources/config.yml b/connector/src/main/resources/config.yml index 79e59be9a..5c22037df 100644 --- a/connector/src/main/resources/config.yml +++ b/connector/src/main/resources/config.yml @@ -109,6 +109,9 @@ cache-chunks: true # A value of 0 is disabled. (Default: 0) cache-images: 0 +# Allows custom skulls to be displayed. Keeping them enabled may cause a performance decrease on older/weaker devices. +allow-custom-skulls: true + # Bedrock prevents building and displaying blocks above Y127 in the Nether - # enabling this config option works around that by changing the Nether dimension ID # to the End ID. The main downside to this is that the sky will resemble that of