diff --git a/README.md b/README.md
index 096a81486..31339e04d 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,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!
## Supported Versions
-Geyser is currently supporting Minecraft Bedrock 1.21.90 - 1.21.110 and Minecraft Java 1.21.7 - 1.21.8. For more information, please see [here](https://geysermc.org/wiki/geyser/supported-versions/).
+Geyser is currently supporting Minecraft Bedrock 1.21.90 - 1.21.110 and Minecraft Java 1.21.9 - 1.21.10. For more information, please see [here](https://geysermc.org/wiki/geyser/supported-versions/).
## Setting Up
Take a look [here](https://geysermc.org/wiki/geyser/setup/) for how to set up Geyser.
diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockState.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockState.java
index 61b868cc3..c1950f965 100644
--- a/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockState.java
+++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockState.java
@@ -59,7 +59,9 @@ public interface JavaBlockState {
* Gets the pick item of the block state
*
* @return the pick item of the block state
+ * @deprecated the pick item is sent by the Java server
*/
+ @Deprecated
@Nullable String pickItem();
/**
@@ -103,6 +105,7 @@ public interface JavaBlockState {
Builder canBreakWithHand(boolean canBreakWithHand);
+ @Deprecated
Builder pickItem(@Nullable String pickItem);
Builder pistonBehavior(@Nullable String pistonBehavior);
diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/property/BatchPropertyUpdater.java b/api/src/main/java/org/geysermc/geyser/api/entity/property/BatchPropertyUpdater.java
new file mode 100644
index 000000000..308d23cd1
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/property/BatchPropertyUpdater.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2025 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.entity.property;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.geysermc.geyser.api.event.lifecycle.GeyserDefineEntityPropertiesEvent;
+
+/**
+ * Collects property changes to be applied as a single, batched update to an entity.
+ *
+ * Notes:
+ *
+ * - Passing {@code null} as a value resets the property to its default.
+ * - Numeric properties must be within declared ranges; enum properties must use an allowed value.
+ * - Multiple updates to the same property within a single batch will result in the last value being applied.
+ * - The updater is short-lived and should not be retained outside the batching callback.
+ *
+ *
+ * {@code
+ * entity.updatePropertiesBatched(updater -> {
+ * updater.update(SOME_FLOAT_PROPERTY, 0.15f);
+ * updater.update(SOME_BOOLEAN_PROPERTY, true);
+ * updater.update(SOME_INT_PROPERTY, null); // reset to default
+ * });
+ * }
+ *
+ * @since 2.9.0
+ */
+@FunctionalInterface
+public interface BatchPropertyUpdater {
+
+ /**
+ * Queues an update for the given property within the current batch.
+ *
+ * If {@code value} is {@code null}, the property will be reset to its default value
+ * as declared when the property was registered during the {@link GeyserDefineEntityPropertiesEvent}.
+ *
+ * @param property a {@link GeyserEntityProperty} registered for the target entity type
+ * @param value the new value, or {@code null} to reset to the default
+ * @param the property's value type
+ *
+ * @since 2.9.0
+ */
+ void update(@NonNull GeyserEntityProperty property, @Nullable T value);
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/property/GeyserEntityProperty.java b/api/src/main/java/org/geysermc/geyser/api/entity/property/GeyserEntityProperty.java
new file mode 100644
index 000000000..9489a9946
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/property/GeyserEntityProperty.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2025 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.entity.property;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.geysermc.geyser.api.util.Identifier;
+
+/**
+ * Represents a property that can be attached to an entity.
+ *
+ * Entity properties are used to describe metadata about an entity, such as
+ * integers, floats, booleans, or enums.
+ * @see
+ * Official documentation for info
+ *
+ * @param the type of value stored by this property
+ *
+ * @since 2.9.0
+ */
+public interface GeyserEntityProperty {
+
+ /**
+ * Gets the unique name of this property.
+ * Custom properties cannot use the vanilla namespace
+ * to avoid collisions with vanilla entity properties.
+ *
+ * @return the property identifier
+ * @since 2.9.0
+ */
+ @NonNull
+ Identifier identifier();
+
+ /**
+ * Gets the default value of this property which
+ * is set upon spawning entities.
+ *
+ * @return the default value of this property
+ * @since 2.9.0
+ */
+ @NonNull
+ T defaultValue();
+}
diff --git a/core/src/main/java/org/geysermc/geyser/level/block/type/HoneyBlock.java b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserBooleanEntityProperty.java
similarity index 77%
rename from core/src/main/java/org/geysermc/geyser/level/block/type/HoneyBlock.java
rename to api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserBooleanEntityProperty.java
index 642240915..1c988d5d7 100644
--- a/core/src/main/java/org/geysermc/geyser/level/block/type/HoneyBlock.java
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserBooleanEntityProperty.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024 GeyserMC. http://geysermc.org
+ * Copyright (c) 2025 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
@@ -23,10 +23,13 @@
* @link https://github.com/GeyserMC/Geyser
*/
-package org.geysermc.geyser.level.block.type;
+package org.geysermc.geyser.api.entity.property.type;
-public class HoneyBlock extends Block {
- public HoneyBlock(String javaIdentifier, Builder builder) {
- super(javaIdentifier, builder);
- }
+import org.geysermc.geyser.api.entity.property.GeyserEntityProperty;
+
+/**
+ * Represents a boolean entity property.
+ * @since 2.9.0
+ */
+public interface GeyserBooleanEntityProperty extends GeyserEntityProperty {
}
diff --git a/core/src/main/java/org/geysermc/geyser/level/block/type/PistonHeadBlock.java b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserEnumEntityProperty.java
similarity index 62%
rename from core/src/main/java/org/geysermc/geyser/level/block/type/PistonHeadBlock.java
rename to api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserEnumEntityProperty.java
index 8a6b4f41c..283111c7f 100644
--- a/core/src/main/java/org/geysermc/geyser/level/block/type/PistonHeadBlock.java
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserEnumEntityProperty.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024 GeyserMC. http://geysermc.org
+ * Copyright (c) 2025 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
@@ -23,20 +23,20 @@
* @link https://github.com/GeyserMC/Geyser
*/
-package org.geysermc.geyser.level.block.type;
+package org.geysermc.geyser.api.entity.property.type;
-import org.geysermc.geyser.level.block.Blocks;
-import org.geysermc.geyser.level.block.property.Properties;
-import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;
+import org.geysermc.geyser.api.entity.property.GeyserEntityProperty;
-public class PistonHeadBlock extends Block {
- public PistonHeadBlock(String javaIdentifier, Builder builder) {
- super(javaIdentifier, builder);
- }
-
- @Override
- public ItemStack pickItem(BlockState state) {
- Block block = state.getValue(Properties.PISTON_TYPE).equals("sticky") ? Blocks.STICKY_PISTON : Blocks.PISTON;
- return new ItemStack(block.asItem().javaId());
- }
+/**
+ * Represents a Java enum-backed enum property.
+ * There are a few key limitations:
+ *
+ * - There cannot be more than 16 values
+ * - Enum names cannot be longer than 32 chars, must start with a letter, and may contain numbers and underscores
+ *
+ *
+ * @param the enum type
+ * @since 2.9.0
+ */
+public interface GeyserEnumEntityProperty> extends GeyserEntityProperty {
}
diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserFloatEntityProperty.java b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserFloatEntityProperty.java
new file mode 100644
index 000000000..8337e09fa
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserFloatEntityProperty.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2025 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.entity.property.type;
+
+import org.geysermc.geyser.api.entity.property.GeyserEntityProperty;
+import org.geysermc.geyser.api.event.lifecycle.GeyserDefineEntityPropertiesEvent;
+import org.geysermc.geyser.api.util.Identifier;
+
+/**
+ * Represents a float-backed entity property with inclusive bounds.
+ * Values associated with this property must be always within the {@code [min(), max()]} bounds.
+ *
+ * @see GeyserDefineEntityPropertiesEvent#registerFloatProperty(Identifier, Identifier, float, float, Float)
+ * @since 2.9.0
+ */
+public interface GeyserFloatEntityProperty extends GeyserEntityProperty {
+
+ /**
+ * @return the inclusive lower bound for this property
+ * @since 2.9.0
+ */
+ float min();
+
+ /**
+ * @return the inclusive upper bound for this property
+ * @since 2.9.0
+ */
+ float max();
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserIntEntityProperty.java b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserIntEntityProperty.java
new file mode 100644
index 000000000..36c4996bd
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserIntEntityProperty.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2025 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.entity.property.type;
+
+import org.geysermc.geyser.api.entity.property.GeyserEntityProperty;
+import org.geysermc.geyser.api.event.lifecycle.GeyserDefineEntityPropertiesEvent;
+import org.geysermc.geyser.api.util.Identifier;
+
+/**
+ * Represents an int-backed entity property with inclusive bounds.
+ * There are a few key limitations:
+ *
+ * - Values must be always within the {@code [min(), max()]} bounds
+ * - Molang evaluation uses floats under the hood; very large integers can lose precision.
+ * Prefer keeping values in a practical range to avoid rounding issues.
+ *
+ *
+ * @see GeyserDefineEntityPropertiesEvent#registerIntegerProperty(Identifier, Identifier, int, int, Integer)
+ * @since 2.9.0
+ */
+public interface GeyserIntEntityProperty extends GeyserEntityProperty {
+
+ /**
+ * @return the inclusive lower bound for this property
+ * @since 2.9.0
+ */
+ int min();
+
+ /**
+ * @return the inclusive upper bound for this property
+ * @since 2.9.0
+ */
+ int max();
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserStringEnumProperty.java b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserStringEnumProperty.java
new file mode 100644
index 000000000..ebd541ef7
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserStringEnumProperty.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2025 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.entity.property.type;
+
+import org.geysermc.geyser.api.entity.property.GeyserEntityProperty;
+
+import java.util.List;
+
+/**
+ * Represents a string-backed enum property.
+ * There are a few key limitations:
+ *
+ * - There cannot be more than 16 values
+ * - The values' names cannot be longer than 32 chars, must start with a letter, and may contain numbers and underscores
+ *
+ *
+ * @since 2.9.0
+ */
+public interface GeyserStringEnumProperty extends GeyserEntityProperty {
+
+ /**
+ * @return an unmodifiable list of all registered values
+ * @since 2.9.0
+ */
+ List values();
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/type/GeyserEntity.java b/api/src/main/java/org/geysermc/geyser/api/entity/type/GeyserEntity.java
index 02acb4e21..1f8698518 100644
--- a/api/src/main/java/org/geysermc/geyser/api/entity/type/GeyserEntity.java
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/type/GeyserEntity.java
@@ -26,9 +26,17 @@
package org.geysermc.geyser.api.entity.type;
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.connection.GeyserConnection;
+import org.geysermc.geyser.api.entity.property.BatchPropertyUpdater;
+import org.geysermc.geyser.api.entity.property.GeyserEntityProperty;
+import org.geysermc.geyser.api.event.lifecycle.GeyserDefineEntityPropertiesEvent;
+
+import java.util.function.Consumer;
/**
- * Represents a unique instance of an entity. Each {@link org.geysermc.geyser.api.connection.GeyserConnection}
+ * Represents a unique instance of an entity. Each {@link GeyserConnection}
* have their own sets of entities - no two instances will share the same GeyserEntity instance.
*/
public interface GeyserEntity {
@@ -37,4 +45,24 @@ public interface GeyserEntity {
*/
@NonNegative
int javaId();
+
+ /**
+ * Updates an entity property with a new value.
+ * If the new value is null, the property is reset to the default value.
+ *
+ * @param property a {@link GeyserEntityProperty} registered for this type in the {@link GeyserDefineEntityPropertiesEvent}
+ * @param value the new property value
+ * @param the type of the value
+ * @since 2.9.0
+ */
+ default void updateProperty(@NonNull GeyserEntityProperty property, @Nullable T value) {
+ this.updatePropertiesBatched(consumer -> consumer.update(property, value));
+ }
+
+ /**
+ * Updates multiple properties with just one update packet.
+ * @see BatchPropertyUpdater
+ * @since 2.9.0
+ */
+ void updatePropertiesBatched(Consumer consumer);
}
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionAcceptCodeOfConductEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionAcceptCodeOfConductEvent.java
new file mode 100644
index 000000000..0df79765e
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionAcceptCodeOfConductEvent.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2025 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.bedrock;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.geysermc.geyser.api.connection.GeyserConnection;
+import org.geysermc.geyser.api.event.connection.ConnectionEvent;
+import org.geysermc.geyser.api.event.java.ServerCodeOfConductEvent;
+
+/**
+ * Fired when a player accepts a code of conduct sent by the Java server. API users can listen to this event
+ * to store the acceptance in a cache, and tell Geyser not to do so.
+ *
+ * Java clients cache acceptance locally, but bedrock clients don't. Normally Geyser uses a simple JSON file to implement this,
+ * but an alternative solution may be preferred when using multiple Geyser instances. Such a solution can be implemented through this event and {@link ServerCodeOfConductEvent}.
+ *
+ * @see ServerCodeOfConductEvent
+ * @since 2.9.0
+ */
+public class SessionAcceptCodeOfConductEvent extends ConnectionEvent {
+ private final String codeOfConduct;
+ private boolean skipSaving = false;
+
+ public SessionAcceptCodeOfConductEvent(@NonNull GeyserConnection connection, String codeOfConduct) {
+ super(connection);
+ this.codeOfConduct = codeOfConduct;
+ }
+
+ /**
+ * @return the code of conduct sent by the server
+ * @since 2.9.0
+ */
+ public String codeOfConduct() {
+ return codeOfConduct;
+ }
+
+ /**
+ * @return {@code true} if Geyser should not save the acceptance of the code of conduct in its own cache (through a JSON file), because it was saved elsewhere
+ * @since 2.9.0
+ */
+ public boolean shouldSkipSaving() {
+ return skipSaving;
+ }
+
+ /**
+ * Sets {@link SessionAcceptCodeOfConductEvent#shouldSkipSaving()} to {@code true}.
+ * @since 2.9.0
+ */
+ public void skipSaving() {
+ this.skipSaving = true;
+ }
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/java/ServerCodeOfConductEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerCodeOfConductEvent.java
new file mode 100644
index 000000000..d40094fb6
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerCodeOfConductEvent.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2025 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.java;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.geysermc.geyser.api.connection.GeyserConnection;
+import org.geysermc.geyser.api.event.bedrock.SessionAcceptCodeOfConductEvent;
+import org.geysermc.geyser.api.event.connection.ConnectionEvent;
+
+/**
+ * Fired when the Java server sends a code of conduct during the configuration phase.
+ * API users can listen to this event and tell Geyser the player has accepted the code of conduct before, which will result in the
+ * code of conduct not being shown to the player.
+ *
+ * Java clients cache this locally, but bedrock clients don't. Normally Geyser uses a simple JSON file to implement this,
+ * but an alternative solution may be preferred when using multiple Geyser instances. Such a solution can be implemented through this event and {@link SessionAcceptCodeOfConductEvent}.
+ *
+ * @see SessionAcceptCodeOfConductEvent
+ * @since 2.9.0
+ */
+public final class ServerCodeOfConductEvent extends ConnectionEvent {
+ private final String codeOfConduct;
+ private boolean hasAccepted = false;
+
+ public ServerCodeOfConductEvent(@NonNull GeyserConnection connection, String codeOfConduct) {
+ super(connection);
+ this.codeOfConduct = codeOfConduct;
+ }
+
+ /**
+ * @return the code of conduct sent by the server
+ * @since 2.9.0
+ */
+ public String codeOfConduct() {
+ return codeOfConduct;
+ }
+
+ /**
+ * @return {@code true} if Geyser should not show the code of conduct to the player, because they have already accepted it
+ * @since 2.9.0
+ */
+ public boolean accepted() {
+ return hasAccepted;
+ }
+
+ /**
+ * Sets {@link ServerCodeOfConductEvent#accepted()} to {@code true}.
+ * @since 2.9.0
+ */
+ public void accept() {
+ this.hasAccepted = true;
+ }
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineEntityPropertiesEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineEntityPropertiesEvent.java
new file mode 100644
index 000000000..ef8380041
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineEntityPropertiesEvent.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (c) 2025 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.checkerframework.checker.nullness.qual.Nullable;
+import org.geysermc.event.Event;
+import org.geysermc.geyser.api.entity.EntityData;
+import org.geysermc.geyser.api.entity.property.GeyserEntityProperty;
+import org.geysermc.geyser.api.entity.property.type.GeyserBooleanEntityProperty;
+import org.geysermc.geyser.api.entity.property.type.GeyserEnumEntityProperty;
+import org.geysermc.geyser.api.entity.property.type.GeyserFloatEntityProperty;
+import org.geysermc.geyser.api.entity.property.type.GeyserIntEntityProperty;
+import org.geysermc.geyser.api.entity.property.type.GeyserStringEnumProperty;
+import org.geysermc.geyser.api.entity.type.GeyserEntity;
+import org.geysermc.geyser.api.util.Identifier;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * Lifecycle event fired during Geyser's startup to allow custom entity properties
+ * to be registered for a specific entity type.
+ *
+ * Listeners can add new properties for any entity by passing the target entity's
+ * identifier (e.g., {@code Identifier.of("player")}) to the registration methods.
+ * The returned {@link GeyserEntityProperty} is used to identify the properties and to
+ * update the value of a specific entity instance.
+ *
+ *
Example usage
+ * {@code
+ * public void onDefine(GeyserDefineEntityPropertiesEvent event) {
+ * Identifier player = Identifier.of("player");
+ * GeyserFloatEntityProperty ANIMATION_SPEED =
+ * event.registerFloatProperty(player, Identifier.of("my_group:animation_speed"), 0.0f, 1.0f, 0.1f);
+ * GeyserBooleanEntityProperty SHOW_SHORTS =
+ * event.registerBooleanProperty(player, Identifier.of("my_group:show_shorts"), false);
+ * }
+ * }
+ *
+ * Retrieving entity instances is possible with the {@link EntityData#entityByJavaId(int)} method, or
+ * {@link EntityData#playerEntity()} for the connection player entity.
+ * To update the value of a property on a specific entity, use {@link GeyserEntity#updateProperty(GeyserEntityProperty, Object)},
+ * or {@link GeyserEntity#updatePropertiesBatched(Consumer)} to update multiple properties efficiently at once.
+ *
+ * Notes:
+ *
+ * - Default values must fall within the provided bounds.
+ * - There cannot be more than 32 properties registered per entity type in total
+ * - {@link #properties(Identifier)} returns properties registered for the given entity
+ * (including those added earlier in the same callback), including vanilla properties.
+ *
+ *
+ * @since 2.9.0
+ */
+public interface GeyserDefineEntityPropertiesEvent extends Event {
+
+ /**
+ * Returns an unmodifiable view of all properties that have been registered
+ * so far for the given entity type. This includes entity properties used for vanilla gameplay,
+ * such as those used for creaking animations.
+ *
+ * @param entityType the Java edition entity type identifier
+ * @return an unmodifiable collection of registered properties
+ *
+ * @since 2.9.0
+ */
+ Collection> properties(@NonNull Identifier entityType);
+
+ /**
+ * Registers a {@code float}-backed entity property.
+ *
+ * @param entityType the Java edition entity type identifier
+ * @param propertyIdentifier the unique property identifier
+ * @param min the minimum allowed value (inclusive)
+ * @param max the maximum allowed value (inclusive)
+ * @param defaultValue the default value assigned initially on entity spawn - if null, it will be the minimum value
+ * @return the created float property
+ *
+ * @since 2.9.0
+ */
+ GeyserFloatEntityProperty registerFloatProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, float min, float max, @Nullable Float defaultValue);
+
+ /**
+ * Registers a {@code float}-backed entity property with a default value set to the minimum value.
+ * @see #registerFloatProperty(Identifier, Identifier, float, float, Float)
+ *
+ * @param entityType the Java edition entity type identifier
+ * @param propertyIdentifier the unique property identifier
+ * @param min the minimum allowed value (inclusive)
+ * @param max the maximum allowed value (inclusive)
+ * @return the created float property
+ *
+ * @since 2.9.0
+ */
+ default GeyserFloatEntityProperty registerFloatProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, float min, float max) {
+ return registerFloatProperty(entityType, propertyIdentifier, min, max, null);
+ }
+
+ /**
+ * Registers an {@code int}-backed entity property.
+ *
+ * @param entityType the Java edition entity type identifier
+ * @param propertyIdentifier the unique property identifier
+ * @param min the minimum allowed value (inclusive)
+ * @param max the maximum allowed value (inclusive)
+ * @param defaultValue the default value assigned initially on entity spawn - if null, it will be the minimum value
+ * @return the created int property
+ *
+ * @since 2.9.0
+ */
+ GeyserIntEntityProperty registerIntegerProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, int min, int max, @Nullable Integer defaultValue);
+
+ /**
+ * Registers an {@code int}-backed entity property with a default value set to the minimum value.
+ *
+ * @param entityType the Java edition entity type identifier
+ * @param propertyIdentifier the unique property identifier
+ * @param min the minimum allowed value (inclusive)
+ * @param max the maximum allowed value (inclusive)
+ * @return the created int property
+ *
+ * @since 2.9.0
+ */
+ default GeyserIntEntityProperty registerIntegerProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, int min, int max) {
+ return registerIntegerProperty(entityType, propertyIdentifier, min, max, null);
+ }
+
+ /**
+ * Registers a {@code boolean}-backed entity property.
+ *
+ * @param entityType the Java edition entity type identifier
+ * @param propertyIdentifier the unique property identifier
+ * @param defaultValue the default boolean value
+ * @return the created boolean property handle
+ *
+ * @since 2.9.0
+ */
+ GeyserBooleanEntityProperty registerBooleanProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, boolean defaultValue);
+
+ /**
+ * Registers a {@code boolean}-backed entity property with a default of {@code false}.
+ * @see #registerBooleanProperty(Identifier, Identifier, boolean)
+ *
+ * @param entityType the Java edition entity type identifier
+ * @param propertyIdentifier the unique property identifier
+ * @return the created boolean property
+ * @since 2.9.0
+ */
+ default GeyserBooleanEntityProperty registerBooleanProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier) {
+ return registerBooleanProperty(entityType, propertyIdentifier, false);
+ }
+
+ /**
+ * Registers a typed {@linkplain Enum enum}-backed entity property.
+ *
+ * The enum constants define the allowed values. If {@code defaultValue} is {@code null},
+ * the first enum value is set as the default.
+ * @see GeyserEnumEntityProperty for further limitations
+ *
+ * @param entityType the Java edition entity type identifier
+ * @param propertyIdentifier the unique property identifier
+ * @param enumClass the enum class that defines allowed values
+ * @param defaultValue the default enum value, or {@code null} for the first enum value to be the default
+ * @param the enum type
+ * @return the created enum property
+ *
+ * @since 2.9.0
+ */
+ > GeyserEnumEntityProperty registerEnumProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, @NonNull Class enumClass, @Nullable E defaultValue);
+
+ /**
+ * Registers a typed {@linkplain Enum enum}-backed entity property with the first value set as the default.
+ * @see #registerEnumProperty(Identifier, Identifier, Class, Enum)
+ *
+ * @param entityType the Java edition entity type identifier
+ * @param propertyIdentifier the unique property identifier
+ * @param enumClass the enum class that defines allowed values
+ * @param the enum type
+ * @return the created enum property
+ *
+ * @since 2.9.0
+ */
+ default > GeyserEnumEntityProperty registerEnumProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, @NonNull Class enumClass) {
+ return registerEnumProperty(entityType, propertyIdentifier, enumClass, null);
+ }
+
+ /**
+ * Registers a string-backed "enum-like" entity property where the set of allowed values
+ * is defined by the provided list. If {@code defaultValue} is {@code null}, the first value is used as the default
+ * on entity spawn. The default must be one of the values in {@code values}.
+ * @see GeyserStringEnumProperty
+ *
+ * @param entityType the Java edition entity type identifier
+ * @param propertyIdentifier the unique property identifier
+ * @param values the allowed string values
+ * @param defaultValue the default string value, or {@code null} for the first value to be used
+ * @return the created string-enum property
+ *
+ * @since 2.9.0
+ */
+ GeyserStringEnumProperty registerEnumProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, @NonNull List values, @Nullable String defaultValue);
+
+ /**
+ * Registers a string-backed "enum-like" entity property with the first value as the default.
+ * @see #registerEnumProperty(Identifier, Identifier, List, String)
+ *
+ * @param entityType the Java edition entity type identifier
+ * @param propertyIdentifier the unique property identifier
+ * @param values the allowed string values
+ * @return the created string-enum property handle
+ *
+ * @since 2.9.0
+ */
+ default GeyserStringEnumProperty registerEnumProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, @NonNull List values) {
+ return registerEnumProperty(entityType, propertyIdentifier, values, null);
+ }
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/util/Identifier.java b/api/src/main/java/org/geysermc/geyser/api/util/Identifier.java
new file mode 100644
index 000000000..e82a695d1
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/util/Identifier.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2024-2025 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;
+import org.geysermc.geyser.api.GeyserApi;
+
+/**
+ * An identifying object for representing unique objects.
+ * This identifier consists of two parts:
+ *
+ * -
+ * a namespace, which is usually a name identifying your work
+ *
+ * -
+ * a path, which holds a value.
+ *
+ *
+ *
+ * Examples of identifiers:
+ *
+ * - {@code minecraft:fox}
+ * - {@code geysermc:one_fun_example}
+ *
+ *
+ * If this identifier is referencing anything not in the
+ * vanilla Minecraft game, the namespace cannot be "minecraft".
+ * Further, paths cannot contain colons ({@code :}).
+ *
+ * @since 2.9.0
+ */
+public interface Identifier {
+
+ /**
+ * The namespace for Minecraft.
+ * @since 2.9.0
+ */
+ String DEFAULT_NAMESPACE = "minecraft";
+
+ /**
+ * Attempts to create a new identifier from a namespace and path.
+ *
+ * @return the identifier for this namespace and path
+ * @throws IllegalArgumentException if either namespace or path are invalid.
+ * @since 2.9.0
+ */
+ static Identifier of(@NonNull String namespace, @NonNull String path) {
+ return GeyserApi.api().provider(Identifier.class, namespace, path);
+ }
+
+ /**
+ * Attempts to create a new identifier from a string representation.
+ *
+ * @return the identifier for this namespace and path
+ * @throws IllegalArgumentException if either the namespace or path are invalid
+ * @since 2.9.0
+ */
+ static Identifier of(String identifier) {
+ String[] split = identifier.split(":");
+ String namespace;
+ String path;
+ if (split.length == 1) {
+ namespace = DEFAULT_NAMESPACE;
+ path = split[0];
+ } else if (split.length == 2) {
+ namespace = split[0];
+ path = split[1];
+ } else {
+ throw new IllegalArgumentException("':' in identifier path: " + identifier);
+ }
+ return of(namespace, path);
+ }
+
+ /**
+ * @return the namespace of this identifier.
+ * @since 2.9.0
+ */
+ String namespace();
+
+ /**
+ * @return the path of this identifier.
+ * @since 2.9.0
+ */
+ String path();
+
+ /**
+ * Checks whether this identifier is using the "minecraft" namespace.
+ * @since 2.9.0
+ */
+ default boolean vanilla() {
+ return namespace().equals(DEFAULT_NAMESPACE);
+ }
+}
diff --git a/bootstrap/mod/fabric/build.gradle.kts b/bootstrap/mod/fabric/build.gradle.kts
index ebe727253..5b66e0f37 100644
--- a/bootstrap/mod/fabric/build.gradle.kts
+++ b/bootstrap/mod/fabric/build.gradle.kts
@@ -8,8 +8,6 @@ architectury {
fabric()
}
-val includeTransitive: Configuration = configurations.getByName("includeTransitive")
-
dependencies {
modImplementation(libs.fabric.loader)
modApi(libs.fabric.api)
diff --git a/bootstrap/mod/fabric/src/main/resources/fabric.mod.json b/bootstrap/mod/fabric/src/main/resources/fabric.mod.json
index 69071cea4..b042bb252 100644
--- a/bootstrap/mod/fabric/src/main/resources/fabric.mod.json
+++ b/bootstrap/mod/fabric/src/main/resources/fabric.mod.json
@@ -23,8 +23,8 @@
"geyser.mixins.json"
],
"depends": {
- "fabricloader": ">=0.16.7",
+ "fabricloader": ">=0.17.2",
"fabric-api": "*",
- "minecraft": ">=1.21.6"
+ "minecraft": ">=1.21.9"
}
}
diff --git a/bootstrap/mod/neoforge/build.gradle.kts b/bootstrap/mod/neoforge/build.gradle.kts
index 2c58192fb..310e8c3f6 100644
--- a/bootstrap/mod/neoforge/build.gradle.kts
+++ b/bootstrap/mod/neoforge/build.gradle.kts
@@ -16,8 +16,6 @@ provided("com.google.errorprone", "error_prone_annotations")
// Jackson shipped by Minecraft is too old, so we shade & relocate our newer version
relocate("com.fasterxml.jackson")
-val includeTransitive: Configuration = configurations.getByName("includeTransitive")
-
dependencies {
// See https://github.com/google/guava/issues/6618
modules {
diff --git a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeBootstrap.java b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeBootstrap.java
index aa731befc..803a06b6e 100644
--- a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeBootstrap.java
+++ b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeBootstrap.java
@@ -111,7 +111,7 @@ public class GeyserNeoForgeBootstrap extends GeyserModBootstrap {
@Override
public boolean isServer() {
- return FMLLoader.getDist().isDedicatedServer();
+ return FMLLoader.getCurrent().getDist().isDedicatedServer();
}
private void onPermissionGather(PermissionGatherEvent.Nodes event) {
diff --git a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeDumpInfo.java b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeDumpInfo.java
index 623f68d3a..8348ca160 100644
--- a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeDumpInfo.java
+++ b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeDumpInfo.java
@@ -54,10 +54,10 @@ public class GeyserNeoForgeDumpInfo extends BootstrapDumpInfo {
private final List mods;
public GeyserNeoForgeDumpInfo(MinecraftServer server) {
- this.platformName = FMLLoader.launcherHandlerName();
- this.platformVersion = FMLLoader.versionInfo().neoForgeVersion();
- this.minecraftVersion = FMLLoader.versionInfo().mcVersion();
- this.dist = FMLLoader.getDist();
+ this.platformName = server.getServerModName();
+ this.platformVersion = FMLLoader.getCurrent().getVersionInfo().neoForgeVersion();
+ this.minecraftVersion = FMLLoader.getCurrent().getVersionInfo().mcVersion();
+ this.dist = FMLLoader.getCurrent().getDist();
this.serverIP = server.getLocalIp() == null ? "unknown" : server.getLocalIp();
this.serverPort = server.getPort();
this.onlineMode = server.usesAuthentication();
@@ -81,4 +81,4 @@ public class GeyserNeoForgeDumpInfo extends BootstrapDumpInfo {
public String version;
public String url;
}
-}
\ No newline at end of file
+}
diff --git a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgePlatform.java b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgePlatform.java
index 41562baf3..c1e0b67e0 100644
--- a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgePlatform.java
+++ b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgePlatform.java
@@ -38,7 +38,6 @@ import org.geysermc.geyser.platform.mod.platform.GeyserModPlatform;
import java.io.IOException;
import java.io.InputStream;
-import java.nio.file.Files;
import java.nio.file.Path;
public class GeyserNeoForgePlatform implements GeyserModPlatform {
@@ -82,8 +81,7 @@ public class GeyserNeoForgePlatform implements GeyserModPlatform {
@Override
public @Nullable InputStream resolveResource(@NonNull String resource) {
try {
- Path path = container.getModInfo().getOwningFile().getFile().findResource(resource);
- return Files.newInputStream(path);
+ return container.getModInfo().getOwningFile().getFile().getContents().openFile(resource);
} catch (IOException e) {
return null;
}
diff --git a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/PermissionUtils.java b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/PermissionUtils.java
index 5755e407f..6ed5bc608 100644
--- a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/PermissionUtils.java
+++ b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/PermissionUtils.java
@@ -71,7 +71,7 @@ public class PermissionUtils {
case FALSE -> false;
case NOT_SET -> {
if (player != null) {
- yield player.createCommandSourceStack().hasPermission(Objects.requireNonNull(player.getServer()).getOperatorUserPermissionLevel());
+ yield player.createCommandSourceStack().hasPermission(Objects.requireNonNull(player.level()).getServer().operatorUserPermissionLevel());
}
yield false; // NeoForge javadocs say player is null in the case of an offline player.
}
diff --git a/bootstrap/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/bootstrap/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml
index 3f1fe574d..7eb124724 100644
--- a/bootstrap/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml
+++ b/bootstrap/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml
@@ -16,12 +16,12 @@ config = "geyser_neoforge.mixins.json"
[[dependencies.geyser_neoforge]]
modId="neoforge"
type="required"
- versionRange="[21.6.0-beta,)"
+ versionRange="[21.9.14-beta,)"
ordering="NONE"
side="BOTH"
[[dependencies.geyser_neoforge]]
modId="minecraft"
type="required"
- versionRange="[1.21.6,)"
+ versionRange="[1.21.9,)"
ordering="NONE"
side="BOTH"
diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/DedicatedServerMixin.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/DedicatedServerMixin.java
index 3b809d321..84e3303c9 100644
--- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/DedicatedServerMixin.java
+++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/DedicatedServerMixin.java
@@ -30,7 +30,7 @@ import net.minecraft.server.MinecraftServer;
import net.minecraft.server.Services;
import net.minecraft.server.WorldStem;
import net.minecraft.server.dedicated.DedicatedServer;
-import net.minecraft.server.level.progress.ChunkProgressListenerFactory;
+import net.minecraft.server.level.progress.LevelLoadListener;
import net.minecraft.server.packs.repository.PackRepository;
import net.minecraft.world.level.storage.LevelStorageSource;
import org.geysermc.geyser.platform.mod.GeyserServerPortGetter;
@@ -40,8 +40,9 @@ import java.net.Proxy;
@Mixin(DedicatedServer.class)
public abstract class DedicatedServerMixin extends MinecraftServer implements GeyserServerPortGetter {
- public DedicatedServerMixin(Thread thread, LevelStorageSource.LevelStorageAccess levelStorageAccess, PackRepository packRepository, WorldStem worldStem, Proxy proxy, DataFixer dataFixer, Services services, ChunkProgressListenerFactory chunkProgressListenerFactory) {
- super(thread, levelStorageAccess, packRepository, worldStem, proxy, dataFixer, services, chunkProgressListenerFactory);
+
+ public DedicatedServerMixin(Thread thread, LevelStorageSource.LevelStorageAccess levelStorageAccess, PackRepository packRepository, WorldStem worldStem, Proxy proxy, DataFixer dataFixer, Services services, LevelLoadListener levelLoadListener) {
+ super(thread, levelStorageAccess, packRepository, worldStem, proxy, dataFixer, services, levelLoadListener);
}
@Override
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotCompressionDisabler.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotCompressionDisabler.java
index 2a6056df9..f0fa71ab2 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotCompressionDisabler.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotCompressionDisabler.java
@@ -96,19 +96,31 @@ public class GeyserSpigotCompressionDisabler extends ChannelOutboundHandlerAdapt
private static Class> findCompressionPacket() throws ClassNotFoundException {
try {
- return Class.forName("net.minecraft.network.protocol.login.PacketLoginOutSetCompression");
+ // Mojmaps
+ return Class.forName("net.minecraft.network.protocol.login.ClientboundLoginCompressionPacket");
} catch (ClassNotFoundException e) {
- String prefix = Bukkit.getServer().getClass().getPackage().getName().replace("org.bukkit.craftbukkit", "net.minecraft.server");
- return Class.forName(prefix + ".PacketLoginOutSetCompression");
+ try {
+ // Spigot mappings
+ return Class.forName("net.minecraft.network.protocol.login.PacketLoginOutSetCompression");
+ } catch (ClassNotFoundException ex) {
+ String prefix = Bukkit.getServer().getClass().getPackage().getName().replace("org.bukkit.craftbukkit", "net.minecraft.server");
+ return Class.forName(prefix + ".PacketLoginOutSetCompression");
+ }
}
}
private static Class> findLoginSuccessPacket() throws ClassNotFoundException {
try {
- return Class.forName("net.minecraft.network.protocol.login.PacketLoginOutSuccess");
+ // Mojmaps
+ return Class.forName("net.minecraft.network.protocol.login.ClientboundLoginFinishedPacket");
} catch (ClassNotFoundException e) {
- String prefix = Bukkit.getServer().getClass().getPackage().getName().replace("org.bukkit.craftbukkit", "net.minecraft.server");
- return Class.forName(prefix + ".PacketLoginOutSuccess");
+ try {
+ // Spigot mappings
+ return Class.forName("net.minecraft.network.protocol.login.PacketLoginOutSuccess");
+ } catch (ClassNotFoundException ex) {
+ String prefix = Bukkit.getServer().getClass().getPackage().getName().replace("org.bukkit.craftbukkit", "net.minecraft.server");
+ return Class.forName(prefix + ".PacketLoginOutSuccess");
+ }
}
}
}
diff --git a/build-logic/src/main/kotlin/geyser.modded-conventions.gradle.kts b/build-logic/src/main/kotlin/geyser.modded-conventions.gradle.kts
index b157c0e53..903d82258 100644
--- a/build-logic/src/main/kotlin/geyser.modded-conventions.gradle.kts
+++ b/build-logic/src/main/kotlin/geyser.modded-conventions.gradle.kts
@@ -96,8 +96,8 @@ tasks {
afterEvaluate {
val providedDependencies = providedDependencies[project.name]!!
- val shadedDependencies = configurations.getByName("shadowBundle")
- .dependencies.stream().map { dependency -> "${dependency.group}:${dependency.name}" }.toList()
+ val shadedDependencies = configurations.getByName("shadowBundle").resolvedConfiguration.resolvedArtifacts.stream()
+ .map { dependency -> "${dependency.moduleVersion.id.module}" }.toList()
// Now: Include all transitive dependencies that aren't excluded
configurations["includeTransitive"].resolvedConfiguration.resolvedArtifacts.forEach { dep ->
diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
index 58068425e..4908c0614 100644
--- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
+++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
@@ -94,6 +94,7 @@ import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.text.MinecraftLocale;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.geyser.util.AssetUtils;
+import org.geysermc.geyser.util.CodeOfConductManager;
import org.geysermc.geyser.util.CooldownUtils;
import org.geysermc.geyser.util.Metrics;
import org.geysermc.geyser.util.NewsHandler;
@@ -296,7 +297,10 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
if (isReloading) {
// If we're reloading, the default locale in the config might have changed.
GeyserLocale.finalizeDefaultLocale(this);
+ } else {
+ CodeOfConductManager.load();
}
+
GeyserLogger logger = bootstrap.getGeyserLogger();
GeyserConfiguration config = bootstrap.getGeyserConfig();
@@ -683,6 +687,7 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
runIfNonNull(erosionUnixListener, UnixSocketClientListener::close);
ResourcePackLoader.clear();
+ CodeOfConductManager.getInstance().save();
this.setEnabled(false);
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java
index d26a25d2c..1cc529a07 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java
@@ -31,6 +31,7 @@ import lombok.experimental.Accessors;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.entity.factory.EntityFactory;
import org.geysermc.geyser.entity.properties.GeyserEntityProperties;
+import org.geysermc.geyser.entity.properties.type.PropertyType;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.translator.entity.EntityMetadataTranslator;
@@ -54,7 +55,7 @@ public record EntityDefinition(EntityFactory factory, Entit
float width, float height, float offset, GeyserEntityProperties registeredProperties, List> translators) {
public static Builder inherited(EntityFactory factory, EntityDefinition super T> parent) {
- return new Builder<>(factory, parent.entityType, parent.identifier, parent.width, parent.height, parent.offset, parent.registeredProperties, new ObjectArrayList<>(parent.translators));
+ return new Builder<>(factory, parent.entityType, parent.identifier, parent.width, parent.height, parent.offset, new ObjectArrayList<>(parent.translators));
}
public static Builder builder(EntityFactory factory) {
@@ -89,7 +90,7 @@ public record EntityDefinition(EntityFactory factory, Entit
private float width;
private float height;
private float offset = 0.00001f;
- private GeyserEntityProperties registeredProperties;
+ private GeyserEntityProperties.Builder propertiesBuilder;
private final List> translators;
private Builder(EntityFactory factory) {
@@ -97,14 +98,13 @@ public record EntityDefinition(EntityFactory factory, Entit
translators = new ObjectArrayList<>();
}
- public Builder(EntityFactory factory, EntityType type, String identifier, float width, float height, float offset, GeyserEntityProperties registeredProperties, List> translators) {
+ public Builder(EntityFactory factory, EntityType type, String identifier, float width, float height, float offset, List> translators) {
this.factory = factory;
this.type = type;
this.identifier = identifier;
this.width = width;
this.height = height;
this.offset = offset;
- this.registeredProperties = registeredProperties;
this.translators = translators;
}
@@ -131,8 +131,11 @@ public record EntityDefinition(EntityFactory factory, Entit
return this;
}
- public Builder properties(GeyserEntityProperties registeredProperties) {
- this.registeredProperties = registeredProperties;
+ public Builder property(PropertyType, ?> propertyType) {
+ if (this.propertiesBuilder == null) {
+ this.propertiesBuilder = new GeyserEntityProperties.Builder(this.identifier);
+ }
+ propertiesBuilder.add(propertyType);
return this;
}
@@ -158,13 +161,11 @@ public record EntityDefinition(EntityFactory factory, Entit
if (identifier == null && type != null) {
identifier = "minecraft:" + type.name().toLowerCase(Locale.ROOT);
}
+ GeyserEntityProperties registeredProperties = propertiesBuilder == null ? null : propertiesBuilder.build();
EntityDefinition definition = new EntityDefinition<>(factory, type, identifier, width, height, offset, registeredProperties, translators);
if (register && definition.entityType() != null) {
Registries.ENTITY_DEFINITIONS.get().putIfAbsent(definition.entityType(), definition);
Registries.JAVA_ENTITY_IDENTIFIERS.get().putIfAbsent("minecraft:" + type.name().toLowerCase(Locale.ROOT), definition);
- if (definition.registeredProperties() != null) {
- Registries.BEDROCK_ENTITY_PROPERTIES.get().add(definition.registeredProperties().toNbtMap(identifier));
- }
}
return definition;
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java
index 960369de7..fbe8b7883 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java
@@ -25,10 +25,23 @@
package org.geysermc.geyser.entity;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
+import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.api.entity.property.GeyserEntityProperty;
+import org.geysermc.geyser.api.entity.property.type.GeyserFloatEntityProperty;
+import org.geysermc.geyser.api.entity.property.type.GeyserStringEnumProperty;
+import org.geysermc.geyser.api.event.lifecycle.GeyserDefineEntityPropertiesEvent;
+import org.geysermc.geyser.api.util.Identifier;
import org.geysermc.geyser.entity.factory.EntityFactory;
-import org.geysermc.geyser.entity.properties.VanillaEntityProperties;
+import org.geysermc.geyser.entity.properties.type.BooleanProperty;
+import org.geysermc.geyser.entity.properties.type.EnumProperty;
+import org.geysermc.geyser.entity.properties.type.FloatProperty;
+import org.geysermc.geyser.entity.properties.type.IntProperty;
+import org.geysermc.geyser.entity.properties.type.PropertyType;
+import org.geysermc.geyser.entity.properties.type.StringEnumProperty;
import org.geysermc.geyser.entity.type.AbstractArrowEntity;
import org.geysermc.geyser.entity.type.AbstractWindChargeEntity;
import org.geysermc.geyser.entity.type.AreaEffectCloudEntity;
@@ -37,8 +50,6 @@ import org.geysermc.geyser.entity.type.BoatEntity;
import org.geysermc.geyser.entity.type.ChestBoatEntity;
import org.geysermc.geyser.entity.type.CommandBlockMinecartEntity;
import org.geysermc.geyser.entity.type.DisplayBaseEntity;
-import org.geysermc.geyser.entity.type.HangingEntity;
-import org.geysermc.geyser.entity.type.ThrowableEggEntity;
import org.geysermc.geyser.entity.type.EnderCrystalEntity;
import org.geysermc.geyser.entity.type.EnderEyeEntity;
import org.geysermc.geyser.entity.type.Entity;
@@ -49,6 +60,7 @@ import org.geysermc.geyser.entity.type.FireballEntity;
import org.geysermc.geyser.entity.type.FireworkEntity;
import org.geysermc.geyser.entity.type.FishingHookEntity;
import org.geysermc.geyser.entity.type.FurnaceMinecartEntity;
+import org.geysermc.geyser.entity.type.HangingEntity;
import org.geysermc.geyser.entity.type.InteractionEntity;
import org.geysermc.geyser.entity.type.ItemEntity;
import org.geysermc.geyser.entity.type.ItemFrameEntity;
@@ -60,6 +72,7 @@ import org.geysermc.geyser.entity.type.PaintingEntity;
import org.geysermc.geyser.entity.type.SpawnerMinecartEntity;
import org.geysermc.geyser.entity.type.TNTEntity;
import org.geysermc.geyser.entity.type.TextDisplayEntity;
+import org.geysermc.geyser.entity.type.ThrowableEggEntity;
import org.geysermc.geyser.entity.type.ThrowableEntity;
import org.geysermc.geyser.entity.type.ThrowableItemEntity;
import org.geysermc.geyser.entity.type.ThrownPotionEntity;
@@ -70,6 +83,7 @@ import org.geysermc.geyser.entity.type.living.AgeableEntity;
import org.geysermc.geyser.entity.type.living.AllayEntity;
import org.geysermc.geyser.entity.type.living.ArmorStandEntity;
import org.geysermc.geyser.entity.type.living.BatEntity;
+import org.geysermc.geyser.entity.type.living.CopperGolemEntity;
import org.geysermc.geyser.entity.type.living.DolphinEntity;
import org.geysermc.geyser.entity.type.living.GlowSquidEntity;
import org.geysermc.geyser.entity.type.living.IronGolemEntity;
@@ -82,17 +96,14 @@ import org.geysermc.geyser.entity.type.living.TadpoleEntity;
import org.geysermc.geyser.entity.type.living.animal.ArmadilloEntity;
import org.geysermc.geyser.entity.type.living.animal.AxolotlEntity;
import org.geysermc.geyser.entity.type.living.animal.BeeEntity;
-import org.geysermc.geyser.entity.type.living.animal.HappyGhastEntity;
-import org.geysermc.geyser.entity.type.living.animal.farm.ChickenEntity;
-import org.geysermc.geyser.entity.type.living.animal.farm.CowEntity;
import org.geysermc.geyser.entity.type.living.animal.FoxEntity;
import org.geysermc.geyser.entity.type.living.animal.FrogEntity;
import org.geysermc.geyser.entity.type.living.animal.GoatEntity;
+import org.geysermc.geyser.entity.type.living.animal.HappyGhastEntity;
import org.geysermc.geyser.entity.type.living.animal.HoglinEntity;
import org.geysermc.geyser.entity.type.living.animal.MooshroomEntity;
import org.geysermc.geyser.entity.type.living.animal.OcelotEntity;
import org.geysermc.geyser.entity.type.living.animal.PandaEntity;
-import org.geysermc.geyser.entity.type.living.animal.farm.PigEntity;
import org.geysermc.geyser.entity.type.living.animal.PolarBearEntity;
import org.geysermc.geyser.entity.type.living.animal.PufferFishEntity;
import org.geysermc.geyser.entity.type.living.animal.RabbitEntity;
@@ -101,6 +112,10 @@ import org.geysermc.geyser.entity.type.living.animal.SnifferEntity;
import org.geysermc.geyser.entity.type.living.animal.StriderEntity;
import org.geysermc.geyser.entity.type.living.animal.TropicalFishEntity;
import org.geysermc.geyser.entity.type.living.animal.TurtleEntity;
+import org.geysermc.geyser.entity.type.living.animal.farm.ChickenEntity;
+import org.geysermc.geyser.entity.type.living.animal.farm.CowEntity;
+import org.geysermc.geyser.entity.type.living.animal.farm.PigEntity;
+import org.geysermc.geyser.entity.type.living.animal.farm.TemperatureVariantAnimal;
import org.geysermc.geyser.entity.type.living.animal.horse.AbstractHorseEntity;
import org.geysermc.geyser.entity.type.living.animal.horse.CamelEntity;
import org.geysermc.geyser.entity.type.living.animal.horse.ChestedHorseEntity;
@@ -147,6 +162,8 @@ import org.geysermc.geyser.entity.type.living.monster.raid.RaidParticipantEntity
import org.geysermc.geyser.entity.type.living.monster.raid.RavagerEntity;
import org.geysermc.geyser.entity.type.living.monster.raid.SpellcasterIllagerEntity;
import org.geysermc.geyser.entity.type.living.monster.raid.VindicatorEntity;
+import org.geysermc.geyser.entity.type.player.AvatarEntity;
+import org.geysermc.geyser.entity.type.player.MannequinEntity;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.translator.text.MessageTranslator;
@@ -155,6 +172,10 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.Boolea
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+
public final class EntityDefinitions {
public static final EntityDefinition ACACIA_BOAT;
public static final EntityDefinition ACACIA_CHEST_BOAT;
@@ -182,6 +203,7 @@ public final class EntityDefinitions {
public static final EntityDefinition CHEST_MINECART;
public static final EntityDefinition CHICKEN;
public static final EntityDefinition COD;
+ public static final EntityDefinition COPPER_GOLEM;
public static final EntityDefinition COMMAND_BLOCK_MINECART;
public static final EntityDefinition COW;
public static final EntityDefinition CREAKING;
@@ -236,6 +258,7 @@ public final class EntityDefinitions {
public static final EntityDefinition MAGMA_CUBE;
public static final EntityDefinition MANGROVE_BOAT;
public static final EntityDefinition MANGROVE_CHEST_BOAT;
+ public static final EntityDefinition MANNEQUIN;
public static final EntityDefinition MINECART;
public static final EntityDefinition MOOSHROOM;
public static final EntityDefinition MULE;
@@ -464,7 +487,7 @@ public final class EntityDefinitions {
EGG = EntityDefinition.inherited(ThrowableEggEntity::new, throwableItemBase)
.type(EntityType.EGG)
.heightAndWidth(0.25f)
- .properties(VanillaEntityProperties.CLIMATE_VARIANT)
+ .property(TemperatureVariantAnimal.TEMPERATE_VARIANT_PROPERTY)
.build();
ENDER_PEARL = EntityDefinition.inherited(ThrowableItemEntity::new, throwableItemBase)
.type(EntityType.ENDER_PEARL)
@@ -651,16 +674,27 @@ public final class EntityDefinitions {
.addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setLeftLegRotation)
.addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setRightLegRotation)
.build();
- PLAYER = EntityDefinition.inherited(null, livingEntityBase)
+
+ EntityDefinition avatarEntityBase = EntityDefinition.inherited(null, livingEntityBase)
+ .height(1.8f).width(0.6f)
+ .offset(1.62f)
+ .addTranslator(null) // Player main hand
+ .addTranslator(MetadataTypes.BYTE, AvatarEntity::setSkinVisibility)
+ .build();
+
+ MANNEQUIN = EntityDefinition.inherited(MannequinEntity::new, avatarEntityBase)
+ .type(EntityType.MANNEQUIN)
+ .addTranslator(MetadataTypes.RESOLVABLE_PROFILE, MannequinEntity::setProfile)
+ .addTranslator(null) // Immovable
+ .addTranslator(MetadataTypes.OPTIONAL_COMPONENT, MannequinEntity::setDescription)
+ .build();
+
+ PLAYER = EntityDefinition.inherited(null, avatarEntityBase)
.type(EntityType.PLAYER)
- .height(1.8f).width(0.6f)
- .offset(1.62f)
.addTranslator(MetadataTypes.FLOAT, PlayerEntity::setAbsorptionHearts)
.addTranslator(null) // Player score
- .addTranslator(MetadataTypes.BYTE, PlayerEntity::setSkinVisibility)
- .addTranslator(null) // Player main hand
- .addTranslator(MetadataTypes.COMPOUND_TAG, PlayerEntity::setLeftParrot)
- .addTranslator(MetadataTypes.COMPOUND_TAG, PlayerEntity::setRightParrot)
+ .addTranslator(MetadataTypes.OPTIONAL_UNSIGNED_INT, PlayerEntity::setLeftParrot)
+ .addTranslator(MetadataTypes.OPTIONAL_UNSIGNED_INT, PlayerEntity::setRightParrot)
.build();
EntityDefinition mobEntityBase = EntityDefinition.inherited(MobEntity::new, livingEntityBase)
@@ -694,6 +728,15 @@ public final class EntityDefinitions {
.type(EntityType.BREEZE)
.height(1.77f).width(0.6f)
.build();
+ COPPER_GOLEM = EntityDefinition.inherited(CopperGolemEntity::new, mobEntityBase)
+ .type(EntityType.COPPER_GOLEM)
+ .height(0.49f).width(0.98f)
+ .addTranslator(MetadataTypes.WEATHERING_COPPER_STATE, CopperGolemEntity::setWeatheringState)
+ .addTranslator(MetadataTypes.COPPER_GOLEM_STATE, CopperGolemEntity::setGolemState)
+ .property(CopperGolemEntity.CHEST_INTERACTION_PROPERTY)
+ .property(CopperGolemEntity.HAS_FLOWER_PROPERTY)
+ .property(CopperGolemEntity.OXIDATION_LEVEL_STATE_ENUM_PROPERTY)
+ .build();
CREAKING = EntityDefinition.inherited(CreakingEntity::new, mobEntityBase)
.type(EntityType.CREAKING)
.height(2.7f).width(0.9f)
@@ -701,7 +744,8 @@ public final class EntityDefinitions {
.addTranslator(MetadataTypes.BOOLEAN, CreakingEntity::setActive)
.addTranslator(MetadataTypes.BOOLEAN, CreakingEntity::setIsTearingDown)
.addTranslator(MetadataTypes.OPTIONAL_BLOCK_POS, CreakingEntity::setHomePos)
- .properties(VanillaEntityProperties.CREAKING)
+ .property(CreakingEntity.STATE_PROPERTY)
+ .property(CreakingEntity.SWAYING_TICKS_PROPERTY)
.build();
CREEPER = EntityDefinition.inherited(CreeperEntity::new, mobEntityBase)
.type(EntityType.CREEPER)
@@ -954,7 +998,7 @@ public final class EntityDefinitions {
ARMADILLO = EntityDefinition.inherited(ArmadilloEntity::new, ageableEntityBase)
.type(EntityType.ARMADILLO)
.height(0.65f).width(0.7f)
- .properties(VanillaEntityProperties.ARMADILLO)
+ .property(ArmadilloEntity.STATE_PROPERTY)
.addTranslator(MetadataTypes.ARMADILLO_STATE, ArmadilloEntity::setArmadilloState)
.build();
AXOLOTL = EntityDefinition.inherited(AxolotlEntity::new, ageableEntityBase)
@@ -967,20 +1011,20 @@ public final class EntityDefinitions {
BEE = EntityDefinition.inherited(BeeEntity::new, ageableEntityBase)
.type(EntityType.BEE)
.heightAndWidth(0.6f)
- .properties(VanillaEntityProperties.BEE)
+ .property(BeeEntity.NECTAR_PROPERTY)
.addTranslator(MetadataTypes.BYTE, BeeEntity::setBeeFlags)
.addTranslator(MetadataTypes.INT, BeeEntity::setAngerTime)
.build();
CHICKEN = EntityDefinition.inherited(ChickenEntity::new, ageableEntityBase)
.type(EntityType.CHICKEN)
.height(0.7f).width(0.4f)
- .properties(VanillaEntityProperties.CLIMATE_VARIANT)
+ .property(TemperatureVariantAnimal.TEMPERATE_VARIANT_PROPERTY)
.addTranslator(MetadataTypes.CHICKEN_VARIANT, ChickenEntity::setVariant)
.build();
COW = EntityDefinition.inherited(CowEntity::new, ageableEntityBase)
.type(EntityType.COW)
.height(1.4f).width(0.9f)
- .properties(VanillaEntityProperties.CLIMATE_VARIANT)
+ .property(TemperatureVariantAnimal.TEMPERATE_VARIANT_PROPERTY)
.addTranslator(MetadataTypes.COW_VARIANT, CowEntity::setVariant)
.build();
FOX = EntityDefinition.inherited(FoxEntity::new, ageableEntityBase)
@@ -1000,7 +1044,7 @@ public final class EntityDefinitions {
HAPPY_GHAST = EntityDefinition.inherited(HappyGhastEntity::new, ageableEntityBase)
.type(EntityType.HAPPY_GHAST)
.heightAndWidth(4f)
- .properties(VanillaEntityProperties.HAPPY_GHAST)
+ .property(HappyGhastEntity.CAN_MOVE_PROPERTY)
.addTranslator(null) // Is leash holder
.addTranslator(MetadataTypes.BOOLEAN, HappyGhastEntity::setStaysStill)
.build();
@@ -1039,7 +1083,7 @@ public final class EntityDefinitions {
PIG = EntityDefinition.inherited(PigEntity::new, ageableEntityBase)
.type(EntityType.PIG)
.heightAndWidth(0.9f)
- .properties(VanillaEntityProperties.CLIMATE_VARIANT)
+ .property(TemperatureVariantAnimal.TEMPERATE_VARIANT_PROPERTY)
.addTranslator(MetadataTypes.INT, PigEntity::setBoost)
.addTranslator(MetadataTypes.PIG_VARIANT, PigEntity::setVariant)
.build();
@@ -1185,7 +1229,7 @@ public final class EntityDefinitions {
WOLF = EntityDefinition.inherited(WolfEntity::new, tameableEntityBase)
.type(EntityType.WOLF)
.height(0.85f).width(0.6f)
- .properties(VanillaEntityProperties.WOLF_SOUND_VARIANT)
+ .property(WolfEntity.SOUND_VARIANT)
// "Begging" on wiki.vg, "Interested" in Nukkit - the tilt of the head
.addTranslator(MetadataTypes.BOOLEAN, (wolfEntity, entityMetadata) -> wolfEntity.setFlag(EntityFlag.INTERESTED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue()))
.addTranslator(MetadataTypes.INT, WolfEntity::setCollarColor)
@@ -1219,7 +1263,95 @@ public final class EntityDefinitions {
}
public static void init() {
- // no-op
+ // entities would be initialized before this event is called
+ GeyserImpl.getInstance().getEventBus().fire(new GeyserDefineEntityPropertiesEvent() {
+ @Override
+ public GeyserFloatEntityProperty registerFloatProperty(@NonNull Identifier identifier, @NonNull Identifier propertyId, float min, float max, @Nullable Float defaultValue) {
+ Objects.requireNonNull(identifier);
+ Objects.requireNonNull(propertyId);
+ if (propertyId.vanilla()) {
+ throw new IllegalArgumentException("Cannot register custom property in vanilla namespace! " + propertyId);
+ }
+ FloatProperty property = new FloatProperty(propertyId, min, max, defaultValue);
+ registerProperty(identifier, property);
+ return property;
+ }
+
+ @Override
+ public IntProperty registerIntegerProperty(@NonNull Identifier identifier, @NonNull Identifier propertyId, int min, int max, @Nullable Integer defaultValue) {
+ Objects.requireNonNull(identifier);
+ Objects.requireNonNull(propertyId);
+ if (propertyId.vanilla()) {
+ throw new IllegalArgumentException("Cannot register custom property in vanilla namespace! " + propertyId);
+ }
+ IntProperty property = new IntProperty(propertyId, min, max, defaultValue);
+ registerProperty(identifier, property);
+ return property;
+ }
+
+ @Override
+ public BooleanProperty registerBooleanProperty(@NonNull Identifier identifier, @NonNull Identifier propertyId, boolean defaultValue) {
+ Objects.requireNonNull(identifier);
+ Objects.requireNonNull(propertyId);
+ if (propertyId.vanilla()) {
+ throw new IllegalArgumentException("Cannot register custom property in vanilla namespace! " + propertyId);
+ }
+ BooleanProperty property = new BooleanProperty(propertyId, defaultValue);
+ registerProperty(identifier, property);
+ return property;
+ }
+
+ @Override
+ public > EnumProperty registerEnumProperty(@NonNull Identifier identifier, @NonNull Identifier propertyId, @NonNull Class enumClass, @Nullable E defaultValue) {
+ Objects.requireNonNull(identifier);
+ Objects.requireNonNull(propertyId);
+ Objects.requireNonNull(enumClass);
+ if (propertyId.vanilla()) {
+ throw new IllegalArgumentException("Cannot register custom property in vanilla namespace! " + propertyId);
+ }
+ EnumProperty property = new EnumProperty<>(propertyId, enumClass, defaultValue == null ? enumClass.getEnumConstants()[0] : defaultValue);
+ registerProperty(identifier, property);
+ return property;
+ }
+
+ @Override
+ public GeyserStringEnumProperty registerEnumProperty(@NonNull Identifier identifier, @NonNull Identifier propertyId, @NonNull List values, @Nullable String defaultValue) {
+ Objects.requireNonNull(identifier);
+ Objects.requireNonNull(propertyId);
+ Objects.requireNonNull(values);
+ if (propertyId.vanilla()) {
+ throw new IllegalArgumentException("Cannot register custom property in vanilla namespace! " + propertyId);
+ }
+ StringEnumProperty property = new StringEnumProperty(propertyId, values, defaultValue);
+ registerProperty(identifier, property);
+ return property;
+ }
+
+ @Override
+ public Collection> properties(@NonNull Identifier identifier) {
+ Objects.requireNonNull(identifier);
+ var definition = Registries.JAVA_ENTITY_IDENTIFIERS.get(identifier.toString());
+ if (definition == null) {
+ throw new IllegalArgumentException("Unknown entity type: " + identifier);
+ }
+ return List.copyOf(definition.registeredProperties().getProperties());
+ }
+ });
+
+ for (var definition : Registries.ENTITY_DEFINITIONS.get().values()) {
+ if (definition.registeredProperties() != null) {
+ Registries.BEDROCK_ENTITY_PROPERTIES.get().add(definition.registeredProperties().toNbtMap(definition.identifier()));
+ }
+ }
+ }
+
+ private static void registerProperty(Identifier entityType, PropertyType property) {
+ var definition = Registries.JAVA_ENTITY_IDENTIFIERS.get(entityType.toString());
+ if (definition == null) {
+ throw new IllegalArgumentException("Unknown entity type: " + entityType);
+ }
+
+ definition.registeredProperties().add(entityType.toString(), property);
}
private EntityDefinitions() {
diff --git a/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java b/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java
index 6f8f2525f..e0267ff13 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java
@@ -44,7 +44,6 @@ import java.util.concurrent.CompletableFuture;
public class GeyserEntityData implements EntityData {
private final GeyserSession session;
-
private final Set movementLockOwners = new HashSet<>();
public GeyserEntityData(GeyserSession session) {
diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityProperties.java b/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityProperties.java
index eaa7b7448..fa9439998 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityProperties.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityProperties.java
@@ -31,36 +31,38 @@ import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtMapBuilder;
import org.cloudburstmc.nbt.NbtType;
-import org.geysermc.geyser.entity.properties.type.BooleanProperty;
-import org.geysermc.geyser.entity.properties.type.EnumProperty;
-import org.geysermc.geyser.entity.properties.type.FloatProperty;
-import org.geysermc.geyser.entity.properties.type.IntProperty;
+import org.cloudburstmc.protocol.bedrock.data.entity.EntityProperty;
import org.geysermc.geyser.entity.properties.type.PropertyType;
+import org.geysermc.geyser.registry.Registries;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
+import java.util.Objects;
+import java.util.regex.Pattern;
@EqualsAndHashCode
@ToString
public class GeyserEntityProperties {
- private final ObjectArrayList properties;
+
+ private final static Pattern ENTITY_PROPERTY_PATTERN = Pattern.compile("^[a-z0-9_.:-]*:[a-z0-9_.:-]*$");
+
+ private final ObjectArrayList> properties;
private final Object2IntMap propertyIndices;
- private GeyserEntityProperties(ObjectArrayList properties,
- Object2IntMap propertyIndices) {
- this.properties = properties;
- this.propertyIndices = propertyIndices;
+ private GeyserEntityProperties() {
+ this.properties = new ObjectArrayList<>();
+ this.propertyIndices = new Object2IntOpenHashMap<>();
}
public NbtMap toNbtMap(String entityType) {
NbtMapBuilder mapBuilder = NbtMap.builder();
List nbtProperties = new ArrayList<>();
- for (PropertyType property : properties) {
+ for (PropertyType, ?> property : properties) {
nbtProperties.add(property.nbtMap());
}
mapBuilder.putList("properties", NbtType.COMPOUND, nbtProperties);
@@ -68,7 +70,30 @@ public class GeyserEntityProperties {
return mapBuilder.putString("type", entityType).build();
}
- public @NonNull List getProperties() {
+ public void add(String entityType, @NonNull PropertyType property) {
+ if (!Registries.BEDROCK_ENTITY_PROPERTIES.get().isEmpty()) {
+ throw new IllegalStateException("Cannot add properties outside the GeyserDefineEntityProperties event!");
+ }
+
+ if (this.properties.size() > 32) {
+ throw new IllegalArgumentException("Cannot register more than 32 properties for entity type " + entityType);
+ }
+
+ Objects.requireNonNull(property, "property cannot be null!");
+ String name = property.identifier().toString();
+ if (propertyIndices.containsKey(name)) {
+ throw new IllegalArgumentException(
+ "Property with name " + name + " already exists on builder!");
+ } else if (!ENTITY_PROPERTY_PATTERN.matcher(name).matches()) {
+ throw new IllegalArgumentException(
+ "Cannot register property with name " + name + " because property name is invalid! Must match: " + ENTITY_PROPERTY_PATTERN.pattern()
+ );
+ }
+ this.properties.add(property);
+ propertyIndices.put(name, properties.size() - 1);
+ }
+
+ public @NonNull List> getProperties() {
return properties;
}
@@ -77,89 +102,24 @@ public class GeyserEntityProperties {
}
public static class Builder {
- private final ObjectArrayList properties = new ObjectArrayList<>();
- private final Object2IntMap propertyIndices = new Object2IntOpenHashMap<>();
+ private GeyserEntityProperties properties;
+ private final String identifier;
- public Builder addInt(@NonNull String name, int min, int max) {
- if (propertyIndices.containsKey(name)) {
- throw new IllegalArgumentException(
- "Property with name " + name + " already exists on builder!");
+ public Builder(String identifier) {
+ this.identifier = identifier;
+ }
+
+ public Builder add(@NonNull PropertyType property) {
+ Objects.requireNonNull(property, "property cannot be null!");
+ if (properties == null) {
+ properties = new GeyserEntityProperties();
}
- PropertyType property = new IntProperty(name, min, max);
- this.properties.add(property);
- propertyIndices.put(name, properties.size() - 1);
+ properties.add(identifier, property);
return this;
}
- public Builder addInt(@NonNull String name) {
- if (propertyIndices.containsKey(name)) {
- throw new IllegalArgumentException(
- "Property with name " + name + " already exists on builder!");
- }
- PropertyType property = new IntProperty(name, Integer.MIN_VALUE, Integer.MAX_VALUE);
- this.properties.add(property);
- propertyIndices.put(name, properties.size() - 1);
- return this;
- }
-
- public Builder addFloat(@NonNull String name, float min, float max) {
- if (propertyIndices.containsKey(name)) {
- throw new IllegalArgumentException(
- "Property with name " + name + " already exists on builder!");
- }
- PropertyType property = new FloatProperty(name, min, max);
- this.properties.add(property);
- propertyIndices.put(name, properties.size() - 1);
- return this;
- }
-
- public Builder addFloat(@NonNull String name) {
- if (propertyIndices.containsKey(name)) {
- throw new IllegalArgumentException(
- "Property with name " + name + " already exists on builder!");
- }
- PropertyType property = new FloatProperty(name, Float.MIN_NORMAL, Float.MAX_VALUE);
- this.properties.add(property);
- propertyIndices.put(name, properties.size() - 1);
- return this;
- }
-
- public Builder addBoolean(@NonNull String name) {
- if (propertyIndices.containsKey(name)) {
- throw new IllegalArgumentException(
- "Property with name " + name + " already exists on builder!");
- }
- PropertyType property = new BooleanProperty(name);
- this.properties.add(property);
- propertyIndices.put(name, properties.size() - 1);
- return this;
- }
-
- public Builder addEnum(@NonNull String name, List values) {
- if (propertyIndices.containsKey(name)) {
- throw new IllegalArgumentException(
- "Property with name " + name + " already exists on builder!");
- }
- PropertyType property = new EnumProperty(name, values);
- this.properties.add(property);
- propertyIndices.put(name, properties.size() - 1);
- return this;
- }
-
- public Builder addEnum(@NonNull String name, String... values) {
- if (propertyIndices.containsKey(name)) {
- throw new IllegalArgumentException(
- "Property with name " + name + " already exists on builder!");
- }
- List valuesList = Arrays.asList(values); // Convert array to list
- PropertyType property = new EnumProperty(name, valuesList);
- this.properties.add(property);
- propertyIndices.put(name, properties.size() - 1);
- return this;
- }
-
- public GeyserEntityProperties build() {
- return new GeyserEntityProperties(properties, propertyIndices);
+ public @Nullable GeyserEntityProperties build() {
+ return properties;
}
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityPropertyManager.java b/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityPropertyManager.java
index 29026b172..a5f189cc7 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityPropertyManager.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityPropertyManager.java
@@ -25,10 +25,12 @@
package org.geysermc.geyser.entity.properties;
-import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap;
+import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
+import org.cloudburstmc.nbt.NbtMap;
+import org.cloudburstmc.protocol.bedrock.data.entity.EntityProperty;
import org.cloudburstmc.protocol.bedrock.data.entity.FloatEntityProperty;
import org.cloudburstmc.protocol.bedrock.data.entity.IntEntityProperty;
-import org.geysermc.geyser.entity.properties.type.EnumProperty;
import org.geysermc.geyser.entity.properties.type.PropertyType;
import java.util.List;
@@ -36,34 +38,29 @@ import java.util.List;
public class GeyserEntityPropertyManager {
private final GeyserEntityProperties properties;
-
- private final ObjectArrayList intEntityProperties = new ObjectArrayList<>();
- private final ObjectArrayList floatEntityProperties = new ObjectArrayList<>();
+ private final Object2ObjectMap intEntityProperties = new Object2ObjectArrayMap<>();
+ private final Object2ObjectMap floatEntityProperties = new Object2ObjectArrayMap<>();
public GeyserEntityPropertyManager(GeyserEntityProperties properties) {
this.properties = properties;
+ for (PropertyType, ?> property : properties.getProperties()) {
+ String name = property.identifier().toString();
+ int index = properties.getPropertyIndex(name);
+ addProperty(name, property.defaultValue(index));
+ }
}
- public void add(String propertyName, int value) {
- int index = properties.getPropertyIndex(propertyName);
- intEntityProperties.add(new IntEntityProperty(index, value));
+ public void addProperty(PropertyType propertyType, T value) {
+ int index = properties.getPropertyIndex(propertyType.identifier().toString());
+ this.addProperty(propertyType.identifier().toString(), propertyType.createValue(index, value));
}
- public void add(String propertyName, boolean value) {
- int index = properties.getPropertyIndex(propertyName);
- intEntityProperties.add(new IntEntityProperty(index, value ? 1 : 0));
- }
-
- public void add(String propertyName, String value) {
- int index = properties.getPropertyIndex(propertyName);
- PropertyType property = properties.getProperties().get(index);
- int enumIndex = ((EnumProperty) property).getIndex(value);
- intEntityProperties.add(new IntEntityProperty(index, enumIndex));
- }
-
- public void add(String propertyName, float value) {
- int index = properties.getPropertyIndex(propertyName);
- floatEntityProperties.add(new FloatEntityProperty(index, value));
+ private void addProperty(String propertyName, EntityProperty entityProperty) {
+ if (entityProperty instanceof FloatEntityProperty floatEntityProperty) {
+ floatEntityProperties.put(propertyName, floatEntityProperty);
+ } else if (entityProperty instanceof IntEntityProperty intEntityProperty) {
+ intEntityProperties.put(propertyName, intEntityProperty);
+ }
}
public boolean hasFloatProperties() {
@@ -78,21 +75,17 @@ public class GeyserEntityPropertyManager {
return hasFloatProperties() || hasIntProperties();
}
- public ObjectArrayList intProperties() {
- return this.intEntityProperties;
- }
-
public void applyIntProperties(List properties) {
- properties.addAll(intEntityProperties);
+ properties.addAll(intEntityProperties.values());
intEntityProperties.clear();
}
- public ObjectArrayList floatProperties() {
- return this.floatEntityProperties;
- }
-
public void applyFloatProperties(List properties) {
- properties.addAll(floatEntityProperties);
+ properties.addAll(floatEntityProperties.values());
floatEntityProperties.clear();
}
-}
\ No newline at end of file
+
+ public NbtMap toNbtMap(String entityType) {
+ return this.properties.toNbtMap(entityType);
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/VanillaEntityProperties.java b/core/src/main/java/org/geysermc/geyser/entity/properties/VanillaEntityProperties.java
deleted file mode 100644
index 06a715f18..000000000
--- a/core/src/main/java/org/geysermc/geyser/entity/properties/VanillaEntityProperties.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (c) 2025 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.entity.properties;
-
-import org.geysermc.geyser.entity.type.living.monster.CreakingEntity;
-
-public class VanillaEntityProperties {
-
- public static final String CLIMATE_VARIANT_ID = "minecraft:climate_variant";
-
- public static final GeyserEntityProperties ARMADILLO = new GeyserEntityProperties.Builder()
- .addEnum("minecraft:armadillo_state",
- "unrolled",
- "rolled_up",
- "rolled_up_peeking",
- "rolled_up_relaxing",
- "rolled_up_unrolling")
- .build();
-
- public static final GeyserEntityProperties BEE = new GeyserEntityProperties.Builder()
- .addBoolean("minecraft:has_nectar")
- .build();
-
- public static final GeyserEntityProperties CLIMATE_VARIANT = new GeyserEntityProperties.Builder()
- .addEnum(CLIMATE_VARIANT_ID,
- "temperate",
- "warm",
- "cold")
- .build();
-
- public static final GeyserEntityProperties CREAKING = new GeyserEntityProperties.Builder()
- .addEnum(CreakingEntity.CREAKING_STATE,
- "neutral",
- "hostile_observed",
- "hostile_unobserved",
- "twitching",
- "crumbling")
- .addInt(CreakingEntity.CREAKING_SWAYING_TICKS, 0, 6)
- .build();
-
- public static final GeyserEntityProperties HAPPY_GHAST = new GeyserEntityProperties.Builder()
- .addBoolean("minecraft:can_move")
- .build();
-
- public static final GeyserEntityProperties WOLF_SOUND_VARIANT = new GeyserEntityProperties.Builder()
- .addEnum("minecraft:sound_variant",
- "default",
- "big",
- "cute",
- "grumpy",
- "mad",
- "puglin",
- "sad")
- .build();
-}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/type/AbstractEnumProperty.java b/core/src/main/java/org/geysermc/geyser/entity/properties/type/AbstractEnumProperty.java
new file mode 100644
index 000000000..0c921b54e
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/entity/properties/type/AbstractEnumProperty.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2025 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.entity.properties.type;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.cloudburstmc.nbt.NbtMap;
+import org.cloudburstmc.nbt.NbtType;
+import org.cloudburstmc.protocol.bedrock.data.entity.IntEntityProperty;
+import org.geysermc.geyser.api.util.Identifier;
+
+import java.util.List;
+import java.util.regex.Pattern;
+
+public interface AbstractEnumProperty extends PropertyType {
+
+ Pattern VALUE_VALIDATION_REGEX = Pattern.compile("^[A-Za-z][A-Za-z0-9_]{0,31}$");
+
+ @Override
+ default NbtMap nbtMap() {
+ return NbtMap.builder()
+ .putString("name", identifier().toString())
+ .putList("enum", NbtType.STRING, allBedrockValues())
+ .putInt("type", 3)
+ .build();
+ }
+
+ default void validateAllValues(Identifier name, List values) {
+ if (values.size() > 16) {
+ throw new IllegalArgumentException("Cannot register enum property with name " + name + " because it has more than 16 values!");
+ }
+
+ for (String value : values) {
+ if (!VALUE_VALIDATION_REGEX.matcher(value).matches()) {
+ throw new IllegalArgumentException(
+ "Cannot register enum property with name " + name + " and value " + value +
+ " because enum values can only contain alphanumeric characters and underscores."
+ );
+ }
+ }
+ }
+
+ List allBedrockValues();
+
+ @Override
+ default IntEntityProperty defaultValue(int index) {
+ return new IntEntityProperty(index, defaultIndex());
+ }
+
+ @Override
+ default IntEntityProperty createValue(int index, @Nullable T value) {
+ if (value == null) {
+ return defaultValue(index);
+ }
+
+ int valueIndex = indexOf(value);
+ if (valueIndex == -1) {
+ throw new IllegalArgumentException("Enum value " + value + " is not a valid enum value!");
+ }
+ return new IntEntityProperty(index, valueIndex);
+ }
+
+ int indexOf(T value);
+
+ int defaultIndex();
+}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/type/BooleanProperty.java b/core/src/main/java/org/geysermc/geyser/entity/properties/type/BooleanProperty.java
index 6fc64ad4b..9bbb1cc2d 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/properties/type/BooleanProperty.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/properties/type/BooleanProperty.java
@@ -26,19 +26,33 @@
package org.geysermc.geyser.entity.properties.type;
import org.cloudburstmc.nbt.NbtMap;
+import org.cloudburstmc.protocol.bedrock.data.entity.IntEntityProperty;
+import org.geysermc.geyser.api.entity.property.type.GeyserBooleanEntityProperty;
+import org.geysermc.geyser.api.util.Identifier;
-public class BooleanProperty implements PropertyType {
- private final String name;
-
- public BooleanProperty(String name) {
- this.name = name;
- }
+public record BooleanProperty(
+ Identifier identifier,
+ Boolean defaultValue
+) implements PropertyType, GeyserBooleanEntityProperty {
@Override
public NbtMap nbtMap() {
return NbtMap.builder()
- .putString("name", name)
+ .putString("name", identifier.toString())
.putInt("type", 2)
.build();
}
-}
\ No newline at end of file
+
+ @Override
+ public IntEntityProperty defaultValue(int index) {
+ return createValue(index, defaultValue != null && defaultValue);
+ }
+
+ @Override
+ public IntEntityProperty createValue(int index, Boolean value) {
+ if (value == null) {
+ return defaultValue(index);
+ }
+ return new IntEntityProperty(index, value ? 1 : 0);
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/type/EnumProperty.java b/core/src/main/java/org/geysermc/geyser/entity/properties/type/EnumProperty.java
index 05e12ba61..42fbb6a81 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/properties/type/EnumProperty.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/properties/type/EnumProperty.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org
+ * Copyright (c) 2025 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
@@ -25,37 +25,40 @@
package org.geysermc.geyser.entity.properties.type;
-import it.unimi.dsi.fastutil.objects.Object2IntMap;
-import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
-import org.cloudburstmc.nbt.NbtMap;
-import org.cloudburstmc.nbt.NbtType;
+import org.geysermc.geyser.api.entity.property.type.GeyserEnumEntityProperty;
+import org.geysermc.geyser.api.util.Identifier;
+import java.util.Arrays;
import java.util.List;
+import java.util.Locale;
-public class EnumProperty implements PropertyType {
- private final String name;
- private final List values;
- private final Object2IntMap valueIndexMap;
+public record EnumProperty>(
+ Identifier identifier,
+ Class enumClass,
+ E defaultValue
+) implements AbstractEnumProperty, GeyserEnumEntityProperty {
- public EnumProperty(String name, List values) {
- this.name = name;
- this.values = values;
- this.valueIndexMap = new Object2IntOpenHashMap<>(values.size());
- for (int i = 0; i < values.size(); i++) {
- valueIndexMap.put(values.get(i), i);
- }
+ public EnumProperty {
+ validateAllValues(identifier, Arrays.stream(enumClass.getEnumConstants()).map(value -> value.name().toLowerCase(Locale.ROOT)).toList());
+ }
+
+ public List values() {
+ return List.of(enumClass.getEnumConstants());
+ }
+
+ public List allBedrockValues() {
+ return values().stream().map(
+ value -> value.name().toLowerCase(Locale.ROOT)
+ ).toList();
}
@Override
- public NbtMap nbtMap() {
- return NbtMap.builder()
- .putString("name", name)
- .putList("enum", NbtType.STRING, values)
- .putInt("type", 3)
- .build();
+ public int indexOf(E value) {
+ return value.ordinal();
}
- public int getIndex(String value) {
- return valueIndexMap.getOrDefault(value, -1);
+ @Override
+ public int defaultIndex() {
+ return defaultValue.ordinal();
}
-}
\ No newline at end of file
+}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/type/FloatProperty.java b/core/src/main/java/org/geysermc/geyser/entity/properties/type/FloatProperty.java
index 8b808ebc3..16997dc8a 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/properties/type/FloatProperty.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/properties/type/FloatProperty.java
@@ -26,25 +26,48 @@
package org.geysermc.geyser.entity.properties.type;
import org.cloudburstmc.nbt.NbtMap;
+import org.cloudburstmc.protocol.bedrock.data.entity.FloatEntityProperty;
+import org.geysermc.geyser.api.entity.property.type.GeyserFloatEntityProperty;
+import org.geysermc.geyser.api.util.Identifier;
-public class FloatProperty implements PropertyType {
- private final String name;
- private final float max;
- private final float min;
+public record FloatProperty(
+ Identifier identifier,
+ float max,
+ float min,
+ Float defaultValue
+) implements PropertyType, GeyserFloatEntityProperty {
- public FloatProperty(String name, float min, float max) {
- this.name = name;
- this.max = max;
- this.min = min;
+ public FloatProperty {
+ if (min > max) {
+ throw new IllegalArgumentException("Cannot create float entity property (%s) with a minimum value (%s) greater than maximum (%s)!"
+ .formatted(identifier, min, max));
+ }
+ if (defaultValue < min || defaultValue > max) {
+ throw new IllegalArgumentException("Cannot create float entity property (%s) with a default value (%s) outside of the range (%s - %s)!"
+ .formatted(identifier, defaultValue, min, max));
+ }
}
@Override
public NbtMap nbtMap() {
return NbtMap.builder()
- .putString("name", name)
+ .putString("name", identifier.toString())
.putFloat("max", max)
.putFloat("min", min)
.putInt("type", 1)
.build();
}
-}
\ No newline at end of file
+
+ @Override
+ public FloatEntityProperty defaultValue(int index) {
+ return createValue(index, defaultValue == null ? min : defaultValue);
+ }
+
+ @Override
+ public FloatEntityProperty createValue(int index, Float value) {
+ if (value == null) {
+ return defaultValue(index);
+ }
+ return new FloatEntityProperty(index, value);
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/type/IntProperty.java b/core/src/main/java/org/geysermc/geyser/entity/properties/type/IntProperty.java
index 9e38db7c7..966277696 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/properties/type/IntProperty.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/properties/type/IntProperty.java
@@ -26,25 +26,53 @@
package org.geysermc.geyser.entity.properties.type;
import org.cloudburstmc.nbt.NbtMap;
+import org.cloudburstmc.protocol.bedrock.data.entity.IntEntityProperty;
+import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.api.entity.property.type.GeyserIntEntityProperty;
+import org.geysermc.geyser.api.util.Identifier;
-public class IntProperty implements PropertyType {
- private final String name;
- private final int max;
- private final int min;
+public record IntProperty(
+ Identifier identifier,
+ int max,
+ int min,
+ Integer defaultValue
+) implements PropertyType, GeyserIntEntityProperty {
- public IntProperty(String name, int min, int max) {
- this.name = name;
- this.max = max;
- this.min = min;
+ public IntProperty {
+ if (min > max) {
+ throw new IllegalArgumentException("Cannot create int entity property (%s) with a minimum value (%s) greater than maximum (%s)!"
+ .formatted(identifier, min, max));
+ }
+ if (defaultValue < min || defaultValue > max) {
+ throw new IllegalArgumentException("Cannot create int entity property (%s) with a default value (%s) outside of the range (%s - %s)!"
+ .formatted(identifier, defaultValue, min, max));
+ }
+ if (min < -1000000 || max > 1000000) {
+ // https://learn.microsoft.com/en-us/minecraft/creator/documents/introductiontoentityproperties?view=minecraft-bedrock-stable#a-note-on-large-integer-entity-property-values
+ GeyserImpl.getInstance().getLogger().warning("Using int entity properties with min / max values larger than +- 1 million is not recommended!");
+ }
}
@Override
public NbtMap nbtMap() {
return NbtMap.builder()
- .putString("name", name)
+ .putString("name", identifier.toString())
.putInt("max", max)
.putInt("min", min)
.putInt("type", 0)
.build();
}
-}
\ No newline at end of file
+
+ @Override
+ public IntEntityProperty defaultValue(int index) {
+ return createValue(index, defaultValue == null ? min : defaultValue);
+ }
+
+ @Override
+ public IntEntityProperty createValue(int index, Integer value) {
+ if (value == null) {
+ return defaultValue(index);
+ }
+ return new IntEntityProperty(index, value);
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/type/PropertyType.java b/core/src/main/java/org/geysermc/geyser/entity/properties/type/PropertyType.java
index a64d7246a..cd1471273 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/properties/type/PropertyType.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/properties/type/PropertyType.java
@@ -25,8 +25,20 @@
package org.geysermc.geyser.entity.properties.type;
+import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.nbt.NbtMap;
+import org.cloudburstmc.protocol.bedrock.data.entity.EntityProperty;
+import org.geysermc.geyser.api.entity.property.GeyserEntityProperty;
+import org.geysermc.geyser.entity.properties.GeyserEntityPropertyManager;
-public interface PropertyType {
+public interface PropertyType extends GeyserEntityProperty {
NbtMap nbtMap();
-}
\ No newline at end of file
+
+ NetworkRepresentation defaultValue(int index);
+
+ NetworkRepresentation createValue(int index, @Nullable Type value);
+
+ default void apply(GeyserEntityPropertyManager manager, Type value) {
+ manager.addProperty(this, value);
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/type/StringEnumProperty.java b/core/src/main/java/org/geysermc/geyser/entity/properties/type/StringEnumProperty.java
new file mode 100644
index 000000000..278f60c7f
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/entity/properties/type/StringEnumProperty.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2019-2024 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.entity.properties.type;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.geysermc.geyser.api.entity.property.type.GeyserStringEnumProperty;
+import org.geysermc.geyser.api.util.Identifier;
+
+import java.util.List;
+
+public record StringEnumProperty(
+ Identifier identifier,
+ List values,
+ int defaultIndex
+) implements AbstractEnumProperty, GeyserStringEnumProperty {
+
+ public StringEnumProperty {
+ if (defaultIndex < 0) {
+ throw new IllegalArgumentException("Unable to find default value for enum property with name " + identifier);
+ }
+ validateAllValues(identifier, values);
+ }
+
+ public StringEnumProperty(Identifier name, List values, @Nullable String defaultValue) {
+ this(name, values, defaultValue == null ? 0 : values.indexOf(defaultValue));
+ }
+
+ @Override
+ public List allBedrockValues() {
+ return values;
+ }
+
+ @Override
+ public int indexOf(String value) {
+ return values.indexOf(value);
+ }
+
+ @Override
+ public @NonNull String defaultValue() {
+ return values.get(defaultIndex);
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java b/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java
index b03dea3ed..15584a2be 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java
@@ -29,22 +29,28 @@ import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import net.kyori.adventure.text.Component;
+import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector2f;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
+import org.cloudburstmc.protocol.bedrock.data.entity.EntityProperty;
import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket;
import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket;
import org.cloudburstmc.protocol.bedrock.packet.MoveEntityAbsolutePacket;
import org.cloudburstmc.protocol.bedrock.packet.MoveEntityDeltaPacket;
import org.cloudburstmc.protocol.bedrock.packet.RemoveEntityPacket;
import org.cloudburstmc.protocol.bedrock.packet.SetEntityDataPacket;
+import org.geysermc.geyser.api.entity.property.BatchPropertyUpdater;
+import org.geysermc.geyser.api.entity.property.GeyserEntityProperty;
import org.geysermc.geyser.api.entity.type.GeyserEntity;
import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.entity.GeyserDirtyMetadata;
+import org.geysermc.geyser.entity.properties.GeyserEntityProperties;
import org.geysermc.geyser.entity.properties.GeyserEntityPropertyManager;
+import org.geysermc.geyser.entity.properties.type.PropertyType;
import org.geysermc.geyser.entity.type.living.MobEntity;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.item.Items;
@@ -71,6 +77,7 @@ import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
+import java.util.function.Consumer;
@Getter
@Setter
@@ -118,7 +125,7 @@ public class Entity implements GeyserEntity {
@Setter(AccessLevel.NONE)
private float boundingBoxWidth;
@Setter(AccessLevel.NONE)
- private String displayName;
+ protected String displayName;
@Setter(AccessLevel.NONE)
protected boolean silent = false;
/* Metadata end */
@@ -201,6 +208,10 @@ public class Entity implements GeyserEntity {
addEntityPacket.setBodyRotation(yaw); // TODO: This should be bodyYaw
addEntityPacket.getMetadata().putFlags(flags);
dirtyMetadata.apply(addEntityPacket.getMetadata());
+ if (propertyManager != null) {
+ propertyManager.applyIntProperties(addEntityPacket.getProperties().getIntProperties());
+ propertyManager.applyFloatProperties(addEntityPacket.getProperties().getFloatProperties());
+ }
addAdditionalSpawnData(addEntityPacket);
valid = true;
@@ -672,7 +683,7 @@ public class Entity implements GeyserEntity {
// Note this might be client side. Has yet to be an issue though, as of Java 1.21.
return InteractiveTag.REMOVE_LEASH;
}
- if (session.getPlayerInventory().getItemInHand(hand).asItem() == Items.LEAD && leashable.canBeLeashed()) {
+ if (session.getPlayerInventory().getItemInHand(hand).is(Items.LEAD) && leashable.canBeLeashed()) {
// We shall leash
return InteractiveTag.LEASH;
}
@@ -701,7 +712,7 @@ public class Entity implements GeyserEntity {
// Has yet to be an issue though, as of Java 1.21.
return InteractionResult.SUCCESS;
}
- if (session.getPlayerInventory().getItemInHand(hand).asItem() == Items.LEAD
+ if (session.getPlayerInventory().getItemInHand(hand).is(Items.LEAD)
&& !(session.getEntityCache().getEntityByGeyserId(leashable.leashHolderBedrockId()) instanceof PlayerEntity)) {
// We shall leash
return InteractionResult.SUCCESS;
@@ -752,4 +763,42 @@ public class Entity implements GeyserEntity {
packet.setData(data);
session.sendUpstreamPacket(packet);
}
+
+ @Override
+ public void updatePropertiesBatched(Consumer consumer) {
+ if (this.propertyManager != null) {
+ Objects.requireNonNull(consumer);
+ GeyserEntityProperties propertyDefinitions = definition.registeredProperties();
+ consumer.accept(new BatchPropertyUpdater() {
+ @Override
+ public void update(@NonNull GeyserEntityProperty property, @Nullable T value) {
+ Objects.requireNonNull(property, "property must not be null!");
+ if (!(property instanceof PropertyType propertyType)) {
+ throw new IllegalArgumentException("Invalid property implementation! Got: " + property.getClass().getSimpleName());
+ }
+ int index = propertyDefinitions.getPropertyIndex(property.identifier().toString());
+ if (index < 0) {
+ throw new IllegalArgumentException("No property with the name " + property.identifier() + " has been registered.");
+ }
+
+ var expectedProperty = propertyDefinitions.getProperties().get(index);
+ if (!expectedProperty.equals(propertyType)) {
+ throw new IllegalArgumentException("The supplied property was not registered with this entity type!");
+ }
+
+ propertyType.apply(propertyManager, value);
+ }
+ });
+
+ if (propertyManager.hasProperties()) {
+ SetEntityDataPacket packet = new SetEntityDataPacket();
+ packet.setRuntimeEntityId(getGeyserId());
+ propertyManager.applyFloatProperties(packet.getProperties().getFloatProperties());
+ propertyManager.applyIntProperties(packet.getProperties().getIntProperties());
+ session.sendUpstreamPacket(packet);
+ }
+ } else {
+ throw new IllegalArgumentException("Given entity has no registered properties!");
+ }
+ }
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java
index 92048eb25..d6d880d0a 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java
@@ -35,7 +35,6 @@ import org.cloudburstmc.protocol.bedrock.data.AttributeData;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId;
-import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
import org.cloudburstmc.protocol.bedrock.packet.MobArmorEquipmentPacket;
import org.cloudburstmc.protocol.bedrock.packet.MobEquipmentPacket;
import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket;
@@ -46,9 +45,10 @@ import org.geysermc.geyser.entity.vehicle.ClientVehicle;
import org.geysermc.geyser.entity.vehicle.HappyGhastVehicleComponent;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.item.Items;
-import org.geysermc.geyser.registry.type.ItemMapping;
+import org.geysermc.geyser.item.type.Item;
import org.geysermc.geyser.scoreboard.Team;
import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.session.cache.tags.ItemTag;
import org.geysermc.geyser.translator.item.ItemTranslator;
import org.geysermc.geyser.util.AttributeUtils;
import org.geysermc.geyser.util.EntityUtils;
@@ -82,15 +82,6 @@ import java.util.UUID;
public class LivingEntity extends Entity {
protected EnumMap equipment = new EnumMap<>(EquipmentSlot.class);
- protected ItemData helmet = ItemData.AIR;
- protected ItemData chestplate = ItemData.AIR;
- protected ItemData leggings = ItemData.AIR;
- protected ItemData boots = ItemData.AIR;
- protected ItemData body = ItemData.AIR;
- protected ItemData saddle = ItemData.AIR;
- protected ItemData hand = ItemData.AIR;
- protected ItemData offhand = ItemData.AIR;
-
@Getter(value = AccessLevel.NONE)
protected float health = 1f; // The default value in Java Edition before any entity metadata is sent
@Getter(value = AccessLevel.NONE)
@@ -118,34 +109,48 @@ public class LivingEntity extends Entity {
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
}
+ public GeyserItemStack getItemInSlot(EquipmentSlot slot) {
+ GeyserItemStack stack = equipment.get(slot);
+ if (stack == null) {
+ return GeyserItemStack.EMPTY;
+ }
+ return stack;
+ }
+
+ public GeyserItemStack getMainHandItem() {
+ return getItemInSlot(EquipmentSlot.MAIN_HAND);
+ }
+
+ public GeyserItemStack getOffHandItem() {
+ return getItemInSlot(EquipmentSlot.OFF_HAND);
+ }
+
+ public boolean isHolding(Item item) {
+ return getMainHandItem().is(item) || getOffHandItem().is(item);
+ }
+
public void setHelmet(GeyserItemStack stack) {
this.equipment.put(EquipmentSlot.HELMET, stack);
- this.helmet = ItemTranslator.translateToBedrock(session, stack);
}
public void setChestplate(GeyserItemStack stack) {
this.equipment.put(EquipmentSlot.CHESTPLATE, stack);
- this.chestplate = ItemTranslator.translateToBedrock(session, stack);
}
public void setLeggings(GeyserItemStack stack) {
this.equipment.put(EquipmentSlot.LEGGINGS, stack);
- this.leggings = ItemTranslator.translateToBedrock(session, stack);
}
public void setBoots(GeyserItemStack stack) {
this.equipment.put(EquipmentSlot.BOOTS, stack);
- this.boots = ItemTranslator.translateToBedrock(session, stack);
}
public void setBody(GeyserItemStack stack) {
this.equipment.put(EquipmentSlot.BODY, stack);
- this.body = ItemTranslator.translateToBedrock(session, stack);
}
public void setSaddle(GeyserItemStack stack) {
this.equipment.put(EquipmentSlot.SADDLE, stack);
- this.saddle = ItemTranslator.translateToBedrock(session, stack);
boolean saddled = false;
if (!stack.isEmpty()) {
@@ -158,12 +163,10 @@ public class LivingEntity extends Entity {
public void setHand(GeyserItemStack stack) {
this.equipment.put(EquipmentSlot.MAIN_HAND, stack);
- this.hand = ItemTranslator.translateToBedrock(session, stack);
}
public void setOffhand(GeyserItemStack stack) {
this.equipment.put(EquipmentSlot.OFF_HAND, stack);
- this.offhand = ItemTranslator.translateToBedrock(session, stack);
}
protected void updateSaddled(boolean saddled) {
@@ -178,13 +181,9 @@ public class LivingEntity extends Entity {
}
public void switchHands() {
- GeyserItemStack javaOffhand = this.equipment.get(EquipmentSlot.OFF_HAND);
+ GeyserItemStack offhand = this.equipment.get(EquipmentSlot.OFF_HAND);
this.equipment.put(EquipmentSlot.OFF_HAND, this.equipment.get(EquipmentSlot.MAIN_HAND));
- this.equipment.put(EquipmentSlot.MAIN_HAND, javaOffhand);
-
- ItemData bedrockOffhand = this.offhand;
- this.offhand = this.hand;
- this.hand = bedrockOffhand;
+ this.equipment.put(EquipmentSlot.MAIN_HAND, offhand);
}
@Override
@@ -279,11 +278,10 @@ public class LivingEntity extends Entity {
}
protected boolean hasShield(boolean offhand) {
- ItemMapping shieldMapping = session.getItemMappings().getStoredItems().shield();
if (offhand) {
- return this.offhand.getDefinition().equals(shieldMapping.getBedrockDefinition());
+ return getOffHandItem().is(Items.SHIELD);
} else {
- return hand.getDefinition().equals(shieldMapping.getBedrockDefinition());
+ return getMainHandItem().is(Items.SHIELD);
}
}
@@ -342,7 +340,7 @@ public class LivingEntity extends Entity {
@Override
public InteractionResult interact(Hand hand) {
GeyserItemStack itemStack = session.getPlayerInventory().getItemInHand(hand);
- if (itemStack.asItem() == Items.NAME_TAG) {
+ if (itemStack.is(Items.NAME_TAG)) {
InteractionResult result = checkInteractWithNameTag(itemStack);
if (result.consumesAction()) {
return result;
@@ -394,39 +392,38 @@ public class LivingEntity extends Entity {
return InteractionResult.PASS;
}
- public void updateArmor(GeyserSession session) {
+ public void updateArmor() {
if (!valid) return;
- ItemData helmet = this.helmet;
- ItemData chestplate = this.chestplate;
+ GeyserItemStack helmet = getItemInSlot(EquipmentSlot.HELMET);
+ GeyserItemStack chestplate = getItemInSlot(EquipmentSlot.CHESTPLATE);
// 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 (ItemData.AIR.equals(chestplate) && helmet.getDefinition().equals(banner.getBedrockDefinition())) {
- chestplate = this.helmet;
- helmet = ItemData.AIR;
- } else if (chestplate.getDefinition().equals(banner.getBedrockDefinition())) {
+ if (chestplate.isEmpty() && helmet.is(session, ItemTag.BANNERS)) {
+ chestplate = helmet;
+ helmet = GeyserItemStack.EMPTY;
+ } else if (chestplate.is(session, ItemTag.BANNERS)) {
// Prevent chestplate banners from showing erroneously
- chestplate = ItemData.AIR;
+ chestplate = GeyserItemStack.EMPTY;
}
MobArmorEquipmentPacket armorEquipmentPacket = new MobArmorEquipmentPacket();
armorEquipmentPacket.setRuntimeEntityId(geyserId);
- armorEquipmentPacket.setHelmet(helmet);
- armorEquipmentPacket.setChestplate(chestplate);
- armorEquipmentPacket.setLeggings(leggings);
- armorEquipmentPacket.setBoots(boots);
- armorEquipmentPacket.setBody(body);
+ armorEquipmentPacket.setHelmet(ItemTranslator.translateToBedrock(session, helmet));
+ armorEquipmentPacket.setChestplate(ItemTranslator.translateToBedrock(session, chestplate));
+ armorEquipmentPacket.setLeggings(ItemTranslator.translateToBedrock(session, getItemInSlot(EquipmentSlot.LEGGINGS)));
+ armorEquipmentPacket.setBoots(ItemTranslator.translateToBedrock(session, getItemInSlot(EquipmentSlot.BOOTS)));
+ armorEquipmentPacket.setBody(ItemTranslator.translateToBedrock(session, getItemInSlot(EquipmentSlot.BODY)));
session.sendUpstreamPacket(armorEquipmentPacket);
}
- public void updateMainHand(GeyserSession session) {
+ public void updateMainHand() {
if (!valid) return;
MobEquipmentPacket handPacket = new MobEquipmentPacket();
handPacket.setRuntimeEntityId(geyserId);
- handPacket.setItem(hand);
+ handPacket.setItem(ItemTranslator.translateToBedrock(session, getMainHandItem()));
handPacket.setHotbarSlot(-1);
handPacket.setInventorySlot(0);
handPacket.setContainerId(ContainerId.INVENTORY);
@@ -434,12 +431,12 @@ public class LivingEntity extends Entity {
session.sendUpstreamPacket(handPacket);
}
- public void updateOffHand(GeyserSession session) {
+ public void updateOffHand() {
if (!valid) return;
MobEquipmentPacket offHandPacket = new MobEquipmentPacket();
offHandPacket.setRuntimeEntityId(geyserId);
- offHandPacket.setItem(offhand);
+ offHandPacket.setItem(ItemTranslator.translateToBedrock(session, getOffHandItem()));
offHandPacket.setHotbarSlot(-1);
offHandPacket.setInventorySlot(0);
offHandPacket.setContainerId(ContainerId.OFFHAND);
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEggEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEggEntity.java
index 3167ed305..a32762abe 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEggEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEggEntity.java
@@ -28,9 +28,7 @@ package org.geysermc.geyser.entity.type;
import lombok.Getter;
import net.kyori.adventure.key.Key;
import org.cloudburstmc.math.vector.Vector3f;
-import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket;
import org.geysermc.geyser.entity.EntityDefinition;
-import org.geysermc.geyser.entity.properties.VanillaEntityProperties;
import org.geysermc.geyser.entity.type.living.animal.farm.TemperatureVariantAnimal;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.item.Items;
@@ -41,7 +39,6 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetad
import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes;
-import java.util.Locale;
import java.util.UUID;
@Getter
@@ -54,31 +51,25 @@ public class ThrowableEggEntity extends ThrowableItemEntity {
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
}
- @Override
- public void addAdditionalSpawnData(AddEntityPacket addEntityPacket) {
- propertyManager.add(VanillaEntityProperties.CLIMATE_VARIANT_ID, "temperate");
- propertyManager.applyIntProperties(addEntityPacket.getProperties().getIntProperties());
- }
-
@Override
public void setItem(EntityMetadata entityMetadata) {
GeyserItemStack stack = GeyserItemStack.from(entityMetadata.getValue());
- propertyManager.add(VanillaEntityProperties.CLIMATE_VARIANT_ID, getVariantOrFallback(session, stack));
+ TemperatureVariantAnimal.TEMPERATE_VARIANT_PROPERTY.apply(propertyManager, getVariantOrFallback(session, stack));
updateBedrockEntityProperties();
this.itemStack = stack;
}
- private static String getVariantOrFallback(GeyserSession session, GeyserItemStack stack) {
+ private static TemperatureVariantAnimal.BuiltInVariant getVariantOrFallback(GeyserSession session, GeyserItemStack stack) {
Holder holder = stack.getComponent(DataComponentTypes.CHICKEN_VARIANT);
if (holder != null) {
Key chickenVariant = holder.getOrCompute(id -> JavaRegistries.CHICKEN_VARIANT.key(session, id));
for (var variant : TemperatureVariantAnimal.BuiltInVariant.values()) {
if (chickenVariant.asMinimalString().equalsIgnoreCase(variant.name())) {
- return chickenVariant.asMinimalString().toLowerCase(Locale.ROOT);
+ return variant;
}
}
}
- return TemperatureVariantAnimal.BuiltInVariant.TEMPERATE.toBedrock();
+ return TemperatureVariantAnimal.BuiltInVariant.TEMPERATE;
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/AllayEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/AllayEntity.java
index 01a42e527..e87e20236 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/AllayEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/AllayEntity.java
@@ -30,8 +30,8 @@ import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.inventory.GeyserItemStack;
-import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.session.cache.tags.ItemTag;
import org.geysermc.geyser.util.InteractionResult;
import org.geysermc.geyser.util.InteractiveTag;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
@@ -60,9 +60,9 @@ public class AllayEntity extends MobEntity {
if (this.canDuplicate && getFlag(EntityFlag.DANCING) && isDuplicationItem(itemInHand)) {
// Maybe better as another tag?
return InteractiveTag.GIVE_ITEM_TO_ALLAY;
- } else if (!this.hand.isValid() && !itemInHand.isEmpty()) {
+ } else if (getMainHandItem().isEmpty() && !itemInHand.isEmpty()) {
return InteractiveTag.GIVE_ITEM_TO_ALLAY;
- } else if (this.hand.isValid() && hand == Hand.MAIN_HAND && itemInHand.isEmpty()) {
+ } else if (!getMainHandItem().isEmpty() && hand == Hand.MAIN_HAND && itemInHand.isEmpty()) {
// Seems like there isn't a good tag for this yet
return InteractiveTag.GIVE_ITEM_TO_ALLAY;
} else {
@@ -76,10 +76,10 @@ public class AllayEntity extends MobEntity {
if (this.canDuplicate && getFlag(EntityFlag.DANCING) && isDuplicationItem(itemInHand)) {
//TOCHECK sound
return InteractionResult.SUCCESS;
- } else if (!this.hand.isValid() && !itemInHand.isEmpty()) {
+ } else if (getMainHandItem().isEmpty() && !itemInHand.isEmpty()) {
//TODO play sound?
return InteractionResult.SUCCESS;
- } else if (this.hand.isValid() && hand == Hand.MAIN_HAND && itemInHand.isEmpty()) {
+ } else if (!getMainHandItem().isEmpty() && hand == Hand.MAIN_HAND && itemInHand.isEmpty()) {
//TOCHECK also play sound here?
return InteractionResult.SUCCESS;
} else {
@@ -88,6 +88,6 @@ public class AllayEntity extends MobEntity {
}
private boolean isDuplicationItem(GeyserItemStack itemStack) {
- return itemStack.asItem() == Items.AMETHYST_SHARD;
+ return itemStack.is(session, ItemTag.DUPLICATES_ALLAYS);
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java
index afbdcca31..956d641bb 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java
@@ -32,7 +32,6 @@ import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataType;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
-import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.entity.type.LivingEntity;
@@ -42,6 +41,7 @@ import org.geysermc.geyser.scoreboard.Team;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.util.InteractionResult;
import org.geysermc.geyser.util.MathUtils;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
@@ -257,7 +257,7 @@ public class ArmorStandEntity extends LivingEntity {
@Override
public InteractionResult interactAt(Hand hand) {
- if (!isMarker && session.getPlayerInventory().getItemInHand(hand).asItem() != Items.NAME_TAG) {
+ if (!isMarker && !session.getPlayerInventory().getItemInHand(hand).is(Items.NAME_TAG)) {
// Java Edition returns SUCCESS if in spectator mode, but this is overridden with an earlier check on the client
return InteractionResult.CONSUME;
} else {
@@ -332,8 +332,7 @@ public class ArmorStandEntity extends LivingEntity {
return;
}
boolean isNametagEmpty = nametag.isEmpty();
- if (!isNametagEmpty && (!helmet.equals(ItemData.AIR) || !chestplate.equals(ItemData.AIR) || !leggings.equals(ItemData.AIR)
- || !boots.equals(ItemData.AIR) || !hand.equals(ItemData.AIR) || !offhand.equals(ItemData.AIR))) {
+ if (!isNametagEmpty && hasAnyEquipment()) {
// Reset scale of the proper armor stand
setScale(getScale());
// Set the proper armor stand to invisible to show armor
@@ -396,6 +395,12 @@ public class ArmorStandEntity extends LivingEntity {
}
}
+ private boolean hasAnyEquipment() {
+ return (!getItemInSlot(EquipmentSlot.HELMET).isEmpty() || !getItemInSlot(EquipmentSlot.CHESTPLATE).isEmpty()
+ || !getItemInSlot(EquipmentSlot.LEGGINGS).isEmpty() || !getItemInSlot(EquipmentSlot.BOOTS).isEmpty()
+ || !getMainHandItem().isEmpty() || !getOffHandItem().isEmpty());
+ }
+
@Override
public float getBoundingBoxWidth() {
// For consistency with getBoundingBoxHeight()
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/CopperGolemEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/CopperGolemEntity.java
new file mode 100644
index 000000000..946d96169
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/CopperGolemEntity.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (c) 2025 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.entity.type.living;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.cloudburstmc.math.vector.Vector3f;
+import org.geysermc.geyser.entity.EntityDefinition;
+import org.geysermc.geyser.entity.properties.type.BooleanProperty;
+import org.geysermc.geyser.entity.properties.type.EnumProperty;
+import org.geysermc.geyser.impl.IdentifierImpl;
+import org.geysermc.geyser.inventory.GeyserItemStack;
+import org.geysermc.geyser.item.Items;
+import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.session.cache.tags.ItemTag;
+import org.geysermc.geyser.util.InteractionResult;
+import org.geysermc.geyser.util.InteractiveTag;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.CopperGolemState;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.MetadataType;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.WeatheringCopperState;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand;
+
+import java.util.UUID;
+
+public class CopperGolemEntity extends GolemEntity {
+ public static final BooleanProperty HAS_FLOWER_PROPERTY = new BooleanProperty(
+ IdentifierImpl.of("has_flower"),
+ false
+ );
+
+ public static final EnumProperty CHEST_INTERACTION_PROPERTY = new EnumProperty<>(
+ IdentifierImpl.of("chest_interaction"),
+ ChestInteractionState.class,
+ ChestInteractionState.NONE
+ );
+
+ public static final EnumProperty OXIDATION_LEVEL_STATE_ENUM_PROPERTY = new EnumProperty<>(
+ IdentifierImpl.of("oxidation_level"),
+ OxidationLevelState.class,
+ OxidationLevelState.UNOXIDIZED
+ );
+
+ public enum ChestInteractionState {
+ NONE,
+ TAKE,
+ TAKE_FAIL,
+ PUT,
+ PUT_FAIL
+ }
+
+ public enum OxidationLevelState {
+ UNOXIDIZED,
+ EXPOSED,
+ WEATHERED,
+ OXIDIZED
+ }
+
+ public CopperGolemEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+ super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
+ }
+
+ @Override
+ protected @NonNull InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
+ if (itemInHand.isEmpty() && !getMainHandItem().isEmpty()) {
+ return InteractiveTag.DROP_ITEM;
+ } else if (itemInHand.is(Items.SHEARS) && canBeSheared()) {
+ return InteractiveTag.SHEAR;
+ } else if (itemInHand.is(Items.HONEYCOMB)) {
+ return InteractiveTag.WAX_ON;
+ } else if (itemInHand.is(session, ItemTag.AXES)) {
+ // There is no way of knowing if the copper golem is waxed or not,
+ // so just always send a scrape tag :(
+ return InteractiveTag.SCRAPE;
+ }
+
+ return super.testMobInteraction(hand, itemInHand);
+ }
+
+ @Override
+ protected @NonNull InteractionResult mobInteract(@NonNull Hand usedHand, @NonNull GeyserItemStack itemInHand) {
+ if ((itemInHand.isEmpty() && !getMainHandItem().isEmpty()) || (itemInHand.is(Items.SHEARS) && canBeSheared())) {
+ return InteractionResult.SUCCESS;
+ }
+
+ return InteractionResult.PASS;
+ }
+
+ private boolean canBeSheared() {
+ return isAlive() && getItemInSlot(EquipmentSlot.HELMET).is(session, ItemTag.SHEARABLE_FROM_COPPER_GOLEM);
+ }
+
+ @Override
+ public void setSaddle(GeyserItemStack stack) {
+ super.setSaddle(stack);
+
+ // Equipment on Java, entity property on bedrock
+ HAS_FLOWER_PROPERTY.apply(propertyManager, stack.is(Items.POPPY));
+ updateBedrockEntityProperties();
+ }
+
+ public void setWeatheringState(EntityMetadata> metadata) {
+ WeatheringCopperState state = metadata.getValue();
+ OXIDATION_LEVEL_STATE_ENUM_PROPERTY.apply(propertyManager, switch (state) {
+ case UNAFFECTED -> OxidationLevelState.UNOXIDIZED;
+ case EXPOSED -> OxidationLevelState.EXPOSED;
+ case WEATHERED -> OxidationLevelState.WEATHERED;
+ case OXIDIZED -> OxidationLevelState.OXIDIZED;
+ });
+ updateBedrockEntityProperties();
+ }
+
+ public void setGolemState(EntityMetadata> metadata) {
+ CopperGolemState state = metadata.getValue();
+ CHEST_INTERACTION_PROPERTY.apply(propertyManager, switch (state) {
+ case IDLE -> ChestInteractionState.NONE;
+ case GETTING_ITEM -> ChestInteractionState.TAKE;
+ case GETTING_NO_ITEM -> ChestInteractionState.TAKE_FAIL;
+ case DROPPING_ITEM -> ChestInteractionState.PUT;
+ case DROPPING_NO_ITEM -> ChestInteractionState.PUT_FAIL;
+ });
+ updateBedrockEntityProperties();
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/DolphinEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/DolphinEntity.java
index 8c404be97..cb300a7c9 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/DolphinEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/DolphinEntity.java
@@ -50,7 +50,7 @@ public class DolphinEntity extends AgeableWaterEntity {
@NonNull
@Override
protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
- if (!itemInHand.isEmpty() && session.getTagCache().is(ItemTag.FISHES, itemInHand)) {
+ if (!itemInHand.isEmpty() && itemInHand.is(session, ItemTag.FISHES)) {
return InteractiveTag.FEED;
}
return super.testMobInteraction(hand, itemInHand);
@@ -59,7 +59,7 @@ public class DolphinEntity extends AgeableWaterEntity {
@NonNull
@Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
- if (!itemInHand.isEmpty() && session.getTagCache().is(ItemTag.FISHES, itemInHand)) {
+ if (!itemInHand.isEmpty() && itemInHand.is(session, ItemTag.FISHES)) {
// Feed
return InteractionResult.SUCCESS;
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/IronGolemEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/IronGolemEntity.java
index 58a349cc9..8cbed6373 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/IronGolemEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/IronGolemEntity.java
@@ -54,7 +54,7 @@ public class IronGolemEntity extends GolemEntity {
@NonNull
@Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
- if (itemInHand.asItem() == Items.IRON_INGOT) {
+ if (itemInHand.is(Items.IRON_INGOT)) {
if (health < maxHealth) {
// Healing the iron golem
return InteractionResult.SUCCESS;
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/MobEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/MobEntity.java
index 558bffd99..d6c00f694 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/MobEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/MobEntity.java
@@ -85,7 +85,7 @@ public class MobEntity extends LivingEntity implements Leashable {
return InteractiveTag.REMOVE_LEASH;
} else {
GeyserItemStack itemStack = session.getPlayerInventory().getItemInHand(hand);
- if (itemStack.asItem() == Items.NAME_TAG) {
+ if (itemStack.is(Items.NAME_TAG)) {
InteractionResult result = checkInteractWithNameTag(itemStack);
if (result.consumesAction()) {
return InteractiveTag.NAME;
@@ -120,8 +120,10 @@ public class MobEntity extends LivingEntity implements Leashable {
}
for (EquipmentSlot slot : EquipmentSlot.values()) {
- GeyserItemStack equipped = equipment.get(slot);
- if (equipped == null || equipped.isEmpty()) continue;
+ GeyserItemStack equipped = getItemInSlot(slot);
+ if (equipped.isEmpty()) {
+ continue;
+ }
Equippable equippable = equipped.getComponent(DataComponentTypes.EQUIPPABLE);
if (equippable != null && equippable.canBeSheared()) {
@@ -135,7 +137,7 @@ public class MobEntity extends LivingEntity implements Leashable {
}
private InteractionResult checkPriorityInteractions(GeyserItemStack itemInHand) {
- if (itemInHand.asItem() == Items.NAME_TAG) {
+ if (itemInHand.is(Items.NAME_TAG)) {
InteractionResult result = checkInteractWithNameTag(itemInHand);
if (result.consumesAction()) {
return result;
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/SnowGolemEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/SnowGolemEntity.java
index 50aa7b90e..f766cd402 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/SnowGolemEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/SnowGolemEntity.java
@@ -54,7 +54,7 @@ public class SnowGolemEntity extends GolemEntity {
@NonNull
@Override
protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
- if (Items.SHEARS == itemInHand.asItem() && isAlive() && !getFlag(EntityFlag.SHEARED)) {
+ if (itemInHand.is(Items.SHEARS) && isAlive() && !getFlag(EntityFlag.SHEARED)) {
// Shearing the snow golem
return InteractiveTag.SHEAR;
}
@@ -64,7 +64,7 @@ public class SnowGolemEntity extends GolemEntity {
@NonNull
@Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
- if (Items.SHEARS == itemInHand.asItem() && isAlive() && !getFlag(EntityFlag.SHEARED)) {
+ if (itemInHand.is(Items.SHEARS) && isAlive() && !getFlag(EntityFlag.SHEARED)) {
// Shearing the snow golem
return InteractionResult.SUCCESS;
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/TadpoleEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/TadpoleEntity.java
index 68cf763c3..42ef51f67 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/TadpoleEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/TadpoleEntity.java
@@ -62,6 +62,6 @@ public class TadpoleEntity extends AbstractFishEntity {
}
private boolean isFood(GeyserItemStack itemStack) {
- return session.getTagCache().is(ItemTag.FROG_FOOD, itemStack);
+ return itemStack.is(session, ItemTag.FROG_FOOD);
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/AnimalEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/AnimalEntity.java
index 57cbdc783..d0c097ec8 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/AnimalEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/AnimalEntity.java
@@ -53,7 +53,7 @@ public abstract class AnimalEntity extends AgeableEntity {
if (tag == null) {
return false;
}
- return session.getTagCache().is(tag, itemStack);
+ return itemStack.is(session, tag);
}
/**
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/ArmadilloEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/ArmadilloEntity.java
index 2b443f5e4..0ebd92021 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/ArmadilloEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/ArmadilloEntity.java
@@ -28,6 +28,8 @@ package org.geysermc.geyser.entity.type.living.animal;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector3f;
import org.geysermc.geyser.entity.EntityDefinition;
+import org.geysermc.geyser.entity.properties.type.EnumProperty;
+import org.geysermc.geyser.impl.IdentifierImpl;
import org.geysermc.geyser.item.type.Item;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.tags.ItemTag;
@@ -39,6 +41,13 @@ import java.util.UUID;
import java.util.concurrent.TimeUnit;
public class ArmadilloEntity extends AnimalEntity {
+
+ public static final EnumProperty STATE_PROPERTY = new EnumProperty<>(
+ IdentifierImpl.of("armadillo_state"),
+ State.class,
+ State.UNROLLED
+ );
+
private ArmadilloState armadilloState = ArmadilloState.IDLE;
public ArmadilloEntity(GeyserSession session, int entityId, long geyserId, UUID uuid,
@@ -50,10 +59,10 @@ public class ArmadilloEntity extends AnimalEntity {
armadilloState = entityMetadata.getValue();
switch (armadilloState) {
- case IDLE -> propertyManager.add("minecraft:armadillo_state", "unrolled");
- case ROLLING -> propertyManager.add("minecraft:armadillo_state", "rolled_up");
- case SCARED -> propertyManager.add("minecraft:armadillo_state", "rolled_up_relaxing");
- case UNROLLING -> propertyManager.add("minecraft:armadillo_state", "rolled_up_unrolling");
+ case IDLE -> STATE_PROPERTY.apply(propertyManager, State.UNROLLED);
+ case ROLLING -> STATE_PROPERTY.apply(propertyManager, State.ROLLED_UP);
+ case SCARED -> STATE_PROPERTY.apply(propertyManager, State.ROLLED_UP_RELAXING);
+ case UNROLLING -> STATE_PROPERTY.apply(propertyManager, State.ROLLED_UP_UNROLLING);
}
updateBedrockEntityProperties();
@@ -62,13 +71,13 @@ public class ArmadilloEntity extends AnimalEntity {
public void onPeeking() {
// Technically we should wait if not currently scared
if (armadilloState == ArmadilloState.SCARED) {
- propertyManager.add("minecraft:armadillo_state", "rolled_up_peeking");
+ STATE_PROPERTY.apply(propertyManager, State.ROLLED_UP_PEEKING);
updateBedrockEntityProperties();
// Needed for consecutive peeks
session.scheduleInEventLoop(() -> {
if (armadilloState == ArmadilloState.SCARED) {
- propertyManager.add("minecraft:armadillo_state", "rolled_up_relaxing");
+ STATE_PROPERTY.apply(propertyManager, State.ROLLED_UP_RELAXING);
updateBedrockEntityProperties();
}
}, 250, TimeUnit.MILLISECONDS);
@@ -80,4 +89,12 @@ public class ArmadilloEntity extends AnimalEntity {
protected Tag- getFoodTag() {
return ItemTag.ARMADILLO_FOOD;
}
+
+ public enum State {
+ UNROLLED,
+ ROLLED_UP,
+ ROLLED_UP_PEEKING,
+ ROLLED_UP_RELAXING,
+ ROLLED_UP_UNROLLING
+ }
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/BeeEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/BeeEntity.java
index 5f8956b6a..919af7c22 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/BeeEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/BeeEntity.java
@@ -32,6 +32,8 @@ import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket;
import org.geysermc.geyser.entity.EntityDefinition;
+import org.geysermc.geyser.entity.properties.type.BooleanProperty;
+import org.geysermc.geyser.impl.IdentifierImpl;
import org.geysermc.geyser.item.type.Item;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.tags.ItemTag;
@@ -43,6 +45,11 @@ import java.util.UUID;
public class BeeEntity extends AnimalEntity {
+ public static final BooleanProperty NECTAR_PROPERTY = new BooleanProperty(
+ IdentifierImpl.of("has_nectar"),
+ false
+ );
+
public BeeEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
}
@@ -60,7 +67,7 @@ public class BeeEntity extends AnimalEntity {
// If the bee has stung
dirtyMetadata.put(EntityDataTypes.MARK_VARIANT, (xd & 0x04) == 0x04 ? 1 : 0);
// If the bee has nectar or not
- propertyManager.add("minecraft:has_nectar", (xd & 0x08) == 0x08);
+ NECTAR_PROPERTY.apply(propertyManager, (xd & 0x08) == 0x08);
updateBedrockEntityProperties();
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/GoatEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/GoatEntity.java
index b954bb7a5..02d7c62de 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/GoatEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/GoatEntity.java
@@ -77,7 +77,7 @@ public class GoatEntity extends AnimalEntity {
@NonNull
@Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
- if (!getFlag(EntityFlag.BABY) && itemInHand.asItem() == Items.BUCKET) {
+ if (!getFlag(EntityFlag.BABY) && itemInHand.is(Items.BUCKET)) {
session.playSoundEvent(isScreamer ? SoundEvent.MILK_SCREAMER : SoundEvent.MILK, position);
return InteractionResult.SUCCESS;
} else {
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HappyGhastEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HappyGhastEntity.java
index 15bcd09fa..51f61c6bf 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HappyGhastEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HappyGhastEntity.java
@@ -33,11 +33,13 @@ import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.data.AttributeData;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.geysermc.geyser.entity.EntityDefinition;
+import org.geysermc.geyser.entity.properties.type.BooleanProperty;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
import org.geysermc.geyser.entity.vehicle.ClientVehicle;
import org.geysermc.geyser.entity.vehicle.HappyGhastVehicleComponent;
import org.geysermc.geyser.entity.vehicle.VehicleComponent;
+import org.geysermc.geyser.impl.IdentifierImpl;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.item.type.Item;
@@ -61,6 +63,11 @@ public class HappyGhastEntity extends AnimalEntity implements ClientVehicle {
public static final float[] X_OFFSETS = {0.0F, -1.7F, 0.0F, 1.7F};
public static final float[] Z_OFFSETS = {1.7F, 0.0F, -1.7F, 0.0F};
+ public static final BooleanProperty CAN_MOVE_PROPERTY = new BooleanProperty(
+ IdentifierImpl.of("can_move"),
+ true
+ );
+
private final HappyGhastVehicleComponent vehicleComponent = new HappyGhastVehicleComponent(this, 0.0f);
private boolean staysStill;
@@ -80,8 +87,6 @@ public class HappyGhastEntity extends AnimalEntity implements ClientVehicle {
setFlag(EntityFlag.WASD_AIR_CONTROLLED, true);
setFlag(EntityFlag.DOES_SERVER_AUTH_ONLY_DISMOUNT, true);
-
- propertyManager.add("minecraft:can_move", true);
}
@Override
@@ -111,7 +116,7 @@ public class HappyGhastEntity extends AnimalEntity implements ClientVehicle {
public void setStaysStill(BooleanEntityMetadata entityMetadata) {
staysStill = entityMetadata.getPrimitiveValue();
- propertyManager.add("minecraft:can_move", !entityMetadata.getPrimitiveValue());
+ CAN_MOVE_PROPERTY.apply(propertyManager, !entityMetadata.getPrimitiveValue());
updateBedrockEntityProperties();
}
@@ -122,12 +127,12 @@ public class HappyGhastEntity extends AnimalEntity implements ClientVehicle {
return super.testMobInteraction(hand, itemInHand);
} else {
if (!itemInHand.isEmpty()) {
- if (session.getTagCache().is(ItemTag.HARNESSES, itemInHand)) {
- if (this.equipment.get(EquipmentSlot.BODY) == null) {
+ if (itemInHand.is(session, ItemTag.HARNESSES)) {
+ if (getItemInSlot(EquipmentSlot.BODY).isEmpty()) {
// Harnesses the ghast
return InteractiveTag.EQUIP_HARNESS;
}
- } else if (itemInHand.asItem() == Items.SHEARS) {
+ } else if (itemInHand.is(Items.SHEARS)) {
if (this.canShearEquipment() && !session.isSneaking()) {
// Shears the harness off of the ghast
return InteractiveTag.REMOVE_HARNESS;
@@ -135,7 +140,7 @@ public class HappyGhastEntity extends AnimalEntity implements ClientVehicle {
}
}
- if (this.equipment.get(EquipmentSlot.BODY) != null && !session.isSneaking()) {
+ if (!getItemInSlot(EquipmentSlot.BODY).isEmpty() && !session.isSneaking()) {
// Rides happy ghast
return InteractiveTag.RIDE_HORSE;
} else {
@@ -151,12 +156,12 @@ public class HappyGhastEntity extends AnimalEntity implements ClientVehicle {
return super.mobInteract(hand, itemInHand);
} else {
if (!itemInHand.isEmpty()) {
- if (session.getTagCache().is(ItemTag.HARNESSES, itemInHand)) {
- if (this.equipment.get(EquipmentSlot.BODY) == null) {
+ if (itemInHand.is(session, ItemTag.HARNESSES)) {
+ if (getItemInSlot(EquipmentSlot.BODY).isEmpty()) {
// Harnesses the ghast
return InteractionResult.SUCCESS;
}
- } else if (itemInHand.asItem() == Items.SHEARS) {
+ } else if (itemInHand.is(Items.SHEARS)) {
if (this.canShearEquipment() && !session.isSneaking()) {
// Shears the harness off of the ghast
return InteractionResult.SUCCESS;
@@ -164,7 +169,7 @@ public class HappyGhastEntity extends AnimalEntity implements ClientVehicle {
}
}
- if (this.equipment.get(EquipmentSlot.BODY) == null && !session.isSneaking()) {
+ if (!getItemInSlot(EquipmentSlot.BODY).isEmpty() && !session.isSneaking()) {
// Rides happy ghast
return InteractionResult.SUCCESS;
} else {
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java
index 3314344cb..78f2ea025 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java
@@ -63,10 +63,10 @@ public class MooshroomEntity extends CowEntity {
@Override
protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
if (!isBaby()) {
- if (itemInHand.asItem() == Items.BOWL) {
+ if (itemInHand.is(Items.BOWL)) {
// Stew
return InteractiveTag.MOOSHROOM_MILK_STEW;
- } else if (isAlive() && itemInHand.asItem() == Items.SHEARS) {
+ } else if (isAlive() && itemInHand.is(Items.SHEARS)) {
// Shear items
return InteractiveTag.MOOSHROOM_SHEAR;
}
@@ -78,13 +78,13 @@ public class MooshroomEntity extends CowEntity {
@Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
boolean isBaby = isBaby();
- if (!isBaby && itemInHand.asItem() == Items.BOWL) {
+ if (!isBaby && itemInHand.is(Items.BOWL)) {
// Stew
return InteractionResult.SUCCESS;
- } else if (!isBaby && isAlive() && itemInHand.asItem() == Items.SHEARS) {
+ } else if (!isBaby && isAlive() && itemInHand.is(Items.SHEARS)) {
// Shear items
return InteractionResult.SUCCESS;
- } else if (isBrown && session.getTagCache().is(ItemTag.SMALL_FLOWERS, itemInHand)) {
+ } else if (isBrown && itemInHand.is(session, ItemTag.SMALL_FLOWERS)) {
// ?
return InteractionResult.SUCCESS;
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PandaEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PandaEntity.java
index 022e58bc0..358643b0e 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PandaEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PandaEntity.java
@@ -64,7 +64,7 @@ public class PandaEntity extends AnimalEntity {
packet.setRuntimeEntityId(geyserId);
packet.setType(EntityEventType.EATING_ITEM);
// As of 1.20.5 - pandas can eat cake
- packet.setData(this.hand.getDefinition().getRuntimeId() << 16);
+ packet.setData(session.getItemMappings().getMapping(getMainHandItem()).getBedrockDefinition().getRuntimeId() << 16);
session.sendUpstreamPacket(packet);
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/SheepEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/SheepEntity.java
index e26b0be61..35863071c 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/SheepEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/SheepEntity.java
@@ -68,7 +68,7 @@ public class SheepEntity extends AnimalEntity {
@NonNull
@Override
protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
- if (itemInHand.asItem() == Items.SHEARS) {
+ if (itemInHand.is(Items.SHEARS)) {
return InteractiveTag.SHEAR;
} else {
InteractiveTag tag = super.testMobInteraction(hand, itemInHand);
@@ -86,7 +86,7 @@ public class SheepEntity extends AnimalEntity {
@NonNull
@Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
- if (itemInHand.asItem() == Items.SHEARS) {
+ if (itemInHand.is(Items.SHEARS)) {
return InteractionResult.CONSUME;
} else {
InteractionResult superResult = super.mobInteract(hand, itemInHand);
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java
index 36a126687..186ecd2dd 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java
@@ -29,7 +29,6 @@ import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector2f;
import org.cloudburstmc.math.vector.Vector3f;
-import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.entity.type.Entity;
@@ -157,8 +156,7 @@ public class StriderEntity extends AnimalEntity implements Tickable, ClientVehic
vehicleComponent.tickBoost();
}
} else { // getHand() for session player seems to always return air
- ItemDefinition itemDefinition = session.getItemMappings().getStoredItems().warpedFungusOnAStick().getBedrockDefinition();
- if (player.getHand().getDefinition() == itemDefinition || player.getOffhand().getDefinition() == itemDefinition) {
+ if (player.isHolding(Items.WARPED_FUNGUS_ON_A_STICK)) {
vehicleComponent.tickBoost();
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java
index de79e9a52..8c1a27b95 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java
@@ -54,7 +54,7 @@ public class CowEntity extends TemperatureVariantAnimal {
@NonNull
@Override
protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
- if (getFlag(EntityFlag.BABY) || itemInHand.asItem() != Items.BUCKET) {
+ if (getFlag(EntityFlag.BABY) || !itemInHand.is(Items.BUCKET)) {
return super.testMobInteraction(hand, itemInHand);
}
@@ -64,7 +64,7 @@ public class CowEntity extends TemperatureVariantAnimal {
@NonNull
@Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
- if (getFlag(EntityFlag.BABY) || itemInHand.asItem() != Items.BUCKET) {
+ if (getFlag(EntityFlag.BABY) || !itemInHand.is(Items.BUCKET)) {
return super.mobInteract(hand, itemInHand);
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java
index 7ae672bcc..bb629c3d7 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java
@@ -29,7 +29,6 @@ import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector2f;
import org.cloudburstmc.math.vector.Vector3f;
-import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.entity.type.Tickable;
@@ -116,8 +115,7 @@ public class PigEntity extends TemperatureVariantAnimal implements Tickable, Cli
vehicleComponent.tickBoost();
}
} else { // getHand() for session player seems to always return air
- ItemDefinition itemDefinition = session.getItemMappings().getStoredItems().carrotOnAStick().getBedrockDefinition();
- if (player.getHand().getDefinition() == itemDefinition || player.getOffhand().getDefinition() == itemDefinition) {
+ if (player.isHolding(Items.CARROT_ON_A_STICK)) {
vehicleComponent.tickBoost();
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java
index b61a3a80d..9d772c8a2 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java
@@ -26,19 +26,24 @@
package org.geysermc.geyser.entity.type.living.animal.farm;
import org.cloudburstmc.math.vector.Vector3f;
-import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket;
import org.geysermc.geyser.entity.EntityDefinition;
-import org.geysermc.geyser.entity.properties.VanillaEntityProperties;
+import org.geysermc.geyser.entity.properties.type.EnumProperty;
import org.geysermc.geyser.entity.type.living.animal.AnimalEntity;
import org.geysermc.geyser.entity.type.living.animal.VariantHolder;
+import org.geysermc.geyser.impl.IdentifierImpl;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.RegistryCache;
-import java.util.Locale;
import java.util.UUID;
public abstract class TemperatureVariantAnimal extends AnimalEntity implements VariantHolder {
+ public static final EnumProperty TEMPERATE_VARIANT_PROPERTY = new EnumProperty<>(
+ IdentifierImpl.of("climate_variant"),
+ BuiltInVariant.class,
+ BuiltInVariant.TEMPERATE
+ );
+
public static final RegistryCache.RegistryReader VARIANT_READER = VariantHolder.reader(BuiltInVariant.class, BuiltInVariant.TEMPERATE);
public TemperatureVariantAnimal(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition> definition,
@@ -46,25 +51,15 @@ public abstract class TemperatureVariantAnimal extends AnimalEntity implements V
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
}
- @Override
- public void addAdditionalSpawnData(AddEntityPacket addEntityPacket) {
- propertyManager.add(VanillaEntityProperties.CLIMATE_VARIANT_ID, "temperate");
- propertyManager.applyIntProperties(addEntityPacket.getProperties().getIntProperties());
- }
-
@Override
public void setBedrockVariant(BuiltInVariant variant) {
- propertyManager.add(VanillaEntityProperties.CLIMATE_VARIANT_ID, variant.toBedrock());
+ TEMPERATE_VARIANT_PROPERTY.apply(propertyManager, variant);
updateBedrockEntityProperties();
}
public enum BuiltInVariant implements VariantHolder.BuiltIn {
- COLD,
TEMPERATE,
- WARM;
-
- public String toBedrock() {
- return name().toLowerCase(Locale.ROOT);
- }
+ WARM,
+ COLD;
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java
index 8b0e77c73..65e2ed051 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java
@@ -165,7 +165,7 @@ public class AbstractHorseEntity extends AnimalEntity {
return InteractiveTag.ATTACH_CHEST;
}
- if (additionalTestForInventoryOpen(itemInHand) || !isBaby && !getFlag(EntityFlag.SADDLED) && itemInHand.asItem() == Items.SADDLE) {
+ if (additionalTestForInventoryOpen(itemInHand) || !isBaby && !getFlag(EntityFlag.SADDLED) && itemInHand.is(Items.SADDLE)) {
// Will open the inventory to be saddled
return InteractiveTag.OPEN_CONTAINER;
}
@@ -221,7 +221,7 @@ public class AbstractHorseEntity extends AnimalEntity {
}
// Note: yes, this code triggers for llamas too. lol (as of Java Edition 1.18.1)
- if (additionalTestForInventoryOpen(itemInHand) || (!isBaby && !getFlag(EntityFlag.SADDLED) && itemInHand.asItem() == Items.SADDLE)) {
+ if (additionalTestForInventoryOpen(itemInHand) || (!isBaby && !getFlag(EntityFlag.SADDLED) && itemInHand.is(Items.SADDLE))) {
// Will open the inventory to be saddled
return InteractionResult.SUCCESS;
}
@@ -245,6 +245,7 @@ public class AbstractHorseEntity extends AnimalEntity {
}
protected boolean additionalTestForInventoryOpen(@NonNull GeyserItemStack itemInHand) {
+ // TODO this doesn't seem right anymore... (as of Java 1.21.9)
return itemInHand.asItem().javaIdentifier().endsWith("_horse_armor");
}
@@ -260,7 +261,7 @@ public class AbstractHorseEntity extends AnimalEntity {
} else if (!passengers.isEmpty()) {
return testHorseInteraction(hand, itemInHand);
} else {
- if (Items.SADDLE == itemInHand.asItem()) {
+ if (itemInHand.is(Items.SADDLE)) {
return InteractiveTag.OPEN_CONTAINER;
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/ChestedHorseEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/ChestedHorseEntity.java
index fc8584de3..491ca02df 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/ChestedHorseEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/ChestedHorseEntity.java
@@ -54,7 +54,7 @@ public class ChestedHorseEntity extends AbstractHorseEntity {
@Override
protected boolean testForChest(@NonNull GeyserItemStack itemInHand) {
- return itemInHand.asItem() == Items.CHEST && !getFlag(EntityFlag.CHESTED);
+ return itemInHand.is(Items.CHEST) && !getFlag(EntityFlag.CHESTED);
}
@Override
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/ParrotEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/ParrotEntity.java
index 95e9c901b..ef6121456 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/ParrotEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/ParrotEntity.java
@@ -52,21 +52,21 @@ public class ParrotEntity extends TameableEntity {
return null;
}
- private boolean isTameFood(Item item) {
- return session.getTagCache().is(ItemTag.PARROT_FOOD, item);
+ private boolean isTameFood(GeyserItemStack item) {
+ return item.is(session, ItemTag.PARROT_FOOD);
}
- private boolean isPoisonousFood(Item item) {
- return session.getTagCache().is(ItemTag.PARROT_POISONOUS_FOOD, item);
+ private boolean isPoisonousFood(GeyserItemStack item) {
+ return item.is(session, ItemTag.PARROT_POISONOUS_FOOD);
}
@NonNull
@Override
protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
boolean tame = getFlag(EntityFlag.TAMED);
- if (!tame && isTameFood(itemInHand.asItem())) {
+ if (!tame && isTameFood(itemInHand)) {
return InteractiveTag.FEED;
- } else if (isPoisonousFood(itemInHand.asItem())) {
+ } else if (isPoisonousFood(itemInHand)) {
return InteractiveTag.FEED;
} else if (onGround && tame && ownerBedrockId == session.getPlayerEntity().getGeyserId()) {
// Sitting/standing
@@ -79,9 +79,9 @@ public class ParrotEntity extends TameableEntity {
@Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
boolean tame = getFlag(EntityFlag.TAMED);
- if (!tame && isTameFood(itemInHand.asItem())) {
+ if (!tame && isTameFood(itemInHand)) {
return InteractionResult.SUCCESS;
- } else if (isPoisonousFood(itemInHand.asItem())) {
+ } else if (isPoisonousFood(itemInHand)) {
return InteractionResult.SUCCESS;
} else if (onGround && tame && ownerBedrockId == session.getPlayerEntity().getGeyserId()) {
// Sitting/standing
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java
index 46911480e..eee7a0052 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java
@@ -30,10 +30,11 @@ import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
-import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket;
import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket;
import org.geysermc.geyser.entity.EntityDefinition;
+import org.geysermc.geyser.entity.properties.type.StringEnumProperty;
import org.geysermc.geyser.entity.type.living.animal.VariantIntHolder;
+import org.geysermc.geyser.impl.IdentifierImpl;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.item.enchantment.EnchantmentComponent;
@@ -47,6 +48,7 @@ import org.geysermc.geyser.session.cache.tags.Tag;
import org.geysermc.geyser.util.InteractionResult;
import org.geysermc.geyser.util.InteractiveTag;
import org.geysermc.geyser.util.ItemUtils;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode;
@@ -55,9 +57,25 @@ import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponen
import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet;
import java.util.Collections;
+import java.util.List;
import java.util.UUID;
public class WolfEntity extends TameableEntity implements VariantIntHolder {
+
+ public static final StringEnumProperty SOUND_VARIANT = new StringEnumProperty(
+ IdentifierImpl.of("sound_variant"),
+ List.of(
+ "default",
+ "big",
+ "cute",
+ "grumpy",
+ "mad",
+ "puglin",
+ "sad"
+ ),
+ null
+ );
+
private byte collarColor = 14; // Red - default
private HolderSet repairableItems = null;
private boolean isCurseOfBinding = false;
@@ -66,12 +84,6 @@ public class WolfEntity extends TameableEntity implements VariantIntHolder {
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
}
- @Override
- public void addAdditionalSpawnData(AddEntityPacket addEntityPacket) {
- propertyManager.add("minecraft:sound_variant", "default");
- propertyManager.applyIntProperties(addEntityPacket.getProperties().getIntProperties());
- }
-
@Override
public void setTameableFlags(ByteEntityMetadata entityMetadata) {
super.setTameableFlags(entityMetadata);
@@ -146,7 +158,7 @@ public class WolfEntity extends TameableEntity implements VariantIntHolder {
if (getFlag(EntityFlag.ANGRY)) {
return InteractiveTag.NONE;
}
- if (itemInHand.asItem() == Items.BONE && !getFlag(EntityFlag.TAMED)) {
+ if (itemInHand.is(Items.BONE) && !getFlag(EntityFlag.TAMED)) {
// Bone and untamed - can tame
return InteractiveTag.TAME;
}
@@ -159,17 +171,15 @@ public class WolfEntity extends TameableEntity implements VariantIntHolder {
return super.testMobInteraction(hand, itemInHand);
}
}
- if (itemInHand.asItem() == Items.WOLF_ARMOR && !this.body.isValid() && !getFlag(EntityFlag.BABY)) {
+ if (itemInHand.is(Items.WOLF_ARMOR) && !getItemInSlot(EquipmentSlot.BODY).isEmpty() && !getFlag(EntityFlag.BABY)) {
return InteractiveTag.EQUIP_WOLF_ARMOR;
}
- if (itemInHand.asItem() == Items.SHEARS && this.body.isValid()
+ if (itemInHand.is(Items.SHEARS) && !getItemInSlot(EquipmentSlot.BODY).isEmpty()
&& (!isCurseOfBinding || session.getGameMode().equals(GameMode.CREATIVE))) {
return InteractiveTag.REMOVE_WOLF_ARMOR;
}
- if (getFlag(EntityFlag.SITTING) &&
- session.getTagCache().isItem(repairableItems, itemInHand.asItem()) &&
- this.body.isValid() && this.body.getTag() != null &&
- this.body.getTag().getInt("Damage") > 0) {
+ if (getFlag(EntityFlag.SITTING) && itemInHand.is(session, repairableItems) &&
+ !getItemInSlot(EquipmentSlot.BODY).isEmpty() && getItemInSlot(EquipmentSlot.BODY).isDamaged()) {
return InteractiveTag.REPAIR_WOLF_ARMOR;
}
// Tamed and owned by player - can sit/stand
@@ -182,7 +192,7 @@ public class WolfEntity extends TameableEntity implements VariantIntHolder {
@Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
if (ownerBedrockId == session.getPlayerEntity().getGeyserId() || getFlag(EntityFlag.TAMED)
- || itemInHand.asItem() == Items.BONE && !getFlag(EntityFlag.ANGRY)) {
+ || itemInHand.is(Items.BONE) && !getFlag(EntityFlag.ANGRY)) {
// Sitting toggle or feeding; not angry
return InteractionResult.CONSUME;
} else {
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/merchant/AbstractMerchantEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/merchant/AbstractMerchantEntity.java
index 2492aabd7..ecc517489 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/merchant/AbstractMerchantEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/merchant/AbstractMerchantEntity.java
@@ -54,7 +54,7 @@ public class AbstractMerchantEntity extends AgeableEntity {
@NonNull
@Override
protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
- if (itemInHand.asItem() != Items.VILLAGER_SPAWN_EGG
+ if (!itemInHand.is(Items.VILLAGER_SPAWN_EGG)
&& (definition != EntityDefinitions.VILLAGER || !getFlag(EntityFlag.SLEEPING) && ((VillagerEntity) this).isCanTradeWith())) {
// An additional check we know cannot work
if (!isBaby()) {
@@ -67,7 +67,7 @@ public class AbstractMerchantEntity extends AgeableEntity {
@NonNull
@Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
- if (itemInHand.asItem() != Items.VILLAGER_SPAWN_EGG
+ if (!itemInHand.is(Items.VILLAGER_SPAWN_EGG)
&& (definition != EntityDefinitions.VILLAGER || !getFlag(EntityFlag.SLEEPING))
&& (definition != EntityDefinitions.WANDERING_TRADER || !getFlag(EntityFlag.BABY))) {
// Trading time
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/AbstractSkeletonEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/AbstractSkeletonEntity.java
index d08fff06a..16b5cc01c 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/AbstractSkeletonEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/AbstractSkeletonEntity.java
@@ -26,10 +26,10 @@
package org.geysermc.geyser.entity.type.living.monster;
import org.cloudburstmc.math.vector.Vector3f;
-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.geysermc.geyser.entity.EntityDefinition;
+import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
@@ -49,8 +49,7 @@ public class AbstractSkeletonEntity extends MonsterEntity {
dirtyMetadata.put(EntityDataTypes.TARGET_EID, ((xd & 4) == 4) ? geyserId : 0);
if ((xd & 4) == 4) {
- ItemDefinition bow = session.getItemMappings().getStoredItems().bow().getBedrockDefinition();
- setFlag(EntityFlag.FACING_TARGET_TO_RANGE_ATTACK, this.hand.getDefinition() == bow || this.offhand.getDefinition() == bow);
+ setFlag(EntityFlag.FACING_TARGET_TO_RANGE_ATTACK, isHolding(Items.BOW));
} else {
setFlag(EntityFlag.FACING_TARGET_TO_RANGE_ATTACK, false);
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/BoggedEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/BoggedEntity.java
index 806d58ed1..c4d8eeff3 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/BoggedEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/BoggedEntity.java
@@ -53,7 +53,7 @@ public class BoggedEntity extends AbstractSkeletonEntity {
@Override
protected @NonNull InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
- if (itemInHand.asItem() == Items.SHEARS && readyForShearing()) {
+ if (itemInHand.is(Items.SHEARS) && readyForShearing()) {
return InteractiveTag.SHEAR;
}
return super.testMobInteraction(hand, itemInHand);
@@ -61,7 +61,7 @@ public class BoggedEntity extends AbstractSkeletonEntity {
@Override
protected @NonNull InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
- if (itemInHand.asItem() == Items.SHEARS && readyForShearing()) {
+ if (itemInHand.is(Items.SHEARS) && readyForShearing()) {
return InteractionResult.SUCCESS;
}
return super.mobInteract(hand, itemInHand);
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/CreakingEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/CreakingEntity.java
index 166cdc053..dc9755a2b 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/CreakingEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/CreakingEntity.java
@@ -30,9 +30,11 @@ import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.protocol.bedrock.data.LevelEvent;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
-import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket;
import org.cloudburstmc.protocol.bedrock.packet.LevelEventGenericPacket;
import org.geysermc.geyser.entity.EntityDefinition;
+import org.geysermc.geyser.entity.properties.type.EnumProperty;
+import org.geysermc.geyser.entity.properties.type.IntProperty;
+import org.geysermc.geyser.impl.IdentifierImpl;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.MetadataType;
@@ -41,8 +43,23 @@ import java.util.Optional;
import java.util.UUID;
public class CreakingEntity extends MonsterEntity {
- public static final String CREAKING_STATE = "minecraft:creaking_state";
- public static final String CREAKING_SWAYING_TICKS = "minecraft:creaking_swaying_ticks";
+
+ public static final EnumProperty STATE_PROPERTY = new EnumProperty<>(
+ IdentifierImpl.of("creaking_state"),
+ CreakingState.class,
+ CreakingState.NEUTRAL
+ );
+
+ // also, the creaking seems to have this minecraft:creaking_swaying_ticks thingy
+ // which i guess is responsible for some animation?
+ // it's sent over the network, all 6 "stages" 50ms in between of each other.
+ // no clue what it's used for tbh, so i'm not gonna bother implementing it
+ // - chris
+ // update: this still holds true, even a refactor later :(
+ public static final IntProperty SWAYING_TICKS_PROPERTY = new IntProperty(
+ IdentifierImpl.of("creaking_swaying_ticks"),
+ 6, 0, 0
+ );
private Vector3i homePosition;
@@ -56,33 +73,21 @@ public class CreakingEntity extends MonsterEntity {
setFlag(EntityFlag.FIRE_IMMUNE, true);
}
- @Override
- public void addAdditionalSpawnData(AddEntityPacket addEntityPacket) {
- propertyManager.add(CREAKING_STATE, "neutral");
- // also, the creaking seems to have this minecraft:creaking_swaying_ticks thingy
- // which i guess is responsible for some animation?
- // it's sent over the network, all 6 "stages" 50ms in between of each other.
- // no clue what it's used for tbh, so i'm not gonna bother implementing it
- // - chris
- propertyManager.add(CREAKING_SWAYING_TICKS, 0);
- propertyManager.applyIntProperties(addEntityPacket.getProperties().getIntProperties());
- }
-
public void setCanMove(EntityMetadata> booleanEntityMetadata) {
setFlag(EntityFlag.BODY_ROTATION_BLOCKED, !booleanEntityMetadata.getValue());
- propertyManager.add(CREAKING_STATE, booleanEntityMetadata.getValue() ? "hostile_unobserved" : "hostile_observed");
+ STATE_PROPERTY.apply(propertyManager, booleanEntityMetadata.getValue() ? CreakingState.HOSTILE_UNOBSERVED : CreakingState.HOSTILE_OBSERVED);
updateBedrockEntityProperties();
}
public void setActive(EntityMetadata> booleanEntityMetadata) {
if (!booleanEntityMetadata.getValue()) {
- propertyManager.add(CREAKING_STATE, "neutral");
+ STATE_PROPERTY.apply(propertyManager, CreakingState.NEUTRAL);
}
}
public void setIsTearingDown(EntityMetadata> booleanEntityMetadata) {
if (booleanEntityMetadata.getValue()) {
- propertyManager.add(CREAKING_STATE, "crumbling");
+ STATE_PROPERTY.apply(propertyManager, CreakingState.CRUMBLING);
updateBedrockEntityProperties();
}
}
@@ -115,4 +120,12 @@ public class CreakingEntity extends MonsterEntity {
session.sendUpstreamPacket(levelEventGenericPacket);
}
}
+
+ public enum CreakingState {
+ NEUTRAL,
+ HOSTILE_OBSERVED,
+ HOSTILE_UNOBSERVED,
+ TWITCHING,
+ CRUMBLING
+ }
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/CreeperEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/CreeperEntity.java
index 5f54d2942..cc58aa7aa 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/CreeperEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/CreeperEntity.java
@@ -66,7 +66,7 @@ public class CreeperEntity extends MonsterEntity {
@NonNull
@Override
protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
- if (session.getTagCache().is(ItemTag.CREEPER_IGNITERS, itemInHand)) {
+ if (itemInHand.is(session, ItemTag.CREEPER_IGNITERS)) {
return InteractiveTag.IGNITE_CREEPER;
} else {
return super.testMobInteraction(hand, itemInHand);
@@ -76,7 +76,7 @@ public class CreeperEntity extends MonsterEntity {
@NonNull
@Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
- if (session.getTagCache().is(ItemTag.CREEPER_IGNITERS, itemInHand)) {
+ if (itemInHand.is(session, ItemTag.CREEPER_IGNITERS)) {
// Ignite creeper - as of 1.19.3
session.playSoundEvent(SoundEvent.IGNITE, position);
return InteractionResult.SUCCESS;
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PiglinEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PiglinEntity.java
index 36c412ba7..b35e6a17f 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PiglinEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PiglinEntity.java
@@ -35,13 +35,13 @@ import org.cloudburstmc.protocol.bedrock.packet.MobEquipmentPacket;
import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.item.Items;
-import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.tags.ItemTag;
import org.geysermc.geyser.util.InteractionResult;
import org.geysermc.geyser.util.InteractiveTag;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand;
+import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes;
import java.util.UUID;
@@ -71,17 +71,16 @@ public class PiglinEntity extends BasePiglinEntity {
@Override
public void setHand(GeyserItemStack stack) {
- ItemMapping crossbow = session.getItemMappings().getStoredItems().crossbow();
- boolean toCrossbow = stack != null && stack.asItem() == crossbow.getJavaItem();
+ boolean toCrossbow = stack != null && stack.is(Items.CROSSBOW);
- if (toCrossbow ^ this.hand.getDefinition() == crossbow.getBedrockDefinition()) { // If switching to/from crossbow
+ if (toCrossbow ^ getMainHandItem().is(Items.CROSSBOW)) { // If switching to/from crossbow
dirtyMetadata.put(EntityDataTypes.BLOCK, session.getBlockMappings().getDefinition(toCrossbow ? 0 : 1));
dirtyMetadata.put(EntityDataTypes.CHARGE_AMOUNT, (byte) 0);
setFlag(EntityFlag.CHARGED, false);
setFlag(EntityFlag.USING_ITEM, false);
updateBedrockMetadata();
- if (this.hand.isValid()) {
+ if (!getMainHandItem().isEmpty()) {
MobEquipmentPacket mobEquipmentPacket = new MobEquipmentPacket();
mobEquipmentPacket.setRuntimeEntityId(geyserId);
mobEquipmentPacket.setContainerId(ContainerId.INVENTORY);
@@ -96,11 +95,11 @@ public class PiglinEntity extends BasePiglinEntity {
}
@Override
- public void updateMainHand(GeyserSession session) {
- super.updateMainHand(session);
+ public void updateMainHand() {
+ super.updateMainHand();
- if (this.hand.getDefinition() == session.getItemMappings().getStoredItems().crossbow().getBedrockDefinition()) {
- if (this.hand.getTag() != null && this.hand.getTag().containsKey("chargedItem")) {
+ if (getMainHandItem().is(Items.CROSSBOW)) {
+ if (getMainHandItem().getComponent(DataComponentTypes.CHARGED_PROJECTILES) != null) {
dirtyMetadata.put(EntityDataTypes.CHARGE_AMOUNT, Byte.MAX_VALUE);
setFlag(EntityFlag.CHARGING, false);
setFlag(EntityFlag.CHARGED, true);
@@ -116,12 +115,12 @@ public class PiglinEntity extends BasePiglinEntity {
}
@Override
- public void updateOffHand(GeyserSession session) {
+ public void updateOffHand() {
// Check if the Piglin is holding Gold and set the ADMIRING flag accordingly so its pose updates
- setFlag(EntityFlag.ADMIRING, session.getTagCache().is(ItemTag.PIGLIN_LOVED, session.getItemMappings().getMapping(this.offhand).getJavaItem()));
+ setFlag(EntityFlag.ADMIRING, getOffHandItem().is(session, ItemTag.PIGLIN_LOVED));
super.updateBedrockMetadata();
- super.updateOffHand(session);
+ super.updateOffHand();
}
@NonNull
@@ -147,6 +146,6 @@ public class PiglinEntity extends BasePiglinEntity {
}
private boolean canGiveGoldTo(@NonNull GeyserItemStack itemInHand) {
- return !getFlag(EntityFlag.BABY) && itemInHand.asItem() == Items.GOLD_INGOT && !getFlag(EntityFlag.ADMIRING);
+ return !getFlag(EntityFlag.BABY) && itemInHand.is(Items.GOLD_INGOT) && !getFlag(EntityFlag.ADMIRING);
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZombieVillagerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZombieVillagerEntity.java
index 6e03e4f98..e09568102 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZombieVillagerEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZombieVillagerEntity.java
@@ -70,7 +70,7 @@ public class ZombieVillagerEntity extends ZombieEntity {
@NonNull
@Override
protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
- if (itemInHand.asItem() == Items.GOLDEN_APPLE) {
+ if (itemInHand.is(Items.GOLDEN_APPLE)) {
return InteractiveTag.CURE;
} else {
return super.testMobInteraction(hand, itemInHand);
@@ -80,7 +80,7 @@ public class ZombieVillagerEntity extends ZombieEntity {
@NonNull
@Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
- if (itemInHand.asItem() == Items.GOLDEN_APPLE) {
+ if (itemInHand.is(Items.GOLDEN_APPLE)) {
// The client doesn't know if the entity has weakness as that's not usually sent over the network
return InteractionResult.CONSUME;
} else {
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/raid/PillagerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/raid/PillagerEntity.java
index fd7448e29..e73052286 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/raid/PillagerEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/raid/PillagerEntity.java
@@ -28,11 +28,12 @@ package org.geysermc.geyser.entity.type.living.monster.raid;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
-import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
import org.geysermc.geyser.entity.EntityDefinition;
-import org.geysermc.geyser.registry.type.ItemMapping;
+import org.geysermc.geyser.inventory.GeyserItemStack;
+import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
+import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes;
import java.util.UUID;
@@ -49,33 +50,32 @@ public class PillagerEntity extends AbstractIllagerEntity {
}
@Override
- public void updateMainHand(GeyserSession session) {
+ public void updateMainHand() {
updateCrossbow();
- super.updateMainHand(session);
+ super.updateMainHand();
}
@Override
- public void updateOffHand(GeyserSession session) {
+ public void updateOffHand() {
updateCrossbow();
- super.updateOffHand(session);
+ super.updateOffHand();
}
/**
* Check for a crossbow in either the mainhand or offhand. If one exists, indicate that the pillager should be posing
*/
protected void updateCrossbow() {
- ItemMapping crossbow = session.getItemMappings().getStoredItems().crossbow();
- ItemData activeCrossbow = null;
- if (this.hand.getDefinition() == crossbow.getBedrockDefinition()) {
- activeCrossbow = this.hand;
- } else if (this.offhand.getDefinition() == crossbow.getBedrockDefinition()) {
- activeCrossbow = this.offhand;
+ GeyserItemStack activeCrossbow = null;
+ if (getMainHandItem().is(Items.CROSSBOW)) {
+ activeCrossbow = getMainHandItem();
+ } else if (getOffHandItem().is(Items.CROSSBOW)) {
+ activeCrossbow = getOffHandItem();
}
if (activeCrossbow != null) {
- if (activeCrossbow.getTag() != null && activeCrossbow.getTag().containsKey("chargedItem")) {
+ if (activeCrossbow.getComponent(DataComponentTypes.CHARGED_PROJECTILES) != null) {
dirtyMetadata.put(EntityDataTypes.CHARGE_AMOUNT, Byte.MAX_VALUE);
setFlag(EntityFlag.CHARGING, false);
setFlag(EntityFlag.CHARGED, true);
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/AvatarEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/AvatarEntity.java
new file mode 100644
index 000000000..f8bb7f658
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/AvatarEntity.java
@@ -0,0 +1,361 @@
+/*
+ * Copyright (c) 2025 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.entity.type.player;
+
+import lombok.Getter;
+import lombok.Setter;
+import net.kyori.adventure.text.Component;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.cloudburstmc.math.vector.Vector3f;
+import org.cloudburstmc.math.vector.Vector3i;
+import org.cloudburstmc.protocol.bedrock.data.Ability;
+import org.cloudburstmc.protocol.bedrock.data.AbilityLayer;
+import org.cloudburstmc.protocol.bedrock.data.GameType;
+import org.cloudburstmc.protocol.bedrock.data.PlayerPermission;
+import org.cloudburstmc.protocol.bedrock.data.command.CommandPermission;
+import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
+import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
+import org.cloudburstmc.protocol.bedrock.packet.AddPlayerPacket;
+import org.cloudburstmc.protocol.bedrock.packet.MovePlayerPacket;
+import org.geysermc.geyser.entity.EntityDefinition;
+import org.geysermc.geyser.entity.type.LivingEntity;
+import org.geysermc.geyser.level.block.Blocks;
+import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.skin.SkinManager;
+import org.geysermc.geyser.skin.SkullSkinManager;
+import org.geysermc.geyser.translator.item.ItemTranslator;
+import org.geysermc.geyser.util.ChunkUtils;
+import org.geysermc.mcprotocollib.auth.GameProfile;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.player.ResolvableProfile;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.UUID;
+
+public class AvatarEntity extends LivingEntity {
+ public static final float SNEAKING_POSE_HEIGHT = 1.5f;
+ protected static final List BASE_ABILITY_LAYER;
+
+ @Getter
+ protected String username;
+
+ /**
+ * The textures property from the GameProfile.
+ */
+ @Getter
+ @Setter
+ @Nullable
+ protected String texturesProperty; // TODO no direct setter, rather one that updates the skin
+
+ private String cachedScore = "";
+ private boolean scoreVisible = true;
+
+ @Getter
+ @Nullable
+ private Vector3i bedPosition;
+
+ static {
+ AbilityLayer abilityLayer = new AbilityLayer();
+ abilityLayer.setLayerType(AbilityLayer.Type.BASE);
+ Ability[] abilities = Ability.values();
+ Collections.addAll(abilityLayer.getAbilitiesSet(), abilities); // Apparently all the abilities you're working with
+ Collections.addAll(abilityLayer.getAbilityValues(), abilities); // Apparently all the abilities the player can work with
+ BASE_ABILITY_LAYER = Collections.singletonList(abilityLayer);
+ }
+
+ public AvatarEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition> definition,
+ Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw, String username) {
+ super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
+ this.username = username;
+ this.nametag = username;
+ }
+
+ @Override
+ protected void initializeMetadata() {
+ super.initializeMetadata();
+ // For the OptionalPack, set all bits as invisible by default as this matches Java Edition behavior
+ dirtyMetadata.put(EntityDataTypes.MARK_VARIANT, 0xff);
+ }
+
+ @Override
+ public void spawnEntity() {
+ AddPlayerPacket addPlayerPacket = new AddPlayerPacket();
+ addPlayerPacket.setUuid(uuid);
+ addPlayerPacket.setUsername(username);
+ addPlayerPacket.setRuntimeEntityId(geyserId);
+ addPlayerPacket.setUniqueEntityId(geyserId);
+ addPlayerPacket.setPosition(position.sub(0, definition.offset(), 0));
+ addPlayerPacket.setRotation(getBedrockRotation());
+ addPlayerPacket.setMotion(motion);
+ addPlayerPacket.setHand(ItemTranslator.translateToBedrock(session, getMainHandItem()));
+ addPlayerPacket.getAdventureSettings().setCommandPermission(CommandPermission.ANY);
+ addPlayerPacket.getAdventureSettings().setPlayerPermission(PlayerPermission.MEMBER);
+ addPlayerPacket.setDeviceId("");
+ addPlayerPacket.setPlatformChatId("");
+ addPlayerPacket.setGameType(GameType.SURVIVAL); //TODO
+ addPlayerPacket.setAbilityLayers(BASE_ABILITY_LAYER); // Recommended to be added since 1.19.10, but only needed here for permissions viewing
+ addPlayerPacket.getMetadata().putFlags(flags);
+ dirtyMetadata.apply(addPlayerPacket.getMetadata());
+
+ setFlagsDirty(false);
+
+ valid = true;
+ session.sendUpstreamPacket(addPlayerPacket);
+ }
+
+ @Override
+ public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) {
+ setPosition(position);
+ setYaw(yaw);
+ setPitch(pitch);
+ setHeadYaw(headYaw);
+
+ setOnGround(isOnGround);
+
+ MovePlayerPacket movePlayerPacket = new MovePlayerPacket();
+ movePlayerPacket.setRuntimeEntityId(geyserId);
+ movePlayerPacket.setPosition(this.position);
+ movePlayerPacket.setRotation(getBedrockRotation());
+ movePlayerPacket.setOnGround(isOnGround);
+ movePlayerPacket.setMode(this instanceof SessionPlayerEntity || teleported ? MovePlayerPacket.Mode.TELEPORT : MovePlayerPacket.Mode.NORMAL);
+ if (movePlayerPacket.getMode() == MovePlayerPacket.Mode.TELEPORT) {
+ movePlayerPacket.setTeleportationCause(MovePlayerPacket.TeleportationCause.BEHAVIOR);
+ }
+
+ session.sendUpstreamPacket(movePlayerPacket);
+
+ if (teleported && !(this instanceof SessionPlayerEntity)) {
+ // As of 1.19.0, head yaw seems to be ignored during teleports, also don't do this for session player.
+ updateHeadLookRotation(headYaw);
+ }
+ }
+
+ @Override
+ public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) {
+ setYaw(yaw);
+ setPitch(pitch);
+ setHeadYaw(headYaw);
+ this.position = Vector3f.from(position.getX() + relX, position.getY() + relY, position.getZ() + relZ);
+
+ setOnGround(isOnGround);
+
+ MovePlayerPacket movePlayerPacket = new MovePlayerPacket();
+ movePlayerPacket.setRuntimeEntityId(geyserId);
+ movePlayerPacket.setPosition(position);
+ movePlayerPacket.setRotation(getBedrockRotation());
+ movePlayerPacket.setOnGround(isOnGround);
+ movePlayerPacket.setMode(this instanceof SessionPlayerEntity ? MovePlayerPacket.Mode.TELEPORT : MovePlayerPacket.Mode.NORMAL);
+ // If the player is moved while sleeping, we have to adjust their y, so it appears
+ // correctly on Bedrock. This fixes GSit's lay.
+ if (getFlag(EntityFlag.SLEEPING)) {
+ if (bedPosition != null && (bedPosition.getY() == 0 || bedPosition.distanceSquared(position.toInt()) > 4)) {
+ // Force the player movement by using a teleport
+ movePlayerPacket.setPosition(Vector3f.from(position.getX(), position.getY() - definition.offset() + 0.2f, position.getZ()));
+ movePlayerPacket.setMode(MovePlayerPacket.Mode.TELEPORT);
+ }
+ }
+
+ if (movePlayerPacket.getMode() == MovePlayerPacket.Mode.TELEPORT) {
+ movePlayerPacket.setTeleportationCause(MovePlayerPacket.TeleportationCause.BEHAVIOR);
+ }
+
+ session.sendUpstreamPacket(movePlayerPacket);
+ }
+
+ @Override
+ public void setPosition(Vector3f position) {
+ if (this.bedPosition != null) {
+ // As of Bedrock 1.21.22 and Fabric 1.21.1
+ // Messes with Bedrock if we send this to the client itself, though.
+ super.setPosition(position.up(0.2f));
+ } else {
+ super.setPosition(position.add(0, definition.offset(), 0));
+ }
+ }
+
+ @Override
+ public @Nullable Vector3i setBedPosition(EntityMetadata, ?> entityMetadata) {
+ bedPosition = super.setBedPosition(entityMetadata);
+ if (bedPosition != null) {
+ // Required to sync position of entity to bed
+ // Fixes https://github.com/GeyserMC/Geyser/issues/3595 on vanilla 1.19.3 servers - did not happen on Paper
+ this.setPosition(bedPosition.toFloat());
+
+ // TODO evaluate if needed
+ int bed = session.getGeyser().getWorldManager().getBlockAt(session, bedPosition);
+ // Bed has to be updated, or else player is floating in the air
+ ChunkUtils.updateBlock(session, bed, bedPosition);
+
+ // Indicate that the player should enter the sleep cycle
+ // Has to be a byte or it does not work
+ // (Bed position is what actually triggers sleep - "pose" is only optional)
+ dirtyMetadata.put(EntityDataTypes.PLAYER_FLAGS, (byte) 2);
+ } else {
+ // Player is no longer sleeping
+ dirtyMetadata.put(EntityDataTypes.PLAYER_FLAGS, (byte) 0);
+ return null;
+ }
+ return bedPosition;
+ }
+
+ public void setSkin(ResolvableProfile profile, boolean cape, Runnable after) {
+ SkinManager.resolveProfile(profile).thenAccept(resolved -> setSkin(resolved, cape, after));
+ }
+
+ public void setSkin(GameProfile profile, boolean cape, Runnable after) {
+ GameProfile.Property textures = profile.getProperty("textures");
+ if (textures != null) {
+ setSkin(textures.getValue(), cape, after);
+ } else {
+ setSkin((String) null, cape, after);
+ }
+ }
+
+ public void setSkin(String texturesProperty, boolean cape, Runnable after) {
+ if (Objects.equals(texturesProperty, this.texturesProperty)) {
+ return;
+ }
+
+ this.texturesProperty = texturesProperty;
+ if (cape) {
+ SkinManager.requestAndHandleSkinAndCape(this, session, skin -> after.run());
+ } else {
+ SkullSkinManager.requestAndHandleSkin(this, session, skin -> after.run());
+ }
+ }
+
+ public void setSkinVisibility(ByteEntityMetadata entityMetadata) {
+ // OptionalPack usage for toggling skin bits
+ // In Java Edition, a bit being set means that part should be enabled
+ // However, to ensure that the pack still works on other servers, we invert the bit so all values by default
+ // are true (0).
+ dirtyMetadata.put(EntityDataTypes.MARK_VARIANT, ~entityMetadata.getPrimitiveValue() & 0xff);
+ }
+
+ @Override
+ public String getDisplayName() {
+ return username;
+ }
+
+ @Override
+ public void setDisplayName(EntityMetadata, ?> entityMetadata) {
+ // Doesn't do anything for players
+ if (!(this instanceof PlayerEntity)) {
+ super.setDisplayName(entityMetadata);
+ }
+ }
+
+ @Override
+ public void setDisplayNameVisible(BooleanEntityMetadata entityMetadata) {
+ // Doesn't do anything for players
+ if (!(this instanceof PlayerEntity)) {
+ super.setDisplayNameVisible(entityMetadata);
+ }
+ }
+
+ public void setBelowNameText(String text) {
+ if (text == null) {
+ text = "";
+ }
+
+ boolean changed = !Objects.equals(cachedScore, text);
+ cachedScore = text;
+ if (scoreVisible && changed) {
+ dirtyMetadata.put(EntityDataTypes.SCORE, text);
+ }
+ }
+
+ @Override
+ protected void scoreVisibility(boolean show) {
+ boolean visibilityChanged = scoreVisible != show;
+ scoreVisible = show;
+ if (!visibilityChanged) {
+ return;
+ }
+ // if the player has no cachedScore, we never have to change the score.
+ // hide = set to "" (does nothing), show = change from "" (does nothing)
+ if (cachedScore.isEmpty()) {
+ return;
+ }
+ dirtyMetadata.put(EntityDataTypes.SCORE, show ? cachedScore : "");
+ }
+
+ @Override
+ public void setPose(Pose pose) {
+ super.setPose(pose);
+ setFlag(EntityFlag.SWIMMING, false);
+ setFlag(EntityFlag.CRAWLING, false);
+
+ if (pose == Pose.SWIMMING) {
+ // This is just for, so we know if player is swimming or crawling.
+ // TODO test, changed from position (field) to position() (method), which adds offset
+ if (session.getGeyser().getWorldManager().blockAt(session, position.toInt()).is(Blocks.WATER)) {
+ setFlag(EntityFlag.SWIMMING, true);
+ } else {
+ setFlag(EntityFlag.CRAWLING, true);
+ // Look at https://github.com/GeyserMC/Geyser/issues/5316, we're fixing this by spoofing player pitch to 0.
+ updateRotation(this.yaw, 0, this.onGround);
+ }
+ }
+ }
+
+ @Override
+ public void setPitch(float pitch) {
+ super.setPitch(getFlag(EntityFlag.CRAWLING) ? 0 : pitch);
+ }
+
+ @Override
+ public void setDimensionsFromPose(Pose pose) {
+ float height;
+ float width;
+ switch (pose) {
+ case SNEAKING -> {
+ height = SNEAKING_POSE_HEIGHT;
+ width = definition.width();
+ }
+ case FALL_FLYING, SPIN_ATTACK, SWIMMING -> {
+ height = 0.6f;
+ width = definition.width();
+ }
+ case DYING -> {
+ height = 0.2f;
+ width = 0.2f;
+ }
+ default -> {
+ super.setDimensionsFromPose(pose);
+ return;
+ }
+ }
+ setBoundingBoxWidth(width);
+ setBoundingBoxHeight(height);
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/MannequinEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/MannequinEntity.java
new file mode 100644
index 000000000..c45221a1a
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/MannequinEntity.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2025 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.entity.type.player;
+
+import net.kyori.adventure.text.Component;
+import org.cloudburstmc.math.vector.Vector3f;
+import org.geysermc.geyser.entity.EntityDefinition;
+import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.player.ResolvableProfile;
+
+import java.util.Optional;
+import java.util.UUID;
+
+public class MannequinEntity extends AvatarEntity {
+
+ public MannequinEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+ super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw, "");
+ }
+
+ public void setProfile(EntityMetadata entityMetadata) {
+ setSkin(entityMetadata.getValue(), true, () -> {});
+ }
+
+ @Override
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public void setDescription(EntityMetadata, ?> entityMetadata) {
+ // TODO
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java
index a8ca770aa..4e1eb2dcb 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java
@@ -27,73 +27,28 @@ package org.geysermc.geyser.entity.type.player;
import lombok.Getter;
import lombok.Setter;
-import net.kyori.adventure.text.Component;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector3f;
-import org.cloudburstmc.math.vector.Vector3i;
-import org.cloudburstmc.nbt.NbtMap;
-import org.cloudburstmc.protocol.bedrock.data.Ability;
-import org.cloudburstmc.protocol.bedrock.data.AbilityLayer;
-import org.cloudburstmc.protocol.bedrock.data.GameType;
-import org.cloudburstmc.protocol.bedrock.data.PlayerPermission;
-import org.cloudburstmc.protocol.bedrock.data.command.CommandPermission;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
-import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityLinkData;
-import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
-import org.cloudburstmc.protocol.bedrock.packet.AddPlayerPacket;
-import org.cloudburstmc.protocol.bedrock.packet.MovePlayerPacket;
import org.cloudburstmc.protocol.bedrock.packet.SetEntityLinkPacket;
import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket;
import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity;
import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.entity.attribute.GeyserAttributeType;
import org.geysermc.geyser.entity.type.Entity;
-import org.geysermc.geyser.entity.type.LivingEntity;
import org.geysermc.geyser.entity.type.living.animal.tameable.ParrotEntity;
-import org.geysermc.geyser.level.block.Blocks;
import org.geysermc.geyser.session.GeyserSession;
-import org.geysermc.geyser.util.ChunkUtils;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata;
-import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose;
-import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
-import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatEntityMetadata;
import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
+import java.util.OptionalInt;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Getter @Setter
-public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
- public static final float SNEAKING_POSE_HEIGHT = 1.5f;
- protected static final List BASE_ABILITY_LAYER;
-
- static {
- AbilityLayer abilityLayer = new AbilityLayer();
- abilityLayer.setLayerType(AbilityLayer.Type.BASE);
- Ability[] abilities = Ability.values();
- Collections.addAll(abilityLayer.getAbilitiesSet(), abilities); // Apparently all the abilities you're working with
- Collections.addAll(abilityLayer.getAbilityValues(), abilities); // Apparently all the abilities the player can work with
- BASE_ABILITY_LAYER = Collections.singletonList(abilityLayer);
- }
-
- private String username;
-
- private String cachedScore = "";
- private boolean scoreVisible = true;
-
- /**
- * The textures property from the GameProfile.
- */
- @Nullable
- private String texturesProperty;
-
- @Nullable
- private Vector3i bedPosition;
+public class PlayerEntity extends AvatarEntity implements GeyserPlayerEntity {
/**
* Saves the parrot currently on the player's left shoulder; otherwise null
@@ -111,48 +66,17 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
public PlayerEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, Vector3f position,
Vector3f motion, float yaw, float pitch, float headYaw, String username, @Nullable String texturesProperty) {
- super(session, entityId, geyserId, uuid, EntityDefinitions.PLAYER, position, motion, yaw, pitch, headYaw);
-
- this.username = username;
- this.nametag = username;
+ super(session, entityId, geyserId, uuid, EntityDefinitions.PLAYER, position, motion, yaw, pitch, headYaw, username);
this.texturesProperty = texturesProperty;
}
@Override
protected void initializeMetadata() {
super.initializeMetadata();
- // For the OptionalPack, set all bits as invisible by default as this matches Java Edition behavior
- dirtyMetadata.put(EntityDataTypes.MARK_VARIANT, 0xff);
- }
-
- @Override
- public void spawnEntity() {
- AddPlayerPacket addPlayerPacket = new AddPlayerPacket();
- addPlayerPacket.setUuid(uuid);
- addPlayerPacket.setUsername(username);
- addPlayerPacket.setRuntimeEntityId(geyserId);
- addPlayerPacket.setUniqueEntityId(geyserId);
- addPlayerPacket.setPosition(position.sub(0, definition.offset(), 0));
- addPlayerPacket.setRotation(getBedrockRotation());
- addPlayerPacket.setMotion(motion);
- addPlayerPacket.setHand(hand);
- addPlayerPacket.getAdventureSettings().setCommandPermission(CommandPermission.ANY);
- addPlayerPacket.getAdventureSettings().setPlayerPermission(PlayerPermission.MEMBER);
- addPlayerPacket.setDeviceId("");
- addPlayerPacket.setPlatformChatId("");
- addPlayerPacket.setGameType(GameType.SURVIVAL); //TODO
- addPlayerPacket.setAbilityLayers(BASE_ABILITY_LAYER); // Recommended to be added since 1.19.10, but only needed here for permissions viewing
- addPlayerPacket.getMetadata().putFlags(flags);
// Since 1.20.60, the nametag does not show properly if this is not set :/
// The nametag does disappear properly when the player is invisible though.
dirtyMetadata.put(EntityDataTypes.NAMETAG_ALWAYS_SHOW, (byte) 1);
- dirtyMetadata.apply(addPlayerPacket.getMetadata());
-
- setFlagsDirty(false);
-
- valid = true;
- session.sendUpstreamPacket(addPlayerPacket);
}
@Override
@@ -164,12 +88,6 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
this.nametag = username;
this.equipment.clear();
- this.hand = ItemData.AIR;
- this.offhand = ItemData.AIR;
- this.boots = ItemData.AIR;
- this.leggings = ItemData.AIR;
- this.chestplate = ItemData.AIR;
- this.helmet = ItemData.AIR;
}
public void resetMetadata() {
@@ -179,8 +97,8 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
this.initializeMetadata();
// Explicitly reset all metadata not handled by initializeMetadata
- setParrot(null, true);
- setParrot(null, false);
+ setParrot(OptionalInt.empty(), true);
+ setParrot(OptionalInt.empty(), false);
}
public void sendPlayer() {
@@ -192,30 +110,7 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
@Override
public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) {
- setPosition(position);
- setYaw(yaw);
- setPitch(pitch);
- setHeadYaw(headYaw);
-
- setOnGround(isOnGround);
-
- MovePlayerPacket movePlayerPacket = new MovePlayerPacket();
- movePlayerPacket.setRuntimeEntityId(geyserId);
- movePlayerPacket.setPosition(this.position);
- movePlayerPacket.setRotation(getBedrockRotation());
- movePlayerPacket.setOnGround(isOnGround);
- movePlayerPacket.setMode(this instanceof SessionPlayerEntity || teleported ? MovePlayerPacket.Mode.TELEPORT : MovePlayerPacket.Mode.NORMAL);
- if (movePlayerPacket.getMode() == MovePlayerPacket.Mode.TELEPORT) {
- movePlayerPacket.setTeleportationCause(MovePlayerPacket.TeleportationCause.BEHAVIOR);
- }
-
- session.sendUpstreamPacket(movePlayerPacket);
-
- if (teleported && !(this instanceof SessionPlayerEntity)) {
- // As of 1.19.0, head yaw seems to be ignored during teleports, also don't do this for session player.
- updateHeadLookRotation(headYaw);
- }
-
+ super.moveAbsolute(position, yaw, pitch, headYaw, isOnGround, teleported);
if (leftParrot != null) {
leftParrot.moveAbsolute(position, yaw, pitch, headYaw, true, teleported);
}
@@ -226,34 +121,7 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
@Override
public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) {
- setYaw(yaw);
- setPitch(pitch);
- setHeadYaw(headYaw);
- this.position = Vector3f.from(position.getX() + relX, position.getY() + relY, position.getZ() + relZ);
-
- setOnGround(isOnGround);
-
- MovePlayerPacket movePlayerPacket = new MovePlayerPacket();
- movePlayerPacket.setRuntimeEntityId(geyserId);
- movePlayerPacket.setPosition(position);
- movePlayerPacket.setRotation(getBedrockRotation());
- movePlayerPacket.setOnGround(isOnGround);
- movePlayerPacket.setMode(this instanceof SessionPlayerEntity ? MovePlayerPacket.Mode.TELEPORT : MovePlayerPacket.Mode.NORMAL);
- // If the player is moved while sleeping, we have to adjust their y, so it appears
- // correctly on Bedrock. This fixes GSit's lay.
- if (getFlag(EntityFlag.SLEEPING)) {
- if (bedPosition != null && (bedPosition.getY() == 0 || bedPosition.distanceSquared(position.toInt()) > 4)) {
- // Force the player movement by using a teleport
- movePlayerPacket.setPosition(Vector3f.from(position.getX(), position.getY() - definition.offset() + 0.2f, position.getZ()));
- movePlayerPacket.setMode(MovePlayerPacket.Mode.TELEPORT);
- }
- }
-
- if (movePlayerPacket.getMode() == MovePlayerPacket.Mode.TELEPORT) {
- movePlayerPacket.setTeleportationCause(MovePlayerPacket.TeleportationCause.BEHAVIOR);
- }
-
- session.sendUpstreamPacket(movePlayerPacket);
+ super.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, isOnGround);
if (leftParrot != null) {
leftParrot.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, true);
}
@@ -262,42 +130,6 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
}
}
- @Override
- public void setPosition(Vector3f position) {
- if (this.bedPosition != null) {
- // As of Bedrock 1.21.22 and Fabric 1.21.1
- // Messes with Bedrock if we send this to the client itself, though.
- super.setPosition(position.up(0.2f));
- } else {
- super.setPosition(position.add(0, definition.offset(), 0));
- }
- }
-
- @Override
- public @Nullable Vector3i setBedPosition(EntityMetadata, ?> entityMetadata) {
- bedPosition = super.setBedPosition(entityMetadata);
- if (bedPosition != null) {
- // Required to sync position of entity to bed
- // Fixes https://github.com/GeyserMC/Geyser/issues/3595 on vanilla 1.19.3 servers - did not happen on Paper
- this.setPosition(bedPosition.toFloat());
-
- // TODO evaluate if needed
- int bed = session.getGeyser().getWorldManager().getBlockAt(session, bedPosition);
- // Bed has to be updated, or else player is floating in the air
- ChunkUtils.updateBlock(session, bed, bedPosition);
-
- // Indicate that the player should enter the sleep cycle
- // Has to be a byte or it does not work
- // (Bed position is what actually triggers sleep - "pose" is only optional)
- dirtyMetadata.put(EntityDataTypes.PLAYER_FLAGS, (byte) 2);
- } else {
- // Player is no longer sleeping
- dirtyMetadata.put(EntityDataTypes.PLAYER_FLAGS, (byte) 0);
- return null;
- }
- return bedPosition;
- }
-
public void setAbsorptionHearts(FloatEntityMetadata entityMetadata) {
// Extra hearts - is not metadata but an attribute on Bedrock
UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket();
@@ -308,19 +140,11 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
session.sendUpstreamPacket(attributesPacket);
}
- public void setSkinVisibility(ByteEntityMetadata entityMetadata) {
- // OptionalPack usage for toggling skin bits
- // In Java Edition, a bit being set means that part should be enabled
- // However, to ensure that the pack still works on other servers, we invert the bit so all values by default
- // are true (0).
- dirtyMetadata.put(EntityDataTypes.MARK_VARIANT, ~entityMetadata.getPrimitiveValue() & 0xff);
- }
-
- public void setLeftParrot(EntityMetadata entityMetadata) {
+ public void setLeftParrot(EntityMetadata entityMetadata) {
setParrot(entityMetadata.getValue(), true);
}
- public void setRightParrot(EntityMetadata entityMetadata) {
+ public void setRightParrot(EntityMetadata entityMetadata) {
setParrot(entityMetadata.getValue(), false);
}
@@ -328,17 +152,17 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
* Sets the parrot occupying the shoulder. Bedrock Edition requires a full entity whereas Java Edition just
* spawns it from the NBT data provided
*/
- protected void setParrot(NbtMap tag, boolean isLeft) {
- if (tag != null && !tag.isEmpty()) {
+ protected void setParrot(OptionalInt variant, boolean isLeft) {
+ if (variant.isPresent()) {
if ((isLeft && leftParrot != null) || (!isLeft && rightParrot != null)) {
// No need to update a parrot's data when it already exists
return;
}
- // The parrot is a separate entity in Bedrock, but part of the player entity in Java //TODO is a UUID provided in NBT?
+ // The parrot is a separate entity in Bedrock, but part of the player entity in Java
ParrotEntity parrot = new ParrotEntity(session, 0, session.getEntityCache().getNextEntityId().incrementAndGet(),
null, EntityDefinitions.PARROT, position, motion, getYaw(), getPitch(), getHeadYaw());
parrot.spawnEntity();
- parrot.getDirtyMetadata().put(EntityDataTypes.VARIANT, (Integer) tag.get("Variant"));
+ parrot.getDirtyMetadata().put(EntityDataTypes.VARIANT, variant.getAsInt());
// Different position whether the parrot is left or right
float offset = isLeft ? 0.4f : -0.4f;
parrot.getDirtyMetadata().put(EntityDataTypes.SEAT_OFFSET, Vector3f.from(offset, -0.22, -0.1));
@@ -368,21 +192,12 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
}
}
- @Override
- public String getDisplayName() {
- return username;
- }
-
- @Override
- public void setDisplayName(EntityMetadata, ?> entityMetadata) {
- // Doesn't do anything for players
- }
-
@Override
public String teamIdentifier() {
return username;
}
+ // TODO test mannequins
@Override
protected void setNametag(@Nullable String nametag, boolean fromDisplayName) {
// when fromDisplayName, LivingEntity will call scoreboard code. After that
@@ -394,85 +209,8 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
super.setNametag(nametag, fromDisplayName);
}
- @Override
- public void setDisplayNameVisible(BooleanEntityMetadata entityMetadata) {
- // Doesn't do anything for players
- }
-
- public void setBelowNameText(String text) {
- if (text == null) {
- text = "";
- }
-
- boolean changed = !Objects.equals(cachedScore, text);
- cachedScore = text;
- if (isScoreVisible() && changed) {
- dirtyMetadata.put(EntityDataTypes.SCORE, text);
- }
- }
-
- @Override
- protected void scoreVisibility(boolean show) {
- boolean visibilityChanged = scoreVisible != show;
- scoreVisible = show;
- if (!visibilityChanged) {
- return;
- }
- // if the player has no cachedScore, we never have to change the score.
- // hide = set to "" (does nothing), show = change from "" (does nothing)
- if (cachedScore.isEmpty()) {
- return;
- }
- dirtyMetadata.put(EntityDataTypes.SCORE, show ? cachedScore : "");
- }
-
- @Override
- public void setPose(Pose pose) {
- super.setPose(pose);
- setFlag(EntityFlag.SWIMMING, false);
- setFlag(EntityFlag.CRAWLING, false);
-
- if (pose == Pose.SWIMMING) {
- // This is just for, so we know if player is swimming or crawling.
- if (session.getGeyser().getWorldManager().blockAt(session, this.position().toInt()).is(Blocks.WATER)) {
- setFlag(EntityFlag.SWIMMING, true);
- } else {
- setFlag(EntityFlag.CRAWLING, true);
- // Look at https://github.com/GeyserMC/Geyser/issues/5316, we're fixing this by spoofing player pitch to 0.
- updateRotation(this.yaw, 0, this.onGround);
- }
- }
- }
-
- @Override
- public void setPitch(float pitch) {
- super.setPitch(getFlag(EntityFlag.CRAWLING) ? 0 : pitch);
- }
-
- @Override
- public void setDimensionsFromPose(Pose pose) {
- float height;
- float width;
- switch (pose) {
- case SNEAKING -> {
- height = SNEAKING_POSE_HEIGHT;
- width = definition.width();
- }
- case FALL_FLYING, SPIN_ATTACK, SWIMMING -> {
- height = 0.6f;
- width = definition.width();
- }
- case DYING -> {
- height = 0.2f;
- width = 0.2f;
- }
- default -> {
- super.setDimensionsFromPose(pose);
- return;
- }
- }
- setBoundingBoxWidth(width);
- setBoundingBoxHeight(height);
+ public void setUsername(String username) {
+ this.username = username;
}
/**
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java
index 145421a5f..a60dd0b78 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java
@@ -322,9 +322,9 @@ public class SessionPlayerEntity extends PlayerEntity {
protected boolean hasShield(boolean offhand) {
// Must be overridden to point to the player's inventory cache
if (offhand) {
- return session.getPlayerInventory().getOffhand().asItem() == Items.SHIELD;
+ return session.getPlayerInventory().getOffhand().is(Items.SHIELD);
} else {
- return session.getPlayerInventory().getItemInHand().asItem() == Items.SHIELD;
+ return session.getPlayerInventory().getItemInHand().is(Items.SHIELD);
}
}
@@ -498,7 +498,7 @@ public class SessionPlayerEntity extends PlayerEntity {
}
Vector3i pos = getPosition().down(EntityDefinitions.PLAYER.offset()).toInt();
BlockState state = session.getGeyser().getWorldManager().blockAt(session, pos);
- if (session.getTagCache().is(BlockTag.CLIMBABLE, state.block())) {
+ if (state.block().is(session, BlockTag.CLIMBABLE)) {
return true;
}
@@ -539,7 +539,7 @@ public class SessionPlayerEntity extends PlayerEntity {
}
// Bedrock will NOT allow flight when not wearing an elytra; even if it doesn't have a glider component
- if (entry.getKey() == EquipmentSlot.CHESTPLATE && !entry.getValue().asItem().equals(Items.ELYTRA)) {
+ if (entry.getKey() == EquipmentSlot.CHESTPLATE && !entry.getValue().is(Items.ELYTRA)) {
return false;
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/SkullPlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/SkullPlayerEntity.java
index 4c8d1d20b..2819d9486 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/player/SkullPlayerEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/SkullPlayerEntity.java
@@ -34,6 +34,7 @@ import org.cloudburstmc.protocol.bedrock.data.command.CommandPermission;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.cloudburstmc.protocol.bedrock.packet.AddPlayerPacket;
+import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.level.block.property.Properties;
import org.geysermc.geyser.level.block.type.BlockState;
import org.geysermc.geyser.level.block.type.WallSkullBlock;
@@ -41,6 +42,7 @@ import org.geysermc.geyser.level.physics.Direction;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.SkullCache;
import org.geysermc.geyser.skin.SkullSkinManager;
+import org.geysermc.geyser.translator.item.ItemTranslator;
import java.util.Objects;
import java.util.UUID;
@@ -50,7 +52,7 @@ import java.util.concurrent.TimeUnit;
* A wrapper to handle skulls more effectively - skulls have to be treated as entities since there are no
* custom player skulls in Bedrock.
*/
-public class SkullPlayerEntity extends PlayerEntity {
+public class SkullPlayerEntity extends AvatarEntity {
@Getter
private UUID skullUUID;
@@ -59,7 +61,7 @@ public class SkullPlayerEntity extends PlayerEntity {
private Vector3i skullPosition;
public SkullPlayerEntity(GeyserSession session, long geyserId) {
- super(session, 0, geyserId, UUID.randomUUID(), Vector3f.ZERO, Vector3f.ZERO, 0, 0, 0, "", null);
+ super(session, 0, geyserId, UUID.randomUUID(), EntityDefinitions.PLAYER, Vector3f.ZERO, Vector3f.ZERO, 0, 0, 0, "");
}
@Override
@@ -73,51 +75,20 @@ public class SkullPlayerEntity extends PlayerEntity {
setFlag(EntityFlag.INVISIBLE, true); // Until the skin is loaded
}
- /**
- * Overwritten so each entity doesn't check for a linked entity
- */
- @Override
- public void spawnEntity() {
- AddPlayerPacket addPlayerPacket = new AddPlayerPacket();
- addPlayerPacket.setUuid(getUuid());
- addPlayerPacket.setUsername(getUsername());
- addPlayerPacket.setRuntimeEntityId(geyserId);
- addPlayerPacket.setUniqueEntityId(geyserId);
- addPlayerPacket.setPosition(position.sub(0, definition.offset(), 0));
- addPlayerPacket.setRotation(getBedrockRotation());
- addPlayerPacket.setMotion(motion);
- addPlayerPacket.setHand(hand);
- addPlayerPacket.getAdventureSettings().setCommandPermission(CommandPermission.ANY);
- addPlayerPacket.getAdventureSettings().setPlayerPermission(PlayerPermission.MEMBER);
- addPlayerPacket.setDeviceId("");
- addPlayerPacket.setPlatformChatId("");
- addPlayerPacket.setGameType(GameType.SURVIVAL);
- addPlayerPacket.setAbilityLayers(BASE_ABILITY_LAYER);
- addPlayerPacket.getMetadata().putFlags(flags);
- dirtyMetadata.apply(addPlayerPacket.getMetadata());
-
- setFlagsDirty(false);
-
- valid = true;
- session.sendUpstreamPacket(addPlayerPacket);
- }
-
public void updateSkull(SkullCache.Skull skull) {
skullPosition = skull.getPosition();
- if (!Objects.equals(skull.getTexturesProperty(), getTexturesProperty()) || !Objects.equals(skullUUID, skull.getUuid())) {
+ if (!Objects.equals(skull.getTexturesProperty(), texturesProperty) || !Objects.equals(skullUUID, skull.getUuid())) {
// Make skull invisible as we change skins
setFlag(EntityFlag.INVISIBLE, true);
updateBedrockMetadata();
skullUUID = skull.getUuid();
- setTexturesProperty(skull.getTexturesProperty());
-
- SkullSkinManager.requestAndHandleSkin(this, session, (skin -> session.scheduleInEventLoop(() -> {
+ setSkin(skull.getTexturesProperty(), false, () -> session.scheduleInEventLoop(() -> {
// Delay to minimize split-second "player" pop-in
setFlag(EntityFlag.INVISIBLE, false);
updateBedrockMetadata();
- }, 250, TimeUnit.MILLISECONDS)));
+ }, 250, TimeUnit.MILLISECONDS));
} else {
// Just a rotation/position change
setFlag(EntityFlag.INVISIBLE, false);
diff --git a/core/src/main/java/org/geysermc/geyser/entity/vehicle/VehicleComponent.java b/core/src/main/java/org/geysermc/geyser/entity/vehicle/VehicleComponent.java
index 44e922950..64796d052 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/vehicle/VehicleComponent.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/vehicle/VehicleComponent.java
@@ -683,7 +683,7 @@ public class VehicleComponent {
}
BlockState blockState = ctx.centerBlock();
- if (vehicle.getSession().getTagCache().is(BlockTag.CLIMBABLE, blockState.block())) {
+ if (blockState.block().is(vehicle.getSession(), BlockTag.CLIMBABLE)) {
return true;
}
diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionClassLoader.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionClassLoader.java
index dca11dfcd..a8f5c5584 100644
--- a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionClassLoader.java
+++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionClassLoader.java
@@ -40,11 +40,11 @@ import java.nio.file.Path;
public class GeyserExtensionClassLoader extends URLClassLoader {
private final GeyserExtensionLoader loader;
- private final ExtensionDescription description;
+ private final GeyserExtensionDescription description;
private final Object2ObjectMap> classes = new Object2ObjectOpenHashMap<>();
private boolean warnedForExternalClassAccess;
- public GeyserExtensionClassLoader(GeyserExtensionLoader loader, ClassLoader parent, Path path, ExtensionDescription description) throws MalformedURLException {
+ public GeyserExtensionClassLoader(GeyserExtensionLoader loader, ClassLoader parent, Path path, GeyserExtensionDescription description) throws MalformedURLException {
super(new URL[] { path.toUri().toURL() }, parent);
this.loader = loader;
this.description = description;
@@ -89,7 +89,7 @@ public class GeyserExtensionClassLoader extends URLClassLoader {
// If class is not found in current extension, check in the global class loader
// This is used for classes that are not in the extension, but are in other extensions
if (checkGlobal) {
- if (!warnedForExternalClassAccess) {
+ if (!warnedForExternalClassAccess && this.description.dependencies().isEmpty()) { // Don't warn when the extension has dependencies, it is probably using it's dependencies!
GeyserImpl.getInstance().getLogger().warning("Extension " + this.description.name() + " loads class " + name + " from an external source. " +
"This can change at any time and break the extension, additionally to potentially causing unexpected behaviour!");
warnedForExternalClassAccess = true;
diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionDescription.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionDescription.java
index a84f12813..702ab0f68 100644
--- a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionDescription.java
+++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionDescription.java
@@ -47,7 +47,8 @@ public record GeyserExtensionDescription(@NonNull String id,
int majorApiVersion,
int minorApiVersion,
@NonNull String version,
- @NonNull List authors) implements ExtensionDescription {
+ @NonNull List authors,
+ @NonNull Map dependencies) implements ExtensionDescription {
private static final Yaml YAML = new Yaml(new CustomClassLoaderConstructor(Source.class.getClassLoader(), new LoaderOptions()));
@@ -94,7 +95,12 @@ public record GeyserExtensionDescription(@NonNull String id,
authors.addAll(source.authors);
}
- return new GeyserExtensionDescription(id, name, main, humanApi, majorApi, minorApi, version, authors);
+ Map dependencies = new LinkedHashMap<>();
+ if (source.dependencies != null) {
+ dependencies.putAll(source.dependencies);
+ }
+
+ return new GeyserExtensionDescription(id, name, main, humanApi, majorApi, minorApi, version, authors, dependencies);
}
@NonNull
@@ -116,5 +122,17 @@ public record GeyserExtensionDescription(@NonNull String id,
String version;
String author;
List authors;
+ Map dependencies;
+ }
+
+ @Getter
+ @Setter
+ public static class Dependency {
+ boolean required = true; // Defaults to true
+ LoadOrder load = LoadOrder.BEFORE; // Defaults to ensure the dependency loads before this extension
+ }
+
+ public enum LoadOrder {
+ BEFORE, AFTER
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java
index 2bfa6cce6..2be6d2f8f 100644
--- a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java
+++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java
@@ -54,11 +54,16 @@ import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
+import java.util.function.Consumer;
import java.util.regex.Pattern;
@RequiredArgsConstructor
@@ -167,6 +172,8 @@ public class GeyserExtensionLoader extends ExtensionLoader {
Map extensions = new LinkedHashMap<>();
Map loadedExtensions = new LinkedHashMap<>();
+ Map descriptions = new LinkedHashMap<>();
+ Map extensionPaths = new LinkedHashMap<>();
Path updateDirectory = extensionsDirectory.resolve("update");
if (Files.isDirectory(updateDirectory)) {
@@ -195,10 +202,126 @@ public class GeyserExtensionLoader extends ExtensionLoader {
});
}
- // Step 3: Load the extensions
+ // Step 3: Order the extensions to allow dependencies to load in the correct order
this.processExtensionsFolder(extensionsDirectory, (path, description) -> {
- String name = description.name();
String id = description.id();
+ descriptions.put(id, description);
+ extensionPaths.put(id, path);
+
+ }, (path, e) -> {
+ logger.error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_with_name", path.getFileName(), path.toAbsolutePath()), e);
+ });
+
+ // The graph to back out loading order (Funny I just learnt these too)
+ Map> loadOrderGraph = new HashMap<>();
+
+ // Looks like the graph needs to be prepopulated otherwise issues happen
+ for (String id : descriptions.keySet()) {
+ loadOrderGraph.putIfAbsent(id, new ArrayList<>());
+ }
+
+ for (GeyserExtensionDescription description : descriptions.values()) {
+ for (Map.Entry dependency : description.dependencies().entrySet()) {
+ String from = null;
+ String to = null; // Java complains if this isn't initialised, but not from, so, both null.
+
+ // Check if the extension is even loaded
+ if (!descriptions.containsKey(dependency.getKey())) {
+ if (dependency.getValue().isRequired()) { // Only disable the extension if this dependency is required
+ // The extension we are checking is missing 1 or more dependencies
+ logger.error(
+ GeyserLocale.getLocaleStringLog(
+ "geyser.extensions.load.failed_dependency_missing",
+ description.id(),
+ dependency.getKey()
+ )
+ );
+
+ descriptions.remove(description.id()); // Prevents it from being loaded later
+ }
+
+ continue;
+ }
+
+ if (
+ !(description.humanApiVersion() >= 2 &&
+ description.majorApiVersion() >= 9 &&
+ description.minorApiVersion() >= 0)
+ ) {
+ logger.error(
+ GeyserLocale.getLocaleStringLog(
+ "geyser.extensions.load.failed_cannot_use_dependencies",
+ description.id(),
+ description.apiVersion()
+ )
+ );
+
+ descriptions.remove(description.id()); // Prevents it from being loaded later
+
+ continue;
+ }
+
+ // Determine which way they should go in the graph
+ switch (dependency.getValue().getLoad()) {
+ case BEFORE -> {
+ from = dependency.getKey();
+ to = description.id();
+ }
+ case AFTER -> {
+ from = description.id();
+ to = dependency.getKey();
+ }
+ }
+
+ loadOrderGraph.get(from).add(to);
+ }
+ }
+
+ Set visited = new HashSet<>();
+ List visiting = new ArrayList<>();
+ List loadOrder = new ArrayList<>();
+
+ AtomicReference> sortMethod = new AtomicReference<>(); // yay, lambdas. This doesn't feel to suited to be a method
+ sortMethod.set((node) -> {
+ if (visiting.contains(node)) {
+ logger.error(
+ GeyserLocale.getLocaleStringLog(
+ "geyser.extensions.load.failed_cyclical_dependencies",
+ node,
+ visiting.get(visiting.indexOf(node) - 1)
+ )
+ );
+
+ visiting.remove(node);
+ return;
+ }
+
+ if (visited.contains(node)) return;
+
+ visiting.add(node);
+ for (String neighbor : loadOrderGraph.get(node)) {
+ sortMethod.get().accept(neighbor);
+ }
+ visiting.remove(node);
+ visited.add(node);
+ loadOrder.add(node);
+ });
+
+ for (String ext : descriptions.keySet()) {
+ if (!visited.contains(ext)) {
+ // Time to sort the graph to get a load order, this reveals any cycles we may have
+ sortMethod.get().accept(ext);
+ }
+ }
+ Collections.reverse(loadOrder); // This is inverted due to how the graph is created
+
+ // Step 4: Load the extensions
+ for (String id : loadOrder) {
+ // Grab path and description found from before, since we want a custom load order now
+ Path path = extensionPaths.get(id);
+ GeyserExtensionDescription description = descriptions.get(id);
+
+ String name = description.name();
if (extensions.containsKey(id) || extensionManager.extension(id) != null) {
logger.warning(GeyserLocale.getLocaleStringLog("geyser.extensions.load.duplicate", name, path.toString()));
return;
@@ -222,20 +345,22 @@ public class GeyserExtensionLoader extends ExtensionLoader {
}
}
- GeyserExtensionContainer container = this.loadExtension(path, description);
- extensions.put(id, path);
- loadedExtensions.put(id, container);
- }, (path, e) -> {
- logger.error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_with_name", path.getFileName(), path.toAbsolutePath()), e);
- });
+ try {
+ GeyserExtensionContainer container = this.loadExtension(path, description);
+ extensions.put(id, path);
+ loadedExtensions.put(id, container);
+ } catch (Throwable e) {
+ logger.error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_with_name", path.getFileName(), path.toAbsolutePath()), e);
+ }
+ }
- // Step 4: Register the extensions
+ // Step 5: Register the extensions
for (GeyserExtensionContainer container : loadedExtensions.values()) {
this.extensionContainers.put(container.extension(), container);
this.register(container.extension(), extensionManager);
}
} catch (IOException ex) {
- ex.printStackTrace();
+ logger.error("Unable to read extensions.", ex);
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/impl/IdentifierImpl.java b/core/src/main/java/org/geysermc/geyser/impl/IdentifierImpl.java
new file mode 100644
index 000000000..ca0324665
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/impl/IdentifierImpl.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2025 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.impl;
+
+import net.kyori.adventure.key.Key;
+import org.geysermc.geyser.api.util.Identifier;
+import org.geysermc.geyser.util.MinecraftKey;
+
+import java.util.Objects;
+
+public record IdentifierImpl(Key identifier) implements Identifier {
+
+ public static IdentifierImpl of(String namespace, String value) throws IllegalArgumentException {
+ Objects.requireNonNull(namespace, "namespace cannot be null!");
+ Objects.requireNonNull(value, "value cannot be null!");
+ try {
+ return new IdentifierImpl(MinecraftKey.key(namespace, value));
+ } catch (Throwable e) {
+ throw new IllegalArgumentException(e.getMessage());
+ }
+ }
+
+ // FIXME using the identifier interface from the API breaks tests
+ public static IdentifierImpl of(String value) {
+ return of(Identifier.DEFAULT_NAMESPACE, value);
+ }
+
+ @Override
+ public String namespace() {
+ return identifier.namespace();
+ }
+
+ @Override
+ public String path() {
+ return identifier.value();
+ }
+
+ @Override
+ public String toString() {
+ return identifier.toString();
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java b/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java
index 5f4ce6b45..0de4580aa 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java
@@ -40,11 +40,14 @@ import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.BundleCache;
+import org.geysermc.geyser.session.cache.registry.JavaRegistries;
+import org.geysermc.geyser.session.cache.tags.Tag;
import org.geysermc.geyser.translator.item.ItemTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents;
+import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet;
import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.EmptySlotDisplay;
import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.ItemSlotDisplay;
import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.ItemStackSlotDisplay;
@@ -115,6 +118,22 @@ public class GeyserItemStack {
return isEmpty() ? 0 : amount;
}
+ public boolean is(Item item) {
+ return javaId == item.javaId();
+ }
+
+ public boolean is(GeyserSession session, Tag
- tag) {
+ return session.getTagCache().is(tag, javaId);
+ }
+
+ public boolean is(GeyserSession session, HolderSet set) {
+ return session.getTagCache().is(set, JavaRegistries.ITEM, javaId);
+ }
+
+ public boolean isSameItem(GeyserItemStack other) {
+ return javaId == other.javaId;
+ }
+
/**
* Returns all components of this item - base and additional components sent over the network.
* These are NOT modifiable! To add components, use {@link #getOrCreateComponents()}.
@@ -248,6 +267,10 @@ public class GeyserItemStack {
return new ItemStackSlotDisplay(this.getItemStack());
}
+ public int getMaxStackSize() {
+ return getComponentElseGet(DataComponentTypes.MAX_STACK_SIZE, () -> 1);
+ }
+
public int getMaxDamage() {
return getComponentElseGet(DataComponentTypes.MAX_DAMAGE, () -> 0);
}
@@ -266,6 +289,10 @@ public class GeyserItemStack {
return getComponent(DataComponentTypes.MAX_DAMAGE) != null && getComponent(DataComponentTypes.UNBREAKABLE) == null && getComponent(DataComponentTypes.DAMAGE) != null;
}
+ public boolean isDamaged() {
+ return isDamageable() && getDamage() > 0;
+ }
+
public Item asItem() {
if (isEmpty()) {
return Items.AIR;
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java b/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java
index 2d8b0e351..414ad2951 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java
@@ -148,7 +148,7 @@ public abstract class Inventory {
items[slot] = newItem;
// Lodestone caching
- if (newItem.asItem() == Items.COMPASS) {
+ if (newItem.is(Items.COMPASS)) {
var tracker = newItem.getComponent(DataComponentTypes.LODESTONE_TRACKER);
if (tracker != null) {
session.getLodestoneCache().cacheInventoryItem(newItem, tracker);
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java b/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java
index 200bf522b..6c34aa7d5 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java
@@ -71,7 +71,7 @@ public class PlayerInventory extends Inventory {
* @return If the player is holding the item in either hand
*/
public boolean isHolding(@NonNull Item item) {
- return getItemInHand().asItem() == item || getOffhand().asItem() == item;
+ return getItemInHand().is(item) || getOffhand().is(item);
}
public GeyserItemStack getItemInHand(@NonNull Hand hand) {
@@ -98,10 +98,6 @@ public class PlayerInventory extends Inventory {
);
}
- public boolean eitherHandMatchesItem(@NonNull Item item) {
- return getItemInHand().asItem() == item || getItemInHand(Hand.OFF_HAND).asItem() == item;
- }
-
public void setItemInHand(@NonNull GeyserItemStack item) {
if (36 + heldItemSlot > this.size) {
GeyserImpl.getInstance().getLogger().debug("Held item slot was larger than expected!");
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/StonecutterContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/StonecutterContainer.java
index e9d884cdd..8a33db7d2 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/StonecutterContainer.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/StonecutterContainer.java
@@ -45,7 +45,7 @@ public class StonecutterContainer extends Container {
@Override
public void setItem(int slot, @NonNull GeyserItemStack newItem, GeyserSession session) {
- if (slot == 0 && newItem.getJavaId() != items[slot].getJavaId()) {
+ if (slot == 0 && !newItem.isSameItem(items[slot])) {
// The pressed stonecutter button output resets whenever the input item changes
this.stonecutterButton = -1;
}
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java b/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java
index f856af35c..a58c218f0 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java
@@ -40,37 +40,25 @@ import java.util.Map;
@Getter
@Accessors(fluent = true)
public class StoredItemMappings {
- private final ItemMapping banner;
private final ItemMapping barrier;
- private final ItemMapping bow;
- private final ItemMapping carrotOnAStick;
private final ItemMapping compass;
- private final ItemMapping crossbow;
private final ItemMapping glassBottle;
private final ItemMapping milkBucket;
private final ItemMapping powderSnowBucket;
- private final ItemMapping shield;
private final ItemMapping totem;
private final ItemMapping upgradeTemplate;
- private final ItemMapping warpedFungusOnAStick;
private final ItemMapping wheat;
private final ItemMapping writableBook;
private final ItemMapping writtenBook;
public StoredItemMappings(Map
- itemMappings) {
- this.banner = load(itemMappings, Items.WHITE_BANNER); // As of 1.17.10, all banners have the same Bedrock ID
this.barrier = load(itemMappings, Items.BARRIER);
- this.bow = load(itemMappings, Items.BOW);
- this.carrotOnAStick = load(itemMappings, Items.CARROT_ON_A_STICK);
this.compass = load(itemMappings, Items.COMPASS);
- this.crossbow = load(itemMappings, Items.CROSSBOW);
this.glassBottle = load(itemMappings, Items.GLASS_BOTTLE);
this.milkBucket = load(itemMappings, Items.MILK_BUCKET);
this.powderSnowBucket = load(itemMappings, Items.POWDER_SNOW_BUCKET);
- this.shield = load(itemMappings, Items.SHIELD);
this.totem = load(itemMappings, Items.TOTEM_OF_UNDYING);
this.upgradeTemplate = load(itemMappings, Items.NETHERITE_UPGRADE_SMITHING_TEMPLATE);
- this.warpedFungusOnAStick = load(itemMappings, Items.WARPED_FUNGUS_ON_A_STICK);
this.wheat = load(itemMappings, Items.WHEAT);
this.writableBook = load(itemMappings, Items.WRITABLE_BOOK);
this.writtenBook = load(itemMappings, Items.WRITTEN_BOOK);
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/recipe/TrimRecipe.java b/core/src/main/java/org/geysermc/geyser/inventory/recipe/TrimRecipe.java
index ae7a8bd6a..9b49f8577 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/recipe/TrimRecipe.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/recipe/TrimRecipe.java
@@ -86,12 +86,12 @@ public final class TrimRecipe {
return new TrimMaterial(key, color, trimItem.getBedrockIdentifier());
}
- // TODO this is WRONG. this changed. FIXME in 1.21.5
public static TrimPattern readTrimPattern(RegistryEntryContext context) {
String key = context.id().asMinimalString();
- String itemIdentifier = context.data().getString("template_item");
- ItemMapping itemMapping = context.session().getItemMappings().getMapping(itemIdentifier);
+ // Not ideal, Java edition also gives us a translatable description... Bedrock wants the template item
+ String identifier = context.id().asString() + "_armor_trim_smithing_template";
+ ItemMapping itemMapping = context.session().getItemMappings().getMapping(identifier);
if (itemMapping == null) {
// This should never happen so not sure what to do here.
itemMapping = ItemMapping.AIR;
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java
index bea3e0507..89af5842b 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java
@@ -223,7 +223,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater {
if (!material.isEmpty()) {
totalRepairCost += getRepairCost(material);
if (isCombining(input, material)) {
- if (hasDurability(input) && input.getJavaId() == material.getJavaId()) {
+ if (hasDurability(input) && input.isSameItem(material)) {
cost += calcMergeRepairCost(input, material);
}
@@ -312,7 +312,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater {
for (Object2IntMap.Entry entry : getEnchantments(session, material).object2IntEntrySet()) {
Enchantment enchantment = entry.getKey();
- boolean canApply = isEnchantedBook(input) || session.getTagCache().is(enchantment.supportedItems(), input.asItem());
+ boolean canApply = isEnchantedBook(input) || enchantment.supportedItems().contains(session, input.asItem());
List incompatibleEnchantments = enchantment.exclusiveSet().resolve(session);
for (Enchantment incompatible : incompatibleEnchantments) {
@@ -388,11 +388,11 @@ public class AnvilInventoryUpdater extends InventoryUpdater {
}
private boolean isEnchantedBook(GeyserItemStack itemStack) {
- return itemStack.asItem() == Items.ENCHANTED_BOOK;
+ return itemStack.is(Items.ENCHANTED_BOOK);
}
private boolean isCombining(GeyserItemStack input, GeyserItemStack material) {
- return isEnchantedBook(material) || (input.getJavaId() == material.getJavaId() && hasDurability(input));
+ return isEnchantedBook(material) || (input.isSameItem(material) && hasDurability(input));
}
private boolean isRepairing(GeyserItemStack input, GeyserItemStack material, GeyserSession session) {
@@ -401,7 +401,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater {
return false;
}
- return session.getTagCache().isItem(repairable, material.asItem());
+ return material.is(session, repairable);
}
private boolean isRenaming(GeyserSession session, AnvilContainer anvilContainer, boolean bedrock) {
diff --git a/core/src/main/java/org/geysermc/geyser/item/Items.java b/core/src/main/java/org/geysermc/geyser/item/Items.java
index e86a93a42..d0bf6655d 100644
--- a/core/src/main/java/org/geysermc/geyser/item/Items.java
+++ b/core/src/main/java/org/geysermc/geyser/item/Items.java
@@ -371,6 +371,18 @@ public final class Items {
public static final Item SMOOTH_SANDSTONE = register(new BlockItem(builder(), Blocks.SMOOTH_SANDSTONE));
public static final Item SMOOTH_STONE = register(new BlockItem(builder(), Blocks.SMOOTH_STONE));
public static final Item BRICKS = register(new BlockItem(builder(), Blocks.BRICKS));
+ public static final Item ACACIA_SHELF = register(new BlockItem(builder(), Blocks.ACACIA_SHELF));
+ public static final Item BAMBOO_SHELF = register(new BlockItem(builder(), Blocks.BAMBOO_SHELF));
+ public static final Item BIRCH_SHELF = register(new BlockItem(builder(), Blocks.BIRCH_SHELF));
+ public static final Item CHERRY_SHELF = register(new BlockItem(builder(), Blocks.CHERRY_SHELF));
+ public static final Item CRIMSON_SHELF = register(new BlockItem(builder(), Blocks.CRIMSON_SHELF));
+ public static final Item DARK_OAK_SHELF = register(new BlockItem(builder(), Blocks.DARK_OAK_SHELF));
+ public static final Item JUNGLE_SHELF = register(new BlockItem(builder(), Blocks.JUNGLE_SHELF));
+ public static final Item MANGROVE_SHELF = register(new BlockItem(builder(), Blocks.MANGROVE_SHELF));
+ public static final Item OAK_SHELF = register(new BlockItem(builder(), Blocks.OAK_SHELF));
+ public static final Item PALE_OAK_SHELF = register(new BlockItem(builder(), Blocks.PALE_OAK_SHELF));
+ public static final Item SPRUCE_SHELF = register(new BlockItem(builder(), Blocks.SPRUCE_SHELF));
+ public static final Item WARPED_SHELF = register(new BlockItem(builder(), Blocks.WARPED_SHELF));
public static final Item BOOKSHELF = register(new BlockItem(builder(), Blocks.BOOKSHELF));
public static final Item CHISELED_BOOKSHELF = register(new BlockItem(builder(), Blocks.CHISELED_BOOKSHELF));
public static final Item DECORATED_POT = register(new DecoratedPotItem(builder(), Blocks.DECORATED_POT));
@@ -420,6 +432,7 @@ public final class Items {
public static final Item POLISHED_BASALT = register(new BlockItem(builder(), Blocks.POLISHED_BASALT));
public static final Item SMOOTH_BASALT = register(new BlockItem(builder(), Blocks.SMOOTH_BASALT));
public static final Item SOUL_TORCH = register(new BlockItem(builder(), Blocks.SOUL_TORCH, Blocks.SOUL_WALL_TORCH));
+ public static final Item COPPER_TORCH = register(new BlockItem(builder(), Blocks.COPPER_TORCH, Blocks.COPPER_WALL_TORCH));
public static final Item GLOWSTONE = register(new BlockItem(builder(), Blocks.GLOWSTONE));
public static final Item INFESTED_STONE = register(new BlockItem(builder(), Blocks.INFESTED_STONE));
public static final Item INFESTED_COBBLESTONE = register(new BlockItem(builder(), Blocks.INFESTED_COBBLESTONE));
@@ -444,7 +457,23 @@ public final class Items {
public static final Item RED_MUSHROOM_BLOCK = register(new BlockItem(builder(), Blocks.RED_MUSHROOM_BLOCK));
public static final Item MUSHROOM_STEM = register(new BlockItem(builder(), Blocks.MUSHROOM_STEM));
public static final Item IRON_BARS = register(new BlockItem(builder(), Blocks.IRON_BARS));
- public static final Item CHAIN = register(new BlockItem(builder(), Blocks.CHAIN));
+ public static final Item COPPER_BARS = register(new BlockItem(builder(), Blocks.COPPER_BARS));
+ public static final Item EXPOSED_COPPER_BARS = register(new BlockItem(builder(), Blocks.EXPOSED_COPPER_BARS));
+ public static final Item WEATHERED_COPPER_BARS = register(new BlockItem(builder(), Blocks.WEATHERED_COPPER_BARS));
+ public static final Item OXIDIZED_COPPER_BARS = register(new BlockItem(builder(), Blocks.OXIDIZED_COPPER_BARS));
+ public static final Item WAXED_COPPER_BARS = register(new BlockItem(builder(), Blocks.WAXED_COPPER_BARS));
+ public static final Item WAXED_EXPOSED_COPPER_BARS = register(new BlockItem(builder(), Blocks.WAXED_EXPOSED_COPPER_BARS));
+ public static final Item WAXED_WEATHERED_COPPER_BARS = register(new BlockItem(builder(), Blocks.WAXED_WEATHERED_COPPER_BARS));
+ public static final Item WAXED_OXIDIZED_COPPER_BARS = register(new BlockItem(builder(), Blocks.WAXED_OXIDIZED_COPPER_BARS));
+ public static final Item IRON_CHAIN = register(new BlockItem(builder(), Blocks.IRON_CHAIN));
+ public static final Item COPPER_CHAIN = register(new BlockItem(builder(), Blocks.COPPER_CHAIN));
+ public static final Item EXPOSED_COPPER_CHAIN = register(new BlockItem(builder(), Blocks.EXPOSED_COPPER_CHAIN));
+ public static final Item WEATHERED_COPPER_CHAIN = register(new BlockItem(builder(), Blocks.WEATHERED_COPPER_CHAIN));
+ public static final Item OXIDIZED_COPPER_CHAIN = register(new BlockItem(builder(), Blocks.OXIDIZED_COPPER_CHAIN));
+ public static final Item WAXED_COPPER_CHAIN = register(new BlockItem(builder(), Blocks.WAXED_COPPER_CHAIN));
+ public static final Item WAXED_EXPOSED_COPPER_CHAIN = register(new BlockItem(builder(), Blocks.WAXED_EXPOSED_COPPER_CHAIN));
+ public static final Item WAXED_WEATHERED_COPPER_CHAIN = register(new BlockItem(builder(), Blocks.WAXED_WEATHERED_COPPER_CHAIN));
+ public static final Item WAXED_OXIDIZED_COPPER_CHAIN = register(new BlockItem(builder(), Blocks.WAXED_OXIDIZED_COPPER_CHAIN));
public static final Item GLASS_PANE = register(new BlockItem(builder(), Blocks.GLASS_PANE));
public static final Item MELON = register(new BlockItem(builder(), Blocks.MELON));
public static final Item VINE = register(new BlockItem(builder(), Blocks.VINE));
@@ -771,6 +800,13 @@ public final class Items {
public static final Item TARGET = register(new BlockItem(builder(), Blocks.TARGET));
public static final Item LEVER = register(new BlockItem(builder(), Blocks.LEVER));
public static final Item LIGHTNING_ROD = register(new BlockItem(builder(), Blocks.LIGHTNING_ROD));
+ public static final Item EXPOSED_LIGHTNING_ROD = register(new BlockItem(builder(), Blocks.EXPOSED_LIGHTNING_ROD));
+ public static final Item WEATHERED_LIGHTNING_ROD = register(new BlockItem(builder(), Blocks.WEATHERED_LIGHTNING_ROD));
+ public static final Item OXIDIZED_LIGHTNING_ROD = register(new BlockItem(builder(), Blocks.OXIDIZED_LIGHTNING_ROD));
+ public static final Item WAXED_LIGHTNING_ROD = register(new BlockItem(builder(), Blocks.WAXED_LIGHTNING_ROD));
+ public static final Item WAXED_EXPOSED_LIGHTNING_ROD = register(new BlockItem(builder(), Blocks.WAXED_EXPOSED_LIGHTNING_ROD));
+ public static final Item WAXED_WEATHERED_LIGHTNING_ROD = register(new BlockItem(builder(), Blocks.WAXED_WEATHERED_LIGHTNING_ROD));
+ public static final Item WAXED_OXIDIZED_LIGHTNING_ROD = register(new BlockItem(builder(), Blocks.WAXED_OXIDIZED_LIGHTNING_ROD));
public static final Item DAYLIGHT_DETECTOR = register(new BlockItem(builder(), Blocks.DAYLIGHT_DETECTOR));
public static final Item SCULK_SENSOR = register(new BlockItem(builder(), Blocks.SCULK_SENSOR));
public static final Item CALIBRATED_SCULK_SENSOR = register(new BlockItem(builder(), Blocks.CALIBRATED_SCULK_SENSOR));
@@ -946,6 +982,11 @@ public final class Items {
public static final Item WOODEN_PICKAXE = register(new Item("wooden_pickaxe", builder().attackDamage(2.0)));
public static final Item WOODEN_AXE = register(new Item("wooden_axe", builder().attackDamage(7.0)));
public static final Item WOODEN_HOE = register(new Item("wooden_hoe", builder().attackDamage(1.0)));
+ public static final Item COPPER_SWORD = register(new Item("copper_sword", builder().attackDamage(5.0)));
+ public static final Item COPPER_SHOVEL = register(new Item("copper_shovel", builder().attackDamage(3.5)));
+ public static final Item COPPER_PICKAXE = register(new Item("copper_pickaxe", builder().attackDamage(3.0)));
+ public static final Item COPPER_AXE = register(new Item("copper_axe", builder().attackDamage(9.0)));
+ public static final Item COPPER_HOE = register(new Item("copper_hoe", builder().attackDamage(1.0)));
public static final Item STONE_SWORD = register(new Item("stone_sword", builder().attackDamage(5.0)));
public static final Item STONE_SHOVEL = register(new Item("stone_shovel", builder().attackDamage(3.5)));
public static final Item STONE_PICKAXE = register(new Item("stone_pickaxe", builder().attackDamage(3.0)));
@@ -983,6 +1024,10 @@ public final class Items {
public static final Item LEATHER_CHESTPLATE = register(new DyeableArmorItem("leather_chestplate", builder()));
public static final Item LEATHER_LEGGINGS = register(new DyeableArmorItem("leather_leggings", builder()));
public static final Item LEATHER_BOOTS = register(new DyeableArmorItem("leather_boots", builder()));
+ public static final Item COPPER_HELMET = register(new ArmorItem("copper_helmet", builder()));
+ public static final Item COPPER_CHESTPLATE = register(new ArmorItem("copper_chestplate", builder()));
+ public static final Item COPPER_LEGGINGS = register(new ArmorItem("copper_leggings", builder()));
+ public static final Item COPPER_BOOTS = register(new ArmorItem("copper_boots", builder()));
public static final Item CHAINMAIL_HELMET = register(new ArmorItem("chainmail_helmet", builder()));
public static final Item CHAINMAIL_CHESTPLATE = register(new ArmorItem("chainmail_chestplate", builder()));
public static final Item CHAINMAIL_LEGGINGS = register(new ArmorItem("chainmail_leggings", builder()));
@@ -1148,7 +1193,7 @@ public final class Items {
public static final Item BLAZE_POWDER = register(new Item("blaze_powder", builder()));
public static final Item MAGMA_CREAM = register(new Item("magma_cream", builder()));
public static final Item BREWING_STAND = register(new BlockItem(builder(), Blocks.BREWING_STAND));
- public static final Item CAULDRON = register(new BlockItem(builder(), Blocks.CAULDRON, Blocks.LAVA_CAULDRON, Blocks.POWDER_SNOW_CAULDRON, Blocks.WATER_CAULDRON));
+ public static final Item CAULDRON = register(new BlockItem(builder(), Blocks.CAULDRON, Blocks.WATER_CAULDRON, Blocks.POWDER_SNOW_CAULDRON, Blocks.LAVA_CAULDRON));
public static final Item ENDER_EYE = register(new Item("ender_eye", builder()));
public static final Item GLISTERING_MELON_SLICE = register(new Item("glistering_melon_slice", builder()));
public static final Item ARMADILLO_SPAWN_EGG = register(new SpawnEggItem("armadillo_spawn_egg", builder()));
@@ -1164,6 +1209,7 @@ public final class Items {
public static final Item CAVE_SPIDER_SPAWN_EGG = register(new SpawnEggItem("cave_spider_spawn_egg", builder()));
public static final Item CHICKEN_SPAWN_EGG = register(new SpawnEggItem("chicken_spawn_egg", builder()));
public static final Item COD_SPAWN_EGG = register(new SpawnEggItem("cod_spawn_egg", builder()));
+ public static final Item COPPER_GOLEM_SPAWN_EGG = register(new SpawnEggItem("copper_golem_spawn_egg", builder()));
public static final Item COW_SPAWN_EGG = register(new SpawnEggItem("cow_spawn_egg", builder()));
public static final Item CREEPER_SPAWN_EGG = register(new SpawnEggItem("creeper_spawn_egg", builder()));
public static final Item DOLPHIN_SPAWN_EGG = register(new SpawnEggItem("dolphin_spawn_egg", builder()));
@@ -1271,6 +1317,7 @@ public final class Items {
public static final Item RABBIT_FOOT = register(new Item("rabbit_foot", builder()));
public static final Item RABBIT_HIDE = register(new Item("rabbit_hide", builder()));
public static final Item ARMOR_STAND = register(new Item("armor_stand", builder()));
+ public static final Item COPPER_HORSE_ARMOR = register(new Item("copper_horse_armor", builder()));
public static final Item IRON_HORSE_ARMOR = register(new Item("iron_horse_armor", builder()));
public static final Item GOLDEN_HORSE_ARMOR = register(new Item("golden_horse_armor", builder()));
public static final Item DIAMOND_HORSE_ARMOR = register(new Item("diamond_horse_armor", builder()));
@@ -1313,6 +1360,7 @@ public final class Items {
public static final Item TOTEM_OF_UNDYING = register(new Item("totem_of_undying", builder()));
public static final Item SHULKER_SHELL = register(new Item("shulker_shell", builder()));
public static final Item IRON_NUGGET = register(new Item("iron_nugget", builder()));
+ public static final Item COPPER_NUGGET = register(new Item("copper_nugget", builder()));
public static final Item KNOWLEDGE_BOOK = register(new Item("knowledge_book", builder()));
public static final Item DEBUG_STICK = register(new Item("debug_stick", builder()));
public static final Item MUSIC_DISC_13 = register(new Item("music_disc_13", builder()));
@@ -1366,6 +1414,14 @@ public final class Items {
public static final Item BELL = register(new BlockItem(builder(), Blocks.BELL));
public static final Item LANTERN = register(new BlockItem(builder(), Blocks.LANTERN));
public static final Item SOUL_LANTERN = register(new BlockItem(builder(), Blocks.SOUL_LANTERN));
+ public static final Item COPPER_LANTERN = register(new BlockItem(builder(), Blocks.COPPER_LANTERN));
+ public static final Item EXPOSED_COPPER_LANTERN = register(new BlockItem(builder(), Blocks.EXPOSED_COPPER_LANTERN));
+ public static final Item WEATHERED_COPPER_LANTERN = register(new BlockItem(builder(), Blocks.WEATHERED_COPPER_LANTERN));
+ public static final Item OXIDIZED_COPPER_LANTERN = register(new BlockItem(builder(), Blocks.OXIDIZED_COPPER_LANTERN));
+ public static final Item WAXED_COPPER_LANTERN = register(new BlockItem(builder(), Blocks.WAXED_COPPER_LANTERN));
+ public static final Item WAXED_EXPOSED_COPPER_LANTERN = register(new BlockItem(builder(), Blocks.WAXED_EXPOSED_COPPER_LANTERN));
+ public static final Item WAXED_WEATHERED_COPPER_LANTERN = register(new BlockItem(builder(), Blocks.WAXED_WEATHERED_COPPER_LANTERN));
+ public static final Item WAXED_OXIDIZED_COPPER_LANTERN = register(new BlockItem(builder(), Blocks.WAXED_OXIDIZED_COPPER_LANTERN));
public static final Item SWEET_BERRIES = register(new BlockItem("sweet_berries", builder(), Blocks.SWEET_BERRY_BUSH));
public static final Item GLOW_BERRIES = register(new BlockItem("glow_berries", builder(), Blocks.CAVE_VINES));
public static final Item CAMPFIRE = register(new BlockItem(builder(), Blocks.CAMPFIRE));
@@ -1477,6 +1533,22 @@ public final class Items {
public static final Item WAXED_EXPOSED_COPPER_BULB = register(new BlockItem(builder(), Blocks.WAXED_EXPOSED_COPPER_BULB));
public static final Item WAXED_WEATHERED_COPPER_BULB = register(new BlockItem(builder(), Blocks.WAXED_WEATHERED_COPPER_BULB));
public static final Item WAXED_OXIDIZED_COPPER_BULB = register(new BlockItem(builder(), Blocks.WAXED_OXIDIZED_COPPER_BULB));
+ public static final Item COPPER_CHEST = register(new BlockItem(builder(), Blocks.COPPER_CHEST));
+ public static final Item EXPOSED_COPPER_CHEST = register(new BlockItem(builder(), Blocks.EXPOSED_COPPER_CHEST));
+ public static final Item WEATHERED_COPPER_CHEST = register(new BlockItem(builder(), Blocks.WEATHERED_COPPER_CHEST));
+ public static final Item OXIDIZED_COPPER_CHEST = register(new BlockItem(builder(), Blocks.OXIDIZED_COPPER_CHEST));
+ public static final Item WAXED_COPPER_CHEST = register(new BlockItem(builder(), Blocks.WAXED_COPPER_CHEST));
+ public static final Item WAXED_EXPOSED_COPPER_CHEST = register(new BlockItem(builder(), Blocks.WAXED_EXPOSED_COPPER_CHEST));
+ public static final Item WAXED_WEATHERED_COPPER_CHEST = register(new BlockItem(builder(), Blocks.WAXED_WEATHERED_COPPER_CHEST));
+ public static final Item WAXED_OXIDIZED_COPPER_CHEST = register(new BlockItem(builder(), Blocks.WAXED_OXIDIZED_COPPER_CHEST));
+ public static final Item COPPER_GOLEM_STATUE = register(new BlockItem(builder(), Blocks.COPPER_GOLEM_STATUE));
+ public static final Item EXPOSED_COPPER_GOLEM_STATUE = register(new BlockItem(builder(), Blocks.EXPOSED_COPPER_GOLEM_STATUE));
+ public static final Item WEATHERED_COPPER_GOLEM_STATUE = register(new BlockItem(builder(), Blocks.WEATHERED_COPPER_GOLEM_STATUE));
+ public static final Item OXIDIZED_COPPER_GOLEM_STATUE = register(new BlockItem(builder(), Blocks.OXIDIZED_COPPER_GOLEM_STATUE));
+ public static final Item WAXED_COPPER_GOLEM_STATUE = register(new BlockItem(builder(), Blocks.WAXED_COPPER_GOLEM_STATUE));
+ public static final Item WAXED_EXPOSED_COPPER_GOLEM_STATUE = register(new BlockItem(builder(), Blocks.WAXED_EXPOSED_COPPER_GOLEM_STATUE));
+ public static final Item WAXED_WEATHERED_COPPER_GOLEM_STATUE = register(new BlockItem(builder(), Blocks.WAXED_WEATHERED_COPPER_GOLEM_STATUE));
+ public static final Item WAXED_OXIDIZED_COPPER_GOLEM_STATUE = register(new BlockItem(builder(), Blocks.WAXED_OXIDIZED_COPPER_GOLEM_STATUE));
public static final Item TRIAL_SPAWNER = register(new BlockItem(builder(), Blocks.TRIAL_SPAWNER));
public static final Item TRIAL_KEY = register(new Item("trial_key", builder()));
public static final Item OMINOUS_TRIAL_KEY = register(new Item("ominous_trial_key", builder()));
diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/DataComponentHashers.java b/core/src/main/java/org/geysermc/geyser/item/hashing/DataComponentHashers.java
index b3343c9bb..ca9840994 100644
--- a/core/src/main/java/org/geysermc/geyser/item/hashing/DataComponentHashers.java
+++ b/core/src/main/java/org/geysermc/geyser/item/hashing/DataComponentHashers.java
@@ -74,6 +74,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.item.component.MobEffectIns
import org.geysermc.mcprotocollib.protocol.data.game.item.component.PotionContents;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.ToolData;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.TooltipDisplay;
+import org.geysermc.mcprotocollib.protocol.data.game.item.component.TypedEntityData;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.Unit;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.UseCooldown;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.Weapon;
@@ -213,9 +214,15 @@ public class DataComponentHashers {
register(DataComponentTypes.TRIM, RegistryHasher.ARMOR_TRIM);
register(DataComponentTypes.DEBUG_STICK_STATE, MinecraftHasher.NBT_MAP);
- register(DataComponentTypes.ENTITY_DATA, MinecraftHasher.NBT_MAP);
+ registerMap(DataComponentTypes.ENTITY_DATA, builder -> builder
+ .accept("id", RegistryHasher.ENTITY_TYPE_KEY, TypedEntityData::type)
+ .inlineNbt(TypedEntityData::tag)
+ );
register(DataComponentTypes.BUCKET_ENTITY_DATA, MinecraftHasher.NBT_MAP);
- register(DataComponentTypes.BLOCK_ENTITY_DATA, MinecraftHasher.NBT_MAP);
+ registerMap(DataComponentTypes.BLOCK_ENTITY_DATA, builder -> builder
+ .accept("id", RegistryHasher.BLOCK_ENTITY_TYPE_KEY, TypedEntityData::type)
+ .inlineNbt(TypedEntityData::tag)
+ );
register(DataComponentTypes.INSTRUMENT, RegistryHasher.INSTRUMENT_COMPONENT);
register(DataComponentTypes.PROVIDES_TRIM_MATERIAL, RegistryHasher.PROVIDES_TRIM_MATERIAL);
@@ -235,7 +242,7 @@ public class DataComponentHashers {
.optional("flight_duration", MinecraftHasher.BYTE, fireworks -> (byte) fireworks.getFlightDuration(), (byte) 0)
.optionalList("explosions", RegistryHasher.FIREWORK_EXPLOSION, Fireworks::getExplosions));
- register(DataComponentTypes.PROFILE, MinecraftHasher.GAME_PROFILE);
+ register(DataComponentTypes.PROFILE, MinecraftHasher.RESOLVABLE_PROFILE);
register(DataComponentTypes.NOTE_BLOCK_SOUND, MinecraftHasher.KEY);
register(DataComponentTypes.BANNER_PATTERNS, RegistryHasher.BANNER_PATTERN_LAYER.list());
register(DataComponentTypes.BASE_COLOR, MinecraftHasher.DYE_COLOR);
diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/MapHasher.java b/core/src/main/java/org/geysermc/geyser/item/hashing/MapHasher.java
index 2072ace29..187360308 100644
--- a/core/src/main/java/org/geysermc/geyser/item/hashing/MapHasher.java
+++ b/core/src/main/java/org/geysermc/geyser/item/hashing/MapHasher.java
@@ -26,6 +26,8 @@
package org.geysermc.geyser.item.hashing;
import com.google.common.hash.HashCode;
+import org.cloudburstmc.nbt.NbtList;
+import org.cloudburstmc.nbt.NbtMap;
import java.util.HashMap;
import java.util.List;
@@ -67,6 +69,14 @@ public class MapHasher {
return this;
}
+ private MapHasher accept(String key, Object value, HashCode valueHash) {
+ if (unhashed != null) {
+ unhashed.put(key, value);
+ }
+ map.put(encoder.string(key), valueHash);
+ return this;
+ }
+
/**
* Adds a constant {@link Value} to the map.
*
@@ -82,6 +92,25 @@ public class MapHasher {
return accept(key, hasher.hash(value, encoder));
}
+ public MapHasher inlineNbt(Function extractor) {
+ NbtMap nbtMap = extractor.apply(object);
+ for (String key : nbtMap.keySet()) {
+ Object value = nbtMap.get(key);
+ if (value instanceof NbtList> list) {
+ accept(key, value, encoder.nbtList(list));
+ } else {
+ nbtMap.listenForNumber(key, n -> accept(key, value, encoder.number(n)));
+ nbtMap.listenForString(key, s -> accept(key, value, encoder.string(s)));
+ nbtMap.listenForCompound(key, compound -> accept(key, value, encoder.nbtMap(compound)));
+
+ nbtMap.listenForByteArray(key, bytes -> accept(key, value, encoder.byteArray(bytes)));
+ nbtMap.listenForIntArray(key, ints -> accept(key, value, encoder.intArray(ints)));
+ nbtMap.listenForLongArray(key, longs -> accept(key, value, encoder.longArray(longs)));
+ }
+ }
+ return this;
+ }
+
/**
* Extracts a {@link Value} from a {@link Type} using the {@code extractor}, and adds it to the map.
*
diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java b/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java
index 08c79c049..7f135e2d6 100644
--- a/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java
+++ b/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java
@@ -37,6 +37,7 @@ import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.mcprotocollib.auth.GameProfile;
import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.GlobalPos;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.player.ResolvableProfile;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.Filterable;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.ItemAttributeModifiers;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.Unit;
@@ -45,6 +46,7 @@ import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Optional;
import java.util.UUID;
import java.util.function.BiFunction;
import java.util.function.Function;
@@ -131,10 +133,15 @@ public interface MinecraftHasher {
.accept("value", STRING, GameProfile.Property::getValue)
.optionalNullable("signature", STRING, GameProfile.Property::getSignature));
- MinecraftHasher GAME_PROFILE = mapBuilder(builder -> builder
- .optionalNullable("name", STRING, GameProfile::getName)
- .optionalNullable("id", UUID, GameProfile::getId)
- .optionalList("properties", GAME_PROFILE_PROPERTY, GameProfile::getProperties));
+ MinecraftHasher RESOLVABLE_PROFILE = mapBuilder(builder -> builder
+ .optionalNullable("name", STRING, resolvableProfile -> resolvableProfile.getProfile().getName())
+ .optionalNullable("id", UUID, resolvableProfile -> resolvableProfile.getProfile().getId())
+ .optionalList("properties", GAME_PROFILE_PROPERTY, resolvableProfile -> resolvableProfile.getProfile().getProperties())
+ .optionalNullable("texture", KEY, ResolvableProfile::getBody)
+ .optionalNullable("cape", KEY, ResolvableProfile::getCape)
+ .optionalNullable("elytra", KEY, ResolvableProfile::getElytra)
+ .optional("model", STRING, resolvableProfile -> Optional.ofNullable(resolvableProfile.getModel()).map(GameProfile.TextureModel::name))
+ );
MinecraftHasher RARITY = fromIdEnum(Rarity.values(), Rarity::getName);
diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/RegistryHasher.java b/core/src/main/java/org/geysermc/geyser/item/hashing/RegistryHasher.java
index cd72874e5..85916592c 100644
--- a/core/src/main/java/org/geysermc/geyser/item/hashing/RegistryHasher.java
+++ b/core/src/main/java/org/geysermc/geyser/item/hashing/RegistryHasher.java
@@ -27,7 +27,6 @@ package org.geysermc.geyser.item.hashing;
import com.google.common.hash.HashCode;
import net.kyori.adventure.key.Key;
-import org.cloudburstmc.nbt.NbtMap;
import org.geysermc.geyser.inventory.item.Potion;
import org.geysermc.geyser.item.hashing.data.ConsumeEffectType;
import org.geysermc.geyser.item.hashing.data.FireworkExplosionShape;
@@ -74,6 +73,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.item.component.ProvidesTrim
import org.geysermc.mcprotocollib.protocol.data.game.item.component.SuspiciousStewEffect;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.ToolData;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.Unit;
+import org.geysermc.mcprotocollib.protocol.data.game.level.block.BlockEntityType;
import org.geysermc.mcprotocollib.protocol.data.game.level.sound.BuiltinSound;
import org.geysermc.mcprotocollib.protocol.data.game.level.sound.CustomSound;
import org.geysermc.mcprotocollib.protocol.data.game.level.sound.Sound;
@@ -110,6 +110,10 @@ public interface RegistryHasher extends MinecraftHasher {
RegistryHasher> ENTITY_TYPE = enumIdRegistry(EntityType.values());
+ MinecraftHasher ENTITY_TYPE_KEY = enumRegistry();
+
+ MinecraftHasher BLOCK_ENTITY_TYPE_KEY = enumRegistry();
+
RegistryHasher> ENCHANTMENT = registry(JavaRegistries.ENCHANTMENT);
RegistryHasher> ATTRIBUTE = enumIdRegistry(AttributeType.Builtin.values(), AttributeType::getIdentifier);
@@ -346,7 +350,8 @@ public interface RegistryHasher extends MinecraftHasher {
.optional("has_twinkle", BOOL, Fireworks.FireworkExplosion::isHasTwinkle, false));
MinecraftHasher BEEHIVE_OCCUPANT = MinecraftHasher.mapBuilder(builder -> builder
- .optional("entity_data", NBT_MAP, BeehiveOccupant::getEntityData, NbtMap.EMPTY)
+ .accept("id", RegistryHasher.ENTITY_TYPE_KEY, beehiveOccupant -> beehiveOccupant.getEntityData().type())
+ .inlineNbt(beehiveOccupant -> beehiveOccupant.getEntityData().tag())
.accept("ticks_in_hive", INT, BeehiveOccupant::getTicksInHive)
.accept("min_ticks_in_hive", INT, BeehiveOccupant::getMinTicksInHive));
diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/data/FireworkExplosionShape.java b/core/src/main/java/org/geysermc/geyser/item/hashing/data/FireworkExplosionShape.java
index 316253e3e..bc880ef85 100644
--- a/core/src/main/java/org/geysermc/geyser/item/hashing/data/FireworkExplosionShape.java
+++ b/core/src/main/java/org/geysermc/geyser/item/hashing/data/FireworkExplosionShape.java
@@ -25,11 +25,22 @@
package org.geysermc.geyser.item.hashing.data;
+import java.util.Locale;
+
// Ordered and named by Java ID
public enum FireworkExplosionShape {
SMALL_BALL,
LARGE_BALL,
STAR,
CREEPER,
- BURST
+ BURST;
+
+ public static FireworkExplosionShape fromJavaIdentifier(String identifier) {
+ for (FireworkExplosionShape shape : values()) {
+ if (shape.name().toLowerCase(Locale.ROOT).equals(identifier)) {
+ return shape;
+ }
+ }
+ return null;
+ }
}
diff --git a/core/src/main/java/org/geysermc/geyser/item/parser/ItemStackParser.java b/core/src/main/java/org/geysermc/geyser/item/parser/ItemStackParser.java
new file mode 100644
index 000000000..69b330791
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/item/parser/ItemStackParser.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (c) 2025 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.parser;
+
+import it.unimi.dsi.fastutil.ints.Int2IntMap;
+import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
+import net.kyori.adventure.key.Key;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.cloudburstmc.nbt.NbtMap;
+import org.cloudburstmc.nbt.NbtMapBuilder;
+import org.cloudburstmc.nbt.NbtType;
+import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
+import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.inventory.item.DyeColor;
+import org.geysermc.geyser.inventory.item.Potion;
+import org.geysermc.geyser.item.Items;
+import org.geysermc.geyser.item.hashing.data.FireworkExplosionShape;
+import org.geysermc.geyser.item.type.Item;
+import org.geysermc.geyser.registry.Registries;
+import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.session.cache.registry.JavaRegistries;
+import org.geysermc.geyser.translator.item.BedrockItemBuilder;
+import org.geysermc.geyser.translator.item.ItemTranslator;
+import org.geysermc.geyser.translator.level.block.entity.SkullBlockEntityTranslator;
+import org.geysermc.geyser.util.MinecraftKey;
+import org.geysermc.mcprotocollib.protocol.data.game.Holder;
+import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;
+import org.geysermc.mcprotocollib.protocol.data.game.item.component.BannerPatternLayer;
+import org.geysermc.mcprotocollib.protocol.data.game.item.component.CustomModelData;
+import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType;
+import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes;
+import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents;
+import org.geysermc.mcprotocollib.protocol.data.game.item.component.Fireworks;
+import org.geysermc.mcprotocollib.protocol.data.game.item.component.ItemEnchantments;
+import org.geysermc.mcprotocollib.protocol.data.game.item.component.PotionContents;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+/**
+ * Utility class to parse an item stack, or a data component patch, from NBT data.
+ *
+ *
This class does NOT parse all possible data components in a data component patch, only those that
+ * can visually change the way an item looks. This class should/is usually used for parsing block entity NBT data,
+ * such as for vault or shelf block entities.
+ *
+ * Be sure to update this class for Java updates!
+ */
+// Lots of unchecked casting happens here. It should all be handled properly.
+@SuppressWarnings("unchecked")
+// TODO only log some things once (like was done in vault translator)
+public final class ItemStackParser {
+ private static final Map, DataComponentParser, ?>> PARSERS = new Reference2ObjectOpenHashMap<>();
+
+ // We need the rawClass parameter here because the Raw type can't be inferred from the parser alone
+ private static void register(DataComponentType component, Class rawClass, DataComponentParser parser) {
+ if (PARSERS.containsKey(component)) {
+ throw new IllegalStateException("Duplicate data component parser registered for " + component);
+ }
+ PARSERS.put(component, parser);
+ }
+
+ private static void registerSimple(DataComponentType component, Class rawClass, Function parser) {
+ register(component, rawClass, (session, raw) -> parser.apply(raw));
+ }
+
+ private static void registerSimple(DataComponentType component, Class parsedClass) {
+ registerSimple(component, parsedClass, Function.identity());
+ }
+
+ private static int javaItemIdentifierToNetworkId(String identifier) {
+ if (identifier == null || identifier.isEmpty()) {
+ return Items.AIR_ID;
+ }
+
+ Item item = Registries.JAVA_ITEM_IDENTIFIERS.get(identifier);
+ if (item == null) {
+ GeyserImpl.getInstance().getLogger().warning("Received unknown item ID " + identifier + " whilst parsing NBT item stack!");
+ return Items.AIR_ID;
+ }
+ return item.javaId();
+ }
+
+ private static ItemEnchantments parseEnchantments(GeyserSession session, NbtMap map) {
+ Int2IntMap enchantments = new Int2IntOpenHashMap(map.size());
+ for (Map.Entry entry : map.entrySet()) {
+ enchantments.put(JavaRegistries.ENCHANTMENT.networkId(session, MinecraftKey.key(entry.getKey())), (int) entry.getValue());
+ }
+ return new ItemEnchantments(enchantments);
+ }
+
+ static {
+ // The various ignored null-warnings are for things that should never be null as they shouldn't be missing from the data component
+ // If they are null an exception will be thrown, but this will be caught with an error message logged
+
+ register(DataComponentTypes.BANNER_PATTERNS, List.class, (session, raw) -> {
+ List casted = (List) raw;
+ List layers = new ArrayList<>();
+ for (NbtMap layer : casted) {
+ DyeColor colour = DyeColor.getByJavaIdentifier(layer.getString("color"));
+
+ // Patterns can be an ID or inline
+ Object pattern = layer.get("pattern");
+ Holder patternHolder;
+ if (pattern instanceof String id) {
+ patternHolder = Holder.ofId(JavaRegistries.BANNER_PATTERN.networkId(session, MinecraftKey.key(id)));
+ } else {
+ NbtMap inline = (NbtMap) pattern;
+ Key assetId = MinecraftKey.key(inline.getString("asset_id"));
+ String translationKey = inline.getString("translation_key");
+ patternHolder = Holder.ofCustom(new BannerPatternLayer.BannerPattern(assetId, translationKey));
+ }
+ layers.add(new BannerPatternLayer(patternHolder, colour.ordinal()));
+ }
+ return layers;
+ });
+ registerSimple(DataComponentTypes.BASE_COLOR, String.class, raw -> DyeColor.getByJavaIdentifier(raw).ordinal());
+ register(DataComponentTypes.CHARGED_PROJECTILES, List.class,
+ (session, projectiles) -> projectiles.stream()
+ .map(object -> parseItemStack(session, (NbtMap) object))
+ .toList());
+ registerSimple(DataComponentTypes.CUSTOM_MODEL_DATA, NbtMap.class, raw -> {
+ List floats = raw.getList("floats", NbtType.FLOAT);
+ List flags = raw.getList("flags", NbtType.BYTE).stream().map(b -> b != 0).toList();
+ List strings = raw.getList("strings", NbtType.STRING);
+ List colours = raw.getList("colors", NbtType.INT);
+ return new CustomModelData(floats, flags, strings, colours);
+ });
+ registerSimple(DataComponentTypes.DYED_COLOR, Integer.class);
+ registerSimple(DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE, Boolean.class);
+ register(DataComponentTypes.ENCHANTMENTS, NbtMap.class, ItemStackParser::parseEnchantments);
+ registerSimple(DataComponentTypes.FIREWORK_EXPLOSION, NbtMap.class, raw -> {
+ FireworkExplosionShape shape = FireworkExplosionShape.fromJavaIdentifier(raw.getString("shape"));
+ List colours = raw.getList("colors", NbtType.INT);
+ List fadeColours = raw.getList("fade_colors", NbtType.INT);
+ boolean hasTrail = raw.getBoolean("has_trail");
+ boolean hasTwinkle = raw.getBoolean("has_twinkle");
+ return new Fireworks.FireworkExplosion(shape.ordinal(),
+ colours.stream()
+ .mapToInt(i -> i) // We need to do this because MCPL wants an int[] array
+ .toArray(),
+ fadeColours.stream()
+ .mapToInt(i -> i)
+ .toArray(),
+ hasTrail, hasTwinkle);
+ });
+ registerSimple(DataComponentTypes.ITEM_MODEL, String.class, MinecraftKey::key);
+ registerSimple(DataComponentTypes.MAP_COLOR, Integer.class);
+ registerSimple(DataComponentTypes.POT_DECORATIONS, List.class, list -> list.stream()
+ .map(item -> javaItemIdentifierToNetworkId((String) item))
+ .toList());
+ register(DataComponentTypes.POTION_CONTENTS, NbtMap.class, (session, map) -> {
+ Potion potion = Potion.getByJavaIdentifier(map.getString("potion"));
+ int customColour = map.getInt("custom_color", -1);
+ // Not reading custom effects
+ String customName = map.getString("custom_name", null);
+ return new PotionContents(potion == null ? -1 : potion.ordinal(), customColour, List.of(), customName);
+ });
+ registerSimple(DataComponentTypes.PROFILE, NbtMap.class, SkullBlockEntityTranslator::parseResolvableProfile);
+ register(DataComponentTypes.STORED_ENCHANTMENTS, NbtMap.class, ItemStackParser::parseEnchantments);
+ }
+
+ private static void parseDataComponent(GeyserSession session, DataComponents patch, DataComponentType> type,
+ DataComponentParser parser, Object raw) {
+ try {
+ patch.put((DataComponentType) type, parser.parse(session, (Raw) raw));
+ } catch (ClassCastException exception) {
+ GeyserImpl.getInstance().getLogger().error("Received incorrect object type for component " + type + "!", exception);
+ } catch (Exception exception) {
+ GeyserImpl.getInstance().getLogger().error("Failed to parse component" + type + " from " + raw + "!", exception);
+ }
+ }
+
+ public static @Nullable DataComponents parseDataComponentPatch(GeyserSession session, @Nullable NbtMap map) {
+ if (map == null || map.isEmpty()) {
+ return null;
+ }
+
+ DataComponents patch = new DataComponents(new Reference2ObjectOpenHashMap<>());
+ try {
+ for (Map.Entry patchEntry : map.entrySet()) {
+ String rawType = patchEntry.getKey();
+ // When a component starts with a '!', indicates removal of the component from the default component set
+ boolean removal = rawType.startsWith("!");
+ if (removal) {
+ rawType = rawType.substring(1);
+ }
+
+ DataComponentType> type = DataComponentTypes.fromKey(MinecraftKey.key(rawType));
+ if (type == null) {
+ GeyserImpl.getInstance().getLogger().warning("Received unknown data component " + rawType + " in NBT data component patch: " + map);
+ } else if (removal) {
+ // Removals are easy, we don't have to parse anything
+ patch.put(type, null);
+ } else {
+ DataComponentParser, ?> parser = PARSERS.get(type);
+ if (parser != null) {
+ parseDataComponent(session, patch, type, parser, patchEntry.getValue());
+ } else {
+ GeyserImpl.getInstance().getLogger().debug("Ignoring data component " + type + " whilst parsing NBT patch because there is no parser registered for it");
+ }
+ }
+ }
+ } catch (Exception exception) {
+ GeyserImpl.getInstance().getLogger().error("Failed to parse data component patch from NBT data!", exception);
+ }
+
+ return patch;
+ }
+
+ public static ItemStack parseItemStack(GeyserSession session, @Nullable NbtMap map) {
+ if (map == null) {
+ return new ItemStack(Items.AIR_ID);
+ }
+
+ try {
+ int id = javaItemIdentifierToNetworkId(map.getString("id"));
+ int count = map.getInt("count");
+ DataComponents patch = parseDataComponentPatch(session, map.getCompound("components"));
+ return new ItemStack(id, count, patch);
+ } catch (Exception exception) {
+ GeyserImpl.getInstance().getLogger().error("Failed to parse item stack from NBT data!", exception);
+ }
+ return new ItemStack(Items.AIR_ID);
+ }
+
+ /**
+ * Shorthand method for calling the following methods:
+ *
+ *
+ * - {@link ItemStackParser#parseItemStack(GeyserSession, NbtMap)}
+ * - {@link ItemTranslator#translateToBedrock(GeyserSession, ItemStack)}
+ * - {@link BedrockItemBuilder#createItemNbt(ItemData)}
+ *
+ */
+ public static NbtMapBuilder javaItemStackToBedrock(GeyserSession session, @Nullable NbtMap map) {
+ return BedrockItemBuilder.createItemNbt(ItemTranslator.translateToBedrock(session, parseItemStack(session, map)));
+ }
+
+ private ItemStackParser() {}
+
+ @FunctionalInterface
+ private interface DataComponentParser {
+
+ Parsed parse(GeyserSession session, Raw raw) throws Exception;
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/item/type/CrossbowItem.java b/core/src/main/java/org/geysermc/geyser/item/type/CrossbowItem.java
index 9eb95e4fb..27224f027 100644
--- a/core/src/main/java/org/geysermc/geyser/item/type/CrossbowItem.java
+++ b/core/src/main/java/org/geysermc/geyser/item/type/CrossbowItem.java
@@ -52,11 +52,8 @@ public class CrossbowItem extends Item {
if (chargedProjectiles != null && !chargedProjectiles.isEmpty()) {
ItemStack javaProjectile = chargedProjectiles.get(0);
- ItemMapping projectileMapping = session.getItemMappings().getMapping(javaProjectile.getId());
ItemData itemData = ItemTranslator.translateToBedrock(session, javaProjectile);
-
- NbtMapBuilder newProjectile = BedrockItemBuilder.createItemNbt(projectileMapping, itemData.getCount(), itemData.getDamage());
-
+ NbtMapBuilder newProjectile = BedrockItemBuilder.createItemNbt(itemData);
builder.putCompound("chargedItem", newProjectile.build());
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/item/type/Item.java b/core/src/main/java/org/geysermc/geyser/item/type/Item.java
index 84d3e37fb..7c614255a 100644
--- a/core/src/main/java/org/geysermc/geyser/item/type/Item.java
+++ b/core/src/main/java/org/geysermc/geyser/item/type/Item.java
@@ -45,6 +45,7 @@ import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.registry.type.ItemMappings;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.registry.JavaRegistries;
+import org.geysermc.geyser.session.cache.tags.Tag;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.text.MinecraftLocale;
import org.geysermc.geyser.translator.item.BedrockItemBuilder;
@@ -53,6 +54,7 @@ import org.geysermc.geyser.util.MinecraftKey;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents;
+import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.ItemEnchantments;
import org.jetbrains.annotations.UnmodifiableView;
@@ -101,6 +103,14 @@ public class Item {
return baseComponents.getOrDefault(DataComponentTypes.MAX_STACK_SIZE, 1);
}
+ public boolean is(GeyserSession session, Tag- tag) {
+ return session.getTagCache().is(tag, javaId);
+ }
+
+ public boolean is(GeyserSession session, HolderSet set) {
+ return session.getTagCache().is(set, JavaRegistries.ITEM, javaId);
+ }
+
/**
* Returns an unmodifiable {@link DataComponents} view containing known data components.
* Optionally, additional components can be provided to replace (or add to)
diff --git a/core/src/main/java/org/geysermc/geyser/item/type/PlayerHeadItem.java b/core/src/main/java/org/geysermc/geyser/item/type/PlayerHeadItem.java
index 1ead9f2ad..5184e7ac6 100644
--- a/core/src/main/java/org/geysermc/geyser/item/type/PlayerHeadItem.java
+++ b/core/src/main/java/org/geysermc/geyser/item/type/PlayerHeadItem.java
@@ -30,10 +30,12 @@ import org.geysermc.geyser.item.TooltipOptions;
import org.geysermc.geyser.item.components.Rarity;
import org.geysermc.geyser.level.block.type.Block;
import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.skin.SkinManager;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.text.MinecraftLocale;
import org.geysermc.geyser.translator.item.BedrockItemBuilder;
import org.geysermc.mcprotocollib.auth.GameProfile;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.player.ResolvableProfile;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents;
@@ -49,18 +51,25 @@ public class PlayerHeadItem extends BlockItem {
// Use the correct color, determined by the rarity of the item
char rarity = Rarity.fromId(components.getOrDefault(DataComponentTypes.RARITY, Rarity.COMMON.ordinal())).getColor();
- GameProfile profile = components.get(DataComponentTypes.PROFILE);
+ ResolvableProfile profile = components.get(DataComponentTypes.PROFILE);
if (profile != null) {
- String name = profile.getName();
- if (name != null) {
- // Add correct name of player skull
- String displayName = ChatColor.RESET + ChatColor.ESCAPE + rarity +
+ // Ideally we'd update the item once the profile is resolved,
+ // but there's no good way of doing this as we don't know where the item is in an inventory after we have translated it
+ // So, we request a resolve here, and if the profile has already been resolved it will be returned instantly from cache.
+ // If not, the next time the item will be translated the profile will probably have been resolved
+ GameProfile resolved = SkinManager.resolveProfile(profile).getNow(null);
+ if (resolved != null) {
+ String name = resolved.getName();
+ if (name != null) {
+ // Add correct name of player skull
+ String displayName = ChatColor.RESET + ChatColor.ESCAPE + rarity +
MinecraftLocale.getLocaleString("block.minecraft.player_head.named", session.locale()).replace("%s", name);
- builder.setCustomName(displayName);
- } else {
- // No name found so default to "Player Head"
- builder.setCustomName(ChatColor.RESET + ChatColor.ESCAPE + rarity +
+ builder.setCustomName(displayName);
+ } else {
+ // No name found so default to "Player Head"
+ builder.setCustomName(ChatColor.RESET + ChatColor.ESCAPE + rarity +
MinecraftLocale.getLocaleString("block.minecraft.player_head", session.locale()));
+ }
}
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/level/GameRule.java b/core/src/main/java/org/geysermc/geyser/level/GameRule.java
index 8a1af095a..a77d5f09c 100644
--- a/core/src/main/java/org/geysermc/geyser/level/GameRule.java
+++ b/core/src/main/java/org/geysermc/geyser/level/GameRule.java
@@ -68,7 +68,12 @@ public enum GameRule {
SPAWNRADIUS("spawnRadius", 10),
SPECTATORSGENERATECHUNKS("spectatorsGenerateChunks", true), // JE only
UNIVERSALANGER("universalAnger", false),
- LOCATORBAR("locatorBar", true);
+ LOCATORBAR("locatorBar", true),
+ ALLOWENTERINGNETHERUSINGPORTALS("allowEnteringNetherUsingPortals", true), // JE only
+ COMMANDBLOCKSENABLED("commandBlocksEnabled", true),
+ PVP("pvp", true),
+ SPAWNMONSTERS("spawnMonsters", true),
+ SPAWNERBLOCKSENABLED("spawnerBlocksEnabled", true);
public static final GameRule[] VALUES = values();
diff --git a/core/src/main/java/org/geysermc/geyser/level/block/Blocks.java b/core/src/main/java/org/geysermc/geyser/level/block/Blocks.java
index 2f3cdfa00..875328738 100644
--- a/core/src/main/java/org/geysermc/geyser/level/block/Blocks.java
+++ b/core/src/main/java/org/geysermc/geyser/level/block/Blocks.java
@@ -25,10 +25,24 @@
package org.geysermc.geyser.level.block;
-import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.level.block.property.ChestType;
import org.geysermc.geyser.level.block.property.FrontAndTop;
-import org.geysermc.geyser.level.block.type.*;
+import org.geysermc.geyser.level.block.type.BannerBlock;
+import org.geysermc.geyser.level.block.type.BedBlock;
+import org.geysermc.geyser.level.block.type.Block;
+import org.geysermc.geyser.level.block.type.ButtonBlock;
+import org.geysermc.geyser.level.block.type.CauldronBlock;
+import org.geysermc.geyser.level.block.type.ChestBlock;
+import org.geysermc.geyser.level.block.type.DoorBlock;
+import org.geysermc.geyser.level.block.type.FlowerPotBlock;
+import org.geysermc.geyser.level.block.type.FurnaceBlock;
+import org.geysermc.geyser.level.block.type.LecternBlock;
+import org.geysermc.geyser.level.block.type.MovingPistonBlock;
+import org.geysermc.geyser.level.block.type.PistonBlock;
+import org.geysermc.geyser.level.block.type.SkullBlock;
+import org.geysermc.geyser.level.block.type.TrapDoorBlock;
+import org.geysermc.geyser.level.block.type.WallSkullBlock;
+import org.geysermc.geyser.level.block.type.WaterBlock;
import org.geysermc.geyser.level.physics.Axis;
import org.geysermc.geyser.level.physics.Direction;
import org.geysermc.geyser.level.physics.PistonBehavior;
@@ -329,12 +343,12 @@ public final class Blocks {
public static final Block SHORT_DRY_GRASS = register(new Block("short_dry_grass", builder().pushReaction(PistonBehavior.DESTROY)));
public static final Block TALL_DRY_GRASS = register(new Block("tall_dry_grass", builder().pushReaction(PistonBehavior.DESTROY)));
public static final Block SEAGRASS = register(new Block("seagrass", builder().pushReaction(PistonBehavior.DESTROY)));
- public static final Block TALL_SEAGRASS = register(new Block("tall_seagrass", builder().pushReaction(PistonBehavior.DESTROY).pickItem(() -> Items.SEAGRASS)
+ public static final Block TALL_SEAGRASS = register(new Block("tall_seagrass", builder().pushReaction(PistonBehavior.DESTROY)
.enumState(DOUBLE_BLOCK_HALF)));
public static final Block PISTON = register(new PistonBlock("piston", builder().destroyTime(1.5f).pushReaction(PistonBehavior.BLOCK)
.booleanState(EXTENDED)
.enumState(FACING, Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST, Direction.UP, Direction.DOWN)));
- public static final Block PISTON_HEAD = register(new PistonHeadBlock("piston_head", builder().destroyTime(1.5f).pushReaction(PistonBehavior.BLOCK)
+ public static final Block PISTON_HEAD = register(new Block("piston_head", builder().destroyTime(1.5f).pushReaction(PistonBehavior.BLOCK)
.enumState(FACING, Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST, Direction.UP, Direction.DOWN)
.booleanState(SHORT)
.enumState(PISTON_TYPE)));
@@ -381,12 +395,72 @@ public final class Blocks {
public static final Block BOOKSHELF = register(new Block("bookshelf", builder().destroyTime(1.5f)));
public static final Block CHISELED_BOOKSHELF = register(new Block("chiseled_bookshelf", builder().setBlockEntity(BlockEntityType.CHISELED_BOOKSHELF).destroyTime(1.5f)
.enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
- .booleanState(CHISELED_BOOKSHELF_SLOT_0_OCCUPIED)
- .booleanState(CHISELED_BOOKSHELF_SLOT_1_OCCUPIED)
- .booleanState(CHISELED_BOOKSHELF_SLOT_2_OCCUPIED)
- .booleanState(CHISELED_BOOKSHELF_SLOT_3_OCCUPIED)
- .booleanState(CHISELED_BOOKSHELF_SLOT_4_OCCUPIED)
- .booleanState(CHISELED_BOOKSHELF_SLOT_5_OCCUPIED)));
+ .booleanState(SLOT_0_OCCUPIED)
+ .booleanState(SLOT_1_OCCUPIED)
+ .booleanState(SLOT_2_OCCUPIED)
+ .booleanState(SLOT_3_OCCUPIED)
+ .booleanState(SLOT_4_OCCUPIED)
+ .booleanState(SLOT_5_OCCUPIED)));
+ public static final Block ACACIA_SHELF = register(new Block("acacia_shelf", builder().setBlockEntity(BlockEntityType.SHELF).destroyTime(2.0f)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .booleanState(POWERED)
+ .enumState(SIDE_CHAIN_PART)
+ .booleanState(WATERLOGGED)));
+ public static final Block BAMBOO_SHELF = register(new Block("bamboo_shelf", builder().setBlockEntity(BlockEntityType.SHELF).destroyTime(2.0f)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .booleanState(POWERED)
+ .enumState(SIDE_CHAIN_PART)
+ .booleanState(WATERLOGGED)));
+ public static final Block BIRCH_SHELF = register(new Block("birch_shelf", builder().setBlockEntity(BlockEntityType.SHELF).destroyTime(2.0f)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .booleanState(POWERED)
+ .enumState(SIDE_CHAIN_PART)
+ .booleanState(WATERLOGGED)));
+ public static final Block CHERRY_SHELF = register(new Block("cherry_shelf", builder().setBlockEntity(BlockEntityType.SHELF).destroyTime(2.0f)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .booleanState(POWERED)
+ .enumState(SIDE_CHAIN_PART)
+ .booleanState(WATERLOGGED)));
+ public static final Block CRIMSON_SHELF = register(new Block("crimson_shelf", builder().setBlockEntity(BlockEntityType.SHELF).destroyTime(2.0f)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .booleanState(POWERED)
+ .enumState(SIDE_CHAIN_PART)
+ .booleanState(WATERLOGGED)));
+ public static final Block DARK_OAK_SHELF = register(new Block("dark_oak_shelf", builder().setBlockEntity(BlockEntityType.SHELF).destroyTime(2.0f)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .booleanState(POWERED)
+ .enumState(SIDE_CHAIN_PART)
+ .booleanState(WATERLOGGED)));
+ public static final Block JUNGLE_SHELF = register(new Block("jungle_shelf", builder().setBlockEntity(BlockEntityType.SHELF).destroyTime(2.0f)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .booleanState(POWERED)
+ .enumState(SIDE_CHAIN_PART)
+ .booleanState(WATERLOGGED)));
+ public static final Block MANGROVE_SHELF = register(new Block("mangrove_shelf", builder().setBlockEntity(BlockEntityType.SHELF).destroyTime(2.0f)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .booleanState(POWERED)
+ .enumState(SIDE_CHAIN_PART)
+ .booleanState(WATERLOGGED)));
+ public static final Block OAK_SHELF = register(new Block("oak_shelf", builder().setBlockEntity(BlockEntityType.SHELF).destroyTime(2.0f)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .booleanState(POWERED)
+ .enumState(SIDE_CHAIN_PART)
+ .booleanState(WATERLOGGED)));
+ public static final Block PALE_OAK_SHELF = register(new Block("pale_oak_shelf", builder().setBlockEntity(BlockEntityType.SHELF).destroyTime(2.0f)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .booleanState(POWERED)
+ .enumState(SIDE_CHAIN_PART)
+ .booleanState(WATERLOGGED)));
+ public static final Block SPRUCE_SHELF = register(new Block("spruce_shelf", builder().setBlockEntity(BlockEntityType.SHELF).destroyTime(2.0f)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .booleanState(POWERED)
+ .enumState(SIDE_CHAIN_PART)
+ .booleanState(WATERLOGGED)));
+ public static final Block WARPED_SHELF = register(new Block("warped_shelf", builder().setBlockEntity(BlockEntityType.SHELF).destroyTime(2.0f)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .booleanState(POWERED)
+ .enumState(SIDE_CHAIN_PART)
+ .booleanState(WATERLOGGED)));
public static final Block MOSSY_COBBLESTONE = register(new Block("mossy_cobblestone", builder().requiresCorrectToolForDrops().destroyTime(2.0f)));
public static final Block OBSIDIAN = register(new Block("obsidian", builder().requiresCorrectToolForDrops().destroyTime(50.0f)));
public static final Block TORCH = register(new Block("torch", builder().pushReaction(PistonBehavior.DESTROY)));
@@ -400,7 +474,7 @@ public final class Blocks {
.booleanState(UP)
.booleanState(WEST)));
public static final Block SOUL_FIRE = register(new Block("soul_fire", builder().pushReaction(PistonBehavior.DESTROY)));
- public static final Block SPAWNER = register(new SpawnerBlock("spawner", builder().setBlockEntity(BlockEntityType.MOB_SPAWNER).requiresCorrectToolForDrops().destroyTime(5.0f)));
+ public static final Block SPAWNER = register(new Block("spawner", builder().setBlockEntity(BlockEntityType.MOB_SPAWNER).requiresCorrectToolForDrops().destroyTime(5.0f)));
public static final Block CREAKING_HEART = register(new Block("creaking_heart", builder().setBlockEntity(BlockEntityType.CREAKING_HEART).destroyTime(10.0f)
.enumState(AXIS, Axis.VALUES)
.enumState(CREAKING_HEART_STATE)
@@ -665,6 +739,9 @@ public final class Blocks {
public static final Block SOUL_TORCH = register(new Block("soul_torch", builder().pushReaction(PistonBehavior.DESTROY)));
public static final Block SOUL_WALL_TORCH = register(new Block("soul_wall_torch", builder().pushReaction(PistonBehavior.DESTROY)
.enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)));
+ public static final Block COPPER_TORCH = register(new Block("copper_torch", builder().pushReaction(PistonBehavior.DESTROY)));
+ public static final Block COPPER_WALL_TORCH = register(new Block("copper_wall_torch", builder().pushReaction(PistonBehavior.DESTROY)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)));
public static final Block GLOWSTONE = register(new Block("glowstone", builder().destroyTime(0.3f)));
public static final Block NETHER_PORTAL = register(new Block("nether_portal", builder().destroyTime(-1.0f).pushReaction(PistonBehavior.BLOCK)
.enumState(HORIZONTAL_AXIS, Axis.X, Axis.Z)));
@@ -794,7 +871,79 @@ public final class Blocks {
.booleanState(SOUTH)
.booleanState(WATERLOGGED)
.booleanState(WEST)));
- public static final Block CHAIN = register(new Block("chain", builder().requiresCorrectToolForDrops().destroyTime(5.0f)
+ public static final Block COPPER_BARS = register(new Block("copper_bars", builder().requiresCorrectToolForDrops().destroyTime(5.0f)
+ .booleanState(EAST)
+ .booleanState(NORTH)
+ .booleanState(SOUTH)
+ .booleanState(WATERLOGGED)
+ .booleanState(WEST)));
+ public static final Block EXPOSED_COPPER_BARS = register(new Block("exposed_copper_bars", builder().requiresCorrectToolForDrops().destroyTime(5.0f)
+ .booleanState(EAST)
+ .booleanState(NORTH)
+ .booleanState(SOUTH)
+ .booleanState(WATERLOGGED)
+ .booleanState(WEST)));
+ public static final Block WEATHERED_COPPER_BARS = register(new Block("weathered_copper_bars", builder().requiresCorrectToolForDrops().destroyTime(5.0f)
+ .booleanState(EAST)
+ .booleanState(NORTH)
+ .booleanState(SOUTH)
+ .booleanState(WATERLOGGED)
+ .booleanState(WEST)));
+ public static final Block OXIDIZED_COPPER_BARS = register(new Block("oxidized_copper_bars", builder().requiresCorrectToolForDrops().destroyTime(5.0f)
+ .booleanState(EAST)
+ .booleanState(NORTH)
+ .booleanState(SOUTH)
+ .booleanState(WATERLOGGED)
+ .booleanState(WEST)));
+ public static final Block WAXED_COPPER_BARS = register(new Block("waxed_copper_bars", builder().requiresCorrectToolForDrops().destroyTime(5.0f)
+ .booleanState(EAST)
+ .booleanState(NORTH)
+ .booleanState(SOUTH)
+ .booleanState(WATERLOGGED)
+ .booleanState(WEST)));
+ public static final Block WAXED_EXPOSED_COPPER_BARS = register(new Block("waxed_exposed_copper_bars", builder().requiresCorrectToolForDrops().destroyTime(5.0f)
+ .booleanState(EAST)
+ .booleanState(NORTH)
+ .booleanState(SOUTH)
+ .booleanState(WATERLOGGED)
+ .booleanState(WEST)));
+ public static final Block WAXED_WEATHERED_COPPER_BARS = register(new Block("waxed_weathered_copper_bars", builder().requiresCorrectToolForDrops().destroyTime(5.0f)
+ .booleanState(EAST)
+ .booleanState(NORTH)
+ .booleanState(SOUTH)
+ .booleanState(WATERLOGGED)
+ .booleanState(WEST)));
+ public static final Block WAXED_OXIDIZED_COPPER_BARS = register(new Block("waxed_oxidized_copper_bars", builder().requiresCorrectToolForDrops().destroyTime(5.0f)
+ .booleanState(EAST)
+ .booleanState(NORTH)
+ .booleanState(SOUTH)
+ .booleanState(WATERLOGGED)
+ .booleanState(WEST)));
+ public static final Block IRON_CHAIN = register(new Block("iron_chain", builder().requiresCorrectToolForDrops().destroyTime(5.0f)
+ .enumState(AXIS, Axis.VALUES)
+ .booleanState(WATERLOGGED)));
+ public static final Block COPPER_CHAIN = register(new Block("copper_chain", builder().requiresCorrectToolForDrops().destroyTime(5.0f)
+ .enumState(AXIS, Axis.VALUES)
+ .booleanState(WATERLOGGED)));
+ public static final Block EXPOSED_COPPER_CHAIN = register(new Block("exposed_copper_chain", builder().requiresCorrectToolForDrops().destroyTime(5.0f)
+ .enumState(AXIS, Axis.VALUES)
+ .booleanState(WATERLOGGED)));
+ public static final Block WEATHERED_COPPER_CHAIN = register(new Block("weathered_copper_chain", builder().requiresCorrectToolForDrops().destroyTime(5.0f)
+ .enumState(AXIS, Axis.VALUES)
+ .booleanState(WATERLOGGED)));
+ public static final Block OXIDIZED_COPPER_CHAIN = register(new Block("oxidized_copper_chain", builder().requiresCorrectToolForDrops().destroyTime(5.0f)
+ .enumState(AXIS, Axis.VALUES)
+ .booleanState(WATERLOGGED)));
+ public static final Block WAXED_COPPER_CHAIN = register(new Block("waxed_copper_chain", builder().requiresCorrectToolForDrops().destroyTime(5.0f)
+ .enumState(AXIS, Axis.VALUES)
+ .booleanState(WATERLOGGED)));
+ public static final Block WAXED_EXPOSED_COPPER_CHAIN = register(new Block("waxed_exposed_copper_chain", builder().requiresCorrectToolForDrops().destroyTime(5.0f)
+ .enumState(AXIS, Axis.VALUES)
+ .booleanState(WATERLOGGED)));
+ public static final Block WAXED_WEATHERED_COPPER_CHAIN = register(new Block("waxed_weathered_copper_chain", builder().requiresCorrectToolForDrops().destroyTime(5.0f)
+ .enumState(AXIS, Axis.VALUES)
+ .booleanState(WATERLOGGED)));
+ public static final Block WAXED_OXIDIZED_COPPER_CHAIN = register(new Block("waxed_oxidized_copper_chain", builder().requiresCorrectToolForDrops().destroyTime(5.0f)
.enumState(AXIS, Axis.VALUES)
.booleanState(WATERLOGGED)));
public static final Block GLASS_PANE = register(new Block("glass_pane", builder().destroyTime(0.3f)
@@ -805,9 +954,9 @@ public final class Blocks {
.booleanState(WEST)));
public static final Block PUMPKIN = register(new Block("pumpkin", builder().destroyTime(1.0f).pushReaction(PistonBehavior.DESTROY)));
public static final Block MELON = register(new Block("melon", builder().destroyTime(1.0f).pushReaction(PistonBehavior.DESTROY)));
- public static final Block ATTACHED_PUMPKIN_STEM = register(new Block("attached_pumpkin_stem", builder().pushReaction(PistonBehavior.DESTROY).pickItem(() -> Items.PUMPKIN_SEEDS)
+ public static final Block ATTACHED_PUMPKIN_STEM = register(new Block("attached_pumpkin_stem", builder().pushReaction(PistonBehavior.DESTROY)
.enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)));
- public static final Block ATTACHED_MELON_STEM = register(new Block("attached_melon_stem", builder().pushReaction(PistonBehavior.DESTROY).pickItem(() -> Items.MELON_SEEDS)
+ public static final Block ATTACHED_MELON_STEM = register(new Block("attached_melon_stem", builder().pushReaction(PistonBehavior.DESTROY)
.enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)));
public static final Block PUMPKIN_STEM = register(new Block("pumpkin_stem", builder().pushReaction(PistonBehavior.DESTROY)
.intState(AGE_7)));
@@ -1797,7 +1946,7 @@ public final class Blocks {
public static final Block BLACK_CONCRETE_POWDER = register(new Block("black_concrete_powder", builder().destroyTime(0.5f)));
public static final Block KELP = register(new Block("kelp", builder().pushReaction(PistonBehavior.DESTROY)
.intState(AGE_25)));
- public static final Block KELP_PLANT = register(new Block("kelp_plant", builder().pushReaction(PistonBehavior.DESTROY).pickItem(() -> Items.KELP)));
+ public static final Block KELP_PLANT = register(new Block("kelp_plant", builder().pushReaction(PistonBehavior.DESTROY)));
public static final Block DRIED_KELP_BLOCK = register(new Block("dried_kelp_block", builder().destroyTime(0.5f)));
public static final Block TURTLE_EGG = register(new Block("turtle_egg", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY)
.intState(EGGS)
@@ -1894,7 +2043,7 @@ public final class Blocks {
public static final Block BLUE_ICE = register(new Block("blue_ice", builder().destroyTime(2.8f)));
public static final Block CONDUIT = register(new Block("conduit", builder().setBlockEntity(BlockEntityType.CONDUIT).destroyTime(3.0f)
.booleanState(WATERLOGGED)));
- public static final Block BAMBOO_SAPLING = register(new Block("bamboo_sapling", builder().destroyTime(1.0f).pushReaction(PistonBehavior.DESTROY).pickItem(() -> Items.BAMBOO)));
+ public static final Block BAMBOO_SAPLING = register(new Block("bamboo_sapling", builder().destroyTime(1.0f).pushReaction(PistonBehavior.DESTROY)));
public static final Block BAMBOO = register(new Block("bamboo", builder().destroyTime(1.0f).pushReaction(PistonBehavior.DESTROY)
.intState(AGE_1)
.enumState(BAMBOO_LEAVES)
@@ -2141,6 +2290,30 @@ public final class Blocks {
public static final Block SOUL_LANTERN = register(new Block("soul_lantern", builder().destroyTime(3.5f).pushReaction(PistonBehavior.DESTROY)
.booleanState(HANGING)
.booleanState(WATERLOGGED)));
+ public static final Block COPPER_LANTERN = register(new Block("copper_lantern", builder().destroyTime(3.5f).pushReaction(PistonBehavior.DESTROY)
+ .booleanState(HANGING)
+ .booleanState(WATERLOGGED)));
+ public static final Block EXPOSED_COPPER_LANTERN = register(new Block("exposed_copper_lantern", builder().destroyTime(3.5f).pushReaction(PistonBehavior.DESTROY)
+ .booleanState(HANGING)
+ .booleanState(WATERLOGGED)));
+ public static final Block WEATHERED_COPPER_LANTERN = register(new Block("weathered_copper_lantern", builder().destroyTime(3.5f).pushReaction(PistonBehavior.DESTROY)
+ .booleanState(HANGING)
+ .booleanState(WATERLOGGED)));
+ public static final Block OXIDIZED_COPPER_LANTERN = register(new Block("oxidized_copper_lantern", builder().destroyTime(3.5f).pushReaction(PistonBehavior.DESTROY)
+ .booleanState(HANGING)
+ .booleanState(WATERLOGGED)));
+ public static final Block WAXED_COPPER_LANTERN = register(new Block("waxed_copper_lantern", builder().destroyTime(3.5f).pushReaction(PistonBehavior.DESTROY)
+ .booleanState(HANGING)
+ .booleanState(WATERLOGGED)));
+ public static final Block WAXED_EXPOSED_COPPER_LANTERN = register(new Block("waxed_exposed_copper_lantern", builder().destroyTime(3.5f).pushReaction(PistonBehavior.DESTROY)
+ .booleanState(HANGING)
+ .booleanState(WATERLOGGED)));
+ public static final Block WAXED_WEATHERED_COPPER_LANTERN = register(new Block("waxed_weathered_copper_lantern", builder().destroyTime(3.5f).pushReaction(PistonBehavior.DESTROY)
+ .booleanState(HANGING)
+ .booleanState(WATERLOGGED)));
+ public static final Block WAXED_OXIDIZED_COPPER_LANTERN = register(new Block("waxed_oxidized_copper_lantern", builder().destroyTime(3.5f).pushReaction(PistonBehavior.DESTROY)
+ .booleanState(HANGING)
+ .booleanState(WATERLOGGED)));
public static final Block CAMPFIRE = register(new Block("campfire", builder().setBlockEntity(BlockEntityType.CAMPFIRE).destroyTime(2.0f)
.enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
.booleanState(LIT)
@@ -2179,10 +2352,10 @@ public final class Blocks {
public static final Block SHROOMLIGHT = register(new Block("shroomlight", builder().destroyTime(1.0f)));
public static final Block WEEPING_VINES = register(new Block("weeping_vines", builder().pushReaction(PistonBehavior.DESTROY)
.intState(AGE_25)));
- public static final Block WEEPING_VINES_PLANT = register(new Block("weeping_vines_plant", builder().pushReaction(PistonBehavior.DESTROY).pickItem(() -> Items.WEEPING_VINES)));
+ public static final Block WEEPING_VINES_PLANT = register(new Block("weeping_vines_plant", builder().pushReaction(PistonBehavior.DESTROY)));
public static final Block TWISTING_VINES = register(new Block("twisting_vines", builder().pushReaction(PistonBehavior.DESTROY)
.intState(AGE_25)));
- public static final Block TWISTING_VINES_PLANT = register(new Block("twisting_vines_plant", builder().pushReaction(PistonBehavior.DESTROY).pickItem(() -> Items.TWISTING_VINES)));
+ public static final Block TWISTING_VINES_PLANT = register(new Block("twisting_vines_plant", builder().pushReaction(PistonBehavior.DESTROY)));
public static final Block CRIMSON_ROOTS = register(new Block("crimson_roots", builder().pushReaction(PistonBehavior.DESTROY)));
public static final Block CRIMSON_PLANKS = register(new Block("crimson_planks", builder().destroyTime(2.0f)));
public static final Block WARPED_PLANKS = register(new Block("warped_planks", builder().destroyTime(2.0f)));
@@ -2289,7 +2462,7 @@ public final class Blocks {
public static final Block BEEHIVE = register(new Block("beehive", builder().setBlockEntity(BlockEntityType.BEEHIVE).destroyTime(0.6f)
.enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
.intState(LEVEL_HONEY)));
- public static final Block HONEY_BLOCK = register(new HoneyBlock("honey_block", builder()));
+ public static final Block HONEY_BLOCK = register(new Block("honey_block", builder()));
public static final Block HONEYCOMB_BLOCK = register(new Block("honeycomb_block", builder().destroyTime(0.6f)));
public static final Block NETHERITE_BLOCK = register(new Block("netherite_block", builder().requiresCorrectToolForDrops().destroyTime(50.0f)));
public static final Block ANCIENT_DEBRIS = register(new Block("ancient_debris", builder().requiresCorrectToolForDrops().destroyTime(30.0f)));
@@ -2429,39 +2602,39 @@ public final class Blocks {
.intState(CANDLES)
.booleanState(LIT)
.booleanState(WATERLOGGED)));
- public static final Block CANDLE_CAKE = register(new Block("candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY).pickItem(() -> Items.CAKE)
+ public static final Block CANDLE_CAKE = register(new Block("candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY)
.booleanState(LIT)));
- public static final Block WHITE_CANDLE_CAKE = register(new Block("white_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY).pickItem(() -> Items.CAKE)
+ public static final Block WHITE_CANDLE_CAKE = register(new Block("white_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY)
.booleanState(LIT)));
- public static final Block ORANGE_CANDLE_CAKE = register(new Block("orange_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY).pickItem(() -> Items.CAKE)
+ public static final Block ORANGE_CANDLE_CAKE = register(new Block("orange_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY)
.booleanState(LIT)));
- public static final Block MAGENTA_CANDLE_CAKE = register(new Block("magenta_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY).pickItem(() -> Items.CAKE)
+ public static final Block MAGENTA_CANDLE_CAKE = register(new Block("magenta_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY)
.booleanState(LIT)));
- public static final Block LIGHT_BLUE_CANDLE_CAKE = register(new Block("light_blue_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY).pickItem(() -> Items.CAKE)
+ public static final Block LIGHT_BLUE_CANDLE_CAKE = register(new Block("light_blue_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY)
.booleanState(LIT)));
- public static final Block YELLOW_CANDLE_CAKE = register(new Block("yellow_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY).pickItem(() -> Items.CAKE)
+ public static final Block YELLOW_CANDLE_CAKE = register(new Block("yellow_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY)
.booleanState(LIT)));
- public static final Block LIME_CANDLE_CAKE = register(new Block("lime_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY).pickItem(() -> Items.CAKE)
+ public static final Block LIME_CANDLE_CAKE = register(new Block("lime_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY)
.booleanState(LIT)));
- public static final Block PINK_CANDLE_CAKE = register(new Block("pink_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY).pickItem(() -> Items.CAKE)
+ public static final Block PINK_CANDLE_CAKE = register(new Block("pink_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY)
.booleanState(LIT)));
- public static final Block GRAY_CANDLE_CAKE = register(new Block("gray_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY).pickItem(() -> Items.CAKE)
+ public static final Block GRAY_CANDLE_CAKE = register(new Block("gray_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY)
.booleanState(LIT)));
- public static final Block LIGHT_GRAY_CANDLE_CAKE = register(new Block("light_gray_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY).pickItem(() -> Items.CAKE)
+ public static final Block LIGHT_GRAY_CANDLE_CAKE = register(new Block("light_gray_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY)
.booleanState(LIT)));
- public static final Block CYAN_CANDLE_CAKE = register(new Block("cyan_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY).pickItem(() -> Items.CAKE)
+ public static final Block CYAN_CANDLE_CAKE = register(new Block("cyan_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY)
.booleanState(LIT)));
- public static final Block PURPLE_CANDLE_CAKE = register(new Block("purple_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY).pickItem(() -> Items.CAKE)
+ public static final Block PURPLE_CANDLE_CAKE = register(new Block("purple_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY)
.booleanState(LIT)));
- public static final Block BLUE_CANDLE_CAKE = register(new Block("blue_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY).pickItem(() -> Items.CAKE)
+ public static final Block BLUE_CANDLE_CAKE = register(new Block("blue_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY)
.booleanState(LIT)));
- public static final Block BROWN_CANDLE_CAKE = register(new Block("brown_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY).pickItem(() -> Items.CAKE)
+ public static final Block BROWN_CANDLE_CAKE = register(new Block("brown_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY)
.booleanState(LIT)));
- public static final Block GREEN_CANDLE_CAKE = register(new Block("green_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY).pickItem(() -> Items.CAKE)
+ public static final Block GREEN_CANDLE_CAKE = register(new Block("green_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY)
.booleanState(LIT)));
- public static final Block RED_CANDLE_CAKE = register(new Block("red_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY).pickItem(() -> Items.CAKE)
+ public static final Block RED_CANDLE_CAKE = register(new Block("red_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY)
.booleanState(LIT)));
- public static final Block BLACK_CANDLE_CAKE = register(new Block("black_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY).pickItem(() -> Items.CAKE)
+ public static final Block BLACK_CANDLE_CAKE = register(new Block("black_candle_cake", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY)
.booleanState(LIT)));
public static final Block AMETHYST_BLOCK = register(new Block("amethyst_block", builder().requiresCorrectToolForDrops().destroyTime(1.5f)));
public static final Block BUDDING_AMETHYST = register(new Block("budding_amethyst", builder().requiresCorrectToolForDrops().destroyTime(1.5f).pushReaction(PistonBehavior.DESTROY)));
@@ -2780,10 +2953,102 @@ public final class Blocks {
public static final Block WAXED_OXIDIZED_COPPER_BULB = register(new Block("waxed_oxidized_copper_bulb", builder().requiresCorrectToolForDrops().destroyTime(3.0f)
.booleanState(LIT)
.booleanState(POWERED)));
+ public static final Block COPPER_CHEST = register(new ChestBlock("copper_chest", builder().setBlockEntity(BlockEntityType.CHEST).requiresCorrectToolForDrops().destroyTime(3.0f)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .enumState(CHEST_TYPE, ChestType.VALUES)
+ .booleanState(WATERLOGGED)));
+ public static final Block EXPOSED_COPPER_CHEST = register(new ChestBlock("exposed_copper_chest", builder().setBlockEntity(BlockEntityType.CHEST).requiresCorrectToolForDrops().destroyTime(3.0f)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .enumState(CHEST_TYPE, ChestType.VALUES)
+ .booleanState(WATERLOGGED)));
+ public static final Block WEATHERED_COPPER_CHEST = register(new ChestBlock("weathered_copper_chest", builder().setBlockEntity(BlockEntityType.CHEST).requiresCorrectToolForDrops().destroyTime(3.0f)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .enumState(CHEST_TYPE, ChestType.VALUES)
+ .booleanState(WATERLOGGED)));
+ public static final Block OXIDIZED_COPPER_CHEST = register(new ChestBlock("oxidized_copper_chest", builder().setBlockEntity(BlockEntityType.CHEST).requiresCorrectToolForDrops().destroyTime(3.0f)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .enumState(CHEST_TYPE, ChestType.VALUES)
+ .booleanState(WATERLOGGED)));
+ public static final Block WAXED_COPPER_CHEST = register(new ChestBlock("waxed_copper_chest", builder().setBlockEntity(BlockEntityType.CHEST).requiresCorrectToolForDrops().destroyTime(3.0f)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .enumState(CHEST_TYPE, ChestType.VALUES)
+ .booleanState(WATERLOGGED)));
+ public static final Block WAXED_EXPOSED_COPPER_CHEST = register(new ChestBlock("waxed_exposed_copper_chest", builder().setBlockEntity(BlockEntityType.CHEST).requiresCorrectToolForDrops().destroyTime(3.0f)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .enumState(CHEST_TYPE, ChestType.VALUES)
+ .booleanState(WATERLOGGED)));
+ public static final Block WAXED_WEATHERED_COPPER_CHEST = register(new ChestBlock("waxed_weathered_copper_chest", builder().setBlockEntity(BlockEntityType.CHEST).requiresCorrectToolForDrops().destroyTime(3.0f)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .enumState(CHEST_TYPE, ChestType.VALUES)
+ .booleanState(WATERLOGGED)));
+ public static final Block WAXED_OXIDIZED_COPPER_CHEST = register(new ChestBlock("waxed_oxidized_copper_chest", builder().setBlockEntity(BlockEntityType.CHEST).requiresCorrectToolForDrops().destroyTime(3.0f)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .enumState(CHEST_TYPE, ChestType.VALUES)
+ .booleanState(WATERLOGGED)));
+ public static final Block COPPER_GOLEM_STATUE = register(new Block("copper_golem_statue", builder().setBlockEntity(BlockEntityType.COPPER_GOLEM_STATUE).destroyTime(3.0f).pushReaction(PistonBehavior.DESTROY)
+ .enumState(COPPER_GOLEM_POSE)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .booleanState(WATERLOGGED)));
+ public static final Block EXPOSED_COPPER_GOLEM_STATUE = register(new Block("exposed_copper_golem_statue", builder().setBlockEntity(BlockEntityType.COPPER_GOLEM_STATUE).destroyTime(3.0f).pushReaction(PistonBehavior.DESTROY)
+ .enumState(COPPER_GOLEM_POSE)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .booleanState(WATERLOGGED)));
+ public static final Block WEATHERED_COPPER_GOLEM_STATUE = register(new Block("weathered_copper_golem_statue", builder().setBlockEntity(BlockEntityType.COPPER_GOLEM_STATUE).destroyTime(3.0f).pushReaction(PistonBehavior.DESTROY)
+ .enumState(COPPER_GOLEM_POSE)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .booleanState(WATERLOGGED)));
+ public static final Block OXIDIZED_COPPER_GOLEM_STATUE = register(new Block("oxidized_copper_golem_statue", builder().setBlockEntity(BlockEntityType.COPPER_GOLEM_STATUE).destroyTime(3.0f).pushReaction(PistonBehavior.DESTROY)
+ .enumState(COPPER_GOLEM_POSE)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .booleanState(WATERLOGGED)));
+ public static final Block WAXED_COPPER_GOLEM_STATUE = register(new Block("waxed_copper_golem_statue", builder().setBlockEntity(BlockEntityType.COPPER_GOLEM_STATUE).destroyTime(3.0f).pushReaction(PistonBehavior.DESTROY)
+ .enumState(COPPER_GOLEM_POSE)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .booleanState(WATERLOGGED)));
+ public static final Block WAXED_EXPOSED_COPPER_GOLEM_STATUE = register(new Block("waxed_exposed_copper_golem_statue", builder().setBlockEntity(BlockEntityType.COPPER_GOLEM_STATUE).destroyTime(3.0f).pushReaction(PistonBehavior.DESTROY)
+ .enumState(COPPER_GOLEM_POSE)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .booleanState(WATERLOGGED)));
+ public static final Block WAXED_WEATHERED_COPPER_GOLEM_STATUE = register(new Block("waxed_weathered_copper_golem_statue", builder().setBlockEntity(BlockEntityType.COPPER_GOLEM_STATUE).destroyTime(3.0f).pushReaction(PistonBehavior.DESTROY)
+ .enumState(COPPER_GOLEM_POSE)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .booleanState(WATERLOGGED)));
+ public static final Block WAXED_OXIDIZED_COPPER_GOLEM_STATUE = register(new Block("waxed_oxidized_copper_golem_statue", builder().setBlockEntity(BlockEntityType.COPPER_GOLEM_STATUE).destroyTime(3.0f).pushReaction(PistonBehavior.DESTROY)
+ .enumState(COPPER_GOLEM_POSE)
+ .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
+ .booleanState(WATERLOGGED)));
public static final Block LIGHTNING_ROD = register(new Block("lightning_rod", builder().requiresCorrectToolForDrops().destroyTime(3.0f)
.enumState(FACING, Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST, Direction.UP, Direction.DOWN)
.booleanState(POWERED)
.booleanState(WATERLOGGED)));
+ public static final Block EXPOSED_LIGHTNING_ROD = register(new Block("exposed_lightning_rod", builder().requiresCorrectToolForDrops().destroyTime(3.0f)
+ .enumState(FACING, Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST, Direction.UP, Direction.DOWN)
+ .booleanState(POWERED)
+ .booleanState(WATERLOGGED)));
+ public static final Block WEATHERED_LIGHTNING_ROD = register(new Block("weathered_lightning_rod", builder().requiresCorrectToolForDrops().destroyTime(3.0f)
+ .enumState(FACING, Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST, Direction.UP, Direction.DOWN)
+ .booleanState(POWERED)
+ .booleanState(WATERLOGGED)));
+ public static final Block OXIDIZED_LIGHTNING_ROD = register(new Block("oxidized_lightning_rod", builder().requiresCorrectToolForDrops().destroyTime(3.0f)
+ .enumState(FACING, Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST, Direction.UP, Direction.DOWN)
+ .booleanState(POWERED)
+ .booleanState(WATERLOGGED)));
+ public static final Block WAXED_LIGHTNING_ROD = register(new Block("waxed_lightning_rod", builder().requiresCorrectToolForDrops().destroyTime(3.0f)
+ .enumState(FACING, Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST, Direction.UP, Direction.DOWN)
+ .booleanState(POWERED)
+ .booleanState(WATERLOGGED)));
+ public static final Block WAXED_EXPOSED_LIGHTNING_ROD = register(new Block("waxed_exposed_lightning_rod", builder().requiresCorrectToolForDrops().destroyTime(3.0f)
+ .enumState(FACING, Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST, Direction.UP, Direction.DOWN)
+ .booleanState(POWERED)
+ .booleanState(WATERLOGGED)));
+ public static final Block WAXED_WEATHERED_LIGHTNING_ROD = register(new Block("waxed_weathered_lightning_rod", builder().requiresCorrectToolForDrops().destroyTime(3.0f)
+ .enumState(FACING, Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST, Direction.UP, Direction.DOWN)
+ .booleanState(POWERED)
+ .booleanState(WATERLOGGED)));
+ public static final Block WAXED_OXIDIZED_LIGHTNING_ROD = register(new Block("waxed_oxidized_lightning_rod", builder().requiresCorrectToolForDrops().destroyTime(3.0f)
+ .enumState(FACING, Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST, Direction.UP, Direction.DOWN)
+ .booleanState(POWERED)
+ .booleanState(WATERLOGGED)));
public static final Block POINTED_DRIPSTONE = register(new Block("pointed_dripstone", builder().destroyTime(1.5f).pushReaction(PistonBehavior.DESTROY)
.enumState(DRIPSTONE_THICKNESS)
.enumState(VERTICAL_DIRECTION, Direction.UP, Direction.DOWN)
@@ -2792,7 +3057,7 @@ public final class Blocks {
public static final Block CAVE_VINES = register(new Block("cave_vines", builder().pushReaction(PistonBehavior.DESTROY)
.intState(AGE_25)
.booleanState(BERRIES)));
- public static final Block CAVE_VINES_PLANT = register(new Block("cave_vines_plant", builder().pushReaction(PistonBehavior.DESTROY).pickItem(() -> Items.GLOW_BERRIES)
+ public static final Block CAVE_VINES_PLANT = register(new Block("cave_vines_plant", builder().pushReaction(PistonBehavior.DESTROY)
.booleanState(BERRIES)));
public static final Block SPORE_BLOSSOM = register(new Block("spore_blossom", builder().pushReaction(PistonBehavior.DESTROY)));
public static final Block AZALEA = register(new Block("azalea", builder().pushReaction(PistonBehavior.DESTROY)));
diff --git a/core/src/main/java/org/geysermc/geyser/level/block/GeyserJavaBlockState.java b/core/src/main/java/org/geysermc/geyser/level/block/GeyserJavaBlockState.java
index 782f664fc..5848d0a76 100644
--- a/core/src/main/java/org/geysermc/geyser/level/block/GeyserJavaBlockState.java
+++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserJavaBlockState.java
@@ -14,7 +14,6 @@ public class GeyserJavaBlockState implements JavaBlockState {
boolean waterlogged;
JavaBoundingBox[] collision;
boolean canBreakWithHand;
- String pickItem;
String pistonBehavior;
private GeyserJavaBlockState(Builder builder) {
@@ -25,7 +24,6 @@ public class GeyserJavaBlockState implements JavaBlockState {
this.waterlogged = builder.waterlogged;
this.collision = builder.collision;
this.canBreakWithHand = builder.canBreakWithHand;
- this.pickItem = builder.pickItem;
this.pistonBehavior = builder.pistonBehavior;
}
@@ -66,7 +64,7 @@ public class GeyserJavaBlockState implements JavaBlockState {
@Override
public @Nullable String pickItem() {
- return pickItem;
+ return null;
}
@Override
@@ -88,7 +86,6 @@ public class GeyserJavaBlockState implements JavaBlockState {
private boolean waterlogged;
private JavaBoundingBox[] collision;
private boolean canBreakWithHand;
- private String pickItem;
private String pistonBehavior;
@Override
@@ -134,8 +131,8 @@ public class GeyserJavaBlockState implements JavaBlockState {
}
@Override
+ @Deprecated
public Builder pickItem(@Nullable String pickItem) {
- this.pickItem = pickItem;
return this;
}
diff --git a/core/src/main/java/org/geysermc/geyser/level/block/property/Properties.java b/core/src/main/java/org/geysermc/geyser/level/block/property/Properties.java
index 5506130e3..df3a0c3cc 100644
--- a/core/src/main/java/org/geysermc/geyser/level/block/property/Properties.java
+++ b/core/src/main/java/org/geysermc/geyser/level/block/property/Properties.java
@@ -90,6 +90,7 @@ public final class Properties {
public static final BasicEnumProperty WEST_REDSTONE = BasicEnumProperty.create("west", "up", "side", "none");
public static final BasicEnumProperty DOUBLE_BLOCK_HALF = BasicEnumProperty.create("half", "upper", "lower");
public static final BasicEnumProperty HALF = BasicEnumProperty.create("half", "top", "bottom");
+ public static final BasicEnumProperty SIDE_CHAIN_PART = BasicEnumProperty.create("side_chain", "unconnected", "right", "center", "left");
public static final BasicEnumProperty RAIL_SHAPE = BasicEnumProperty.create("shape", "north_south", "east_west", "ascending_east", "ascending_west", "ascending_north", "ascending_south", "south_east", "south_west", "north_west", "north_east");
public static final BasicEnumProperty RAIL_SHAPE_STRAIGHT = BasicEnumProperty.create("shape", "north_south", "east_west", "ascending_east", "ascending_west", "ascending_north", "ascending_south");
public static final IntegerProperty AGE_1 = IntegerProperty.create("age", 0, 1);
@@ -135,12 +136,12 @@ public final class Properties {
public static final EnumProperty VERTICAL_DIRECTION = EnumProperty.create("vertical_direction", Direction.UP, Direction.DOWN);
public static final BasicEnumProperty DRIPSTONE_THICKNESS = BasicEnumProperty.create("thickness", "tip_merge", "tip", "frustum", "middle", "base");
public static final BasicEnumProperty SCULK_SENSOR_PHASE = BasicEnumProperty.create("sculk_sensor_phase", "inactive", "active", "cooldown");
- public static final BooleanProperty CHISELED_BOOKSHELF_SLOT_0_OCCUPIED = BooleanProperty.create("slot_0_occupied");
- public static final BooleanProperty CHISELED_BOOKSHELF_SLOT_1_OCCUPIED = BooleanProperty.create("slot_1_occupied");
- public static final BooleanProperty CHISELED_BOOKSHELF_SLOT_2_OCCUPIED = BooleanProperty.create("slot_2_occupied");
- public static final BooleanProperty CHISELED_BOOKSHELF_SLOT_3_OCCUPIED = BooleanProperty.create("slot_3_occupied");
- public static final BooleanProperty CHISELED_BOOKSHELF_SLOT_4_OCCUPIED = BooleanProperty.create("slot_4_occupied");
- public static final BooleanProperty CHISELED_BOOKSHELF_SLOT_5_OCCUPIED = BooleanProperty.create("slot_5_occupied");
+ public static final BooleanProperty SLOT_0_OCCUPIED = BooleanProperty.create("slot_0_occupied");
+ public static final BooleanProperty SLOT_1_OCCUPIED = BooleanProperty.create("slot_1_occupied");
+ public static final BooleanProperty SLOT_2_OCCUPIED = BooleanProperty.create("slot_2_occupied");
+ public static final BooleanProperty SLOT_3_OCCUPIED = BooleanProperty.create("slot_3_occupied");
+ public static final BooleanProperty SLOT_4_OCCUPIED = BooleanProperty.create("slot_4_occupied");
+ public static final BooleanProperty SLOT_5_OCCUPIED = BooleanProperty.create("slot_5_occupied");
public static final IntegerProperty DUSTED = IntegerProperty.create("dusted", 0, 3);
public static final BooleanProperty CRACKED = BooleanProperty.create("cracked");
public static final BooleanProperty CRAFTING = BooleanProperty.create("crafting");
@@ -150,4 +151,5 @@ public final class Properties {
public static final BooleanProperty OMINOUS = BooleanProperty.create("ominous");
public static final BasicEnumProperty TEST_BLOCK_MODE = BasicEnumProperty.create("mode", "start", "log", "fail", "accept");
public static final BooleanProperty MAP = BooleanProperty.create("map");
+ public static final BasicEnumProperty COPPER_GOLEM_POSE = BasicEnumProperty.create("copper_golem_pose", "standing", "sitting", "running", "star");
}
diff --git a/core/src/main/java/org/geysermc/geyser/level/block/type/Block.java b/core/src/main/java/org/geysermc/geyser/level/block/type/Block.java
index f15f73cac..a32328c35 100644
--- a/core/src/main/java/org/geysermc/geyser/level/block/type/Block.java
+++ b/core/src/main/java/org/geysermc/geyser/level/block/type/Block.java
@@ -41,12 +41,17 @@ import org.geysermc.geyser.level.block.property.Property;
import org.geysermc.geyser.level.physics.PistonBehavior;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.session.GeyserSession;
-import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;
+import org.geysermc.geyser.session.cache.registry.JavaRegistries;
+import org.geysermc.geyser.session.cache.tags.Tag;
+import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet;
import org.geysermc.mcprotocollib.protocol.data.game.level.block.BlockEntityType;
import org.intellij.lang.annotations.Subst;
-import java.util.*;
-import java.util.function.Supplier;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
import java.util.stream.Stream;
public class Block {
@@ -60,11 +65,6 @@ public class Block {
private final @Nullable BlockEntityType blockEntityType;
private final float destroyTime;
private final @NonNull PistonBehavior pushReaction;
- /**
- * Used for classes we don't have implemented yet that override Mojmap getCloneItemStack with their own item.
- * A supplier prevents any issues arising where the Items class finishes before the Blocks class.
- */
- private final Supplier
- pickItem;
protected Item item = null;
private int javaId = -1;
@@ -80,7 +80,6 @@ public class Block {
this.blockEntityType = builder.blockEntityType;
this.destroyTime = builder.destroyTime;
this.pushReaction = builder.pushReaction;
- this.pickItem = builder.pickItem;
BlockState firstState = builder.build(this).get(0);
this.propertyKeys = builder.propertyKeys; // Ensure this is not null before iterating over states
@@ -158,13 +157,6 @@ public class Block {
return this.item;
}
- public ItemStack pickItem(BlockState state) {
- if (this.pickItem != null) {
- return new ItemStack(this.pickItem.get().javaId());
- }
- return new ItemStack(this.asItem().javaId());
- }
-
/**
* Should only be ran on block creation. Can be overridden.
* @param firstState the first state created from this block
@@ -215,6 +207,14 @@ public class Block {
this.javaId = javaId;
}
+ public boolean is(GeyserSession session, Tag tag) {
+ return session.getTagCache().is(tag, javaId);
+ }
+
+ public boolean is(GeyserSession session, HolderSet set) {
+ return session.getTagCache().is(set, JavaRegistries.BLOCK, javaId);
+ }
+
@Override
public String toString() {
return "Block{" +
@@ -237,7 +237,6 @@ public class Block {
private BlockEntityType blockEntityType = null;
private PistonBehavior pushReaction = PistonBehavior.NORMAL;
private float destroyTime;
- private Supplier
- pickItem;
// We'll use this field after building
private Property>[] propertyKeys = null;
@@ -294,11 +293,6 @@ public class Block {
return this;
}
- public Builder pickItem(Supplier
- pickItem) {
- this.pickItem = pickItem;
- return this;
- }
-
public Builder javaId(int javaId) {
this.javaId = javaId;
return this;
diff --git a/core/src/main/java/org/geysermc/geyser/level/block/type/FlowerPotBlock.java b/core/src/main/java/org/geysermc/geyser/level/block/type/FlowerPotBlock.java
index 5107616af..28cddb2c8 100644
--- a/core/src/main/java/org/geysermc/geyser/level/block/type/FlowerPotBlock.java
+++ b/core/src/main/java/org/geysermc/geyser/level/block/type/FlowerPotBlock.java
@@ -34,7 +34,6 @@ import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.level.block.entity.BedrockChunkWantsBlockEntityTag;
import org.geysermc.geyser.translator.level.block.entity.BlockEntityTranslator;
import org.geysermc.geyser.util.BlockEntityUtils;
-import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;
public class FlowerPotBlock extends Block implements BedrockChunkWantsBlockEntityTag {
private final Block flower;
@@ -78,14 +77,6 @@ public class FlowerPotBlock extends Block implements BedrockChunkWantsBlockEntit
return tagBuilder.build();
}
- @Override
- public ItemStack pickItem(BlockState state) {
- if (this.flower != Blocks.AIR) {
- return new ItemStack(this.flower.asItem().javaId());
- }
- return super.pickItem(state);
- }
-
public Block flower() {
return flower;
}
diff --git a/core/src/main/java/org/geysermc/geyser/level/block/type/SkullBlock.java b/core/src/main/java/org/geysermc/geyser/level/block/type/SkullBlock.java
index d41d160f8..76b532919 100644
--- a/core/src/main/java/org/geysermc/geyser/level/block/type/SkullBlock.java
+++ b/core/src/main/java/org/geysermc/geyser/level/block/type/SkullBlock.java
@@ -25,20 +25,10 @@
package org.geysermc.geyser.level.block.type;
-import org.geysermc.mcprotocollib.auth.GameProfile;
import org.cloudburstmc.math.vector.Vector3i;
-import org.cloudburstmc.nbt.NbtMap;
-import org.cloudburstmc.nbt.NbtMapBuilder;
import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition;
-import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.SkullCache;
-import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;
-import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes;
-import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents;
-
-import java.util.Collections;
-import java.util.UUID;
public class SkullBlock extends Block {
private final Type type;
@@ -65,32 +55,6 @@ public class SkullBlock extends Block {
// It's not an empty skull.
}
- public ItemStack pickItem(GeyserSession session, BlockState state, Vector3i position) {
- SkullCache.Skull skull = session.getSkullCache().getSkulls().get(position);
- if (skull == null) {
- return new ItemStack(pickItem(state).getId());
- }
-
- GeyserItemStack itemStack = GeyserItemStack.of(pickItem(state).getId(), 1);
- // This is a universal block entity behavior, but hardcode how it works for now.
- NbtMapBuilder builder = NbtMap.builder()
- .putString("id", "minecraft:skull")
- .putInt("x", position.getX())
- .putInt("y", position.getY())
- .putInt("z", position.getZ());
- DataComponents components = itemStack.getOrCreateComponents();
- components.put(DataComponentTypes.BLOCK_ENTITY_DATA, builder.build());
-
- UUID uuid = skull.getUuid();
- String texturesProperty = skull.getTexturesProperty();
- GameProfile profile = new GameProfile(uuid, null);
- if (texturesProperty != null) {
- profile.setProperties(Collections.singletonList(new GameProfile.Property("textures", texturesProperty)));
- }
- components.put(DataComponentTypes.PROFILE, profile);
- return itemStack.getItemStack();
- }
-
public Type skullType() {
return type;
}
diff --git a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java
index 8031aa200..5ddda8f38 100644
--- a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java
+++ b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java
@@ -138,6 +138,10 @@ public final class GameProtocol {
/* Bedrock convenience methods to gatekeep features and easily remove the check on version removal */
+ public static boolean is1_21_100(GeyserSession session) {
+ return session.protocolVersion() == Bedrock_v827.CODEC.getProtocolVersion();
+ }
+
public static boolean is1_21_110orHigher(GeyserSession session) {
return is1_21_110orHigher(session.protocolVersion());
}
@@ -152,7 +156,7 @@ public final class GameProtocol {
* @return the supported Minecraft: Java Edition version names
*/
public static List getJavaVersions() {
- return List.of(DEFAULT_JAVA_CODEC.getMinecraftVersion(), "1.21.8");
+ return List.of(DEFAULT_JAVA_CODEC.getMinecraftVersion());
}
/**
@@ -170,7 +174,7 @@ public final class GameProtocol {
* @return the supported Minecraft: Java Edition version
*/
public static String getJavaMinecraftVersion() {
- return "1.21.8";
+ return "1.21.9";
}
/**
diff --git a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java
index 4154164be..e8c6745c8 100644
--- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java
+++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java
@@ -30,6 +30,7 @@ import org.cloudburstmc.math.vector.Vector2f;
import org.cloudburstmc.protocol.bedrock.BedrockDisconnectReasons;
import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec;
import org.cloudburstmc.protocol.bedrock.codec.compat.BedrockCompat;
+import org.cloudburstmc.protocol.bedrock.data.ExperimentData;
import org.cloudburstmc.protocol.bedrock.data.PacketCompressionAlgorithm;
import org.cloudburstmc.protocol.bedrock.data.ResourcePackType;
import org.cloudburstmc.protocol.bedrock.netty.codec.compression.CompressionStrategy;
@@ -288,6 +289,11 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
stackPacket.setGameVersion(session.getClientData().getGameVersion());
stackPacket.getResourcePacks().addAll(this.resourcePackLoadEvent.orderedPacks());
+ if (GameProtocol.is1_21_100(session)) {
+ // Support copper age drop features (or some of them) in 1.21.100
+ stackPacket.getExperiments().add(new ExperimentData("y_2025_drop_3", true));
+ }
+
session.sendUpstreamPacket(stackPacket);
}
default -> {
diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java
index b1f62ca99..48e08cc9c 100644
--- a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java
+++ b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java
@@ -44,8 +44,10 @@ import org.geysermc.geyser.api.pack.UrlPackCodec;
import org.geysermc.geyser.api.pack.option.PriorityOption;
import org.geysermc.geyser.api.pack.option.SubpackOption;
import org.geysermc.geyser.api.pack.option.UrlFallbackOption;
+import org.geysermc.geyser.api.util.Identifier;
import org.geysermc.geyser.event.GeyserEventRegistrar;
import org.geysermc.geyser.extension.command.GeyserExtensionCommand;
+import org.geysermc.geyser.impl.IdentifierImpl;
import org.geysermc.geyser.impl.camera.GeyserCameraFade;
import org.geysermc.geyser.impl.camera.GeyserCameraPosition;
import org.geysermc.geyser.item.GeyserCustomItemData;
@@ -74,6 +76,9 @@ public class ProviderRegistryLoader implements RegistryLoader