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