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/Jenkinsfile b/Jenkinsfile
index e7f2ec4e2..501491361 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,13 @@ 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'
+ build propagate: false, wait: false, job: 'GeyserMC/GeyserAndroid/master'
+ }
+ }
+ }
}
}
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 816f765d6..1d1657edc 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.101/v1.16.200 and Minecraft Java v1.16.4.
+### Currently supporting Minecraft Bedrock v1.16.100 - v1.16.201 and Minecraft Java v1.16.4.
## Setting Up
Take a look [here](https://github.com/GeyserMC/Geyser/wiki#Setup) for how to set up Geyser.
diff --git a/bootstrap/bungeecord/pom.xml b/bootstrap/bungeecord/pom.xml
index 124967b0a..54e0d56ef 100644
--- a/bootstrap/bungeecord/pom.xml
+++ b/bootstrap/bungeecord/pom.xml
@@ -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/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/GeyserSpigotPlugin.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotPlugin.java
index 74b9e03d4..86247300a 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotPlugin.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotPlugin.java
@@ -43,6 +43,7 @@ import org.geysermc.connector.utils.LanguageUtils;
import org.geysermc.platform.spigot.command.GeyserSpigotCommandExecutor;
import org.geysermc.platform.spigot.command.GeyserSpigotCommandManager;
import org.geysermc.platform.spigot.command.SpigotCommandSender;
+import org.geysermc.platform.spigot.world.GeyserSpigot1_11CraftingListener;
import org.geysermc.platform.spigot.world.GeyserSpigotBlockPlaceListener;
import org.geysermc.platform.spigot.world.manager.*;
import us.myles.ViaVersion.api.Pair;
@@ -146,8 +147,9 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
geyserLogger.debug("Legacy version of Minecraft (1.15.2 or older) detected; not using 3D biomes.");
}
+ boolean isPre1_12 = !isCompatible(Bukkit.getServer().getVersion(), "1.12.0");
// Set if we need to use a different method for getting a player's locale
- SpigotCommandSender.setUseLegacyLocaleMethod(!isCompatible(Bukkit.getServer().getVersion(), "1.12.0"));
+ SpigotCommandSender.setUseLegacyLocaleMethod(isPre1_12);
if (connector.getConfig().isUseAdapters()) {
try {
@@ -191,9 +193,13 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
geyserLogger.debug("Using default world manager: " + this.geyserWorldManager.getClass());
}
GeyserSpigotBlockPlaceListener blockPlaceListener = new GeyserSpigotBlockPlaceListener(connector, this.geyserWorldManager);
-
Bukkit.getServer().getPluginManager().registerEvents(blockPlaceListener, this);
+ if (isPre1_12) {
+ // Register events needed to send all recipes to the client
+ Bukkit.getServer().getPluginManager().registerEvents(new GeyserSpigot1_11CraftingListener(this, connector), this);
+ }
+
this.getCommand("geyser").setExecutor(new GeyserSpigotCommandExecutor(connector));
}
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserSpigot1_11CraftingListener.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserSpigot1_11CraftingListener.java
new file mode 100644
index 000000000..d20b7637d
--- /dev/null
+++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserSpigot1_11CraftingListener.java
@@ -0,0 +1,197 @@
+/*
+ * 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.platform.spigot.world;
+
+import com.github.steveice10.mc.protocol.MinecraftConstants;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
+import com.github.steveice10.mc.protocol.data.game.recipe.Ingredient;
+import com.github.steveice10.mc.protocol.data.game.recipe.RecipeType;
+import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapedRecipeData;
+import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapelessRecipeData;
+import com.nukkitx.protocol.bedrock.data.inventory.CraftingData;
+import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
+import com.nukkitx.protocol.bedrock.packet.CraftingDataPacket;
+import org.bukkit.Bukkit;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerJoinEvent;
+import org.bukkit.inventory.Recipe;
+import org.bukkit.inventory.ShapedRecipe;
+import org.bukkit.inventory.ShapelessRecipe;
+import org.geysermc.connector.GeyserConnector;
+import org.geysermc.connector.network.session.GeyserSession;
+import org.geysermc.connector.network.translators.item.ItemTranslator;
+import org.geysermc.connector.network.translators.item.RecipeRegistry;
+import org.geysermc.platform.spigot.GeyserSpigotPlugin;
+import us.myles.ViaVersion.api.Pair;
+import us.myles.ViaVersion.api.data.MappingData;
+import us.myles.ViaVersion.api.protocol.Protocol;
+import us.myles.ViaVersion.api.protocol.ProtocolRegistry;
+import us.myles.ViaVersion.api.protocol.ProtocolVersion;
+import us.myles.ViaVersion.protocols.protocol1_13to1_12_2.Protocol1_13To1_12_2;
+
+import java.util.*;
+
+/**
+ * Used to send all available recipes from the server to the client, as a valid recipe book packet won't be sent by the server.
+ * Requires ViaVersion.
+ */
+public class GeyserSpigot1_11CraftingListener implements Listener {
+
+ private final GeyserConnector connector;
+ /**
+ * Specific mapping data for 1.12 to 1.13. Used to convert the 1.12 item into 1.13.
+ */
+ private final MappingData mappingData1_12to1_13;
+ /**
+ * The list of all protocols from the client's version to 1.13.
+ */
+ private final List> protocolList;
+ private final ProtocolVersion version;
+
+ public GeyserSpigot1_11CraftingListener(GeyserSpigotPlugin plugin, GeyserConnector connector) {
+ this.connector = connector;
+ this.mappingData1_12to1_13 = ProtocolRegistry.getProtocol(Protocol1_13To1_12_2.class).getMappingData();
+ this.protocolList = ProtocolRegistry.getProtocolPath(MinecraftConstants.PROTOCOL_VERSION,
+ ProtocolVersion.v1_13.getVersion());
+ this.version = plugin.getServerProtocolVersion();
+ }
+
+ @EventHandler
+ public void onPlayerJoin(PlayerJoinEvent event) {
+ GeyserSession session = null;
+ for (GeyserSession otherSession : connector.getPlayers()) {
+ if (otherSession.getName().equals(event.getPlayer().getName())) {
+ session = otherSession;
+ break;
+ }
+ }
+ if (session == null) {
+ return;
+ }
+
+ System.out.println("Sending recipes!");
+ sendServerRecipes(session);
+ }
+
+ public void sendServerRecipes(GeyserSession session) {
+ int netId = RecipeRegistry.LAST_RECIPE_NET_ID;
+
+ CraftingDataPacket craftingDataPacket = new CraftingDataPacket();
+ craftingDataPacket.setCleanRecipes(true);
+ Iterator recipeIterator = Bukkit.getServer().recipeIterator();
+ while (recipeIterator.hasNext()) {
+ Recipe recipe = recipeIterator.next();
+ Pair outputs = translateToBedrock(session, recipe.getResult());
+ ItemStack javaOutput = outputs.getKey();
+ ItemData output = outputs.getValue();
+ if (output.getId() == 0) continue; // If items make air we don't want that
+ boolean isNotAllAir = false; // Check for all-air recipes
+ if (recipe instanceof ShapedRecipe) {
+ ShapedRecipe shapedRecipe = (ShapedRecipe) recipe;
+ int size = shapedRecipe.getShape().length * shapedRecipe.getShape()[0].length();
+ Ingredient[] ingredients = new Ingredient[size];
+ ItemData[] input = new ItemData[size];
+ for (int i = 0; i < input.length; i++) {
+ // Index is converting char to integer, adding i then converting back to char based on ASCII code
+ Pair result = translateToBedrock(session, shapedRecipe.getIngredientMap().get((char) ('a' + i)));
+ ingredients[i] = new Ingredient(new ItemStack[]{result.getKey()});
+ input[i] = result.getValue();
+ isNotAllAir = isNotAllAir || input[i].getId() != 0;
+ }
+ if (!isNotAllAir) continue;
+ UUID uuid = UUID.randomUUID();
+ // Add recipe to our internal cache
+ ShapedRecipeData data = new ShapedRecipeData(shapedRecipe.getShape()[0].length(), shapedRecipe.getShape().length,
+ "", ingredients, javaOutput);
+ session.getCraftingRecipes().put(netId,
+ new com.github.steveice10.mc.protocol.data.game.recipe.Recipe(RecipeType.CRAFTING_SHAPED, uuid.toString(), data));
+ // Add recipe for Bedrock
+ craftingDataPacket.getCraftingData().add(CraftingData.fromShaped(uuid.toString(),
+ shapedRecipe.getShape()[0].length(), shapedRecipe.getShape().length, Arrays.asList(input),
+ Collections.singletonList(output), uuid, "crafting_table", 0, netId++));
+ } else if (recipe instanceof ShapelessRecipe) {
+ ShapelessRecipe shapelessRecipe = (ShapelessRecipe) recipe;
+ Ingredient[] ingredients = new Ingredient[shapelessRecipe.getIngredientList().size()];
+ ItemData[] input = new ItemData[shapelessRecipe.getIngredientList().size()];
+ for (int i = 0; i < input.length; i++) {
+ Pair result = translateToBedrock(session, shapelessRecipe.getIngredientList().get(i));
+ ingredients[i] = new Ingredient(new ItemStack[]{result.getKey()});
+ input[i] = result.getValue();
+ isNotAllAir = isNotAllAir || input[i].getId() != 0;
+ }
+ if (!isNotAllAir) continue;
+ UUID uuid = UUID.randomUUID();
+ // Add recipe to our internal cache
+ ShapelessRecipeData data = new ShapelessRecipeData("", ingredients, javaOutput);
+ session.getCraftingRecipes().put(netId,
+ new com.github.steveice10.mc.protocol.data.game.recipe.Recipe(RecipeType.CRAFTING_SHAPELESS, uuid.toString(), data));
+ // Add recipe for Bedrock
+ craftingDataPacket.getCraftingData().add(CraftingData.fromShapeless(uuid.toString(),
+ Arrays.asList(input), Collections.singletonList(output), uuid, "crafting_table", 0, netId++));
+ }
+ }
+
+ session.sendUpstreamPacket(craftingDataPacket);
+ }
+
+ @SuppressWarnings("deprecation")
+ private Pair translateToBedrock(GeyserSession session, org.bukkit.inventory.ItemStack itemStack) {
+ if (itemStack != null && itemStack.getData() != null) {
+ if (itemStack.getType().getId() == 0) {
+ return new Pair<>(null, ItemData.AIR);
+ }
+ int legacyId = (itemStack.getType().getId() << 4) | (itemStack.getData().getData() & 0xFFFF);
+ if (itemStack.getType().getId() == 355 && itemStack.getData().getData() == (byte) 0) { // Handle bed color since the server will always be pre-1.12
+ legacyId = (itemStack.getType().getId() << 4) | ((byte) 14 & 0xFFFF);
+ }
+ // old version -> 1.13 -> 1.13.1 -> 1.14 -> 1.15 -> 1.16 and so on
+ int itemId;
+ if (mappingData1_12to1_13.getItemMappings().containsKey(legacyId)) {
+ itemId = mappingData1_12to1_13.getNewItemId(legacyId);
+ } else if (mappingData1_12to1_13.getItemMappings().containsKey((itemStack.getType().getId() << 4) | (0))) {
+ itemId = mappingData1_12to1_13.getNewItemId((itemStack.getType().getId() << 4) | (0));
+ } else {
+ // No ID found, just send back air
+ return new Pair<>(null, ItemData.AIR);
+ }
+
+ for (int i = protocolList.size() - 1; i >= 0; i--) {
+ MappingData mappingData = protocolList.get(i).getValue().getMappingData();
+ if (mappingData != null) {
+ itemId = mappingData.getNewItemId(itemId);
+ }
+ }
+
+ ItemStack mcItemStack = new ItemStack(itemId, itemStack.getAmount());
+ ItemData finalData = ItemTranslator.translateToBedrock(session, mcItemStack);
+ return new Pair<>(mcItemStack, finalData);
+ }
+ // Empty slot, most likely
+ return new Pair<>(null, ItemData.AIR);
+ }
+
+}
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/velocity/pom.xml b/bootstrap/velocity/pom.xml
index 2fedca71a..5c0824def 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
+ org.geysermc.platform.velocity.shaded.kyori
diff --git a/connector/pom.xml b/connector/pom.xml
index 21f442df5..5be089afd 100644
--- a/connector/pom.xml
+++ b/connector/pom.xml
@@ -132,6 +132,10 @@
com.github.steveice10
packetlib
+
+ com.github.steveice10
+ mcauthlib
+
@@ -198,6 +202,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 5dd00dabe..d61500e04 100644
--- a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java
+++ b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java
@@ -86,6 +86,11 @@ public class GeyserConnector {
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
+ /**
+ * 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";
private final List players = new ArrayList<>();
@@ -101,8 +106,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/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/living/monster/EndermanEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/EndermanEntity.java
index e12b60d61..3151ae474 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/EndermanEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/EndermanEntity.java
@@ -27,8 +27,10 @@ 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.SoundEvent;
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
+import com.nukkitx.protocol.bedrock.packet.LevelSoundEvent2Packet;
import org.geysermc.connector.entity.type.EntityType;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
@@ -45,11 +47,21 @@ public class EndermanEntity extends MonsterEntity {
if (entityMetadata.getId() == 15) {
metadata.put(EntityData.CARRIED_BLOCK, BlockTranslator.getBedrockBlockId((int) entityMetadata.getValue()));
}
- // 'Angry' - mouth open
+ // "Is screaming" - controls sound
if (entityMetadata.getId() == 16) {
+ if ((boolean) entityMetadata.getValue()) {
+ LevelSoundEvent2Packet packet = new LevelSoundEvent2Packet();
+ packet.setSound(SoundEvent.STARE);
+ packet.setPosition(this.position);
+ packet.setExtraData(-1);
+ packet.setIdentifier("minecraft:enderman");
+ session.sendUpstreamPacket(packet);
+ }
+ }
+ // "Is staring/provoked" - controls visuals
+ if (entityMetadata.getId() == 17) {
metadata.getFlags().setFlag(EntityFlag.ANGRY, (boolean) entityMetadata.getValue());
}
- // TODO: ID 17 is stared at but I don't believe it's used - maybe only for the sound effect. Check after particle merge
super.updateBedrockMetadata(entityMetadata, session);
}
}
diff --git a/connector/src/main/java/org/geysermc/connector/inventory/GeyserItemStack.java b/connector/src/main/java/org/geysermc/connector/inventory/GeyserItemStack.java
index 76f7674a9..7cdaf1801 100644
--- a/connector/src/main/java/org/geysermc/connector/inventory/GeyserItemStack.java
+++ b/connector/src/main/java/org/geysermc/connector/inventory/GeyserItemStack.java
@@ -95,7 +95,11 @@ public class GeyserItemStack {
}
public ItemStack getItemStack() {
- return isEmpty() ? null : new ItemStack(javaId, amount, nbt);
+ return getItemStack(amount);
+ }
+
+ public ItemStack getItemStack(int newAmount) {
+ return isEmpty() ? null : new ItemStack(javaId, newAmount, nbt);
}
public ItemData getItemData(GeyserSession session) {
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 a6a369e45..3922a95ff 100644
--- a/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java
+++ b/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java
@@ -161,6 +161,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 e2587ee08..5d8ca3925 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;
@@ -36,9 +40,9 @@ import com.github.steveice10.mc.protocol.data.game.recipe.Recipe;
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.ingame.server.ServerRespawnPacket;
import com.github.steveice10.mc.protocol.packet.login.server.LoginSuccessPacket;
import com.github.steveice10.packetlib.BuiltinFlags;
import com.github.steveice10.packetlib.Client;
@@ -69,6 +73,7 @@ import lombok.Setter;
import org.geysermc.common.window.CustomFormWindow;
import org.geysermc.common.window.FormWindow;
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;
@@ -113,8 +118,13 @@ public class GeyserSession implements CommandSender {
@Setter
private BedrockClientData clientData;
+ @Deprecated
+ @Setter
+ private boolean microsoftAccount;
+
private final SessionPlayerEntity playerEntity;
+ private BookEditCache bookEditCache;
private ChunkCache chunkCache;
private EntityCache entityCache;
private EntityEffectCache effectCache;
@@ -178,7 +188,11 @@ public class GeyserSession implements CommandSender {
@Setter
private GameMode gameMode = GameMode.SURVIVAL;
- private final AtomicInteger pendingDimSwitches = new AtomicInteger(0);
+ /**
+ * Keeps track of the world name for respawning.
+ */
+ @Setter
+ private String worldName = null;
private boolean sneaking;
@@ -215,9 +229,6 @@ public class GeyserSession implements CommandSender {
@Setter
private Vector3i lastInteractionPosition = Vector3i.ZERO;
- private boolean manyDimPackets = false;
- private ServerRespawnPacket lastDimPacket = null;
-
@Setter
private Entity ridingVehicleEntity;
@@ -232,6 +243,7 @@ public class GeyserSession implements CommandSender {
@Setter
private Int2ObjectMap craftingRecipes;
private final Set unlockedRecipes;
+ private AtomicInteger lastRecipeNetId;
/**
* Saves a list of all stonecutter recipes, for use in a stonecutter inventory.
@@ -279,15 +291,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;
@@ -362,12 +373,18 @@ 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.bookEditCache = new BookEditCache(this);
this.chunkCache = new ChunkCache(this);
this.entityCache = new EntityCache(this);
this.effectCache = new EntityEffectCache();
@@ -385,6 +402,7 @@ public class GeyserSession implements CommandSender {
this.inventoryFuture = CompletableFuture.completedFuture(null);
this.craftingRecipes = new Int2ObjectOpenHashMap<>();
this.unlockedRecipes = new ObjectOpenHashSet<>();
+ this.lastRecipeNetId = new AtomicInteger(1);
this.spawned = false;
this.loggedIn = false;
@@ -471,146 +489,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;
- final PublicKey publicKey;
-
- if (floodgate) {
- PublicKey key = null;
- try {
- key = EncryptionUtil.getKeyFromFile(
- connector.getConfig().getFloodgateKeyPath(),
- PublicKey.class
- );
- } catch (IOException | InvalidKeySpecException | NoSuchAlgorithmException e) {
- connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.bad_key"), e);
- }
- publicKey = key;
- } else publicKey = null;
-
- if (publicKey != null) {
- connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.loaded_key"));
- }
-
- 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) {
- String encrypted = "";
- try {
- encrypted = EncryptionUtil.encryptBedrockData(publicKey, new BedrockData(
- clientData.getGameVersion(),
- authData.getName(),
- authData.getXboxUUID(),
- clientData.getDeviceOS().ordinal(),
- clientData.getLanguageCode(),
- clientData.getCurrentInputMode().ordinal(),
- upstream.getSession().getAddress().getAddress().getHostAddress()
- ));
- } catch (Exception e) {
- connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e);
- }
-
- HandshakePacket handshakePacket = event.getPacket();
- event.setPacket(new HandshakePacket(
- handshakePacket.getProtocolVersion(),
- handshakePacket.getHostname() + '\0' + BedrockData.FLOODGATE_IDENTIFIER + '\0' + encrypted,
- 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) {
- //handle consecutive respawn packets
- if (event.getPacket().getClass().equals(ServerRespawnPacket.class)) {
- manyDimPackets = lastDimPacket != null;
- lastDimPacket = event.getPacket();
- return;
- } else if (lastDimPacket != null) {
- PacketTranslatorRegistry.JAVA_TRANSLATOR.translate(lastDimPacket.getClass(), lastDimPacket, GeyserSession.this);
- lastDimPacket = null;
- }
-
- // 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()));
@@ -620,6 +514,199 @@ 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.showMicrosoftCodeWindow(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;
+ final PublicKey publicKey;
+
+ if (floodgate) {
+ PublicKey key = null;
+ try {
+ key = EncryptionUtil.getKeyFromFile(
+ connector.getConfig().getFloodgateKeyPath(),
+ PublicKey.class
+ );
+ } catch (IOException | InvalidKeySpecException | NoSuchAlgorithmException e) {
+ connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.bad_key"), e);
+ }
+ publicKey = key;
+ } else publicKey = null;
+
+ if (publicKey != null) {
+ connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.loaded_key"));
+ }
+
+ // 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) {
+ String encrypted = "";
+ try {
+ encrypted = EncryptionUtil.encryptBedrockData(publicKey, new BedrockData(
+ clientData.getGameVersion(),
+ authData.getName(),
+ authData.getXboxUUID(),
+ clientData.getDeviceOS().ordinal(),
+ clientData.getLanguageCode(),
+ clientData.getCurrentInputMode().ordinal(),
+ upstream.getSession().getAddress().getAddress().getHostAddress()
+ ));
+ } catch (Exception e) {
+ connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e);
+ }
+
+ HandshakePacket handshakePacket = event.getPacket();
+ event.setPacket(new HandshakePacket(
+ handshakePacket.getProtocolVersion(),
+ handshakePacket.getHostname() + '\0' + BedrockData.FLOODGATE_IDENTIFIER + '\0' + encrypted,
+ 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);
+ }
+ });
+
+ if (!daylightCycle) {
+ setDaylightCycle(true);
+ }
+ downstream.getSession().connect();
+ connector.addPlayer(this);
+ }
+
public void disconnect(String reason) {
if (!closed) {
loggedIn = false;
@@ -632,6 +719,11 @@ public class GeyserSession implements CommandSender {
}
}
+ if (tickThread != null) {
+ tickThread.cancel(true);
+ }
+
+ this.bookEditCache = null;
this.chunkCache = null;
this.entityCache = null;
this.effectCache = null;
@@ -645,6 +737,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;
}
@@ -921,6 +1035,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/BookEditCache.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/BookEditCache.java
new file mode 100644
index 000000000..c82645dbf
--- /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.packet.ingame.client.window.ClientEditBookPacket;
+import lombok.Setter;
+import org.geysermc.connector.inventory.GeyserItemStack;
+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
+ GeyserItemStack itemStack = session.getPlayerInventory().getItemInHand();
+ if (itemStack == null || itemStack.getJavaId() != 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..40000551c 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;
@@ -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/translators/bedrock/BedrockBookEditTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockBookEditTranslator.java
new file mode 100644
index 000000000..67778e822
--- /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.inventory.GeyserItemStack;
+import org.geysermc.connector.network.session.GeyserSession;
+import org.geysermc.connector.network.translators.PacketTranslator;
+import org.geysermc.connector.network.translators.Translator;
+
+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) {
+ GeyserItemStack itemStack = session.getPlayerInventory().getItemInHand();
+ if (itemStack != null) {
+ CompoundTag tag = itemStack.getNbt() != null ? itemStack.getNbt() : new CompoundTag("");
+ ItemStack bookItem = new ItemStack(itemStack.getJavaId(), 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.getPlayerInventory().setItem(36 + session.getPlayerInventory().getHeldItemSlot(), GeyserItemStack.from(bookItem), session);
+ session.getInventoryTranslator().updateInventory(session, session.getPlayerInventory());
+
+ session.getBookEditCache().setPacket(new ClientEditBookPacket(bookItem, packet.getAction() == BookEditPacket.Action.SIGN_BOOK, session.getPlayerInventory().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/BedrockInventoryTransactionTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java
index 2c34236a7..238ab347b 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java
@@ -38,14 +38,13 @@ import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlaye
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.protocol.bedrock.data.LevelEventType;
+import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
+import com.nukkitx.protocol.bedrock.data.entity.EntityFlags;
import com.nukkitx.protocol.bedrock.data.inventory.ContainerId;
import com.nukkitx.protocol.bedrock.data.inventory.ContainerType;
-import com.nukkitx.protocol.bedrock.packet.ContainerOpenPacket;
-import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket;
import com.nukkitx.protocol.bedrock.data.inventory.InventoryActionData;
import com.nukkitx.protocol.bedrock.data.inventory.InventorySource;
-import com.nukkitx.protocol.bedrock.packet.InventoryTransactionPacket;
-import com.nukkitx.protocol.bedrock.packet.LevelEventPacket;
+import com.nukkitx.protocol.bedrock.packet.*;
import org.geysermc.connector.entity.CommandBlockMinecartEntity;
import org.geysermc.connector.entity.Entity;
import org.geysermc.connector.entity.ItemFrameEntity;
@@ -63,11 +62,23 @@ import org.geysermc.connector.utils.BlockUtils;
import java.util.concurrent.TimeUnit;
+/**
+ * BedrockInventoryTransactionTranslator handles most interactions between the client and the world,
+ * or the client and their inventory.
+ */
@Translator(packet = InventoryTransactionPacket.class)
public class BedrockInventoryTransactionTranslator 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:
System.out.println(packet);
@@ -129,6 +140,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()],
@@ -176,7 +227,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());
@@ -286,4 +349,34 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator {
@@ -50,7 +46,7 @@ public class BedrockMovePlayerTranslator extends PacketTranslator 0) return;
+ if (!session.isSpawned()) return;
if (!session.getUpstream().isInitialized()) {
MoveEntityAbsolutePacket moveEntityBack = new MoveEntityAbsolutePacket();
@@ -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/inventory/InventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/InventoryTranslator.java
index f029a6051..654a7b422 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/InventoryTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/InventoryTranslator.java
@@ -968,13 +968,12 @@ public abstract class InventoryTranslator {
}
public boolean checkNetId(GeyserSession session, Inventory inventory, StackRequestSlotInfoData slotInfoData) {
- if (slotInfoData.getStackNetworkId() < 0)
+ int netId = slotInfoData.getStackNetworkId();
+ if (netId < 0 || netId == 1)
return true;
-// if (slotInfoData.getContainer() == ContainerSlotType.CURSOR) //TODO: temporary
-// return true;
GeyserItemStack currentItem = isCursor(slotInfoData) ? session.getPlayerInventory().getCursor() : inventory.getItem(bedrockSlotToJava(slotInfoData));
- return currentItem.getNetId() == slotInfoData.getStackNetworkId();
+ return currentItem.getNetId() == netId;
}
/**
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/AnvilInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/AnvilInventoryTranslator.java
index b131544b2..9d4fbfeec 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/AnvilInventoryTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/AnvilInventoryTranslator.java
@@ -26,20 +26,80 @@
package org.geysermc.connector.network.translators.inventory.translators;
import com.github.steveice10.mc.protocol.data.game.window.WindowType;
+import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientRenameItemPacket;
+import com.nukkitx.nbt.NbtMap;
import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType;
import com.nukkitx.protocol.bedrock.data.inventory.ContainerType;
+import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
import com.nukkitx.protocol.bedrock.data.inventory.StackRequestSlotInfoData;
+import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.CraftResultsDeprecatedStackRequestActionData;
+import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.StackRequestActionData;
+import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.StackRequestActionType;
+import com.nukkitx.protocol.bedrock.packet.ItemStackRequestPacket;
+import com.nukkitx.protocol.bedrock.packet.ItemStackResponsePacket;
import org.geysermc.connector.inventory.AnvilContainer;
+import org.geysermc.connector.inventory.GeyserItemStack;
import org.geysermc.connector.inventory.Inventory;
import org.geysermc.connector.inventory.PlayerInventory;
+import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.inventory.BedrockContainerSlot;
import org.geysermc.connector.network.translators.inventory.updater.UIInventoryUpdater;
+import org.geysermc.connector.network.translators.item.ItemTranslator;
public class AnvilInventoryTranslator extends AbstractBlockInventoryTranslator {
public AnvilInventoryTranslator() {
super(3, "minecraft:anvil[facing=north]", ContainerType.ANVIL, UIInventoryUpdater.INSTANCE);
}
+ /* 1.16.100 support start */
+ @Override
+ @Deprecated
+ public boolean shouldHandleRequestFirst(StackRequestActionData action, Inventory inventory) {
+ return action.getType() == StackRequestActionType.CRAFT_NON_IMPLEMENTED_DEPRECATED;
+ }
+
+ @Override
+ @Deprecated
+ public ItemStackResponsePacket.Response translateSpecialRequest(GeyserSession session, Inventory inventory, ItemStackRequestPacket.Request request) {
+ if (!(request.getActions()[1] instanceof CraftResultsDeprecatedStackRequestActionData)) {
+ // Just silently log an error
+ session.getConnector().getLogger().debug("Something isn't quite right with taking an item out of an anvil.");
+ return translateRequest(session, inventory, request);
+ }
+ CraftResultsDeprecatedStackRequestActionData actionData = (CraftResultsDeprecatedStackRequestActionData) request.getActions()[1];
+ ItemData resultItem = actionData.getResultItems()[0];
+ if (resultItem.getTag() != null) {
+ NbtMap displayTag = resultItem.getTag().getCompound("display");
+ if (displayTag != null && displayTag.containsKey("Name")) {
+ ItemData sourceSlot = inventory.getItem(0).getItemData(session);
+
+ if (sourceSlot.getTag() != null) {
+ NbtMap oldDisplayTag = sourceSlot.getTag().getCompound("display");
+ if (oldDisplayTag != null && oldDisplayTag.containsKey("Name")) {
+ if (!displayTag.getString("Name").equals(oldDisplayTag.getString("Name"))) {
+ // Name has changed
+ sendRenamePacket(session, inventory, resultItem, displayTag.getString("Name"));
+ }
+ } else {
+ // No display tag on the old item
+ sendRenamePacket(session, inventory, resultItem, displayTag.getString("Name"));
+ }
+ } else {
+ // New NBT tag
+ sendRenamePacket(session, inventory, resultItem, displayTag.getString("Name"));
+ }
+ }
+ }
+ return translateRequest(session, inventory, request);
+ }
+
+ private void sendRenamePacket(GeyserSession session, Inventory inventory, ItemData outputItem, String name) {
+ session.sendDownstreamPacket(new ClientRenameItemPacket(name));
+ inventory.setItem(2, GeyserItemStack.from(ItemTranslator.translateToJava(outputItem)), session);
+ }
+
+ /* 1.16.100 support end */
+
@Override
public int bedrockSlotToJava(StackRequestSlotInfoData slotInfoData) {
if (slotInfoData.getContainer() == ContainerSlotType.ANVIL_INPUT) {
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 4237f7d6a..33181b44e 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;
@@ -195,6 +199,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/JavaDeclareRecipesTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareRecipesTranslator.java
index 3b4f14d71..07b839f9c 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareRecipesTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareRecipesTranslator.java
@@ -190,6 +190,7 @@ public class JavaDeclareRecipesTranslator extends PacketTranslator
@Override
public void translate(ServerRespawnPacket packet, GeyserSession session) {
Entity entity = session.getPlayerEntity();
- if (entity == null)
- return;
float maxHealth = entity.getAttributes().containsKey(AttributeType.MAX_HEALTH) ? entity.getAttributes().get(AttributeType.MAX_HEALTH).getValue() : 20f;
// Max health must be divisible by two in bedrock
@@ -66,18 +64,24 @@ public class JavaRespawnTranslator extends PacketTranslator
session.setRaining(false);
}
+ if (session.isThunder()) {
+ LevelEventPacket stopThunderPacket = new LevelEventPacket();
+ stopThunderPacket.setType(LevelEventType.STOP_THUNDERSTORM);
+ stopThunderPacket.setData(0);
+ stopThunderPacket.setPosition(Vector3f.ZERO);
+ session.sendUpstreamPacket(stopThunderPacket);
+ session.setThunder(false);
+ }
+
String newDimension = DimensionUtils.getNewDimension(packet.getDimension());
- if (!session.getDimension().equals(newDimension)) {
- DimensionUtils.switchDimension(session, newDimension);
- } else {
- if (session.isManyDimPackets()) { //reloading world
- String fakeDim = session.getDimension().equals(DimensionUtils.OVERWORLD) ? DimensionUtils.NETHER : DimensionUtils.OVERWORLD;
+ if (!session.getDimension().equals(newDimension) || !packet.getWorldName().equals(session.getWorldName())) {
+ if (!packet.getWorldName().equals(session.getWorldName()) && session.getDimension().equals(newDimension)) {
+ // Switching to a new world (based off the world name change); send a fake dimension change
+ String fakeDim = DimensionUtils.getTemporaryDimension(session.getDimension(), newDimension);
DimensionUtils.switchDimension(session, fakeDim);
- DimensionUtils.switchDimension(session, newDimension);
- } else {
- // Handled in JavaPlayerPositionRotationTranslator
- session.setSpawned(false);
}
+ session.setWorldName(packet.getWorldName());
+ DimensionUtils.switchDimension(session, newDimension);
}
}
}
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 {
@@ -55,10 +74,207 @@ public class JavaSetSlotTranslator extends PacketTranslator
InventoryTranslator translator = session.getInventoryTranslator();
if (translator != null) {
+ updateCraftingGrid(session, packet, inventory, translator);
+
GeyserItemStack newItem = GeyserItemStack.from(packet.getItem());
inventory.setItem(packet.getSlot(), newItem, session);
translator.updateSlot(session, inventory, packet.getSlot());
}
});
}
+
+ private static void updateCraftingGrid(GeyserSession session, ServerSetSlotPacket packet, Inventory inventory, InventoryTranslator translator) {
+ if (packet.getSlot() == 0) {
+ int gridSize;
+ if (translator instanceof PlayerInventoryTranslator) {
+ gridSize = 4;
+ } else if (translator instanceof CraftingInventoryTranslator) {
+ gridSize = 9;
+ } else {
+ return;
+ }
+
+ if (packet.getItem() == null || packet.getItem().getId() == 0) {
+ return;
+ }
+
+ int offset = gridSize == 4 ? 28 : 32;
+ int gridDimensions = gridSize == 4 ? 2 : 3;
+ int firstRow = -1, height = -1;
+ int firstCol = -1, width = -1;
+ for (int row = 0; row < gridDimensions; row++) {
+ for (int col = 0; col < gridDimensions; col++) {
+ if (!inventory.getItem(col + (row * gridDimensions) + 1).isEmpty()) {
+ if (firstRow == -1) {
+ firstRow = row;
+ firstCol = col;
+ } else {
+ firstCol = Math.min(firstCol, col);
+ }
+ height = Math.max(height, row);
+ width = Math.max(width, col);
+ }
+ }
+ }
+
+ //empty grid
+ if (firstRow == -1) {
+ return;
+ }
+
+ height += -firstRow + 1;
+ width += -firstCol + 1;
+
+ System.out.println("Start Row: " + firstRow);
+ System.out.println("Start Column: " + firstCol);
+ System.out.println("Rows: " + height);
+ System.out.println("Columns: " + width);
+
+ //TODO
+ recipes:
+ for (Recipe recipe : session.getCraftingRecipes().values()) {
+ if (recipe.getType() == RecipeType.CRAFTING_SHAPED) {
+ ShapedRecipeData data = (ShapedRecipeData) recipe.getData();
+ if (!data.getResult().equals(packet.getItem())) {
+ continue;
+ }
+ if (data.getWidth() != width || data.getHeight() != height || width * height != data.getIngredients().length) {
+ continue;
+ }
+
+ Ingredient[] ingredients = data.getIngredients();
+ if (!testShapedRecipe(ingredients, inventory, gridDimensions, firstRow, height, firstCol, width)) {
+ Ingredient[] mirroredIngredients = new Ingredient[data.getIngredients().length];
+ for (int row = 0; row < height; row++) {
+ for (int col = 0; col < width; col++) {
+ mirroredIngredients[col + (row * width)] = ingredients[(width - 1 - col) + (row * width)];
+ }
+ }
+
+ if (Arrays.equals(ingredients, mirroredIngredients)) {
+ continue;
+ } else if (!testShapedRecipe(mirroredIngredients, inventory, gridDimensions, firstRow, height, firstCol, width)) {
+ continue;
+ }
+ }
+ System.out.println("FOUND SHAPED RECIPE :)");
+ System.out.println(recipe);
+ // Recipe is had, don't sent packet
+ return;
+ } else if (recipe.getType() == RecipeType.CRAFTING_SHAPELESS) {
+ ShapelessRecipeData data = (ShapelessRecipeData) recipe.getData();
+ if (!data.getResult().equals(packet.getItem())) {
+ continue;
+ }
+ for (int i = 0; i < data.getIngredients().length; i++) {
+ Ingredient ingredient = data.getIngredients()[i];
+ for (ItemStack itemStack : ingredient.getOptions()) {
+ boolean inventoryHasItem = false;
+ for (int j = 0; j < inventory.getSize(); j++) {
+ GeyserItemStack geyserItemStack = inventory.getItem(j);
+ if (geyserItemStack.isEmpty()) {
+ inventoryHasItem = itemStack == null || itemStack.getId() == 0;
+ if (inventoryHasItem) {
+ break;
+ }
+ } else if (itemStack.equals(geyserItemStack.getItemStack(1))) {
+ inventoryHasItem = true;
+ break;
+ }
+ }
+ if (!inventoryHasItem) {
+ continue recipes;
+ }
+ }
+ }
+ // Recipe is had, don't sent packet
+ return;
+ }
+ }
+ System.out.println("Sending packet!");
+
+ UUID uuid = UUID.randomUUID();
+ int newRecipeId = session.getLastRecipeNetId().incrementAndGet();
+
+ ItemData[] ingredients = new ItemData[height * width];
+ //construct ingredient list and clear slots on client
+ Ingredient[] javaIngredients = new Ingredient[height * width];
+ int index = 0;
+ for (int row = firstRow; row < height + firstRow; row++) {
+ for (int col = firstCol; col < width + firstCol; col++) {
+ GeyserItemStack geyserItemStack = inventory.getItem(col + (row * gridDimensions) + 1);
+ ingredients[index] = geyserItemStack.getItemData(session);
+ ItemStack[] itemStacks = new ItemStack[] {geyserItemStack.isEmpty() ? null : geyserItemStack.getItemStack(1)};
+ javaIngredients[index] = new Ingredient(itemStacks);
+
+ InventorySlotPacket slotPacket = new InventorySlotPacket();
+ slotPacket.setContainerId(ContainerId.UI);
+ slotPacket.setSlot(col + (row * gridDimensions) + offset);
+ slotPacket.setItem(ItemData.AIR);
+ session.sendUpstreamPacket(slotPacket);
+ index++;
+ }
+ }
+
+ ShapedRecipeData data = new ShapedRecipeData(width, height, "", javaIngredients, packet.getItem());
+ session.getConnector().getLogger().error(data.toString());
+ // Cache this recipe so we know the client has received it
+ session.getCraftingRecipes().put(newRecipeId, new Recipe(RecipeType.CRAFTING_SHAPED, uuid.toString(), data));
+
+ CraftingDataPacket craftPacket = new CraftingDataPacket();
+ craftPacket.getCraftingData().add(CraftingData.fromShaped(
+ uuid.toString(),
+ width,
+ height,
+ Arrays.asList(ingredients),
+ Collections.singletonList(ItemTranslator.translateToBedrock(session, packet.getItem())),
+ uuid,
+ "crafting_table",
+ 0,
+ newRecipeId
+ ));
+ craftPacket.setCleanRecipes(false);
+ System.out.println(craftPacket);
+ session.sendUpstreamPacket(craftPacket);
+
+ index = 0;
+ for (int row = firstRow; row < height + firstRow; row++) {
+ for (int col = firstCol; col < width + firstCol; col++) {
+ InventorySlotPacket slotPacket = new InventorySlotPacket();
+ slotPacket.setContainerId(ContainerId.UI);
+ slotPacket.setSlot(col + (row * gridDimensions) + offset);
+ slotPacket.setItem(ingredients[index]);
+ session.sendUpstreamPacket(slotPacket);
+ index++;
+ }
+ }
+ }
+ }
+
+ private static boolean testShapedRecipe(Ingredient[] ingredients, Inventory inventory, int gridDimensions, int firstRow, int height, int firstCol, int width) {
+ int ingredientIndex = 0;
+ for (int row = firstRow; row < height + firstRow; row++) {
+ for (int col = firstCol; col < width + firstCol; col++) {
+ GeyserItemStack geyserItemStack = inventory.getItem(col + (row * gridDimensions) + 1);
+ Ingredient ingredient = ingredients[ingredientIndex++];
+ if (ingredient.getOptions().length == 0) {
+ if (!geyserItemStack.isEmpty()) {
+ return false;
+ }
+ } else {
+ boolean inventoryHasItem = false;
+ for (ItemStack item : ingredient.getOptions()) {
+ if (Objects.equals(geyserItemStack.getItemStack(1), item)) {
+ inventoryHasItem = true;
+ break;
+ }
+ }
+ if (!inventoryHasItem) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
}
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaBlockChangeTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaBlockChangeTranslator.java
index 7f2b3561e..d74165b14 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaBlockChangeTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaBlockChangeTranslator.java
@@ -26,6 +26,7 @@
package org.geysermc.connector.network.translators.java.world;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
+import com.github.steveice10.mc.protocol.packet.ingame.server.world.ServerBlockChangePacket;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.protocol.bedrock.data.SoundEvent;
import com.nukkitx.protocol.bedrock.packet.LevelSoundEventPacket;
@@ -37,17 +38,17 @@ import org.geysermc.connector.network.translators.sound.BlockSoundInteractionHan
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
import org.geysermc.connector.utils.ChunkUtils;
-import com.github.steveice10.mc.protocol.packet.ingame.server.world.ServerBlockChangePacket;
-
@Translator(packet = ServerBlockChangePacket.class)
public class JavaBlockChangeTranslator 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/JavaPlayerPlaySoundTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaPlaySoundTranslator.java
similarity index 93%
rename from connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaPlayerPlaySoundTranslator.java
rename to connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaPlaySoundTranslator.java
index bce96f65d..238e9ba32 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaPlayerPlaySoundTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaPlaySoundTranslator.java
@@ -36,14 +36,14 @@ import org.geysermc.connector.network.translators.Translator;
import org.geysermc.connector.network.translators.sound.SoundRegistry;
@Translator(packet = ServerPlaySoundPacket.class)
-public class JavaPlayerPlaySoundTranslator extends PacketTranslator {
+public class JavaPlaySoundTranslator extends PacketTranslator {
@Override
public void translate(ServerPlaySoundPacket packet, GeyserSession session) {
String packetSound;
- if(packet.getSound() instanceof BuiltinSound) {
+ if (packet.getSound() instanceof BuiltinSound) {
packetSound = ((BuiltinSound) packet.getSound()).getName();
- } else if(packet.getSound() instanceof CustomSound) {
+ } else if (packet.getSound() instanceof CustomSound) {
packetSound = ((CustomSound) packet.getSound()).getName();
} else {
session.getConnector().getLogger().debug("Unknown sound packet, we were unable to map this. " + packet.toString());
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerStopSoundTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaStopSoundTranslator.java
similarity index 82%
rename from connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerStopSoundTranslator.java
rename to connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaStopSoundTranslator.java
index 61856755f..d7d0f0738 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerStopSoundTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaStopSoundTranslator.java
@@ -23,7 +23,7 @@
* @link https://github.com/GeyserMC/Geyser
*/
-package org.geysermc.connector.network.translators.java.entity.player;
+package org.geysermc.connector.network.translators.java.world;
import com.github.steveice10.mc.protocol.data.game.world.sound.BuiltinSound;
import com.github.steveice10.mc.protocol.data.game.world.sound.CustomSound;
@@ -35,26 +35,35 @@ import org.geysermc.connector.network.translators.Translator;
import org.geysermc.connector.network.translators.sound.SoundRegistry;
@Translator(packet = ServerStopSoundPacket.class)
-public class JavaPlayerStopSoundTranslator extends PacketTranslator {
+public class JavaStopSoundTranslator extends PacketTranslator {
@Override
public void translate(ServerStopSoundPacket packet, GeyserSession session) {
+ // Runs if all sounds are stopped
+ if (packet.getSound() == null) {
+ StopSoundPacket stopPacket = new StopSoundPacket();
+ stopPacket.setStoppingAllSound(true);
+ stopPacket.setSoundName("");
+ session.sendUpstreamPacket(stopPacket);
+ return;
+ }
+
String packetSound;
- if(packet.getSound() instanceof BuiltinSound) {
+ if (packet.getSound() instanceof BuiltinSound) {
packetSound = ((BuiltinSound) packet.getSound()).getName();
- } else if(packet.getSound() instanceof CustomSound) {
+ } else if (packet.getSound() instanceof CustomSound) {
packetSound = ((CustomSound) packet.getSound()).getName();
} else {
session.getConnector().getLogger().debug("Unknown sound packet, we were unable to map this. " + packet.toString());
return;
}
- SoundRegistry.SoundMapping soundMapping = SoundRegistry.fromJava(packetSound);
+ SoundRegistry.SoundMapping soundMapping = SoundRegistry.fromJava(packetSound.replace("minecraft:", ""));
session.getConnector().getLogger()
.debug("[StopSound] Sound mapping " + packetSound + " -> "
+ soundMapping + (soundMapping == null ? "[not found]" : "")
+ " - " + packet.toString());
String playsound;
- if(soundMapping == null || soundMapping.getPlaysound() == null) {
+ if (soundMapping == null || soundMapping.getPlaysound() == null) {
// no mapping
session.getConnector().getLogger()
.debug("[StopSound] Defaulting to sound server gave us.");
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/network/translators/world/block/entity/CampfireBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/CampfireBlockEntityTranslator.java
index e25bb8263..40f305ad6 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/CampfireBlockEntityTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/CampfireBlockEntityTranslator.java
@@ -47,7 +47,7 @@ public class CampfireBlockEntityTranslator extends BlockEntityTranslator {
protected NbtMap getItem(CompoundTag tag) {
ItemEntry entry = ItemRegistry.getItemEntry((String) tag.get("id").getValue());
NbtMapBuilder tagBuilder = NbtMap.builder()
- .putShort("id", (short) entry.getBedrockId())
+ .putString("Name", entry.getBedrockIdentifier())
.putByte("Count", (byte) tag.get("Count").getValue())
.putShort("Damage", (short) entry.getBedrockData());
tagBuilder.put("tag", NbtMap.builder().build());
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 89ea9298e..24b137f77 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 0efa8a5aa..e002162c3 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java
@@ -57,20 +57,11 @@ public class DimensionUtils {
public static void switchDimension(GeyserSession session, String javaDimension) {
int bedrockDimension = javaToBedrock(javaDimension);
Entity player = session.getPlayerEntity();
- if (javaDimension.equals(session.getDimension()))
- return;
-
- if (session.getMovementSendIfIdle() != null) {
- session.getMovementSendIfIdle().cancel(true);
- }
session.getEntityCache().removeAllEntities();
session.getItemFrameCache().clear();
session.getLecternCache().clear();
session.getSkullCache().clear();
- if (session.getPendingDimSwitches().getAndIncrement() > 0) {
- ChunkUtils.sendEmptyChunks(session, player.getPosition().toInt(), 3, true);
- }
Vector3i pos = Vector3i.from(0, Short.MAX_VALUE, 0);
@@ -151,4 +142,20 @@ public class DimensionUtils {
// Change dimension ID to the End to allow for building above Bedrock
BEDROCK_NETHER_ID = isAboveNetherBedrockBuilding ? 2 : 1;
}
+
+ /**
+ * Gets the fake, temporary dimension we send clients to so we aren't switching to the same dimension without an additional
+ * dimension switch.
+ *
+ * @param currentDimension the current dimension of the player
+ * @param newDimension the new dimension that the player will be transferred to
+ * @return the fake dimension to transfer to
+ */
+ public static String getTemporaryDimension(String currentDimension, String newDimension) {
+ if (BEDROCK_NETHER_ID == 2) {
+ // Prevents rare instances of Bedrock locking up
+ return javaToBedrock(newDimension) == 2 ? OVERWORLD : NETHER;
+ }
+ return currentDimension.equals(OVERWORLD) ? NETHER : OVERWORLD;
+ }
}
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 fd3e9a8db..862af548d 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java
@@ -36,6 +36,7 @@ import org.reflections.util.ConfigurationBuilder;
import java.io.*;
import java.net.URL;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.util.function.Function;
@@ -62,7 +63,8 @@ public class FileUtils {
}
public static T loadJson(InputStream src, Class valueType) throws IOException {
- return GeyserConnector.JSON_MAPPER.readValue(src, valueType);
+ // Read specifically with UTF-8 to allow any non-UTF-encoded JSON to read
+ return GeyserConnector.JSON_MAPPER.readValue(new InputStreamReader(src, StandardCharsets.UTF_8), valueType);
}
/**
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 c6ab715d4..1a1f758d6 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java
@@ -187,7 +187,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/LoginEncryptionUtils.java b/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java
index 9e16c428d..fd7ef4e64 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java
@@ -29,19 +29,18 @@ 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.common.window.CustomFormBuilder;
-import org.geysermc.common.window.CustomFormWindow;
-import org.geysermc.common.window.FormWindow;
-import org.geysermc.common.window.SimpleFormWindow;
+import org.geysermc.common.window.*;
import org.geysermc.common.window.button.FormButton;
import org.geysermc.common.window.component.InputComponent;
import org.geysermc.common.window.component.LabelComponent;
import org.geysermc.common.window.response.CustomFormResponse;
+import org.geysermc.common.window.response.ModalFormResponse;
import org.geysermc.common.window.response.SimpleFormResponse;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.network.session.GeyserSession;
@@ -156,13 +155,21 @@ public class LoginEncryptionUtils {
session.sendUpstreamPacketImmediately(packet);
}
- private static int AUTH_FORM_ID = 1336;
- private static int AUTH_DETAILS_FORM_ID = 1337;
+ private static final int AUTH_MSA_DETAILS_FORM_ID = 1334;
+ private static final int AUTH_MSA_CODE_FORM_ID = 1335;
+ private static final int AUTH_FORM_ID = 1336;
+ private static final int AUTH_DETAILS_FORM_ID = 1337;
public static void showLoginWindow(GeyserSession session) {
+ // Set DoDaylightCycle to false so the time doesn't accelerate while we're here
+ session.setDaylightCycle(false);
+
String userLanguage = session.getLocale();
SimpleFormWindow window = new SimpleFormWindow(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.title", userLanguage), LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.desc", userLanguage));
- window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_login", userLanguage)));
+ if (session.getConnector().getConfig().getRemote().isPasswordAuthentication()) {
+ window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_login.mojang", userLanguage)));
+ }
+ window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_login.microsoft", userLanguage)));
window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_disconnect", userLanguage)));
session.sendForm(window, AUTH_FORM_ID);
@@ -179,12 +186,33 @@ public class LoginEncryptionUtils {
session.sendForm(window, AUTH_DETAILS_FORM_ID);
}
+ /**
+ * Prompts the user between either OAuth code login or manual password authentication
+ */
+ public static void showMicrosoftAuthenticationWindow(GeyserSession session) {
+ String userLanguage = session.getLocale();
+ SimpleFormWindow window = new SimpleFormWindow(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_login.microsoft", userLanguage), "");
+ window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.method.browser", userLanguage)));
+ window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.method.password", userLanguage))); // This form won't show if password authentication is disabled
+ window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_disconnect", userLanguage)));
+ session.sendForm(window, AUTH_MSA_DETAILS_FORM_ID);
+ }
+
+ /**
+ * Shows the code that a user must input into their browser
+ */
+ public static void showMicrosoftCodeWindow(GeyserSession session, MsaAuthenticationService.MsCodeResponse response) {
+ ModalFormWindow msaCodeWindow = new ModalFormWindow("%xbox.signin", "%xbox.signin.website\n%xbox.signin.url\n%xbox.signin.enterCode\n" +
+ response.user_code, "Done", "%menu.disconnect");
+ session.sendForm(msaCodeWindow, LoginEncryptionUtils.AUTH_MSA_CODE_FORM_ID);
+ }
+
public static boolean authenticateFromForm(GeyserSession session, GeyserConnector connector, int formId, String formData) {
WindowCache windowCache = session.getWindowCache();
if (!windowCache.getWindows().containsKey(formId))
return false;
- if(formId == AUTH_FORM_ID || formId == AUTH_DETAILS_FORM_ID) {
+ if (formId == AUTH_MSA_DETAILS_FORM_ID || formId == AUTH_FORM_ID || formId == AUTH_DETAILS_FORM_ID || formId == AUTH_MSA_CODE_FORM_ID) {
FormWindow window = windowCache.getWindows().remove(formId);
window.setResponse(formData.trim());
@@ -198,23 +226,57 @@ public class LoginEncryptionUtils {
String password = response.getInputResponses().get(2);
session.authenticate(email, password);
+
+ // Clear windows so authentication data isn't accidentally cached
+ windowCache.getWindows().clear();
} else {
showLoginDetailsWindow(session);
}
-
- // Clear windows so authentication data isn't accidentally cached
- windowCache.getWindows().clear();
} else if (formId == AUTH_FORM_ID && window instanceof SimpleFormWindow) {
+ boolean isPasswordAuthentication = session.getConnector().getConfig().getRemote().isPasswordAuthentication();
+ int microsoftButton = isPasswordAuthentication ? 1 : 0;
+ int disconnectButton = isPasswordAuthentication ? 2 : 1;
SimpleFormResponse response = (SimpleFormResponse) window.getResponse();
if (response != null) {
- if (response.getClickedButtonId() == 0) {
+ if (isPasswordAuthentication && response.getClickedButtonId() == 0) {
+ session.setMicrosoftAccount(false);
showLoginDetailsWindow(session);
- } else if(response.getClickedButtonId() == 1) {
+ } else if (response.getClickedButtonId() == microsoftButton) {
+ session.setMicrosoftAccount(true);
+ if (isPasswordAuthentication) {
+ showMicrosoftAuthenticationWindow(session);
+ } else {
+ // Just show the OAuth code
+ session.authenticateWithMicrosoftCode();
+ }
+ } else if (response.getClickedButtonId() == disconnectButton) {
session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.disconnect", session.getLocale()));
}
} else {
showLoginWindow(session);
}
+ } else if (formId == AUTH_MSA_DETAILS_FORM_ID && window instanceof SimpleFormWindow) {
+ SimpleFormResponse response = (SimpleFormResponse) window.getResponse();
+ if (response != null) {
+ if (response.getClickedButtonId() == 0) {
+ session.authenticateWithMicrosoftCode();
+ } else if (response.getClickedButtonId() == 1) {
+ showLoginDetailsWindow(session);
+ } else if (response.getClickedButtonId() == 2) {
+ session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.disconnect", session.getLocale()));
+ }
+ } else {
+ showLoginWindow(session);
+ }
+ } else if (formId == AUTH_MSA_CODE_FORM_ID && window instanceof ModalFormWindow) {
+ ModalFormResponse response = (ModalFormResponse) window.getResponse();
+ if (response != null) {
+ if (response.getClickedButtonId() == 1) {
+ session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.disconnect", session.getLocale()));
+ }
+ } else {
+ showMicrosoftAuthenticationWindow(session);
+ }
}
}
}
diff --git a/connector/src/main/java/org/geysermc/connector/utils/ResourcePack.java b/connector/src/main/java/org/geysermc/connector/utils/ResourcePack.java
index 16a1812ee..bcb1ffd50 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/ResourcePack.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/ResourcePack.java
@@ -63,7 +63,7 @@ public class ResourcePack {
// As we just created the directory it will be empty
return;
}
-
+
for (File file : directory.listFiles()) {
if (file.getName().endsWith(".zip") || file.getName().endsWith(".mcpack")) {
ResourcePack pack = new ResourcePack();
@@ -77,12 +77,15 @@ public class ResourcePack {
if (x.getName().contains("manifest.json")) {
try {
ResourcePackManifest manifest = FileUtils.loadJson(zip.getInputStream(x), ResourcePackManifest.class);
+ // Sometimes a pack_manifest file is present and not in a valid format,
+ // but a manifest file is, so we null check through that one
+ if (manifest.getHeader().getUuid() != null) {
+ pack.file = file;
+ pack.manifest = manifest;
+ pack.version = ResourcePackManifest.Version.fromArray(manifest.getHeader().getVersion());
- pack.file = file;
- pack.manifest = manifest;
- pack.version = ResourcePackManifest.Version.fromArray(manifest.getHeader().getVersion());
-
- PACKS.put(pack.getManifest().getHeader().getUuid().toString(), pack);
+ PACKS.put(pack.getManifest().getHeader().getUuid().toString(), pack);
+ }
} catch (Exception e) {
e.printStackTrace();
}
diff --git a/connector/src/main/resources/config.yml b/connector/src/main/resources/config.yml
index ac9ec753d..07b73173e 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: public-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..6f246c24d 160000
--- a/connector/src/main/resources/languages
+++ b/connector/src/main/resources/languages
@@ -1 +1 @@
-Subproject commit 1a00766840baf1f512d98f5a75c177c8bcfba6f3
+Subproject commit 6f246c24ddbd543a359d651e706da470fe53ceeb
diff --git a/connector/src/main/resources/mappings b/connector/src/main/resources/mappings
index 07f65c380..dd0347bd5 160000
--- a/connector/src/main/resources/mappings
+++ b/connector/src/main/resources/mappings
@@ -1 +1 @@
-Subproject commit 07f65c3803dcd3f83358ee574e54bf129cad0840
+Subproject commit dd0347bd51e00e42ea58faaf68b562526c4d2817
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