From fb283fcce8600eca01c777d0df94217e978f1413 Mon Sep 17 00:00:00 2001 From: YHDiamond <47502993+YHDiamond@users.noreply.github.com> Date: Mon, 11 Jan 2021 21:23:09 -0500 Subject: [PATCH 1/7] Add advancements GUI (#1579) Using /geyser advancements, Bedrock clients can get a visual on their progress. Co-authored-by: yehudahrrs <47502993+yehudahrrs@users.noreply.github.com> Co-authored-by: Olivia Co-authored-by: rtm516 Co-authored-by: DoctorMacc Co-authored-by: rtm516 Co-authored-by: Camotoy <20743703+Camotoy@users.noreply.github.com> --- .../connector/command/CommandManager.java | 1 + .../command/defaults/AdvancementsCommand.java | 74 ++++ .../network/UpstreamPacketHandler.java | 28 +- .../network/session/GeyserSession.java | 3 + .../session/cache/AdvancementsCache.java | 321 ++++++++++++++++++ .../network/session/cache/WindowCache.java | 4 +- .../java/JavaAdvancementsTabTranslator.java | 45 +++ .../java/JavaAdvancementsTranslator.java | 103 ++++++ .../connector/utils/GeyserAdvancement.java | 89 +++++ connector/src/main/resources/languages | 2 +- 10 files changed, 654 insertions(+), 16 deletions(-) create mode 100644 connector/src/main/java/org/geysermc/connector/command/defaults/AdvancementsCommand.java create mode 100644 connector/src/main/java/org/geysermc/connector/network/session/cache/AdvancementsCache.java create mode 100644 connector/src/main/java/org/geysermc/connector/network/translators/java/JavaAdvancementsTabTranslator.java create mode 100644 connector/src/main/java/org/geysermc/connector/network/translators/java/JavaAdvancementsTranslator.java create mode 100644 connector/src/main/java/org/geysermc/connector/utils/GeyserAdvancement.java diff --git a/connector/src/main/java/org/geysermc/connector/command/CommandManager.java b/connector/src/main/java/org/geysermc/connector/command/CommandManager.java index 469d613c7..d31983eb4 100644 --- a/connector/src/main/java/org/geysermc/connector/command/CommandManager.java +++ b/connector/src/main/java/org/geysermc/connector/command/CommandManager.java @@ -52,6 +52,7 @@ public abstract class CommandManager { registerCommand(new VersionCommand(connector, "version", "geyser.commands.version.desc", "geyser.command.version")); registerCommand(new SettingsCommand(connector, "settings", "geyser.commands.settings.desc", "geyser.command.settings")); registerCommand(new StatisticsCommand(connector, "statistics", "geyser.commands.statistics.desc", "geyser.command.statistics")); + registerCommand(new AdvancementsCommand(connector, "advancements", "geyser.commands.advancements.desc", "geyser.command.advancements")); } public void registerCommand(GeyserCommand command) { diff --git a/connector/src/main/java/org/geysermc/connector/command/defaults/AdvancementsCommand.java b/connector/src/main/java/org/geysermc/connector/command/defaults/AdvancementsCommand.java new file mode 100644 index 000000000..3067f3d53 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/command/defaults/AdvancementsCommand.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2019-2021 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.connector.command.defaults; + +import org.geysermc.common.window.SimpleFormWindow; +import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.command.CommandSender; +import org.geysermc.connector.command.GeyserCommand; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.session.cache.AdvancementsCache; + +public class AdvancementsCommand extends GeyserCommand { + + private final GeyserConnector connector; + + public AdvancementsCommand(GeyserConnector connector, String name, String description, String permission) { + super(name, description, permission); + + this.connector = connector; + } + + @Override + public void execute(CommandSender sender, String[] args) { + if (sender.isConsole()) { + return; + } + + // Make sure the sender is a Bedrock edition client + GeyserSession session = null; + if (sender instanceof GeyserSession) { + session = (GeyserSession) sender; + } else { + // Needed for Spigot - sender is not an instance of GeyserSession + for (GeyserSession otherSession : connector.getPlayers()) { + if (sender.getName().equals(otherSession.getPlayerEntity().getUsername())) { + session = otherSession; + break; + } + } + } + if (session == null) return; + + SimpleFormWindow window = session.getAdvancementsCache().buildMenuForm(); + session.sendForm(window, AdvancementsCache.ADVANCEMENTS_MENU_FORM_ID); + } + + @Override + public boolean isExecutableOnConsole() { + return false; + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java b/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java index 3922a95ff..7ebfaeda5 100644 --- a/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java +++ b/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java @@ -33,14 +33,9 @@ import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.common.AuthType; import org.geysermc.connector.configuration.GeyserConfiguration; import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.session.cache.AdvancementsCache; import org.geysermc.connector.network.translators.PacketTranslatorRegistry; -import org.geysermc.connector.utils.LanguageUtils; -import org.geysermc.connector.utils.LoginEncryptionUtils; -import org.geysermc.connector.utils.MathUtils; -import org.geysermc.connector.utils.ResourcePack; -import org.geysermc.connector.utils.ResourcePackManifest; -import org.geysermc.connector.utils.SettingsUtils; -import org.geysermc.connector.utils.StatisticsUtils; +import org.geysermc.connector.utils.*; import java.io.FileInputStream; import java.io.InputStream; @@ -144,12 +139,19 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { @Override public boolean handle(ModalFormResponsePacket packet) { - if (packet.getFormId() == SettingsUtils.SETTINGS_FORM_ID) { - return SettingsUtils.handleSettingsForm(session, packet.getFormData()); - } else if (packet.getFormId() == StatisticsUtils.STATISTICS_MENU_FORM_ID) { - return StatisticsUtils.handleMenuForm(session, packet.getFormData()); - } else if (packet.getFormId() == StatisticsUtils.STATISTICS_LIST_FORM_ID) { - return StatisticsUtils.handleListForm(session, packet.getFormData()); + switch (packet.getFormId()) { + case AdvancementsCache.ADVANCEMENT_INFO_FORM_ID: + return session.getAdvancementsCache().handleInfoForm(packet.getFormData()); + case AdvancementsCache.ADVANCEMENTS_LIST_FORM_ID: + return session.getAdvancementsCache().handleListForm(packet.getFormData()); + case AdvancementsCache.ADVANCEMENTS_MENU_FORM_ID: + return session.getAdvancementsCache().handleMenuForm(packet.getFormData()); + case SettingsUtils.SETTINGS_FORM_ID: + return SettingsUtils.handleSettingsForm(session, packet.getFormData()); + case StatisticsUtils.STATISTICS_LIST_FORM_ID: + return StatisticsUtils.handleListForm(session, packet.getFormData()); + case StatisticsUtils.STATISTICS_MENU_FORM_ID: + return StatisticsUtils.handleMenuForm(session, packet.getFormData()); } return LoginEncryptionUtils.authenticateFromForm(session, connector, packet.getFormId(), packet.getFormData()); diff --git a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java index a82ea061e..5b43fec04 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java @@ -121,6 +121,7 @@ public class GeyserSession implements CommandSender { private final SessionPlayerEntity playerEntity; private PlayerInventory inventory; + private AdvancementsCache advancementsCache; private BookEditCache bookEditCache; private ChunkCache chunkCache; private EntityCache entityCache; @@ -350,6 +351,7 @@ public class GeyserSession implements CommandSender { this.connector = connector; this.upstream = new UpstreamSession(bedrockServerSession); + this.advancementsCache = new AdvancementsCache(this); this.bookEditCache = new BookEditCache(this); this.chunkCache = new ChunkCache(this); this.entityCache = new EntityCache(this); @@ -684,6 +686,7 @@ public class GeyserSession implements CommandSender { tickThread.cancel(true); } + this.advancementsCache = null; this.bookEditCache = null; this.chunkCache = null; this.entityCache = null; diff --git a/connector/src/main/java/org/geysermc/connector/network/session/cache/AdvancementsCache.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/AdvancementsCache.java new file mode 100644 index 000000000..369967acc --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/AdvancementsCache.java @@ -0,0 +1,321 @@ +/* + * Copyright (c) 2019-2021 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.connector.network.session.cache; + +import com.github.steveice10.mc.protocol.data.game.advancement.Advancement; +import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientAdvancementTabPacket; +import lombok.Getter; +import lombok.Setter; +import org.geysermc.common.window.SimpleFormWindow; +import org.geysermc.common.window.button.FormButton; +import org.geysermc.common.window.response.SimpleFormResponse; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.chat.MessageTranslator; +import org.geysermc.connector.utils.GeyserAdvancement; +import org.geysermc.connector.utils.LanguageUtils; +import org.geysermc.connector.utils.LocaleUtils; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class AdvancementsCache { + + // Different form IDs + public static final int ADVANCEMENTS_MENU_FORM_ID = 1341; + public static final int ADVANCEMENTS_LIST_FORM_ID = 1342; + public static final int ADVANCEMENT_INFO_FORM_ID = 1343; + + /** + * Stores the player's advancement progress + */ + @Getter + private final Map> storedAdvancementProgress = new HashMap<>(); + + /** + * Stores advancements for the player. + */ + @Getter + private final Map storedAdvancements = new HashMap<>(); + + /** + * Stores player's chosen advancement's ID and title for use in form creators. + */ + @Setter + private String currentAdvancementCategoryId = null; + + private final GeyserSession session; + + public AdvancementsCache(GeyserSession session) { + this.session = session; + } + + /** + * Build a form with all advancement categories + * + * @return The built advancement category menu + */ + public SimpleFormWindow buildMenuForm() { + // Cache the language for cleaner access + String language = session.getClientData().getLanguageCode(); + + // Created menu window for advancement categories + SimpleFormWindow window = new SimpleFormWindow(LocaleUtils.getLocaleString("gui.advancements", language), ""); + for (Map.Entry advancement : storedAdvancements.entrySet()) { + if (advancement.getValue().getParentId() == null) { // No parent means this is a root advancement + window.getButtons().add(new FormButton(MessageTranslator.convertMessage(advancement.getValue().getDisplayData().getTitle(), language))); + } + } + + if (window.getButtons().isEmpty()) { + window.setContent(LocaleUtils.getLocaleString("advancements.empty", language)); + } + + return window; + } + + /** + * Builds the list of advancements + * + * @return The built list form + */ + public SimpleFormWindow buildListForm() { + // Cache the language for easier access + String language = session.getLocale(); + String id = currentAdvancementCategoryId; + GeyserAdvancement categoryAdvancement = storedAdvancements.get(currentAdvancementCategoryId); + + // Create the window + SimpleFormWindow window = new SimpleFormWindow(MessageTranslator.convertMessage(categoryAdvancement.getDisplayData().getTitle(), language), + MessageTranslator.convertMessage(categoryAdvancement.getDisplayData().getDescription(), language)); + + if (id != null) { + for (Map.Entry advancementEntry : storedAdvancements.entrySet()) { + GeyserAdvancement advancement = advancementEntry.getValue(); + if (advancement != null) { + if (advancement.getParentId() != null && currentAdvancementCategoryId.equals(advancement.getRootId(this))) { + boolean earned = isEarned(advancement); + + if (earned || !advancement.getDisplayData().isShowToast()) { + window.getButtons().add(new FormButton("§6" + MessageTranslator.convertMessage(advancementEntry.getValue().getDisplayData().getTitle()) + "\n")); + } else { + window.getButtons().add(new FormButton(MessageTranslator.convertMessage(advancementEntry.getValue().getDisplayData().getTitle()) + "\n")); + } + } + } + } + } + + window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("gui.back", language))); + + return window; + } + + /** + * Builds the advancement display info based on the chosen category + * + * @param advancement The advancement used to create the info display + * @return The information for the chosen advancement + */ + public SimpleFormWindow buildInfoForm(GeyserAdvancement advancement) { + // Cache language for easier access + String language = session.getLocale(); + + String earned = isEarned(advancement) ? "yes" : "no"; + + String description = getColorFromAdvancementFrameType(advancement) + MessageTranslator.convertMessage(advancement.getDisplayData().getDescription(), language); + String earnedString = LanguageUtils.getPlayerLocaleString("geyser.advancements.earned", language, LocaleUtils.getLocaleString("gui." + earned, language)); + + /* + Layout will look like: + + (Form title) Stone Age + + (Description) Mine stone with your new pickaxe + + Earned: Yes + Parent Advancement: Minecraft // If relevant + */ + + String content = description + "\n\n§f" + + earnedString + "\n"; + if (!currentAdvancementCategoryId.equals(advancement.getParentId())) { + // Only display the parent if it is not the category + content += LanguageUtils.getPlayerLocaleString("geyser.advancements.parentid", language, MessageTranslator.convertMessage(storedAdvancements.get(advancement.getParentId()).getDisplayData().getTitle(), language)); + } + SimpleFormWindow window = new SimpleFormWindow(MessageTranslator.convertMessage(advancement.getDisplayData().getTitle()), content); + window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("gui.back", language))); + + return window; + } + + /** + * Determine if this advancement has been earned. + * + * @param advancement the advancement to determine + * @return true if the advancement has been earned. + */ + public boolean isEarned(GeyserAdvancement advancement) { + boolean earned = false; + if (advancement.getRequirements().size() == 0) { + // Minecraft handles this case, so we better as well + return false; + } + Map progress = storedAdvancementProgress.get(advancement.getId()); + if (progress != null) { + // Each advancement's requirement must be fulfilled + // For example, [[zombie, blaze, skeleton]] means that one of those three categories must be achieved + // But [[zombie], [blaze], [skeleton]] means that all three requirements must be completed + for (List requirements : advancement.getRequirements()) { + boolean requirementsDone = false; + for (String requirement : requirements) { + Long obtained = progress.get(requirement); + // -1 means that this particular component required for completing the advancement + // has yet to be fulfilled + if (obtained != null && !obtained.equals(-1L)) { + requirementsDone = true; + break; + } + } + if (!requirementsDone) { + return false; + } + } + earned = true; + } + return earned; + } + + /** + * Handle the menu form response + * + * @param response The response string to parse + * @return True if the form was parsed correctly, false if not + */ + public boolean handleMenuForm(String response) { + SimpleFormWindow menuForm = (SimpleFormWindow) session.getWindowCache().getWindows().get(ADVANCEMENTS_MENU_FORM_ID); + menuForm.setResponse(response); + + SimpleFormResponse formResponse = (SimpleFormResponse) menuForm.getResponse(); + + String id = ""; + if (formResponse != null && formResponse.getClickedButton() != null) { + int advancementIndex = 0; + for (Map.Entry advancement : storedAdvancements.entrySet()) { + if (advancement.getValue().getParentId() == null) { // Root advancement + if (advancementIndex == formResponse.getClickedButtonId()) { + id = advancement.getKey(); + break; + } else { + advancementIndex++; + } + } + } + } + if (!id.equals("")) { + if (id.equals(currentAdvancementCategoryId)) { + // The server thinks we are already on this tab + session.sendForm(buildListForm(), ADVANCEMENTS_LIST_FORM_ID); + } else { + // Send a packet indicating that we intend to open this particular advancement window + ClientAdvancementTabPacket packet = new ClientAdvancementTabPacket(id); + session.sendDownstreamPacket(packet); + // Wait for a response there + } + } + + return true; + } + + /** + * Handle the list form response (Advancement category choice) + * + * @param response The response string to parse + * @return True if the form was parsed correctly, false if not + */ + public boolean handleListForm(String response) { + SimpleFormWindow listForm = (SimpleFormWindow) session.getWindowCache().getWindows().get(ADVANCEMENTS_LIST_FORM_ID); + listForm.setResponse(response); + + SimpleFormResponse formResponse = (SimpleFormResponse) listForm.getResponse(); + + if (!listForm.isClosed() && formResponse != null && formResponse.getClickedButton() != null) { + GeyserAdvancement advancement = null; + int advancementIndex = 0; + // Loop around to find the advancement that the client pressed + for (GeyserAdvancement advancementEntry : storedAdvancements.values()) { + if (advancementEntry.getParentId() != null && + currentAdvancementCategoryId.equals(advancementEntry.getRootId(this))) { + if (advancementIndex == formResponse.getClickedButtonId()) { + advancement = advancementEntry; + break; + } else { + advancementIndex++; + } + } + } + if (advancement != null) { + session.sendForm(buildInfoForm(advancement), ADVANCEMENT_INFO_FORM_ID); + } else { + session.sendForm(buildMenuForm(), ADVANCEMENTS_MENU_FORM_ID); + // Indicate that we have closed the current advancement tab + session.sendDownstreamPacket(new ClientAdvancementTabPacket()); + } + } else { + // Indicate that we have closed the current advancement tab + session.sendDownstreamPacket(new ClientAdvancementTabPacket()); + } + + return true; + } + + /** + * Handle the info form response + * + * @param response The response string to parse + * @return True if the form was parsed correctly, false if not + */ + public boolean handleInfoForm(String response) { + SimpleFormWindow listForm = (SimpleFormWindow) session.getWindowCache().getWindows().get(ADVANCEMENT_INFO_FORM_ID); + listForm.setResponse(response); + + SimpleFormResponse formResponse = (SimpleFormResponse) listForm.getResponse(); + + if (!listForm.isClosed() && formResponse != null && formResponse.getClickedButton() != null) { + session.sendForm(buildListForm(), ADVANCEMENTS_LIST_FORM_ID); + } + + return true; + } + + public String getColorFromAdvancementFrameType(GeyserAdvancement advancement) { + String base = "\u00a7"; + if (advancement.getDisplayData().getFrameType() == Advancement.DisplayData.FrameType.CHALLENGE) { + return base + "5"; + } + return base + "a"; // Used for types TASK and GOAL + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/session/cache/WindowCache.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/WindowCache.java index d9625ff67..a114b8bbc 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/cache/WindowCache.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/WindowCache.java @@ -37,10 +37,10 @@ import org.geysermc.connector.network.session.GeyserSession; public class WindowCache { - private GeyserSession session; + private final GeyserSession session; @Getter - private Int2ObjectMap windows = new Int2ObjectOpenHashMap<>(); + private final Int2ObjectMap windows = new Int2ObjectOpenHashMap<>(); public WindowCache(GeyserSession session) { this.session = session; diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaAdvancementsTabTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaAdvancementsTabTranslator.java new file mode 100644 index 000000000..17a3b3792 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaAdvancementsTabTranslator.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019-2021 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.connector.network.translators.java; + +import com.github.steveice10.mc.protocol.packet.ingame.server.ServerAdvancementTabPacket; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.session.cache.AdvancementsCache; +import org.geysermc.connector.network.translators.PacketTranslator; +import org.geysermc.connector.network.translators.Translator; + +/** + * Indicates that the client should open a particular advancement tab + */ +@Translator(packet = ServerAdvancementTabPacket.class) +public class JavaAdvancementsTabTranslator extends PacketTranslator { + + @Override + public void translate(ServerAdvancementTabPacket packet, GeyserSession session) { + session.getAdvancementsCache().setCurrentAdvancementCategoryId(packet.getTabId()); + session.sendForm(session.getAdvancementsCache().buildListForm(), AdvancementsCache.ADVANCEMENTS_LIST_FORM_ID); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaAdvancementsTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaAdvancementsTranslator.java new file mode 100644 index 000000000..714578e9a --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaAdvancementsTranslator.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2019-2021 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.connector.network.translators.java; + +import com.github.steveice10.mc.protocol.data.game.advancement.Advancement; +import com.github.steveice10.mc.protocol.packet.ingame.server.ServerAdvancementsPacket; +import com.nukkitx.protocol.bedrock.packet.SetTitlePacket; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.PacketTranslator; +import org.geysermc.connector.network.translators.Translator; +import org.geysermc.connector.network.translators.chat.MessageTranslator; +import org.geysermc.connector.network.session.cache.AdvancementsCache; +import org.geysermc.connector.utils.GeyserAdvancement; +import org.geysermc.connector.utils.LocaleUtils; + +import java.util.Map; + +@Translator(packet = ServerAdvancementsPacket.class) +public class JavaAdvancementsTranslator extends PacketTranslator { + + @Override + public void translate(ServerAdvancementsPacket packet, GeyserSession session) { + AdvancementsCache advancementsCache = session.getAdvancementsCache(); + if (packet.isReset()) { + advancementsCache.getStoredAdvancements().clear(); + advancementsCache.getStoredAdvancementProgress().clear(); + } + + // Removes removed advancements from player's stored advancements + for (String removedAdvancement : packet.getRemovedAdvancements()) { + advancementsCache.getStoredAdvancements().remove(removedAdvancement); + } + + advancementsCache.getStoredAdvancementProgress().putAll(packet.getProgress()); + + sendToolbarAdvancementUpdates(session, packet); + + // Adds advancements to the player's stored advancements when advancements are sent + for (Advancement advancement : packet.getAdvancements()) { + if (advancement.getDisplayData() != null && !advancement.getDisplayData().isHidden()) { + GeyserAdvancement geyserAdvancement = GeyserAdvancement.from(advancement); + advancementsCache.getStoredAdvancements().put(advancement.getId(), geyserAdvancement); + } else { + advancementsCache.getStoredAdvancements().remove(advancement.getId()); + } + } + } + + /** + * Handle all advancements progress updates + */ + public void sendToolbarAdvancementUpdates(GeyserSession session, ServerAdvancementsPacket packet) { + if (packet.isReset()) { + // Advancements are being cleared, so they can't be granted + return; + } + for (Map.Entry> progress : packet.getProgress().entrySet()) { + GeyserAdvancement advancement = session.getAdvancementsCache().getStoredAdvancements().get(progress.getKey()); + if (advancement != null && advancement.getDisplayData() != null) { + if (session.getAdvancementsCache().isEarned(advancement)) { + // Java uses some pink color for toast challenge completes + String color = advancement.getDisplayData().getFrameType() == Advancement.DisplayData.FrameType.CHALLENGE ? + "§d" : "§a"; + String advancementName = MessageTranslator.convertMessage(advancement.getDisplayData().getTitle(), session.getLocale()); + + // Send an action bar message stating they earned an achievement + // Sent for instances where broadcasting advancements through chat are disabled + SetTitlePacket titlePacket = new SetTitlePacket(); + titlePacket.setText(color + "[" + LocaleUtils.getLocaleString("advancements.toast." + + advancement.getDisplayData().getFrameType().toString().toLowerCase(), session.getLocale()) + "]§f " + advancementName); + titlePacket.setType(SetTitlePacket.Type.ACTIONBAR); + titlePacket.setFadeOutTime(3); + titlePacket.setFadeInTime(3); + titlePacket.setStayTime(3); + session.sendUpstreamPacket(titlePacket); + } + } + } + } +} diff --git a/connector/src/main/java/org/geysermc/connector/utils/GeyserAdvancement.java b/connector/src/main/java/org/geysermc/connector/utils/GeyserAdvancement.java new file mode 100644 index 000000000..31560498a --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/utils/GeyserAdvancement.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2019-2021 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.connector.utils; + +import com.github.steveice10.mc.protocol.data.game.advancement.Advancement; +import lombok.NonNull; +import org.geysermc.connector.network.session.cache.AdvancementsCache; + +import java.util.List; + +/** + * A wrapper around MCProtocolLib's {@link Advancement} class so we can control the parent of an advancement + */ +public class GeyserAdvancement { + private final Advancement advancement; + private String rootId = null; + + public static GeyserAdvancement from(Advancement advancement) { + return new GeyserAdvancement(advancement); + } + + private GeyserAdvancement(Advancement advancement) { + this.advancement = advancement; + } + + @NonNull + public String getId() { + return this.advancement.getId(); + } + + @NonNull + public List getCriteria() { + return this.advancement.getCriteria(); + } + + @NonNull + public List> getRequirements() { + return this.advancement.getRequirements(); + } + + public String getParentId() { + return this.advancement.getParentId(); + } + + public Advancement.DisplayData getDisplayData() { + return this.advancement.getDisplayData(); + } + + public String getRootId(AdvancementsCache advancementsCache) { + if (rootId == null) { + if (this.advancement.getParentId() == null) { + // We are the root ID + this.rootId = this.advancement.getId(); + } else { + // Go through our cache, and descend until we find the root ID + GeyserAdvancement advancement = advancementsCache.getStoredAdvancements().get(this.advancement.getParentId()); + if (advancement.getParentId() == null) { + this.rootId = advancement.getId(); + } else { + this.rootId = advancement.getRootId(advancementsCache); + } + } + } + return rootId; + } +} diff --git a/connector/src/main/resources/languages b/connector/src/main/resources/languages index 6f246c24d..8141bc6ae 160000 --- a/connector/src/main/resources/languages +++ b/connector/src/main/resources/languages @@ -1 +1 @@ -Subproject commit 6f246c24ddbd543a359d651e706da470fe53ceeb +Subproject commit 8141bc6aed878a95ed9ee3ca83a2381f9906c4b4 From af405f320a2669fcf4ddb3bc15eac956878d9e4a Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Tue, 12 Jan 2021 14:42:53 -0500 Subject: [PATCH 2/7] Prevent CME when adding players' emotes (#1831) --- .../org/geysermc/connector/network/session/GeyserSession.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java index 5b43fec04..104e72cd3 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java @@ -370,7 +370,9 @@ public class GeyserSession implements CommandSender { this.inventoryCache.getInventories().put(0, inventory); - connector.getPlayers().forEach(player -> this.emotes.addAll(player.getEmotes())); + // Make a copy to prevent ConcurrentModificationException + final List tmpPlayers = new ArrayList<>(connector.getPlayers()); + tmpPlayers.forEach(player -> this.emotes.addAll(player.getEmotes())); bedrockServerSession.addDisconnectHandler(disconnectReason -> { connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.disconnect", bedrockServerSession.getAddress().getAddress(), disconnectReason)); From dd0b4bafe8f76dfdc7fd8a3dd6d593db922b9994 Mon Sep 17 00:00:00 2001 From: Extollite <42713788+Extollite@users.noreply.github.com> Date: Tue, 12 Jan 2021 21:06:48 +0100 Subject: [PATCH 3/7] Close locale streams (#1832) * Close locale streams * Fix formatting --- .../main/java/org/geysermc/connector/utils/FileUtils.java | 4 ++-- .../main/java/org/geysermc/connector/utils/LocaleUtils.java | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java b/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java index 862af548d..d1dd6fd78 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java @@ -217,8 +217,8 @@ public class FileUtils { * @return The byte array of the file */ public static byte[] readAllBytes(File file) { - try { - return readAllBytes(new FileInputStream(file)); + try (InputStream inputStream = new FileInputStream(file)) { + return readAllBytes(inputStream); } catch (IOException e) { throw new RuntimeException("Cannot read " + file); } diff --git a/connector/src/main/java/org/geysermc/connector/utils/LocaleUtils.java b/connector/src/main/java/org/geysermc/connector/utils/LocaleUtils.java index e180682d6..8619eaf0d 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/LocaleUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/LocaleUtils.java @@ -208,6 +208,12 @@ public class LocaleUtils { // Insert the locale into the mappings LOCALE_MAPPINGS.put(locale.toLowerCase(), langMap); + + try { + localeStream.close(); + } catch (IOException e) { + throw new AssertionError(LanguageUtils.getLocaleStringLog("geyser.locale.fail.file", locale, e.getMessage())); + } } else { GeyserConnector.getInstance().getLogger().warning(LanguageUtils.getLocaleStringLog("geyser.locale.fail.missing", locale)); } From c0a64659c24a137c2d606ce2369de91bb446fc7b Mon Sep 17 00:00:00 2001 From: Extollite <42713788+Extollite@users.noreply.github.com> Date: Tue, 12 Jan 2021 21:41:15 +0100 Subject: [PATCH 4/7] Close en_us.hash stream (#1833) --- .../main/java/org/geysermc/connector/utils/LocaleUtils.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/connector/src/main/java/org/geysermc/connector/utils/LocaleUtils.java b/connector/src/main/java/org/geysermc/connector/utils/LocaleUtils.java index 8619eaf0d..f2ec43bf6 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/LocaleUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/LocaleUtils.java @@ -142,8 +142,9 @@ public class LocaleUtils { try { File hashFile = GeyserConnector.getInstance().getBootstrap().getConfigFolder().resolve("locales/en_us.hash").toFile(); if (hashFile.exists()) { - BufferedReader br = new BufferedReader(new FileReader(hashFile)); - curHash = br.readLine().trim(); + try (BufferedReader br = new BufferedReader(new FileReader(hashFile))) { + curHash = br.readLine().trim(); + } } } catch (IOException ignored) { } targetHash = clientJarInfo.getSha1(); From 6fdfcfb35c46c7dd5a707c8413e98ce3058fe635 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Wed, 13 Jan 2021 14:28:39 -0500 Subject: [PATCH 5/7] Clarify that we do support 1.16.201 (#1836) --- .../org/geysermc/connector/network/BedrockProtocol.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/connector/src/main/java/org/geysermc/connector/network/BedrockProtocol.java b/connector/src/main/java/org/geysermc/connector/network/BedrockProtocol.java index fbc7849fb..d24cea328 100644 --- a/connector/src/main/java/org/geysermc/connector/network/BedrockProtocol.java +++ b/connector/src/main/java/org/geysermc/connector/network/BedrockProtocol.java @@ -40,7 +40,9 @@ public class BedrockProtocol { * Default Bedrock codec that should act as a fallback. Should represent the latest available * release of the game that Geyser supports. */ - public static final BedrockPacketCodec DEFAULT_BEDROCK_CODEC = Bedrock_v422.V422_CODEC; + public static final BedrockPacketCodec DEFAULT_BEDROCK_CODEC = Bedrock_v422.V422_CODEC.toBuilder() + .minecraftVersion("1.16.201") + .build(); /** * A list of all supported Bedrock versions that can join Geyser */ @@ -50,7 +52,9 @@ public class BedrockProtocol { SUPPORTED_BEDROCK_CODECS.add(Bedrock_v419.V419_CODEC.toBuilder() .minecraftVersion("1.16.100/1.16.101") // We change this as 1.16.100.60 is a beta .build()); - SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC); + SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC.toBuilder() + .minecraftVersion("1.16.200/1.16.201") + .build()); } /** From 8e5a6efa042b52c08748fdf6a1f65fd55f6ac140 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 13 Jan 2021 12:29:34 -0700 Subject: [PATCH 6/7] Update TODO inventory-blocks: grindstone (#1827) Re: https://github.com/GeyserMC/Geyser/issues/1087 https://github.com/GeyserMC/Geyser/issues/1670 This is seemingly not yet fixed on 1.2.0-SNAPSHOT-561 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1d1657edc..ad001d63a 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ Take a look [here](https://github.com/GeyserMC/Geyser/wiki#Setup) for how to set - Horse Inventory - Loom - Smithing Table + - Grindstone ## What can't be fixed The following things can't be fixed because of Bedrock limitations. They might be fixable in the future, but not as of now. From 16fdf51d760cd05382dcf8c9ca5c3c3282443d9b Mon Sep 17 00:00:00 2001 From: rtm516 Date: Thu, 14 Jan 2021 00:15:00 +0000 Subject: [PATCH 7/7] Disable build notifications for sub-builds and build GeyserConnect (#1838) Co-authored-by: EasyClifton <63668444+EasyClifton@users.noreply.github.com> --- Jenkinsfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 501491361..6564bd1f9 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -92,8 +92,9 @@ pipeline { success { script { if (env.BRANCH_NAME == 'master') { - build propagate: false, wait: false, job: 'GeyserMC/Geyser-Fabric/java-1.16' - build propagate: false, wait: false, job: 'GeyserMC/GeyserAndroid/master' + build propagate: false, wait: false, job: 'GeyserMC/Geyser-Fabric/java-1.16', parameters: [booleanParam(name: 'SKIP_DISCORD', value: true)] + build propagate: false, wait: false, job: 'GeyserMC/GeyserAndroid/master', parameters: [booleanParam(name: 'SKIP_DISCORD', value: true)] + build propagate: false, wait: false, job: 'GeyserMC/GeyserConnect/master', parameters: [booleanParam(name: 'SKIP_DISCORD', value: true)] } } }