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>
<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>

View file

@ -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);

View file

@ -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);

View file

@ -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");
}
}

View file

@ -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
*/

View file

@ -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
*/

View file

@ -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;
}

View file

@ -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());
}
}

View file

@ -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
*/

View file

@ -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 {

View file

@ -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);
}

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.
// 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;
}

View file

@ -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(),

View file

@ -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;
}
}

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.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);
}

View file

@ -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);
}
}
}

View file

@ -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 &&

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.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;

View file

@ -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) {

View file

@ -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();
}

View file

@ -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<Statistic, Integer> 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<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.");
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<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)).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<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)).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<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)).append(": ").append(entry.getValue()).append("\n");
}
}
break;
case 5:
builder.title("stat.itemsButton - stat_type.minecraft.picked_up");
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");
}
}
break;
case 1:
title = LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.mined", language);
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)).append(": ").append(entry.getValue()).append("\n");
}
}
break;
case 6:
builder.title("stat.itemsButton - stat_type.minecraft.dropped");
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");
}
}
break;
case 2:
title = LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.broken", language);
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)).append(": ").append(entry.getValue()).append("\n");
}
}
break;
case 7:
builder.title("stat.mobsButton - geyser.statistics.killed");
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");
}
}
break;
case 3:
title = LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.crafted", language);
for (Map.Entry<Statistic, Integer> 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<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");
}
}
break;
case 4:
title = LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.used", language);
for (Map.Entry<Statistic, Integer> 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<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");
}
}
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<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");
}
}
break;
case 6:
title = LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.dropped", language);
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");
}
}
break;
case 7:
title = LocaleUtils.getLocaleString("stat.mobsButton", language) + " - " + LanguageUtils.getPlayerLocaleString("geyser.statistics.killed", language);
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");
}
}
break;
case 8:
title = LocaleUtils.getLocaleString("stat.mobsButton", language) + " - " + LanguageUtils.getPlayerLocaleString("geyser.statistics.killed_by", language);
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");
}
}
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);
}
}