diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockData.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockData.java new file mode 100644 index 000000000..9f142faab --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockData.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.block.custom; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents; +import org.geysermc.geyser.api.block.custom.property.CustomBlockProperty; +import org.geysermc.geyser.api.util.CreativeCategory; + +import java.util.List; +import java.util.Map; + +/** + * This class is used to store data for a custom block. + */ +public interface CustomBlockData { + /** + * Gets the name of the custom block + * + * @return The name of the custom block. + */ + @NonNull String name(); + + /** + * Gets the identifier of the custom block + * + * @return The identifier of the custom block. + */ + @NonNull String identifier(); + + /** + * Gets if the custom block is included in the creative inventory + * + * @return If the custom block is included in the creative inventory. + */ + boolean includedInCreativeInventory(); + + /** + * Gets the item's creative category, or tab id. + * + * @return the item's creative category + */ + @Nullable CreativeCategory creativeCategory(); + + /** + * Gets the item's creative group. + * + * @return the item's creative group + */ + @Nullable String creativeGroup(); + + /** + * Gets the components of the custom block + * + * @return The components of the custom block. + */ + @Nullable CustomBlockComponents components(); + + /** + * Gets the custom block's map of block property names to CustomBlockProperty + * objects + * + * @return The custom block's map of block property names to CustomBlockProperty objects. + */ + @NonNull Map> properties(); + + /** + * Gets the list of the custom block's permutations + * + * @return The permutations of the custom block. + */ + @NonNull List permutations(); + + /** + * Gets the custom block's default block state + * + * @return The default block state of the custom block. + */ + @NonNull CustomBlockState defaultBlockState(); + + /** + * Gets a builder for a custom block state + * + * @return The builder for a custom block state. + */ + CustomBlockState.@NonNull Builder blockStateBuilder(); + + /** + * Create a Builder for CustomBlockData + * + * @return A CustomBlockData Builder + */ + static CustomBlockData.Builder builder() { + return GeyserApi.api().provider(CustomBlockData.Builder.class); + } + + interface Builder { + Builder name(@NonNull String name); + + Builder includedInCreativeInventory(boolean includedInCreativeInventory); + + Builder creativeCategory(@Nullable CreativeCategory creativeCategory); + + Builder creativeGroup(@Nullable String creativeGroup); + + Builder components(@NonNull CustomBlockComponents components); + + Builder booleanProperty(@NonNull String propertyName); + + Builder intProperty(@NonNull String propertyName, List values); + + Builder stringProperty(@NonNull String propertyName, List values); + + Builder permutations(@NonNull List permutations); + + CustomBlockData build(); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockPermutation.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockPermutation.java new file mode 100644 index 000000000..fca39b533 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockPermutation.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.block.custom; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents; + +/** + * This class is used to store a custom block permutations, which contain custom + * block components mapped to a Molang query that should return true or false + * + * @param components The components of the block + * @param condition The Molang query that should return true or false + */ +public record CustomBlockPermutation(@NonNull CustomBlockComponents components, @NonNull String condition) { +} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockState.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockState.java new file mode 100644 index 000000000..70b3c1e2d --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockState.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.block.custom; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Map; + +/** + * This class is used to store a custom block state, which contains CustomBlockData + * tied to defined properties and values + */ +public interface CustomBlockState { + /** + * Gets the custom block data associated with the state + * + * @return The custom block data for the state. + */ + @NonNull CustomBlockData block(); + + /** + * Gets the name of the state + * + * @return The name of the state. + */ + @NonNull String name(); + + /** + * Gets the given property for the state + * + * @param propertyName the property name + * @return the boolean, int, or string property. + */ + @NonNull T property(@NonNull String propertyName); + + /** + * Gets a map of the properties for the state + * + * @return The properties for the state. + */ + @NonNull Map properties(); + + interface Builder { + Builder booleanProperty(@NonNull String propertyName, boolean value); + + Builder intProperty(@NonNull String propertyName, int value); + + Builder stringProperty(@NonNull String propertyName, @NonNull String value); + + CustomBlockState build(); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/NonVanillaCustomBlockData.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/NonVanillaCustomBlockData.java new file mode 100644 index 000000000..4ab186ce6 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/NonVanillaCustomBlockData.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.block.custom; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents; +import org.geysermc.geyser.api.util.CreativeCategory; + +import java.util.List; + +/** + * Represents a completely custom block that is not based on an existing vanilla Minecraft block. + */ +public interface NonVanillaCustomBlockData extends CustomBlockData { + /** + * Gets the namespace of the custom block + * + * @return The namespace of the custom block. + */ + @NonNull String namespace(); + + + /** + * Create a Builder for NonVanillaCustomBlockData + * + * @return A NonVanillaCustomBlockData Builder + */ + static NonVanillaCustomBlockData.Builder builder() { + return GeyserApi.api().provider(NonVanillaCustomBlockData.Builder.class); + } + + interface Builder extends CustomBlockData.Builder { + + Builder namespace(@NonNull String namespace); + + @Override + Builder name(@NonNull String name); + + @Override + Builder includedInCreativeInventory(boolean includedInCreativeInventory); + + @Override + Builder creativeCategory(@Nullable CreativeCategory creativeCategory); + + @Override + Builder creativeGroup(@Nullable String creativeGroup); + + @Override + Builder components(@NonNull CustomBlockComponents components); + + @Override + Builder booleanProperty(@NonNull String propertyName); + + @Override + Builder intProperty(@NonNull String propertyName, List values); + + @Override + Builder stringProperty(@NonNull String propertyName, List values); + + @Override + Builder permutations(@NonNull List permutations); + + @Override + NonVanillaCustomBlockData build(); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/component/BoxComponent.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/BoxComponent.java new file mode 100644 index 000000000..0b577742d --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/BoxComponent.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.block.custom.component; + +/** + * This class is used to store a box component for the selection and + * collision boxes of a custom block. + * + * @param originX The origin X of the box + * @param originY The origin Y of the box + * @param originZ The origin Z of the box + * @param sizeX The size X of the box + * @param sizeY The size Y of the box + * @param sizeZ The size Z of the box + */ +public record BoxComponent(float originX, float originY, float originZ, float sizeX, float sizeY, float sizeZ) { + private static final BoxComponent FULL_BOX = new BoxComponent(-8, 0, -8, 16, 16, 16); + private static final BoxComponent EMPTY_BOX = new BoxComponent(0, 0, 0, 0, 0, 0); + + /** + * Gets a full box component + * + * @return A full box component + */ + public static BoxComponent fullBox() { + return FULL_BOX; + } + + /** + * Gets an empty box component + * + * @return An empty box component + */ + public static BoxComponent emptyBox() { + return EMPTY_BOX; + } + + /** + * Gets if the box component is empty + * + * @return If the box component is empty. + */ + public boolean isEmpty() { + return sizeX == 0 && sizeY == 0 && sizeZ == 0; + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/component/CustomBlockComponents.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/CustomBlockComponents.java new file mode 100644 index 000000000..036723092 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/CustomBlockComponents.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.block.custom.component; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.GeyserApi; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * This class is used to store components for a custom block or custom block permutation. + */ +public interface CustomBlockComponents { + + /** + * Gets the selection box component + * Equivalent to "minecraft:selection_box" + * + * @return The selection box. + */ + @Nullable BoxComponent selectionBox(); + + /** + * Gets the collision box component + * Equivalent to "minecraft:collision_box" + * @return The collision box. + */ + @Nullable BoxComponent collisionBox(); + + /** + * Gets the display name component + * Equivalent to "minecraft:display_name" + * + * @return The display name. + */ + @Nullable String displayName(); + + /** + * Gets the geometry component + * Equivalent to "minecraft:geometry" + * + * @return The geometry. + */ + @Nullable GeometryComponent geometry(); + + /** + * Gets the material instances component + * Equivalent to "minecraft:material_instances" + * + * @return The material instances. + */ + @NonNull Map materialInstances(); + + /** + * Gets the placement filter component + * Equivalent to "minecraft:placement_filter" + * + * @return The placement filter. + */ + @Nullable List placementFilter(); + + /** + * Gets the destructible by mining component + * Equivalent to "minecraft:destructible_by_mining" + * + * @return The destructible by mining value. + */ + @Nullable Float destructibleByMining(); + + /** + * Gets the friction component + * Equivalent to "minecraft:friction" + * + * @return The friction value. + */ + @Nullable Float friction(); + + /** + * Gets the light emission component + * Equivalent to "minecraft:light_emission" + * + * @return The light emission value. + */ + @Nullable Integer lightEmission(); + + /** + * Gets the light dampening component + * Equivalent to "minecraft:light_dampening" + * + * @return The light dampening value. + */ + @Nullable Integer lightDampening(); + + /** + * Gets the transformation component + * Equivalent to "minecraft:transformation" + * + * @return The transformation. + */ + @Nullable TransformationComponent transformation(); + + /** + * Gets the unit cube component + * Equivalent to "minecraft:unit_cube" + * + * @return The rotation. + */ + boolean unitCube(); + + /** + * Gets if the block should place only air + * Equivalent to setting a dummy event to run on "minecraft:on_player_placing" + * + * @return If the block should place only air. + */ + boolean placeAir(); + + /** + * Gets the set of tags + * Equivalent to "tag:some_tag" + * + * @return The set of tags. + */ + @NonNull Set tags(); + + /** + * Create a Builder for CustomBlockComponents + * + * @return A CustomBlockComponents Builder + */ + static CustomBlockComponents.Builder builder() { + return GeyserApi.api().provider(CustomBlockComponents.Builder.class); + } + + interface Builder { + Builder selectionBox(BoxComponent selectionBox); + + Builder collisionBox(BoxComponent collisionBox); + + Builder displayName(String displayName); + + Builder geometry(GeometryComponent geometry); + + Builder materialInstance(@NonNull String name, @NonNull MaterialInstance materialInstance); + + Builder placementFilter(List placementConditions); + + Builder destructibleByMining(Float destructibleByMining); + + Builder friction(Float friction); + + Builder lightEmission(Integer lightEmission); + + Builder lightDampening(Integer lightDampening); + + Builder transformation(TransformationComponent transformation); + + Builder unitCube(boolean unitCube); + + Builder placeAir(boolean placeAir); + + Builder tags(Set tags); + + CustomBlockComponents build(); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/component/GeometryComponent.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/GeometryComponent.java new file mode 100644 index 000000000..6d85989a7 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/GeometryComponent.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.block.custom.component; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.GeyserApi; + +import java.util.Map; + +/** + * This class is used to store data for a geometry component. + */ +public interface GeometryComponent { + + /** + * Gets the identifier of the geometry + * + * @return The identifier of the geometry. + */ + @NonNull String identifier(); + + /** + * Gets the bone visibility of the geometry + * + * @return The bone visibility of the geometry. + */ + @Nullable Map boneVisibility(); + + /** + * Creates a builder for GeometryComponent + * + * @return a builder for GeometryComponent. + */ + static GeometryComponent.Builder builder() { + return GeyserApi.api().provider(GeometryComponent.Builder.class); + } + + interface Builder { + Builder identifier(@NonNull String identifier); + + Builder boneVisibility(@Nullable Map boneVisibility); + + GeometryComponent build(); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/component/MaterialInstance.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/MaterialInstance.java new file mode 100644 index 000000000..26edab2f6 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/MaterialInstance.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.block.custom.component; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.GeyserApi; + +/** + * This class is used to store data for a material instance. + */ +public interface MaterialInstance { + /** + * Gets the texture of the block + * + * @return The texture of the block. + */ + @NonNull String texture(); + + /** + * Gets the render method of the block + * + * @return The render method of the block. + */ + @Nullable String renderMethod(); + + /** + * Gets if the block should be dimmed on certain faces + * + * @return If the block should be dimmed on certain faces. + */ + @Nullable boolean faceDimming(); + + /** + * Gets if the block should have ambient occlusion + * + * @return If the block should have ambient occlusion. + */ + @Nullable boolean ambientOcclusion(); + + /** + * Creates a builder for MaterialInstance. + * + * @return a builder for MaterialInstance + */ + static MaterialInstance.Builder builder() { + return GeyserApi.api().provider(MaterialInstance.Builder.class); + } + + interface Builder { + Builder texture(@NonNull String texture); + + Builder renderMethod(@Nullable String renderMethod); + + Builder faceDimming(@Nullable boolean faceDimming); + + Builder ambientOcclusion(@Nullable boolean ambientOcclusion); + + MaterialInstance build(); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/component/PlacementConditions.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/PlacementConditions.java new file mode 100644 index 000000000..d06d9a967 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/PlacementConditions.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser +*/ + +package org.geysermc.geyser.api.block.custom.component; + +import java.util.LinkedHashMap; +import java.util.Set; + +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * This class is used to store conditions for a placement filter for a custom block. + * + * @param allowedFaces The faces that the block can be placed on + * @param blockFilters The block filters that control what blocks the block can be placed on + */ +public record PlacementConditions(@NonNull Set allowedFaces, @NonNull LinkedHashMap blockFilters) { + public enum Face { + DOWN, + UP, + NORTH, + SOUTH, + WEST, + EAST; + } + + public enum BlockFilterType { + BLOCK, + TAG + } +} \ No newline at end of file diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/component/TransformationComponent.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/TransformationComponent.java new file mode 100644 index 000000000..d52565e22 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/TransformationComponent.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.block.custom.component; + +/** + * This class is used to store the transformation component of a block + * + * @param rx The rotation on the x axis + * @param ry The rotation on the y axis + * @param rz The rotation on the z axis + * @param sx The scale on the x axis + * @param sy The scale on the y axis + * @param sz The scale on the z axis + * @param tx The translation on the x axis + * @param ty The translation on the y axis + * @param tz The translation on the z axis + */ +public record TransformationComponent(int rx, int ry, int rz, float sx, float sy, float sz, float tx, float ty, float tz) { + + /** + * Constructs a new TransformationComponent with the rotation values and assumes default scale and translation + * + * @param rx The rotation on the x axis + * @param ry The rotation on the y axis + * @param rz The rotation on the z axis + */ + public TransformationComponent(int rx, int ry, int rz) { + this(rx, ry, rz, 1, 1, 1, 0, 0, 0); + } + + /** + * Constructs a new TransformationComponent with the rotation and scale values and assumes default translation + * + * @param rx The rotation on the x axis + * @param ry The rotation on the y axis + * @param rz The rotation on the z axis + * @param sx The scale on the x axis + * @param sy The scale on the y axis + * @param sz The scale on the z axis + */ + public TransformationComponent(int rx, int ry, int rz, float sx, float sy, float sz) { + this(rx, ry, rz, sx, sy, sz, 0, 0, 0); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockItem.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockItem.java new file mode 100644 index 000000000..5143246d9 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockItem.java @@ -0,0 +1,7 @@ +package org.geysermc.geyser.api.block.custom.nonvanilla; + +import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.checker.nullness.qual.NonNull; + +public record JavaBlockItem(@NonNull String identifier, @NonNegative int javaId, @NonNegative int stackSize) { +} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockState.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockState.java new file mode 100644 index 000000000..6293506a8 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockState.java @@ -0,0 +1,111 @@ +package org.geysermc.geyser.api.block.custom.nonvanilla; + +import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.GeyserApi; + +public interface JavaBlockState { + /** + * Gets the identifier of the block state + * + * @return the identifier of the block state + */ + @NonNull String identifier(); + + /** + * Gets the Java ID of the block state + * + * @return the Java ID of the block state + */ + @NonNegative int javaId(); + + /** + * Gets the state group ID of the block state + * + * @return the state group ID of the block state + */ + @NonNegative int stateGroupId(); + + /** + * Gets the block hardness of the block state + * + * @return the block hardness of the block state + */ + @NonNegative float blockHardness(); + + /** + * Gets whether the block state is waterlogged + * + * @return whether the block state is waterlogged + */ + @NonNull boolean waterlogged(); + + /** + * Gets the collision of the block state + * + * @return the collision of the block state + */ + @NonNull JavaBoundingBox[] collision(); + + /** + * Gets whether the block state can be broken with hand + * + * @return whether the block state can be broken with hand + */ + @NonNull boolean canBreakWithHand(); + + /** + * Gets the pick item of the block state + * + * @return the pick item of the block state + */ + @Nullable String pickItem(); + + /** + * Gets the piston behavior of the block state + * + * @return the piston behavior of the block state + */ + @Nullable String pistonBehavior(); + + /** + * Gets whether the block state has block entity + * + * @return whether the block state has block entity + */ + @Nullable boolean hasBlockEntity(); + + /** + * Creates a new {@link JavaBlockState.Builder} instance + * + * @return a new {@link JavaBlockState.Builder} instance + */ + static JavaBlockState.Builder builder() { + return GeyserApi.api().provider(JavaBlockState.Builder.class); + } + + interface Builder { + Builder identifier(@NonNull String identifier); + + Builder javaId(@NonNegative int javaId); + + Builder stateGroupId(@NonNegative int stateGroupId); + + Builder blockHardness(@NonNegative float blockHardness); + + Builder waterlogged(@NonNull boolean waterlogged); + + Builder collision(@NonNull JavaBoundingBox[] collision); + + Builder canBreakWithHand(@NonNull boolean canBreakWithHand); + + Builder pickItem(@Nullable String pickItem); + + Builder pistonBehavior(@Nullable String pistonBehavior); + + Builder hasBlockEntity(@Nullable boolean hasBlockEntity); + + JavaBlockState build(); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBoundingBox.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBoundingBox.java new file mode 100644 index 000000000..56a4ca3da --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBoundingBox.java @@ -0,0 +1,6 @@ +package org.geysermc.geyser.api.block.custom.nonvanilla; + +import org.checkerframework.checker.nullness.qual.NonNull; + +public record JavaBoundingBox(@NonNull double middleX, @NonNull double middleY, @NonNull double middleZ, @NonNull double sizeX, @NonNull double sizeY, @NonNull double sizeZ) { +} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/property/CustomBlockProperty.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/property/CustomBlockProperty.java new file mode 100644 index 000000000..fc39c4663 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/property/CustomBlockProperty.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.block.custom.property; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.List; + +/** + * This class is used to store a property of a custom block of a generic type. + */ +public interface CustomBlockProperty { + /** + * Gets the name of the property + * + * @return The name of the property. + */ + @NonNull String name(); + + /** + * Gets the values of the property + * + * @return The values of the property. + */ + @NonNull List values(); + + /** + * Gets the type of the property + * + * @return The type of the property. + */ + @NonNull PropertyType type(); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/property/PropertyType.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/property/PropertyType.java new file mode 100644 index 000000000..48f25c36c --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/property/PropertyType.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.block.custom.property; + +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * This class is used to define a custom block property's type. + */ +public class PropertyType { + private static final PropertyType BOOLEAN = new PropertyType(Boolean.class); + private static final PropertyType INTEGER = new PropertyType(Integer.class); + private static final PropertyType STRING = new PropertyType(String.class); + + /** + * Gets the property type for a boolean. + * + * @return The property type for a boolean. + */ + @NonNull public static PropertyType booleanProp() { + return BOOLEAN; + } + + /** + * Gets the property type for an integer. + * + * @return The property type for an integer. + */ + @NonNull public static PropertyType integerProp() { + return INTEGER; + } + + /** + * Gets the property type for a string. + * + * @return The property type for a string. + */ + @NonNull public static PropertyType stringProp() { + return STRING; + } + + private final Class typeClass; + + /** + * Gets the class of the property type + * + * @return The class of the property type. + */ + @NonNull public Class typeClass() { + return typeClass; + } + + private PropertyType(Class typeClass) { + this.typeClass = typeClass; + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomBlocksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomBlocksEvent.java new file mode 100644 index 000000000..b1a01d7e6 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomBlocksEvent.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.event.lifecycle; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.block.custom.CustomBlockData; +import org.geysermc.geyser.api.block.custom.CustomBlockState; +import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockItem; +import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockState; +import org.geysermc.event.Event; + +/** + * Called on Geyser's startup when looking for custom blocks. Custom blocks must be registered through this event. + * + * This event will not be called if the "add-non-bedrock-items" setting is disabled in the Geyser config. + */ +public abstract class GeyserDefineCustomBlocksEvent implements Event { + /** + * Registers the given {@link CustomBlockData} as a custom block + * + * @param customBlockData the custom block to register + */ + public abstract void register(@NonNull CustomBlockData customBlockData); + + /** + * Registers the given {@link CustomBlockState} as an override for the + * given java state identifier + * Java state identifiers are listed in + * https://raw.githubusercontent.com/GeyserMC/mappings/master/blocks.json + * + * @param javaIdentifier the java state identifier to override + * @param customBlockState the custom block state with which to override java state identifier + */ + public abstract void registerOverride(@NonNull String javaIdentifier, @NonNull CustomBlockState customBlockState); + + /** + * Registers the given {@link CustomBlockData} as an override for the + * given java item identifier + * + * @param javaIdentifier the java item identifier to override + * @param customBlockData the custom block data with which to override java item identifier + */ + public abstract void registerItemOverride(@NonNull String javaIdentifier, @NonNull CustomBlockData customBlockData); + + /** + * Registers the given {@link CustomBlockState} as an override for the + * given {@link JavaBlockState} + * + * @param javaBlockState the java block state for the non-vanilla block + * @param customBlockState the custom block state with which to override java state identifier + */ + public abstract void registerOverride(@NonNull JavaBlockState javaBlockState, @NonNull CustomBlockState customBlockState); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomSkullsEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomSkullsEvent.java new file mode 100644 index 000000000..17f7b599a --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomSkullsEvent.java @@ -0,0 +1,28 @@ +package org.geysermc.geyser.api.event.lifecycle; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.event.Event; + +/** + * Called on Geyser's startup when looking for custom skulls. Custom skulls must be registered through this event. + * + * This event will not be called if the "add-non-bedrock-items" setting is disabled in the Geyser config. + */ +public abstract class GeyserDefineCustomSkullsEvent implements Event { + /** + * The type of texture provided + */ + public enum SkullTextureType { + USERNAME, + UUID, + PROFILE, + SKIN_HASH + } + + /** + * Registers the given username, UUID, base64 encoded profile, or skin hash as a custom skull blocks + * @param texture the username, UUID, base64 encoded profile, or skin hash + * @param type the type of texture provided + */ + public abstract void register(@NonNull String texture, @NonNull SkullTextureType type); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/util/CreativeCategory.java b/api/src/main/java/org/geysermc/geyser/api/util/CreativeCategory.java new file mode 100644 index 000000000..7519ff157 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/util/CreativeCategory.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.util; + +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Represents the creative menu categories or tabs. + */ +public enum CreativeCategory { + COMMANDS("commands", 1), + CONSTRUCTION("construction", 2), + EQUIPMENT("equipment", 3), + ITEMS("items", 4), + NATURE("nature", 5), + NONE("none", 6); + + private final String internalName; + private final int id; + + CreativeCategory(String internalName, int id) { + this.internalName = internalName; + this.id = id; + } + + /** + * Gets the internal name of the category. + * + * @return the name of the category + */ + @NonNull public String internalName() { + return internalName; + } + + /** + * Gets the internal ID of the category. + * + * @return the ID of the category + */ + public int id() { + return id; + } +} diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index 3d1fb47f7..c0c683920 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -8,7 +8,7 @@ repositories { } dependencies { - implementation("net.kyori", "indra-common", "3.0.1") + implementation("net.kyori", "indra-common", "3.1.1") implementation("com.github.johnrengelman", "shadow", "7.1.3-SNAPSHOT") // Within the gradle plugin classpath, there is a version conflict between loom and some other diff --git a/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts b/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts index 709867300..93d702939 100644 --- a/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts @@ -34,4 +34,4 @@ tasks { ) } } -} +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 2eed8de91..4cb5b0223 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,9 +5,15 @@ plugins { } allprojects { - group = "org.geysermc.geyser" - version = "2.1.2-SNAPSHOT" - description = "Allows for players from Minecraft: Bedrock Edition to join Minecraft: Java Edition servers." + group = properties["group"] as String + "." + properties["id"] as String + version = properties["version"] as String + description = properties["description"] as String +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(16)) + } } val platforms = setOf( diff --git a/core/src/main/java/org/geysermc/geyser/Constants.java b/core/src/main/java/org/geysermc/geyser/Constants.java index 12e8b0d8c..5de8e6e6b 100644 --- a/core/src/main/java/org/geysermc/geyser/Constants.java +++ b/core/src/main/java/org/geysermc/geyser/Constants.java @@ -41,6 +41,10 @@ public final class Constants { static final String SAVED_REFRESH_TOKEN_FILE = "saved-refresh-tokens.json"; + public static final String GEYSER_CUSTOM_NAMESPACE = "geyser_custom"; + + public static final String MINECRAFT_SKIN_SERVER_URL = "https://textures.minecraft.net/texture/"; + static { URI wsUri = null; try { diff --git a/core/src/main/java/org/geysermc/geyser/configuration/GeyserCustomSkullConfiguration.java b/core/src/main/java/org/geysermc/geyser/configuration/GeyserCustomSkullConfiguration.java new file mode 100644 index 000000000..1af3578a3 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserCustomSkullConfiguration.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.configuration; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +@JsonIgnoreProperties(ignoreUnknown = true) +@SuppressWarnings("FieldMayBeFinal") // Jackson requires that the fields are not final +public class GeyserCustomSkullConfiguration { + @JsonProperty("player-usernames") + private List playerUsernames; + + @JsonProperty("player-uuids") + private List playerUUIDs; + + @JsonProperty("player-profiles") + private List playerProfiles; + + @JsonProperty("skin-hashes") + private List skinHashes; + + public List getPlayerUsernames() { + return Objects.requireNonNullElse(playerUsernames, Collections.emptyList()); + } + + public List getPlayerUUIDs() { + return Objects.requireNonNullElse(playerUUIDs, Collections.emptyList()); + } + + public List getPlayerProfiles() { + return Objects.requireNonNullElse(playerProfiles, Collections.emptyList()); + } + + public List getPlayerSkinHashes() { + return Objects.requireNonNullElse(skinHashes, Collections.emptyList()); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/inventory/holder/BlockInventoryHolder.java b/core/src/main/java/org/geysermc/geyser/inventory/holder/BlockInventoryHolder.java index 01365be34..c81fe6598 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/holder/BlockInventoryHolder.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/holder/BlockInventoryHolder.java @@ -77,15 +77,18 @@ public class BlockInventoryHolder extends InventoryHolder { // (This could be a virtual inventory that the player is opening) if (checkInteractionPosition(session)) { // Then, check to see if the interacted block is valid for this inventory by ensuring the block state identifier is valid + // and the bedrock block is vanilla int javaBlockId = session.getGeyser().getWorldManager().getBlockAt(session, session.getLastInteractionBlockPosition()); - String[] javaBlockString = BlockRegistries.JAVA_BLOCKS.getOrDefault(javaBlockId, BlockMapping.AIR).getJavaIdentifier().split("\\["); - if (isValidBlock(javaBlockString)) { - // We can safely use this block - inventory.setHolderPosition(session.getLastInteractionBlockPosition()); - ((Container) inventory).setUsingRealBlock(true, javaBlockString[0]); - setCustomName(session, session.getLastInteractionBlockPosition(), inventory, javaBlockId); + if (!BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.get().containsKey(javaBlockId)) { + String[] javaBlockString = BlockRegistries.JAVA_BLOCKS.getOrDefault(javaBlockId, BlockMapping.AIR).getJavaIdentifier().split("\\["); + if (isValidBlock(javaBlockString)) { + // We can safely use this block + inventory.setHolderPosition(session.getLastInteractionBlockPosition()); + ((Container) inventory).setUsingRealBlock(true, javaBlockString[0]); + setCustomName(session, session.getLastInteractionBlockPosition(), inventory, javaBlockId); - return true; + return true; + } } } @@ -97,7 +100,7 @@ public class BlockInventoryHolder extends InventoryHolder { UpdateBlockPacket blockPacket = new UpdateBlockPacket(); blockPacket.setDataLayer(0); blockPacket.setBlockPosition(position); - blockPacket.setDefinition(session.getBlockMappings().getBedrockBlock(defaultJavaBlockState)); + blockPacket.setDefinition(session.getBlockMappings().getVanillaBedrockBlock(defaultJavaBlockState)); blockPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY); session.sendUpstreamPacket(blockPacket); inventory.setHolderPosition(position); diff --git a/core/src/main/java/org/geysermc/geyser/item/mappings/versions/MappingsReader_v1.java b/core/src/main/java/org/geysermc/geyser/item/mappings/versions/MappingsReader_v1.java deleted file mode 100644 index e08190aff..000000000 --- a/core/src/main/java/org/geysermc/geyser/item/mappings/versions/MappingsReader_v1.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Geyser - */ - -package org.geysermc.geyser.item.mappings.versions; - -import com.fasterxml.jackson.databind.JsonNode; -import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.api.item.custom.CustomItemData; -import org.geysermc.geyser.api.item.custom.CustomItemOptions; -import org.geysermc.geyser.item.exception.InvalidCustomMappingsFileException; - -import java.nio.file.Path; -import java.util.function.BiConsumer; - -public class MappingsReader_v1 extends MappingsReader { - @Override - public void readMappings(Path file, JsonNode mappingsRoot, BiConsumer consumer) { - this.readItemMappings(file, mappingsRoot, consumer); - } - - public void readItemMappings(Path file, JsonNode mappingsRoot, BiConsumer consumer) { - JsonNode itemsNode = mappingsRoot.get("items"); - - if (itemsNode != null && itemsNode.isObject()) { - itemsNode.fields().forEachRemaining(entry -> { - if (entry.getValue().isArray()) { - entry.getValue().forEach(data -> { - try { - CustomItemData customItemData = this.readItemMappingEntry(data); - consumer.accept(entry.getKey(), customItemData); - } catch (InvalidCustomMappingsFileException e) { - GeyserImpl.getInstance().getLogger().error("Error in custom mapping file: " + file.toString(), e); - } - }); - } - }); - } - } - - private CustomItemOptions readItemCustomItemOptions(JsonNode node) { - CustomItemOptions.Builder customItemOptions = CustomItemOptions.builder(); - - JsonNode customModelData = node.get("custom_model_data"); - if (customModelData != null && customModelData.isInt()) { - customItemOptions.customModelData(customModelData.asInt()); - } - - JsonNode damagePredicate = node.get("damage_predicate"); - if (damagePredicate != null && damagePredicate.isInt()) { - customItemOptions.damagePredicate(damagePredicate.asInt()); - } - - JsonNode unbreakable = node.get("unbreakable"); - if (unbreakable != null && unbreakable.isBoolean()) { - customItemOptions.unbreakable(unbreakable.asBoolean()); - } - - JsonNode defaultItem = node.get("default"); - if (defaultItem != null && defaultItem.isBoolean()) { - customItemOptions.defaultItem(defaultItem.asBoolean()); - } - - return customItemOptions.build(); - } - - @Override - public CustomItemData readItemMappingEntry(JsonNode node) throws InvalidCustomMappingsFileException { - if (node == null || !node.isObject()) { - throw new InvalidCustomMappingsFileException("Invalid item mappings entry"); - } - - JsonNode name = node.get("name"); - if (name == null || !name.isTextual() || name.asText().isEmpty()) { - throw new InvalidCustomMappingsFileException("An item entry has no name"); - } - - CustomItemData.Builder customItemData = CustomItemData.builder() - .name(name.asText()) - .customItemOptions(this.readItemCustomItemOptions(node)); - - //The next entries are optional - if (node.has("display_name")) { - customItemData.displayName(node.get("display_name").asText()); - } - - if (node.has("icon")) { - customItemData.icon(node.get("icon").asText()); - } - - if (node.has("allow_offhand")) { - customItemData.allowOffhand(node.get("allow_offhand").asBoolean()); - } - - if (node.has("display_handheld")) { - customItemData.displayHandheld(node.get("display_handheld").asBoolean()); - } - - if (node.has("texture_size")) { - customItemData.textureSize(node.get("texture_size").asInt()); - } - - if (node.has("render_offsets")) { - JsonNode tmpNode = node.get("render_offsets"); - - customItemData.renderOffsets(fromJsonNode(tmpNode)); - } - - return customItemData.build(); - } -} diff --git a/core/src/main/java/org/geysermc/geyser/item/type/PlayerHeadItem.java b/core/src/main/java/org/geysermc/geyser/item/type/PlayerHeadItem.java index 5c985c732..662448a52 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/PlayerHeadItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/PlayerHeadItem.java @@ -32,6 +32,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.MinecraftLocale; +import org.geysermc.geyser.translator.text.MessageTranslator; public class PlayerHeadItem extends Item { public PlayerHeadItem(String javaIdentifier, Builder builder) { @@ -42,29 +43,35 @@ public class PlayerHeadItem extends Item { public void translateNbtToBedrock(@NonNull GeyserSession session, @NonNull CompoundTag tag) { super.translateNbtToBedrock(session, tag); - Tag display = tag.get("display"); - if (!(display instanceof CompoundTag) || !((CompoundTag) display).contains("Name")) { - Tag skullOwner = tag.get("SkullOwner"); - if (skullOwner != null) { + CompoundTag displayTag; + if (tag.get("display") instanceof CompoundTag existingDisplayTag) { + displayTag = existingDisplayTag; + } else { + displayTag = new CompoundTag("display"); + tag.put(displayTag); + } + + if (displayTag.get("Name") instanceof StringTag nameTag) { + // Custom names are always yellow and italic + displayTag.put(new StringTag("Name", ChatColor.YELLOW + ChatColor.ITALIC + MessageTranslator.convertMessageLenient(nameTag.getValue(), session.locale()))); + } else { + if (tag.contains("SkullOwner")) { StringTag name; - if (skullOwner instanceof StringTag) { - name = (StringTag) skullOwner; + Tag skullOwner = tag.get("SkullOwner"); + if (skullOwner instanceof StringTag skullName) { + name = skullName; } else { - StringTag skullName; - if (skullOwner instanceof CompoundTag && (skullName = ((CompoundTag) skullOwner).get("Name")) != null) { + if (skullOwner instanceof CompoundTag && ((CompoundTag) skullOwner).get("Name") instanceof StringTag skullName) { name = skullName; } else { - session.getGeyser().getLogger().debug("Not sure how to handle skull head item display. " + tag); + // No name found so default to "Player Head" + displayTag.put(new StringTag("Name", ChatColor.RESET + ChatColor.YELLOW + MinecraftLocale.getLocaleString("block.minecraft.player_head", session.locale()))); return; } } // Add correct name of player skull - // TODO: It's always yellow, even with a custom name. Handle? String displayName = ChatColor.RESET + ChatColor.YELLOW + MinecraftLocale.getLocaleString("block.minecraft.player_head.named", session.locale()).replace("%s", name.getValue()); - if (!(display instanceof CompoundTag)) { - tag.put(display = new CompoundTag("display")); - } - ((CompoundTag) display).put(new StringTag("Name", displayName)); + displayTag.put(new StringTag("Name", displayName)); } } } diff --git a/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockComponents.java b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockComponents.java new file mode 100644 index 000000000..e43e168ee --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockComponents.java @@ -0,0 +1,305 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.level.block; + +import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMaps; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import lombok.Value; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.block.custom.component.BoxComponent; +import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents; +import org.geysermc.geyser.api.block.custom.component.GeometryComponent; +import org.geysermc.geyser.api.block.custom.component.MaterialInstance; +import org.geysermc.geyser.api.block.custom.component.PlacementConditions; +import org.geysermc.geyser.api.block.custom.component.TransformationComponent; +import org.jetbrains.annotations.NotNull; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Value +public class GeyserCustomBlockComponents implements CustomBlockComponents { + BoxComponent selectionBox; + BoxComponent collisionBox; + String displayName; + GeometryComponent geometry; + Map materialInstances; + List placementFilter; + Float destructibleByMining; + Float friction; + Integer lightEmission; + Integer lightDampening; + TransformationComponent transformation; + boolean unitCube; + boolean placeAir; + Set tags; + + private GeyserCustomBlockComponents(CustomBlockComponentsBuilder builder) { + this.selectionBox = builder.selectionBox; + this.collisionBox = builder.collisionBox; + this.displayName = builder.displayName; + this.geometry = builder.geometry; + if (builder.materialInstances.isEmpty()) { + this.materialInstances = Object2ObjectMaps.emptyMap(); + } else { + this.materialInstances = Object2ObjectMaps.unmodifiable(new Object2ObjectArrayMap<>(builder.materialInstances)); + } + this.placementFilter = builder.placementFilter; + this.destructibleByMining = builder.destructibleByMining; + this.friction = builder.friction; + this.lightEmission = builder.lightEmission; + this.lightDampening = builder.lightDampening; + this.transformation = builder.transformation; + this.unitCube = builder.unitCube; + this.placeAir = builder.placeAir; + if (builder.tags.isEmpty()) { + this.tags = Set.of(); + } else { + this.tags = Set.copyOf(builder.tags); + } + } + + @Override + public BoxComponent selectionBox() { + return selectionBox; + } + + @Override + public BoxComponent collisionBox() { + return collisionBox; + } + + @Override + public String displayName() { + return displayName; + } + + @Override + public GeometryComponent geometry() { + return geometry; + } + + @Override + public @NonNull Map materialInstances() { + return materialInstances; + } + + @Override + public List placementFilter() { + return placementFilter; + } + + @Override + public Float destructibleByMining() { + return destructibleByMining; + } + + @Override + public Float friction() { + return friction; + } + + @Override + public Integer lightEmission() { + return lightEmission; + } + + @Override + public Integer lightDampening() { + return lightDampening; + } + + @Override + public TransformationComponent transformation() { + return transformation; + } + + @Override + public boolean unitCube() { + return unitCube; + } + + @Override + public boolean placeAir() { + return placeAir; + } + + @Override + public @NotNull Set tags() { + return tags; + } + + public static class CustomBlockComponentsBuilder implements Builder { + protected BoxComponent selectionBox; + protected BoxComponent collisionBox; + protected String displayName; + protected GeometryComponent geometry; + protected final Object2ObjectMap materialInstances = new Object2ObjectOpenHashMap<>(); + protected List placementFilter; + protected Float destructibleByMining; + protected Float friction; + protected Integer lightEmission; + protected Integer lightDampening; + protected TransformationComponent transformation; + protected boolean unitCube = false; + protected boolean placeAir = false; + protected final Set tags = new HashSet<>(); + + private void validateBox(BoxComponent box) { + if (box == null) { + return; + } + if (box.sizeX() < 0 || box.sizeY() < 0 || box.sizeZ() < 0) { + throw new IllegalArgumentException("Box size must be non-negative."); + } + float minX = box.originX() + 8; + float minY = box.originY(); + float minZ = box.originZ() + 8; + float maxX = minX + box.sizeX(); + float maxY = minY + box.sizeY(); + float maxZ = minZ + box.sizeZ(); + if (minX < 0 || minY < 0 || minZ < 0 || maxX > 16 || maxY > 16 || maxZ > 16) { + throw new IllegalArgumentException("Box bounds must be within (0, 0, 0) and (16, 16, 16). Recieved: (" + minX + ", " + minY + ", " + minZ + ") to (" + maxX + ", " + maxY + ", " + maxZ + ")"); + } + } + + @Override + public Builder selectionBox(BoxComponent selectionBox) { + validateBox(selectionBox); + this.selectionBox = selectionBox; + return this; + } + + @Override + public Builder collisionBox(BoxComponent collisionBox) { + validateBox(collisionBox); + this.collisionBox = collisionBox; + return this; + } + + @Override + public Builder displayName(String displayName) { + this.displayName = displayName; + return this; + } + + @Override + public Builder geometry(GeometryComponent geometry) { + this.geometry = geometry; + return this; + } + + @Override + public Builder materialInstance(@NotNull String name, @NotNull MaterialInstance materialInstance) { + this.materialInstances.put(name, materialInstance); + return this; + } + + @Override + public Builder placementFilter(List placementFilter) { + this.placementFilter = placementFilter; + return this; + } + + @Override + public Builder destructibleByMining(Float destructibleByMining) { + if (destructibleByMining != null && destructibleByMining < 0) { + throw new IllegalArgumentException("Destructible by mining must be non-negative"); + } + this.destructibleByMining = destructibleByMining; + return this; + } + + @Override + public Builder friction(Float friction) { + if (friction != null) { + if (friction < 0 || friction > 1) { + throw new IllegalArgumentException("Friction must be in the range 0-1"); + } + } + this.friction = friction; + return this; + } + + @Override + public Builder lightEmission(Integer lightEmission) { + if (lightEmission != null) { + if (lightEmission < 0 || lightEmission > 15) { + throw new IllegalArgumentException("Light emission must be in the range 0-15"); + } + } + this.lightEmission = lightEmission; + return this; + } + + @Override + public Builder lightDampening(Integer lightDampening) { + if (lightDampening != null) { + if (lightDampening < 0 || lightDampening > 15) { + throw new IllegalArgumentException("Light dampening must be in the range 0-15"); + } + } + this.lightDampening = lightDampening; + return this; + } + + @Override + public Builder transformation(TransformationComponent transformation) { + if (transformation.rx() % 90 != 0 || transformation.ry() % 90 != 0 || transformation.rz() % 90 != 0) { + throw new IllegalArgumentException("Rotation of transformation must be a multiple of 90 degrees."); + } + this.transformation = transformation; + return this; + } + + @Override + public Builder unitCube(boolean unitCube) { + this.unitCube = unitCube; + return this; + } + + @Override + public Builder placeAir(boolean placeAir) { + this.placeAir = placeAir; + return this; + } + + @Override + public Builder tags(Set tags) { + this.tags.addAll(tags); + return this; + } + + @Override + public CustomBlockComponents build() { + return new GeyserCustomBlockComponents(this); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockData.java b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockData.java new file mode 100644 index 000000000..413a4d1ed --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockData.java @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.level.block; + +import it.unimi.dsi.fastutil.objects.*; +import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.Constants; +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.CustomBlockComponents; +import org.geysermc.geyser.api.block.custom.property.CustomBlockProperty; +import org.geysermc.geyser.api.block.custom.property.PropertyType; +import org.geysermc.geyser.api.util.CreativeCategory; +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; + +import java.util.List; +import java.util.Map; + +@RequiredArgsConstructor +public class GeyserCustomBlockData implements CustomBlockData { + private final String name; + private final boolean includedInCreativeInventory; + private final CreativeCategory creativeCategory; + private final String creativeGroup; + private final CustomBlockComponents components; + private final Map> properties; + private final List permutations; + + private final Map defaultProperties; + + GeyserCustomBlockData(CustomBlockDataBuilder builder) { + this.name = builder.name; + if (name == null) { + throw new IllegalStateException("Name must be set"); + } + + this.includedInCreativeInventory = builder.includedInCreativeInventory; + this.creativeCategory = builder.creativeCategory; + this.creativeGroup = builder.creativeGroup; + + this.components = builder.components; + + if (!builder.properties.isEmpty()) { + this.properties = Object2ObjectMaps.unmodifiable(new Object2ObjectArrayMap<>(builder.properties)); + Object2ObjectMap defaultProperties = new Object2ObjectOpenHashMap<>(this.properties.size()); + for (CustomBlockProperty property : properties.values()) { + if (property.values().size() > 16) { + GeyserImpl.getInstance().getLogger().warning(property.name() + " contains more than 16 values, but BDS specifies it should not. This may break in future versions."); + } + if (property.values().stream().distinct().count() != property.values().size()) { + throw new IllegalStateException(property.name() + " has duplicate values."); + } + if (property.values().isEmpty()) { + throw new IllegalStateException(property.name() + " contains no values."); + } + defaultProperties.put(property.name(), property.values().get(0)); + } + this.defaultProperties = Object2ObjectMaps.unmodifiable(defaultProperties); + } else { + this.properties = Object2ObjectMaps.emptyMap(); + this.defaultProperties = Object2ObjectMaps.emptyMap(); + } + + if (!builder.permutations.isEmpty()) { + this.permutations = List.of(builder.permutations.toArray(new CustomBlockPermutation[0])); + } else { + this.permutations = ObjectLists.emptyList(); + } + } + + @Override + public @NonNull String name() { + return name; + } + + @Override + public @NonNull String identifier() { + return Constants.GEYSER_CUSTOM_NAMESPACE + ":" + name; + } + + @Override + public boolean includedInCreativeInventory() { + return includedInCreativeInventory; + } + + @Override + public @Nullable CreativeCategory creativeCategory() { + return creativeCategory; + } + + @Override + public @Nullable String creativeGroup() { + return creativeGroup; + } + + @Override + public CustomBlockComponents components() { + return components; + } + + @Override + public @NonNull Map> properties() { + return properties; + } + + @Override + public @NonNull List permutations() { + return permutations; + } + + @Override + public @NonNull CustomBlockState defaultBlockState() { + return new GeyserCustomBlockState(this, defaultProperties); + } + + @Override + public CustomBlockState.@NotNull Builder blockStateBuilder() { + return new GeyserCustomBlockState.CustomBlockStateBuilder(this); + } + + public static class CustomBlockDataBuilder implements Builder { + private String name; + private boolean includedInCreativeInventory; + private CreativeCategory creativeCategory; + private String creativeGroup; + private CustomBlockComponents components; + private final Object2ObjectMap> properties = new Object2ObjectOpenHashMap<>(); + private List permutations = ObjectLists.emptyList(); + + @Override + public Builder name(@NonNull String name) { + this.name = name; + return this; + } + + @Override + public Builder includedInCreativeInventory(boolean includedInCreativeInventory) { + this.includedInCreativeInventory = includedInCreativeInventory; + return this; + } + + @Override + public Builder creativeCategory(@Nullable CreativeCategory creativeCategory) { + this.creativeCategory = creativeCategory; + return this; + } + + @Override + public Builder creativeGroup(@Nullable String creativeGroup) { + this.creativeGroup = creativeGroup; + return this; + } + + @Override + public Builder components(@NonNull CustomBlockComponents components) { + this.components = components; + return this; + } + + @Override + public Builder booleanProperty(@NonNull String propertyName) { + this.properties.put(propertyName, new GeyserCustomBlockProperty<>(propertyName, List.of((byte) 0, (byte) 1), PropertyType.booleanProp())); + return this; + } + + @Override + public Builder intProperty(@NonNull String propertyName, List values) { + this.properties.put(propertyName, new GeyserCustomBlockProperty<>(propertyName, values, PropertyType.integerProp())); + return this; + } + + @Override + public Builder stringProperty(@NonNull String propertyName, List values) { + this.properties.put(propertyName, new GeyserCustomBlockProperty<>(propertyName, values, PropertyType.stringProp())); + return this; + } + + @Override + public Builder permutations(@NonNull List permutations) { + this.permutations = permutations; + return this; + } + + @Override + public CustomBlockData build() { + return new GeyserCustomBlockData(this); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockProperty.java b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockProperty.java new file mode 100644 index 000000000..139df1fcd --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockProperty.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.level.block; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.block.custom.property.CustomBlockProperty; +import org.geysermc.geyser.api.block.custom.property.PropertyType; + +import java.util.List; + +/** + * A custom block property that can be used to store custom data for a block. + * + * @param The type of the property + * @param name The name of the property + * @param values The values of the property + * @param type The type of the property + */ +public record GeyserCustomBlockProperty(@NonNull String name, @NonNull List values, + @NonNull PropertyType type) implements CustomBlockProperty { +} diff --git a/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockState.java b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockState.java new file mode 100644 index 000000000..576bd9743 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockState.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.level.block; + +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMaps; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import lombok.RequiredArgsConstructor; +import lombok.Value; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.block.custom.CustomBlockData; +import org.geysermc.geyser.api.block.custom.CustomBlockState; +import org.geysermc.geyser.api.block.custom.property.CustomBlockProperty; + +import java.util.Map; + +@Value +public class GeyserCustomBlockState implements CustomBlockState { + CustomBlockData block; + Map properties; + + @Override + public @NonNull CustomBlockData block() { + return block; + } + + @Override + public @NonNull String name() { + return block.name(); + } + + @SuppressWarnings("unchecked") + @Override + public @NonNull T property(String propertyName) { + return (T) properties.get(propertyName); + } + + @Override + public @NonNull Map properties() { + return properties; + } + + @RequiredArgsConstructor + public static class CustomBlockStateBuilder implements CustomBlockState.Builder { + private final CustomBlockData blockData; + private final Object2ObjectMap properties = new Object2ObjectOpenHashMap<>(); + + @Override + public Builder booleanProperty(@NonNull String propertyName, boolean value) { + properties.put(propertyName, value ? (byte) 1 : (byte) 0); + return this; + } + + @Override + public Builder intProperty(@NonNull String propertyName, int value) { + properties.put(propertyName, value); + return this; + } + + @Override + public Builder stringProperty(@NonNull String propertyName, @NonNull String value) { + properties.put(propertyName, value); + return this; + } + + @Override + public CustomBlockState build() { + for (String propertyName : blockData.properties().keySet()) { + if (!properties.containsKey(propertyName)) { + throw new IllegalArgumentException("Missing property: " + propertyName); + } + } + + for (Map.Entry entry : properties.entrySet()) { + String propertyName = entry.getKey(); + Object propertyValue = entry.getValue(); + + CustomBlockProperty property = blockData.properties().get(propertyName); + if (property == null) { + throw new IllegalArgumentException("Unknown property: " + propertyName); + } else if (!property.values().contains(propertyValue)) { + throw new IllegalArgumentException("Invalid value: " + propertyValue + " for property: " + propertyName); + } + } + + return new GeyserCustomBlockState(blockData, Object2ObjectMaps.unmodifiable(properties)); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/level/block/GeyserGeometryComponent.java b/core/src/main/java/org/geysermc/geyser/level/block/GeyserGeometryComponent.java new file mode 100644 index 000000000..c23fc87a2 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserGeometryComponent.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.level.block; + +import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.block.custom.component.GeometryComponent; + +import java.util.Map; + +@RequiredArgsConstructor +public class GeyserGeometryComponent implements GeometryComponent { + private final String identifier; + private final Map boneVisibility; + + GeyserGeometryComponent(GeometryComponentBuilder builder) { + this.identifier = builder.identifier; + this.boneVisibility = builder.boneVisibility; + } + + @Override + public @NonNull String identifier() { + return identifier; + } + + @Override + public @Nullable Map boneVisibility() { + return boneVisibility; + } + + public static class GeometryComponentBuilder implements Builder { + private String identifier; + private Map boneVisibility; + + @Override + public GeometryComponent.Builder identifier(@NonNull String identifier) { + this.identifier = identifier; + return this; + } + + @Override + public GeometryComponent.Builder boneVisibility(@Nullable Map boneVisibility) { + this.boneVisibility = boneVisibility; + return this; + } + + @Override + public GeometryComponent build() { + return new GeyserGeometryComponent(this); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/level/block/GeyserJavaBlockState.java b/core/src/main/java/org/geysermc/geyser/level/block/GeyserJavaBlockState.java new file mode 100644 index 000000000..725afe6df --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserJavaBlockState.java @@ -0,0 +1,161 @@ +package org.geysermc.geyser.level.block; + +import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockState; +import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBoundingBox; + +public class GeyserJavaBlockState implements JavaBlockState { + String identifier; + int javaId; + int stateGroupId; + float blockHardness; + boolean waterlogged; + JavaBoundingBox[] collision; + boolean canBreakWithHand; + String pickItem; + String pistonBehavior; + boolean hasBlockEntity; + + private GeyserJavaBlockState(JavaBlockStateBuilder builder) { + this.identifier = builder.identifier; + this.javaId = builder.javaId; + this.stateGroupId = builder.stateGroupId; + this.blockHardness = builder.blockHardness; + this.waterlogged = builder.waterlogged; + this.collision = builder.collision; + this.canBreakWithHand = builder.canBreakWithHand; + this.pickItem = builder.pickItem; + this.pistonBehavior = builder.pistonBehavior; + this.hasBlockEntity = builder.hasBlockEntity; + } + + @Override + public @NonNull String identifier() { + return identifier; + } + + @Override + public @NonNegative int javaId() { + return javaId; + } + + @Override + public @NonNegative int stateGroupId() { + return stateGroupId; + } + + @Override + public @NonNegative float blockHardness() { + return blockHardness; + } + + @Override + public @NonNull boolean waterlogged() { + return waterlogged; + } + + @Override + public @NonNull JavaBoundingBox[] collision() { + return collision; + } + + @Override + public @NonNull boolean canBreakWithHand() { + return canBreakWithHand; + } + + @Override + public @Nullable String pickItem() { + return pickItem; + } + + @Override + public @Nullable String pistonBehavior() { + return pistonBehavior; + } + + @Override + public @Nullable boolean hasBlockEntity() { + return hasBlockEntity; + } + + public static class JavaBlockStateBuilder implements Builder { + private String identifier; + private int javaId; + private int stateGroupId; + private float blockHardness; + private boolean waterlogged; + private JavaBoundingBox[] collision; + private boolean canBreakWithHand; + private String pickItem; + private String pistonBehavior; + private boolean hasBlockEntity; + + @Override + public Builder identifier(@NonNull String identifier) { + this.identifier = identifier; + return this; + } + + @Override + public Builder javaId(@NonNegative int javaId) { + this.javaId = javaId; + return this; + } + + @Override + public Builder stateGroupId(@NonNegative int stateGroupId) { + this.stateGroupId = stateGroupId; + return this; + } + + @Override + public Builder blockHardness(@NonNegative float blockHardness) { + this.blockHardness = blockHardness; + return this; + } + + @Override + public Builder waterlogged(@NonNull boolean waterlogged) { + this.waterlogged = waterlogged; + return this; + } + + @Override + public Builder collision(@NonNull JavaBoundingBox[] collision) { + this.collision = collision; + return this; + } + + @Override + public Builder canBreakWithHand(@NonNull boolean canBreakWithHand) { + this.canBreakWithHand = canBreakWithHand; + return this; + } + + @Override + public Builder pickItem(@Nullable String pickItem) { + this.pickItem = pickItem; + return this; + } + + @Override + public Builder pistonBehavior(@Nullable String pistonBehavior) { + this.pistonBehavior = pistonBehavior; + return this; + } + + @Override + public Builder hasBlockEntity(@Nullable boolean hasBlockEntity) { + this.hasBlockEntity = hasBlockEntity; + return this; + } + + @Override + public JavaBlockState build() { + return new GeyserJavaBlockState(this); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/level/block/GeyserMaterialInstance.java b/core/src/main/java/org/geysermc/geyser/level/block/GeyserMaterialInstance.java new file mode 100644 index 000000000..80a880152 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserMaterialInstance.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.level.block; + +import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.block.custom.component.MaterialInstance; + +@RequiredArgsConstructor +public class GeyserMaterialInstance implements MaterialInstance { + private final String texture; + private final String renderMethod; + private final boolean faceDimming; + private final boolean ambientOcclusion; + + GeyserMaterialInstance(MaterialInstanceBuilder builder) { + this.texture = builder.texture; + this.renderMethod = builder.renderMethod; + this.faceDimming = builder.faceDimming; + this.ambientOcclusion = builder.ambientOcclusion; + } + + @Override + public @NonNull String texture() { + return texture; + } + + @Override + public @Nullable String renderMethod() { + return renderMethod; + } + + @Override + public @Nullable boolean faceDimming() { + return faceDimming; + } + + @Override + public @Nullable boolean ambientOcclusion() { + return ambientOcclusion; + } + + public static class MaterialInstanceBuilder implements Builder { + private String texture; + private String renderMethod; + private boolean faceDimming; + private boolean ambientOcclusion; + + @Override + public Builder texture(@NonNull String texture) { + this.texture = texture; + return this; + } + + @Override + public Builder renderMethod(@Nullable String renderMethod) { + this.renderMethod = renderMethod; + return this; + } + + @Override + public Builder faceDimming(@Nullable boolean faceDimming) { + this.faceDimming = faceDimming; + return this; + } + + @Override + public Builder ambientOcclusion(@Nullable boolean ambientOcclusion) { + this.ambientOcclusion = ambientOcclusion; + return this; + } + + @Override + public MaterialInstance build() { + return new GeyserMaterialInstance(this); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/level/block/GeyserNonVanillaCustomBlockData.java b/core/src/main/java/org/geysermc/geyser/level/block/GeyserNonVanillaCustomBlockData.java new file mode 100644 index 000000000..8b5056a6f --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserNonVanillaCustomBlockData.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.level.block; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.block.custom.CustomBlockPermutation; +import org.geysermc.geyser.api.block.custom.NonVanillaCustomBlockData; +import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents; +import org.geysermc.geyser.api.util.CreativeCategory; + +import java.util.List; + +public class GeyserNonVanillaCustomBlockData extends GeyserCustomBlockData implements NonVanillaCustomBlockData { + private final String namespace; + + GeyserNonVanillaCustomBlockData(NonVanillaCustomBlockDataBuilder builder) { + super(builder); + + this.namespace = builder.namespace; + if (namespace == null) { + throw new IllegalStateException("Identifier must be set"); + } + } + + @Override + public @NonNull String identifier() { + return this.namespace + ":" + super.name(); + } + + @Override + public @NonNull String namespace() { + return this.namespace; + } + + public static class NonVanillaCustomBlockDataBuilder extends CustomBlockDataBuilder implements NonVanillaCustomBlockData.Builder { + private String namespace; + + @Override + public NonVanillaCustomBlockDataBuilder namespace(@NonNull String namespace) { + this.namespace = namespace; + return this; + } + + @Override + public NonVanillaCustomBlockDataBuilder name(@NonNull String name) { + return (NonVanillaCustomBlockDataBuilder) super.name(name); + } + + @Override + public NonVanillaCustomBlockDataBuilder includedInCreativeInventory(boolean includedInCreativeInventory) { + return (NonVanillaCustomBlockDataBuilder) super.includedInCreativeInventory(includedInCreativeInventory); + } + + @Override + public NonVanillaCustomBlockDataBuilder creativeCategory(@Nullable CreativeCategory creativeCategories) { + return (NonVanillaCustomBlockDataBuilder) super.creativeCategory(creativeCategories); + } + + @Override + public NonVanillaCustomBlockDataBuilder creativeGroup(@Nullable String creativeGroup) { + return (NonVanillaCustomBlockDataBuilder) super.creativeGroup(creativeGroup); + } + + @Override + public NonVanillaCustomBlockDataBuilder components(@NonNull CustomBlockComponents components) { + return (NonVanillaCustomBlockDataBuilder) super.components(components); + } + + @Override + public NonVanillaCustomBlockDataBuilder booleanProperty(@NonNull String propertyName) { + return (NonVanillaCustomBlockDataBuilder) super.booleanProperty(propertyName); + } + + @Override + public NonVanillaCustomBlockDataBuilder intProperty(@NonNull String propertyName, List values) { + return (NonVanillaCustomBlockDataBuilder) super.intProperty(propertyName, values); + } + + @Override + public NonVanillaCustomBlockDataBuilder stringProperty(@NonNull String propertyName, List values) { + return (NonVanillaCustomBlockDataBuilder) super.stringProperty(propertyName, values); + } + + @Override + public NonVanillaCustomBlockDataBuilder permutations(@NonNull List permutations) { + return (NonVanillaCustomBlockDataBuilder) super.permutations(permutations); + } + + @Override + public NonVanillaCustomBlockData build() { + return new GeyserNonVanillaCustomBlockData(this); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/pack/SkullResourcePackManager.java b/core/src/main/java/org/geysermc/geyser/pack/SkullResourcePackManager.java new file mode 100644 index 000000000..d9f7a6327 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/pack/SkullResourcePackManager.java @@ -0,0 +1,306 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.pack; + +import it.unimi.dsi.fastutil.Pair; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import org.geysermc.geyser.Constants; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.pack.ResourcePackManifest; +import org.geysermc.geyser.registry.BlockRegistries; +import org.geysermc.geyser.registry.type.CustomSkull; +import org.geysermc.geyser.skin.SkinProvider; +import org.geysermc.geyser.util.FileUtils; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.*; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +public class SkullResourcePackManager { + + private static final long RESOURCE_PACK_VERSION = 8; + + private static final Path SKULL_SKIN_CACHE_PATH = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("player_skulls"); + + public static final Map SKULL_SKINS = new Object2ObjectOpenHashMap<>(); + + @SuppressWarnings("ResultOfMethodCallIgnored") + public static Path createResourcePack() { + Path cachePath = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache"); + try { + Files.createDirectories(cachePath); + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().severe("Unable to create directories for player skull resource pack!", e); + return null; + } + cleanSkullSkinCache(); + + Path packPath = cachePath.resolve("player_skulls.mcpack"); + File packFile = packPath.toFile(); + if (BlockRegistries.CUSTOM_SKULLS.get().isEmpty() || !GeyserImpl.getInstance().getConfig().isAddNonBedrockItems()) { + packFile.delete(); // No need to keep resource pack + return null; + } + if (packFile.exists() && canReusePack(packFile)) { + GeyserImpl.getInstance().getLogger().info("Reusing cached player skull resource pack."); + return packPath; + } + + // We need to create the resource pack from scratch + GeyserImpl.getInstance().getLogger().info("Creating skull resource pack."); + packFile.delete(); + try (ZipOutputStream zipOS = new ZipOutputStream(Files.newOutputStream(packPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE))) { + addBaseResources(zipOS); + addSkinTextures(zipOS); + addAttachables(zipOS); + GeyserImpl.getInstance().getLogger().info("Finished creating skull resource pack."); + return packPath; + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().severe("Unable to create player skull resource pack!", e); + GeyserImpl.getInstance().getLogger().severe("Bedrock players will see dirt blocks instead of custom skull blocks."); + packFile.delete(); + } + return null; + } + + public static void cacheSkullSkin(String skinHash) throws IOException { + String skinUrl = Constants.MINECRAFT_SKIN_SERVER_URL + skinHash; + Path skinPath = SKULL_SKINS.get(skinHash); + if (skinPath != null) { + return; + } + + Files.createDirectories(SKULL_SKIN_CACHE_PATH); + skinPath = SKULL_SKIN_CACHE_PATH.resolve(skinHash + ".png"); + if (Files.exists(skinPath)) { + SKULL_SKINS.put(skinHash, skinPath); + return; + } + + BufferedImage image = SkinProvider.requestImage(skinUrl, null); + // Resize skins to 48x16 to save on space and memory + BufferedImage skullTexture = new BufferedImage(48, 16, image.getType()); + // Reorder skin parts to fit into the space + // Right, Front, Left, Back, Top, Bottom - head + // Right, Front, Left, Back, Top, Bottom - hat + Graphics g = skullTexture.createGraphics(); + // Right, Front, Left, Back of the head + g.drawImage(image, 0, 0, 32, 8, 0, 8, 32, 16, null); + // Right, Front, Left, Back of the hat + g.drawImage(image, 0, 8, 32, 16, 32, 8, 64, 16, null); + // Top and bottom of the head + g.drawImage(image, 32, 0, 48, 8, 8, 0, 24, 8, null); + // Top and bottom of the hat + g.drawImage(image, 32, 8, 48, 16, 40, 0, 56, 8, null); + g.dispose(); + image.flush(); + + ImageIO.write(skullTexture, "png", skinPath.toFile()); + SKULL_SKINS.put(skinHash, skinPath); + GeyserImpl.getInstance().getLogger().debug("Cached player skull to " + skinPath + " for " + skinHash); + } + + public static void cleanSkullSkinCache() { + try (Stream stream = Files.list(SKULL_SKIN_CACHE_PATH)) { + int removeCount = 0; + for (Path path : stream.toList()) { + String skinHash = path.getFileName().toString(); + skinHash = skinHash.substring(0, skinHash.length() - ".png".length()); + if (!SKULL_SKINS.containsKey(skinHash) && path.toFile().delete()) { + removeCount++; + } + } + if (removeCount != 0) { + GeyserImpl.getInstance().getLogger().debug("Removed " + removeCount + " unnecessary skull skins."); + } + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().debug("Unable to clean up skull skin cache."); + if (GeyserImpl.getInstance().getConfig().isDebugMode()) { + e.printStackTrace(); + } + } + } + + private static void addBaseResources(ZipOutputStream zipOS) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(GeyserImpl.getInstance().getBootstrap().getResource("bedrock/skull_resource_pack_files.txt")))) { + List lines = reader.lines().toList(); + for (String path : lines) { + ZipEntry entry = new ZipEntry(path); + + zipOS.putNextEntry(entry); + String resourcePath = "bedrock/" + path; + switch (path) { + case "skull_resource_pack/manifest.json" -> + fillTemplate(zipOS, resourcePath, SkullResourcePackManager::fillManifestJson); + case "skull_resource_pack/textures/terrain_texture.json" -> + fillTemplate(zipOS, resourcePath, SkullResourcePackManager::fillTerrainTextureJson); + default -> zipOS.write(FileUtils.readAllBytes(resourcePath)); + } + zipOS.closeEntry(); + } + + addFloorGeometries(zipOS); + + ZipEntry entry = new ZipEntry("skull_resource_pack/pack_icon.png"); + zipOS.putNextEntry(entry); + zipOS.write(FileUtils.readAllBytes("icon.png")); + zipOS.closeEntry(); + } + } + + private static void addFloorGeometries(ZipOutputStream zipOS) throws IOException { + String template = FileUtils.readToString("bedrock/skull_resource_pack/models/blocks/player_skull_floor.geo.json"); + String[] quadrants = {"a", "b", "c", "d"}; + for (int i = 0; i < quadrants.length; i++) { + String quadrant = quadrants[i]; + float yRotation = i * 22.5f; + String contents = template + .replace("${quadrant}", quadrant) + .replace("${y_rotation}", String.valueOf(yRotation)); + + ZipEntry entry = new ZipEntry("skull_resource_pack/models/blocks/player_skull_floor_" + quadrant + ".geo.json"); + zipOS.putNextEntry(entry); + zipOS.write(contents.getBytes(StandardCharsets.UTF_8)); + zipOS.closeEntry(); + } + } + + private static void addAttachables(ZipOutputStream zipOS) throws IOException { + String template = FileUtils.readToString("bedrock/skull_resource_pack/attachables/template_attachable.json"); + for (CustomSkull skull : BlockRegistries.CUSTOM_SKULLS.get().values()) { + ZipEntry entry = new ZipEntry("skull_resource_pack/attachables/" + truncateHash(skull.getSkinHash()) + ".json"); + zipOS.putNextEntry(entry); + zipOS.write(fillAttachableJson(template, skull).getBytes(StandardCharsets.UTF_8)); + zipOS.closeEntry(); + } + } + + private static void addSkinTextures(ZipOutputStream zipOS) throws IOException { + for (Path skinPath : SKULL_SKINS.values()) { + ZipEntry entry = new ZipEntry("skull_resource_pack/textures/blocks/" + truncateHash(skinPath.getFileName().toString()) + ".png"); + zipOS.putNextEntry(entry); + try (InputStream stream = Files.newInputStream(skinPath)) { + stream.transferTo(zipOS); + } + zipOS.closeEntry(); + } + } + + private static void fillTemplate(ZipOutputStream zipOS, String path, UnaryOperator filler) throws IOException { + String template = FileUtils.readToString(path); + String result = filler.apply(template); + zipOS.write(result.getBytes(StandardCharsets.UTF_8)); + } + + private static String fillAttachableJson(String template, CustomSkull skull) { + return template.replace("${identifier}", skull.getCustomBlockData().identifier()) + .replace("${texture}", truncateHash(skull.getSkinHash())); + } + + private static String fillManifestJson(String template) { + Pair uuids = generatePackUUIDs(); + return template.replace("${uuid1}", uuids.first().toString()) + .replace("${uuid2}", uuids.second().toString()); + } + + private static String fillTerrainTextureJson(String template) { + StringBuilder textures = new StringBuilder(); + for (String skinHash : SKULL_SKINS.keySet()) { + String texture = String.format("\"geyser.%s_player_skin\":{\"textures\":\"textures/blocks/%s\"},\n", skinHash, truncateHash(skinHash)); + textures.append(texture); + } + if (textures.length() != 0) { + // Remove trailing comma + textures.delete(textures.length() - 2, textures.length()); + } + return template.replace("${texture_data}", textures); + } + + private static Pair generatePackUUIDs() { + UUID uuid1 = UUID.randomUUID(); + UUID uuid2 = UUID.randomUUID(); + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + for (int i = 0; i < 8; i++) { + md.update((byte) ((RESOURCE_PACK_VERSION >> (i * 8)) & 0xFF)); + } + SKULL_SKINS.keySet().stream() + .sorted() + .map(hash -> hash.getBytes(StandardCharsets.UTF_8)) + .forEach(md::update); + + ByteBuffer skinHashes = ByteBuffer.wrap(md.digest()); + uuid1 = new UUID(skinHashes.getLong(), skinHashes.getLong()); + uuid2 = new UUID(skinHashes.getLong(), skinHashes.getLong()); + } catch (NoSuchAlgorithmException e) { + GeyserImpl.getInstance().getLogger().severe("Unable to get SHA-256 Message Digest instance! Bedrock players will have to re-downloaded the player skull resource pack after each server restart.", e); + } + + return Pair.of(uuid1, uuid2); + } + + private static boolean canReusePack(File packFile) { + Pair uuids = generatePackUUIDs(); + try (ZipFile zipFile = new ZipFile(packFile)) { + Optional manifestEntry = zipFile.stream() + .filter(entry -> entry.getName().contains("manifest.json")) + .findFirst(); + if (manifestEntry.isPresent()) { + GeyserResourcePackManifest manifest = FileUtils.loadJson(zipFile.getInputStream(manifestEntry.get()), GeyserResourcePackManifest.class); + if (!uuids.first().equals(manifest.header().uuid())) { + return false; + } + Optional resourceUUID = manifest.modules().stream() + .filter(module -> "resources".equals(module.type())) + .findFirst() + .map(ResourcePackManifest.Module::uuid); + return resourceUUID.isPresent() && uuids.second().equals(resourceUUID.get()); + } + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().debug("Cached player skull resource pack was invalid! The pack will be recreated."); + } + return false; + } + + private static String truncateHash(String hash) { + return hash.substring(0, Math.min(hash.length(), 32)); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/registry/BlockRegistries.java b/core/src/main/java/org/geysermc/geyser/registry/BlockRegistries.java index 8b576a673..49fd90cf7 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/BlockRegistries.java +++ b/core/src/main/java/org/geysermc/geyser/registry/BlockRegistries.java @@ -25,17 +25,30 @@ package org.geysermc.geyser.registry; +import it.unimi.dsi.fastutil.Pair; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import org.geysermc.geyser.api.block.custom.CustomBlockData; +import org.geysermc.geyser.api.block.custom.CustomBlockState; +import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockItem; +import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockState; +import org.geysermc.geyser.registry.loader.CollisionRegistryLoader; import org.geysermc.geyser.registry.loader.RegistryLoaders; import org.geysermc.geyser.registry.populator.BlockRegistryPopulator; +import org.geysermc.geyser.registry.populator.CustomBlockRegistryPopulator; +import org.geysermc.geyser.registry.populator.CustomSkullRegistryPopulator; import org.geysermc.geyser.registry.type.BlockMapping; import org.geysermc.geyser.registry.type.BlockMappings; +import org.geysermc.geyser.registry.type.CustomSkull; +import org.geysermc.geyser.translator.collision.BlockCollision; import java.util.BitSet; +import java.util.Set; + /** * Holds all the block registries in Geyser. */ @@ -57,6 +70,11 @@ public class BlockRegistries { */ public static final ArrayRegistry JAVA_BLOCKS = ArrayRegistry.create(RegistryLoaders.uninitialized()); + /** + * A mapped registry containing which holds block IDs to its {@link BlockCollision}. + */ + public static final IntMappedRegistry COLLISIONS; + /** * A mapped registry containing the Java identifiers to IDs. */ @@ -83,11 +101,51 @@ public class BlockRegistries { */ public static final SimpleRegistry INTERACTIVE_MAY_BUILD = SimpleRegistry.create(RegistryLoaders.uninitialized()); + /** + * A registry containing all the custom blocks. + */ + public static final ArrayRegistry CUSTOM_BLOCKS = ArrayRegistry.create(RegistryLoaders.empty(() -> new CustomBlockData[] {})); + + /** + * A registry which stores Java Ids and the custom block state it should be replaced with. + */ + public static final MappedRegistry> CUSTOM_BLOCK_STATE_OVERRIDES = MappedRegistry.create(RegistryLoaders.empty(Int2ObjectOpenHashMap::new)); + + /** + * A registry which stores non vanilla java blockstates and the custom block state it should be replaced with. + */ + public static final SimpleMappedRegistry NON_VANILLA_BLOCK_STATE_OVERRIDES = SimpleMappedRegistry.create(RegistryLoaders.empty(Object2ObjectOpenHashMap::new)); + + /** + * A registry which stores clean Java Ids and the custom block it should be replaced with in the context of items. + */ + public static final SimpleMappedRegistry CUSTOM_BLOCK_ITEM_OVERRIDES = SimpleMappedRegistry.create(RegistryLoaders.empty(Object2ObjectOpenHashMap::new)); + + /** + * A registry which stores Custom Block Data for extended collision boxes and the Java IDs of blocks that will have said extended collision boxes placed above them. + */ + public static final SimpleMappedRegistry> EXTENDED_COLLISION_BOXES = SimpleMappedRegistry.create(RegistryLoaders.empty(Object2ObjectOpenHashMap::new)); + + /** + * A registry which stores skin texture hashes to custom skull blocks. + */ + public static final SimpleMappedRegistry CUSTOM_SKULLS = SimpleMappedRegistry.create(RegistryLoaders.empty(Object2ObjectOpenHashMap::new)); + static { - BlockRegistryPopulator.populate(); + CustomSkullRegistryPopulator.populate(); + BlockRegistryPopulator.populate(BlockRegistryPopulator.Stage.PRE_INIT); + CustomBlockRegistryPopulator.populate(CustomBlockRegistryPopulator.Stage.DEFINITION); + CustomBlockRegistryPopulator.populate(CustomBlockRegistryPopulator.Stage.NON_VANILLA_REGISTRATION); + BlockRegistryPopulator.populate(BlockRegistryPopulator.Stage.INIT_JAVA); + COLLISIONS = IntMappedRegistry.create(Pair.of("org.geysermc.geyser.translator.collision.CollisionRemapper", "mappings/collision.json"), CollisionRegistryLoader::new); + CustomBlockRegistryPopulator.populate(CustomBlockRegistryPopulator.Stage.VANILLA_REGISTRATION); + CustomBlockRegistryPopulator.populate(CustomBlockRegistryPopulator.Stage.CUSTOM_REGISTRATION); + BlockRegistryPopulator.populate(BlockRegistryPopulator.Stage.INIT_BEDROCK); + BlockRegistryPopulator.populate(BlockRegistryPopulator.Stage.POST_INIT); } public static void init() { // no-op } + } \ No newline at end of file diff --git a/core/src/main/java/org/geysermc/geyser/registry/Registries.java b/core/src/main/java/org/geysermc/geyser/registry/Registries.java index 73ae23e9e..9b5ed8ae6 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/Registries.java +++ b/core/src/main/java/org/geysermc/geyser/registry/Registries.java @@ -31,7 +31,6 @@ import com.github.steveice10.mc.protocol.data.game.level.event.LevelEvent; import com.github.steveice10.mc.protocol.data.game.level.particle.ParticleType; import com.github.steveice10.mc.protocol.data.game.recipe.RecipeType; import com.github.steveice10.packetlib.packet.Packet; -import it.unimi.dsi.fastutil.Pair; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2IntMap; @@ -56,7 +55,6 @@ import org.geysermc.geyser.registry.type.EnchantmentData; import org.geysermc.geyser.registry.type.ItemMappings; import org.geysermc.geyser.registry.type.ParticleMapping; import org.geysermc.geyser.registry.type.SoundMapping; -import org.geysermc.geyser.translator.collision.BlockCollision; import org.geysermc.geyser.translator.level.block.entity.BlockEntityTranslator; import org.geysermc.geyser.translator.level.event.LevelEventTranslator; import org.geysermc.geyser.translator.sound.SoundInteractionTranslator; @@ -68,6 +66,12 @@ import java.util.*; * Holds all the common registries in Geyser. */ public final class Registries { + /** + * A registry holding all the providers. + * This has to be initialized first to allow extensions to access providers during other registry events. + */ + public static final SimpleMappedRegistry, ProviderSupplier> PROVIDERS = SimpleMappedRegistry.create(new IdentityHashMap<>(), ProviderRegistryLoader::new); + /** * A registry holding a CompoundTag of the known entity identifiers. */ @@ -93,11 +97,6 @@ public final class Registries { */ public static final SimpleMappedRegistry BLOCK_ENTITIES = SimpleMappedRegistry.create("org.geysermc.geyser.translator.level.block.entity.BlockEntity", BlockEntityRegistryLoader::new); - /** - * A mapped registry containing which holds block IDs to its {@link BlockCollision}. - */ - public static final IntMappedRegistry COLLISIONS = IntMappedRegistry.create(Pair.of("org.geysermc.geyser.translator.collision.CollisionRemapper", "mappings/collision.json"), CollisionRegistryLoader::new); - /** * A versioned registry which holds a {@link RecipeType} to a corresponding list of {@link RecipeData}. */ @@ -144,11 +143,6 @@ public final class Registries { */ public static final VersionedRegistry> POTION_MIXES; - /** - * A registry holding all the - */ - public static final SimpleMappedRegistry, ProviderSupplier> PROVIDERS = SimpleMappedRegistry.create(new IdentityHashMap<>(), ProviderRegistryLoader::new); - /** * A versioned registry holding all the recipes, with the net ID being the key, and {@link GeyserRecipe} as the value. */ diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/CollisionRegistryLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/CollisionRegistryLoader.java index 69ad16743..bf2d72b27 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/CollisionRegistryLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/CollisionRegistryLoader.java @@ -79,6 +79,11 @@ public class CollisionRegistryLoader extends MultiResourceRegistryLoader collisionInstances = new Object2ObjectOpenHashMap<>(); for (int i = 0; i < blockMappings.length; i++) { BlockMapping blockMapping = blockMappings[i]; + if (blockMapping == null) { + GeyserImpl.getInstance().getLogger().warning("Missing block mapping for Java block " + i); + continue; + } + BlockCollision newCollision = instantiateCollision(blockMapping, annotationMap, collisionList); if (newCollision != null) { diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java index d32b11cc0..13d7a4d77 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java @@ -25,6 +25,12 @@ package org.geysermc.geyser.registry.loader; +import org.geysermc.geyser.api.block.custom.CustomBlockData; +import org.geysermc.geyser.api.block.custom.NonVanillaCustomBlockData; +import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents; +import org.geysermc.geyser.api.block.custom.component.GeometryComponent; +import org.geysermc.geyser.api.block.custom.component.MaterialInstance; +import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockState; import org.geysermc.geyser.api.command.Command; import org.geysermc.geyser.api.event.EventRegistrar; import org.geysermc.geyser.api.extension.Extension; @@ -37,6 +43,12 @@ import org.geysermc.geyser.event.GeyserEventRegistrar; import org.geysermc.geyser.item.GeyserCustomItemData; import org.geysermc.geyser.item.GeyserCustomItemOptions; import org.geysermc.geyser.item.GeyserNonVanillaCustomItemData; +import org.geysermc.geyser.level.block.GeyserCustomBlockComponents; +import org.geysermc.geyser.level.block.GeyserCustomBlockData; +import org.geysermc.geyser.level.block.GeyserGeometryComponent; +import org.geysermc.geyser.level.block.GeyserJavaBlockState; +import org.geysermc.geyser.level.block.GeyserMaterialInstance; +import org.geysermc.geyser.level.block.GeyserNonVanillaCustomBlockData; import org.geysermc.geyser.pack.path.GeyserPathPackCodec; import org.geysermc.geyser.registry.provider.ProviderSupplier; @@ -52,6 +64,14 @@ public class ProviderRegistryLoader implements RegistryLoader, Prov public Map, ProviderSupplier> load(Map, ProviderSupplier> providers) { // misc providers.put(Command.Builder.class, args -> new GeyserCommandManager.CommandBuilder<>((Extension) args[0])); + + providers.put(CustomBlockComponents.Builder.class, args -> new GeyserCustomBlockComponents.CustomBlockComponentsBuilder()); + providers.put(CustomBlockData.Builder.class, args -> new GeyserCustomBlockData.CustomBlockDataBuilder()); + providers.put(JavaBlockState.Builder.class, args -> new GeyserJavaBlockState.JavaBlockStateBuilder()); + providers.put(NonVanillaCustomBlockData.Builder.class, args -> new GeyserNonVanillaCustomBlockData.NonVanillaCustomBlockDataBuilder()); + providers.put(MaterialInstance.Builder.class, args -> new GeyserMaterialInstance.MaterialInstanceBuilder()); + providers.put(GeometryComponent.Builder.class, args -> new GeyserGeometryComponent.GeometryComponentBuilder()); + providers.put(EventRegistrar.class, args -> new GeyserEventRegistrar(args[0])); providers.put(PathPackCodec.class, args -> new GeyserPathPackCodec((Path) args[0])); diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/ResourcePackLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/ResourcePackLoader.java index 452550d87..800a3d22c 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/ResourcePackLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/ResourcePackLoader.java @@ -30,6 +30,7 @@ import org.geysermc.geyser.api.event.lifecycle.GeyserLoadResourcePacksEvent; import org.geysermc.geyser.api.pack.ResourcePack; import org.geysermc.geyser.pack.GeyserResourcePack; import org.geysermc.geyser.pack.GeyserResourcePackManifest; +import org.geysermc.geyser.pack.SkullResourcePackManager; import org.geysermc.geyser.pack.path.GeyserPathPackCodec; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.util.FileUtils; @@ -87,6 +88,12 @@ public class ResourcePackLoader implements RegistryLoader(); } + // Add custom skull pack + Path skullResourcePack = SkullResourcePackManager.createResourcePack(); + if (skullResourcePack != null) { + resourcePacks.add(skullResourcePack); + } + GeyserLoadResourcePacksEvent event = new GeyserLoadResourcePacksEvent(resourcePacks); GeyserImpl.getInstance().eventBus().fire(event); diff --git a/core/src/main/java/org/geysermc/geyser/item/mappings/MappingsConfigReader.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/MappingsConfigReader.java similarity index 58% rename from core/src/main/java/org/geysermc/geyser/item/mappings/MappingsConfigReader.java rename to core/src/main/java/org/geysermc/geyser/registry/mappings/MappingsConfigReader.java index eaf07c382..039412957 100644 --- a/core/src/main/java/org/geysermc/geyser/item/mappings/MappingsConfigReader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/MappingsConfigReader.java @@ -23,15 +23,16 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.item.mappings; +package org.geysermc.geyser.registry.mappings; import com.fasterxml.jackson.databind.JsonNode; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.item.custom.CustomItemData; -import org.geysermc.geyser.item.mappings.versions.MappingsReader; -import org.geysermc.geyser.item.mappings.versions.MappingsReader_v1; +import org.geysermc.geyser.registry.mappings.util.CustomBlockMapping; +import org.geysermc.geyser.registry.mappings.versions.MappingsReader; +import org.geysermc.geyser.registry.mappings.versions.MappingsReader_v1; import java.io.IOException; import java.nio.file.Files; @@ -56,43 +57,88 @@ public class MappingsConfigReader { } } - public void loadMappingsFromJson(BiConsumer consumer) { - Path customMappingsDirectory = this.customMappingsDirectory; - if (!Files.exists(customMappingsDirectory)) { + public boolean ensureMappingsDirectory(Path mappingsDirectory) { + if (!Files.exists(mappingsDirectory)) { try { - Files.createDirectories(customMappingsDirectory); + Files.createDirectories(mappingsDirectory); + return true; } catch (IOException e) { - GeyserImpl.getInstance().getLogger().error("Failed to create custom mappings directory", e); - return; + GeyserImpl.getInstance().getLogger().error("Failed to create mappings directory", e); + return false; } } + return true; + } + + public void loadItemMappingsFromJson(BiConsumer consumer) { + if (!ensureMappingsDirectory(this.customMappingsDirectory)) { + return; + } Path[] mappingsFiles = this.getCustomMappingsFiles(); for (Path mappingsFile : mappingsFiles) { - this.readMappingsFromJson(mappingsFile, consumer); + this.readItemMappingsFromJson(mappingsFile, consumer); } } - public void readMappingsFromJson(Path file, BiConsumer consumer) { + public void loadBlockMappingsFromJson(BiConsumer consumer) { + if (!ensureMappingsDirectory(this.customMappingsDirectory)) { + return; + } + + Path[] mappingsFiles = this.getCustomMappingsFiles(); + for (Path mappingsFile : mappingsFiles) { + this.readBlockMappingsFromJson(mappingsFile, consumer); + } + } + + public JsonNode getMappingsRoot(Path file) { JsonNode mappingsRoot; try { mappingsRoot = GeyserImpl.JSON_MAPPER.readTree(file.toFile()); } catch (IOException e) { GeyserImpl.getInstance().getLogger().error("Failed to read custom mapping file: " + file, e); - return; + return null; } if (!mappingsRoot.has("format_version")) { GeyserImpl.getInstance().getLogger().error("Mappings file " + file + " is missing the format version field!"); - return; + return null; } - int formatVersion = mappingsRoot.get("format_version").asInt(); + return mappingsRoot; + } + + public int getFormatVersion(JsonNode mappingsRoot, Path file) { + int formatVersion = mappingsRoot.get("format_version").asInt(); if (!this.mappingReaders.containsKey(formatVersion)) { GeyserImpl.getInstance().getLogger().error("Mappings file " + file + " has an unknown format version: " + formatVersion); + return -1; + } + return formatVersion; + } + + public void readItemMappingsFromJson(Path file, BiConsumer consumer) { + JsonNode mappingsRoot = getMappingsRoot(file); + + int formatVersion = getFormatVersion(mappingsRoot, file); + + if (formatVersion < 0 || mappingsRoot == null) { return; } - this.mappingReaders.get(formatVersion).readMappings(file, mappingsRoot, consumer); + this.mappingReaders.get(formatVersion).readItemMappings(file, mappingsRoot, consumer); + } + + public void readBlockMappingsFromJson(Path file, BiConsumer consumer) { + JsonNode mappingsRoot = getMappingsRoot(file); + + int formatVersion = getFormatVersion(mappingsRoot, file); + + if (formatVersion < 0 || mappingsRoot == null) { + return; + } + + this.mappingReaders.get(formatVersion).readBlockMappings(file, mappingsRoot, consumer); } } diff --git a/core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockComponentsMapping.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockComponentsMapping.java new file mode 100644 index 000000000..3e5d934ab --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockComponentsMapping.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.registry.mappings.util; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.block.custom.component.BoxComponent; +import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents; + +/** + * This class is used to store a custom block components mapping, which contains custom + * block components and a potenially null extended collision box + * + * @param components The components of the block + * @param extendedCollisionBox The extended collision box of the block + */ +public record CustomBlockComponentsMapping(@NonNull CustomBlockComponents components, BoxComponent extendedCollisionBox) { +} 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 new file mode 100644 index 000000000..3dbb7908e --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockMapping.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.registry.mappings.util; + +import java.util.Map; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.block.custom.CustomBlockData; + +/** + * This class is used to store a custom block mappings, which contain all of the + * data required to register a custom block that overrides a group of java block + * states. + * + * @param data The custom block data + * @param states The custom block state mappings + * @param javaIdentifier The java identifier of the block + * @param overrideItem Whether or not the custom block should override the java item + */ +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/util/CustomBlockStateBuilderMapping.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockStateBuilderMapping.java new file mode 100644 index 000000000..e627c04a6 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockStateBuilderMapping.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.registry.mappings.util; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.block.custom.CustomBlockState; +import org.geysermc.geyser.api.block.custom.component.BoxComponent; + +import java.util.function.Function; + +/** + * This class is used to store a custom block state builder mapping, which contains custom + * block state builders and a potenially null extended collision box + * + * @param builder The builder of the block + * @param extendedCollisionBox The extended collision box of the block + */ +public record CustomBlockStateBuilderMapping(@NonNull Function builder, BoxComponent extendedCollisionBox) { +} diff --git a/core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockStateMapping.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockStateMapping.java new file mode 100644 index 000000000..4c04bd657 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/util/CustomBlockStateMapping.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.registry.mappings.util; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.block.custom.CustomBlockState; +import org.geysermc.geyser.api.block.custom.component.BoxComponent; + +/** + * This class is used to store a custom block state mapping, which contains custom + * block states and a potenially null extended collision box + * + * @param state The state of the block + * @param extendedCollisionBox The extended collision box of the block + */ +public record CustomBlockStateMapping(@NonNull CustomBlockState state, BoxComponent extendedCollisionBox) { +} diff --git a/core/src/main/java/org/geysermc/geyser/item/mappings/versions/MappingsReader.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/versions/MappingsReader.java similarity index 86% rename from core/src/main/java/org/geysermc/geyser/item/mappings/versions/MappingsReader.java rename to core/src/main/java/org/geysermc/geyser/registry/mappings/versions/MappingsReader.java index ef553f488..e76df2834 100644 --- a/core/src/main/java/org/geysermc/geyser/item/mappings/versions/MappingsReader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/versions/MappingsReader.java @@ -23,20 +23,23 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.item.mappings.versions; +package org.geysermc.geyser.registry.mappings.versions; import com.fasterxml.jackson.databind.JsonNode; import org.geysermc.geyser.api.item.custom.CustomItemData; import org.geysermc.geyser.api.item.custom.CustomRenderOffsets; import org.geysermc.geyser.item.exception.InvalidCustomMappingsFileException; +import org.geysermc.geyser.registry.mappings.util.CustomBlockMapping; import java.nio.file.Path; import java.util.function.BiConsumer; public abstract class MappingsReader { - public abstract void readMappings(Path file, JsonNode mappingsRoot, BiConsumer consumer); + public abstract void readItemMappings(Path file, JsonNode mappingsRoot, BiConsumer consumer); + public abstract void readBlockMappings(Path file, JsonNode mappingsRoot, BiConsumer consumer); public abstract CustomItemData readItemMappingEntry(JsonNode node) throws InvalidCustomMappingsFileException; + public abstract CustomBlockMapping readBlockMappingEntry(String identifier, JsonNode node) throws InvalidCustomMappingsFileException; protected CustomRenderOffsets fromJsonNode(JsonNode node) { if (node == null || !node.isObject()) { 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 new file mode 100644 index 000000000..51c2220cb --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/versions/MappingsReader_v1.java @@ -0,0 +1,713 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.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 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.*; +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.api.util.CreativeCategory; +import org.geysermc.geyser.item.exception.InvalidCustomMappingsFileException; +import org.geysermc.geyser.level.block.GeyserCustomBlockComponents.CustomBlockComponentsBuilder; +import org.geysermc.geyser.level.block.GeyserCustomBlockData.CustomBlockDataBuilder; +import org.geysermc.geyser.level.block.GeyserGeometryComponent.GeometryComponentBuilder; +import org.geysermc.geyser.level.block.GeyserMaterialInstance.MaterialInstanceBuilder; +import org.geysermc.geyser.level.physics.BoundingBox; +import org.geysermc.geyser.registry.BlockRegistries; +import org.geysermc.geyser.registry.mappings.util.CustomBlockComponentsMapping; +import org.geysermc.geyser.registry.mappings.util.CustomBlockMapping; +import org.geysermc.geyser.registry.mappings.util.CustomBlockStateBuilderMapping; +import org.geysermc.geyser.registry.mappings.util.CustomBlockStateMapping; +import org.geysermc.geyser.translator.collision.BlockCollision; +import org.geysermc.geyser.util.BlockUtils; +import org.geysermc.geyser.util.MathUtils; + +import java.nio.file.Path; +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * A class responsible for reading custom item and block mappings from a JSON file + */ +public class MappingsReader_v1 extends MappingsReader { + @Override + public void readItemMappings(Path file, JsonNode mappingsRoot, BiConsumer consumer) { + this.readItemMappingsV1(file, mappingsRoot, consumer); + } + + /** + * Read item block from a JSON node + * + * @param file The path to the file + * @param mappingsRoot The {@link JsonNode} containing the mappings + * @param consumer The consumer to accept the mappings + * @see #readBlockMappingsV1(Path, JsonNode, BiConsumer) + */ + @Override + public void readBlockMappings(Path file, JsonNode mappingsRoot, BiConsumer consumer) { + this.readBlockMappingsV1(file, mappingsRoot, consumer); + } + + public void readItemMappingsV1(Path file, JsonNode mappingsRoot, BiConsumer consumer) { + JsonNode itemsNode = mappingsRoot.get("items"); + + if (itemsNode != null && itemsNode.isObject()) { + itemsNode.fields().forEachRemaining(entry -> { + if (entry.getValue().isArray()) { + entry.getValue().forEach(data -> { + try { + CustomItemData customItemData = this.readItemMappingEntry(data); + consumer.accept(entry.getKey(), customItemData); + } catch (InvalidCustomMappingsFileException e) { + GeyserImpl.getInstance().getLogger().error("Error in registering items for custom mapping file: " + file.toString(), e); + } + }); + } + }); + } + } + + /** + * Read block mappings from a JSON node + * + * @param file The path to the file + * @param mappingsRoot The {@link JsonNode} containing the mappings + * @param consumer The consumer to accept the mappings + * @see #readBlockMappings(Path, JsonNode, BiConsumer) + */ + public void readBlockMappingsV1(Path file, JsonNode mappingsRoot, BiConsumer consumer) { + JsonNode blocksNode = mappingsRoot.get("blocks"); + + if (blocksNode != null && blocksNode.isObject()) { + blocksNode.fields().forEachRemaining(entry -> { + if (entry.getValue().isObject()) { + try { + String identifier = Identifier.formalize(entry.getKey()); + CustomBlockMapping customBlockMapping = this.readBlockMappingEntry(identifier, entry.getValue()); + consumer.accept(identifier, customBlockMapping); + } catch (Exception e) { + GeyserImpl.getInstance().getLogger().error("Error in registering blocks for custom mapping file: " + file.toString()); + GeyserImpl.getInstance().getLogger().error("due to entry: " + entry, e); + } + } + }); + } + } + + private CustomItemOptions readItemCustomItemOptions(JsonNode node) { + CustomItemOptions.Builder customItemOptions = CustomItemOptions.builder(); + + JsonNode customModelData = node.get("custom_model_data"); + if (customModelData != null && customModelData.isInt()) { + customItemOptions.customModelData(customModelData.asInt()); + } + + JsonNode damagePredicate = node.get("damage_predicate"); + if (damagePredicate != null && damagePredicate.isInt()) { + customItemOptions.damagePredicate(damagePredicate.asInt()); + } + + JsonNode unbreakable = node.get("unbreakable"); + if (unbreakable != null && unbreakable.isBoolean()) { + customItemOptions.unbreakable(unbreakable.asBoolean()); + } + + JsonNode defaultItem = node.get("default"); + if (defaultItem != null && defaultItem.isBoolean()) { + customItemOptions.defaultItem(defaultItem.asBoolean()); + } + + return customItemOptions.build(); + } + + @Override + public CustomItemData readItemMappingEntry(JsonNode node) throws InvalidCustomMappingsFileException { + if (node == null || !node.isObject()) { + throw new InvalidCustomMappingsFileException("Invalid item mappings entry"); + } + + JsonNode name = node.get("name"); + if (name == null || !name.isTextual() || name.asText().isEmpty()) { + throw new InvalidCustomMappingsFileException("An item entry has no name"); + } + + CustomItemData.Builder customItemData = CustomItemData.builder() + .name(name.asText()) + .customItemOptions(this.readItemCustomItemOptions(node)); + + //The next entries are optional + if (node.has("display_name")) { + customItemData.displayName(node.get("display_name").asText()); + } + + if (node.has("icon")) { + customItemData.icon(node.get("icon").asText()); + } + + if (node.has("allow_offhand")) { + customItemData.allowOffhand(node.get("allow_offhand").asBoolean()); + } + + if (node.has("display_handheld")) { + customItemData.displayHandheld(node.get("display_handheld").asBoolean()); + } + + if (node.has("texture_size")) { + customItemData.textureSize(node.get("texture_size").asInt()); + } + + if (node.has("render_offsets")) { + JsonNode tmpNode = node.get("render_offsets"); + + customItemData.renderOffsets(fromJsonNode(tmpNode)); + } + + 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} + * @throws InvalidCustomMappingsFileException If the JSON node is invalid + */ + @Override + public CustomBlockMapping readBlockMappingEntry(String identifier, JsonNode node) throws InvalidCustomMappingsFileException { + if (node == null || !node.isObject()) { + throw new InvalidCustomMappingsFileException("Invalid block mappings entry:" + node); + } + + String name = node.get("name").asText(); + if (name == null || name.isEmpty()) { + throw new InvalidCustomMappingsFileException("A block entry has no name"); + } + + boolean includedInCreativeInventory = node.has("included_in_creative_inventory") && node.get("included_in_creative_inventory").asBoolean(); + + CreativeCategory creativeCategory = CreativeCategory.NONE; + if (node.has("creative_category")) { + String categoryName = node.get("creative_category").asText(); + try { + creativeCategory = CreativeCategory.valueOf(categoryName.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new InvalidCustomMappingsFileException("Invalid creative category \"" + categoryName + "\" for block \"" + name + "\""); + } + } + + String creativeGroup = ""; + if (node.has("creative_group")) { + creativeGroup = node.get("creative_group").asText(); + } + + // 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(); + + // Create the data for the overall block + CustomBlockData.Builder customBlockDataBuilder = new CustomBlockDataBuilder() + .name(name) + .includedInCreativeInventory(includedInCreativeInventory) + .creativeCategory(creativeCategory) + .creativeGroup(creativeGroup); + + if (BlockRegistries.JAVA_IDENTIFIER_TO_ID.get().containsKey(identifier)) { + // There is only one Java block state to override + CustomBlockComponentsMapping componentsMapping = createCustomBlockComponentsMapping(node, identifier, name); + CustomBlockData blockData = customBlockDataBuilder + .components(componentsMapping.components()) + .build(); + return new CustomBlockMapping(blockData, Map.of(identifier, new CustomBlockStateMapping(blockData.defaultBlockState(), componentsMapping.extendedCollisionBox())), identifier, !onlyOverrideStates); + } + + Map componentsMap = new LinkedHashMap<>(); + + JsonNode stateOverrides = node.get("state_overrides"); + if (stateOverrides != null && stateOverrides.isObject()) { + // Load components for specific Java block states + Iterator> fields = stateOverrides.fields(); + while (fields.hasNext()) { + Map.Entry overrideEntry = fields.next(); + String state = identifier + "[" + overrideEntry.getKey() + "]"; + if (!BlockRegistries.JAVA_IDENTIFIER_TO_ID.get().containsKey(state)) { + throw new InvalidCustomMappingsFileException("Unknown Java block state: " + state + " for state_overrides."); + } + componentsMap.put(state, createCustomBlockComponentsMapping(overrideEntry.getValue(), state, name)); + } + } + if (componentsMap.isEmpty() && onlyOverrideStates) { + throw new InvalidCustomMappingsFileException("Block entry for " + identifier + " has only_override_states set to true, but has no state_overrides."); + } + + if (!onlyOverrideStates) { + // Create components for any remaining Java block states + BlockRegistries.JAVA_IDENTIFIER_TO_ID.get().keySet() + .stream() + .filter(s -> s.startsWith(identifier + "[")) + .filter(Predicate.not(componentsMap::containsKey)) + .forEach(state -> componentsMap.put(state, createCustomBlockComponentsMapping(null, state, name))); + } + + if (componentsMap.isEmpty()) { + throw new InvalidCustomMappingsFileException("Unknown Java block: " + identifier); + } + + // 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 + String firstState = componentsMap.keySet().iterator().next(); + CustomBlockComponentsMapping firstComponentsMapping = createCustomBlockComponentsMapping(node, firstState, name); + customBlockDataBuilder.components(firstComponentsMapping.components()); + + return createCustomBlockMapping(customBlockDataBuilder, componentsMap, identifier, !onlyOverrideStates); + } + + private CustomBlockMapping createCustomBlockMapping(CustomBlockData.Builder customBlockDataBuilder, Map componentsMap, String identifier, boolean overrideItem) { + Map> valuesMap = new Object2ObjectOpenHashMap<>(); + + List permutations = new ArrayList<>(); + Map blockStateBuilders = new Object2ObjectOpenHashMap<>(); + + // 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); + + 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]; + + valuesMap.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().components(), String.join(" && ", conditions))); + blockStateBuilders.put(state, new CustomBlockStateBuilderMapping(blockStateBuilder.andThen(CustomBlockState.Builder::build), entry.getValue().extendedCollisionBox())); + } + + valuesMap.forEach((key, value) -> customBlockDataBuilder.stringProperty(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 -> new CustomBlockStateMapping(e.getValue().builder().apply(customBlockData.blockStateBuilder()), e.getValue().extendedCollisionBox()))); + + return new CustomBlockMapping(customBlockData, states, identifier, overrideItem); + } + + /** + * 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 CustomBlockComponentsMapping createCustomBlockComponentsMapping(JsonNode node, String stateKey, String name) { + // This is needed to find the correct selection box for the given block + int id = BlockRegistries.JAVA_IDENTIFIER_TO_ID.getOrDefault(stateKey, -1); + BoxComponent boxComponent = createBoxComponent(id); + BoxComponent extendedBoxComponent = createExtendedBoxComponent(id); + CustomBlockComponents.Builder builder = new CustomBlockComponentsBuilder() + .collisionBox(boxComponent) + .selectionBox(boxComponent); + + if (node == null) { + // No other components were defined + return new CustomBlockComponentsMapping(builder.build(), extendedBoxComponent); + } + + 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); + } + BoxComponent extendedCollisionBox = createBoxComponent(node.get("extended_collision_box")); + if (extendedCollisionBox != null) { + extendedBoxComponent = extendedCollisionBox; + } + + + // We set this to max value by default so that we may dictate the correct destroy time ourselves + float destructibleByMining = Float.MAX_VALUE; + if (node.has("destructible_by_mining")) { + destructibleByMining = node.get("destructible_by_mining").floatValue(); + } + builder.destructibleByMining(destructibleByMining); + + if (node.has("geometry")) { + if (node.get("geometry").isTextual()) { + builder.geometry(new GeometryComponentBuilder() + .identifier(node.get("geometry").asText()) + .build()); + } else { + JsonNode geometry = node.get("geometry"); + GeometryComponentBuilder geometryBuilder = new GeometryComponentBuilder(); + if (geometry.has("identifier")) { + geometryBuilder.identifier(geometry.get("identifier").asText()); + } + if (geometry.has("bone_visibility")) { + JsonNode boneVisibility = geometry.get("bone_visibility"); + if (boneVisibility.isObject()) { + Map boneVisibilityMap = new Object2ObjectOpenHashMap<>(); + boneVisibility.fields().forEachRemaining(entry -> { + boneVisibilityMap.put(entry.getKey(), entry.getValue().isBoolean() ? (entry.getValue().asBoolean() ? "1" : "0") : entry.getValue().asText()); + }); + geometryBuilder.boneVisibility(boneVisibilityMap); + } + } + builder.geometry(geometryBuilder.build()); + } + } + + String displayName = name; + if (node.has("display_name")) { + displayName = node.get("display_name").asText(); + } + builder.displayName(displayName); + + if (node.has("friction")) { + builder.friction(node.get("friction").floatValue()); + } + + if (node.has("light_emission")) { + builder.lightEmission(node.get("light_emission").asInt()); + } + + if (node.has("light_dampening")) { + builder.lightDampening(node.get("light_dampening").asInt()); + } + + boolean placeAir = true; + if (node.has("place_air")) { + placeAir = node.get("place_air").asBoolean(); + } + builder.placeAir(placeAir); + + if (node.has("transformation")) { + JsonNode transformation = node.get("transformation"); + + int rotationX = 0; + int rotationY = 0; + int rotationZ = 0; + float scaleX = 1; + float scaleY = 1; + float scaleZ = 1; + float transformX = 0; + float transformY = 0; + float transformZ = 0; + + if (transformation.has("rotation")) { + JsonNode rotation = transformation.get("rotation"); + rotationX = rotation.get(0).asInt(); + rotationY = rotation.get(1).asInt(); + rotationZ = rotation.get(2).asInt(); + } + if (transformation.has("scale")) { + JsonNode scale = transformation.get("scale"); + scaleX = scale.get(0).floatValue(); + scaleY = scale.get(1).floatValue(); + scaleZ = scale.get(2).floatValue(); + } + if (transformation.has("translation")) { + JsonNode translation = transformation.get("translation"); + transformX = translation.get(0).floatValue(); + transformY = translation.get(1).floatValue(); + transformZ = translation.get(2).floatValue(); + } + builder.transformation(new TransformationComponent(rotationX, rotationY, rotationZ, scaleX, scaleY, scaleZ, transformX, transformY, transformZ)); + } + + if (node.has("unit_cube")) { + builder.unitCube(node.get("unit_cube").asBoolean()); + } + + if (node.has("material_instances")) { + JsonNode materialInstances = node.get("material_instances"); + if (materialInstances.isObject()) { + materialInstances.fields().forEachRemaining(entry -> { + String key = entry.getKey(); + JsonNode value = entry.getValue(); + if (value.isObject()) { + MaterialInstance materialInstance = createMaterialInstanceComponent(value, name); + builder.materialInstance(key, materialInstance); + } + }); + } + } + + if (node.has("placement_filter")) { + JsonNode placementFilter = node.get("placement_filter"); + if (placementFilter.isObject()) { + if (placementFilter.has("conditions")) { + JsonNode conditions = placementFilter.get("conditions"); + if (conditions.isArray()) { + List filter = createPlacementFilterComponent(conditions); + builder.placementFilter(filter); + } + } + } + } + + // 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 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); + } + + return new CustomBlockComponentsMapping(builder.build(), extendedBoxComponent); + } + + /** + * Creates a {@link BoxComponent} based on a Java block's collision with provided bounds and offsets + * + * @param javaId the block's Java ID + * @param heightTranslation the height translation of the box + * @return the {@link BoxComponent} + */ + private BoxComponent createBoxComponent(int javaId, float heightTranslation) { + // Some blocks (e.g. plants) have no collision box + BlockCollision blockCollision = BlockUtils.getCollision(javaId); + if (blockCollision == null || blockCollision.getBoundingBoxes().length == 0) { + return BoxComponent.emptyBox(); + } + + float minX = 5; + float minY = 5; + float minZ = 5; + float maxX = -5; + float maxY = -5; + float maxZ = -5; + for (BoundingBox boundingBox : blockCollision.getBoundingBoxes()) { + double offsetX = boundingBox.getSizeX() * 0.5; + double offsetY = boundingBox.getSizeY() * 0.5; + double offsetZ = boundingBox.getSizeZ() * 0.5; + + minX = Math.min(minX, (float) (boundingBox.getMiddleX() - offsetX)); + minY = Math.min(minY, (float) (boundingBox.getMiddleY() - offsetY)); + minZ = Math.min(minZ, (float) (boundingBox.getMiddleZ() - offsetZ)); + + maxX = Math.max(maxX, (float) (boundingBox.getMiddleX() + offsetX)); + maxY = Math.max(maxY, (float) (boundingBox.getMiddleY() + offsetY)); + maxZ = Math.max(maxZ, (float) (boundingBox.getMiddleZ() + offsetZ)); + } + minX = MathUtils.clamp(minX, 0, 1); + minY = MathUtils.clamp(minY + heightTranslation, 0, 1); + minZ = MathUtils.clamp(minZ, 0, 1); + maxX = MathUtils.clamp(maxX, 0, 1); + maxY = MathUtils.clamp(maxY + heightTranslation, 0, 1); + maxZ = MathUtils.clamp(maxZ, 0, 1); + + return new BoxComponent( + 16 * (1 - maxX) - 8, // For some odd reason X is mirrored on Bedrock + 16 * minY, + 16 * minZ - 8, + 16 * (maxX - minX), + 16 * (maxY - minY), + 16 * (maxZ - minZ) + ); + } + + /** + * 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 javaId) { + return createBoxComponent(javaId, 0); + } + + /** + * Creates the {@link BoxComponent} for an extended collision box based on a Java block's collision + * + * @param javaId the block's Java ID + * @return the {@link BoxComponent} or null if the block's collision box would not exceed 16 y units + */ + private BoxComponent createExtendedBoxComponent(int javaId) { + BlockCollision blockCollision = BlockUtils.getCollision(javaId); + if (blockCollision == null) { + return null; + } + for (BoundingBox box : blockCollision.getBoundingBoxes()) { + double maxY = 0.5 * box.getSizeY() + box.getMiddleY(); + if (maxY > 1) { + return createBoxComponent(javaId, -1); + } + } + return null; + } + + /** + * 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; + } + + /** + * 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) { + // Set default values, and use what the user provides if they have provided something + String texture = name; + if (node.has("texture")) { + texture = node.get("texture").asText(); + } + + String renderMethod = "opaque"; + if (node.has("render_method")) { + renderMethod = node.get("render_method").asText(); + } + + boolean faceDimming = true; + if (node.has("face_dimming")) { + faceDimming = node.get("face_dimming").asBoolean(); + } + + boolean ambientOcclusion = true; + if (node.has("ambient_occlusion")) { + ambientOcclusion = node.get("ambient_occlusion").asBoolean(); + } + + return new MaterialInstanceBuilder() + .texture(texture) + .renderMethod(renderMethod) + .faceDimming(faceDimming) + .ambientOcclusion(ambientOcclusion) + .build(); + } + + /** + * Creates the list of {@link PlacementConditions} for the passed conditions node + * + * @param node the conditions node + * @return the list of {@link PlacementConditions} + */ + private List createPlacementFilterComponent(JsonNode node) { + List conditions = new ArrayList<>(); + + // The structure of the placement filter component is the most complex of the current components + // Each condition effectively separated into two arrays: one of allowed faces, and one of blocks/block Molang queries + node.forEach(condition -> { + 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()))); + } + } + + LinkedHashMap blockFilters = new LinkedHashMap<>(); + if (condition.has("block_filter")) { + JsonNode blockFilter = condition.get("block_filter"); + if (blockFilter.isArray()) { + blockFilter.forEach(filter -> { + if (filter.isObject()) { + if (filter.has("tags")) { + JsonNode tags = filter.get("tags"); + blockFilters.put(tags.asText(), BlockFilterType.TAG); + } + } else if (filter.isTextual()) { + blockFilters.put(filter.asText(), BlockFilterType.BLOCK); + } + }); + } + } + + conditions.add(new PlacementConditions(faces, blockFilters)); + }); + + return conditions; + } + + /** + * 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) { + int openBracketIndex = state.indexOf("["); + + String states = state.substring(openBracketIndex + 1, state.length() - 1); + return states.split(","); + } + +} diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java index 8e30143e8..ce6cec075 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java @@ -30,13 +30,19 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Interner; import com.google.common.collect.Interners; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.*; import org.cloudburstmc.nbt.*; import org.cloudburstmc.protocol.bedrock.codec.v582.Bedrock_v582; +import org.cloudburstmc.protocol.bedrock.data.BlockPropertyData; import org.cloudburstmc.protocol.bedrock.codec.v589.Bedrock_v589; import org.cloudburstmc.protocol.bedrock.codec.v594.Bedrock_v594; import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.block.custom.CustomBlockData; +import org.geysermc.geyser.api.block.custom.CustomBlockState; +import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockState; import org.geysermc.geyser.level.block.BlockStateValues; import org.geysermc.geyser.level.physics.PistonBehavior; import org.geysermc.geyser.registry.BlockRegistries; @@ -47,6 +53,7 @@ import org.geysermc.geyser.util.BlockUtils; import java.io.DataInputStream; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.*; import java.util.function.BiFunction; import java.util.zip.GZIPInputStream; @@ -55,15 +62,35 @@ import java.util.zip.GZIPInputStream; * Populates the block registries. */ public final class BlockRegistryPopulator { + /** + * The stage of population + */ + public enum Stage { + PRE_INIT, + INIT_JAVA, + INIT_BEDROCK, + POST_INIT; + } + + public static void populate(Stage stage) { + switch (stage) { + case PRE_INIT -> { nullifyBlocksNode(); } + case INIT_JAVA -> { registerJavaBlocks(); } + case INIT_BEDROCK -> { registerBedrockBlocks(); } + case POST_INIT -> { nullifyBlocksNode(); } + default -> { throw new IllegalArgumentException("Unknown stage: " + stage); } + } + } + /** * Stores the raw blocks JSON until it is no longer needed. */ private static JsonNode BLOCKS_JSON; + private static int minCustomRuntimeID = -1; + private static int maxCustomRuntimeID = -1; + private static int javaBlocksSize = -1; - public static void populate() { - registerJavaBlocks(); - registerBedrockBlocks(); - + private static void nullifyBlocksNode() { BLOCKS_JSON = null; } @@ -154,36 +181,83 @@ public final class BlockRegistryPopulator { Interner statesInterner = Interners.newStrongInterner(); for (Map.Entry, BiFunction> palette : blockMappers.entrySet()) { - NbtList blocksTag; + int protocolVersion = palette.getKey().valueInt(); + List vanillaBlockStates; + List blockStates; try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResource(String.format("bedrock/block_palette.%s.nbt", palette.getKey().key())); - NBTInputStream nbtInputStream = new NBTInputStream(new DataInputStream(new GZIPInputStream(stream)), true, true)) { + NBTInputStream nbtInputStream = new NBTInputStream(new DataInputStream(new GZIPInputStream(stream)), true, true)) { NbtMap blockPalette = (NbtMap) nbtInputStream.readTag(); - blocksTag = (NbtList) blockPalette.getList("blocks", NbtType.COMPOUND); + + vanillaBlockStates = new ArrayList<>(blockPalette.getList("blocks", NbtType.COMPOUND)); + for (int i = 0; i < vanillaBlockStates.size(); i++) { + NbtMapBuilder builder = vanillaBlockStates.get(i).toBuilder(); + builder.remove("name_hash"); // Quick workaround - was added in 1.19.20 + builder.remove("network_id"); // Added in 1.19.80 - ???? + builder.putCompound("states", statesInterner.intern((NbtMap) builder.remove("states"))); + vanillaBlockStates.set(i, builder.build()); + } + + blockStates = new ArrayList<>(vanillaBlockStates); } catch (Exception e) { throw new AssertionError("Unable to get blocks from runtime block states", e); } + int stateVersion = vanillaBlockStates.get(0).getInt("version"); + + List customBlockProperties = new ArrayList<>(); + List customBlockStates = new ArrayList<>(); + List customExtBlockStates = new ArrayList<>(); + int[] remappedVanillaIds = new int[0]; + if (BlockRegistries.CUSTOM_BLOCKS.get().length != 0) { + for (CustomBlockData customBlock : BlockRegistries.CUSTOM_BLOCKS.get()) { + customBlockProperties.add(CustomBlockRegistryPopulator.generateBlockPropertyData(customBlock, protocolVersion)); + CustomBlockRegistryPopulator.generateCustomBlockStates(customBlock, customBlockStates, customExtBlockStates, stateVersion); + } + blockStates.addAll(customBlockStates); + GeyserImpl.getInstance().getLogger().debug("Added " + customBlockStates.size() + " custom block states to v" + protocolVersion + " palette."); + + // The palette is sorted by the FNV1 64-bit hash of the name + blockStates.sort((a, b) -> Long.compareUnsigned(fnv164(a.getString("name")), fnv164(b.getString("name")))); + } + // New since 1.16.100 - find the block runtime ID by the order given to us in the block palette, // as we no longer send a block palette - Object2ObjectMap blockStateOrderedMap = new Object2ObjectOpenHashMap<>(blocksTag.size()); - GeyserBedrockBlock[] bedrockRuntimeMap = new GeyserBedrockBlock[blocksTag.size()]; - - int stateVersion = -1; - for (int i = 0; i < blocksTag.size(); i++) { - NbtMapBuilder builder = blocksTag.get(i).toBuilder(); - builder.remove("name_hash"); // Quick workaround - was added in 1.19.20 - builder.remove("network_id"); // Added in 1.19.80 - ???? - builder.putCompound("states", statesInterner.intern((NbtMap) builder.remove("states"))); - NbtMap tag = builder.build(); + Object2ObjectMap blockStateOrderedMap = new Object2ObjectOpenHashMap<>(blockStates.size()); + GeyserBedrockBlock[] bedrockRuntimeMap = new GeyserBedrockBlock[blockStates.size()]; + for (int i = 0; i < blockStates.size(); i++) { + NbtMap tag = blockStates.get(i); if (blockStateOrderedMap.containsKey(tag)) { throw new AssertionError("Duplicate block states in Bedrock palette: " + tag); } GeyserBedrockBlock block = new GeyserBedrockBlock(i, tag); blockStateOrderedMap.put(tag, block); bedrockRuntimeMap[i] = block; - if (stateVersion == -1) { - stateVersion = tag.getInt("version"); + } + + Object2ObjectMap customBlockStateDefinitions = Object2ObjectMaps.emptyMap(); + Int2ObjectMap extendedCollisionBoxes = new Int2ObjectOpenHashMap<>(); + if (BlockRegistries.CUSTOM_BLOCKS.get().length != 0) { + customBlockStateDefinitions = new Object2ObjectOpenHashMap<>(customExtBlockStates.size()); + for (int i = 0; i < customExtBlockStates.size(); i++) { + NbtMap tag = customBlockStates.get(i); + CustomBlockState blockState = customExtBlockStates.get(i); + GeyserBedrockBlock bedrockBlock = blockStateOrderedMap.get(tag); + customBlockStateDefinitions.put(blockState, bedrockBlock); + + Set extendedCollisionjavaIds = BlockRegistries.EXTENDED_COLLISION_BOXES.getOrDefault(blockState.block(), null); + if (extendedCollisionjavaIds != null) { + for (int javaId : extendedCollisionjavaIds) { + extendedCollisionBoxes.put(javaId, bedrockBlock); + } + } + } + + remappedVanillaIds = new int[vanillaBlockStates.size()]; + for (int i = 0; i < vanillaBlockStates.size(); i++) { + GeyserBedrockBlock bedrockBlock = blockStateOrderedMap.get(vanillaBlockStates.get(i)); + remappedVanillaIds[i] = bedrockBlock != null ? bedrockBlock.getRuntimeId() : -1; } } + int javaRuntimeId = -1; GeyserBedrockBlock airDefinition = null; @@ -194,7 +268,8 @@ public final class BlockRegistryPopulator { BiFunction stateMapper = blockMappers.getOrDefault(palette.getKey(), emptyMapper); - GeyserBedrockBlock[] javaToBedrockBlocks = new GeyserBedrockBlock[BLOCKS_JSON.size()]; + GeyserBedrockBlock[] javaToBedrockBlocks = new GeyserBedrockBlock[javaBlocksSize]; + GeyserBedrockBlock[] javaToVanillaBedrockBlocks = new GeyserBedrockBlock[javaBlocksSize]; Map flowerPotBlocks = new Object2ObjectOpenHashMap<>(); Map itemFrames = new Object2ObjectOpenHashMap<>(); @@ -206,11 +281,22 @@ public final class BlockRegistryPopulator { javaRuntimeId++; Map.Entry entry = blocksIterator.next(); String javaId = entry.getKey(); + GeyserBedrockBlock vanillaBedrockDefinition = blockStateOrderedMap.get(buildBedrockState(entry.getValue(), stateVersion, stateMapper)); - GeyserBedrockBlock bedrockDefinition = blockStateOrderedMap.get(buildBedrockState(entry.getValue(), stateVersion, stateMapper)); - if (bedrockDefinition == null) { - throw new RuntimeException("Unable to find " + javaId + " Bedrock BlockDefinition on version " - + palette.getKey().key() + "! Built NBT tag: \n" + buildBedrockState(entry.getValue(), stateVersion, stateMapper)); + GeyserBedrockBlock bedrockDefinition; + CustomBlockState blockStateOverride = BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.get(javaRuntimeId); + if (blockStateOverride == null) { + bedrockDefinition = vanillaBedrockDefinition; + if (bedrockDefinition == null) { + throw new RuntimeException("Unable to find " + javaId + " Bedrock runtime ID! Built NBT tag: \n" + + palette.getKey().key() + buildBedrockState(entry.getValue(), stateVersion, stateMapper)); + } + } else { + bedrockDefinition = customBlockStateDefinitions.get(blockStateOverride); + if (bedrockDefinition == null) { + throw new RuntimeException("Unable to find " + javaId + " Bedrock runtime ID! Custom block override: \n" + + blockStateOverride); + } } switch (javaId) { @@ -236,9 +322,10 @@ public final class BlockRegistryPopulator { // Get the tag needed for non-empty flower pots if (entry.getValue().get("pottable") != null) { - flowerPotBlocks.put(cleanJavaIdentifier.intern(), blocksTag.get(bedrockDefinition.getRuntimeId())); + flowerPotBlocks.put(cleanJavaIdentifier.intern(), blockStates.get(bedrockDefinition.getRuntimeId())); } + javaToVanillaBedrockBlocks[javaRuntimeId] = vanillaBedrockDefinition; javaToBedrockBlocks[javaRuntimeId] = bedrockDefinition; } @@ -263,6 +350,33 @@ public final class BlockRegistryPopulator { } builder.bedrockMovingBlock(movingBlockDefinition); + Map nonVanillaStateOverrides = BlockRegistries.NON_VANILLA_BLOCK_STATE_OVERRIDES.get(); + if (nonVanillaStateOverrides.size() > 0) { + // First ensure all non vanilla runtime IDs at minimum are air in case they aren't consecutive + Arrays.fill(javaToVanillaBedrockBlocks, minCustomRuntimeID, javaToVanillaBedrockBlocks.length, airDefinition); + Arrays.fill(javaToBedrockBlocks, minCustomRuntimeID, javaToBedrockBlocks.length, airDefinition); + + for (Map.Entry entry : nonVanillaStateOverrides.entrySet()) { + GeyserBedrockBlock bedrockDefinition = customBlockStateDefinitions.get(entry.getValue()); + if (bedrockDefinition == null) { + GeyserImpl.getInstance().getLogger().warning("Unable to find custom block for " + entry.getValue()); + continue; + } + + JavaBlockState javaState = entry.getKey(); + int stateRuntimeId = javaState.javaId(); + + boolean waterlogged = javaState.waterlogged(); + + if (waterlogged) { + BlockRegistries.WATERLOGGED.register(set -> set.set(stateRuntimeId)); + } + + javaToVanillaBedrockBlocks[stateRuntimeId] = bedrockDefinition; // TODO: Check this? + javaToBedrockBlocks[stateRuntimeId] = bedrockDefinition; + } + } + // Loop around again to find all item frame runtime IDs Object2ObjectMaps.fastForEach(blockStateOrderedMap, entry -> { String name = entry.getKey().getString("name"); @@ -274,10 +388,15 @@ public final class BlockRegistryPopulator { BlockRegistries.BLOCKS.register(palette.getKey().valueInt(), builder.blockStateVersion(stateVersion) .bedrockRuntimeMap(bedrockRuntimeMap) .javaToBedrockBlocks(javaToBedrockBlocks) + .javaToVanillaBedrockBlocks(javaToVanillaBedrockBlocks) .stateDefinitionMap(blockStateOrderedMap) .itemFrames(itemFrames) .flowerPotBlocks(flowerPotBlocks) .jigsawStates(jigsawDefinitions) + .remappedVanillaIds(remappedVanillaIds) + .blockProperties(customBlockProperties) + .customBlockStateDefinitions(customBlockStateDefinitions) + .extendedCollisionBoxes(extendedCollisionBoxes) .build()); } } @@ -290,7 +409,20 @@ public final class BlockRegistryPopulator { throw new AssertionError("Unable to load Java block mappings", e); } - BlockRegistries.JAVA_BLOCKS.set(new BlockMapping[blocksJson.size()]); // Set array size to number of blockstates + javaBlocksSize = blocksJson.size(); + + if (BlockRegistries.NON_VANILLA_BLOCK_STATE_OVERRIDES.get().size() > 0) { + minCustomRuntimeID = BlockRegistries.NON_VANILLA_BLOCK_STATE_OVERRIDES.get().keySet().stream().min(Comparator.comparing(JavaBlockState::javaId)).get().javaId(); + maxCustomRuntimeID = BlockRegistries.NON_VANILLA_BLOCK_STATE_OVERRIDES.get().keySet().stream().max(Comparator.comparing(JavaBlockState::javaId)).get().javaId(); + + if (minCustomRuntimeID < blocksJson.size()) { + throw new RuntimeException("Non vanilla custom block state overrides runtime ID must start after the last vanilla block state (" + javaBlocksSize + ")"); + } + + javaBlocksSize = maxCustomRuntimeID + 1; // Runtime ids start at 0, so we need to add 1 + } + + BlockRegistries.JAVA_BLOCKS.set(new BlockMapping[javaBlocksSize]); // Set array size to number of blockstates Deque cleanIdentifiers = new ArrayDeque<>(); @@ -428,6 +560,46 @@ public final class BlockRegistryPopulator { } BlockStateValues.JAVA_WATER_ID = waterRuntimeId; + if (BlockRegistries.NON_VANILLA_BLOCK_STATE_OVERRIDES.get().size() > 0) { + Set usedNonVanillaRuntimeIDs = new HashSet<>(); + + for (JavaBlockState javaBlockState : BlockRegistries.NON_VANILLA_BLOCK_STATE_OVERRIDES.get().keySet()) { + if (!usedNonVanillaRuntimeIDs.add(javaBlockState.javaId())) { + throw new RuntimeException("Duplicate runtime ID " + javaBlockState.javaId() + " for non vanilla Java block state " + javaBlockState.identifier()); + } + + CustomBlockState customBlockState = BlockRegistries.NON_VANILLA_BLOCK_STATE_OVERRIDES.get().get(javaBlockState); + + String javaId = javaBlockState.identifier(); + int stateRuntimeId = javaBlockState.javaId(); + BlockMapping blockMapping = BlockMapping.builder() + .canBreakWithHand(javaBlockState.canBreakWithHand()) + .pickItem(javaBlockState.pickItem()) + .isNonVanilla(true) + .javaIdentifier(javaId) + .javaBlockId(javaBlockState.stateGroupId()) + .hardness(javaBlockState.blockHardness()) + .pistonBehavior(javaBlockState.pistonBehavior() == null ? PistonBehavior.NORMAL : PistonBehavior.getByName(javaBlockState.pistonBehavior())) + .isBlockEntity(javaBlockState.hasBlockEntity()) + .build(); + + String cleanJavaIdentifier = BlockUtils.getCleanIdentifier(javaBlockState.identifier()); + String bedrockIdentifier = customBlockState.block().identifier(); + + if (!cleanJavaIdentifier.equals(cleanIdentifiers.peekLast())) { + uniqueJavaId++; + cleanIdentifiers.add(cleanJavaIdentifier.intern()); + } + + BlockRegistries.JAVA_IDENTIFIER_TO_ID.register(javaId, stateRuntimeId); + BlockRegistries.JAVA_BLOCKS.register(stateRuntimeId, blockMapping); + + // Keeping this here since this is currently unchanged between versions + // It's possible to only have this store differences in names, but the key set of all Java names is used in sending command suggestions + BlockRegistries.JAVA_TO_BEDROCK_IDENTIFIERS.register(cleanJavaIdentifier.intern(), bedrockIdentifier.intern()); + } + } + BlockRegistries.CLEAN_JAVA_IDENTIFIERS.set(cleanIdentifiers.toArray(new String[0])); BLOCKS_JSON = blocksJson; @@ -481,4 +653,22 @@ public final class BlockRegistryPopulator { tagBuilder.put("states", statesBuilder.build()); return tagBuilder.build(); } + + private static final long FNV1_64_OFFSET_BASIS = 0xcbf29ce484222325L; + private static final long FNV1_64_PRIME = 1099511628211L; + + /** + * Hashes a string using the FNV-1a 64-bit algorithm. + * + * @param str The string to hash + * @return The hashed string + */ + private static long fnv164(String str) { + long hash = FNV1_64_OFFSET_BASIS; + for (byte b : str.getBytes(StandardCharsets.UTF_8)) { + hash *= FNV1_64_PRIME; + hash ^= b; + } + return hash; + } } diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/CustomBlockRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/CustomBlockRegistryPopulator.java new file mode 100644 index 000000000..5d453582c --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/CustomBlockRegistryPopulator.java @@ -0,0 +1,480 @@ +package org.geysermc.geyser.registry.populator; + +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.nbt.NbtMapBuilder; +import org.cloudburstmc.nbt.NbtType; +import org.cloudburstmc.protocol.bedrock.codec.v594.Bedrock_v594; +import org.cloudburstmc.protocol.bedrock.data.BlockPropertyData; +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.CustomBlockComponents; +import org.geysermc.geyser.api.block.custom.component.MaterialInstance; +import org.geysermc.geyser.api.block.custom.component.PlacementConditions; +import org.geysermc.geyser.api.block.custom.component.PlacementConditions.Face; +import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockState; +import org.geysermc.geyser.api.block.custom.property.CustomBlockProperty; +import org.geysermc.geyser.api.block.custom.property.PropertyType; +import org.geysermc.geyser.api.event.lifecycle.GeyserDefineCustomBlocksEvent; +import org.geysermc.geyser.api.util.CreativeCategory; +import org.geysermc.geyser.level.block.GeyserCustomBlockState; +import org.geysermc.geyser.level.block.GeyserCustomBlockComponents.CustomBlockComponentsBuilder; +import org.geysermc.geyser.level.block.GeyserCustomBlockData.CustomBlockDataBuilder; +import org.geysermc.geyser.level.block.GeyserGeometryComponent.GeometryComponentBuilder; +import org.geysermc.geyser.level.block.GeyserMaterialInstance.MaterialInstanceBuilder; +import org.geysermc.geyser.registry.BlockRegistries; +import org.geysermc.geyser.registry.mappings.MappingsConfigReader; +import org.geysermc.geyser.registry.type.CustomSkull; +import org.geysermc.geyser.util.MathUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class CustomBlockRegistryPopulator { + /** + * The stage of population + */ + public enum Stage { + DEFINITION, + VANILLA_REGISTRATION, + NON_VANILLA_REGISTRATION, + CUSTOM_REGISTRATION; + } + + /** + * Populates the custom block registries by stage + * + * @param stage the stage to populate + */ + public static void populate(Stage stage) { + if (!GeyserImpl.getInstance().getConfig().isAddNonBedrockItems()) { + return; + } + + switch (stage) { + case DEFINITION -> { populateBedrock(); } + case VANILLA_REGISTRATION -> { populateVanilla(); } + case NON_VANILLA_REGISTRATION -> { populateNonVanilla(); } + case CUSTOM_REGISTRATION -> { registration(); } + default -> { throw new IllegalArgumentException("Unknown stage: " + stage); } + } + } + + private static Set customBlocks; + private static Set customBlockNames; + private static Int2ObjectMap blockStateOverrides; + private static Map customBlockItemOverrides; + private static Map nonVanillaBlockStateOverrides; + + /** + * Initializes custom blocks defined by API + */ + private static void populateBedrock() { + customBlocks = new ObjectOpenHashSet<>(); + customBlockNames = new ObjectOpenHashSet<>(); + blockStateOverrides = new Int2ObjectOpenHashMap<>(); + customBlockItemOverrides = new HashMap<>(); + nonVanillaBlockStateOverrides = new HashMap<>(); + + GeyserImpl.getInstance().getEventBus().fire(new GeyserDefineCustomBlocksEvent() { + @Override + public void register(@NonNull CustomBlockData customBlockData) { + if (customBlockData.name().length() == 0) { + throw new IllegalArgumentException("Custom block name must have at least 1 character."); + } + if (!customBlockNames.add(customBlockData.name())) { + throw new IllegalArgumentException("Another custom block was already registered under the name: " + customBlockData.name()); + } + if (Character.isDigit(customBlockData.name().charAt(0))) { + throw new IllegalArgumentException("Custom block can not start with a digit. Name: " + customBlockData.name()); + } + customBlocks.add(customBlockData); + } + + @Override + public void registerOverride(@NonNull String javaIdentifier, @NonNull CustomBlockState customBlockState) { + int id = BlockRegistries.JAVA_IDENTIFIER_TO_ID.getOrDefault(javaIdentifier, -1); + if (id == -1) { + throw new IllegalArgumentException("Unknown Java block state. Identifier: " + javaIdentifier); + } + if (!customBlocks.contains(customBlockState.block())) { + throw new IllegalArgumentException("Custom block is unregistered. Name: " + customBlockState.name()); + } + CustomBlockState oldBlockState = blockStateOverrides.put(id, customBlockState); + if (oldBlockState != null) { + GeyserImpl.getInstance().getLogger().debug("Duplicate block state override for Java Identifier: " + + javaIdentifier + " Old override: " + oldBlockState.name() + " New override: " + customBlockState.name()); + } + } + + @Override + public void registerItemOverride(@NonNull String javaIdentifier, @NonNull CustomBlockData customBlockData) { + if (!customBlocks.contains(customBlockData)) { + throw new IllegalArgumentException("Custom block is unregistered. Name: " + customBlockData.name()); + } + customBlockItemOverrides.put(javaIdentifier, customBlockData); + } + + @Override + public void registerOverride(@NonNull JavaBlockState javaBlockState, @NonNull CustomBlockState customBlockState) { + if (!customBlocks.contains(customBlockState.block())) { + throw new IllegalArgumentException("Custom block is unregistered. Name: " + customBlockState.name()); + } + nonVanillaBlockStateOverrides.put(javaBlockState, customBlockState); + } + }); + } + + /** + * Registers all vanilla custom blocks and skulls defined by API and mappings + */ + private static void populateVanilla() { + for (CustomSkull customSkull : BlockRegistries.CUSTOM_SKULLS.get().values()) { + customBlocks.add(customSkull.getCustomBlockData()); + } + + Map> extendedCollisionBoxes = new HashMap<>(); + Map extendedCollisionBoxSet = new HashMap<>(); + MappingsConfigReader mappingsConfigReader = new MappingsConfigReader(); + mappingsConfigReader.loadBlockMappingsFromJson((key, block) -> { + customBlocks.add(block.data()); + if (block.overrideItem()) { + customBlockItemOverrides.put(block.javaIdentifier(), block.data()); + } + block.states().forEach((javaIdentifier, customBlockState) -> { + int id = BlockRegistries.JAVA_IDENTIFIER_TO_ID.getOrDefault(javaIdentifier, -1); + blockStateOverrides.put(id, customBlockState.state()); + BoxComponent extendedCollisionBox = customBlockState.extendedCollisionBox(); + if (extendedCollisionBox != null) { + CustomBlockData extendedCollisionBlock = extendedCollisionBoxSet.computeIfAbsent(extendedCollisionBox, box -> { + CustomBlockData collisionBlock = createExtendedCollisionBlock(box, extendedCollisionBoxSet.size()); + customBlocks.add(collisionBlock); + return collisionBlock; + }); + extendedCollisionBoxes.computeIfAbsent(extendedCollisionBlock, k -> new HashSet<>()) + .add(id); + } + }); + }); + + BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.set(blockStateOverrides); + GeyserImpl.getInstance().getLogger().info("Registered " + blockStateOverrides.size() + " custom block overrides."); + + BlockRegistries.CUSTOM_BLOCK_ITEM_OVERRIDES.set(customBlockItemOverrides); + GeyserImpl.getInstance().getLogger().info("Registered " + customBlockItemOverrides.size() + " custom block item overrides."); + + BlockRegistries.EXTENDED_COLLISION_BOXES.set(extendedCollisionBoxes); + GeyserImpl.getInstance().getLogger().info("Registered " + extendedCollisionBoxes.size() + " custom block extended collision boxes."); + } + + /** + * Registers all non-vanilla custom blocks defined by API + */ + private static void populateNonVanilla() { + BlockRegistries.NON_VANILLA_BLOCK_STATE_OVERRIDES.set(nonVanillaBlockStateOverrides); + GeyserImpl.getInstance().getLogger().info("Registered " + nonVanillaBlockStateOverrides.size() + " non-vanilla block overrides."); + } + + /** + * Registers all bedrock custom blocks defined in previous stages + */ + private static void registration() { + BlockRegistries.CUSTOM_BLOCKS.set(customBlocks.toArray(new CustomBlockData[0])); + GeyserImpl.getInstance().getLogger().info("Registered " + customBlocks.size() + " custom blocks."); + } + + /** + * 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 blockStates, List customExtBlockStates, int stateVersion) { + int totalPermutations = 1; + for (CustomBlockProperty property : customBlock.properties().values()) { + totalPermutations *= property.values().size(); + } + + for (int i = 0; i < totalPermutations; i++) { + NbtMapBuilder statesBuilder = NbtMap.builder(); + int permIndex = i; + for (CustomBlockProperty property : customBlock.properties().values()) { + statesBuilder.put(property.name(), property.values().get(permIndex % property.values().size())); + permIndex /= property.values().size(); + } + NbtMap states = statesBuilder.build(); + + blockStates.add(NbtMap.builder() + .putString("name", customBlock.identifier()) + .putInt("version", stateVersion) + .putCompound("states", states) + .build()); + customExtBlockStates.add(new GeyserCustomBlockState(customBlock, states)); + } + } + + /** + * 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") + static BlockPropertyData generateBlockPropertyData(CustomBlockData customBlock, int protocolVersion) { + List permutations = new ArrayList<>(); + for (CustomBlockPermutation permutation : customBlock.permutations()) { + permutations.add(NbtMap.builder() + .putCompound("components", CustomBlockRegistryPopulator.convertComponents(permutation.components(), protocolVersion)) + .putString("condition", permutation.condition()) + .build()); + } + + // The order that properties are defined influences the order that block states are generated + List properties = new ArrayList<>(); + for (CustomBlockProperty property : customBlock.properties().values()) { + NbtMapBuilder propertyBuilder = NbtMap.builder() + .putString("name", property.name()); + if (property.type() == PropertyType.booleanProp()) { + propertyBuilder.putList("enum", NbtType.BYTE, List.of((byte) 0, (byte) 1)); + } else if (property.type() == PropertyType.integerProp()) { + propertyBuilder.putList("enum", NbtType.INT, (List) property.values()); + } else if (property.type() == PropertyType.stringProp()) { + propertyBuilder.putList("enum", NbtType.STRING, (List) property.values()); + } + properties.add(propertyBuilder.build()); + } + + CreativeCategory creativeCategory = customBlock.creativeCategory() != null ? customBlock.creativeCategory() : CreativeCategory.NONE; + String creativeGroup = customBlock.creativeGroup() != null ? customBlock.creativeGroup() : ""; + NbtMap propertyTag = NbtMap.builder() + .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() + .putString("category", creativeCategory.internalName()) + .putString("group", creativeGroup) + .putBoolean("is_hidden_in_commands", false) + .build()) + // meaning of this version is unknown, but it's required for tags to work and should probably be checked periodically + .putInt("molangVersion", 1) + .putList("permutations", NbtType.COMPOUND, permutations) + .putList("properties", NbtType.COMPOUND, properties) + .build(); + 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 + */ + private static NbtMap convertComponents(CustomBlockComponents components, int protocolVersion) { + if (components == null) { + return NbtMap.EMPTY; + } + + NbtMapBuilder builder = NbtMap.builder(); + if (components.displayName() != null) { + builder.putCompound("minecraft:display_name", NbtMap.builder() + .putString("value", components.displayName()) + .build()); + } + + if (components.selectionBox() != null) { + builder.putCompound("minecraft:selection_box", convertBox(components.selectionBox())); + } + + if (components.collisionBox() != null) { + builder.putCompound("minecraft:collision_box", convertBox(components.collisionBox())); + } + + if (components.geometry() != null) { + NbtMapBuilder geometryBuilder = NbtMap.builder(); + if (protocolVersion >= Bedrock_v594.CODEC.getProtocolVersion()) { + geometryBuilder.putString("identifier", components.geometry().identifier()); + if (components.geometry().boneVisibility() != null) { + NbtMapBuilder boneVisibilityBuilder = NbtMap.builder(); + components.geometry().boneVisibility().entrySet().forEach( + entry -> boneVisibilityBuilder.putString(entry.getKey(), entry.getValue())); + geometryBuilder.putCompound("bone_visibility", boneVisibilityBuilder.build()); + } + } else { + geometryBuilder.putString("value", components.geometry().identifier()); + } + builder.putCompound("minecraft:geometry", geometryBuilder.build()); + } + + if (!components.materialInstances().isEmpty()) { + NbtMapBuilder materialsBuilder = NbtMap.builder(); + for (Map.Entry entry : components.materialInstances().entrySet()) { + MaterialInstance materialInstance = entry.getValue(); + materialsBuilder.putCompound(entry.getKey(), NbtMap.builder() + .putString("texture", materialInstance.texture()) + .putString("render_method", materialInstance.renderMethod()) + .putBoolean("face_dimming", materialInstance.faceDimming()) + .putBoolean("ambient_occlusion", materialInstance.faceDimming()) + .build()); + } + + 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("materials", materialsBuilder.build()) + .build()); + } + + if (components.placementFilter() != null) { + builder.putCompound("minecraft:placement_filter", NbtMap.builder() + .putList("conditions", NbtType.COMPOUND, convertPlacementFilter(components.placementFilter())) + .build()); + } + + if (components.destructibleByMining() != null) { + builder.putCompound("minecraft:destructible_by_mining", NbtMap.builder() + .putFloat("value", components.destructibleByMining()) + .build()); + } + + if (components.friction() != null) { + builder.putCompound("minecraft:friction", NbtMap.builder() + .putFloat("value", components.friction()) + .build()); + } + + if (components.lightEmission() != null) { + builder.putCompound("minecraft:light_emission", NbtMap.builder() + .putByte("emission", components.lightEmission().byteValue()) + .build()); + } + + if (components.lightDampening() != null) { + builder.putCompound("minecraft:light_dampening", NbtMap.builder() + .putByte("lightLevel", components.lightDampening().byteValue()) + .build()); + } + + if (components.transformation() != null) { + builder.putCompound("minecraft:transformation", NbtMap.builder() + .putInt("RX", MathUtils.unwrapDegreesToInt(components.transformation().rx()) / 90) + .putInt("RY", MathUtils.unwrapDegreesToInt(components.transformation().ry()) / 90) + .putInt("RZ", MathUtils.unwrapDegreesToInt(components.transformation().rz()) / 90) + .putFloat("SX", components.transformation().sx()) + .putFloat("SY", components.transformation().sy()) + .putFloat("SZ", components.transformation().sz()) + .putFloat("TX", components.transformation().tx()) + .putFloat("TY", components.transformation().ty()) + .putFloat("TZ", components.transformation().tz()) + .build()); + } + + if (components.unitCube()) { + 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()) { + builder.putCompound("minecraft:on_player_placing", NbtMap.builder() + .putString("triggerType", "geyser:place_event") + .build()); + } + + if (!components.tags().isEmpty()) { + components.tags().forEach(tag -> builder.putCompound("tag:" + tag, NbtMap.EMPTY)); + } + + 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) { + return NbtMap.builder() + .putBoolean("enabled", !boxComponent.isEmpty()) + .putList("origin", NbtType.FLOAT, boxComponent.originX(), boxComponent.originY(), boxComponent.originZ()) + .putList("size", NbtType.FLOAT, boxComponent.sizeX(), boxComponent.sizeY(), boxComponent.sizeZ()) + .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 convertPlacementFilter(List placementFilter) { + List conditions = new ArrayList<>(); + placementFilter.forEach((condition) -> { + 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; + for (Face face : condition.allowedFaces()) { allowedFaces |= (1 << face.ordinal()); } + 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 blockFilters = new ArrayList<>(); + condition.blockFilters().forEach((value, type) -> { + NbtMapBuilder blockFilterBuilder = NbtMap.builder(); + switch (type) { + 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); + } + blockFilters.add(blockFilterBuilder.build()); + }); + conditionBuilder.putList("block_filters", NbtType.COMPOUND, blockFilters); + conditions.add(conditionBuilder.build()); + }); + + return conditions; + } + + private static CustomBlockData createExtendedCollisionBlock(BoxComponent boxComponent, int extendedCollisionBlock) { + CustomBlockData customBlockData = new CustomBlockDataBuilder() + .name("extended_collision_" + extendedCollisionBlock) + .components( + new CustomBlockComponentsBuilder() + .collisionBox(boxComponent) + .selectionBox(BoxComponent.emptyBox()) + .materialInstance("*", new MaterialInstanceBuilder() + .texture("glass") + .renderMethod("alpha_test") + .faceDimming(false) + .ambientOcclusion(false) + .build()) + .lightDampening(0) + .geometry(new GeometryComponentBuilder() + .identifier("geometry.invisible") + .build()) + .build()) + .build(); + return customBlockData; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/CustomItemRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/CustomItemRegistryPopulator.java index 629fc17c5..3f3f5a4ba 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/CustomItemRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/CustomItemRegistryPopulator.java @@ -41,10 +41,9 @@ import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.event.type.GeyserDefineCustomItemsEventImpl; import org.geysermc.geyser.item.GeyserCustomMappingData; import org.geysermc.geyser.item.Items; -import org.geysermc.geyser.item.components.ToolBreakSpeedsUtils; import org.geysermc.geyser.item.components.WearableSlot; -import org.geysermc.geyser.item.mappings.MappingsConfigReader; import org.geysermc.geyser.item.type.Item; +import org.geysermc.geyser.registry.mappings.MappingsConfigReader; import org.geysermc.geyser.registry.type.GeyserMappingItem; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.registry.type.NonVanillaItemRegistration; @@ -56,7 +55,7 @@ public class CustomItemRegistryPopulator { public static void populate(Map items, Multimap customItems, List nonVanillaCustomItems) { MappingsConfigReader mappingsConfigReader = new MappingsConfigReader(); // Load custom items from mappings files - mappingsConfigReader.loadMappingsFromJson((key, item) -> { + mappingsConfigReader.loadItemMappingsFromJson((key, item) -> { if (CustomItemRegistryPopulator.initialCheck(key, item, items)) { customItems.get(key).add(item); } @@ -294,34 +293,45 @@ public class CustomItemRegistryPopulator { boolean canDestroyInCreative = true; float miningSpeed = 1.0f; - if (toolType.equals("shears")) { - componentBuilder.putCompound("minecraft:digger", ToolBreakSpeedsUtils.getShearsDigger(15)); - } else { - int toolSpeed = ToolBreakSpeedsUtils.toolTierToSpeed(toolTier); - switch (toolType) { - case "sword" -> { - miningSpeed = 1.5f; - canDestroyInCreative = false; - componentBuilder.putCompound("minecraft:digger", ToolBreakSpeedsUtils.getSwordDigger(toolSpeed)); - componentBuilder.putCompound("minecraft:weapon", NbtMap.EMPTY); - } - case "pickaxe" -> { - componentBuilder.putCompound("minecraft:digger", ToolBreakSpeedsUtils.getPickaxeDigger(toolSpeed, toolTier)); - setItemTag(componentBuilder, "pickaxe"); - } - case "axe" -> { - componentBuilder.putCompound("minecraft:digger", ToolBreakSpeedsUtils.getAxeDigger(toolSpeed)); - setItemTag(componentBuilder, "axe"); - } - case "shovel" -> { - componentBuilder.putCompound("minecraft:digger", ToolBreakSpeedsUtils.getShovelDigger(toolSpeed)); - setItemTag(componentBuilder, "shovel"); - } - case "hoe" -> { - componentBuilder.putCompound("minecraft:digger", ToolBreakSpeedsUtils.getHoeDigger(toolSpeed)); - setItemTag(componentBuilder, "hoe"); - } - } + // This means client side the tool can never destroy a block + // This works because the molang '1' for tags will be true for all blocks and the speed will be 0 + // We want this since we calculate break speed server side in BedrockActionTranslator + List speed = new ArrayList<>(List.of( + NbtMap.builder() + .putCompound("block", NbtMap.builder() + .putString("tags", "1") + .build()) + .putCompound("on_dig", NbtMap.builder() + .putCompound("condition", NbtMap.builder() + .putString("expression", "") + .putInt("version", -1) + .build()) + .putString("event", "tool_durability") + .putString("target", "self") + .build()) + .putInt("speed", 0) + .build() + )); + + componentBuilder.putCompound("minecraft:digger", + NbtMap.builder() + .putList("destroy_speeds", NbtType.COMPOUND, speed) + .putCompound("on_dig", NbtMap.builder() + .putCompound("condition", NbtMap.builder() + .putString("expression", "") + .putInt("version", -1) + .build()) + .putString("event", "tool_durability") + .putString("target", "self") + .build()) + .putBoolean("use_efficiency", true) + .build() + ); + + if (toolType.equals("sword")) { + miningSpeed = 1.5f; + canDestroyInCreative = false; + componentBuilder.putCompound("minecraft:weapon", NbtMap.EMPTY); } itemProperties.putBoolean("hand_equipped", true); diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/CustomSkullRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/CustomSkullRegistryPopulator.java new file mode 100644 index 000000000..9c17ca952 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/CustomSkullRegistryPopulator.java @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.registry.populator; + +import it.unimi.dsi.fastutil.objects.Object2ObjectMaps; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import lombok.NonNull; +import org.geysermc.geyser.GeyserBootstrap; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.event.lifecycle.GeyserDefineCustomSkullsEvent; +import org.geysermc.geyser.configuration.GeyserCustomSkullConfiguration; +import org.geysermc.geyser.pack.SkullResourcePackManager; +import org.geysermc.geyser.registry.BlockRegistries; +import org.geysermc.geyser.registry.type.CustomSkull; +import org.geysermc.geyser.skin.SkinManager; +import org.geysermc.geyser.skin.SkinProvider; +import org.geysermc.geyser.text.GeyserLocale; +import org.geysermc.geyser.util.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.function.Function; + +public class CustomSkullRegistryPopulator { + + public static void populate() { + SkullResourcePackManager.SKULL_SKINS.clear(); // Remove skins after reloading + BlockRegistries.CUSTOM_SKULLS.set(Object2ObjectMaps.emptyMap()); + + if (!GeyserImpl.getInstance().getConfig().isAddNonBedrockItems()) { + return; + } + + GeyserCustomSkullConfiguration skullConfig; + try { + GeyserBootstrap bootstrap = GeyserImpl.getInstance().getBootstrap(); + Path skullConfigPath = bootstrap.getConfigFolder().resolve("custom-skulls.yml"); + File skullConfigFile = FileUtils.fileOrCopiedFromResource(skullConfigPath.toFile(), "custom-skulls.yml", Function.identity(), bootstrap); + skullConfig = FileUtils.loadConfig(skullConfigFile, GeyserCustomSkullConfiguration.class); + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().severe(GeyserLocale.getLocaleStringLog("geyser.config.failed"), e); + return; + } + + BlockRegistries.CUSTOM_SKULLS.set(new Object2ObjectOpenHashMap<>()); + + List profiles = new ArrayList<>(skullConfig.getPlayerProfiles()); + List usernames = new ArrayList<>(skullConfig.getPlayerUsernames()); + List uuids = new ArrayList<>(skullConfig.getPlayerUUIDs()); + List skinHashes = new ArrayList<>(skullConfig.getPlayerSkinHashes()); + + GeyserImpl.getInstance().getEventBus().fire(new GeyserDefineCustomSkullsEvent() { + @Override + public void register(@NonNull String texture, @NonNull SkullTextureType type) { + switch (type) { + case USERNAME -> usernames.add(texture); + case UUID -> uuids.add(texture); + case PROFILE -> profiles.add(texture); + case SKIN_HASH -> skinHashes.add(texture); + } + } + }); + + usernames.forEach((username) -> { + String profile = getProfileFromUsername(username); + if (profile != null) { + String skinHash = getSkinHash(profile); + if (skinHash != null) { + skinHashes.add(skinHash); + } + } + }); + + uuids.forEach((uuid) -> { + String profile = getProfileFromUuid(uuid); + if (profile != null) { + String skinHash = getSkinHash(profile); + if (skinHash != null) { + skinHashes.add(skinHash); + } + } + }); + + profiles.forEach((profile) -> { + String skinHash = getSkinHash(profile); + if (skinHash != null) { + skinHashes.add(skinHash); + } + }); + + skinHashes.forEach((skinHash) -> { + if (!skinHash.matches("^[a-fA-F0-9]+$")) { + GeyserImpl.getInstance().getLogger().error("Skin hash " + skinHash + " does not match required format ^[a-fA-F0-9]{64}$ and will not be added as a custom block."); + return; + } + + try { + SkullResourcePackManager.cacheSkullSkin(skinHash); + BlockRegistries.CUSTOM_SKULLS.register(skinHash, new CustomSkull(skinHash)); + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error("Failed to cache skin for skull texture " + skinHash + " This skull will not be added as a custom block.", e); + } + }); + + GeyserImpl.getInstance().getLogger().info("Registered " + BlockRegistries.CUSTOM_SKULLS.get().size() + " custom skulls as custom blocks."); + } + + /** + * Gets the skin hash from a base64 encoded profile + * @param profile the base64 encoded profile + * @return the skin hash or null if the profile is invalid + */ + private static String getSkinHash(String profile) { + try { + SkinManager.GameProfileData profileData = SkinManager.GameProfileData.loadFromJson(profile); + if (profileData == null) { + GeyserImpl.getInstance().getLogger().warning("Skull texture " + profile + " contained no skins and will not be added as a custom block."); + return null; + } + String skinUrl = profileData.skinUrl(); + return skinUrl.substring(skinUrl.lastIndexOf("/") + 1); + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error("Skull texture " + profile + " is invalid and will not be added as a custom block.", e); + return null; + } + } + + /** + * Gets the base64 encoded profile from a player's username + * @param username the player username + * @return the base64 encoded profile or null if the request failed + */ + private static String getProfileFromUsername(String username) { + try { + return SkinProvider.requestTexturesFromUsername(username).get(); + } catch (InterruptedException | ExecutionException e) { + GeyserImpl.getInstance().getLogger().error("Unable to request skull textures for " + username + " This skull will not be added as a custom block.", e); + return null; + } + } + + /** + * Gets the base64 encoded profile from a player's UUID + * @param uuid the player UUID + * @return the base64 encoded profile or null if the request failed + */ + private static String getProfileFromUuid(String uuid) { + try { + String uuidDigits = uuid.replace("-", ""); + if (uuidDigits.length() != 32) { + GeyserImpl.getInstance().getLogger().error("Invalid skull uuid " + uuid + " This skull will not be added as a custom block."); + return null; + } + return SkinProvider.requestTexturesFromUUID(uuid).get(); + } catch (InterruptedException | ExecutionException e) { + GeyserImpl.getInstance().getLogger().error("Unable to request skull textures for " + uuid + " This skull will not be added as a custom block.", e); + return null; + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java index b0d88c3e7..f243d358a 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java @@ -34,6 +34,7 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntSet; import it.unimi.dsi.fastutil.objects.*; +import org.geysermc.geyser.Constants; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; @@ -49,15 +50,18 @@ import org.cloudburstmc.protocol.bedrock.data.inventory.ComponentItemData; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.block.custom.CustomBlockData; +import org.geysermc.geyser.api.block.custom.CustomBlockState; +import org.geysermc.geyser.api.block.custom.NonVanillaCustomBlockData; import org.geysermc.geyser.api.item.custom.CustomItemData; import org.geysermc.geyser.api.item.custom.CustomItemOptions; import org.geysermc.geyser.api.item.custom.NonVanillaCustomItemData; import org.geysermc.geyser.inventory.item.StoredItemMappings; import org.geysermc.geyser.item.GeyserCustomMappingData; -import org.geysermc.geyser.item.Items; -import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.registry.type.*; import java.io.InputStream; @@ -176,6 +180,8 @@ public class ItemRegistryPopulator { Object2ObjectMap bedrockBlockIdOverrides = new Object2ObjectOpenHashMap<>(); Object2IntMap blacklistedIdentifiers = new Object2IntOpenHashMap<>(); + Object2ObjectMap customBlockItemDefinitions = new Object2ObjectOpenHashMap<>(); + List buckets = new ObjectArrayList<>(); List carpets = new ObjectArrayList<>(); @@ -249,15 +255,29 @@ public class ItemRegistryPopulator { BlockDefinition bedrockBlock = null; Integer firstBlockRuntimeId = entry.getValue().getFirstBlockRuntimeId(); + BlockDefinition customBlockItemOverride = null; if (firstBlockRuntimeId != null) { BlockDefinition blockOverride = bedrockBlockIdOverrides.get(bedrockIdentifier); - if (blockOverride != null) { + + // We'll do this here for custom blocks we want in the creative inventory so we can piggyback off the existing logic to find these + // blocks in creativeItems + CustomBlockData customBlockData = BlockRegistries.CUSTOM_BLOCK_ITEM_OVERRIDES.getOrDefault(javaItem.javaIdentifier(), null); + if (customBlockData != null) { + // this block has a custom item override and thus we should use its runtime ID for the ItemMapping + if (customBlockData.includedInCreativeInventory()) { + CustomBlockState customBlockState = customBlockData.defaultBlockState(); + customBlockItemOverride = blockMappings.getCustomBlockStateDefinitions().getOrDefault(customBlockState, null); + } + } + + // If it' s a custom block we can't do this because we need to make sure we find the creative item + if (blockOverride != null && customBlockItemOverride == null) { // Straight from BDS is our best chance of getting an item that doesn't run into issues bedrockBlock = blockOverride; } else { // Try to get an example block runtime ID from the creative contents packet, for Bedrock identifier obtaining - int aValidBedrockBlockId = blacklistedIdentifiers.getOrDefault(bedrockIdentifier, -1); - if (aValidBedrockBlockId == -1) { + int aValidBedrockBlockId = blacklistedIdentifiers.getOrDefault(bedrockIdentifier, customBlockItemOverride != null ? customBlockItemOverride.getRuntimeId() : -1); + if (aValidBedrockBlockId == -1 && customBlockItemOverride == null) { // Fallback bedrockBlock = blockMappings.getBedrockBlock(firstBlockRuntimeId); } else { @@ -273,7 +293,7 @@ public class ItemRegistryPopulator { // 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(); for (int i = firstBlockRuntimeId; i <= lastBlockRuntimeId; i++) { - GeyserBedrockBlock bedrockBlockRuntimeId = blockMappings.getBedrockBlock(i); + GeyserBedrockBlock bedrockBlockRuntimeId = blockMappings.getVanillaBedrockBlock(i); NbtMap blockTag = bedrockBlockRuntimeId.getState(); String bedrockName = blockTag.getString("name"); if (!bedrockName.equals(correctBedrockIdentifier)) { @@ -339,6 +359,12 @@ public class ItemRegistryPopulator { // Because we have replaced the Bedrock block ID, we also need to replace the creative contents block runtime ID // That way, creative items work correctly for these blocks + + // Set our custom block override now if there is one + if (customBlockItemOverride != null) { + bedrockBlock = customBlockItemOverride; + } + for (int j = 0; j < creativeItems.size(); j++) { ItemData itemData = creativeItems.get(j); if (itemData.getDefinition().equals(definition)) { @@ -347,16 +373,35 @@ public class ItemRegistryPopulator { } NbtMap states = ((GeyserBedrockBlock) itemData.getBlockDefinition()).getState().getCompound("states"); + boolean valid = true; for (Map.Entry nbtEntry : requiredBlockStates.entrySet()) { - if (!states.get(nbtEntry.getKey()).equals(nbtEntry.getValue())) { + if (states.getOrDefault(nbtEntry.getKey(), null) == null || !states.get(nbtEntry.getKey()).equals(nbtEntry.getValue())) { // A required block state doesn't match - this one is not valid valid = false; break; } } if (valid) { - creativeItems.set(j, itemData.toBuilder().blockDefinition(bedrockBlock).build()); + if (customBlockItemOverride != null && customBlockData != null) { + // Assuming this is a valid custom block override we'll just register it now while we have the creative item + int customProtocolId = nextFreeBedrockId++; + mappingItem.setBedrockData(customProtocolId); + bedrockIdentifier = customBlockData.identifier(); + definition = new SimpleItemDefinition(bedrockIdentifier, customProtocolId, true); + registry.put(customProtocolId, definition); + customBlockItemDefinitions.put(customBlockData, definition); + customIdMappings.put(customProtocolId, bedrockIdentifier); + + creativeItems.set(j, itemData.toBuilder() + .definition(definition) + .blockDefinition(bedrockBlock) + .netId(itemData.getNetId()) + .count(1) + .build()); + } else { + creativeItems.set(j, itemData.toBuilder().blockDefinition(bedrockBlock).build()); + } break; } } @@ -397,10 +442,10 @@ public class ItemRegistryPopulator { for (CustomItemData customItem : customItemsToLoad) { int customProtocolId = nextFreeBedrockId++; - String customItemName = "geyser_custom:" + customItem.name(); + String customItemName = customItem instanceof NonVanillaCustomItemData nonVanillaItem ? nonVanillaItem.identifier() : Constants.GEYSER_CUSTOM_NAMESPACE + ":" + customItem.name(); if (!registeredItemNames.add(customItemName)) { 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 '" + customItemName + "' already exists and was registered again! Skipping..."); } continue; } @@ -516,6 +561,41 @@ public class ItemRegistryPopulator { } } + // Register the item forms of custom blocks + if (BlockRegistries.CUSTOM_BLOCKS.get().length != 0) { + for (CustomBlockData customBlock : BlockRegistries.CUSTOM_BLOCKS.get()) { + // We might've registered it already with the vanilla blocks so check first + if (customBlockItemDefinitions.containsKey(customBlock)) { + continue; + } + + // Non-vanilla custom blocks will be handled in the item + // registry, so we don't need to do anything here. + if (customBlock instanceof NonVanillaCustomBlockData) { + continue; + } + + int customProtocolId = nextFreeBedrockId++; + String identifier = customBlock.identifier(); + + final ItemDefinition definition = new SimpleItemDefinition(identifier, customProtocolId, true); + registry.put(customProtocolId, definition); + customBlockItemDefinitions.put(customBlock, definition); + customIdMappings.put(customProtocolId, identifier); + + GeyserBedrockBlock bedrockBlock = blockMappings.getCustomBlockStateDefinitions().getOrDefault(customBlock.defaultBlockState(), null); + + if (bedrockBlock != null && customBlock.includedInCreativeInventory()) { + creativeItems.add(ItemData.builder() + .definition(definition) + .blockDefinition(bedrockBlock) + .netId(creativeNetId.incrementAndGet()) + .count(1) + .build()); + } + } + } + ItemMappings itemMappings = ItemMappings.builder() .items(mappings.toArray(new ItemMapping[0])) .creativeItems(creativeItems.toArray(new ItemData[0])) @@ -527,6 +607,7 @@ public class ItemRegistryPopulator { .componentItemData(componentItemData) .lodestoneCompass(lodestoneEntry) .customIdMappings(customIdMappings) + .customBlockItemDefinitions(customBlockItemDefinitions) .build(); Registries.ITEMS.register(palette.protocolVersion(), itemMappings); diff --git a/core/src/main/java/org/geysermc/geyser/registry/type/BlockMapping.java b/core/src/main/java/org/geysermc/geyser/registry/type/BlockMapping.java index 34cde0acf..528999158 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/type/BlockMapping.java +++ b/core/src/main/java/org/geysermc/geyser/registry/type/BlockMapping.java @@ -56,6 +56,7 @@ public class BlockMapping { @Nonnull PistonBehavior pistonBehavior; boolean isBlockEntity; + boolean isNonVanilla; /** * @return the identifier without the additional block states diff --git a/core/src/main/java/org/geysermc/geyser/registry/type/BlockMappings.java b/core/src/main/java/org/geysermc/geyser/registry/type/BlockMappings.java index 992ae324a..3c33ea097 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/type/BlockMappings.java +++ b/core/src/main/java/org/geysermc/geyser/registry/type/BlockMappings.java @@ -25,12 +25,17 @@ package org.geysermc.geyser.registry.type; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; import lombok.Builder; import lombok.Value; import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.protocol.bedrock.data.BlockPropertyData; import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; import org.cloudburstmc.protocol.common.DefinitionRegistry; +import org.geysermc.geyser.api.block.custom.CustomBlockState; +import java.util.List; import java.util.Map; import java.util.Set; @@ -44,9 +49,11 @@ public class BlockMappings implements DefinitionRegistry { int blockStateVersion; GeyserBedrockBlock[] javaToBedrockBlocks; + GeyserBedrockBlock[] javaToVanillaBedrockBlocks; Map stateDefinitionMap; GeyserBedrockBlock[] bedrockRuntimeMap; + int[] remappedVanillaIds; BlockDefinition commandBlock; @@ -55,6 +62,10 @@ public class BlockMappings implements DefinitionRegistry { Set jigsawStates; + List blockProperties; + Object2ObjectMap customBlockStateDefinitions; + Int2ObjectMap extendedCollisionBoxes; + public int getBedrockBlockId(int javaState) { return getBedrockBlock(javaState).getRuntimeId(); } @@ -66,6 +77,13 @@ public class BlockMappings implements DefinitionRegistry { return this.javaToBedrockBlocks[javaState]; } + public GeyserBedrockBlock getVanillaBedrockBlock(int javaState) { + if (javaState < 0 || javaState >= this.javaToVanillaBedrockBlocks.length) { + return bedrockAir; + } + return this.javaToVanillaBedrockBlocks[javaState]; + } + public BlockDefinition getItemFrame(NbtMap tag) { return this.itemFrames.get(tag); } diff --git a/core/src/main/java/org/geysermc/geyser/registry/type/CustomSkull.java b/core/src/main/java/org/geysermc/geyser/registry/type/CustomSkull.java new file mode 100644 index 000000000..5fe8a0edf --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/registry/type/CustomSkull.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.registry.type; + +import lombok.Data; +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.CustomBlockComponents; +import org.geysermc.geyser.api.block.custom.component.TransformationComponent; +import org.geysermc.geyser.level.block.GeyserCustomBlockComponents; +import org.geysermc.geyser.level.block.GeyserCustomBlockData; +import org.geysermc.geyser.level.block.GeyserGeometryComponent.GeometryComponentBuilder; +import org.geysermc.geyser.level.block.GeyserMaterialInstance.MaterialInstanceBuilder; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.IntStream; + +@Data +public class CustomSkull { + private final String skinHash; + + private CustomBlockData customBlockData; + + private static final String BITS_A_PROPERTY = "geyser_skull:bits_a"; + private static final String BITS_B_PROPERTY = "geyser_skull:bits_b"; + + private static final int[] ROTATIONS = {0, -90, 180, 90}; + + private static final BoxComponent FLOOR_BOX = new BoxComponent( + -4, 0, -4, + 8, 8, 8 + ); + + private static final BoxComponent WALL_BOX = new BoxComponent( + -4, 4, 0, + 8, 8, 8 + ); + + public CustomSkull(String skinHash) { + this.skinHash = skinHash; + + CustomBlockComponents components = new GeyserCustomBlockComponents.CustomBlockComponentsBuilder() + .destructibleByMining(1.5f) + .materialInstance("*", new MaterialInstanceBuilder() + .texture("geyser." + skinHash + "_player_skin") + .renderMethod("alpha_test") + .faceDimming(true) + .ambientOcclusion(true) + .build()) + .lightDampening(0) + .placeAir(true) + .build(); + + List permutations = new ArrayList<>(); + addDefaultPermutation(permutations); + addFloorPermutations(permutations); + addWallPermutations(permutations); + + customBlockData = new GeyserCustomBlockData.CustomBlockDataBuilder() + .name("player_skull_" + skinHash) + .components(components) + .intProperty(BITS_A_PROPERTY, IntStream.rangeClosed(0, 6).boxed().toList()) // This gives us exactly 21 block states + .intProperty(BITS_B_PROPERTY, IntStream.rangeClosed(0, 2).boxed().toList()) + .permutations(permutations) + .build(); + } + + public CustomBlockState getWallBlockState(int wallDirection) { + wallDirection = switch (wallDirection) { + case 0 -> 2; // South + case 90 -> 3; // West + case 180 -> 0; // North + case 270 -> 1; // East + default -> throw new IllegalArgumentException("Unknown skull wall direction: " + wallDirection); + }; + + return customBlockData.blockStateBuilder() + .intProperty(BITS_A_PROPERTY, wallDirection + 1) + .intProperty(BITS_B_PROPERTY, 0) + .build(); + } + + public CustomBlockState getFloorBlockState(int floorRotation) { + return customBlockData.blockStateBuilder() + .intProperty(BITS_A_PROPERTY, (5 + floorRotation) % 7) + .intProperty(BITS_B_PROPERTY, (5 + floorRotation) / 7) + .build(); + } + + private void addDefaultPermutation(List permutations) { + CustomBlockComponents components = new GeyserCustomBlockComponents.CustomBlockComponentsBuilder() + .geometry(new GeometryComponentBuilder() + .identifier("geometry.geyser.player_skull_hand") + .build()) + .transformation(new TransformationComponent(0, 180, 0)) + .build(); + + String condition = String.format("query.block_property('%s') == 0 && query.block_property('%s') == 0", BITS_A_PROPERTY, BITS_B_PROPERTY); + permutations.add(new CustomBlockPermutation(components, condition)); + } + + private void addFloorPermutations(List permutations) { + String[] quadrantNames = {"a", "b", "c", "d"}; + + for (int quadrant = 0; quadrant < 4; quadrant++) { + for (int i = 0; i < 4; i++) { + int floorRotation = 4 * quadrant + i; + CustomBlockComponents components = new GeyserCustomBlockComponents.CustomBlockComponentsBuilder() + .selectionBox(FLOOR_BOX) + .collisionBox(FLOOR_BOX) + .geometry(new GeometryComponentBuilder() + .identifier("geometry.geyser.player_skull_floor_" + quadrantNames[i]) + .build()) + .transformation(new TransformationComponent(0, ROTATIONS[quadrant], 0)) + .build(); + + int bitsA = (5 + floorRotation) % 7; + int bitsB = (5 + floorRotation) / 7; + String condition = String.format("query.block_property('%s') == %d && query.block_property('%s') == %d", BITS_A_PROPERTY, bitsA, BITS_B_PROPERTY, bitsB); + permutations.add(new CustomBlockPermutation(components, condition)); + } + } + } + + private void addWallPermutations(List permutations) { + for (int i = 0; i < 4; i++) { + CustomBlockComponents components = new GeyserCustomBlockComponents.CustomBlockComponentsBuilder() + .selectionBox(WALL_BOX) + .collisionBox(WALL_BOX) + .geometry(new GeometryComponentBuilder() + .identifier("geometry.geyser.player_skull_wall") + .build()) + .transformation(new TransformationComponent(0, ROTATIONS[i], 0)) + .build(); + + String condition = String.format("query.block_property('%s') == %d && query.block_property('%s') == %d", BITS_A_PROPERTY, i + 1, BITS_B_PROPERTY, 0); + permutations.add(new CustomBlockPermutation(components, condition)); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/registry/type/GeyserMappingItem.java b/core/src/main/java/org/geysermc/geyser/registry/type/GeyserMappingItem.java index ab8c52bf6..c1ef09b87 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/type/GeyserMappingItem.java +++ b/core/src/main/java/org/geysermc/geyser/registry/type/GeyserMappingItem.java @@ -30,6 +30,7 @@ import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; import lombok.ToString; import lombok.With; @@ -39,6 +40,7 @@ import lombok.With; @ToString @EqualsAndHashCode @Getter +@Setter @With @NoArgsConstructor @AllArgsConstructor diff --git a/core/src/main/java/org/geysermc/geyser/registry/type/ItemMappings.java b/core/src/main/java/org/geysermc/geyser/registry/type/ItemMappings.java index 65cc28420..704d5c211 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/type/ItemMappings.java +++ b/core/src/main/java/org/geysermc/geyser/registry/type/ItemMappings.java @@ -27,6 +27,7 @@ package org.geysermc.geyser.registry.type; import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; import lombok.Builder; import lombok.Value; import org.checkerframework.checker.nullness.qual.NonNull; @@ -36,6 +37,7 @@ import org.cloudburstmc.protocol.bedrock.data.inventory.ComponentItemData; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.cloudburstmc.protocol.common.DefinitionRegistry; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.block.custom.CustomBlockData; import org.geysermc.geyser.inventory.item.StoredItemMappings; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; @@ -72,6 +74,8 @@ public class ItemMappings implements DefinitionRegistry { List componentItemData; Int2ObjectMap customIdMappings; + Object2ObjectMap customBlockItemDefinitions; + /** * Gets an {@link ItemMapping} from the given {@link ItemStack}. * diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index e09cff9e4..7197a4fc4 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -464,6 +464,12 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { @Setter private long lastInteractionTime; + /** + * Stores when the player started to break a block. Used to allow correct break time for custom blocks. + */ + @Setter + private long blockBreakStartTime; + /** * Stores whether the player intended to place a bucket. */ @@ -1563,6 +1569,17 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { startGamePacket.setItemDefinitions(this.itemMappings.getItemDefinitions().values().stream().toList()); // TODO // startGamePacket.setBlockPalette(this.blockMappings.getBedrockBlockPalette()); + // Needed for custom block mappings and custom skulls system + startGamePacket.getBlockProperties().addAll(this.blockMappings.getBlockProperties()); + + // See https://learn.microsoft.com/en-us/minecraft/creator/documents/experimentalfeaturestoggle for info on each experiment + // data_driven_items (Holiday Creator Features) is needed for blocks and items + startGamePacket.getExperiments().add(new ExperimentData("data_driven_items", true)); + // Needed for block properties for states + startGamePacket.getExperiments().add(new ExperimentData("upcoming_creator_features", true)); + // Needed for certain molang queries used in blocks and items + startGamePacket.getExperiments().add(new ExperimentData("experimental_molang_features", true)); + startGamePacket.setVanillaVersion("*"); startGamePacket.setInventoriesServerAuthoritative(true); startGamePacket.setServerEngine(""); // Do we want to fill this in? diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java index ab8528c06..d6e376d8f 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java @@ -31,9 +31,17 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import lombok.Data; import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.block.custom.CustomBlockState; import org.geysermc.geyser.entity.type.player.SkullPlayerEntity; +import org.geysermc.geyser.level.block.BlockStateValues; +import org.geysermc.geyser.registry.BlockRegistries; +import org.geysermc.geyser.registry.type.CustomSkull; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.skin.SkinManager; +import java.io.IOException; import java.util.*; public class SkullCache { @@ -71,21 +79,44 @@ public class SkullCache { this.skullRenderDistanceSquared = distance * distance; } - public void putSkull(Vector3i position, UUID uuid, String texturesProperty, int blockState) { + public Skull putSkull(Vector3i position, UUID uuid, String texturesProperty, int blockState) { Skull skull = skulls.computeIfAbsent(position, Skull::new); skull.uuid = uuid; - skull.texturesProperty = texturesProperty; + if (!texturesProperty.equals(skull.texturesProperty)) { + skull.texturesProperty = texturesProperty; + skull.skinHash = null; + try { + SkinManager.GameProfileData gameProfileData = SkinManager.GameProfileData.loadFromJson(texturesProperty); + if (gameProfileData != null && gameProfileData.skinUrl() != null) { + String skinUrl = gameProfileData.skinUrl(); + skull.skinHash = skinUrl.substring(skinUrl.lastIndexOf('/') + 1); + } else { + session.getGeyser().getLogger().debug("Player skull with invalid Skin tag: " + position + " Textures: " + texturesProperty); + } + } catch (IOException e) { + session.getGeyser().getLogger().debug("Player skull with invalid Skin tag: " + position + " Textures: " + texturesProperty); + if (GeyserImpl.getInstance().getConfig().isDebugMode()) { + e.printStackTrace(); + } + } + } skull.blockState = blockState; + skull.blockDefinition = translateCustomSkull(skull.skinHash, blockState); + + if (skull.blockDefinition != null) { + reassignSkullEntity(skull); + return skull; + } if (skull.entity != null) { skull.entity.updateSkull(skull); } else { if (!cullingEnabled) { assignSkullEntity(skull); - return; + return skull; } if (lastPlayerPosition == null) { - return; + return skull; } skull.distanceSquared = position.distanceSquared(lastPlayerPosition.getX(), lastPlayerPosition.getY(), lastPlayerPosition.getZ()); if (skull.distanceSquared < skullRenderDistanceSquared) { @@ -105,24 +136,24 @@ public class SkullCache { } } } + return skull; } public void removeSkull(Vector3i position) { Skull skull = skulls.remove(position); if (skull != null) { - boolean hadEntity = skull.entity != null; - freeSkullEntity(skull); - - if (cullingEnabled) { - inRangeSkulls.remove(skull); - if (hadEntity && inRangeSkulls.size() >= maxVisibleSkulls) { - // Reassign entity to the closest skull without an entity - assignSkullEntity(inRangeSkulls.get(maxVisibleSkulls - 1)); - } - } + reassignSkullEntity(skull); } } + public Skull updateSkull(Vector3i position, int blockState) { + Skull skull = skulls.get(position); + if (skull != null) { + putSkull(position, skull.uuid, skull.texturesProperty, blockState); + } + return skull; + } + public void updateVisibleSkulls() { if (cullingEnabled) { // No need to recheck skull visibility for small movements @@ -133,6 +164,10 @@ public class SkullCache { inRangeSkulls.clear(); for (Skull skull : skulls.values()) { + if (skull.blockDefinition != null) { + continue; + } + skull.distanceSquared = skull.position.distanceSquared(lastPlayerPosition.getX(), lastPlayerPosition.getY(), lastPlayerPosition.getZ()); if (skull.distanceSquared > skullRenderDistanceSquared) { freeSkullEntity(skull); @@ -191,6 +226,19 @@ public class SkullCache { } } + private void reassignSkullEntity(Skull skull) { + boolean hadEntity = skull.entity != null; + freeSkullEntity(skull); + + if (cullingEnabled) { + inRangeSkulls.remove(skull); + if (hadEntity && inRangeSkulls.size() >= maxVisibleSkulls) { + // Reassign entity to the closest skull without an entity + assignSkullEntity(inRangeSkulls.get(maxVisibleSkulls - 1)); + } + } + } + public void clear() { skulls.clear(); inRangeSkulls.clear(); @@ -199,12 +247,33 @@ public class SkullCache { lastPlayerPosition = null; } + private BlockDefinition translateCustomSkull(String skinHash, int blockState) { + CustomSkull customSkull = BlockRegistries.CUSTOM_SKULLS.get(skinHash); + if (customSkull != null) { + byte floorRotation = BlockStateValues.getSkullRotation(blockState); + CustomBlockState customBlockState; + if (floorRotation == -1) { + // Wall skull + int wallDirection = BlockStateValues.getSkullWallDirections().get(blockState); + customBlockState = customSkull.getWallBlockState(wallDirection); + } else { + customBlockState = customSkull.getFloorBlockState(floorRotation); + } + + return session.getBlockMappings().getCustomBlockStateDefinitions().get(customBlockState); + } + return null; + } + @RequiredArgsConstructor @Data public static class Skull { private UUID uuid; private String texturesProperty; + private String skinHash; + private int blockState; + private BlockDefinition blockDefinition; private SkullPlayerEntity entity; private final Vector3i position; diff --git a/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java b/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java index b88bbe23c..79f181636 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java +++ b/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java @@ -277,7 +277,7 @@ public class SkinManager { return null; } - static GameProfileData loadFromJson(String encodedJson) throws IOException, IllegalArgumentException { + public static GameProfileData loadFromJson(String encodedJson) throws IOException, IllegalArgumentException { JsonNode skinObject = GeyserImpl.JSON_MAPPER.readTree(new String(Base64.getDecoder().decode(encodedJson), StandardCharsets.UTF_8)); JsonNode textures = skinObject.get("textures"); diff --git a/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java b/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java index 1037b88ff..41f750990 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java +++ b/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java @@ -460,7 +460,7 @@ public class SkinProvider { private static Skin supplySkin(UUID uuid, String textureUrl) { try { - byte[] skin = requestImage(textureUrl, null); + byte[] skin = requestImageData(textureUrl, null); return new Skin(uuid, textureUrl, skin, System.currentTimeMillis(), false, false); } catch (Exception ignored) {} // just ignore I guess @@ -470,7 +470,7 @@ public class SkinProvider { private static Cape supplyCape(String capeUrl, CapeProvider provider) { byte[] cape = EMPTY_CAPE.capeData(); try { - cape = requestImage(capeUrl, provider); + cape = requestImageData(capeUrl, provider); } catch (Exception ignored) { } // just ignore I guess @@ -527,7 +527,7 @@ public class SkinProvider { } @SuppressWarnings("ResultOfMethodCallIgnored") - private static byte[] requestImage(String imageUrl, CapeProvider provider) throws Exception { + public static BufferedImage requestImage(String imageUrl, CapeProvider provider) throws IOException { BufferedImage image = null; // First see if we have a cached file. We also update the modification stamp so we know when the file was last used @@ -587,6 +587,11 @@ public class SkinProvider { // TODO remove alpha channel } + return image; + } + + private static byte[] requestImageData(String imageUrl, CapeProvider provider) throws Exception { + BufferedImage image = requestImage(imageUrl, provider); byte[] data = bufferedImageToImageData(image); image.flush(); return data; diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/chest/DoubleChestInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/chest/DoubleChestInventoryTranslator.java index 0934f33a7..23961694d 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/chest/DoubleChestInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/chest/DoubleChestInventoryTranslator.java @@ -57,30 +57,32 @@ public class DoubleChestInventoryTranslator extends ChestInventoryTranslator { // See BlockInventoryHolder - same concept there except we're also dealing with a specific block state if (session.getLastInteractionPlayerPosition().equals(session.getPlayerEntity().getPosition())) { int javaBlockId = session.getGeyser().getWorldManager().getBlockAt(session, session.getLastInteractionBlockPosition()); - String[] javaBlockString = BlockRegistries.JAVA_BLOCKS.getOrDefault(javaBlockId, BlockMapping.AIR).getJavaIdentifier().split("\\["); - if (javaBlockString.length > 1 && (javaBlockString[0].equals("minecraft:chest") || javaBlockString[0].equals("minecraft:trapped_chest")) - && !javaBlockString[1].contains("type=single")) { - inventory.setHolderPosition(session.getLastInteractionBlockPosition()); - ((Container) inventory).setUsingRealBlock(true, javaBlockString[0]); + if (!BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.get().containsKey(javaBlockId)) { + String[] javaBlockString = BlockRegistries.JAVA_BLOCKS.getOrDefault(javaBlockId, BlockMapping.AIR).getJavaIdentifier().split("\\["); + if (javaBlockString.length > 1 && (javaBlockString[0].equals("minecraft:chest") || javaBlockString[0].equals("minecraft:trapped_chest")) + && !javaBlockString[1].contains("type=single")) { + inventory.setHolderPosition(session.getLastInteractionBlockPosition()); + ((Container) inventory).setUsingRealBlock(true, javaBlockString[0]); - NbtMapBuilder tag = NbtMap.builder() - .putString("id", "Chest") - .putInt("x", session.getLastInteractionBlockPosition().getX()) - .putInt("y", session.getLastInteractionBlockPosition().getY()) - .putInt("z", session.getLastInteractionBlockPosition().getZ()) - .putString("CustomName", inventory.getTitle()) - .putString("id", "Chest"); + NbtMapBuilder tag = NbtMap.builder() + .putString("id", "Chest") + .putInt("x", session.getLastInteractionBlockPosition().getX()) + .putInt("y", session.getLastInteractionBlockPosition().getY()) + .putInt("z", session.getLastInteractionBlockPosition().getZ()) + .putString("CustomName", inventory.getTitle()) + .putString("id", "Chest"); - DoubleChestValue chestValue = BlockStateValues.getDoubleChestValues().get(javaBlockId); - DoubleChestBlockEntityTranslator.translateChestValue(tag, chestValue, - session.getLastInteractionBlockPosition().getX(), session.getLastInteractionBlockPosition().getZ()); + DoubleChestValue chestValue = BlockStateValues.getDoubleChestValues().get(javaBlockId); + DoubleChestBlockEntityTranslator.translateChestValue(tag, chestValue, + session.getLastInteractionBlockPosition().getX(), session.getLastInteractionBlockPosition().getZ()); - BlockEntityDataPacket dataPacket = new BlockEntityDataPacket(); - dataPacket.setData(tag.build()); - dataPacket.setBlockPosition(session.getLastInteractionBlockPosition()); - session.sendUpstreamPacket(dataPacket); + BlockEntityDataPacket dataPacket = new BlockEntityDataPacket(); + dataPacket.setData(tag.build()); + dataPacket.setBlockPosition(session.getLastInteractionBlockPosition()); + session.sendUpstreamPacket(dataPacket); - return true; + return true; + } } } @@ -90,7 +92,7 @@ public class DoubleChestInventoryTranslator extends ChestInventoryTranslator { } Vector3i pairPosition = position.add(Vector3i.UNIT_X); - BlockDefinition definition = session.getBlockMappings().getBedrockBlock(defaultJavaBlockState); + BlockDefinition definition = session.getBlockMappings().getVanillaBedrockBlock(defaultJavaBlockState); UpdateBlockPacket blockPacket = new UpdateBlockPacket(); blockPacket.setDataLayer(0); diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/item/ItemTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/ItemTranslator.java index 0f0421be8..336809e8a 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/item/ItemTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/ItemTranslator.java @@ -28,9 +28,23 @@ package org.geysermc.geyser.translator.inventory.item; import com.github.steveice10.mc.protocol.data.game.Identifier; import com.github.steveice10.mc.protocol.data.game.entity.attribute.ModifierOperation; import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; -import com.github.steveice10.opennbt.tag.builtin.*; +import com.github.steveice10.opennbt.tag.builtin.ByteArrayTag; +import com.github.steveice10.opennbt.tag.builtin.ByteTag; +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.github.steveice10.opennbt.tag.builtin.DoubleTag; +import com.github.steveice10.opennbt.tag.builtin.FloatTag; +import com.github.steveice10.opennbt.tag.builtin.IntArrayTag; +import com.github.steveice10.opennbt.tag.builtin.IntTag; +import com.github.steveice10.opennbt.tag.builtin.ListTag; +import com.github.steveice10.opennbt.tag.builtin.LongArrayTag; +import com.github.steveice10.opennbt.tag.builtin.LongTag; +import com.github.steveice10.opennbt.tag.builtin.ShortTag; +import com.github.steveice10.opennbt.tag.builtin.StringTag; +import com.github.steveice10.opennbt.tag.builtin.Tag; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; +import org.geysermc.geyser.api.block.custom.CustomBlockData; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.nbt.NbtList; @@ -41,12 +55,15 @@ import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.inventory.GeyserItemStack; +import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.registry.BlockRegistries; -import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.registry.type.CustomSkull; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.registry.type.ItemMappings; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.skin.SkinManager; +import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.MinecraftLocale; import org.geysermc.geyser.translator.text.MessageTranslator; @@ -143,9 +160,21 @@ public final class ItemTranslator { ItemData.Builder builder = javaItem.translateToBedrock(itemStack, bedrockItem, session.getItemMappings()); if (bedrockItem.isBlock()) { - builder.blockDefinition(bedrockItem.getBedrockBlockDefinition()); + CustomBlockData customBlockData = BlockRegistries.CUSTOM_BLOCK_ITEM_OVERRIDES.getOrDefault( + bedrockItem.getJavaItem().javaIdentifier(), null); + if (customBlockData != null) { + translateCustomBlock(customBlockData, session, builder); + } else { + builder.blockDefinition(bedrockItem.getBedrockBlockDefinition()); + } } + if (bedrockItem.getJavaItem().equals(Items.PLAYER_HEAD)) { + translatePlayerHead(session, nbt, builder); + } + + translateCustomItem(nbt, builder, bedrockItem); + if (nbt != null) { // Translate the canDestroy and canPlaceOn Java NBT ListTag canDestroy = nbt.get("CanDestroy"); @@ -362,10 +391,24 @@ public final class ItemTranslator { ItemMapping mapping = itemStack.asItem().toBedrockDefinition(itemStack.getNbt(), session.getItemMappings()); + ItemDefinition itemDefinition = mapping.getBedrockDefinition(); + CustomBlockData customBlockData = BlockRegistries.CUSTOM_BLOCK_ITEM_OVERRIDES.getOrDefault( + mapping.getJavaItem().javaIdentifier(), null); + if (customBlockData != null) { + itemDefinition = session.getItemMappings().getCustomBlockItemDefinitions().get(customBlockData); + } + + if (mapping.getJavaItem().equals(Items.PLAYER_HEAD)) { + CustomSkull customSkull = getCustomSkull(session, itemStack.getNbt()); + if (customSkull != null) { + itemDefinition = session.getItemMappings().getCustomBlockItemDefinitions().get(customSkull.getCustomBlockData()); + } + } + ItemDefinition definition = CustomItemTranslator.getCustomItem(itemStack.getNbt(), mapping); if (definition == null) { // No custom item - return mapping.getBedrockDefinition(); + return itemDefinition; } else { return definition; } @@ -553,6 +596,46 @@ public final class ItemTranslator { ItemDefinition definition = CustomItemTranslator.getCustomItem(nbt, mapping); if (definition != null) { builder.definition(definition); + builder.blockDefinition(null); + } + } + + /** + * Translates a custom block override + */ + private static void translateCustomBlock(CustomBlockData customBlockData, GeyserSession session, ItemData.Builder builder) { + ItemDefinition itemDefinition = session.getItemMappings().getCustomBlockItemDefinitions().get(customBlockData); + BlockDefinition blockDefinition = session.getBlockMappings().getCustomBlockStateDefinitions().get(customBlockData.defaultBlockState()); + builder.definition(itemDefinition); + builder.blockDefinition(blockDefinition); + } + + private static CustomSkull getCustomSkull(GeyserSession session, CompoundTag nbt) { + if (nbt != null && nbt.contains("SkullOwner")) { + if (!(nbt.get("SkullOwner") instanceof CompoundTag skullOwner)) { + // It's a username give up d: + return null; + } + SkinManager.GameProfileData data = SkinManager.GameProfileData.from(skullOwner); + if (data == null) { + session.getGeyser().getLogger().debug("Not sure how to handle skull head item display. " + nbt); + return null; + } + + String skinHash = data.skinUrl().substring(data.skinUrl().lastIndexOf('/') + 1); + return BlockRegistries.CUSTOM_SKULLS.get(skinHash); + } + return null; + } + + private static void translatePlayerHead(GeyserSession session, CompoundTag nbt, ItemData.Builder builder) { + CustomSkull customSkull = getCustomSkull(session, nbt); + if (customSkull != null) { + CustomBlockData customBlockData = customSkull.getCustomBlockData(); + ItemDefinition itemDefinition = session.getItemMappings().getCustomBlockItemDefinitions().get(customBlockData); + BlockDefinition blockDefinition = session.getBlockMappings().getCustomBlockStateDefinitions().get(customBlockData.defaultBlockState()); + builder.definition(itemDefinition); + builder.blockDefinition(blockDefinition); } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/PistonBlockEntity.java b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/PistonBlockEntity.java index cab42f3d0..634b7c6f1 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/PistonBlockEntity.java +++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/PistonBlockEntity.java @@ -42,7 +42,7 @@ import org.geysermc.geyser.level.physics.Axis; import org.geysermc.geyser.level.physics.BoundingBox; import org.geysermc.geyser.level.physics.CollisionManager; import org.geysermc.geyser.level.physics.Direction; -import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.PistonCache; import org.geysermc.geyser.translator.collision.BlockCollision; @@ -95,7 +95,7 @@ public class PistonBlockEntity { static { // Create a ~1 x ~0.5 x ~1 bounding box above the honey block - BlockCollision blockCollision = Registries.COLLISIONS.get(BlockStateValues.JAVA_HONEY_BLOCK_ID); + BlockCollision blockCollision = BlockRegistries.COLLISIONS.get(BlockStateValues.JAVA_HONEY_BLOCK_ID); if (blockCollision == null) { throw new RuntimeException("Failed to find honey block collision"); } @@ -485,7 +485,7 @@ public class PistonBlockEntity { pistonCache.displacePlayer(movement.mul(delta)); } else { // Move the player out of collision - BlockCollision blockCollision = Registries.COLLISIONS.get(javaId); + BlockCollision blockCollision = BlockRegistries.COLLISIONS.get(javaId); if (blockCollision != null) { Vector3d extend = movement.mul(Math.min(1 - blockMovement, 0.5)); Direction movementDirection = orientation; diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SkullBlockEntityTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SkullBlockEntityTranslator.java index bc624ed4e..ace4d77b8 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SkullBlockEntityTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SkullBlockEntityTranslator.java @@ -30,10 +30,14 @@ import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.github.steveice10.opennbt.tag.builtin.IntArrayTag; import com.github.steveice10.opennbt.tag.builtin.ListTag; import com.github.steveice10.opennbt.tag.builtin.StringTag; +import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; +import org.cloudburstmc.protocol.bedrock.packet.UpdateBlockPacket; +import org.geysermc.geyser.GeyserImpl; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.nbt.NbtMapBuilder; import org.geysermc.geyser.level.block.BlockStateValues; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.SkullCache; import org.geysermc.geyser.skin.SkinProvider; import java.nio.charset.StandardCharsets; @@ -41,6 +45,7 @@ import java.util.LinkedHashMap; import java.util.Locale; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; @BlockEntity(type = BlockEntityType.SKULL) public class SkullBlockEntityTranslator extends BlockEntityTranslator implements RequiresBlockState { @@ -91,21 +96,55 @@ public class SkullBlockEntityTranslator extends BlockEntityTranslator implements return CompletableFuture.completedFuture(texture.getValue()); } - public static void translateSkull(GeyserSession session, CompoundTag tag, int posX, int posY, int posZ, int blockState) { - Vector3i blockPosition = Vector3i.from(posX, posY, posZ); + public static BlockDefinition translateSkull(GeyserSession session, CompoundTag tag, Vector3i blockPosition, int blockState) { CompoundTag owner = tag.get("SkullOwner"); if (owner == null) { session.getSkullCache().removeSkull(blockPosition); - return; + return null; + } + UUID uuid = getUUID(owner); + + CompletableFuture texturesFuture = getTextures(owner, uuid); + if (texturesFuture.isDone()) { + try { + SkullCache.Skull skull = session.getSkullCache().putSkull(blockPosition, uuid, texturesFuture.get(), blockState); + return skull.getBlockDefinition(); + } catch (InterruptedException | ExecutionException e) { + session.getGeyser().getLogger().debug("Failed to acquire textures for custom skull: " + blockPosition + " " + tag); + if (GeyserImpl.getInstance().getConfig().isDebugMode()) { + e.printStackTrace(); + } + } + return null; } - UUID uuid = getUUID(owner); - getTextures(owner, uuid).whenComplete((texturesProperty, throwable) -> { + // SkullOwner contained a username, so we have to wait for it to be retrieved + texturesFuture.whenComplete((texturesProperty, throwable) -> { + if (texturesProperty == null) { + session.getGeyser().getLogger().debug("Custom skull with invalid SkullOwner tag: " + blockPosition + " " + tag); + return; + } if (session.getEventLoop().inEventLoop()) { - session.getSkullCache().putSkull(blockPosition, uuid, texturesProperty, blockState); + putSkull(session, blockPosition, uuid, texturesProperty, blockState); } else { - session.executeInEventLoop(() -> session.getSkullCache().putSkull(blockPosition, uuid, texturesProperty, blockState)); + session.executeInEventLoop(() -> putSkull(session, blockPosition, uuid, texturesProperty, blockState)); } }); + + // We don't have the textures yet, so we can't determine if a custom block was defined for this skull + return null; + } + + private static void putSkull(GeyserSession session, Vector3i blockPosition, UUID uuid, String texturesProperty, int blockState) { + SkullCache.Skull skull = session.getSkullCache().putSkull(blockPosition, uuid, texturesProperty, blockState); + if (skull.getBlockDefinition() != null) { + UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket(); + updateBlockPacket.setDataLayer(0); + updateBlockPacket.setBlockPosition(blockPosition); + updateBlockPacket.setDefinition(skull.getBlockDefinition()); + updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NEIGHBORS); + updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NETWORK); + session.sendUpstreamPacket(updateBlockPacket); + } } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java index bf2c8b1cc..a614663ed 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java @@ -39,6 +39,7 @@ import org.cloudburstmc.math.vector.Vector3d; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.protocol.bedrock.data.LevelEvent; +import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; @@ -66,6 +67,7 @@ import org.geysermc.geyser.item.type.SpawnEggItem; import org.geysermc.geyser.level.block.BlockStateValues; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.SkullCache; import org.geysermc.geyser.skin.FakeHeadProvider; import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.translator.inventory.item.ItemTranslator; @@ -181,6 +183,27 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator belowBlockPos = blockPos.add(0, -2, 0); + case 2 -> belowBlockPos = blockPos.add(0, -1, 1); + case 3 -> belowBlockPos = blockPos.add(0, -1, -1); + case 4 -> belowBlockPos = blockPos.add(1, -1, 0); + case 5 -> belowBlockPos = blockPos.add(-1, -1, 0); + } + + if (belowBlockPos != null) { + int belowBlock = session.getGeyser().getWorldManager().getBlockAt(session, belowBlockPos); + BlockDefinition extendedCollisionDefinition = session.getBlockMappings().getExtendedCollisionBoxes().get(belowBlock); + if (extendedCollisionDefinition != null && (System.currentTimeMillis() - session.getLastInteractionTime()) < 200) { + restoreCorrectBlock(session, blockPos, packet); + return; + } + } + } + // Check to make sure the client isn't spamming interaction // Based on Nukkit 1.0, with changes to ensure holding down still works boolean hasAlreadyClicked = System.currentTimeMillis() - session.getLastInteractionTime() < 110.0 && @@ -265,6 +288,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator= (breakTime+=2) * 50) { + // Play break sound and particle + LevelEventPacket effectPacket = new LevelEventPacket(); + effectPacket.setPosition(vectorFloat); + effectPacket.setType(LevelEvent.PARTICLE_DESTROY_BLOCK); + effectPacket.setData(session.getBlockMappings().getBedrockBlockId(breakingBlock)); + session.sendUpstreamPacket(effectPacket); + + // Break the block + ServerboundPlayerActionPacket finishBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.FINISH_DIGGING, + vector, Direction.VALUES[packet.getFace()], session.getWorldCache().nextPredictionSequence()); + session.sendDownstreamPacket(finishBreakingPacket); + session.setBlockBreakStartTime(0); + break; + } + } + updateBreak.setData((int) (65535 / breakTime)); session.sendUpstreamPacket(updateBreak); break; diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java index 1af0ff814..7a14cebcb 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java @@ -110,7 +110,7 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator { ShapelessRecipeData shapelessRecipeData = (ShapelessRecipeData) recipe.getData(); ItemData output = ItemTranslator.translateToBedrock(session, shapelessRecipeData.getResult()); - if (output.equals(ItemData.AIR)) { + if (!output.isValid()) { // Likely modded item that Bedrock will complain about if it persists continue; } @@ -131,7 +131,7 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator { ShapedRecipeData shapedRecipeData = (ShapedRecipeData) recipe.getData(); ItemData output = ItemTranslator.translateToBedrock(session, shapedRecipeData.getResult()); - if (output.equals(ItemData.AIR)) { + if (!output.isValid()) { // Likely modded item that Bedrock will complain about if it persists continue; } @@ -213,7 +213,7 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator { + private static final ThreadLocal EXTENDED_COLLISIONS_STORAGE = ThreadLocal.withInitial(ExtendedCollisionsStorage::new); @Override public void translate(GeyserSession session, ClientboundLevelChunkWithLightPacket packet) { + final boolean useExtendedCollisions = !session.getBlockMappings().getExtendedCollisionBoxes().isEmpty(); + if (session.isSpawned()) { ChunkUtils.updateChunkPosition(session, session.getPlayerEntity().getPosition().toInt()); } @@ -111,19 +118,47 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator> 4)); if (bedrockSectionY < 0 || maxBedrockSectionY < bedrockSectionY) { // Ignore this chunk section since it goes outside the bounds accepted by the Bedrock client + if (useExtendedCollisions) { + EXTENDED_COLLISIONS_STORAGE.get().clear(); + } + extendedCollisionNextSection = false; continue; } // No need to encode an empty section... if (javaSection.isBlockCountEmpty()) { + // Unless we need to send extended collisions + if (useExtendedCollisions) { + if (extendedCollision) { + int blocks = EXTENDED_COLLISIONS_STORAGE.get().bottomLayerCollisions() + 1; + BitArray bedrockData = BitArrayVersion.forBitsCeil(Integer.SIZE - Integer.numberOfLeadingZeros(blocks)).createArray(BlockStorage.SIZE); + BlockStorage layer0 = new BlockStorage(bedrockData, new IntArrayList(blocks)); + + layer0.idFor(session.getBlockMappings().getBedrockAir().getRuntimeId()); + for (int yzx = 0; yzx < BlockStorage.SIZE / 16; yzx++) { + if (EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY) != 0) { + bedrockData.set(indexYZXtoXZY(yzx), layer0.idFor(EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY))); + EXTENDED_COLLISIONS_STORAGE.get().set(yzx, 0, sectionY); + } + } + + BlockStorage[] layers = new BlockStorage[]{ layer0 }; + sections[bedrockSectionY] = new GeyserChunkSection(layers, bedrockSectionY); + } + EXTENDED_COLLISIONS_STORAGE.get().clear(); + extendedCollisionNextSection = false; + } continue; } @@ -143,6 +178,24 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator> 5]; @@ -231,6 +311,64 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator> 5] |= 1 << (xzy & 0x1F); } } + + // V1 palette + IntList layer1Palette = IntList.of( + session.getBlockMappings().getBedrockAir().getRuntimeId(), // Air - see BlockStorage's constructor for more information + session.getBlockMappings().getBedrockWater().getRuntimeId()); + + layers = new BlockStorage[]{ layer0, new BlockStorage(BitArrayVersion.V1.createArray(BlockStorage.SIZE, layer1Data), layer1Palette) }; + } else if (waterloggedPaletteIds.isEmpty() && extendedCollision) { + for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) { + int paletteId = javaData.get(yzx); + int xzy = indexYZXtoXZY(yzx); + bedrockData.set(xzy, paletteId); + + if (EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY) != 0) { + if (paletteId == airPaletteId) { + bedrockData.set(xzy, layer0.idFor(EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY))); + } + EXTENDED_COLLISIONS_STORAGE.get().set(yzx, 0, sectionY); + continue; + } + BlockDefinition aboveBedrockExtendedCollisionDefinition = session.getBlockMappings() + .getExtendedCollisionBoxes().get(javaPalette.idToState(paletteId)); + if (aboveBedrockExtendedCollisionDefinition != null) { + EXTENDED_COLLISIONS_STORAGE.get().set((yzx + 0x100) & 0xFFF, aboveBedrockExtendedCollisionDefinition.getRuntimeId(), sectionY); + if ((xzy & 0xF) == 15) { + thisExtendedCollisionNextSection = true; + } + } + } + + layers = new BlockStorage[]{ layer0 }; + } else { + int[] layer1Data = new int[BlockStorage.SIZE >> 5]; + for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) { + int paletteId = javaData.get(yzx); + int xzy = indexYZXtoXZY(yzx); + bedrockData.set(xzy, paletteId); + + if (waterloggedPaletteIds.get(paletteId)) { + layer1Data[xzy >> 5] |= 1 << (xzy & 0x1F); + } + + if (EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY) != 0) { + if (paletteId == airPaletteId) { + bedrockData.set(xzy, layer0.idFor(EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY))); + } + EXTENDED_COLLISIONS_STORAGE.get().set(yzx, 0, sectionY); + continue; + } + BlockDefinition aboveBedrockExtendedCollisionDefinition = session.getBlockMappings().getExtendedCollisionBoxes() + .get(javaPalette.idToState(paletteId)); + if (aboveBedrockExtendedCollisionDefinition != null) { + EXTENDED_COLLISIONS_STORAGE.get().set((yzx + 0x100) & 0xFFF, aboveBedrockExtendedCollisionDefinition.getRuntimeId(), sectionY); + if ((xzy & 0xF) == 15) { + thisExtendedCollisionNextSection = true; + } + } + } // V1 palette IntList layer1Palette = IntList.of( @@ -241,6 +379,7 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator> 4) - (bedrockDimension.minY() >> 4); + if (0 <= bedrockSectionY && bedrockSectionY < maxBedrockSectionY) { + // Custom skull is in a section accepted by Bedrock + GeyserChunkSection bedrockSection = sections[bedrockSectionY]; + IntList palette = bedrockSection.getBlockStorageArray()[0].getPalette(); + if (palette instanceof IntImmutableList || palette instanceof IntLists.Singleton) { + // TODO there has to be a better way to expand the palette .-. + bedrockSection = bedrockSection.copy(bedrockSectionY); + sections[bedrockSectionY] = bedrockSection; + } + bedrockSection.setFullBlock(x, y & 0xF, z, 0, blockDefinition.getRuntimeId()); + } + } } } @@ -380,4 +533,50 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator