1
0
mirror of https://github.com/GeyserMC/Geyser.git synced 2025-12-19 14:59:27 +00:00

Support 1.21.9/1.21.10

This commit is contained in:
chris
2025-10-11 17:00:15 +02:00
committed by GitHub
165 changed files with 4277 additions and 24772 deletions

View File

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

View File

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

View File

@@ -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.
* <p>
* Notes:
* <ul>
* <li>Passing {@code null} as a value resets the property to its default.</li>
* <li>Numeric properties must be within declared ranges; enum properties must use an allowed value.</li>
* <li>Multiple updates to the same property within a single batch will result in the last value being applied.</li>
* <li>The updater is short-lived and should not be retained outside the batching callback.</li>
* </ul>
*
* <pre>{@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
* });
* }</pre>
*
* @since 2.9.0
*/
@FunctionalInterface
public interface BatchPropertyUpdater {
/**
* Queues an update for the given property within the current batch.
* <p>
* 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 <T> the property's value type
*
* @since 2.9.0
*/
<T> void update(@NonNull GeyserEntityProperty<T> property, @Nullable T value);
}

View File

@@ -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.
* <p>
* Entity properties are used to describe metadata about an entity, such as
* integers, floats, booleans, or enums.
* @see <a href="https://learn.microsoft.com/en-us/minecraft/creator/documents/introductiontoentityproperties?view=minecraft-bedrock-stable#number-of-entity-properties-per-entity-type">
* Official documentation for info</a>
*
* @param <T> the type of value stored by this property
*
* @since 2.9.0
*/
public interface GeyserEntityProperty<T> {
/**
* 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();
}

View File

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

View File

@@ -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:
* <ul>
* <li>There cannot be more than 16 values</li>
* <li>Enum names cannot be longer than 32 chars, must start with a letter, and may contain numbers and underscores</li>
* </ul>
*
* @param <E> the enum type
* @since 2.9.0
*/
public interface GeyserEnumEntityProperty<E extends Enum<E>> extends GeyserEntityProperty<E> {
}

View File

@@ -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<Float> {
/**
* @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();
}

View File

@@ -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:
* <ul>
* <li>Values must be always within the {@code [min(), max()]} bounds</li>
* <li>Molang evaluation uses floats under the hood; very large integers can lose precision.
* Prefer keeping values in a practical range to avoid rounding issues.</li>
* </ul>
*
* @see GeyserDefineEntityPropertiesEvent#registerIntegerProperty(Identifier, Identifier, int, int, Integer)
* @since 2.9.0
*/
public interface GeyserIntEntityProperty extends GeyserEntityProperty<Integer> {
/**
* @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();
}

View File

@@ -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:
* <ul>
* <li>There cannot be more than 16 values</li>
* <li>The values' names cannot be longer than 32 chars, must start with a letter, and may contain numbers and underscores</li>
* </ul>
*
* @since 2.9.0
*/
public interface GeyserStringEnumProperty extends GeyserEntityProperty<String> {
/**
* @return an unmodifiable list of all registered values
* @since 2.9.0
*/
List<String> values();
}

View File

@@ -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 <T> the type of the value
* @since 2.9.0
*/
default <T> void updateProperty(@NonNull GeyserEntityProperty<T> 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<BatchPropertyUpdater> consumer);
}

View File

@@ -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.
*
* <p>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}.</p>
*
* @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;
}
}

View File

@@ -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.
*
* <p>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}.</p>
*
* @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;
}
}

View File

@@ -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.
* <p>
* 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.
*
* <h2>Example usage</h2>
* <pre>{@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);
* }
* }</pre>
*
* 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.
*
* <p><b>Notes:</b>
* <ul>
* <li>Default values must fall within the provided bounds.</li>
* <li>There cannot be more than 32 properties registered per entity type in total</li>
* <li>{@link #properties(Identifier)} returns properties registered for the given entity
* (including those added earlier in the same callback), including vanilla properties.</li>
* </ul>
*
* @since 2.9.0
*/
public interface GeyserDefineEntityPropertiesEvent extends Event {
/**
* Returns an <em>unmodifiable</em> 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<GeyserEntityProperty<?>> 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.
* <p>
* 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 <E> the enum type
* @return the created enum property
*
* @since 2.9.0
*/
<E extends Enum<E>> GeyserEnumEntityProperty<E> registerEnumProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, @NonNull Class<E> 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 <E> the enum type
* @return the created enum property
*
* @since 2.9.0
*/
default <E extends Enum<E>> GeyserEnumEntityProperty<E> registerEnumProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, @NonNull Class<E> 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<String> 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<String> values) {
return registerEnumProperty(entityType, propertyIdentifier, values, null);
}
}

View File

@@ -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:
* <ul>
* <li>
* a namespace, which is usually a name identifying your work
* </li>
* <li>
* a path, which holds a value.
* </li>
* </ul>
*
* Examples of identifiers:
* <ul>
* <li>{@code minecraft:fox}</li>
* <li>{@code geysermc:one_fun_example}</li>
* </ul>
*
* 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);
}
}

View File

@@ -8,8 +8,6 @@ architectury {
fabric()
}
val includeTransitive: Configuration = configurations.getByName("includeTransitive")
dependencies {
modImplementation(libs.fabric.loader)
modApi(libs.fabric.api)

View File

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

View File

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

View File

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

View File

@@ -54,10 +54,10 @@ public class GeyserNeoForgeDumpInfo extends BootstrapDumpInfo {
private final List<ModInfo> 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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<T extends Entity>(EntityFactory<T> factory, Entit
float width, float height, float offset, GeyserEntityProperties registeredProperties, List<EntityMetadataTranslator<? super T, ?, ?>> translators) {
public static <T extends Entity> Builder<T> inherited(EntityFactory<T> 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 <T extends Entity> Builder<T> builder(EntityFactory<T> factory) {
@@ -89,7 +90,7 @@ public record EntityDefinition<T extends Entity>(EntityFactory<T> factory, Entit
private float width;
private float height;
private float offset = 0.00001f;
private GeyserEntityProperties registeredProperties;
private GeyserEntityProperties.Builder propertiesBuilder;
private final List<EntityMetadataTranslator<? super T, ?, ?>> translators;
private Builder(EntityFactory<T> factory) {
@@ -97,14 +98,13 @@ public record EntityDefinition<T extends Entity>(EntityFactory<T> factory, Entit
translators = new ObjectArrayList<>();
}
public Builder(EntityFactory<T> factory, EntityType type, String identifier, float width, float height, float offset, GeyserEntityProperties registeredProperties, List<EntityMetadataTranslator<? super T, ?, ?>> translators) {
public Builder(EntityFactory<T> factory, EntityType type, String identifier, float width, float height, float offset, List<EntityMetadataTranslator<? super T, ?, ?>> 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<T extends Entity>(EntityFactory<T> factory, Entit
return this;
}
public Builder<T> properties(GeyserEntityProperties registeredProperties) {
this.registeredProperties = registeredProperties;
public Builder<T> 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<T extends Entity>(EntityFactory<T> factory, Entit
if (identifier == null && type != null) {
identifier = "minecraft:" + type.name().toLowerCase(Locale.ROOT);
}
GeyserEntityProperties registeredProperties = propertiesBuilder == null ? null : propertiesBuilder.build();
EntityDefinition<T> 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;
}

View File

@@ -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<BoatEntity> ACACIA_BOAT;
public static final EntityDefinition<ChestBoatEntity> ACACIA_CHEST_BOAT;
@@ -182,6 +203,7 @@ public final class EntityDefinitions {
public static final EntityDefinition<MinecartEntity> CHEST_MINECART;
public static final EntityDefinition<ChickenEntity> CHICKEN;
public static final EntityDefinition<AbstractFishEntity> COD;
public static final EntityDefinition<CopperGolemEntity> COPPER_GOLEM;
public static final EntityDefinition<CommandBlockMinecartEntity> COMMAND_BLOCK_MINECART;
public static final EntityDefinition<CowEntity> COW;
public static final EntityDefinition<CreakingEntity> CREAKING;
@@ -236,6 +258,7 @@ public final class EntityDefinitions {
public static final EntityDefinition<MagmaCubeEntity> MAGMA_CUBE;
public static final EntityDefinition<BoatEntity> MANGROVE_BOAT;
public static final EntityDefinition<ChestBoatEntity> MANGROVE_CHEST_BOAT;
public static final EntityDefinition<MannequinEntity> MANNEQUIN;
public static final EntityDefinition<MinecartEntity> MINECART;
public static final EntityDefinition<MooshroomEntity> MOOSHROOM;
public static final EntityDefinition<ChestedHorseEntity> 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.<PlayerEntity>inherited(null, livingEntityBase)
.type(EntityType.PLAYER)
EntityDefinition<AvatarEntity> avatarEntityBase = EntityDefinition.<AvatarEntity>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.<PlayerEntity>inherited(null, avatarEntityBase)
.type(EntityType.PLAYER)
.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<MobEntity> 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 <E extends Enum<E>> EnumProperty<E> registerEnumProperty(@NonNull Identifier identifier, @NonNull Identifier propertyId, @NonNull Class<E> 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<E> 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<String> 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<GeyserEntityProperty<?>> 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 <T> void registerProperty(Identifier entityType, PropertyType<T, ?> 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() {

View File

@@ -44,7 +44,6 @@ import java.util.concurrent.CompletableFuture;
public class GeyserEntityData implements EntityData {
private final GeyserSession session;
private final Set<UUID> movementLockOwners = new HashSet<>();
public GeyserEntityData(GeyserSession session) {

View File

@@ -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<PropertyType> properties;
private final static Pattern ENTITY_PROPERTY_PATTERN = Pattern.compile("^[a-z0-9_.:-]*:[a-z0-9_.:-]*$");
private final ObjectArrayList<PropertyType<?, ?>> properties;
private final Object2IntMap<String> propertyIndices;
private GeyserEntityProperties(ObjectArrayList<PropertyType> properties,
Object2IntMap<String> 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<NbtMap> 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<PropertyType> getProperties() {
public <T> void add(String entityType, @NonNull PropertyType<T, ? extends EntityProperty> 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<PropertyType<?, ?>> getProperties() {
return properties;
}
@@ -77,89 +102,24 @@ public class GeyserEntityProperties {
}
public static class Builder {
private final ObjectArrayList<PropertyType> properties = new ObjectArrayList<>();
private final Object2IntMap<String> 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;
}
PropertyType property = new IntProperty(name, min, max);
this.properties.add(property);
propertyIndices.put(name, properties.size() - 1);
public <T> Builder add(@NonNull PropertyType<T, ? extends EntityProperty> property) {
Objects.requireNonNull(property, "property cannot be null!");
if (properties == null) {
properties = new GeyserEntityProperties();
}
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<String> 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<String> 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;
}
}
}

View File

@@ -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<IntEntityProperty> intEntityProperties = new ObjectArrayList<>();
private final ObjectArrayList<FloatEntityProperty> floatEntityProperties = new ObjectArrayList<>();
private final Object2ObjectMap<String, IntEntityProperty> intEntityProperties = new Object2ObjectArrayMap<>();
private final Object2ObjectMap<String, FloatEntityProperty> 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 <T> void addProperty(PropertyType<T, ? extends EntityProperty> 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));
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 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));
}
public boolean hasFloatProperties() {
@@ -78,21 +75,17 @@ public class GeyserEntityPropertyManager {
return hasFloatProperties() || hasIntProperties();
}
public ObjectArrayList<IntEntityProperty> intProperties() {
return this.intEntityProperties;
}
public void applyIntProperties(List<IntEntityProperty> properties) {
properties.addAll(intEntityProperties);
properties.addAll(intEntityProperties.values());
intEntityProperties.clear();
}
public ObjectArrayList<FloatEntityProperty> floatProperties() {
return this.floatEntityProperties;
}
public void applyFloatProperties(List<FloatEntityProperty> properties) {
properties.addAll(floatEntityProperties);
properties.addAll(floatEntityProperties.values());
floatEntityProperties.clear();
}
public NbtMap toNbtMap(String entityType) {
return this.properties.toNbtMap(entityType);
}
}

View File

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

View File

@@ -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<T> extends PropertyType<T, IntEntityProperty> {
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<String> 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<String> 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();
}

View File

@@ -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<Boolean, IntEntityProperty>, GeyserBooleanEntityProperty {
@Override
public NbtMap nbtMap() {
return NbtMap.builder()
.putString("name", name)
.putString("name", identifier.toString())
.putInt("type", 2)
.build();
}
@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);
}
}

View File

@@ -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<String> values;
private final Object2IntMap<String> valueIndexMap;
public record EnumProperty<E extends Enum<E>>(
Identifier identifier,
Class<E> enumClass,
E defaultValue
) implements AbstractEnumProperty<E>, GeyserEnumEntityProperty<E> {
public EnumProperty(String name, List<String> 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<E> values() {
return List.of(enumClass.getEnumConstants());
}
public List<String> 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();
}
}

View File

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

View File

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

View File

@@ -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<Type, NetworkRepresentation extends EntityProperty> extends GeyserEntityProperty<Type> {
NbtMap nbtMap();
NetworkRepresentation defaultValue(int index);
NetworkRepresentation createValue(int index, @Nullable Type value);
default void apply(GeyserEntityPropertyManager manager, Type value) {
manager.addProperty(this, value);
}
}

View File

@@ -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<String> values,
int defaultIndex
) implements AbstractEnumProperty<String>, 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<String> values, @Nullable String defaultValue) {
this(name, values, defaultValue == null ? 0 : values.indexOf(defaultValue));
}
@Override
public List<String> allBedrockValues() {
return values;
}
@Override
public int indexOf(String value) {
return values.indexOf(value);
}
@Override
public @NonNull String defaultValue() {
return values.get(defaultIndex);
}
}

View File

@@ -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<BatchPropertyUpdater> consumer) {
if (this.propertyManager != null) {
Objects.requireNonNull(consumer);
GeyserEntityProperties propertyDefinitions = definition.registeredProperties();
consumer.accept(new BatchPropertyUpdater() {
@Override
public <T> void update(@NonNull GeyserEntityProperty<T> property, @Nullable T value) {
Objects.requireNonNull(property, "property must not be null!");
if (!(property instanceof PropertyType<T, ? extends EntityProperty> 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!");
}
}
}

View File

@@ -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<EquipmentSlot, GeyserItemStack> 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);

View File

@@ -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<ItemStack, ?> 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<Key> 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;
}
}

View File

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

View File

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

View File

@@ -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<ChestInteractionState> CHEST_INTERACTION_PROPERTY = new EnumProperty<>(
IdentifierImpl.of("chest_interaction"),
ChestInteractionState.class,
ChestInteractionState.NONE
);
public static final EnumProperty<OxidationLevelState> 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<WeatheringCopperState, ? extends MetadataType<WeatheringCopperState>> 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<CopperGolemState, ? extends MetadataType<CopperGolemState>> 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();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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> 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<Item> getFoodTag() {
return ItemTag.ARMADILLO_FOOD;
}
public enum State {
UNROLLED,
ROLLED_UP,
ROLLED_UP_PEEKING,
ROLLED_UP_RELAXING,
ROLLED_UP_UNROLLING
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<TemperatureVariantAnimal.BuiltInVariant> {
public static final EnumProperty<BuiltInVariant> TEMPERATE_VARIANT_PROPERTY = new EnumProperty<>(
IdentifierImpl.of("climate_variant"),
BuiltInVariant.class,
BuiltInVariant.TEMPERATE
);
public static final RegistryCache.RegistryReader<BuiltInVariant> 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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<CreakingState> 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<Boolean,? extends MetadataType<Boolean>> 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<Boolean,? extends MetadataType<Boolean>> booleanEntityMetadata) {
if (!booleanEntityMetadata.getValue()) {
propertyManager.add(CREAKING_STATE, "neutral");
STATE_PROPERTY.apply(propertyManager, CreakingState.NEUTRAL);
}
}
public void setIsTearingDown(EntityMetadata<Boolean,? extends MetadataType<Boolean>> 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
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<AbilityLayer> 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<Optional<Vector3i>, ?> 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<Optional<Component>, ?> 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);
}
}

View File

@@ -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<ResolvableProfile, ?> entityMetadata) {
setSkin(entityMetadata.getValue(), true, () -> {});
}
@Override
public String getDisplayName() {
return displayName;
}
public void setDescription(EntityMetadata<Optional<Component>, ?> entityMetadata) {
// TODO
}
}

View File

@@ -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<AbilityLayer> 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<Optional<Vector3i>, ?> 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<NbtMap, ?> entityMetadata) {
public void setLeftParrot(EntityMetadata<OptionalInt, ?> entityMetadata) {
setParrot(entityMetadata.getValue(), true);
}
public void setRightParrot(EntityMetadata<NbtMap, ?> entityMetadata) {
public void setRightParrot(EntityMetadata<OptionalInt, ?> 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<Optional<Component>, ?> 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;
}
/**

View File

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

View File

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

View File

@@ -683,7 +683,7 @@ public class VehicleComponent<T extends LivingEntity & ClientVehicle> {
}
BlockState blockState = ctx.centerBlock();
if (vehicle.getSession().getTagCache().is(BlockTag.CLIMBABLE, blockState.block())) {
if (blockState.block().is(vehicle.getSession(), BlockTag.CLIMBABLE)) {
return true;
}

View File

@@ -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<String, Class<?>> 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;

View File

@@ -47,7 +47,8 @@ public record GeyserExtensionDescription(@NonNull String id,
int majorApiVersion,
int minorApiVersion,
@NonNull String version,
@NonNull List<String> authors) implements ExtensionDescription {
@NonNull List<String> authors,
@NonNull Map<String, Dependency> 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<String, Dependency> 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<String> authors;
Map<String, Dependency> 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
}
}

View File

@@ -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<String, Path> extensions = new LinkedHashMap<>();
Map<String, GeyserExtensionContainer> loadedExtensions = new LinkedHashMap<>();
Map<String, GeyserExtensionDescription> descriptions = new LinkedHashMap<>();
Map<String, Path> 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<String, List<String>> 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<String, GeyserExtensionDescription.Dependency> 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<String> visited = new HashSet<>();
List<String> visiting = new ArrayList<>();
List<String> loadOrder = new ArrayList<>();
AtomicReference<Consumer<String>> 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 {
}
}
try {
GeyserExtensionContainer container = this.loadExtension(path, description);
extensions.put(id, path);
loadedExtensions.put(id, container);
}, (path, e) -> {
} 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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<Item, ItemMapping> 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);

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<Type> {
return this;
}
private MapHasher<Type> 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<Type> {
return accept(key, hasher.hash(value, encoder));
}
public MapHasher<Type> inlineNbt(Function<Type, NbtMap> 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.
*

View File

@@ -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<Type> {
.accept("value", STRING, GameProfile.Property::getValue)
.optionalNullable("signature", STRING, GameProfile.Property::getSignature));
MinecraftHasher<GameProfile> GAME_PROFILE = mapBuilder(builder -> builder
.optionalNullable("name", STRING, GameProfile::getName)
.optionalNullable("id", UUID, GameProfile::getId)
.optionalList("properties", GAME_PROFILE_PROPERTY, GameProfile::getProperties));
MinecraftHasher<ResolvableProfile> 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<Integer> RARITY = fromIdEnum(Rarity.values(), Rarity::getName);

View File

@@ -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<DirectType> extends MinecraftHasher<Integer> {
RegistryHasher<?> ENTITY_TYPE = enumIdRegistry(EntityType.values());
MinecraftHasher<EntityType> ENTITY_TYPE_KEY = enumRegistry();
MinecraftHasher<BlockEntityType> 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<DirectType> extends MinecraftHasher<Integer> {
.optional("has_twinkle", BOOL, Fireworks.FireworkExplosion::isHasTwinkle, false));
MinecraftHasher<BeehiveOccupant> 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));

View File

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

View File

@@ -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.
*
* <p>This class does <em>NOT</em> 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.</p>
*
* <p>Be sure to update this class for Java updates!</p>
*/
// 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<DataComponentType<?>, DataComponentParser<?, ?>> PARSERS = new Reference2ObjectOpenHashMap<>();
// We need the rawClass parameter here because the Raw type can't be inferred from the parser alone
private static <Raw, Parsed> void register(DataComponentType<Parsed> component, Class<Raw> rawClass, DataComponentParser<Raw, Parsed> parser) {
if (PARSERS.containsKey(component)) {
throw new IllegalStateException("Duplicate data component parser registered for " + component);
}
PARSERS.put(component, parser);
}
private static <Raw, Parsed> void registerSimple(DataComponentType<Parsed> component, Class<Raw> rawClass, Function<Raw, Parsed> parser) {
register(component, rawClass, (session, raw) -> parser.apply(raw));
}
private static <Parsed> void registerSimple(DataComponentType<Parsed> component, Class<Parsed> 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<String, Object> 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<NbtMap> casted = (List<NbtMap>) raw;
List<BannerPatternLayer> 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<BannerPatternLayer.BannerPattern> 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<Float> floats = raw.getList("floats", NbtType.FLOAT);
List<Boolean> flags = raw.getList("flags", NbtType.BYTE).stream().map(b -> b != 0).toList();
List<String> strings = raw.getList("strings", NbtType.STRING);
List<Integer> 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<Integer> colours = raw.getList("colors", NbtType.INT);
List<Integer> 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 <Raw, Parsed> void parseDataComponent(GeyserSession session, DataComponents patch, DataComponentType<?> type,
DataComponentParser<Raw, Parsed> parser, Object raw) {
try {
patch.put((DataComponentType<Parsed>) 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<String, Object> 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:
*
* <ul>
* <li>{@link ItemStackParser#parseItemStack(GeyserSession, NbtMap)}</li>
* <li>{@link ItemTranslator#translateToBedrock(GeyserSession, ItemStack)}</li>
* <li>{@link BedrockItemBuilder#createItemNbt(ItemData)}</li>
* </ul>
*/
public static NbtMapBuilder javaItemStackToBedrock(GeyserSession session, @Nullable NbtMap map) {
return BedrockItemBuilder.createItemNbt(ItemTranslator.translateToBedrock(session, parseItemStack(session, map)));
}
private ItemStackParser() {}
@FunctionalInterface
private interface DataComponentParser<Raw, Parsed> {
Parsed parse(GeyserSession session, Raw raw) throws Exception;
}
}

View File

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

View File

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

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