Refactor TagCache to be extensible

Previously, for any new tag, we would have to add a field, add the line to load it, add the line to clear it, and make a method for that tag. Now, you just add an enum.
This commit is contained in:
Camotoy 2024-04-23 12:33:06 -04:00
parent c48428daf0
commit b81408820b
No known key found for this signature in database
GPG key ID: 7EEFB66FE798081F
14 changed files with 177 additions and 161 deletions

View file

@ -31,6 +31,7 @@ import org.cloudburstmc.math.vector.Vector3f;
import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.tags.ItemTag;
import org.geysermc.geyser.util.InteractionResult;
import org.geysermc.geyser.util.InteractiveTag;
@ -49,7 +50,7 @@ public class DolphinEntity extends WaterEntity {
@NonNull
@Override
protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
if (!itemInHand.isEmpty() && session.getTagCache().isFish(itemInHand)) {
if (!itemInHand.isEmpty() && session.getTagCache().is(ItemTag.FISHES, itemInHand)) {
return InteractiveTag.FEED;
}
return super.testMobInteraction(hand, itemInHand);
@ -58,7 +59,7 @@ public class DolphinEntity extends WaterEntity {
@NonNull
@Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
if (!itemInHand.isEmpty() && session.getTagCache().isFish(itemInHand)) {
if (!itemInHand.isEmpty() && session.getTagCache().is(ItemTag.FISHES, itemInHand)) {
// Feed
return InteractionResult.SUCCESS;
}

View file

@ -36,6 +36,7 @@ import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.item.type.Item;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.tags.ItemTag;
import org.geysermc.geyser.util.EntityUtils;
import org.geysermc.geyser.util.InteractionResult;
@ -61,7 +62,7 @@ public class AxolotlEntity extends AnimalEntity {
@Override
public boolean canEat(Item item) {
return session.getTagCache().isAxolotlFood(item);
return session.getTagCache().is(ItemTag.AXOLOTL_FOOD, item);
}
@Override

View file

@ -35,6 +35,7 @@ import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket;
import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.item.type.Item;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.tags.ItemTag;
import java.util.UUID;
@ -67,6 +68,6 @@ public class BeeEntity extends AnimalEntity {
@Override
public boolean canEat(Item item) {
return session.getTagCache().isFlower(item);
return session.getTagCache().is(ItemTag.FLOWERS, item);
}
}

View file

@ -33,6 +33,7 @@ import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.item.type.Item;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.tags.ItemTag;
import java.util.UUID;
@ -56,6 +57,6 @@ public class FoxEntity extends AnimalEntity {
@Override
public boolean canEat(Item item) {
return session.getTagCache().isFoxFood(item);
return session.getTagCache().is(ItemTag.FOX_FOOD, item);
}
}

View file

@ -33,8 +33,8 @@ import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.item.type.FlowerItem;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.tags.ItemTag;
import org.geysermc.geyser.util.InteractionResult;
import org.geysermc.geyser.util.InteractiveTag;
@ -77,7 +77,7 @@ public class MooshroomEntity extends AnimalEntity {
} else if (!isBaby && isAlive() && itemInHand.asItem() == Items.SHEARS) {
// Shear items
return InteractionResult.SUCCESS;
} else if (isBrown && session.getTagCache().isSmallFlower(itemInHand) && itemInHand.asItem() instanceof FlowerItem) {
} else if (isBrown && session.getTagCache().is(ItemTag.SMALL_FLOWERS, itemInHand)) {
// ?
return InteractionResult.SUCCESS;
}

View file

@ -39,6 +39,7 @@ import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.entity.type.Tickable;
import org.geysermc.geyser.item.type.Item;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.tags.ItemTag;
import java.util.UUID;
@ -72,7 +73,7 @@ public class SnifferEntity extends AnimalEntity implements Tickable {
@Override
public boolean canEat(Item item) {
return session.getTagCache().isSnifferFood(item);
return session.getTagCache().is(ItemTag.SNIFFER_FOOD, item);
}
public void setSnifferState(ObjectEntityMetadata<SnifferState> entityMetadata) {

View file

@ -35,6 +35,7 @@ import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.tags.ItemTag;
import org.geysermc.geyser.util.InteractionResult;
import org.geysermc.geyser.util.InteractiveTag;
@ -65,7 +66,7 @@ public class CreeperEntity extends MonsterEntity {
@NonNull
@Override
protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
if (session.getTagCache().isCreeperIgniter(itemInHand.asItem())) {
if (session.getTagCache().is(ItemTag.CREEPER_IGNITERS, itemInHand)) {
return InteractiveTag.IGNITE_CREEPER;
} else {
return super.testMobInteraction(hand, itemInHand);
@ -75,7 +76,7 @@ public class CreeperEntity extends MonsterEntity {
@NonNull
@Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
if (session.getTagCache().isCreeperIgniter(itemInHand.asItem())) {
if (session.getTagCache().is(ItemTag.CREEPER_IGNITERS, itemInHand)) {
// Ignite creeper - as of 1.19.3
session.playSoundEvent(SoundEvent.IGNITE, position);
return InteractionResult.SUCCESS;

View file

@ -35,6 +35,7 @@ import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.tags.ItemTag;
import org.geysermc.geyser.util.InteractionResult;
import org.geysermc.geyser.util.InteractiveTag;
@ -65,7 +66,7 @@ public class PiglinEntity extends BasePiglinEntity {
@Override
public void updateOffHand(GeyserSession session) {
// Check if the Piglin is holding Gold and set the ADMIRING flag accordingly so its pose updates
setFlag(EntityFlag.ADMIRING, session.getTagCache().shouldPiglinAdmire(session.getItemMappings().getMapping(this.offHand).getJavaItem()));
setFlag(EntityFlag.ADMIRING, session.getTagCache().is(ItemTag.PIGLIN_LOVED, session.getItemMappings().getMapping(this.offHand).getJavaItem()));
super.updateBedrockMetadata();
super.updateOffHand(session);

View file

@ -57,7 +57,7 @@ import java.util.function.ToIntFunction;
* Stores any information sent via Java registries. May not contain all data in a given registry - we'll strip what's
* unneeded.
*
* Crafted as of 1.20.5 for easy "add new registry" in the future.
* Crafted as of 1.20.5 for easy "add new registry" functionality in the future.
*/
@Accessors(fluent = true)
@Getter
@ -118,6 +118,7 @@ public final class RegistryCache {
REGISTRIES.put("minecraft:" + registry, (registryCache, entries) -> {
Int2ObjectMap<T> localCache = localCacheFunction.apply(registryCache);
// Clear each local cache every time a new registry entry is given to us
// (e.g. proxy server switches)
localCache.clear();
for (int i = 0; i < entries.size(); i++) {
RegistryEntry entry = entries.get(i);

View file

@ -27,64 +27,45 @@ package org.geysermc.geyser.session.cache;
import com.github.steveice10.mc.protocol.packet.common.clientbound.ClientboundUpdateTagsPacket;
import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.ints.IntLists;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.GeyserLogger;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.item.type.Item;
import org.geysermc.geyser.registry.type.BlockMapping;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.tags.BlockTag;
import org.geysermc.geyser.session.cache.tags.ItemTag;
import javax.annotation.ParametersAreNonnullByDefault;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
/**
* Manages information sent from the {@link ClientboundUpdateTagsPacket}. If that packet is not sent, all lists here
* will remain empty, matching Java Edition behavior.
*
* This system is designed for easy extensibility - just add an enum to {@link BlockTag} or {@link ItemTag}.
*/
@ParametersAreNonnullByDefault
public class TagCache {
/* Blocks */
private IntList leaves;
private IntList wool;
public final class TagCache {
// Put these here so the enums can load without a static map
public static final Map<String, BlockTag> ALL_BLOCK_TAGS = new HashMap<>();
public static final Map<String, ItemTag> ALL_ITEM_TAGS = new HashMap<>();
private IntList axeEffective;
private IntList hoeEffective;
private IntList pickaxeEffective;
private IntList shovelEffective;
private IntList requiresStoneTool;
private IntList requiresIronTool;
private IntList requiresDiamondTool;
/* Items */
private IntList axolotlFood;
private IntList creeperIgniters;
private IntList fishes;
private IntList flowers;
private IntList foxFood;
private IntList piglinLoved;
private IntList smallFlowers;
private IntList snifferFood;
public TagCache() {
// Ensure all lists are non-null
clear();
}
private final Map<BlockTag, IntList> blocks = new EnumMap<>(BlockTag.class);
private final Map<ItemTag, IntList> items = new EnumMap<>(ItemTag.class);
public void loadPacket(GeyserSession session, ClientboundUpdateTagsPacket packet) {
Map<String, int[]> blockTags = packet.getTags().get("minecraft:block");
this.leaves = IntList.of(blockTags.get("minecraft:leaves"));
this.wool = IntList.of(blockTags.get("minecraft:wool"));
this.axeEffective = IntList.of(blockTags.get("minecraft:mineable/axe"));
this.hoeEffective = IntList.of(blockTags.get("minecraft:mineable/hoe"));
this.pickaxeEffective = IntList.of(blockTags.get("minecraft:mineable/pickaxe"));
this.shovelEffective = IntList.of(blockTags.get("minecraft:mineable/shovel"));
this.requiresStoneTool = IntList.of(blockTags.get("minecraft:needs_stone_tool"));
this.requiresIronTool = IntList.of(blockTags.get("minecraft:needs_iron_tool"));
this.requiresDiamondTool = IntList.of(blockTags.get("minecraft:needs_diamond_tool"));
this.blocks.clear();
ALL_BLOCK_TAGS.forEach((location, tag) -> {
int[] values = blockTags.get(location);
if (values != null) {
this.blocks.put(tag, IntList.of(values));
} else {
session.getGeyser().getLogger().debug("Block tag not found from server: " + location);
}
});
// Hack btw
GeyserLogger logger = session.getGeyser().getLogger();
@ -96,14 +77,15 @@ public class TagCache {
}
Map<String, int[]> itemTags = packet.getTags().get("minecraft:item");
this.axolotlFood = IntList.of(itemTags.get("minecraft:axolotl_food"));
this.creeperIgniters = load(itemTags.get("minecraft:creeper_igniters"));
this.fishes = IntList.of(itemTags.get("minecraft:fishes"));
this.flowers = IntList.of(itemTags.get("minecraft:flowers"));
this.foxFood = IntList.of(itemTags.get("minecraft:fox_food"));
this.piglinLoved = IntList.of(itemTags.get("minecraft:piglin_loved"));
this.smallFlowers = IntList.of(itemTags.get("minecraft:small_flowers"));
this.snifferFood = load(itemTags.get("minecraft:sniffer_food"));
this.items.clear();
ALL_ITEM_TAGS.forEach((location, tag) -> {
int[] values = itemTags.get(location);
if (values != null) {
this.items.put(tag, IntList.of(values));
} else {
session.getGeyser().getLogger().debug("Item tag not found from server: " + location);
}
});
// Hack btw
boolean emulatePost1_13Logic = itemTags.get("minecraft:signs").length > 1;
@ -113,98 +95,32 @@ public class TagCache {
}
}
private IntList load(int @Nullable[] tags) {
if (tags == null) {
return IntLists.EMPTY_LIST;
/**
* @return true if the block tag is present and contains this block mapping's Java ID.
*/
public boolean is(BlockTag tag, BlockMapping mapping) {
IntList values = this.blocks.get(tag);
if (values != null) {
return values.contains(mapping.getJavaBlockId());
}
return IntList.of(tags);
return false;
}
public void clear() {
this.leaves = IntLists.emptyList();
this.wool = IntLists.emptyList();
this.axeEffective = IntLists.emptyList();
this.hoeEffective = IntLists.emptyList();
this.pickaxeEffective = IntLists.emptyList();
this.shovelEffective = IntLists.emptyList();
this.requiresStoneTool = IntLists.emptyList();
this.requiresIronTool = IntLists.emptyList();
this.requiresDiamondTool = IntLists.emptyList();
this.axolotlFood = IntLists.emptyList();
this.creeperIgniters = IntLists.emptyList();
this.fishes = IntLists.emptyList();
this.flowers = IntLists.emptyList();
this.foxFood = IntLists.emptyList();
this.piglinLoved = IntLists.emptyList();
this.smallFlowers = IntLists.emptyList();
this.snifferFood = IntLists.emptyList();
/**
* @return true if the item tag is present and contains this item stack's Java ID.
*/
public boolean is(ItemTag tag, GeyserItemStack itemStack) {
return is(tag, itemStack.asItem());
}
public boolean isAxolotlFood(Item item) {
return axolotlFood.contains(item.javaId());
}
public boolean isCreeperIgniter(Item item) {
return creeperIgniters.contains(item.javaId());
}
public boolean isFish(GeyserItemStack itemStack) {
return fishes.contains(itemStack.getJavaId());
}
public boolean isFlower(Item item) {
return flowers.contains(item.javaId());
}
public boolean isFoxFood(Item item) {
return foxFood.contains(item.javaId());
}
public boolean shouldPiglinAdmire(Item item) {
return piglinLoved.contains(item.javaId());
}
public boolean isSmallFlower(GeyserItemStack itemStack) {
return smallFlowers.contains(itemStack.getJavaId());
}
public boolean isSnifferFood(Item item) {
return snifferFood.contains(item.javaId());
}
public boolean isAxeEffective(BlockMapping blockMapping) {
return axeEffective.contains(blockMapping.getJavaBlockId());
}
public boolean isHoeEffective(BlockMapping blockMapping) {
return hoeEffective.contains(blockMapping.getJavaBlockId());
}
public boolean isPickaxeEffective(BlockMapping blockMapping) {
return pickaxeEffective.contains(blockMapping.getJavaBlockId());
}
public boolean isShovelEffective(BlockMapping blockMapping) {
return shovelEffective.contains(blockMapping.getJavaBlockId());
}
public boolean isShearsEffective(BlockMapping blockMapping) {
int javaBlockId = blockMapping.getJavaBlockId();
return leaves.contains(javaBlockId) || wool.contains(javaBlockId);
}
public boolean requiresStoneTool(BlockMapping blockMapping) {
return requiresStoneTool.contains(blockMapping.getJavaBlockId());
}
public boolean requiresIronTool(BlockMapping blockMapping) {
return requiresIronTool.contains(blockMapping.getJavaBlockId());
}
public boolean requiresDiamondTool(BlockMapping blockMapping) {
return requiresDiamondTool.contains(blockMapping.getJavaBlockId());
/**
* @return true if the item tag is present and contains this item's Java ID.
*/
public boolean is(ItemTag tag, Item item) {
IntList values = this.items.get(tag);
if (values != null) {
return values.contains(item.javaId());
}
return false;
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2019-2024 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.geyser.session.cache.tags;
import org.geysermc.geyser.session.cache.TagCache;
public enum BlockTag {
LEAVES("leaves"),
WOOL("wool"),
AXE_EFFECTIVE("mineable/axe"),
HOE_EFFECTIVE("mineable/hoe"),
PICKAXE_EFFECTIVE("mineable/pickaxe"),
SHOVEL_EFFECTIVE("mineable/shovel"),
NEEDS_STONE_TOOL("needs_stone_tool"),
NEEDS_IRON_TOOL("needs_iron_tool"),
NEEDS_DIAMOND_TOOL("needs_diamond_tool");
BlockTag(String identifier) {
register(identifier, this);
}
private static void register(String name, BlockTag tag) {
TagCache.ALL_BLOCK_TAGS.put("minecraft:" + name, tag);
}
}

View file

@ -0,0 +1,47 @@
/*
* Copyright (c) 2019-2024 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.geyser.session.cache.tags;
import org.geysermc.geyser.session.cache.TagCache;
public enum ItemTag {
AXOLOTL_FOOD("axolotl_food"),
CREEPER_IGNITERS("creeper_igniters"),
FISHES("fishes"),
FLOWERS("flowers"),
FOX_FOOD("fox_food"),
PIGLIN_LOVED("piglin_loved"),
SMALL_FLOWERS("small_flowers"),
SNIFFER_FOOD("sniffer_food");
ItemTag(String identifier) {
register(identifier, this);
}
private static void register(String name, ItemTag tag) {
TagCache.ALL_ITEM_TAGS.put("minecraft:" + name, tag);
}
}

View file

@ -47,10 +47,6 @@ public class BiomeTranslator {
public static int loadServerBiome(RegistryEntry entry) {
String javaIdentifier = entry.getId();
return Registries.BIOME_IDENTIFIERS.get().getOrDefault(javaIdentifier, 0);
// if (javaId == 0) {
// // Matches Java behavior when it sees an invalid biome - it just replaces it with ID 0
// biomeTranslations.defaultReturnValue(bedrockId);
// }
}
public static BlockStorage toNewBedrockBiome(GeyserSession session, DataPalette biomeData) {

View file

@ -36,17 +36,18 @@ import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.type.BlockMapping;
import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.tags.BlockTag;
import org.geysermc.geyser.translator.collision.BlockCollision;
public final class BlockUtils {
private static boolean correctTool(GeyserSession session, BlockMapping blockMapping, String itemToolType) {
return switch (itemToolType) {
case "axe" -> session.getTagCache().isAxeEffective(blockMapping);
case "hoe" -> session.getTagCache().isHoeEffective(blockMapping);
case "pickaxe" -> session.getTagCache().isPickaxeEffective(blockMapping);
case "shears" -> session.getTagCache().isShearsEffective(blockMapping);
case "shovel" -> session.getTagCache().isShovelEffective(blockMapping);
case "axe" -> session.getTagCache().is(BlockTag.AXE_EFFECTIVE, blockMapping);
case "hoe" -> session.getTagCache().is(BlockTag.HOE_EFFECTIVE, blockMapping);
case "pickaxe" -> session.getTagCache().is(BlockTag.PICKAXE_EFFECTIVE, blockMapping);
case "shears" -> session.getTagCache().is(BlockTag.LEAVES, blockMapping) || session.getTagCache().is(BlockTag.WOOL, blockMapping);
case "shovel" -> session.getTagCache().is(BlockTag.SHOVEL_EFFECTIVE, blockMapping);
case "sword" -> blockMapping.getJavaBlockId() == BlockStateValues.JAVA_COBWEB_ID;
default -> {
session.getGeyser().getLogger().warning("Unknown tool type: " + itemToolType);
@ -79,15 +80,15 @@ public final class BlockUtils {
switch (toolTier) {
// Use intentional fall-throughs to check each tier with this block
default:
if (session.getTagCache().requiresStoneTool(blockMapping)) {
if (session.getTagCache().is(BlockTag.NEEDS_STONE_TOOL, blockMapping)) {
return false;
}
case "stone":
if (session.getTagCache().requiresIronTool(blockMapping)) {
if (session.getTagCache().is(BlockTag.NEEDS_IRON_TOOL, blockMapping)) {
return false;
}
case "iron":
if (session.getTagCache().requiresDiamondTool(blockMapping)) {
if (session.getTagCache().is(BlockTag.NEEDS_DIAMOND_TOOL, blockMapping)) {
return false;
}
}
@ -131,7 +132,7 @@ public final class BlockUtils {
}
public static double getBreakTime(GeyserSession session, BlockMapping blockMapping, ItemMapping item, @Nullable DataComponents components, boolean isSessionPlayer) {
boolean isShearsEffective = session.getTagCache().isShearsEffective(blockMapping); //TODO called twice
boolean isShearsEffective = session.getTagCache().is(BlockTag.LEAVES, blockMapping) || session.getTagCache().is(BlockTag.WOOL, blockMapping); //TODO called twice
boolean canHarvestWithHand = blockMapping.isCanBreakWithHand();
String toolType = "";
String toolTier = "";