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