gameruleCache = new Object2ObjectOpenHashMap<>();
+
+ @Override
+ public int getBlockAt(GeyserSession session, int x, int y, int z) {
+ ChunkCache chunkCache = session.getChunkCache();
+ if (chunkCache != null) { // Chunk cache can be null if the session is closed asynchronously
+ return chunkCache.getBlockAt(x, y, z);
+ }
+ return 0;
+ }
+
+ @Override
+ public void getBlocksInSection(GeyserSession session, int x, int y, int z, Chunk chunk) {
+ ChunkCache chunkCache = session.getChunkCache();
+ Column cachedColumn;
+ Chunk cachedChunk;
+ if (chunkCache == null || (cachedColumn = chunkCache.getChunk(x, z)) == null || (cachedChunk = cachedColumn.getChunks()[y]) == null) {
+ return;
+ }
+
+ // Copy state IDs from cached chunk to output chunk
+ for (int blockY = 0; blockY < 16; blockY++) { // Cache-friendly iteration order
+ for (int blockZ = 0; blockZ < 16; blockZ++) {
+ for (int blockX = 0; blockX < 16; blockX++) {
+ chunk.set(blockX, blockY, blockZ, cachedChunk.get(blockX, blockY, blockZ));
+ }
+ }
+ }
+ }
+
+ @Override
+ public boolean hasMoreBlockDataThanChunkCache() {
+ // This implementation can only fetch data from the session chunk cache
+ return false;
+ }
+
+ @Override
+ public int[] getBiomeDataAt(GeyserSession session, int x, int z) {
+ if (session.getConnector().getConfig().isCacheChunks()) {
+ ChunkCache chunkCache = session.getChunkCache();
+ if (chunkCache != null) { // Chunk cache can be null if the session is closed asynchronously
+ Column column = chunkCache.getChunk(x, z);
+ if (column != null) { // Column can be null if the server sent a partial chunk update before the first ground-up-continuous one
+ return column.getBiomeData();
+ }
+ }
+ }
+ return new int[1024];
+ }
+
+ @Override
+ public void setGameRule(GeyserSession session, String name, Object value) {
+ session.sendDownstreamPacket(new ClientChatPacket("/gamerule " + name + " " + value));
+ gameruleCache.put(name, String.valueOf(value));
+ }
+
+ @Override
+ public Boolean getGameRuleBool(GeyserSession session, GameRule gameRule) {
+ String value = gameruleCache.get(gameRule.getJavaID());
+ if (value != null) {
+ return Boolean.parseBoolean(value);
+ }
+
+ return gameRule.getDefaultValue() != null ? (Boolean) gameRule.getDefaultValue() : false;
+ }
+
+ @Override
+ public int getGameRuleInt(GeyserSession session, GameRule gameRule) {
+ String value = gameruleCache.get(gameRule.getJavaID());
+ if (value != null) {
+ return Integer.parseInt(value);
+ }
+
+ return gameRule.getDefaultValue() != null ? (int) gameRule.getDefaultValue() : 0;
+ }
+
+ @Override
+ public void setPlayerGameMode(GeyserSession session, GameMode gameMode) {
+ session.sendDownstreamPacket(new ClientChatPacket("/gamemode " + gameMode.name().toLowerCase()));
+ }
+
+ @Override
+ public void setDifficulty(GeyserSession session, Difficulty difficulty) {
+ session.sendDownstreamPacket(new ClientChatPacket("/difficulty " + difficulty.name().toLowerCase()));
+ }
+
+ @Override
+ public boolean hasPermission(GeyserSession session, String permission) {
+ return false;
+ }
+}
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/WorldManager.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/WorldManager.java
index 325e68609..fec3bb33a 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/world/WorldManager.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/WorldManager.java
@@ -25,9 +25,13 @@
package org.geysermc.connector.network.translators.world;
+import com.github.steveice10.mc.protocol.data.game.chunk.Chunk;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
+import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
+import com.github.steveice10.mc.protocol.data.game.setting.Difficulty;
import com.nukkitx.math.vector.Vector3i;
import org.geysermc.connector.network.session.GeyserSession;
+import org.geysermc.connector.utils.GameRule;
/**
* Class that manages or retrieves various information
@@ -70,4 +74,87 @@ public abstract class WorldManager {
* @return the block state at the specified location
*/
public abstract int getBlockAt(GeyserSession session, int x, int y, int z);
+
+ /**
+ * Gets all block states in the specified chunk section.
+ *
+ * @param session the session
+ * @param x the chunk's X coordinate
+ * @param y the chunk's Y coordinate
+ * @param z the chunk's Z coordinate
+ * @param section the chunk section to store the block data in
+ */
+ public abstract void getBlocksInSection(GeyserSession session, int x, int y, int z, Chunk section);
+
+ /**
+ * Checks whether or not this world manager has access to more block data than the chunk cache.
+ *
+ * Some world managers (e.g. Spigot) can provide access to block data outside of the chunk cache, and even with chunk caching disabled. This
+ * method provides a means to check if this manager has this capability.
+ *
+ * @return whether or not this world manager has access to more block data than the chunk cache
+ */
+ public abstract boolean hasMoreBlockDataThanChunkCache();
+
+ /**
+ * Gets the biome data for the specified chunk.
+ *
+ * @param session the session of the player
+ * @param x the chunk's X coordinate
+ * @param z the chunk's Z coordinate
+ * @return the biome data for the specified region with a length of 1024.
+ */
+ public abstract int[] getBiomeDataAt(GeyserSession session, int x, int z);
+
+ /**
+ * Updates a gamerule value on the Java server
+ *
+ * @param session The session of the user that requested the change
+ * @param name The gamerule to change
+ * @param value The new value for the gamerule
+ */
+ public abstract void setGameRule(GeyserSession session, String name, Object value);
+
+ /**
+ * Get a gamerule value as a boolean
+ *
+ * @param session The session of the user that requested the value
+ * @param gameRule The gamerule to fetch the value of
+ * @return The boolean representation of the value
+ */
+ public abstract Boolean getGameRuleBool(GeyserSession session, GameRule gameRule);
+
+ /**
+ * Get a gamerule value as an integer
+ *
+ * @param session The session of the user that requested the value
+ * @param gameRule The gamerule to fetch the value of
+ * @return The integer representation of the value
+ */
+ public abstract int getGameRuleInt(GeyserSession session, GameRule gameRule);
+
+ /**
+ * Change the game mode of the given session
+ *
+ * @param session The session of the player to change the game mode of
+ * @param gameMode The game mode to change the player to
+ */
+ public abstract void setPlayerGameMode(GeyserSession session, GameMode gameMode);
+
+ /**
+ * Change the difficulty of the Java server
+ *
+ * @param session The session of the user that requested the change
+ * @param difficulty The difficulty to change to
+ */
+ public abstract void setDifficulty(GeyserSession session, Difficulty difficulty);
+
+ /**
+ * Checks if the given session's player has a permission
+ *
+ * @param session The session of the player to check the permission of
+ * @param permission The permission node to check
+ * @return True if the player has the requested permission, false if not
+ */
+ public abstract boolean hasPermission(GeyserSession session, String permission);
}
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 53607317a..305118e6f 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
@@ -39,6 +39,7 @@ public class BlockStateValues {
private static final Int2IntMap BANNER_COLORS = new Int2IntOpenHashMap();
private static final Int2ByteMap BED_COLORS = new Int2ByteOpenHashMap();
+ private static final Int2ByteMap COMMAND_BLOCK_VALUES = new Int2ByteOpenHashMap();
private static final Int2ObjectMap DOUBLE_CHEST_VALUES = new Int2ObjectOpenHashMap<>();
private static final Int2ObjectMap FLOWER_POT_VALUES = new Int2ObjectOpenHashMap<>();
private static final Map FLOWER_POT_BLOCKS = new HashMap<>();
@@ -67,6 +68,11 @@ public class BlockStateValues {
return;
}
+ if (entry.getKey().contains("command_block")) {
+ COMMAND_BLOCK_VALUES.put(javaBlockState, entry.getKey().contains("conditional=true") ? (byte) 1 : (byte) 0);
+ return;
+ }
+
if (entry.getValue().get("double_chest_position") != null) {
boolean isX = (entry.getValue().get("x") != null);
boolean isDirectionPositive = ((entry.getValue().get("x") != null && entry.getValue().get("x").asBoolean()) ||
@@ -138,6 +144,16 @@ public class BlockStateValues {
return -1;
}
+ /**
+ * The block state in Java and Bedrock both contain the conditional bit, however command block block entity tags
+ * in Bedrock need the conditional information.
+ *
+ * @return the list of all command blocks and if they are conditional (1 or 0)
+ */
+ public static Int2ByteMap getCommandBlockValues() {
+ return COMMAND_BLOCK_VALUES;
+ }
+
/**
* All double chest values are part of the block state in Java and part of the block entity tag in Bedrock.
* This gives the DoubleChestValue that can be calculated into the final tag.
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 e627b8454..e5f8d6aa0 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
@@ -28,15 +28,12 @@ package org.geysermc.connector.network.translators.world.block;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
-import com.nukkitx.nbt.NBTInputStream;
-import com.nukkitx.nbt.NbtList;
-import com.nukkitx.nbt.NbtMap;
-import com.nukkitx.nbt.NbtMapBuilder;
-import com.nukkitx.nbt.NbtType;
-import com.nukkitx.nbt.NbtUtils;
+import com.nukkitx.nbt.*;
import it.unimi.dsi.fastutil.ints.*;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.network.translators.world.block.entity.BlockEntity;
import org.geysermc.connector.utils.FileUtils;
@@ -52,6 +49,11 @@ public class BlockTranslator {
private static final Int2IntMap JAVA_TO_BEDROCK_BLOCK_MAP = new Int2IntOpenHashMap();
private static final Int2IntMap BEDROCK_TO_JAVA_BLOCK_MAP = new Int2IntOpenHashMap();
+ /**
+ * Stores a list of differences in block identifiers.
+ * Items will not be added to this list if the key and value is the same.
+ */
+ private static final Object2ObjectMap JAVA_TO_BEDROCK_IDENTIFIERS = new Object2ObjectOpenHashMap<>();
private static final BiMap JAVA_ID_BLOCK_MAP = HashBiMap.create();
private static final IntSet WATERLOGGED = new IntOpenHashSet();
private static final Object2IntMap ITEM_FRAMES = new Object2IntOpenHashMap<>();
@@ -65,6 +67,11 @@ public class BlockTranslator {
public static final Int2BooleanMap JAVA_RUNTIME_ID_TO_CAN_HARVEST_WITH_HAND = new Int2BooleanOpenHashMap();
public static final Int2ObjectMap JAVA_RUNTIME_ID_TO_TOOL_TYPE = new Int2ObjectOpenHashMap<>();
+ /**
+ * Runtime command block ID, used for fixing command block minecart appearances
+ */
+ public static final int BEDROCK_RUNTIME_COMMAND_BLOCK_ID;
+
// For block breaking animation math
public static final IntSet JAVA_RUNTIME_WOOL_IDS = new IntOpenHashSet();
public static final int JAVA_RUNTIME_COBWEB_ID;
@@ -106,13 +113,14 @@ public class BlockTranslator {
addedStatesMap.defaultReturnValue(-1);
List paletteList = new ArrayList<>();
- Reflections ref = new Reflections("org.geysermc.connector.network.translators.world.block.entity");
+ Reflections ref = GeyserConnector.getInstance().useXmlReflections() ? FileUtils.getReflections("org.geysermc.connector.network.translators.world.block.entity") : new Reflections("org.geysermc.connector.network.translators.world.block.entity");
ref.getTypesAnnotatedWith(BlockEntity.class);
int waterRuntimeId = -1;
int javaRuntimeId = -1;
int bedrockRuntimeId = 0;
int cobwebRuntimeId = -1;
+ int commandBlockRuntimeId = -1;
int furnaceRuntimeId = -1;
int furnaceLitRuntimeId = -1;
int spawnerRuntimeId = -1;
@@ -140,23 +148,15 @@ public class BlockTranslator {
JAVA_RUNTIME_ID_TO_TOOL_TYPE.put(javaRuntimeId, toolTypeNode.textValue());
}
- if (javaId.contains("wool")) {
- JAVA_RUNTIME_WOOL_IDS.add(javaRuntimeId);
- }
-
- if (javaId.contains("cobweb")) {
- cobwebRuntimeId = javaRuntimeId;
- }
-
JAVA_ID_BLOCK_MAP.put(javaId, javaRuntimeId);
// Used for adding all "special" Java block states to block state map
String identifier;
- String bedrock_identifer = entry.getValue().get("bedrock_identifier").asText();
+ String bedrockIdentifier = entry.getValue().get("bedrock_identifier").asText();
for (Class> clazz : ref.getTypesAnnotatedWith(BlockEntity.class)) {
identifier = clazz.getAnnotation(BlockEntity.class).regex();
// Endswith, or else the block bedrock gets picked up for bed
- if (bedrock_identifer.endsWith(identifier) && !identifier.equals("")) {
+ if (bedrockIdentifier.endsWith(identifier) && !identifier.equals("")) {
JAVA_ID_TO_BLOCK_ENTITY_MAP.put(javaRuntimeId, clazz.getAnnotation(BlockEntity.class).name());
break;
}
@@ -164,9 +164,15 @@ public class BlockTranslator {
BlockStateValues.storeBlockStateValues(entry, javaRuntimeId);
+ String cleanJavaIdentifier = entry.getKey().split("\\[")[0];
+
+ if (!cleanJavaIdentifier.equals(bedrockIdentifier)) {
+ JAVA_TO_BEDROCK_IDENTIFIERS.put(cleanJavaIdentifier, bedrockIdentifier);
+ }
+
// Get the tag needed for non-empty flower pots
if (entry.getValue().get("pottable") != null) {
- BlockStateValues.getFlowerPotBlocks().put(entry.getKey().split("\\[")[0], buildBedrockState(entry.getValue()));
+ BlockStateValues.getFlowerPotBlocks().put(cleanJavaIdentifier, buildBedrockState(entry.getValue()));
}
if ("minecraft:water[level=0]".equals(javaId)) {
@@ -197,15 +203,23 @@ public class BlockTranslator {
}
JAVA_TO_BEDROCK_BLOCK_MAP.put(javaRuntimeId, bedrockRuntimeId);
- if (javaId.startsWith("minecraft:furnace[facing=north")) {
+ if (javaId.contains("wool")) {
+ JAVA_RUNTIME_WOOL_IDS.add(javaRuntimeId);
+
+ } else if (javaId.contains("cobweb")) {
+ cobwebRuntimeId = javaRuntimeId;
+
+ } else if (javaId.equals("minecraft:command_block[conditional=false,facing=north]")) {
+ commandBlockRuntimeId = bedrockRuntimeId;
+
+ } else if (javaId.startsWith("minecraft:furnace[facing=north")) {
if (javaId.contains("lit=true")) {
furnaceLitRuntimeId = javaRuntimeId;
} else {
furnaceRuntimeId = javaRuntimeId;
}
- }
- if (javaId.startsWith("minecraft:spawner")) {
+ } else if (javaId.startsWith("minecraft:spawner")) {
spawnerRuntimeId = javaRuntimeId;
}
@@ -217,6 +231,11 @@ public class BlockTranslator {
}
JAVA_RUNTIME_COBWEB_ID = cobwebRuntimeId;
+ if (commandBlockRuntimeId == -1) {
+ throw new AssertionError("Unable to find command block in palette");
+ }
+ BEDROCK_RUNTIME_COMMAND_BLOCK_ID = commandBlockRuntimeId;
+
if (furnaceRuntimeId == -1) {
throw new AssertionError("Unable to find furnace in palette");
}
@@ -297,6 +316,14 @@ public class BlockTranslator {
return BEDROCK_TO_JAVA_BLOCK_MAP.get(bedrockId);
}
+ /**
+ * @param javaIdentifier the Java identifier of the block to search for
+ * @return the Bedrock identifier if different, or else the Java identifier
+ */
+ public static String getBedrockBlockIdentifier(String javaIdentifier) {
+ return JAVA_TO_BEDROCK_IDENTIFIERS.getOrDefault(javaIdentifier, javaIdentifier);
+ }
+
public static int getItemFrame(NbtMap tag) {
return ITEM_FRAMES.getOrDefault(tag, -1);
}
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BannerBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BannerBlockEntityTranslator.java
index 9e86cb4cf..57393a6c5 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BannerBlockEntityTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BannerBlockEntityTranslator.java
@@ -27,12 +27,9 @@ package org.geysermc.connector.network.translators.world.block.entity;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.ListTag;
-import com.nukkitx.nbt.NbtMap;
-import com.nukkitx.nbt.NbtType;
import org.geysermc.connector.network.translators.item.translators.BannerTranslator;
import org.geysermc.connector.network.translators.world.block.BlockStateValues;
-import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@@ -65,17 +62,4 @@ public class BannerBlockEntityTranslator extends BlockEntityTranslator implement
return tags;
}
- @Override
- public CompoundTag getDefaultJavaTag(String javaId, int x, int y, int z) {
- CompoundTag tag = getConstantJavaTag(javaId, x, y, z);
- tag.put(new ListTag("Patterns"));
- return tag;
- }
-
- @Override
- public NbtMap getDefaultBedrockTag(String bedrockId, int x, int y, int z) {
- return getConstantBedrockTag(bedrockId, x, y, z).toBuilder()
- .putList("Patterns", NbtType.COMPOUND, new ArrayList<>())
- .build();
- }
}
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BedBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BedBlockEntityTranslator.java
index b84aad984..080bdc3b2 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BedBlockEntityTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BedBlockEntityTranslator.java
@@ -26,7 +26,6 @@
package org.geysermc.connector.network.translators.world.block.entity;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
-import com.nukkitx.nbt.NbtMap;
import org.geysermc.connector.network.translators.world.block.BlockStateValues;
import java.util.HashMap;
@@ -50,15 +49,4 @@ public class BedBlockEntityTranslator extends BlockEntityTranslator implements R
return tags;
}
- @Override
- public CompoundTag getDefaultJavaTag(String javaId, int x, int y, int z) {
- return null;
- }
-
- @Override
- public NbtMap getDefaultBedrockTag(String bedrockId, int x, int y, int z) {
- return getConstantBedrockTag(bedrockId, x, y, z).toBuilder()
- .putByte("color", (byte) 0)
- .build();
- }
}
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BlockEntityTranslator.java
index c4401c4c8..4df4fd95e 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BlockEntityTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BlockEntityTranslator.java
@@ -28,12 +28,13 @@ package org.geysermc.connector.network.translators.world.block.entity;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.IntTag;
import com.github.steveice10.opennbt.tag.builtin.StringTag;
+import com.github.steveice10.opennbt.tag.builtin.Tag;
import com.nukkitx.nbt.NbtMap;
import com.nukkitx.nbt.NbtMapBuilder;
-
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.utils.BlockEntityUtils;
+import org.geysermc.connector.utils.FileUtils;
import org.geysermc.connector.utils.LanguageUtils;
import org.reflections.Reflections;
@@ -52,6 +53,7 @@ public abstract class BlockEntityTranslator {
{
// Bedrock/Java differences
put("minecraft:enchanting_table", "EnchantTable");
+ put("minecraft:jigsaw", "JigsawBlock");
put("minecraft:piston_head", "PistonArm");
put("minecraft:trapped_chest", "Chest");
// There are some legacy IDs sent but as far as I can tell they are not needed for things to work properly
@@ -66,7 +68,7 @@ public abstract class BlockEntityTranslator {
}
static {
- Reflections ref = new Reflections("org.geysermc.connector.network.translators.world.block.entity");
+ Reflections ref = GeyserConnector.getInstance().useXmlReflections() ? FileUtils.getReflections("org.geysermc.connector.network.translators.world.block.entity") : new Reflections("org.geysermc.connector.network.translators.world.block.entity");
for (Class> clazz : ref.getTypesAnnotatedWith(BlockEntity.class)) {
GeyserConnector.getInstance().getLogger().debug("Found annotated block entity: " + clazz.getCanonicalName());
@@ -89,10 +91,6 @@ public abstract class BlockEntityTranslator {
public abstract Map translateTag(CompoundTag tag, int blockState);
- public abstract CompoundTag getDefaultJavaTag(String javaId, int x, int y, int z);
-
- public abstract NbtMap getDefaultBedrockTag(String bedrockId, int x, int y, int z);
-
public NbtMap getBlockEntityTag(String id, CompoundTag tag, int blockState) {
int x = Integer.parseInt(String.valueOf(tag.getValue().get("x").getValue()));
int y = Integer.parseInt(String.valueOf(tag.getValue().get("y").getValue()));
@@ -123,7 +121,7 @@ public abstract class BlockEntityTranslator {
}
@SuppressWarnings("unchecked")
- protected T getOrDefault(com.github.steveice10.opennbt.tag.builtin.Tag tag, T defaultValue) {
+ protected T getOrDefault(Tag tag, T defaultValue) {
return (tag != null && tag.getValue() != null) ? (T) tag.getValue() : defaultValue;
}
}
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 e3d2c9f5e..d6ac0281b 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
@@ -50,18 +50,6 @@ public class CampfireBlockEntityTranslator extends BlockEntityTranslator {
return tags;
}
- @Override
- public CompoundTag getDefaultJavaTag(String javaId, int x, int y, int z) {
- CompoundTag tag = getConstantJavaTag(javaId, x, y, z);
- tag.put(new ListTag("Items"));
- return tag;
- }
-
- @Override
- public NbtMap getDefaultBedrockTag(String bedrockId, int x, int y, int z) {
- return getConstantBedrockTag(bedrockId, x, y, z);
- }
-
protected NbtMap getItem(CompoundTag tag) {
ItemEntry entry = ItemRegistry.getItemEntry((String) tag.get("id").getValue());
NbtMapBuilder tagBuilder = NbtMap.builder()
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/CommandBlockBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/CommandBlockBlockEntityTranslator.java
new file mode 100644
index 000000000..6bc940adb
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/CommandBlockBlockEntityTranslator.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2019-2020 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.block.entity;
+
+import com.github.steveice10.opennbt.tag.builtin.*;
+import org.geysermc.connector.network.translators.world.block.BlockStateValues;
+import org.geysermc.connector.utils.MessageUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@BlockEntity(name = "CommandBlock", regex = "command_block")
+public class CommandBlockBlockEntityTranslator extends BlockEntityTranslator implements RequiresBlockState {
+
+ @Override
+ public Map translateTag(CompoundTag tag, int blockState) {
+ Map map = new HashMap<>();
+ if (tag.size() < 5) {
+ return map; // These values aren't here
+ }
+ // Java infers from the block state, but Bedrock needs it in the tag
+ map.put("conditionalMode", BlockStateValues.getCommandBlockValues().getOrDefault(blockState, (byte) 0));
+ // Java and Bedrock values
+ map.put("conditionMet", ((ByteTag) tag.get("conditionMet")).getValue());
+ map.put("auto", ((ByteTag) tag.get("auto")).getValue());
+ map.put("CustomName", MessageUtils.getBedrockMessage(((StringTag) tag.get("CustomName")).getValue()));
+ map.put("powered", ((ByteTag) tag.get("powered")).getValue());
+ map.put("Command", ((StringTag) tag.get("Command")).getValue());
+ map.put("SuccessCount", ((IntTag) tag.get("SuccessCount")).getValue());
+ map.put("TrackOutput", ((ByteTag) tag.get("TrackOutput")).getValue());
+ map.put("UpdateLastExecution", ((ByteTag) tag.get("UpdateLastExecution")).getValue());
+ if (tag.get("LastExecution") != null) {
+ map.put("LastExecution", ((LongTag) tag.get("LastExecution")).getValue());
+ } else {
+ map.put("LastExecution", (long) 0);
+ }
+ return map;
+ }
+
+ @Override
+ public boolean isBlock(int blockState) {
+ return BlockStateValues.getCommandBlockValues().containsKey(blockState);
+ }
+}
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/DoubleChestBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/DoubleChestBlockEntityTranslator.java
index fa8bab3b0..5b59420e0 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/DoubleChestBlockEntityTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/DoubleChestBlockEntityTranslator.java
@@ -27,7 +27,6 @@ package org.geysermc.connector.network.translators.world.block.entity;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.nukkitx.math.vector.Vector3i;
-import com.nukkitx.nbt.NbtMap;
import com.nukkitx.nbt.NbtMapBuilder;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.world.block.BlockStateValues;
@@ -92,13 +91,4 @@ public class DoubleChestBlockEntityTranslator extends BlockEntityTranslator impl
return tags;
}
- @Override
- public CompoundTag getDefaultJavaTag(String javaId, int x, int y, int z) {
- return null;
- }
-
- @Override
- public NbtMap getDefaultBedrockTag(String bedrockId, int x, int y, int z) {
- return null;
- }
}
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/EmptyBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/EmptyBlockEntityTranslator.java
index 6de136119..e9715bd32 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/EmptyBlockEntityTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/EmptyBlockEntityTranslator.java
@@ -26,7 +26,6 @@
package org.geysermc.connector.network.translators.world.block.entity;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
-import com.nukkitx.nbt.NbtMap;
import java.util.HashMap;
import java.util.Map;
@@ -39,13 +38,4 @@ public class EmptyBlockEntityTranslator extends BlockEntityTranslator {
return new HashMap<>();
}
- @Override
- public CompoundTag getDefaultJavaTag(String javaId, int x, int y, int z) {
- return getConstantJavaTag(javaId, x, y, z);
- }
-
- @Override
- public NbtMap getDefaultBedrockTag(String bedrockId, int x, int y, int z) {
- return getConstantBedrockTag(bedrockId, x, y, z);
- }
}
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/EndGatewayBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/EndGatewayBlockEntityTranslator.java
index 784afed5b..af94c560d 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/EndGatewayBlockEntityTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/EndGatewayBlockEntityTranslator.java
@@ -26,14 +26,12 @@
package org.geysermc.connector.network.translators.world.block.entity;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
-import com.github.steveice10.opennbt.tag.builtin.LongTag;
+import com.github.steveice10.opennbt.tag.builtin.IntTag;
import com.nukkitx.nbt.NbtList;
-import com.nukkitx.nbt.NbtMap;
import com.nukkitx.nbt.NbtType;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
-import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -56,25 +54,11 @@ public class EndGatewayBlockEntityTranslator extends BlockEntityTranslator {
return tags;
}
- @Override
- public CompoundTag getDefaultJavaTag(String javaId, int x, int y, int z) {
- CompoundTag tag = getConstantJavaTag(javaId, x, y, z);
- tag.put(new LongTag("Age"));
- return tag;
- }
-
- @Override
- public NbtMap getDefaultBedrockTag(String bedrockId, int x, int y, int z) {
- return getConstantBedrockTag(bedrockId, x, y, z).toBuilder()
- .putList("ExitPortal", NbtType.INT, Arrays.asList(0, 0, 0))
- .build();
- }
-
private int getExitPortalCoordinate(CompoundTag tag, String axis) {
// Return 0 if it doesn't exist, otherwise give proper value
if (tag.get("ExitPortal") != null) {
LinkedHashMap, ?> compoundTag = (LinkedHashMap, ?>) tag.get("ExitPortal").getValue();
- com.github.steveice10.opennbt.tag.builtin.IntTag intTag = (com.github.steveice10.opennbt.tag.builtin.IntTag) compoundTag.get(axis);
+ IntTag intTag = (IntTag) compoundTag.get(axis);
return intTag.getValue();
} return 0;
}
diff --git a/connector/src/main/java/org/geysermc/connector/network/session/cache/DataCache.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/JigsawBlockBlockEntityTranslator.java
similarity index 59%
rename from connector/src/main/java/org/geysermc/connector/network/session/cache/DataCache.java
rename to connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/JigsawBlockBlockEntityTranslator.java
index 4b2af9630..43ac1a96f 100644
--- a/connector/src/main/java/org/geysermc/connector/network/session/cache/DataCache.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/JigsawBlockBlockEntityTranslator.java
@@ -23,15 +23,25 @@
* @link https://github.com/GeyserMC/Geyser
*/
-package org.geysermc.connector.network.session.cache;
+package org.geysermc.connector.network.translators.world.block.entity;
-import lombok.Getter;
+import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
+import com.github.steveice10.opennbt.tag.builtin.StringTag;
import java.util.HashMap;
import java.util.Map;
-public class DataCache {
+@BlockEntity(name = "JigsawBlock", regex = "jigsaw")
+public class JigsawBlockBlockEntityTranslator extends BlockEntityTranslator {
- @Getter
- private Map cachedValues = new HashMap();
+ @Override
+ public Map translateTag(CompoundTag tag, int blockState) {
+ Map map = new HashMap<>();
+ map.put("joint", ((StringTag) tag.get("joint")).getValue());
+ map.put("name", ((StringTag) tag.get("name")).getValue());
+ map.put("target_pool", ((StringTag) tag.get("pool")).getValue());
+ map.put("final_state", ((StringTag) tag.get("final_state")).getValue());
+ map.put("target", ((StringTag) tag.get("target")).getValue());
+ return map;
+ }
}
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/ShulkerBoxBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/ShulkerBoxBlockEntityTranslator.java
index 08a7ae187..08e3abaab 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/ShulkerBoxBlockEntityTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/ShulkerBoxBlockEntityTranslator.java
@@ -26,7 +26,6 @@
package org.geysermc.connector.network.translators.world.block.entity;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
-import com.nukkitx.nbt.NbtMap;
import org.geysermc.connector.network.translators.world.block.BlockStateValues;
import java.util.HashMap;
@@ -46,15 +45,4 @@ public class ShulkerBoxBlockEntityTranslator extends BlockEntityTranslator {
return tags;
}
- @Override
- public CompoundTag getDefaultJavaTag(String javaId, int x, int y, int z) {
- return null;
- }
-
- @Override
- public NbtMap getDefaultBedrockTag(String bedrockId, int x, int y, int z) {
- return getConstantBedrockTag(bedrockId, x, y, z).toBuilder()
- .putByte("facing", (byte) 1)
- .build();
- }
}
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SignBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SignBlockEntityTranslator.java
index a95c853e7..b40ed42c9 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SignBlockEntityTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SignBlockEntityTranslator.java
@@ -27,8 +27,8 @@ package org.geysermc.connector.network.translators.world.block.entity;
import com.github.steveice10.mc.protocol.data.message.MessageSerializer;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
-import com.nukkitx.nbt.NbtMap;
import org.geysermc.connector.utils.MessageUtils;
+import org.geysermc.connector.utils.SignUtils;
import java.util.HashMap;
import java.util.Map;
@@ -46,12 +46,25 @@ public class SignBlockEntityTranslator extends BlockEntityTranslator {
String signLine = getOrDefault(tag.getValue().get("Text" + currentLine), "");
signLine = MessageUtils.getBedrockMessage(MessageSerializer.fromString(signLine));
- //Java allows up to 16+ characters on certain symbols.
- if(signLine.length() >= 15 && (signLine.contains("-") || signLine.contains("="))) {
- signLine = signLine.substring(0, 14);
+ // 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();
+ for (char c : signLine.toCharArray()) {
+ signWidth += SignUtils.getCharacterWidth(c);
+ if (signWidth <= SignUtils.BEDROCK_CHARACTER_WIDTH_MAX) {
+ finalSignLine.append(c);
+ } else {
+ break;
+ }
}
- signText.append(signLine);
+ // 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()));
+ }
+
+ signText.append(finalSignLine.toString());
signText.append("\n");
}
@@ -59,20 +72,65 @@ public class SignBlockEntityTranslator extends BlockEntityTranslator {
return tags;
}
- @Override
- public CompoundTag getDefaultJavaTag(String javaId, int x, int y, int z) {
- CompoundTag tag = getConstantJavaTag(javaId, x, y, z);
- tag.put(new com.github.steveice10.opennbt.tag.builtin.StringTag("Text1", "{\"text\":\"\"}"));
- tag.put(new com.github.steveice10.opennbt.tag.builtin.StringTag("Text2", "{\"text\":\"\"}"));
- tag.put(new com.github.steveice10.opennbt.tag.builtin.StringTag("Text3", "{\"text\":\"\"}"));
- tag.put(new com.github.steveice10.opennbt.tag.builtin.StringTag("Text4", "{\"text\":\"\"}"));
- return tag;
+ /**
+ * Maps a color stored in a sign's Color tag to a Bedrock Edition formatting code.
+ *
+ * The color names correspond to dye names, because of this we can't use {@link MessageUtils#getColor(String)}.
+ *
+ * @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.
+ */
+ private static String getBedrockSignColor(String javaColor) {
+ String base = "\u00a7";
+ switch (javaColor) {
+ case "white":
+ base += 'f';
+ break;
+ case "orange":
+ base += '6';
+ break;
+ case "magenta":
+ case "purple":
+ base += '5';
+ break;
+ case "light_blue":
+ base += 'b';
+ break;
+ case "yellow":
+ base += 'e';
+ break;
+ case "lime":
+ base += 'a';
+ break;
+ case "pink":
+ base += 'd';
+ break;
+ case "gray":
+ base += '8';
+ break;
+ case "light_gray":
+ base += '7';
+ break;
+ case "cyan":
+ base += '3';
+ break;
+ case "blue":
+ base += '9';
+ break;
+ case "brown": // Brown does not have a bedrock counterpart.
+ case "red": // In Java Edition light red (&c) can only be applied using commands. Red dye gives &4.
+ base += '4';
+ break;
+ case "green":
+ base += '2';
+ break;
+ case "black":
+ base += '0';
+ break;
+ default:
+ return "";
+ }
+ return base;
}
- @Override
- public NbtMap getDefaultBedrockTag(String bedrockId, int x, int y, int z) {
- return getConstantBedrockTag(bedrockId, x, y, z).toBuilder()
- .putString("Text", "")
- .build();
- }
}
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 9547ba2ff..6d350c0cc 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
@@ -25,7 +25,6 @@
package org.geysermc.connector.network.translators.world.block.entity;
-import com.nukkitx.nbt.NbtMap;
import org.geysermc.connector.network.translators.world.block.BlockStateValues;
import java.util.HashMap;
@@ -51,16 +50,4 @@ public class SkullBlockEntityTranslator extends BlockEntityTranslator implements
return tags;
}
- @Override
- public com.github.steveice10.opennbt.tag.builtin.CompoundTag getDefaultJavaTag(String javaId, int x, int y, int z) {
- return null;
- }
-
- @Override
- public NbtMap getDefaultBedrockTag(String bedrockId, int x, int y, int z) {
- return getConstantBedrockTag(bedrockId, x, y, z).toBuilder()
- .putFloat("Rotation", 0f)
- .putByte("SkullType", (byte) 0)
- .build();
- }
}
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 3c443eeeb..2601e3de9 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
@@ -26,7 +26,6 @@
package org.geysermc.connector.network.translators.world.block.entity;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
-import com.nukkitx.nbt.NbtMap;
import org.geysermc.connector.entity.type.EntityType;
import java.util.HashMap;
@@ -86,16 +85,4 @@ public class SpawnerBlockEntityTranslator extends BlockEntityTranslator {
return tags;
}
- @Override
- public CompoundTag getDefaultJavaTag(String javaId, int x, int y, int z) {
- return null;
- }
-
- @Override
- public NbtMap getDefaultBedrockTag(String bedrockId, int x, int y, int z) {
- return getConstantBedrockTag(bedrockId, x, y, z).toBuilder()
- .putByte("isMovable", (byte) 1)
- .putString("id", "MobSpawner")
- .build();
- }
}
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 30edf1781..d8cd75206 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
@@ -29,14 +29,16 @@ import com.nukkitx.network.VarInts;
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.chunk.bitarray.BitArray;
import org.geysermc.connector.network.translators.world.chunk.bitarray.BitArrayVersion;
import java.util.function.IntConsumer;
+@Getter
public class BlockStorage {
- private static final int SIZE = 4096;
+ public static final int SIZE = 4096;
private final IntList palette;
private BitArray bitArray;
@@ -46,12 +48,12 @@ public class BlockStorage {
}
public BlockStorage(BitArrayVersion version) {
- this.bitArray = version.createPalette(SIZE);
+ this.bitArray = version.createArray(SIZE);
this.palette = new IntArrayList(16);
this.palette.add(0); // Air is at the start of every palette.
}
- private BlockStorage(BitArray bitArray, IntArrayList palette) {
+ public BlockStorage(BitArray bitArray, IntList palette) {
this.palette = palette;
this.bitArray = bitArray;
}
@@ -64,16 +66,16 @@ public class BlockStorage {
return BitArrayVersion.get(header >> 1, true);
}
- public synchronized int getFullBlock(int index) {
+ public int getFullBlock(int index) {
return this.palette.getInt(this.bitArray.get(index));
}
- public synchronized void setFullBlock(int index, int runtimeId) {
+ public void setFullBlock(int index, int runtimeId) {
int idx = this.idFor(runtimeId);
this.bitArray.set(index, idx);
}
- public synchronized void writeToNetwork(ByteBuf buffer) {
+ public void writeToNetwork(ByteBuf buffer) {
buffer.writeByte(getPaletteHeader(bitArray.getVersion(), true));
for (int word : bitArray.getWords()) {
@@ -84,8 +86,18 @@ public class BlockStorage {
palette.forEach((IntConsumer) id -> VarInts.writeInt(buffer, id));
}
+ public int estimateNetworkSize() {
+ int size = 1; // Palette header
+ size += this.bitArray.getWords().length * 4;
+
+ // We assume that none of the VarInts will be larger than 3 bytes
+ size += 3; // Palette size
+ size += this.palette.size() * 3;
+ return size;
+ }
+
private void onResize(BitArrayVersion version) {
- BitArray newBitArray = version.createPalette(SIZE);
+ BitArray newBitArray = version.createArray(SIZE);
for (int i = 0; i < SIZE; i++) {
newBitArray.set(i, this.bitArray.get(i));
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 48ec88064..979b79c93 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
@@ -27,42 +27,19 @@ package org.geysermc.connector.network.translators.world.chunk;
import com.nukkitx.network.util.Preconditions;
import io.netty.buffer.ByteBuf;
-import lombok.Synchronized;
public class ChunkSection {
private static final int CHUNK_SECTION_VERSION = 8;
- public static final int SIZE = 4096;
private final BlockStorage[] storage;
- private final NibbleArray blockLight;
- private final NibbleArray skyLight;
public ChunkSection() {
- this(new BlockStorage[]{new BlockStorage(), new BlockStorage()}, new NibbleArray(SIZE),
- new NibbleArray(SIZE));
+ this(new BlockStorage[]{new BlockStorage(), new BlockStorage()});
}
- public ChunkSection(BlockStorage[] blockStorage) {
- this(blockStorage, new NibbleArray(SIZE), new NibbleArray(SIZE));
- }
-
- public ChunkSection(BlockStorage[] storage, byte[] blockLight, byte[] skyLight) {
- Preconditions.checkNotNull(storage, "storage");
- Preconditions.checkArgument(storage.length > 1, "Block storage length must be at least 2");
- for (BlockStorage blockStorage : storage) {
- Preconditions.checkNotNull(blockStorage, "storage");
- }
-
+ public ChunkSection(BlockStorage[] storage) {
this.storage = storage;
- this.blockLight = new NibbleArray(blockLight);
- this.skyLight = new NibbleArray(skyLight);
- }
-
- private ChunkSection(BlockStorage[] storage, NibbleArray blockLight, NibbleArray skyLight) {
- this.storage = storage;
- this.blockLight = blockLight;
- this.skyLight = skyLight;
}
public int getFullBlock(int x, int y, int z, int layer) {
@@ -77,30 +54,6 @@ public class ChunkSection {
this.storage[layer].setFullBlock(blockPosition(x, y, z), fullBlock);
}
- @Synchronized("skyLight")
- public byte getSkyLight(int x, int y, int z) {
- checkBounds(x, y, z);
- return this.skyLight.get(blockPosition(x, y, z));
- }
-
- @Synchronized("skyLight")
- public void setSkyLight(int x, int y, int z, byte val) {
- checkBounds(x, y, z);
- this.skyLight.set(blockPosition(x, y, z), val);
- }
-
- @Synchronized("blockLight")
- public byte getBlockLight(int x, int y, int z) {
- checkBounds(x, y, z);
- return this.blockLight.get(blockPosition(x, y, z));
- }
-
- @Synchronized("blockLight")
- public void setBlockLight(int x, int y, int z, byte val) {
- checkBounds(x, y, z);
- this.blockLight.set(blockPosition(x, y, z), val);
- }
-
public void writeToNetwork(ByteBuf buffer) {
buffer.writeByte(CHUNK_SECTION_VERSION);
buffer.writeByte(this.storage.length);
@@ -109,12 +62,12 @@ public class ChunkSection {
}
}
- public NibbleArray getSkyLightArray() {
- return skyLight;
- }
-
- public NibbleArray getBlockLightArray() {
- return blockLight;
+ public int estimateNetworkSize() {
+ int size = 2; // Version + storage count
+ for (BlockStorage blockStorage : this.storage) {
+ size += blockStorage.estimateNetworkSize();
+ }
+ return size;
}
public BlockStorage[] getBlockStorageArray() {
@@ -135,7 +88,7 @@ public class ChunkSection {
for (int i = 0; i < storage.length; i++) {
storage[i] = this.storage[i].copy();
}
- return new ChunkSection(storage, skyLight.copy(), blockLight.copy());
+ return new ChunkSection(storage);
}
public static int blockPosition(int x, int y, int z) {
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/bitarray/BitArrayVersion.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/bitarray/BitArrayVersion.java
index 20fa849c2..47a73f7c1 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/bitarray/BitArrayVersion.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/bitarray/BitArrayVersion.java
@@ -37,6 +37,8 @@ public enum BitArrayVersion {
V2(2, 16, V3),
V1(1, 32, V2);
+ private static final BitArrayVersion[] VALUES = values();
+
final byte bits;
final byte entriesPerWord;
final int maxEntryValue;
@@ -58,8 +60,14 @@ public enum BitArrayVersion {
throw new IllegalArgumentException("Invalid palette version: " + version);
}
- public BitArray createPalette(int size) {
- return this.createPalette(size, new int[MathUtils.ceil((float) size / entriesPerWord)]);
+ public static BitArrayVersion forBitsCeil(int bits) {
+ for (int i = VALUES.length - 1; i >= 0; i--) {
+ BitArrayVersion version = VALUES[i];
+ if (version.bits >= bits) {
+ return version;
+ }
+ }
+ return null;
}
public byte getId() {
@@ -78,7 +86,11 @@ public enum BitArrayVersion {
return next;
}
- public BitArray createPalette(int size, int[] words) {
+ public BitArray createArray(int size) {
+ return this.createArray(size, new int[MathUtils.ceil((float) size / entriesPerWord)]);
+ }
+
+ public BitArray createArray(int size, int[] words) {
if (this == V3 || this == V5 || this == V6) {
// Padded palettes aren't able to use bitwise operations due to their padding.
return new PaddedBitArray(this, size, words);
diff --git a/connector/src/main/java/org/geysermc/connector/scoreboard/Objective.java b/connector/src/main/java/org/geysermc/connector/scoreboard/Objective.java
index c3e6c863c..3accbc120 100644
--- a/connector/src/main/java/org/geysermc/connector/scoreboard/Objective.java
+++ b/connector/src/main/java/org/geysermc/connector/scoreboard/Objective.java
@@ -29,23 +29,23 @@ import com.github.steveice10.mc.protocol.data.game.scoreboard.ScoreboardPosition
import lombok.Getter;
import lombok.Setter;
-import java.util.HashMap;
import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
@Getter
public class Objective {
- private Scoreboard scoreboard;
- private long id;
- private boolean temp;
+ private final Scoreboard scoreboard;
+ private final long id;
+ private boolean active = true;
@Setter
private UpdateType updateType = UpdateType.ADD;
private String objectiveName;
- private String displaySlot;
+ private String displaySlotName;
private String displayName = "unknown";
private int type = 0; // 0 = integer, 1 = heart
- private Map scores = new HashMap<>();
+ private Map scores = new ConcurrentHashMap<>();
private Objective(Scoreboard scoreboard) {
this.id = scoreboard.getNextId().getAndIncrement();
@@ -54,23 +54,20 @@ public class Objective {
/**
* /!\ This method is made for temporary objectives until the real objective is received
- * @param scoreboard the scoreboard
+ *
+ * @param scoreboard the scoreboard
* @param objectiveName the name of the objective
*/
public Objective(Scoreboard scoreboard, String objectiveName) {
this(scoreboard);
this.objectiveName = objectiveName;
- this.temp = true;
+ this.active = false;
}
public Objective(Scoreboard scoreboard, String objectiveName, ScoreboardPosition displaySlot, String displayName, int type) {
- this(scoreboard, objectiveName, displaySlot.name().toLowerCase(), displayName, type);
- }
-
- public Objective(Scoreboard scoreboard, String objectiveName, String displaySlot, String displayName, int type) {
this(scoreboard);
this.objectiveName = objectiveName;
- this.displaySlot = displaySlot;
+ this.displaySlotName = translateDisplaySlot(displaySlot);
this.displayName = displayName;
this.type = type;
}
@@ -79,29 +76,18 @@ public class Objective {
if (!scores.containsKey(id)) {
Score score1 = new Score(this, id)
.setScore(score)
- .setTeam(scoreboard.getTeamFor(id));
+ .setTeam(scoreboard.getTeamFor(id))
+ .setUpdateType(UpdateType.ADD);
scores.put(id, score1);
}
}
public void setScore(String id, int score) {
if (scores.containsKey(id)) {
- scores.get(id).setScore(score).setUpdateType(UpdateType.ADD);
- } else {
- registerScore(id, score);
+ scores.get(id).setScore(score);
+ return;
}
- }
-
- public void setScoreText(String oldText, String newText) {
- if (!scores.containsKey(oldText) || oldText.equals(newText)) return;
- Score oldScore = scores.get(oldText);
-
- Score newScore = new Score(this, newText)
- .setScore(oldScore.getScore())
- .setTeam(scoreboard.getTeamFor(newText));
-
- scores.put(newText, newScore);
- oldScore.setUpdateType(UpdateType.REMOVE);
+ registerScore(id, score);
}
public int getScore(String id) {
@@ -111,39 +97,54 @@ public class Objective {
return 0;
}
- public Score getScore(int line) {
- for (Score score : scores.values()) {
- if (score.getScore() == line) return score;
- }
- return null;
- }
-
- public void resetScore(String id) {
+ public void removeScore(String id) {
if (scores.containsKey(id)) {
scores.get(id).setUpdateType(UpdateType.REMOVE);
}
}
- public void removeScore(String id) {
+ /**
+ * Used internally to remove a score from the score map
+ */
+ public void removeScore0(String id) {
scores.remove(id);
}
public Objective setDisplayName(String displayName) {
this.displayName = displayName;
- if (updateType == UpdateType.NOTHING) updateType = UpdateType.UPDATE;
+ if (updateType == UpdateType.NOTHING) {
+ updateType = UpdateType.UPDATE;
+ }
return this;
}
public Objective setType(int type) {
this.type = type;
- if (updateType == UpdateType.NOTHING) updateType = UpdateType.UPDATE;
+ if (updateType == UpdateType.NOTHING) {
+ updateType = UpdateType.UPDATE;
+ }
return this;
}
- public void removeTemp(ScoreboardPosition displaySlot) {
- if (temp) {
- temp = false;
- this.displaySlot = displaySlot.name().toLowerCase();
+ public void setActive(ScoreboardPosition displaySlot) {
+ if (!active) {
+ active = true;
+ displaySlotName = translateDisplaySlot(displaySlot);
+ }
+ }
+
+ public void removed() {
+ scores = null;
+ }
+
+ private static String translateDisplaySlot(ScoreboardPosition displaySlot) {
+ switch (displaySlot) {
+ case BELOW_NAME:
+ return "belowname";
+ case PLAYER_LIST:
+ return "list";
+ default:
+ return "sidebar";
}
}
}
diff --git a/connector/src/main/java/org/geysermc/connector/scoreboard/Score.java b/connector/src/main/java/org/geysermc/connector/scoreboard/Score.java
index d5a65e8c0..3dfd6ed38 100644
--- a/connector/src/main/java/org/geysermc/connector/scoreboard/Score.java
+++ b/connector/src/main/java/org/geysermc/connector/scoreboard/Score.java
@@ -25,20 +25,24 @@
package org.geysermc.connector.scoreboard;
+import com.nukkitx.protocol.bedrock.data.ScoreInfo;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
-@Getter @Setter
+@Getter
@Accessors(chain = true)
public class Score {
- private Objective objective;
- private long id;
+ private final Objective objective;
+ private ScoreInfo cachedInfo;
+ private final long id;
+ @Setter
private UpdateType updateType = UpdateType.ADD;
- private String name;
+ private final String name;
private Team team;
private int score;
+ @Setter
private int oldScore = Integer.MIN_VALUE;
public Score(Objective objective, String name) {
@@ -48,17 +52,35 @@ public class Score {
}
public String getDisplayName() {
- if (team != null && team.getUpdateType() != UpdateType.REMOVE) {
+ if (team != null) {
return team.getPrefix() + name + team.getSuffix();
}
return name;
}
public Score setScore(int score) {
- if (oldScore == Integer.MIN_VALUE) {
- this.oldScore = score;
- }
this.score = score;
+ updateType = UpdateType.UPDATE;
return this;
}
+
+ public Score setTeam(Team team) {
+ if (this.team != null && team != null) {
+ if (!this.team.equals(team)) {
+ this.team = team;
+ updateType = UpdateType.UPDATE;
+ }
+ return this;
+ }
+ // simplified from (this.team != null && team == null) || (this.team == null && team != null)
+ if (this.team != null || team != null) {
+ this.team = team;
+ updateType = UpdateType.UPDATE;
+ }
+ return this;
+ }
+
+ public void update() {
+ cachedInfo = new ScoreInfo(id, objective.getObjectiveName(), score, getDisplayName());
+ }
}
diff --git a/connector/src/main/java/org/geysermc/connector/scoreboard/Scoreboard.java b/connector/src/main/java/org/geysermc/connector/scoreboard/Scoreboard.java
index 5fdda617f..732a056eb 100644
--- a/connector/src/main/java/org/geysermc/connector/scoreboard/Scoreboard.java
+++ b/connector/src/main/java/org/geysermc/connector/scoreboard/Scoreboard.java
@@ -30,69 +30,67 @@ import com.nukkitx.protocol.bedrock.data.ScoreInfo;
import com.nukkitx.protocol.bedrock.packet.RemoveObjectivePacket;
import com.nukkitx.protocol.bedrock.packet.SetDisplayObjectivePacket;
import com.nukkitx.protocol.bedrock.packet.SetScorePacket;
-import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
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.LanguageUtils;
import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import static org.geysermc.connector.scoreboard.UpdateType.*;
@Getter
public class Scoreboard {
- private GeyserSession session;
- private AtomicLong nextId = new AtomicLong(0);
+ private final GeyserSession session;
+ private final GeyserLogger logger;
+ private final AtomicLong nextId = new AtomicLong(0);
- private Map objectives = new HashMap<>();
- private Map teams = new HashMap<>();
+ private final Map objectives = new ConcurrentHashMap<>();
+ private final Map teams = new HashMap<>();
+
+ private int lastScoreCount = 0;
public Scoreboard(GeyserSession session) {
this.session = session;
+ this.logger = GeyserConnector.getInstance().getLogger();
}
- public Objective registerNewObjective(String objectiveId, boolean temp) {
- if (!temp || objectives.containsKey(objectiveId)) return objectives.get(objectiveId);
+ public Objective registerNewObjective(String objectiveId, boolean active) {
+ if (active || objectives.containsKey(objectiveId)) {
+ return objectives.get(objectiveId);
+ }
Objective objective = new Objective(this, objectiveId);
objectives.put(objectiveId, objective);
return objective;
}
public Objective registerNewObjective(String objectiveId, ScoreboardPosition displaySlot) {
- Objective objective = null;
- if (objectives.containsKey(objectiveId)) {
- objective = objectives.get(objectiveId);
- if (objective.isTemp()) objective.removeTemp(displaySlot);
- else {
- despawnObjective(objective);
- objective = null;
+ Objective objective = objectives.get(objectiveId);
+ if (objective != null) {
+ if (!objective.isActive()) {
+ objective.setActive(displaySlot);
+ return objective;
}
+ despawnObjective(objective);
}
- if (objective == null) {
- objective = new Objective(this, objectiveId, displaySlot, "unknown", 0);
- objectives.put(objectiveId, objective);
- }
+
+ objective = new Objective(this, objectiveId, displaySlot, "unknown", 0);
+ objectives.put(objectiveId, objective);
return objective;
}
public Team registerNewTeam(String teamName, Set players) {
- if (teams.containsKey(teamName)) {
- session.getConnector().getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.translator.team.failed_overrides", teamName));
- return getTeam(teamName);
+ Team team = teams.get(teamName);
+ if (team != null) {
+ logger.info(LanguageUtils.getLocaleStringLog("geyser.network.translator.team.failed_overrides", teamName));
+ return team;
}
- Team team = new Team(this, teamName).setEntities(players);
+ team = new Team(this, teamName).setEntities(players);
teams.put(teamName, team);
-
- for (Objective objective : objectives.values()) {
- for (Score score : objective.getScores().values()) {
- if (players.contains(score.getName())) {
- score.setTeam(team);
- }
- }
- }
return team;
}
@@ -106,102 +104,122 @@ public class Scoreboard {
public void unregisterObjective(String objectiveName) {
Objective objective = getObjective(objectiveName);
- if (objective != null) objective.setUpdateType(REMOVE);
+ if (objective != null) {
+ objective.setUpdateType(REMOVE);
+ }
}
public void removeTeam(String teamName) {
Team remove = teams.remove(teamName);
- if (remove != null) remove.setUpdateType(REMOVE);
+ if (remove != null) {
+ remove.setUpdateType(REMOVE);
+ }
}
public void onUpdate() {
- Set changedObjectives = new ObjectOpenHashSet<>();
- List addScores = new ArrayList<>();
- List removeScores = new ArrayList<>();
+ List addScores = new ArrayList<>(getLastScoreCount());
+ List removeScores = new ArrayList<>(getLastScoreCount());
- for (String objectiveId : new ArrayList<>(objectives.keySet())) {
- Objective objective = objectives.get(objectiveId);
- if (objective.isTemp()) {
- session.getConnector().getLogger().debug("Ignoring temp Scoreboard Objective '"+ objectiveId +'\'');
+ for (Objective objective : objectives.values()) {
+ if (!objective.isActive()) {
+ logger.debug("Ignoring non-active Scoreboard Objective '" + objective.getObjectiveName() + '\'');
continue;
}
- if (objective.getUpdateType() != NOTHING) changedObjectives.add(objective);
+ // hearts can't hold teams, so we treat them differently
+ if (objective.getType() == 1) {
+ for (Score score : objective.getScores().values()) {
+ if (score.getUpdateType() == NOTHING) {
+ continue;
+ }
+
+ boolean update = score.getUpdateType() == UPDATE;
+ if (update) {
+ score.update();
+ }
+
+ if (score.getUpdateType() == ADD || update) {
+ addScores.add(score.getCachedInfo());
+ }
+ if (score.getUpdateType() == REMOVE || update) {
+ removeScores.add(score.getCachedInfo());
+ }
+ }
+ continue;
+ }
boolean globalUpdate = objective.getUpdateType() == UPDATE;
- boolean globalAdd = objective.getUpdateType() == ADD || globalUpdate;
- boolean globalRemove = objective.getUpdateType() == REMOVE || globalUpdate;
+ boolean globalAdd = objective.getUpdateType() == ADD;
+ boolean globalRemove = objective.getUpdateType() == REMOVE;
- boolean hasUpdate = globalUpdate;
-
- List handledScores = new ArrayList<>();
- for (String identifier : new ObjectOpenHashSet<>(objective.getScores().keySet())) {
- Score score = objective.getScores().get(identifier);
+ for (Score score : objective.getScores().values()) {
Team team = score.getTeam();
- boolean inTeam = team != null && team.getEntities().contains(score.getName());
+ boolean add = globalAdd || globalUpdate;
+ boolean remove = globalRemove;
+ boolean teamChanged = false;
+ if (team != null) {
+ if (team.getUpdateType() == REMOVE || !team.hasEntity(score.getName())) {
+ score.setTeam(null);
+ teamChanged = true;
+ }
- boolean teamAdd = team != null && (team.getUpdateType() == ADD || team.getUpdateType() == UPDATE);
- boolean teamRemove = team != null && (team.getUpdateType() == REMOVE || team.getUpdateType() == UPDATE);
+ teamChanged |= team.getUpdateType() == UPDATE;
- if (team != null && (team.getUpdateType() == REMOVE || !inTeam)) score.setTeam(null);
-
- boolean add = (hasUpdate || globalAdd || teamAdd || teamRemove || score.getUpdateType() == ADD || score.getUpdateType() == UPDATE) && (score.getUpdateType() != REMOVE);
- boolean remove = hasUpdate || globalRemove || teamAdd || teamRemove || score.getUpdateType() == REMOVE || score.getUpdateType() == UPDATE;
-
- boolean updated = false;
- if (!hasUpdate) {
- updated = hasUpdate = add;
+ add |= team.getUpdateType() == ADD || team.getUpdateType() == UPDATE;
+ remove |= team.getUpdateType() != NOTHING;
}
- if (updated) {
- for (Score score1 : handledScores) {
- ScoreInfo scoreInfo = new ScoreInfo(score1.getId(), score1.getObjective().getObjectiveName(), score1.getScore(), score1.getDisplayName());
- addScores.add(scoreInfo);
- removeScores.add(scoreInfo);
- }
+ add |= score.getUpdateType() == ADD || score.getUpdateType() == UPDATE;
+ remove |= score.getUpdateType() == REMOVE || score.getUpdateType() == UPDATE;
+
+ if (score.getUpdateType() == REMOVE || globalRemove) {
+ add = false;
+ }
+
+ if (score.getUpdateType() == ADD) {
+ remove = false;
+ }
+
+ if (score.getUpdateType() == ADD || score.getUpdateType() == UPDATE || teamChanged) {
+ score.update();
}
if (add) {
- addScores.add(new ScoreInfo(score.getId(), score.getObjective().getObjectiveName(), score.getScore(), score.getDisplayName()));
+ addScores.add(score.getCachedInfo());
}
if (remove) {
- removeScores.add(new ScoreInfo(score.getId(), score.getObjective().getObjectiveName(), score.getOldScore(), score.getDisplayName()));
+ removeScores.add(score.getCachedInfo());
}
- score.setOldScore(score.getScore());
+ // score is pending to be removed, so we can remove it from the objective
if (score.getUpdateType() == REMOVE) {
- objective.removeScore(score.getName());
+ objective.removeScore0(score.getName());
}
- if (add || remove) {
- changedObjectives.add(objective);
- } else { // stays the same like before
- handledScores.add(score);
- }
score.setUpdateType(NOTHING);
}
- }
- for (Objective objective : changedObjectives) {
- boolean update = objective.getUpdateType() == NOTHING || objective.getUpdateType() == UPDATE;
- if (objective.getUpdateType() == REMOVE || update) {
+ if (globalRemove || globalUpdate) {
RemoveObjectivePacket removeObjectivePacket = new RemoveObjectivePacket();
removeObjectivePacket.setObjectiveId(objective.getObjectiveName());
session.sendUpstreamPacket(removeObjectivePacket);
- if (objective.getUpdateType() == REMOVE) {
+ if (globalRemove) {
objectives.remove(objective.getObjectiveName()); // now we can deregister
+ objective.removed();
}
}
- if (objective.getUpdateType() == ADD || update) {
+
+ if ((globalAdd || globalUpdate) && !globalRemove) {
SetDisplayObjectivePacket displayObjectivePacket = new SetDisplayObjectivePacket();
displayObjectivePacket.setObjectiveId(objective.getObjectiveName());
displayObjectivePacket.setDisplayName(objective.getDisplayName());
displayObjectivePacket.setCriteria("dummy");
- displayObjectivePacket.setDisplaySlot(objective.getDisplaySlot());
+ displayObjectivePacket.setDisplaySlot(objective.getDisplaySlotName());
displayObjectivePacket.setSortOrder(1); // ??
session.sendUpstreamPacket(displayObjectivePacket);
}
+
objective.setUpdateType(NOTHING);
}
@@ -218,6 +236,8 @@ public class Scoreboard {
setScorePacket.setInfos(addScores);
session.sendUpstreamPacket(setScorePacket);
}
+
+ lastScoreCount = addScores.size();
}
public void despawnObjective(Objective objective) {
@@ -234,6 +254,8 @@ public class Scoreboard {
0, ""
));
}
+
+ objective.removed();
if (!toRemove.isEmpty()) {
SetScorePacket setScorePacket = new SetScorePacket();
diff --git a/connector/src/main/java/org/geysermc/connector/scoreboard/ScoreboardUpdater.java b/connector/src/main/java/org/geysermc/connector/scoreboard/ScoreboardUpdater.java
new file mode 100644
index 000000000..3812fb141
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/scoreboard/ScoreboardUpdater.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (c) 2019-2020 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.scoreboard;
+
+import org.geysermc.connector.GeyserConnector;
+import org.geysermc.connector.configuration.GeyserConfiguration;
+import org.geysermc.connector.network.session.GeyserSession;
+import org.geysermc.connector.network.session.cache.WorldCache;
+import org.geysermc.connector.utils.LanguageUtils;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class ScoreboardUpdater extends Thread {
+ public static final int FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD;
+ public static final int SECOND_SCORE_PACKETS_PER_SECOND_THRESHOLD = 250;
+
+ private static final int FIRST_MILLIS_BETWEEN_UPDATES = 250; // 4 updates per second
+ private static final int SECOND_MILLIS_BETWEEN_UPDATES = 1000 * 3; // 1 update per 3 seconds
+
+ private static final boolean DEBUG_ENABLED;
+
+ private final WorldCache worldCache;
+ private final GeyserSession session;
+
+ private int millisBetweenUpdates = FIRST_MILLIS_BETWEEN_UPDATES;
+ private long lastUpdate = System.currentTimeMillis();
+ private long lastLog = -1;
+
+ private long lastPacketsPerSecondUpdate = System.currentTimeMillis();
+ private final AtomicInteger packetsPerSecond = new AtomicInteger(0);
+ private final AtomicInteger pendingPacketsPerSecond = new AtomicInteger(0);
+
+ public ScoreboardUpdater(WorldCache worldCache) {
+ super("Scoreboard Updater");
+ this.worldCache = worldCache;
+ session = worldCache.getSession();
+ }
+
+ @Override
+ public void run() {
+ if (!session.isClosed()) {
+ long currentTime = System.currentTimeMillis();
+
+ // reset score-packets per second every second
+ if (currentTime - lastPacketsPerSecondUpdate > 1000) {
+ lastPacketsPerSecondUpdate = currentTime;
+ packetsPerSecond.set(pendingPacketsPerSecond.get());
+ pendingPacketsPerSecond.set(0);
+ }
+
+ if (currentTime - lastUpdate > millisBetweenUpdates) {
+ lastUpdate = currentTime;
+
+ int pps = packetsPerSecond.get();
+ if (pps >= FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD) {
+ boolean reachedSecondThreshold = pps >= SECOND_SCORE_PACKETS_PER_SECOND_THRESHOLD;
+ if (reachedSecondThreshold) {
+ millisBetweenUpdates = SECOND_MILLIS_BETWEEN_UPDATES;
+ } else {
+ millisBetweenUpdates = FIRST_MILLIS_BETWEEN_UPDATES;
+ }
+
+ worldCache.getScoreboard().onUpdate();
+
+ if (DEBUG_ENABLED && (currentTime - lastLog > 60000)) { // one minute
+ int threshold = reachedSecondThreshold ?
+ SECOND_SCORE_PACKETS_PER_SECOND_THRESHOLD :
+ FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD;
+
+ GeyserConnector.getInstance().getLogger().info(
+ LanguageUtils.getLocaleStringLog("geyser.scoreboard.updater.threshold_reached.log", session.getName(), threshold, pps) +
+ LanguageUtils.getLocaleStringLog("geyser.scoreboard.updater.threshold_reached", (millisBetweenUpdates / 1000.0))
+ );
+
+ lastLog = currentTime;
+ }
+ }
+ }
+
+ session.getConnector().getGeneralThreadPool().schedule(this, 50, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ public int getPacketsPerSecond() {
+ return packetsPerSecond.get();
+ }
+
+ /**
+ * Increase the Scoreboard Packets Per Second and return the updated value
+ */
+ public int incrementAndGetPacketsPerSecond() {
+ return pendingPacketsPerSecond.incrementAndGet();
+ }
+
+ static {
+ GeyserConfiguration config = GeyserConnector.getInstance().getConfig();
+ FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD = Math.min(config.getScoreboardPacketThreshold(), SECOND_SCORE_PACKETS_PER_SECOND_THRESHOLD);
+ DEBUG_ENABLED = config.isDebugMode();
+ }
+}
diff --git a/connector/src/main/java/org/geysermc/connector/scoreboard/Team.java b/connector/src/main/java/org/geysermc/connector/scoreboard/Team.java
index c2fcc02c8..a073e2e99 100644
--- a/connector/src/main/java/org/geysermc/connector/scoreboard/Team.java
+++ b/connector/src/main/java/org/geysermc/connector/scoreboard/Team.java
@@ -25,6 +25,7 @@
package org.geysermc.connector.scoreboard;
+import com.github.steveice10.mc.protocol.data.game.scoreboard.NameTagVisibility;
import com.github.steveice10.mc.protocol.data.game.scoreboard.TeamColor;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import lombok.Getter;
@@ -35,8 +36,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Set;
-@Getter
-@Setter
+@Getter @Setter
@Accessors(chain = true)
public class Team {
private final Scoreboard scoreboard;
@@ -44,6 +44,8 @@ public class Team {
private UpdateType updateType = UpdateType.ADD;
private String name;
+
+ private NameTagVisibility nameTagVisibility;
private String prefix;
private TeamColor color;
private String suffix;
@@ -57,8 +59,7 @@ public class Team {
public void addEntities(String... names) {
List added = new ArrayList<>();
for (String name : names) {
- if (!entities.contains(name)) {
- entities.add(name);
+ if (entities.add(name)) {
added.add(name);
}
}
@@ -78,4 +79,35 @@ public class Team {
}
setUpdateType(UpdateType.UPDATE);
}
+
+ public boolean hasEntity(String name) {
+ return entities.contains(name);
+ }
+
+ public Team setPrefix(String prefix) {
+ // replace "null" to an empty string,
+ // we do this here to improve the performance of Score#getDisplayName
+ if (prefix.length() == 4 && "null".equals(prefix)) {
+ this.prefix = "";
+ return this;
+ }
+ this.prefix = prefix;
+ return this;
+ }
+
+ public Team setSuffix(String suffix) {
+ // replace "null" to an empty string,
+ // we do this here to improve the performance of Score#getDisplayName
+ if (suffix.length() == 4 && "null".equals(suffix)) {
+ this.suffix = "";
+ return this;
+ }
+ this.suffix = suffix;
+ return this;
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
}
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 6b9ab8cae..7e1d58b4c 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java
@@ -28,6 +28,7 @@ package org.geysermc.connector.utils;
import com.github.steveice10.mc.protocol.data.game.entity.Effect;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
+import com.nukkitx.math.vector.Vector3i;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
import org.geysermc.connector.network.translators.item.ItemEntry;
@@ -138,4 +139,28 @@ public class BlockUtils {
return calculateBreakTime(blockHardness, toolTier, canHarvestWithHand, correctTool, toolType, isWoolBlock, isCobweb, toolEfficiencyLevel, hasteLevel, miningFatigueLevel, insideOfWaterWithoutAquaAffinity, outOfWaterButNotOnGround, insideWaterNotOnGround);
}
+ /**
+ * Given a position, return the position if a block were located on the specified block face.
+ * @param blockPos the block position
+ * @param face the face of the block - see {@link com.github.steveice10.mc.protocol.data.game.world.block.BlockFace}
+ * @return the block position with the block face accounted for
+ */
+ public static Vector3i getBlockPosition(Vector3i blockPos, int face) {
+ switch (face) {
+ case 0:
+ return blockPos.sub(0, 1, 0);
+ case 1:
+ return blockPos.add(0, 1, 0);
+ case 2:
+ return blockPos.sub(0, 0, 1);
+ case 3:
+ return blockPos.add(0, 0, 1);
+ case 4:
+ return blockPos.sub(1, 0, 0);
+ case 5:
+ return blockPos.add(1, 0, 0);
+ }
+ return blockPos;
+ }
+
}
diff --git a/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java b/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java
index 06b400908..a63eeb424 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java
@@ -25,8 +25,10 @@
package org.geysermc.connector.utils;
+import com.github.steveice10.mc.protocol.data.game.chunk.BitStorage;
import com.github.steveice10.mc.protocol.data.game.chunk.Chunk;
import com.github.steveice10.mc.protocol.data.game.chunk.Column;
+import com.github.steveice10.mc.protocol.data.game.chunk.palette.Palette;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.StringTag;
@@ -36,27 +38,39 @@ import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.nbt.NBTOutputStream;
import com.nukkitx.nbt.NbtMap;
import com.nukkitx.nbt.NbtUtils;
-import com.nukkitx.protocol.bedrock.packet.*;
+import com.nukkitx.protocol.bedrock.packet.LevelChunkPacket;
+import com.nukkitx.protocol.bedrock.packet.NetworkChunkPublisherUpdatePacket;
+import com.nukkitx.protocol.bedrock.packet.UpdateBlockPacket;
+import it.unimi.dsi.fastutil.ints.IntArrayList;
+import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
-import it.unimi.dsi.fastutil.objects.ObjectArrayList;
-import lombok.Getter;
+import lombok.Data;
+import lombok.experimental.UtilityClass;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.entity.Entity;
import org.geysermc.connector.entity.ItemFrameEntity;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.world.block.BlockStateValues;
-import org.geysermc.connector.network.translators.world.block.entity.*;
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
-import org.geysermc.connector.network.translators.world.chunk.ChunkPosition;
+import org.geysermc.connector.network.translators.world.block.entity.BedrockOnlyBlockEntity;
+import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator;
+import org.geysermc.connector.network.translators.world.block.entity.RequiresBlockState;
+import org.geysermc.connector.network.translators.world.chunk.BlockStorage;
import org.geysermc.connector.network.translators.world.chunk.ChunkSection;
+import org.geysermc.connector.network.translators.world.chunk.bitarray.BitArray;
+import org.geysermc.connector.network.translators.world.chunk.bitarray.BitArrayVersion;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
-import static org.geysermc.connector.network.translators.world.block.BlockTranslator.AIR;
-import static org.geysermc.connector.network.translators.world.block.BlockTranslator.BEDROCK_WATER_ID;
+import static org.geysermc.connector.network.translators.world.block.BlockTranslator.*;
+@UtilityClass
public class ChunkUtils {
/**
@@ -67,6 +81,9 @@ public class ChunkUtils {
private static final NbtMap EMPTY_TAG = NbtMap.builder().build();
public static final byte[] EMPTY_LEVEL_CHUNK_DATA;
+ public static final BlockStorage EMPTY_STORAGE = new BlockStorage();
+ public static final ChunkSection EMPTY_SECTION = new ChunkSection(new BlockStorage[]{ EMPTY_STORAGE });
+
static {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
outputStream.write(new byte[258]); // Biomes + Border Size + Extra Data Size
@@ -76,61 +93,144 @@ public class ChunkUtils {
}
EMPTY_LEVEL_CHUNK_DATA = outputStream.toByteArray();
- }catch (IOException e) {
+ } catch (IOException e) {
throw new AssertionError("Unable to generate empty level chunk data");
}
}
- public static ChunkData translateToBedrock(Column column) {
- ChunkData chunkData = new ChunkData();
- Chunk[] chunks = column.getChunks();
- chunkData.sections = new ChunkSection[chunks.length];
+ private static int indexYZXtoXZY(int yzx) {
+ return (yzx >> 8) | (yzx & 0x0F0) | ((yzx & 0x00F) << 8);
+ }
- CompoundTag[] blockEntities = column.getTileEntities();
- // Temporarily stores positions of BlockState values per chunk load
- Object2IntMap blockEntityPositions = new Object2IntOpenHashMap<>();
+ public static ChunkData translateToBedrock(GeyserSession session, Column column, boolean isNonFullChunk) {
+ Chunk[] javaSections = column.getChunks();
+ ChunkSection[] sections = new ChunkSection[javaSections.length];
// Temporarily stores compound tags of Bedrock-only block entities
- ObjectArrayList bedrockOnlyBlockEntities = new ObjectArrayList<>();
+ List bedrockOnlyBlockEntities = Collections.emptyList();
- for (int chunkY = 0; chunkY < chunks.length; chunkY++) {
- chunkData.sections[chunkY] = new ChunkSection();
- Chunk chunk = chunks[chunkY];
+ BitSet waterloggedPaletteIds = new BitSet();
+ BitSet pistonOrFlowerPaletteIds = new BitSet();
- if (chunk == null || chunk.isEmpty())
+ boolean worldManagerHasMoreBlockDataThanCache = session.getConnector().getWorldManager().hasMoreBlockDataThanChunkCache();
+
+ // If the received packet was a full chunk update, null sections in the chunk are guaranteed to also be null in the world manager
+ boolean shouldCheckWorldManagerOnMissingSections = isNonFullChunk && worldManagerHasMoreBlockDataThanCache;
+ Chunk temporarySection = null;
+
+ for (int sectionY = 0; sectionY < javaSections.length; sectionY++) {
+ Chunk javaSection = javaSections[sectionY];
+
+ // Section is null, the cache will not contain anything of use
+ if (javaSection == null) {
+ // The column parameter contains all data currently available from the cache. If the chunk is null and the world manager
+ // reports the ability to access more data than the cache, attempt to fetch from the world manager instead.
+ if (shouldCheckWorldManagerOnMissingSections) {
+ // Ensure that temporary chunk is set
+ if (temporarySection == null) {
+ temporarySection = new Chunk();
+ }
+
+ // Read block data in section
+ session.getConnector().getWorldManager().getBlocksInSection(session, column.getX(), sectionY, column.getZ(), temporarySection);
+
+ if (temporarySection.isEmpty()) {
+ // The world manager only contains air for the given section
+ // We can leave temporarySection as-is to allow it to potentially be re-used for later sections
+ continue;
+ } else {
+ javaSection = temporarySection;
+
+ // Section contents have been modified, we can't re-use it
+ temporarySection = null;
+ }
+ } else {
+ continue;
+ }
+ }
+
+ // No need to encode an empty section...
+ if (javaSection.isEmpty()) {
continue;
+ }
- ChunkSection section = chunkData.sections[chunkY];
- for (int x = 0; x < 16; x++) {
- for (int y = 0; y < 16; y++) {
- for (int z = 0; z < 16; z++) {
- int blockState = chunk.get(x, y, z);
- int id = BlockTranslator.getBedrockBlockId(blockState);
+ Palette javaPalette = javaSection.getPalette();
+ IntList bedrockPalette = new IntArrayList(javaPalette.size());
+ waterloggedPaletteIds.clear();
+ pistonOrFlowerPaletteIds.clear();
- // Check to see if the name is in BlockTranslator.getBlockEntityString, and therefore must be handled differently
- if (BlockTranslator.getBlockEntityString(blockState) != null) {
- Position pos = new ChunkPosition(column.getX(), column.getZ()).getBlock(x, (chunkY << 4) + y, z);
- blockEntityPositions.put(pos, blockState);
- }
+ // Iterate through palette and convert state IDs to Bedrock, doing some additional checks as we go
+ for (int i = 0; i < javaPalette.size(); i++) {
+ int javaId = javaPalette.idToState(i);
+ bedrockPalette.add(BlockTranslator.getBedrockBlockId(javaId));
- section.getBlockStorageArray()[0].setFullBlock(ChunkSection.blockPosition(x, y, z), id);
+ if (BlockTranslator.isWaterlogged(javaId)) {
+ waterloggedPaletteIds.set(i);
+ }
- // Check if block is piston or flower - only block entities in Bedrock
- if (BlockStateValues.getFlowerPotValues().containsKey(blockState) ||
- BlockStateValues.getPistonValues().containsKey(blockState)) {
- Position pos = new ChunkPosition(column.getX(), column.getZ()).getBlock(x, (chunkY << 4) + y, z);
- bedrockOnlyBlockEntities.add(BedrockOnlyBlockEntity.getTag(Vector3i.from(pos.getX(), pos.getY(), pos.getZ()), blockState));
- }
+ // Check if block is piston or flower to see if we'll need to create additional block entities, as they're only block entities in Bedrock
+ if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId)) {
+ pistonOrFlowerPaletteIds.set(i);
+ }
+ }
- if (BlockTranslator.isWaterlogged(blockState)) {
- section.getBlockStorageArray()[1].setFullBlock(ChunkSection.blockPosition(x, y, z), BEDROCK_WATER_ID);
- }
+ BitStorage javaData = javaSection.getStorage();
+
+ // Add Bedrock-exclusive block entities
+ // We only if the palette contained any blocks that are Bedrock-exclusive block entities to avoid iterating through the whole block data
+ // for no reason, as most sections will not contain any pistons or flower pots
+ if (!pistonOrFlowerPaletteIds.isEmpty()) {
+ bedrockOnlyBlockEntities = new ArrayList<>();
+ for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) {
+ int paletteId = javaData.get(yzx);
+ if (pistonOrFlowerPaletteIds.get(paletteId)) {
+ bedrockOnlyBlockEntities.add(BedrockOnlyBlockEntity.getTag(
+ Vector3i.from((column.getX() << 4) + (yzx & 0xF), (sectionY << 4) + ((yzx >> 8) & 0xF), (column.getZ() << 4) + ((yzx >> 4) & 0xF)),
+ javaPalette.idToState(paletteId)
+ ));
}
}
}
+ BitArray bedrockData = BitArrayVersion.forBitsCeil(javaData.getBitsPerEntry()).createArray(BlockStorage.SIZE);
+ BlockStorage layer0 = new BlockStorage(bedrockData, bedrockPalette);
+ BlockStorage[] layers;
+
+ // Convert data array from YZX to XZY coordinate order
+ if (waterloggedPaletteIds.isEmpty()) {
+ // No blocks are waterlogged, simply convert coordinate order
+ // This could probably be optimized further...
+ for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) {
+ bedrockData.set(indexYZXtoXZY(yzx), javaData.get(yzx));
+ }
+
+ layers = new BlockStorage[]{ layer0 };
+ } else {
+ // The section contains waterlogged blocks, we need to convert coordinate order AND generate a V1 block storage for
+ // layer 1 with palette ID 1 indicating water
+ int[] layer1Data = new int[BlockStorage.SIZE >> 5];
+ for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) {
+ int paletteId = javaData.get(yzx);
+ int xzy = indexYZXtoXZY(yzx);
+ bedrockData.set(xzy, paletteId);
+
+ if (waterloggedPaletteIds.get(paletteId)) {
+ layer1Data[xzy >> 5] |= 1 << (xzy & 0x1F);
+ }
+ }
+
+ // V1 palette
+ IntList layer1Palette = new IntArrayList(2);
+ layer1Palette.add(0); // Air
+ layer1Palette.add(BEDROCK_WATER_ID);
+
+ layers = new BlockStorage[]{ layer0, new BlockStorage(BitArrayVersion.V1.createArray(BlockStorage.SIZE, layer1Data), layer1Palette) };
+ }
+
+ sections[sectionY] = new ChunkSection(layers);
}
+ CompoundTag[] blockEntities = column.getTileEntities();
NbtMap[] bedrockBlockEntities = new NbtMap[blockEntities.length + bedrockOnlyBlockEntities.size()];
int i = 0;
while (i < blockEntities.length) {
@@ -144,7 +244,7 @@ public class ChunkUtils {
for (Tag subTag : tag) {
if (subTag instanceof StringTag) {
StringTag stringTag = (StringTag) subTag;
- if (stringTag.getValue().equals("")) {
+ if (stringTag.getValue().isEmpty()) {
tagName = stringTag.getName();
break;
}
@@ -158,17 +258,25 @@ public class ChunkUtils {
String id = BlockEntityUtils.getBedrockBlockEntityId(tagName);
BlockEntityTranslator blockEntityTranslator = BlockEntityUtils.getBlockEntityTranslator(id);
Position pos = new Position((int) tag.get("x").getValue(), (int) tag.get("y").getValue(), (int) tag.get("z").getValue());
- int blockState = blockEntityPositions.getOrDefault(pos, 0);
+
+ // Get Java blockstate ID from block entity position
+ int blockState = 0;
+ Chunk section = column.getChunks()[pos.getY() >> 4];
+ if (section != null) {
+ blockState = section.get(pos.getX() & 0xF, pos.getY() & 0xF, pos.getZ() & 0xF);
+ }
+
bedrockBlockEntities[i] = blockEntityTranslator.getBlockEntityTag(tagName, tag, blockState);
i++;
}
+
+ // Append Bedrock-exclusive block entities to output array
for (NbtMap tag : bedrockOnlyBlockEntities) {
bedrockBlockEntities[i] = tag;
i++;
}
- chunkData.blockEntities = bedrockBlockEntities;
- return chunkData;
+ return new ChunkData(sections, bedrockBlockEntities);
}
public static void updateChunkPosition(GeyserSession session, Vector3i position) {
@@ -238,7 +346,7 @@ public class ChunkUtils {
break; //No block will be a part of two classes
}
}
- session.getChunkCache().updateBlock(new Position(position.getX(), position.getY(), position.getZ()), blockState);
+ session.getChunkCache().updateBlock(position.getX(), position.getY(), position.getZ(), blockState);
}
public static void sendEmptyChunks(GeyserSession session, Vector3i position, int radius, boolean forceUpdate) {
@@ -266,12 +374,10 @@ public class ChunkUtils {
}
}
+ @Data
public static final class ChunkData {
- public ChunkSection[] sections;
+ private final ChunkSection[] sections;
- @Getter
- private NbtMap[] blockEntities = new NbtMap[0];
- @Getter
- private Object2IntMap loadBlockEntitiesLater = new Object2IntOpenHashMap<>();
+ private final NbtMap[] blockEntities;
}
}
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 74db16bb5..7b283e9cb 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java
@@ -26,8 +26,13 @@
package org.geysermc.connector.utils;
import com.github.steveice10.mc.protocol.data.game.entity.Effect;
+import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
+import com.github.steveice10.opennbt.tag.builtin.StringTag;
import com.nukkitx.math.vector.Vector3i;
-import com.nukkitx.protocol.bedrock.packet.*;
+import com.nukkitx.protocol.bedrock.packet.ChangeDimensionPacket;
+import com.nukkitx.protocol.bedrock.packet.MobEffectPacket;
+import com.nukkitx.protocol.bedrock.packet.StopSoundPacket;
+import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.entity.Entity;
import org.geysermc.connector.network.session.GeyserSession;
@@ -80,6 +85,10 @@ public class DimensionUtils {
stopSoundPacket.setStoppingAllSound(true);
stopSoundPacket.setSoundName("");
session.sendUpstreamPacket(stopSoundPacket);
+
+ // TODO - fix this hack of a fix by sending the final dimension switching logic after chunks have been sent.
+ // The client wants chunks sent to it before it can successfully respawn.
+ ChunkUtils.sendEmptyChunks(session, player.getPosition().toInt(), 3, true);
}
/**
@@ -99,6 +108,24 @@ public class DimensionUtils {
}
}
+ /**
+ * Determines the new dimension based on the {@link CompoundTag} sent by either the {@link com.github.steveice10.mc.protocol.packet.ingame.server.ServerJoinGamePacket}
+ * or {@link com.github.steveice10.mc.protocol.packet.ingame.server.ServerRespawnPacket}.
+ * @param dimensionTag the packet's dimension tag.
+ * @return the dimension identifier.
+ */
+ public static String getNewDimension(CompoundTag dimensionTag) {
+ if (dimensionTag == null || dimensionTag.isEmpty()) {
+ GeyserConnector.getInstance().getLogger().debug("Dimension tag was null or empty.");
+ return "minecraft:overworld";
+ }
+ if (dimensionTag.getValue().get("effects") != null) {
+ return ((StringTag) dimensionTag.getValue().get("effects")).getValue();
+ }
+ GeyserConnector.getInstance().getLogger().debug("Effects portion of the tag was null or empty.");
+ return "minecraft:overworld";
+ }
+
public static void changeBedrockNetherId() {
// Change dimension ID to the End to allow for building above Bedrock
BEDROCK_NETHER_ID = 2;
diff --git a/connector/src/main/java/org/geysermc/connector/utils/DockerCheck.java b/connector/src/main/java/org/geysermc/connector/utils/DockerCheck.java
index 74d020bca..59a039887 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/DockerCheck.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/DockerCheck.java
@@ -25,36 +25,13 @@
package org.geysermc.connector.utils;
-import org.geysermc.connector.bootstrap.GeyserBootstrap;
-
-import java.net.InetAddress;
import java.nio.file.Files;
import java.nio.file.Paths;
public class DockerCheck {
- public static void check(GeyserBootstrap bootstrap) {
- try {
- String OS = System.getProperty("os.name").toLowerCase();
- String ipAddress = InetAddress.getLocalHost().getHostAddress();
-
- // Check if the user is already using the recommended IP
- if (ipAddress.equals(bootstrap.getGeyserConfig().getRemote().getAddress())) {
- return;
- }
-
- if (OS.indexOf("nix") >= 0 || OS.indexOf("nux") >= 0 || OS.indexOf("aix") > 0) {
- bootstrap.getGeyserLogger().debug("We are on a Unix system, checking for Docker...");
-
- String output = new String(Files.readAllBytes(Paths.get("/proc/1/cgroup")));
-
- if (output.contains("docker")) {
- bootstrap.getGeyserLogger().warning(LanguageUtils.getLocaleStringLog("geyser.bootstrap.docker_warn.line1"));
- bootstrap.getGeyserLogger().warning(LanguageUtils.getLocaleStringLog("geyser.bootstrap.docker_warn.line2", ipAddress));
- }
- }
- } catch (Exception e) { } // Ignore any errors, inc ip failed to fetch, process could not run or access denied
- }
+ // By default, Geyser now sets the IP to the local IP in all cases on plugin versions so we don't notify the user of anything
+ // However we still have this check for the potential future bug
public static boolean checkBasic() {
try {
String OS = System.getProperty("os.name").toLowerCase();
diff --git a/connector/src/main/java/org/geysermc/connector/utils/EntityUtils.java b/connector/src/main/java/org/geysermc/connector/utils/EntityUtils.java
index b5033c947..51102202d 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/EntityUtils.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/EntityUtils.java
@@ -42,8 +42,7 @@ public class EntityUtils {
case LUCK:
case UNLUCK:
case DOLPHINS_GRACE:
- case BAD_OMEN:
- case HERO_OF_THE_VILLAGE:
+ // All Java-exclusive effects as of 1.16.2
return 0;
case LEVITATION:
return 24;
@@ -51,6 +50,10 @@ public class EntityUtils {
return 26;
case SLOW_FALLING:
return 27;
+ case BAD_OMEN:
+ return 28;
+ case HERO_OF_THE_VILLAGE:
+ return 29;
default:
return effect.ordinal() + 1;
}
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 0b7b5c5cf..63255cfa0 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java
@@ -25,14 +25,22 @@
package org.geysermc.connector.utils;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import org.geysermc.connector.GeyserConnector;
+import org.reflections.Reflections;
+import org.reflections.serializers.XmlSerializer;
+import org.reflections.util.ConfigurationBuilder;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.net.URL;
+import java.nio.file.Files;
+import java.security.MessageDigest;
import java.util.function.Function;
public class FileUtils {
@@ -42,6 +50,7 @@ public class FileUtils {
*
* @param src File to load
* @param valueType Class to load file into
+ * @param the type
* @return The data as the given class
* @throws IOException if the config could not be loaded
*/
@@ -50,6 +59,15 @@ public class FileUtils {
return objectMapper.readValue(src, valueType);
}
+ public static T loadYaml(InputStream src, Class valueType) throws IOException {
+ ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()).enable(JsonParser.Feature.IGNORE_UNDEFINED).disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+ return objectMapper.readValue(src, valueType);
+ }
+
+ public static T loadJson(InputStream src, Class valueType) throws IOException {
+ return GeyserConnector.JSON_MAPPER.readValue(src, valueType);
+ }
+
/**
* Open the specified file or copy if from resources
*
@@ -139,4 +157,38 @@ public class FileUtils {
}
return stream;
}
+
+ /**
+ * Calculate the SHA256 hash of the resource pack file
+ * @param file File to calculate the hash for
+ * @return A byte[] representation of the hash
+ */
+ public static byte[] calculateSHA256(File file) {
+ byte[] sha256;
+
+ try {
+ sha256 = MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(file.toPath()));
+ } catch (Exception e) {
+ throw new RuntimeException("Could not calculate pack hash", e);
+ }
+
+ return sha256;
+ }
+
+ /**
+ * Get the stored reflection data for a given path
+ *
+ * @param path The path to get the reflection data for
+ * @return The created Reflections object
+ */
+ public static Reflections getReflections(String path) {
+ Reflections reflections = new Reflections(new ConfigurationBuilder());
+ XmlSerializer serializer = new XmlSerializer();
+ URL resource = FileUtils.class.getClassLoader().getResource("META-INF/reflections/" + path + "-reflections.xml");
+ try (InputStream inputStream = resource.openConnection().getInputStream()) {
+ reflections.merge(serializer.read(inputStream));
+ } catch (IOException e) { }
+
+ return reflections;
+ }
}
diff --git a/connector/src/main/java/org/geysermc/connector/utils/GameRule.java b/connector/src/main/java/org/geysermc/connector/utils/GameRule.java
new file mode 100644
index 000000000..48feb1c18
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/utils/GameRule.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2019-2020 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.connector.utils;
+
+import lombok.Getter;
+
+/**
+ * This enum stores each gamerule along with the value type and the default.
+ * It is used to construct the list for the settings menu
+ */
+public enum GameRule {
+ ANNOUNCEADVANCEMENTS("announceAdvancements", Boolean.class, true), // JE only
+ COMMANDBLOCKOUTPUT("commandBlockOutput", Boolean.class, true),
+ DISABLEELYTRAMOVEMENTCHECK("disableElytraMovementCheck", Boolean.class, false), // JE only
+ DISABLERAIDS("disableRaids", Boolean.class, false), // JE only
+ DODAYLIGHTCYCLE("doDaylightCycle", Boolean.class, true),
+ DOENTITYDROPS("doEntityDrops", Boolean.class, true),
+ DOFIRETICK("doFireTick", Boolean.class, true),
+ DOIMMEDIATERESPAWN("doImmediateRespawn", Boolean.class, false),
+ DOINSOMNIA("doInsomnia", Boolean.class, true),
+ DOLIMITEDCRAFTING("doLimitedCrafting", Boolean.class, false), // JE only
+ DOMOBLOOT("doMobLoot", Boolean.class, true),
+ DOMOBSPAWNING("doMobSpawning", Boolean.class, true),
+ DOPATROLSPAWNING("doPatrolSpawning", Boolean.class, true), // JE only
+ DOTILEDROPS("doTileDrops", Boolean.class, true),
+ DOTRADERSPAWNING("doTraderSpawning", Boolean.class, true), // JE only
+ DOWEATHERCYCLE("doWeatherCycle", Boolean.class, true),
+ DROWNINGDAMAGE("drowningDamage", Boolean.class, true),
+ FALLDAMAGE("fallDamage", Boolean.class, true),
+ FIREDAMAGE("fireDamage", Boolean.class, true),
+ FORGIVEDEADPLAYERS("forgiveDeadPlayers", Boolean.class, true), // JE only
+ KEEPINVENTORY("keepInventory", Boolean.class, false),
+ LOGADMINCOMMANDS("logAdminCommands", Boolean.class, true), // JE only
+ MAXCOMMANDCHAINLENGTH("maxCommandChainLength", Integer.class, 65536),
+ MAXENTITYCRAMMING("maxEntityCramming", Integer.class, 24), // JE only
+ MOBGRIEFING("mobGriefing", Boolean.class, true),
+ NATURALREGENERATION("naturalRegeneration", Boolean.class, true),
+ RANDOMTICKSPEED("randomTickSpeed", Integer.class, 3),
+ REDUCEDDEBUGINFO("reducedDebugInfo", Boolean.class, false), // JE only
+ SENDCOMMANDFEEDBACK("sendCommandFeedback", Boolean.class, true),
+ SHOWDEATHMESSAGES("showDeathMessages", Boolean.class, true),
+ SPAWNRADIUS("spawnRadius", Integer.class, 10),
+ SPECTATORSGENERATECHUNKS("spectatorsGenerateChunks", Boolean.class, true), // JE only
+ UNIVERSALANGER("universalAnger", Boolean.class, false), // JE only
+
+ UNKNOWN("unknown", Object.class);
+
+ private static final GameRule[] VALUES = values();
+
+ @Getter
+ private String javaID;
+
+ @Getter
+ private Class> type;
+
+ @Getter
+ private Object defaultValue;
+
+ GameRule(String javaID, Class> type) {
+ this(javaID, type, null);
+ }
+
+ GameRule(String javaID, Class> type, Object defaultValue) {
+ this.javaID = javaID;
+ this.type = type;
+ this.defaultValue = defaultValue;
+ }
+
+ /**
+ * Convert a string to an object of the correct type for the current gamerule
+ *
+ * @param value The string value to convert
+ * @return The converted and formatted value
+ */
+ public Object convertValue(String value) {
+ if (type.equals(Boolean.class)) {
+ return Boolean.parseBoolean(value);
+ } else if (type.equals(Integer.class)) {
+ return Integer.parseInt(value);
+ }
+
+ return null;
+ }
+
+ /**
+ * Fetch a game rule by the given Java ID
+ *
+ * @param id The ID of the gamerule
+ * @return A {@link GameRule} object representing the requested ID or {@link GameRule#UNKNOWN}
+ */
+ public static GameRule fromJavaID(String id) {
+ for (GameRule gamerule : VALUES) {
+ if (gamerule.javaID.equals(id)) {
+ return gamerule;
+ }
+ }
+
+ return UNKNOWN;
+ }
+}
diff --git a/connector/src/main/java/org/geysermc/connector/utils/InventoryUtils.java b/connector/src/main/java/org/geysermc/connector/utils/InventoryUtils.java
index 6d83da0ad..c1224e6e2 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/InventoryUtils.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/InventoryUtils.java
@@ -26,6 +26,9 @@
package org.geysermc.connector.utils;
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.ClientCreativeInventoryActionPacket;
+import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientMoveItemToHotbarPacket;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.nukkitx.nbt.NbtMap;
import com.nukkitx.nbt.NbtMapBuilder;
@@ -33,12 +36,14 @@ import com.nukkitx.nbt.NbtType;
import com.nukkitx.protocol.bedrock.data.inventory.ContainerId;
import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket;
-import org.geysermc.connector.common.ChatColor;
+import com.nukkitx.protocol.bedrock.packet.PlayerHotbarPacket;
import org.geysermc.connector.GeyserConnector;
+import org.geysermc.connector.common.ChatColor;
import org.geysermc.connector.inventory.Inventory;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.inventory.DoubleChestInventoryTranslator;
import org.geysermc.connector.network.translators.inventory.InventoryTranslator;
+import org.geysermc.connector.network.translators.item.ItemEntry;
import org.geysermc.connector.network.translators.item.ItemRegistry;
import org.geysermc.connector.network.translators.item.ItemTranslator;
@@ -136,6 +141,9 @@ public class InventoryUtils {
/**
* Returns a barrier block with custom name and lore to explain why
* part of the inventory is unusable.
+ *
+ * @param description the description
+ * @return the unusable space block
*/
public static ItemData createUnusableSpaceBlock(String description) {
NbtMapBuilder root = NbtMap.builder();
@@ -148,4 +156,90 @@ public class InventoryUtils {
root.put("display", display.build());
return ItemData.of(ItemRegistry.ITEM_ENTRIES.get(ItemRegistry.BARRIER_INDEX).getBedrockId(), (short) 0, 1, root.build());
}
+
+ /**
+ * Attempt to find the specified item name in the session's inventory.
+ * If it is found and in the hotbar, set the user's held item to that slot.
+ * If it is found in another part of the inventory, move it.
+ * If it is not found and the user is in creative mode, create the item,
+ * overriding the current item slot if no other hotbar slots are empty, or otherwise selecting the empty slot.
+ *
+ * This attempts to mimic Java Edition behavior as best as it can.
+ * @param session the Bedrock client's session
+ * @param itemName the Java identifier of the item to search/select
+ */
+ public static void findOrCreatePickedBlock(GeyserSession session, String itemName) {
+ // Get the inventory to choose a slot to pick
+ Inventory inventory = session.getInventoryCache().getOpenInventory();
+ if (inventory == null) {
+ inventory = session.getInventory();
+ }
+
+ // Check hotbar for item
+ for (int i = 36; i < 45; i++) {
+ if (inventory.getItem(i) == null) {
+ continue;
+ }
+ ItemEntry item = ItemRegistry.getItem(inventory.getItem(i));
+ // If this isn't the item we're looking for
+ if (!item.getJavaIdentifier().equals(itemName)) {
+ continue;
+ }
+
+ setHotbarItem(session, i);
+ // Don't check inventory if item was in hotbar
+ return;
+ }
+
+ // Check inventory for item
+ for (int i = 9; i < 36; i++) {
+ if (inventory.getItem(i) == null) {
+ continue;
+ }
+ ItemEntry item = ItemRegistry.getItem(inventory.getItem(i));
+ // If this isn't the item we're looking for
+ if (!item.getJavaIdentifier().equals(itemName)) {
+ continue;
+ }
+
+ ClientMoveItemToHotbarPacket packetToSend = new ClientMoveItemToHotbarPacket(i); // https://wiki.vg/Protocol#Pick_Item
+ session.sendDownstreamPacket(packetToSend);
+ return;
+ }
+
+ // If we still have not found the item, and we're in creative, ask for the item from the server.
+ if (session.getGameMode() == GameMode.CREATIVE) {
+ int slot = session.getInventory().getHeldItemSlot() + 36;
+ if (session.getInventory().getItemInHand() != null) { // Otherwise we should just use the current slot
+ for (int i = 36; i < 45; i++) {
+ if (inventory.getItem(i) == null) {
+ slot = i;
+ break;
+ }
+ }
+ }
+
+ ClientCreativeInventoryActionPacket actionPacket = new ClientCreativeInventoryActionPacket(slot,
+ new ItemStack(ItemRegistry.getItemEntry(itemName).getJavaId()));
+ if ((slot - 36) != session.getInventory().getHeldItemSlot()) {
+ setHotbarItem(session, slot);
+ }
+ session.sendDownstreamPacket(actionPacket);
+ }
+ }
+
+ /**
+ * Changes the held item slot to the specified slot
+ * @param session GeyserSession
+ * @param slot inventory slot to be selected
+ */
+ private static void setHotbarItem(GeyserSession session, int slot) {
+ PlayerHotbarPacket hotbarPacket = new PlayerHotbarPacket();
+ hotbarPacket.setContainerId(0);
+ // Java inventory slot to hotbar slot ID
+ hotbarPacket.setSelectedHotbarSlot(slot - 36);
+ hotbarPacket.setSelectHotbarSlot(true);
+ session.sendUpstreamPacket(hotbarPacket);
+ // No need to send a Java packet as Bedrock sends a confirmation packet back that we translate
+ }
}
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 61e23470d..de6796a26 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java
@@ -122,7 +122,7 @@ public class LanguageUtils {
formatString = key;
}
- return MessageFormat.format(formatString.replace("&", "\u00a7"), values);
+ return MessageFormat.format(formatString.replace("'", "''").replace("&", "\u00a7"), values);
}
/**
diff --git a/connector/src/main/java/org/geysermc/connector/utils/LocaleUtils.java b/connector/src/main/java/org/geysermc/connector/utils/LocaleUtils.java
index 285846a97..d1d59490f 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/LocaleUtils.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/LocaleUtils.java
@@ -133,7 +133,7 @@ public class LocaleUtils {
* @param locale Locale to download
*/
private static void downloadLocale(String locale) {
- File localeFile = Paths.get(GeyserConnector.getInstance().getBootstrap().getConfigFolder().toString(),"locales",locale + ".json").toFile();
+ File localeFile = GeyserConnector.getInstance().getBootstrap().getConfigFolder().resolve("locales/" + locale + ".json").toFile();
// Check if we have already downloaded the locale file
if (localeFile.exists()) {
@@ -150,7 +150,7 @@ public class LocaleUtils {
// Get the hash and download the locale
String hash = ASSET_MAP.get("minecraft/lang/" + locale + ".json").getHash();
- WebUtils.downloadFile("http://resources.download.minecraft.net/" + hash.substring(0, 2) + "/" + hash, localeFile.toString());
+ WebUtils.downloadFile("https://resources.download.minecraft.net/" + hash.substring(0, 2) + "/" + hash, localeFile.toString());
}
/**
@@ -246,6 +246,11 @@ public class LocaleUtils {
Map localeStrings = LocaleUtils.LOCALE_MAPPINGS.get(locale.toLowerCase());
if (localeStrings == null)
localeStrings = LocaleUtils.LOCALE_MAPPINGS.get(LanguageUtils.getDefaultLocale());
+ if (localeStrings == null) {
+ // Don't cause a NPE if the locale is STILL missing
+ GeyserConnector.getInstance().getLogger().debug("MISSING DEFAULT LOCALE: " + LanguageUtils.getDefaultLocale());
+ return messageText;
+ }
return localeStrings.getOrDefault(messageText, messageText);
}
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 0cd646c4c..12e40f278 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java
@@ -72,7 +72,7 @@ public class LoginEncryptionUtils {
}
if (lastKey != null) {
- EncryptionUtils.verifyJwt(jwt, lastKey);
+ if (!EncryptionUtils.verifyJwt(jwt, lastKey)) return false;
}
JsonNode payloadNode = JSON_MAPPER.readTree(jwt.getPayload().toString());
@@ -105,6 +105,10 @@ public class LoginEncryptionUtils {
connector.getLogger().debug(String.format("Is player data valid? %s", validChain));
+ if (!validChain && !session.getConnector().getConfig().isEnableProxyConnections()) {
+ session.disconnect(LanguageUtils.getLocaleStringLog("geyser.network.remote.invalid_xbox_account"));
+ return;
+ }
JWSObject jwt = JWSObject.parse(certChainData.get(certChainData.size() - 1).asText());
JsonNode payload = JSON_MAPPER.readTree(jwt.getPayload().toBytes());
diff --git a/connector/src/main/java/org/geysermc/connector/utils/MathUtils.java b/connector/src/main/java/org/geysermc/connector/utils/MathUtils.java
index 487024922..3ce4fea86 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/MathUtils.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/MathUtils.java
@@ -40,6 +40,26 @@ public class MathUtils {
return floatNumber > truncated ? truncated + 1 : truncated;
}
+ /**
+ * If number is greater than the max, set it to max, and if number is lower than low, set it to low.
+ * @param num number to calculate
+ * @param min the lowest value the number can be
+ * @param max the greatest value the number can be
+ * @return - min if num is lower than min
+ * - max if num is greater than max
+ * - num otherwise
+ */
+ public static double constrain(double num, double min, double max) {
+ if (num > max) {
+ num = max;
+ }
+ if (num < min) {
+ num = min;
+ }
+
+ return num;
+ }
+
/**
* Converts the given object from an int or byte to byte.
* This is used for NBT data that might be either an int
@@ -54,4 +74,15 @@ public class MathUtils {
}
return (Byte) value;
}
+
+ /**
+ * Packs a chunk's X and Z coordinates into a single {@code long}.
+ *
+ * @param x the X coordinate
+ * @param z the Z coordinate
+ * @return the packed coordinates
+ */
+ public static long chunkPositionToLong(int x, int z) {
+ return ((x & 0xFFFFFFFFL) << 32L) | (z & 0xFFFFFFFFL);
+ }
}
diff --git a/connector/src/main/java/org/geysermc/connector/utils/MessageUtils.java b/connector/src/main/java/org/geysermc/connector/utils/MessageUtils.java
index 0b4958950..a127fd8db 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/MessageUtils.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/MessageUtils.java
@@ -95,7 +95,7 @@ public class MessageUtils {
* @param messages A {@link List} of {@link Message} to parse
* @param locale A locale loaded to get the message for
* @param parent A {@link Message} to use as the parent (can be null)
- * @return
+ * @return the translation parameters
*/
public static List getTranslationParams(List messages, String locale, Message parent) {
List strings = new ArrayList<>();
@@ -110,14 +110,6 @@ public class MessageUtils {
strings.add(builder);
}
- if (translation.getKey().equals("commands.gamemode.success.other")) {
- strings.add("");
- }
-
- if (translation.getKey().equals("command.context.here")) {
- strings.add(" - no permission or invalid command!");
- }
-
// Collect all params and add format corrections to the end of them
List furtherParams = new ArrayList<>();
for (String param : getTranslationParams(translation.getWith(), locale, message)) {
@@ -133,9 +125,16 @@ public class MessageUtils {
}
if (locale != null) {
- strings.add(insertParams(LocaleUtils.getLocaleString(translation.getKey(), locale), furtherParams));
+ String builder = getFormat(message.getStyle().getFormats()) +
+ getColor(message.getStyle().getColor());
+ builder += insertParams(LocaleUtils.getLocaleString(translation.getKey(), locale), furtherParams);
+ strings.add(builder);
} else {
- strings.addAll(furtherParams);
+ String format = getFormat(message.getStyle().getFormats()) +
+ getColor(message.getStyle().getColor());
+ for (String param : furtherParams) {
+ strings.add(format + param);
+ }
}
} else {
String builder = getFormat(message.getStyle().getFormats()) +
@@ -160,10 +159,10 @@ public class MessageUtils {
* Translate a given {@link TranslationMessage} to the given locale
*
* @param message The {@link Message} to send
- * @param locale
- * @param shouldTranslate
- * @param parent
- * @return
+ * @param locale the locale
+ * @param shouldTranslate if the message should be translated
+ * @param parent the parent message
+ * @return the given translation message translated from the given locale
*/
public static String getTranslatedBedrockMessage(Message message, String locale, boolean shouldTranslate, Message parent) {
JsonParser parser = new JsonParser();
diff --git a/connector/src/main/java/org/geysermc/connector/utils/PluginMessageUtils.java b/connector/src/main/java/org/geysermc/connector/utils/PluginMessageUtils.java
new file mode 100644
index 000000000..2d828038c
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/utils/PluginMessageUtils.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2019-2020 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.connector.utils;
+
+import org.geysermc.connector.GeyserConnector;
+
+import java.nio.charset.StandardCharsets;
+
+public class PluginMessageUtils {
+
+ private static final byte[] BRAND_DATA;
+
+ static {
+ byte[] data = GeyserConnector.NAME.getBytes(StandardCharsets.UTF_8);
+ byte[] varInt = writeVarInt(data.length);
+ BRAND_DATA = new byte[varInt.length + data.length];
+ System.arraycopy(varInt, 0, BRAND_DATA, 0, varInt.length);
+ System.arraycopy(data, 0, BRAND_DATA, varInt.length, data.length);
+ }
+
+ /**
+ * Get the prebuilt brand as a byte array
+ * @return the brand information of the Geyser client
+ */
+ public static byte[] getGeyserBrandData() {
+ return BRAND_DATA;
+ }
+
+ private static byte[] writeVarInt(int value) {
+ byte[] data = new byte[getVarIntLength(value)];
+ int index = 0;
+ do {
+ byte temp = (byte)(value & 0b01111111);
+ value >>>= 7;
+ if (value != 0) {
+ temp |= 0b10000000;
+ }
+ data[index] = temp;
+ index++;
+ } while (value != 0);
+ return data;
+ }
+
+ private static int getVarIntLength(int number) {
+ if ((number & 0xFFFFFF80) == 0) {
+ return 1;
+ } else if ((number & 0xFFFFC000) == 0) {
+ return 2;
+ } else if ((number & 0xFFE00000) == 0) {
+ return 3;
+ } else if ((number & 0xF0000000) == 0) {
+ return 4;
+ }
+ return 5;
+ }
+}
diff --git a/connector/src/main/java/org/geysermc/connector/utils/ResourcePack.java b/connector/src/main/java/org/geysermc/connector/utils/ResourcePack.java
new file mode 100644
index 000000000..3e9848dbe
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/utils/ResourcePack.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2019-2020 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.connector.utils;
+
+import org.geysermc.connector.GeyserConnector;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.zip.ZipFile;
+
+/**
+ * This represents a resource pack and all the data relevant to it
+ */
+public class ResourcePack {
+ /**
+ * The list of loaded resource packs
+ */
+ public static final Map PACKS = new HashMap<>();
+
+ /**
+ * The size of each chunk to use when sending the resource packs to clients in bytes
+ */
+ public static final int CHUNK_SIZE = 102400;
+
+ private byte[] sha256;
+ private File file;
+ private ResourcePackManifest manifest;
+ private ResourcePackManifest.Version version;
+
+ /**
+ * Loop through the packs directory and locate valid resource pack files
+ */
+ public static void loadPacks() {
+ File directory = GeyserConnector.getInstance().getBootstrap().getConfigFolder().resolve("packs").toFile();
+
+ if (!directory.exists()) {
+ directory.mkdir();
+
+ // 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();
+
+ pack.sha256 = FileUtils.calculateSHA256(file);
+
+ try {
+ ZipFile zip = new ZipFile(file);
+
+ zip.stream().forEach((x) -> {
+ if (x.getName().contains("manifest.json")) {
+ try {
+ ResourcePackManifest manifest = FileUtils.loadJson(zip.getInputStream(x), ResourcePackManifest.class);
+
+ pack.file = file;
+ pack.manifest = manifest;
+ pack.version = ResourcePackManifest.Version.fromArray(manifest.getHeader().getVersion());
+
+ PACKS.put(pack.getManifest().getHeader().getUuid().toString(), pack);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ });
+ } catch (Exception e) {
+ GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.resource_pack.broken", file.getName()));
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ public byte[] getSha256() {
+ return sha256;
+ }
+
+ public File getFile() {
+ return file;
+ }
+
+ public ResourcePackManifest getManifest() {
+ return manifest;
+ }
+
+ public ResourcePackManifest.Version getVersion() {
+ return version;
+ }
+}
diff --git a/connector/src/main/java/org/geysermc/connector/utils/ResourcePackManifest.java b/connector/src/main/java/org/geysermc/connector/utils/ResourcePackManifest.java
new file mode 100644
index 000000000..6a08c4dbc
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/utils/ResourcePackManifest.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2019-2020 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.connector.utils;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import lombok.Value;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.UUID;
+
+/**
+ * author: NukkitX
+ * Nukkit Project
+ */
+@Getter
+@EqualsAndHashCode
+public class ResourcePackManifest {
+ @JsonProperty("format_version")
+ private Integer formatVersion;
+ private Header header;
+ private Collection modules;
+ protected Collection dependencies;
+
+ public Collection getModules() {
+ return Collections.unmodifiableCollection(modules);
+ }
+
+ @Getter
+ @ToString
+ public static class Header {
+ private String description;
+ private String name;
+ private UUID uuid;
+ private int[] version;
+ @JsonProperty("min_engine_version")
+ private int[] minimumSupportedMinecraftVersion;
+
+ public String getVersionString() {
+ return version[0] + "." + version[1] + "." + version[2];
+ }
+ }
+
+ @Getter
+ @ToString
+ public static class Module {
+ private String description;
+ private String name;
+ private UUID uuid;
+ private int[] version;
+ }
+
+ @Getter
+ @ToString
+ public static class Dependency {
+ private UUID uuid;
+ private int[] version;
+ }
+
+ @Value
+ public static class Version {
+ private final int major;
+ private final int minor;
+ private final int patch;
+
+ public static Version fromString(String ver) {
+ String[] split = ver.replace(']', ' ')
+ .replace('[', ' ')
+ .replaceAll(" ", "").split(",");
+
+ return new Version(Integer.parseInt(split[0]), Integer.parseInt(split[1]), Integer.parseInt(split[2]));
+ }
+
+ public static Version fromArray(int[] ver) {
+ return new Version(ver[0], ver[1], ver[2]);
+ }
+
+ private Version(int major, int minor, int patch) {
+ this.major = major;
+ this.minor = minor;
+ this.patch = patch;
+ }
+
+
+ @Override
+ public String toString() {
+ return major + "." + minor + "." + patch;
+ }
+ }
+}
+
diff --git a/connector/src/main/java/org/geysermc/connector/utils/SettingsUtils.java b/connector/src/main/java/org/geysermc/connector/utils/SettingsUtils.java
new file mode 100644
index 000000000..13db4682c
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/utils/SettingsUtils.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (c) 2019-2020 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.connector.utils;
+
+import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
+import com.github.steveice10.mc.protocol.data.game.setting.Difficulty;
+import org.geysermc.common.window.CustomFormBuilder;
+import org.geysermc.common.window.CustomFormWindow;
+import org.geysermc.common.window.button.FormImage;
+import org.geysermc.common.window.component.DropdownComponent;
+import org.geysermc.common.window.component.InputComponent;
+import org.geysermc.common.window.component.LabelComponent;
+import org.geysermc.common.window.component.ToggleComponent;
+import org.geysermc.common.window.response.CustomFormResponse;
+import org.geysermc.connector.GeyserConnector;
+import org.geysermc.connector.network.session.GeyserSession;
+
+import java.util.ArrayList;
+
+public class SettingsUtils {
+
+ // Used in UpstreamPacketHandler.java
+ public static final int SETTINGS_FORM_ID = 1338;
+
+ /**
+ * Build a settings form for the given session and store it for later
+ *
+ * @param session The session to build the form for
+ */
+ public static void buildForm(GeyserSession session) {
+ // Cache the language for cleaner access
+ String language = session.getClientData().getLanguageCode();
+
+ CustomFormBuilder builder = new CustomFormBuilder(LanguageUtils.getPlayerLocaleString("geyser.settings.title.main", language));
+ builder.setIcon(new FormImage(FormImage.FormImageType.PATH, "textures/ui/settings_glyph_color_2x.png"));
+
+ builder.addComponent(new LabelComponent(LanguageUtils.getPlayerLocaleString("geyser.settings.title.client", language)));
+ builder.addComponent(new ToggleComponent(LanguageUtils.getPlayerLocaleString("geyser.settings.option.coordinates", language), session.getWorldCache().isShowCoordinates()));
+
+
+ if (session.getOpPermissionLevel() >= 2 || session.hasPermission("geyser.settings.server")) {
+ builder.addComponent(new LabelComponent(LanguageUtils.getPlayerLocaleString("geyser.settings.title.server", language)));
+
+ DropdownComponent gamemodeDropdown = new DropdownComponent();
+ gamemodeDropdown.setText("%createWorldScreen.gameMode.personal");
+ gamemodeDropdown.setOptions(new ArrayList<>());
+ for (GameMode gamemode : GameMode.values()) {
+ gamemodeDropdown.addOption(LocaleUtils.getLocaleString("selectWorld.gameMode." + gamemode.name().toLowerCase(), language), session.getGameMode() == gamemode);
+ }
+ builder.addComponent(gamemodeDropdown);
+
+ DropdownComponent difficultyDropdown = new DropdownComponent();
+ difficultyDropdown.setText("%options.difficulty");
+ difficultyDropdown.setOptions(new ArrayList<>());
+ for (Difficulty difficulty : Difficulty.values()) {
+ difficultyDropdown.addOption("%options.difficulty." + difficulty.name().toLowerCase(), session.getWorldCache().getDifficulty() == difficulty);
+ }
+ builder.addComponent(difficultyDropdown);
+ }
+
+ if (session.getOpPermissionLevel() >= 2 || session.hasPermission("geyser.settings.gamerules")) {
+ builder.addComponent(new LabelComponent(LanguageUtils.getPlayerLocaleString("geyser.settings.title.game_rules", language)));
+ for (GameRule gamerule : GameRule.values()) {
+ if (gamerule.equals(GameRule.UNKNOWN)) {
+ continue;
+ }
+
+ // Add the relevant form item based on the gamerule type
+ if (Boolean.class.equals(gamerule.getType())) {
+ builder.addComponent(new ToggleComponent(LocaleUtils.getLocaleString("gamerule." + gamerule.getJavaID(), language), GeyserConnector.getInstance().getWorldManager().getGameRuleBool(session, gamerule)));
+ } else if (Integer.class.equals(gamerule.getType())) {
+ builder.addComponent(new InputComponent(LocaleUtils.getLocaleString("gamerule." + gamerule.getJavaID(), language), "", String.valueOf(GeyserConnector.getInstance().getWorldManager().getGameRuleInt(session, gamerule))));
+ }
+ }
+ }
+
+ session.setSettingsForm(builder.build());
+ }
+
+ /**
+ * Handle the settings form response
+ *
+ * @param session The session that sent the response
+ * @param response The response string to parse
+ * @return True if the form was parsed correctly, false if not
+ */
+ public static boolean handleSettingsForm(GeyserSession session, String response) {
+ CustomFormWindow settingsForm = session.getSettingsForm();
+ settingsForm.setResponse(response);
+
+ CustomFormResponse settingsResponse = (CustomFormResponse) settingsForm.getResponse();
+ int offset = 0;
+
+ offset++; // Client settings title
+
+ session.getWorldCache().setShowCoordinates(settingsResponse.getToggleResponses().get(offset));
+ offset++;
+
+ if (session.getOpPermissionLevel() >= 2 || session.hasPermission("geyser.settings.server")) {
+ offset++; // Server settings title
+
+ GameMode gameMode = GameMode.values()[settingsResponse.getDropdownResponses().get(offset).getElementID()];
+ if (gameMode != null && gameMode != session.getGameMode()) {
+ session.getConnector().getWorldManager().setPlayerGameMode(session, gameMode);
+ }
+ offset++;
+
+ Difficulty difficulty = Difficulty.values()[settingsResponse.getDropdownResponses().get(offset).getElementID()];
+ if (difficulty != null && difficulty != session.getWorldCache().getDifficulty()) {
+ session.getConnector().getWorldManager().setDifficulty(session, difficulty);
+ }
+ offset++;
+ }
+
+ if (session.getOpPermissionLevel() >= 2 || session.hasPermission("geyser.settings.gamerules")) {
+ offset++; // Game rule title
+
+ for (GameRule gamerule : GameRule.values()) {
+ if (gamerule.equals(GameRule.UNKNOWN)) {
+ continue;
+ }
+
+ if (Boolean.class.equals(gamerule.getType())) {
+ Boolean value = settingsResponse.getToggleResponses().get(offset).booleanValue();
+ if (value != session.getConnector().getWorldManager().getGameRuleBool(session, gamerule)) {
+ session.getConnector().getWorldManager().setGameRule(session, gamerule.getJavaID(), value);
+ }
+ } else if (Integer.class.equals(gamerule.getType())) {
+ int value = Integer.parseInt(settingsResponse.getInputResponses().get(offset));
+ if (value != session.getConnector().getWorldManager().getGameRuleInt(session, gamerule)) {
+ session.getConnector().getWorldManager().setGameRule(session, gamerule.getJavaID(), value);
+ }
+ }
+ offset++;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/connector/src/main/java/org/geysermc/connector/utils/SignUtils.java b/connector/src/main/java/org/geysermc/connector/utils/SignUtils.java
new file mode 100644
index 000000000..06406b55e
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/utils/SignUtils.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2019-2020 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.connector.utils;
+
+/**
+ * Provides utilities for interacting with signs. Mainly, it deals with the widths of each character.
+ * Since Bedrock auto-wraps signs and Java does not, we have to take this into account when translating signs.
+ */
+public class SignUtils {
+
+ // TODO: If we send the Java font via resource pack, does width change?
+ /**
+ * The maximum character width that a sign can hold in Bedrock
+ */
+ public static final int BEDROCK_CHARACTER_WIDTH_MAX = 88;
+
+ /**
+ * The maximum character width that a sign can hold in Java
+ */
+ public static final int JAVA_CHARACTER_WIDTH_MAX = 90;
+
+ /**
+ * Gets the Minecraft width of a character
+ * @param c character to determine
+ * @return width of the character
+ */
+ public static int getCharacterWidth(char c) {
+ switch (c) {
+ case '!':
+ case ',':
+ case '.':
+ case ':':
+ case ';':
+ case 'i':
+ case '|':
+ case '¡':
+ return 2;
+
+ case '\'':
+ case 'l':
+ case 'ì':
+ case 'í':
+ return 3;
+
+ case ' ':
+ case 'I':
+ case '[':
+ case ']':
+ case 't':
+ case '×':
+ case 'ï':
+ return 4;
+
+ case '"':
+ case '(':
+ case ')':
+ case '*':
+ case '<':
+ case '>':
+ case 'f':
+ case 'k':
+ case '{':
+ case '}':
+ return 5;
+
+ case '@':
+ case '~':
+ case '®':
+ return 7;
+
+ default:
+ return 6;
+ }
+ }
+
+}
diff --git a/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java b/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java
index 352a0b0f9..7c30e48ab 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java
@@ -45,6 +45,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Base64;
+import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
@@ -58,6 +59,10 @@ public class SkinProvider {
public static final Skin EMPTY_SKIN = new Skin(-1, "steve", STEVE_SKIN);
public static final byte[] ALEX_SKIN = new ProvidedSkin("bedrock/skin/skin_alex.png").getSkin();
public static final Skin EMPTY_SKIN_ALEX = new Skin(-1, "alex", ALEX_SKIN);
+ private static final Map permanentSkins = new HashMap() {{
+ put("steve", EMPTY_SKIN);
+ put("alex", EMPTY_SKIN_ALEX);
+ }};
private static final Cache cachedSkins = CacheBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.HOURS)
.build();
@@ -114,7 +119,7 @@ public class SkinProvider {
// Schedule Daily Image Expiry if we are caching them
if (GeyserConnector.getInstance().getConfig().getCacheImages() > 0) {
GeyserConnector.getInstance().getGeneralThreadPool().scheduleAtFixedRate(() -> {
- File cacheFolder = Paths.get("cache", "images").toFile();
+ File cacheFolder = GeyserConnector.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("images").toFile();
if (!cacheFolder.exists()) {
return;
}
@@ -141,8 +146,7 @@ public class SkinProvider {
}
public static Skin getCachedSkin(String skinUrl) {
- Skin skin = cachedSkins.getIfPresent(skinUrl);
- return skin != null ? skin : EMPTY_SKIN;
+ return permanentSkins.getOrDefault(skinUrl, cachedSkins.getIfPresent(skinUrl));
}
public static Cape getCachedCape(String capeUrl) {
@@ -169,7 +173,7 @@ public class SkinProvider {
if (textureUrl == null || textureUrl.isEmpty()) return CompletableFuture.completedFuture(EMPTY_SKIN);
if (requestedSkins.containsKey(textureUrl)) return requestedSkins.get(textureUrl); // already requested
- Skin cachedSkin = cachedSkins.getIfPresent(textureUrl);
+ Skin cachedSkin = getCachedSkin(textureUrl);
if (cachedSkin != null) {
return CompletableFuture.completedFuture(cachedSkin);
}
@@ -391,7 +395,7 @@ public class SkinProvider {
BufferedImage image = null;
// First see if we have a cached file. We also update the modification stamp so we know when the file was last used
- File imageFile = Paths.get("cache", "images", UUID.nameUUIDFromBytes(imageUrl.getBytes()).toString() + ".png").toFile();
+ File imageFile = GeyserConnector.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("images").resolve(UUID.nameUUIDFromBytes(imageUrl.getBytes()).toString() + ".png").toFile();
if (imageFile.exists()) {
try {
GeyserConnector.getInstance().getLogger().debug("Reading cached image from file " + imageFile.getPath() + " for " + imageUrl);
@@ -596,7 +600,7 @@ public class SkinProvider {
@Getter
public enum CapeProvider {
MINECRAFT,
- OPTIFINE("http://s.optifine.net/capes/%s.png", CapeUrlType.USERNAME),
+ OPTIFINE("https://optifine.net/capes/%s.png", CapeUrlType.USERNAME),
LABYMOD("https://www.labymod.net/page/php/getCapeTexture.php?uuid=%s", CapeUrlType.UUID_DASHED),
FIVEZIG("https://textures.5zigreborn.eu/profile/%s", CapeUrlType.UUID_DASHED),
MINECRAFTCAPES("https://minecraftcapes.net/profile/%s/cape", CapeUrlType.UUID);
diff --git a/connector/src/main/java/org/geysermc/connector/utils/SkinUtils.java b/connector/src/main/java/org/geysermc/connector/utils/SkinUtils.java
index 77ab7f939..5505acdf6 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/SkinUtils.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/SkinUtils.java
@@ -33,8 +33,8 @@ import com.nukkitx.protocol.bedrock.data.skin.SerializedSkin;
import com.nukkitx.protocol.bedrock.packet.PlayerListPacket;
import lombok.AllArgsConstructor;
import lombok.Getter;
-import org.geysermc.connector.common.AuthType;
import org.geysermc.connector.GeyserConnector;
+import org.geysermc.connector.common.AuthType;
import org.geysermc.connector.entity.PlayerEntity;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.session.auth.BedrockClientData;
@@ -54,6 +54,9 @@ public class SkinUtils {
SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.getLegacy(data.isAlex());
SkinProvider.Skin skin = SkinProvider.getCachedSkin(data.getSkinUrl());
+ if (skin == null) {
+ skin = SkinProvider.EMPTY_SKIN;
+ }
return buildEntryManually(
session,
@@ -69,21 +72,6 @@ public class SkinUtils {
);
}
- public static PlayerListPacket.Entry buildDefaultEntry(GeyserSession session, GameProfile profile, long geyserId) {
- return buildEntryManually(
- session,
- profile.getId(),
- profile.getName(),
- geyserId,
- "default",
- SkinProvider.STEVE_SKIN,
- SkinProvider.EMPTY_CAPE.getCapeId(),
- SkinProvider.EMPTY_CAPE.getCapeData(),
- SkinProvider.EMPTY_GEOMETRY.getGeometryName(),
- SkinProvider.EMPTY_GEOMETRY.getGeometryData()
- );
- }
-
public static PlayerListPacket.Entry buildEntryManually(GeyserSession session, UUID uuid, String username, long geyserId,
String skinId, byte[] skinData,
String capeId, byte[] capeData,
@@ -93,6 +81,15 @@ public class SkinUtils {
ImageData.of(capeData), geometryData, "", true, false, !capeId.equals(SkinProvider.EMPTY_CAPE.getCapeId()), capeId, skinId
);
+ // This attempts to find the xuid of the player so profile images show up for xbox accounts
+ String xuid = "";
+ for (GeyserSession player : GeyserConnector.getInstance().getPlayers()) {
+ if (player.getPlayerEntity().getUuid().equals(uuid)) {
+ xuid = player.getAuthData().getXboxUUID();
+ break;
+ }
+ }
+
PlayerListPacket.Entry entry;
// If we are building a PlayerListEntry for our own session we use our AuthData UUID instead of the Java UUID
@@ -106,167 +103,120 @@ public class SkinUtils {
entry.setName(username);
entry.setEntityId(geyserId);
entry.setSkin(serializedSkin);
- entry.setXuid("");
+ entry.setXuid(xuid);
entry.setPlatformChatId("");
entry.setTeacher(false);
entry.setTrustedSkin(true);
return entry;
}
- @AllArgsConstructor
- @Getter
- public static class GameProfileData {
- private String skinUrl;
- private String capeUrl;
- private boolean alex;
-
- /**
- * Generate the GameProfileData from the given GameProfile
- *
- * @param profile GameProfile to build the GameProfileData from
- * @return The built GameProfileData
- */
- public static GameProfileData from(GameProfile profile) {
- // Fallback to the offline mode of working it out
- boolean isAlex = ((profile.getId().hashCode() % 2) == 1);
-
- try {
- GameProfile.Property skinProperty = profile.getProperty("textures");
-
- JsonNode skinObject = new ObjectMapper().readTree(new String(Base64.getDecoder().decode(skinProperty.getValue()), StandardCharsets.UTF_8));
- JsonNode textures = skinObject.get("textures");
-
- JsonNode skinTexture = textures.get("SKIN");
- String skinUrl = skinTexture.get("url").asText();
-
- isAlex = skinTexture.has("metadata");
-
- String capeUrl = null;
- if (textures.has("CAPE")) {
- JsonNode capeTexture = textures.get("CAPE");
- capeUrl = capeTexture.get("url").asText();
- }
-
- return new GameProfileData(skinUrl, capeUrl, isAlex);
- } catch (Exception exception) {
- if (GeyserConnector.getInstance().getAuthType() != AuthType.OFFLINE) {
- GeyserConnector.getInstance().getLogger().debug("Got invalid texture data for " + profile.getName() + " " + exception.getMessage());
- }
- // return default skin with default cape when texture data is invalid
- return new GameProfileData((isAlex ? SkinProvider.EMPTY_SKIN_ALEX.getTextureUrl() : SkinProvider.EMPTY_SKIN.getTextureUrl()), SkinProvider.EMPTY_CAPE.getTextureUrl(), isAlex);
- }
- }
- }
-
public static void requestAndHandleSkinAndCape(PlayerEntity entity, GeyserSession session,
Consumer skinAndCapeConsumer) {
- GeyserConnector.getInstance().getGeneralThreadPool().execute(() -> {
- GameProfileData data = GameProfileData.from(entity.getProfile());
+ GameProfileData data = GameProfileData.from(entity.getProfile());
- SkinProvider.requestSkinAndCape(entity.getUuid(), data.getSkinUrl(), data.getCapeUrl())
- .whenCompleteAsync((skinAndCape, throwable) -> {
- try {
- SkinProvider.Skin skin = skinAndCape.getSkin();
- SkinProvider.Cape cape = skinAndCape.getCape();
+ SkinProvider.requestSkinAndCape(entity.getUuid(), data.getSkinUrl(), data.getCapeUrl())
+ .whenCompleteAsync((skinAndCape, throwable) -> {
+ try {
+ SkinProvider.Skin skin = skinAndCape.getSkin();
+ SkinProvider.Cape cape = skinAndCape.getCape();
- if (cape.isFailed()) {
- cape = SkinProvider.getOrDefault(SkinProvider.requestBedrockCape(
- entity.getUuid(), false
- ), SkinProvider.EMPTY_CAPE, 3);
- }
-
- if (cape.isFailed() && SkinProvider.ALLOW_THIRD_PARTY_CAPES) {
- cape = SkinProvider.getOrDefault(SkinProvider.requestUnofficialCape(
- cape, entity.getUuid(),
- entity.getUsername(), false
- ), SkinProvider.EMPTY_CAPE, SkinProvider.CapeProvider.VALUES.length * 3);
- }
-
- SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.getLegacy(data.isAlex());
- geometry = SkinProvider.getOrDefault(SkinProvider.requestBedrockGeometry(
- geometry, entity.getUuid(), false
- ), geometry, 3);
-
- // Not a bedrock player check for ears
- if (geometry.isFailed() && SkinProvider.ALLOW_THIRD_PARTY_EARS) {
- boolean isEars = false;
-
- // Its deadmau5, gotta support his skin :)
- if (entity.getUuid().toString().equals("1e18d5ff-643d-45c8-b509-43b8461d8614")) {
- isEars = true;
- } else {
- // Get the ears texture for the player
- skin = SkinProvider.getOrDefault(SkinProvider.requestUnofficialEars(
- skin, entity.getUuid(), entity.getUsername(), false
- ), skin, 3);
-
- isEars = skin.isEars();
- }
-
- // Does the skin have an ears texture
- if (isEars) {
- // Get the new geometry
- geometry = SkinProvider.SkinGeometry.getEars(data.isAlex());
-
- // Store the skin and geometry for the ears
- SkinProvider.storeEarSkin(entity.getUuid(), skin);
- SkinProvider.storeEarGeometry(entity.getUuid(), data.isAlex());
- }
- }
-
- entity.setLastSkinUpdate(skin.getRequestedOn());
-
- if (session.getUpstream().isInitialized()) {
- PlayerListPacket.Entry updatedEntry = buildEntryManually(
- session,
- entity.getUuid(),
- entity.getUsername(),
- entity.getGeyserId(),
- skin.getTextureUrl(),
- skin.getSkinData(),
- cape.getCapeId(),
- cape.getCapeData(),
- geometry.getGeometryName(),
- geometry.getGeometryData()
- );
-
-
- PlayerListPacket playerAddPacket = new PlayerListPacket();
- playerAddPacket.setAction(PlayerListPacket.Action.ADD);
- playerAddPacket.getEntries().add(updatedEntry);
- session.sendUpstreamPacket(playerAddPacket);
-
- if (!entity.isPlayerList()) {
- PlayerListPacket playerRemovePacket = new PlayerListPacket();
- playerRemovePacket.setAction(PlayerListPacket.Action.REMOVE);
- playerRemovePacket.getEntries().add(updatedEntry);
- session.sendUpstreamPacket(playerRemovePacket);
-
- }
- }
- } catch (Exception e) {
- GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e);
+ if (cape.isFailed()) {
+ cape = SkinProvider.getOrDefault(SkinProvider.requestBedrockCape(
+ entity.getUuid(), false
+ ), SkinProvider.EMPTY_CAPE, 3);
}
- if (skinAndCapeConsumer != null) skinAndCapeConsumer.accept(skinAndCape);
- });
- });
+ if (cape.isFailed() && SkinProvider.ALLOW_THIRD_PARTY_CAPES) {
+ cape = SkinProvider.getOrDefault(SkinProvider.requestUnofficialCape(
+ cape, entity.getUuid(),
+ entity.getUsername(), false
+ ), SkinProvider.EMPTY_CAPE, SkinProvider.CapeProvider.VALUES.length * 3);
+ }
+
+ SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.getLegacy(data.isAlex());
+ geometry = SkinProvider.getOrDefault(SkinProvider.requestBedrockGeometry(
+ geometry, entity.getUuid(), false
+ ), geometry, 3);
+
+ // Not a bedrock player check for ears
+ if (geometry.isFailed() && SkinProvider.ALLOW_THIRD_PARTY_EARS) {
+ boolean isEars;
+
+ // Its deadmau5, gotta support his skin :)
+ if (entity.getUuid().toString().equals("1e18d5ff-643d-45c8-b509-43b8461d8614")) {
+ isEars = true;
+ } else {
+ // Get the ears texture for the player
+ skin = SkinProvider.getOrDefault(SkinProvider.requestUnofficialEars(
+ skin, entity.getUuid(), entity.getUsername(), false
+ ), skin, 3);
+
+ isEars = skin.isEars();
+ }
+
+ // Does the skin have an ears texture
+ if (isEars) {
+ // Get the new geometry
+ geometry = SkinProvider.SkinGeometry.getEars(data.isAlex());
+
+ // Store the skin and geometry for the ears
+ SkinProvider.storeEarSkin(entity.getUuid(), skin);
+ SkinProvider.storeEarGeometry(entity.getUuid(), data.isAlex());
+ }
+ }
+
+ entity.setLastSkinUpdate(skin.getRequestedOn());
+
+ if (session.getUpstream().isInitialized()) {
+ PlayerListPacket.Entry updatedEntry = buildEntryManually(
+ session,
+ entity.getUuid(),
+ entity.getUsername(),
+ entity.getGeyserId(),
+ skin.getTextureUrl(),
+ skin.getSkinData(),
+ cape.getCapeId(),
+ cape.getCapeData(),
+ geometry.getGeometryName(),
+ geometry.getGeometryData()
+ );
+
+
+ PlayerListPacket playerAddPacket = new PlayerListPacket();
+ playerAddPacket.setAction(PlayerListPacket.Action.ADD);
+ playerAddPacket.getEntries().add(updatedEntry);
+ session.sendUpstreamPacket(playerAddPacket);
+
+ if (!entity.isPlayerList()) {
+ PlayerListPacket playerRemovePacket = new PlayerListPacket();
+ playerRemovePacket.setAction(PlayerListPacket.Action.REMOVE);
+ playerRemovePacket.getEntries().add(updatedEntry);
+ session.sendUpstreamPacket(playerRemovePacket);
+
+ }
+ }
+ } catch (Exception e) {
+ GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e);
+ }
+
+ if (skinAndCapeConsumer != null) {
+ skinAndCapeConsumer.accept(skinAndCape);
+ }
+ });
}
public static void handleBedrockSkin(PlayerEntity playerEntity, BedrockClientData clientData) {
- GameProfileData data = GameProfileData.from(playerEntity.getProfile());
-
GeyserConnector.getInstance().getLogger().info(LanguageUtils.getLocaleStringLog("geyser.skin.bedrock.register", playerEntity.getUsername(), playerEntity.getUuid()));
try {
- byte[] skinBytes = com.github.steveice10.mc.auth.util.Base64.decode(clientData.getSkinData().getBytes("UTF-8"));
+ byte[] skinBytes = Base64.getDecoder().decode(clientData.getSkinData().getBytes(StandardCharsets.UTF_8));
byte[] capeBytes = clientData.getCapeData();
- byte[] geometryNameBytes = Base64.getDecoder().decode(clientData.getGeometryName().getBytes("UTF-8"));
- byte[] geometryBytes = Base64.getDecoder().decode(clientData.getGeometryData().getBytes("UTF-8"));
+ byte[] geometryNameBytes = Base64.getDecoder().decode(clientData.getGeometryName().getBytes(StandardCharsets.UTF_8));
+ byte[] geometryBytes = Base64.getDecoder().decode(clientData.getGeometryData().getBytes(StandardCharsets.UTF_8));
if (skinBytes.length <= (128 * 128 * 4) && !clientData.isPersonaSkin()) {
- SkinProvider.storeBedrockSkin(playerEntity.getUuid(), data.getSkinUrl(), skinBytes);
+ SkinProvider.storeBedrockSkin(playerEntity.getUuid(), clientData.getSkinId(), skinBytes);
SkinProvider.storeBedrockGeometry(playerEntity.getUuid(), geometryNameBytes, geometryBytes);
} else {
GeyserConnector.getInstance().getLogger().info(LanguageUtils.getLocaleStringLog("geyser.skin.bedrock.fail", playerEntity.getUsername()));
@@ -280,4 +230,49 @@ public class SkinUtils {
throw new AssertionError("Failed to cache skin for bedrock user (" + playerEntity.getUsername() + "): ", e);
}
}
+
+ @AllArgsConstructor
+ @Getter
+ public static class GameProfileData {
+ private final String skinUrl;
+ private final String capeUrl;
+ private final boolean alex;
+
+ /**
+ * Generate the GameProfileData from the given GameProfile
+ *
+ * @param profile GameProfile to build the GameProfileData from
+ * @return The built GameProfileData
+ */
+ public static GameProfileData from(GameProfile profile) {
+ // Fallback to the offline mode of working it out
+ boolean isAlex = (Math.abs(profile.getId().hashCode() % 2) == 1);
+
+ try {
+ GameProfile.Property skinProperty = profile.getProperty("textures");
+
+ JsonNode skinObject = new ObjectMapper().readTree(new String(Base64.getDecoder().decode(skinProperty.getValue()), StandardCharsets.UTF_8));
+ JsonNode textures = skinObject.get("textures");
+
+ JsonNode skinTexture = textures.get("SKIN");
+ String skinUrl = skinTexture.get("url").asText().replace("http://", "https://");
+
+ isAlex = skinTexture.has("metadata");
+
+ String capeUrl = null;
+ if (textures.has("CAPE")) {
+ JsonNode capeTexture = textures.get("CAPE");
+ capeUrl = capeTexture.get("url").asText().replace("http://", "https://");
+ }
+
+ return new GameProfileData(skinUrl, capeUrl, isAlex);
+ } catch (Exception exception) {
+ if (GeyserConnector.getInstance().getAuthType() != AuthType.OFFLINE) {
+ GeyserConnector.getInstance().getLogger().debug("Got invalid texture data for " + profile.getName() + " " + exception.getMessage());
+ }
+ // return default skin with default cape when texture data is invalid
+ return new GameProfileData((isAlex ? SkinProvider.EMPTY_SKIN_ALEX.getTextureUrl() : SkinProvider.EMPTY_SKIN.getTextureUrl()), SkinProvider.EMPTY_CAPE.getTextureUrl(), isAlex);
+ }
+ }
+ }
}
diff --git a/connector/src/main/resources/config.yml b/connector/src/main/resources/config.yml
index 6ccaa3065..43e3e8edc 100644
--- a/connector/src/main/resources/config.yml
+++ b/connector/src/main/resources/config.yml
@@ -13,17 +13,22 @@ bedrock:
address: 0.0.0.0
# The port that will listen for connections
port: 19132
- # Some hosting services change your Java port everytime you open the server, and require the same port to be used for Bedrock.
+ # Some hosting services change your Java port everytime you start the server and require the same port to be used for Bedrock.
# This option makes the Bedrock port the same as the Java port every time you start the server.
# This option is for the plugin version only.
clone-remote-port: false
- # The MOTD that will be broadcasted to Minecraft: Bedrock Edition clients. Irrelevant if "passthrough-motd" is set to true
+ # The MOTD that will be broadcasted to Minecraft: Bedrock Edition clients. This is irrelevant if "passthrough-motd" is set to true
motd1: "GeyserMC"
motd2: "Another GeyserMC forced host."
+ # The Server Name that will be sent to Minecraft: Bedrock Edition clients. This is visible in both the pause menu and the settings menu.
+ server-name: "Geyser"
remote:
# The IP address of the remote (Java Edition) server
- address: 127.0.0.1
+ # If it is "auto", for standalone version the remote address will be set to 127.0.0.1,
+ # for plugin versions, Geyser will attempt to find the best address to connect to.
+ address: auto
# The port of the remote (Java Edition) server
+ # For plugin versions, if address has been set to "auto", the port will also follow the server's listening port.
port: 25565
# Authentication type. Can be offline, online, or floodgate (see https://github.com/GeyserMC/Geyser/wiki/Floodgate).
auth-type: online
@@ -33,26 +38,29 @@ remote:
# You can ignore this when not using Floodgate.
floodgate-key-file: public-key.pem
-## the Xbox/MCPE username is the key for the Java server auth-info
-## this allows automatic configuration/login to the remote Java server
-## if you are brave/stupid enough to put your Mojang account info into
-## a config file
+# The Xbox/Minecraft Bedrock username is the key for the Java server auth-info.
+# This allows automatic configuration/login to the remote Java server.
+# If you are brave enough to put your Mojang account info into a config file.
+# Uncomment the lines below to enable this feature.
#userAuths:
-# bluerkelp2: # MCPE/Xbox username
-# email: not_really_my_email_address_mr_minecrafter53267@gmail.com # Mojang account email address
-# password: "this isn't really my password"
+# BedrockAccountUsername: # Your Minecraft: Bedrock Edition username
+# email: javaccountemail@example.com # Your Minecraft: Java Edition email
+# password: javaccountpassword123 # Your Minecraft: Java Edition password
#
-# herpderp40300499303040503030300500293858393589:
-# email: herpderp@derpherp.com
-# password: dooooo
+# bluerkelp2:
+# email: not_really_my_email_address_mr_minecrafter53267@gmail.com
+# password: "this isn't really my password"
# 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.
command-suggestions: true
-# The following two options enable "ping passthrough" - the MOTD and/or player count gets retrieved from the Java server.
+# The following three options enable "ping passthrough" - the MOTD, player count and/or protocol name gets retrieved from the Java server.
# Relay the MOTD from the remote server to Bedrock players.
passthrough-motd: false
+# Relay the protocol name (e.g. BungeeCord [X.X], Paper 1.X) - only really useful when using a custom protocol name!
+# This will also show up on sites like MCSrvStatus.
+passthrough-protocol-name: false
# Relay the player count and max players from the remote server to Bedrock players.
passthrough-player-counts: false
# Enable LEGACY ping passthrough. There is no need to enable this unless your MOTD or player count does not appear properly.
@@ -104,6 +112,11 @@ cache-images: 0
# the end sky in the nether, but ultimately it's the only way for this feature to work.
above-bedrock-nether-building: false
+# Force clients to load all resource packs if there are any.
+# If set to false it allows the user to disconnect from the server if they don't
+# want to download the resource packs
+force-resource-packs: true
+
# bStats is a stat tracker that is entirely anonymous and tracks only basic information
# about Geyser, such as how many people are online, how many servers are using Geyser,
# what OS is being used, etc. You can learn more about bStats here: https://bstats.org/.
@@ -114,9 +127,20 @@ metrics:
# UUID of server, don't change!
uuid: generateduuid
-# ADVANCED OPTIONS - DO NOT TOUCH UNLESS YOU KNOW WHAT YOU ARE DOING!
+# ADVANCED OPTIONS - DO NOT TOUCH UNLESS YOU KNOW WHAT YOU ARE DOING!
+
+# Geyser updates the Scoreboard after every Scoreboard packet, but when Geyser tries to handle
+# a lot of scoreboard packets per second can cause serious lag.
+# This option allows you to specify after how many Scoreboard packets per seconds
+# the Scoreboard updates will be limited to four updates per second.
+scoreboard-packet-threshold: 20
+
+# Allow connections from ProxyPass and Waterdog.
+# See https://www.spigotmc.org/wiki/firewall-guide/ for assistance - use UDP instead of TCP.
+enable-proxy-connections: false
+
# The internet supports a maximum MTU of 1492 but could cause issues with packet fragmentation.
# 1400 is the default.
# mtu: 1400
-config-version: 3
+config-version: 4
diff --git a/connector/src/main/resources/languages b/connector/src/main/resources/languages
index 7cc503e2f..5f2179226 160000
--- a/connector/src/main/resources/languages
+++ b/connector/src/main/resources/languages
@@ -1 +1 @@
-Subproject commit 7cc503e2f7c0871a24beb3a114726d764a4836f1
+Subproject commit 5f21792264a364e32425014e0be79db93593da1e
diff --git a/connector/src/main/resources/mappings b/connector/src/main/resources/mappings
index 83a346876..0fae8d3f0 160000
--- a/connector/src/main/resources/mappings
+++ b/connector/src/main/resources/mappings
@@ -1 +1 @@
-Subproject commit 83a3468766b82ea97bd1ae8a1e92bffcc2745349
+Subproject commit 0fae8d3f0de6210a10435a36128db14cb7650ae6