From c928bafdddd6e71d9eaeaeb651ea8a55c09d105f Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Sat, 7 Jan 2023 14:46:22 -0500 Subject: [PATCH 1/9] Should fix some instances of chat not showing --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f7f488f17..a54513df9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ websocket = "1.5.1" protocol = "2.9.15-20221129.204554-2" raknet = "1.6.28-20220125.214016-6" mcauthlib = "d9d773e" -mcprotocollib = "1.19.3-20230104.210231-9" +mcprotocollib = "1.19.3-20230107.194116-10" packetlib = "3.0.1" adventure = "4.12.0-20220629.025215-9" adventure-platform = "4.1.2" From 0b80c589584129ba2280463f4c65fcd84c6d77db Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Mon, 16 Jan 2023 19:16:01 -0500 Subject: [PATCH 2/9] Handle invalid items in shulker box --- .../inventory/item/nbt/ShulkerBoxItemTranslator.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/item/nbt/ShulkerBoxItemTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/nbt/ShulkerBoxItemTranslator.java index f95c54e18..29d97dc27 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/item/nbt/ShulkerBoxItemTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/nbt/ShulkerBoxItemTranslator.java @@ -52,6 +52,11 @@ public class ShulkerBoxItemTranslator extends NbtItemStackTranslator { ItemMapping boxMapping = session.getItemMappings().getMapping(Identifier.formalize(((StringTag) itemData.get("id")).getValue())); + if (boxMapping == null) { + // If invalid ID + continue; + } + boxItemTag.put(new StringTag("Name", boxMapping.getBedrockIdentifier())); boxItemTag.put(new ShortTag("Damage", (short) boxMapping.getBedrockData())); boxItemTag.put(new ByteTag("Count", MathUtils.getNbtByte(itemData.get("Count").getValue()))); From b8040a1d9849d9f4ba62ff9e5a9a2447820dd438 Mon Sep 17 00:00:00 2001 From: Kas-tle <26531652+Kas-tle@users.noreply.github.com> Date: Fri, 20 Jan 2023 07:02:20 -0800 Subject: [PATCH 3/9] Fix some instances of chunks not appearing (#3498) --- .../main/java/org/geysermc/geyser/session/GeyserSession.java | 4 ++++ 1 file changed, 4 insertions(+) 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 9ca124393..b629fa4d4 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -1402,6 +1402,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { } public void setServerRenderDistance(int renderDistance) { + // +1 is for Fabric and Spigot + // Without the client misses loading some chunks per https://github.com/GeyserMC/Geyser/issues/3490 + // Fog still appears essentially normally + renderDistance = renderDistance + 1; this.serverRenderDistance = renderDistance; ChunkRadiusUpdatedPacket chunkRadiusUpdatedPacket = new ChunkRadiusUpdatedPacket(); From 48d78720a170ba7630f26d415f1905400fe36d25 Mon Sep 17 00:00:00 2001 From: David Choo <4722249+davchoo@users.noreply.github.com> Date: Sun, 22 Jan 2023 13:23:16 -0500 Subject: [PATCH 4/9] Apply fallback skins to custom skulls with invalid or empty texture values (#3515) --- .../entity/type/player/SkullPlayerEntity.java | 14 +++- .../geyser/session/cache/SkullCache.java | 4 +- .../org/geysermc/geyser/skin/SkinManager.java | 40 ++++++---- .../geysermc/geyser/skin/SkinProvider.java | 78 ++++++++++--------- .../geyser/skin/SkullSkinManager.java | 52 ++++++++----- .../entity/SkullBlockEntityTranslator.java | 65 +++++++++++----- 6 files changed, 159 insertions(+), 94 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/SkullPlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/SkullPlayerEntity.java index c2af2e36b..369436b21 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/player/SkullPlayerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/SkullPlayerEntity.java @@ -26,17 +26,20 @@ package org.geysermc.geyser.entity.type.player; import com.nukkitx.math.vector.Vector3f; +import com.nukkitx.math.vector.Vector3i; import com.nukkitx.protocol.bedrock.data.GameType; import com.nukkitx.protocol.bedrock.data.PlayerPermission; import com.nukkitx.protocol.bedrock.data.command.CommandPermission; import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import com.nukkitx.protocol.bedrock.packet.AddPlayerPacket; +import lombok.Getter; import org.geysermc.geyser.level.block.BlockStateValues; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.SkullCache; import org.geysermc.geyser.skin.SkullSkinManager; +import java.util.Objects; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -46,6 +49,12 @@ import java.util.concurrent.TimeUnit; */ public class SkullPlayerEntity extends PlayerEntity { + @Getter + private UUID skullUUID; + + @Getter + private Vector3i skullPosition; + public SkullPlayerEntity(GeyserSession session, long geyserId) { super(session, 0, geyserId, UUID.randomUUID(), Vector3f.ZERO, Vector3f.ZERO, 0, 0, 0, "", null); } @@ -102,11 +111,14 @@ public class SkullPlayerEntity extends PlayerEntity { } public void updateSkull(SkullCache.Skull skull) { - if (!skull.getTexturesProperty().equals(getTexturesProperty())) { + skullPosition = skull.getPosition(); + + if (!Objects.equals(skull.getTexturesProperty(), getTexturesProperty()) || !Objects.equals(skullUUID, skull.getUuid())) { // Make skull invisible as we change skins setFlag(EntityFlag.INVISIBLE, true); updateBedrockMetadata(); + skullUUID = skull.getUuid(); setTexturesProperty(skull.getTexturesProperty()); SkullSkinManager.requestAndHandleSkin(this, session, (skin -> session.scheduleInEventLoop(() -> { diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java index f26e1cce3..6b25e84b4 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java @@ -71,8 +71,9 @@ public class SkullCache { this.skullRenderDistanceSquared = distance * distance; } - public void putSkull(Vector3i position, String texturesProperty, int blockState) { + public void putSkull(Vector3i position, UUID uuid, String texturesProperty, int blockState) { Skull skull = skulls.computeIfAbsent(position, Skull::new); + skull.uuid = uuid; skull.texturesProperty = texturesProperty; skull.blockState = blockState; @@ -201,6 +202,7 @@ public class SkullCache { @RequiredArgsConstructor @Data public static class Skull { + private UUID uuid; private String texturesProperty; private int blockState; private SkullPlayerEntity entity; 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 800b71c96..48233fe3d 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java +++ b/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java @@ -35,6 +35,7 @@ import com.nukkitx.protocol.bedrock.packet.PlayerListPacket; import com.nukkitx.protocol.bedrock.packet.PlayerSkinPacket; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.entity.type.player.PlayerEntity; +import org.geysermc.geyser.entity.type.player.SkullPlayerEntity; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.auth.BedrockClientData; import org.geysermc.geyser.text.GeyserLocale; @@ -69,7 +70,7 @@ public class SkinManager { // The server either didn't have a texture to send, or we didn't have the texture ID cached. // Let's see if this player is a Bedrock player, and if so, let's pull their skin. // Otherwise, grab the default player skin - SkinProvider.SkinData fallbackSkinData = SkinProvider.determineFallbackSkinData(playerEntity); + SkinProvider.SkinData fallbackSkinData = SkinProvider.determineFallbackSkinData(playerEntity.getUuid()); if (skin == null) { skin = fallbackSkinData.skin(); geometry = fallbackSkinData.geometry(); @@ -255,24 +256,28 @@ public class SkinManager { * @return The built GameProfileData */ public static @Nullable GameProfileData from(PlayerEntity entity) { - try { - String texturesProperty = entity.getTexturesProperty(); + String texturesProperty = entity.getTexturesProperty(); + if (texturesProperty == null) { + // Likely offline mode + return null; + } - if (texturesProperty == null) { - // Likely offline mode - return null; - } + try { return loadFromJson(texturesProperty); - } catch (IOException exception) { - GeyserImpl.getInstance().getLogger().debug("Something went wrong while processing skin for " + entity.getUsername()); + } catch (Exception exception) { + if (entity instanceof SkullPlayerEntity skullEntity) { + GeyserImpl.getInstance().getLogger().debug("Something went wrong while processing skin for skull at " + skullEntity.getSkullPosition() + " with Value: " + texturesProperty); + } else { + GeyserImpl.getInstance().getLogger().debug("Something went wrong while processing skin for " + entity.getUsername() + " with Value: " + texturesProperty); + } if (GeyserImpl.getInstance().getConfig().isDebugMode()) { exception.printStackTrace(); } - return null; } + return null; } - private static GameProfileData loadFromJson(String encodedJson) throws IOException { + private 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"); @@ -285,14 +290,23 @@ public class SkinManager { return null; } - String skinUrl = skinTexture.get("url").asText().replace("http://", "https://"); + String skinUrl; + JsonNode skinUrlNode = skinTexture.get("url"); + if (skinUrlNode != null && skinUrlNode.isTextual()) { + skinUrl = skinUrlNode.asText().replace("http://", "https://"); + } else { + return null; + } boolean isAlex = skinTexture.has("metadata"); String capeUrl = null; JsonNode capeTexture = textures.get("CAPE"); if (capeTexture != null) { - capeUrl = capeTexture.get("url").asText().replace("http://", "https://"); + JsonNode capeUrlNode = capeTexture.get("url"); + if (capeUrlNode != null && capeUrlNode.isTextual()) { + capeUrl = capeUrlNode.asText().replace("http://", "https://"); + } } return new GameProfileData(skinUrl, capeUrl, isAlex); diff --git a/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java b/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java index 61f24ac1e..38ff92e8f 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java +++ b/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java @@ -26,9 +26,6 @@ package org.geysermc.geyser.skin; import com.fasterxml.jackson.databind.JsonNode; -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 it.unimi.dsi.fastutil.bytes.ByteArrays; @@ -172,14 +169,13 @@ public class SkinProvider { /** * If skin data fails to apply, or there is no skin data to apply, determine what skin we should give as a fallback. */ - static SkinData determineFallbackSkinData(PlayerEntity entity) { + static SkinData determineFallbackSkinData(UUID uuid) { Skin skin = null; Cape cape = null; SkinGeometry geometry = SkinGeometry.WIDE; if (GeyserImpl.getInstance().getConfig().getRemote().authType() != AuthType.ONLINE) { // Let's see if this player is a Bedrock player, and if so, let's pull their skin. - UUID uuid = entity.getUuid(); GeyserSession session = GeyserImpl.getInstance().connectionByUuid(uuid); if (session != null) { String skinId = session.getClientData().getSkinId(); @@ -192,7 +188,7 @@ public class SkinProvider { if (skin == null) { // We don't have a skin for the player right now. Fall back to a default. - ProvidedSkins.ProvidedSkin providedSkin = ProvidedSkins.getDefaultPlayerSkin(entity.getUuid()); + ProvidedSkins.ProvidedSkin providedSkin = ProvidedSkins.getDefaultPlayerSkin(uuid); skin = providedSkin.getData(); geometry = providedSkin.isSlim() ? SkinProvider.SkinGeometry.SLIM : SkinProvider.SkinGeometry.WIDE; } @@ -232,7 +228,7 @@ public class SkinProvider { SkinManager.GameProfileData data = SkinManager.GameProfileData.from(entity); if (data == null) { // This player likely does not have a textures property - return CompletableFuture.completedFuture(determineFallbackSkinData(entity)); + return CompletableFuture.completedFuture(determineFallbackSkinData(entity.getUuid())); } return requestSkinAndCape(entity.getUuid(), data.skinUrl(), data.capeUrl()) @@ -597,48 +593,23 @@ public class SkinProvider { } /** - * If a skull has a username but no textures, request them. + * Request textures from a player's UUID * - * @param skullOwner the CompoundTag of the skull with no textures + * @param uuid the player's UUID without any hyphens * @return a completable GameProfile with textures included */ - public static CompletableFuture requestTexturesFromUsername(CompoundTag skullOwner) { + public static CompletableFuture requestTexturesFromUUID(String uuid) { return CompletableFuture.supplyAsync(() -> { - Tag uuidTag = skullOwner.get("Id"); - String uuidToString = ""; - JsonNode node; - 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) { - GeyserImpl.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); + JsonNode node = WebUtils.getJson("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid); JsonNode properties = node.get("properties"); if (properties == null) { - GeyserImpl.getInstance().getLogger().debug("No properties found in Mojang response for " + uuidToString); + GeyserImpl.getInstance().getLogger().debug("No properties found in Mojang response for " + uuid); return null; } return node.get("properties").get(0).get("value").asText(); } catch (Exception e) { + GeyserImpl.getInstance().getLogger().debug("Unable to request textures for " + uuid); if (GeyserImpl.getInstance().getConfig().isDebugMode()) { e.printStackTrace(); } @@ -647,6 +618,37 @@ public class SkinProvider { }, EXECUTOR_SERVICE); } + /** + * Request textures from a player's username + * + * @param username the player's username + * @return a completable GameProfile with textures included + */ + public static CompletableFuture requestTexturesFromUsername(String username) { + return CompletableFuture.supplyAsync(() -> { + try { + // Offline skin, or no present UUID + JsonNode node = WebUtils.getJson("https://api.mojang.com/users/profiles/minecraft/" + username); + JsonNode id = node.get("id"); + if (id == null) { + GeyserImpl.getInstance().getLogger().debug("No UUID found in Mojang response for " + username); + return null; + } + return id.asText(); + } catch (Exception e) { + if (GeyserImpl.getInstance().getConfig().isDebugMode()) { + e.printStackTrace(); + } + return null; + } + }, EXECUTOR_SERVICE).thenCompose(uuid -> { + if (uuid == null) { + return CompletableFuture.completedFuture(null); + } + return requestTexturesFromUUID(uuid); + }); + } + private static BufferedImage downloadImage(String imageUrl, CapeProvider provider) throws IOException { if (provider == CapeProvider.FIVEZIG) return readFiveZigCape(imageUrl); 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 2759b1408..7f1605561 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/SkullSkinManager.java +++ b/core/src/main/java/org/geysermc/geyser/skin/SkullSkinManager.java @@ -29,11 +29,12 @@ import com.nukkitx.protocol.bedrock.data.skin.ImageData; import com.nukkitx.protocol.bedrock.data.skin.SerializedSkin; import com.nukkitx.protocol.bedrock.packet.PlayerSkinPacket; import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.entity.type.player.PlayerEntity; +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 { @@ -48,28 +49,37 @@ public class SkullSkinManager extends SkinManager { ); } - public static void requestAndHandleSkin(PlayerEntity entity, GeyserSession session, + public static void requestAndHandleSkin(SkullPlayerEntity entity, GeyserSession session, Consumer skinConsumer) { + BiConsumer applySkin = (skin, throwable) -> { + try { + 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) { + GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e); + } + + if (skinConsumer != null) { + skinConsumer.accept(skin); + } + }; + GameProfileData data = GameProfileData.from(entity); - - SkinProvider.requestSkin(entity.getUuid(), data.skinUrl(), true) - .whenCompleteAsync((skin, throwable) -> { - try { - 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) { - GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e); - } - - if (skinConsumer != null) { - skinConsumer.accept(skin); - } - }); + if (data == null) { + GeyserImpl.getInstance().getLogger().debug("Using fallback skin for skull at " + entity.getSkullPosition() + + " with texture value: " + entity.getTexturesProperty() + " and UUID: " + entity.getSkullUUID()); + // No texture available, fallback using the UUID + SkinProvider.SkinData fallback = SkinProvider.determineFallbackSkinData(entity.getSkullUUID()); + applySkin.accept(fallback.skin(), null); + } else { + SkinProvider.requestSkin(entity.getUuid(), data.skinUrl(), true) + .whenCompleteAsync(applySkin); + } } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SkullBlockEntityTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SkullBlockEntityTranslator.java index 94e2d4767..2130206e1 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SkullBlockEntityTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SkullBlockEntityTranslator.java @@ -27,6 +27,7 @@ package org.geysermc.geyser.translator.level.block.entity; import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityType; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.github.steveice10.opennbt.tag.builtin.IntArrayTag; import com.github.steveice10.opennbt.tag.builtin.ListTag; import com.github.steveice10.opennbt.tag.builtin.StringTag; import com.nukkitx.math.vector.Vector3i; @@ -35,7 +36,10 @@ import org.geysermc.geyser.level.block.BlockStateValues; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.skin.SkinProvider; +import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.UUID; import java.util.concurrent.CompletableFuture; @BlockEntity(type = BlockEntityType.SKULL) @@ -53,33 +57,54 @@ public class SkullBlockEntityTranslator extends BlockEntityTranslator implements builder.put("SkullType", skullVariant); } - private static CompletableFuture getTextures(CompoundTag tag) { - CompoundTag owner = tag.get("SkullOwner"); - if (owner != null) { - 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"); - return CompletableFuture.completedFuture(texture.getValue()); + private static UUID getUUID(CompoundTag owner) { + if (owner.get("Id") instanceof IntArrayTag uuidTag && uuidTag.length() == 4) { + int[] uuidAsArray = uuidTag.getValue(); + // thank u viaversion + return new UUID((long) uuidAsArray[0] << 32 | ((long) uuidAsArray[1] & 0xFFFFFFFFL), + (long) uuidAsArray[2] << 32 | ((long) uuidAsArray[3] & 0xFFFFFFFFL)); } - return CompletableFuture.completedFuture(null); + // Convert username to an offline UUID + String username = null; + if (owner.get("Name") instanceof StringTag nameTag) { + username = nameTag.getValue().toLowerCase(Locale.ROOT); + } + return UUID.nameUUIDFromBytes(("OfflinePlayer:" + username).getBytes(StandardCharsets.UTF_8)); + } + + private static CompletableFuture getTextures(CompoundTag owner, UUID uuid) { + CompoundTag properties = owner.get("Properties"); + if (properties == null) { + if (uuid != null && uuid.version() == 4) { + String uuidString = uuid.toString().replace("-", ""); + return SkinProvider.requestTexturesFromUUID(uuidString); + } else if (owner.get("Name") instanceof StringTag nameTag) { + // Fall back to username if UUID was missing or was an offline mode UUID + return SkinProvider.requestTexturesFromUsername(nameTag.getValue()); + } + return CompletableFuture.completedFuture(null); + } + + ListTag textures = properties.get("textures"); + LinkedHashMap tag1 = (LinkedHashMap) textures.get(0).getValue(); + StringTag texture = (StringTag) tag1.get("Value"); + return CompletableFuture.completedFuture(texture.getValue()); } public static void translateSkull(GeyserSession session, CompoundTag tag, int posX, int posY, int posZ, int blockState) { Vector3i blockPosition = Vector3i.from(posX, posY, posZ); - getTextures(tag).whenComplete((texturesProperty, throwable) -> { - if (texturesProperty == null) { - session.getGeyser().getLogger().debug("Custom skull with invalid SkullOwner tag: " + blockPosition + " " + tag); - return; - } + CompoundTag owner = tag.get("SkullOwner"); + if (owner == null) { + session.getSkullCache().removeSkull(blockPosition); + return; + } + + UUID uuid = getUUID(owner); + getTextures(owner, uuid).whenComplete((texturesProperty, throwable) -> { if (session.getEventLoop().inEventLoop()) { - session.getSkullCache().putSkull(blockPosition, texturesProperty, blockState); + session.getSkullCache().putSkull(blockPosition, uuid, texturesProperty, blockState); } else { - session.executeInEventLoop(() -> session.getSkullCache().putSkull(blockPosition, texturesProperty, blockState)); + session.executeInEventLoop(() -> session.getSkullCache().putSkull(blockPosition, uuid, texturesProperty, blockState)); } }); } From af5d03f5dd69f06255627f57127caecb54b48d35 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Wed, 25 Jan 2023 11:05:04 -0500 Subject: [PATCH 5/9] Show teams in command suggestions --- .../geysermc/geyser/scoreboard/Scoreboard.java | 12 ++++++++++++ .../geysermc/geyser/session/GeyserSession.java | 17 +++++++++++++++++ .../protocol/java/JavaCommandsTranslator.java | 15 ++++++++++++++- gradle/libs.versions.toml | 2 +- 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/scoreboard/Scoreboard.java b/core/src/main/java/org/geysermc/geyser/scoreboard/Scoreboard.java index a6e80a375..f26d5846d 100644 --- a/core/src/main/java/org/geysermc/geyser/scoreboard/Scoreboard.java +++ b/core/src/main/java/org/geysermc/geyser/scoreboard/Scoreboard.java @@ -37,6 +37,7 @@ import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.player.PlayerEntity; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.GeyserLocale; +import org.jetbrains.annotations.Contract; import javax.annotation.Nullable; import java.util.*; @@ -132,6 +133,10 @@ public final class Scoreboard { team = new Team(this, teamName); team.addEntities(players); teams.put(teamName, team); + + // Update command parameters - is safe to send even if the command enum doesn't exist on the client (as of 1.19.51) + session.addCommandEnum("Geyser_Teams", team.getId()); + return team; } @@ -343,9 +348,16 @@ public final class Scoreboard { // We need to use the direct entities list here, so #refreshSessionPlayerDisplays also updates accordingly // With the player's lack of a team in visibility checks updateEntityNames(remove, remove.getEntities(), true); + + session.removeCommandEnum("Geyser_Teams", remove.getId()); } } + @Contract("-> new") + public String[] getTeamNames() { + return teams.keySet().toArray(new String[0]); + } + /** * Updates the display names of all entities in a given team. * @param teamChange the players have either joined or left the team. Used for optimizations when just the display name updated. 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 b629fa4d4..33655beda 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -67,7 +67,9 @@ import com.nukkitx.nbt.NbtMap; import com.nukkitx.protocol.bedrock.BedrockPacket; import com.nukkitx.protocol.bedrock.BedrockServerSession; import com.nukkitx.protocol.bedrock.data.*; +import com.nukkitx.protocol.bedrock.data.command.CommandEnumData; import com.nukkitx.protocol.bedrock.data.command.CommandPermission; +import com.nukkitx.protocol.bedrock.data.command.SoftEnumUpdateType; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import com.nukkitx.protocol.bedrock.packet.*; import io.netty.channel.Channel; @@ -1895,4 +1897,19 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { sendUpstreamPacket(transferPacket); return true; } + + public void addCommandEnum(String name, String... enums) { + softEnumPacket(name, SoftEnumUpdateType.ADD, enums); + } + + public void removeCommandEnum(String name, String... enums) { + softEnumPacket(name, SoftEnumUpdateType.REMOVE, enums); + } + + private void softEnumPacket(String name, SoftEnumUpdateType type, String... enums) { + UpdateSoftEnumPacket packet = new UpdateSoftEnumPacket(); + packet.setType(type); + packet.setSoftEnum(new CommandEnumData(name, enums, true)); + sendUpstreamPacket(packet); + } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCommandsTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCommandsTranslator.java index 11311b63c..bd0fab85f 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCommandsTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCommandsTranslator.java @@ -240,6 +240,7 @@ public class JavaCommandsTranslator extends PacketTranslator handleResource(context, ((ResourceProperties) node.getProperties()).getRegistryKey(), false); case RESOURCE_OR_TAG -> handleResource(context, ((ResourceProperties) node.getProperties()).getRegistryKey(), true); case DIMENSION -> context.session.getLevels(); + case TEAM -> context.getTeams(); // Note: as of Java 1.19.3, objectives are currently parsed from the server default -> CommandParam.STRING; }; } @@ -271,6 +272,7 @@ public class JavaCommandsTranslator extends PacketTranslator Date: Sun, 29 Jan 2023 22:15:26 -0500 Subject: [PATCH 6/9] Fix /geyser reload on Spigot Fixes #3478 --- .../geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java index 5f0061382..1be2eb32a 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java @@ -195,6 +195,9 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { geyserConfig.loadFloodgate(this); + this.geyserCommandManager = new GeyserSpigotCommandManager(geyser); + this.geyserCommandManager.init(); + if (!INITIALIZED) { // Needs to be an anonymous inner class otherwise Bukkit complains about missing classes Bukkit.getPluginManager().registerEvents(new Listener() { @@ -206,9 +209,6 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { } }, this); - this.geyserCommandManager = new GeyserSpigotCommandManager(geyser); - this.geyserCommandManager.init(); - // Because Bukkit locks its command map upon startup, we need to // add our plugin commands in onEnable, but populating the executor // can happen at any time From c909b2b1a52ee617961b56cd8677f2f5fa1a1969 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Tue, 31 Jan 2023 13:01:20 -0500 Subject: [PATCH 7/9] Fix #3521 --- .../player/BedrockMovePlayerTranslator.java | 55 +++++++++++-------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java index 6078b7ebd..c6f42c48c 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java @@ -92,11 +92,29 @@ public class BedrockMovePlayerTranslator extends PacketTranslator= packet.getPosition().getY()) { + int floorY = position.getFloorY(); + // The void floor is offset about 40 blocks below the bottom of the world + BedrockDimension bedrockDimension = session.getChunkCache().getBedrockDimension(); + int voidFloorLocation = bedrockDimension.minY() - 40; + teleportThroughVoidFloor = floorY <= (voidFloorLocation + 2) && floorY >= voidFloorLocation; + if (teleportThroughVoidFloor) { + // https://github.com/GeyserMC/Geyser/issues/3521 - no void floor in Java so we cannot be on the ground. + onGround = false; + } + } else { + teleportThroughVoidFloor = false; + } + Packet movePacket; if (rotationChanged) { // Send rotation updates as well movePacket = new ServerboundMovePlayerPosRotPacket( - packet.isOnGround(), + onGround, position.getX(), position.getY(), position.getZ(), yaw, pitch ); @@ -105,35 +123,26 @@ public class BedrockMovePlayerTranslator extends PacketTranslator= packet.getPosition().getY(); - entity.setPositionManual(packet.getPosition()); - entity.setOnGround(packet.isOnGround()); + entity.setOnGround(onGround); // Send final movement changes session.sendDownstreamPacket(movePacket); - if (notMovingUp) { - int floorY = position.getFloorY(); - // The void floor is offset about 40 blocks below the bottom of the world - BedrockDimension bedrockDimension = session.getChunkCache().getBedrockDimension(); - int voidFloorLocation = bedrockDimension.minY() - 40; - if (floorY <= (voidFloorLocation + 2) && floorY >= voidFloorLocation) { - // Work around there being a floor at the bottom of the world and teleport the player below it - // Moving from below to above the void floor works fine - entity.setPosition(entity.getPosition().sub(0, 4f, 0)); - MovePlayerPacket movePlayerPacket = new MovePlayerPacket(); - movePlayerPacket.setRuntimeEntityId(entity.getGeyserId()); - movePlayerPacket.setPosition(entity.getPosition()); - movePlayerPacket.setRotation(entity.getBedrockRotation()); - movePlayerPacket.setMode(MovePlayerPacket.Mode.TELEPORT); - movePlayerPacket.setTeleportationCause(MovePlayerPacket.TeleportationCause.BEHAVIOR); - session.sendUpstreamPacket(movePlayerPacket); - } + if (teleportThroughVoidFloor) { + // Work around there being a floor at the bottom of the world and teleport the player below it + // Moving from below to above the void floor works fine + entity.setPosition(entity.getPosition().sub(0, 4f, 0)); + MovePlayerPacket movePlayerPacket = new MovePlayerPacket(); + movePlayerPacket.setRuntimeEntityId(entity.getGeyserId()); + movePlayerPacket.setPosition(entity.getPosition()); + movePlayerPacket.setRotation(entity.getBedrockRotation()); + movePlayerPacket.setMode(MovePlayerPacket.Mode.TELEPORT); + movePlayerPacket.setTeleportationCause(MovePlayerPacket.TeleportationCause.BEHAVIOR); + session.sendUpstreamPacket(movePlayerPacket); } session.getSkullCache().updateVisibleSkulls(); From 0388785ea7d37cbdd5f4af7e02363c040d2026e9 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Thu, 2 Feb 2023 15:25:05 -0500 Subject: [PATCH 8/9] Fix some instances of team-applied nametags not working Fixes #3531 --- .../entity/type/player/PlayerEntity.java | 35 ++++++------------- .../geyser/scoreboard/Scoreboard.java | 21 ++++++----- .../org/geysermc/geyser/scoreboard/Team.java | 2 ++ 3 files changed, 25 insertions(+), 33 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java index 3e3a298bd..3501eb296 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java @@ -100,6 +100,7 @@ public class PlayerEntity extends LivingEntity { super(session, entityId, geyserId, uuid, EntityDefinitions.PLAYER, position, motion, yaw, pitch, headYaw); this.username = username; + this.nametag = username; this.texturesProperty = texturesProperty; } @@ -119,7 +120,7 @@ public class PlayerEntity extends LivingEntity { } // The name can't be updated later (the entity metadata for it is ignored), so we need to check for this now - updateDisplayName(null, false); + updateDisplayName(session.getWorldCache().getScoreboard().getTeamFor(username)); AddPlayerPacket addPlayerPacket = new AddPlayerPacket(); addPlayerPacket.setUuid(uuid); @@ -315,19 +316,10 @@ public class PlayerEntity extends LivingEntity { } //todo this will become common entity logic once UUID support is implemented for them - /** - * @param useGivenTeam even if there is no team, update the username in the entity metadata anyway, and don't look for a team - */ - public void updateDisplayName(@Nullable Team team, boolean useGivenTeam) { - if (team == null && !useGivenTeam) { - // Only search for the team if we are not supposed to use the given team - // If the given team is null, this is intentional that we are being removed from the team - team = session.getWorldCache().getScoreboard().getTeamFor(username); - } - + public void updateDisplayName(@Nullable Team team) { boolean needsUpdate; - String newDisplayName = this.username; if (team != null) { + String newDisplayName; if (team.isVisibleFor(session.getPlayerEntity().getUsername())) { TeamColor color = team.getColor(); String chatColor = MessageTranslator.toChatColor(color); @@ -339,23 +331,16 @@ public class PlayerEntity extends LivingEntity { // The name is not visible to the session player; clear name newDisplayName = ""; } - needsUpdate = useGivenTeam && !newDisplayName.equals(nametag); - nametag = newDisplayName; - dirtyMetadata.put(EntityData.NAMETAG, newDisplayName); - } else if (useGivenTeam) { - // The name has reset, if it was previously something else - needsUpdate = !newDisplayName.equals(nametag); - dirtyMetadata.put(EntityData.NAMETAG, this.username); + needsUpdate = !newDisplayName.equals(this.nametag); + this.nametag = newDisplayName; } else { - needsUpdate = false; + // The name has reset, if it was previously something else + needsUpdate = !this.nametag.equals(this.username); + this.nametag = this.username; } if (needsUpdate) { - // Update the metadata as it won't be updated later - SetEntityDataPacket packet = new SetEntityDataPacket(); - packet.getMetadata().put(EntityData.NAMETAG, newDisplayName); - packet.setRuntimeEntityId(geyserId); - session.sendUpstreamPacket(packet); + dirtyMetadata.put(EntityData.NAMETAG, this.nametag); } } diff --git a/core/src/main/java/org/geysermc/geyser/scoreboard/Scoreboard.java b/core/src/main/java/org/geysermc/geyser/scoreboard/Scoreboard.java index f26d5846d..56e1e67f8 100644 --- a/core/src/main/java/org/geysermc/geyser/scoreboard/Scoreboard.java +++ b/core/src/main/java/org/geysermc/geyser/scoreboard/Scoreboard.java @@ -30,6 +30,7 @@ import com.nukkitx.protocol.bedrock.data.ScoreInfo; import com.nukkitx.protocol.bedrock.packet.RemoveObjectivePacket; import com.nukkitx.protocol.bedrock.packet.SetDisplayObjectivePacket; import com.nukkitx.protocol.bedrock.packet.SetScorePacket; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import lombok.Getter; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserLogger; @@ -56,6 +57,13 @@ public final class Scoreboard { @Getter private final Map objectiveSlots = new EnumMap<>(ScoreboardPosition.class); private final Map teams = new ConcurrentHashMap<>(); // updated on multiple threads + /** + * Required to preserve vanilla behavior, which also uses a map. + * Otherwise, for example, if TAB has a team for a player and vanilla has a team, "race conditions" that do not + * match vanilla could occur. + */ + @Getter + private final Map playerToTeam = new Object2ObjectOpenHashMap<>(); private int lastAddScoreCount = 0; private int lastRemoveScoreCount = 0; @@ -333,12 +341,7 @@ public final class Scoreboard { } public Team getTeamFor(String entity) { - for (Team team : teams.values()) { - if (team.hasEntity(entity)) { - return team; - } - } - return null; + return playerToTeam.get(entity); } public void removeTeam(String teamName) { @@ -380,7 +383,8 @@ public final class Scoreboard { for (Entity entity : session.getEntityCache().getEntities().values()) { // This more complex logic is for the future to iterate over all entities, not just players if (entity instanceof PlayerEntity player && names.remove(player.getUsername())) { - player.updateDisplayName(team, true); + player.updateDisplayName(team); + player.updateBedrockMetadata(); if (names.isEmpty()) { break; } @@ -396,7 +400,8 @@ public final class Scoreboard { for (Entity entity : session.getEntityCache().getEntities().values()) { if (entity instanceof PlayerEntity player) { Team playerTeam = session.getWorldCache().getScoreboard().getTeamFor(player.getUsername()); - player.updateDisplayName(playerTeam, true); + player.updateDisplayName(playerTeam); + player.updateBedrockMetadata(); } } } diff --git a/core/src/main/java/org/geysermc/geyser/scoreboard/Team.java b/core/src/main/java/org/geysermc/geyser/scoreboard/Team.java index d7840627f..34db4a048 100644 --- a/core/src/main/java/org/geysermc/geyser/scoreboard/Team.java +++ b/core/src/main/java/org/geysermc/geyser/scoreboard/Team.java @@ -65,6 +65,7 @@ public final class Team { if (entities.add(name)) { added.add(name); } + scoreboard.getPlayerToTeam().put(name, this); } if (added.isEmpty()) { @@ -93,6 +94,7 @@ public final class Team { if (entities.remove(name)) { removed.add(name); } + scoreboard.getPlayerToTeam().remove(name, this); } return removed; } From 25c2d30881ec2a0c7abf39be950db503303c2b03 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Thu, 2 Feb 2023 16:31:26 -0500 Subject: [PATCH 9/9] Remove players from player team map on team remove --- .../main/java/org/geysermc/geyser/scoreboard/Scoreboard.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/src/main/java/org/geysermc/geyser/scoreboard/Scoreboard.java b/core/src/main/java/org/geysermc/geyser/scoreboard/Scoreboard.java index 56e1e67f8..f97693a62 100644 --- a/core/src/main/java/org/geysermc/geyser/scoreboard/Scoreboard.java +++ b/core/src/main/java/org/geysermc/geyser/scoreboard/Scoreboard.java @@ -351,6 +351,9 @@ public final class Scoreboard { // We need to use the direct entities list here, so #refreshSessionPlayerDisplays also updates accordingly // With the player's lack of a team in visibility checks updateEntityNames(remove, remove.getEntities(), true); + for (String name : remove.getEntities()) { + playerToTeam.remove(name, remove); + } session.removeCommandEnum("Geyser_Teams", remove.getId()); }