diff --git a/core/src/main/java/org/geysermc/geyser/skin/FakeHeadProvider.java b/core/src/main/java/org/geysermc/geyser/skin/FakeHeadProvider.java index b312f9811..95a2dcb1a 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/FakeHeadProvider.java +++ b/core/src/main/java/org/geysermc/geyser/skin/FakeHeadProvider.java @@ -26,6 +26,8 @@ package org.geysermc.geyser.skin; 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 com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; @@ -41,7 +43,9 @@ import org.geysermc.geyser.text.GeyserLocale; import javax.annotation.Nonnull; import java.awt.*; import java.awt.image.BufferedImage; +import java.io.IOException; import java.util.Objects; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -91,21 +95,48 @@ public class FakeHeadProvider { } }); - public static void setHead(GeyserSession session, PlayerEntity entity, CompoundTag profileTag) { - SkinManager.GameProfileData gameProfileData = SkinManager.GameProfileData.from(profileTag); - if (gameProfileData == null) { + public static void setHead(GeyserSession session, PlayerEntity entity, Tag skullOwner) { + if (skullOwner == null) { return; } + if (skullOwner instanceof CompoundTag profileTag) { + SkinManager.GameProfileData gameProfileData = SkinManager.GameProfileData.from(profileTag); + if (gameProfileData == null) { + return; + } + loadHead(session, entity, gameProfileData); + } else if (skullOwner instanceof StringTag ownerTag) { + String owner = ownerTag.getValue(); + if (owner.isEmpty()) { + return; + } + CompletableFuture completableFuture = SkinProvider.requestTexturesFromUsername(owner); + completableFuture.whenCompleteAsync((encodedJson, throwable) -> { + if (throwable != null) { + GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), throwable); + return; + } + try { + SkinManager.GameProfileData gameProfileData = SkinManager.GameProfileData.loadFromJson(encodedJson); + if (gameProfileData == null) { + return; + } + loadHead(session, entity, gameProfileData); + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid(), e.getMessage())); + } + }); + } + } + + public static void loadHead(GeyserSession session, PlayerEntity entity, SkinManager.GameProfileData gameProfileData) { String fakeHeadSkinUrl = gameProfileData.skinUrl(); session.getPlayerWithCustomHeads().add(entity.getUuid()); - String texturesProperty = entity.getTexturesProperty(); - SkinProvider.EXECUTOR_SERVICE.execute(() -> { try { SkinProvider.SkinData mergedSkinData = MERGED_SKINS_LOADING_CACHE.get(new FakeHeadEntry(texturesProperty, fakeHeadSkinUrl, entity)); - SkinManager.sendSkinPacket(session, entity, mergedSkinData); } catch (ExecutionException e) { GeyserImpl.getInstance().getLogger().error("Couldn't merge skin of " + entity.getUsername() + " with head skin url " + fakeHeadSkinUrl, e); @@ -155,4 +186,4 @@ public class FakeHeadProvider { } } -} +} \ No newline at end of file diff --git a/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java b/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java index b30f428ba..b88bbe23c 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java +++ b/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java @@ -277,7 +277,7 @@ public class SkinManager { return null; } - private static GameProfileData loadFromJson(String encodedJson) throws IOException, IllegalArgumentException { + static GameProfileData loadFromJson(String encodedJson) throws IOException, IllegalArgumentException { JsonNode skinObject = GeyserImpl.JSON_MAPPER.readTree(new String(Base64.getDecoder().decode(encodedJson), StandardCharsets.UTF_8)); JsonNode textures = skinObject.get("textures"); @@ -312,4 +312,4 @@ public class SkinManager { return new GameProfileData(skinUrl, capeUrl, isAlex); } } -} +} \ No newline at end of file diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java index 4a43ea055..e39e62c04 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java @@ -49,7 +49,9 @@ import org.geysermc.geyser.inventory.click.ClickPlan; import org.geysermc.geyser.inventory.recipe.GeyserRecipe; import org.geysermc.geyser.inventory.recipe.GeyserShapedRecipe; import org.geysermc.geyser.inventory.recipe.GeyserShapelessRecipe; +import org.geysermc.geyser.item.Items; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.skin.FakeHeadProvider; import org.geysermc.geyser.translator.inventory.chest.DoubleChestInventoryTranslator; import org.geysermc.geyser.translator.inventory.chest.SingleChestInventoryTranslator; import org.geysermc.geyser.translator.inventory.furnace.BlastFurnaceInventoryTranslator; @@ -216,6 +218,20 @@ public abstract class InventoryTranslator { boolean isSourceCursor = isCursor(transferAction.getSource()); boolean isDestCursor = isCursor(transferAction.getDestination()); + if ((this) instanceof PlayerInventoryTranslator) { + if (destSlot == 5) { + //only set the head if the destination is the head slot + GeyserItemStack javaItem = inventory.getItem(sourceSlot); + if (javaItem.asItem() == Items.PLAYER_HEAD + && javaItem.getNbt() != null) { + FakeHeadProvider.setHead(session, session.getPlayerEntity(), javaItem.getNbt().get("SkullOwner")); + } + } else if (sourceSlot == 5) { + //we are probably removing the head, so restore the original skin + FakeHeadProvider.restoreOriginalSkin(session, session.getPlayerEntity()); + } + } + if (shouldRejectItemPlace(session, inventory, transferAction.getSource().getContainer(), isSourceCursor ? -1 : sourceSlot, transferAction.getDestination().getContainer(), isDestCursor ? -1 : destSlot)) { @@ -916,4 +932,4 @@ public abstract class InventoryTranslator { TRANSFER, DONE } -} +} \ No newline at end of file diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java index 2bfc9a18b..b9468ac4f 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java @@ -29,7 +29,6 @@ import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType; import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundSetCreativeModeSlotPacket; -import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import it.unimi.dsi.fastutil.ints.IntIterator; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntSet; @@ -94,7 +93,13 @@ public class PlayerInventoryTranslator extends InventoryTranslator { armorContentPacket.setContainerId(ContainerId.ARMOR); contents = new ItemData[4]; for (int i = 5; i < 9; i++) { - contents[i - 5] = inventory.getItem(i).getItemData(session); + GeyserItemStack item = inventory.getItem(i); + contents[i - 5] = item.getItemData(session); + if (i == 5 && + item.asItem() == Items.PLAYER_HEAD && + item.getNbt() != null) { + FakeHeadProvider.setHead(session, session.getPlayerEntity(), item.getNbt().get("SkullOwner")); + } } armorContentPacket.setContents(Arrays.asList(contents)); session.sendUpstreamPacket(armorContentPacket); @@ -136,9 +141,8 @@ public class PlayerInventoryTranslator extends InventoryTranslator { if (slot == 5) { // Check for custom skull if (javaItem.asItem() == Items.PLAYER_HEAD - && javaItem.getNbt() != null - && javaItem.getNbt().get("SkullOwner") instanceof CompoundTag profile) { - FakeHeadProvider.setHead(session, session.getPlayerEntity(), profile); + && javaItem.getNbt() != null) { + FakeHeadProvider.setHead(session, session.getPlayerEntity(), javaItem.getNbt().get("SkullOwner")); } else { FakeHeadProvider.restoreOriginalSkin(session, session.getPlayerEntity()); } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java index 303fdb13a..7d0fa4691 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java @@ -44,6 +44,7 @@ import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.cloudburstmc.protocol.bedrock.data.inventory.transaction.InventoryActionData; import org.cloudburstmc.protocol.bedrock.data.inventory.transaction.InventorySource; +import org.cloudburstmc.protocol.bedrock.data.inventory.transaction.LegacySetItemSlotData; import org.cloudburstmc.protocol.bedrock.packet.ContainerOpenPacket; import org.cloudburstmc.protocol.bedrock.packet.InventoryTransactionPacket; import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; @@ -63,8 +64,8 @@ import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.item.type.SpawnEggItem; import org.geysermc.geyser.level.block.BlockStateValues; import org.geysermc.geyser.registry.BlockRegistries; -import org.geysermc.geyser.registry.type.ItemMappings; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.skin.FakeHeadProvider; import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.translator.inventory.item.ItemTranslator; import org.geysermc.geyser.translator.protocol.PacketTranslator; @@ -73,7 +74,9 @@ import org.geysermc.geyser.util.BlockUtils; import org.geysermc.geyser.util.CooldownUtils; import org.geysermc.geyser.util.EntityUtils; import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InventoryUtils; +import java.util.List; import java.util.concurrent.TimeUnit; /** @@ -93,8 +96,6 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator legacySlots = packet.getLegacySlots(); + if (packet.getActions().size() == 1 && legacySlots.size() > 0) { + InventoryActionData actionData = packet.getActions().get(0); + LegacySetItemSlotData slotData = legacySlots.get(0); + if (slotData.getContainerId() == 6 && !actionData.getFromItem().isNull()) { + // The player is trying to swap out an armor piece that already has an item in it + // 1.19.4 brings this natively, but we need this specific case for custom head rendering to work + int bedrockHotbarSlot = packet.getHotbarSlot(); + Click click = InventoryUtils.getClickForHotbarSwap(bedrockHotbarSlot); + if (click != null && slotData.getSlots().length != 0) { + Inventory playerInventory = session.getPlayerInventory(); + // Bedrock sends us the index of the slot in the armor container; armor in Java + // Edition is offset by 5 in the player inventory + int armorSlot = slotData.getSlots()[0] + 5; + if (armorSlot == 5) { + GeyserItemStack armorSlotItem = playerInventory.getItem(armorSlot); + if (armorSlotItem.asItem() == Items.PLAYER_HEAD) { + FakeHeadProvider.restoreOriginalSkin(session, session.getPlayerEntity()); + } + } + } + } + } } case 2 -> { int blockState = session.getGameMode() == GameMode.CREATIVE ? @@ -618,4 +643,4 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator