diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index 3e18a2829..261e57aed 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -35,7 +35,6 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; @@ -94,6 +93,7 @@ import org.cloudburstmc.protocol.bedrock.packet.ItemComponentPacket; import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEvent2Packet; import org.cloudburstmc.protocol.bedrock.packet.PlayStatusPacket; +import org.cloudburstmc.protocol.bedrock.packet.SetCommandsEnabledPacket; import org.cloudburstmc.protocol.bedrock.packet.SetTimePacket; import org.cloudburstmc.protocol.bedrock.packet.StartGamePacket; import org.cloudburstmc.protocol.bedrock.packet.SyncEntityPropertyPacket; @@ -331,10 +331,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { private final Map itemFrameCache = new Object2ObjectOpenHashMap<>(); /** - * A list of all players that have a player head on with a custom texture. + * A map of all players (and their heads) that are wearing a player head with a custom texture. * Our workaround for these players is to give them a custom skin and geometry to emulate wearing a custom skull. */ - private final Set playerWithCustomHeads = new ObjectOpenHashSet<>(); + private final Map playerWithCustomHeads = new Object2ObjectOpenHashMap<>(); @Setter private boolean droppingLecternBook; @@ -787,6 +787,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { playStatusPacket.setStatus(PlayStatusPacket.Status.PLAYER_SPAWN); upstream.sendPacket(playStatusPacket); + SetCommandsEnabledPacket setCommandsEnabledPacket = new SetCommandsEnabledPacket(); + setCommandsEnabledPacket.setCommandsEnabled(!geyser.getConfig().isXboxAchievementsEnabled()); + upstream.sendPacket(setCommandsEnabledPacket); + UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket(); attributesPacket.setRuntimeEntityId(getPlayerEntity().getGeyserId()); // Default move speed 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 2434d6d91..12f002025 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/FakeHeadProvider.java +++ b/core/src/main/java/org/geysermc/geyser/skin/FakeHeadProvider.java @@ -109,6 +109,12 @@ public class FakeHeadProvider { return; } + GameProfile current = session.getPlayerWithCustomHeads().get(entity.getUuid()); + if (profile.equals(current)) { + // We already did this, no need to re-compute + return; + } + Map textures; try { textures = profile.getTextures(false); @@ -118,7 +124,7 @@ public class FakeHeadProvider { } if (textures == null || textures.isEmpty()) { - loadHead(session, entity, profile.getName()); + loadHeadFromProfile(session, entity, profile); return; } @@ -133,16 +139,18 @@ public class FakeHeadProvider { boolean isAlex = skinTexture.getModel() == TextureModel.SLIM; - loadHead(session, entity, new GameProfileData(skinTexture.getURL(), capeUrl, isAlex)); + loadHeadFromProfile(session, entity, new GameProfileData(skinTexture.getURL(), capeUrl, isAlex), profile); } - public static void loadHead(GeyserSession session, PlayerEntity entity, String owner) { - if (owner == null || owner.isEmpty()) { - return; + public static void loadHeadFromProfile(GeyserSession session, PlayerEntity entity, GameProfile profile) { + CompletableFuture texturesFuture; + if (profile.getId() != null) { + texturesFuture = SkinProvider.requestTexturesFromUUID(profile.getId().toString()); + } else { + texturesFuture = SkinProvider.requestTexturesFromUsername(profile.getName()); } - CompletableFuture completableFuture = SkinProvider.requestTexturesFromUsername(owner); - completableFuture.whenCompleteAsync((encodedJson, throwable) -> { + texturesFuture.whenCompleteAsync((encodedJson, throwable) -> { if (throwable != null) { GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), throwable); return; @@ -152,17 +160,17 @@ public class FakeHeadProvider { if (gameProfileData == null) { return; } - loadHead(session, entity, gameProfileData); + loadHeadFromProfile(session, entity, gameProfileData, profile); } 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) { + public static void loadHeadFromProfile(GeyserSession session, PlayerEntity entity, SkinManager.GameProfileData gameProfileData, GameProfile profile) { String fakeHeadSkinUrl = gameProfileData.skinUrl(); - session.getPlayerWithCustomHeads().add(entity.getUuid()); + session.getPlayerWithCustomHeads().put(entity.getUuid(), profile); String texturesProperty = entity.getTexturesProperty(); SkinProvider.getExecutorService().execute(() -> { try { @@ -179,7 +187,7 @@ public class FakeHeadProvider { return; } - if (!session.getPlayerWithCustomHeads().remove(entity.getUuid())) { + if (session.getPlayerWithCustomHeads().remove(entity.getUuid()) == null) { return; } 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 4c3db7504..8d4090ee4 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java +++ b/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java @@ -47,7 +47,6 @@ import org.geysermc.geyser.text.GeyserLocale; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Base64; -import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.function.Consumer; @@ -102,7 +101,7 @@ public class SkinManager { Skin skin, Cape cape, SkinGeometry geometry) { - SerializedSkin serializedSkin = getSkin(skin.textureUrl(), skin, cape, geometry); + SerializedSkin serializedSkin = getSkin(session, skin.textureUrl(), skin, cape, geometry); // This attempts to find the XUID of the player so profile images show up for Xbox accounts String xuid = ""; @@ -138,7 +137,6 @@ public class SkinManager { SkinGeometry geometry = skinData.geometry(); if (entity.getUuid().equals(session.getPlayerEntity().getUuid())) { - // TODO is this special behavior needed? PlayerListPacket.Entry updatedEntry = buildEntryManually( session, entity.getUuid(), @@ -158,17 +156,24 @@ public class SkinManager { packet.setUuid(entity.getUuid()); packet.setOldSkinName(""); packet.setNewSkinName(skin.textureUrl()); - packet.setSkin(getSkin(skin.textureUrl(), skin, cape, geometry)); + packet.setSkin(getSkin(session, skin.textureUrl(), skin, cape, geometry)); packet.setTrustedSkin(true); session.sendUpstreamPacket(packet); } } - private static SerializedSkin getSkin(String skinId, Skin skin, Cape cape, SkinGeometry geometry) { - return SerializedSkin.of(skinId, "", geometry.geometryName(), - ImageData.of(skin.skinData()), Collections.emptyList(), - ImageData.of(cape.capeData()), geometry.geometryData(), - "", true, false, false, cape.capeId(), skinId); + private static SerializedSkin getSkin(GeyserSession session, String skinId, Skin skin, Cape cape, SkinGeometry geometry) { + return SerializedSkin.builder() + .skinId(skinId) + .skinResourcePatch(geometry.geometryName()) + .skinData(ImageData.of(skin.skinData())) + .capeData(ImageData.of(cape.capeData())) + .geometryData(geometry.geometryData()) + .premium(true) + .capeId(cape.capeId()) + .fullSkinId(skinId) + .geometryDataEngineVersion(session.getClientData().getGameVersion()) + .build(); } public static void requestAndHandleSkinAndCape(PlayerEntity entity, GeyserSession session, @@ -334,4 +339,4 @@ public class SkinManager { private static final String DEFAULT_FLOODGATE_STEVE = "https://textures.minecraft.net/texture/31f477eb1a7beee631c2ca64d06f8f68fa93a3386d04452ab27f43acdf1b60cb"; } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/geysermc/geyser/skin/SkullSkinManager.java b/core/src/main/java/org/geysermc/geyser/skin/SkullSkinManager.java index e3f00d3b7..41e4025f1 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/SkullSkinManager.java +++ b/core/src/main/java/org/geysermc/geyser/skin/SkullSkinManager.java @@ -35,20 +35,24 @@ import org.geysermc.geyser.entity.type.player.SkullPlayerEntity; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.GeyserLocale; -import java.util.Collections; import java.util.function.BiConsumer; import java.util.function.Consumer; public class SkullSkinManager extends SkinManager { - public static SerializedSkin buildSkullEntryManually(String skinId, byte[] skinData) { - // Prevents https://cdn.discordapp.com/attachments/613194828359925800/779458146191147008/unknown.png + public static SerializedSkin buildSkullEntryManually(GeyserSession session, String skinId, byte[] skinData) { skinId = skinId + "_skull"; - return SerializedSkin.of( - skinId, "", SkinProvider.SKULL_GEOMETRY.geometryName(), ImageData.of(skinData), Collections.emptyList(), - ImageData.of(SkinProvider.EMPTY_CAPE.capeData()), SkinProvider.SKULL_GEOMETRY.geometryData(), - "", true, false, false, SkinProvider.EMPTY_CAPE.capeId(), skinId - ); + return SerializedSkin.builder() + .skinId(skinId) + .skinResourcePatch(SkinProvider.SKULL_GEOMETRY.geometryName()) + .skinData(ImageData.of(skinData)) + .capeData(ImageData.of(SkinProvider.EMPTY_CAPE.capeData())) + .geometryData(SkinProvider.SKULL_GEOMETRY.geometryData()) + .premium(true) + .capeId(SkinProvider.EMPTY_CAPE.capeId()) + .fullSkinId(skinId) + .geometryDataEngineVersion(session.getClientData().getGameVersion()) + .build(); } public static void requestAndHandleSkin(SkullPlayerEntity entity, GeyserSession session, @@ -59,7 +63,7 @@ public class SkullSkinManager extends SkinManager { packet.setUuid(entity.getUuid()); packet.setOldSkinName(""); packet.setNewSkinName(skin.textureUrl()); - packet.setSkin(buildSkullEntryManually(skin.textureUrl(), skin.skinData())); + packet.setSkin(buildSkullEntryManually(session, skin.textureUrl(), skin.skinData())); packet.setTrustedSkin(true); session.sendUpstreamPacket(packet); } catch (Exception e) { 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 21ffdfa96..6394c5312 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 @@ -76,7 +76,6 @@ import org.geysermc.geyser.util.InventoryUtils; import org.geysermc.geyser.util.ItemUtils; import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; -import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.EmptySlotDisplay; import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.SlotDisplay; @@ -258,14 +257,14 @@ public abstract class InventoryTranslator { if (this instanceof PlayerInventoryTranslator) { if (destSlot == 5) { - //only set the head if the destination is the head slot + // only set the head if the destination is the head slot GeyserItemStack javaItem = inventory.getItem(sourceSlot); if (javaItem.asItem() == Items.PLAYER_HEAD && javaItem.hasNonBaseComponents()) { FakeHeadProvider.setHead(session, session.getPlayerEntity(), javaItem.getComponent(DataComponentTypes.PROFILE)); } } else if (sourceSlot == 5) { - //we are probably removing the head, so restore the original skin + // we are probably removing the head, so restore the original skin FakeHeadProvider.restoreOriginalSkin(session, session.getPlayerEntity()); } } 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 f95bbb7c4..7064f1169 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 @@ -279,9 +279,22 @@ public class PlayerInventoryTranslator extends InventoryTranslator { return bundleResponse; } + int sourceSlot = bedrockSlotToJava(transferAction.getSource()); + int destSlot = bedrockSlotToJava(transferAction.getDestination()); + 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.hasNonBaseComponents()) { + FakeHeadProvider.setHead(session, session.getPlayerEntity(), javaItem.getComponent(DataComponentTypes.PROFILE)); + } + } else if (sourceSlot == 5) { + // we are probably removing the head, so restore the original skin + FakeHeadProvider.restoreOriginalSkin(session, session.getPlayerEntity()); + } + int transferAmount = transferAction.getCount(); if (isCursor(transferAction.getDestination())) { - int sourceSlot = bedrockSlotToJava(transferAction.getSource()); GeyserItemStack sourceItem = inventory.getItem(sourceSlot); if (playerInv.getCursor().isEmpty()) { playerInv.setCursor(sourceItem.copy(0), session); @@ -294,7 +307,6 @@ public class PlayerInventoryTranslator extends InventoryTranslator { affectedSlots.add(sourceSlot); } else if (isCursor(transferAction.getSource())) { - int destSlot = bedrockSlotToJava(transferAction.getDestination()); GeyserItemStack sourceItem = playerInv.getCursor(); if (inventory.getItem(destSlot).isEmpty()) { inventory.setItem(destSlot, sourceItem.copy(0), session); @@ -307,8 +319,6 @@ public class PlayerInventoryTranslator extends InventoryTranslator { affectedSlots.add(destSlot); } else { - int sourceSlot = bedrockSlotToJava(transferAction.getSource()); - int destSlot = bedrockSlotToJava(transferAction.getDestination()); GeyserItemStack sourceItem = inventory.getItem(sourceSlot); if (inventory.getItem(destSlot).isEmpty()) { inventory.setItem(destSlot, sourceItem.copy(0), session);