1.#.#
+ */
+ private String minecraftVersion;
+
@Override
public void onEnable() {
// This is manually done instead of using Bukkit methods to save the config because otherwise comments get removed
@@ -118,6 +124,9 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
geyserConfig.loadFloodgate(this);
+ // Turn "(MC: 1.16.4)" into 1.16.4.
+ this.minecraftVersion = Bukkit.getServer().getVersion().split("\\(MC: ")[1].split("\\)")[0];
+
this.connector = GeyserConnector.start(PlatformType.SPIGOT, this);
if (geyserConfig.isLegacyPingPassthrough()) {
@@ -146,8 +155,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 {
@@ -157,14 +167,14 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
if (isViaVersion && isViaVersionNeeded()) {
if (isLegacy) {
// Pre-1.13
- this.geyserWorldManager = new GeyserSpigot1_12NativeWorldManager();
+ this.geyserWorldManager = new GeyserSpigot1_12NativeWorldManager(this);
} else {
// Post-1.13
this.geyserWorldManager = new GeyserSpigotLegacyNativeWorldManager(this, use3dBiomes);
}
} else {
// No ViaVersion
- this.geyserWorldManager = new GeyserSpigotNativeWorldManager(use3dBiomes);
+ this.geyserWorldManager = new GeyserSpigotNativeWorldManager(this, use3dBiomes);
}
geyserLogger.debug("Using NMS adapter: " + this.geyserWorldManager.getClass() + ", " + nmsVersion);
} catch (Exception e) {
@@ -180,20 +190,24 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
// No NMS adapter
if (isLegacy && isViaVersion) {
// Use ViaVersion for converting pre-1.13 block states
- this.geyserWorldManager = new GeyserSpigot1_12WorldManager();
+ this.geyserWorldManager = new GeyserSpigot1_12WorldManager(this);
} else if (isLegacy) {
// Not sure how this happens - without ViaVersion, we don't know any block states, so just assume everything is air
- this.geyserWorldManager = new GeyserSpigotFallbackWorldManager();
+ this.geyserWorldManager = new GeyserSpigotFallbackWorldManager(this);
} else {
// Post-1.13
- this.geyserWorldManager = new GeyserSpigotWorldManager(use3dBiomes);
+ this.geyserWorldManager = new GeyserSpigotWorldManager(this, use3dBiomes);
}
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(connector), this);
+ }
+
this.getCommand("geyser").setExecutor(new GeyserSpigotCommandExecutor(connector));
}
@@ -239,6 +253,11 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
return new GeyserSpigotDumpInfo();
}
+ @Override
+ public String getMinecraftServerVersion() {
+ return this.minecraftVersion;
+ }
+
public boolean isCompatible(String version, String whichVersion) {
int[] currentVersion = parseVersion(version);
int[] otherVersion = parseVersion(whichVersion);
@@ -277,10 +296,7 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
* @return the server version before ViaVersion finishes initializing
*/
public ProtocolVersion getServerProtocolVersion() {
- String bukkitVersion = Bukkit.getServer().getVersion();
- // Turn "(MC: 1.16.4)" into 1.16.4.
- String version = bukkitVersion.split("\\(MC: ")[1].split("\\)")[0];
- return ProtocolVersion.getClosest(version);
+ return ProtocolVersion.getClosest(this.minecraftVersion);
}
/**
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..2ee6457ac
--- /dev/null
+++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserSpigot1_11CraftingListener.java
@@ -0,0 +1,205 @@
+/*
+ * 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 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 Listnull
if there is no player online with this xuid
*/
+ @SuppressWarnings("unused") // API usage
public GeyserSession getPlayerByXuid(String xuid) {
for (GeyserSession session : players) {
if (session.getAuthData() != null && session.getAuthData().getXboxUUID().equals(xuid)) {
diff --git a/connector/src/main/java/org/geysermc/connector/bootstrap/GeyserBootstrap.java b/connector/src/main/java/org/geysermc/connector/bootstrap/GeyserBootstrap.java
index 7308fb674..59dc58c2b 100644
--- a/connector/src/main/java/org/geysermc/connector/bootstrap/GeyserBootstrap.java
+++ b/connector/src/main/java/org/geysermc/connector/bootstrap/GeyserBootstrap.java
@@ -33,6 +33,7 @@ import org.geysermc.connector.command.CommandManager;
import org.geysermc.connector.network.translators.world.GeyserWorldManager;
import org.geysermc.connector.network.translators.world.WorldManager;
+import javax.annotation.Nullable;
import java.nio.file.Path;
public interface GeyserBootstrap {
@@ -99,4 +100,18 @@ public interface GeyserBootstrap {
* @return The info about the bootstrap
*/
BootstrapDumpInfo getDumpInfo();
+
+ /**
+ * Returns the Minecraft version currently being used on the server. This should be only be implemented on platforms
+ * that have direct server access - platforms such as proxies always have to be on their latest version to support
+ * the newest Minecraft version, but older servers can use ViaVersion to enable newer versions to join.
+ * null
if not applicable
+ */
+ @Nullable
+ default String getMinecraftServerVersion() {
+ return null;
+ }
}
diff --git a/connector/src/main/java/org/geysermc/connector/common/main/IGeyserMain.java b/connector/src/main/java/org/geysermc/connector/common/main/IGeyserMain.java
index 3f674d7fa..f91da11b5 100644
--- a/connector/src/main/java/org/geysermc/connector/common/main/IGeyserMain.java
+++ b/connector/src/main/java/org/geysermc/connector/common/main/IGeyserMain.java
@@ -52,7 +52,7 @@ public class IGeyserMain {
* @return The formatted message
*/
private String createMessage() {
- String message = "";
+ StringBuilder message = new StringBuilder();
InputStream helpStream = IGeyserMain.class.getClassLoader().getResourceAsStream("languages/run-help/" + Locale.getDefault().toString() + ".txt");
@@ -68,10 +68,10 @@ public class IGeyserMain {
line = line.replace("${plugin_type}", this.getPluginType());
line = line.replace("${plugin_folder}", this.getPluginFolder());
- message += line + "\n";
+ message.append(line).append("\n");
}
- return message;
+ return message.toString();
}
/**
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 e21aa6bb8..6052bd283 100644
--- a/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java
+++ b/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java
@@ -27,9 +27,11 @@ package org.geysermc.connector.configuration;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.geysermc.connector.GeyserLogger;
+import org.geysermc.connector.network.CIDRMatcher;
import org.geysermc.connector.utils.LanguageUtils;
import java.nio.file.Path;
+import java.util.List;
import java.util.Map;
public interface GeyserConfiguration {
@@ -59,6 +61,8 @@ public interface GeyserConfiguration {
int getPingPassthroughInterval();
+ boolean isForwardPlayerPing();
+
int getMaxPlayers();
boolean isDebugMode();
@@ -104,6 +108,15 @@ public interface GeyserConfiguration {
String getMotd2();
String getServerName();
+
+ boolean isEnableProxyProtocol();
+
+ Listboolean translate(Class extends P> clazz, P packet, GeyserSession session) { if (!session.getUpstream().isClosed() && !session.isClosed()) { try { - if (translators.containsKey(clazz)) { - ((PacketTranslator
) translators.get(clazz)).translate(packet, session); + PacketTranslator
translator = (PacketTranslator
) translators.get(clazz);
+ if (translator != null) {
+ translator.translate(packet, session);
return true;
} else {
- if (!IGNORED_PACKETS.contains(clazz))
+ if ((GeyserConnector.getInstance().getPlatformType() != PlatformType.STANDALONE || !(packet instanceof BedrockPacket)) && !IGNORED_PACKETS.contains(clazz)) {
+ // Other debug logs already take care of Bedrock packets for us if on standalone
GeyserConnector.getInstance().getLogger().debug("Could not find packet for " + (packet.toString().length() > 25 ? packet.getClass().getSimpleName() : packet));
+ }
}
} catch (Throwable ex) {
GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.network.translator.packet.failed", packet.getClass().getSimpleName()), ex);
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
index dd5d08a2c..8e2d77df7 100644
--- 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
@@ -26,17 +26,16 @@
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 org.geysermc.connector.network.translators.inventory.InventoryTranslator;
import java.util.Collections;
import java.util.LinkedList;
@@ -47,58 +46,55 @@ public class BedrockBookEditTranslator extends PacketTranslator
+ * Therefore, this function will return
+ * Here's an example of how the above would be presented to Bedrock (as of 1.16.200). Notice how the top two > commandArgs = new Int2ObjectOpenHashMap<>();
// Get the first node, it should be a root node
- CommandNode rootNode = packet.getNodes()[packet.getFirstNodeIndex()];
+ CommandNode rootNode = nodes[packet.getFirstNodeIndex()];
// Loop through the root nodes to get all commands
for (int nodeIndex : rootNode.getChildIndices()) {
- CommandNode node = packet.getNodes()[nodeIndex];
+ CommandNode node = nodes[nodeIndex];
// Make sure we don't have duplicated commands (happens if there is more than 1 root node)
- if (commands.containsKey(nodeIndex)) { continue; }
- if (commands.containsValue(node.getName())) { continue; }
+ if (!commandNodes.add(nodeIndex) || !knownAliases.add(node.getName().toLowerCase())) continue;
// Get and update the commandArgs list with the found arguments
if (node.getChildIndices().length >= 1) {
for (int childIndex : node.getChildIndices()) {
- commandArgs.putIfAbsent(nodeIndex, new ArrayList<>());
- commandArgs.get(nodeIndex).add(packet.getNodes()[childIndex]);
+ commandArgs.computeIfAbsent(nodeIndex, ArrayList::new).add(nodes[childIndex]);
}
}
- // Insert the command name into the list
- commands.put(nodeIndex, node.getName());
+ // Get and parse all params
+ CommandParamData[][] params = getParams(nodes[nodeIndex], nodes);
+
+ // Insert the alias name into the command list
+ commands.computeIfAbsent(params, index -> new HashSet<>()).add(node.getName().toLowerCase());
}
// The command flags, not sure what these do apart from break things
List
gamerule
command, and let's present three "subcommands" you can perform:
+ *
+ *
+ *
+ *
+ * While all three of them are indeed part of the same command, the command setting randomTickSpeed parses an int,
+ * while the others use boolean. In Bedrock, this should be presented as a separate overload to indicate that this
+ * does something a little different.
+ * gamerule doDaylightCycle true
gamerule announceAdvancements false
gamerule randomTickSpeed 3
true
if the first two are compared, as they use the same
+ * parsers. If the third is compared with either of the others, this function will return false
.
+ * CommandParamData
+ * classes of each array are identical in type, but the following class is different:
+ *
+ * overloads=[
+ * [
+ * CommandParamData(name=doDaylightCycle, optional=false, enumData=CommandEnumData(name=announceAdvancements, values=[announceAdvancements, doDaylightCycle], isSoft=false), type=STRING, postfix=null, options=[])
+ * CommandParamData(name=value, optional=false, enumData=CommandEnumData(name=value, values=[true, false], isSoft=false), type=null, postfix=null, options=[])
+ * ]
+ * [
+ * CommandParamData(name=randomTickSpeed, optional=false, enumData=CommandEnumData(name=randomTickSpeed, values=[randomTickSpeed], isSoft=false), type=STRING, postfix=null, options=[])
+ * CommandParamData(name=value, optional=false, enumData=null, type=INT, postfix=null, options=[])
+ * ]
+ * ]
+ *
+ *
+ * @return if these two can be merged into one overload.
+ */
+ private boolean isCompatible(CommandNode[] allNodes, CommandNode a, CommandNode b) {
+ if (a == b) return true;
+ if (a.getParser() != b.getParser()) return false;
+ if (a.getChildIndices().length != b.getChildIndices().length) return false;
+
+ for (int i = 0; i < a.getChildIndices().length; i++) {
+ boolean hasSimilarity = false;
+ CommandNode a1 = allNodes[a.getChildIndices()[i]];
+ // Search "b" until we find a child that matches this one
+ for (int j = 0; j < b.getChildIndices().length; j++) {
+ if (isCompatible(allNodes, a1, allNodes[b.getChildIndices()[j]])) {
+ hasSimilarity = true;
+ break;
+ }
+ }
+
+ if (!hasSimilarity) {
+ return false;
+ }
+ }
+ return true;
+ }
+
/**
* Get the tree of every parameter node (recursive)
*
@@ -301,13 +414,10 @@ public class JavaDeclareCommandsTranslator extends PacketTranslator> unsortedStonecutterData = new Int2ObjectOpenHashMap<>();
CraftingDataPacket craftingDataPacket = new CraftingDataPacket();
craftingDataPacket.setCleanRecipes(true);
for (Recipe recipe : packet.getRecipes()) {
@@ -60,25 +68,29 @@ public class JavaDeclareRecipesTranslator extends PacketTranslator
> data : unsortedStonecutterData.int2ObjectEntrySet()) {
+ // Sort the list by each output item's Java identifier - this is how it's sorted on Java, and therefore
+ // We can get the correct order for button pressing
+ data.getValue().sort(Comparator.comparing((stoneCuttingRecipeData ->
+ ItemRegistry.getItem(stoneCuttingRecipeData.getResult()).getJavaIdentifier())));
+
+ // Now that it's sorted, let's translate these recipes
+ for (StoneCuttingRecipeData stoneCuttingData : data.getValue()) {
+ // As of 1.16.4, all stonecutter recipes have one ingredient option
+ ItemStack ingredient = stoneCuttingData.getIngredient().getOptions()[0];
+ ItemData input = ItemTranslator.translateToBedrock(session, ingredient);
+ ItemData output = ItemTranslator.translateToBedrock(session, stoneCuttingData.getResult());
+ UUID uuid = UUID.randomUUID();
+
+ // We need to register stonecutting recipes so they show up on Bedrock
+ craftingDataPacket.getCraftingData().add(CraftingData.fromShapeless(uuid.toString(),
+ Collections.singletonList(input), Collections.singletonList(output), uuid, "stonecutter", 0, netId++));
+
+ // Save the recipe list for reference when crafting
+ IntList outputs = stonecutterRecipeMap.get(ingredient.getId());
+ if (outputs == null) {
+ outputs = new IntArrayList();
+ // Add the ingredient as the key and all possible values as the value
+ stonecutterRecipeMap.put(ingredient.getId(), outputs);
+ }
+ outputs.add(stoneCuttingData.getResult().getId());
+ }
+ }
+
session.sendUpstreamPacket(craftingDataPacket);
+ session.setCraftingRecipes(recipeMap);
+ session.getUnlockedRecipes().clear();
+ session.setStonecutterRecipes(stonecutterRecipeMap);
+ session.getLastRecipeNetId().set(netId);
}
//TODO: rewrite
+ /**
+ * The Java server sends an array of items for each ingredient you can use per slot in the crafting grid.
+ * Bedrock recipes take only one ingredient per crafting grid slot.
+ *
+ * @return the Java ingredient list as an array that Bedrock can understand
+ */
private ItemData[][] combinations(GeyserSession session, Ingredient[] ingredients) {
Map
+ *
+ * So, on Java Edition, the lectern is an inventory. Java opens it and gets the contents of the book there.
+ * On Bedrock, the lectern contents are part of the block entity tag. Therefore, Bedrock expects to have the contents
+ * of the lectern ready and present in the world. If the contents are not there, it takes at least two clicks for the
+ * lectern to update the tag and then present itself.
+ *
+ * We solve this problem by querying all loaded lecterns, where possible, and sending their information in a block entity
+ * tag.
+ *
+ * @param session the session of the player
+ * @param x the x coordinate of the lectern
+ * @param y the y coordinate of the lectern
+ * @param z the z coordinate of the lectern
+ * @param isChunkLoad if this is called during a chunk load or not. Changes behavior in certain instances.
+ * @return the Bedrock lectern block entity tag. This may not be the exact block entity tag - for example, Spigot's
+ * block handled must be done on the server thread, so we send the tag manually there.
+ */
+ public abstract NbtMap getLecternDataAt(GeyserSession session, int x, int y, int z, boolean isChunkLoad);
+
+ /**
+ * @return whether we should expect lectern data to update, or if we have to fall back on a workaround.
+ */
+ public abstract boolean shouldExpectLecternHandled();
+
/**
* Updates a gamerule value on the Java server
*
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockStateValues.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockStateValues.java
index 4ffb96d00..ff51562b9 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockStateValues.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockStateValues.java
@@ -26,11 +26,10 @@
package org.geysermc.connector.network.translators.world.block;
import com.fasterxml.jackson.databind.JsonNode;
-import com.nukkitx.nbt.NbtMap;
import it.unimi.dsi.fastutil.ints.*;
-import java.util.HashMap;
import java.util.Map;
+import java.util.function.BiFunction;
/**
* Used for block entities if the Java block state contains Bedrock block information.
@@ -41,7 +40,7 @@ public class BlockStateValues {
private static final Int2ByteMap COMMAND_BLOCK_VALUES = new Int2ByteOpenHashMap();
private static final Int2ObjectMap
- * The color names correspond to dye names, because of this we can't use {@link MessageTranslator#getColor(String)}.
+ * The color names correspond to dye names, because of this we can't use a more global method.
*
* @param javaColor The dye color stored in the sign's Color tag.
* @return A Bedrock Edition formatting code for valid dye colors, otherwise an empty string.
@@ -101,27 +102,34 @@ public class SignBlockEntityTranslator extends BlockEntityTranslator {
String signLine = getOrDefault(tag.getValue().get("Text" + currentLine), "");
signLine = MessageTranslator.convertMessageLenient(signLine);
- // Trim any trailing formatting codes
- if (signLine.length() > 2 && signLine.toCharArray()[signLine.length() - 2] == '\u00a7') {
- signLine = signLine.substring(0, signLine.length() - 2);
- }
-
// Check the character width on the sign to ensure there is no overflow that is usually hidden
// to Java Edition clients but will appear to Bedrock clients
int signWidth = 0;
StringBuilder finalSignLine = new StringBuilder();
+ boolean previousCharacterWasFormatting = false; // Color changes do not count for maximum width
for (char c : signLine.toCharArray()) {
- signWidth += SignUtils.getCharacterWidth(c);
+ if (c == '\u00a7') {
+ // Don't count this character
+ previousCharacterWasFormatting = true;
+ } else if (previousCharacterWasFormatting) {
+ // Don't count this character either
+ previousCharacterWasFormatting = false;
+ } else {
+ signWidth += SignUtils.getCharacterWidth(c);
+ }
+
if (signWidth <= SignUtils.BEDROCK_CHARACTER_WIDTH_MAX) {
finalSignLine.append(c);
} else {
+ // Adding the character would make Bedrock move to the next line - Java doesn't do that, so we do not want to
break;
}
}
// Java Edition 1.14 added the ability to change the text color of the whole sign using dye
- if (tag.contains("Color")) {
- signText.append(getBedrockSignColor(tag.get("Color").getValue().toString()));
+ Tag color = tag.get("Color");
+ if (color != null) {
+ signText.append(getBedrockSignColor(color.getValue().toString()));
}
signText.append(finalSignLine.toString());
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SkullBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SkullBlockEntityTranslator.java
index c41d45150..b3e25b212 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SkullBlockEntityTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SkullBlockEntityTranslator.java
@@ -46,7 +46,7 @@ import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
-@BlockEntity(name = "Skull", regex = "skull")
+@BlockEntity(name = "Skull")
public class SkullBlockEntityTranslator extends BlockEntityTranslator implements RequiresBlockState {
public static boolean ALLOW_CUSTOM_SKULLS;
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SpawnerBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SpawnerBlockEntityTranslator.java
index 5a6f974be..a9a37b60a 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SpawnerBlockEntityTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SpawnerBlockEntityTranslator.java
@@ -30,7 +30,7 @@ import com.github.steveice10.opennbt.tag.builtin.Tag;
import com.nukkitx.nbt.NbtMapBuilder;
import org.geysermc.connector.entity.type.EntityType;
-@BlockEntity(name = "MobSpawner", regex = "mob_spawner")
+@BlockEntity(name = "MobSpawner")
public class SpawnerBlockEntityTranslator extends BlockEntityTranslator {
@Override
public void translateTag(NbtMapBuilder builder, CompoundTag tag, int blockState) {
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/BlockStorage.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/BlockStorage.java
index 7a5086241..672fa1a35 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/BlockStorage.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/BlockStorage.java
@@ -30,7 +30,6 @@ import io.netty.buffer.ByteBuf;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import lombok.Getter;
-import org.geysermc.connector.network.translators.world.block.BlockTranslator;
import org.geysermc.connector.network.translators.world.chunk.bitarray.BitArray;
import org.geysermc.connector.network.translators.world.chunk.bitarray.BitArrayVersion;
@@ -44,14 +43,14 @@ public class BlockStorage {
private final IntList palette;
private BitArray bitArray;
- public BlockStorage() {
- this(BitArrayVersion.V2);
+ public BlockStorage(int airBlockId) {
+ this(airBlockId, BitArrayVersion.V2);
}
- public BlockStorage(BitArrayVersion version) {
+ public BlockStorage(int airBlockId, BitArrayVersion version) {
this.bitArray = version.createArray(SIZE);
this.palette = new IntArrayList(16);
- this.palette.add(BlockTranslator.BEDROCK_AIR_ID); // Air is at the start of every palette and controls what the default block is in second-layer non-air block spaces.
+ this.palette.add(airBlockId); // Air is at the start of every palette and controls what the default block is in second-layer non-air block spaces.
}
public BlockStorage(BitArray bitArray, IntList palette) {
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/ChunkSection.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/ChunkSection.java
index 2709e3e23..53528d654 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/ChunkSection.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/ChunkSection.java
@@ -34,8 +34,8 @@ public class ChunkSection {
private final BlockStorage[] storage;
- public ChunkSection() {
- this(new BlockStorage[]{new BlockStorage(), new BlockStorage()});
+ public ChunkSection(int airBlockId) {
+ this(new BlockStorage[]{new BlockStorage(airBlockId), new BlockStorage(airBlockId)});
}
public ChunkSection(BlockStorage[] storage) {
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/EmptyChunkProvider.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/EmptyChunkProvider.java
new file mode 100644
index 000000000..1bc7d684f
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/EmptyChunkProvider.java
@@ -0,0 +1,58 @@
+/*
+ * 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.world.chunk;
+
+import com.nukkitx.nbt.NBTOutputStream;
+import com.nukkitx.nbt.NbtMap;
+import com.nukkitx.nbt.NbtUtils;
+import lombok.Getter;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+public class EmptyChunkProvider {
+ @Getter
+ private final byte[] emptyLevelChunkData;
+ @Getter
+ private final ChunkSection emptySection;
+
+ public EmptyChunkProvider(int airId) {
+ BlockStorage emptyStorage = new BlockStorage(airId);
+ emptySection = new ChunkSection(new BlockStorage[]{emptyStorage});
+
+ try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
+ outputStream.write(new byte[258]); // Biomes + Border Size + Extra Data Size
+
+ try (NBTOutputStream stream = NbtUtils.createNetworkWriter(outputStream)) {
+ stream.writeTag(NbtMap.EMPTY);
+ }
+
+ emptyLevelChunkData = outputStream.toByteArray();
+ } catch (IOException e) {
+ throw new AssertionError("Unable to generate empty level chunk data");
+ }
+ }
+}
diff --git a/connector/src/main/java/org/geysermc/connector/skin/FloodgateSkinUploader.java b/connector/src/main/java/org/geysermc/connector/skin/FloodgateSkinUploader.java
new file mode 100644
index 000000000..9f1a515a0
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/skin/FloodgateSkinUploader.java
@@ -0,0 +1,211 @@
+/*
+ * 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.skin;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import lombok.Getter;
+import org.geysermc.connector.GeyserConnector;
+import org.geysermc.connector.GeyserLogger;
+import org.geysermc.connector.network.session.GeyserSession;
+import org.geysermc.connector.utils.Constants;
+import org.geysermc.connector.utils.PluginMessageUtils;
+import org.geysermc.floodgate.util.WebsocketEventType;
+import org.java_websocket.client.WebSocketClient;
+import org.java_websocket.handshake.ServerHandshake;
+
+import java.net.ConnectException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
+
+import static org.geysermc.connector.utils.PluginMessageUtils.getSkinChannel;
+
+public final class FloodgateSkinUploader {
+ private final ObjectMapper JACKSON = new ObjectMapper();
+ private final List