diff --git a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java index cfc877716..df152da6a 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java @@ -214,7 +214,7 @@ public class GeyserSession implements CommandSender { * See {@link org.geysermc.connector.network.translators.world.WorldManager#getLecternDataAt(GeyserSession, int, int, int, boolean)} * for more information. */ - private final Set lecternCache = new ObjectOpenHashSet<>(); + private final Set lecternCache; @Setter private boolean droppingLecternBook; @@ -474,6 +474,13 @@ public class GeyserSession implements CommandSender { this.spawned = false; this.loggedIn = false; + if (connector.getWorldManager().shouldExpectLecternHandled()) { + // Unneeded on these platforms + this.lecternCache = null; + } else { + this.lecternCache = new ObjectOpenHashSet<>(); + } + if (connector.getConfig().getEmoteOffhandWorkaround() != EmoteOffhandWorkaroundOption.NO_EMOTES) { this.emotes = new HashSet<>(); // Make a copy to prevent ConcurrentModificationException @@ -603,12 +610,15 @@ public class GeyserSession implements CommandSender { connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.login.invalid", username)); disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.invalid.kick", getClientData().getLanguageCode())); } catch (RequestException ex) { - ex.printStackTrace(); disconnect(ex.getMessage()); } return null; }).whenComplete((aVoid, ex) -> { + if (ex != null) { + disconnect(ex.toString()); + } if (this.closed) { + connector.getLogger().error("", ex); // Client disconnected during the authentication attempt return; } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaUnloadChunkTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaUnloadChunkTranslator.java index 3d428327a..d651eceae 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaUnloadChunkTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaUnloadChunkTranslator.java @@ -50,12 +50,14 @@ public class JavaUnloadChunkTranslator extends PacketTranslator> 4) == packet.getX() && (position.getZ() >> 4) == packet.getZ()) { - iterator.remove(); + if (!session.getConnector().getWorldManager().shouldExpectLecternHandled()) { + // Do the same thing with lecterns + iterator = session.getLecternCache().iterator(); + while (iterator.hasNext()) { + Vector3i position = iterator.next(); + if ((position.getX() >> 4) == packet.getX() && (position.getZ() >> 4) == packet.getZ()) { + iterator.remove(); + } } } } 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 2c297613f..fe60bd4de 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 @@ -33,31 +33,31 @@ import org.geysermc.connector.registry.BlockRegistries; import org.geysermc.connector.registry.type.BlockMapping; import org.geysermc.connector.utils.Direction; import org.geysermc.connector.utils.PistonBehavior; - -import java.util.Map; -import java.util.function.BiFunction; +import org.geysermc.connector.utils.collections.FixedInt2ByteMap; +import org.geysermc.connector.utils.collections.FixedInt2IntMap; +import org.geysermc.connector.utils.collections.LecternHasBookMap; /** * Used for block entities if the Java block state contains Bedrock block information. */ public class BlockStateValues { - private static final Int2IntMap BANNER_COLORS = new Int2IntOpenHashMap(); - private static final Int2ByteMap BED_COLORS = new Int2ByteOpenHashMap(); + private static final Int2IntMap BANNER_COLORS = new FixedInt2IntMap(); + private static final Int2ByteMap BED_COLORS = new FixedInt2ByteMap(); 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 Int2BooleanMap LECTERN_BOOK_STATES = new Int2BooleanOpenHashMap(); - private static final Int2IntMap NOTEBLOCK_PITCHES = new Int2IntOpenHashMap(); + private static final LecternHasBookMap LECTERN_BOOK_STATES = new LecternHasBookMap(); + private static final Int2IntMap NOTEBLOCK_PITCHES = new FixedInt2IntMap(); private static final Int2BooleanMap PISTON_VALUES = new Int2BooleanOpenHashMap(); private static final Int2BooleanMap IS_STICKY_PISTON = new Int2BooleanOpenHashMap(); private static final Object2IntMap PISTON_HEADS = new Object2IntOpenHashMap<>(); private static final Int2ObjectMap PISTON_ORIENTATION = new Int2ObjectOpenHashMap<>(); private static final IntSet ALL_PISTON_HEADS = new IntOpenHashSet(); private static final IntSet MOVING_PISTONS = new IntOpenHashSet(); - private static final Int2ByteMap SKULL_VARIANTS = new Int2ByteOpenHashMap(); + private static final Int2ByteMap SKULL_VARIANTS = new FixedInt2ByteMap(); private static final Int2ByteMap SKULL_ROTATIONS = new Int2ByteOpenHashMap(); private static final Int2IntMap SKULL_WALL_DIRECTIONS = new Int2IntOpenHashMap(); - private static final Int2ByteMap SHULKERBOX_DIRECTIONS = new Int2ByteOpenHashMap(); + private static final Int2ByteMap SHULKERBOX_DIRECTIONS = new FixedInt2ByteMap(); private static final Int2IntMap WATER_LEVEL = new Int2IntOpenHashMap(); public static final int JAVA_AIR_ID = 0; @@ -237,12 +237,9 @@ public class BlockStateValues { } /** - * This returns a Map interface so IntelliJ doesn't complain about {@link Int2BooleanMap#compute(int, BiFunction)} - * not returning null. - * * @return the lectern book state map pointing to book present state */ - public static Map getLecternBookStates() { + public static LecternHasBookMap getLecternBookStates() { return LECTERN_BOOK_STATES; } @@ -407,7 +404,7 @@ public class BlockStateValues { * @return The block's slipperiness */ public static float getSlipperiness(int state) { - String blockIdentifier = BlockRegistries.JAVA_BLOCKS.get(state).getJavaIdentifier(); + String blockIdentifier = BlockRegistries.JAVA_BLOCKS.getOrDefault(state, BlockMapping.AIR).getJavaIdentifier(); switch (blockIdentifier) { case "minecraft:slime_block": return 0.8f; diff --git a/connector/src/main/java/org/geysermc/connector/registry/BlockRegistries.java b/connector/src/main/java/org/geysermc/connector/registry/BlockRegistries.java index 275353300..191b45952 100644 --- a/connector/src/main/java/org/geysermc/connector/registry/BlockRegistries.java +++ b/connector/src/main/java/org/geysermc/connector/registry/BlockRegistries.java @@ -33,7 +33,7 @@ import org.geysermc.connector.registry.loader.RegistryLoaders; import org.geysermc.connector.registry.populator.BlockRegistryPopulator; import org.geysermc.connector.registry.type.BlockMapping; import org.geysermc.connector.registry.type.BlockMappings; -import org.geysermc.connector.utils.Object2IntBiMap; +import org.geysermc.connector.utils.collections.Object2IntBiMap; /** * Holds all the block registries in Geyser. 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 993555a10..854c367ec 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java @@ -51,7 +51,6 @@ import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.entity.ItemFrameEntity; import org.geysermc.connector.entity.player.SkullPlayerEntity; import org.geysermc.connector.network.session.GeyserSession; -import org.geysermc.connector.network.translators.inventory.translators.LecternInventoryTranslator; import org.geysermc.connector.network.translators.world.block.BlockStateValues; import org.geysermc.connector.network.translators.world.block.entity.BedrockOnlyBlockEntity; import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator; @@ -391,30 +390,7 @@ public class ChunkUtils { session.sendUpstreamPacket(waterPacket); } - BlockStateValues.getLecternBookStates().compute(blockState, (key, newLecternHasBook) -> { - // Determine if this block is a lectern - if (newLecternHasBook != null) { - boolean lecternCachedHasBook = session.getLecternCache().contains(position); - if (!session.getConnector().getWorldManager().shouldExpectLecternHandled() && lecternCachedHasBook != newLecternHasBook) { - // Refresh the block entirely - it either has a book or no longer has a book - NbtMap newLecternTag; - if (newLecternHasBook) { - newLecternTag = session.getConnector().getWorldManager().getLecternDataAt(session, position.getX(), position.getY(), position.getZ(), false); - } else { - session.getLecternCache().remove(position); - newLecternTag = LecternInventoryTranslator.getBaseLecternTag(position.getX(), position.getY(), position.getZ(), 0).build(); - } - BlockEntityUtils.updateBlockEntity(session, newLecternTag, position); - } else { - // As of right now, no tag can be added asynchronously - session.getConnector().getWorldManager().getLecternDataAt(session, position.getX(), position.getY(), position.getZ(), false); - } - } else { - // Lectern has been destroyed, if it existed - session.getLecternCache().remove(position); - } - return newLecternHasBook; - }); + BlockStateValues.getLecternBookStates().handleBlockChange(session, blockState, position); // Iterates through all Bedrock-only block entity translators and determines if a manual block entity packet // needs to be sent 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 0e4de64e4..886bb416f 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java @@ -63,7 +63,9 @@ public class DimensionUtils { session.getChunkCache().clear(); session.getEntityCache().removeAllEntities(); session.getItemFrameCache().clear(); - session.getLecternCache().clear(); + if (session.getLecternCache() != null) { + session.getLecternCache().clear(); + } session.getLodestoneCache().clear(); session.getPistonCache().clear(); session.getSkullCache().clear(); diff --git a/connector/src/main/java/org/geysermc/connector/utils/collections/FixedInt2BooleanMap.java b/connector/src/main/java/org/geysermc/connector/utils/collections/FixedInt2BooleanMap.java new file mode 100644 index 000000000..0e31e4be6 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/utils/collections/FixedInt2BooleanMap.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2019-2021 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.connector.utils.collections; + +import it.unimi.dsi.fastutil.ints.AbstractInt2BooleanMap; +import it.unimi.dsi.fastutil.objects.ObjectSet; + +public class FixedInt2BooleanMap extends AbstractInt2BooleanMap { + protected boolean[] value; + protected int start = -1; + + @Override + public int size() { + return value.length; + } + + @Override + public ObjectSet int2BooleanEntrySet() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean get(int i) { + return getOrDefault(i, defRetValue); + } + + @Override + public boolean getOrDefault(int key, boolean defaultValue) { + int offset = key - start; + if (offset < 0 || offset >= value.length) { + return defaultValue; + } + + return value[offset]; + } + + @Override + public boolean put(int key, boolean value) { + if (start == -1) { + start = key; + this.value = new boolean[] {value}; + } else { + int offset = key - start; + if (offset >= 0 && offset < this.value.length) { + boolean curr = this.value[offset]; + this.value[offset] = value; + return curr; + } else if (offset != this.value.length) { + throw new IndexOutOfBoundsException("Expected: " + (this.value.length + start) + ", got " + key); + } + + boolean[] newValue = new boolean[offset + 1]; + System.arraycopy(this.value, 0, newValue, 0, this.value.length); + this.value = newValue; + this.value[offset] = value; + } + + return this.defRetValue; + } + + @Override + public boolean containsKey(int k) { + int offset = k - start; + return offset >= 0 && offset < value.length; + } + + @Override + public boolean containsValue(boolean v) { + for (boolean b : value) { + if (b == v) { + return true; + } + } + + return false; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("{"); + int index = start; + for (boolean b : value) { + builder.append(index++).append("=>").append(b); + if (index < value.length + start) { + // Add commas while there are still more entries in the list + builder.append(", "); + } + } + return builder.append('}').toString(); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/utils/collections/FixedInt2ByteMap.java b/connector/src/main/java/org/geysermc/connector/utils/collections/FixedInt2ByteMap.java new file mode 100644 index 000000000..5aa4bc528 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/utils/collections/FixedInt2ByteMap.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2019-2021 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.connector.utils.collections; + +import it.unimi.dsi.fastutil.ints.AbstractInt2ByteMap; +import it.unimi.dsi.fastutil.ints.Int2ByteMap; +import it.unimi.dsi.fastutil.objects.ObjectSet; + +public class FixedInt2ByteMap extends AbstractInt2ByteMap { + protected byte[] value; + protected int start = -1; + + @Override + public int size() { + return value.length; + } + + @Override + public ObjectSet int2ByteEntrySet() { + throw new UnsupportedOperationException(); + } + + @Override + public byte get(int i) { + return getOrDefault(i, defRetValue); + } + + @Override + public byte getOrDefault(int key, byte defaultValue) { + int offset = key - start; + if (offset < 0 || offset >= value.length) { + return defaultValue; + } + + return value[offset]; + } + + @Override + public byte put(int key, byte value) { + if (start == -1) { + start = key; + this.value = new byte[] {value}; + } else { + int offset = key - start; + if (offset >= 0 && offset < this.value.length) { + byte curr = this.value[offset]; + this.value[offset] = value; + return curr; + } else if (offset != this.value.length) { + throw new IndexOutOfBoundsException("Expected: " + (this.value.length + start) + ", got " + key); + } + + byte[] newValue = new byte[offset + 1]; + System.arraycopy(this.value, 0, newValue, 0, this.value.length); + this.value = newValue; + this.value[offset] = value; + } + + return this.defRetValue; + } + + @Override + public boolean containsKey(int k) { + int offset = k - start; + return offset >= 0 && offset < value.length; + } + + @Override + public boolean containsValue(byte v) { + for (byte i : value) { + if (i == v) { + return true; + } + } + + return false; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("{"); + int index = start; + for (byte b : value) { + builder.append(index++).append("=>").append(b); + if (index < value.length + start) { + // Add commas while there are still more entries in the list + builder.append(", "); + } + } + return builder.append('}').toString(); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/utils/collections/FixedInt2IntMap.java b/connector/src/main/java/org/geysermc/connector/utils/collections/FixedInt2IntMap.java new file mode 100644 index 000000000..77e6740bc --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/utils/collections/FixedInt2IntMap.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2019-2021 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.connector.utils.collections; + +import it.unimi.dsi.fastutil.ints.AbstractInt2IntMap; +import it.unimi.dsi.fastutil.ints.Int2IntMap; +import it.unimi.dsi.fastutil.objects.ObjectSet; + +public class FixedInt2IntMap extends AbstractInt2IntMap { + protected int[] value; + protected int start = -1; + + @Override + public int size() { + return value.length; + } + + @Override + public ObjectSet int2IntEntrySet() { + throw new UnsupportedOperationException(); + } + + @Override + public int get(int i) { + return getOrDefault(i, defRetValue); + } + + @Override + public int getOrDefault(int key, int defaultValue) { + int offset = key - start; + if (offset < 0 || offset >= value.length) { + return defaultValue; + } + + return value[offset]; + } + + @Override + public int put(int key, int value) { + if (start == -1) { + start = key; + this.value = new int[] {value}; + } else { + int offset = key - start; + if (offset >= 0 && offset < this.value.length) { + int curr = this.value[offset]; + this.value[offset] = value; + return curr; + } else if (offset != this.value.length) { + throw new IndexOutOfBoundsException("Expected: " + (this.value.length + start) + ", got " + key); + } + + int[] newValue = new int[offset + 1]; + System.arraycopy(this.value, 0, newValue, 0, this.value.length); + this.value = newValue; + this.value[offset] = value; + } + + return this.defRetValue; + } + + @Override + public boolean containsKey(int k) { + int offset = k - start; + return offset >= 0 && offset < value.length; + } + + @Override + public boolean containsValue(int v) { + for (int i : value) { + if (i == v) { + return true; + } + } + + return false; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("{"); + int index = start; + for (int i : value) { + builder.append(index++).append("=>").append(i); + if (index < value.length + start) { + // Add commas while there are still more entries in the list + builder.append(", "); + } + } + return builder.append('}').toString(); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/utils/collections/LecternHasBookMap.java b/connector/src/main/java/org/geysermc/connector/utils/collections/LecternHasBookMap.java new file mode 100644 index 000000000..3252963e4 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/utils/collections/LecternHasBookMap.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2019-2021 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.connector.utils.collections; + +import com.nukkitx.math.vector.Vector3i; +import com.nukkitx.nbt.NbtMap; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.inventory.translators.LecternInventoryTranslator; +import org.geysermc.connector.network.translators.world.WorldManager; +import org.geysermc.connector.utils.BlockEntityUtils; + +/** + * Map that takes advantage of its internals for fast operations on block states to determine if they are lecterns. + */ +public class LecternHasBookMap extends FixedInt2BooleanMap { + + /** + * Update a potential lectern within the world. This is a map method so it can use the internal fields to + * optimize lectern determining. + */ + public void handleBlockChange(GeyserSession session, int blockState, Vector3i position) { + WorldManager worldManager = session.getConnector().getWorldManager(); + + int offset = blockState - this.start; + if (offset < 0 || offset > this.value.length) { + // Block state is out of bounds of this map - lectern has been destroyed, if it existed + if (!worldManager.shouldExpectLecternHandled()) { + session.getLecternCache().remove(position); + } + return; + } + + boolean newLecternHasBook; + if (worldManager.shouldExpectLecternHandled()) { + // As of right now, no tag can be added asynchronously + worldManager.getLecternDataAt(session, position.getX(), position.getY(), position.getZ(), false); + } else if ((newLecternHasBook = this.value[offset]) != this.get(worldManager.getBlockAt(session, position))) { + // If the lectern block was updated, or it previously had a book + NbtMap newLecternTag; + // newLecternHasBook = the new lectern block state's "has book" toggle. + if (newLecternHasBook) { + newLecternTag = worldManager.getLecternDataAt(session, position.getX(), position.getY(), position.getZ(), false); + } else { + session.getLecternCache().remove(position); + newLecternTag = LecternInventoryTranslator.getBaseLecternTag(position.getX(), position.getY(), position.getZ(), 0).build(); + } + BlockEntityUtils.updateBlockEntity(session, newLecternTag, position); + } + } +} diff --git a/connector/src/main/java/org/geysermc/connector/utils/Object2IntBiMap.java b/connector/src/main/java/org/geysermc/connector/utils/collections/Object2IntBiMap.java similarity index 99% rename from connector/src/main/java/org/geysermc/connector/utils/Object2IntBiMap.java rename to connector/src/main/java/org/geysermc/connector/utils/collections/Object2IntBiMap.java index 27277923b..29364437c 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/Object2IntBiMap.java +++ b/connector/src/main/java/org/geysermc/connector/utils/collections/Object2IntBiMap.java @@ -23,7 +23,7 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.connector.utils; +package org.geysermc.connector.utils.collections; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; diff --git a/connector/src/main/java/org/geysermc/connector/utils/collections/package-info.java b/connector/src/main/java/org/geysermc/connector/utils/collections/package-info.java new file mode 100644 index 000000000..3400f3f2a --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/utils/collections/package-info.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2019-2021 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +/** + * Contains useful collections for use in Geyser. + * + * Of note are the fixed int maps. Designed for use with block states that are positive and sequential, they do not allow keys to be + * added that are not greater by one versus the previous key. Because of this, speedy operations of {@link java.util.Map#get(java.lang.Object)} + * and {@link java.util.Map#containsKey(java.lang.Object)} can be performed by simply checking the bounds of the map + * size and its "start" integer. + */ +package org.geysermc.connector.utils.collections; \ No newline at end of file diff --git a/connector/src/test/java/org/geysermc/connector/utils/collections/GeyserCollectionsTest.java b/connector/src/test/java/org/geysermc/connector/utils/collections/GeyserCollectionsTest.java new file mode 100644 index 000000000..acd8bcbd0 --- /dev/null +++ b/connector/src/test/java/org/geysermc/connector/utils/collections/GeyserCollectionsTest.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2019-2021 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.connector.utils.collections; + +import org.junit.Assert; +import org.junit.Test; + +public class GeyserCollectionsTest { + private final byte[] bytes = new byte[] {(byte) 5, (byte) 4, (byte) 3, (byte) 2, (byte) 2, (byte) 1}; + private final boolean[] booleans = new boolean[] {true, false, false, true}; + private final int[] ints = new int[] {76, 3006, 999, 2323, 888, 0, 111, 999}; + + private final int[] startBlockRanges = new int[] {0, 70, 600, 450, 787, 1980}; + + @Test + public void testBytes() { + for (int startRange : startBlockRanges) { + testBytes(startRange, new FixedInt2ByteMap()); + } + } + + private void testBytes(final int start, final FixedInt2ByteMap map) { + int index = start; + for (byte b : bytes) { + map.put(index++, b); + } + + int lastKey = index; + + // Easy, understandable out-of-bounds checks + Assert.assertFalse("Map contains key bigger by one!", map.containsKey(lastKey)); + Assert.assertTrue("Map doesn't contain final key!", map.containsKey(lastKey - 1)); + + // Ensure the first and last values do not throw an exception on get, and test getOrDefault + map.get(start - 1); + map.get(lastKey); + Assert.assertEquals(map.getOrDefault(start - 1, Byte.MAX_VALUE), Byte.MAX_VALUE); + Assert.assertEquals(map.getOrDefault(lastKey, Byte.MAX_VALUE), Byte.MAX_VALUE); + Assert.assertEquals(map.getOrDefault(lastKey, Byte.MIN_VALUE), Byte.MIN_VALUE); + + Assert.assertEquals(map.size(), bytes.length); + + for (int i = start; i < bytes.length; i++) { + Assert.assertTrue(map.containsKey(i)); + Assert.assertEquals(map.get(i), bytes[i - start]); + } + + for (int i = start - 1; i >= (start - 6); i--) { + // Lower than expected check + Assert.assertFalse(i + " is in a map that starts with " + start, map.containsKey(i)); + } + + for (int i = bytes.length + start; i < bytes.length + 5 + start; i++) { + // Higher than expected check + Assert.assertFalse(i + " is in a map that ends with " + (start + bytes.length), map.containsKey(i)); + } + + for (byte b : bytes) { + Assert.assertTrue(map.containsValue(b)); + } + } + + @Test + public void testBooleans() { + for (int startRange : startBlockRanges) { + testBooleans(startRange, new FixedInt2BooleanMap()); + } + } + + private void testBooleans(final int start, final FixedInt2BooleanMap map) { + int index = start; + for (boolean b : booleans) { + map.put(index++, b); + } + + int lastKey = index; + + // Easy, understandable out-of-bounds checks + Assert.assertFalse("Map contains key bigger by one!", map.containsKey(lastKey)); + Assert.assertTrue("Map doesn't contain final key!", map.containsKey(lastKey - 1)); + + // Ensure the first and last values do not throw an exception on get + map.get(start - 1); + map.get(lastKey); + Assert.assertTrue(map.getOrDefault(lastKey, true)); + + Assert.assertEquals(map.size(), booleans.length); + + for (int i = start; i < booleans.length; i++) { + Assert.assertTrue(map.containsKey(i)); + Assert.assertEquals(map.get(i), booleans[i - start]); + } + + for (int i = start - 1; i >= (start - 6); i--) { + // Lower than expected check + Assert.assertFalse(i + " is in a map that starts with " + start, map.containsKey(i)); + } + + for (int i = booleans.length + start; i < booleans.length + start + 5; i++) { + // Higher than expected check + Assert.assertFalse(i + " is in a map that ends with " + (start + booleans.length), map.containsKey(i)); + } + + for (boolean b : booleans) { + Assert.assertTrue(map.containsValue(b)); + } + } + + @Test + public void testInts() { + for (int startRange : startBlockRanges) { + testInts(startRange, new FixedInt2IntMap()); + } + } + + private void testInts(final int start, final FixedInt2IntMap map) { + int index = start; + for (int i : ints) { + map.put(index++, i); + } + + int lastKey = index; + + // Easy, understandable out-of-bounds checks + Assert.assertFalse("Map contains key bigger by one!", map.containsKey(lastKey)); + Assert.assertTrue("Map doesn't contain final key!", map.containsKey(lastKey - 1)); + + // Ensure the first and last values do not throw an exception on get, and test getOrDefault + map.get(start - 1); + map.get(lastKey); + Assert.assertEquals(map.getOrDefault(start - 1, Integer.MAX_VALUE), Integer.MAX_VALUE); + Assert.assertEquals(map.getOrDefault(lastKey, Integer.MAX_VALUE), Integer.MAX_VALUE); + Assert.assertEquals(map.getOrDefault(lastKey, Integer.MIN_VALUE), Integer.MIN_VALUE); + + Assert.assertEquals(map.size(), ints.length); + + for (int i = start; i < ints.length; i++) { + Assert.assertTrue(map.containsKey(i)); + Assert.assertEquals(map.get(i), ints[i - start]); + } + + for (int i = start - 1; i >= (start - 6); i--) { + // Lower than expected check + Assert.assertFalse(i + " is in a map that starts with " + start, map.containsKey(i)); + } + + for (int i = ints.length + start; i < ints.length + 5 + start; i++) { + // Higher than expected check + Assert.assertFalse(i + " is in a map that ends with " + (start + ints.length), map.containsKey(i)); + } + + for (int i : ints) { + Assert.assertTrue(map.containsValue(i)); + } + } +}