From 7e3a736f20c9810f86c328f034e907399a338fb7 Mon Sep 17 00:00:00 2001 From: Tim203 Date: Wed, 18 Nov 2020 19:38:49 +0100 Subject: [PATCH] Register Floodgate payload, updated Statistics, smaller jar, fixed bugs Quite a lot of changes, but I was too lazy to split them in different commits (and they'll be squashed later anyway): * Floodgate plugin message channels are now registered (because Spigot requires that, and I guess it's better practice) * Updated the Statistics form to match the new Forms API * The common jar is now much smaller, because Jackson isn't needed anymore in the common module * Fixed some bugs in Forms where empty fields would lead to excluding them in the serialization (making Bedrock complain) And a few other things, like a new boolean in RawSkin saying if the Skin is an Alex or Steve model. --- common/pom.xml | 6 - .../org/geysermc/common/form/ModalForm.java | 6 +- .../org/geysermc/common/form/SimpleForm.java | 2 +- .../common/window/CustomFormWindow.java | 0 .../common/window/SimpleFormWindow.java | 0 .../geysermc/floodgate/util/BedrockData.java | 49 +-- .../org/geysermc/floodgate/util/DeviceOs.java | 3 +- .../geysermc/floodgate/util/InputMode.java | 4 +- .../geysermc/floodgate/util/LinkedPlayer.java | 15 +- .../org/geysermc/floodgate/util/RawSkin.java | 33 +- .../geysermc/floodgate/util/UiProfile.java | 7 +- .../geysermc/connector/GeyserConnector.java | 2 +- .../org/geysermc/connector/dump/DumpInfo.java | 2 +- .../connector/entity/FireworkEntity.java | 2 +- .../network/session/GeyserSession.java | 4 +- .../session/auth/BedrockClientData.java | 69 +++- .../java/JavaJoinGameTranslator.java | 7 +- .../java/JavaStatisticsTranslator.java | 3 +- .../connector/utils/LanguageUtils.java | 13 +- .../connector/utils/LoginEncryptionUtils.java | 2 - .../connector/utils/PluginMessageUtils.java | 36 +- .../connector/utils/SkinProvider.java | 3 +- .../connector/utils/StatisticsUtils.java | 317 +++++++++--------- 23 files changed, 324 insertions(+), 261 deletions(-) delete mode 100644 common/src/main/java/org/geysermc/common/window/CustomFormWindow.java delete mode 100644 common/src/main/java/org/geysermc/common/window/SimpleFormWindow.java diff --git a/common/pom.xml b/common/pom.xml index 30c8afc06..9223331be 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -18,11 +18,5 @@ 2.8.5 compile - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - 2.9.8 - compile - \ No newline at end of file diff --git a/common/src/main/java/org/geysermc/common/form/ModalForm.java b/common/src/main/java/org/geysermc/common/form/ModalForm.java index 10309f9c2..ea58a466e 100644 --- a/common/src/main/java/org/geysermc/common/form/ModalForm.java +++ b/common/src/main/java/org/geysermc/common/form/ModalForm.java @@ -69,9 +69,9 @@ public class ModalForm extends Form { } public static final class Builder extends Form.Builder { - private String content; - private String button1; - private String button2; + private String content = ""; + private String button1 = ""; + private String button2 = ""; public Builder content(String content) { this.content = translate(content); diff --git a/common/src/main/java/org/geysermc/common/form/SimpleForm.java b/common/src/main/java/org/geysermc/common/form/SimpleForm.java index 1a1ce616b..275fb8a86 100644 --- a/common/src/main/java/org/geysermc/common/form/SimpleForm.java +++ b/common/src/main/java/org/geysermc/common/form/SimpleForm.java @@ -80,7 +80,7 @@ public final class SimpleForm extends Form { public static final class Builder extends Form.Builder { private final List buttons = new ArrayList<>(); - private String content; + private String content = ""; public Builder content(String content) { this.content = translate(content); diff --git a/common/src/main/java/org/geysermc/common/window/CustomFormWindow.java b/common/src/main/java/org/geysermc/common/window/CustomFormWindow.java deleted file mode 100644 index e69de29bb..000000000 diff --git a/common/src/main/java/org/geysermc/common/window/SimpleFormWindow.java b/common/src/main/java/org/geysermc/common/window/SimpleFormWindow.java deleted file mode 100644 index e69de29bb..000000000 diff --git a/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java b/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java index 89eaf5395..08c1e28d3 100644 --- a/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java +++ b/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java @@ -30,14 +30,14 @@ import lombok.AllArgsConstructor; import lombok.Getter; /** - * This class contains the raw data send by Geyser to Floodgate or from Floodgate to Floodgate. - * This class is only used internally, and you should look at FloodgatePlayer instead - * (FloodgatePlayer is present in the common module in the Floodgate repo) + * This class contains the raw data send by Geyser to Floodgate or from Floodgate to Floodgate. This + * class is only used internally, and you should look at FloodgatePlayer instead (FloodgatePlayer is + * present in the API module of the Floodgate repo) */ -@AllArgsConstructor(access = AccessLevel.PACKAGE) @Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) public final class BedrockData { - public static final int EXPECTED_LENGTH = 9; + public static final int EXPECTED_LENGTH = 10; private final String version; private final String username; @@ -48,22 +48,21 @@ public final class BedrockData { private final int inputMode; private final String ip; private final LinkedPlayer linkedPlayer; + private final boolean fromProxy; + private final int dataLength; - public BedrockData(String version, String username, String xuid, int deviceOs, - String languageCode, int uiProfile, int inputMode, String ip, - LinkedPlayer linkedPlayer) { - this(version, username, xuid, deviceOs, languageCode, - inputMode, uiProfile, ip, linkedPlayer, EXPECTED_LENGTH); + public static BedrockData of(String version, String username, String xuid, int deviceOs, + String languageCode, int uiProfile, int inputMode, String ip, + LinkedPlayer linkedPlayer, boolean fromProxy) { + return new BedrockData(version, username, xuid, deviceOs, languageCode, inputMode, + uiProfile, ip, linkedPlayer, fromProxy, EXPECTED_LENGTH); } - public BedrockData(String version, String username, String xuid, int deviceOs, - String languageCode, int uiProfile, int inputMode, String ip) { - this(version, username, xuid, deviceOs, languageCode, uiProfile, inputMode, ip, null); - } - - public boolean hasPlayerLink() { - return linkedPlayer != null; + public static BedrockData of(String version, String username, String xuid, int deviceOs, + String languageCode, int uiProfile, int inputMode, String ip) { + return of(version, username, xuid, deviceOs, languageCode, + uiProfile, inputMode, ip, null, false); } public static BedrockData fromString(String data) { @@ -77,19 +76,23 @@ public final class BedrockData { return new BedrockData( split[0], split[1], split[2], Integer.parseInt(split[3]), split[4], Integer.parseInt(split[5]), Integer.parseInt(split[6]), split[7], - linkedPlayer, split.length + linkedPlayer, Boolean.parseBoolean(split[9]), split.length ); } + private static BedrockData emptyData(int dataLength) { + return new BedrockData(null, null, null, -1, null, -1, -1, null, null, false, dataLength); + } + + public boolean hasPlayerLink() { + return linkedPlayer != null; + } + @Override public String toString() { // The format is the same as the order of the fields in this class return version + '\0' + username + '\0' + xuid + '\0' + deviceOs + '\0' + languageCode + '\0' + uiProfile + '\0' + inputMode + '\0' + ip + '\0' + - (linkedPlayer != null ? linkedPlayer.toString() : "null"); - } - - private static BedrockData emptyData(int dataLength) { - return new BedrockData(null, null, null, -1, null, -1, -1, null, null, dataLength); + fromProxy + '\0' + (linkedPlayer != null ? linkedPlayer.toString() : "null"); } } diff --git a/common/src/main/java/org/geysermc/floodgate/util/DeviceOs.java b/common/src/main/java/org/geysermc/floodgate/util/DeviceOs.java index da783982c..b3245ef81 100644 --- a/common/src/main/java/org/geysermc/floodgate/util/DeviceOs.java +++ b/common/src/main/java/org/geysermc/floodgate/util/DeviceOs.java @@ -25,7 +25,6 @@ package org.geysermc.floodgate.util; -import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; @@ -34,7 +33,6 @@ import lombok.RequiredArgsConstructor; */ @RequiredArgsConstructor(access = AccessLevel.PRIVATE) public enum DeviceOs { - @JsonEnumDefaultValue UNKNOWN("Unknown"), ANDROID("Android"), IOS("iOS"), @@ -57,6 +55,7 @@ public enum DeviceOs { /** * Get the DeviceOs instance from the identifier. + * * @param id the DeviceOs identifier * @return The DeviceOs or {@link #UNKNOWN} if the DeviceOs wasn't found */ diff --git a/common/src/main/java/org/geysermc/floodgate/util/InputMode.java b/common/src/main/java/org/geysermc/floodgate/util/InputMode.java index 4dcaa8ab3..9664e18ae 100644 --- a/common/src/main/java/org/geysermc/floodgate/util/InputMode.java +++ b/common/src/main/java/org/geysermc/floodgate/util/InputMode.java @@ -26,10 +26,7 @@ package org.geysermc.floodgate.util; -import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; - public enum InputMode { - @JsonEnumDefaultValue UNKNOWN, KEYBOARD_MOUSE, TOUCH, // I guess Touch? @@ -40,6 +37,7 @@ public enum InputMode { /** * Get the InputMode instance from the identifier. + * * @param id the InputMode identifier * @return The InputMode or {@link #UNKNOWN} if the DeviceOs wasn't found */ diff --git a/common/src/main/java/org/geysermc/floodgate/util/LinkedPlayer.java b/common/src/main/java/org/geysermc/floodgate/util/LinkedPlayer.java index 53a167f9b..c29d461d1 100644 --- a/common/src/main/java/org/geysermc/floodgate/util/LinkedPlayer.java +++ b/common/src/main/java/org/geysermc/floodgate/util/LinkedPlayer.java @@ -26,11 +26,14 @@ package org.geysermc.floodgate.util; +import lombok.AccessLevel; import lombok.Getter; +import lombok.RequiredArgsConstructor; import java.util.UUID; @Getter +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) public final class LinkedPlayer { /** * The Java username of the linked player @@ -45,19 +48,17 @@ public final class LinkedPlayer { */ private final UUID bedrockId; /** - * If the LinkedPlayer is send from a different platform. - * For example the LinkedPlayer is from Bungee but the data has been sent to the Bukkit server. + * If the LinkedPlayer is send from a different platform. For example the LinkedPlayer is from + * Bungee but the data has been sent to the Bukkit server. */ private boolean fromDifferentPlatform = false; - public LinkedPlayer(String javaUsername, UUID javaUniqueId, UUID bedrockId) { - this.javaUsername = javaUsername; - this.javaUniqueId = javaUniqueId; - this.bedrockId = bedrockId; + public static LinkedPlayer of(String javaUsername, UUID javaUniqueId, UUID bedrockId) { + return new LinkedPlayer(javaUsername, javaUniqueId, bedrockId); } static LinkedPlayer fromString(String data) { - if (data.length() != 3) { + if (data.length() == 4) { return null; } diff --git a/common/src/main/java/org/geysermc/floodgate/util/RawSkin.java b/common/src/main/java/org/geysermc/floodgate/util/RawSkin.java index 2ff0e3fb2..152cbec5a 100644 --- a/common/src/main/java/org/geysermc/floodgate/util/RawSkin.java +++ b/common/src/main/java/org/geysermc/floodgate/util/RawSkin.java @@ -31,31 +31,36 @@ import lombok.ToString; import java.nio.ByteBuffer; import java.util.Base64; +import static java.lang.String.format; + @AllArgsConstructor @ToString public final class RawSkin { public int width; public int height; public byte[] data; + public boolean alex; - private RawSkin() {} + private RawSkin() { + } public static RawSkin decode(byte[] data) throws InvalidFormatException { if (data == null) { return null; } - int maxEncodedLength = 4 * (((64 * 64 * 4 + 8) + 2) / 3); + int maxEncodedLength = 4 * (((64 * 64 * 4 + 9) + 2) / 3); // if the RawSkin is longer then the max Java Edition skin length if (data.length > maxEncodedLength) { - throw new InvalidFormatException( - "Encoded data cannot be longer then " + maxEncodedLength + " bytes!" - ); + throw new InvalidFormatException(format( + "Encoded data cannot be longer then %s bytes! Got %s", + maxEncodedLength, data.length + )); } - // if the encoded data doesn't even contain the width and height (8 bytes, 2 ints) - if (data.length < 4 * ((8 + 2) / 3)) { - throw new InvalidFormatException("Encoded data must be at least 12 bytes long!"); + // if the encoded data doesn't even contain the width, height (8 bytes, 2 ints) and isAlex + if (data.length < 4 * ((9 + 2) / 3)) { + throw new InvalidFormatException("Encoded data must be at least 16 bytes long!"); } data = Base64.getDecoder().decode(data); @@ -65,23 +70,25 @@ public final class RawSkin { RawSkin skin = new RawSkin(); skin.width = buffer.getInt(); skin.height = buffer.getInt(); - if (buffer.remaining() != (skin.width * skin.height * 4)) { - throw new InvalidFormatException(String.format( + if (buffer.remaining() - 1 != (skin.width * skin.height * 4)) { + throw new InvalidFormatException(format( "Expected skin length to be %s, got %s", (skin.width * skin.height * 4), buffer.remaining() )); } - skin.data = new byte[buffer.remaining()]; + skin.data = new byte[buffer.remaining() - 1]; buffer.get(skin.data); + skin.alex = buffer.get() == 1; return skin; } public byte[] encode() { - // 2 x int = 8 bytes - ByteBuffer buffer = ByteBuffer.allocate(8 + data.length); + // 2 x int + 1 = 9 bytes + ByteBuffer buffer = ByteBuffer.allocate(9 + data.length); buffer.putInt(width); buffer.putInt(height); buffer.put(data); + buffer.put((byte) (alex ? 1 : 0)); return Base64.getEncoder().encode(buffer.array()); } } diff --git a/common/src/main/java/org/geysermc/floodgate/util/UiProfile.java b/common/src/main/java/org/geysermc/floodgate/util/UiProfile.java index 441e9202a..58d615eaa 100644 --- a/common/src/main/java/org/geysermc/floodgate/util/UiProfile.java +++ b/common/src/main/java/org/geysermc/floodgate/util/UiProfile.java @@ -26,17 +26,14 @@ package org.geysermc.floodgate.util; -import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; - public enum UiProfile { - @JsonEnumDefaultValue - CLASSIC, - POCKET; + CLASSIC, POCKET; private static final UiProfile[] VALUES = values(); /** * Get the UiProfile instance from the identifier. + * * @param id the UiProfile identifier * @return The UiProfile or {@link #CLASSIC} if the UiProfile wasn't found */ diff --git a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java index 1c35b23de..c44f8f434 100644 --- a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java +++ b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java @@ -229,7 +229,7 @@ public class GeyserConnector { for (GeyserSession session : players) { if (session == null) continue; if (session.getClientData() == null) continue; - String os = session.getClientData().getDeviceOS().toString(); + String os = session.getClientData().getDeviceOs().toString(); if (!valueMap.containsKey(os)) { valueMap.put(os, 1); } else { diff --git a/connector/src/main/java/org/geysermc/connector/dump/DumpInfo.java b/connector/src/main/java/org/geysermc/connector/dump/DumpInfo.java index b229e1670..d41ad64a3 100644 --- a/connector/src/main/java/org/geysermc/connector/dump/DumpInfo.java +++ b/connector/src/main/java/org/geysermc/connector/dump/DumpInfo.java @@ -73,7 +73,7 @@ public class DumpInfo { this.userPlatforms = new Object2IntOpenHashMap(); for (GeyserSession session : GeyserConnector.getInstance().getPlayers()) { - DeviceOs device = session.getClientData().getDeviceOS(); + DeviceOs device = session.getClientData().getDeviceOs(); userPlatforms.put(device, userPlatforms.getOrDefault(device, 0) + 1); } diff --git a/connector/src/main/java/org/geysermc/connector/entity/FireworkEntity.java b/connector/src/main/java/org/geysermc/connector/entity/FireworkEntity.java index ede3b78bb..630a5edd9 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/FireworkEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/FireworkEntity.java @@ -67,7 +67,7 @@ public class FireworkEntity extends Entity { // TODO: Remove once Mojang fixes bugs with fireworks crashing clients on these specific devices. // https://bugs.mojang.com/browse/MCPE-89115 - if (session.getClientData().getDeviceOS() == DeviceOs.XBOX_ONE || session.getClientData().getDeviceOS() == DeviceOs.ORBIS) { + if (session.getClientData().getDeviceOs() == DeviceOs.XBOX_ONE || session.getClientData().getDeviceOs() == DeviceOs.ORBIS) { return; } 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 bea234b50..99aada2a5 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 @@ -402,11 +402,11 @@ public class GeyserSession implements CommandSender { try { FloodgateCipher cipher = connector.getCipher(); - encryptedData = cipher.encryptFromString(new BedrockData( + encryptedData = cipher.encryptFromString(BedrockData.of( clientData.getGameVersion(), authData.getName(), authData.getXboxUUID(), - clientData.getDeviceOS().ordinal(), + clientData.getDeviceOs().ordinal(), clientData.getLanguageCode(), clientData.getUiProfile().ordinal(), clientData.getCurrentInputMode().ordinal(), diff --git a/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java b/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java index ab0fad845..905245ee4 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java @@ -29,6 +29,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.base.Charsets; import lombok.Getter; import org.geysermc.connector.utils.SkinProvider; import org.geysermc.floodgate.util.DeviceOs; @@ -87,7 +88,7 @@ public final class BedrockClientData { @JsonProperty(value = "DeviceModel") private String deviceModel; @JsonProperty(value = "DeviceOS") - private DeviceOs deviceOS; + private DeviceOs deviceOs; @JsonProperty(value = "UIProfile") private UiProfile uiProfile; @JsonProperty(value = "GuiScale") @@ -114,13 +115,7 @@ public final class BedrockClientData { @JsonProperty(value = "ThirdPartyNameOnly") private boolean thirdPartyNameOnly; - public void setJsonData(JsonNode data) { - if (this.jsonData == null && data != null) { - this.jsonData = data; - } - } - - private static RawSkin getLegacyImage(byte[] imageData) { + private static RawSkin getLegacyImage(byte[] imageData, boolean alex) { if (imageData == null) { return null; } @@ -128,43 +123,54 @@ public final class BedrockClientData { // width * height * 4 (rgba) switch (imageData.length) { case 8192: - return new RawSkin(64, 32, imageData); + return new RawSkin(64, 32, imageData, alex); case 16384: - return new RawSkin(64, 64, imageData); + return new RawSkin(64, 64, imageData, alex); case 32768: - return new RawSkin(64, 128, imageData); + return new RawSkin(64, 128, imageData, alex); case 65536: - return new RawSkin(128, 128, imageData); + return new RawSkin(128, 128, imageData, alex); default: throw new IllegalArgumentException("Unknown legacy skin size"); } } + public void setJsonData(JsonNode data) { + if (this.jsonData == null && data != null) { + this.jsonData = data; + } + } + /** * Taken from https://github.com/NukkitX/Nukkit/blob/master/src/main/java/cn/nukkit/network/protocol/LoginPacket.java
* Internally only used for Skins, but can be used for Capes too */ public RawSkin getImage(String name) { - System.out.println(jsonData.toString()); if (jsonData == null || !jsonData.has(name + "Data")) { return null; } + boolean alex = false; + if (name.equals("Skin")) { + alex = isAlex(); + } + byte[] image = Base64.getDecoder().decode(jsonData.get(name + "Data").asText()); if (jsonData.has(name + "ImageWidth") && jsonData.has(name + "ImageHeight")) { return new RawSkin( jsonData.get(name + "ImageWidth").asInt(), jsonData.get(name + "ImageHeight").asInt(), - image + image, alex ); } - return getLegacyImage(image); + return getLegacyImage(image, alex); } public RawSkin getAndTransformImage(String name) { RawSkin skin = getImage(name); if (skin != null && (skin.width > 64 || skin.height > 64)) { - BufferedImage scaledImage = SkinProvider.imageDataToBufferedImage(skin.data, skin.width, skin.height); + BufferedImage scaledImage = + SkinProvider.imageDataToBufferedImage(skin.data, skin.width, skin.height); int max = Math.max(skin.width, skin.height); while (max > 64) { @@ -179,4 +185,35 @@ public final class BedrockClientData { } return skin; } + + public boolean isAlex() { + try { + byte[] bytes = Base64.getDecoder().decode(geometryName.getBytes(Charsets.UTF_8)); + String geometryName = + SkinProvider.OBJECT_MAPPER + .readTree(bytes) + .get("geometry").get("default") + .asText(); + return "geometry.humanoid.customSlim".equals(geometryName); + } catch (Exception exception) { + exception.printStackTrace(); + return false; + } + } + + public DeviceOs getDeviceOs() { + return deviceOs != null ? deviceOs : DeviceOs.UNKNOWN; + } + + public InputMode getCurrentInputMode() { + return currentInputMode != null ? currentInputMode : InputMode.UNKNOWN; + } + + public InputMode getDefaultInputMode() { + return defaultInputMode != null ? defaultInputMode : InputMode.UNKNOWN; + } + + public UiProfile getUiProfile() { + return uiProfile != null ? uiProfile : UiProfile.CLASSIC; + } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaJoinGameTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaJoinGameTranslator.java index a86c1a971..c0207841d 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaJoinGameTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaJoinGameTranslator.java @@ -34,6 +34,7 @@ import com.github.steveice10.mc.protocol.packet.ingame.server.ServerJoinGamePack import com.nukkitx.protocol.bedrock.data.GameRuleData; import com.nukkitx.protocol.bedrock.data.PlayerPermission; import com.nukkitx.protocol.bedrock.packet.*; +import org.geysermc.connector.common.AuthType; import org.geysermc.connector.entity.PlayerEntity; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; @@ -46,7 +47,6 @@ import java.util.List; @Translator(packet = ServerJoinGamePacket.class) public class JavaJoinGameTranslator extends PacketTranslator { - @Override public void translate(ServerJoinGamePacket packet, GeyserSession session) { PlayerEntity entity = session.getPlayerEntity(); @@ -96,6 +96,11 @@ public class JavaJoinGameTranslator extends PacketTranslator { - @Override public void translate(ServerStatisticsPacket packet, GeyserSession session) { session.updateStatistics(packet.getStatistics()); if (session.isWaitingForStatistics()) { session.setWaitingForStatistics(false); - session.sendForm(StatisticsUtils.buildMenuForm(session), StatisticsUtils.STATISTICS_MENU_FORM_ID); + StatisticsUtils.buildAndSendStatisticsMenu(session); } } } diff --git a/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java b/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java index 06d2936ef..fd25ce0a0 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java @@ -60,7 +60,9 @@ public class LanguageUtils { public static void loadGeyserLocale(String locale) { locale = formatLocale(locale); // Don't load the locale if it's already loaded. - if (LOCALE_MAPPINGS.containsKey(locale)) return; + if (LOCALE_MAPPINGS.containsKey(locale)) { + return; + } InputStream localeStream = GeyserConnector.class.getClassLoader().getResourceAsStream("languages/texts/" + locale + ".properties"); @@ -109,7 +111,7 @@ public class LanguageUtils { // Try and get the key from the default locale if (formatString == null) { - properties = LOCALE_MAPPINGS.get(formatLocale(getDefaultLocale())); + properties = LOCALE_MAPPINGS.get(getDefaultLocale()); formatString = properties.getProperty(key); } @@ -121,7 +123,7 @@ public class LanguageUtils { // Final fallback if (formatString == null) { - formatString = key; + return key; } return MessageFormat.format(formatString.replace("'", "''").replace("&", "\u00a7"), values); @@ -147,7 +149,10 @@ public class LanguageUtils { * @return the current default locale */ public static String getDefaultLocale() { - if (CACHED_LOCALE != null) return CACHED_LOCALE; // We definitely know the locale the user is using + if (CACHED_LOCALE != null) { + return CACHED_LOCALE; // We definitely know the locale the user is using + } + String locale; boolean isValid = true; if (GeyserConnector.getInstance() != null && diff --git a/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java b/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java index 391a02849..84997deb6 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java @@ -35,10 +35,8 @@ import com.nukkitx.protocol.bedrock.packet.LoginPacket; import com.nukkitx.protocol.bedrock.packet.ServerToClientHandshakePacket; import com.nukkitx.protocol.bedrock.util.EncryptionUtils; import org.geysermc.common.form.CustomForm; -import org.geysermc.common.form.ModalForm; import org.geysermc.common.form.SimpleForm; import org.geysermc.common.form.response.CustomFormResponse; -import org.geysermc.common.form.response.ModalFormResponse; import org.geysermc.common.form.response.SimpleFormResponse; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.network.session.GeyserSession; diff --git a/connector/src/main/java/org/geysermc/connector/utils/PluginMessageUtils.java b/connector/src/main/java/org/geysermc/connector/utils/PluginMessageUtils.java index eff9ee57b..4c7547d3a 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/PluginMessageUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/PluginMessageUtils.java @@ -25,27 +25,47 @@ package org.geysermc.connector.utils; +import com.google.common.base.Charsets; import org.geysermc.connector.GeyserConnector; -import java.nio.charset.StandardCharsets; +import java.nio.ByteBuffer; public class PluginMessageUtils { - private static final byte[] BRAND_DATA; + private static final byte[] GEYSER_BRAND_DATA; + private static final byte[] FLOODGATE_REGISTER_DATA; static { - byte[] data = GeyserConnector.NAME.getBytes(StandardCharsets.UTF_8); - byte[] varInt = writeVarInt(data.length); - BRAND_DATA = new byte[varInt.length + data.length]; - System.arraycopy(varInt, 0, BRAND_DATA, 0, varInt.length); - System.arraycopy(data, 0, BRAND_DATA, varInt.length, data.length); + byte[] data = GeyserConnector.NAME.getBytes(Charsets.UTF_8); + GEYSER_BRAND_DATA = + ByteBuffer.allocate(data.length + getVarIntLength(data.length)) + .put(writeVarInt(data.length)) + .put(data) + .array(); + + data = "floodgate:skin\0floodgate:form".getBytes(Charsets.UTF_8); + FLOODGATE_REGISTER_DATA = + ByteBuffer.allocate(data.length + getVarIntLength(data.length)) + .put(writeVarInt(data.length)) + .put(data) + .array(); } /** * Get the prebuilt brand as a byte array + * * @return the brand information of the Geyser client */ public static byte[] getGeyserBrandData() { - return BRAND_DATA; + return GEYSER_BRAND_DATA; + } + + /** + * Get the prebuilt register data as a byte array + * + * @return the register data of the Floodgate channels + */ + public static byte[] getFloodgateRegisterData() { + return FLOODGATE_REGISTER_DATA; } private static byte[] writeVarInt(int value) { 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 b74ecefad..530c551ab 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java +++ b/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java @@ -77,7 +77,7 @@ public class SkinProvider { public static String EARS_GEOMETRY; public static String EARS_GEOMETRY_SLIM; - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); static { /* Load in the normal ears geometry */ @@ -525,7 +525,6 @@ public class SkinProvider { outputStream.write((rgba >> 24) & 0xFF); } } - return outputStream.toByteArray(); } diff --git a/connector/src/main/java/org/geysermc/connector/utils/StatisticsUtils.java b/connector/src/main/java/org/geysermc/connector/utils/StatisticsUtils.java index 732217c24..41084504a 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/StatisticsUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/StatisticsUtils.java @@ -28,196 +28,170 @@ package org.geysermc.connector.utils; import com.github.steveice10.mc.protocol.data.MagicValues; import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType; import com.github.steveice10.mc.protocol.data.game.statistic.*; -import org.geysermc.common.window.SimpleFormWindow; -import org.geysermc.common.window.button.FormButton; -import org.geysermc.common.window.response.SimpleFormResponse; +import org.geysermc.common.form.SimpleForm; +import org.geysermc.common.form.response.SimpleFormResponse; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.item.ItemRegistry; import org.geysermc.connector.network.translators.world.block.BlockTranslator; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; -public class StatisticsUtils { //todo make Geyser compilable - - // Used in UpstreamPacketHandler.java - public static final int STATISTICS_MENU_FORM_ID = 1339; - public static final int STATISTICS_LIST_FORM_ID = 1340; +public class StatisticsUtils { + private static final Pattern CONTENT_PATTERN = Pattern.compile("^\\S+:", Pattern.MULTILINE); /** * Build a form for the given session with all statistic categories * * @param session The session to build the form for */ - public static SimpleFormWindow buildMenuForm(GeyserSession session) { + public static void buildAndSendStatisticsMenu(GeyserSession session) { // Cache the language for cleaner access - String language = session.getClientData().getLanguageCode(); + String language = session.getLocale(); - SimpleFormWindow window = new SimpleFormWindow(LocaleUtils.getLocaleString("gui.stats", language), ""); + session.sendForm( + SimpleForm.builder() + .translator(StatisticsUtils::translate, language) + .title("gui.stats") + .button("stat.generalButton") + .button("stat.itemsButton - stat_type.minecraft.mined") + .button("stat.itemsButton - stat_type.minecraft.broken") + .button("stat.itemsButton - stat_type.minecraft.crafted") + .button("stat.itemsButton - stat_type.minecraft.used") + .button("stat.itemsButton - stat_type.minecraft.picked_up") + .button("stat.itemsButton - stat_type.minecraft.dropped") + .button("stat.mobsButton - geyser.statistics.killed") + .button("stat.mobsButton - geyser.statistics.killed_by") + .responseHandler((form, responseData) -> { + SimpleFormResponse response = form.parseResponse(responseData); + if (!response.isCorrect()) { + return; + } - window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.generalButton", language))); + SimpleForm.Builder builder = + SimpleForm.builder() + .translator(StatisticsUtils::translate, language); - window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.mined", language))); - window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.broken", language))); - window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.crafted", language))); - window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.used", language))); - window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.picked_up", language))); - window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.dropped", language))); + StringBuilder content = new StringBuilder(); - window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.mobsButton", language) + " - " + LanguageUtils.getPlayerLocaleString("geyser.statistics.killed", language))); - window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.mobsButton", language) + " - " + LanguageUtils.getPlayerLocaleString("geyser.statistics.killed_by", language))); + switch (response.getClickedButtonId()) { + case 0: + builder.title("stat.generalButton"); - return window; - } + for (Map.Entry entry : session.getStatistics().entrySet()) { + if (entry.getKey() instanceof GenericStatistic) { + String statName = ((GenericStatistic) entry.getKey()).name().toLowerCase(); + content.append("stat.minecraft.").append(statName).append(": ").append(entry.getValue()).append("\n"); + } + } + break; + case 1: + builder.title("stat.itemsButton - stat_type.minecraft.mined"); - /** - * Handle the menu form response - * - * @param session The session that sent the response - * @param response The response string to parse - * @return True if the form was parsed correctly, false if not - */ - public static boolean handleMenuForm(GeyserSession session, String response) { - SimpleFormWindow menuForm = (SimpleFormWindow) session.getWindowCache().getWindows().get(STATISTICS_MENU_FORM_ID); - menuForm.setResponse(response); - SimpleFormResponse formResponse = (SimpleFormResponse) menuForm.getResponse(); + for (Map.Entry entry : session.getStatistics().entrySet()) { + if (entry.getKey() instanceof BreakBlockStatistic) { + String block = BlockTranslator.JAVA_ID_TO_JAVA_IDENTIFIER_MAP.get(((BreakBlockStatistic) entry.getKey()).getId()); + block = block.replace("minecraft:", "block.minecraft."); + content.append(block).append(": ").append(entry.getValue()).append("\n"); + } + } + break; + case 2: + builder.title("stat.itemsButton - stat_type.minecraft.broken"); - // Cache the language for cleaner access - String language = session.getClientData().getLanguageCode(); + for (Map.Entry entry : session.getStatistics().entrySet()) { + if (entry.getKey() instanceof BreakItemStatistic) { + String item = ItemRegistry.ITEM_ENTRIES.get(((BreakItemStatistic) entry.getKey()).getId()).getJavaIdentifier(); + content.append(getItemTranslateKey(item, language)).append(": ").append(entry.getValue()).append("\n"); + } + } + break; + case 3: + builder.title("stat.itemsButton - stat_type.minecraft.crafted"); - if (formResponse != null && formResponse.getClickedButton() != null) { - String title; - StringBuilder content = new StringBuilder(); + for (Map.Entry entry : session.getStatistics().entrySet()) { + if (entry.getKey() instanceof CraftItemStatistic) { + String item = ItemRegistry.ITEM_ENTRIES.get(((CraftItemStatistic) entry.getKey()).getId()).getJavaIdentifier(); + content.append(getItemTranslateKey(item, language)).append(": ").append(entry.getValue()).append("\n"); + } + } + break; + case 4: + builder.title("stat.itemsButton - stat_type.minecraft.used"); - switch (formResponse.getClickedButtonId()) { - case 0: - title = LocaleUtils.getLocaleString("stat.generalButton", language); + for (Map.Entry entry : session.getStatistics().entrySet()) { + if (entry.getKey() instanceof UseItemStatistic) { + String item = ItemRegistry.ITEM_ENTRIES.get(((UseItemStatistic) entry.getKey()).getId()).getJavaIdentifier(); + content.append(getItemTranslateKey(item, language)).append(": ").append(entry.getValue()).append("\n"); + } + } + break; + case 5: + builder.title("stat.itemsButton - stat_type.minecraft.picked_up"); - for (Map.Entry entry : session.getStatistics().entrySet()) { - if (entry.getKey() instanceof GenericStatistic) { - content.append(LocaleUtils.getLocaleString("stat.minecraft." + ((GenericStatistic) entry.getKey()).name().toLowerCase(), language) + ": " + entry.getValue() + "\n"); - } - } - break; - case 1: - title = LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.mined", language); + for (Map.Entry entry : session.getStatistics().entrySet()) { + if (entry.getKey() instanceof PickupItemStatistic) { + String item = ItemRegistry.ITEM_ENTRIES.get(((PickupItemStatistic) entry.getKey()).getId()).getJavaIdentifier(); + content.append(getItemTranslateKey(item, language)).append(": ").append(entry.getValue()).append("\n"); + } + } + break; + case 6: + builder.title("stat.itemsButton - stat_type.minecraft.dropped"); - for (Map.Entry entry : session.getStatistics().entrySet()) { - if (entry.getKey() instanceof BreakBlockStatistic) { - String block = BlockTranslator.JAVA_ID_TO_JAVA_IDENTIFIER_MAP.get(((BreakBlockStatistic) entry.getKey()).getId()); - block = block.replace("minecraft:", "block.minecraft."); - block = LocaleUtils.getLocaleString(block, language); - content.append(block + ": " + entry.getValue() + "\n"); - } - } - break; - case 2: - title = LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.broken", language); + for (Map.Entry entry : session.getStatistics().entrySet()) { + if (entry.getKey() instanceof DropItemStatistic) { + String item = ItemRegistry.ITEM_ENTRIES.get(((DropItemStatistic) entry.getKey()).getId()).getJavaIdentifier(); + content.append(getItemTranslateKey(item, language)).append(": ").append(entry.getValue()).append("\n"); + } + } + break; + case 7: + builder.title("stat.mobsButton - geyser.statistics.killed"); - for (Map.Entry entry : session.getStatistics().entrySet()) { - if (entry.getKey() instanceof BreakItemStatistic) { - String item = ItemRegistry.ITEM_ENTRIES.get(((BreakItemStatistic) entry.getKey()).getId()).getJavaIdentifier(); - content.append(getItemTranslateKey(item, language) + ": " + entry.getValue() + "\n"); - } - } - break; - case 3: - title = LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.crafted", language); + for (Map.Entry entry : session.getStatistics().entrySet()) { + if (entry.getKey() instanceof KillEntityStatistic) { + String entityName = MagicValues.key(EntityType.class, ((KillEntityStatistic) entry.getKey()).getId()).name().toLowerCase(); + content.append("entity.minecraft.").append(entityName).append(": ").append(entry.getValue()).append("\n"); + } + } + break; + case 8: + builder.title("stat.mobsButton - geyser.statistics.killed_by"); - for (Map.Entry entry : session.getStatistics().entrySet()) { - if (entry.getKey() instanceof CraftItemStatistic) { - String item = ItemRegistry.ITEM_ENTRIES.get(((CraftItemStatistic) entry.getKey()).getId()).getJavaIdentifier(); - content.append(getItemTranslateKey(item, language) + ": " + entry.getValue() + "\n"); - } - } - break; - case 4: - title = LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.used", language); + for (Map.Entry entry : session + .getStatistics().entrySet()) { + if (entry.getKey() instanceof KilledByEntityStatistic) { + String entityName = MagicValues.key(EntityType.class, ((KilledByEntityStatistic) entry.getKey()).getId()).name().toLowerCase(); + content.append("entity.minecraft.").append(entityName).append(": ").append(entry.getValue()).append("\n"); + } + } + break; + default: + return; + } - for (Map.Entry entry : session.getStatistics().entrySet()) { - if (entry.getKey() instanceof UseItemStatistic) { - String item = ItemRegistry.ITEM_ENTRIES.get(((UseItemStatistic) entry.getKey()).getId()).getJavaIdentifier(); - content.append(getItemTranslateKey(item, language) + ": " + entry.getValue() + "\n"); - } - } - break; - case 5: - title = LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.picked_up", language); + if (content.length() == 0) { + content = new StringBuilder("geyser.statistics.none"); + } - for (Map.Entry entry : session.getStatistics().entrySet()) { - if (entry.getKey() instanceof PickupItemStatistic) { - String item = ItemRegistry.ITEM_ENTRIES.get(((PickupItemStatistic) entry.getKey()).getId()).getJavaIdentifier(); - content.append(getItemTranslateKey(item, language) + ": " + entry.getValue() + "\n"); - } - } - break; - case 6: - title = LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.dropped", language); - - for (Map.Entry entry : session.getStatistics().entrySet()) { - if (entry.getKey() instanceof DropItemStatistic) { - String item = ItemRegistry.ITEM_ENTRIES.get(((DropItemStatistic) entry.getKey()).getId()).getJavaIdentifier(); - content.append(getItemTranslateKey(item, language) + ": " + entry.getValue() + "\n"); - } - } - break; - case 7: - title = LocaleUtils.getLocaleString("stat.mobsButton", language) + " - " + LanguageUtils.getPlayerLocaleString("geyser.statistics.killed", language); - - for (Map.Entry entry : session.getStatistics().entrySet()) { - if (entry.getKey() instanceof KillEntityStatistic) { - String mob = LocaleUtils.getLocaleString("entity.minecraft." + MagicValues.key(EntityType.class, ((KillEntityStatistic) entry.getKey()).getId()).name().toLowerCase(), language); - content.append(mob + ": " + entry.getValue() + "\n"); - } - } - break; - case 8: - title = LocaleUtils.getLocaleString("stat.mobsButton", language) + " - " + LanguageUtils.getPlayerLocaleString("geyser.statistics.killed_by", language); - - for (Map.Entry entry : session.getStatistics().entrySet()) { - if (entry.getKey() instanceof KilledByEntityStatistic) { - String mob = LocaleUtils.getLocaleString("entity.minecraft." + MagicValues.key(EntityType.class, ((KilledByEntityStatistic) entry.getKey()).getId()).name().toLowerCase(), language); - content.append(mob + ": " + entry.getValue() + "\n"); - } - } - break; - default: - return false; - } - - if (content.length() == 0) { - content = new StringBuilder(LanguageUtils.getPlayerLocaleString("geyser.statistics.none", language)); - } - - SimpleFormWindow window = new SimpleFormWindow(title, content.toString()); - window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("gui.back", language))); - session.sendForm(window, STATISTICS_LIST_FORM_ID); - } - - return true; - } - - /** - * Handle the list form response - * - * @param session The session that sent the response - * @param response The response string to parse - * @return True if the form was parsed correctly, false if not - */ - public static boolean handleListForm(GeyserSession session, String response) { - SimpleFormWindow listForm = (SimpleFormWindow) session.getWindowCache().getWindows().get(STATISTICS_LIST_FORM_ID); - listForm.setResponse(response); - - if (!listForm.isClosed()) { - session.sendForm(buildMenuForm(session), STATISTICS_MENU_FORM_ID); - } - - return true; + session.sendForm( + builder.content(content.toString()) + .button("gui.back") + .responseHandler((form1, responseData1) -> { + SimpleFormResponse response1 = form.parseResponse(responseData1); + if (response1.isCorrect()) { + buildAndSendStatisticsMenu(session); + } + })); + })); } /** * Finds the item translation key from the Java locale. - * - * @param item the namespaced item to search for. + * + * @param item the namespaced item to search for. * @param language the language to search in * @return the full name of the item */ @@ -230,4 +204,31 @@ public class StatisticsUtils { //todo make Geyser compilable } return translatedItem; } + + private static String translate(String keys, String locale) { + Matcher matcher = CONTENT_PATTERN.matcher(keys); + + StringBuffer buffer = new StringBuffer(); + while (matcher.find()) { + String group = matcher.group(); + matcher.appendReplacement(buffer, translateEntry(group.substring(0, group.length() - 1), locale) + ":"); + } + + if (buffer.length() != 0) { + return matcher.appendTail(buffer).toString(); + } + + String[] keySplitted = keys.split(" - "); + for (int i = 0; i < keySplitted.length; i++) { + keySplitted[i] = translateEntry(keySplitted[i], locale); + } + return String.join(" - ", keySplitted); + } + + private static String translateEntry(String key, String locale) { + if (key.startsWith("geyser.")) { + return LanguageUtils.getPlayerLocaleString(key, locale); + } + return LocaleUtils.getLocaleString(key, locale); + } }