3
0
Mirror von https://github.com/GeyserMC/Geyser.git synchronisiert 2024-11-19 22:40:18 +01:00

Minor cleanup and javadocs

Dieser Commit ist enthalten in:
Joshua Castle 2023-01-15 16:27:28 -08:00
Ursprung bfdebadacc
Commit cc2e409f05
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
GPG-Schlüssel-ID: F674F38216C35D5D
5 geänderte Dateien mit 206 neuen und 14 gelöschten Zeilen

Datei anzeigen

@ -41,7 +41,7 @@ public final class Constants {
static final String SAVED_REFRESH_TOKEN_FILE = "saved-refresh-tokens.json"; static final String SAVED_REFRESH_TOKEN_FILE = "saved-refresh-tokens.json";
public static final String GEYSER_NAMESPACE = "geyser:"; public static final String GEYSER_NAMESPACE = "geyser_custom";
static { static {
URI wsUri = null; URI wsUri = null;

Datei anzeigen

@ -94,7 +94,7 @@ public class GeyserCustomBlockData implements CustomBlockData {
@Override @Override
public @NonNull String identifier() { public @NonNull String identifier() {
return Constants.GEYSER_NAMESPACE + name; return Constants.GEYSER_NAMESPACE + ":" + name;
} }
@Override @Override

Datei anzeigen

@ -29,6 +29,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.google.common.base.CharMatcher; import com.google.common.base.CharMatcher;
import org.geysermc.geyser.Constants;
import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.block.custom.CustomBlockData; import org.geysermc.geyser.api.block.custom.CustomBlockData;
import org.geysermc.geyser.api.block.custom.CustomBlockPermutation; import org.geysermc.geyser.api.block.custom.CustomBlockPermutation;
@ -178,6 +179,13 @@ public class MappingsReader_v1 extends MappingsReader {
return customItemData.build(); return customItemData.build();
} }
/**
* 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
*/
@Override @Override
public CustomBlockMapping readBlockMappingEntry(String identifier, JsonNode node) throws InvalidCustomMappingsFileException { public CustomBlockMapping readBlockMappingEntry(String identifier, JsonNode node) throws InvalidCustomMappingsFileException {
if (node == null || !node.isObject()) { if (node == null || !node.isObject()) {
@ -189,6 +197,7 @@ public class MappingsReader_v1 extends MappingsReader {
throw new InvalidCustomMappingsFileException("A block entry has no name"); 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
boolean onlyOverrideStates = node.has("only_override_states") && node.get("only_override_states").asBoolean(); boolean onlyOverrideStates = node.has("only_override_states") && node.get("only_override_states").asBoolean();
JsonNode stateOverrides = node.get("state_overrides"); JsonNode stateOverrides = node.get("state_overrides");
@ -199,6 +208,7 @@ public class MappingsReader_v1 extends MappingsReader {
List<String> stateKeys = new ArrayList<>(); 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
if (stateOverrides != null && stateOverrides.isObject()) { if (stateOverrides != null && stateOverrides.isObject()) {
Iterator<Map.Entry<String, JsonNode>> fields = stateOverrides.fields(); Iterator<Map.Entry<String, JsonNode>> fields = stateOverrides.fields();
while (fields.hasNext()) { while (fields.hasNext()) {
@ -206,54 +216,92 @@ public class MappingsReader_v1 extends MappingsReader {
} }
} }
// Find all the default states for the block we wish to override
List<String> defaultStates = List.copyOf(BlockRegistries.JAVA_IDENTIFIERS.get().keySet()) List<String> defaultStates = List.copyOf(BlockRegistries.JAVA_IDENTIFIERS.get().keySet())
.stream() .stream()
.filter(s -> s.startsWith(identifier + "[")) .filter(s -> s.startsWith(identifier + "["))
.collect(Collectors.toList()); .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 (defaultStates.isEmpty()) defaultStates.add(identifier);
// Create the data for the overall block
CustomBlockDataBuilder customBlockDataBuilder = new CustomBlockDataBuilder(); CustomBlockDataBuilder customBlockDataBuilder = new CustomBlockDataBuilder();
customBlockDataBuilder.name(name) 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)) .components(createCustomBlockComponents(node, defaultStates.get(0), name))
// We must create permutation for every state override
.permutations(createCustomBlockPermutations(stateOverrides, identifier, name)) .permutations(createCustomBlockPermutations(stateOverrides, identifier, name))
.booleanProperty("geyser_custom:default"); // This property is use to display the default state when onlyOverrideStates is false
.booleanProperty(String.format("%s:default", Constants.GEYSER_NAMESPACE));
// 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 blockPropertyTypeMaps = createBlockPropertyTypeMaps(onlyOverrideStates ? stateKeys : defaultStates);
blockPropertyTypeMaps.stringValuesMap().forEach((key, value) -> customBlockDataBuilder.stringProperty(key, new ArrayList<String>(value))); 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.intValuesMap().forEach((key, value) -> customBlockDataBuilder.intProperty(key, new ArrayList<Integer>(value)));
blockPropertyTypeMaps.booleanValuesSet().forEach((value) -> customBlockDataBuilder.booleanProperty(value)); blockPropertyTypeMaps.booleanValuesSet().forEach((value) -> customBlockDataBuilder.booleanProperty(value));
// Finally, build the custom block data
CustomBlockData customBlockData = customBlockDataBuilder.build(); CustomBlockData customBlockData = customBlockDataBuilder.build();
Map<String, CustomBlockState> states = createCustomBlockStatesMap(identifier, stateKeys, defaultStates, onlyOverrideStates, customBlockData, // 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()); 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); return new CustomBlockMapping(customBlockData, states, 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 List<CustomBlockPermutation> createCustomBlockPermutations(JsonNode node, String identifier, String name) {
List<CustomBlockPermutation> permutations = new ArrayList<>(); List<CustomBlockPermutation> permutations = new ArrayList<>();
// Create a custom block permutation record for each permutation passed into the mappings for the given block
if (node != null && node.isObject()) { if (node != null && node.isObject()) {
node.fields().forEachRemaining(entry -> { node.fields().forEachRemaining(entry -> {
String key = entry.getKey(); String key = entry.getKey();
JsonNode value = entry.getValue(); JsonNode value = entry.getValue();
if (value.isObject()) { 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))); permutations.add(new CustomBlockPermutation(createCustomBlockComponents(value, (identifier + key), name), createCustomBlockPropertyQuery(key)));
} }
}); });
} }
permutations.add(new CustomBlockPermutation(new GeyserCustomBlockComponents.CustomBlockComponentsBuilder().build(), "q.block_property('geyser_custom:default') == 1")); // 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; return permutations;
} }
private Map<String, CustomBlockState> createCustomBlockStatesMap(String identifier, List<String> stateKeys,List<String> defaultStates, boolean onlyOverrideStates, CustomBlockData customBlockData, /**
* 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, Map<String, String>> stateKeyStrings, Map<String, Map<String, Integer>> stateKeyInts, Map<String, Map<String, Boolean>> stateKeyBools) {
Map<String, CustomBlockState> states = new HashMap<>(); 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) { if (!onlyOverrideStates) {
defaultStates.removeAll(stateKeys); defaultStates.removeAll(stateKeys);
createCustomBlockStates(defaultStates, true, customBlockData, stateKeyStrings, stateKeyInts, stateKeyBools, states); createCustomBlockStates(defaultStates, true, customBlockData, stateKeyStrings, stateKeyInts, stateKeyBools, states);
@ -263,13 +311,25 @@ public class MappingsReader_v1 extends MappingsReader {
return 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, 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, String>> stateKeyStrings, Map<String, Map<String, Integer>> stateKeyInts,
Map<String, Map<String, Boolean>> stateKeyBools, Map<String, CustomBlockState> states) { Map<String, Map<String, Boolean>> stateKeyBools, Map<String, CustomBlockState> states) {
stateKeys.forEach((key) -> { stateKeys.forEach((key) -> {
CustomBlockState.Builder builder = customBlockData.blockStateBuilder(); CustomBlockState.Builder builder = customBlockData.blockStateBuilder();
builder.booleanProperty("geyser_custom:default", defaultState); // 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)); stateKeyStrings.getOrDefault(key, Collections.emptyMap()).forEach((property, stringValue) -> builder.stringProperty(property, stringValue));
stateKeyInts.getOrDefault(key, Collections.emptyMap()).forEach((property, intValue) -> builder.intProperty(property, intValue)); stateKeyInts.getOrDefault(key, Collections.emptyMap()).forEach((property, intValue) -> builder.intProperty(property, intValue));
stateKeyBools.getOrDefault(key, Collections.emptyMap()).forEach((property, boolValue) -> builder.booleanProperty(property, boolValue)); stateKeyBools.getOrDefault(key, Collections.emptyMap()).forEach((property, boolValue) -> builder.booleanProperty(property, boolValue));
@ -280,7 +340,15 @@ public class MappingsReader_v1 extends MappingsReader {
}); });
} }
/**
* 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) { 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, LinkedHashSet<String>> stringValuesMap = new HashMap<>();
Map<String, Map<String, String>> stateKeyStrings = new HashMap<>(); Map<String, Map<String, String>> stateKeyStrings = new HashMap<>();
@ -292,14 +360,19 @@ public class MappingsReader_v1 extends MappingsReader {
for (String state : usedStateKeys) { for (String state : usedStateKeys) {
// No bracket means that there is only one state, so the maps should be empty
if (!state.contains("[")) continue; if (!state.contains("[")) continue;
// Split the state string into an array containing each property=value pair
String[] pairs = splitStateString(state); String[] pairs = splitStateString(state);
for (String pair : pairs) { for (String pair : pairs) {
// Get the property and value individually
String[] parts = pair.split("="); String[] parts = pair.split("=");
String property = parts[0]; String property = parts[0];
String value = parts[1]; String value = parts[1];
// Figure out what property type we are dealing with
if (value.equals("true") || value.equals("false")) { if (value.equals("true") || value.equals("false")) {
booleanValuesSet.add(property); booleanValuesSet.add(property);
Map<String, Boolean> propertyMap = stateKeyBools.getOrDefault(state, new HashMap<>()); Map<String, Boolean> propertyMap = stateKeyBools.getOrDefault(state, new HashMap<>());
@ -308,6 +381,7 @@ public class MappingsReader_v1 extends MappingsReader {
} else if (CharMatcher.inRange('0', '9').matchesAllOf(value)) { } else if (CharMatcher.inRange('0', '9').matchesAllOf(value)) {
int intValue = Integer.parseInt(value); int intValue = Integer.parseInt(value);
LinkedHashSet<Integer> values = intValuesMap.get(property); LinkedHashSet<Integer> values = intValuesMap.get(property);
// Initialize the property to values map if it doesn't exist
if (values == null) { if (values == null) {
values = new LinkedHashSet<>(); values = new LinkedHashSet<>();
intValuesMap.put(property, values); intValuesMap.put(property, values);
@ -317,7 +391,9 @@ public class MappingsReader_v1 extends MappingsReader {
propertyMap.put(property, intValue); propertyMap.put(property, intValue);
stateKeyInts.put(state, propertyMap); stateKeyInts.put(state, propertyMap);
} else { } else {
// If it's n not a boolean or int it must be a string
LinkedHashSet<String> values = stringValuesMap.get(property); LinkedHashSet<String> values = stringValuesMap.get(property);
// Initialize the property to values map if it doesn't exist
if (values == null) { if (values == null) {
values = new LinkedHashSet<>(); values = new LinkedHashSet<>();
stringValuesMap.put(property, values); stringValuesMap.put(property, values);
@ -329,14 +405,22 @@ public class MappingsReader_v1 extends MappingsReader {
} }
} }
} }
// We should now have all of the maps
return new BlockPropertyTypeMaps(stringValuesMap, stateKeyStrings, intValuesMap, stateKeyInts, booleanValuesSet, stateKeyBools); return new BlockPropertyTypeMaps(stringValuesMap, stateKeyStrings, intValuesMap, stateKeyInts, booleanValuesSet, stateKeyBools);
} }
/**
* 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
* @return the {@link CustomBlockComponents} object
*/
private CustomBlockComponents createCustomBlockComponents(JsonNode node, String stateKey, String name) { 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); int id = BlockRegistries.JAVA_IDENTIFIERS.getOrDefault(stateKey, -1);
CustomBlockComponentsBuilder builder = new CustomBlockComponentsBuilder(); CustomBlockComponentsBuilder builder = new CustomBlockComponentsBuilder();
// .friction()
BoxComponent boxComponent = createBoxComponent(id); BoxComponent boxComponent = createBoxComponent(id);
builder.selectionBox(boxComponent).collisionBox(boxComponent); builder.selectionBox(boxComponent).collisionBox(boxComponent);
@ -358,6 +442,10 @@ public class MappingsReader_v1 extends MappingsReader {
} }
builder.displayName(displayName); builder.displayName(displayName);
if (node.has("friction")) {
builder.friction(node.get("friction").floatValue());
}
if (node.has("light_emission")) { if (node.has("light_emission")) {
builder.lightEmission(node.get("light_emission").asInt()); builder.lightEmission(node.get("light_emission").asInt());
} }
@ -411,6 +499,11 @@ 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")) { if (node.has("tags")) {
JsonNode tags = node.get("tags"); JsonNode tags = node.get("tags");
if (tags.isArray()) { if (tags.isArray()) {
@ -426,6 +519,11 @@ public class MappingsReader_v1 extends MappingsReader {
return components; return components;
} }
/**
* Creates the {@link BoxComponent} for the passed collision box index
* @param id the collision box index
* @return the {@link BoxComponent}
*/
private BoxComponent createBoxComponent(int id) { private BoxComponent createBoxComponent(int id) {
BoundingBox boundingBox = BlockUtils.getCollision(id).getBoundingBoxes()[0]; BoundingBox boundingBox = BlockUtils.getCollision(id).getBoundingBoxes()[0];
@ -433,6 +531,11 @@ public class MappingsReader_v1 extends MappingsReader {
float offsetY = (float) boundingBox.getSizeY() * 8; float offsetY = (float) boundingBox.getSizeY() * 8;
float offsetZ = (float) boundingBox.getSizeZ() * 8; float offsetZ = (float) boundingBox.getSizeZ() * 8;
// Unfortunately we need to clamp the values here to a 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
// I am hopeful this will be extended slightly since the geometry of blocks can be 1.875^3
float cornerX = clamp((float) boundingBox.getMiddleX() * 16 - 8 - offsetX, -8, 8); float cornerX = clamp((float) boundingBox.getMiddleX() * 16 - 8 - offsetX, -8, 8);
float cornerY = clamp((float) boundingBox.getMiddleY() * 16 - offsetY, 0, 16); float cornerY = clamp((float) boundingBox.getMiddleY() * 16 - offsetY, 0, 16);
float cornerZ = clamp((float) boundingBox.getMiddleZ() * 16 - 8 - offsetZ, -8, 8); float cornerZ = clamp((float) boundingBox.getMiddleZ() * 16 - 8 - offsetZ, -8, 8);
@ -446,7 +549,15 @@ public class MappingsReader_v1 extends MappingsReader {
return boxComponent; return boxComponent;
} }
/**
* Creates the {@link MaterialInstance} for the passed material instance node and custom block name
* The name is used as a fallback if no texture is provided by the node
* @param node the material instance node
* @param name the custom block name
* @return the {@link MaterialInstance}
*/
private MaterialInstance createMaterialInstanceComponent(JsonNode node, String name) { private MaterialInstance createMaterialInstanceComponent(JsonNode node, String name) {
// Set default values, and use what the user provides if they have provided something
String texture = name; String texture = name;
if (node.has("texture")) { if (node.has("texture")) {
texture = node.get("texture").asText(); texture = node.get("texture").asText();
@ -470,9 +581,16 @@ public class MappingsReader_v1 extends MappingsReader {
return new MaterialInstance(texture, renderMethod, faceDimming, ambientOcclusion); return new MaterialInstance(texture, renderMethod, faceDimming, ambientOcclusion);
} }
/**
* Creates the {@link PlacementFilter} for the passed conditions node
* @param node the conditions node
* @return the {@link PlacementFilter}
*/
private PlacementFilter createPlacementFilterComponent(JsonNode node) { private PlacementFilter createPlacementFilterComponent(JsonNode node) {
List<Conditions> conditions = new ArrayList<>(); List<Conditions> 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
node.forEach(condition -> { node.forEach(condition -> {
Set<Face> faces = new HashSet<>(); Set<Face> faces = new HashSet<>();
if (condition.has("allowed_faces")) { if (condition.has("allowed_faces")) {
@ -507,7 +625,13 @@ public class MappingsReader_v1 extends MappingsReader {
return new PlacementFilter(conditions); return new PlacementFilter(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) { private String createCustomBlockPropertyQuery(String state) {
// This creates a molang query from the given input blockstate string
String[] conditions = splitStateString(state); String[] conditions = splitStateString(state);
String[] queries = new String[conditions.length]; String[] queries = new String[conditions.length];
@ -527,10 +651,17 @@ public class MappingsReader_v1 extends MappingsReader {
String query = String.join(" && ", queries); String query = String.join(" && ", queries);
return String.format("q.block_property('geyser_custom:default') == 0 && %s", query); // 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) { private String[] splitStateString(String state) {
// Split the given state string into an array of property=value pairs
int openBracketIndex = state.indexOf("["); int openBracketIndex = state.indexOf("[");
int closeBracketIndex = state.indexOf("]"); int closeBracketIndex = state.indexOf("]");
@ -541,6 +672,13 @@ public class MappingsReader_v1 extends MappingsReader {
return pairs; return pairs;
} }
/**
* Clamps the given value between the given min and max
* @param value the value to clamp
* @param min the minimum value
* @param max the maximum value
* @return the clamped value
*/
private float clamp(float value, float min, float max) { private float clamp(float value, float min, float max) {
return Math.max(min, Math.min(max, value)); return Math.max(min, Math.min(max, value));
} }

Datei anzeigen

@ -35,6 +35,9 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
public class CustomBlockRegistryPopulator { public class CustomBlockRegistryPopulator {
/**
* Registers all custom blocks defined by extensions and user supplied mappings
*/
public static void registerCustomBedrockBlocks() { public static void registerCustomBedrockBlocks() {
if (!GeyserImpl.getInstance().getConfig().isAddCustomBlocks()) { if (!GeyserImpl.getInstance().getConfig().isAddCustomBlocks()) {
return; return;
@ -69,7 +72,6 @@ public class CustomBlockRegistryPopulator {
} }
CustomBlockState oldBlockState = blockStateOverrides.put(id, customBlockState); CustomBlockState oldBlockState = blockStateOverrides.put(id, customBlockState);
if (oldBlockState != null) { if (oldBlockState != null) {
// TODO should this be an error? Allow extensions to query block state overrides?
GeyserImpl.getInstance().getLogger().debug("Duplicate block state override for Java Identifier: " + GeyserImpl.getInstance().getLogger().debug("Duplicate block state override for Java Identifier: " +
javaIdentifier + " Old override: " + oldBlockState.name() + " New override: " + customBlockState.name()); javaIdentifier + " Old override: " + oldBlockState.name() + " New override: " + customBlockState.name());
} }
@ -101,15 +103,23 @@ public class CustomBlockRegistryPopulator {
}); });
BlockRegistries.CUSTOM_BLOCKS.set(customBlocks.toArray(new CustomBlockData[0])); BlockRegistries.CUSTOM_BLOCKS.set(customBlocks.toArray(new CustomBlockData[0]));
GeyserImpl.getInstance().getLogger().debug("Registered " + customBlocks.size() + " custom blocks."); GeyserImpl.getInstance().getLogger().info("Registered " + customBlocks.size() + " custom blocks.");
BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.set(blockStateOverrides); BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.set(blockStateOverrides);
GeyserImpl.getInstance().getLogger().debug("Registered " + blockStateOverrides.size() + " custom block overrides."); GeyserImpl.getInstance().getLogger().info("Registered " + blockStateOverrides.size() + " custom block overrides.");
BlockRegistries.CUSTOM_BLOCK_ITEM_OVERRIDES.set(customBlockItemOverrides); BlockRegistries.CUSTOM_BLOCK_ITEM_OVERRIDES.set(customBlockItemOverrides);
GeyserImpl.getInstance().getLogger().debug("Registered " + customBlockItemOverrides.size() + " custom block item overrides."); GeyserImpl.getInstance().getLogger().info("Registered " + customBlockItemOverrides.size() + " custom block item overrides.");
} }
/**
* Generates and appends all custom block states to the provided list of custom block states
* Appends the custom block states to the provided list of NBT maps
* @param customBlock the custom block data to generate states for
* @param blockStates the list of NBT maps to append the custom block states to
* @param customExtBlockStates the list of custom block states to append the custom block states to
* @param stateVersion the state version to use for the custom block states
*/
static void generateCustomBlockStates(CustomBlockData customBlock, List<NbtMap> blockStates, List<CustomBlockState> customExtBlockStates, int stateVersion) { static void generateCustomBlockStates(CustomBlockData customBlock, List<NbtMap> blockStates, List<CustomBlockState> customExtBlockStates, int stateVersion) {
int totalPermutations = 1; int totalPermutations = 1;
for (CustomBlockProperty<?> property : customBlock.properties().values()) { for (CustomBlockProperty<?> property : customBlock.properties().values()) {
@ -134,6 +144,12 @@ public class CustomBlockRegistryPopulator {
} }
} }
/**
* Generates and returns the block property data for the provided custom block
* @param customBlock the custom block to generate block property data for
* @param protocolVersion the protocol version to use for the block property data
* @return the block property data for the provided custom block
*/
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
static BlockPropertyData generateBlockPropertyData(CustomBlockData customBlock, int protocolVersion) { static BlockPropertyData generateBlockPropertyData(CustomBlockData customBlock, int protocolVersion) {
List<NbtMap> permutations = new ArrayList<>(); List<NbtMap> permutations = new ArrayList<>();
@ -161,11 +177,15 @@ public class CustomBlockRegistryPopulator {
NbtMap propertyTag = NbtMap.builder() NbtMap propertyTag = NbtMap.builder()
.putCompound("components", CustomBlockRegistryPopulator.convertComponents(customBlock.components(), protocolVersion)) .putCompound("components", CustomBlockRegistryPopulator.convertComponents(customBlock.components(), protocolVersion))
// this is required or the client will crash
// in the future, this can be used to replace items in the creative inventory
// this would require us to map https://wiki.bedrock.dev/documentation/creative-categories.html#for-blocks programatically
.putCompound("menu_category", NbtMap.builder() .putCompound("menu_category", NbtMap.builder()
.putString("category", "none") .putString("category", "none")
.putString("group", "") .putString("group", "")
.putBoolean("is_hidden_in_commands", false) .putBoolean("is_hidden_in_commands", false)
.build()) .build())
// meaning of this version is unknown, but it's required for tags to work and should probably be checked periodically
.putInt("molangVersion", 1) .putInt("molangVersion", 1)
.putList("permutations", NbtType.COMPOUND, permutations) .putList("permutations", NbtType.COMPOUND, permutations)
.putList("properties", NbtType.COMPOUND, properties) .putList("properties", NbtType.COMPOUND, properties)
@ -173,6 +193,12 @@ public class CustomBlockRegistryPopulator {
return new BlockPropertyData(customBlock.identifier(), propertyTag); return new BlockPropertyData(customBlock.identifier(), propertyTag);
} }
/**
* Converts the provided custom block components to an {@link NbtMap} to be sent to the client in the StartGame packet
* @param components the custom block components to convert
* @param protocolVersion the protocol version to use for the conversion
* @return the NBT representation of the provided custom block components
*/
static NbtMap convertComponents(CustomBlockComponents components, int protocolVersion) { static NbtMap convertComponents(CustomBlockComponents components, int protocolVersion) {
if (components == null) { if (components == null) {
return NbtMap.EMPTY; return NbtMap.EMPTY;
@ -206,6 +232,9 @@ public class CustomBlockRegistryPopulator {
.build()); .build());
} }
builder.putCompound("minecraft:material_instances", NbtMap.builder() builder.putCompound("minecraft:material_instances", NbtMap.builder()
// we could read these, but there is no functional reason to use them at the moment
// they only allow you to make aliases for material instances
// but you could already just define the same instance twice if this was really needed
.putCompound("mappings", NbtMap.EMPTY) .putCompound("mappings", NbtMap.EMPTY)
.putCompound("materials", materialsBuilder.build()) .putCompound("materials", materialsBuilder.build())
.build()); .build());
@ -230,6 +259,9 @@ public class CustomBlockRegistryPopulator {
.putInt("value", components.lightEmission()) .putInt("value", components.lightEmission())
.build()); .build());
} }
// This is supposed to be sent as "light_dampening" since "block_light_filter" is the old value
// However, it seems they forgot to actually update it on the network despite all the documentation changing
// So we'll send this for now
if (components.lightDampening() != null) { if (components.lightDampening() != null) {
builder.putCompound("minecraft:block_light_filter", NbtMap.builder() builder.putCompound("minecraft:block_light_filter", NbtMap.builder()
.putByte("value", components.lightDampening().byteValue()) .putByte("value", components.lightDampening().byteValue())
@ -245,6 +277,9 @@ public class CustomBlockRegistryPopulator {
if (components.unitCube()) { if (components.unitCube()) {
builder.putCompound("minecraft:unit_cube", NbtMap.EMPTY); builder.putCompound("minecraft:unit_cube", NbtMap.EMPTY);
} }
// place_air is not an actual component
// We just apply a dummy event to prevent the client from trying to place a block
// This mitigates the issue with the client sometimes double placing blocks
if (components.placeAir()) { if (components.placeAir()) {
builder.putCompound("minecraft:on_player_placing", NbtMap.builder() builder.putCompound("minecraft:on_player_placing", NbtMap.builder()
.putString("triggerType", "geyser:place_event") .putString("triggerType", "geyser:place_event")
@ -256,6 +291,11 @@ public class CustomBlockRegistryPopulator {
return builder.build(); return builder.build();
} }
/**
* Converts the provided box component to an {@link NbtMap}
* @param boxComponent the box component to convert
* @return the NBT representation of the provided box component
*/
private static NbtMap convertBox(BoxComponent boxComponent) { private static NbtMap convertBox(BoxComponent boxComponent) {
return NbtMap.builder() return NbtMap.builder()
.putBoolean("enabled", !boxComponent.isEmpty()) .putBoolean("enabled", !boxComponent.isEmpty())
@ -264,20 +304,30 @@ public class CustomBlockRegistryPopulator {
.build(); .build();
} }
/**
* Converts the provided placement filter to a list of {@link NbtMap}
* @param placementFilter the placement filter to convert
* @return the NBT representation of the provided placement filter
*/
private static List<NbtMap> convertPlacementFilter(PlacementFilter placementFilter) { private static List<NbtMap> convertPlacementFilter(PlacementFilter placementFilter) {
List<NbtMap> conditions = new ArrayList<>(); List<NbtMap> conditions = new ArrayList<>();
placementFilter.conditions().forEach((condition) -> { placementFilter.conditions().forEach((condition) -> {
NbtMapBuilder conditionBuilder = NbtMap.builder(); NbtMapBuilder conditionBuilder = NbtMap.builder();
// allowed_faces on the network is represented by 6 bits for the 6 possible faces
// the enum has the proper values for that face only, so we just bitwise OR them together
byte allowedFaces = 0; byte allowedFaces = 0;
for (Face face : condition.allowedFaces()) { allowedFaces |= face.getValue(); } for (Face face : condition.allowedFaces()) { allowedFaces |= face.getValue(); }
conditionBuilder.putByte("allowed_faces", allowedFaces); conditionBuilder.putByte("allowed_faces", allowedFaces);
// block_filters is a list of either blocks or queries for block tags
// if these match the block the player is trying to place on, the placement is allowed by the client
List <NbtMap> blockFilters = new ArrayList<>(); List <NbtMap> blockFilters = new ArrayList<>();
condition.blockFilters().forEach((value, type) -> { condition.blockFilters().forEach((value, type) -> {
NbtMapBuilder blockFilterBuilder = NbtMap.builder(); NbtMapBuilder blockFilterBuilder = NbtMap.builder();
switch (type) { switch (type) {
case BLOCK -> blockFilterBuilder.putString("name", value); case BLOCK -> blockFilterBuilder.putString("name", value);
// meaning of this version is unknown, but it's required for tags to work and should probably be checked periodically
case TAG -> blockFilterBuilder.putString("tags", value).putInt("tags_version", 6); case TAG -> blockFilterBuilder.putString("tags", value).putInt("tags_version", 6);
} }
blockFilters.add(blockFilterBuilder.build()); blockFilters.add(blockFilterBuilder.build());

Datei anzeigen

@ -38,6 +38,7 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.Constants;
import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserBootstrap;
import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.block.custom.CustomBlockData; import org.geysermc.geyser.api.block.custom.CustomBlockData;
@ -375,6 +376,9 @@ public class ItemRegistryPopulator {
// and the last, if relevant. We then iterate over all those values and get their Bedrock equivalents // and the last, if relevant. We then iterate over all those values and get their Bedrock equivalents
Integer lastBlockRuntimeId = entry.getValue().getLastBlockRuntimeId() == null ? firstBlockRuntimeId : entry.getValue().getLastBlockRuntimeId(); Integer lastBlockRuntimeId = entry.getValue().getLastBlockRuntimeId() == null ? firstBlockRuntimeId : entry.getValue().getLastBlockRuntimeId();
for (int i = firstBlockRuntimeId; i <= lastBlockRuntimeId; i++) { for (int i = firstBlockRuntimeId; i <= lastBlockRuntimeId; i++) {
// For now we opt to preserve the pre custom block phase and just use the vanilla blocks in the creative inventory
// In the future if we get the mappings for categories it we could put the custom blocks in the creative inventory
// This would likely also require changes to recipe handling
int bedrockBlockRuntimeId = blockMappings.getVanillaBedrockBlockId(i); int bedrockBlockRuntimeId = blockMappings.getVanillaBedrockBlockId(i);
NbtMap blockTag = blockMappings.getBedrockBlockStates().get(bedrockBlockRuntimeId); NbtMap blockTag = blockMappings.getBedrockBlockStates().get(bedrockBlockRuntimeId);
String bedrockName = blockTag.getString("name"); String bedrockName = blockTag.getString("name");
@ -504,7 +508,7 @@ public class ItemRegistryPopulator {
for (CustomItemData customItem : customItemsToLoad) { for (CustomItemData customItem : customItemsToLoad) {
int customProtocolId = nextFreeBedrockId++; int customProtocolId = nextFreeBedrockId++;
String customItemName = "geyser_custom:" + customItem.name(); String customItemName = Constants.GEYSER_NAMESPACE + ":" + customItem.name();
if (!registeredItemNames.add(customItemName)) { if (!registeredItemNames.add(customItemName)) {
if (firstMappingsPass) { if (firstMappingsPass) {
GeyserImpl.getInstance().getLogger().error("Custom item name '" + customItem.name() + "' already exists and was registered again! Skipping..."); GeyserImpl.getInstance().getLogger().error("Custom item name '" + customItem.name() + "' already exists and was registered again! Skipping...");