mirror of
https://github.com/GeyserMC/Geyser.git
synced 2024-08-14 23:57:35 +00:00
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:
parent
819ff09ee6
commit
7e3a736f20
23 changed files with 324 additions and 261 deletions
|
@ -18,11 +18,5 @@
|
|||
<version>2.8.5</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
<version>2.9.8</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
|
@ -69,9 +69,9 @@ public class ModalForm extends Form {
|
|||
}
|
||||
|
||||
public static final class Builder extends Form.Builder<Builder, ModalForm> {
|
||||
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);
|
||||
|
|
|
@ -80,7 +80,7 @@ public final class SimpleForm extends Form {
|
|||
|
||||
public static final class Builder extends Form.Builder<Builder, SimpleForm> {
|
||||
private final List<ButtonComponent> buttons = new ArrayList<>();
|
||||
private String content;
|
||||
private String content = "";
|
||||
|
||||
public Builder content(String content) {
|
||||
this.content = translate(content);
|
||||
|
|
|
@ -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,
|
||||
public static BedrockData of(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);
|
||||
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,
|
||||
public static BedrockData of(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;
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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<br>
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ServerJoinGamePacket> {
|
||||
|
||||
@Override
|
||||
public void translate(ServerJoinGamePacket packet, GeyserSession session) {
|
||||
PlayerEntity entity = session.getPlayerEntity();
|
||||
|
@ -96,6 +96,11 @@ public class JavaJoinGameTranslator extends PacketTranslator<ServerJoinGamePacke
|
|||
|
||||
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())) {
|
||||
DimensionUtils.switchDimension(session, newDimension);
|
||||
}
|
||||
|
|
|
@ -33,14 +33,13 @@ import org.geysermc.connector.utils.StatisticsUtils;
|
|||
|
||||
@Translator(packet = ServerStatisticsPacket.class)
|
||||
public class JavaStatisticsTranslator extends PacketTranslator<ServerStatisticsPacket> {
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -28,190 +28,164 @@ 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), "");
|
||||
|
||||
window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.generalButton", 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)));
|
||||
|
||||
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)));
|
||||
|
||||
return window;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
SimpleForm.Builder builder =
|
||||
SimpleForm.builder()
|
||||
.translator(StatisticsUtils::translate, language);
|
||||
|
||||
// Cache the language for cleaner access
|
||||
String language = session.getClientData().getLanguageCode();
|
||||
|
||||
if (formResponse != null && formResponse.getClickedButton() != null) {
|
||||
String title;
|
||||
StringBuilder content = new StringBuilder();
|
||||
|
||||
switch (formResponse.getClickedButtonId()) {
|
||||
switch (response.getClickedButtonId()) {
|
||||
case 0:
|
||||
title = LocaleUtils.getLocaleString("stat.generalButton", language);
|
||||
builder.title("stat.generalButton");
|
||||
|
||||
for (Map.Entry<Statistic, Integer> entry : session.getStatistics().entrySet()) {
|
||||
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;
|
||||
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()) {
|
||||
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");
|
||||
content.append(block).append(": ").append(entry.getValue()).append("\n");
|
||||
}
|
||||
}
|
||||
break;
|
||||
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()) {
|
||||
if (entry.getKey() instanceof BreakItemStatistic) {
|
||||
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;
|
||||
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()) {
|
||||
if (entry.getKey() instanceof CraftItemStatistic) {
|
||||
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;
|
||||
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()) {
|
||||
if (entry.getKey() instanceof UseItemStatistic) {
|
||||
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;
|
||||
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()) {
|
||||
if (entry.getKey() instanceof PickupItemStatistic) {
|
||||
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;
|
||||
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()) {
|
||||
if (entry.getKey() instanceof DropItemStatistic) {
|
||||
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;
|
||||
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()) {
|
||||
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");
|
||||
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:
|
||||
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) {
|
||||
String mob = LocaleUtils.getLocaleString("entity.minecraft." + MagicValues.key(EntityType.class, ((KilledByEntityStatistic) entry.getKey()).getId()).name().toLowerCase(), language);
|
||||
content.append(mob + ": " + entry.getValue() + "\n");
|
||||
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 false;
|
||||
return;
|
||||
}
|
||||
|
||||
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());
|
||||
window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("gui.back", language)));
|
||||
session.sendForm(window, STATISTICS_LIST_FORM_ID);
|
||||
session.sendForm(
|
||||
builder.content(content.toString())
|
||||
.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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue