Merge remote-tracking branch 'upstream/master' into feature/cloud

# Conflicts:
#	bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricMod.java
#	bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/command/FabricCommandSource.java
#	bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/command/GeyserFabricCommandExecutor.java
#	bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java
#	core/src/main/java/org/geysermc/geyser/command/defaults/HelpCommand.java
#	core/src/main/java/org/geysermc/geyser/command/defaults/VersionCommand.java
#	core/src/main/resources/languages
#	gradle/libs.versions.toml
This commit is contained in:
Konicai 2023-08-30 18:40:03 -04:00
commit 42ada0bd87
148 changed files with 13672 additions and 7551 deletions

View File

@ -14,7 +14,7 @@ The ultimate goal of this project is to allow Minecraft: Bedrock Edition users t
Special thanks to the DragonProxy project for being a trailblazer in protocol translation and for all the team members who have joined us here!
### Currently supporting Minecraft Bedrock 1.19.80 - 1.20 and Minecraft Java 1.20.
### Currently supporting Minecraft Bedrock 1.20.0 - 1.20.10 and Minecraft Java 1.20/1.20.1.
## Setting Up
Take a look [here](https://wiki.geysermc.org/geyser/setup/) for how to set up Geyser.

View File

@ -0,0 +1,31 @@
/*
* 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.bedrock.camera;
public enum CameraShake {
POSITIONAL,
ROTATIONAL;
}

View File

@ -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<String, CustomBlockProperty<?>> properties();
/**
* Gets the list of the custom block's permutations
*
* @return The permutations of the custom block.
*/
@NonNull List<CustomBlockPermutation> 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<Integer> values);
Builder stringProperty(@NonNull String propertyName, List<String> values);
Builder permutations(@NonNull List<CustomBlockPermutation> permutations);
CustomBlockData build();
}
}

View File

@ -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) {
}

View File

@ -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> T property(@NonNull String propertyName);
/**
* Gets a map of the properties for the state
*
* @return The properties for the state.
*/
@NonNull Map<String, Object> 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();
}
}

View File

@ -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<Integer> values);
@Override
Builder stringProperty(@NonNull String propertyName, List<String> values);
@Override
Builder permutations(@NonNull List<CustomBlockPermutation> permutations);
@Override
NonVanillaCustomBlockData build();
}
}

View File

@ -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;
}
}

View File

@ -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<String, MaterialInstance> materialInstances();
/**
* Gets the placement filter component
* Equivalent to "minecraft:placement_filter"
*
* @return The placement filter.
*/
@Nullable List<PlacementConditions> 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<String> 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> 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<String> tags);
CustomBlockComponents build();
}
}

View File

@ -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<String, String> 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<String, String> boneVisibility);
GeometryComponent build();
}
}

View File

@ -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();
}
}

View File

@ -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<Face> allowedFaces, @NonNull LinkedHashMap<String, BlockFilterType> blockFilters) {
public enum Face {
DOWN,
UP,
NORTH,
SOUTH,
WEST,
EAST;
}
public enum BlockFilterType {
BLOCK,
TAG
}
}

View File

@ -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);
}
}

View File

@ -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) {
}

View File

@ -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();
}
}

View File

@ -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) {
}

View File

@ -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<T> {
/**
* 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<T> values();
/**
* Gets the type of the property
*
* @return The type of the property.
*/
@NonNull PropertyType type();
}

View File

@ -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;
}
}

View File

@ -29,10 +29,12 @@ import org.checkerframework.checker.index.qual.NonNegative;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.api.connection.Connection;
import org.geysermc.geyser.api.bedrock.camera.CameraShake;
import org.geysermc.geyser.api.command.CommandSource;
import org.geysermc.geyser.api.entity.type.GeyserEntity;
import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
/**
@ -47,9 +49,50 @@ public interface GeyserConnection extends Connection, CommandSource {
CompletableFuture<@Nullable GeyserEntity> entityByJavaId(@NonNegative int javaId);
/**
* Displays a player entity as emoting to this client.
*
* @param emoter the player entity emoting.
* @param emoteId the emote ID to send to the client.
* @param emoteId the emote ID to send to this client.
*/
void showEmote(@NonNull GeyserPlayerEntity emoter, @NonNull String emoteId);
/**
* Shakes the client's camera.<br><br>
* If the camera is already shaking with the same {@link CameraShake} type, then the additional intensity
* will be layered on top of the existing intensity, with their own distinct durations.<br>
* If the existing shake type is different and the new intensity/duration are not positive, the existing shake only
* switches to the new type. Otherwise, the existing shake is completely overridden.
*
* @param intensity the intensity of the shake. The client has a maximum total intensity of 4.
* @param duration the time in seconds that the shake will occur for
* @param type the type of shake
*/
void shakeCamera(float intensity, float duration, @NonNull CameraShake type);
/**
* Stops all camera shake of any type.
*/
void stopCameraShake();
/**
* Adds the given fog IDs to the fog cache, then sends all fog IDs in the cache to the client.
* <p>
* Fog IDs can be found <a href="https://wiki.bedrock.dev/documentation/fog-ids.html">here</a>
*
* @param fogNameSpaces the fog IDs to add. If empty, the existing cached IDs will still be sent.
*/
void sendFog(String... fogNameSpaces);
/**
* Removes the given fog IDs from the fog cache, then sends all fog IDs in the cache to the client.
*
* @param fogNameSpaces the fog IDs to remove. If empty, all fog IDs will be removed.
*/
void removeFog(String... fogNameSpaces);
/**
* Returns an immutable copy of all fog affects currently applied to this client.
*/
@NonNull
Set<String> fogEffects();
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -67,4 +67,11 @@ public interface RemoteServer {
*/
@NonNull
AuthType authType();
/**
* Gets if we should attempt to resolve the SRV record for this server.
*
* @return if we should attempt to resolve the SRV record for this server
*/
boolean resolveSrv();
}

View File

@ -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;
}
}

View File

@ -38,6 +38,10 @@ dependencies {
}
}
loom {
mixin.defaultRefmapName.set("geyser-fabric-refmap.json")
}
repositories {
mavenLocal()
maven("https://repo.opencollab.dev/maven-releases/")
@ -117,7 +121,7 @@ modrinth {
syncBodyFrom.set(rootProject.file("README.md").readText())
uploadFile.set(tasks.getByPath("remapModrinthJar"))
gameVersions.addAll("1.20")
gameVersions.addAll("1.20", "1.20.1")
loaders.add("fabric")
failSilently.set(true)

View File

@ -38,7 +38,6 @@ import net.minecraft.commands.CommandSourceStack;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.entity.player.Player;
import org.apache.logging.log4j.LogManager;
import org.geysermc.geyser.api.util.PlatformType;
import org.geysermc.geyser.GeyserBootstrap;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.GeyserLogger;
@ -212,7 +211,6 @@ public class GeyserFabricMod implements ModInitializer, GeyserBootstrap {
return this.server.getServerVersion();
}
@SuppressWarnings("ConstantConditions") // IDEA thinks that ip cannot be null
@NotNull
@Override
public String getServerBindAddress() {

View File

@ -2,6 +2,7 @@
"required": true,
"package": "org.geysermc.geyser.platform.fabric.mixin",
"compatibilityLevel": "JAVA_16",
"refmap": "geyser-fabric-refmap.json",
"client": [
"client.IntegratedServerMixin"
],

View File

@ -4,7 +4,9 @@ dependencies {
isTransitive = false
}
implementation(libs.adapters.spigot)
implementation(variantOf(libs.adapters.spigot) {
classifier("all") // otherwise the unshaded jar is used without the shaded NMS implementations
})
implementation(libs.cloud.paper)
implementation(libs.commodore)

View File

@ -163,13 +163,6 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
return;
}
// Remove this in like a year
if (Bukkit.getPluginManager().getPlugin("floodgate-bukkit") != null) {
geyserLogger.severe(GeyserLocale.getLocaleStringLog("geyser.bootstrap.floodgate.outdated", Constants.FLOODGATE_DOWNLOAD_LOCATION));
this.getPluginLoader().disablePlugin(this);
return;
}
var sourceConverter = new CommandSourceConverter<>(CommandSender.class, Bukkit::getPlayer, Bukkit::getConsoleSender);
PaperCommandManager<GeyserCommandSource> cloud;
try {
@ -309,6 +302,12 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
continue;
}
// Avoid registering the same permission twice, e.g. for the extension help commands
if (Bukkit.getPluginManager().getPermission(command.permission()) != null) {
GeyserImpl.getInstance().getLogger().debug("Skipping permission " + command.permission() + " as it is already registered");
continue;
}
Bukkit.getPluginManager().addPermission(new Permission(command.permission(),
GeyserLocale.getLocaleStringLog(command.description()),
command.isSuggestedOpOnly() ? PermissionDefault.OP : PermissionDefault.TRUE));

View File

@ -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

View File

@ -34,4 +34,4 @@ tasks {
)
}
}
}
}

View File

@ -5,12 +5,14 @@ plugins {
}
allprojects {
group = "org.geysermc.geyser"
version = "2.1.1-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
}
tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(16))
}
}

View File

@ -6,3 +6,9 @@ dependencies {
api(libs.cumulus)
api(libs.gson)
}
indra {
javaVersions {
target(8)
}
}

View File

@ -36,11 +36,15 @@ public final class Constants {
public static final String FLOODGATE_DOWNLOAD_LOCATION = "https://ci.opencollab.dev/job/GeyserMC/job/Floodgate/job/master/";
public static final String GEYSER_DOWNLOAD_LOCATION = "https://ci.geysermc.org";
public static final String GEYSER_DOWNLOAD_LOCATION = "https://geysermc.org/download";
public static final String UPDATE_PERMISSION = "geyser.update";
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 {

View File

@ -110,7 +110,8 @@ public class GeyserCommandManager {
// Register help commands for all extensions with commands
for (Map.Entry<Extension, Map<String, Command>> entry : this.extensionCommands.entrySet()) {
registerExtensionCommand(entry.getKey(), new HelpCommand(this.geyser, "help", "geyser.commands.exthelp.desc", "geyser.command.exthelp", entry.getKey().description().id(), entry.getValue()));
String id = entry.getKey().description().id();
registerExtensionCommand(entry.getKey(), new HelpCommand(this.geyser, "help", "geyser.commands.exthelp.desc", "geyser.command.exthelp." + id, id, entry.getValue()));
}
}

View File

@ -65,7 +65,8 @@ public class HelpCommand extends GeyserCommand {
// todo: pagination
int page = 1;
int maxPage = 1;
String header = GeyserLocale.getPlayerLocaleString("geyser.commands.help.header", source.locale(), page, maxPage);
String translationKey = this.baseCommand.equals("geyser") ? "geyser.commands.help.header" : "geyser.commands.extensions.header";
String header = GeyserLocale.getPlayerLocaleString(translationKey, source.locale(), page, maxPage);
source.sendMessage(header);
this.commands.stream()

View File

@ -29,6 +29,7 @@ import cloud.commandframework.Command;
import cloud.commandframework.CommandManager;
import cloud.commandframework.context.CommandContext;
import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec;
import org.geysermc.geyser.Constants;
import org.geysermc.geyser.api.util.PlatformType;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.command.GeyserCommand;
@ -93,7 +94,7 @@ public class VersionCommand extends GeyserCommand {
source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.no_updates", source.locale()));
} else {
source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.outdated",
source.locale(), (latestBuildNum - buildNum), "https://ci.geysermc.org/"));
source.locale(), (latestBuildNum - buildNum), Constants.GEYSER_DOWNLOAD_LOCATION));
}
} else {
throw new AssertionError("buildNumber missing");

View File

@ -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<String> playerUsernames;
@JsonProperty("player-uuids")
private List<String> playerUUIDs;
@JsonProperty("player-profiles")
private List<String> playerProfiles;
@JsonProperty("skin-hashes")
private List<String> skinHashes;
public List<String> getPlayerUsernames() {
return Objects.requireNonNullElse(playerUsernames, Collections.emptyList());
}
public List<String> getPlayerUUIDs() {
return Objects.requireNonNullElse(playerUUIDs, Collections.emptyList());
}
public List<String> getPlayerProfiles() {
return Objects.requireNonNullElse(playerProfiles, Collections.emptyList());
}
public List<String> getPlayerSkinHashes() {
return Objects.requireNonNullElse(skinHashes, Collections.emptyList());
}
}

View File

@ -270,6 +270,11 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
return authType;
}
@Override
public boolean resolveSrv() {
return false;
}
@Getter
@JsonProperty("allow-password-authentication")
private boolean passwordAuthentication = true;

View File

@ -788,7 +788,7 @@ public final class EntityDefinitions {
.build();
FOX = EntityDefinition.inherited(FoxEntity::new, ageableEntityBase)
.type(EntityType.FOX)
.height(0.5f).width(1.25f)
.height(0.7f).width(0.6f)
.addTranslator(MetadataType.INT, FoxEntity::setFoxVariant)
.addTranslator(MetadataType.BYTE, FoxEntity::setFoxFlags)
.addTranslator(null) // Trusted player 1

View File

@ -66,7 +66,7 @@ public class BoatEntity extends Entity {
private int variant;
// Looks too fast and too choppy with 0.1f, which is how I believe the Microsoftian client handles it
private final float ROWING_SPEED = 0.05f;
private final float ROWING_SPEED = 0.1f;
public BoatEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
// Initial rotation is incorrect

View File

@ -41,7 +41,6 @@ import lombok.Setter;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.protocol.bedrock.data.AttributeData;
import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId;
@ -211,10 +210,10 @@ public class LivingEntity extends Entity {
// If an entity has a banner on them, it will be in the helmet slot in Java but the chestplate spot in Bedrock
// But don't overwrite the chestplate if it isn't empty
ItemMapping banner = session.getItemMappings().getStoredItems().banner();
if (ItemDefinition.AIR.equals(chestplate.getDefinition()) && helmet.getDefinition().equals(banner)) {
if (ItemData.AIR.equals(chestplate) && helmet.getDefinition().equals(banner.getBedrockDefinition())) {
chestplate = this.helmet;
helmet = ItemData.AIR;
} else if (chestplate.getDefinition().equals(banner)) {
} else if (chestplate.getDefinition().equals(banner.getBedrockDefinition())) {
// Prevent chestplate banners from showing erroneously
chestplate = ItemData.AIR;
}

View File

@ -75,7 +75,7 @@ public class AnvilContainer extends Container {
String originalName = ItemUtils.getCustomName(getInput().getNbt());
String plainOriginalName = MessageTranslator.convertToPlainText(originalName, session.locale());
String plainOriginalName = MessageTranslator.convertToPlainTextLenient(originalName, session.locale());
String plainNewName = MessageTranslator.convertToPlainText(rename);
if (!plainOriginalName.equals(plainNewName)) {
// Strip out formatting since Java Edition does not allow it

View File

@ -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);

View File

@ -45,11 +45,12 @@ public class StoredItemMappings {
private final ItemMapping barrier;
private final ItemMapping compass;
private final ItemMapping crossbow;
private final ItemMapping egg;
private final ItemMapping glassBottle;
private final ItemMapping milkBucket;
private final ItemMapping powderSnowBucket;
private final ItemMapping egg;
private final ItemMapping shield;
private final ItemMapping upgradeTemplate;
private final ItemMapping wheat;
private final ItemMapping writableBook;
@ -59,11 +60,12 @@ public class StoredItemMappings {
this.barrier = load(itemMappings, Items.BARRIER);
this.compass = load(itemMappings, Items.COMPASS);
this.crossbow = load(itemMappings, Items.CROSSBOW);
this.egg = load(itemMappings, Items.EGG);
this.glassBottle = load(itemMappings, Items.GLASS_BOTTLE);
this.milkBucket = load(itemMappings, Items.MILK_BUCKET);
this.powderSnowBucket = load(itemMappings, Items.POWDER_SNOW_BUCKET);
this.egg = load(itemMappings, Items.EGG);
this.shield = load(itemMappings, Items.SHIELD);
this.upgradeTemplate = load(itemMappings, Items.NETHERITE_UPGRADE_SMITHING_TEMPLATE);
this.wheat = load(itemMappings, Items.WHEAT);
this.writableBook = load(itemMappings, Items.WRITABLE_BOOK);
}

View File

@ -118,7 +118,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater {
// Changing the item in the input slot resets the name field on Bedrock, but
// does not result in a FilterTextPacket
String originalName = MessageTranslator.convertToPlainText(ItemUtils.getCustomName(input.getNbt()), session.locale());
String originalName = MessageTranslator.convertToPlainTextLenient(ItemUtils.getCustomName(input.getNbt()), session.locale());
ServerboundRenameItemPacket renameItemPacket = new ServerboundRenameItemPacket(originalName);
session.sendDownstreamPacket(renameItemPacket);

View File

@ -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<String, CustomItemData> consumer) {
this.readItemMappings(file, mappingsRoot, consumer);
}
public void readItemMappings(Path file, JsonNode mappingsRoot, BiConsumer<String, CustomItemData> 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();
}
}

View File

@ -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));
}
}
}

View File

@ -35,7 +35,7 @@ package org.geysermc.geyser.level;
* @param doUpperHeightWarn whether to warn in the console if the Java dimension height exceeds Bedrock's.
*/
public record BedrockDimension(int minY, int height, boolean doUpperHeightWarn) {
public static BedrockDimension OVERWORLD = new BedrockDimension(-64, 384, true);
public static BedrockDimension THE_NETHER = new BedrockDimension(0, 128, false);
public static BedrockDimension THE_END = new BedrockDimension(0, 256, true);
public static final BedrockDimension OVERWORLD = new BedrockDimension(-64, 384, true);
public static final BedrockDimension THE_NETHER = new BedrockDimension(0, 128, false);
public static final BedrockDimension THE_END = new BedrockDimension(0, 256, true);
}

View File

@ -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<String, MaterialInstance> materialInstances;
List<PlacementConditions> placementFilter;
Float destructibleByMining;
Float friction;
Integer lightEmission;
Integer lightDampening;
TransformationComponent transformation;
boolean unitCube;
boolean placeAir;
Set<String> 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<String, MaterialInstance> materialInstances() {
return materialInstances;
}
@Override
public List<PlacementConditions> 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<String> tags() {
return tags;
}
public static class CustomBlockComponentsBuilder implements Builder {
protected BoxComponent selectionBox;
protected BoxComponent collisionBox;
protected String displayName;
protected GeometryComponent geometry;
protected final Object2ObjectMap<String, MaterialInstance> materialInstances = new Object2ObjectOpenHashMap<>();
protected List<PlacementConditions> 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<String> 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<PlacementConditions> 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<String> tags) {
this.tags.addAll(tags);
return this;
}
@Override
public CustomBlockComponents build() {
return new GeyserCustomBlockComponents(this);
}
}
}

View File

@ -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<String, CustomBlockProperty<?>> properties;
private final List<CustomBlockPermutation> permutations;
private final Map<String, Object> 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<String, Object> 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<String, CustomBlockProperty<?>> properties() {
return properties;
}
@Override
public @NonNull List<CustomBlockPermutation> 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<String, CustomBlockProperty<?>> properties = new Object2ObjectOpenHashMap<>();
private List<CustomBlockPermutation> 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<Integer> values) {
this.properties.put(propertyName, new GeyserCustomBlockProperty<>(propertyName, values, PropertyType.integerProp()));
return this;
}
@Override
public Builder stringProperty(@NonNull String propertyName, List<String> values) {
this.properties.put(propertyName, new GeyserCustomBlockProperty<>(propertyName, values, PropertyType.stringProp()));
return this;
}
@Override
public Builder permutations(@NonNull List<CustomBlockPermutation> permutations) {
this.permutations = permutations;
return this;
}
@Override
public CustomBlockData build() {
return new GeyserCustomBlockData(this);
}
}
}

View File

@ -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 <T> 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<T>(@NonNull String name, @NonNull List<T> values,
@NonNull PropertyType type) implements CustomBlockProperty<T> {
}

View File

@ -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<String, Object> properties;
@Override
public @NonNull CustomBlockData block() {
return block;
}
@Override
public @NonNull String name() {
return block.name();
}
@SuppressWarnings("unchecked")
@Override
public <T> @NonNull T property(String propertyName) {
return (T) properties.get(propertyName);
}
@Override
public @NonNull Map<String, Object> properties() {
return properties;
}
@RequiredArgsConstructor
public static class CustomBlockStateBuilder implements CustomBlockState.Builder {
private final CustomBlockData blockData;
private final Object2ObjectMap<String, Object> 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<String, Object> 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));
}
}
}

View File

@ -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<String, String> boneVisibility;
GeyserGeometryComponent(GeometryComponentBuilder builder) {
this.identifier = builder.identifier;
this.boneVisibility = builder.boneVisibility;
}
@Override
public @NonNull String identifier() {
return identifier;
}
@Override
public @Nullable Map<String, String> boneVisibility() {
return boneVisibility;
}
public static class GeometryComponentBuilder implements Builder {
private String identifier;
private Map<String, String> boneVisibility;
@Override
public GeometryComponent.Builder identifier(@NonNull String identifier) {
this.identifier = identifier;
return this;
}
@Override
public GeometryComponent.Builder boneVisibility(@Nullable Map<String, String> boneVisibility) {
this.boneVisibility = boneVisibility;
return this;
}
@Override
public GeometryComponent build() {
return new GeyserGeometryComponent(this);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<Integer> values) {
return (NonVanillaCustomBlockDataBuilder) super.intProperty(propertyName, values);
}
@Override
public NonVanillaCustomBlockDataBuilder stringProperty(@NonNull String propertyName, List<String> values) {
return (NonVanillaCustomBlockDataBuilder) super.stringProperty(propertyName, values);
}
@Override
public NonVanillaCustomBlockDataBuilder permutations(@NonNull List<CustomBlockPermutation> permutations) {
return (NonVanillaCustomBlockDataBuilder) super.permutations(permutations);
}
@Override
public NonVanillaCustomBlockData build() {
return new GeyserNonVanillaCustomBlockData(this);
}
}
}

View File

@ -30,16 +30,19 @@ import org.cloudburstmc.protocol.common.util.Preconditions;
public class GeyserChunkSection {
private static final int CHUNK_SECTION_VERSION = 8;
// As of at least 1.19.80
private static final int CHUNK_SECTION_VERSION = 9;
private final BlockStorage[] storage;
private final int sectionY;
public GeyserChunkSection(int airBlockId) {
this(new BlockStorage[]{new BlockStorage(airBlockId), new BlockStorage(airBlockId)});
public GeyserChunkSection(int airBlockId, int sectionY) {
this(new BlockStorage[]{new BlockStorage(airBlockId), new BlockStorage(airBlockId)}, sectionY);
}
public GeyserChunkSection(BlockStorage[] storage) {
public GeyserChunkSection(BlockStorage[] storage, int sectionY) {
this.storage = storage;
this.sectionY = sectionY;
}
public int getFullBlock(int x, int y, int z, int layer) {
@ -57,6 +60,8 @@ public class GeyserChunkSection {
public void writeToNetwork(ByteBuf buffer) {
buffer.writeByte(CHUNK_SECTION_VERSION);
buffer.writeByte(this.storage.length);
// Required for chunk version 9+
buffer.writeByte(this.sectionY);
for (BlockStorage blockStorage : this.storage) {
blockStorage.writeToNetwork(buffer);
}
@ -83,12 +88,12 @@ public class GeyserChunkSection {
return true;
}
public GeyserChunkSection copy() {
public GeyserChunkSection copy(int sectionY) {
BlockStorage[] storage = new BlockStorage[this.storage.length];
for (int i = 0; i < storage.length; i++) {
storage[i] = this.storage[i].copy();
}
return new GeyserChunkSection(storage);
return new GeyserChunkSection(storage, sectionY);
}
public static int blockPosition(int x, int y, int z) {

View File

@ -30,6 +30,7 @@ import com.github.steveice10.mc.protocol.codec.PacketCodec;
import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec;
import org.cloudburstmc.protocol.bedrock.codec.v582.Bedrock_v582;
import org.cloudburstmc.protocol.bedrock.codec.v589.Bedrock_v589;
import org.cloudburstmc.protocol.bedrock.codec.v594.Bedrock_v594;
import org.cloudburstmc.protocol.bedrock.netty.codec.packet.BedrockPacketCodec;
import org.geysermc.geyser.session.GeyserSession;
@ -45,7 +46,7 @@ public final class GameProtocol {
* Default Bedrock codec that should act as a fallback. Should represent the latest available
* release of the game that Geyser supports.
*/
public static final BedrockCodec DEFAULT_BEDROCK_CODEC = Bedrock_v589.CODEC;
public static final BedrockCodec DEFAULT_BEDROCK_CODEC = Bedrock_v594.CODEC;
/**
* A list of all supported Bedrock versions that can join Geyser
@ -59,9 +60,7 @@ public final class GameProtocol {
private static final PacketCodec DEFAULT_JAVA_CODEC = MinecraftCodec.CODEC;
static {
SUPPORTED_BEDROCK_CODECS.add(Bedrock_v582.CODEC.toBuilder()
.minecraftVersion("1.19.80/1.19.81")
.build());
SUPPORTED_BEDROCK_CODECS.add(Bedrock_v589.CODEC);
SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC);
}
@ -81,8 +80,8 @@ public final class GameProtocol {
/* Bedrock convenience methods to gatekeep features and easily remove the check on version removal */
public static boolean isPre1_20(GeyserSession session) {
return session.getUpstream().getProtocolVersion() < Bedrock_v589.CODEC.getProtocolVersion();
public static boolean isPre1_20_10(GeyserSession session) {
return session.getUpstream().getProtocolVersion() < Bedrock_v594.CODEC.getProtocolVersion();
}
/**

View File

@ -231,11 +231,6 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
stackPacket.getExperiments().add(new ExperimentData("data_driven_items", true));
}
if (GameProtocol.isPre1_20(session)) {
stackPacket.getExperiments().add(new ExperimentData("next_major_update", true));
stackPacket.getExperiments().add(new ExperimentData("sniffer", true));
}
session.sendUpstreamPacket(stackPacket);
break;

View File

@ -0,0 +1,311 @@
/*
* 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<String, Path> 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() {
// No need to clean up if skin cache does not exist
if (!Files.exists(SKULL_SKIN_CACHE_PATH)) {
return;
}
try (Stream<Path> 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<String> 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<String> 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<UUID, UUID> 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<UUID, UUID> 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<UUID, UUID> uuids = generatePackUUIDs();
try (ZipFile zipFile = new ZipFile(packFile)) {
Optional<? extends ZipEntry> 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<UUID> 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));
}
}

View File

@ -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<BlockMapping> JAVA_BLOCKS = ArrayRegistry.create(RegistryLoaders.uninitialized());
/**
* A mapped registry containing which holds block IDs to its {@link BlockCollision}.
*/
public static final IntMappedRegistry<BlockCollision> COLLISIONS;
/**
* A mapped registry containing the Java identifiers to IDs.
*/
@ -83,11 +101,51 @@ public class BlockRegistries {
*/
public static final SimpleRegistry<BitSet> INTERACTIVE_MAY_BUILD = SimpleRegistry.create(RegistryLoaders.uninitialized());
/**
* A registry containing all the custom blocks.
*/
public static final ArrayRegistry<CustomBlockData> 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<Integer, CustomBlockState, Int2ObjectMap<CustomBlockState>> 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<JavaBlockState, CustomBlockState> 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<String, CustomBlockData> 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<CustomBlockData, Set<Integer>> EXTENDED_COLLISION_BOXES = SimpleMappedRegistry.create(RegistryLoaders.empty(Object2ObjectOpenHashMap::new));
/**
* A registry which stores skin texture hashes to custom skull blocks.
*/
public static final SimpleMappedRegistry<String, CustomSkull> 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
}
}

View File

@ -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<Class<?>, 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<BlockEntityType, BlockEntityTranslator> 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<BlockCollision> 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<Set<PotionMixData>> POTION_MIXES;
/**
* A registry holding all the
*/
public static final SimpleMappedRegistry<Class<?>, 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.
*/

View File

@ -79,6 +79,11 @@ public class CollisionRegistryLoader extends MultiResourceRegistryLoader<String,
Map<BlockCollision, BlockCollision> 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) {

View File

@ -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<Map<Class<?>, Prov
public Map<Class<?>, ProviderSupplier> load(Map<Class<?>, 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]));

View File

@ -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<Path, Map<String, Reso
resourcePacks = new ArrayList<>();
}
// Add custom skull pack
Path skullResourcePack = SkullResourcePackManager.createResourcePack();
if (skullResourcePack != null) {
resourcePacks.add(skullResourcePack);
}
GeyserLoadResourcePacksEvent event = new GeyserLoadResourcePacksEvent(resourcePacks);
GeyserImpl.getInstance().eventBus().fire(event);
@ -146,7 +153,7 @@ public class ResourcePackLoader implements RegistryLoader<Path, Map<String, Reso
// Check if a file exists with the same name as the resource pack suffixed by .key,
// and set this as content key. (e.g. test.zip, key file would be test.zip.key)
Path keyFile = path.resolveSibling(path.getFileName().toString() + ".key");
String contentKey = Files.exists(keyFile) ? Files.readString(path, StandardCharsets.UTF_8) : "";
String contentKey = Files.exists(keyFile) ? Files.readString(keyFile, StandardCharsets.UTF_8) : "";
return new GeyserResourcePack(new GeyserPathPackCodec(path), manifest, contentKey);
} catch (Exception e) {

View File

@ -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<String, CustomItemData> 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<String, CustomItemData> 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<String, CustomItemData> consumer) {
public void loadBlockMappingsFromJson(BiConsumer<String, CustomBlockMapping> 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<String, CustomItemData> 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<String, CustomBlockMapping> consumer) {
JsonNode mappingsRoot = getMappingsRoot(file);
int formatVersion = getFormatVersion(mappingsRoot, file);
if (formatVersion < 0 || mappingsRoot == null) {
return;
}
this.mappingReaders.get(formatVersion).readBlockMappings(file, mappingsRoot, consumer);
}
}

View File

@ -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) {
}

View File

@ -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<String, CustomBlockStateMapping> states, @NonNull String javaIdentifier, boolean overrideItem) {
}

View File

@ -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<CustomBlockState.Builder, CustomBlockState> builder, BoxComponent extendedCollisionBox) {
}

View File

@ -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) {
}

View File

@ -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<String, CustomItemData> consumer);
public abstract void readItemMappings(Path file, JsonNode mappingsRoot, BiConsumer<String, CustomItemData> consumer);
public abstract void readBlockMappings(Path file, JsonNode mappingsRoot, BiConsumer<String, CustomBlockMapping> 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()) {

View File

@ -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<String, CustomItemData> 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<String, CustomBlockMapping> consumer) {
this.readBlockMappingsV1(file, mappingsRoot, consumer);
}
public void readItemMappingsV1(Path file, JsonNode mappingsRoot, BiConsumer<String, CustomItemData> 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<String, CustomBlockMapping> 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<String, CustomBlockComponentsMapping> componentsMap = new LinkedHashMap<>();
JsonNode stateOverrides = node.get("state_overrides");
if (stateOverrides != null && stateOverrides.isObject()) {
// Load components for specific Java block states
Iterator<Map.Entry<String, JsonNode>> fields = stateOverrides.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> 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<String, CustomBlockComponentsMapping> componentsMap, String identifier, boolean overrideItem) {
Map<String, LinkedHashSet<String>> valuesMap = new Object2ObjectOpenHashMap<>();
List<CustomBlockPermutation> permutations = new ArrayList<>();
Map<String, CustomBlockStateBuilderMapping> blockStateBuilders = new Object2ObjectOpenHashMap<>();
// For each Java block state, extract the property values, create a CustomBlockPermutation,
// and a CustomBlockState builder
for (Map.Entry<String, CustomBlockComponentsMapping> entry : componentsMap.entrySet()) {
String state = entry.getKey();
String[] pairs = splitStateString(state);
String[] conditions = new String[pairs.length];
Function<CustomBlockState.Builder, CustomBlockState.Builder> blockStateBuilder = Function.identity();
for (int i = 0; i < pairs.length; i++) {
String[] parts = pairs[i].split("=");
String property = parts[0];
String value = parts[1];
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<String, CustomBlockStateMapping> 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<String, String> 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<PlacementConditions> 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<String> 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<PlacementConditions> createPlacementFilterComponent(JsonNode node) {
List<PlacementConditions> 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<Face> faces = EnumSet.noneOf(Face.class);
if (condition.has("allowed_faces")) {
JsonNode allowedFaces = condition.get("allowed_faces");
if (allowedFaces.isArray()) {
allowedFaces.forEach(face -> faces.add(Face.valueOf(face.asText().toUpperCase())));
}
}
LinkedHashMap<String, BlockFilterType> 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(",");
}
}

View File

@ -30,12 +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;
@ -46,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;
@ -54,67 +62,74 @@ 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;
}
private static void registerBedrockBlocks() {
BiFunction<String, NbtMapBuilder, String> emptyMapper = (bedrockIdentifier, statesBuilder) -> null;
// We are using mappings that directly support 1.20, so this maps it back to 1.19.80
BiFunction<String, NbtMapBuilder, String> legacyMapper = (bedrockIdentifier, statesBuilder) -> {
if (bedrockIdentifier.endsWith("pumpkin")) {
String direction = statesBuilder.remove("minecraft:cardinal_direction").toString();
statesBuilder.putInt("direction", switch (direction) {
case "north" -> 2;
case "east" -> 3;
case "west" -> 1;
default -> 0; // south
});
} else if (bedrockIdentifier.endsWith("carpet") && !bedrockIdentifier.startsWith("minecraft:moss")) {
String color = bedrockIdentifier.replace("minecraft:", "").replace("_carpet", "");
if (color.equals("light_gray")) {
color = "silver";
// adapt 1.20 mappings to 1.20.10+
BiFunction<String, NbtMapBuilder, String> concreteAndShulkerBoxMapper = (bedrockIdentifier, statesBuilder) -> {
if (bedrockIdentifier.equals("minecraft:concrete")) {
String color = (String) statesBuilder.remove("color");
if (color.equals("silver")) {
color = "light_gray";
}
statesBuilder.putString("color", color);
return "minecraft:carpet";
} else if (bedrockIdentifier.equals("minecraft:sniffer_egg")) {
statesBuilder.remove("cracked_state");
return "minecraft:dragon_egg";
} else if (bedrockIdentifier.endsWith("coral")) {
statesBuilder.putString("coral_color", "blue"); // all blue
statesBuilder.putBoolean("dead_bit", bedrockIdentifier.startsWith("minecraft:dead"));
return "minecraft:coral";
} else if (bedrockIdentifier.endsWith("sculk_sensor")) {
int phase = (int) statesBuilder.remove("sculk_sensor_phase");
statesBuilder.putBoolean("powered_bit", phase != 0);
} else if (bedrockIdentifier.endsWith("pitcher_plant")) {
statesBuilder.putString("double_plant_type", "sunflower");
return "minecraft:double_plant";
} else if (bedrockIdentifier.endsWith("pitcher_crop")) {
statesBuilder.remove("growth");
if (((byte) statesBuilder.remove("upper_block_bit")) == 1){
statesBuilder.putString("flower_type", "orchid");
return "minecraft:red_flower"; // top
}
statesBuilder.putBoolean("update_bit", false);
return "minecraft:flower_pot"; // bottom
return "minecraft:" + color + "_concrete";
}
if (bedrockIdentifier.equals("minecraft:shulker_box")) {
String color = (String) statesBuilder.remove("color");
if (color.equals("silver")) {
color = "light_gray";
}
return "minecraft:" + color + "_shulker_box";
}
if (bedrockIdentifier.equals("minecraft:observer")) {
int direction = (int) statesBuilder.remove("facing_direction");
statesBuilder.putString("minecraft:facing_direction", switch (direction) {
case 0 -> "down";
case 1 -> "up";
case 2 -> "north";
case 3 -> "south";
case 4 -> "west";
default -> "east";
});
}
return null;
};
ImmutableMap<ObjectIntPair<String>, BiFunction<String, NbtMapBuilder, String>> blockMappers = ImmutableMap.<ObjectIntPair<String>, BiFunction<String, NbtMapBuilder, String>>builder()
.put(ObjectIntPair.of("1_19_80", Bedrock_v582.CODEC.getProtocolVersion()), legacyMapper)
.put(ObjectIntPair.of("1_20_0", Bedrock_v589.CODEC.getProtocolVersion()), emptyMapper)
.put(ObjectIntPair.of("1_20_10", Bedrock_v594.CODEC.getProtocolVersion()), concreteAndShulkerBoxMapper)
.build();
// We can keep this strong as nothing should be garbage collected
@ -122,36 +137,83 @@ public final class BlockRegistryPopulator {
Interner<NbtMap> statesInterner = Interners.newStrongInterner();
for (Map.Entry<ObjectIntPair<String>, BiFunction<String, NbtMapBuilder, String>> palette : blockMappers.entrySet()) {
NbtList<NbtMap> blocksTag;
int protocolVersion = palette.getKey().valueInt();
List<NbtMap> vanillaBlockStates;
List<NbtMap> 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<NbtMap>) 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<BlockPropertyData> customBlockProperties = new ArrayList<>();
List<NbtMap> customBlockStates = new ArrayList<>();
List<CustomBlockState> 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<NbtMap, GeyserBedrockBlock> 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<NbtMap, GeyserBedrockBlock> 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<CustomBlockState, GeyserBedrockBlock> customBlockStateDefinitions = Object2ObjectMaps.emptyMap();
Int2ObjectMap<GeyserBedrockBlock> 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<Integer> 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;
@ -162,7 +224,8 @@ public final class BlockRegistryPopulator {
BiFunction<String, NbtMapBuilder, String> stateMapper = blockMappers.getOrDefault(palette.getKey(), emptyMapper);
GeyserBedrockBlock[] javaToBedrockBlocks = new GeyserBedrockBlock[BLOCKS_JSON.size()];
GeyserBedrockBlock[] javaToBedrockBlocks = new GeyserBedrockBlock[javaBlocksSize];
GeyserBedrockBlock[] javaToVanillaBedrockBlocks = new GeyserBedrockBlock[javaBlocksSize];
Map<String, NbtMap> flowerPotBlocks = new Object2ObjectOpenHashMap<>();
Map<NbtMap, BlockDefinition> itemFrames = new Object2ObjectOpenHashMap<>();
@ -174,11 +237,22 @@ public final class BlockRegistryPopulator {
javaRuntimeId++;
Map.Entry<String, JsonNode> 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) {
@ -204,9 +278,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;
}
@ -231,6 +306,33 @@ public final class BlockRegistryPopulator {
}
builder.bedrockMovingBlock(movingBlockDefinition);
Map<JavaBlockState, CustomBlockState> 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<JavaBlockState, CustomBlockState> 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");
@ -242,9 +344,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());
}
}
@ -257,7 +365,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<String> cleanIdentifiers = new ArrayDeque<>();
@ -395,6 +516,46 @@ public final class BlockRegistryPopulator {
}
BlockStateValues.JAVA_WATER_ID = waterRuntimeId;
if (BlockRegistries.NON_VANILLA_BLOCK_STATE_OVERRIDES.get().size() > 0) {
Set<Integer> 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;
@ -448,4 +609,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;
}
}

View File

@ -26,7 +26,10 @@
package org.geysermc.geyser.registry.populator;
import com.fasterxml.jackson.databind.JsonNode;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectMaps;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtMapBuilder;
import org.cloudburstmc.nbt.NbtUtils;
import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
@ -34,6 +37,7 @@ import org.geysermc.geyser.GeyserBootstrap;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.type.BlockMappings;
import org.geysermc.geyser.registry.type.GeyserBedrockBlock;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@ -99,12 +103,32 @@ public class CreativeItemRegistryPopulator {
count = countNode.asInt();
}
GeyserBedrockBlock blockDefinition = null;
JsonNode blockRuntimeIdNode = itemNode.get("blockRuntimeId");
JsonNode blockStateNode;
if (blockRuntimeIdNode != null) {
bedrockBlockRuntimeId = blockRuntimeIdNode.asInt();
if (bedrockBlockRuntimeId == 0 && !identifier.equals("minecraft:blue_candle")) { // FIXME
bedrockBlockRuntimeId = -1;
}
blockDefinition = bedrockBlockRuntimeId == -1 ? null : blockMappings.getDefinition(bedrockBlockRuntimeId);
} else if ((blockStateNode = itemNode.get("block_state_b64")) != null) {
byte[] bytes = Base64.getDecoder().decode(blockStateNode.asText());
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
try {
NbtMap stateTag = (NbtMap) NbtUtils.createReaderLE(bais).readTag();
// We remove these from the state definition map in
// BlockMappings, so we need to remove it from here
NbtMapBuilder builder = stateTag.toBuilder();
builder.remove("name_hash");
builder.remove("network_id");
blockDefinition = blockMappings.getDefinition(builder.build());
} catch (IOException e) {
e.printStackTrace();
}
}
JsonNode nbtNode = itemNode.get("nbt_b64");
@ -129,6 +153,6 @@ public class CreativeItemRegistryPopulator {
.damage(damage)
.count(count)
.tag(tag)
.blockDefinition(bedrockBlockRuntimeId == -1 ? null : blockMappings.getDefinition(bedrockBlockRuntimeId));
.blockDefinition(blockDefinition);
}
}

View File

@ -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<CustomBlockData> customBlocks;
private static Set<String> customBlockNames;
private static Int2ObjectMap<CustomBlockState> blockStateOverrides;
private static Map<String, CustomBlockData> customBlockItemOverrides;
private static Map<JavaBlockState, CustomBlockState> 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<CustomBlockData, Set<Integer>> extendedCollisionBoxes = new HashMap<>();
Map<BoxComponent, CustomBlockData> 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<NbtMap> blockStates, List<CustomBlockState> 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<NbtMap> 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<NbtMap> 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<Integer>) property.values());
} else if (property.type() == PropertyType.stringProp()) {
propertyBuilder.putList("enum", NbtType.STRING, (List<String>) 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<String, MaterialInstance> 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<NbtMap> convertPlacementFilter(List<PlacementConditions> placementFilter) {
List<NbtMap> 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 <NbtMap> 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;
}
}

View File

@ -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<String, GeyserMappingItem> items, Multimap<String, CustomItemData> customItems, List<NonVanillaCustomItemData> 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<NbtMap> 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);

View File

@ -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<String> profiles = new ArrayList<>(skullConfig.getPlayerProfiles());
List<String> usernames = new ArrayList<>(skullConfig.getPlayerUsernames());
List<String> uuids = new ArrayList<>(skullConfig.getPlayerUUIDs());
List<String> 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;
}
}
}

View File

@ -34,29 +34,34 @@ 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;
import org.cloudburstmc.nbt.NbtType;
import org.cloudburstmc.protocol.bedrock.codec.v582.Bedrock_v582;
import org.cloudburstmc.protocol.bedrock.codec.v589.Bedrock_v589;
import org.cloudburstmc.protocol.bedrock.codec.v594.Bedrock_v594;
import org.cloudburstmc.protocol.bedrock.data.SoundEvent;
import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition;
import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition;
import org.cloudburstmc.protocol.bedrock.data.definitions.SimpleItemDefinition;
import org.cloudburstmc.protocol.bedrock.data.inventory.ComponentItemData;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
import org.cloudburstmc.protocol.bedrock.data.SoundEvent;
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;
@ -82,26 +87,19 @@ public class ItemRegistryPopulator {
}
public static void populate() {
Map<Item, String> legacyJavaOnly = new HashMap<>();
legacyJavaOnly.put(Items.MUSIC_DISC_RELIC, "minecraft:music_disc_wait");
legacyJavaOnly.put(Items.PITCHER_PLANT, "minecraft:chorus_flower");
legacyJavaOnly.put(Items.PITCHER_POD, "minecraft:beetroot");
legacyJavaOnly.put(Items.SNIFFER_EGG, "minecraft:sniffer_spawn_egg"); // the BlockItem of the sniffer egg block
List<PaletteVersion> paletteVersions = new ArrayList<>(2);
paletteVersions.add(new PaletteVersion("1_19_80", Bedrock_v582.CODEC.getProtocolVersion(), legacyJavaOnly, (item, mapping) -> {
paletteVersions.add(new PaletteVersion("1_20_0", Bedrock_v589.CODEC.getProtocolVersion()));
paletteVersions.add(new PaletteVersion("1_20_10", Bedrock_v594.CODEC.getProtocolVersion(), Collections.emptyMap(), (item, mapping) -> {
// Forward-map 1.20 mappings to 1.20.10
// 1.20.10+ received parity for concrete and shulker boxes
String id = item.javaIdentifier();
if (id.endsWith("pottery_sherd")) {
return mapping.withBedrockIdentifier(id.replace("sherd", "shard"));
} else if (id.endsWith("carpet") && !id.startsWith("minecraft:moss")) {
return mapping.withBedrockIdentifier("minecraft:carpet");
} else if (id.endsWith("coral")) {
return mapping.withBedrockIdentifier("minecraft:coral");
if (id.endsWith("_concrete") || id.endsWith("_shulker_box")) {
// the first underscore in "_shulker_box" accounts for ignoring "minecraft:shulker_box"
// which is mapped to "minecraft:undyed_shulker_box"
return mapping.withBedrockIdentifier(id);
}
return mapping;
}));
paletteVersions.add(new PaletteVersion("1_20_0", Bedrock_v589.CODEC.getProtocolVersion()));
GeyserBootstrap bootstrap = GeyserImpl.getInstance().getBootstrap();
@ -162,6 +160,8 @@ public class ItemRegistryPopulator {
Object2ObjectMap<String, BlockDefinition> bedrockBlockIdOverrides = new Object2ObjectOpenHashMap<>();
Object2IntMap<String> blacklistedIdentifiers = new Object2IntOpenHashMap<>();
Object2ObjectMap<CustomBlockData, ItemDefinition> customBlockItemDefinitions = new Object2ObjectOpenHashMap<>();
List<ItemDefinition> buckets = new ObjectArrayList<>();
List<ItemData> carpets = new ObjectArrayList<>();
@ -235,15 +235,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 {
@ -259,7 +273,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)) {
@ -325,6 +339,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)) {
@ -333,16 +353,35 @@ public class ItemRegistryPopulator {
}
NbtMap states = ((GeyserBedrockBlock) itemData.getBlockDefinition()).getState().getCompound("states");
boolean valid = true;
for (Map.Entry<String, Object> 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;
}
}
@ -383,10 +422,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;
}
@ -502,6 +541,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]))
@ -513,6 +587,7 @@ public class ItemRegistryPopulator {
.componentItemData(componentItemData)
.lodestoneCompass(lodestoneEntry)
.customIdMappings(customIdMappings)
.customBlockItemDefinitions(customBlockItemDefinitions)
.build();
Registries.ITEMS.register(palette.protocolVersion(), itemMappings);

View File

@ -56,6 +56,7 @@ public class BlockMapping {
@Nonnull
PistonBehavior pistonBehavior;
boolean isBlockEntity;
boolean isNonVanilla;
/**
* @return the identifier without the additional block states

View File

@ -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,8 +49,11 @@ public class BlockMappings implements DefinitionRegistry<GeyserBedrockBlock> {
int blockStateVersion;
GeyserBedrockBlock[] javaToBedrockBlocks;
GeyserBedrockBlock[] javaToVanillaBedrockBlocks;
Map<NbtMap, GeyserBedrockBlock> stateDefinitionMap;
GeyserBedrockBlock[] bedrockRuntimeMap;
int[] remappedVanillaIds;
BlockDefinition commandBlock;
@ -54,6 +62,10 @@ public class BlockMappings implements DefinitionRegistry<GeyserBedrockBlock> {
Set<BlockDefinition> jigsawStates;
List<BlockPropertyData> blockProperties;
Object2ObjectMap<CustomBlockState, GeyserBedrockBlock> customBlockStateDefinitions;
Int2ObjectMap<GeyserBedrockBlock> extendedCollisionBoxes;
public int getBedrockBlockId(int javaState) {
return getBedrockBlock(javaState).getRuntimeId();
}
@ -65,6 +77,13 @@ public class BlockMappings implements DefinitionRegistry<GeyserBedrockBlock> {
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);
}
@ -85,6 +104,14 @@ public class BlockMappings implements DefinitionRegistry<GeyserBedrockBlock> {
return bedrockRuntimeMap[bedrockId];
}
public GeyserBedrockBlock getDefinition(NbtMap tag) {
if (tag == null) {
return null;
}
return this.stateDefinitionMap.get(tag);
}
@Override
public boolean isRegistered(GeyserBedrockBlock bedrockBlock) {
return getDefinition(bedrockBlock.getRuntimeId()) == bedrockBlock;

View File

@ -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<CustomBlockPermutation> 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<CustomBlockPermutation> 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<CustomBlockPermutation> 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<CustomBlockPermutation> 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));
}
}
}

View File

@ -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

View File

@ -27,19 +27,22 @@ 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;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition;
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;
import org.geysermc.geyser.item.type.PotionItem;
import javax.annotation.Nonnull;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -71,13 +74,15 @@ public class ItemMappings implements DefinitionRegistry<ItemDefinition> {
List<ComponentItemData> componentItemData;
Int2ObjectMap<String> customIdMappings;
Object2ObjectMap<CustomBlockData, ItemDefinition> customBlockItemDefinitions;
/**
* Gets an {@link ItemMapping} from the given {@link ItemStack}.
*
* @param itemStack the itemstack
* @return an item entry from the given java edition identifier
*/
@Nonnull
@NonNull
public ItemMapping getMapping(ItemStack itemStack) {
return this.getMapping(itemStack.getId());
}
@ -89,11 +94,12 @@ public class ItemMappings implements DefinitionRegistry<ItemDefinition> {
* @param javaId the id
* @return an item entry from the given java edition identifier
*/
@Nonnull
@NonNull
public ItemMapping getMapping(int javaId) {
return javaId >= 0 && javaId < this.items.length ? this.items[javaId] : ItemMapping.AIR;
}
@Nullable
public ItemMapping getMapping(Item javaItem) {
return getMapping(javaItem.javaIdentifier());
}
@ -105,6 +111,7 @@ public class ItemMappings implements DefinitionRegistry<ItemDefinition> {
* @param javaIdentifier the block state identifier
* @return an item entry from the given java edition identifier
*/
@Nullable
public ItemMapping getMapping(String javaIdentifier) {
return this.cachedJavaMappings.computeIfAbsent(javaIdentifier, key -> {
for (ItemMapping mapping : this.items) {
@ -122,6 +129,7 @@ public class ItemMappings implements DefinitionRegistry<ItemDefinition> {
* @param data the item data
* @return an item entry from the given item data
*/
@NonNull
public ItemMapping getMapping(ItemData data) {
ItemDefinition definition = data.getDefinition();
if (ItemDefinition.AIR.equals(definition)) {
@ -158,11 +166,22 @@ public class ItemMappings implements DefinitionRegistry<ItemDefinition> {
return ItemMapping.AIR;
}
@Nullable
@Override
public ItemDefinition getDefinition(int bedrockId) {
return this.itemDefinitions.get(bedrockId);
}
@Nullable
public ItemDefinition getDefinition(String bedrockIdentifier) {
for (ItemDefinition itemDefinition : this.itemDefinitions.values()) {
if (itemDefinition.getIdentifier().equals(bedrockIdentifier)) {
return itemDefinition;
}
}
return null;
}
@Override
public boolean isRegistered(ItemDefinition definition) {
return getDefinition(definition.getRuntimeId()) == definition;

View File

@ -52,6 +52,7 @@ import static org.geysermc.geyser.scoreboard.UpdateType.*;
public final class Scoreboard {
private static final boolean SHOW_SCOREBOARD_LOGS = Boolean.parseBoolean(System.getProperty("Geyser.ShowScoreboardLogs", "true"));
private static final boolean ADD_TEAM_SUGGESTIONS = Boolean.parseBoolean(System.getProperty("Geyser.AddTeamSuggestions", "true"));
private final GeyserSession session;
private final GeyserLogger logger;
@ -150,8 +151,9 @@ public final class Scoreboard {
teams.put(teamName, team);
// Update command parameters - is safe to send even if the command enum doesn't exist on the client (as of 1.19.51)
session.addCommandEnum("Geyser_Teams", team.getId());
if (ADD_TEAM_SUGGESTIONS) {
session.addCommandEnum("Geyser_Teams", team.getId());
}
return team;
}

View File

@ -64,7 +64,6 @@ import com.github.steveice10.packetlib.event.session.SessionAdapter;
import com.github.steveice10.packetlib.packet.Packet;
import com.github.steveice10.packetlib.tcp.TcpClientSession;
import com.github.steveice10.packetlib.tcp.TcpSession;
import com.nimbusds.jwt.SignedJWT;
import io.netty.channel.Channel;
import io.netty.channel.EventLoop;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
@ -99,6 +98,7 @@ import org.cloudburstmc.protocol.common.util.OptionalBoolean;
import org.geysermc.api.util.BedrockPlatform;
import org.geysermc.api.util.InputMode;
import org.geysermc.api.util.UiProfile;
import org.geysermc.geyser.api.bedrock.camera.CameraShake;
import org.geysermc.geyser.api.util.PlatformType;
import org.geysermc.cumulus.form.Form;
import org.geysermc.cumulus.form.util.FormBuilder;
@ -114,6 +114,7 @@ import org.geysermc.geyser.api.network.AuthType;
import org.geysermc.geyser.api.network.RemoteServer;
import org.geysermc.geyser.command.GeyserCommandSource;
import org.geysermc.geyser.configuration.EmoteOffhandWorkaroundOption;
import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.entity.attribute.GeyserAttributeType;
import org.geysermc.geyser.entity.type.Entity;
@ -155,6 +156,7 @@ import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@ -178,7 +180,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
* Used for Floodgate skin uploading
*/
@Setter
private List<SignedJWT> certChainData;
private List<String> certChainData;
@NotNull
@Setter
@ -423,6 +425,14 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
@Setter
private boolean emulatePost1_18Logic = true;
/**
* Whether to emulate pre-1.20 smithing table behavior.
* Adapts ViaVersion's furnace UI to one Bedrock can use.
* See {@link org.geysermc.geyser.translator.inventory.OldSmithingTableTranslator}.
*/
@Setter
private boolean oldSmithingTable = false;
/**
* The current attack speed of the player. Used for sending proper cooldown timings.
* Setting a default fixes cooldowns not showing up on a fresh world.
@ -454,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.
*/
@ -534,7 +550,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
@Setter
private boolean waitingForStatistics = false;
private final Set<String> fogNameSpaces = new HashSet<>();
/**
* All fog effects that are currently applied to the client.
*/
private final Set<String> appliedFog = new HashSet<>();
private final Set<UUID> emotes;
@ -561,6 +580,12 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
@Setter
private ScheduledFuture<?> mountVehicleScheduledFuture = null;
/**
* A cache of IDs from ClientboundKeepAlivePackets that have been sent to the Bedrock client, but haven't been returned to the server.
* Only used if {@link GeyserConfiguration#isForwardPlayerPing()} is enabled.
*/
private final Queue<Long> keepAliveCache = new ConcurrentLinkedQueue<>();
private MinecraftProtocol protocol;
public GeyserSession(GeyserImpl geyser, BedrockServerSession bedrockServerSession, EventLoop eventLoop) {
@ -910,7 +935,15 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
} else {
downstream = new TcpClientSession(this.remoteServer.address(), this.remoteServer.port(), this.protocol);
this.downstream = new DownstreamSession(downstream);
disableSrvResolving();
boolean resolveSrv = false;
try {
resolveSrv = this.remoteServer.resolveSrv();
} catch (AbstractMethodError | NoSuchMethodError ignored) {
// Ignore if the method doesn't exist
// This will happen with extensions using old APIs
}
this.downstream.getSession().setFlag(BuiltinFlags.ATTEMPT_SRV_RESOLVE, resolveSrv);
}
if (geyser.getConfig().getRemote().isUseProxyProtocol()) {
@ -1394,13 +1427,6 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
sendDownstreamPacket(swapHandsPacket);
}
/**
* Will be overwritten for GeyserConnect.
*/
protected void disableSrvResolving() {
this.downstream.getSession().setFlag(BuiltinFlags.ATTEMPT_SRV_RESOLVE, false);
}
@Override
public String name() {
return playerEntity.getUsername();
@ -1561,6 +1587,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?
@ -1574,11 +1611,6 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
startGamePacket.setRewindHistorySize(0);
startGamePacket.setServerAuthoritativeBlockBreaking(false);
if (GameProtocol.isPre1_20(this)) {
startGamePacket.getExperiments().add(new ExperimentData("next_major_update", true));
startGamePacket.getExperiments().add(new ExperimentData("sniffer", true));
}
upstream.sendPacket(startGamePacket);
}
@ -1835,38 +1867,6 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
}
}
/**
* Send the following fog IDs, as well as the cached ones, to the client.
*
* Fog IDs can be found here:
* https://wiki.bedrock.dev/documentation/fog-ids.html
*
* @param fogNameSpaces the fog ids to add
*/
public void sendFog(String... fogNameSpaces) {
this.fogNameSpaces.addAll(Arrays.asList(fogNameSpaces));
PlayerFogPacket packet = new PlayerFogPacket();
packet.getFogStack().addAll(this.fogNameSpaces);
sendUpstreamPacket(packet);
}
/**
* Removes the following fog IDs from the client and the cache.
*
* @param fogNameSpaces the fog ids to remove
*/
public void removeFog(String... fogNameSpaces) {
if (fogNameSpaces.length == 0) {
this.fogNameSpaces.clear();
} else {
this.fogNameSpaces.removeAll(Arrays.asList(fogNameSpaces));
}
PlayerFogPacket packet = new PlayerFogPacket();
packet.getFogStack().addAll(this.fogNameSpaces);
sendUpstreamPacket(packet);
}
public boolean canUseCommandBlocks() {
return instabuild && opPermissionLevel >= 2;
}
@ -1978,6 +1978,54 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
sendUpstreamPacket(packet);
}
@Override
public void shakeCamera(float intensity, float duration, @NonNull CameraShake type) {
CameraShakePacket packet = new CameraShakePacket();
packet.setIntensity(intensity);
packet.setDuration(duration);
packet.setShakeType(type == CameraShake.POSITIONAL ? CameraShakeType.POSITIONAL : CameraShakeType.ROTATIONAL);
packet.setShakeAction(CameraShakeAction.ADD);
sendUpstreamPacket(packet);
}
@Override
public void stopCameraShake() {
CameraShakePacket packet = new CameraShakePacket();
// CameraShakeAction.STOP removes all types regardless of the given type, but regardless it can't be null
packet.setShakeType(CameraShakeType.POSITIONAL);
packet.setShakeAction(CameraShakeAction.STOP);
sendUpstreamPacket(packet);
}
@Override
public void sendFog(String... fogNameSpaces) {
Collections.addAll(this.appliedFog, fogNameSpaces);
PlayerFogPacket packet = new PlayerFogPacket();
packet.getFogStack().addAll(this.appliedFog);
sendUpstreamPacket(packet);
}
@Override
public void removeFog(String... fogNameSpaces) {
if (fogNameSpaces.length == 0) {
this.appliedFog.clear();
} else {
for (String id : fogNameSpaces) {
this.appliedFog.remove(id);
}
}
PlayerFogPacket packet = new PlayerFogPacket();
packet.getFogStack().addAll(this.appliedFog);
sendUpstreamPacket(packet);
}
@Override
public @NonNull Set<String> fogEffects() {
// Use a copy so that sendFog/removeFog can be called while iterating the returned set (avoid CME)
return Set.copyOf(this.appliedFog);
}
public void addCommandEnum(String name, String enums) {
softEnumPacket(name, SoftEnumUpdateType.ADD, enums);
}
@ -1987,6 +2035,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
}
private void softEnumPacket(String name, SoftEnumUpdateType type, String enums) {
// There is no need to send command enums if command suggestions are disabled
if (!this.geyser.getConfig().isCommandSuggestions()) {
return;
}
UpdateSoftEnumPacket packet = new UpdateSoftEnumPacket();
packet.setType(type);
packet.setSoftEnum(new CommandEnumData(name, Collections.singletonMap(enums, Collections.emptySet()), true));

View File

@ -42,6 +42,13 @@ import java.util.concurrent.atomic.AtomicInteger;
@RequiredArgsConstructor
public class FormCache {
/**
* The magnitude of this doesn't actually matter, but it must be negative so that
* BedrockNetworkStackLatencyTranslator can detect the hack.
*/
private static final long MAGIC_FORM_IMAGE_HACK_TIMESTAMP = -1234567890L;
private final FormDefinitions formDefinitions = FormDefinitions.instance();
private final AtomicInteger formIdCounter = new AtomicInteger(0);
private final Int2ObjectMap<Form> forms = new Int2ObjectOpenHashMap<>();
@ -73,7 +80,7 @@ public class FormCache {
if (form instanceof SimpleForm) {
NetworkStackLatencyPacket latencyPacket = new NetworkStackLatencyPacket();
latencyPacket.setFromServer(true);
latencyPacket.setTimestamp(-System.currentTimeMillis());
latencyPacket.setTimestamp(MAGIC_FORM_IMAGE_HACK_TIMESTAMP);
session.scheduleInEventLoop(
() -> session.sendUpstreamPacket(latencyPacket),
500, TimeUnit.MILLISECONDS

View File

@ -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;

View File

@ -30,7 +30,6 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.nimbusds.jwt.SignedJWT;
import lombok.Getter;
import org.geysermc.floodgate.pluginmessage.PluginMessageChannels;
import org.geysermc.floodgate.util.WebsocketEventType;
@ -191,14 +190,14 @@ public final class FloodgateSkinUploader {
};
}
public void uploadSkin(List<SignedJWT> chainData, String clientData) {
public void uploadSkin(List<String> chainData, String clientData) {
if (chainData == null || clientData == null) {
return;
}
ObjectNode node = JACKSON.createObjectNode();
ArrayNode chainDataNode = JACKSON.createArrayNode();
chainData.forEach(jwt -> chainDataNode.add(jwt.serialize()));
chainData.forEach(chainDataNode::add);
node.set("chain_data", chainDataNode);
node.put("client_data", clientData);

View File

@ -277,8 +277,15 @@ public class SkinManager {
return null;
}
static GameProfileData loadFromJson(String encodedJson) throws IOException, IllegalArgumentException {
JsonNode skinObject = GeyserImpl.JSON_MAPPER.readTree(new String(Base64.getDecoder().decode(encodedJson), StandardCharsets.UTF_8));
public static GameProfileData loadFromJson(String encodedJson) throws IOException, IllegalArgumentException {
JsonNode skinObject;
try {
skinObject = GeyserImpl.JSON_MAPPER.readTree(new String(Base64.getDecoder().decode(encodedJson), StandardCharsets.UTF_8));
} catch (IllegalArgumentException e) {
GeyserImpl.getInstance().getLogger().debug("Invalid base64 encoded skin entry: " + encodedJson);
return null;
}
JsonNode textures = skinObject.get("textures");
if (textures == null) {

View File

@ -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;

View File

@ -28,7 +28,7 @@ package org.geysermc.geyser.text;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.serializer.gson.LegacyHoverEventSerializer;
import net.kyori.adventure.text.serializer.json.LegacyHoverEventSerializer;
import net.kyori.adventure.util.Codec;
import org.jetbrains.annotations.NotNull;
@ -40,9 +40,9 @@ public final class DummyLegacyHoverEventSerializer implements LegacyHoverEventSe
private final HoverEvent.ShowItem dummyShowItem;
public DummyLegacyHoverEventSerializer() {
dummyShowEntity = HoverEvent.ShowEntity.of(Key.key("geysermc", "dummyshowitem"),
dummyShowEntity = HoverEvent.ShowEntity.showEntity(Key.key("geysermc", "dummyshowitem"),
UUID.nameUUIDFromBytes("entitiesareprettyneat".getBytes(StandardCharsets.UTF_8)));
dummyShowItem = HoverEvent.ShowItem.of(Key.key("geysermc", "dummyshowentity"), 0);
dummyShowItem = HoverEvent.ShowItem.showItem(Key.key("geysermc", "dummyshowentity"), 0);
}
@Override

View File

@ -0,0 +1,147 @@
/*
* 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.translator.inventory;
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId;
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.ItemStackRequest;
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.ItemStackRequestSlotData;
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.action.DropAction;
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.action.ItemStackRequestAction;
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.action.PlaceAction;
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.action.SwapAction;
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.action.TakeAction;
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.response.ItemStackResponse;
import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket;
import org.geysermc.geyser.inventory.BedrockContainerSlot;
import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.inventory.updater.UIInventoryUpdater;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.util.InventoryUtils;
import java.util.function.IntFunction;
/**
* Translator for smithing tables for pre-1.20 servers.
* This adapts ViaVersion's furnace ui to the 1.20+ smithing table; with the addition of a fake smithing template so Bedrock clients can use it.
*/
public class OldSmithingTableTranslator extends AbstractBlockInventoryTranslator {
public static final OldSmithingTableTranslator INSTANCE = new OldSmithingTableTranslator();
private static final IntFunction<ItemData> UPGRADE_TEMPLATE = InventoryUtils.getUpgradeTemplate();
private OldSmithingTableTranslator() {
super(3, "minecraft:smithing_table", ContainerType.SMITHING_TABLE, UIInventoryUpdater.INSTANCE);
}
@Override
public int bedrockSlotToJava(ItemStackRequestSlotData slotInfoData) {
return switch (slotInfoData.getContainer()) {
case SMITHING_TABLE_INPUT -> 0;
case SMITHING_TABLE_MATERIAL -> 1;
case SMITHING_TABLE_RESULT, CREATED_OUTPUT -> 2;
default -> super.bedrockSlotToJava(slotInfoData);
};
}
@Override
public BedrockContainerSlot javaSlotToBedrockContainer(int slot) {
return switch (slot) {
case 0 -> new BedrockContainerSlot(ContainerSlotType.SMITHING_TABLE_INPUT, 51);
case 1 -> new BedrockContainerSlot(ContainerSlotType.SMITHING_TABLE_MATERIAL, 52);
case 2 -> new BedrockContainerSlot(ContainerSlotType.SMITHING_TABLE_RESULT, 50);
default -> super.javaSlotToBedrockContainer(slot);
};
}
@Override
public int javaSlotToBedrock(int slot) {
return switch (slot) {
case 0 -> 51;
case 1 -> 52;
case 2 -> 50;
default -> super.javaSlotToBedrock(slot);
};
}
@Override
public boolean shouldHandleRequestFirst(ItemStackRequestAction action, Inventory inventory) {
return true;
}
@Override
protected ItemStackResponse translateSpecialRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) {
for (var action: request.getActions()) {
switch (action.getType()) {
case DROP -> {
if (isInvalidAction(((DropAction) action).getSource())) {
return rejectRequest(request, false);
}
}
case TAKE -> {
if (isInvalidAction(((TakeAction) action).getSource()) ||
isInvalidAction(((TakeAction) action).getDestination())) {
return rejectRequest(request, false);
}
}
case SWAP -> {
if (isInvalidAction(((SwapAction) action).getSource()) ||
isInvalidAction(((SwapAction) action).getDestination())) {
return rejectRequest(request, false);
}
}
case PLACE -> {
if (isInvalidAction(((PlaceAction) action).getSource()) ||
isInvalidAction(((PlaceAction) action).getDestination())) {
return rejectRequest(request, false);
}
}
}
}
// Allow everything else that doesn't involve the fake template
return super.translateRequest(session, inventory, request);
}
private boolean isInvalidAction(ItemStackRequestSlotData slotData) {
return slotData.getContainer().equals(ContainerSlotType.SMITHING_TABLE_TEMPLATE);
}
@Override
public void openInventory(GeyserSession session, Inventory inventory) {
super.openInventory(session, inventory);
// pre-1.20 server has no concept of templates, but we are working with a 1.20 client
// put a fake netherite upgrade template in the template slot otherwise the client doesn't recognize a valid recipe
InventorySlotPacket slotPacket = new InventorySlotPacket();
slotPacket.setContainerId(ContainerId.UI);
slotPacket.setSlot(53);
slotPacket.setItem(UPGRADE_TEMPLATE.apply(session.getUpstream().getProtocolVersion()));
session.sendUpstreamPacket(slotPacket);
}
}

View File

@ -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);

View File

@ -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);
}
}

View File

@ -39,11 +39,12 @@ import org.geysermc.geyser.registry.type.ItemMapping;
public class CampfireBlockEntityTranslator extends BlockEntityTranslator {
@Override
public void translateTag(NbtMapBuilder builder, CompoundTag tag, int blockState) {
ListTag items = tag.get("Items");
int i = 1;
for (Tag itemTag : items.getValue()) {
builder.put("Item" + i, getItem((CompoundTag) itemTag));
i++;
if (tag.get("Items") instanceof ListTag items) {
int i = 1;
for (Tag itemTag : items.getValue()) {
builder.put("Item" + i, getItem((CompoundTag) itemTag));
i++;
}
}
}

View File

@ -43,7 +43,7 @@ public class CommandBlockBlockEntityTranslator extends BlockEntityTranslator imp
// Java and Bedrock values
builder.put("conditionMet", ((ByteTag) tag.get("conditionMet")).getValue());
builder.put("auto", ((ByteTag) tag.get("auto")).getValue());
builder.put("CustomName", MessageTranslator.convertMessage(((StringTag) tag.get("CustomName")).getValue()));
builder.put("CustomName", MessageTranslator.convertJsonMessage(((StringTag) tag.get("CustomName")).getValue()));
builder.put("powered", ((ByteTag) tag.get("powered")).getValue());
builder.put("Command", ((StringTag) tag.get("Command")).getValue());
builder.put("SuccessCount", ((IntTag) tag.get("SuccessCount")).getValue());

View File

@ -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;

View File

@ -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<String> 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);
}
}
}

View File

@ -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<Inve
}
}
// Check if this is a double placement due to an extended collision block
if (!session.getBlockMappings().getExtendedCollisionBoxes().isEmpty()) {
Vector3i belowBlockPos = null;
switch (packet.getBlockFace()) {
case 1 -> 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<Inve
restoreCorrectBlock(session, blockPos, packet);
return;
}
/*
Block place checks end - client is good to go
*/
@ -524,10 +548,20 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
*/
private void restoreCorrectBlock(GeyserSession session, Vector3i blockPos, InventoryTransactionPacket packet) {
int javaBlockState = session.getGeyser().getWorldManager().getBlockAt(session, blockPos);
BlockDefinition bedrockBlock = session.getBlockMappings().getBedrockBlock(javaBlockState);
if (BlockStateValues.getSkullVariant(javaBlockState) == 3) {
// The changed block was a player skull so check if a custom block was defined for this skull
SkullCache.Skull skull = session.getSkullCache().getSkulls().get(blockPos);
if (skull != null && skull.getBlockDefinition() != null) {
bedrockBlock = skull.getBlockDefinition();
}
}
UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket();
updateBlockPacket.setDataLayer(0);
updateBlockPacket.setBlockPosition(blockPos);
updateBlockPacket.setDefinition(session.getBlockMappings().getBedrockBlock(javaBlockState));
updateBlockPacket.setDefinition(bedrockBlock);
updateBlockPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY);
session.sendUpstreamPacket(updateBlockPacket);

View File

@ -36,7 +36,7 @@ import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
/**
* Sent by the client when moving a horse.
* Sent by the client when moving a horse or boat.
*/
@Translator(packet = MoveEntityAbsolutePacket.class)
public class BedrockMoveEntityAbsoluteTranslator extends PacketTranslator<MoveEntityAbsolutePacket> {
@ -64,7 +64,7 @@ public class BedrockMoveEntityAbsoluteTranslator extends PacketTranslator<MoveEn
}
float y = packet.getPosition().getY();
if (ridingEntity instanceof BoatEntity) {
if (ridingEntity instanceof BoatEntity && !ridingEntity.isOnGround()) {
// Remove the offset to prevents boats from looking like they're floating in water
y -= EntityDefinitions.BOAT.offset();
}

View File

@ -29,7 +29,6 @@ import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundKe
import org.cloudburstmc.protocol.bedrock.data.AttributeData;
import org.cloudburstmc.protocol.bedrock.packet.NetworkStackLatencyPacket;
import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket;
import org.geysermc.floodgate.util.DeviceOs;
import org.geysermc.geyser.entity.attribute.GeyserAttributeType;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
@ -46,37 +45,43 @@ public class BedrockNetworkStackLatencyTranslator extends PacketTranslator<Netwo
@Override
public void translate(GeyserSession session, NetworkStackLatencyPacket packet) {
long pingId;
// so apparently, as of 1.16.200
// PS4 divides the network stack latency timestamp FOR US!!!
// WTF
if (session.getClientData().getDeviceOs().equals(DeviceOs.PS4)) {
pingId = packet.getTimestamp();
} else {
pingId = packet.getTimestamp() / 1000;
}
// negative timestamps are used as hack to fix the url image loading bug
if (packet.getTimestamp() > 0) {
if (packet.getTimestamp() >= 0) {
if (session.getGeyser().getConfig().isForwardPlayerPing()) {
ServerboundKeepAlivePacket keepAlivePacket = new ServerboundKeepAlivePacket(pingId);
// use our cached value because
// a) bedrock can be inaccurate with the value returned
// b) playstation replies with a different magnitude than other platforms
// c) 1.20.10 and later reply with a different magnitude
Long keepAliveId = session.getKeepAliveCache().poll();
if (keepAliveId == null) {
session.getGeyser().getLogger().debug("Received a latency packet that we don't have a KeepAlive for: " + packet);
return;
}
ServerboundKeepAlivePacket keepAlivePacket = new ServerboundKeepAlivePacket(keepAliveId);
session.sendDownstreamPacket(keepAlivePacket);
}
return;
}
// Hack to fix the url image loading bug
UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket();
attributesPacket.setRuntimeEntityId(session.getPlayerEntity().getGeyserId());
session.scheduleInEventLoop(() -> {
// Hack to fix the url image loading bug
UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket();
attributesPacket.setRuntimeEntityId(session.getPlayerEntity().getGeyserId());
AttributeData attribute = session.getPlayerEntity().getAttributes().get(GeyserAttributeType.EXPERIENCE_LEVEL);
if (attribute != null) {
attributesPacket.setAttributes(Collections.singletonList(attribute));
} else {
attributesPacket.setAttributes(Collections.singletonList(GeyserAttributeType.EXPERIENCE_LEVEL.getAttribute(0)));
}
AttributeData attribute = session.getPlayerEntity().getAttributes().get(GeyserAttributeType.EXPERIENCE_LEVEL);
if (attribute != null) {
attributesPacket.setAttributes(Collections.singletonList(attribute));
} else {
attributesPacket.setAttributes(Collections.singletonList(GeyserAttributeType.EXPERIENCE_LEVEL.getAttribute(0)));
}
session.scheduleInEventLoop(() -> session.sendUpstreamPacket(attributesPacket),
500, TimeUnit.MILLISECONDS);
session.sendUpstreamPacket(attributesPacket);
}, 500, TimeUnit.MILLISECONDS);
}
@Override
public boolean shouldExecuteInEventLoop() {
return false;
}
}

Some files were not shown because too many files have changed in this diff Show More