diff --git a/connector/pom.xml b/connector/pom.xml
index 37218bda2..6f6599b07 100644
--- a/connector/pom.xml
+++ b/connector/pom.xml
@@ -20,13 +20,7 @@
com.fasterxml.jackson.dataformat
jackson-dataformat-yaml
- 2.9.8
- compile
-
-
- com.fasterxml.jackson.datatype
- jackson-datatype-jsr310
- 2.9.8
+ 2.10.2
compile
@@ -175,25 +169,25 @@
net.kyori
adventure-api
- 4.3.0
+ 4.5.0
compile
net.kyori
adventure-text-serializer-gson
- 4.3.0
+ 4.5.0
compile
net.kyori
adventure-text-serializer-legacy
- 4.3.0
+ 4.5.0
compile
net.kyori
adventure-text-serializer-gson-legacy-impl
- 4.3.0
+ 4.5.0
compile
diff --git a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java
index f86e0b1e6..3494f8c20 100644
--- a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java
+++ b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java
@@ -77,7 +77,8 @@ public class GeyserConnector {
.enable(JsonParser.Feature.IGNORE_UNDEFINED)
.enable(JsonParser.Feature.ALLOW_COMMENTS)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
- .enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES);
+ .enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES)
+ .enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES);
public static final String NAME = "Geyser";
public static final String GIT_VERSION = "DEV"; // A fallback for running in IDEs
diff --git a/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java b/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java
index 87883087d..a1ebbc8d0 100644
--- a/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java
+++ b/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java
@@ -42,6 +42,15 @@ import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
public class ConnectorServerEventHandler implements BedrockServerEventHandler {
+ /*
+ The following constants are all used to ensure the ping does not reach a length where it is unparsable by the Bedrock client
+ */
+ private static final int MINECRAFT_VERSION_BYTES_LENGTH = BedrockProtocol.DEFAULT_BEDROCK_CODEC.getMinecraftVersion().getBytes(StandardCharsets.UTF_8).length;
+ private static final int BRAND_BYTES_LENGTH = GeyserConnector.NAME.getBytes(StandardCharsets.UTF_8).length;
+ /**
+ * The MOTD, sub-MOTD and Minecraft version ({@link #MINECRAFT_VERSION_BYTES_LENGTH}) combined cannot reach this length.
+ */
+ private static final int MAGIC_RAKNET_LENGTH = 338;
private final GeyserConnector connector;
@@ -69,16 +78,16 @@ public class ConnectorServerEventHandler implements BedrockServerEventHandler {
BedrockPong pong = new BedrockPong();
pong.setEdition("MCPE");
- pong.setGameType("Default");
+ pong.setGameType("Survival"); // Can only be Survival or Creative as of 1.16.210.59
pong.setNintendoLimited(false);
pong.setProtocolVersion(BedrockProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion());
- pong.setVersion(null); // Server tries to connect either way and it looks better
+ pong.setVersion(BedrockProtocol.DEFAULT_BEDROCK_CODEC.getMinecraftVersion()); // Required to not be empty as of 1.16.210.59. Can only contain . and numbers.
pong.setIpv4Port(config.getBedrock().getPort());
if (config.isPassthroughMotd() && pingInfo != null && pingInfo.getDescription() != null) {
String[] motd = MessageTranslator.convertMessageLenient(pingInfo.getDescription()).split("\n");
String mainMotd = motd[0]; // First line of the motd.
- String subMotd = (motd.length != 1) ? motd[1] : ""; // Second line of the motd if present, otherwise blank.
+ String subMotd = (motd.length != 1) ? motd[1] : GeyserConnector.NAME; // Second line of the motd if present, otherwise default.
pong.setMotd(mainMotd.trim());
pong.setSubMotd(subMotd.trim()); // Trimmed to shift it to the left, prevents the universe from collapsing on us just because we went 2 characters over the text box's limit.
@@ -95,15 +104,28 @@ public class ConnectorServerEventHandler implements BedrockServerEventHandler {
pong.setMaximumPlayerCount(config.getMaxPlayers());
}
+ // Fallbacks to prevent errors and allow Bedrock to see the server
+ if (pong.getMotd() == null || pong.getMotd().trim().isEmpty()) {
+ pong.setMotd(GeyserConnector.NAME);
+ }
+ if (pong.getSubMotd() == null || pong.getSubMotd().trim().isEmpty()) {
+ // Sub-MOTD cannot be empty as of 1.16.210.59
+ pong.setSubMotd(GeyserConnector.NAME);
+ }
+
// The ping will not appear if the MOTD + sub-MOTD is of a certain length.
// We don't know why, though
byte[] motdArray = pong.getMotd().getBytes(StandardCharsets.UTF_8);
- if (motdArray.length + pong.getSubMotd().getBytes(StandardCharsets.UTF_8).length > 338) {
- // Remove the sub-MOTD first since that only appears locally
- pong.setSubMotd("");
- if (motdArray.length > 338) {
+ int subMotdLength = pong.getSubMotd().getBytes(StandardCharsets.UTF_8).length;
+ if (motdArray.length + subMotdLength > (MAGIC_RAKNET_LENGTH - MINECRAFT_VERSION_BYTES_LENGTH)) {
+ // Shorten the sub-MOTD first since that only appears locally
+ if (subMotdLength > BRAND_BYTES_LENGTH) {
+ pong.setSubMotd(GeyserConnector.NAME);
+ subMotdLength = BRAND_BYTES_LENGTH;
+ }
+ if (motdArray.length > (MAGIC_RAKNET_LENGTH - MINECRAFT_VERSION_BYTES_LENGTH - subMotdLength)) {
// If the top MOTD is still too long, we chop it down
- byte[] newMotdArray = new byte[339];
+ byte[] newMotdArray = new byte[MAGIC_RAKNET_LENGTH - MINECRAFT_VERSION_BYTES_LENGTH - subMotdLength];
System.arraycopy(motdArray, 0, newMotdArray, 0, newMotdArray.length);
pong.setMotd(new String(newMotdArray, StandardCharsets.UTF_8));
}
diff --git a/connector/src/main/java/org/geysermc/connector/skin/SkinManager.java b/connector/src/main/java/org/geysermc/connector/skin/SkinManager.java
index ae3abc943..fb8336aca 100644
--- a/connector/src/main/java/org/geysermc/connector/skin/SkinManager.java
+++ b/connector/src/main/java/org/geysermc/connector/skin/SkinManager.java
@@ -33,6 +33,7 @@ import com.nukkitx.protocol.bedrock.packet.PlayerListPacket;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.geysermc.connector.GeyserConnector;
+import org.geysermc.connector.common.AuthType;
import org.geysermc.connector.entity.player.PlayerEntity;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.session.auth.BedrockClientData;
@@ -163,7 +164,7 @@ public class SkinManager {
geometry = SkinProvider.SkinGeometry.getEars(data.isAlex());
// Store the skin and geometry for the ears
- SkinProvider.storeEarSkin(entity.getUuid(), skin);
+ SkinProvider.storeEarSkin(skin);
SkinProvider.storeEarGeometry(entity.getUuid(), data.isAlex());
}
}
@@ -267,7 +268,10 @@ public class SkinManager {
return new GameProfileData(skinUrl, capeUrl, isAlex);
} catch (Exception exception) {
- GeyserConnector.getInstance().getLogger().debug("Something went wrong while processing skin for " + profile.getName() + ": " + exception.getMessage());
+ GeyserConnector.getInstance().getLogger().debug("Something went wrong while processing skin for " + profile.getName());
+ if (GeyserConnector.getInstance().getConfig().isDebugMode()) {
+ exception.printStackTrace();
+ }
return loadBedrockOrOfflineSkin(profile);
}
}
@@ -282,7 +286,7 @@ public class SkinManager {
String skinUrl = isAlex ? SkinProvider.EMPTY_SKIN_ALEX.getTextureUrl() : SkinProvider.EMPTY_SKIN.getTextureUrl();
String capeUrl = SkinProvider.EMPTY_CAPE.getTextureUrl();
- if ("steve".equals(skinUrl) || "alex".equals(skinUrl)) {
+ if (("steve".equals(skinUrl) || "alex".equals(skinUrl)) && GeyserConnector.getInstance().getAuthType() != AuthType.ONLINE) {
GeyserSession session = GeyserConnector.getInstance().getPlayerByUuid(profile.getId());
if (session != null) {
diff --git a/connector/src/main/java/org/geysermc/connector/skin/SkinProvider.java b/connector/src/main/java/org/geysermc/connector/skin/SkinProvider.java
index 3f236932a..c4d4bc486 100644
--- a/connector/src/main/java/org/geysermc/connector/skin/SkinProvider.java
+++ b/connector/src/main/java/org/geysermc/connector/skin/SkinProvider.java
@@ -79,13 +79,12 @@ public class SkinProvider {
.build();
private static final Map> requestedCapes = new ConcurrentHashMap<>();
- public static final SkinGeometry EMPTY_GEOMETRY = SkinProvider.SkinGeometry.getLegacy(false);
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;
- public static SkinGeometry SKULL_GEOMETRY;
+ public static final String EARS_GEOMETRY;
+ public static final String EARS_GEOMETRY_SLIM;
+ public static final SkinGeometry SKULL_GEOMETRY;
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@@ -229,15 +228,15 @@ public class SkinProvider {
return CompletableFuture.completedFuture(officialCape);
}
- public static CompletableFuture requestEars(String earsUrl, EarsProvider provider, boolean newThread, Skin skin) {
+ public static CompletableFuture requestEars(String earsUrl, boolean newThread, Skin skin) {
if (earsUrl == null || earsUrl.isEmpty()) return CompletableFuture.completedFuture(skin);
CompletableFuture future;
if (newThread) {
- future = CompletableFuture.supplyAsync(() -> supplyEars(skin, earsUrl, provider), EXECUTOR_SERVICE)
+ future = CompletableFuture.supplyAsync(() -> supplyEars(skin, earsUrl), EXECUTOR_SERVICE)
.whenCompleteAsync((outSkin, throwable) -> { });
} else {
- Skin ears = supplyEars(skin, earsUrl, provider); // blocking
+ Skin ears = supplyEars(skin, earsUrl); // blocking
future = CompletableFuture.completedFuture(ears);
}
return future;
@@ -255,7 +254,7 @@ public class SkinProvider {
public static CompletableFuture requestUnofficialEars(Skin officialSkin, UUID playerId, String username, boolean newThread) {
for (EarsProvider provider : EarsProvider.VALUES) {
Skin skin1 = getOrDefault(
- requestEars(provider.getUrlFor(playerId, username), provider, newThread, officialSkin),
+ requestEars(provider.getUrlFor(playerId, username), newThread, officialSkin),
officialSkin, 4
);
if (skin1.isEars()) {
@@ -295,12 +294,11 @@ public class SkinProvider {
}
/**
- * Stores the ajusted skin with the ear texture to the cache
+ * Stores the adjusted skin with the ear texture to the cache
*
- * @param playerID The UUID to cache it against
* @param skin The skin to cache
*/
- public static void storeEarSkin(UUID playerID, Skin skin) {
+ public static void storeEarSkin(Skin skin) {
cachedSkins.put(skin.getTextureUrl(), skin);
}
@@ -324,7 +322,7 @@ public class SkinProvider {
}
private static Cape supplyCape(String capeUrl, CapeProvider provider) {
- byte[] cape = new byte[0];
+ byte[] cape = EMPTY_CAPE.getCapeData();
try {
cape = requestImage(capeUrl, provider);
} catch (Exception ignored) {} // just ignore I guess
@@ -334,7 +332,7 @@ public class SkinProvider {
return new Cape(
capeUrl,
urlSection[urlSection.length - 1], // get the texture id and use it as cape id
- cape.length > 0 ? cape : EMPTY_CAPE.getCapeData(),
+ cape,
System.currentTimeMillis(),
cape.length == 0
);
@@ -345,10 +343,9 @@ public class SkinProvider {
*
* @param existingSkin The players current skin
* @param earsUrl The URL to get the ears texture from
- * @param provider The ears texture provider
* @return The updated skin with ears
*/
- private static Skin supplyEars(Skin existingSkin, String earsUrl, EarsProvider provider) {
+ private static Skin supplyEars(Skin existingSkin, String earsUrl) {
try {
// Get the ears texture
BufferedImage ears = ImageIO.read(new URL(earsUrl));
@@ -415,14 +412,15 @@ public class SkinProvider {
// if the requested image is a cape
if (provider != null) {
- while(image.getWidth() > 64) {
- image = scale(image);
+ if (image.getWidth() > 64) {
+ image = scale(image, 64, 32);
+ }
+ } else {
+ // Very rarely, skins can be larger than Minecraft's default.
+ // Bedrock will not render anything above a width of 128.
+ if (image.getWidth() > 128) {
+ image = scale(image, 128, image.getHeight() / (image.getWidth() / 128));
}
- BufferedImage newImage = new BufferedImage(64, 32, BufferedImage.TYPE_INT_ARGB);
- Graphics g = newImage.createGraphics();
- g.drawImage(image, 0, 0, image.getWidth(), image.getHeight(), null);
- g.dispose();
- image = newImage;
}
byte[] data = bufferedImageToImageData(image);
@@ -506,12 +504,13 @@ public class SkinProvider {
return null;
}
- private static BufferedImage scale(BufferedImage bufferedImage) {
- BufferedImage resized = new BufferedImage(bufferedImage.getWidth() / 2, bufferedImage.getHeight() / 2, BufferedImage.TYPE_INT_ARGB);
+ private static BufferedImage scale(BufferedImage bufferedImage, int newWidth, int newHeight) {
+ BufferedImage resized = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = resized.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
- g2.drawImage(bufferedImage, 0, 0, bufferedImage.getWidth() / 2, bufferedImage.getHeight() / 2, null);
+ g2.drawImage(bufferedImage, 0, 0, newWidth, newHeight, null);
g2.dispose();
+ bufferedImage.flush();
return resized;
}
@@ -579,17 +578,17 @@ public class SkinProvider {
@AllArgsConstructor
@Getter
public static class SkinAndCape {
- private Skin skin;
- private Cape cape;
+ private final Skin skin;
+ private final Cape cape;
}
@AllArgsConstructor
@Getter
public static class Skin {
private UUID skinOwner;
- private String textureUrl;
- private byte[] skinData;
- private long requestedOn;
+ private final String textureUrl;
+ private final byte[] skinData;
+ private final long requestedOn;
private boolean updated;
private boolean ears;
@@ -603,19 +602,19 @@ public class SkinProvider {
@AllArgsConstructor
@Getter
public static class Cape {
- private String textureUrl;
- private String capeId;
- private byte[] capeData;
- private long requestedOn;
- private boolean failed;
+ private final String textureUrl;
+ private final String capeId;
+ private final byte[] capeData;
+ private final long requestedOn;
+ private final boolean failed;
}
@AllArgsConstructor
@Getter
public static class SkinGeometry {
- private String geometryName;
- private String geometryData;
- private boolean failed;
+ private final String geometryName;
+ private final String geometryData;
+ private final boolean failed;
/**
* Generate generic geometry
diff --git a/connector/src/main/java/org/geysermc/connector/skin/SkullSkinManager.java b/connector/src/main/java/org/geysermc/connector/skin/SkullSkinManager.java
index 644323a42..562e2c50f 100644
--- a/connector/src/main/java/org/geysermc/connector/skin/SkullSkinManager.java
+++ b/connector/src/main/java/org/geysermc/connector/skin/SkullSkinManager.java
@@ -27,65 +27,42 @@ package org.geysermc.connector.skin;
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.connector.GeyserConnector;
import org.geysermc.connector.entity.player.PlayerEntity;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.utils.LanguageUtils;
import java.util.Collections;
-import java.util.UUID;
import java.util.function.Consumer;
public class SkullSkinManager extends SkinManager {
- public static PlayerListPacket.Entry buildSkullEntryManually(UUID uuid, String username, long geyserId,
- String skinId, byte[] skinData) {
+ public static SerializedSkin buildSkullEntryManually(String skinId, byte[] skinData) {
// Prevents https://cdn.discordapp.com/attachments/613194828359925800/779458146191147008/unknown.png
skinId = skinId + "_skull";
- SerializedSkin serializedSkin = SerializedSkin.of(
+ return SerializedSkin.of(
skinId, SkinProvider.SKULL_GEOMETRY.getGeometryName(), ImageData.of(skinData), Collections.emptyList(),
ImageData.of(SkinProvider.EMPTY_CAPE.getCapeData()), SkinProvider.SKULL_GEOMETRY.getGeometryData(),
"", true, false, false, SkinProvider.EMPTY_CAPE.getCapeId(), skinId
);
-
- PlayerListPacket.Entry entry = new PlayerListPacket.Entry(uuid);
- entry.setName(username);
- entry.setEntityId(geyserId);
- entry.setSkin(serializedSkin);
- entry.setXuid("");
- entry.setPlatformChatId("");
- entry.setTeacher(false);
- entry.setTrustedSkin(true);
- return entry;
}
public static void requestAndHandleSkin(PlayerEntity entity, GeyserSession session,
Consumer skinConsumer) {
GameProfileData data = GameProfileData.from(entity.getProfile());
- SkinProvider.requestSkin(entity.getUuid(), data.getSkinUrl(), false)
+ SkinProvider.requestSkin(entity.getUuid(), data.getSkinUrl(), true)
.whenCompleteAsync((skin, throwable) -> {
try {
if (session.getUpstream().isInitialized()) {
- PlayerListPacket.Entry updatedEntry = buildSkullEntryManually(
- entity.getUuid(),
- entity.getUsername(),
- entity.getGeyserId(),
- skin.getTextureUrl(),
- skin.getSkinData()
- );
-
- PlayerListPacket playerAddPacket = new PlayerListPacket();
- playerAddPacket.setAction(PlayerListPacket.Action.ADD);
- playerAddPacket.getEntries().add(updatedEntry);
- session.sendUpstreamPacket(playerAddPacket);
-
- // It's a skull. We don't want them in the player list.
- PlayerListPacket playerRemovePacket = new PlayerListPacket();
- playerRemovePacket.setAction(PlayerListPacket.Action.REMOVE);
- playerRemovePacket.getEntries().add(updatedEntry);
- session.sendUpstreamPacket(playerRemovePacket);
+ 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) {
GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e);
diff --git a/connector/src/main/resources/config.yml b/connector/src/main/resources/config.yml
index cdb2cb362..da46f5804 100644
--- a/connector/src/main/resources/config.yml
+++ b/connector/src/main/resources/config.yml
@@ -18,8 +18,9 @@ bedrock:
# This option is for the plugin version only.
clone-remote-port: false
# The MOTD that will be broadcasted to Minecraft: Bedrock Edition clients. This is irrelevant if "passthrough-motd" is set to true
- motd1: "GeyserMC"
- motd2: "Another GeyserMC forced host."
+ # If either of these are empty, the respective string will default to "Geyser"
+ motd1: "Geyser"
+ motd2: "Another Geyser server."
# The Server Name that will be sent to Minecraft: Bedrock Edition clients. This is visible in both the pause menu and the settings menu.
server-name: "Geyser"
remote: