Rework MappingsReader_v1 to avoid passing maps around

This commit is contained in:
davchoo 2023-01-30 15:01:49 -05:00
parent 1efe7342eb
commit d99cb468f1
No known key found for this signature in database
GPG key ID: 501B6F4FD961CF9A
3 changed files with 143 additions and 319 deletions

View file

@ -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<String, LinkedHashSet<String>> stringValuesMap, @NonNull Map<String, Map<String, String>> stateKeyStrings,
@NonNull Map<String, LinkedHashSet<Integer>> intValuesMap, @NonNull Map<String, Map<String, Integer>> stateKeyInts,
@NonNull Set<String> booleanValuesSet, @NonNull Map<String, Map<String, Boolean>> stateKeyBools) {
}

View file

@ -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<String, CustomBlockState> states, @NonNull String javaIdentifier, @NonNull boolean overrideItem) {
public record CustomBlockMapping(@NonNull CustomBlockData data, @NonNull Map<String, CustomBlockState> states, @NonNull String javaIdentifier, boolean overrideItem) {
}

View file

@ -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<String> stateKeys = new ArrayList<>();
// Add the block's identifier to the object so we can use the resulting string to search the block mappings
Map<String, CustomBlockComponents> componentsMap = new LinkedHashMap<>();
if (stateOverrides != null && stateOverrides.isObject()) {
// Load components for specific Java block states
Iterator<Map.Entry<String, JsonNode>> fields = stateOverrides.fields();
while (fields.hasNext()) {
stateKeys.add(identifier + fields.next().getKey());
Map.Entry<String, JsonNode> 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<String> 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<String>(value)));
blockPropertyTypeMaps.intValuesMap().forEach((key, value) -> customBlockDataBuilder.intProperty(key, new ArrayList<Integer>(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<String, CustomBlockState> 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<CustomBlockPermutation> createCustomBlockPermutations(JsonNode node, String identifier, String name) {
private CustomBlockMapping createCustomBlockMapping(CustomBlockData.Builder customBlockDataBuilder, Map<String, CustomBlockComponents> componentsMap, String identifier, boolean overrideItem) {
Map<String, LinkedHashSet<String>> stringValuesMap = new Object2ObjectOpenHashMap<>();
Map<String, LinkedHashSet<Integer>> intValuesMap = new Object2ObjectOpenHashMap<>();
List<CustomBlockPermutation> permutations = new ArrayList<>();
Map<String, Function<CustomBlockState.Builder, CustomBlockState>> 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<String, CustomBlockState> createCustomBlockStatesMap(List<String> stateKeys,List<String> defaultStates, boolean onlyOverrideStates, CustomBlockData customBlockData,
Map<String, Map<String, String>> stateKeyStrings, Map<String, Map<String, Integer>> stateKeyInts, Map<String, Map<String, Boolean>> stateKeyBools) {
Map<String, CustomBlockState> 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<String> stateKeys, boolean defaultState, CustomBlockData customBlockData,
Map<String, Map<String, String>> stateKeyStrings, Map<String, Map<String, Integer>> stateKeyInts,
Map<String, Map<String, Boolean>> stateKeyBools, Map<String, CustomBlockState> 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<String> 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<String, LinkedHashSet<String>> stringValuesMap = new HashMap<>();
Map<String, Map<String, String>> stateKeyStrings = new HashMap<>();
Map<String, LinkedHashSet<Integer>> intValuesMap = new HashMap<>();
Map<String, Map<String, Integer>> stateKeyInts = new HashMap<>();
Set<String> booleanValuesSet = new HashSet<>();
Map<String, Map<String, Boolean>> 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<String, CustomBlockComponents> 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<CustomBlockState.Builder, CustomBlockState.Builder> 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<String, Boolean> 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<Integer> 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<String, Integer> 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<String> 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<String, String> 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<String, CustomBlockState> 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<String> 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<String> 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<PlacementConditions> 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<Face> faces = new HashSet<>();
Set<Face> 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(",");
}
}