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.
This commit is contained in:
Tim203 2020-11-18 19:38:49 +01:00
parent 819ff09ee6
commit 7e3a736f20
No known key found for this signature in database
GPG key ID: 064EE9F5BF7C3EE8
23 changed files with 324 additions and 261 deletions

View file

@ -18,11 +18,5 @@
<version>2.8.5</version> <version>2.8.5</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.9.8</version>
<scope>compile</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View file

@ -69,9 +69,9 @@ public class ModalForm extends Form {
} }
public static final class Builder extends Form.Builder<Builder, ModalForm> { public static final class Builder extends Form.Builder<Builder, ModalForm> {
private String content; private String content = "";
private String button1; private String button1 = "";
private String button2; private String button2 = "";
public Builder content(String content) { public Builder content(String content) {
this.content = translate(content); this.content = translate(content);

View file

@ -80,7 +80,7 @@ public final class SimpleForm extends Form {
public static final class Builder extends Form.Builder<Builder, SimpleForm> { public static final class Builder extends Form.Builder<Builder, SimpleForm> {
private final List<ButtonComponent> buttons = new ArrayList<>(); private final List<ButtonComponent> buttons = new ArrayList<>();
private String content; private String content = "";
public Builder content(String content) { public Builder content(String content) {
this.content = translate(content); this.content = translate(content);

View file

@ -30,14 +30,14 @@ import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
/** /**
* This class contains the raw data send by Geyser to Floodgate or from Floodgate to Floodgate. * This class contains the raw data send by Geyser to Floodgate or from Floodgate to Floodgate. This
* This class is only used internally, and you should look at FloodgatePlayer instead * class is only used internally, and you should look at FloodgatePlayer instead (FloodgatePlayer is
* (FloodgatePlayer is present in the common module in the Floodgate repo) * present in the API module of the Floodgate repo)
*/ */
@AllArgsConstructor(access = AccessLevel.PACKAGE)
@Getter @Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public final class BedrockData { 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 version;
private final String username; private final String username;
@ -48,22 +48,21 @@ public final class BedrockData {
private final int inputMode; private final int inputMode;
private final String ip; private final String ip;
private final LinkedPlayer linkedPlayer; private final LinkedPlayer linkedPlayer;
private final boolean fromProxy;
private final int dataLength; private final int dataLength;
public BedrockData(String version, String username, String xuid, int deviceOs, public static BedrockData of(String version, String username, String xuid, int deviceOs,
String languageCode, int uiProfile, int inputMode, String ip, String languageCode, int uiProfile, int inputMode, String ip,
LinkedPlayer linkedPlayer) { LinkedPlayer linkedPlayer, boolean fromProxy) {
this(version, username, xuid, deviceOs, languageCode, return new BedrockData(version, username, xuid, deviceOs, languageCode, inputMode,
inputMode, uiProfile, ip, linkedPlayer, EXPECTED_LENGTH); uiProfile, ip, linkedPlayer, fromProxy, EXPECTED_LENGTH);
} }
public BedrockData(String version, String username, String xuid, int deviceOs, public static BedrockData of(String version, String username, String xuid, int deviceOs,
String languageCode, int uiProfile, int inputMode, String ip) { String languageCode, int uiProfile, int inputMode, String ip) {
this(version, username, xuid, deviceOs, languageCode, uiProfile, inputMode, ip, null); return of(version, username, xuid, deviceOs, languageCode,
} uiProfile, inputMode, ip, null, false);
public boolean hasPlayerLink() {
return linkedPlayer != null;
} }
public static BedrockData fromString(String data) { public static BedrockData fromString(String data) {
@ -77,19 +76,23 @@ public final class BedrockData {
return new BedrockData( return new BedrockData(
split[0], split[1], split[2], Integer.parseInt(split[3]), split[4], split[0], split[1], split[2], Integer.parseInt(split[3]), split[4],
Integer.parseInt(split[5]), Integer.parseInt(split[6]), split[7], 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 @Override
public String toString() { public String toString() {
// The format is the same as the order of the fields in this class // The format is the same as the order of the fields in this class
return version + '\0' + username + '\0' + xuid + '\0' + deviceOs + '\0' + return version + '\0' + username + '\0' + xuid + '\0' + deviceOs + '\0' +
languageCode + '\0' + uiProfile + '\0' + inputMode + '\0' + ip + '\0' + languageCode + '\0' + uiProfile + '\0' + inputMode + '\0' + ip + '\0' +
(linkedPlayer != null ? linkedPlayer.toString() : "null"); fromProxy + '\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);
} }
} }

View file

@ -25,7 +25,6 @@
package org.geysermc.floodgate.util; package org.geysermc.floodgate.util;
import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -34,7 +33,6 @@ import lombok.RequiredArgsConstructor;
*/ */
@RequiredArgsConstructor(access = AccessLevel.PRIVATE) @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public enum DeviceOs { public enum DeviceOs {
@JsonEnumDefaultValue
UNKNOWN("Unknown"), UNKNOWN("Unknown"),
ANDROID("Android"), ANDROID("Android"),
IOS("iOS"), IOS("iOS"),
@ -57,6 +55,7 @@ public enum DeviceOs {
/** /**
* Get the DeviceOs instance from the identifier. * Get the DeviceOs instance from the identifier.
*
* @param id the DeviceOs identifier * @param id the DeviceOs identifier
* @return The DeviceOs or {@link #UNKNOWN} if the DeviceOs wasn't found * @return The DeviceOs or {@link #UNKNOWN} if the DeviceOs wasn't found
*/ */

View file

@ -26,10 +26,7 @@
package org.geysermc.floodgate.util; package org.geysermc.floodgate.util;
import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
public enum InputMode { public enum InputMode {
@JsonEnumDefaultValue
UNKNOWN, UNKNOWN,
KEYBOARD_MOUSE, KEYBOARD_MOUSE,
TOUCH, // I guess Touch? TOUCH, // I guess Touch?
@ -40,6 +37,7 @@ public enum InputMode {
/** /**
* Get the InputMode instance from the identifier. * Get the InputMode instance from the identifier.
*
* @param id the InputMode identifier * @param id the InputMode identifier
* @return The InputMode or {@link #UNKNOWN} if the DeviceOs wasn't found * @return The InputMode or {@link #UNKNOWN} if the DeviceOs wasn't found
*/ */

View file

@ -26,11 +26,14 @@
package org.geysermc.floodgate.util; package org.geysermc.floodgate.util;
import lombok.AccessLevel;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.UUID; import java.util.UUID;
@Getter @Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public final class LinkedPlayer { public final class LinkedPlayer {
/** /**
* The Java username of the linked player * The Java username of the linked player
@ -45,19 +48,17 @@ public final class LinkedPlayer {
*/ */
private final UUID bedrockId; private final UUID bedrockId;
/** /**
* If the LinkedPlayer is send from a different platform. * If the LinkedPlayer is send from a different platform. For example the LinkedPlayer is from
* For example the LinkedPlayer is from Bungee but the data has been sent to the Bukkit server. * Bungee but the data has been sent to the Bukkit server.
*/ */
private boolean fromDifferentPlatform = false; private boolean fromDifferentPlatform = false;
public LinkedPlayer(String javaUsername, UUID javaUniqueId, UUID bedrockId) { public static LinkedPlayer of(String javaUsername, UUID javaUniqueId, UUID bedrockId) {
this.javaUsername = javaUsername; return new LinkedPlayer(javaUsername, javaUniqueId, bedrockId);
this.javaUniqueId = javaUniqueId;
this.bedrockId = bedrockId;
} }
static LinkedPlayer fromString(String data) { static LinkedPlayer fromString(String data) {
if (data.length() != 3) { if (data.length() == 4) {
return null; return null;
} }

View file

@ -31,31 +31,36 @@ import lombok.ToString;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Base64; import java.util.Base64;
import static java.lang.String.format;
@AllArgsConstructor @AllArgsConstructor
@ToString @ToString
public final class RawSkin { public final class RawSkin {
public int width; public int width;
public int height; public int height;
public byte[] data; public byte[] data;
public boolean alex;
private RawSkin() {} private RawSkin() {
}
public static RawSkin decode(byte[] data) throws InvalidFormatException { public static RawSkin decode(byte[] data) throws InvalidFormatException {
if (data == null) { if (data == null) {
return 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 the RawSkin is longer then the max Java Edition skin length
if (data.length > maxEncodedLength) { if (data.length > maxEncodedLength) {
throw new InvalidFormatException( throw new InvalidFormatException(format(
"Encoded data cannot be longer then " + maxEncodedLength + " bytes!" "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 the encoded data doesn't even contain the width, height (8 bytes, 2 ints) and isAlex
if (data.length < 4 * ((8 + 2) / 3)) { if (data.length < 4 * ((9 + 2) / 3)) {
throw new InvalidFormatException("Encoded data must be at least 12 bytes long!"); throw new InvalidFormatException("Encoded data must be at least 16 bytes long!");
} }
data = Base64.getDecoder().decode(data); data = Base64.getDecoder().decode(data);
@ -65,23 +70,25 @@ public final class RawSkin {
RawSkin skin = new RawSkin(); RawSkin skin = new RawSkin();
skin.width = buffer.getInt(); skin.width = buffer.getInt();
skin.height = buffer.getInt(); skin.height = buffer.getInt();
if (buffer.remaining() != (skin.width * skin.height * 4)) { if (buffer.remaining() - 1 != (skin.width * skin.height * 4)) {
throw new InvalidFormatException(String.format( throw new InvalidFormatException(format(
"Expected skin length to be %s, got %s", "Expected skin length to be %s, got %s",
(skin.width * skin.height * 4), buffer.remaining() (skin.width * skin.height * 4), buffer.remaining()
)); ));
} }
skin.data = new byte[buffer.remaining()]; skin.data = new byte[buffer.remaining() - 1];
buffer.get(skin.data); buffer.get(skin.data);
skin.alex = buffer.get() == 1;
return skin; return skin;
} }
public byte[] encode() { public byte[] encode() {
// 2 x int = 8 bytes // 2 x int + 1 = 9 bytes
ByteBuffer buffer = ByteBuffer.allocate(8 + data.length); ByteBuffer buffer = ByteBuffer.allocate(9 + data.length);
buffer.putInt(width); buffer.putInt(width);
buffer.putInt(height); buffer.putInt(height);
buffer.put(data); buffer.put(data);
buffer.put((byte) (alex ? 1 : 0));
return Base64.getEncoder().encode(buffer.array()); return Base64.getEncoder().encode(buffer.array());
} }
} }

View file

@ -26,17 +26,14 @@
package org.geysermc.floodgate.util; package org.geysermc.floodgate.util;
import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
public enum UiProfile { public enum UiProfile {
@JsonEnumDefaultValue CLASSIC, POCKET;
CLASSIC,
POCKET;
private static final UiProfile[] VALUES = values(); private static final UiProfile[] VALUES = values();
/** /**
* Get the UiProfile instance from the identifier. * Get the UiProfile instance from the identifier.
*
* @param id the UiProfile identifier * @param id the UiProfile identifier
* @return The UiProfile or {@link #CLASSIC} if the UiProfile wasn't found * @return The UiProfile or {@link #CLASSIC} if the UiProfile wasn't found
*/ */

View file

@ -229,7 +229,7 @@ public class GeyserConnector {
for (GeyserSession session : players) { for (GeyserSession session : players) {
if (session == null) continue; if (session == null) continue;
if (session.getClientData() == null) continue; if (session.getClientData() == null) continue;
String os = session.getClientData().getDeviceOS().toString(); String os = session.getClientData().getDeviceOs().toString();
if (!valueMap.containsKey(os)) { if (!valueMap.containsKey(os)) {
valueMap.put(os, 1); valueMap.put(os, 1);
} else { } else {

View file

@ -73,7 +73,7 @@ public class DumpInfo {
this.userPlatforms = new Object2IntOpenHashMap(); this.userPlatforms = new Object2IntOpenHashMap();
for (GeyserSession session : GeyserConnector.getInstance().getPlayers()) { for (GeyserSession session : GeyserConnector.getInstance().getPlayers()) {
DeviceOs device = session.getClientData().getDeviceOS(); DeviceOs device = session.getClientData().getDeviceOs();
userPlatforms.put(device, userPlatforms.getOrDefault(device, 0) + 1); userPlatforms.put(device, userPlatforms.getOrDefault(device, 0) + 1);
} }

View file

@ -67,7 +67,7 @@ public class FireworkEntity extends Entity {
// TODO: Remove once Mojang fixes bugs with fireworks crashing clients on these specific devices. // TODO: Remove once Mojang fixes bugs with fireworks crashing clients on these specific devices.
// https://bugs.mojang.com/browse/MCPE-89115 // 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; return;
} }

View file

@ -402,11 +402,11 @@ public class GeyserSession implements CommandSender {
try { try {
FloodgateCipher cipher = connector.getCipher(); FloodgateCipher cipher = connector.getCipher();
encryptedData = cipher.encryptFromString(new BedrockData( encryptedData = cipher.encryptFromString(BedrockData.of(
clientData.getGameVersion(), clientData.getGameVersion(),
authData.getName(), authData.getName(),
authData.getXboxUUID(), authData.getXboxUUID(),
clientData.getDeviceOS().ordinal(), clientData.getDeviceOs().ordinal(),
clientData.getLanguageCode(), clientData.getLanguageCode(),
clientData.getUiProfile().ordinal(), clientData.getUiProfile().ordinal(),
clientData.getCurrentInputMode().ordinal(), clientData.getCurrentInputMode().ordinal(),

View file

@ -29,6 +29,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.base.Charsets;
import lombok.Getter; import lombok.Getter;
import org.geysermc.connector.utils.SkinProvider; import org.geysermc.connector.utils.SkinProvider;
import org.geysermc.floodgate.util.DeviceOs; import org.geysermc.floodgate.util.DeviceOs;
@ -87,7 +88,7 @@ public final class BedrockClientData {
@JsonProperty(value = "DeviceModel") @JsonProperty(value = "DeviceModel")
private String deviceModel; private String deviceModel;
@JsonProperty(value = "DeviceOS") @JsonProperty(value = "DeviceOS")
private DeviceOs deviceOS; private DeviceOs deviceOs;
@JsonProperty(value = "UIProfile") @JsonProperty(value = "UIProfile")
private UiProfile uiProfile; private UiProfile uiProfile;
@JsonProperty(value = "GuiScale") @JsonProperty(value = "GuiScale")
@ -114,13 +115,7 @@ public final class BedrockClientData {
@JsonProperty(value = "ThirdPartyNameOnly") @JsonProperty(value = "ThirdPartyNameOnly")
private boolean thirdPartyNameOnly; private boolean thirdPartyNameOnly;
public void setJsonData(JsonNode data) { private static RawSkin getLegacyImage(byte[] imageData, boolean alex) {
if (this.jsonData == null && data != null) {
this.jsonData = data;
}
}
private static RawSkin getLegacyImage(byte[] imageData) {
if (imageData == null) { if (imageData == null) {
return null; return null;
} }
@ -128,43 +123,54 @@ public final class BedrockClientData {
// width * height * 4 (rgba) // width * height * 4 (rgba)
switch (imageData.length) { switch (imageData.length) {
case 8192: case 8192:
return new RawSkin(64, 32, imageData); return new RawSkin(64, 32, imageData, alex);
case 16384: case 16384:
return new RawSkin(64, 64, imageData); return new RawSkin(64, 64, imageData, alex);
case 32768: case 32768:
return new RawSkin(64, 128, imageData); return new RawSkin(64, 128, imageData, alex);
case 65536: case 65536:
return new RawSkin(128, 128, imageData); return new RawSkin(128, 128, imageData, alex);
default: default:
throw new IllegalArgumentException("Unknown legacy skin size"); 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<br> * Taken from https://github.com/NukkitX/Nukkit/blob/master/src/main/java/cn/nukkit/network/protocol/LoginPacket.java<br>
* Internally only used for Skins, but can be used for Capes too * Internally only used for Skins, but can be used for Capes too
*/ */
public RawSkin getImage(String name) { public RawSkin getImage(String name) {
System.out.println(jsonData.toString());
if (jsonData == null || !jsonData.has(name + "Data")) { if (jsonData == null || !jsonData.has(name + "Data")) {
return null; return null;
} }
boolean alex = false;
if (name.equals("Skin")) {
alex = isAlex();
}
byte[] image = Base64.getDecoder().decode(jsonData.get(name + "Data").asText()); byte[] image = Base64.getDecoder().decode(jsonData.get(name + "Data").asText());
if (jsonData.has(name + "ImageWidth") && jsonData.has(name + "ImageHeight")) { if (jsonData.has(name + "ImageWidth") && jsonData.has(name + "ImageHeight")) {
return new RawSkin( return new RawSkin(
jsonData.get(name + "ImageWidth").asInt(), jsonData.get(name + "ImageWidth").asInt(),
jsonData.get(name + "ImageHeight").asInt(), jsonData.get(name + "ImageHeight").asInt(),
image image, alex
); );
} }
return getLegacyImage(image); return getLegacyImage(image, alex);
} }
public RawSkin getAndTransformImage(String name) { public RawSkin getAndTransformImage(String name) {
RawSkin skin = getImage(name); RawSkin skin = getImage(name);
if (skin != null && (skin.width > 64 || skin.height > 64)) { 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); int max = Math.max(skin.width, skin.height);
while (max > 64) { while (max > 64) {
@ -179,4 +185,35 @@ public final class BedrockClientData {
} }
return skin; 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;
}
} }

View file

@ -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.GameRuleData;
import com.nukkitx.protocol.bedrock.data.PlayerPermission; import com.nukkitx.protocol.bedrock.data.PlayerPermission;
import com.nukkitx.protocol.bedrock.packet.*; import com.nukkitx.protocol.bedrock.packet.*;
import org.geysermc.connector.common.AuthType;
import org.geysermc.connector.entity.PlayerEntity; import org.geysermc.connector.entity.PlayerEntity;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.PacketTranslator;
@ -46,7 +47,6 @@ import java.util.List;
@Translator(packet = ServerJoinGamePacket.class) @Translator(packet = ServerJoinGamePacket.class)
public class JavaJoinGameTranslator extends PacketTranslator<ServerJoinGamePacket> { public class JavaJoinGameTranslator extends PacketTranslator<ServerJoinGamePacket> {
@Override @Override
public void translate(ServerJoinGamePacket packet, GeyserSession session) { public void translate(ServerJoinGamePacket packet, GeyserSession session) {
PlayerEntity entity = session.getPlayerEntity(); PlayerEntity entity = session.getPlayerEntity();
@ -96,6 +96,11 @@ public class JavaJoinGameTranslator extends PacketTranslator<ServerJoinGamePacke
session.sendDownstreamPacket(new ClientPluginMessagePacket("minecraft:brand", PluginMessageUtils.getGeyserBrandData())); session.sendDownstreamPacket(new ClientPluginMessagePacket("minecraft:brand", PluginMessageUtils.getGeyserBrandData()));
// register the plugin messaging channels used in Floodgate
if (session.getConnector().getAuthType() == AuthType.FLOODGATE) {
session.sendDownstreamPacket(new ClientPluginMessagePacket("minecraft:register", PluginMessageUtils.getFloodgateRegisterData()));
}
if (!newDimension.equals(entity.getDimension())) { if (!newDimension.equals(entity.getDimension())) {
DimensionUtils.switchDimension(session, newDimension); DimensionUtils.switchDimension(session, newDimension);
} }

View file

@ -33,14 +33,13 @@ import org.geysermc.connector.utils.StatisticsUtils;
@Translator(packet = ServerStatisticsPacket.class) @Translator(packet = ServerStatisticsPacket.class)
public class JavaStatisticsTranslator extends PacketTranslator<ServerStatisticsPacket> { public class JavaStatisticsTranslator extends PacketTranslator<ServerStatisticsPacket> {
@Override @Override
public void translate(ServerStatisticsPacket packet, GeyserSession session) { public void translate(ServerStatisticsPacket packet, GeyserSession session) {
session.updateStatistics(packet.getStatistics()); session.updateStatistics(packet.getStatistics());
if (session.isWaitingForStatistics()) { if (session.isWaitingForStatistics()) {
session.setWaitingForStatistics(false); session.setWaitingForStatistics(false);
session.sendForm(StatisticsUtils.buildMenuForm(session), StatisticsUtils.STATISTICS_MENU_FORM_ID); StatisticsUtils.buildAndSendStatisticsMenu(session);
} }
} }
} }

View file

@ -60,7 +60,9 @@ public class LanguageUtils {
public static void loadGeyserLocale(String locale) { public static void loadGeyserLocale(String locale) {
locale = formatLocale(locale); locale = formatLocale(locale);
// Don't load the locale if it's already loaded. // 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"); 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 // Try and get the key from the default locale
if (formatString == null) { if (formatString == null) {
properties = LOCALE_MAPPINGS.get(formatLocale(getDefaultLocale())); properties = LOCALE_MAPPINGS.get(getDefaultLocale());
formatString = properties.getProperty(key); formatString = properties.getProperty(key);
} }
@ -121,7 +123,7 @@ public class LanguageUtils {
// Final fallback // Final fallback
if (formatString == null) { if (formatString == null) {
formatString = key; return key;
} }
return MessageFormat.format(formatString.replace("'", "''").replace("&", "\u00a7"), values); return MessageFormat.format(formatString.replace("'", "''").replace("&", "\u00a7"), values);
@ -147,7 +149,10 @@ public class LanguageUtils {
* @return the current default locale * @return the current default locale
*/ */
public static String getDefaultLocale() { 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; String locale;
boolean isValid = true; boolean isValid = true;
if (GeyserConnector.getInstance() != null && if (GeyserConnector.getInstance() != null &&

View file

@ -35,10 +35,8 @@ import com.nukkitx.protocol.bedrock.packet.LoginPacket;
import com.nukkitx.protocol.bedrock.packet.ServerToClientHandshakePacket; import com.nukkitx.protocol.bedrock.packet.ServerToClientHandshakePacket;
import com.nukkitx.protocol.bedrock.util.EncryptionUtils; import com.nukkitx.protocol.bedrock.util.EncryptionUtils;
import org.geysermc.common.form.CustomForm; import org.geysermc.common.form.CustomForm;
import org.geysermc.common.form.ModalForm;
import org.geysermc.common.form.SimpleForm; import org.geysermc.common.form.SimpleForm;
import org.geysermc.common.form.response.CustomFormResponse; import org.geysermc.common.form.response.CustomFormResponse;
import org.geysermc.common.form.response.ModalFormResponse;
import org.geysermc.common.form.response.SimpleFormResponse; import org.geysermc.common.form.response.SimpleFormResponse;
import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;

View file

@ -25,27 +25,47 @@
package org.geysermc.connector.utils; package org.geysermc.connector.utils;
import com.google.common.base.Charsets;
import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.GeyserConnector;
import java.nio.charset.StandardCharsets; import java.nio.ByteBuffer;
public class PluginMessageUtils { public class PluginMessageUtils {
private static final byte[] BRAND_DATA; private static final byte[] GEYSER_BRAND_DATA;
private static final byte[] FLOODGATE_REGISTER_DATA;
static { static {
byte[] data = GeyserConnector.NAME.getBytes(StandardCharsets.UTF_8); byte[] data = GeyserConnector.NAME.getBytes(Charsets.UTF_8);
byte[] varInt = writeVarInt(data.length); GEYSER_BRAND_DATA =
BRAND_DATA = new byte[varInt.length + data.length]; ByteBuffer.allocate(data.length + getVarIntLength(data.length))
System.arraycopy(varInt, 0, BRAND_DATA, 0, varInt.length); .put(writeVarInt(data.length))
System.arraycopy(data, 0, BRAND_DATA, varInt.length, 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 * Get the prebuilt brand as a byte array
*
* @return the brand information of the Geyser client * @return the brand information of the Geyser client
*/ */
public static byte[] getGeyserBrandData() { 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) { private static byte[] writeVarInt(int value) {

View file

@ -77,7 +77,7 @@ public class SkinProvider {
public static String EARS_GEOMETRY; public static String EARS_GEOMETRY;
public static String EARS_GEOMETRY_SLIM; public static String EARS_GEOMETRY_SLIM;
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
static { static {
/* Load in the normal ears geometry */ /* Load in the normal ears geometry */
@ -525,7 +525,6 @@ public class SkinProvider {
outputStream.write((rgba >> 24) & 0xFF); outputStream.write((rgba >> 24) & 0xFF);
} }
} }
return outputStream.toByteArray(); return outputStream.toByteArray();
} }

View file

@ -28,190 +28,164 @@ package org.geysermc.connector.utils;
import com.github.steveice10.mc.protocol.data.MagicValues; 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.entity.type.EntityType;
import com.github.steveice10.mc.protocol.data.game.statistic.*; import com.github.steveice10.mc.protocol.data.game.statistic.*;
import org.geysermc.common.window.SimpleFormWindow; import org.geysermc.common.form.SimpleForm;
import org.geysermc.common.window.button.FormButton; import org.geysermc.common.form.response.SimpleFormResponse;
import org.geysermc.common.window.response.SimpleFormResponse;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.item.ItemRegistry; import org.geysermc.connector.network.translators.item.ItemRegistry;
import org.geysermc.connector.network.translators.world.block.BlockTranslator; import org.geysermc.connector.network.translators.world.block.BlockTranslator;
import java.util.Map; import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class StatisticsUtils { //todo make Geyser compilable public class StatisticsUtils {
private static final Pattern CONTENT_PATTERN = Pattern.compile("^\\S+:", Pattern.MULTILINE);
// Used in UpstreamPacketHandler.java
public static final int STATISTICS_MENU_FORM_ID = 1339;
public static final int STATISTICS_LIST_FORM_ID = 1340;
/** /**
* Build a form for the given session with all statistic categories * Build a form for the given session with all statistic categories
* *
* @param session The session to build the form for * @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 // 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()
window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.generalButton", language))); .translator(StatisticsUtils::translate, language)
.title("gui.stats")
window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.mined", language))); .button("stat.generalButton")
window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.broken", language))); .button("stat.itemsButton - stat_type.minecraft.mined")
window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.crafted", language))); .button("stat.itemsButton - stat_type.minecraft.broken")
window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.used", language))); .button("stat.itemsButton - stat_type.minecraft.crafted")
window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.picked_up", language))); .button("stat.itemsButton - stat_type.minecraft.used")
window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.dropped", language))); .button("stat.itemsButton - stat_type.minecraft.picked_up")
.button("stat.itemsButton - stat_type.minecraft.dropped")
window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.mobsButton", language) + " - " + LanguageUtils.getPlayerLocaleString("geyser.statistics.killed", language))); .button("stat.mobsButton - geyser.statistics.killed")
window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.mobsButton", language) + " - " + LanguageUtils.getPlayerLocaleString("geyser.statistics.killed_by", language))); .button("stat.mobsButton - geyser.statistics.killed_by")
.responseHandler((form, responseData) -> {
return window; SimpleFormResponse response = form.parseResponse(responseData);
if (!response.isCorrect()) {
return;
} }
/** SimpleForm.Builder builder =
* Handle the menu form response SimpleForm.builder()
* .translator(StatisticsUtils::translate, language);
* @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();
// Cache the language for cleaner access
String language = session.getClientData().getLanguageCode();
if (formResponse != null && formResponse.getClickedButton() != null) {
String title;
StringBuilder content = new StringBuilder(); StringBuilder content = new StringBuilder();
switch (formResponse.getClickedButtonId()) { switch (response.getClickedButtonId()) {
case 0: case 0:
title = LocaleUtils.getLocaleString("stat.generalButton", language); builder.title("stat.generalButton");
for (Map.Entry<Statistic, Integer> entry : session.getStatistics().entrySet()) { for (Map.Entry<Statistic, Integer> entry : session.getStatistics().entrySet()) {
if (entry.getKey() instanceof GenericStatistic) { if (entry.getKey() instanceof GenericStatistic) {
content.append(LocaleUtils.getLocaleString("stat.minecraft." + ((GenericStatistic) entry.getKey()).name().toLowerCase(), language) + ": " + entry.getValue() + "\n"); String statName = ((GenericStatistic) entry.getKey()).name().toLowerCase();
content.append("stat.minecraft.").append(statName).append(": ").append(entry.getValue()).append("\n");
} }
} }
break; break;
case 1: case 1:
title = LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.mined", language); builder.title("stat.itemsButton - stat_type.minecraft.mined");
for (Map.Entry<Statistic, Integer> entry : session.getStatistics().entrySet()) { for (Map.Entry<Statistic, Integer> entry : session.getStatistics().entrySet()) {
if (entry.getKey() instanceof BreakBlockStatistic) { if (entry.getKey() instanceof BreakBlockStatistic) {
String block = BlockTranslator.JAVA_ID_TO_JAVA_IDENTIFIER_MAP.get(((BreakBlockStatistic) entry.getKey()).getId()); String block = BlockTranslator.JAVA_ID_TO_JAVA_IDENTIFIER_MAP.get(((BreakBlockStatistic) entry.getKey()).getId());
block = block.replace("minecraft:", "block.minecraft."); block = block.replace("minecraft:", "block.minecraft.");
block = LocaleUtils.getLocaleString(block, language); content.append(block).append(": ").append(entry.getValue()).append("\n");
content.append(block + ": " + entry.getValue() + "\n");
} }
} }
break; break;
case 2: case 2:
title = LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.broken", language); builder.title("stat.itemsButton - stat_type.minecraft.broken");
for (Map.Entry<Statistic, Integer> entry : session.getStatistics().entrySet()) { for (Map.Entry<Statistic, Integer> entry : session.getStatistics().entrySet()) {
if (entry.getKey() instanceof BreakItemStatistic) { if (entry.getKey() instanceof BreakItemStatistic) {
String item = ItemRegistry.ITEM_ENTRIES.get(((BreakItemStatistic) entry.getKey()).getId()).getJavaIdentifier(); String item = ItemRegistry.ITEM_ENTRIES.get(((BreakItemStatistic) entry.getKey()).getId()).getJavaIdentifier();
content.append(getItemTranslateKey(item, language) + ": " + entry.getValue() + "\n"); content.append(getItemTranslateKey(item, language)).append(": ").append(entry.getValue()).append("\n");
} }
} }
break; break;
case 3: case 3:
title = LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.crafted", language); builder.title("stat.itemsButton - stat_type.minecraft.crafted");
for (Map.Entry<Statistic, Integer> entry : session.getStatistics().entrySet()) { for (Map.Entry<Statistic, Integer> entry : session.getStatistics().entrySet()) {
if (entry.getKey() instanceof CraftItemStatistic) { if (entry.getKey() instanceof CraftItemStatistic) {
String item = ItemRegistry.ITEM_ENTRIES.get(((CraftItemStatistic) entry.getKey()).getId()).getJavaIdentifier(); String item = ItemRegistry.ITEM_ENTRIES.get(((CraftItemStatistic) entry.getKey()).getId()).getJavaIdentifier();
content.append(getItemTranslateKey(item, language) + ": " + entry.getValue() + "\n"); content.append(getItemTranslateKey(item, language)).append(": ").append(entry.getValue()).append("\n");
} }
} }
break; break;
case 4: case 4:
title = LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.used", language); builder.title("stat.itemsButton - stat_type.minecraft.used");
for (Map.Entry<Statistic, Integer> entry : session.getStatistics().entrySet()) { for (Map.Entry<Statistic, Integer> entry : session.getStatistics().entrySet()) {
if (entry.getKey() instanceof UseItemStatistic) { if (entry.getKey() instanceof UseItemStatistic) {
String item = ItemRegistry.ITEM_ENTRIES.get(((UseItemStatistic) entry.getKey()).getId()).getJavaIdentifier(); String item = ItemRegistry.ITEM_ENTRIES.get(((UseItemStatistic) entry.getKey()).getId()).getJavaIdentifier();
content.append(getItemTranslateKey(item, language) + ": " + entry.getValue() + "\n"); content.append(getItemTranslateKey(item, language)).append(": ").append(entry.getValue()).append("\n");
} }
} }
break; break;
case 5: case 5:
title = LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.picked_up", language); builder.title("stat.itemsButton - stat_type.minecraft.picked_up");
for (Map.Entry<Statistic, Integer> entry : session.getStatistics().entrySet()) { for (Map.Entry<Statistic, Integer> entry : session.getStatistics().entrySet()) {
if (entry.getKey() instanceof PickupItemStatistic) { if (entry.getKey() instanceof PickupItemStatistic) {
String item = ItemRegistry.ITEM_ENTRIES.get(((PickupItemStatistic) entry.getKey()).getId()).getJavaIdentifier(); String item = ItemRegistry.ITEM_ENTRIES.get(((PickupItemStatistic) entry.getKey()).getId()).getJavaIdentifier();
content.append(getItemTranslateKey(item, language) + ": " + entry.getValue() + "\n"); content.append(getItemTranslateKey(item, language)).append(": ").append(entry.getValue()).append("\n");
} }
} }
break; break;
case 6: case 6:
title = LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.dropped", language); builder.title("stat.itemsButton - stat_type.minecraft.dropped");
for (Map.Entry<Statistic, Integer> entry : session.getStatistics().entrySet()) { for (Map.Entry<Statistic, Integer> entry : session.getStatistics().entrySet()) {
if (entry.getKey() instanceof DropItemStatistic) { if (entry.getKey() instanceof DropItemStatistic) {
String item = ItemRegistry.ITEM_ENTRIES.get(((DropItemStatistic) entry.getKey()).getId()).getJavaIdentifier(); String item = ItemRegistry.ITEM_ENTRIES.get(((DropItemStatistic) entry.getKey()).getId()).getJavaIdentifier();
content.append(getItemTranslateKey(item, language) + ": " + entry.getValue() + "\n"); content.append(getItemTranslateKey(item, language)).append(": ").append(entry.getValue()).append("\n");
} }
} }
break; break;
case 7: case 7:
title = LocaleUtils.getLocaleString("stat.mobsButton", language) + " - " + LanguageUtils.getPlayerLocaleString("geyser.statistics.killed", language); builder.title("stat.mobsButton - geyser.statistics.killed");
for (Map.Entry<Statistic, Integer> entry : session.getStatistics().entrySet()) { for (Map.Entry<Statistic, Integer> entry : session.getStatistics().entrySet()) {
if (entry.getKey() instanceof KillEntityStatistic) { if (entry.getKey() instanceof KillEntityStatistic) {
String mob = LocaleUtils.getLocaleString("entity.minecraft." + MagicValues.key(EntityType.class, ((KillEntityStatistic) entry.getKey()).getId()).name().toLowerCase(), language); String entityName = MagicValues.key(EntityType.class, ((KillEntityStatistic) entry.getKey()).getId()).name().toLowerCase();
content.append(mob + ": " + entry.getValue() + "\n"); content.append("entity.minecraft.").append(entityName).append(": ").append(entry.getValue()).append("\n");
} }
} }
break; break;
case 8: case 8:
title = LocaleUtils.getLocaleString("stat.mobsButton", language) + " - " + LanguageUtils.getPlayerLocaleString("geyser.statistics.killed_by", language); builder.title("stat.mobsButton - geyser.statistics.killed_by");
for (Map.Entry<Statistic, Integer> entry : session.getStatistics().entrySet()) { for (Map.Entry<Statistic, Integer> entry : session
.getStatistics().entrySet()) {
if (entry.getKey() instanceof KilledByEntityStatistic) { if (entry.getKey() instanceof KilledByEntityStatistic) {
String mob = LocaleUtils.getLocaleString("entity.minecraft." + MagicValues.key(EntityType.class, ((KilledByEntityStatistic) entry.getKey()).getId()).name().toLowerCase(), language); String entityName = MagicValues.key(EntityType.class, ((KilledByEntityStatistic) entry.getKey()).getId()).name().toLowerCase();
content.append(mob + ": " + entry.getValue() + "\n"); content.append("entity.minecraft.").append(entityName).append(": ").append(entry.getValue()).append("\n");
} }
} }
break; break;
default: default:
return false; return;
} }
if (content.length() == 0) { if (content.length() == 0) {
content = new StringBuilder(LanguageUtils.getPlayerLocaleString("geyser.statistics.none", language)); content = new StringBuilder("geyser.statistics.none");
} }
SimpleFormWindow window = new SimpleFormWindow(title, content.toString()); session.sendForm(
window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("gui.back", language))); builder.content(content.toString())
session.sendForm(window, STATISTICS_LIST_FORM_ID); .button("gui.back")
.responseHandler((form1, responseData1) -> {
SimpleFormResponse response1 = form.parseResponse(responseData1);
if (response1.isCorrect()) {
buildAndSendStatisticsMenu(session);
} }
}));
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;
} }
/** /**
@ -230,4 +204,31 @@ public class StatisticsUtils { //todo make Geyser compilable
} }
return translatedItem; 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);
}
} }