diff --git a/.gitignore b/.gitignore index 88b8bc730..c4c878af4 100644 --- a/.gitignore +++ b/.gitignore @@ -241,3 +241,4 @@ config.yml logs/ public-key.pem locales/ +cache/ \ No newline at end of file diff --git a/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongeConfiguration.java b/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongeConfiguration.java index 279aca544..734fcca67 100644 --- a/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongeConfiguration.java +++ b/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongeConfiguration.java @@ -149,6 +149,11 @@ public class GeyserSpongeConfiguration implements GeyserConfiguration { return node.getNode("cache-chunks").getBoolean(false); } + @Override + public int getCacheImages() { + return node.getNode("cache-skins").getInt(0); + } + @Override public boolean isAboveBedrockNetherBuilding() { return node.getNode("above-bedrock-nether-building").getBoolean(false); 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 94322db9b..4d9933ff5 100644 --- a/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java +++ b/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java @@ -77,6 +77,8 @@ public interface GeyserConfiguration { boolean isCacheChunks(); + int getCacheImages(); + IMetricsInfo getMetrics(); interface IBedrockConfiguration { 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 d6eaf9343..3873db3cc 100644 --- a/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java +++ b/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java @@ -87,6 +87,9 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration @JsonProperty("cache-chunks") private boolean cacheChunks; + @JsonProperty("cache-images") + private int cacheImages = 0; + @JsonProperty("above-bedrock-nether-building") private boolean aboveBedrockNetherBuilding; diff --git a/connector/src/main/java/org/geysermc/connector/entity/PlayerEntity.java b/connector/src/main/java/org/geysermc/connector/entity/PlayerEntity.java index fe4dc905c..424f51870 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/PlayerEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/PlayerEntity.java @@ -38,7 +38,6 @@ import com.nukkitx.protocol.bedrock.data.entity.EntityLinkData; import com.nukkitx.protocol.bedrock.packet.*; import lombok.Getter; import lombok.Setter; -import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.entity.attribute.Attribute; import org.geysermc.connector.entity.attribute.AttributeType; import org.geysermc.connector.entity.type.EntityType; @@ -47,7 +46,6 @@ import org.geysermc.connector.network.session.cache.EntityEffectCache; import org.geysermc.connector.scoreboard.Team; import org.geysermc.connector.utils.AttributeUtils; import org.geysermc.connector.utils.MessageUtils; -import org.geysermc.connector.utils.SkinUtils; import java.util.ArrayList; import java.util.List; @@ -61,7 +59,7 @@ public class PlayerEntity extends LivingEntity { private UUID uuid; private String username; private long lastSkinUpdate = -1; - private boolean playerList = true; + private boolean playerList = true; // Player is in the player list private final EntityEffectCache effectCache; private Entity leftParrot; @@ -117,30 +115,12 @@ public class PlayerEntity extends LivingEntity { public void sendPlayer(GeyserSession session) { if(session.getEntityCache().getPlayerEntity(uuid) == null) return; - if (getLastSkinUpdate() == -1) { - if (playerList) { - PlayerListPacket playerList = new PlayerListPacket(); - playerList.setAction(PlayerListPacket.Action.ADD); - playerList.getEntries().add(SkinUtils.buildDefaultEntry(profile, geyserId)); - session.sendUpstreamPacket(playerList); - } - } if (session.getUpstream().isInitialized() && session.getEntityCache().getEntityByGeyserId(geyserId) == null) { session.getEntityCache().spawnEntity(this); } else { spawnEntity(session); } - - if (!playerList) { - // remove from playerlist if player isn't on playerlist - GeyserConnector.getInstance().getGeneralThreadPool().execute(() -> { - PlayerListPacket playerList = new PlayerListPacket(); - playerList.setAction(PlayerListPacket.Action.REMOVE); - playerList.getEntries().add(new PlayerListPacket.Entry(uuid)); - session.sendUpstreamPacket(playerList); - }); - } } @Override 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 b861f64c4..5378900d0 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 @@ -251,17 +251,6 @@ public class GeyserSession implements CommandSender { upstream.sendPacket(attributesPacket); } - public void fetchOurSkin(PlayerListPacket.Entry entry) { - PlayerSkinPacket playerSkinPacket = new PlayerSkinPacket(); - playerSkinPacket.setUuid(authData.getUUID()); - playerSkinPacket.setSkin(entry.getSkin()); - playerSkinPacket.setOldSkinName("OldName"); - playerSkinPacket.setNewSkinName("NewName"); - playerSkinPacket.setTrustedSkin(true); - upstream.sendPacket(playerSkinPacket); - getConnector().getLogger().debug("Sending skin for " + playerEntity.getUsername() + " " + authData.getUUID()); - } - public void login() { if (connector.getAuthType() != AuthType.ONLINE) { if (connector.getAuthType() == AuthType.OFFLINE) { 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 87da2d00c..c1ad7409a 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 @@ -44,8 +44,8 @@ public class BedrockSetLocalPlayerAsInitializedTranslator extends PacketTranslat for (PlayerEntity entity : session.getEntityCache().getEntitiesByType(PlayerEntity.class)) { if (!entity.isValid()) { - // async skin loading - SkinUtils.requestAndHandleSkinAndCape(entity, session, skinAndCape -> entity.sendPlayer(session)); + SkinUtils.requestAndHandleSkinAndCape(entity, session, null); + entity.sendPlayer(session); } } } 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 f387daecf..10b2ba9ad 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 @@ -82,18 +82,7 @@ public class JavaPlayerListEntryTranslator extends PacketTranslator entity.sendPlayer(session)); + entity.sendPlayer(session); + SkinUtils.requestAndHandleSkinAndCape(entity, session, null); } } } diff --git a/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java b/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java index 36507db08..352a0b0f9 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java +++ b/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java @@ -27,6 +27,8 @@ package org.geysermc.connector.utils; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -40,9 +42,11 @@ import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; import java.util.Arrays; import java.util.Base64; import java.util.Map; +import java.util.Objects; import java.util.UUID; import java.util.concurrent.*; @@ -54,22 +58,26 @@ public class SkinProvider { public static final Skin EMPTY_SKIN = new Skin(-1, "steve", STEVE_SKIN); public static final byte[] ALEX_SKIN = new ProvidedSkin("bedrock/skin/skin_alex.png").getSkin(); public static final Skin EMPTY_SKIN_ALEX = new Skin(-1, "alex", ALEX_SKIN); - private static Map cachedSkins = new ConcurrentHashMap<>(); - private static Map> requestedSkins = new ConcurrentHashMap<>(); + private static final Cache cachedSkins = CacheBuilder.newBuilder() + .expireAfterAccess(1, TimeUnit.HOURS) + .build(); + + private static final Map> requestedSkins = new ConcurrentHashMap<>(); public static final Cape EMPTY_CAPE = new Cape("", "no-cape", new byte[0], -1, true); - private static Map cachedCapes = new ConcurrentHashMap<>(); - private static Map> requestedCapes = new ConcurrentHashMap<>(); + private static final Cache cachedCapes = CacheBuilder.newBuilder() + .expireAfterAccess(1, TimeUnit.HOURS) + .build(); + private static final Map> requestedCapes = new ConcurrentHashMap<>(); public static final SkinGeometry EMPTY_GEOMETRY = SkinProvider.SkinGeometry.getLegacy(false); - private static Map cachedGeometry = new ConcurrentHashMap<>(); + private static final Map cachedGeometry = new ConcurrentHashMap<>(); public static final boolean ALLOW_THIRD_PARTY_EARS = GeyserConnector.getInstance().getConfig().isAllowThirdPartyEars(); public static String EARS_GEOMETRY; public static String EARS_GEOMETRY_SLIM; private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - private static final int CACHE_INTERVAL = 8 * 60 * 1000; // 8 minutes static { /* Load in the normal ears geometry */ @@ -102,22 +110,44 @@ public class SkinProvider { } EARS_GEOMETRY_SLIM = earsDataBuilder.toString(); - } - public static boolean hasSkinCached(UUID uuid) { - return cachedSkins.containsKey(uuid); + // Schedule Daily Image Expiry if we are caching them + if (GeyserConnector.getInstance().getConfig().getCacheImages() > 0) { + GeyserConnector.getInstance().getGeneralThreadPool().scheduleAtFixedRate(() -> { + File cacheFolder = Paths.get("cache", "images").toFile(); + if (!cacheFolder.exists()) { + return; + } + + int count = 0; + final long expireTime = ((long)GeyserConnector.getInstance().getConfig().getCacheImages()) * ((long)1000 * 60 * 60 * 24); + for (File imageFile : Objects.requireNonNull(cacheFolder.listFiles())) { + if (imageFile.lastModified() < System.currentTimeMillis() - expireTime) { + //noinspection ResultOfMethodCallIgnored + imageFile.delete(); + count++; + } + } + + if (count > 0) { + GeyserConnector.getInstance().getLogger().debug(String.format("Removed %d cached image files as they have expired", count)); + } + }, 10, 1440, TimeUnit.MINUTES); + } } public static boolean hasCapeCached(String capeUrl) { - return cachedCapes.containsKey(capeUrl); + return cachedCapes.getIfPresent(capeUrl) != null; } - public static Skin getCachedSkin(UUID uuid) { - return cachedSkins.getOrDefault(uuid, EMPTY_SKIN); + public static Skin getCachedSkin(String skinUrl) { + Skin skin = cachedSkins.getIfPresent(skinUrl); + return skin != null ? skin : EMPTY_SKIN; } public static Cape getCachedCape(String capeUrl) { - return capeUrl != null ? cachedCapes.getOrDefault(capeUrl, EMPTY_CAPE) : EMPTY_CAPE; + Cape cape = capeUrl != null ? cachedCapes.getIfPresent(capeUrl) : EMPTY_CAPE; + return cape != null ? cape : EMPTY_CAPE; } public static CompletableFuture requestSkinAndCape(UUID playerId, String skinUrl, String capeUrl) { @@ -137,28 +167,26 @@ public class SkinProvider { public static CompletableFuture requestSkin(UUID playerId, String textureUrl, boolean newThread) { if (textureUrl == null || textureUrl.isEmpty()) return CompletableFuture.completedFuture(EMPTY_SKIN); - if (requestedSkins.containsKey(playerId)) return requestedSkins.get(playerId); // already requested + if (requestedSkins.containsKey(textureUrl)) return requestedSkins.get(textureUrl); // already requested - if ((System.currentTimeMillis() - CACHE_INTERVAL) < cachedSkins.getOrDefault(playerId, EMPTY_SKIN).getRequestedOn()) { - // no need to update, still cached - return CompletableFuture.completedFuture(cachedSkins.get(playerId)); + Skin cachedSkin = cachedSkins.getIfPresent(textureUrl); + if (cachedSkin != null) { + return CompletableFuture.completedFuture(cachedSkin); } CompletableFuture future; if (newThread) { future = CompletableFuture.supplyAsync(() -> supplySkin(playerId, textureUrl), EXECUTOR_SERVICE) .whenCompleteAsync((skin, throwable) -> { - if (!cachedSkins.getOrDefault(playerId, EMPTY_SKIN).getTextureUrl().equals(textureUrl)) { - skin.updated = true; - cachedSkins.put(playerId, skin); - } - requestedSkins.remove(skin.getSkinOwner()); + skin.updated = true; + cachedSkins.put(textureUrl, skin); + requestedSkins.remove(textureUrl); }); - requestedSkins.put(playerId, future); + requestedSkins.put(textureUrl, future); } else { Skin skin = supplySkin(playerId, textureUrl); future = CompletableFuture.completedFuture(skin); - cachedSkins.put(playerId, skin); + cachedSkins.put(textureUrl, skin); } return future; } @@ -168,11 +196,9 @@ public class SkinProvider { if (requestedCapes.containsKey(capeUrl)) return requestedCapes.get(capeUrl); // already requested boolean officialCape = provider == CapeProvider.MINECRAFT; - boolean validCache = (System.currentTimeMillis() - CACHE_INTERVAL) < cachedCapes.getOrDefault(capeUrl, EMPTY_CAPE).getRequestedOn(); - - if ((cachedCapes.containsKey(capeUrl) && officialCape) || validCache) { - // the cape is an official cape (static) or the cape doesn't need a update yet - return CompletableFuture.completedFuture(cachedCapes.get(capeUrl)); + Cape cachedCape = cachedCapes.getIfPresent(capeUrl); + if (cachedCape != null) { + return CompletableFuture.completedFuture(cachedCape); } CompletableFuture future; @@ -245,7 +271,10 @@ public class SkinProvider { } public static CompletableFuture requestBedrockCape(UUID playerID, boolean newThread) { - Cape bedrockCape = cachedCapes.getOrDefault(playerID.toString() + ".Bedrock", EMPTY_CAPE); + Cape bedrockCape = cachedCapes.getIfPresent(playerID.toString() + ".Bedrock"); + if (bedrockCape == null) { + bedrockCape = EMPTY_CAPE; + } return CompletableFuture.completedFuture(bedrockCape); } @@ -256,7 +285,7 @@ public class SkinProvider { public static void storeBedrockSkin(UUID playerID, String skinID, byte[] skinData) { Skin skin = new Skin(playerID, skinID, skinData, System.currentTimeMillis(), true, false); - cachedSkins.put(playerID, skin); + cachedSkins.put(skin.getTextureUrl(), skin); } public static void storeBedrockCape(UUID playerID, byte[] capeData) { @@ -276,7 +305,7 @@ public class SkinProvider { * @param skin The skin to cache */ public static void storeEarSkin(UUID playerID, Skin skin) { - cachedSkins.put(playerID, skin); + cachedSkins.put(skin.getTextureUrl(), skin); } /** @@ -290,11 +319,12 @@ public class SkinProvider { } private static Skin supplySkin(UUID uuid, String textureUrl) { - byte[] skin = EMPTY_SKIN.getSkinData(); try { - skin = requestImage(textureUrl, null); + byte[] skin = requestImage(textureUrl, null); + return new Skin(uuid, textureUrl, skin, System.currentTimeMillis(), false, false); } catch (Exception ignored) {} // just ignore I guess - return new Skin(uuid, textureUrl, skin, System.currentTimeMillis(), false, false); + + return new Skin(uuid, "empty", EMPTY_SKIN.getSkinData(), System.currentTimeMillis(), false, false); } private static Cape supplyCape(String capeUrl, CapeProvider provider) { @@ -356,11 +386,38 @@ public class SkinProvider { return existingSkin; } + @SuppressWarnings("ResultOfMethodCallIgnored") private static byte[] requestImage(String imageUrl, CapeProvider provider) throws Exception { - BufferedImage image = downloadImage(imageUrl, provider); - GeyserConnector.getInstance().getLogger().debug("Downloaded " + imageUrl); + BufferedImage image = null; - // if the requested image is an cape + // First see if we have a cached file. We also update the modification stamp so we know when the file was last used + File imageFile = Paths.get("cache", "images", UUID.nameUUIDFromBytes(imageUrl.getBytes()).toString() + ".png").toFile(); + if (imageFile.exists()) { + try { + GeyserConnector.getInstance().getLogger().debug("Reading cached image from file " + imageFile.getPath() + " for " + imageUrl); + imageFile.setLastModified(System.currentTimeMillis()); + image = ImageIO.read(imageFile); + } catch (IOException ignored) {} + } + + // If no image we download it + if (image == null) { + image = downloadImage(imageUrl, provider); + GeyserConnector.getInstance().getLogger().debug("Downloaded " + imageUrl); + + // Write to cache if we are allowed + if (GeyserConnector.getInstance().getConfig().getCacheImages() > 0) { + imageFile.getParentFile().mkdirs(); + try { + ImageIO.write(image, "png", imageFile); + GeyserConnector.getInstance().getLogger().debug("Writing cached skin to file " + imageFile.getPath() + " for " + imageUrl); + } catch (IOException e) { + GeyserConnector.getInstance().getLogger().error("Failed to write cached skin to file " + imageFile.getPath() + " for " + imageUrl); + } + } + } + + // if the requested image is a cape if (provider != null) { while(image.getWidth() > 64) { image = scale(image); diff --git a/connector/src/main/java/org/geysermc/connector/utils/SkinUtils.java b/connector/src/main/java/org/geysermc/connector/utils/SkinUtils.java index 225885769..77ab7f939 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/SkinUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/SkinUtils.java @@ -47,18 +47,21 @@ import java.util.function.Consumer; public class SkinUtils { - public static PlayerListPacket.Entry buildCachedEntry(GameProfile profile, long geyserId) { + public static PlayerListPacket.Entry buildCachedEntry(GeyserSession session, GameProfile profile, long geyserId) { GameProfileData data = GameProfileData.from(profile); SkinProvider.Cape cape = SkinProvider.getCachedCape(data.getCapeUrl()); SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.getLegacy(data.isAlex()); + SkinProvider.Skin skin = SkinProvider.getCachedSkin(data.getSkinUrl()); + return buildEntryManually( + session, profile.getId(), profile.getName(), geyserId, - profile.getIdAsString(), - SkinProvider.getCachedSkin(profile.getId()).getSkinData(), + skin.getTextureUrl(), + skin.getSkinData(), cape.getCapeId(), cape.getCapeData(), geometry.getGeometryName(), @@ -66,12 +69,13 @@ public class SkinUtils { ); } - public static PlayerListPacket.Entry buildDefaultEntry(GameProfile profile, long geyserId) { + public static PlayerListPacket.Entry buildDefaultEntry(GeyserSession session, GameProfile profile, long geyserId) { return buildEntryManually( + session, profile.getId(), profile.getName(), geyserId, - profile.getIdAsString(), + "default", SkinProvider.STEVE_SKIN, SkinProvider.EMPTY_CAPE.getCapeId(), SkinProvider.EMPTY_CAPE.getCapeData(), @@ -80,16 +84,25 @@ public class SkinUtils { ); } - public static PlayerListPacket.Entry buildEntryManually(UUID uuid, String username, long geyserId, + 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) { SerializedSkin serializedSkin = SerializedSkin.of( skinId, geometryName, ImageData.of(skinData), Collections.emptyList(), - ImageData.of(capeData), geometryData, "", true, false, !capeId.equals(SkinProvider.EMPTY_CAPE.getCapeId()), capeId, uuid.toString() + ImageData.of(capeData), geometryData, "", true, false, !capeId.equals(SkinProvider.EMPTY_CAPE.getCapeId()), capeId, skinId ); - PlayerListPacket.Entry entry = new PlayerListPacket.Entry(uuid); + PlayerListPacket.Entry entry; + + // If we are building a PlayerListEntry for our own session we use our AuthData UUID instead of the Java UUID + // as bedrock expects to get back its own provided uuid + if (session.getPlayerEntity().getUuid().equals(uuid)) { + entry = new PlayerListPacket.Entry(session.getAuthData().getUUID()); + } else { + entry = new PlayerListPacket.Entry(uuid); + } + entry.setName(username); entry.setEntityId(geyserId); entry.setSkin(serializedSkin); @@ -201,48 +214,34 @@ public class SkinUtils { } } - if (entity.getLastSkinUpdate() < skin.getRequestedOn()) { - entity.setLastSkinUpdate(skin.getRequestedOn()); + entity.setLastSkinUpdate(skin.getRequestedOn()); - if (session.getUpstream().isInitialized()) { - PlayerListPacket.Entry updatedEntry = buildEntryManually( - entity.getUuid(), - entity.getUsername(), - entity.getGeyserId(), - entity.getUuid().toString(), - skin.getSkinData(), - cape.getCapeId(), - cape.getCapeData(), - geometry.getGeometryName(), - geometry.getGeometryData() - ); + if (session.getUpstream().isInitialized()) { + PlayerListPacket.Entry updatedEntry = buildEntryManually( + session, + entity.getUuid(), + entity.getUsername(), + entity.getGeyserId(), + skin.getTextureUrl(), + skin.getSkinData(), + cape.getCapeId(), + cape.getCapeData(), + geometry.getGeometryName(), + geometry.getGeometryData() + ); - // If it is our skin we replace the UUID with the authdata UUID - if (session.getPlayerEntity() == entity) { - // Copy the entry with our identity instead. - PlayerListPacket.Entry copy = new PlayerListPacket.Entry(session.getAuthData().getUUID()); - copy.setName(updatedEntry.getName()); - copy.setEntityId(updatedEntry.getEntityId()); - copy.setSkin(updatedEntry.getSkin()); - copy.setXuid(updatedEntry.getXuid()); - copy.setPlatformChatId(updatedEntry.getPlatformChatId()); - copy.setTeacher(updatedEntry.isTeacher()); - updatedEntry = copy; - } + 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); - PlayerListPacket playerAddPacket = new PlayerListPacket(); - playerAddPacket.setAction(PlayerListPacket.Action.ADD); - playerAddPacket.getEntries().add(updatedEntry); - session.sendUpstreamPacket(playerAddPacket); - - if(entity.getUuid().equals(session.getPlayerEntity().getUuid())) { - session.fetchOurSkin(updatedEntry); - } } } } catch (Exception e) { diff --git a/connector/src/main/resources/config.yml b/connector/src/main/resources/config.yml index d8961270e..6ccaa3065 100644 --- a/connector/src/main/resources/config.yml +++ b/connector/src/main/resources/config.yml @@ -94,6 +94,10 @@ show-cooldown: true # Geyser has direct access to the server itself. cache-chunks: false +# Specify how many days images will be cached to disk to save downloading them from the internet. +# A value of 0 is disabled. (Default: 0) +cache-images: 0 + # 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