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 8e600b1a8..3e3a298bd 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 @@ -77,7 +77,6 @@ public class PlayerEntity extends LivingEntity { } private String username; - private boolean playerList = true; // Player is in the player list /** * The textures property from the GameProfile. @@ -417,4 +416,11 @@ public class PlayerEntity extends LivingEntity { session.sendUpstreamPacket(packet); } } + + /** + * @return the UUID that should be used when dealing with Bedrock's tab list. + */ + public UUID getTabListUuid() { + return getUuid(); + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java index 74b95b73c..99517b208 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java @@ -250,4 +250,9 @@ public class SessionPlayerEntity extends PlayerEntity { dirtyMetadata.put(EntityData.PLAYER_HAS_DIED, (byte) 0); } } + + @Override + public UUID getTabListUuid() { + return session.getAuthData().uuid(); + } } 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 176d171de..c2af2e36b 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 @@ -48,7 +48,6 @@ public class SkullPlayerEntity extends PlayerEntity { public SkullPlayerEntity(GeyserSession session, long geyserId) { super(session, 0, geyserId, UUID.randomUUID(), Vector3f.ZERO, Vector3f.ZERO, 0, 0, 0, "", null); - setPlayerList(false); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/EntityCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/EntityCache.java index 012606615..9dc89215a 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/EntityCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/EntityCache.java @@ -123,7 +123,8 @@ public class EntityCache { } public void addPlayerEntity(PlayerEntity entity) { - playerEntities.put(entity.getUuid(), entity); + // putIfAbsent matches the behavior of playerInfoMap in Java as of 1.19.3 + playerEntities.putIfAbsent(entity.getUuid(), entity); } public PlayerEntity getPlayerEntity(UUID uuid) { 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 6794af498..464f53d48 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/FakeHeadProvider.java +++ b/core/src/main/java/org/geysermc/geyser/skin/FakeHeadProvider.java @@ -29,10 +29,6 @@ import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; -import com.nukkitx.protocol.bedrock.data.skin.ImageData; -import com.nukkitx.protocol.bedrock.data.skin.SerializedSkin; -import com.nukkitx.protocol.bedrock.packet.PlayerListPacket; -import com.nukkitx.protocol.bedrock.packet.PlayerSkinPacket; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; @@ -45,7 +41,6 @@ import org.geysermc.geyser.text.GeyserLocale; import javax.annotation.Nonnull; import java.awt.*; import java.awt.image.BufferedImage; -import java.util.Collections; import java.util.Objects; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -111,7 +106,7 @@ public class FakeHeadProvider { try { SkinProvider.SkinData mergedSkinData = MERGED_SKINS_LOADING_CACHE.get(new FakeHeadEntry(texturesProperty, fakeHeadSkinUrl, entity)); - sendSkinPacket(session, entity, mergedSkinData); + 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); } @@ -133,50 +128,10 @@ public class FakeHeadProvider { return; } - sendSkinPacket(session, entity, skinData); + SkinManager.sendSkinPacket(session, entity, skinData); }); } - private static void sendSkinPacket(GeyserSession session, PlayerEntity entity, SkinProvider.SkinData skinData) { - SkinProvider.Skin skin = skinData.skin(); - SkinProvider.Cape cape = skinData.cape(); - SkinProvider.SkinGeometry geometry = skinData.geometry(); - - if (entity.getUuid().equals(session.getPlayerEntity().getUuid())) { - PlayerListPacket.Entry updatedEntry = SkinManager.buildEntryManually( - session, - entity.getUuid(), - entity.getUsername(), - entity.getGeyserId(), - skin.getTextureUrl(), - skin.getSkinData(), - cape.getCapeId(), - cape.getCapeData(), - geometry - ); - - PlayerListPacket playerAddPacket = new PlayerListPacket(); - playerAddPacket.setAction(PlayerListPacket.Action.ADD); - playerAddPacket.getEntries().add(updatedEntry); - session.sendUpstreamPacket(playerAddPacket); - } else { - PlayerSkinPacket packet = new PlayerSkinPacket(); - packet.setUuid(entity.getUuid()); - packet.setOldSkinName(""); - packet.setNewSkinName(skin.getTextureUrl()); - packet.setSkin(getSkin(skin.getTextureUrl(), skin, cape, geometry)); - packet.setTrustedSkin(true); - session.sendUpstreamPacket(packet); - } - } - - private static SerializedSkin getSkin(String skinId, SkinProvider.Skin skin, SkinProvider.Cape cape, SkinProvider.SkinGeometry geometry) { - return SerializedSkin.of(skinId, "", geometry.getGeometryName(), - ImageData.of(skin.getSkinData()), Collections.emptyList(), - ImageData.of(cape.getCapeData()), geometry.getGeometryData(), - "", true, false, false, cape.getCapeId(), skinId); - } - @AllArgsConstructor @Getter @Setter 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 730d46908..85a1539bc 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java +++ b/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java @@ -32,6 +32,7 @@ import com.github.steveice10.opennbt.tag.builtin.StringTag; import com.nukkitx.protocol.bedrock.data.skin.ImageData; import com.nukkitx.protocol.bedrock.data.skin.SerializedSkin; import com.nukkitx.protocol.bedrock.packet.PlayerListPacket; +import com.nukkitx.protocol.bedrock.packet.PlayerSkinPacket; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.network.AuthType; import org.geysermc.geyser.entity.type.player.PlayerEntity; @@ -67,10 +68,8 @@ public class SkinManager { playerEntity.getUuid(), playerEntity.getUsername(), playerEntity.getGeyserId(), - skin.getTextureUrl(), - skin.getSkinData(), - cape.getCapeId(), - cape.getCapeData(), + skin, + cape, geometry ); } @@ -79,14 +78,10 @@ public class SkinManager { * With all the information needed, build a Bedrock player entry with translated skin information. */ public static PlayerListPacket.Entry buildEntryManually(GeyserSession session, UUID uuid, String username, long geyserId, - String skinId, byte[] skinData, - String capeId, byte[] capeData, + SkinProvider.Skin skin, + SkinProvider.Cape cape, SkinProvider.SkinGeometry geometry) { - SerializedSkin serializedSkin = SerializedSkin.of( - skinId, "", geometry.getGeometryName(), ImageData.of(skinData), Collections.emptyList(), - ImageData.of(capeData), geometry.getGeometryData(), "", true, false, - !capeId.equals(SkinProvider.EMPTY_CAPE.getCapeId()), capeId, skinId - ); + SerializedSkin serializedSkin = getSkin(skin.getTextureUrl(), skin, cape, geometry); // This attempts to find the XUID of the player so profile images show up for Xbox accounts String xuid = ""; @@ -116,6 +111,45 @@ public class SkinManager { return entry; } + public static void sendSkinPacket(GeyserSession session, PlayerEntity entity, SkinProvider.SkinData skinData) { + SkinProvider.Skin skin = skinData.skin(); + SkinProvider.Cape cape = skinData.cape(); + SkinProvider.SkinGeometry geometry = skinData.geometry(); + + if (entity.getUuid().equals(session.getPlayerEntity().getUuid())) { + // TODO is this special behavior needed? + PlayerListPacket.Entry updatedEntry = buildEntryManually( + session, + entity.getUuid(), + entity.getUsername(), + entity.getGeyserId(), + skin, + cape, + geometry + ); + + PlayerListPacket playerAddPacket = new PlayerListPacket(); + playerAddPacket.setAction(PlayerListPacket.Action.ADD); + playerAddPacket.getEntries().add(updatedEntry); + session.sendUpstreamPacket(playerAddPacket); + } else { + PlayerSkinPacket packet = new PlayerSkinPacket(); + packet.setUuid(entity.getUuid()); + packet.setOldSkinName(""); + packet.setNewSkinName(skin.getTextureUrl()); + packet.setSkin(getSkin(skin.getTextureUrl(), skin, cape, geometry)); + packet.setTrustedSkin(true); + session.sendUpstreamPacket(packet); + } + } + + private static SerializedSkin getSkin(String skinId, SkinProvider.Skin skin, SkinProvider.Cape cape, SkinProvider.SkinGeometry geometry) { + return SerializedSkin.of(skinId, "", geometry.getGeometryName(), + ImageData.of(skin.getSkinData()), Collections.emptyList(), + ImageData.of(cape.getCapeData()), geometry.getGeometryData(), + "", true, false, false, cape.getCapeId(), skinId); + } + public static void requestAndHandleSkinAndCape(PlayerEntity entity, GeyserSession session, Consumer skinAndCapeConsumer) { SkinProvider.requestSkinData(entity).whenCompleteAsync((skinData, throwable) -> { @@ -128,34 +162,7 @@ public class SkinManager { } if (skinData.geometry() != null) { - SkinProvider.Skin skin = skinData.skin(); - SkinProvider.Cape cape = skinData.cape(); - SkinProvider.SkinGeometry geometry = skinData.geometry(); - - PlayerListPacket.Entry updatedEntry = buildEntryManually( - session, - entity.getUuid(), - entity.getUsername(), - entity.getGeyserId(), - skin.getTextureUrl(), - skin.getSkinData(), - cape.getCapeId(), - cape.getCapeData(), - geometry - ); - - - PlayerListPacket playerAddPacket = new PlayerListPacket(); - playerAddPacket.setAction(PlayerListPacket.Action.ADD); - playerAddPacket.getEntries().add(updatedEntry); - session.sendUpstreamPacket(playerAddPacket); - - if (!entity.isPlayerList()) { - PlayerListPacket playerRemovePacket = new PlayerListPacket(); - playerRemovePacket.setAction(PlayerListPacket.Action.REMOVE); - playerRemovePacket.getEntries().add(updatedEntry); - session.sendUpstreamPacket(playerRemovePacket); - } + sendSkinPacket(session, entity, skinData); } if (skinAndCapeConsumer != null) { diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerInfoRemoveTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerInfoRemoveTranslator.java index 4e9f0ca42..1aa9e33d9 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerInfoRemoveTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerInfoRemoveTranslator.java @@ -45,17 +45,15 @@ public class JavaPlayerInfoRemoveTranslator extends PacketTranslator { @Override public void translate(GeyserSession session, ClientboundPlayerInfoUpdatePacket packet) { - if (!packet.getActions().contains(PlayerListEntryAction.ADD_PLAYER)) { - return; - } + Set actions = packet.getActions(); - PlayerListPacket translate = new PlayerListPacket(); - translate.setAction(PlayerListPacket.Action.ADD); + if (actions.contains(PlayerListEntryAction.ADD_PLAYER)) { + for (PlayerListEntry entry : packet.getEntries()) { + GameProfile profile = entry.getProfile(); + PlayerEntity playerEntity; + boolean self = profile.getId().equals(session.getPlayerEntity().getUuid()); - for (PlayerListEntry entry : packet.getEntries()) { - GameProfile profile = entry.getProfile(); - PlayerEntity playerEntity; - boolean self = profile.getId().equals(session.getPlayerEntity().getUuid()); + GameProfile.Property textures = profile.getProperty("textures"); + String texturesProperty = textures == null ? null : textures.getValue(); - if (self) { - // Entity is ourself - playerEntity = session.getPlayerEntity(); - } else { - playerEntity = session.getEntityCache().getPlayerEntity(profile.getId()); - } + if (self) { + // Entity is ourself + playerEntity = session.getPlayerEntity(); + } else { + // It's a new player + playerEntity = new PlayerEntity( + session, + -1, + session.getEntityCache().getNextEntityId().incrementAndGet(), + profile.getId(), + Vector3f.ZERO, + Vector3f.ZERO, + 0, 0, 0, + profile.getName(), + texturesProperty + ); - GameProfile.Property textures = profile.getProperty("textures"); - String texturesProperty = textures == null ? null : textures.getValue(); - - if (playerEntity == null) { - // It's a new player - playerEntity = new PlayerEntity( - session, - -1, - session.getEntityCache().getNextEntityId().incrementAndGet(), - profile.getId(), - Vector3f.ZERO, - Vector3f.ZERO, - 0, 0, 0, - profile.getName(), - texturesProperty - ); - - session.getEntityCache().addPlayerEntity(playerEntity); - } else { + session.getEntityCache().addPlayerEntity(playerEntity); + } playerEntity.setUsername(profile.getName()); playerEntity.setTexturesProperty(texturesProperty); - } - playerEntity.setPlayerList(true); - // We'll send our own PlayerListEntry in requestAndHandleSkinAndCape - // But we need to send other player's entries so they show up in the player list - // without processing their skin information - that'll be processed when they spawn in - if (self) { - SkinManager.requestAndHandleSkinAndCape(playerEntity, session, skinAndCape -> - GeyserImpl.getInstance().getLogger().debug("Loaded Local Bedrock Java Skin Data for " + session.getClientData().getUsername())); - } else { - playerEntity.setValid(true); - PlayerListPacket.Entry playerListEntry = SkinManager.buildCachedEntry(session, playerEntity); - - translate.getEntries().add(playerListEntry); + // We'll send our own PlayerListEntry in requestAndHandleSkinAndCape + // But we need to send other player's entries so they show up in the player list + // without processing their skin information - that'll be processed when they spawn in + if (self) { + SkinManager.requestAndHandleSkinAndCape(playerEntity, session, skinAndCape -> + GeyserImpl.getInstance().getLogger().debug("Loaded Local Bedrock Java Skin Data for " + session.getClientData().getUsername())); + } else { + playerEntity.setValid(true); + } } } - if (!translate.getEntries().isEmpty()) { - session.sendUpstreamPacket(translate); + if (actions.contains(PlayerListEntryAction.UPDATE_LISTED)) { + List toAdd = new ArrayList<>(); + List toRemove = new ArrayList<>(); + + for (PlayerListEntry entry : packet.getEntries()) { + PlayerEntity entity = session.getEntityCache().getPlayerEntity(entry.getProfileId()); + if (entity == null) { + session.getGeyser().getLogger().debug("Ignoring player info update for " + entry.getProfileId()); + continue; + } + + if (entry.isListed()) { + PlayerListPacket.Entry playerListEntry = SkinManager.buildCachedEntry(session, entity); + toAdd.add(playerListEntry); + } else { + toRemove.add(new PlayerListPacket.Entry(entity.getTabListUuid())); + } + } + + if (!toAdd.isEmpty()) { + PlayerListPacket tabListPacket = new PlayerListPacket(); + tabListPacket.setAction(PlayerListPacket.Action.ADD); + tabListPacket.getEntries().addAll(toAdd); + session.sendUpstreamPacket(tabListPacket); + } + if (!toRemove.isEmpty()) { + PlayerListPacket tabListPacket = new PlayerListPacket(); + tabListPacket.setAction(PlayerListPacket.Action.REMOVE); + tabListPacket.getEntries().addAll(toRemove); + session.sendUpstreamPacket(tabListPacket); + } } } }