diff --git a/core/src/main/java/org/geysermc/geyser/codeofconduct/CodeOfConductManager.java b/core/src/main/java/org/geysermc/geyser/codeofconduct/CodeOfConductManager.java new file mode 100644 index 000000000..f5c10d1f9 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/codeofconduct/CodeOfConductManager.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2025 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.geyser.codeofconduct; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.session.GeyserSession; + +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class CodeOfConductManager { + private static final Path SAVE_PATH = Path.of("cache/codeofconducts.json"); + private static CodeOfConductManager loaded = null; + + private final ExecutorService saveService = Executors.newFixedThreadPool(1); + private final Object2IntMap playerAcceptedCodeOfConducts = new Object2IntOpenHashMap<>(); + + private CodeOfConductManager() {} + + public boolean hasAcceptedCodeOfConduct(GeyserSession session, String codeOfConduct) { + return playerAcceptedCodeOfConducts.getInt(session.xuid()) == codeOfConduct.hashCode(); + } + + public void saveCodeOfConduct(GeyserSession session, String codeOfConduct) { + playerAcceptedCodeOfConducts.put(session.xuid(), codeOfConduct.hashCode()); + CompletableFuture.runAsync(this::save, saveService); + } + + private void save() { + JsonObject saved = new JsonObject(); + playerAcceptedCodeOfConducts.forEach(saved::addProperty); + Path path = GeyserImpl.getInstance().configDirectory().resolve(SAVE_PATH); + try { + Files.writeString(path, saved.toString()); + } catch (IOException exception) { + GeyserImpl.getInstance().getLogger().error("Failed to write code of conduct cache!", exception); + } + } + + // TODO load at startup + public static CodeOfConductManager getInstance() { + if (loaded != null) { + return loaded; + } + + CodeOfConductManager manager = new CodeOfConductManager(); + Path path = GeyserImpl.getInstance().configDirectory().resolve(SAVE_PATH); + if (Files.exists(path) && Files.isRegularFile(path)) { + try (Reader reader = new FileReader(path.toFile())) { + JsonObject object = JsonParser.parseReader(reader).getAsJsonObject(); + for (Map.Entry entry : object.entrySet()) { + manager.playerAcceptedCodeOfConducts.put(entry.getKey(), entry.getValue().getAsInt()); + } + } catch (IOException exception) { + GeyserImpl.getInstance().getLogger().error("Failed to read code of conduct cache!", exception); + } + } + + loaded = manager; + return loaded; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index ea291a2ee..b900db84e 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -221,6 +221,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.statistic.CustomStatistic; import org.geysermc.mcprotocollib.protocol.data.game.statistic.Statistic; import org.geysermc.mcprotocollib.protocol.data.handshake.HandshakeIntent; import org.geysermc.mcprotocollib.protocol.packet.common.serverbound.ServerboundClientInformationPacket; +import org.geysermc.mcprotocollib.protocol.packet.configuration.serverbound.ServerboundAcceptCodeOfConductPacket; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.ServerboundChatCommandSignedPacket; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.ServerboundChatPacket; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerAbilitiesPacket; @@ -745,6 +746,9 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { @Setter private boolean allowVibrantVisuals = true; + @Accessors(fluent = true) + private boolean hasAcceptedCodeOfConduct = false; + public GeyserSession(GeyserImpl geyser, BedrockServerSession bedrockServerSession, EventLoop tickEventLoop) { this.geyser = geyser; this.upstream = new UpstreamSession(bedrockServerSession); @@ -1696,6 +1700,23 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { return true; } + public void acceptCodeOfConduct() { + if (hasAcceptedCodeOfConduct) { + return; + } + hasAcceptedCodeOfConduct = true; + sendDownstreamConfigurationPacket(new ServerboundAcceptCodeOfConductPacket(null)); // TODO fix in MCPL + } + + public void prepareForConfigurationForm() { + if (!sentSpawnPacket) { + connect(); + } + // Disable time progression whilst the form is open + // Once logged into the game this is set correctly when receiving a time packet from the server + setDaylightCycle(false); + } + public @NonNull PlayerInventory getPlayerInventory() { return this.playerInventoryHolder.inventory(); } @@ -1895,6 +1916,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { sendDownstreamPacket(packet, ProtocolState.GAME); } + public void sendDownstreamConfigurationPacket(Packet packet) { + sendDownstreamPacket(packet, ProtocolState.CONFIGURATION); + } + /** * Send a packet to the remote server if in the login state. * diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCodeOfConductTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCodeOfConductTranslator.java new file mode 100644 index 000000000..5c8e2e394 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCodeOfConductTranslator.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2025 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.geyser.translator.protocol.java; + +import org.geysermc.cumulus.form.CustomForm; +import org.geysermc.geyser.codeofconduct.CodeOfConductManager; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.protocol.PacketTranslator; +import org.geysermc.geyser.translator.protocol.Translator; +import org.geysermc.mcprotocollib.protocol.packet.configuration.clientbound.ClientboundCodeOfConductPacket; + +@Translator(packet = ClientboundCodeOfConductPacket.class) +public class JavaCodeOfConductTranslator extends PacketTranslator { + + @Override + public void translate(GeyserSession session, ClientboundCodeOfConductPacket packet) { + if (session.hasAcceptedCodeOfConduct()) { + return; + } else if (CodeOfConductManager.getInstance().hasAcceptedCodeOfConduct(session, packet.getCodeOfConduct())) { + session.acceptCodeOfConduct(); + return; + } + showCodeOfConductForm(session, packet.getCodeOfConduct()); + } + + private static void showCodeOfConductForm(GeyserSession session, String codeOfConduct) { + session.prepareForConfigurationForm(); + session.sendForm(CustomForm.builder() + .title("Server Code of Conduct") // TODO translate + .label(codeOfConduct) + .toggle("Do not notify again for this Code of Conduct") // TODO translate + .validResultHandler(response -> { + if (response.asToggle()) { + CodeOfConductManager.getInstance().saveCodeOfConduct(session, codeOfConduct); + } + session.acceptCodeOfConduct(); + }) + .closedResultHandler(() -> session.disconnect("Rejected code of conduct")) // TODO geyser translate + ); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/dialogues/JavaShowDialogueConfigurationTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/dialogues/JavaShowDialogueConfigurationTranslator.java index 526394420..6802c0c27 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/dialogues/JavaShowDialogueConfigurationTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/dialogues/JavaShowDialogueConfigurationTranslator.java @@ -36,13 +36,7 @@ public class JavaShowDialogueConfigurationTranslator extends PacketTranslator