From d99cb468f16772ee278da2994a838b2da5f806bb Mon Sep 17 00:00:00 2001 From: davchoo <4722249+davchoo@users.noreply.github.com> Date: Mon, 30 Jan 2023 15:01:49 -0500 Subject: [PATCH] Rework MappingsReader_v1 to avoid passing maps around --- .../mappings/util/BlockPropertyTypeMaps.java | 13 - .../mappings/util/CustomBlockMapping.java | 2 +- .../mappings/versions/MappingsReader_v1.java | 447 ++++++------------ 3 files changed, 143 insertions(+), 319 deletions(-) delete mode 100644 core/src/main/java/org/geysermc/geyser/registry/mappings/util/BlockPropertyTypeMaps.java diff --git a/core/src/main/java/org/geysermc/geyser/registry/mappings/util/BlockPropertyTypeMaps.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/util/BlockPropertyTypeMaps.java deleted file mode 100644 index 0157cc47c..000000000 --- a/core/src/main/java/org/geysermc/geyser/registry/mappings/util/BlockPropertyTypeMaps.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.geysermc.geyser.registry.mappings.util; - -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; - -import org.checkerframework.checker.nullness.qual.NonNull; - -public record BlockPropertyTypeMaps( - @NonNull Map> stringValuesMap, @NonNull Map> stateKeyStrings, - @NonNull Map> intValuesMap, @NonNull Map> stateKeyInts, - @NonNull Set booleanValuesSet, @NonNull Map> stateKeyBools) { -} diff --git a/core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockMapping.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockMapping.java index a4a525317..259189826 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockMapping.java +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockMapping.java @@ -36,5 +36,5 @@ import org.geysermc.geyser.api.block.custom.CustomBlockState; * data required to register a custom block that overrides a group of java block * states. */ -public record CustomBlockMapping(@NonNull CustomBlockData data, @NonNull Map states, @NonNull String javaIdentifier, @NonNull boolean overrideItem) { +public record CustomBlockMapping(@NonNull CustomBlockData data, @NonNull Map states, @NonNull String javaIdentifier, boolean overrideItem) { } diff --git a/core/src/main/java/org/geysermc/geyser/registry/mappings/versions/MappingsReader_v1.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/versions/MappingsReader_v1.java index c0199ad52..0db39fe39 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/mappings/versions/MappingsReader_v1.java +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/versions/MappingsReader_v1.java @@ -27,29 +27,24 @@ package org.geysermc.geyser.registry.mappings.versions; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; +import com.github.steveice10.mc.protocol.data.game.Identifier; import com.google.common.base.CharMatcher; - -import org.geysermc.geyser.Constants; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.block.custom.CustomBlockData; import org.geysermc.geyser.api.block.custom.CustomBlockPermutation; import org.geysermc.geyser.api.block.custom.CustomBlockState; -import org.geysermc.geyser.api.block.custom.component.BoxComponent; -import org.geysermc.geyser.api.block.custom.component.PlacementConditions; -import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents; -import org.geysermc.geyser.api.block.custom.component.MaterialInstance; -import org.geysermc.geyser.api.block.custom.component.RotationComponent; +import org.geysermc.geyser.api.block.custom.component.*; import org.geysermc.geyser.api.block.custom.component.PlacementConditions.BlockFilterType; import org.geysermc.geyser.api.block.custom.component.PlacementConditions.Face; import org.geysermc.geyser.api.item.custom.CustomItemData; import org.geysermc.geyser.api.item.custom.CustomItemOptions; import org.geysermc.geyser.item.exception.InvalidCustomMappingsFileException; -import org.geysermc.geyser.level.block.GeyserCustomBlockComponents; import org.geysermc.geyser.level.block.GeyserCustomBlockComponents.CustomBlockComponentsBuilder; import org.geysermc.geyser.level.block.GeyserCustomBlockData.CustomBlockDataBuilder; import org.geysermc.geyser.level.physics.BoundingBox; import org.geysermc.geyser.registry.BlockRegistries; -import org.geysermc.geyser.registry.mappings.util.BlockPropertyTypeMaps; import org.geysermc.geyser.registry.mappings.util.CustomBlockMapping; import org.geysermc.geyser.registry.type.BlockMapping; import org.geysermc.geyser.translator.collision.BlockCollision; @@ -57,17 +52,10 @@ import org.geysermc.geyser.util.BlockUtils; import org.geysermc.geyser.util.MathUtils; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; public class MappingsReader_v1 extends MappingsReader { @@ -107,7 +95,7 @@ public class MappingsReader_v1 extends MappingsReader { blocksNode.fields().forEachRemaining(entry -> { if (entry.getValue().isObject()) { try { - String identifier = entry.getKey(); + String identifier = Identifier.formalize(entry.getKey()); CustomBlockMapping customBlockMapping = this.readBlockMappingEntry(identifier, entry.getValue()); consumer.accept(identifier, customBlockMapping); } catch (InvalidCustomMappingsFileException e) { @@ -181,8 +169,8 @@ public class MappingsReader_v1 extends MappingsReader { } /** - * Read a block mapping entry from a JSON node and java identifier - * @param identifier The java identifier of the block + * Read a block mapping entry from a JSON node and Java identifier + * @param identifier The Java identifier of the block * @param node The {@link JsonNode} containing the block mapping entry * @return The {@link CustomBlockMapping} record to be read by {@link org.geysermc.geyser.registry.populator.CustomBlockRegistryPopulator#registerCustomBedrockBlocks} * @throws InvalidCustomMappingsFileException If the JSON node is invalid @@ -198,272 +186,142 @@ public class MappingsReader_v1 extends MappingsReader { throw new InvalidCustomMappingsFileException("A block entry has no name"); } - // If this is true, we will only register the states the user has specified rather than all of the blocks possible states + // If this is true, we will only register the states the user has specified rather than all the possible block states boolean onlyOverrideStates = node.has("only_override_states") && node.get("only_override_states").asBoolean(); JsonNode stateOverrides = node.get("state_overrides"); - if (onlyOverrideStates && (stateOverrides == null || !stateOverrides.isObject())) { - throw new InvalidCustomMappingsFileException("A block entry has only_override_states set to true but no state_overrides"); + throw new InvalidCustomMappingsFileException("Block entry for " + identifier + " has only_override_states set to true, but has no state_overrides."); } - List stateKeys = new ArrayList<>(); - - // Add the block's identifier to the object so we can use the resulting string to search the block mappings + Map componentsMap = new LinkedHashMap<>(); if (stateOverrides != null && stateOverrides.isObject()) { + // Load components for specific Java block states Iterator> fields = stateOverrides.fields(); while (fields.hasNext()) { - stateKeys.add(identifier + fields.next().getKey()); + Map.Entry overrideEntry = fields.next(); + String state = identifier + "[" + overrideEntry.getKey() + "]"; + if (!BlockRegistries.JAVA_IDENTIFIERS.get().containsKey(state)) { + throw new InvalidCustomMappingsFileException("Unknown Java block state: " + state + " for state_overrides."); + } + componentsMap.put(state, createCustomBlockComponents(overrideEntry.getValue(), state, name)); } } + if (!onlyOverrideStates) { + // Create components for any remaining Java block states + BlockRegistries.JAVA_IDENTIFIERS.get().keySet() + .stream() + .filter(s -> s.startsWith(identifier)) + .filter(Predicate.not(componentsMap::containsKey)) + .forEach(state -> componentsMap.put(state, createCustomBlockComponents(null, state, name))); + } - // Find all the default states for the block we wish to override - List defaultStates = List.copyOf(BlockRegistries.JAVA_IDENTIFIERS.get().keySet()) - .stream() - .filter(s -> s.startsWith(identifier + "[")) - .collect(Collectors.toList()); - // If no states were found, the block must only have one state, so we add its plain identifier - if (defaultStates.isEmpty()) defaultStates.add(identifier); + if (componentsMap.isEmpty()) { + throw new InvalidCustomMappingsFileException("Unknown Java block: " + identifier); + } // Create the data for the overall block - CustomBlockDataBuilder customBlockDataBuilder = new CustomBlockDataBuilder(); - customBlockDataBuilder.name(name) + CustomBlockData.Builder customBlockDataBuilder = new CustomBlockDataBuilder() + .name(name) // We pass in the first state and just use the hitbox from that as the default // Each state will have its own so this is fine - .components(createCustomBlockComponents(node, defaultStates.get(0), name)) - // We must create permutation for every state override - .permutations(createCustomBlockPermutations(stateOverrides, identifier, name)) - // This property is use to display the default state when onlyOverrideStates is false - .booleanProperty(String.format("%s:default", Constants.GEYSER_NAMESPACE)); + .components(createCustomBlockComponents(node, componentsMap.keySet().iterator().next(), name)); - // We need to have three property type maps, one for each type of block property - // Each contains the properties this block has for that type and its possible values, except for boolean since the values must be true/false - // If we are only overriding states, we pass in only the state keys supplied in the mapping - // Otherwise, we pass in all possible states for the block - BlockPropertyTypeMaps blockPropertyTypeMaps = createBlockPropertyTypeMaps(onlyOverrideStates ? stateKeys : defaultStates); - blockPropertyTypeMaps.stringValuesMap().forEach((key, value) -> customBlockDataBuilder.stringProperty(key, new ArrayList(value))); - blockPropertyTypeMaps.intValuesMap().forEach((key, value) -> customBlockDataBuilder.intProperty(key, new ArrayList(value))); - blockPropertyTypeMaps.booleanValuesSet().forEach((value) -> customBlockDataBuilder.booleanProperty(value)); - - // Finally, build the custom block data - CustomBlockData customBlockData = customBlockDataBuilder.build(); - - // Create a map of the custom block states for this block, which contains the full state identifier mapped to the custom block state data - Map states = createCustomBlockStatesMap(stateKeys, defaultStates, onlyOverrideStates, customBlockData, - blockPropertyTypeMaps.stateKeyStrings(), blockPropertyTypeMaps.stateKeyInts(), blockPropertyTypeMaps.stateKeyBools()); - - // Create the custom block mapping record to be passed into the custom block registry populator - return new CustomBlockMapping(customBlockData, states, identifier, !onlyOverrideStates); + if (componentsMap.size() == 1) { + // There are no other block states, so skip creating properties and permutations + CustomBlockData blockData = customBlockDataBuilder.build(); + return new CustomBlockMapping(blockData, Map.of(identifier, blockData.defaultBlockState()), identifier, !onlyOverrideStates); + } + return createCustomBlockMapping(customBlockDataBuilder, componentsMap, identifier, !onlyOverrideStates); } - /** - * Creates a list of {@link CustomBlockPermutation} from the given mappings node containing permutations, java identifier, and custom block name - * @param node an {@link JsonNode} from the mappings file containing the permutations - * @param identifier the java identifier of the block - * @param name the name of the custom block - * @return the list of custom block permutations - */ - private List createCustomBlockPermutations(JsonNode node, String identifier, String name) { + private CustomBlockMapping createCustomBlockMapping(CustomBlockData.Builder customBlockDataBuilder, Map componentsMap, String identifier, boolean overrideItem) { + Map> stringValuesMap = new Object2ObjectOpenHashMap<>(); + Map> intValuesMap = new Object2ObjectOpenHashMap<>(); + List permutations = new ArrayList<>(); + Map> blockStateBuilders = new Object2ObjectOpenHashMap<>(); - // Create a custom block permutation record for each permutation passed into the mappings for the given block - if (node != null && node.isObject()) { - node.fields().forEachRemaining(entry -> { - String key = entry.getKey(); - JsonNode value = entry.getValue(); - if (value.isObject()) { - // Each permutation has its own components, which override the base components when explicitly set - // Based on the input states, we construct a molang query that will evaluate to true when the block properties corresponding to the state are active - permutations.add(new CustomBlockPermutation(createCustomBlockComponents(value, (identifier + key), name), createCustomBlockPropertyQuery(key))); - } - }); - } - - // We also need to create a permutation for the default state of the block with no components - // Functionally, this means the default components will be used - permutations.add(new CustomBlockPermutation(new GeyserCustomBlockComponents.CustomBlockComponentsBuilder().build(), String.format("q.block_property('%s:default') == 1", Constants.GEYSER_NAMESPACE))); - - return permutations; - } - - /** - * Create a map of java block state identifiers to {@link CustomBlockState} so that {@link #readBlockMappingEntry} can include it in the {@link CustomBlockMapping} record - * @param stateKeys the list of java block state identifiers explicitly passed in the mappings - * @param defaultStates the list of all possible java block state identifiers for the block - * @param onlyOverrideStates whether or not we are only overriding the states passed in the mappings - * @param customBlockData the {@link CustomBlockData} for the block - * @param stateKeyStrings the map of java block state identifiers to their string properties - * @param stateKeyInts the map of java block state identifiers to their int properties - * @param stateKeyBools the map of java block state identifiers to their boolean properties - * @return the custom block states maps - */ - private Map createCustomBlockStatesMap(List stateKeys,List defaultStates, boolean onlyOverrideStates, CustomBlockData customBlockData, - Map> stateKeyStrings, Map> stateKeyInts, Map> stateKeyBools) { - - Map states = new HashMap<>(); - - // If not only overriding specified states, we must include the default states in the custom block states map - if (!onlyOverrideStates) { - defaultStates.removeAll(stateKeys); - createCustomBlockStates(defaultStates, true, customBlockData, stateKeyStrings, stateKeyInts, stateKeyBools, states); - } - createCustomBlockStates(stateKeys, false, customBlockData, stateKeyStrings, stateKeyInts, stateKeyBools, states); - - return states; - } - - /** - * Create the custom block states for the given state keys and append them to the passed states map - * @param stateKeys the list of java block state identifiers - * @param defaultState whether or not this is the default state - * @param customBlockData the {@link CustomBlockData} for the block - * @param stateKeyStrings the map of java block state identifiers to their string properties - * @param stateKeyInts the map of java block state identifiers to their int properties - * @param stateKeyBools the map of java block state identifiers to their boolean properties - * @param states the map of java block state identifiers to their {@link CustomBlockState} to append - */ - private void createCustomBlockStates(List stateKeys, boolean defaultState, CustomBlockData customBlockData, - Map> stateKeyStrings, Map> stateKeyInts, - Map> stateKeyBools, Map states) { - stateKeys.forEach((key) -> { - CustomBlockState.Builder builder = customBlockData.blockStateBuilder(); - // We always include the default property, which is used to set the default state when onlyOverrideStates is false - builder.booleanProperty(String.format("%s:default", Constants.GEYSER_NAMESPACE), defaultState); - - // The properties must be added to the builder seperately for each type - stateKeyStrings.getOrDefault(key, Collections.emptyMap()).forEach((property, stringValue) -> builder.stringProperty(property, stringValue)); - stateKeyInts.getOrDefault(key, Collections.emptyMap()).forEach((property, intValue) -> builder.intProperty(property, intValue)); - stateKeyBools.getOrDefault(key, Collections.emptyMap()).forEach((property, boolValue) -> builder.booleanProperty(property, boolValue)); - - CustomBlockState blockState = builder.build(); - - states.put(key, blockState); - }); - } - - /** - * Creates a record of {@link BlockPropertyTypeMaps} for the given list of java block state identifiers that are being actively used by the custom block - * @param usedStateKeys the list of java block state identifiers that are being actively used by the custom block - * @return the {@link BlockPropertyTypeMaps} record - */ - private BlockPropertyTypeMaps createBlockPropertyTypeMaps(List usedStateKeys) { - // Each of the three property type has two maps - // The first map is used to store the possible values for each property - // The second map is used to store the value for each property for each state - Map> stringValuesMap = new HashMap<>(); - Map> stateKeyStrings = new HashMap<>(); - - Map> intValuesMap = new HashMap<>(); - Map> stateKeyInts = new HashMap<>(); - - Set booleanValuesSet = new HashSet<>(); - Map> stateKeyBools = new HashMap<>(); - - - for (String state : usedStateKeys) { - // No bracket means that there is only one state, so the maps should be empty - if (!state.contains("[")) continue; - - // Split the state string into an array containing each property=value pair + // For each Java block state, extract the property values, create a CustomBlockPermutation, + // and a CustomBlockState builder + for (Map.Entry entry : componentsMap.entrySet()) { + String state = entry.getKey(); String[] pairs = splitStateString(state); - for (String pair : pairs) { - // Get the property and value individually - String[] parts = pair.split("="); + String[] conditions = new String[pairs.length]; + Function blockStateBuilder = Function.identity(); + + for (int i = 0; i < pairs.length; i++) { + String[] parts = pairs[i].split("="); String property = parts[0]; String value = parts[1]; // Figure out what property type we are dealing with if (value.equals("true") || value.equals("false")) { - booleanValuesSet.add(property); - Map propertyMap = stateKeyBools.getOrDefault(state, new HashMap<>()); - propertyMap.put(property, Boolean.parseBoolean(value)); - stateKeyBools.put(state, propertyMap); + customBlockDataBuilder.booleanProperty(property); + + conditions[i] = String.format("q.block_property('%s') == %s", property, value); + blockStateBuilder = blockStateBuilder.andThen(builder -> builder.booleanProperty(property, value.equals("true"))); } else if (CharMatcher.inRange('0', '9').matchesAllOf(value)) { int intValue = Integer.parseInt(value); - LinkedHashSet values = intValuesMap.get(property); - // Initialize the property to values map if it doesn't exist - if (values == null) { - values = new LinkedHashSet<>(); - intValuesMap.put(property, values); - } - values.add(intValue); - Map propertyMap = stateKeyInts.getOrDefault(state, new HashMap<>()); - propertyMap.put(property, intValue); - stateKeyInts.put(state, propertyMap); + intValuesMap.computeIfAbsent(property, k -> new LinkedHashSet<>()) + .add(intValue); + + conditions[i] = String.format("q.block_property('%s') == %s", property, value); + blockStateBuilder = blockStateBuilder.andThen(builder -> builder.intProperty(property, intValue)); } else { - // If it's n not a boolean or int it must be a string - LinkedHashSet values = stringValuesMap.get(property); - // Initialize the property to values map if it doesn't exist - if (values == null) { - values = new LinkedHashSet<>(); - stringValuesMap.put(property, values); - } - values.add(value); - Map propertyMap = stateKeyStrings.getOrDefault(state, new HashMap<>()); - propertyMap.put(property, value); - stateKeyStrings.put(state, propertyMap); + stringValuesMap.computeIfAbsent(property, k -> new LinkedHashSet<>()) + .add(value); + + conditions[i] = String.format("q.block_property('%s') == '%s'", property, value); + blockStateBuilder = blockStateBuilder.andThen(builder -> builder.stringProperty(property, value)); } } + permutations.add(new CustomBlockPermutation(entry.getValue(), String.join(" && ", conditions))); + blockStateBuilders.put(state, blockStateBuilder.andThen(CustomBlockState.Builder::build)); } - // We should now have all of the maps - return new BlockPropertyTypeMaps(stringValuesMap, stateKeyStrings, intValuesMap, stateKeyInts, booleanValuesSet, stateKeyBools); + // Define properties for the custom block + stringValuesMap.forEach((key, value) -> customBlockDataBuilder.stringProperty(key, new ArrayList<>(value))); + intValuesMap.forEach((key, value) -> customBlockDataBuilder.intProperty(key, new ArrayList<>(value))); + + CustomBlockData customBlockData = customBlockDataBuilder + .permutations(permutations) + .build(); + // Build CustomBlockStates for each Java block state we wish to override + Map states = blockStateBuilders.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().apply(customBlockData.blockStateBuilder()))); + + return new CustomBlockMapping(customBlockData, states, identifier, overrideItem); } /** - * Creates a {@link CustomBlockComponents} object for the passed permutation or base block node, java block state identifier, and custom block name - * @param node the permutation or base block {@link JsonNode} - * @param stateKey the java block state identifier - * @param name the custom block name + * Creates a {@link CustomBlockComponents} object for the passed state override or base block node, Java block state identifier, and custom block name + * @param node the state override or base block {@link JsonNode} + * @param stateKey the Java block state identifier + * @param name the name of the custom block * @return the {@link CustomBlockComponents} object */ private CustomBlockComponents createCustomBlockComponents(JsonNode node, String stateKey, String name) { // This is needed to find the correct selection box for the given block int id = BlockRegistries.JAVA_IDENTIFIERS.getOrDefault(stateKey, -1); - CustomBlockComponentsBuilder builder = new CustomBlockComponentsBuilder(); BoxComponent boxComponent = createBoxComponent(id); + CustomBlockComponents.Builder builder = new CustomBlockComponentsBuilder() + .collisionBox(boxComponent) + .selectionBox(boxComponent); - BoxComponent selectionBox = boxComponent; - if (node.has("selection_box")) { - JsonNode selectionBoxNode = node.get("selection_box"); - if (selectionBoxNode.isObject()) { - if (selectionBoxNode.has("origin") && selectionBoxNode.has("size")) { - JsonNode origin = selectionBoxNode.get("origin"); - int originX = origin.get(0).intValue(); - int originY = origin.get(1).intValue(); - int originZ = origin.get(2).intValue(); - - JsonNode size = selectionBoxNode.get("size"); - int sizeX = size.get(0).intValue(); - int sizeY = size.get(1).intValue(); - int sizeZ = size.get(2).intValue(); - - selectionBox = new BoxComponent(originX, originY, originZ, sizeX, sizeY, sizeZ); - } - } + if (node == null) { + // No other components were defined + return builder.build(); } - builder.selectionBox(selectionBox); - BoxComponent collisionBox = boxComponent; - if (node.has("collision_box")) { - JsonNode collisionBoxNode = node.get("collision_box"); - if (collisionBoxNode.isObject()) { - if (collisionBoxNode.has("origin") && collisionBoxNode.has("size")) { - JsonNode origin = collisionBoxNode.get("origin"); - int originX = origin.get(0).intValue(); - int originY = origin.get(1).intValue(); - int originZ = origin.get(2).intValue(); - - JsonNode size = collisionBoxNode.get("size"); - int sizeX = size.get(0).intValue(); - int sizeY = size.get(1).intValue(); - int sizeZ = size.get(2).intValue(); - - collisionBox = new BoxComponent(originX, originY, originZ, sizeX, sizeY, sizeZ); - } - } + BoxComponent selectionBox = createBoxComponent(node.get("selection_box")); + if (selectionBox != null) { + builder.selectionBox(selectionBox); + } + BoxComponent collisionBox = createBoxComponent(node.get("collision_box")); + if (collisionBox != null) { + builder.collisionBox(collisionBox); } - builder.collisionBox(collisionBox); // Ideally we would just be able to calculate the right value for this, but it seems that hardness value on bedrock does not follow Java // As such this might as well just be configured for now if people so choose @@ -542,34 +400,28 @@ public class MappingsReader_v1 extends MappingsReader { // Tags can be applied so that blocks will match return true when queried for the tag // Potentially useful for resource pack creators - // Ideally we could programatically extract the tags here https://wiki.bedrock.dev/blocks/block-tags.html - // This would let us automatically apply the correct valilla tags to blocks - // However, its worth noting that vanilla tools do not currently honor these tags anyways - if (node.has("tags")) { - JsonNode tags = node.get("tags"); - if (tags.isArray()) { - ArrayNode tagsArray = (ArrayNode) tags; - Set tagsSet = new HashSet<>(); - tagsArray.forEach(tag -> tagsSet.add(tag.asText())); - builder.tags(tagsSet); - } + // Ideally we could programmatically extract the tags here https://wiki.bedrock.dev/blocks/block-tags.html + // This would let us automatically apply the correct vanilla tags to blocks + // However, its worth noting that vanilla tools do not currently honor these tags anyway + if (node.get("tags") instanceof ArrayNode tags) { + Set tagsSet = new ObjectOpenHashSet<>(); + tags.forEach(tag -> tagsSet.add(tag.asText())); + builder.tags(tagsSet); } - CustomBlockComponents components = builder.build(); - - return components; + return builder.build(); } /** - * Creates the {@link BoxComponent} for the passed collision box index - * @param id the collision box index + * Creates a {@link BoxComponent} based on a Java block's collision + * @param javaId the block's Java ID * @return the {@link BoxComponent} */ - private BoxComponent createBoxComponent(int id) { + private BoxComponent createBoxComponent(int javaId) { // Some blocks (e.g. plants) have no collision box - BlockCollision blockCollision = BlockUtils.getCollision(id); + BlockCollision blockCollision = BlockUtils.getCollision(javaId); if (blockCollision == null) { - return new BoxComponent(0, 0, 0, 0, 0, 0); + return BoxComponent.EMPTY_BOX; } BoundingBox boundingBox = blockCollision.getBoundingBoxes()[0]; @@ -578,7 +430,7 @@ public class MappingsReader_v1 extends MappingsReader { float offsetY = (float) boundingBox.getSizeY() * 8; float offsetZ = (float) boundingBox.getSizeZ() * 8; - // Unfortunately we need to clamp the values here to a an effective size of one block + // Unfortunately we need to clamp the values here to an effective size of one block // This is quite a pain for anything like fences, as the player can just jump over them // One possible solution would be to create invisible blocks that we use only for collision box // These could be placed above the block when a custom block exceeds this limit @@ -591,9 +443,31 @@ public class MappingsReader_v1 extends MappingsReader { float sizeY = MathUtils.clamp((float) boundingBox.getSizeY() * 16, 0, 16); float sizeZ = MathUtils.clamp((float) boundingBox.getSizeZ() * 16, 0, 16); - BoxComponent boxComponent = new BoxComponent(cornerX, cornerY, cornerZ, sizeX, sizeY, sizeZ); + return new BoxComponent(cornerX, cornerY, cornerZ, sizeX, sizeY, sizeZ); + } - return boxComponent; + /** + * Creates a {@link BoxComponent} from a JSON Node + * @param node the JSON node + * @return the {@link BoxComponent} + */ + private BoxComponent createBoxComponent(JsonNode node) { + if (node != null && node.isObject()) { + if (node.has("origin") && node.has("size")) { + JsonNode origin = node.get("origin"); + float originX = origin.get(0).floatValue(); + float originY = origin.get(1).floatValue(); + float originZ = origin.get(2).floatValue(); + + JsonNode size = node.get("size"); + float sizeX = size.get(0).floatValue(); + float sizeY = size.get(1).floatValue(); + float sizeZ = size.get(2).floatValue(); + + return new BoxComponent(originX, originY, originZ, sizeX, sizeY, sizeZ); + } + } + return null; } /** @@ -637,15 +511,13 @@ public class MappingsReader_v1 extends MappingsReader { List conditions = new ArrayList<>(); // The structure of the placement filter component is the most complex of the current components - // Each condition effectively seperated into an two arrays: one of allowed faces, and one of blocks/block molang queries + // Each condition effectively separated into two arrays: one of allowed faces, and one of blocks/block Molang queries node.forEach(condition -> { - Set faces = new HashSet<>(); + Set faces = EnumSet.noneOf(Face.class); if (condition.has("allowed_faces")) { JsonNode allowedFaces = condition.get("allowed_faces"); if (allowedFaces.isArray()) { - allowedFaces.forEach(face -> { - faces.add(Face.valueOf(face.asText().toUpperCase())); - }); + allowedFaces.forEach(face -> faces.add(Face.valueOf(face.asText().toUpperCase()))); } } @@ -672,51 +544,16 @@ public class MappingsReader_v1 extends MappingsReader { return conditions; } - /** - * Creates a molang query that returns true when the given java state identifier is the active state - * @param state the java state identifier - * @return the molang query - */ - private String createCustomBlockPropertyQuery(String state) { - // This creates a molang query from the given input blockstate string - String[] conditions = splitStateString(state); - String[] queries = new String[conditions.length]; - - for (int i = 0; i < conditions.length; i++) { - String[] keyval = conditions[i].split("=", 2); - - if (keyval[1].equals("true")) { - queries[i] = String.format("q.block_property('%1$s') == %2$s", keyval[0], 1); - } else if (keyval[1].equals("false")) { - queries[i] = String.format("q.block_property('%1$s') == %2$s", keyval[0], 0); - } else if (CharMatcher.inRange('0', '9').matchesAllOf(keyval[1])) { - queries[i] = String.format("q.block_property('%1$s') == %2$s", keyval[0], Integer.parseInt(keyval[1])); - } else { - queries[i] = String.format("q.block_property('%1$s') == '%2$s'", keyval[0], keyval[1]); - } - } - - String query = String.join(" && ", queries); - - // Appends the default property to ensure it can be disabled when a state without specific overrides is active - return String.format("q.block_property('%1$s:default') == 0 && %2$s", Constants.GEYSER_NAMESPACE, query); - } - /** * Splits the given java state identifier into an array of property=value pairs * @param state the java state identifier * @return the array of property=value pairs */ private String[] splitStateString(String state) { - // Split the given state string into an array of property=value pairs int openBracketIndex = state.indexOf("["); - int closeBracketIndex = state.indexOf("]"); - String cleanStates = state.substring(openBracketIndex + 1, closeBracketIndex); - - String[] pairs = cleanStates.split("\\s*,\\s*"); - - return pairs; + String states = state.substring(openBracketIndex + 1, state.length() - 1); + return states.split(","); } }