diff --git a/connector/pom.xml b/connector/pom.xml index e4b0b358f..891551cec 100644 --- a/connector/pom.xml +++ b/connector/pom.xml @@ -74,7 +74,7 @@ com.nukkitx.protocol bedrock-v361 - 2.1.3 + 2.2.0 compile 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 d90f25768..213ca100c 100644 --- a/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java +++ b/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java @@ -32,7 +32,6 @@ import java.util.Map; @Getter public class GeyserConfiguration { - private BedrockConfiguration bedrock; private RemoteConfiguration remote; @@ -50,5 +49,8 @@ public class GeyserConfiguration { @JsonProperty("general-thread-pool") private int generalThreadPool; + @JsonProperty("allow-third-party-capes") + private boolean allowThirdPartyCapes; + private MetricInfo metrics; } \ No newline at end of file diff --git a/connector/src/main/java/org/geysermc/connector/console/GeyserLogger.java b/connector/src/main/java/org/geysermc/connector/console/GeyserLogger.java index db31bfc18..0ff122ea2 100644 --- a/connector/src/main/java/org/geysermc/connector/console/GeyserLogger.java +++ b/connector/src/main/java/org/geysermc/connector/console/GeyserLogger.java @@ -25,10 +25,11 @@ package org.geysermc.connector.console; -import org.geysermc.api.ChatColor; import io.sentry.Sentry; +import org.geysermc.api.ChatColor; -import java.io.*; +import java.io.File; +import java.io.IOException; import java.util.Date; import java.util.logging.*; @@ -108,7 +109,7 @@ public class GeyserLogger implements org.geysermc.api.logger.Logger { @Override public void error(String message, Throwable error) { waitFor(); - System.out.println(printConsole(ChatColor.RED + message + "\n" + error.getMessage(), colored)); + System.out.println(printConsole(ChatColor.RED + message + "\n" + error, colored)); } @Override diff --git a/connector/src/main/java/org/geysermc/connector/entity/Entity.java b/connector/src/main/java/org/geysermc/connector/entity/Entity.java index 735d5ac65..fb3c3d5c8 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/Entity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/Entity.java @@ -57,7 +57,9 @@ public class Entity { protected Vector3f position; protected Vector3f motion; - // 1 - pitch, 2 - yaw, 3 - roll (head yaw) + /** + * x = Yaw, y = Pitch, z = HeadYaw + */ protected Vector3f rotation; protected int scale = 1; @@ -89,7 +91,7 @@ public class Entity { addEntityPacket.setUniqueEntityId(geyserId); addEntityPacket.setPosition(position); addEntityPacket.setMotion(motion); - addEntityPacket.setRotation(rotation); + addEntityPacket.setRotation(getBedrockRotation()); addEntityPacket.setEntityType(entityType.getType()); addEntityPacket.getMetadata().putAll(getMetadata()); @@ -99,33 +101,37 @@ public class Entity { GeyserLogger.DEFAULT.debug("Spawned entity " + entityType + " at location " + position + " with id " + geyserId + " (java id " + entityId + ")"); } - public void despawnEntity(GeyserSession session) { - if (!valid) return; + /** + * @return can be deleted + */ + public boolean despawnEntity(GeyserSession session) { + if (!valid) return true; RemoveEntityPacket removeEntityPacket = new RemoveEntityPacket(); removeEntityPacket.setUniqueEntityId(geyserId); session.getUpstream().sendPacket(removeEntityPacket); valid = false; + return true; } - public void moveRelative(double relX, double relY, double relZ, float pitch, float yaw) { - moveRelative(relX, relY, relZ, new Vector3f(pitch, yaw, yaw)); + public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch) { + moveRelative(relX, relY, relZ, new Vector3f(yaw, pitch, yaw)); } public void moveRelative(double relX, double relY, double relZ, Vector3f rotation) { - this.rotation = rotation; + setRotation(rotation); this.position = new Vector3f(position.getX() + relX, position.getY() + relY, position.getZ() + relZ); this.movePending = true; } - public void moveAbsolute(Vector3f position, float pitch, float yaw) { - moveAbsolute(position, new Vector3f(pitch, yaw, yaw)); + public void moveAbsolute(Vector3f position, float yaw, float pitch) { + moveAbsolute(position, new Vector3f(yaw, pitch, yaw)); } public void moveAbsolute(Vector3f position, Vector3f rotation) { setPosition(position); - this.rotation = rotation; + setRotation(rotation); this.movePending = true; } @@ -189,6 +195,13 @@ public class Entity { this.position = position; } + /** + * x = Pitch, y = HeadYaw, z = Yaw + */ + public Vector3f getBedrockRotation() { + return new Vector3f(rotation.getY(), rotation.getZ(), rotation.getX()); + } + @SuppressWarnings("unchecked") public I as(Class entityClass) { return entityClass.isInstance(this) ? (I) this : null; 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 4c65fdabe..a108d988f 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/PlayerEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/PlayerEntity.java @@ -43,13 +43,13 @@ public class PlayerEntity extends Entity { private UUID uuid; private String username; private long lastSkinUpdate = -1; - - private ItemData hand = ItemData.of(0, (short) 0, 0); + private boolean playerList = true; private ItemData helmet; private ItemData chestplate; private ItemData leggings; private ItemData boots; + private ItemData hand = ItemData.of(0, (short) 0, 0); public PlayerEntity(GameProfile gameProfile, long entityId, long geyserId, Vector3f position, Vector3f motion, Vector3f rotation) { super(entityId, geyserId, EntityType.PLAYER, position, motion, rotation); @@ -74,6 +74,12 @@ public class PlayerEntity extends Entity { session.getUpstream().sendPacket(armorEquipmentPacket); } + @Override + public boolean despawnEntity(GeyserSession session) { + super.despawnEntity(session); + return !playerList; // don't remove from cache when still on playerlist + } + @Override public void spawnEntity(GeyserSession session) { if (geyserId == 1) return; @@ -84,7 +90,7 @@ public class PlayerEntity extends Entity { addPlayerPacket.setRuntimeEntityId(geyserId); addPlayerPacket.setUniqueEntityId(geyserId); addPlayerPacket.setPosition(position); - addPlayerPacket.setRotation(rotation); + addPlayerPacket.setRotation(getBedrockRotation()); addPlayerPacket.setMotion(motion); addPlayerPacket.setHand(hand); addPlayerPacket.setPlayerFlags(0); @@ -98,6 +104,5 @@ public class PlayerEntity extends Entity { valid = true; session.getUpstream().sendPacket(addPlayerPacket); -// System.out.println("Spawned player "+uuid+" "+username+" "+geyserId); } } 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 d84f61a71..146edefb5 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 @@ -53,7 +53,6 @@ import org.geysermc.api.session.AuthData; import org.geysermc.api.window.FormWindow; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.entity.PlayerEntity; -import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.inventory.PlayerInventory; import org.geysermc.connector.network.session.cache.*; import org.geysermc.connector.network.translators.Registry; @@ -65,7 +64,7 @@ import java.util.UUID; @Getter public class GeyserSession implements Player { private final GeyserConnector connector; - private final BedrockServerSession upstream; + private final UpstreamSession upstream; private RemoteServer remoteServer; private Client downstream; private AuthData authenticationData; @@ -95,7 +94,7 @@ public class GeyserSession implements Player { public GeyserSession(GeyserConnector connector, BedrockServerSession bedrockServerSession) { this.connector = connector; - this.upstream = bedrockServerSession; + this.upstream = new UpstreamSession(bedrockServerSession); this.chunkCache = new ChunkCache(this); this.entityCache = new EntityCache(this); @@ -169,8 +168,9 @@ public class GeyserSession implements Player { @Override public void packetReceived(PacketReceivedEvent event) { - if (!closed) + if (!closed) { Registry.JAVA.translate(event.getPacket().getClass(), event.getPacket(), GeyserSession.this); + } } }); @@ -297,5 +297,7 @@ public class GeyserSession implements Player { PlayStatusPacket playStatusPacket = new PlayStatusPacket(); playStatusPacket.setStatus(PlayStatusPacket.Status.PLAYER_SPAWN); upstream.sendPacket(playStatusPacket); + + upstream.setFrozen(true); // will freeze until the client decides it is ready } } diff --git a/connector/src/main/java/org/geysermc/connector/network/session/UpstreamSession.java b/connector/src/main/java/org/geysermc/connector/network/session/UpstreamSession.java new file mode 100644 index 000000000..ab3a0e0c5 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/session/UpstreamSession.java @@ -0,0 +1,58 @@ +package org.geysermc.connector.network.session; + +import com.nukkitx.protocol.bedrock.BedrockPacket; +import com.nukkitx.protocol.bedrock.BedrockServerSession; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.geysermc.api.Geyser; + +import java.net.InetSocketAddress; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +@RequiredArgsConstructor +public class UpstreamSession { + @Getter private final BedrockServerSession session; + private Queue packets = new ConcurrentLinkedQueue<>(); + @Getter private boolean frozen = false; + + public void sendPacket(@NonNull BedrockPacket packet) { + if (frozen || !packets.isEmpty()) { + packets.add(packet); + } else { + session.sendPacket(packet); + } + } + + public void sendPacketImmediately(@NonNull BedrockPacket packet) { + session.sendPacketImmediately(packet); + } + + public void setFrozen(boolean frozen) { + if (this.frozen != frozen) { + this.frozen = frozen; + + if (!frozen) { + Geyser.getGeneralThreadPool().execute(() -> { + BedrockPacket packet; + while ((packet = packets.poll()) != null) { + session.sendPacket(packet); + } + }); + } + } + } + + public void disconnect(String reason) { + session.disconnect(reason); + } + + public boolean isClosed() { + return session.isClosed(); + } + + public InetSocketAddress getAddress() { + return session.getAddress(); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java index 1d8c15d48..41af7e579 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java @@ -62,17 +62,18 @@ public class EntityCache { entity.spawnEntity(session); } - public void removeEntity(Entity entity) { - if (entity == null || !entity.isValid()) return; - - Long geyserId = entityIdTranslations.remove(entity.getEntityId()); - if (geyserId != null) { - entities.remove(geyserId); - if (entity.is(PlayerEntity.class)) { - playerEntities.remove(entity.as(PlayerEntity.class).getUuid()); + public boolean removeEntity(Entity entity, boolean force) { + if (entity != null && entity.isValid() && (force || entity.despawnEntity(session))) { + Long geyserId = entityIdTranslations.remove(entity.getEntityId()); + if (geyserId != null) { + entities.remove(geyserId); + if (entity.is(PlayerEntity.class)) { + playerEntities.remove(entity.as(PlayerEntity.class).getUuid()); + } } + return true; } - entity.despawnEntity(session); + return false; } public Entity getEntityByGeyserId(long geyserId) { diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/Registry.java b/connector/src/main/java/org/geysermc/connector/network/translators/Registry.java index e13ce9748..3867c8b52 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/Registry.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/Registry.java @@ -48,13 +48,16 @@ public class Registry { } public

boolean translate(Class clazz, P packet, GeyserSession session) { - try { - if (MAP.containsKey(clazz)) { - ((PacketTranslator

) MAP.get(clazz)).translate(packet, session); - return true; + if (!session.getUpstream().isClosed() && !session.isClosed()) { + try { + if (MAP.containsKey(clazz)) { + ((PacketTranslator

) MAP.get(clazz)).translate(packet, session); + return true; + } + } catch (Throwable ex) { + GeyserLogger.DEFAULT.error("Could not translate packet " + packet.getClass().getSimpleName(), ex); + ex.printStackTrace(); } - } catch (NullPointerException ex) { - GeyserLogger.DEFAULT.error("Could not translate packet " + packet.getClass().getSimpleName(), ex); } return false; } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/TranslatorsInit.java b/connector/src/main/java/org/geysermc/connector/network/translators/TranslatorsInit.java index b48483a41..f9bb11cfc 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/TranslatorsInit.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/TranslatorsInit.java @@ -146,10 +146,11 @@ public class TranslatorsInit { Registry.registerBedrock(AnimatePacket.class, new BedrockAnimateTranslator()); Registry.registerBedrock(CommandRequestPacket.class, new BedrockCommandRequestTranslator()); - Registry.registerBedrock(TextPacket.class, new BedrockTextTranslator()); Registry.registerBedrock(MobEquipmentPacket.class, new BedrockMobEquipmentTranslator()); - Registry.registerBedrock(PlayerActionPacket.class, new BedrockActionTranslator()); Registry.registerBedrock(MovePlayerPacket.class, new BedrockMovePlayerTranslator()); + Registry.registerBedrock(PlayerActionPacket.class, new BedrockActionTranslator()); + Registry.registerBedrock(SetLocalPlayerAsInitializedPacket.class, new BedrockPlayerInitialized()); + Registry.registerBedrock(TextPacket.class, new BedrockTextTranslator()); itemTranslator = new ItemTranslator(); blockTranslator = new BlockTranslator(); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockMovePlayerTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockMovePlayerTranslator.java index a09d0daa3..3891af6f8 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockMovePlayerTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockMovePlayerTranslator.java @@ -53,9 +53,10 @@ public class BedrockMovePlayerTranslator extends PacketTranslator 10 || yRange > 10 || zRange > 10) { + if ((xRange + yRange + zRange) > 100) { session.getConnector().getLogger().warning(session.getName() + " moved too quickly." + " current position: " + currentPosition + ", new position: " + newPosition); @@ -104,7 +105,7 @@ public class BedrockMovePlayerTranslator extends PacketTranslator { + @Override + public void translate(SetLocalPlayerAsInitializedPacket packet, GeyserSession session) { + if (session.getPlayerEntity().getGeyserId() == packet.getRuntimeEntityId()) { + session.getUpstream().setFrozen(false); + } + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/block/BlockTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/block/BlockTranslator.java index 596bd3896..ef22b6305 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/block/BlockTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/block/BlockTranslator.java @@ -12,7 +12,7 @@ public class BlockTranslator { public BedrockItem getBedrockBlock(BlockState state) { BedrockItem bedrockItem = Remapper.BLOCK_REMAPPER.convertToBedrockB(new ItemStack(state.getId())); if (bedrockItem == null) { - GeyserLogger.DEFAULT.debug("Missing mapping for java block " + state.getId() + "/nPlease report this to Geyser."); + GeyserLogger.DEFAULT.debug("Missing mapping for java block " + state.getId() + "\nPlease report this to Geyser."); return BedrockItem.DIRT; // so we can walk and not getting stuck x) } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/GenericInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/GenericInventoryTranslator.java index b6f36ac05..e2f4fa582 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/GenericInventoryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/GenericInventoryTranslator.java @@ -26,7 +26,6 @@ package org.geysermc.connector.network.translators.inventory; import com.flowpowered.math.vector.Vector3i; -import com.nukkitx.protocol.bedrock.data.ContainerId; import com.nukkitx.protocol.bedrock.data.ItemData; import com.nukkitx.protocol.bedrock.packet.ContainerOpenPacket; import com.nukkitx.protocol.bedrock.packet.InventoryContentPacket; @@ -34,7 +33,6 @@ import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket; import org.geysermc.connector.inventory.Inventory; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.TranslatorsInit; -import org.geysermc.connector.utils.InventoryUtils; public class GenericInventoryTranslator extends InventoryTranslator { @@ -54,29 +52,21 @@ public class GenericInventoryTranslator extends InventoryTranslator { @Override public void updateInventory(GeyserSession session, Inventory inventory) { - ContainerId containerId = InventoryUtils.getContainerId(inventory.getId()); - if (containerId == null) - return; - ItemData[] bedrockItems = new ItemData[inventory.getItems().length]; for (int i = 0; i < bedrockItems.length; i++) { bedrockItems[i] = TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItems()[i]); } InventoryContentPacket contentPacket = new InventoryContentPacket(); - contentPacket.setContainerId(containerId); + contentPacket.setContainerId(inventory.getId()); contentPacket.setContents(bedrockItems); session.getUpstream().sendPacket(contentPacket); } @Override public void updateSlot(GeyserSession session, Inventory inventory, int slot) { - ContainerId containerId = InventoryUtils.getContainerId(inventory.getId()); - if (containerId == null) - return; - InventorySlotPacket slotPacket = new InventorySlotPacket(); - slotPacket.setContainerId(containerId); + slotPacket.setContainerId(inventory.getId()); slotPacket.setSlot(TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItems()[slot])); slotPacket.setInventorySlot(slot); session.getUpstream().sendPacket(slotPacket); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityDestroyTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityDestroyTranslator.java index 065463235..5bd481a94 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityDestroyTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityDestroyTranslator.java @@ -38,7 +38,7 @@ public class JavaEntityDestroyTranslator extends PacketTranslator { - private static byte[] providedSkin = new ProvidedSkin("bedrock/skin/skin_steve.png").getSkin(); - @Override public void translate(ServerPlayerListEntryPacket packet, GeyserSession session) { if (packet.getAction() != PlayerListEntryAction.ADD_PLAYER && packet.getAction() != PlayerListEntryAction.REMOVE_PLAYER) return; @@ -25,26 +22,27 @@ public class JavaPlayerListEntryTranslator extends PacketTranslator { @Override public void translate(ServerSpawnExpOrbPacket packet, GeyserSession session) { Vector3f position = new Vector3f(packet.getX(), packet.getY(), packet.getZ()); - Entity entity = new ExpOrbEntity(packet.getExp(), packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(), - EntityType.EXPERIENCE_ORB, position, new Vector3f(0, 0, 0), new Vector3f(0, 0, 0)); - if (entity == null) - return; + Entity entity = new ExpOrbEntity( + packet.getExp(), packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(), + EntityType.EXPERIENCE_ORB, position, Vector3f.ZERO, Vector3f.ZERO + ); session.getEntityCache().spawnEntity(entity); } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaSpawnGlobalEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaSpawnGlobalEntityTranslator.java index cb6b9f126..7d8aa6a3a 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaSpawnGlobalEntityTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaSpawnGlobalEntityTranslator.java @@ -39,11 +39,10 @@ public class JavaSpawnGlobalEntityTranslator extends PacketTranslator { GameProfile.Property skinProperty = entity.getProfile().getProperty("textures"); - JsonObject skinObject = SkinProvider.getGson().fromJson(new String(Base64.getDecoder().decode(skinProperty.getValue()), Charsets.UTF_8), JsonObject.class); + JsonObject skinObject = SkinProvider.GSON.fromJson(new String(Base64.getDecoder().decode(skinProperty.getValue()), Charsets.UTF_8), JsonObject.class); JsonObject textures = skinObject.getAsJsonObject("textures"); JsonObject skinTexture = textures.getAsJsonObject("SKIN"); @@ -82,8 +83,14 @@ public class JavaSpawnPlayerTranslator extends PacketTranslator cachedSkins = new ConcurrentHashMap<>(); - private static Map cachedCapes = new ConcurrentHashMap<>(); + public static final Gson GSON = new GsonBuilder().create(); + public static final boolean ALLOW_THIRD_PARTY_CAPES = ((GeyserConnector)Geyser.getConnector()).getConfig().isAllowThirdPartyCapes(); + private static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(ALLOW_THIRD_PARTY_CAPES ? 21 : 14); public static final Skin EMPTY_SKIN = new Skin(-1, ""); - public static final Cape EMPTY_CAPE = new Cape("", new byte[0]); - private static final int CACHE_INTERVAL = 8 * 60 * 1000; // 8 minutes - + public static final byte[] STEVE_SKIN = new ProvidedSkin("bedrock/skin/skin_steve.png").getSkin(); + private static Map cachedSkins = new ConcurrentHashMap<>(); private static Map> requestedSkins = new ConcurrentHashMap<>(); + + public static final Cape EMPTY_CAPE = new Cape("", new byte[0], -1, true); + private static Map cachedCapes = new ConcurrentHashMap<>(); private static Map> requestedCapes = new ConcurrentHashMap<>(); + private static final int CACHE_INTERVAL = 8 * 60 * 1000; // 8 minutes + public static boolean hasSkinCached(UUID uuid) { return cachedSkins.containsKey(uuid); } @@ -54,9 +58,9 @@ public class SkinProvider { getOrDefault(requestAndHandleCape(capeUrl, false), EMPTY_CAPE, 5) ); - Geyser.getLogger().info("Took " + (System.currentTimeMillis() - time) + "ms for " + playerId); + Geyser.getLogger().debug("Took " + (System.currentTimeMillis() - time) + "ms for " + playerId); return skinAndCape; - }, executorService); + }, EXECUTOR_SERVICE); } public static CompletableFuture requestAndHandleSkin(UUID playerId, String textureUrl, boolean newThread) { @@ -70,7 +74,7 @@ public class SkinProvider { CompletableFuture future; if (newThread) { - future = CompletableFuture.supplyAsync(() -> supplySkin(playerId, textureUrl), executorService) + future = CompletableFuture.supplyAsync(() -> supplySkin(playerId, textureUrl), EXECUTOR_SERVICE) .whenCompleteAsync((skin, throwable) -> { if (!cachedSkins.getOrDefault(playerId, EMPTY_SKIN).getTextureUrl().equals(textureUrl)) { skin.updated = true; @@ -91,14 +95,17 @@ public class SkinProvider { if (capeUrl == null || capeUrl.isEmpty()) return CompletableFuture.completedFuture(EMPTY_CAPE); if (requestedCapes.containsKey(capeUrl)) return requestedCapes.get(capeUrl); // already requested - if (cachedCapes.containsKey(capeUrl)) { - // no need to update the cache, capes are static :D + boolean officialCape = capeUrl.startsWith("https://textures.minecraft.net"); + 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)); } CompletableFuture future; if (newThread) { - future = CompletableFuture.supplyAsync(() -> supplyCape(capeUrl), executorService) + future = CompletableFuture.supplyAsync(() -> supplyCape(capeUrl), EXECUTOR_SERVICE) .whenCompleteAsync((cape, throwable) -> { cachedCapes.put(capeUrl, cape); requestedCapes.remove(capeUrl); @@ -112,26 +119,57 @@ public class SkinProvider { return future; } + public static CompletableFuture requestAndHandleUnofficialCape(Cape officialCape, UUID playerId, + String username, boolean newThread) { + if (officialCape.isFailed() && ALLOW_THIRD_PARTY_CAPES) { + for (UnofficalCape cape : UnofficalCape.VALUES) { + Cape cape1 = getOrDefault( + requestAndHandleCape(cape.getUrlFor(playerId, username), newThread), + EMPTY_CAPE, 4 + ); + if (!cape1.isFailed()) { + return CompletableFuture.completedFuture(cape1); + } + } + } + return CompletableFuture.completedFuture(officialCape); + } + private static Skin supplySkin(UUID uuid, String textureUrl) { byte[] skin = EMPTY_SKIN.getSkinData(); try { - skin = requestImage(textureUrl); + skin = requestImage(textureUrl, false); } catch (Exception ignored) {} // just ignore I guess return new Skin(uuid, textureUrl, skin, System.currentTimeMillis(), false); } private static Cape supplyCape(String capeUrl) { - byte[] cape = EMPTY_CAPE.getCapeData(); + byte[] cape = new byte[0]; try { - cape = requestImage(capeUrl); + cape = requestImage(capeUrl, true); } catch (Exception ignored) {} // just ignore I guess - return new Cape(capeUrl, cape); + + return new Cape( + capeUrl, + cape.length > 0 ? cape : EMPTY_CAPE.getCapeData(), + System.currentTimeMillis(), + cape.length == 0 + ); } - private static byte[] requestImage(String imageUrl) throws Exception { + private static byte[] requestImage(String imageUrl, boolean cape) throws Exception { BufferedImage image = ImageIO.read(new URL(imageUrl)); Geyser.getLogger().debug("Downloaded " + imageUrl); + if (cape) { + BufferedImage newImage = new BufferedImage(64, 32, BufferedImage.TYPE_INT_RGB); + + Graphics g = newImage.createGraphics(); + g.drawImage(image, 0, 0, 64, 32, null); + g.dispose(); + image = newImage; + } + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(image.getWidth() * 4 + image.getHeight() * 4); try { for (int y = 0; y < image.getHeight(); y++) { @@ -152,6 +190,13 @@ public class SkinProvider { } } + public static T getOrDefault(CompletableFuture future, T defaultValue, int timeoutInSeconds) { + try { + return future.get(timeoutInSeconds, TimeUnit.SECONDS); + } catch (Exception ignored) {} + return defaultValue; + } + @AllArgsConstructor @Getter public static class SkinAndCape { @@ -164,7 +209,7 @@ public class SkinProvider { public static class Skin { private UUID skinOwner; private String textureUrl; - private byte[] skinData = new byte[0]; + private byte[] skinData = STEVE_SKIN; private long requestedOn; private boolean updated; @@ -179,12 +224,45 @@ public class SkinProvider { public static class Cape { private String textureUrl; private byte[] capeData; + private long requestedOn; + private boolean failed; } - private static T getOrDefault(CompletableFuture future, T defaultValue, int timeoutInSeconds) { - try { - return future.get(timeoutInSeconds, TimeUnit.SECONDS); - } catch (Exception ignored) {} - return defaultValue; + /* + * Sorted by 'priority' + */ + @AllArgsConstructor + @Getter + public enum UnofficalCape { + OPTIFINE("http://s.optifine.net/capes/%s.png", CapeUrlType.USERNAME), + LABYMOD("http://capes.labymod.net/capes/%s.png", CapeUrlType.UUID_DASHED), + FIVEZIG("http://textures.5zig.net/2/%s", CapeUrlType.UUID), + MINECRAFTCAPES("https://www.minecraftcapes.co.uk/getCape/%s", CapeUrlType.UUID); + + public static final UnofficalCape[] VALUES = values(); + private String url; + private CapeUrlType type; + + public String getUrlFor(String type) { + return String.format(url, type); + } + + public String getUrlFor(UUID uuid, String username) { + return getUrlFor(toRequestedType(type, uuid, username)); + } + + public static String toRequestedType(CapeUrlType type, UUID uuid, String username) { + switch (type) { + case UUID: return uuid.toString().replace("-", ""); + case UUID_DASHED: return uuid.toString(); + default: return username; + } + } + } + + public enum CapeUrlType { + USERNAME, + UUID, + UUID_DASHED } } diff --git a/connector/src/main/resources/config.yml b/connector/src/main/resources/config.yml index d2dff626c..840614ea8 100644 --- a/connector/src/main/resources/config.yml +++ b/connector/src/main/resources/config.yml @@ -49,6 +49,10 @@ debug-mode: false # Thread pool size general-thread-pool: 32 +# Allow third party capes to be visible. Currently allowing: +# OptiFine capes, LabyMod capes, 5Zig capes and MinecraftCapes +allow-third-party-capes: true + # bStats is a stat tracker that is entirely anonymous and tracks only basic information # about Geyser, such as how many people are online, how many servers are using Geyser, # what OS is being used, etc. You can learn more about bStats here: https://bstats.org/. @@ -57,8 +61,4 @@ metrics: # If metrics should be enabled enabled: true # UUID of server, don't change! - uuid: generateduuid - - - - + uuid: generateduuid \ No newline at end of file