diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockData.java b/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockData.java new file mode 100644 index 000000000..6ee764514 --- /dev/null +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockData.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.api.block.custom; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents; +import org.geysermc.geyser.api.block.custom.property.CustomBlockProperty; + +import java.util.List; +import java.util.Map; + +/** + * This is used to store data for a custom block. + */ +public interface CustomBlockData { + @NonNull String name(); + + CustomBlockComponents components(); + + @NonNull Map> properties(); + + @NonNull List permutations(); + + CustomBlockState.Builder blockStateBuilder(); + + interface Builder { + Builder name(@NonNull String name); + + Builder components(@NonNull CustomBlockComponents components); + + Builder booleanProperty(@NonNull String propertyName, List values); + + 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/geyser/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockPermutation.java b/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockPermutation.java new file mode 100644 index 000000000..5766565d2 --- /dev/null +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockPermutation.java @@ -0,0 +1,43 @@ +/* + * 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; + +public interface CustomBlockPermutation { + @NonNull String condition(); + + CustomBlockComponents components(); + + interface Builder { + Builder condition(@NonNull String condition); + + Builder components(CustomBlockComponents components); + + CustomBlockPermutation build(); + } +} diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockState.java b/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockState.java new file mode 100644 index 000000000..a630ecd69 --- /dev/null +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockState.java @@ -0,0 +1,48 @@ +/* + * 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; + +public interface CustomBlockState { + @NonNull String name(); + + @NonNull T property(String propertyName); + + @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/geyser/src/main/java/org/geysermc/geyser/api/block/custom/component/CollisionComponent.java b/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/component/CollisionComponent.java new file mode 100644 index 000000000..6f975921a --- /dev/null +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/component/CollisionComponent.java @@ -0,0 +1,30 @@ +/* + * 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; + +public record CollisionComponent(float originX, float originY, float originZ, + float sizeX, float sizeY, float sizeZ) { +} diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/component/CustomBlockComponents.java b/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/component/CustomBlockComponents.java new file mode 100644 index 000000000..6d60ba49c --- /dev/null +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/component/CustomBlockComponents.java @@ -0,0 +1,70 @@ +/* + * 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.Map; + +public interface CustomBlockComponents { + CollisionComponent aimCollision(); + + CollisionComponent entityCollision(); + + String geometry(); + + Map materialInstances(); + + Float destroyTime(); + + Float friction(); + + Float lightEmission(); + + Integer lightFilter(); + + RotationComponent rotation(); + + interface Builder { + Builder aimCollision(CollisionComponent aimCollision); + + Builder entityCollision(CollisionComponent entityCollision); + + Builder geometry(String geometry); + + Builder materialInstances(Map materialInstances); + + Builder destroyTime(Float destroyTime); + + Builder friction(Float friction); + + Builder lightEmission(Float lightEmission); + + Builder lightFilter(Integer lightFilter); + + Builder rotation(RotationComponent rotation); + + CustomBlockComponents build(); + } +} diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/component/MaterialInstance.java b/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/component/MaterialInstance.java new file mode 100644 index 000000000..8331919e8 --- /dev/null +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/component/MaterialInstance.java @@ -0,0 +1,32 @@ +/* + * 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; + +public record MaterialInstance(@NonNull String texture, @NonNull RenderMethod renderMethod, boolean faceDimming, boolean ambientOcclusion) { + +} diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/component/RenderMethod.java b/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/component/RenderMethod.java new file mode 100644 index 000000000..907d38031 --- /dev/null +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/component/RenderMethod.java @@ -0,0 +1,32 @@ +/* + * 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; + +public enum RenderMethod { + BLEND, + OPAQUE, + ALPHA_TEST +} diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/component/RotationComponent.java b/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/component/RotationComponent.java new file mode 100644 index 000000000..e8c66caf6 --- /dev/null +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/component/RotationComponent.java @@ -0,0 +1,29 @@ +/* + * 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; + +public record RotationComponent(float x, float y, float z) { +} diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/property/CustomBlockProperty.java b/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/property/CustomBlockProperty.java new file mode 100644 index 000000000..194e2dd7d --- /dev/null +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/property/CustomBlockProperty.java @@ -0,0 +1,38 @@ +/* + * 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; + +public interface CustomBlockProperty { + @NonNull String name(); + + @NonNull List values(); + + @NonNull PropertyType type(); +} diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/property/PropertyType.java b/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/property/PropertyType.java new file mode 100644 index 000000000..d3027f846 --- /dev/null +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/block/custom/property/PropertyType.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.api.block.custom.property; + +public class PropertyType { + public static final PropertyType BOOLEAN = new PropertyType<>(Boolean.class); + public static final PropertyType INTEGER = new PropertyType<>(Integer.class); + public static final PropertyType STRING = new PropertyType<>(String.class); + + private final Class typeClass; + + private PropertyType(Class typeClass) { + this.typeClass = typeClass; + } + + public Class getTypeClass() { + return typeClass; + } +} diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/event/GeyserConvertSkullInventoryEvent.java b/api/geyser/src/main/java/org/geysermc/geyser/api/event/GeyserConvertSkullInventoryEvent.java new file mode 100644 index 000000000..ff5edea24 --- /dev/null +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/event/GeyserConvertSkullInventoryEvent.java @@ -0,0 +1,48 @@ +/* + * 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; + +import lombok.RequiredArgsConstructor; +import org.geysermc.geyser.api.block.custom.CustomBlockData; + +@RequiredArgsConstructor +public class GeyserConvertSkullInventoryEvent implements Event { + private final String skinHash; + + private CustomBlockData replacementBlock; + + public void replaceWithBlock(CustomBlockData customBlockData) { + this.replacementBlock = customBlockData; + } + + public String skinHash() { + return skinHash; + } + + public CustomBlockData replacementBlock() { + return replacementBlock; + } +} diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomBlocksEvent.java b/api/geyser/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomBlocksEvent.java new file mode 100644 index 000000000..5ed6e4f4f --- /dev/null +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomBlocksEvent.java @@ -0,0 +1,36 @@ +/* + * 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.event.Event; + +public abstract class GeyserDefineCustomBlocksEvent implements Event { + + public abstract boolean registerCustomBlock(@NonNull CustomBlockData customBlockData); + +} diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/event/world/GeyserConvertSkullEvent.java b/api/geyser/src/main/java/org/geysermc/geyser/api/event/world/GeyserConvertSkullEvent.java new file mode 100644 index 000000000..b6ec543ac --- /dev/null +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/event/world/GeyserConvertSkullEvent.java @@ -0,0 +1,118 @@ +/* + * 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.world; + +import org.geysermc.geyser.api.block.custom.CustomBlockState; +import org.geysermc.geyser.api.event.Cancellable; +import org.geysermc.geyser.api.event.Event; + +public class GeyserConvertSkullEvent implements Event, Cancellable { + private final int x; + private final int y; + private final int z; + + private final boolean onFloor; + + private final WallDirection wallDirection; + private final int floorDirection; + + private final String skinHash; + + private boolean cancelled; + + private CustomBlockState newBlockState; + + public GeyserConvertSkullEvent(int x, int y, int z, boolean onFloor, WallDirection wallDirection, int floorDirection, String skinHash) { + this.x = x; + this.y = y; + this.z = z; + this.onFloor = onFloor; + this.wallDirection = wallDirection; + this.floorDirection = floorDirection; + + if (onFloor && (wallDirection != WallDirection.INVALID || floorDirection == -1)) { + throw new IllegalArgumentException("Skull can't be on the floor and wall at the same time"); + } else if (!onFloor && (wallDirection == WallDirection.INVALID || floorDirection != -1)) { + throw new IllegalArgumentException("Skull can't be on the floor and wall at the same time"); + } + + this.skinHash = skinHash; + } + + public int x() { + return x; + } + + public int y() { + return y; + } + + public int z() { + return z; + } + + public boolean onFloor() { + return onFloor; + } + + public WallDirection wallDirection() { + return wallDirection; + } + + public int floorDirection() { + return floorDirection; + } + + public String skinHash() { + return skinHash; + } + + public void replaceWithBlock(CustomBlockState blockState) { + this.newBlockState = blockState; + } + + public CustomBlockState getNewBlockState() { + return newBlockState; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } + public enum WallDirection { + NORTH, + EAST, + SOUTH, + WEST, + INVALID + + } +} 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..1f20c9b55 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockComponents.java @@ -0,0 +1,173 @@ +/* + * 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.geysermc.geyser.api.block.custom.component.CollisionComponent; +import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents; +import org.geysermc.geyser.api.block.custom.component.MaterialInstance; +import org.geysermc.geyser.api.block.custom.component.RotationComponent; + +import java.util.Map; + +public class GeyserCustomBlockComponents implements CustomBlockComponents { + private final CollisionComponent aimCollision; + private final CollisionComponent entityCollision; + private final String geometry; + private final Map materialInstances; + private final Float destroyTime; + private final Float friction; + private final Float lightEmission; + private final Integer lightFilter; + private final RotationComponent rotation; + + public GeyserCustomBlockComponents(BuilderImpl builder) { + this.aimCollision = builder.aimCollision; + this.entityCollision = builder.entityCollision; + this.geometry = builder.geometry; + this.materialInstances = builder.materialInstances; + this.destroyTime = builder.destroyTime; + this.friction = builder.friction; + this.lightEmission = builder.lightEmission; + this.lightFilter = builder.lightFilter; + this.rotation = builder.rotation; + } + + @Override + public CollisionComponent aimCollision() { + return aimCollision; + } + + @Override + public CollisionComponent entityCollision() { + return entityCollision; + } + + @Override + public String geometry() { + return geometry; + } + + @Override + public Map materialInstances() { + return materialInstances; + } + + @Override + public Float destroyTime() { + return destroyTime; + } + + @Override + public Float friction() { + return friction; + } + + @Override + public Float lightEmission() { + return lightEmission; + } + + @Override + public Integer lightFilter() { + return lightFilter; + } + + @Override + public RotationComponent rotation() { + return rotation; + } + + public static class BuilderImpl implements Builder { + protected CollisionComponent aimCollision; + protected CollisionComponent entityCollision; + protected String geometry; + protected Map materialInstances; + protected Float destroyTime; + protected Float friction; + protected Float lightEmission; + protected Integer lightFilter; + protected RotationComponent rotation; + + @Override + public Builder aimCollision(CollisionComponent aimCollision) { + this.aimCollision = aimCollision; + return this; + } + + @Override + public Builder entityCollision(CollisionComponent entityCollision) { + this.entityCollision = entityCollision; + return this; + } + + @Override + public Builder geometry(String geometry) { + this.geometry = geometry; + return this; + } + + @Override + public Builder materialInstances(Map materialInstances) { + this.materialInstances = materialInstances; + return this; + } + + @Override + public Builder destroyTime(Float destroyTime) { + this.destroyTime = destroyTime; + return this; + } + + @Override + public Builder friction(Float friction) { + this.friction = friction; + return this; + } + + @Override + public Builder lightEmission(Float lightEmission) { + this.lightEmission = lightEmission; + return this; + } + + @Override + public Builder lightFilter(Integer lightFilter) { + this.lightFilter = lightFilter; + return this; + } + + @Override + public Builder rotation(RotationComponent rotation) { + this.rotation = rotation; + 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..1d2351a37 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockData.java @@ -0,0 +1,137 @@ +/* + * 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.Object2ObjectMaps; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectLists; +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.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 java.util.List; +import java.util.Map; + +@Value +public class GeyserCustomBlockData implements CustomBlockData { + String name; + CustomBlockComponents components; + Map> properties; + List permutations; + + @Override + public @NonNull String name() { + return name; + } + + @Override + public CustomBlockComponents components() { + return components; + } + + @Override + public @NonNull Map> properties() { + return properties; + } + + @Override + public @NonNull List permutations() { + return permutations; + } + + @Override + public CustomBlockState.Builder blockStateBuilder() { + return new GeyserCustomBlockState.BuilderImpl(this); + } + + public static class BuilderImpl implements Builder { + private String name; + private CustomBlockComponents components; + private Map> properties = new Object2ObjectOpenHashMap<>(); + private List permutations; + + @Override + public Builder name(@NonNull String name) { + this.name = name; + return this; + } + + @Override + public Builder components(@NonNull CustomBlockComponents components) { + this.components = components; + return this; + } + + @Override + public Builder booleanProperty(@NonNull String propertyName, List values) { + this.properties.put(propertyName, new GeyserCustomBlockProperty<>(propertyName, values, PropertyType.BOOLEAN)); + return this; + } + + @Override + public Builder intProperty(@NonNull String propertyName, List values) { + this.properties.put(propertyName, new GeyserCustomBlockProperty<>(propertyName, values, PropertyType.INTEGER)); + return this; + } + + @Override + public Builder stringProperty(@NonNull String propertyName, List values) { + this.properties.put(propertyName, new GeyserCustomBlockProperty<>(propertyName, values, PropertyType.STRING)); + return this; + } + + @Override + public Builder permutations(@NonNull List permutations) { + this.permutations = permutations; + return this; + } + + @Override + public CustomBlockData build() { + if (name == null) { + throw new IllegalArgumentException("Name must be set"); + } + if (properties == null) { + properties = Object2ObjectMaps.emptyMap(); + } else { + for (CustomBlockProperty property : properties.values()) { + if (property.values().isEmpty() || property.values().size() > 16) { + throw new IllegalArgumentException(property.name() + " must contain 1 to 16 values."); + } + } + } + if (permutations == null) { + permutations = ObjectLists.emptyList(); + } + return new GeyserCustomBlockData(name, components, properties, permutations); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockPermutation.java b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockPermutation.java new file mode 100644 index 000000000..c2bfc8b6c --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockPermutation.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.level.block; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.block.custom.CustomBlockPermutation; +import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents; + + +public class GeyserCustomBlockPermutation implements CustomBlockPermutation { + private final String condition; + private final CustomBlockComponents components; + + public GeyserCustomBlockPermutation(String condition, CustomBlockComponents components) { + this.condition = condition; + this.components = components; + } + + @Override + public @NonNull String condition() { + return condition; + } + + @Override + public CustomBlockComponents components() { + return components; + } + + public static class BuilderImpl implements Builder { + private String condition; + private CustomBlockComponents components; + + @Override + public Builder condition(@NonNull String condition) { + this.condition = condition; + return this; + } + + @Override + public Builder components(CustomBlockComponents components) { + this.components = components; + return this; + } + + @Override + public CustomBlockPermutation build() { + if (condition == null) { + throw new IllegalArgumentException("Condition must be set"); + } + return new GeyserCustomBlockPermutation(condition, components); + } + } +} 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..b91e5cf8d --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockProperty.java @@ -0,0 +1,60 @@ +/* + * 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; + +public class GeyserCustomBlockProperty implements CustomBlockProperty { + private final String name; + private final List values; + + private final PropertyType type; + + public GeyserCustomBlockProperty(String name, List values, PropertyType type) { + this.name = name; + this.values = values; + this.type = type; + } + + @Override + public @NonNull String name() { + return name; + } + + @Override + public @NonNull List values(){ + return values; + } + + @Override + public @NonNull PropertyType type() { + return type; + } +} 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..b2b0e2148 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockState.java @@ -0,0 +1,107 @@ +/* + * 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 { + String name; + Map properties; + + @Override + public @NonNull String name() { + return 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 BuilderImpl 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); + 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 for property: " + propertyName + " Value: " + propertyValue); + } + } + + return new GeyserCustomBlockState(blockData.name(), Object2ObjectMaps.unmodifiable(properties)); + } + } +} 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 609647b2d..d5b5275a6 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/BlockRegistries.java +++ b/core/src/main/java/org/geysermc/geyser/registry/BlockRegistries.java @@ -29,6 +29,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.Object2ObjectOpenHashMap; +import org.geysermc.geyser.api.block.custom.CustomBlockData; import org.geysermc.geyser.registry.loader.RegistryLoaders; import org.geysermc.geyser.registry.populator.BlockRegistryPopulator; import org.geysermc.geyser.registry.type.BlockMapping; @@ -72,6 +73,11 @@ public class BlockRegistries { */ public static final SimpleRegistry WATERLOGGED = SimpleRegistry.create(RegistryLoaders.empty(IntOpenHashSet::new)); + /** + * A registry containing all the custom blocks. + */ + public static final ArrayRegistry CUSTOM_BLOCKS = ArrayRegistry.create(RegistryLoaders.empty(() -> new CustomBlockData[] {})); + static { BlockRegistryPopulator.populate(); } 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 25528a919..a3bee28d1 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 @@ -28,15 +28,24 @@ package org.geysermc.geyser.registry.populator; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.ImmutableMap; import com.nukkitx.nbt.*; +import com.nukkitx.protocol.bedrock.data.BlockPropertyData; import com.nukkitx.protocol.bedrock.v527.Bedrock_v527; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntSet; -import it.unimi.dsi.fastutil.objects.Object2IntMap; -import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.objects.ObjectIntPair; +import it.unimi.dsi.fastutil.objects.*; +import org.checkerframework.checker.nullness.qual.NonNull; 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.CollisionComponent; +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.property.CustomBlockProperty; +import org.geysermc.geyser.api.block.custom.property.PropertyType; +import org.geysermc.geyser.api.event.lifecycle.GeyserDefineCustomBlocksEvent; import org.geysermc.geyser.level.block.BlockStateValues; +import org.geysermc.geyser.level.block.GeyserCustomBlockState; import org.geysermc.geyser.level.physics.PistonBehavior; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.type.BlockMapping; @@ -45,10 +54,8 @@ import org.geysermc.geyser.util.BlockUtils; import java.io.DataInputStream; import java.io.InputStream; -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.Iterator; -import java.util.Map; +import java.nio.charset.StandardCharsets; +import java.util.*; import java.util.function.BiFunction; import java.util.zip.GZIPInputStream; @@ -71,38 +78,172 @@ public class BlockRegistryPopulator { */ private static JsonNode BLOCKS_JSON; + public static final String CUSTOM_PREFIX = ""; + public static void populate() { registerJavaBlocks(); + registerCustomBedrockBlocks(); registerBedrockBlocks(); BLOCKS_JSON = null; } + private static void registerCustomBedrockBlocks() { + List customBlocks = new ArrayList<>(); + GeyserImpl.getInstance().getEventBus().fire(new GeyserDefineCustomBlocksEvent() { + @Override + public boolean registerCustomBlock(@NonNull CustomBlockData customBlockData) { + customBlocks.add(customBlockData); + return true; + } + }); + BlockRegistries.CUSTOM_BLOCKS.set(customBlocks.toArray(new CustomBlockData[0])); + GeyserImpl.getInstance().getLogger().debug("Registered " + customBlocks.size() + " custom blocks."); + } + private static void generateCustomBlockStates(List blockStates, List customExtBlockStates, CustomBlockData customBlock, int stateVersion) { + if (customBlock.properties().isEmpty()) { + blockStates.add(NbtMap.builder() + .putString("name", CUSTOM_PREFIX + customBlock.name()) + .putInt("version", stateVersion) + .putCompound("states", NbtMap.EMPTY) + .build()); + } + 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(); + } + String name = CUSTOM_PREFIX + customBlock.name(); + NbtMap states = statesBuilder.build(); + + blockStates.add(NbtMap.builder() + .putString("name", name) + .putInt("version", stateVersion) + .putCompound("states", states) + .build()); + customExtBlockStates.add(new GeyserCustomBlockState(name, states)); + } + } + + @SuppressWarnings("unchecked") + private static BlockPropertyData generateBlockPropertyData(CustomBlockData customBlock) { + List permutations = new ArrayList<>(); + for (CustomBlockPermutation permutation : customBlock.permutations()) { + permutations.add(NbtMap.builder() + .putCompound("components", convertComponents(permutation.components())) + .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.BOOLEAN) { + List values = new ArrayList<>(); + for (boolean value : (List) property.values()) { + values.add((byte) (value ? 1 : 0)); + } + propertyBuilder.putList("enum", NbtType.BYTE, values); + } else if (property.type() == PropertyType.INTEGER) { + propertyBuilder.putList("enum", NbtType.INT, (List) property.values()); + } else if (property.type() == PropertyType.STRING) { + propertyBuilder.putList("enum", NbtType.STRING, (List) property.values()); + } + properties.add(propertyBuilder.build()); + } + + NbtMap propertyTag = NbtMap.builder() + .putCompound("components", convertComponents(customBlock.components())) + .putInt("molangVersion", 0) + .putList("permutations", NbtType.COMPOUND, permutations) + .putList("properties", NbtType.COMPOUND, properties) + .build(); + return new BlockPropertyData(CUSTOM_PREFIX + customBlock.name(), propertyTag); + } + + private static final long FNV1_64_OFFSET_BASIS = 0xcbf29ce484222325L; + private static final long FNV1_64_PRIME = 1099511628211L; + + 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; + } private static void registerBedrockBlocks() { for (Map.Entry, BiFunction> palette : BLOCK_MAPPERS.entrySet()) { NbtList blocksTag; + 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)) { NbtMap blockPalette = (NbtMap) nbtInputStream.readTag(); blocksTag = (NbtList) blockPalette.getList("blocks", NbtType.COMPOUND); + blockStates = new ArrayList<>(blocksTag); } catch (Exception e) { throw new AssertionError("Unable to get blocks from runtime block states", e); } + int stateVersion = blocksTag.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(generateBlockPropertyData(customBlock)); + generateCustomBlockStates(customBlockStates, customExtBlockStates, customBlock, stateVersion); + } + blockStates.addAll(customBlockStates); + GeyserImpl.getInstance().getLogger().debug("Added " + customBlockStates.size() + " custom block states to v" + palette.getKey().valueInt() + " palette."); + + if (palette.getKey().valueInt() > 486) { + // Version 1.18.30 and above sort the palette by the FNV1 64 bit hash of the name + blockStates.sort((a, b) -> Long.compareUnsigned(fnv164(a.getString("name")), fnv164(b.getString("name")))); + } else { + // Version below 1.18.30 sort the palette alphabetically by name + blockStates.sort(Comparator.comparing(state -> state.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 - Object2IntMap blockStateOrderedMap = new Object2IntOpenHashMap<>(blocksTag.size()); - - int stateVersion = -1; - for (int i = 0; i < blocksTag.size(); i++) { - NbtMap tag = blocksTag.get(i); + Object2IntMap blockStateOrderedMap = new Object2IntOpenHashMap<>(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); } blockStateOrderedMap.put(tag, i); - if (stateVersion == -1) { - stateVersion = tag.getInt("version"); + } + + Object2IntMap customBlockStateIds = Object2IntMaps.emptyMap(); + if (BlockRegistries.CUSTOM_BLOCKS.get().length != 0) { + customBlockStateIds = new Object2IntOpenHashMap<>(customExtBlockStates.size()); + for (int i = 0; i < customExtBlockStates.size(); i++) { + NbtMap tag = customBlockStates.get(i); + CustomBlockState blockState = customExtBlockStates.get(i); + customBlockStateIds.put(blockState, blockStateOrderedMap.getInt(tag)); + } + + remappedVanillaIds = new int[blocksTag.size()]; + for (int i = 0; i < blocksTag.size(); i++) { + remappedVanillaIds[i] = blockStateOrderedMap.getInt(blocksTag.get(i)); } } + int airRuntimeId = -1; int commandBlockRuntimeId = -1; int javaRuntimeId = -1; @@ -154,7 +295,7 @@ public class BlockRegistryPopulator { // Get the tag needed for non-empty flower pots if (entry.getValue().get("pottable") != null) { - flowerPotBlocks.put(cleanJavaIdentifier.intern(), blocksTag.get(bedrockRuntimeId)); + flowerPotBlocks.put(cleanJavaIdentifier.intern(), blockStates.get(bedrockRuntimeId)); } javaToBedrockBlocks[javaRuntimeId] = bedrockRuntimeId; @@ -187,17 +328,92 @@ public class BlockRegistryPopulator { itemFrames.put(entry.getKey(), entry.getIntValue()); } } - builder.bedrockBlockStates(blocksTag); + builder.bedrockBlockStates(new NbtList<>(NbtType.COMPOUND, blockStates)); BlockRegistries.BLOCKS.register(palette.getKey().valueInt(), builder.blockStateVersion(stateVersion) .javaToBedrockBlocks(javaToBedrockBlocks) .itemFrames(itemFrames) .flowerPotBlocks(flowerPotBlocks) .jigsawStateIds(jigsawStateIds) + .remappedVanillaIds(remappedVanillaIds) + .blockProperties(customBlockProperties) + .customBlockStateIds(customBlockStateIds) .build()); } } + private static NbtMap convertComponents(CustomBlockComponents components) { + if (components == null) { + return NbtMap.EMPTY; + } + NbtMapBuilder builder = NbtMap.builder(); + if (components.aimCollision() != null) { + CollisionComponent collisionComponent = components.aimCollision(); + builder.putCompound("minecraft:aim_collision", NbtMap.builder() + .putBoolean("enabled", true) + .putList("origin", NbtType.FLOAT, collisionComponent.originX(), collisionComponent.originY(), collisionComponent.originZ()) + .putList("size", NbtType.FLOAT, collisionComponent.sizeX(), collisionComponent.sizeY(), collisionComponent.sizeZ()) + .build()); + } + if (components.entityCollision() != null) { + CollisionComponent collisionComponent = components.entityCollision(); + builder.putCompound("minecraft:block_collision", NbtMap.builder() + .putBoolean("enabled", true) + .putList("origin", NbtType.FLOAT, collisionComponent.originX(), collisionComponent.originY(), collisionComponent.originZ()) + .putList("size", NbtType.FLOAT, collisionComponent.sizeX(), collisionComponent.sizeY(), collisionComponent.sizeZ()) + .build()); + } + if (components.geometry() != null) { + builder.putCompound("minecraft:geometry", NbtMap.builder() + .putString("value", components.geometry()) + .build()); + } + if (components.materialInstances() != null) { + 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().toString().toLowerCase(Locale.ROOT)) + .putBoolean("face_dimming", materialInstance.faceDimming()) + .putBoolean("ambient_occlusion", materialInstance.faceDimming()) + .build()); + } + builder.putCompound("minecraft:material_instances", NbtMap.builder() + .putCompound("mappings", NbtMap.EMPTY) + .putCompound("materials", materialsBuilder.build()) + .build()); + } + if (components.destroyTime() != null) { + builder.putCompound("minecraft:destroy_time", NbtMap.builder() + .putFloat("value", components.destroyTime()) + .build()); + } + if (components.friction() != null) { + builder.putCompound("minecraft:friction", NbtMap.builder() + .putFloat("value", components.friction()) + .build()); + } + if (components.lightEmission() != null) { + builder.putCompound("minecraft:block_light_emission", NbtMap.builder() + .putFloat("value", components.lightEmission()) + .build()); + } + if (components.lightFilter() != null) { + builder.putCompound("minecraft:block_light_filter", NbtMap.builder() + .putByte("value", components.lightFilter().byteValue()) + .build()); + } + if (components.rotation() != null) { + builder.putCompound("minecraft:rotation", NbtMap.builder() + .putFloat("x", components.rotation().x()) + .putFloat("y", components.rotation().y()) + .putFloat("z", components.rotation().z()) + .build()); + } + return builder.build(); + } + private static void registerJavaBlocks() { JsonNode blocksJson; try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResource("mappings/blocks.json")) { 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 22669fd79..bcfe9b8ee 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 @@ -46,6 +46,7 @@ import it.unimi.dsi.fastutil.objects.*; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.block.custom.CustomBlockData; import org.geysermc.geyser.api.event.lifecycle.GeyserDefineCustomItemsEvent; import org.geysermc.geyser.api.item.custom.CustomItemData; import org.geysermc.geyser.api.item.custom.CustomItemOptions; @@ -198,6 +199,8 @@ public class ItemRegistryPopulator { // Temporary mapping to create stored items Map identifierToMapping = new Object2ObjectOpenHashMap<>(); + BlockMappings blockMappings = BlockRegistries.BLOCKS.forVersion(palette.getValue().protocolVersion()); + int netId = 1; List creativeItems = new ArrayList<>(); for (JsonNode itemNode : creativeItemEntries) { @@ -216,6 +219,10 @@ public class ItemRegistryPopulator { JsonNode blockRuntimeIdNode = itemNode.get("blockRuntimeId"); if (blockRuntimeIdNode != null) { blockRuntimeId = blockRuntimeIdNode.asInt(); + + if (blockMappings.getRemappedVanillaIds().length != 0) { + blockRuntimeId = blockMappings.getRemappedVanillaIds()[blockRuntimeId]; + } } JsonNode nbtNode = itemNode.get("nbt_b64"); if (nbtNode != null) { @@ -270,8 +277,6 @@ public class ItemRegistryPopulator { } } - BlockMappings blockMappings = BlockRegistries.BLOCKS.forVersion(palette.getValue().protocolVersion()); - int itemIndex = 0; int javaFurnaceMinecartId = 0; @@ -631,6 +636,19 @@ public class ItemRegistryPopulator { } } + // Register the item forms of custom blocks + Object2IntMap customBlockItemIds = Object2IntMaps.emptyMap(); + if (BlockRegistries.CUSTOM_BLOCKS.get().length != 0) { + customBlockItemIds = new Object2IntOpenHashMap<>(); + for (CustomBlockData customBlock : BlockRegistries.CUSTOM_BLOCKS.get()) { + int customProtocolId = nextFreeBedrockId++; + String identifier = BlockRegistryPopulator.CUSTOM_PREFIX + customBlock.name(); + + entries.put(identifier, new StartGamePacket.ItemEntry(identifier, (short) customProtocolId)); + customBlockItemIds.put(customBlock, customProtocolId); + } + } + ItemMappings itemMappings = ItemMappings.builder() .items(mappings.toArray(new ItemMapping[0])) .creativeItems(creativeItems.toArray(new ItemData[0])) @@ -645,6 +663,7 @@ public class ItemRegistryPopulator { .componentItemData(componentItemData) .lodestoneCompass(lodestoneEntry) .customIdMappings(customIdMappings) + .customBlockItemIds(customBlockItemIds) .build(); Registries.ITEMS.register(palette.getValue().protocolVersion(), itemMappings); diff --git a/core/src/main/java/org/geysermc/geyser/registry/provider/GeyserBuilderProvider.java b/core/src/main/java/org/geysermc/geyser/registry/provider/GeyserBuilderProvider.java index af289bcda..b40846b2c 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/provider/GeyserBuilderProvider.java +++ b/core/src/main/java/org/geysermc/geyser/registry/provider/GeyserBuilderProvider.java @@ -27,6 +27,9 @@ package org.geysermc.geyser.registry.provider; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.block.custom.CustomBlockData; +import org.geysermc.geyser.api.block.custom.CustomBlockPermutation; +import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents; import org.geysermc.geyser.api.command.Command; import org.geysermc.geyser.api.command.CommandSource; import org.geysermc.geyser.api.item.custom.CustomItemData; @@ -37,6 +40,9 @@ import org.geysermc.geyser.command.GeyserCommandManager; 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.GeyserCustomBlockPermutation; import org.geysermc.geyser.registry.ProviderRegistries; import org.geysermc.geyser.registry.SimpleMappedRegistry; @@ -52,6 +58,11 @@ public class GeyserBuilderProvider extends AbstractProvider implements BuilderPr @Override public void registerProviders(Map, ProviderSupplier> providers) { providers.put(Command.Builder.class, args -> new GeyserCommandManager.CommandBuilder<>((Class) args[0])); + + providers.put(CustomBlockComponents.Builder.class, args -> new GeyserCustomBlockComponents.BuilderImpl()); + providers.put(CustomBlockData.Builder.class, args -> new GeyserCustomBlockData.BuilderImpl()); + providers.put(CustomBlockPermutation.Builder.class, args -> new GeyserCustomBlockPermutation.BuilderImpl()); + providers.put(CustomItemData.Builder.class, args -> new GeyserCustomItemData.CustomItemDataBuilder()); providers.put(CustomItemOptions.Builder.class, args -> new GeyserCustomItemOptions.CustomItemOptionsBuilder()); providers.put(NonVanillaCustomItemData.Builder.class, args -> new GeyserNonVanillaCustomItemData.NonVanillaCustomItemDataBuilder()); 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 41318ee64..e2e99d5e7 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 @@ -27,11 +27,14 @@ package org.geysermc.geyser.registry.type; import com.nukkitx.nbt.NbtList; import com.nukkitx.nbt.NbtMap; +import com.nukkitx.protocol.bedrock.data.BlockPropertyData; import it.unimi.dsi.fastutil.ints.IntSet; import it.unimi.dsi.fastutil.objects.Object2IntMap; import lombok.Builder; import lombok.Value; +import org.geysermc.geyser.api.block.custom.CustomBlockState; +import java.util.List; import java.util.Map; @Builder @@ -46,6 +49,7 @@ public class BlockMappings { int[] javaToBedrockBlocks; NbtList bedrockBlockStates; + int[] remappedVanillaIds; int commandBlockRuntimeId; @@ -54,6 +58,9 @@ public class BlockMappings { IntSet jigsawStateIds; + List blockProperties; + Object2IntMap customBlockStateIds; + public int getBedrockBlockId(int state) { if (state >= this.javaToBedrockBlocks.length) { return bedrockAirId; 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 c4e967dff..24963cc47 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 @@ -31,9 +31,11 @@ import com.nukkitx.protocol.bedrock.data.inventory.ItemData; import com.nukkitx.protocol.bedrock.packet.StartGamePacket; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.IntList; +import it.unimi.dsi.fastutil.objects.Object2IntMap; import lombok.Builder; import lombok.Value; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.block.custom.CustomBlockData; import org.geysermc.geyser.inventory.item.StoredItemMappings; import javax.annotation.Nonnull; @@ -70,6 +72,8 @@ public class ItemMappings { List componentItemData; Int2ObjectMap customIdMappings; + Object2IntMap customBlockItemIds; + /** * 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 99e29dd21..62eb38fc9 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -1467,6 +1467,12 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { startGamePacket.setMultiplayerCorrelationId(""); startGamePacket.setItemEntries(this.itemMappings.getItemEntries()); + startGamePacket.getBlockProperties().addAll(this.blockMappings.getBlockProperties()); + + startGamePacket.getExperiments().add(new ExperimentData("vanilla_experiments", true)); + startGamePacket.getExperiments().add(new ExperimentData("data_driven_items", true)); + startGamePacket.getExperiments().add(new ExperimentData("upcoming_creator_features", true)); + startGamePacket.getExperiments().add(new ExperimentData("experimental_molang_features", true)); startGamePacket.setVanillaVersion("*"); startGamePacket.setInventoriesServerAuthoritative(true); 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 f26e1cce3..133c40337 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 @@ -27,12 +27,14 @@ package org.geysermc.geyser.session.cache; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.math.vector.Vector3i; +import com.nukkitx.protocol.bedrock.packet.UpdateBlockPacket; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import lombok.Data; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.geysermc.geyser.entity.type.player.SkullPlayerEntity; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.level.block.entity.SkullBlockEntityTranslator; import java.util.*; @@ -75,6 +77,17 @@ public class SkullCache { Skull skull = skulls.computeIfAbsent(position, Skull::new); skull.texturesProperty = texturesProperty; skull.blockState = blockState; + if (skull.customRuntimeId != -1) { + skull.customRuntimeId = -1; + + UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket(); + updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NEIGHBORS); + updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NETWORK); + updateBlockPacket.setBlockPosition(position); + updateBlockPacket.setRuntimeId(session.getBlockMappings().getBedrockBlockId(blockState)); + updateBlockPacket.setDataLayer(0); + session.sendUpstreamPacket(updateBlockPacket); + } if (skull.entity != null) { skull.entity.updateSkull(skull); @@ -106,6 +119,24 @@ public class SkullCache { } } + public void putSkull(Vector3i position, String texturesProperty, int blockState, int customRuntimeId) { + Skull skull = skulls.computeIfAbsent(position, Skull::new); + skull.texturesProperty = texturesProperty; + skull.blockState = blockState; + skull.customRuntimeId = customRuntimeId; + + 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 removeSkull(Vector3i position) { Skull skull = skulls.remove(position); if (skull != null) { @@ -122,6 +153,20 @@ public class SkullCache { } } + public int updateSkull(Vector3i position, int blockState) { + Skull skull = skulls.remove(position); + if (skull != null) { + int customRuntimeId = SkullBlockEntityTranslator.translateCustomSkull(session, position, skull.texturesProperty, blockState); + if (customRuntimeId != -1) { + putSkull(position, skull.texturesProperty, blockState, customRuntimeId); + } else { + putSkull(position, skull.texturesProperty, blockState); + } + return customRuntimeId; + } + return -1; + } + public void updateVisibleSkulls() { if (cullingEnabled) { // No need to recheck skull visibility for small movements @@ -132,6 +177,10 @@ public class SkullCache { inRangeSkulls.clear(); for (Skull skull : skulls.values()) { + if (skull.customRuntimeId != -1) { + continue; + } + skull.distanceSquared = skull.position.distanceSquared(lastPlayerPosition.getX(), lastPlayerPosition.getY(), lastPlayerPosition.getZ()); if (skull.distanceSquared > skullRenderDistanceSquared) { freeSkullEntity(skull); @@ -203,6 +252,7 @@ public class SkullCache { public static class Skull { private String texturesProperty; private int blockState; + private int customRuntimeId = -1; 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 38d57dc01..9bc1306fc 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java +++ b/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java @@ -254,7 +254,7 @@ public class SkinManager { } } - private static GameProfileData loadFromJson(String encodedJson) throws IOException { + public static GameProfileData loadFromJson(String encodedJson) throws IOException { 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/translator/inventory/item/ItemTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/ItemTranslator.java index 0a2ab57df..af5e9658f 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 @@ -38,6 +38,8 @@ import it.unimi.dsi.fastutil.objects.Object2IntMap; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.block.custom.CustomBlockData; +import org.geysermc.geyser.api.event.GeyserConvertSkullInventoryEvent; import org.geysermc.geyser.api.item.custom.CustomItemOptions; import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.inventory.GeyserItemStack; @@ -45,11 +47,13 @@ import org.geysermc.geyser.registry.BlockRegistries; 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.text.MinecraftLocale; import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.geyser.util.FileUtils; import javax.annotation.Nonnull; +import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.*; import java.util.stream.Collectors; @@ -175,6 +179,10 @@ public abstract class ItemTranslator { translateCustomItem(nbt, builder, bedrockItem); + if (bedrockItem == session.getItemMappings().getStoredItems().playerHead()) { + translatePlayerHead(session, nbt, builder, bedrockItem); + } + if (nbt != null) { // Translate the canDestroy and canPlaceOn Java NBT ListTag canDestroy = nbt.get("CanDestroy"); @@ -542,6 +550,50 @@ public abstract class ItemTranslator { builder.id(bedrockId); } } + + private static void translatePlayerHead(GeyserSession session, CompoundTag nbt, ItemData.Builder builder, ItemMapping mapping) { + if (nbt != null && nbt.contains("SkullOwner")) { + String skinHash = null; + Tag skullOwner = nbt.get("SkullOwner"); + if (skullOwner instanceof StringTag) { + // It's a username give up d: + return; + } else if (skullOwner instanceof CompoundTag) { + Tag properties = ((CompoundTag) skullOwner).get("Properties"); + if (properties instanceof CompoundTag) { + Tag textures = ((CompoundTag) properties).get("textures"); + if (textures instanceof ListTag) { + List textureTags = ((ListTag) textures).getValue(); + if (!textureTags.isEmpty()) { + Tag skinTag = textureTags.get(0); + if (skinTag instanceof CompoundTag) { + Tag valueTag = ((CompoundTag) skinTag).get("Value"); + String encodedJson = (String) valueTag.getValue(); + try { + SkinManager.GameProfileData data = SkinManager.GameProfileData.loadFromJson(encodedJson); + skinHash = data.skinUrl().substring(data.skinUrl().lastIndexOf('/') + 1); + } catch (IOException e) { + session.getGeyser().getLogger().debug("Not sure how to handle skull head item display. " + nbt + " " + e.getMessage()); + return; + } + } + } + } + } + } + if (skinHash == null) { + session.getGeyser().getLogger().debug("Not sure how to handle skull head item display. " + nbt); + } + GeyserConvertSkullInventoryEvent skullInventoryEvent = new GeyserConvertSkullInventoryEvent(skinHash); + GeyserImpl.getInstance().getEventBus().fire(skullInventoryEvent); + + CustomBlockData replacementBlock = skullInventoryEvent.replacementBlock(); + if (replacementBlock != null) { + int bedrockId = session.getItemMappings().getCustomBlockItemIds().getInt(replacementBlock); + builder.id(bedrockId); + } + } + } private static int getCustomItem(CompoundTag nbt, ItemMapping mapping) { if (nbt == null) { 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 94e2d4767..07c1552a2 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 @@ -31,12 +31,17 @@ import com.github.steveice10.opennbt.tag.builtin.ListTag; import com.github.steveice10.opennbt.tag.builtin.StringTag; import com.nukkitx.math.vector.Vector3i; import com.nukkitx.nbt.NbtMapBuilder; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.event.world.GeyserConvertSkullEvent; import org.geysermc.geyser.level.block.BlockStateValues; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.skin.SkinManager; import org.geysermc.geyser.skin.SkinProvider; +import java.io.IOException; import java.util.LinkedHashMap; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; @BlockEntity(type = BlockEntityType.SKULL) public class SkullBlockEntityTranslator extends BlockEntityTranslator implements RequiresBlockState { @@ -69,18 +74,65 @@ public class SkullBlockEntityTranslator extends BlockEntityTranslator implements return CompletableFuture.completedFuture(null); } - public static void translateSkull(GeyserSession session, CompoundTag tag, int posX, int posY, int posZ, int blockState) { - Vector3i blockPosition = Vector3i.from(posX, posY, posZ); - getTextures(tag).whenComplete((texturesProperty, throwable) -> { + public static int translateSkull(GeyserSession session, CompoundTag tag, int posX, int posY, int posZ, int blockState) { + try { + String texturesProperty = getTextures(tag).get(); + Vector3i blockPosition = Vector3i.from(posX, posY, posZ); if (texturesProperty == null) { session.getGeyser().getLogger().debug("Custom skull with invalid SkullOwner tag: " + blockPosition + " " + tag); - return; + return -1; } - if (session.getEventLoop().inEventLoop()) { + int runtimeId = translateCustomSkull(session, blockPosition, texturesProperty, blockState); + if (runtimeId == -1) { session.getSkullCache().putSkull(blockPosition, texturesProperty, blockState); } else { - session.executeInEventLoop(() -> session.getSkullCache().putSkull(blockPosition, texturesProperty, blockState)); + session.getSkullCache().putSkull(blockPosition, texturesProperty, blockState, runtimeId); } - }); + return runtimeId; + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + public static int translateCustomSkull(GeyserSession session, Vector3i blockPosition, String texturesProperty, int blockState) { + try { + SkinManager.GameProfileData gameProfileData = SkinManager.GameProfileData.loadFromJson(texturesProperty); + if (gameProfileData == null || gameProfileData.skinUrl() == null) { + session.getGeyser().getLogger().debug("Player skull with invalid Skin tag: " + blockPosition + " Textures: " + texturesProperty); + return -1; + } + + String skinUrl = gameProfileData.skinUrl(); + String skinHash = skinUrl.substring(skinUrl.lastIndexOf('/') + 1); + + byte floorRotation = BlockStateValues.getSkullRotation(blockState); + GeyserConvertSkullEvent.WallDirection wallDirection = GeyserConvertSkullEvent.WallDirection.INVALID; + boolean onFloor = true; + if (floorRotation == -1) { + // Wall skull + onFloor = false; + int wallRotation = BlockStateValues.getSkullWallDirections().get(blockState); + wallDirection = switch (wallRotation) { + case 0 -> GeyserConvertSkullEvent.WallDirection.SOUTH; + case 90 -> GeyserConvertSkullEvent.WallDirection.WEST; + case 180 -> GeyserConvertSkullEvent.WallDirection.NORTH; + case 270 -> GeyserConvertSkullEvent.WallDirection.EAST; + default -> GeyserConvertSkullEvent.WallDirection.INVALID; + }; + } + GeyserConvertSkullEvent event = new GeyserConvertSkullEvent(blockPosition.getX(), blockPosition.getY(), blockPosition.getZ(), onFloor, wallDirection, floorRotation, skinHash); + GeyserImpl.getInstance().getEventBus().fire(event); + + if (event.getNewBlockState() != null) { + return session.getBlockMappings().getCustomBlockStateIds().getOrDefault(event.getNewBlockState(), -1); + } + + if (event.isCancelled()) { + return -1; + } + } catch (IOException e) { + session.getGeyser().getLogger().debug("Player skull with invalid Skin tag: " + blockPosition + " Textures: " + texturesProperty + " Message: " + e.getMessage()); + } + return -1; } } 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 243b1cede..5feefa681 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 @@ -505,10 +505,18 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator> 4) - (bedrockDimension.minY() >> 4); + 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(); + sections[bedrockSectionY] = bedrockSection; + } + bedrockSection.setFullBlock(x, y & 0xF, z, 0, runtimeId); + } } } diff --git a/core/src/main/java/org/geysermc/geyser/util/ChunkUtils.java b/core/src/main/java/org/geysermc/geyser/util/ChunkUtils.java index d1ee9f165..fac398472 100644 --- a/core/src/main/java/org/geysermc/geyser/util/ChunkUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/ChunkUtils.java @@ -135,16 +135,21 @@ public class ChunkUtils { // Otherwise, let's still store our reference to the item frame, but let the new block take precedence for now } - if (BlockStateValues.getSkullVariant(blockState) == -1) { + int blockId = session.getBlockMappings().getBedrockBlockId(blockState); + int skullVariant = BlockStateValues.getSkullVariant(blockState); + if (skullVariant == -1) { // Skull is gone session.getSkullCache().removeSkull(position); + } else if (skullVariant == 3) { + int customRuntimeId = session.getSkullCache().updateSkull(position, blockState); + if (customRuntimeId != -1) { + blockId = customRuntimeId; + } } // Prevent moving_piston from being placed // It's used for extending piston heads, but it isn't needed on Bedrock and causes pistons to flicker if (!BlockStateValues.isMovingPiston(blockState)) { - int blockId = session.getBlockMappings().getBedrockBlockId(blockState); - UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket(); updateBlockPacket.setDataLayer(0); updateBlockPacket.setBlockPosition(position);