From 93f5298ee313a807ee06a58d9ea9bf8a61fb19d9 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Sat, 4 Sep 2021 14:55:22 -0400 Subject: [PATCH] Introduce specialized maps for block state operations By introducing fixed maps, we are able to reduce the overhead of needing to store arbitrary numbers; fixed maps start at one number and must be sequential. This commit also reduces some overhead of lecterns in block updating as we are able to utilize our own maps to reduce object boxing and removing the function of Map#compute of trying to re-insert a value. --- .../network/session/GeyserSession.java | 14 +- .../java/world/JavaUnloadChunkTranslator.java | 14 +- .../world/block/BlockStateValues.java | 25 ++- .../connector/registry/BlockRegistries.java | 2 +- .../geysermc/connector/utils/ChunkUtils.java | 26 +-- .../connector/utils/DimensionUtils.java | 4 +- .../collections/FixedInt2BooleanMap.java | 114 +++++++++++ .../utils/collections/FixedInt2ByteMap.java | 115 +++++++++++ .../utils/collections/FixedInt2IntMap.java | 115 +++++++++++ .../utils/collections/LecternHasBookMap.java | 73 +++++++ .../{ => collections}/Object2IntBiMap.java | 2 +- .../utils/collections/package-info.java | 34 ++++ .../collections/GeyserCollectionsTest.java | 179 ++++++++++++++++++ 13 files changed, 667 insertions(+), 50 deletions(-) create mode 100644 connector/src/main/java/org/geysermc/connector/utils/collections/FixedInt2BooleanMap.java create mode 100644 connector/src/main/java/org/geysermc/connector/utils/collections/FixedInt2ByteMap.java create mode 100644 connector/src/main/java/org/geysermc/connector/utils/collections/FixedInt2IntMap.java create mode 100644 connector/src/main/java/org/geysermc/connector/utils/collections/LecternHasBookMap.java rename connector/src/main/java/org/geysermc/connector/utils/{ => collections}/Object2IntBiMap.java (99%) create mode 100644 connector/src/main/java/org/geysermc/connector/utils/collections/package-info.java create mode 100644 connector/src/test/java/org/geysermc/connector/utils/collections/GeyserCollectionsTest.java 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)); + } + } +}