diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 326a1ebd5..17e88f268 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,34 +7,51 @@ assignees: '' --- + + - + **Describe the bug** - + +A clear and concise description of what the bug is. **To Reproduce** - - - - - + +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error **Expected behavior** - + +A clear and concise description of what you expected to happen. **Screenshots / Videos** - -**Server Version** - +If applicable, add screenshots to help explain your problem. -**Geyser Version** - +**Server Version and Plugins** + +If you just run Geyser-Spigot, you can leave this area blank as the next section covers this information. + +If you're running a multi-server instance, or using Geyser Standalone: + +- Give us the exact output from `/version` on all servers involved. Saying "latest" does not help us at all. +- Please list all plugins on all servers involved. + +If this bug occurs on a server you do not control, please fill this in to the best of your knowledge. + +**Geyser Dump** + +If Geyser starts correctly, please also include the link to a dump by using `/geyser dump`. If you use the Standalone GUI, the option can be found under `Commands` => `Dump`. This provides us information about your server that we can use to debug your issue. **Minecraft: Bedrock Edition Version** - + +The version of your Minecraft: Bedrock Edition client you tested with, along with your device type (e.g. Windows 10, Switch...). **Additional Context** - + +Add any other context about the problem here. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c7a5a3d28..527471bae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,29 +11,36 @@ Thank for for considering a contribution! Generally, Geyser welcomes PRs from ev We have some general style guides that should be applied throughout the code: ```java +public class LongClassName { + private static final int AIR_ITEM = 0; // Static item names should be capitalized -private static final AIR_ITEM = 0; // Static item names should be capitalized + public Int2IntMap items = new Int2IntOpenHashMap(); // Use the interface as the class type but initialize with the implementation. -public Int2IntMap items = new Int2IntOpenHashMap(); // Use the interface as the class type but initialize with the implementation. + public int nameWithMultipleWords = 0; -public int nameWithMultipleWords = 0; + /** + * Javadoc comment to explain what a function does. + */ + @RandomAnnotation(stuff = true, moreStuff = "might exist") + public void applyStuff() { + Variable variable = new Variable(); + Variable otherVariable = new Variable(); -/** -* Javadoc comment to explain what a function does. -*/ -public void applyStuff() { - if (condition) { - // Do stuff. - } else if (anotherCondition) { - // Do something else. - } - - switch (value) { - case 0: - break; - case 1: - break: - } + if (condition) { + // Do stuff. + } else if (anotherCondition) { + // Do something else. + } + + switch (value) { + case 0: + stuff(); + break; + case 1: + differentStuff(); + break; + } + } } ``` diff --git a/Jenkinsfile b/Jenkinsfile index e7f2ec4e2..6564bd1f9 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -26,7 +26,27 @@ pipeline { } steps { - sh 'mvn javadoc:jar source:jar deploy -DskipTests' + rtMavenDeployer( + id: "maven-deployer", + serverId: "opencollab-artifactory", + releaseRepo: "maven-releases", + snapshotRepo: "maven-snapshots" + ) + rtMavenResolver( + id: "maven-resolver", + serverId: "opencollab-artifactory", + releaseRepo: "release", + snapshotRepo: "snapshot" + ) + rtMavenRun( + pom: 'pom.xml', + goals: 'javadoc:jar source:jar install -DskipTests', + deployerId: "maven-deployer", + resolverId: "maven-resolver" + ) + rtPublishBuildInfo( + serverId: "opencollab-artifactory" + ) } } } @@ -69,5 +89,14 @@ pipeline { discordSend description: "**Build:** [${currentBuild.id}](${env.BUILD_URL})\n**Status:** [${currentBuild.currentResult}](${env.BUILD_URL})\n${changes}\n\n[**Artifacts on Jenkins**](https://ci.opencollab.dev/job/GeyserMC/job/Geyser)", footer: 'Open Collaboration Jenkins', link: env.BUILD_URL, successful: currentBuild.resultIsBetterOrEqualTo('SUCCESS'), title: "${env.JOB_NAME} #${currentBuild.id}", webhookURL: DISCORD_WEBHOOK } } + success { + script { + if (env.BRANCH_NAME == '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)] + } + } + } } } diff --git a/LICENSE b/LICENSE index acd4af141..0e368d546 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2019-2020 GeyserMC. http://geysermc.org +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 diff --git a/README.md b/README.md index 1d1657edc..343fca94a 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ The ultimate goal of this project is to allow Minecraft: Bedrock Edition users t Special thanks to the DragonProxy project for being a trailblazer in protocol translation and for all the team members who have now joined us here! -### Currently supporting Minecraft Bedrock v1.16.100 - v1.16.201 and Minecraft Java v1.16.4. +### Currently supporting Minecraft Bedrock v1.16.100 - v1.16.201 and Minecraft Java v1.16.4 - v1.16.5. ## Setting Up Take a look [here](https://github.com/GeyserMC/Geyser/wiki#Setup) for how to set up Geyser. @@ -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. @@ -54,6 +55,7 @@ The following things can't be fixed because of Bedrock limitations. They might b - Custom heads in inventories - Clickable links in chat - Glowing effect +- Custom armor stand poses ## Compiling 1. Clone the repo to your computer diff --git a/bootstrap/bungeecord/pom.xml b/bootstrap/bungeecord/pom.xml index 124967b0a..10855aed4 100644 --- a/bootstrap/bungeecord/pom.xml +++ b/bootstrap/bungeecord/pom.xml @@ -20,7 +20,7 @@ net.md-5 bungeecord-api - 1.15-SNAPSHOT + 1.16-R0.4-SNAPSHOT provided @@ -86,8 +86,8 @@ org.geysermc.platform.bungeecord.shaded.dom4j - net.kyori.adventure - org.geysermc.platform.bungeecord.shaded.adventure + net.kyori + org.geysermc.platform.bungeecord.shaded.kyori diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/command/GeyserBungeeCommandExecutor.java b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/command/GeyserBungeeCommandExecutor.java index 2431f0a4e..b391d7b1c 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/command/GeyserBungeeCommandExecutor.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/command/GeyserBungeeCommandExecutor.java @@ -30,7 +30,9 @@ import net.md_5.bungee.api.CommandSender; import net.md_5.bungee.api.plugin.Command; import net.md_5.bungee.api.plugin.TabExecutor; import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.command.CommandExecutor; import org.geysermc.connector.command.GeyserCommand; +import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.utils.LanguageUtils; import java.util.ArrayList; @@ -38,29 +40,42 @@ import java.util.Arrays; public class GeyserBungeeCommandExecutor extends Command implements TabExecutor { + private final CommandExecutor commandExecutor; private final GeyserConnector connector; public GeyserBungeeCommandExecutor(GeyserConnector connector) { super("geyser"); + this.commandExecutor = new CommandExecutor(connector); this.connector = connector; } @Override public void execute(CommandSender sender, String[] args) { if (args.length > 0) { - if (getCommand(args[0]) != null) { - if (!sender.hasPermission(getCommand(args[0]).getPermission())) { - BungeeCommandSender commandSender = new BungeeCommandSender(sender); + GeyserCommand command = this.commandExecutor.getCommand(args[0]); + if (command != null) { + BungeeCommandSender commandSender = new BungeeCommandSender(sender); + if (!sender.hasPermission(command.getPermission())) { String message = LanguageUtils.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", commandSender.getLocale()); commandSender.sendMessage(ChatColor.RED + message); return; } - getCommand(args[0]).execute(new BungeeCommandSender(sender), args.length > 1 ? Arrays.copyOfRange(args, 1, args.length) : new String[0]); + GeyserSession session = null; + if (command.isBedrockOnly()) { + session = this.commandExecutor.getGeyserSession(commandSender); + if (session == null) { + String message = LanguageUtils.getPlayerLocaleString("geyser.bootstrap.command.bedrock_only", commandSender.getLocale()); + + commandSender.sendMessage(ChatColor.RED + message); + return; + } + } + command.execute(session, commandSender, args.length > 1 ? Arrays.copyOfRange(args, 1, args.length) : new String[0]); } } else { - getCommand("help").execute(new BungeeCommandSender(sender), new String[0]); + this.commandExecutor.getCommand("help").execute(null, new BungeeCommandSender(sender), new String[0]); } } @@ -71,8 +86,4 @@ public class GeyserBungeeCommandExecutor extends Command implements TabExecutor } return new ArrayList<>(); } - - private GeyserCommand getCommand(String label) { - return connector.getCommandManager().getCommands().get(label); - } } diff --git a/bootstrap/spigot/pom.xml b/bootstrap/spigot/pom.xml index adaa7557f..93eebc3d2 100644 --- a/bootstrap/spigot/pom.xml +++ b/bootstrap/spigot/pom.xml @@ -97,8 +97,8 @@ org.geysermc.platform.spigot.shaded.dom4j - net.kyori.adventure - org.geysermc.platform.spigot.shaded.adventure + net.kyori + org.geysermc.platform.spigot.shaded.kyori diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/command/GeyserSpigotCommandExecutor.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/command/GeyserSpigotCommandExecutor.java index 1db86856f..6cdcdae67 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/command/GeyserSpigotCommandExecutor.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/command/GeyserSpigotCommandExecutor.java @@ -25,40 +25,51 @@ package org.geysermc.platform.spigot.command; -import lombok.AllArgsConstructor; import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.command.TabExecutor; import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.command.CommandExecutor; import org.geysermc.connector.command.GeyserCommand; +import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.utils.LanguageUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -@AllArgsConstructor -public class GeyserSpigotCommandExecutor implements TabExecutor { +public class GeyserSpigotCommandExecutor extends CommandExecutor implements TabExecutor { - private final GeyserConnector connector; + public GeyserSpigotCommandExecutor(GeyserConnector connector) { + super(connector); + } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (args.length > 0) { - if (getCommand(args[0]) != null) { - if (!sender.hasPermission(getCommand(args[0]).getPermission())) { - SpigotCommandSender commandSender = new SpigotCommandSender(sender); - String message = LanguageUtils.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", commandSender.getLocale());; + GeyserCommand geyserCommand = getCommand(args[0]); + if (geyserCommand != null) { + SpigotCommandSender commandSender = new SpigotCommandSender(sender); + if (!sender.hasPermission(geyserCommand.getPermission())) { + String message = LanguageUtils.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", commandSender.getLocale()); commandSender.sendMessage(ChatColor.RED + message); return true; } - getCommand(args[0]).execute(new SpigotCommandSender(sender), args.length > 1 ? Arrays.copyOfRange(args, 1, args.length) : new String[0]); + GeyserSession session = null; + if (geyserCommand.isBedrockOnly()) { + session = getGeyserSession(commandSender); + if (session == null) { + sender.sendMessage(ChatColor.RED + LanguageUtils.getPlayerLocaleString("geyser.bootstrap.command.bedrock_only", commandSender.getLocale())); + return true; + } + } + geyserCommand.execute(session, commandSender, args.length > 1 ? Arrays.copyOfRange(args, 1, args.length) : new String[0]); return true; } } else { - getCommand("help").execute(new SpigotCommandSender(sender), new String[0]); + getCommand("help").execute(null, new SpigotCommandSender(sender), new String[0]); return true; } return true; @@ -71,8 +82,4 @@ public class GeyserSpigotCommandExecutor implements TabExecutor { } return new ArrayList<>(); } - - private GeyserCommand getCommand(String label) { - return connector.getCommandManager().getCommands().get(label); - } } diff --git a/bootstrap/sponge/pom.xml b/bootstrap/sponge/pom.xml index e6ce8f851..97c4ac8a4 100644 --- a/bootstrap/sponge/pom.xml +++ b/bootstrap/sponge/pom.xml @@ -86,8 +86,8 @@ org.geysermc.platform.sponge.shaded.dom4j - net.kyori.adventure - org.geysermc.platform.sponge.shaded.adventure + net.kyori + org.geysermc.platform.sponge.shaded.kyori diff --git a/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/command/GeyserSpongeCommandExecutor.java b/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/command/GeyserSpongeCommandExecutor.java index 938d19928..8ef23b19e 100644 --- a/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/command/GeyserSpongeCommandExecutor.java +++ b/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/command/GeyserSpongeCommandExecutor.java @@ -25,10 +25,12 @@ package org.geysermc.platform.sponge.command; -import lombok.AllArgsConstructor; import org.geysermc.connector.GeyserConnector; -import org.geysermc.connector.common.ChatColor; +import org.geysermc.connector.command.CommandExecutor; +import org.geysermc.connector.command.CommandSender; import org.geysermc.connector.command.GeyserCommand; +import org.geysermc.connector.common.ChatColor; +import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.utils.LanguageUtils; import org.spongepowered.api.command.CommandCallable; import org.spongepowered.api.command.CommandException; @@ -44,25 +46,36 @@ import java.util.Arrays; import java.util.List; import java.util.Optional; -@AllArgsConstructor -public class GeyserSpongeCommandExecutor implements CommandCallable { +public class GeyserSpongeCommandExecutor extends CommandExecutor implements CommandCallable { - private GeyserConnector connector; + public GeyserSpongeCommandExecutor(GeyserConnector connector) { + super(connector); + } @Override - public CommandResult process(CommandSource source, String arguments) throws CommandException { + public CommandResult process(CommandSource source, String arguments) { String[] args = arguments.split(" "); if (args.length > 0) { - if (getCommand(args[0]) != null) { - if (!source.hasPermission(getCommand(args[0]).getPermission())) { + GeyserCommand command = getCommand(args[0]); + if (command != null) { + CommandSender commandSender = new SpongeCommandSender(source); + if (!source.hasPermission(command.getPermission())) { // Not ideal to use log here but we dont get a session source.sendMessage(Text.of(ChatColor.RED + LanguageUtils.getLocaleStringLog("geyser.bootstrap.command.permission_fail"))); return CommandResult.success(); } - getCommand(args[0]).execute(new SpongeCommandSender(source), args.length > 1 ? Arrays.copyOfRange(args, 1, args.length) : new String[0]); + GeyserSession session = null; + if (command.isBedrockOnly()) { + session = getGeyserSession(commandSender); + if (session == null) { + source.sendMessage(Text.of(ChatColor.RED + LanguageUtils.getLocaleStringLog("geyser.bootstrap.command.bedrock_only"))); + return CommandResult.success(); + } + } + getCommand(args[0]).execute(session, commandSender, args.length > 1 ? Arrays.copyOfRange(args, 1, args.length) : new String[0]); } } else { - getCommand("help").execute(new SpongeCommandSender(source), new String[0]); + getCommand("help").execute(null, new SpongeCommandSender(source), new String[0]); } return CommandResult.success(); } @@ -94,8 +107,4 @@ public class GeyserSpongeCommandExecutor implements CommandCallable { public Text getUsage(CommandSource source) { return Text.of("/geyser help"); } - - private GeyserCommand getCommand(String label) { - return connector.getCommandManager().getCommands().get(label); - } } diff --git a/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/gui/GeyserStandaloneGUI.java b/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/gui/GeyserStandaloneGUI.java index fb6a46f9f..3636dded8 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/gui/GeyserStandaloneGUI.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/gui/GeyserStandaloneGUI.java @@ -271,17 +271,17 @@ public class GeyserStandaloneGUI { JMenuItem commandButton = hasSubCommands ? new JMenu(command.getValue().getName()) : new JMenuItem(command.getValue().getName()); commandButton.getAccessibleContext().setAccessibleDescription(command.getValue().getDescription()); if (!hasSubCommands) { - commandButton.addActionListener(e -> command.getValue().execute(geyserStandaloneLogger, new String[]{ })); + commandButton.addActionListener(e -> command.getValue().execute(null, geyserStandaloneLogger, new String[]{ })); } else { // Add a submenu that's the same name as the menu can't be pressed JMenuItem otherCommandButton = new JMenuItem(command.getValue().getName()); otherCommandButton.getAccessibleContext().setAccessibleDescription(command.getValue().getDescription()); - otherCommandButton.addActionListener(e -> command.getValue().execute(geyserStandaloneLogger, new String[]{ })); + otherCommandButton.addActionListener(e -> command.getValue().execute(null, geyserStandaloneLogger, new String[]{ })); commandButton.add(otherCommandButton); // Add a menu option for all possible subcommands for (String subCommandName : command.getValue().getSubCommands()) { JMenuItem item = new JMenuItem(subCommandName); - item.addActionListener(e -> command.getValue().execute(geyserStandaloneLogger, new String[]{subCommandName})); + item.addActionListener(e -> command.getValue().execute(null, geyserStandaloneLogger, new String[]{subCommandName})); commandButton.add(item); } } diff --git a/bootstrap/velocity/pom.xml b/bootstrap/velocity/pom.xml index 2fedca71a..58eee1f77 100644 --- a/bootstrap/velocity/pom.xml +++ b/bootstrap/velocity/pom.xml @@ -82,8 +82,8 @@ org.geysermc.platform.velocity.shaded.dom4j - net.kyori.adventure - org.geysermc.platform.velocity.shaded.adventure + net.kyori.adventure.text.serializer.gson.legacyimpl + org.geysermc.platform.velocity.shaded.kyori.legacyimpl @@ -105,6 +105,13 @@ io.netty:netty-codec:* org.slf4j:* org.ow2.asm:* + + net.kyori:adventure-api:* + net.kyori:examination-api:* + net.kyori:examination-string:* + net.kyori:adventure-text-serializer-gson:* + net.kyori:adventure-text-serializer-legacy:* + net.kyori:adventure-nbt:* diff --git a/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityPingPassthrough.java b/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityPingPassthrough.java index bab0e3505..bc10bc723 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityPingPassthrough.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityPingPassthrough.java @@ -31,11 +31,10 @@ import com.velocitypowered.api.proxy.InboundConnection; import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.api.proxy.server.ServerPing; import lombok.AllArgsConstructor; -import net.kyori.text.serializer.legacy.LegacyComponentSerializer; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import org.geysermc.connector.common.ping.GeyserPingInfo; import org.geysermc.connector.ping.IGeyserPingPassthrough; -import java.net.Inet4Address; import java.net.InetSocketAddress; import java.util.Optional; import java.util.concurrent.ExecutionException; @@ -50,13 +49,13 @@ public class GeyserVelocityPingPassthrough implements IGeyserPingPassthrough { ProxyPingEvent event; try { event = server.getEventManager().fire(new ProxyPingEvent(new GeyserInboundConnection(inetSocketAddress), ServerPing.builder() - .description(server.getConfiguration().getMotdComponent()).onlinePlayers(server.getPlayerCount()) + .description(server.getConfiguration().getMotd()).onlinePlayers(server.getPlayerCount()) .maximumPlayers(server.getConfiguration().getShowMaxPlayers()).build())).get(); } catch (ExecutionException | InterruptedException e) { throw new RuntimeException(e); } GeyserPingInfo geyserPingInfo = new GeyserPingInfo( - LegacyComponentSerializer.legacy().serialize(event.getPing().getDescription(), '§'), + LegacyComponentSerializer.legacy('§').serialize(event.getPing().getDescriptionComponent()), new GeyserPingInfo.Players( event.getPing().getPlayers().orElseThrow(IllegalStateException::new).getMax(), event.getPing().getPlayers().orElseThrow(IllegalStateException::new).getOnline() diff --git a/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/command/GeyserVelocityCommandExecutor.java b/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/command/GeyserVelocityCommandExecutor.java index 4aab73e59..c8998d8fe 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/command/GeyserVelocityCommandExecutor.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/command/GeyserVelocityCommandExecutor.java @@ -25,37 +25,47 @@ package org.geysermc.platform.velocity.command; -import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.command.SimpleCommand; -import lombok.AllArgsConstructor; import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.command.CommandExecutor; import org.geysermc.connector.command.CommandSender; import org.geysermc.connector.command.GeyserCommand; import org.geysermc.connector.common.ChatColor; +import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.utils.LanguageUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -@AllArgsConstructor -public class GeyserVelocityCommandExecutor implements SimpleCommand { +public class GeyserVelocityCommandExecutor extends CommandExecutor implements SimpleCommand { - private final GeyserConnector connector; + public GeyserVelocityCommandExecutor(GeyserConnector connector) { + super(connector); + } @Override public void execute(Invocation invocation) { if (invocation.arguments().length > 0) { - if (getCommand(invocation.arguments()[0]) != null) { + GeyserCommand command = getCommand(invocation.arguments()[0]); + if (command != null) { + CommandSender sender = new VelocityCommandSender(invocation.source()); if (!invocation.source().hasPermission(getCommand(invocation.arguments()[0]).getPermission())) { - CommandSender sender = new VelocityCommandSender(invocation.source()); sender.sendMessage(ChatColor.RED + LanguageUtils.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", sender.getLocale())); return; } - getCommand(invocation.arguments()[0]).execute(new VelocityCommandSender(invocation.source()), invocation.arguments().length > 1 ? Arrays.copyOfRange(invocation.arguments(), 1, invocation.arguments().length) : new String[0]); + GeyserSession session = null; + if (command.isBedrockOnly()) { + session = getGeyserSession(sender); + if (session == null) { + sender.sendMessage(ChatColor.RED + LanguageUtils.getPlayerLocaleString("geyser.bootstrap.command.bedrock_only", sender.getLocale())); + return; + } + } + command.execute(session, sender, invocation.arguments().length > 1 ? Arrays.copyOfRange(invocation.arguments(), 1, invocation.arguments().length) : new String[0]); } } else { - getCommand("help").execute(new VelocityCommandSender(invocation.source()), new String[0]); + getCommand("help").execute(null, new VelocityCommandSender(invocation.source()), new String[0]); } } @@ -66,8 +76,4 @@ public class GeyserVelocityCommandExecutor implements SimpleCommand { } return new ArrayList<>(); } - - private GeyserCommand getCommand(String label) { - return connector.getCommandManager().getCommands().get(label); - } } diff --git a/connector/pom.xml b/connector/pom.xml index 738d7ebcc..5e78fcfc3 100644 --- a/connector/pom.xml +++ b/connector/pom.xml @@ -131,6 +131,10 @@ com.github.steveice10 packetlib + + com.github.steveice10 + mcauthlib + @@ -197,6 +201,11 @@ 4.13.1 test + + com.github.GeyserMC + MCAuthLib + 0e48a094f2 + diff --git a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java index 6a3317f2d..38c2aa29a 100644 --- a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java +++ b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java @@ -90,6 +90,12 @@ public class GeyserConnector { public static final String NAME = "Geyser"; public static final String GIT_VERSION = "DEV"; // A fallback for running in IDEs public static final String VERSION = "DEV"; // A fallback for running in IDEs + public static final String MINECRAFT_VERSION = "1.16.4 - 1.16.5"; + + /** + * Oauth client ID for Microsoft authentication + */ + public static final String OAUTH_CLIENT_ID = "204cefd1-4818-4de1-b98d-513fae875d88"; private static final String IP_REGEX = "\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b"; @@ -108,8 +114,8 @@ public class GeyserConnector { private final ScheduledExecutorService generalThreadPool; private BedrockServer bedrockServer; - private PlatformType platformType; - private GeyserBootstrap bootstrap; + private final PlatformType platformType; + private final GeyserBootstrap bootstrap; private Metrics metrics; diff --git a/connector/src/main/java/org/geysermc/connector/command/CommandExecutor.java b/connector/src/main/java/org/geysermc/connector/command/CommandExecutor.java new file mode 100644 index 000000000..751f51260 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/command/CommandExecutor.java @@ -0,0 +1,56 @@ +/* + * 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; + +import lombok.AllArgsConstructor; +import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.network.session.GeyserSession; + +/** + * Represents helper functions for listening to {@code /geyser} commands. + */ +@AllArgsConstructor +public class CommandExecutor { + + protected final GeyserConnector connector; + + public GeyserCommand getCommand(String label) { + return connector.getCommandManager().getCommands().get(label); + } + + public GeyserSession getGeyserSession(CommandSender sender) { + if (sender.isConsole()) { + return null; + } + + for (GeyserSession session : connector.getPlayers()) { + if (sender.getName().equals(session.getPlayerEntity().getUsername())) { + return session; + } + } + return null; + } +} 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..71eb2c742 100644 --- a/connector/src/main/java/org/geysermc/connector/command/CommandManager.java +++ b/connector/src/main/java/org/geysermc/connector/command/CommandManager.java @@ -29,6 +29,7 @@ import lombok.Getter; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.command.defaults.*; +import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.utils.LanguageUtils; import java.util.*; @@ -52,6 +53,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( "advancements", "geyser.commands.advancements.desc", "geyser.command.advancements")); } public void registerCommand(GeyserCommand command) { @@ -88,7 +90,15 @@ public abstract class CommandManager { return; } - cmd.execute(sender, args); + if (sender instanceof GeyserSession) { + cmd.execute((GeyserSession) sender, sender, args); + } else { + if (!cmd.isBedrockOnly()) { + cmd.execute(null, sender, args); + } else { + connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.bootstrap.command.bedrock_only")); + } + } } /** diff --git a/connector/src/main/java/org/geysermc/connector/command/GeyserCommand.java b/connector/src/main/java/org/geysermc/connector/command/GeyserCommand.java index c606e2e7b..48fe2eb9a 100644 --- a/connector/src/main/java/org/geysermc/connector/command/GeyserCommand.java +++ b/connector/src/main/java/org/geysermc/connector/command/GeyserCommand.java @@ -28,7 +28,9 @@ package org.geysermc.connector.command; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; +import org.geysermc.connector.network.session.GeyserSession; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -47,7 +49,7 @@ public abstract class GeyserCommand { @Setter private List aliases = new ArrayList<>(); - public abstract void execute(CommandSender sender, String[] args); + public abstract void execute(@Nullable GeyserSession session, CommandSender sender, String[] args); /** * If false, hides the command from being shown on the Geyser Standalone GUI. @@ -75,4 +77,13 @@ public abstract class GeyserCommand { public boolean hasSubCommands() { return !getSubCommands().isEmpty(); } + + /** + * Used to send a deny message to Java players if this command can only be used by Bedrock players. + * + * @return true if this command can only be used by Bedrock players. + */ + public boolean isBedrockOnly() { + return false; + } } \ No newline at end of file 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..8ace83840 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/command/defaults/AdvancementsCommand.java @@ -0,0 +1,53 @@ +/* + * 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.connector.command.CommandSender; +import org.geysermc.connector.command.GeyserCommand; +import org.geysermc.connector.network.session.GeyserSession; + +public class AdvancementsCommand extends GeyserCommand { + public AdvancementsCommand(String name, String description, String permission) { + super(name, description, permission); + } + + @Override + public void execute(GeyserSession session, CommandSender sender, String[] args) { + if (session != null) { + session.getAdvancementsCache().buildAndShowMenuForm(); + } + } + + @Override + public boolean isExecutableOnConsole() { + return false; + } + + @Override + public boolean isBedrockOnly() { + return true; + } +} diff --git a/connector/src/main/java/org/geysermc/connector/command/defaults/DumpCommand.java b/connector/src/main/java/org/geysermc/connector/command/defaults/DumpCommand.java index 5bc3efea7..97d09f7e0 100644 --- a/connector/src/main/java/org/geysermc/connector/command/defaults/DumpCommand.java +++ b/connector/src/main/java/org/geysermc/connector/command/defaults/DumpCommand.java @@ -33,6 +33,7 @@ import org.geysermc.connector.command.GeyserCommand; import org.geysermc.connector.common.ChatColor; import org.geysermc.connector.common.serializer.AsteriskSerializer; import org.geysermc.connector.dump.DumpInfo; +import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.utils.LanguageUtils; import org.geysermc.connector.utils.WebUtils; @@ -54,7 +55,7 @@ public class DumpCommand extends GeyserCommand { } @Override - public void execute(CommandSender sender, String[] args) { + public void execute(GeyserSession session, CommandSender sender, String[] args) { boolean showSensitive = false; boolean offlineDump = false; if (args.length >= 1) { diff --git a/connector/src/main/java/org/geysermc/connector/command/defaults/HelpCommand.java b/connector/src/main/java/org/geysermc/connector/command/defaults/HelpCommand.java index 7ab3aec3c..c2716f206 100644 --- a/connector/src/main/java/org/geysermc/connector/command/defaults/HelpCommand.java +++ b/connector/src/main/java/org/geysermc/connector/command/defaults/HelpCommand.java @@ -29,6 +29,7 @@ import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.command.CommandSender; import org.geysermc.connector.command.GeyserCommand; import org.geysermc.connector.common.ChatColor; +import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.utils.LanguageUtils; import java.util.Collections; @@ -48,7 +49,7 @@ public class HelpCommand extends GeyserCommand { } @Override - public void execute(CommandSender sender, String[] args) { + public void execute(GeyserSession session, CommandSender sender, String[] args) { int page = 1; int maxPage = 1; String header = LanguageUtils.getPlayerLocaleString("geyser.commands.help.header", sender.getLocale(), page, maxPage); diff --git a/connector/src/main/java/org/geysermc/connector/command/defaults/ListCommand.java b/connector/src/main/java/org/geysermc/connector/command/defaults/ListCommand.java index f52ab7f36..8a000f80c 100644 --- a/connector/src/main/java/org/geysermc/connector/command/defaults/ListCommand.java +++ b/connector/src/main/java/org/geysermc/connector/command/defaults/ListCommand.java @@ -44,7 +44,7 @@ public class ListCommand extends GeyserCommand { } @Override - public void execute(CommandSender sender, String[] args) { + public void execute(GeyserSession session, CommandSender sender, String[] args) { String message = ""; message = LanguageUtils.getPlayerLocaleString("geyser.commands.list.message", sender.getLocale(), connector.getPlayers().size(), diff --git a/connector/src/main/java/org/geysermc/connector/command/defaults/OffhandCommand.java b/connector/src/main/java/org/geysermc/connector/command/defaults/OffhandCommand.java index d6916700b..4d7d74045 100644 --- a/connector/src/main/java/org/geysermc/connector/command/defaults/OffhandCommand.java +++ b/connector/src/main/java/org/geysermc/connector/command/defaults/OffhandCommand.java @@ -45,32 +45,23 @@ public class OffhandCommand extends GeyserCommand { } @Override - public void execute(CommandSender sender, String[] args) { - if (sender.isConsole()) { + public void execute(GeyserSession session, CommandSender sender, String[] args) { + if (session == null) { return; } - // Make sure the sender is a Bedrock edition client - if (sender instanceof GeyserSession) { - GeyserSession session = (GeyserSession) sender; - ClientPlayerActionPacket releaseItemPacket = new ClientPlayerActionPacket(PlayerAction.SWAP_HANDS, new Position(0,0,0), - BlockFace.DOWN); - session.sendDownstreamPacket(releaseItemPacket); - return; - } - // Needed for Spigot - sender is not an instance of GeyserSession - for (GeyserSession session : connector.getPlayers()) { - if (sender.getName().equals(session.getPlayerEntity().getUsername())) { - ClientPlayerActionPacket releaseItemPacket = new ClientPlayerActionPacket(PlayerAction.SWAP_HANDS, new Position(0,0,0), - BlockFace.DOWN); - session.sendDownstreamPacket(releaseItemPacket); - break; - } - } + ClientPlayerActionPacket releaseItemPacket = new ClientPlayerActionPacket(PlayerAction.SWAP_HANDS, new Position(0,0,0), + BlockFace.DOWN); + session.sendDownstreamPacket(releaseItemPacket); } @Override public boolean isExecutableOnConsole() { return false; } + + @Override + public boolean isBedrockOnly() { + return true; + } } diff --git a/connector/src/main/java/org/geysermc/connector/command/defaults/ReloadCommand.java b/connector/src/main/java/org/geysermc/connector/command/defaults/ReloadCommand.java index 798dd7a77..2f1c7dc9b 100644 --- a/connector/src/main/java/org/geysermc/connector/command/defaults/ReloadCommand.java +++ b/connector/src/main/java/org/geysermc/connector/command/defaults/ReloadCommand.java @@ -34,7 +34,7 @@ import org.geysermc.connector.utils.LanguageUtils; public class ReloadCommand extends GeyserCommand { - private GeyserConnector connector; + private final GeyserConnector connector; public ReloadCommand(GeyserConnector connector, String name, String description, String permission) { super(name, description, permission); @@ -42,7 +42,7 @@ public class ReloadCommand extends GeyserCommand { } @Override - public void execute(CommandSender sender, String[] args) { + public void execute(GeyserSession session, CommandSender sender, String[] args) { if (!sender.isConsole() && connector.getPlatformType() == PlatformType.STANDALONE) { return; } @@ -51,8 +51,8 @@ public class ReloadCommand extends GeyserCommand { sender.sendMessage(message); - for (GeyserSession session : connector.getPlayers()) { - session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.commands.reload.kick", session.getLocale())); + for (GeyserSession otherSession : connector.getPlayers()) { + otherSession.disconnect(LanguageUtils.getPlayerLocaleString("geyser.commands.reload.kick", session.getLocale())); } connector.reload(); } diff --git a/connector/src/main/java/org/geysermc/connector/command/defaults/SettingsCommand.java b/connector/src/main/java/org/geysermc/connector/command/defaults/SettingsCommand.java index ed2c221c3..0f85e3c8f 100644 --- a/connector/src/main/java/org/geysermc/connector/command/defaults/SettingsCommand.java +++ b/connector/src/main/java/org/geysermc/connector/command/defaults/SettingsCommand.java @@ -32,30 +32,12 @@ import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.utils.SettingsUtils; public class SettingsCommand extends GeyserCommand { - - private final GeyserConnector connector; - public SettingsCommand(GeyserConnector connector, String name, String description, String permission) { super(name, description, permission); - - this.connector = connector; } @Override - public void execute(CommandSender sender, String[] args) { - // 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; - } - } - } + public void execute(GeyserSession session, CommandSender sender, String[] args) { if (session != null) { session.sendForm(SettingsUtils.buildForm(session)); } @@ -65,4 +47,9 @@ public class SettingsCommand extends GeyserCommand { public boolean isExecutableOnConsole() { return false; } + + @Override + public boolean isBedrockOnly() { + return true; + } } diff --git a/connector/src/main/java/org/geysermc/connector/command/defaults/StatisticsCommand.java b/connector/src/main/java/org/geysermc/connector/command/defaults/StatisticsCommand.java index 920ec50c7..3502941d5 100644 --- a/connector/src/main/java/org/geysermc/connector/command/defaults/StatisticsCommand.java +++ b/connector/src/main/java/org/geysermc/connector/command/defaults/StatisticsCommand.java @@ -34,34 +34,14 @@ import org.geysermc.connector.network.session.GeyserSession; public class StatisticsCommand extends GeyserCommand { - private final GeyserConnector connector; - public StatisticsCommand(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; - } - } - } + public void execute(GeyserSession session, CommandSender sender, String[] args) { if (session == null) return; + session.setWaitingForStatistics(true); ClientRequestPacket clientRequestPacket = new ClientRequestPacket(ClientRequest.STATS); session.sendDownstreamPacket(clientRequestPacket); @@ -71,4 +51,9 @@ public class StatisticsCommand extends GeyserCommand { public boolean isExecutableOnConsole() { return false; } + + @Override + public boolean isBedrockOnly() { + return true; + } } diff --git a/connector/src/main/java/org/geysermc/connector/command/defaults/StopCommand.java b/connector/src/main/java/org/geysermc/connector/command/defaults/StopCommand.java index b192c9e9a..b00e44b72 100644 --- a/connector/src/main/java/org/geysermc/connector/command/defaults/StopCommand.java +++ b/connector/src/main/java/org/geysermc/connector/command/defaults/StopCommand.java @@ -29,12 +29,13 @@ import org.geysermc.common.PlatformType; 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 java.util.Collections; public class StopCommand extends GeyserCommand { - private GeyserConnector connector; + private final GeyserConnector connector; public StopCommand(GeyserConnector connector, String name, String description, String permission) { super(name, description, permission); @@ -44,7 +45,7 @@ public class StopCommand extends GeyserCommand { } @Override - public void execute(CommandSender sender, String[] args) { + public void execute(GeyserSession session, CommandSender sender, String[] args) { if (!sender.isConsole() && connector.getPlatformType() == PlatformType.STANDALONE) { return; } diff --git a/connector/src/main/java/org/geysermc/connector/command/defaults/VersionCommand.java b/connector/src/main/java/org/geysermc/connector/command/defaults/VersionCommand.java index 1f807cf63..226a770a6 100644 --- a/connector/src/main/java/org/geysermc/connector/command/defaults/VersionCommand.java +++ b/connector/src/main/java/org/geysermc/connector/command/defaults/VersionCommand.java @@ -32,6 +32,7 @@ import org.geysermc.connector.command.CommandSender; import org.geysermc.connector.command.GeyserCommand; import org.geysermc.connector.common.ChatColor; import org.geysermc.connector.network.BedrockProtocol; +import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.utils.FileUtils; import org.geysermc.connector.utils.LanguageUtils; import org.geysermc.connector.utils.WebUtils; @@ -44,15 +45,12 @@ import java.util.Properties; public class VersionCommand extends GeyserCommand { - public GeyserConnector connector; - public VersionCommand(GeyserConnector connector, String name, String description, String permission) { super(name, description, permission); - this.connector = connector; } @Override - public void execute(CommandSender sender, String[] args) { + public void execute(GeyserSession session, CommandSender sender, String[] args) { String bedrockVersions; List supportedCodecs = BedrockProtocol.SUPPORTED_BEDROCK_CODECS; if (supportedCodecs.size() > 1) { @@ -61,7 +59,7 @@ public class VersionCommand extends GeyserCommand { bedrockVersions = BedrockProtocol.DEFAULT_BEDROCK_CODEC.getMinecraftVersion(); } - sender.sendMessage(LanguageUtils.getPlayerLocaleString("geyser.commands.version.version", sender.getLocale(), GeyserConnector.NAME, GeyserConnector.VERSION, MinecraftConstants.GAME_VERSION, bedrockVersions)); + sender.sendMessage(LanguageUtils.getPlayerLocaleString("geyser.commands.version.version", sender.getLocale(), GeyserConnector.NAME, GeyserConnector.VERSION, GeyserConnector.MINECRAFT_VERSION, bedrockVersions)); // Disable update checking in dev mode //noinspection ConstantConditions - changes in production diff --git a/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java b/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java index d21893f89..e21aa6bb8 100644 --- a/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java +++ b/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java @@ -118,6 +118,8 @@ public interface GeyserConfiguration { String getAuthType(); + boolean isPasswordAuthentication(); + boolean isUseProxyProtocol(); } @@ -125,6 +127,12 @@ public interface GeyserConfiguration { String getEmail(); String getPassword(); + + /** + * Will be removed after Microsoft accounts are fully migrated + */ + @Deprecated + boolean isMicrosoftAccount(); } interface IMetricsInfo { diff --git a/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java b/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java index 3a8946e00..7c9532ff8 100644 --- a/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java +++ b/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java @@ -149,17 +149,24 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration @JsonProperty("auth-type") private String authType = "online"; + @JsonProperty("allow-password-authentication") + private boolean passwordAuthentication = true; + @JsonProperty("use-proxy-protocol") private boolean useProxyProtocol = false; } @Getter + @JsonIgnoreProperties(ignoreUnknown = true) // DO NOT REMOVE THIS! Otherwise, after we remove microsoft-account configs will not load public static class UserAuthenticationInfo implements IUserAuthenticationInfo { @AsteriskSerializer.Asterisk() private String email; @AsteriskSerializer.Asterisk() private String password; + + @JsonProperty("microsoft-account") + private boolean microsoftAccount = false; } @Getter diff --git a/connector/src/main/java/org/geysermc/connector/entity/ItemedFireballEntity.java b/connector/src/main/java/org/geysermc/connector/entity/ItemedFireballEntity.java index 488c0e90c..2b411109a 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/ItemedFireballEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/ItemedFireballEntity.java @@ -38,11 +38,11 @@ public class ItemedFireballEntity extends ThrowableEntity { } @Override - protected void updatePosition(GeyserSession session) { + public void tick(GeyserSession session) { position = position.add(motion); // TODO: While this reduces latency in position updating (needed for better fireball reflecting), - // TODO: movement is incredibly stiff. See if the MoveEntityDeltaPacket in 1.16.100 fixes this, and if not, - // TODO: only use this laggy movement for fireballs that be reflected + // TODO: movement is incredibly stiff. + // TODO: Only use this laggy movement for fireballs that be reflected moveAbsoluteImmediate(session, position, rotation, false, true); float drag = getDrag(session); motion = motion.add(acceleration).mul(drag); diff --git a/connector/src/main/java/org/geysermc/connector/entity/ThrowableEntity.java b/connector/src/main/java/org/geysermc/connector/entity/ThrowableEntity.java index 553e558ea..4e0c25ab5 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/ThrowableEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/ThrowableEntity.java @@ -33,50 +33,35 @@ import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.world.block.BlockTranslator; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - /** * Used as a class for any object-like entity that moves as a projectile */ -public class ThrowableEntity extends Entity { +public class ThrowableEntity extends Entity implements Tickable { private Vector3f lastPosition; - /** - * Updates the position for the Bedrock client. - * - * Java clients assume the next positions of moving items. Bedrock needs to be explicitly told positions - */ - protected ScheduledFuture positionUpdater; public ThrowableEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) { super(entityId, geyserId, entityType, position, motion, rotation); this.lastPosition = position; } + /** + * Updates the position for the Bedrock client. + * + * Java clients assume the next positions of moving items. Bedrock needs to be explicitly told positions + */ @Override - public void spawnEntity(GeyserSession session) { - super.spawnEntity(session); - positionUpdater = session.getConnector().getGeneralThreadPool().scheduleAtFixedRate(() -> { - if (session.isClosed()) { - positionUpdater.cancel(true); - return; - } - updatePosition(session); - }, 0, 50, TimeUnit.MILLISECONDS); - } - - protected void moveAbsoluteImmediate(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) { - super.moveAbsolute(session, position, rotation, isOnGround, teleported); - } - - protected void updatePosition(GeyserSession session) { + public void tick(GeyserSession session) { super.moveRelative(session, motion.getX(), motion.getY(), motion.getZ(), rotation, onGround); float drag = getDrag(session); float gravity = getGravity(); motion = motion.mul(drag).down(gravity); } + protected void moveAbsoluteImmediate(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) { + super.moveAbsolute(session, position, rotation, isOnGround, teleported); + } + /** * Get the gravity of this entity type. Used for applying gravity while the entity is in motion. * @@ -140,7 +125,6 @@ public class ThrowableEntity extends Entity { @Override public boolean despawnEntity(GeyserSession session) { - positionUpdater.cancel(true); if (entityType == EntityType.THROWN_ENDERPEARL) { LevelEventPacket particlePacket = new LevelEventPacket(); particlePacket.setType(LevelEventType.PARTICLE_TELEPORT); diff --git a/connector/src/main/java/org/geysermc/connector/entity/Tickable.java b/connector/src/main/java/org/geysermc/connector/entity/Tickable.java new file mode 100644 index 000000000..a7d571ccb --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/entity/Tickable.java @@ -0,0 +1,35 @@ +/* + * 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.entity; + +import org.geysermc.connector.network.session.GeyserSession; + +/** + * Implemented onto anything that should have code ran every Minecraft tick - 50 milliseconds. + */ +public interface Tickable { + void tick(GeyserSession session); +} diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonEntity.java index 7dbd96a44..621679798 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonEntity.java @@ -28,19 +28,28 @@ package org.geysermc.connector.entity.living.monster; import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.protocol.bedrock.data.AttributeData; +import com.nukkitx.protocol.bedrock.data.LevelEventType; +import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.data.entity.EntityEventType; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; -import com.nukkitx.protocol.bedrock.packet.AddEntityPacket; -import com.nukkitx.protocol.bedrock.packet.EntityEventPacket; +import com.nukkitx.protocol.bedrock.packet.*; import lombok.Data; +import org.geysermc.connector.entity.Tickable; +import org.geysermc.connector.entity.attribute.AttributeType; import org.geysermc.connector.entity.living.InsentientEntity; import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.utils.AttributeUtils; +import org.geysermc.connector.utils.DimensionUtils; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicLong; -public class EnderDragonEntity extends InsentientEntity { +public class EnderDragonEntity extends InsentientEntity implements Tickable { /** * The Ender Dragon has multiple hit boxes, which * are each its own invisible entity @@ -61,9 +70,19 @@ public class EnderDragonEntity extends InsentientEntity { private final Segment[] segmentHistory = new Segment[19]; private int latestSegment = -1; - private boolean hovering; + private int phase; + /** + * The number of ticks since the beginning of the phase + */ + private int phaseTicks; - private ScheduledFuture partPositionUpdater; + private int ticksTillNextGrowl = 100; + + /** + * Used to determine when the wing flap sound should be played + */ + private float wingPosition; + private float lastWingPosition; public EnderDragonEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) { super(entityId, geyserId, entityType, position, motion, rotation); @@ -73,49 +92,67 @@ public class EnderDragonEntity extends InsentientEntity { @Override public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) { - // Phase - if (entityMetadata.getId() == 15) { - int value = (int) entityMetadata.getValue(); - if (value == 5) { - // Performing breath attack + if (entityMetadata.getId() == 15) { // Phase + phase = (int) entityMetadata.getValue(); + phaseTicks = 0; + metadata.getFlags().setFlag(EntityFlag.SITTING, isSitting()); + } + + super.updateBedrockMetadata(entityMetadata, session); + + if (entityMetadata.getId() == 8) { // Health + // Update the health attribute, so that the death animation gets played + // Round health up, so that Bedrock doesn't consider the dragon to be dead when health is between 0 and 1 + float health = (float) Math.ceil(metadata.getFloat(EntityData.HEALTH)); + if (phase == 9 && health <= 0) { // Dying phase EntityEventPacket entityEventPacket = new EntityEventPacket(); - entityEventPacket.setType(EntityEventType.DRAGON_FLAMING); + entityEventPacket.setType(EntityEventType.ENDER_DRAGON_DEATH); entityEventPacket.setRuntimeEntityId(geyserId); entityEventPacket.setData(0); session.sendUpstreamPacket(entityEventPacket); } - metadata.getFlags().setFlag(EntityFlag.SITTING, value == 5 || value == 6 || value == 7); - hovering = value == 10; + attributes.put(AttributeType.HEALTH, AttributeType.HEALTH.getAttribute(health, 200)); + updateBedrockAttributes(session); } - super.updateBedrockMetadata(entityMetadata, session); + } + + /** + * Send an updated list of attributes to the Bedrock client. + * This is overwritten to allow the health attribute to differ from + * the health specified in the metadata. + * + * @param session GeyserSession + */ + @Override + public void updateBedrockAttributes(GeyserSession session) { + if (!valid) return; + + List attributes = new ArrayList<>(); + for (Map.Entry entry : this.attributes.entrySet()) { + if (!entry.getValue().getType().isBedrockAttribute()) + continue; + attributes.add(AttributeUtils.getBedrockAttribute(entry.getValue())); + } + + UpdateAttributesPacket updateAttributesPacket = new UpdateAttributesPacket(); + updateAttributesPacket.setRuntimeEntityId(geyserId); + updateAttributesPacket.setAttributes(attributes); + session.sendUpstreamPacket(updateAttributesPacket); } @Override public void spawnEntity(GeyserSession session) { - AddEntityPacket addEntityPacket = new AddEntityPacket(); - addEntityPacket.setIdentifier("minecraft:" + entityType.name().toLowerCase()); - addEntityPacket.setRuntimeEntityId(geyserId); - addEntityPacket.setUniqueEntityId(geyserId); - addEntityPacket.setPosition(position); - addEntityPacket.setMotion(motion); - addEntityPacket.setRotation(getBedrockRotation()); - addEntityPacket.setEntityType(entityType.getType()); - addEntityPacket.getMetadata().putAll(metadata); + super.spawnEntity(session); - // Otherwise dragon is always 'dying' - addEntityPacket.getAttributes().add(new AttributeData("minecraft:health", 0.0f, 200f, 200f, 200f)); - - valid = true; - session.sendUpstreamPacket(addEntityPacket); - - head = new EnderDragonPartEntity(entityId + 1, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 1, 1); - neck = new EnderDragonPartEntity(entityId + 2, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 3, 3); - body = new EnderDragonPartEntity(entityId + 3, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 5, 3); - leftWing = new EnderDragonPartEntity(entityId + 4, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 4, 2); - rightWing = new EnderDragonPartEntity(entityId + 5, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 4, 2); + AtomicLong nextEntityId = session.getEntityCache().getNextEntityId(); + head = new EnderDragonPartEntity(entityId + 1, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 1, 1); + neck = new EnderDragonPartEntity(entityId + 2, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 3, 3); + body = new EnderDragonPartEntity(entityId + 3, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 5, 3); + leftWing = new EnderDragonPartEntity(entityId + 4, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 4, 2); + rightWing = new EnderDragonPartEntity(entityId + 5, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 4, 2); tail = new EnderDragonPartEntity[3]; for (int i = 0; i < 3; i++) { - tail[i] = new EnderDragonPartEntity(entityId + 6 + i, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 2, 2); + tail[i] = new EnderDragonPartEntity(entityId + 6 + i, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 2, 2); } allParts = new EnderDragonPartEntity[]{head, neck, body, leftWing, rightWing, tail[0], tail[1], tail[2]}; @@ -129,25 +166,25 @@ public class EnderDragonEntity extends InsentientEntity { segmentHistory[i].yaw = rotation.getZ(); segmentHistory[i].y = position.getY(); } - - partPositionUpdater = session.getConnector().getGeneralThreadPool().scheduleAtFixedRate(() -> { - pushSegment(); - updateBoundingBoxes(session); - }, 0, 50, TimeUnit.MILLISECONDS); - - session.getConnector().getLogger().debug("Spawned entity " + entityType + " at location " + position + " with id " + geyserId + " (java id " + entityId + ")"); } @Override public boolean despawnEntity(GeyserSession session) { - partPositionUpdater.cancel(true); - for (EnderDragonPartEntity part : allParts) { part.despawnEntity(session); } return super.despawnEntity(session); } + @Override + public void tick(GeyserSession session) { + effectTick(session); + if (!metadata.getFlags().getFlag(EntityFlag.NO_AI) && isAlive()) { + pushSegment(); + updateBoundingBoxes(session); + } + } + /** * Updates the positions of the Ender Dragon's multiple bounding boxes * @@ -163,7 +200,7 @@ public class EnderDragonEntity extends InsentientEntity { // Lowers the head when the dragon sits/hovers float headDuck; - if (hovering || metadata.getFlags().getFlag(EntityFlag.SITTING)) { + if (isHovering() || isSitting()) { headDuck = -1f; } else { headDuck = baseSegment.y - getSegment(0).y; @@ -193,6 +230,105 @@ public class EnderDragonEntity extends InsentientEntity { } } + /** + * Handles the particles and sounds of the Ender Dragon + * @param session GeyserSession. + */ + private void effectTick(GeyserSession session) { + Random random = ThreadLocalRandom.current(); + if (!metadata.getFlags().getFlag(EntityFlag.SILENT)) { + if (Math.cos(wingPosition * 2f * Math.PI) <= -0.3f && Math.cos(lastWingPosition * 2f * Math.PI) >= -0.3f) { + PlaySoundPacket playSoundPacket = new PlaySoundPacket(); + playSoundPacket.setSound("mob.enderdragon.flap"); + playSoundPacket.setPosition(position); + playSoundPacket.setVolume(5f); + playSoundPacket.setPitch(0.8f + random.nextFloat() * 0.3f); + session.sendUpstreamPacket(playSoundPacket); + } + + if (!isSitting() && !isHovering() && ticksTillNextGrowl-- == 0) { + playGrowlSound(session); + ticksTillNextGrowl = 200 + random.nextInt(200); + } + + lastWingPosition = wingPosition; + } + if (isAlive()) { + if (metadata.getFlags().getFlag(EntityFlag.NO_AI)) { + wingPosition = 0.5f; + } else if (isHovering() || isSitting()) { + wingPosition += 0.1f; + } else { + double speed = motion.length(); + wingPosition += 0.2f / (speed * 10f + 1) * Math.pow(2, motion.getY()); + } + + phaseTicks++; + if (phase == 3) { // Landing Phase + float headHeight = head.getMetadata().getFloat(EntityData.BOUNDING_BOX_HEIGHT); + Vector3f headCenter = head.getPosition().up(headHeight * 0.5f); + + for (int i = 0; i < 8; i++) { + Vector3f particlePos = headCenter.add(random.nextGaussian() / 2f, random.nextGaussian() / 2f, random.nextGaussian() / 2f); + // This is missing velocity information + LevelEventPacket particlePacket = new LevelEventPacket(); + particlePacket.setType(LevelEventType.PARTICLE_DRAGONS_BREATH); + particlePacket.setPosition(particlePos); + session.sendUpstreamPacket(particlePacket); + } + } else if (phase == 5) { // Sitting Flaming Phase + if (phaseTicks % 2 == 0 && phaseTicks < 10) { + // Performing breath attack + // Entity event DRAGON_FLAMING seems to create particles from the origin of the dragon, + // so we need to manually spawn particles + for (int i = 0; i < 8; i++) { + SpawnParticleEffectPacket spawnParticleEffectPacket = new SpawnParticleEffectPacket(); + spawnParticleEffectPacket.setDimensionId(DimensionUtils.javaToBedrock(session.getDimension())); + spawnParticleEffectPacket.setPosition(head.getPosition().add(random.nextGaussian() / 2f, random.nextGaussian() / 2f, random.nextGaussian() / 2f)); + spawnParticleEffectPacket.setIdentifier("minecraft:dragon_breath_fire"); + session.sendUpstreamPacket(spawnParticleEffectPacket); + } + } + } else if (phase == 7) { // Sitting Attacking Phase + playGrowlSound(session); + } else if (phase == 9) { // Dying Phase + // Send explosion particles as the dragon move towards the end portal + if (phaseTicks % 10 == 0) { + float xOffset = 8f * (random.nextFloat() - 0.5f); + float yOffset = 4f * (random.nextFloat() - 0.5f) + 2f; + float zOffset = 8f * (random.nextFloat() - 0.5f); + Vector3f particlePos = position.add(xOffset, yOffset, zOffset); + LevelEventPacket particlePacket = new LevelEventPacket(); + particlePacket.setType(LevelEventType.PARTICLE_EXPLOSION); + particlePacket.setPosition(particlePos); + session.sendUpstreamPacket(particlePacket); + } + } + } + } + + private void playGrowlSound(GeyserSession session) { + Random random = ThreadLocalRandom.current(); + PlaySoundPacket playSoundPacket = new PlaySoundPacket(); + playSoundPacket.setSound("mob.enderdragon.growl"); + playSoundPacket.setPosition(position); + playSoundPacket.setVolume(2.5f); + playSoundPacket.setPitch(0.8f + random.nextFloat() * 0.3f); + session.sendUpstreamPacket(playSoundPacket); + } + + private boolean isAlive() { + return metadata.getFloat(EntityData.HEALTH) > 0; + } + + private boolean isHovering() { + return phase == 10; + } + + private boolean isSitting() { + return phase == 5 || phase == 6 || phase == 7; + } + /** * Store the current yaw and y into the circular buffer */ diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonPartEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonPartEntity.java index 095d12b2c..288a3e423 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonPartEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonPartEntity.java @@ -32,11 +32,12 @@ import org.geysermc.connector.entity.Entity; import org.geysermc.connector.entity.type.EntityType; public class EnderDragonPartEntity extends Entity { - public EnderDragonPartEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation, float width, float height) { - super(entityId, geyserId, entityType, position, motion, rotation); + public EnderDragonPartEntity(long entityId, long geyserId, EntityType entityType, float width, float height) { + super(entityId, geyserId, entityType, Vector3f.ZERO, Vector3f.ZERO, Vector3f.ZERO); metadata.put(EntityData.BOUNDING_BOX_WIDTH, width); metadata.put(EntityData.BOUNDING_BOX_HEIGHT, height); metadata.getFlags().setFlag(EntityFlag.INVISIBLE, true); + metadata.getFlags().setFlag(EntityFlag.FIRE_IMMUNE, true); } } diff --git a/connector/src/main/java/org/geysermc/connector/entity/player/PlayerEntity.java b/connector/src/main/java/org/geysermc/connector/entity/player/PlayerEntity.java index fc7b867bb..5cef3252a 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/player/PlayerEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/player/PlayerEntity.java @@ -103,7 +103,10 @@ public class PlayerEntity extends LivingEntity { long linkedEntityId = session.getEntityCache().getCachedPlayerEntityLink(entityId); if (linkedEntityId != -1) { - addPlayerPacket.getEntityLinks().add(new EntityLinkData(session.getEntityCache().getEntityByJavaId(linkedEntityId).getGeyserId(), geyserId, EntityLinkData.Type.RIDER, false)); + Entity linkedEntity = session.getEntityCache().getEntityByJavaId(linkedEntityId); + if (linkedEntity != null) { + addPlayerPacket.getEntityLinks().add(new EntityLinkData(linkedEntity.getGeyserId(), geyserId, EntityLinkData.Type.RIDER, false, false)); + } } valid = true; 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()); } /** diff --git a/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java b/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java index 79c04f674..87883087d 100644 --- a/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java +++ b/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java @@ -30,15 +30,16 @@ import com.nukkitx.protocol.bedrock.BedrockServerEventHandler; import com.nukkitx.protocol.bedrock.BedrockServerSession; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.socket.DatagramPacket; -import org.geysermc.connector.common.ping.GeyserPingInfo; import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.common.ping.GeyserPingInfo; import org.geysermc.connector.configuration.GeyserConfiguration; import org.geysermc.connector.network.session.GeyserSession; -import org.geysermc.connector.ping.IGeyserPingPassthrough; import org.geysermc.connector.network.translators.chat.MessageTranslator; +import org.geysermc.connector.ping.IGeyserPingPassthrough; import org.geysermc.connector.utils.LanguageUtils; import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; public class ConnectorServerEventHandler implements BedrockServerEventHandler { @@ -94,6 +95,20 @@ public class ConnectorServerEventHandler implements BedrockServerEventHandler { pong.setMaximumPlayerCount(config.getMaxPlayers()); } + // The ping will not appear if the MOTD + sub-MOTD is of a certain length. + // We don't know why, though + byte[] motdArray = pong.getMotd().getBytes(StandardCharsets.UTF_8); + if (motdArray.length + pong.getSubMotd().getBytes(StandardCharsets.UTF_8).length > 338) { + // Remove the sub-MOTD first since that only appears locally + pong.setSubMotd(""); + if (motdArray.length > 338) { + // If the top MOTD is still too long, we chop it down + byte[] newMotdArray = new byte[339]; + System.arraycopy(motdArray, 0, newMotdArray, 0, newMotdArray.length); + pong.setMotd(new String(newMotdArray, StandardCharsets.UTF_8)); + } + } + //Bedrock will not even attempt a connection if the client thinks the server is full //so we have to fake it not being full if (pong.getPlayerCount() >= pong.getMaximumPlayerCount()) { 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 9cd54e9dc..d22645467 100644 --- a/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java +++ b/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java @@ -33,6 +33,7 @@ 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; @@ -152,6 +153,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { if (info != null) { connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.stored_credentials", session.getAuthData().getName())); + session.setMicrosoftAccount(info.isMicrosoftAccount()); session.authenticate(info.getEmail(), info.getPassword()); // TODO send a message to bedrock user telling them they are connected (if nothing like a motd 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 3322ebb7b..8bdb7f2b1 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 @@ -26,8 +26,12 @@ package org.geysermc.connector.network.session; import com.github.steveice10.mc.auth.data.GameProfile; +import com.github.steveice10.mc.auth.exception.request.AuthPendingException; import com.github.steveice10.mc.auth.exception.request.InvalidCredentialsException; import com.github.steveice10.mc.auth.exception.request.RequestException; +import com.github.steveice10.mc.auth.service.AuthenticationService; +import com.github.steveice10.mc.auth.service.MojangAuthenticationService; +import com.github.steveice10.mc.auth.service.MsaAuthenticationService; import com.github.steveice10.mc.protocol.MinecraftConstants; import com.github.steveice10.mc.protocol.MinecraftProtocol; import com.github.steveice10.mc.protocol.data.SubProtocol; @@ -35,6 +39,7 @@ import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; import com.github.steveice10.mc.protocol.data.game.statistic.Statistic; import com.github.steveice10.mc.protocol.data.game.window.VillagerTrade; import com.github.steveice10.mc.protocol.packet.handshake.client.HandshakePacket; +import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerPositionPacket; import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerPositionRotationPacket; import com.github.steveice10.mc.protocol.packet.ingame.client.world.ClientTeleportConfirmPacket; import com.github.steveice10.mc.protocol.packet.login.server.LoginSuccessPacket; @@ -62,6 +67,7 @@ import lombok.Getter; import lombok.NonNull; import lombok.Setter; import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.entity.Tickable; import org.geysermc.connector.command.CommandSender; import org.geysermc.connector.common.AuthType; import org.geysermc.connector.entity.Entity; @@ -91,6 +97,7 @@ import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; @Getter public class GeyserSession implements CommandSender { @@ -104,9 +111,15 @@ public class GeyserSession implements CommandSender { @Setter private BedrockClientData clientData; + @Deprecated + @Setter + private boolean microsoftAccount; + private final SessionPlayerEntity playerEntity; private PlayerInventory inventory; + private AdvancementsCache advancementsCache; + private BookEditCache bookEditCache; private ChunkCache chunkCache; private EntityCache entityCache; private EntityEffectCache effectCache; @@ -242,15 +255,14 @@ public class GeyserSession implements CommandSender { private ScheduledFuture bucketScheduledFuture; /** - * Sends a movement packet every three seconds if the player hasn't moved. Prevents timeouts when AFK in certain instances. + * Used to send a movement packet every three seconds if the player hasn't moved. Prevents timeouts when AFK in certain instances. */ @Setter - private ScheduledFuture movementSendIfIdle; + private long lastMovementTimestamp = System.currentTimeMillis(); /** * Controls whether the daylight cycle gamerule has been sent to the client, so the sun/moon remain motionless. */ - @Setter private boolean daylightCycle = true; private boolean reducedDebugInfo = false; @@ -322,12 +334,19 @@ public class GeyserSession implements CommandSender { private List selectedEmotes = new ArrayList<>(); private final Set emotes = new HashSet<>(); + /** + * The thread that will run every 50 milliseconds - one Minecraft tick. + */ + private ScheduledFuture tickThread = null; + private MinecraftProtocol protocol; public GeyserSession(GeyserConnector connector, BedrockServerSession bedrockServerSession) { 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); this.effectCache = new EntityEffectCache(); @@ -345,7 +364,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)); @@ -427,131 +448,22 @@ public class GeyserSession implements CommandSender { new Thread(() -> { try { if (password != null && !password.isEmpty()) { - protocol = new MinecraftProtocol(username, password); + AuthenticationService authenticationService; + if (microsoftAccount) { + authenticationService = new MsaAuthenticationService(GeyserConnector.OAUTH_CLIENT_ID); + } else { + authenticationService = new MojangAuthenticationService(); + } + authenticationService.setUsername(username); + authenticationService.setPassword(password); + authenticationService.login(); + + protocol = new MinecraftProtocol(authenticationService); } else { protocol = new MinecraftProtocol(username); } - boolean floodgate = connector.getAuthType() == AuthType.FLOODGATE; - - downstream = new Client(remoteServer.getAddress(), remoteServer.getPort(), protocol, new TcpSessionFactory()); - if (connector.getConfig().getRemote().isUseProxyProtocol()) { - downstream.getSession().setFlag(BuiltinFlags.ENABLE_CLIENT_PROXY_PROTOCOL, true); - downstream.getSession().setFlag(BuiltinFlags.CLIENT_PROXIED_ADDRESS, upstream.getAddress()); - } - // Let Geyser handle sending the keep alive - downstream.getSession().setFlag(MinecraftConstants.AUTOMATIC_KEEP_ALIVE_MANAGEMENT, false); - downstream.getSession().addListener(new SessionAdapter() { - @Override - public void packetSending(PacketSendingEvent event) { - //todo move this somewhere else - if (event.getPacket() instanceof HandshakePacket && floodgate) { - byte[] encryptedData; - - try { - FloodgateCipher cipher = connector.getCipher(); - encryptedData = cipher.encryptFromString(BedrockData.of( - clientData.getGameVersion(), - authData.getName(), - authData.getXboxUUID(), - clientData.getDeviceOs().ordinal(), - clientData.getLanguageCode(), - clientData.getUiProfile().ordinal(), - clientData.getCurrentInputMode().ordinal(), - upstream.getSession().getAddress().getAddress().getHostAddress() - ).toString()); - } catch (Exception e) { - connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e); - disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.floodgate.encryption_fail", getClientData().getLanguageCode())); - return; - } - - byte[] rawSkin = clientData.getAndTransformImage("Skin").encode(); - byte[] finalData = new byte[encryptedData.length + rawSkin.length + 1]; - System.arraycopy(encryptedData, 0, finalData, 0, encryptedData.length); - finalData[encryptedData.length] = 0x21; // splitter - System.arraycopy(rawSkin, 0, finalData, encryptedData.length + 1, rawSkin.length); - - String finalDataString = new String(finalData, StandardCharsets.UTF_8); - - HandshakePacket handshakePacket = event.getPacket(); - event.setPacket(new HandshakePacket( - handshakePacket.getProtocolVersion(), - handshakePacket.getHostname() + '\0' + finalDataString, - handshakePacket.getPort(), - handshakePacket.getIntent() - )); - } - } - - @Override - public void connected(ConnectedEvent event) { - loggingIn = false; - loggedIn = true; - if (protocol.getProfile() == null) { - // Java account is offline - disconnect(LanguageUtils.getPlayerLocaleString("geyser.network.remote.invalid_account", clientData.getLanguageCode())); - return; - } - connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.connect", authData.getName(), protocol.getProfile().getName(), remoteServer.getAddress())); - playerEntity.setUuid(protocol.getProfile().getId()); - playerEntity.setUsername(protocol.getProfile().getName()); - - String locale = clientData.getLanguageCode(); - - // Let the user know there locale may take some time to download - // as it has to be extracted from a JAR - if (locale.toLowerCase().equals("en_us") && !LocaleUtils.LOCALE_MAPPINGS.containsKey("en_us")) { - // This should probably be left hardcoded as it will only show for en_us clients - sendMessage("Loading your locale (en_us); if this isn't already downloaded, this may take some time"); - } - - // Download and load the language for the player - LocaleUtils.downloadAndLoadLocale(locale); - } - - @Override - public void disconnected(DisconnectedEvent event) { - loggingIn = false; - loggedIn = false; - connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.disconnect", authData.getName(), remoteServer.getAddress(), event.getReason())); - if (event.getCause() != null) { - event.getCause().printStackTrace(); - } - - upstream.disconnect(MessageTranslator.convertMessageLenient(event.getReason())); - } - - @Override - public void packetReceived(PacketReceivedEvent event) { - if (!closed) { - // Required, or else Floodgate players break with Bukkit chunk caching - if (event.getPacket() instanceof LoginSuccessPacket) { - GameProfile profile = ((LoginSuccessPacket) event.getPacket()).getProfile(); - playerEntity.setUsername(profile.getName()); - playerEntity.setUuid(profile.getId()); - - // Check if they are not using a linked account - if (connector.getAuthType() == AuthType.OFFLINE || playerEntity.getUuid().getMostSignificantBits() == 0) { - SkinManager.handleBedrockSkin(playerEntity, clientData); - } - } - - PacketTranslatorRegistry.JAVA_TRANSLATOR.translate(event.getPacket().getClass(), event.getPacket(), GeyserSession.this); - } - } - - @Override - public void packetError(PacketErrorEvent event) { - connector.getLogger().warning(LanguageUtils.getLocaleStringLog("geyser.network.downstream_error", event.getCause().getMessage())); - if (connector.getConfig().isDebugMode()) - event.getCause().printStackTrace(); - event.setSuppress(true); - } - }); - - downstream.getSession().connect(); - connector.addPlayer(this); + connectDownstream(); } catch (InvalidCredentialsException | IllegalArgumentException e) { connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.login.invalid", username)); disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.invalid.kick", getClientData().getLanguageCode())); @@ -561,6 +473,194 @@ public class GeyserSession implements CommandSender { }).start(); } + /** + * Present a form window to the user asking to log in with another web browser + */ + public void authenticateWithMicrosoftCode() { + if (loggedIn) { + connector.getLogger().severe(LanguageUtils.getLocaleStringLog("geyser.auth.already_loggedin", getAuthData().getName())); + return; + } + + loggingIn = true; + // new thread so clients don't timeout + new Thread(() -> { + try { + MsaAuthenticationService msaAuthenticationService = new MsaAuthenticationService(GeyserConnector.OAUTH_CLIENT_ID); + + MsaAuthenticationService.MsCodeResponse response = msaAuthenticationService.getAuthCode(); + LoginEncryptionUtils.buildAndShowMicrosoftCodeWindow(this, response); + + // This just looks cool + SetTimePacket packet = new SetTimePacket(); + packet.setTime(16000); + sendUpstreamPacket(packet); + + // Wait for the code to validate + attemptCodeAuthentication(msaAuthenticationService); + } catch (InvalidCredentialsException | IllegalArgumentException e) { + connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.login.invalid", getAuthData().getName())); + disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.invalid.kick", getClientData().getLanguageCode())); + } catch (RequestException ex) { + ex.printStackTrace(); + } + }).start(); + } + + /** + * Poll every second to see if the user has successfully signed in + */ + private void attemptCodeAuthentication(MsaAuthenticationService msaAuthenticationService) { + if (loggedIn || closed) { + return; + } + try { + msaAuthenticationService.login(); + protocol = new MinecraftProtocol(msaAuthenticationService); + + connectDownstream(); + } catch (RequestException e) { + if (!(e instanceof AuthPendingException)) { + e.printStackTrace(); + } else { + // Wait one second before trying again + connector.getGeneralThreadPool().schedule(() -> attemptCodeAuthentication(msaAuthenticationService), 1, TimeUnit.SECONDS); + } + } + } + + /** + * After getting whatever credentials needed, we attempt to join the Java server. + */ + private void connectDownstream() { + boolean floodgate = connector.getAuthType() == AuthType.FLOODGATE; + + // Start ticking + tickThread = connector.getGeneralThreadPool().scheduleAtFixedRate(this::tick, 50, 50, TimeUnit.MILLISECONDS); + + downstream = new Client(remoteServer.getAddress(), remoteServer.getPort(), protocol, new TcpSessionFactory()); + if (connector.getConfig().getRemote().isUseProxyProtocol()) { + downstream.getSession().setFlag(BuiltinFlags.ENABLE_CLIENT_PROXY_PROTOCOL, true); + downstream.getSession().setFlag(BuiltinFlags.CLIENT_PROXIED_ADDRESS, upstream.getAddress()); + } + // Let Geyser handle sending the keep alive + downstream.getSession().setFlag(MinecraftConstants.AUTOMATIC_KEEP_ALIVE_MANAGEMENT, false); + downstream.getSession().addListener(new SessionAdapter() { + @Override + public void packetSending(PacketSendingEvent event) { + //todo move this somewhere else + if (event.getPacket() instanceof HandshakePacket && floodgate) { + byte[] encryptedData; + + try { + FloodgateCipher cipher = connector.getCipher(); + encryptedData = cipher.encryptFromString(BedrockData.of( + clientData.getGameVersion(), + authData.getName(), + authData.getXboxUUID(), + clientData.getDeviceOs().ordinal(), + clientData.getLanguageCode(), + clientData.getUiProfile().ordinal(), + clientData.getCurrentInputMode().ordinal(), + upstream.getSession().getAddress().getAddress().getHostAddress() + ).toString()); + } catch (Exception e) { + connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e); + disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.floodgate.encryption_fail", getClientData().getLanguageCode())); + return; + } + + byte[] rawSkin = clientData.getAndTransformImage("Skin").encode(); + byte[] finalData = new byte[encryptedData.length + rawSkin.length + 1]; + System.arraycopy(encryptedData, 0, finalData, 0, encryptedData.length); + finalData[encryptedData.length] = 0x21; // splitter + System.arraycopy(rawSkin, 0, finalData, encryptedData.length + 1, rawSkin.length); + + String finalDataString = new String(finalData, StandardCharsets.UTF_8); + + HandshakePacket handshakePacket = event.getPacket(); + event.setPacket(new HandshakePacket( + handshakePacket.getProtocolVersion(), + handshakePacket.getHostname() + '\0' + finalDataString, + handshakePacket.getPort(), + handshakePacket.getIntent() + )); + } + } + + @Override + public void connected(ConnectedEvent event) { + loggingIn = false; + loggedIn = true; + if (protocol.getProfile() == null) { + // Java account is offline + disconnect(LanguageUtils.getPlayerLocaleString("geyser.network.remote.invalid_account", clientData.getLanguageCode())); + return; + } + connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.connect", authData.getName(), protocol.getProfile().getName(), remoteServer.getAddress())); + playerEntity.setUuid(protocol.getProfile().getId()); + playerEntity.setUsername(protocol.getProfile().getName()); + + String locale = clientData.getLanguageCode(); + + // Let the user know there locale may take some time to download + // as it has to be extracted from a JAR + if (locale.equalsIgnoreCase("en_us") && !LocaleUtils.LOCALE_MAPPINGS.containsKey("en_us")) { + // This should probably be left hardcoded as it will only show for en_us clients + sendMessage("Loading your locale (en_us); if this isn't already downloaded, this may take some time"); + } + + // Download and load the language for the player + LocaleUtils.downloadAndLoadLocale(locale); + } + + @Override + public void disconnected(DisconnectedEvent event) { + loggingIn = false; + loggedIn = false; + connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.disconnect", authData.getName(), remoteServer.getAddress(), event.getReason())); + if (event.getCause() != null) { + event.getCause().printStackTrace(); + } + + upstream.disconnect(MessageTranslator.convertMessageLenient(event.getReason())); + } + + @Override + public void packetReceived(PacketReceivedEvent event) { + if (!closed) { + // Required, or else Floodgate players break with Bukkit chunk caching + if (event.getPacket() instanceof LoginSuccessPacket) { + GameProfile profile = ((LoginSuccessPacket) event.getPacket()).getProfile(); + playerEntity.setUsername(profile.getName()); + playerEntity.setUuid(profile.getId()); + + // Check if they are not using a linked account + if (connector.getAuthType() == AuthType.OFFLINE || playerEntity.getUuid().getMostSignificantBits() == 0) { + SkinManager.handleBedrockSkin(playerEntity, clientData); + } + } + + PacketTranslatorRegistry.JAVA_TRANSLATOR.translate(event.getPacket().getClass(), event.getPacket(), GeyserSession.this); + } + } + + @Override + public void packetError(PacketErrorEvent event) { + connector.getLogger().warning(LanguageUtils.getLocaleStringLog("geyser.network.downstream_error", event.getCause().getMessage())); + if (connector.getConfig().isDebugMode()) + event.getCause().printStackTrace(); + event.setSuppress(true); + } + }); + + if (!daylightCycle) { + setDaylightCycle(true); + } + downstream.getSession().connect(); + connector.addPlayer(this); + } + public void disconnect(String reason) { if (!closed) { loggedIn = false; @@ -573,6 +673,12 @@ public class GeyserSession implements CommandSender { } } + if (tickThread != null) { + tickThread.cancel(true); + } + + this.advancementsCache = null; + this.bookEditCache = null; this.chunkCache = null; this.entityCache = null; this.effectCache = null; @@ -587,6 +693,28 @@ public class GeyserSession implements CommandSender { disconnect(LanguageUtils.getPlayerLocaleString("geyser.network.close", getClientData().getLanguageCode())); } + /** + * Called every 50 milliseconds - one Minecraft tick. + */ + public void tick() { + // Check to see if the player's position needs updating - a position update should be sent once every 3 seconds + if (spawned && (System.currentTimeMillis() - lastMovementTimestamp) > 3000) { + // Recalculate in case something else changed position + Vector3d position = collisionManager.adjustBedrockPosition(playerEntity.getPosition(), playerEntity.isOnGround()); + // A null return value cancels the packet + if (position != null) { + ClientPlayerPositionPacket packet = new ClientPlayerPositionPacket(playerEntity.isOnGround(), + position.getX(), position.getY(), position.getZ()); + sendDownstreamPacket(packet); + } + lastMovementTimestamp = System.currentTimeMillis(); + } + + for (Tickable entity : entityCache.getTickableEntities()) { + entity.tick(this); + } + } + public void setAuthenticationData(AuthData authData) { this.authData = authData; } @@ -821,6 +949,18 @@ public class GeyserSession implements CommandSender { reducedDebugInfo = value; } + /** + * Changes the daylight cycle gamerule on the client + * This is used in the login screen along-side normal usage + * + * @param doCycle If the cycle should continue + */ + public void setDaylightCycle(boolean doCycle) { + sendGameRule("dodaylightcycle", doCycle); + // Save the value so we don't have to constantly send a daylight cycle gamerule update + this.daylightCycle = doCycle; + } + /** * Send a gamerule value to the client * 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..d20eb11dd --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/AdvancementsCache.java @@ -0,0 +1,276 @@ +/* + * 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 lombok.experimental.Accessors; +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 org.geysermc.cumulus.SimpleForm; +import org.geysermc.cumulus.response.SimpleFormResponse; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class AdvancementsCache { + /** + * 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 @Accessors(chain = true) + private String currentAdvancementCategoryId = null; + + private final GeyserSession session; + + public AdvancementsCache(GeyserSession session) { + this.session = session; + } + + /** + * Build and send a form with all advancement categories + */ + public void buildAndShowMenuForm() { + SimpleForm.Builder builder = + SimpleForm.builder() + .translator(LocaleUtils::getLocaleString, session.getLocale()) + .title("gui.advancements"); + + boolean hasAdvancements = false; + for (Map.Entry advancement : storedAdvancements.entrySet()) { + if (advancement.getValue().getParentId() == null) { // No parent means this is a root advancement + hasAdvancements = true; + builder.button(MessageTranslator.convertMessage(advancement.getValue().getDisplayData().getTitle(), session.getLocale())); + } + } + + if (!hasAdvancements) { + builder.content("advancements.empty"); + } + + builder.responseHandler((form, responseData) -> { + SimpleFormResponse response = form.parseResponse(responseData); + if (!response.isCorrect()) { + return; + } + + String id = ""; + + int advancementIndex = 0; + for (Map.Entry advancement : storedAdvancements.entrySet()) { + if (advancement.getValue().getParentId() == null) { // Root advancement + if (advancementIndex == response.getClickedButtonId()) { + id = advancement.getKey(); + break; + } else { + advancementIndex++; + } + } + } + + if (!id.equals("")) { + if (id.equals(currentAdvancementCategoryId)) { + // The server thinks we are already on this tab + buildAndShowListForm(); + } 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 + } + } + }); + + session.sendForm(builder); + } + + /** + * Build and send the list of advancements + */ + public void buildAndShowListForm() { + GeyserAdvancement categoryAdvancement = storedAdvancements.get(currentAdvancementCategoryId); + String language = session.getLocale(); + + SimpleForm.Builder builder = + SimpleForm.builder() + .title(MessageTranslator.convertMessage(categoryAdvancement.getDisplayData().getTitle(), language)) + .content(MessageTranslator.convertMessage(categoryAdvancement.getDisplayData().getDescription(), language)); + + if (currentAdvancementCategoryId != null) { + for (GeyserAdvancement advancement : storedAdvancements.values()) { + if (advancement != null) { + if (advancement.getParentId() != null && currentAdvancementCategoryId.equals(advancement.getRootId(this))) { + boolean color = isEarned(advancement) || !advancement.getDisplayData().isShowToast(); + builder.button((color ? "§6" : "") + MessageTranslator.convertMessage(advancement.getDisplayData().getTitle()) + '\n'); + } + } + } + } + + builder.button(LanguageUtils.getPlayerLocaleString("gui.back", language)); + + builder.responseHandler((form, responseData) -> { + SimpleFormResponse response = form.parseResponse(responseData); + if (!response.isCorrect()) { + // Indicate that we have closed the current advancement tab + session.sendDownstreamPacket(new ClientAdvancementTabPacket()); + return; + } + + 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 == response.getClickedButtonId()) { + advancement = advancementEntry; + break; + } else { + advancementIndex++; + } + } + } + + if (advancement != null) { + buildAndShowInfoForm(advancement); + } else { + buildAndShowMenuForm(); + // Indicate that we have closed the current advancement tab + session.sendDownstreamPacket(new ClientAdvancementTabPacket()); + } + }); + + session.sendForm(builder); + } + + /** + * Builds the advancement display info based on the chosen category + * + * @param advancement The advancement used to create the info display + */ + public void buildAndShowInfoForm(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)); + } + + session.sendForm( + SimpleForm.builder() + .title(MessageTranslator.convertMessage(advancement.getDisplayData().getTitle())) + .content(content) + .button(LanguageUtils.getPlayerLocaleString("gui.back", language)) + .responseHandler((form, responseData) -> { + SimpleFormResponse response = form.parseResponse(responseData); + if (response.isCorrect()) { + buildAndShowListForm(); + } + }) + ); + } + + /** + * 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; + } + + 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/BookEditCache.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/BookEditCache.java new file mode 100644 index 000000000..f81a9fdf9 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/BookEditCache.java @@ -0,0 +1,75 @@ +/* + * 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.entity.metadata.ItemStack; +import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientEditBookPacket; +import lombok.Setter; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.item.ItemRegistry; + +/** + * Manages updating the current writable book. + * + * Java sends book updates less frequently than Bedrock, and this can cause issues with servers that rate limit + * book packets. Because of this, we need to ensure packets are only send every second or so at maximum. + */ +public class BookEditCache { + private final GeyserSession session; + @Setter + private ClientEditBookPacket packet; + /** + * Stores the last time a book update packet was sent to the server. + */ + private long lastBookUpdate; + + public BookEditCache(GeyserSession session) { + this.session = session; + } + + /** + * Check to see if there is a book edit update to send, and if so, send it. + */ + public void checkForSend() { + if (packet == null) { + // No new packet has to be sent + return; + } + // Prevent kicks due to rate limiting - specifically on Spigot servers + if ((System.currentTimeMillis() - lastBookUpdate) < 1000) { + return; + } + // Don't send the update if the player isn't not holding a book, shouldn't happen if we catch all interactions + ItemStack itemStack = session.getInventory().getItemInHand(); + if (itemStack == null || itemStack.getId() != ItemRegistry.WRITABLE_BOOK.getJavaId()) { + packet = null; + return; + } + session.getDownstream().getSession().send(packet); + packet = null; + lastBookUpdate = System.currentTimeMillis(); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java index 62b0dbd6b..a2eb60053 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java @@ -28,6 +28,7 @@ package org.geysermc.connector.network.session.cache; import it.unimi.dsi.fastutil.longs.*; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import lombok.Getter; +import org.geysermc.connector.entity.Tickable; import org.geysermc.connector.entity.Entity; import org.geysermc.connector.entity.player.PlayerEntity; import org.geysermc.connector.network.session.GeyserSession; @@ -40,17 +41,21 @@ import java.util.concurrent.atomic.AtomicLong; * for that player (e.g. seeing vanished players from /vanish) */ public class EntityCache { - private GeyserSession session; + private final GeyserSession session; @Getter private Long2ObjectMap entities = Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>()); + /** + * A list of all entities that must be ticked. + */ + private final List tickableEntities = Collections.synchronizedList(new ArrayList<>()); private Long2LongMap entityIdTranslations = Long2LongMaps.synchronize(new Long2LongOpenHashMap()); private Map playerEntities = Collections.synchronizedMap(new HashMap<>()); private Map bossBars = Collections.synchronizedMap(new HashMap<>()); - private Long2LongMap cachedPlayerEntityLinks = Long2LongMaps.synchronize(new Long2LongOpenHashMap()); + private final Long2LongMap cachedPlayerEntityLinks = Long2LongMaps.synchronize(new Long2LongOpenHashMap()); @Getter - private AtomicLong nextEntityId = new AtomicLong(2L); + private final AtomicLong nextEntityId = new AtomicLong(2L); public EntityCache(GeyserSession session) { this.session = session; @@ -59,6 +64,11 @@ public class EntityCache { public void spawnEntity(Entity entity) { if (cacheEntity(entity)) { entity.spawnEntity(session); + + if (entity instanceof Tickable) { + // Start ticking it + tickableEntities.add((Tickable) entity); + } } } @@ -76,6 +86,10 @@ public class EntityCache { if (entity != null && entity.isValid() && (force || entity.despawnEntity(session))) { long geyserId = entityIdTranslations.remove(entity.getEntityId()); entities.remove(geyserId); + + if (entity instanceof Tickable) { + tickableEntities.remove(entity); + } return true; } return false; @@ -114,8 +128,8 @@ public class EntityCache { return playerEntities.get(uuid); } - public void removePlayerEntity(UUID uuid) { - playerEntities.remove(uuid); + public PlayerEntity removePlayerEntity(UUID uuid) { + return playerEntities.remove(uuid); } public void addBossBar(UUID uuid, BossBar bossBar) { @@ -152,4 +166,8 @@ public class EntityCache { public void addCachedPlayerEntityLink(long playerId, long linkedEntityId) { cachedPlayerEntityLinks.put(playerId, linkedEntityId); } + + public List getTickableEntities() { + return tickableEntities; + } } 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 new file mode 100644 index 000000000..e69de29bb diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockBookEditTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockBookEditTranslator.java new file mode 100644 index 000000000..dd5d08a2c --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockBookEditTranslator.java @@ -0,0 +1,123 @@ +/* + * 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.bedrock; + +import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; +import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; +import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientEditBookPacket; +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.github.steveice10.opennbt.tag.builtin.ListTag; +import com.github.steveice10.opennbt.tag.builtin.StringTag; +import com.github.steveice10.opennbt.tag.builtin.Tag; +import com.nukkitx.protocol.bedrock.packet.BookEditPacket; +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.inventory.InventoryTranslator; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +@Translator(packet = BookEditPacket.class) +public class BedrockBookEditTranslator extends PacketTranslator { + + @Override + public void translate(BookEditPacket packet, GeyserSession session) { + ItemStack itemStack = session.getInventory().getItemInHand(); + if (itemStack != null) { + CompoundTag tag = itemStack.getNbt() != null ? itemStack.getNbt() : new CompoundTag(""); + ItemStack bookItem = new ItemStack(itemStack.getId(), itemStack.getAmount(), tag); + List pages = tag.contains("pages") ? new LinkedList<>(((ListTag) tag.get("pages")).getValue()) : new LinkedList<>(); + + int page = packet.getPageNumber(); + // Creative edits the NBT for us + if (session.getGameMode() != GameMode.CREATIVE) { + switch (packet.getAction()) { + case ADD_PAGE: { + // Add empty pages in between + for (int i = pages.size(); i < page; i++) { + pages.add(i, new StringTag("", "")); + } + pages.add(page, new StringTag("", packet.getText())); + break; + } + // Called whenever a page is modified + case REPLACE_PAGE: { + if (page < pages.size()) { + pages.set(page, new StringTag("", packet.getText())); + } else { + // Add empty pages in between + for (int i = pages.size(); i < page; i++) { + pages.add(i, new StringTag("", "")); + } + pages.add(page, new StringTag("", packet.getText())); + } + break; + } + case DELETE_PAGE: { + if (page < pages.size()) { + pages.remove(page); + } + break; + } + case SWAP_PAGES: { + int page2 = packet.getSecondaryPageNumber(); + if (page < pages.size() && page2 < pages.size()) { + Collections.swap(pages, page, page2); + } + break; + } + case SIGN_BOOK: { + tag.put(new StringTag("author", packet.getAuthor())); + tag.put(new StringTag("title", packet.getTitle())); + break; + } + default: + return; + } + } + // Remove empty pages at the end + while (pages.size() > 0) { + StringTag currentPage = (StringTag) pages.get(pages.size() - 1); + if (currentPage.getValue() == null || currentPage.getValue().isEmpty()) { + pages.remove(pages.size() - 1); + } else { + break; + } + } + tag.put(new ListTag("pages", pages)); + session.getInventory().setItem(36 + session.getInventory().getHeldItemSlot(), bookItem); + InventoryTranslator.INVENTORY_TRANSLATORS.get(null).updateInventory(session, session.getInventory()); + + session.getBookEditCache().setPacket(new ClientEditBookPacket(bookItem, packet.getAction() == BookEditPacket.Action.SIGN_BOOK, session.getInventory().getHeldItemSlot())); + // There won't be any more book updates after this, so we can try sending the edit packet immediately + if (packet.getAction() == BookEditPacket.Action.SIGN_BOOK) { + session.getBookEditCache().checkForSend(); + } + } + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockCommandRequestTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockCommandRequestTranslator.java index 6ff29f5cc..a9ed15cef 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockCommandRequestTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockCommandRequestTranslator.java @@ -43,7 +43,7 @@ public class BedrockCommandRequestTranslator extends PacketTranslator { + private static final float MAXIMUM_BLOCK_PLACING_DISTANCE = 64f; + private static final int CREATIVE_EYE_HEIGHT_PLACE_DISTANCE = 49; + private static final int SURVIVAL_EYE_HEIGHT_PLACE_DISTANCE = 36; + private static final float MAXIMUM_BLOCK_DESTROYING_DISTANCE = 36f; + @Override public void translate(InventoryTransactionPacket packet, GeyserSession session) { + // Send book updates before opening inventories + session.getBookEditCache().checkForSend(); + switch (packet.getTransactionType()) { case NORMAL: Inventory inventory = session.getInventoryCache().getOpenInventory(); @@ -109,6 +120,46 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator + (session.getGameMode().equals(GameMode.CREATIVE) ? CREATIVE_EYE_HEIGHT_PLACE_DISTANCE : SURVIVAL_EYE_HEIGHT_PLACE_DISTANCE)) { + restoreCorrectBlock(session, blockPos, packet); + return; + } + + // Vanilla check + if (!(session.getPlayerEntity().getPosition().sub(0, EntityType.PLAYER.getOffset(), 0) + .distanceSquared(packet.getBlockPosition().toFloat().add(0.5f, 0.5f, 0.5f)) < MAXIMUM_BLOCK_PLACING_DISTANCE)) { + // The client thinks that its blocks have been successfully placed. Restore the server's blocks instead. + restoreCorrectBlock(session, blockPos, packet); + return; + } + /* + Block place checks end - client is good to go + */ + ClientPlayerPlaceBlockPacket blockPacket = new ClientPlayerPlaceBlockPacket( new Position(packet.getBlockPosition().getX(), packet.getBlockPosition().getY(), packet.getBlockPosition().getZ()), BlockFace.values()[packet.getBlockFace()], @@ -156,7 +207,6 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator MAXIMUM_BLOCK_DESTROYING_DISTANCE) { + restoreCorrectBlock(session, packet.getBlockPosition(), packet); + return; } + LevelEventPacket blockBreakPacket = new LevelEventPacket(); + blockBreakPacket.setType(LevelEventType.PARTICLE_DESTROY_BLOCK); + blockBreakPacket.setPosition(packet.getBlockPosition().toFloat()); + blockBreakPacket.setData(BlockTranslator.getBedrockBlockId(blockState)); + session.sendUpstreamPacket(blockBreakPacket); + session.setBreakingBlock(BlockTranslator.JAVA_AIR_ID); + long frameEntityId = ItemFrameEntity.getItemFrameEntityId(session, packet.getBlockPosition()); if (frameEntityId != -1 && session.getEntityCache().getEntityByJavaId(frameEntityId) != null) { ClientPlayerInteractEntityPacket attackPacket = new ClientPlayerInteractEntityPacket((int) frameEntityId, InteractAction.ATTACK, session.isSneaking()); @@ -267,4 +330,34 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator { @@ -63,9 +59,10 @@ public class BedrockMovePlayerTranslator extends PacketTranslator sendPositionIfIdle(session), - 3, TimeUnit.SECONDS)); } - public boolean isValidMove(GeyserSession session, MovePlayerPacket.Mode mode, Vector3f currentPosition, Vector3f newPosition) { + private boolean isValidMove(GeyserSession session, MovePlayerPacket.Mode mode, Vector3f currentPosition, Vector3f newPosition) { if (mode != MovePlayerPacket.Mode.NORMAL) return true; @@ -171,81 +164,5 @@ public class BedrockMovePlayerTranslator extends PacketTranslator sendPositionIfIdle(session), - 3, TimeUnit.SECONDS)); - } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/collision/CollisionManager.java b/connector/src/main/java/org/geysermc/connector/network/translators/collision/CollisionManager.java index 22e5c95fd..203e4406f 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/collision/CollisionManager.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/collision/CollisionManager.java @@ -30,8 +30,12 @@ import com.nukkitx.math.vector.Vector3f; import com.nukkitx.math.vector.Vector3i; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import com.nukkitx.protocol.bedrock.data.entity.EntityFlags; +import com.nukkitx.protocol.bedrock.packet.MovePlayerPacket; +import com.nukkitx.protocol.bedrock.packet.SetEntityDataPacket; import lombok.Getter; import lombok.Setter; +import org.geysermc.connector.entity.player.PlayerEntity; +import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.collision.translators.BlockCollision; @@ -105,6 +109,7 @@ public class CollisionManager { // According to the Minecraft Wiki, when sneaking: // - In Bedrock Edition, the height becomes 1.65 blocks, allowing movement through spaces as small as 1.75 (2 - 1⁄4) blocks high. // - In Java Edition, the height becomes 1.5 blocks. + // TODO: Have this depend on the player's literal bounding box variable if (session.isSneaking()) { playerBoundingBox.setSizeY(1.5); } else { @@ -113,6 +118,65 @@ public class CollisionManager { } } + /** + * Adjust the Bedrock position before sending to the Java server to account for inaccuracies in movement between + * the two versions. + * + * @param bedrockPosition the current Bedrock position of the client + * @param onGround whether the Bedrock player is on the ground + * @return the position to send to the Java server, or null to cancel sending the packet + */ + public Vector3d adjustBedrockPosition(Vector3f bedrockPosition, boolean onGround) { + // We need to parse the float as a string since casting a float to a double causes us to + // lose precision and thus, causes players to get stuck when walking near walls + double javaY = bedrockPosition.getY() - EntityType.PLAYER.getOffset(); + + Vector3d position = Vector3d.from(Double.parseDouble(Float.toString(bedrockPosition.getX())), javaY, + Double.parseDouble(Float.toString(bedrockPosition.getZ()))); + + if (session.getConnector().getConfig().isCacheChunks()) { + // With chunk caching, we can do some proper collision checks + updatePlayerBoundingBox(position); + + // Correct player position + if (!correctPlayerPosition()) { + // Cancel the movement if it needs to be cancelled + recalculatePosition(); + return null; + } + + position = Vector3d.from(playerBoundingBox.getMiddleX(), + playerBoundingBox.getMiddleY() - (playerBoundingBox.getSizeY() / 2), + playerBoundingBox.getMiddleZ()); + } else { + // When chunk caching is off, we have to rely on this + // It rounds the Y position up to the nearest 0.5 + // This snaps players to snap to the top of stairs and slabs like on Java Edition + // However, it causes issues such as the player floating on carpets + if (onGround) javaY = Math.ceil(javaY * 2) / 2; + position = position.up(javaY - position.getY()); + } + + return position; + } + + // TODO: This makes the player look upwards for some reason, rotation values must be wrong + public void recalculatePosition() { + PlayerEntity entity = session.getPlayerEntity(); + // Gravity might need to be reset... + SetEntityDataPacket entityDataPacket = new SetEntityDataPacket(); + entityDataPacket.setRuntimeEntityId(entity.getGeyserId()); + entityDataPacket.getMetadata().putAll(entity.getMetadata()); + session.sendUpstreamPacket(entityDataPacket); + + MovePlayerPacket movePlayerPacket = new MovePlayerPacket(); + movePlayerPacket.setRuntimeEntityId(entity.getGeyserId()); + movePlayerPacket.setPosition(entity.getPosition()); + movePlayerPacket.setRotation(entity.getBedrockRotation()); + movePlayerPacket.setMode(MovePlayerPacket.Mode.NORMAL); + session.sendUpstreamPacket(movePlayerPacket); + } + public List getPlayerCollidableBlocks() { List blocks = new ArrayList<>(); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemRegistry.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemRegistry.java index 02cd839aa..e9b821588 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemRegistry.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemRegistry.java @@ -95,6 +95,10 @@ public class ItemRegistry { * Wheat item entry, used in AbstractHorseEntity.java */ public static ItemEntry WHEAT; + /** + * Writable book item entry, used in BedrockBookEditTranslator.java + */ + public static ItemEntry WRITABLE_BOOK; public static int BARRIER_INDEX = 0; @@ -190,6 +194,9 @@ public class ItemRegistry { case "minecraft:wheat": WHEAT = ITEM_ENTRIES.get(itemIndex); break; + case "minecraft:writable_book": + WRITABLE_BOOK = ITEM_ENTRIES.get(itemIndex); + break; default: break; } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/BookPagesTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/BookPagesTranslator.java index cf97f643c..90eef3bce 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/BookPagesTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/BookPagesTranslator.java @@ -78,9 +78,8 @@ public class BookPagesTranslator extends NbtItemStackTranslator { CompoundTag pageTag = (CompoundTag) tag; StringTag textTag = pageTag.get("text"); - pages.add(new StringTag(MessageTranslator.convertToJavaMessage(textTag.getValue()))); + pages.add(new StringTag("", textTag.getValue())); } - itemTag.remove("pages"); itemTag.put(new ListTag("pages", pages)); } 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..80b9f9155 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaAdvancementsTabTranslator.java @@ -0,0 +1,44 @@ +/* + * 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.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()) + .buildAndShowListForm(); + } +} 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/network/translators/java/entity/JavaEntityStatusTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityStatusTranslator.java index 107282648..59ea29925 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityStatusTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityStatusTranslator.java @@ -31,8 +31,8 @@ import com.nukkitx.protocol.bedrock.data.LevelEventType; import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.data.entity.EntityEventType; import com.nukkitx.protocol.bedrock.packet.EntityEventPacket; -import com.nukkitx.protocol.bedrock.packet.LevelSoundEvent2Packet; import com.nukkitx.protocol.bedrock.packet.LevelEventPacket; +import com.nukkitx.protocol.bedrock.packet.LevelSoundEvent2Packet; import com.nukkitx.protocol.bedrock.packet.SetEntityDataPacket; import com.nukkitx.protocol.bedrock.packet.SetEntityMotionPacket; import org.geysermc.connector.entity.Entity; @@ -183,6 +183,19 @@ public class JavaEntityStatusTranslator extends PacketTranslator { @Override @@ -57,9 +56,6 @@ public class JavaPlayerListEntryTranslator extends PacketTranslator - GeyserConnector.getInstance().getLogger().debug("Loaded Local Bedrock Java Skin Data")); } else { playerEntity = session.getEntityCache().getPlayerEntity(entry.getProfile().getId()); } @@ -74,27 +70,35 @@ public class JavaPlayerListEntryTranslator extends PacketTranslator + GeyserConnector.getInstance().getLogger().debug("Loaded Local Bedrock Java Skin Data for " + session.getClientData().getUsername())); + } else { + playerEntity.setValid(true); + PlayerListPacket.Entry playerListEntry = SkinManager.buildCachedEntry(session, playerEntity); - translate.getEntries().add(playerListEntry); + translate.getEntries().add(playerListEntry); + } break; case REMOVE_PLAYER: - PlayerEntity entity = session.getEntityCache().getPlayerEntity(entry.getProfile().getId()); + // As the player entity is no longer present, we can remove the entry + PlayerEntity entity = session.getEntityCache().removePlayerEntity(entry.getProfile().getId()); if (entity != null) { // Just remove the entity's player list status // Don't despawn the entity - the Java server will also take care of that. entity.setPlayerList(false); } - // As the player entity is no longer present, we can remove the entry - session.getEntityCache().removePlayerEntity(entry.getProfile().getId()); if (entity == session.getPlayerEntity()) { // If removing ourself we use our AuthData UUID translate.getEntries().add(new PlayerListPacket.Entry(session.getAuthData().getUUID())); @@ -105,7 +109,7 @@ public class JavaPlayerListEntryTranslator extends PacketTranslator { @Override public void translate(ServerBlockChangePacket packet, GeyserSession session) { Position pos = packet.getRecord().getPosition(); - boolean updatePlacement = !(session.getConnector().getConfig().isCacheChunks() && session.getConnector().getWorldManager().getBlockAt(session, pos.getX(), pos.getY(), pos.getZ()) == packet.getRecord().getBlock()); - ChunkUtils.updateBlock(session, packet.getRecord().getBlock(), packet.getRecord().getPosition()); - if (updatePlacement && session.getConnector().getPlatformType() != PlatformType.SPIGOT) { + boolean updatePlacement = session.getConnector().getPlatformType() != PlatformType.SPIGOT && // Spigot simply listens for the block place event + !(session.getConnector().getConfig().isCacheChunks() && + session.getConnector().getWorldManager().getBlockAt(session, pos) == packet.getRecord().getBlock()); + ChunkUtils.updateBlock(session, packet.getRecord().getBlock(), pos); + if (updatePlacement) { this.checkPlace(session, packet); } this.checkInteract(session, packet); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaUpdateTimeTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaUpdateTimeTranslator.java index dd1ec68a3..461d8139d 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaUpdateTimeTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaUpdateTimeTranslator.java @@ -46,17 +46,10 @@ public class JavaUpdateTimeTranslator extends PacketTranslator= 0) { // Client thinks there is no daylight cycle but there is - setDoDaylightCycleGamerule(session, true); + session.setDaylightCycle(true); } else if (session.isDaylightCycle() && time < 0) { // Client thinks there is daylight cycle but there isn't - setDoDaylightCycleGamerule(session, false); + session.setDaylightCycle(false); } } - - private void setDoDaylightCycleGamerule(GeyserSession session, boolean doCycle) { - session.sendGameRule("dodaylightcycle", doCycle); - // Save the value so we don't have to constantly send a daylight cycle gamerule update - session.setDaylightCycle(doCycle); - } - } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockTranslator.java index db6f43fea..b047999e7 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockTranslator.java @@ -87,7 +87,9 @@ public class BlockTranslator { */ public static final int BEDROCK_RUNTIME_COMMAND_BLOCK_ID; - // For block breaking animation math + /** + * A list of all Java runtime wool IDs, for use with block breaking math and shears + */ public static final IntSet JAVA_RUNTIME_WOOL_IDS = new IntOpenHashSet(); public static final int JAVA_RUNTIME_COBWEB_ID; diff --git a/connector/src/main/java/org/geysermc/connector/skin/SkinManager.java b/connector/src/main/java/org/geysermc/connector/skin/SkinManager.java index b39e7f352..ae3abc943 100644 --- a/connector/src/main/java/org/geysermc/connector/skin/SkinManager.java +++ b/connector/src/main/java/org/geysermc/connector/skin/SkinManager.java @@ -33,7 +33,6 @@ import com.nukkitx.protocol.bedrock.packet.PlayerListPacket; import lombok.AllArgsConstructor; import lombok.Getter; import org.geysermc.connector.GeyserConnector; -import org.geysermc.connector.common.AuthType; import org.geysermc.connector.entity.player.PlayerEntity; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.auth.BedrockClientData; @@ -47,6 +46,9 @@ import java.util.function.Consumer; public class SkinManager { + /** + * Builds a Bedrock player list entry from our existing, cached Bedrock skin information + */ public static PlayerListPacket.Entry buildCachedEntry(GeyserSession session, PlayerEntity playerEntity) { GameProfileData data = GameProfileData.from(playerEntity.getProfile()); SkinProvider.Cape cape = SkinProvider.getCachedCape(data.getCapeUrl()); @@ -70,27 +72,31 @@ public class SkinManager { ); } + /** + * With all the information needed, build a Bedrock player entry with translated skin information. + */ public static PlayerListPacket.Entry buildEntryManually(GeyserSession session, UUID uuid, String username, long geyserId, String skinId, byte[] skinData, String capeId, byte[] capeData, SkinProvider.SkinGeometry geometry) { SerializedSkin serializedSkin = SerializedSkin.of( skinId, geometry.getGeometryName(), ImageData.of(skinData), Collections.emptyList(), - ImageData.of(capeData), geometry.getGeometryData(), "", true, false, !capeId.equals(SkinProvider.EMPTY_CAPE.getCapeId()), capeId, skinId + ImageData.of(capeData), geometry.getGeometryData(), "", true, false, + !capeId.equals(SkinProvider.EMPTY_CAPE.getCapeId()), capeId, skinId ); - // This attempts to find the xuid of the player so profile images show up for xbox accounts + // This attempts to find the XUID of the player so profile images show up for Xbox accounts String xuid = ""; - GeyserSession player = GeyserConnector.getInstance().getPlayerByUuid(uuid); + GeyserSession playerSession = GeyserConnector.getInstance().getPlayerByUuid(uuid); - if (player != null) { - xuid = player.getAuthData().getXboxUUID(); + if (playerSession != null) { + xuid = playerSession.getAuthData().getXboxUUID(); } PlayerListPacket.Entry entry; // If we are building a PlayerListEntry for our own session we use our AuthData UUID instead of the Java UUID - // as bedrock expects to get back its own provided uuid + // as Bedrock expects to get back its own provided UUID if (session.getPlayerEntity().getUuid().equals(uuid)) { entry = new PlayerListPacket.Entry(session.getAuthData().getUUID()); } else { @@ -134,12 +140,13 @@ public class SkinManager { geometry, entity.getUuid() ), geometry, 3); + boolean isDeadmau5 = "deadmau5".equals(entity.getUsername()); // Not a bedrock player check for ears - if (geometry.isFailed() && SkinProvider.ALLOW_THIRD_PARTY_EARS) { + if (geometry.isFailed() && (SkinProvider.ALLOW_THIRD_PARTY_EARS || isDeadmau5)) { boolean isEars; // Its deadmau5, gotta support his skin :) - if (entity.getUuid().toString().equals("1e18d5ff-643d-45c8-b509-43b8461d8614")) { + if (isDeadmau5) { isEars = true; } else { // Get the ears texture for the player @@ -185,7 +192,6 @@ public class SkinManager { playerRemovePacket.setAction(PlayerListPacket.Action.REMOVE); playerRemovePacket.getEntries().add(updatedEntry); session.sendUpstreamPacket(playerRemovePacket); - } } } catch (Exception e) { @@ -238,20 +244,20 @@ public class SkinManager { * @return The built GameProfileData */ public static GameProfileData from(GameProfile profile) { - // Fallback to the offline mode of working it out - boolean isAlex = (Math.abs(profile.getId().hashCode() % 2) == 1); - try { GameProfile.Property skinProperty = profile.getProperty("textures"); - // TODO: Remove try/catch here + if (skinProperty == null) { + // Likely offline mode + return loadBedrockOrOfflineSkin(profile); + } JsonNode skinObject = GeyserConnector.JSON_MAPPER.readTree(new String(Base64.getDecoder().decode(skinProperty.getValue()), StandardCharsets.UTF_8)); JsonNode textures = skinObject.get("textures"); JsonNode skinTexture = textures.get("SKIN"); String skinUrl = skinTexture.get("url").asText().replace("http://", "https://"); - isAlex = skinTexture.has("metadata"); + boolean isAlex = skinTexture.has("metadata"); String capeUrl = null; if (textures.has("CAPE")) { @@ -261,20 +267,30 @@ public class SkinManager { return new GameProfileData(skinUrl, capeUrl, isAlex); } catch (Exception exception) { - if (GeyserConnector.getInstance().getAuthType() != AuthType.OFFLINE) { - GeyserConnector.getInstance().getLogger().debug("Got invalid texture data for " + profile.getName() + " " + exception.getMessage()); - } - // return default skin with default cape when texture data is invalid - String skinUrl = isAlex ? SkinProvider.EMPTY_SKIN_ALEX.getTextureUrl() : SkinProvider.EMPTY_SKIN.getTextureUrl(); - if ("steve".equals(skinUrl) || "alex".equals(skinUrl)) { - GeyserSession session = GeyserConnector.getInstance().getPlayerByUuid(profile.getId()); - - if (session != null) { - skinUrl = session.getClientData().getSkinId(); - } - } - return new GameProfileData(skinUrl, SkinProvider.EMPTY_CAPE.getTextureUrl(), isAlex); + GeyserConnector.getInstance().getLogger().debug("Something went wrong while processing skin for " + profile.getName() + ": " + exception.getMessage()); + return loadBedrockOrOfflineSkin(profile); } } + + /** + * @return default skin with default cape when texture data is invalid, or the Bedrock player's skin if this + * is a Bedrock player. + */ + private static GameProfileData loadBedrockOrOfflineSkin(GameProfile profile) { + // Fallback to the offline mode of working it out + boolean isAlex = (Math.abs(profile.getId().hashCode() % 2) == 1); + + String skinUrl = isAlex ? SkinProvider.EMPTY_SKIN_ALEX.getTextureUrl() : SkinProvider.EMPTY_SKIN.getTextureUrl(); + String capeUrl = SkinProvider.EMPTY_CAPE.getTextureUrl(); + if ("steve".equals(skinUrl) || "alex".equals(skinUrl)) { + GeyserSession session = GeyserConnector.getInstance().getPlayerByUuid(profile.getId()); + + if (session != null) { + skinUrl = session.getClientData().getSkinId(); + capeUrl = session.getClientData().getCapeId(); + } + } + return new GameProfileData(skinUrl, capeUrl, isAlex); + } } } diff --git a/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java b/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java index 93909b20f..c859b9f65 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java @@ -50,6 +50,7 @@ public class BlockUtils { if (toolType.equals("shears")) return isWoolBlock ? 5.0 : 15.0; if (toolType.equals("")) return 1.0; switch (toolTier) { + // https://minecraft.gamepedia.com/Breaking#Speed case "wooden": return 2.0; case "stone": @@ -58,6 +59,8 @@ public class BlockUtils { return 6.0; case "diamond": return 8.0; + case "netherite": + return 9.0; case "golden": return 12.0; default: diff --git a/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java b/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java index f330aed67..f193a61db 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java @@ -58,10 +58,6 @@ public class DimensionUtils { int bedrockDimension = javaToBedrock(javaDimension); Entity player = session.getPlayerEntity(); - if (session.getMovementSendIfIdle() != null) { - session.getMovementSendIfIdle().cancel(true); - } - session.getEntityCache().removeAllEntities(); session.getItemFrameCache().clear(); session.getSkullCache().clear(); 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/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/java/org/geysermc/connector/utils/LanguageUtils.java b/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java index b58534d7e..a70c291f0 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java @@ -192,7 +192,11 @@ public class LanguageUtils { if (FileUtils.class.getResource("/languages/texts/" + locale + ".properties") == null) { result = false; if (GeyserConnector.getInstance() != null && GeyserConnector.getInstance().getLogger() != null) { // Could be too early for these to be initialized - GeyserConnector.getInstance().getLogger().warning(locale + " is not a valid Bedrock language."); // We can't translate this since we just loaded an invalid language + if (locale.equals("en_US")) { + GeyserConnector.getInstance().getLogger().error("English locale not found in Geyser. Did you clone the submodules? (git submodule update --init)"); + } else { + GeyserConnector.getInstance().getLogger().warning(locale + " is not a valid Bedrock language."); // We can't translate this since we just loaded an invalid language + } } } else { if (!LOCALE_MAPPINGS.containsKey(locale)) { 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..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(); @@ -208,6 +209,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)); } diff --git a/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java b/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java index bcc73ce09..00c7aea0f 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java @@ -29,18 +29,22 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.JsonNodeType; +import com.github.steveice10.mc.auth.service.MsaAuthenticationService; import com.nimbusds.jose.JWSObject; import com.nukkitx.network.util.Preconditions; import com.nukkitx.protocol.bedrock.packet.LoginPacket; import com.nukkitx.protocol.bedrock.packet.ServerToClientHandshakePacket; import com.nukkitx.protocol.bedrock.util.EncryptionUtils; import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.configuration.GeyserConfiguration; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.auth.AuthData; import org.geysermc.connector.network.session.auth.BedrockClientData; import org.geysermc.cumulus.CustomForm; +import org.geysermc.cumulus.ModalForm; import org.geysermc.cumulus.SimpleForm; import org.geysermc.cumulus.response.CustomFormResponse; +import org.geysermc.cumulus.response.ModalFormResponse; import org.geysermc.cumulus.response.SimpleFormResponse; import javax.crypto.SecretKey; @@ -154,12 +158,19 @@ public class LoginEncryptionUtils { } public static void buildAndShowLoginWindow(GeyserSession session) { + // Set DoDaylightCycle to false so the time doesn't accelerate while we're here + session.setDaylightCycle(false); + + GeyserConfiguration config = session.getConnector().getConfig(); + boolean isPasswordAuthEnabled = config.getRemote().isPasswordAuthentication(); + session.sendForm( SimpleForm.builder() .translator(LanguageUtils::getPlayerLocaleString, session.getLocale()) .title("geyser.auth.login.form.notice.title") .content("geyser.auth.login.form.notice.desc") - .button("geyser.auth.login.form.notice.btn_login") // id = 0 + .optionalButton("geyser.auth.login.form.notice.btn_login.mojang", isPasswordAuthEnabled) + .button("geyser.auth.login.form.notice.btn_login.microsoft") .button("geyser.auth.login.form.notice.btn_disconnect") .responseHandler((form, responseData) -> { SimpleFormResponse response = form.parseResponse(responseData); @@ -168,13 +179,26 @@ public class LoginEncryptionUtils { return; } - if (response.getClickedButtonId() == 0) { + if (isPasswordAuthEnabled && response.getClickedButtonId() == 0) { + session.setMicrosoftAccount(false); buildAndShowLoginDetailsWindow(session); return; } - session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.disconnect", session.getClientData().getLanguageCode())); - })); + if (isPasswordAuthEnabled && response.getClickedButtonId() == 1) { + session.setMicrosoftAccount(true); + buildAndShowMicrosoftAuthenticationWindow(session); + return; + } + + if (response.getClickedButtonId() == 0) { + // Just show the OAuth code + session.authenticateWithMicrosoftCode(); + return; + } + + session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.disconnect", session.getLocale())); + })); } public static void buildAndShowLoginDetailsWindow(GeyserSession session) { @@ -195,4 +219,56 @@ public class LoginEncryptionUtils { session.authenticate(response.next(), response.next()); })); } + + /** + * Promts the user between either OAuth code login or manual password authentication + */ + public static void buildAndShowMicrosoftAuthenticationWindow(GeyserSession session) { + session.sendForm( + SimpleForm.builder() + .translator(LanguageUtils::getPlayerLocaleString, session.getLocale()) + .title("geyser.auth.login.form.notice.btn_login.microsoft") + .button("geyser.auth.login.method.browser") + .button("geyser.auth.login.method.password") + .button("geyser.auth.login.form.notice.btn_disconnect") + .responseHandler((form, responseData) -> { + SimpleFormResponse response = form.parseResponse(responseData); + if (!response.isCorrect()) { + buildAndShowLoginWindow(session); + return; + } + + if (response.getClickedButtonId() == 0) { + session.authenticateWithMicrosoftCode(); + } else if (response.getClickedButtonId() == 1) { + buildAndShowLoginDetailsWindow(session); + } else { + session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.disconnect", session.getLocale())); + } + })); + } + + /** + * Shows the code that a user must input into their browser + */ + public static void buildAndShowMicrosoftCodeWindow(GeyserSession session, MsaAuthenticationService.MsCodeResponse msCode) { + session.sendForm( + ModalForm.builder() + .title("%xbox.signin") + .content("%xbox.signin.website\n%xbox.signin.url\n%xbox.signin.enterCode\n" + msCode.user_code) + .button1("%gui.done") + .button2("%menu.disconnect") + .responseHandler((form, responseData) -> { + ModalFormResponse response = form.parseResponse(responseData); + if (!response.isCorrect()) { + buildAndShowMicrosoftAuthenticationWindow(session); + return; + } + + if (response.getClickedButtonId() == 1) { + session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.disconnect", session.getLocale())); + } + }) + ); + } } diff --git a/connector/src/main/java/org/geysermc/connector/utils/StatisticsUtils.java b/connector/src/main/java/org/geysermc/connector/utils/StatisticsUtils.java index 1ab053aa9..f2181d871 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/StatisticsUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/StatisticsUtils.java @@ -33,6 +33,7 @@ import org.geysermc.connector.network.translators.item.ItemRegistry; import org.geysermc.connector.network.translators.world.block.BlockTranslator; import org.geysermc.cumulus.SimpleForm; import org.geysermc.cumulus.response.SimpleFormResponse; +import org.geysermc.cumulus.util.FormImage; import java.util.Map; import java.util.regex.Matcher; @@ -54,15 +55,15 @@ public class StatisticsUtils { 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") + .button("stat.generalButton", FormImage.Type.PATH, "textures/ui/World") + .button("stat.itemsButton - stat_type.minecraft.mined", FormImage.Type.PATH, "textures/items/iron_pickaxe") + .button("stat.itemsButton - stat_type.minecraft.broken", FormImage.Type.PATH, "textures/item/record_11") + .button("stat.itemsButton - stat_type.minecraft.crafted", FormImage.Type.PATH, "textures/blocks/crafting_table_side") + .button("stat.itemsButton - stat_type.minecraft.used", FormImage.Type.PATH, "textures/ui/Wrenches1") + .button("stat.itemsButton - stat_type.minecraft.picked_up", FormImage.Type.PATH, "textures/blocks/chest_front") + .button("stat.itemsButton - stat_type.minecraft.dropped", FormImage.Type.PATH, "textures/ui/trash_default") + .button("stat.mobsButton - geyser.statistics.killed", FormImage.Type.PATH, "textures/items/diamon_sword") + .button("stat.mobsButton - geyser.statistics.killed_by", FormImage.Type.PATH, "textures/ui/wither_heart_flash") .responseHandler((form, responseData) -> { SimpleFormResponse response = form.parseResponse(responseData); if (!response.isCorrect()) { @@ -178,7 +179,7 @@ public class StatisticsUtils { session.sendForm( builder.content(content.toString()) - .button("gui.back") + .button("gui.back", FormImage.Type.PATH, "textures/gui/newgui/undo") .responseHandler((form1, responseData1) -> { SimpleFormResponse response1 = form.parseResponse(responseData1); if (response1.isCorrect()) { diff --git a/connector/src/main/resources/config.yml b/connector/src/main/resources/config.yml index 9d71c7fea..234c4a697 100644 --- a/connector/src/main/resources/config.yml +++ b/connector/src/main/resources/config.yml @@ -32,10 +32,14 @@ remote: port: 25565 # Authentication type. Can be offline, online, or floodgate (see https://github.com/GeyserMC/Geyser/wiki/Floodgate). auth-type: online + # Allow for password-based authentication methods through Geyser. Only useful in online mode. + # If this is false, users must authenticate to Microsoft using a code provided by Geyser on their desktop. + allow-password-authentication: true # Whether to enable PROXY protocol or not while connecting to the server. # This is useful only when: # 1) Your server supports PROXY protocol (it probably doesn't) - # 2) You run Velocity or BungeeCord with respective option enabled. + # 2) You run Velocity or BungeeCord with the option enabled in the proxy's main config. + # IF YOU DON'T KNOW WHAT THIS IS, DON'T TOUCH IT! use-proxy-protocol: false # Floodgate uses encryption to ensure use from authorised sources. @@ -51,10 +55,12 @@ floodgate-key-file: key.pem # BedrockAccountUsername: # Your Minecraft: Bedrock Edition username # email: javaccountemail@example.com # Your Minecraft: Java Edition email # password: javaccountpassword123 # Your Minecraft: Java Edition password +# microsoft-account: true # Whether the account is a Mojang or Microsoft account. # # bluerkelp2: # email: not_really_my_email_address_mr_minecrafter53267@gmail.com # password: "this isn't really my password" +# microsoft-account: false # Bedrock clients can freeze when opening up the command prompt for the first time if given a lot of commands. # Disabling this will prevent command suggestions from being sent and solve freezing for Bedrock clients. @@ -132,8 +138,7 @@ above-bedrock-nether-building: false force-resource-packs: true # Allows Xbox achievements to be unlocked. -# This disables certain commands so the Bedrock client can't to "cheat" to get them. -# Commands such as /gamemode and /give will not work from Bedrock with this enabled +# THIS DISABLES ALL COMMANDS FROM SUCCESSFULLY RUNNING FOR BEDROCK IN-GAME, as otherwise Bedrock thinks you are cheating. xbox-achievements-enabled: false # bStats is a stat tracker that is entirely anonymous and tracks only basic information diff --git a/connector/src/main/resources/languages b/connector/src/main/resources/languages index 1a0076684..bffb5617c 160000 --- a/connector/src/main/resources/languages +++ b/connector/src/main/resources/languages @@ -1 +1 @@ -Subproject commit 1a00766840baf1f512d98f5a75c177c8bcfba6f3 +Subproject commit bffb5617c1ecdacc10031c6ec36988a5f04cb5c6 diff --git a/connector/src/main/resources/mappings b/connector/src/main/resources/mappings index 143285afb..2e52b01cc 160000 --- a/connector/src/main/resources/mappings +++ b/connector/src/main/resources/mappings @@ -1 +1 @@ -Subproject commit 143285afb4bdf4d5ef40ef7a7959477dabf4d34c +Subproject commit 2e52b01cc541c8925346f93be8940087d9af1661 diff --git a/licenseheader.txt b/licenseheader.txt index c22c426c4..8ef205a31 100644 --- a/licenseheader.txt +++ b/licenseheader.txt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2020 GeyserMC. http://geysermc.org + * 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 diff --git a/pom.xml b/pom.xml index 1b544f9ee..011b320f4 100644 --- a/pom.xml +++ b/pom.xml @@ -71,19 +71,6 @@ - - - releases - opencollab-releases - https://repo.opencollab.dev/maven-releases - - - snapshots - opencollab-snapshots - https://repo.opencollab.dev/maven-snapshots - - - org.projectlombok