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! 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 ## 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 ## Setting Up
Take a look [here](https://geysermc.org/wiki/geyser/setup/) for how to set up Geyser. 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 * Gets the pick item of the block state
* *
* @return 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(); @Nullable String pickItem();
/** /**
@@ -103,6 +105,7 @@ public interface JavaBlockState {
Builder canBreakWithHand(boolean canBreakWithHand); Builder canBreakWithHand(boolean canBreakWithHand);
@Deprecated
Builder pickItem(@Nullable String pickItem); Builder pickItem(@Nullable String pickItem);
Builder pistonBehavior(@Nullable String pistonBehavior); 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 * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@@ -23,10 +23,13 @@
* @link https://github.com/GeyserMC/Geyser * @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 { import org.geysermc.geyser.api.entity.property.GeyserEntityProperty;
public HoneyBlock(String javaIdentifier, Builder builder) {
super(javaIdentifier, builder); /**
} * 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 * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@@ -23,20 +23,20 @@
* @link https://github.com/GeyserMC/Geyser * @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.api.entity.property.GeyserEntityProperty;
import org.geysermc.geyser.level.block.property.Properties;
import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;
public class PistonHeadBlock extends Block { /**
public PistonHeadBlock(String javaIdentifier, Builder builder) { * Represents a Java enum-backed enum property.
super(javaIdentifier, builder); * There are a few key limitations:
} * <ul>
* <li>There cannot be more than 16 values</li>
@Override * <li>Enum names cannot be longer than 32 chars, must start with a letter, and may contain numbers and underscores</li>
public ItemStack pickItem(BlockState state) { * </ul>
Block block = state.getValue(Properties.PISTON_TYPE).equals("sticky") ? Blocks.STICKY_PISTON : Blocks.PISTON; *
return new ItemStack(block.asItem().javaId()); * @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; package org.geysermc.geyser.api.entity.type;
import org.checkerframework.checker.index.qual.NonNegative; 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. * have their own sets of entities - no two instances will share the same GeyserEntity instance.
*/ */
public interface GeyserEntity { public interface GeyserEntity {
@@ -37,4 +45,24 @@ public interface GeyserEntity {
*/ */
@NonNegative @NonNegative
int javaId(); 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() fabric()
} }
val includeTransitive: Configuration = configurations.getByName("includeTransitive")
dependencies { dependencies {
modImplementation(libs.fabric.loader) modImplementation(libs.fabric.loader)
modApi(libs.fabric.api) modApi(libs.fabric.api)

View File

@@ -23,8 +23,8 @@
"geyser.mixins.json" "geyser.mixins.json"
], ],
"depends": { "depends": {
"fabricloader": ">=0.16.7", "fabricloader": ">=0.17.2",
"fabric-api": "*", "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 // Jackson shipped by Minecraft is too old, so we shade & relocate our newer version
relocate("com.fasterxml.jackson") relocate("com.fasterxml.jackson")
val includeTransitive: Configuration = configurations.getByName("includeTransitive")
dependencies { dependencies {
// See https://github.com/google/guava/issues/6618 // See https://github.com/google/guava/issues/6618
modules { modules {

View File

@@ -111,7 +111,7 @@ public class GeyserNeoForgeBootstrap extends GeyserModBootstrap {
@Override @Override
public boolean isServer() { public boolean isServer() {
return FMLLoader.getDist().isDedicatedServer(); return FMLLoader.getCurrent().getDist().isDedicatedServer();
} }
private void onPermissionGather(PermissionGatherEvent.Nodes event) { private void onPermissionGather(PermissionGatherEvent.Nodes event) {

View File

@@ -54,10 +54,10 @@ public class GeyserNeoForgeDumpInfo extends BootstrapDumpInfo {
private final List<ModInfo> mods; private final List<ModInfo> mods;
public GeyserNeoForgeDumpInfo(MinecraftServer server) { public GeyserNeoForgeDumpInfo(MinecraftServer server) {
this.platformName = FMLLoader.launcherHandlerName(); this.platformName = server.getServerModName();
this.platformVersion = FMLLoader.versionInfo().neoForgeVersion(); this.platformVersion = FMLLoader.getCurrent().getVersionInfo().neoForgeVersion();
this.minecraftVersion = FMLLoader.versionInfo().mcVersion(); this.minecraftVersion = FMLLoader.getCurrent().getVersionInfo().mcVersion();
this.dist = FMLLoader.getDist(); this.dist = FMLLoader.getCurrent().getDist();
this.serverIP = server.getLocalIp() == null ? "unknown" : server.getLocalIp(); this.serverIP = server.getLocalIp() == null ? "unknown" : server.getLocalIp();
this.serverPort = server.getPort(); this.serverPort = server.getPort();
this.onlineMode = server.usesAuthentication(); 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.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
public class GeyserNeoForgePlatform implements GeyserModPlatform { public class GeyserNeoForgePlatform implements GeyserModPlatform {
@@ -82,8 +81,7 @@ public class GeyserNeoForgePlatform implements GeyserModPlatform {
@Override @Override
public @Nullable InputStream resolveResource(@NonNull String resource) { public @Nullable InputStream resolveResource(@NonNull String resource) {
try { try {
Path path = container.getModInfo().getOwningFile().getFile().findResource(resource); return container.getModInfo().getOwningFile().getFile().getContents().openFile(resource);
return Files.newInputStream(path);
} catch (IOException e) { } catch (IOException e) {
return null; return null;
} }

View File

@@ -71,7 +71,7 @@ public class PermissionUtils {
case FALSE -> false; case FALSE -> false;
case NOT_SET -> { case NOT_SET -> {
if (player != null) { 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. 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]] [[dependencies.geyser_neoforge]]
modId="neoforge" modId="neoforge"
type="required" type="required"
versionRange="[21.6.0-beta,)" versionRange="[21.9.14-beta,)"
ordering="NONE" ordering="NONE"
side="BOTH" side="BOTH"
[[dependencies.geyser_neoforge]] [[dependencies.geyser_neoforge]]
modId="minecraft" modId="minecraft"
type="required" type="required"
versionRange="[1.21.6,)" versionRange="[1.21.9,)"
ordering="NONE" ordering="NONE"
side="BOTH" side="BOTH"

View File

@@ -30,7 +30,7 @@ import net.minecraft.server.MinecraftServer;
import net.minecraft.server.Services; import net.minecraft.server.Services;
import net.minecraft.server.WorldStem; import net.minecraft.server.WorldStem;
import net.minecraft.server.dedicated.DedicatedServer; 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.server.packs.repository.PackRepository;
import net.minecraft.world.level.storage.LevelStorageSource; import net.minecraft.world.level.storage.LevelStorageSource;
import org.geysermc.geyser.platform.mod.GeyserServerPortGetter; import org.geysermc.geyser.platform.mod.GeyserServerPortGetter;
@@ -40,8 +40,9 @@ import java.net.Proxy;
@Mixin(DedicatedServer.class) @Mixin(DedicatedServer.class)
public abstract class DedicatedServerMixin extends MinecraftServer implements GeyserServerPortGetter { 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 @Override

View File

@@ -96,19 +96,31 @@ public class GeyserSpigotCompressionDisabler extends ChannelOutboundHandlerAdapt
private static Class<?> findCompressionPacket() throws ClassNotFoundException { private static Class<?> findCompressionPacket() throws ClassNotFoundException {
try { try {
return Class.forName("net.minecraft.network.protocol.login.PacketLoginOutSetCompression"); // Mojmaps
return Class.forName("net.minecraft.network.protocol.login.ClientboundLoginCompressionPacket");
} catch (ClassNotFoundException e) { } 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"); String prefix = Bukkit.getServer().getClass().getPackage().getName().replace("org.bukkit.craftbukkit", "net.minecraft.server");
return Class.forName(prefix + ".PacketLoginOutSetCompression"); return Class.forName(prefix + ".PacketLoginOutSetCompression");
} }
} }
}
private static Class<?> findLoginSuccessPacket() throws ClassNotFoundException { private static Class<?> findLoginSuccessPacket() throws ClassNotFoundException {
try { try {
return Class.forName("net.minecraft.network.protocol.login.PacketLoginOutSuccess"); // Mojmaps
return Class.forName("net.minecraft.network.protocol.login.ClientboundLoginFinishedPacket");
} catch (ClassNotFoundException e) { } 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"); String prefix = Bukkit.getServer().getClass().getPackage().getName().replace("org.bukkit.craftbukkit", "net.minecraft.server");
return Class.forName(prefix + ".PacketLoginOutSuccess"); return Class.forName(prefix + ".PacketLoginOutSuccess");
} }
} }
}
} }

View File

@@ -96,8 +96,8 @@ tasks {
afterEvaluate { afterEvaluate {
val providedDependencies = providedDependencies[project.name]!! val providedDependencies = providedDependencies[project.name]!!
val shadedDependencies = configurations.getByName("shadowBundle") val shadedDependencies = configurations.getByName("shadowBundle").resolvedConfiguration.resolvedArtifacts.stream()
.dependencies.stream().map { dependency -> "${dependency.group}:${dependency.name}" }.toList() .map { dependency -> "${dependency.moduleVersion.id.module}" }.toList()
// Now: Include all transitive dependencies that aren't excluded // Now: Include all transitive dependencies that aren't excluded
configurations["includeTransitive"].resolvedConfiguration.resolvedArtifacts.forEach { dep -> 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.text.MinecraftLocale;
import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.geyser.util.AssetUtils; import org.geysermc.geyser.util.AssetUtils;
import org.geysermc.geyser.util.CodeOfConductManager;
import org.geysermc.geyser.util.CooldownUtils; import org.geysermc.geyser.util.CooldownUtils;
import org.geysermc.geyser.util.Metrics; import org.geysermc.geyser.util.Metrics;
import org.geysermc.geyser.util.NewsHandler; import org.geysermc.geyser.util.NewsHandler;
@@ -296,7 +297,10 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
if (isReloading) { if (isReloading) {
// If we're reloading, the default locale in the config might have changed. // If we're reloading, the default locale in the config might have changed.
GeyserLocale.finalizeDefaultLocale(this); GeyserLocale.finalizeDefaultLocale(this);
} else {
CodeOfConductManager.load();
} }
GeyserLogger logger = bootstrap.getGeyserLogger(); GeyserLogger logger = bootstrap.getGeyserLogger();
GeyserConfiguration config = bootstrap.getGeyserConfig(); GeyserConfiguration config = bootstrap.getGeyserConfig();
@@ -683,6 +687,7 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
runIfNonNull(erosionUnixListener, UnixSocketClientListener::close); runIfNonNull(erosionUnixListener, UnixSocketClientListener::close);
ResourcePackLoader.clear(); ResourcePackLoader.clear();
CodeOfConductManager.getInstance().save();
this.setEnabled(false); this.setEnabled(false);
} }

View File

@@ -31,6 +31,7 @@ import lombok.experimental.Accessors;
import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.entity.factory.EntityFactory; import org.geysermc.geyser.entity.factory.EntityFactory;
import org.geysermc.geyser.entity.properties.GeyserEntityProperties; 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.entity.type.Entity;
import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.translator.entity.EntityMetadataTranslator; 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) { 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) { 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) { 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 width;
private float height; private float height;
private float offset = 0.00001f; private float offset = 0.00001f;
private GeyserEntityProperties registeredProperties; private GeyserEntityProperties.Builder propertiesBuilder;
private final List<EntityMetadataTranslator<? super T, ?, ?>> translators; private final List<EntityMetadataTranslator<? super T, ?, ?>> translators;
private Builder(EntityFactory<T> factory) { private Builder(EntityFactory<T> factory) {
@@ -97,14 +98,13 @@ public record EntityDefinition<T extends Entity>(EntityFactory<T> factory, Entit
translators = new ObjectArrayList<>(); 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.factory = factory;
this.type = type; this.type = type;
this.identifier = identifier; this.identifier = identifier;
this.width = width; this.width = width;
this.height = height; this.height = height;
this.offset = offset; this.offset = offset;
this.registeredProperties = registeredProperties;
this.translators = translators; this.translators = translators;
} }
@@ -131,8 +131,11 @@ public record EntityDefinition<T extends Entity>(EntityFactory<T> factory, Entit
return this; return this;
} }
public Builder<T> properties(GeyserEntityProperties registeredProperties) { public Builder<T> property(PropertyType<?, ?> propertyType) {
this.registeredProperties = registeredProperties; if (this.propertiesBuilder == null) {
this.propertiesBuilder = new GeyserEntityProperties.Builder(this.identifier);
}
propertiesBuilder.add(propertyType);
return this; return this;
} }
@@ -158,13 +161,11 @@ public record EntityDefinition<T extends Entity>(EntityFactory<T> factory, Entit
if (identifier == null && type != null) { if (identifier == null && type != null) {
identifier = "minecraft:" + type.name().toLowerCase(Locale.ROOT); 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); EntityDefinition<T> definition = new EntityDefinition<>(factory, type, identifier, width, height, offset, registeredProperties, translators);
if (register && definition.entityType() != null) { if (register && definition.entityType() != null) {
Registries.ENTITY_DEFINITIONS.get().putIfAbsent(definition.entityType(), definition); Registries.ENTITY_DEFINITIONS.get().putIfAbsent(definition.entityType(), definition);
Registries.JAVA_ENTITY_IDENTIFIERS.get().putIfAbsent("minecraft:" + type.name().toLowerCase(Locale.ROOT), 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; return definition;
} }

View File

@@ -25,10 +25,23 @@
package org.geysermc.geyser.entity; 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.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; 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.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.AbstractArrowEntity;
import org.geysermc.geyser.entity.type.AbstractWindChargeEntity; import org.geysermc.geyser.entity.type.AbstractWindChargeEntity;
import org.geysermc.geyser.entity.type.AreaEffectCloudEntity; 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.ChestBoatEntity;
import org.geysermc.geyser.entity.type.CommandBlockMinecartEntity; import org.geysermc.geyser.entity.type.CommandBlockMinecartEntity;
import org.geysermc.geyser.entity.type.DisplayBaseEntity; 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.EnderCrystalEntity;
import org.geysermc.geyser.entity.type.EnderEyeEntity; import org.geysermc.geyser.entity.type.EnderEyeEntity;
import org.geysermc.geyser.entity.type.Entity; 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.FireworkEntity;
import org.geysermc.geyser.entity.type.FishingHookEntity; import org.geysermc.geyser.entity.type.FishingHookEntity;
import org.geysermc.geyser.entity.type.FurnaceMinecartEntity; 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.InteractionEntity;
import org.geysermc.geyser.entity.type.ItemEntity; import org.geysermc.geyser.entity.type.ItemEntity;
import org.geysermc.geyser.entity.type.ItemFrameEntity; 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.SpawnerMinecartEntity;
import org.geysermc.geyser.entity.type.TNTEntity; import org.geysermc.geyser.entity.type.TNTEntity;
import org.geysermc.geyser.entity.type.TextDisplayEntity; 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.ThrowableEntity;
import org.geysermc.geyser.entity.type.ThrowableItemEntity; import org.geysermc.geyser.entity.type.ThrowableItemEntity;
import org.geysermc.geyser.entity.type.ThrownPotionEntity; 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.AllayEntity;
import org.geysermc.geyser.entity.type.living.ArmorStandEntity; import org.geysermc.geyser.entity.type.living.ArmorStandEntity;
import org.geysermc.geyser.entity.type.living.BatEntity; 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.DolphinEntity;
import org.geysermc.geyser.entity.type.living.GlowSquidEntity; import org.geysermc.geyser.entity.type.living.GlowSquidEntity;
import org.geysermc.geyser.entity.type.living.IronGolemEntity; 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.ArmadilloEntity;
import org.geysermc.geyser.entity.type.living.animal.AxolotlEntity; 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.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.FoxEntity;
import org.geysermc.geyser.entity.type.living.animal.FrogEntity; 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.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.HoglinEntity;
import org.geysermc.geyser.entity.type.living.animal.MooshroomEntity; 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.OcelotEntity;
import org.geysermc.geyser.entity.type.living.animal.PandaEntity; 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.PolarBearEntity;
import org.geysermc.geyser.entity.type.living.animal.PufferFishEntity; import org.geysermc.geyser.entity.type.living.animal.PufferFishEntity;
import org.geysermc.geyser.entity.type.living.animal.RabbitEntity; 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.StriderEntity;
import org.geysermc.geyser.entity.type.living.animal.TropicalFishEntity; 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.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.AbstractHorseEntity;
import org.geysermc.geyser.entity.type.living.animal.horse.CamelEntity; import org.geysermc.geyser.entity.type.living.animal.horse.CamelEntity;
import org.geysermc.geyser.entity.type.living.animal.horse.ChestedHorseEntity; 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.RavagerEntity;
import org.geysermc.geyser.entity.type.living.monster.raid.SpellcasterIllagerEntity; 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.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.entity.type.player.PlayerEntity;
import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.translator.text.MessageTranslator; 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.metadata.type.FloatEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; 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 final class EntityDefinitions {
public static final EntityDefinition<BoatEntity> ACACIA_BOAT; public static final EntityDefinition<BoatEntity> ACACIA_BOAT;
public static final EntityDefinition<ChestBoatEntity> ACACIA_CHEST_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<MinecartEntity> CHEST_MINECART;
public static final EntityDefinition<ChickenEntity> CHICKEN; public static final EntityDefinition<ChickenEntity> CHICKEN;
public static final EntityDefinition<AbstractFishEntity> COD; 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<CommandBlockMinecartEntity> COMMAND_BLOCK_MINECART;
public static final EntityDefinition<CowEntity> COW; public static final EntityDefinition<CowEntity> COW;
public static final EntityDefinition<CreakingEntity> CREAKING; 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<MagmaCubeEntity> MAGMA_CUBE;
public static final EntityDefinition<BoatEntity> MANGROVE_BOAT; public static final EntityDefinition<BoatEntity> MANGROVE_BOAT;
public static final EntityDefinition<ChestBoatEntity> MANGROVE_CHEST_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<MinecartEntity> MINECART;
public static final EntityDefinition<MooshroomEntity> MOOSHROOM; public static final EntityDefinition<MooshroomEntity> MOOSHROOM;
public static final EntityDefinition<ChestedHorseEntity> MULE; public static final EntityDefinition<ChestedHorseEntity> MULE;
@@ -464,7 +487,7 @@ public final class EntityDefinitions {
EGG = EntityDefinition.inherited(ThrowableEggEntity::new, throwableItemBase) EGG = EntityDefinition.inherited(ThrowableEggEntity::new, throwableItemBase)
.type(EntityType.EGG) .type(EntityType.EGG)
.heightAndWidth(0.25f) .heightAndWidth(0.25f)
.properties(VanillaEntityProperties.CLIMATE_VARIANT) .property(TemperatureVariantAnimal.TEMPERATE_VARIANT_PROPERTY)
.build(); .build();
ENDER_PEARL = EntityDefinition.inherited(ThrowableItemEntity::new, throwableItemBase) ENDER_PEARL = EntityDefinition.inherited(ThrowableItemEntity::new, throwableItemBase)
.type(EntityType.ENDER_PEARL) .type(EntityType.ENDER_PEARL)
@@ -651,16 +674,27 @@ public final class EntityDefinitions {
.addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setLeftLegRotation) .addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setLeftLegRotation)
.addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setRightLegRotation) .addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setRightLegRotation)
.build(); .build();
PLAYER = EntityDefinition.<PlayerEntity>inherited(null, livingEntityBase)
.type(EntityType.PLAYER) EntityDefinition<AvatarEntity> avatarEntityBase = EntityDefinition.<AvatarEntity>inherited(null, livingEntityBase)
.height(1.8f).width(0.6f) .height(1.8f).width(0.6f)
.offset(1.62f) .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(MetadataTypes.FLOAT, PlayerEntity::setAbsorptionHearts)
.addTranslator(null) // Player score .addTranslator(null) // Player score
.addTranslator(MetadataTypes.BYTE, PlayerEntity::setSkinVisibility) .addTranslator(MetadataTypes.OPTIONAL_UNSIGNED_INT, PlayerEntity::setLeftParrot)
.addTranslator(null) // Player main hand .addTranslator(MetadataTypes.OPTIONAL_UNSIGNED_INT, PlayerEntity::setRightParrot)
.addTranslator(MetadataTypes.COMPOUND_TAG, PlayerEntity::setLeftParrot)
.addTranslator(MetadataTypes.COMPOUND_TAG, PlayerEntity::setRightParrot)
.build(); .build();
EntityDefinition<MobEntity> mobEntityBase = EntityDefinition.inherited(MobEntity::new, livingEntityBase) EntityDefinition<MobEntity> mobEntityBase = EntityDefinition.inherited(MobEntity::new, livingEntityBase)
@@ -694,6 +728,15 @@ public final class EntityDefinitions {
.type(EntityType.BREEZE) .type(EntityType.BREEZE)
.height(1.77f).width(0.6f) .height(1.77f).width(0.6f)
.build(); .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) CREAKING = EntityDefinition.inherited(CreakingEntity::new, mobEntityBase)
.type(EntityType.CREAKING) .type(EntityType.CREAKING)
.height(2.7f).width(0.9f) .height(2.7f).width(0.9f)
@@ -701,7 +744,8 @@ public final class EntityDefinitions {
.addTranslator(MetadataTypes.BOOLEAN, CreakingEntity::setActive) .addTranslator(MetadataTypes.BOOLEAN, CreakingEntity::setActive)
.addTranslator(MetadataTypes.BOOLEAN, CreakingEntity::setIsTearingDown) .addTranslator(MetadataTypes.BOOLEAN, CreakingEntity::setIsTearingDown)
.addTranslator(MetadataTypes.OPTIONAL_BLOCK_POS, CreakingEntity::setHomePos) .addTranslator(MetadataTypes.OPTIONAL_BLOCK_POS, CreakingEntity::setHomePos)
.properties(VanillaEntityProperties.CREAKING) .property(CreakingEntity.STATE_PROPERTY)
.property(CreakingEntity.SWAYING_TICKS_PROPERTY)
.build(); .build();
CREEPER = EntityDefinition.inherited(CreeperEntity::new, mobEntityBase) CREEPER = EntityDefinition.inherited(CreeperEntity::new, mobEntityBase)
.type(EntityType.CREEPER) .type(EntityType.CREEPER)
@@ -954,7 +998,7 @@ public final class EntityDefinitions {
ARMADILLO = EntityDefinition.inherited(ArmadilloEntity::new, ageableEntityBase) ARMADILLO = EntityDefinition.inherited(ArmadilloEntity::new, ageableEntityBase)
.type(EntityType.ARMADILLO) .type(EntityType.ARMADILLO)
.height(0.65f).width(0.7f) .height(0.65f).width(0.7f)
.properties(VanillaEntityProperties.ARMADILLO) .property(ArmadilloEntity.STATE_PROPERTY)
.addTranslator(MetadataTypes.ARMADILLO_STATE, ArmadilloEntity::setArmadilloState) .addTranslator(MetadataTypes.ARMADILLO_STATE, ArmadilloEntity::setArmadilloState)
.build(); .build();
AXOLOTL = EntityDefinition.inherited(AxolotlEntity::new, ageableEntityBase) AXOLOTL = EntityDefinition.inherited(AxolotlEntity::new, ageableEntityBase)
@@ -967,20 +1011,20 @@ public final class EntityDefinitions {
BEE = EntityDefinition.inherited(BeeEntity::new, ageableEntityBase) BEE = EntityDefinition.inherited(BeeEntity::new, ageableEntityBase)
.type(EntityType.BEE) .type(EntityType.BEE)
.heightAndWidth(0.6f) .heightAndWidth(0.6f)
.properties(VanillaEntityProperties.BEE) .property(BeeEntity.NECTAR_PROPERTY)
.addTranslator(MetadataTypes.BYTE, BeeEntity::setBeeFlags) .addTranslator(MetadataTypes.BYTE, BeeEntity::setBeeFlags)
.addTranslator(MetadataTypes.INT, BeeEntity::setAngerTime) .addTranslator(MetadataTypes.INT, BeeEntity::setAngerTime)
.build(); .build();
CHICKEN = EntityDefinition.inherited(ChickenEntity::new, ageableEntityBase) CHICKEN = EntityDefinition.inherited(ChickenEntity::new, ageableEntityBase)
.type(EntityType.CHICKEN) .type(EntityType.CHICKEN)
.height(0.7f).width(0.4f) .height(0.7f).width(0.4f)
.properties(VanillaEntityProperties.CLIMATE_VARIANT) .property(TemperatureVariantAnimal.TEMPERATE_VARIANT_PROPERTY)
.addTranslator(MetadataTypes.CHICKEN_VARIANT, ChickenEntity::setVariant) .addTranslator(MetadataTypes.CHICKEN_VARIANT, ChickenEntity::setVariant)
.build(); .build();
COW = EntityDefinition.inherited(CowEntity::new, ageableEntityBase) COW = EntityDefinition.inherited(CowEntity::new, ageableEntityBase)
.type(EntityType.COW) .type(EntityType.COW)
.height(1.4f).width(0.9f) .height(1.4f).width(0.9f)
.properties(VanillaEntityProperties.CLIMATE_VARIANT) .property(TemperatureVariantAnimal.TEMPERATE_VARIANT_PROPERTY)
.addTranslator(MetadataTypes.COW_VARIANT, CowEntity::setVariant) .addTranslator(MetadataTypes.COW_VARIANT, CowEntity::setVariant)
.build(); .build();
FOX = EntityDefinition.inherited(FoxEntity::new, ageableEntityBase) FOX = EntityDefinition.inherited(FoxEntity::new, ageableEntityBase)
@@ -1000,7 +1044,7 @@ public final class EntityDefinitions {
HAPPY_GHAST = EntityDefinition.inherited(HappyGhastEntity::new, ageableEntityBase) HAPPY_GHAST = EntityDefinition.inherited(HappyGhastEntity::new, ageableEntityBase)
.type(EntityType.HAPPY_GHAST) .type(EntityType.HAPPY_GHAST)
.heightAndWidth(4f) .heightAndWidth(4f)
.properties(VanillaEntityProperties.HAPPY_GHAST) .property(HappyGhastEntity.CAN_MOVE_PROPERTY)
.addTranslator(null) // Is leash holder .addTranslator(null) // Is leash holder
.addTranslator(MetadataTypes.BOOLEAN, HappyGhastEntity::setStaysStill) .addTranslator(MetadataTypes.BOOLEAN, HappyGhastEntity::setStaysStill)
.build(); .build();
@@ -1039,7 +1083,7 @@ public final class EntityDefinitions {
PIG = EntityDefinition.inherited(PigEntity::new, ageableEntityBase) PIG = EntityDefinition.inherited(PigEntity::new, ageableEntityBase)
.type(EntityType.PIG) .type(EntityType.PIG)
.heightAndWidth(0.9f) .heightAndWidth(0.9f)
.properties(VanillaEntityProperties.CLIMATE_VARIANT) .property(TemperatureVariantAnimal.TEMPERATE_VARIANT_PROPERTY)
.addTranslator(MetadataTypes.INT, PigEntity::setBoost) .addTranslator(MetadataTypes.INT, PigEntity::setBoost)
.addTranslator(MetadataTypes.PIG_VARIANT, PigEntity::setVariant) .addTranslator(MetadataTypes.PIG_VARIANT, PigEntity::setVariant)
.build(); .build();
@@ -1185,7 +1229,7 @@ public final class EntityDefinitions {
WOLF = EntityDefinition.inherited(WolfEntity::new, tameableEntityBase) WOLF = EntityDefinition.inherited(WolfEntity::new, tameableEntityBase)
.type(EntityType.WOLF) .type(EntityType.WOLF)
.height(0.85f).width(0.6f) .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 // "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.BOOLEAN, (wolfEntity, entityMetadata) -> wolfEntity.setFlag(EntityFlag.INTERESTED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue()))
.addTranslator(MetadataTypes.INT, WolfEntity::setCollarColor) .addTranslator(MetadataTypes.INT, WolfEntity::setCollarColor)
@@ -1219,7 +1263,95 @@ public final class EntityDefinitions {
} }
public static void init() { 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() { private EntityDefinitions() {

View File

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

View File

@@ -31,36 +31,38 @@ import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.ToString; import lombok.ToString;
import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtMapBuilder; import org.cloudburstmc.nbt.NbtMapBuilder;
import org.cloudburstmc.nbt.NbtType; import org.cloudburstmc.nbt.NbtType;
import org.geysermc.geyser.entity.properties.type.BooleanProperty; import org.cloudburstmc.protocol.bedrock.data.entity.EntityProperty;
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.PropertyType;
import org.geysermc.geyser.registry.Registries;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
@EqualsAndHashCode @EqualsAndHashCode
@ToString @ToString
public class GeyserEntityProperties { 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 final Object2IntMap<String> propertyIndices;
private GeyserEntityProperties(ObjectArrayList<PropertyType> properties, private GeyserEntityProperties() {
Object2IntMap<String> propertyIndices) { this.properties = new ObjectArrayList<>();
this.properties = properties; this.propertyIndices = new Object2IntOpenHashMap<>();
this.propertyIndices = propertyIndices;
} }
public NbtMap toNbtMap(String entityType) { public NbtMap toNbtMap(String entityType) {
NbtMapBuilder mapBuilder = NbtMap.builder(); NbtMapBuilder mapBuilder = NbtMap.builder();
List<NbtMap> nbtProperties = new ArrayList<>(); List<NbtMap> nbtProperties = new ArrayList<>();
for (PropertyType property : properties) { for (PropertyType<?, ?> property : properties) {
nbtProperties.add(property.nbtMap()); nbtProperties.add(property.nbtMap());
} }
mapBuilder.putList("properties", NbtType.COMPOUND, nbtProperties); mapBuilder.putList("properties", NbtType.COMPOUND, nbtProperties);
@@ -68,7 +70,30 @@ public class GeyserEntityProperties {
return mapBuilder.putString("type", entityType).build(); 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; return properties;
} }
@@ -77,89 +102,24 @@ public class GeyserEntityProperties {
} }
public static class Builder { public static class Builder {
private final ObjectArrayList<PropertyType> properties = new ObjectArrayList<>(); private GeyserEntityProperties properties;
private final Object2IntMap<String> propertyIndices = new Object2IntOpenHashMap<>(); private final String identifier;
public Builder addInt(@NonNull String name, int min, int max) { public Builder(String identifier) {
if (propertyIndices.containsKey(name)) { this.identifier = identifier;
throw new IllegalArgumentException(
"Property with name " + name + " already exists on builder!");
} }
PropertyType property = new IntProperty(name, min, max);
this.properties.add(property); public <T> Builder add(@NonNull PropertyType<T, ? extends EntityProperty> property) {
propertyIndices.put(name, properties.size() - 1); Objects.requireNonNull(property, "property cannot be null!");
if (properties == null) {
properties = new GeyserEntityProperties();
}
properties.add(identifier, property);
return this; return this;
} }
public Builder addInt(@NonNull String name) { public @Nullable GeyserEntityProperties build() {
if (propertyIndices.containsKey(name)) { return properties;
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);
} }
} }
} }

View File

@@ -25,10 +25,12 @@
package org.geysermc.geyser.entity.properties; 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.FloatEntityProperty;
import org.cloudburstmc.protocol.bedrock.data.entity.IntEntityProperty; 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 org.geysermc.geyser.entity.properties.type.PropertyType;
import java.util.List; import java.util.List;
@@ -36,34 +38,29 @@ import java.util.List;
public class GeyserEntityPropertyManager { public class GeyserEntityPropertyManager {
private final GeyserEntityProperties properties; private final GeyserEntityProperties properties;
private final Object2ObjectMap<String, IntEntityProperty> intEntityProperties = new Object2ObjectArrayMap<>();
private final ObjectArrayList<IntEntityProperty> intEntityProperties = new ObjectArrayList<>(); private final Object2ObjectMap<String, FloatEntityProperty> floatEntityProperties = new Object2ObjectArrayMap<>();
private final ObjectArrayList<FloatEntityProperty> floatEntityProperties = new ObjectArrayList<>();
public GeyserEntityPropertyManager(GeyserEntityProperties properties) { public GeyserEntityPropertyManager(GeyserEntityProperties properties) {
this.properties = 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) { public <T> void addProperty(PropertyType<T, ? extends EntityProperty> propertyType, T value) {
int index = properties.getPropertyIndex(propertyName); int index = properties.getPropertyIndex(propertyType.identifier().toString());
intEntityProperties.add(new IntEntityProperty(index, value)); this.addProperty(propertyType.identifier().toString(), propertyType.createValue(index, value));
} }
public void add(String propertyName, boolean value) { private void addProperty(String propertyName, EntityProperty entityProperty) {
int index = properties.getPropertyIndex(propertyName); if (entityProperty instanceof FloatEntityProperty floatEntityProperty) {
intEntityProperties.add(new IntEntityProperty(index, value ? 1 : 0)); 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() { public boolean hasFloatProperties() {
@@ -78,21 +75,17 @@ public class GeyserEntityPropertyManager {
return hasFloatProperties() || hasIntProperties(); return hasFloatProperties() || hasIntProperties();
} }
public ObjectArrayList<IntEntityProperty> intProperties() {
return this.intEntityProperties;
}
public void applyIntProperties(List<IntEntityProperty> properties) { public void applyIntProperties(List<IntEntityProperty> properties) {
properties.addAll(intEntityProperties); properties.addAll(intEntityProperties.values());
intEntityProperties.clear(); intEntityProperties.clear();
} }
public ObjectArrayList<FloatEntityProperty> floatProperties() {
return this.floatEntityProperties;
}
public void applyFloatProperties(List<FloatEntityProperty> properties) { public void applyFloatProperties(List<FloatEntityProperty> properties) {
properties.addAll(floatEntityProperties); properties.addAll(floatEntityProperties.values());
floatEntityProperties.clear(); 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; package org.geysermc.geyser.entity.properties.type;
import org.cloudburstmc.nbt.NbtMap; 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 { public record BooleanProperty(
private final String name; Identifier identifier,
Boolean defaultValue
public BooleanProperty(String name) { ) implements PropertyType<Boolean, IntEntityProperty>, GeyserBooleanEntityProperty {
this.name = name;
}
@Override @Override
public NbtMap nbtMap() { public NbtMap nbtMap() {
return NbtMap.builder() return NbtMap.builder()
.putString("name", name) .putString("name", identifier.toString())
.putInt("type", 2) .putInt("type", 2)
.build(); .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 * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@@ -25,37 +25,40 @@
package org.geysermc.geyser.entity.properties.type; package org.geysermc.geyser.entity.properties.type;
import it.unimi.dsi.fastutil.objects.Object2IntMap; import org.geysermc.geyser.api.entity.property.type.GeyserEnumEntityProperty;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import org.geysermc.geyser.api.util.Identifier;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtType;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Locale;
public class EnumProperty implements PropertyType { public record EnumProperty<E extends Enum<E>>(
private final String name; Identifier identifier,
private final List<String> values; Class<E> enumClass,
private final Object2IntMap<String> valueIndexMap; E defaultValue
) implements AbstractEnumProperty<E>, GeyserEnumEntityProperty<E> {
public EnumProperty(String name, List<String> values) { public EnumProperty {
this.name = name; validateAllValues(identifier, Arrays.stream(enumClass.getEnumConstants()).map(value -> value.name().toLowerCase(Locale.ROOT)).toList());
this.values = values;
this.valueIndexMap = new Object2IntOpenHashMap<>(values.size());
for (int i = 0; i < values.size(); i++) {
valueIndexMap.put(values.get(i), i);
} }
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 @Override
public NbtMap nbtMap() { public int indexOf(E value) {
return NbtMap.builder() return value.ordinal();
.putString("name", name)
.putList("enum", NbtType.STRING, values)
.putInt("type", 3)
.build();
} }
public int getIndex(String value) { @Override
return valueIndexMap.getOrDefault(value, -1); public int defaultIndex() {
return defaultValue.ordinal();
} }
} }

View File

@@ -26,25 +26,48 @@
package org.geysermc.geyser.entity.properties.type; package org.geysermc.geyser.entity.properties.type;
import org.cloudburstmc.nbt.NbtMap; 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 { public record FloatProperty(
private final String name; Identifier identifier,
private final float max; float max,
private final float min; float min,
Float defaultValue
) implements PropertyType<Float, FloatEntityProperty>, GeyserFloatEntityProperty {
public FloatProperty(String name, float min, float max) { public FloatProperty {
this.name = name; if (min > max) {
this.max = max; throw new IllegalArgumentException("Cannot create float entity property (%s) with a minimum value (%s) greater than maximum (%s)!"
this.min = min; .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 @Override
public NbtMap nbtMap() { public NbtMap nbtMap() {
return NbtMap.builder() return NbtMap.builder()
.putString("name", name) .putString("name", identifier.toString())
.putFloat("max", max) .putFloat("max", max)
.putFloat("min", min) .putFloat("min", min)
.putInt("type", 1) .putInt("type", 1)
.build(); .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; package org.geysermc.geyser.entity.properties.type;
import org.cloudburstmc.nbt.NbtMap; 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 { public record IntProperty(
private final String name; Identifier identifier,
private final int max; int max,
private final int min; int min,
Integer defaultValue
) implements PropertyType<Integer, IntEntityProperty>, GeyserIntEntityProperty {
public IntProperty(String name, int min, int max) { public IntProperty {
this.name = name; if (min > max) {
this.max = max; throw new IllegalArgumentException("Cannot create int entity property (%s) with a minimum value (%s) greater than maximum (%s)!"
this.min = min; .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 @Override
public NbtMap nbtMap() { public NbtMap nbtMap() {
return NbtMap.builder() return NbtMap.builder()
.putString("name", name) .putString("name", identifier.toString())
.putInt("max", max) .putInt("max", max)
.putInt("min", min) .putInt("min", min)
.putInt("type", 0) .putInt("type", 0)
.build(); .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; package org.geysermc.geyser.entity.properties.type;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.nbt.NbtMap; 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(); 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.Getter;
import lombok.Setter; import lombok.Setter;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector2f; import org.cloudburstmc.math.vector.Vector2f;
import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType; import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; 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.AddEntityPacket;
import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket; import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket;
import org.cloudburstmc.protocol.bedrock.packet.MoveEntityAbsolutePacket; import org.cloudburstmc.protocol.bedrock.packet.MoveEntityAbsolutePacket;
import org.cloudburstmc.protocol.bedrock.packet.MoveEntityDeltaPacket; import org.cloudburstmc.protocol.bedrock.packet.MoveEntityDeltaPacket;
import org.cloudburstmc.protocol.bedrock.packet.RemoveEntityPacket; import org.cloudburstmc.protocol.bedrock.packet.RemoveEntityPacket;
import org.cloudburstmc.protocol.bedrock.packet.SetEntityDataPacket; 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.api.entity.type.GeyserEntity;
import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.entity.GeyserDirtyMetadata; 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.GeyserEntityPropertyManager;
import org.geysermc.geyser.entity.properties.type.PropertyType;
import org.geysermc.geyser.entity.type.living.MobEntity; import org.geysermc.geyser.entity.type.living.MobEntity;
import org.geysermc.geyser.entity.type.player.PlayerEntity; import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.Items;
@@ -71,6 +77,7 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.function.Consumer;
@Getter @Getter
@Setter @Setter
@@ -118,7 +125,7 @@ public class Entity implements GeyserEntity {
@Setter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
private float boundingBoxWidth; private float boundingBoxWidth;
@Setter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
private String displayName; protected String displayName;
@Setter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
protected boolean silent = false; protected boolean silent = false;
/* Metadata end */ /* Metadata end */
@@ -201,6 +208,10 @@ public class Entity implements GeyserEntity {
addEntityPacket.setBodyRotation(yaw); // TODO: This should be bodyYaw addEntityPacket.setBodyRotation(yaw); // TODO: This should be bodyYaw
addEntityPacket.getMetadata().putFlags(flags); addEntityPacket.getMetadata().putFlags(flags);
dirtyMetadata.apply(addEntityPacket.getMetadata()); dirtyMetadata.apply(addEntityPacket.getMetadata());
if (propertyManager != null) {
propertyManager.applyIntProperties(addEntityPacket.getProperties().getIntProperties());
propertyManager.applyFloatProperties(addEntityPacket.getProperties().getFloatProperties());
}
addAdditionalSpawnData(addEntityPacket); addAdditionalSpawnData(addEntityPacket);
valid = true; 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. // Note this might be client side. Has yet to be an issue though, as of Java 1.21.
return InteractiveTag.REMOVE_LEASH; 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 // We shall leash
return InteractiveTag.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. // Has yet to be an issue though, as of Java 1.21.
return InteractionResult.SUCCESS; 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)) { && !(session.getEntityCache().getEntityByGeyserId(leashable.leashHolderBedrockId()) instanceof PlayerEntity)) {
// We shall leash // We shall leash
return InteractionResult.SUCCESS; return InteractionResult.SUCCESS;
@@ -752,4 +763,42 @@ public class Entity implements GeyserEntity {
packet.setData(data); packet.setData(data);
session.sendUpstreamPacket(packet); 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.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId; 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.MobArmorEquipmentPacket;
import org.cloudburstmc.protocol.bedrock.packet.MobEquipmentPacket; import org.cloudburstmc.protocol.bedrock.packet.MobEquipmentPacket;
import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket; 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.entity.vehicle.HappyGhastVehicleComponent;
import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.item.Items; 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.scoreboard.Team;
import org.geysermc.geyser.session.GeyserSession; 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.translator.item.ItemTranslator;
import org.geysermc.geyser.util.AttributeUtils; import org.geysermc.geyser.util.AttributeUtils;
import org.geysermc.geyser.util.EntityUtils; import org.geysermc.geyser.util.EntityUtils;
@@ -82,15 +82,6 @@ import java.util.UUID;
public class LivingEntity extends Entity { public class LivingEntity extends Entity {
protected EnumMap<EquipmentSlot, GeyserItemStack> equipment = new EnumMap<>(EquipmentSlot.class); 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) @Getter(value = AccessLevel.NONE)
protected float health = 1f; // The default value in Java Edition before any entity metadata is sent protected float health = 1f; // The default value in Java Edition before any entity metadata is sent
@Getter(value = AccessLevel.NONE) @Getter(value = AccessLevel.NONE)
@@ -118,34 +109,48 @@ public class LivingEntity extends Entity {
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); 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) { public void setHelmet(GeyserItemStack stack) {
this.equipment.put(EquipmentSlot.HELMET, stack); this.equipment.put(EquipmentSlot.HELMET, stack);
this.helmet = ItemTranslator.translateToBedrock(session, stack);
} }
public void setChestplate(GeyserItemStack stack) { public void setChestplate(GeyserItemStack stack) {
this.equipment.put(EquipmentSlot.CHESTPLATE, stack); this.equipment.put(EquipmentSlot.CHESTPLATE, stack);
this.chestplate = ItemTranslator.translateToBedrock(session, stack);
} }
public void setLeggings(GeyserItemStack stack) { public void setLeggings(GeyserItemStack stack) {
this.equipment.put(EquipmentSlot.LEGGINGS, stack); this.equipment.put(EquipmentSlot.LEGGINGS, stack);
this.leggings = ItemTranslator.translateToBedrock(session, stack);
} }
public void setBoots(GeyserItemStack stack) { public void setBoots(GeyserItemStack stack) {
this.equipment.put(EquipmentSlot.BOOTS, stack); this.equipment.put(EquipmentSlot.BOOTS, stack);
this.boots = ItemTranslator.translateToBedrock(session, stack);
} }
public void setBody(GeyserItemStack stack) { public void setBody(GeyserItemStack stack) {
this.equipment.put(EquipmentSlot.BODY, stack); this.equipment.put(EquipmentSlot.BODY, stack);
this.body = ItemTranslator.translateToBedrock(session, stack);
} }
public void setSaddle(GeyserItemStack stack) { public void setSaddle(GeyserItemStack stack) {
this.equipment.put(EquipmentSlot.SADDLE, stack); this.equipment.put(EquipmentSlot.SADDLE, stack);
this.saddle = ItemTranslator.translateToBedrock(session, stack);
boolean saddled = false; boolean saddled = false;
if (!stack.isEmpty()) { if (!stack.isEmpty()) {
@@ -158,12 +163,10 @@ public class LivingEntity extends Entity {
public void setHand(GeyserItemStack stack) { public void setHand(GeyserItemStack stack) {
this.equipment.put(EquipmentSlot.MAIN_HAND, stack); this.equipment.put(EquipmentSlot.MAIN_HAND, stack);
this.hand = ItemTranslator.translateToBedrock(session, stack);
} }
public void setOffhand(GeyserItemStack stack) { public void setOffhand(GeyserItemStack stack) {
this.equipment.put(EquipmentSlot.OFF_HAND, stack); this.equipment.put(EquipmentSlot.OFF_HAND, stack);
this.offhand = ItemTranslator.translateToBedrock(session, stack);
} }
protected void updateSaddled(boolean saddled) { protected void updateSaddled(boolean saddled) {
@@ -178,13 +181,9 @@ public class LivingEntity extends Entity {
} }
public void switchHands() { 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.OFF_HAND, this.equipment.get(EquipmentSlot.MAIN_HAND));
this.equipment.put(EquipmentSlot.MAIN_HAND, javaOffhand); this.equipment.put(EquipmentSlot.MAIN_HAND, offhand);
ItemData bedrockOffhand = this.offhand;
this.offhand = this.hand;
this.hand = bedrockOffhand;
} }
@Override @Override
@@ -279,11 +278,10 @@ public class LivingEntity extends Entity {
} }
protected boolean hasShield(boolean offhand) { protected boolean hasShield(boolean offhand) {
ItemMapping shieldMapping = session.getItemMappings().getStoredItems().shield();
if (offhand) { if (offhand) {
return this.offhand.getDefinition().equals(shieldMapping.getBedrockDefinition()); return getOffHandItem().is(Items.SHIELD);
} else { } else {
return hand.getDefinition().equals(shieldMapping.getBedrockDefinition()); return getMainHandItem().is(Items.SHIELD);
} }
} }
@@ -342,7 +340,7 @@ public class LivingEntity extends Entity {
@Override @Override
public InteractionResult interact(Hand hand) { public InteractionResult interact(Hand hand) {
GeyserItemStack itemStack = session.getPlayerInventory().getItemInHand(hand); GeyserItemStack itemStack = session.getPlayerInventory().getItemInHand(hand);
if (itemStack.asItem() == Items.NAME_TAG) { if (itemStack.is(Items.NAME_TAG)) {
InteractionResult result = checkInteractWithNameTag(itemStack); InteractionResult result = checkInteractWithNameTag(itemStack);
if (result.consumesAction()) { if (result.consumesAction()) {
return result; return result;
@@ -394,39 +392,38 @@ public class LivingEntity extends Entity {
return InteractionResult.PASS; return InteractionResult.PASS;
} }
public void updateArmor(GeyserSession session) { public void updateArmor() {
if (!valid) return; if (!valid) return;
ItemData helmet = this.helmet; GeyserItemStack helmet = getItemInSlot(EquipmentSlot.HELMET);
ItemData chestplate = this.chestplate; 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 // 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 // But don't overwrite the chestplate if it isn't empty
ItemMapping banner = session.getItemMappings().getStoredItems().banner(); if (chestplate.isEmpty() && helmet.is(session, ItemTag.BANNERS)) {
if (ItemData.AIR.equals(chestplate) && helmet.getDefinition().equals(banner.getBedrockDefinition())) { chestplate = helmet;
chestplate = this.helmet; helmet = GeyserItemStack.EMPTY;
helmet = ItemData.AIR; } else if (chestplate.is(session, ItemTag.BANNERS)) {
} else if (chestplate.getDefinition().equals(banner.getBedrockDefinition())) {
// Prevent chestplate banners from showing erroneously // Prevent chestplate banners from showing erroneously
chestplate = ItemData.AIR; chestplate = GeyserItemStack.EMPTY;
} }
MobArmorEquipmentPacket armorEquipmentPacket = new MobArmorEquipmentPacket(); MobArmorEquipmentPacket armorEquipmentPacket = new MobArmorEquipmentPacket();
armorEquipmentPacket.setRuntimeEntityId(geyserId); armorEquipmentPacket.setRuntimeEntityId(geyserId);
armorEquipmentPacket.setHelmet(helmet); armorEquipmentPacket.setHelmet(ItemTranslator.translateToBedrock(session, helmet));
armorEquipmentPacket.setChestplate(chestplate); armorEquipmentPacket.setChestplate(ItemTranslator.translateToBedrock(session, chestplate));
armorEquipmentPacket.setLeggings(leggings); armorEquipmentPacket.setLeggings(ItemTranslator.translateToBedrock(session, getItemInSlot(EquipmentSlot.LEGGINGS)));
armorEquipmentPacket.setBoots(boots); armorEquipmentPacket.setBoots(ItemTranslator.translateToBedrock(session, getItemInSlot(EquipmentSlot.BOOTS)));
armorEquipmentPacket.setBody(body); armorEquipmentPacket.setBody(ItemTranslator.translateToBedrock(session, getItemInSlot(EquipmentSlot.BODY)));
session.sendUpstreamPacket(armorEquipmentPacket); session.sendUpstreamPacket(armorEquipmentPacket);
} }
public void updateMainHand(GeyserSession session) { public void updateMainHand() {
if (!valid) return; if (!valid) return;
MobEquipmentPacket handPacket = new MobEquipmentPacket(); MobEquipmentPacket handPacket = new MobEquipmentPacket();
handPacket.setRuntimeEntityId(geyserId); handPacket.setRuntimeEntityId(geyserId);
handPacket.setItem(hand); handPacket.setItem(ItemTranslator.translateToBedrock(session, getMainHandItem()));
handPacket.setHotbarSlot(-1); handPacket.setHotbarSlot(-1);
handPacket.setInventorySlot(0); handPacket.setInventorySlot(0);
handPacket.setContainerId(ContainerId.INVENTORY); handPacket.setContainerId(ContainerId.INVENTORY);
@@ -434,12 +431,12 @@ public class LivingEntity extends Entity {
session.sendUpstreamPacket(handPacket); session.sendUpstreamPacket(handPacket);
} }
public void updateOffHand(GeyserSession session) { public void updateOffHand() {
if (!valid) return; if (!valid) return;
MobEquipmentPacket offHandPacket = new MobEquipmentPacket(); MobEquipmentPacket offHandPacket = new MobEquipmentPacket();
offHandPacket.setRuntimeEntityId(geyserId); offHandPacket.setRuntimeEntityId(geyserId);
offHandPacket.setItem(offhand); offHandPacket.setItem(ItemTranslator.translateToBedrock(session, getOffHandItem()));
offHandPacket.setHotbarSlot(-1); offHandPacket.setHotbarSlot(-1);
offHandPacket.setInventorySlot(0); offHandPacket.setInventorySlot(0);
offHandPacket.setContainerId(ContainerId.OFFHAND); offHandPacket.setContainerId(ContainerId.OFFHAND);

View File

@@ -28,9 +28,7 @@ package org.geysermc.geyser.entity.type;
import lombok.Getter; import lombok.Getter;
import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Key;
import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket;
import org.geysermc.geyser.entity.EntityDefinition; 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.entity.type.living.animal.farm.TemperatureVariantAnimal;
import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.item.Items; 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.ItemStack;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes;
import java.util.Locale;
import java.util.UUID; import java.util.UUID;
@Getter @Getter
@@ -54,31 +51,25 @@ public class ThrowableEggEntity extends ThrowableItemEntity {
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); 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 @Override
public void setItem(EntityMetadata<ItemStack, ?> entityMetadata) { public void setItem(EntityMetadata<ItemStack, ?> entityMetadata) {
GeyserItemStack stack = GeyserItemStack.from(entityMetadata.getValue()); 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(); updateBedrockEntityProperties();
this.itemStack = stack; 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); Holder<Key> holder = stack.getComponent(DataComponentTypes.CHICKEN_VARIANT);
if (holder != null) { if (holder != null) {
Key chickenVariant = holder.getOrCompute(id -> JavaRegistries.CHICKEN_VARIANT.key(session, id)); Key chickenVariant = holder.getOrCompute(id -> JavaRegistries.CHICKEN_VARIANT.key(session, id));
for (var variant : TemperatureVariantAnimal.BuiltInVariant.values()) { for (var variant : TemperatureVariantAnimal.BuiltInVariant.values()) {
if (chickenVariant.asMinimalString().equalsIgnoreCase(variant.name())) { 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.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.session.GeyserSession; 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.InteractionResult;
import org.geysermc.geyser.util.InteractiveTag; 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.metadata.type.BooleanEntityMetadata;
@@ -60,9 +60,9 @@ public class AllayEntity extends MobEntity {
if (this.canDuplicate && getFlag(EntityFlag.DANCING) && isDuplicationItem(itemInHand)) { if (this.canDuplicate && getFlag(EntityFlag.DANCING) && isDuplicationItem(itemInHand)) {
// Maybe better as another tag? // Maybe better as another tag?
return InteractiveTag.GIVE_ITEM_TO_ALLAY; 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; 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 // Seems like there isn't a good tag for this yet
return InteractiveTag.GIVE_ITEM_TO_ALLAY; return InteractiveTag.GIVE_ITEM_TO_ALLAY;
} else { } else {
@@ -76,10 +76,10 @@ public class AllayEntity extends MobEntity {
if (this.canDuplicate && getFlag(EntityFlag.DANCING) && isDuplicationItem(itemInHand)) { if (this.canDuplicate && getFlag(EntityFlag.DANCING) && isDuplicationItem(itemInHand)) {
//TOCHECK sound //TOCHECK sound
return InteractionResult.SUCCESS; return InteractionResult.SUCCESS;
} else if (!this.hand.isValid() && !itemInHand.isEmpty()) { } else if (getMainHandItem().isEmpty() && !itemInHand.isEmpty()) {
//TODO play sound? //TODO play sound?
return InteractionResult.SUCCESS; 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? //TOCHECK also play sound here?
return InteractionResult.SUCCESS; return InteractionResult.SUCCESS;
} else { } else {
@@ -88,6 +88,6 @@ public class AllayEntity extends MobEntity {
} }
private boolean isDuplicationItem(GeyserItemStack itemStack) { 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.EntityDataType;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; 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.EntityDefinition;
import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.entity.type.LivingEntity; 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.session.GeyserSession;
import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractionResult;
import org.geysermc.geyser.util.MathUtils; 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.EntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; 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.ByteEntityMetadata;
@@ -257,7 +257,7 @@ public class ArmorStandEntity extends LivingEntity {
@Override @Override
public InteractionResult interactAt(Hand hand) { 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 // Java Edition returns SUCCESS if in spectator mode, but this is overridden with an earlier check on the client
return InteractionResult.CONSUME; return InteractionResult.CONSUME;
} else { } else {
@@ -332,8 +332,7 @@ public class ArmorStandEntity extends LivingEntity {
return; return;
} }
boolean isNametagEmpty = nametag.isEmpty(); boolean isNametagEmpty = nametag.isEmpty();
if (!isNametagEmpty && (!helmet.equals(ItemData.AIR) || !chestplate.equals(ItemData.AIR) || !leggings.equals(ItemData.AIR) if (!isNametagEmpty && hasAnyEquipment()) {
|| !boots.equals(ItemData.AIR) || !hand.equals(ItemData.AIR) || !offhand.equals(ItemData.AIR))) {
// Reset scale of the proper armor stand // Reset scale of the proper armor stand
setScale(getScale()); setScale(getScale());
// Set the proper armor stand to invisible to show armor // 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 @Override
public float getBoundingBoxWidth() { public float getBoundingBoxWidth() {
// For consistency with getBoundingBoxHeight() // 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 @NonNull
@Override @Override
protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { 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 InteractiveTag.FEED;
} }
return super.testMobInteraction(hand, itemInHand); return super.testMobInteraction(hand, itemInHand);
@@ -59,7 +59,7 @@ public class DolphinEntity extends AgeableWaterEntity {
@NonNull @NonNull
@Override @Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { 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 // Feed
return InteractionResult.SUCCESS; return InteractionResult.SUCCESS;
} }

View File

@@ -54,7 +54,7 @@ public class IronGolemEntity extends GolemEntity {
@NonNull @NonNull
@Override @Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
if (itemInHand.asItem() == Items.IRON_INGOT) { if (itemInHand.is(Items.IRON_INGOT)) {
if (health < maxHealth) { if (health < maxHealth) {
// Healing the iron golem // Healing the iron golem
return InteractionResult.SUCCESS; return InteractionResult.SUCCESS;

View File

@@ -85,7 +85,7 @@ public class MobEntity extends LivingEntity implements Leashable {
return InteractiveTag.REMOVE_LEASH; return InteractiveTag.REMOVE_LEASH;
} else { } else {
GeyserItemStack itemStack = session.getPlayerInventory().getItemInHand(hand); GeyserItemStack itemStack = session.getPlayerInventory().getItemInHand(hand);
if (itemStack.asItem() == Items.NAME_TAG) { if (itemStack.is(Items.NAME_TAG)) {
InteractionResult result = checkInteractWithNameTag(itemStack); InteractionResult result = checkInteractWithNameTag(itemStack);
if (result.consumesAction()) { if (result.consumesAction()) {
return InteractiveTag.NAME; return InteractiveTag.NAME;
@@ -120,8 +120,10 @@ public class MobEntity extends LivingEntity implements Leashable {
} }
for (EquipmentSlot slot : EquipmentSlot.values()) { for (EquipmentSlot slot : EquipmentSlot.values()) {
GeyserItemStack equipped = equipment.get(slot); GeyserItemStack equipped = getItemInSlot(slot);
if (equipped == null || equipped.isEmpty()) continue; if (equipped.isEmpty()) {
continue;
}
Equippable equippable = equipped.getComponent(DataComponentTypes.EQUIPPABLE); Equippable equippable = equipped.getComponent(DataComponentTypes.EQUIPPABLE);
if (equippable != null && equippable.canBeSheared()) { if (equippable != null && equippable.canBeSheared()) {
@@ -135,7 +137,7 @@ public class MobEntity extends LivingEntity implements Leashable {
} }
private InteractionResult checkPriorityInteractions(GeyserItemStack itemInHand) { private InteractionResult checkPriorityInteractions(GeyserItemStack itemInHand) {
if (itemInHand.asItem() == Items.NAME_TAG) { if (itemInHand.is(Items.NAME_TAG)) {
InteractionResult result = checkInteractWithNameTag(itemInHand); InteractionResult result = checkInteractWithNameTag(itemInHand);
if (result.consumesAction()) { if (result.consumesAction()) {
return result; return result;

View File

@@ -54,7 +54,7 @@ public class SnowGolemEntity extends GolemEntity {
@NonNull @NonNull
@Override @Override
protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { 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 // Shearing the snow golem
return InteractiveTag.SHEAR; return InteractiveTag.SHEAR;
} }
@@ -64,7 +64,7 @@ public class SnowGolemEntity extends GolemEntity {
@NonNull @NonNull
@Override @Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { 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 // Shearing the snow golem
return InteractionResult.SUCCESS; return InteractionResult.SUCCESS;
} }

View File

@@ -62,6 +62,6 @@ public class TadpoleEntity extends AbstractFishEntity {
} }
private boolean isFood(GeyserItemStack itemStack) { 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) { if (tag == null) {
return false; 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.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3f;
import org.geysermc.geyser.entity.EntityDefinition; 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.item.type.Item;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.session.cache.tags.ItemTag;
@@ -39,6 +41,13 @@ import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public class ArmadilloEntity extends AnimalEntity { 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; private ArmadilloState armadilloState = ArmadilloState.IDLE;
public ArmadilloEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, public ArmadilloEntity(GeyserSession session, int entityId, long geyserId, UUID uuid,
@@ -50,10 +59,10 @@ public class ArmadilloEntity extends AnimalEntity {
armadilloState = entityMetadata.getValue(); armadilloState = entityMetadata.getValue();
switch (armadilloState) { switch (armadilloState) {
case IDLE -> propertyManager.add("minecraft:armadillo_state", "unrolled"); case IDLE -> STATE_PROPERTY.apply(propertyManager, State.UNROLLED);
case ROLLING -> propertyManager.add("minecraft:armadillo_state", "rolled_up"); case ROLLING -> STATE_PROPERTY.apply(propertyManager, State.ROLLED_UP);
case SCARED -> propertyManager.add("minecraft:armadillo_state", "rolled_up_relaxing"); case SCARED -> STATE_PROPERTY.apply(propertyManager, State.ROLLED_UP_RELAXING);
case UNROLLING -> propertyManager.add("minecraft:armadillo_state", "rolled_up_unrolling"); case UNROLLING -> STATE_PROPERTY.apply(propertyManager, State.ROLLED_UP_UNROLLING);
} }
updateBedrockEntityProperties(); updateBedrockEntityProperties();
@@ -62,13 +71,13 @@ public class ArmadilloEntity extends AnimalEntity {
public void onPeeking() { public void onPeeking() {
// Technically we should wait if not currently scared // Technically we should wait if not currently scared
if (armadilloState == ArmadilloState.SCARED) { if (armadilloState == ArmadilloState.SCARED) {
propertyManager.add("minecraft:armadillo_state", "rolled_up_peeking"); STATE_PROPERTY.apply(propertyManager, State.ROLLED_UP_PEEKING);
updateBedrockEntityProperties(); updateBedrockEntityProperties();
// Needed for consecutive peeks // Needed for consecutive peeks
session.scheduleInEventLoop(() -> { session.scheduleInEventLoop(() -> {
if (armadilloState == ArmadilloState.SCARED) { if (armadilloState == ArmadilloState.SCARED) {
propertyManager.add("minecraft:armadillo_state", "rolled_up_relaxing"); STATE_PROPERTY.apply(propertyManager, State.ROLLED_UP_RELAXING);
updateBedrockEntityProperties(); updateBedrockEntityProperties();
} }
}, 250, TimeUnit.MILLISECONDS); }, 250, TimeUnit.MILLISECONDS);
@@ -80,4 +89,12 @@ public class ArmadilloEntity extends AnimalEntity {
protected Tag<Item> getFoodTag() { protected Tag<Item> getFoodTag() {
return ItemTag.ARMADILLO_FOOD; 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.data.entity.EntityFlag;
import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket; import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket;
import org.geysermc.geyser.entity.EntityDefinition; 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.item.type.Item;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.session.cache.tags.ItemTag;
@@ -43,6 +45,11 @@ import java.util.UUID;
public class BeeEntity extends AnimalEntity { 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) { 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); 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 // If the bee has stung
dirtyMetadata.put(EntityDataTypes.MARK_VARIANT, (xd & 0x04) == 0x04 ? 1 : 0); dirtyMetadata.put(EntityDataTypes.MARK_VARIANT, (xd & 0x04) == 0x04 ? 1 : 0);
// If the bee has nectar or not // If the bee has nectar or not
propertyManager.add("minecraft:has_nectar", (xd & 0x08) == 0x08); NECTAR_PROPERTY.apply(propertyManager, (xd & 0x08) == 0x08);
updateBedrockEntityProperties(); updateBedrockEntityProperties();
} }

View File

@@ -77,7 +77,7 @@ public class GoatEntity extends AnimalEntity {
@NonNull @NonNull
@Override @Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { 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); session.playSoundEvent(isScreamer ? SoundEvent.MILK_SCREAMER : SoundEvent.MILK, position);
return InteractionResult.SUCCESS; return InteractionResult.SUCCESS;
} else { } 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.AttributeData;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.geysermc.geyser.entity.EntityDefinition; 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.Entity;
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
import org.geysermc.geyser.entity.vehicle.ClientVehicle; import org.geysermc.geyser.entity.vehicle.ClientVehicle;
import org.geysermc.geyser.entity.vehicle.HappyGhastVehicleComponent; import org.geysermc.geyser.entity.vehicle.HappyGhastVehicleComponent;
import org.geysermc.geyser.entity.vehicle.VehicleComponent; import org.geysermc.geyser.entity.vehicle.VehicleComponent;
import org.geysermc.geyser.impl.IdentifierImpl;
import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.item.type.Item; 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[] 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 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 final HappyGhastVehicleComponent vehicleComponent = new HappyGhastVehicleComponent(this, 0.0f);
private boolean staysStill; private boolean staysStill;
@@ -80,8 +87,6 @@ public class HappyGhastEntity extends AnimalEntity implements ClientVehicle {
setFlag(EntityFlag.WASD_AIR_CONTROLLED, true); setFlag(EntityFlag.WASD_AIR_CONTROLLED, true);
setFlag(EntityFlag.DOES_SERVER_AUTH_ONLY_DISMOUNT, true); setFlag(EntityFlag.DOES_SERVER_AUTH_ONLY_DISMOUNT, true);
propertyManager.add("minecraft:can_move", true);
} }
@Override @Override
@@ -111,7 +116,7 @@ public class HappyGhastEntity extends AnimalEntity implements ClientVehicle {
public void setStaysStill(BooleanEntityMetadata entityMetadata) { public void setStaysStill(BooleanEntityMetadata entityMetadata) {
staysStill = entityMetadata.getPrimitiveValue(); staysStill = entityMetadata.getPrimitiveValue();
propertyManager.add("minecraft:can_move", !entityMetadata.getPrimitiveValue()); CAN_MOVE_PROPERTY.apply(propertyManager, !entityMetadata.getPrimitiveValue());
updateBedrockEntityProperties(); updateBedrockEntityProperties();
} }
@@ -122,12 +127,12 @@ public class HappyGhastEntity extends AnimalEntity implements ClientVehicle {
return super.testMobInteraction(hand, itemInHand); return super.testMobInteraction(hand, itemInHand);
} else { } else {
if (!itemInHand.isEmpty()) { if (!itemInHand.isEmpty()) {
if (session.getTagCache().is(ItemTag.HARNESSES, itemInHand)) { if (itemInHand.is(session, ItemTag.HARNESSES)) {
if (this.equipment.get(EquipmentSlot.BODY) == null) { if (getItemInSlot(EquipmentSlot.BODY).isEmpty()) {
// Harnesses the ghast // Harnesses the ghast
return InteractiveTag.EQUIP_HARNESS; return InteractiveTag.EQUIP_HARNESS;
} }
} else if (itemInHand.asItem() == Items.SHEARS) { } else if (itemInHand.is(Items.SHEARS)) {
if (this.canShearEquipment() && !session.isSneaking()) { if (this.canShearEquipment() && !session.isSneaking()) {
// Shears the harness off of the ghast // Shears the harness off of the ghast
return InteractiveTag.REMOVE_HARNESS; 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 // Rides happy ghast
return InteractiveTag.RIDE_HORSE; return InteractiveTag.RIDE_HORSE;
} else { } else {
@@ -151,12 +156,12 @@ public class HappyGhastEntity extends AnimalEntity implements ClientVehicle {
return super.mobInteract(hand, itemInHand); return super.mobInteract(hand, itemInHand);
} else { } else {
if (!itemInHand.isEmpty()) { if (!itemInHand.isEmpty()) {
if (session.getTagCache().is(ItemTag.HARNESSES, itemInHand)) { if (itemInHand.is(session, ItemTag.HARNESSES)) {
if (this.equipment.get(EquipmentSlot.BODY) == null) { if (getItemInSlot(EquipmentSlot.BODY).isEmpty()) {
// Harnesses the ghast // Harnesses the ghast
return InteractionResult.SUCCESS; return InteractionResult.SUCCESS;
} }
} else if (itemInHand.asItem() == Items.SHEARS) { } else if (itemInHand.is(Items.SHEARS)) {
if (this.canShearEquipment() && !session.isSneaking()) { if (this.canShearEquipment() && !session.isSneaking()) {
// Shears the harness off of the ghast // Shears the harness off of the ghast
return InteractionResult.SUCCESS; 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 // Rides happy ghast
return InteractionResult.SUCCESS; return InteractionResult.SUCCESS;
} else { } else {

View File

@@ -63,10 +63,10 @@ public class MooshroomEntity extends CowEntity {
@Override @Override
protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
if (!isBaby()) { if (!isBaby()) {
if (itemInHand.asItem() == Items.BOWL) { if (itemInHand.is(Items.BOWL)) {
// Stew // Stew
return InteractiveTag.MOOSHROOM_MILK_STEW; return InteractiveTag.MOOSHROOM_MILK_STEW;
} else if (isAlive() && itemInHand.asItem() == Items.SHEARS) { } else if (isAlive() && itemInHand.is(Items.SHEARS)) {
// Shear items // Shear items
return InteractiveTag.MOOSHROOM_SHEAR; return InteractiveTag.MOOSHROOM_SHEAR;
} }
@@ -78,13 +78,13 @@ public class MooshroomEntity extends CowEntity {
@Override @Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
boolean isBaby = isBaby(); boolean isBaby = isBaby();
if (!isBaby && itemInHand.asItem() == Items.BOWL) { if (!isBaby && itemInHand.is(Items.BOWL)) {
// Stew // Stew
return InteractionResult.SUCCESS; return InteractionResult.SUCCESS;
} else if (!isBaby && isAlive() && itemInHand.asItem() == Items.SHEARS) { } else if (!isBaby && isAlive() && itemInHand.is(Items.SHEARS)) {
// Shear items // Shear items
return InteractionResult.SUCCESS; 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; return InteractionResult.SUCCESS;
} }

View File

@@ -64,7 +64,7 @@ public class PandaEntity extends AnimalEntity {
packet.setRuntimeEntityId(geyserId); packet.setRuntimeEntityId(geyserId);
packet.setType(EntityEventType.EATING_ITEM); packet.setType(EntityEventType.EATING_ITEM);
// As of 1.20.5 - pandas can eat cake // 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); session.sendUpstreamPacket(packet);
} }
} }

View File

@@ -68,7 +68,7 @@ public class SheepEntity extends AnimalEntity {
@NonNull @NonNull
@Override @Override
protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
if (itemInHand.asItem() == Items.SHEARS) { if (itemInHand.is(Items.SHEARS)) {
return InteractiveTag.SHEAR; return InteractiveTag.SHEAR;
} else { } else {
InteractiveTag tag = super.testMobInteraction(hand, itemInHand); InteractiveTag tag = super.testMobInteraction(hand, itemInHand);
@@ -86,7 +86,7 @@ public class SheepEntity extends AnimalEntity {
@NonNull @NonNull
@Override @Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
if (itemInHand.asItem() == Items.SHEARS) { if (itemInHand.is(Items.SHEARS)) {
return InteractionResult.CONSUME; return InteractionResult.CONSUME;
} else { } else {
InteractionResult superResult = super.mobInteract(hand, itemInHand); 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.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector2f; import org.cloudburstmc.math.vector.Vector2f;
import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.Entity;
@@ -157,8 +156,7 @@ public class StriderEntity extends AnimalEntity implements Tickable, ClientVehic
vehicleComponent.tickBoost(); vehicleComponent.tickBoost();
} }
} else { // getHand() for session player seems to always return air } else { // getHand() for session player seems to always return air
ItemDefinition itemDefinition = session.getItemMappings().getStoredItems().warpedFungusOnAStick().getBedrockDefinition(); if (player.isHolding(Items.WARPED_FUNGUS_ON_A_STICK)) {
if (player.getHand().getDefinition() == itemDefinition || player.getOffhand().getDefinition() == itemDefinition) {
vehicleComponent.tickBoost(); vehicleComponent.tickBoost();
} }
} }

View File

@@ -54,7 +54,7 @@ public class CowEntity extends TemperatureVariantAnimal {
@NonNull @NonNull
@Override @Override
protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { 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); return super.testMobInteraction(hand, itemInHand);
} }
@@ -64,7 +64,7 @@ public class CowEntity extends TemperatureVariantAnimal {
@NonNull @NonNull
@Override @Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { 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); 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.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector2f; import org.cloudburstmc.math.vector.Vector2f;
import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.entity.type.Tickable; import org.geysermc.geyser.entity.type.Tickable;
@@ -116,8 +115,7 @@ public class PigEntity extends TemperatureVariantAnimal implements Tickable, Cli
vehicleComponent.tickBoost(); vehicleComponent.tickBoost();
} }
} else { // getHand() for session player seems to always return air } else { // getHand() for session player seems to always return air
ItemDefinition itemDefinition = session.getItemMappings().getStoredItems().carrotOnAStick().getBedrockDefinition(); if (player.isHolding(Items.CARROT_ON_A_STICK)) {
if (player.getHand().getDefinition() == itemDefinition || player.getOffhand().getDefinition() == itemDefinition) {
vehicleComponent.tickBoost(); vehicleComponent.tickBoost();
} }
} }

View File

@@ -26,19 +26,24 @@
package org.geysermc.geyser.entity.type.living.animal.farm; package org.geysermc.geyser.entity.type.living.animal.farm;
import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket;
import org.geysermc.geyser.entity.EntityDefinition; 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.AnimalEntity;
import org.geysermc.geyser.entity.type.living.animal.VariantHolder; 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.GeyserSession;
import org.geysermc.geyser.session.cache.RegistryCache; import org.geysermc.geyser.session.cache.RegistryCache;
import java.util.Locale;
import java.util.UUID; import java.util.UUID;
public abstract class TemperatureVariantAnimal extends AnimalEntity implements VariantHolder<TemperatureVariantAnimal.BuiltInVariant> { 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 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, 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); 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 @Override
public void setBedrockVariant(BuiltInVariant variant) { public void setBedrockVariant(BuiltInVariant variant) {
propertyManager.add(VanillaEntityProperties.CLIMATE_VARIANT_ID, variant.toBedrock()); TEMPERATE_VARIANT_PROPERTY.apply(propertyManager, variant);
updateBedrockEntityProperties(); updateBedrockEntityProperties();
} }
public enum BuiltInVariant implements VariantHolder.BuiltIn { public enum BuiltInVariant implements VariantHolder.BuiltIn {
COLD,
TEMPERATE, TEMPERATE,
WARM; WARM,
COLD;
public String toBedrock() {
return name().toLowerCase(Locale.ROOT);
}
} }
} }

View File

@@ -165,7 +165,7 @@ public class AbstractHorseEntity extends AnimalEntity {
return InteractiveTag.ATTACH_CHEST; 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 // Will open the inventory to be saddled
return InteractiveTag.OPEN_CONTAINER; 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) // 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 // Will open the inventory to be saddled
return InteractionResult.SUCCESS; return InteractionResult.SUCCESS;
} }
@@ -245,6 +245,7 @@ public class AbstractHorseEntity extends AnimalEntity {
} }
protected boolean additionalTestForInventoryOpen(@NonNull GeyserItemStack itemInHand) { 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"); return itemInHand.asItem().javaIdentifier().endsWith("_horse_armor");
} }
@@ -260,7 +261,7 @@ public class AbstractHorseEntity extends AnimalEntity {
} else if (!passengers.isEmpty()) { } else if (!passengers.isEmpty()) {
return testHorseInteraction(hand, itemInHand); return testHorseInteraction(hand, itemInHand);
} else { } else {
if (Items.SADDLE == itemInHand.asItem()) { if (itemInHand.is(Items.SADDLE)) {
return InteractiveTag.OPEN_CONTAINER; return InteractiveTag.OPEN_CONTAINER;
} }

View File

@@ -54,7 +54,7 @@ public class ChestedHorseEntity extends AbstractHorseEntity {
@Override @Override
protected boolean testForChest(@NonNull GeyserItemStack itemInHand) { protected boolean testForChest(@NonNull GeyserItemStack itemInHand) {
return itemInHand.asItem() == Items.CHEST && !getFlag(EntityFlag.CHESTED); return itemInHand.is(Items.CHEST) && !getFlag(EntityFlag.CHESTED);
} }
@Override @Override

View File

@@ -52,21 +52,21 @@ public class ParrotEntity extends TameableEntity {
return null; return null;
} }
private boolean isTameFood(Item item) { private boolean isTameFood(GeyserItemStack item) {
return session.getTagCache().is(ItemTag.PARROT_FOOD, item); return item.is(session, ItemTag.PARROT_FOOD);
} }
private boolean isPoisonousFood(Item item) { private boolean isPoisonousFood(GeyserItemStack item) {
return session.getTagCache().is(ItemTag.PARROT_POISONOUS_FOOD, item); return item.is(session, ItemTag.PARROT_POISONOUS_FOOD);
} }
@NonNull @NonNull
@Override @Override
protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
boolean tame = getFlag(EntityFlag.TAMED); boolean tame = getFlag(EntityFlag.TAMED);
if (!tame && isTameFood(itemInHand.asItem())) { if (!tame && isTameFood(itemInHand)) {
return InteractiveTag.FEED; return InteractiveTag.FEED;
} else if (isPoisonousFood(itemInHand.asItem())) { } else if (isPoisonousFood(itemInHand)) {
return InteractiveTag.FEED; return InteractiveTag.FEED;
} else if (onGround && tame && ownerBedrockId == session.getPlayerEntity().getGeyserId()) { } else if (onGround && tame && ownerBedrockId == session.getPlayerEntity().getGeyserId()) {
// Sitting/standing // Sitting/standing
@@ -79,9 +79,9 @@ public class ParrotEntity extends TameableEntity {
@Override @Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
boolean tame = getFlag(EntityFlag.TAMED); boolean tame = getFlag(EntityFlag.TAMED);
if (!tame && isTameFood(itemInHand.asItem())) { if (!tame && isTameFood(itemInHand)) {
return InteractionResult.SUCCESS; return InteractionResult.SUCCESS;
} else if (isPoisonousFood(itemInHand.asItem())) { } else if (isPoisonousFood(itemInHand)) {
return InteractionResult.SUCCESS; return InteractionResult.SUCCESS;
} else if (onGround && tame && ownerBedrockId == session.getPlayerEntity().getGeyserId()) { } else if (onGround && tame && ownerBedrockId == session.getPlayerEntity().getGeyserId()) {
// Sitting/standing // Sitting/standing

View File

@@ -30,10 +30,11 @@ import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket;
import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket; import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket;
import org.geysermc.geyser.entity.EntityDefinition; 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.entity.type.living.animal.VariantIntHolder;
import org.geysermc.geyser.impl.IdentifierImpl;
import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.item.enchantment.EnchantmentComponent; 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.InteractionResult;
import org.geysermc.geyser.util.InteractiveTag; import org.geysermc.geyser.util.InteractiveTag;
import org.geysermc.geyser.util.ItemUtils; 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.ByteEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode; 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 org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.UUID; import java.util.UUID;
public class WolfEntity extends TameableEntity implements VariantIntHolder { 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 byte collarColor = 14; // Red - default
private HolderSet repairableItems = null; private HolderSet repairableItems = null;
private boolean isCurseOfBinding = false; 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); 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 @Override
public void setTameableFlags(ByteEntityMetadata entityMetadata) { public void setTameableFlags(ByteEntityMetadata entityMetadata) {
super.setTameableFlags(entityMetadata); super.setTameableFlags(entityMetadata);
@@ -146,7 +158,7 @@ public class WolfEntity extends TameableEntity implements VariantIntHolder {
if (getFlag(EntityFlag.ANGRY)) { if (getFlag(EntityFlag.ANGRY)) {
return InteractiveTag.NONE; return InteractiveTag.NONE;
} }
if (itemInHand.asItem() == Items.BONE && !getFlag(EntityFlag.TAMED)) { if (itemInHand.is(Items.BONE) && !getFlag(EntityFlag.TAMED)) {
// Bone and untamed - can tame // Bone and untamed - can tame
return InteractiveTag.TAME; return InteractiveTag.TAME;
} }
@@ -159,17 +171,15 @@ public class WolfEntity extends TameableEntity implements VariantIntHolder {
return super.testMobInteraction(hand, itemInHand); 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; 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))) { && (!isCurseOfBinding || session.getGameMode().equals(GameMode.CREATIVE))) {
return InteractiveTag.REMOVE_WOLF_ARMOR; return InteractiveTag.REMOVE_WOLF_ARMOR;
} }
if (getFlag(EntityFlag.SITTING) && if (getFlag(EntityFlag.SITTING) && itemInHand.is(session, repairableItems) &&
session.getTagCache().isItem(repairableItems, itemInHand.asItem()) && !getItemInSlot(EquipmentSlot.BODY).isEmpty() && getItemInSlot(EquipmentSlot.BODY).isDamaged()) {
this.body.isValid() && this.body.getTag() != null &&
this.body.getTag().getInt("Damage") > 0) {
return InteractiveTag.REPAIR_WOLF_ARMOR; return InteractiveTag.REPAIR_WOLF_ARMOR;
} }
// Tamed and owned by player - can sit/stand // Tamed and owned by player - can sit/stand
@@ -182,7 +192,7 @@ public class WolfEntity extends TameableEntity implements VariantIntHolder {
@Override @Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
if (ownerBedrockId == session.getPlayerEntity().getGeyserId() || getFlag(EntityFlag.TAMED) 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 // Sitting toggle or feeding; not angry
return InteractionResult.CONSUME; return InteractionResult.CONSUME;
} else { } else {

View File

@@ -54,7 +54,7 @@ public class AbstractMerchantEntity extends AgeableEntity {
@NonNull @NonNull
@Override @Override
protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { 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())) { && (definition != EntityDefinitions.VILLAGER || !getFlag(EntityFlag.SLEEPING) && ((VillagerEntity) this).isCanTradeWith())) {
// An additional check we know cannot work // An additional check we know cannot work
if (!isBaby()) { if (!isBaby()) {
@@ -67,7 +67,7 @@ public class AbstractMerchantEntity extends AgeableEntity {
@NonNull @NonNull
@Override @Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { 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.VILLAGER || !getFlag(EntityFlag.SLEEPING))
&& (definition != EntityDefinitions.WANDERING_TRADER || !getFlag(EntityFlag.BABY))) { && (definition != EntityDefinitions.WANDERING_TRADER || !getFlag(EntityFlag.BABY))) {
// Trading time // Trading time

View File

@@ -26,10 +26,10 @@
package org.geysermc.geyser.entity.type.living.monster; package org.geysermc.geyser.entity.type.living.monster;
import org.cloudburstmc.math.vector.Vector3f; 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.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; 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); dirtyMetadata.put(EntityDataTypes.TARGET_EID, ((xd & 4) == 4) ? geyserId : 0);
if ((xd & 4) == 4) { if ((xd & 4) == 4) {
ItemDefinition bow = session.getItemMappings().getStoredItems().bow().getBedrockDefinition(); setFlag(EntityFlag.FACING_TARGET_TO_RANGE_ATTACK, isHolding(Items.BOW));
setFlag(EntityFlag.FACING_TARGET_TO_RANGE_ATTACK, this.hand.getDefinition() == bow || this.offhand.getDefinition() == bow);
} else { } else {
setFlag(EntityFlag.FACING_TARGET_TO_RANGE_ATTACK, false); setFlag(EntityFlag.FACING_TARGET_TO_RANGE_ATTACK, false);
} }

View File

@@ -53,7 +53,7 @@ public class BoggedEntity extends AbstractSkeletonEntity {
@Override @Override
protected @NonNull InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { 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 InteractiveTag.SHEAR;
} }
return super.testMobInteraction(hand, itemInHand); return super.testMobInteraction(hand, itemInHand);
@@ -61,7 +61,7 @@ public class BoggedEntity extends AbstractSkeletonEntity {
@Override @Override
protected @NonNull InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { 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 InteractionResult.SUCCESS;
} }
return super.mobInteract(hand, itemInHand); 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.nbt.NbtMap;
import org.cloudburstmc.protocol.bedrock.data.LevelEvent; import org.cloudburstmc.protocol.bedrock.data.LevelEvent;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket;
import org.cloudburstmc.protocol.bedrock.packet.LevelEventGenericPacket; import org.cloudburstmc.protocol.bedrock.packet.LevelEventGenericPacket;
import org.geysermc.geyser.entity.EntityDefinition; 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.geyser.session.GeyserSession;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; 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.MetadataType;
@@ -41,8 +43,23 @@ import java.util.Optional;
import java.util.UUID; import java.util.UUID;
public class CreakingEntity extends MonsterEntity { 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; private Vector3i homePosition;
@@ -56,33 +73,21 @@ public class CreakingEntity extends MonsterEntity {
setFlag(EntityFlag.FIRE_IMMUNE, true); 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) { public void setCanMove(EntityMetadata<Boolean,? extends MetadataType<Boolean>> booleanEntityMetadata) {
setFlag(EntityFlag.BODY_ROTATION_BLOCKED, !booleanEntityMetadata.getValue()); 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(); updateBedrockEntityProperties();
} }
public void setActive(EntityMetadata<Boolean,? extends MetadataType<Boolean>> booleanEntityMetadata) { public void setActive(EntityMetadata<Boolean,? extends MetadataType<Boolean>> booleanEntityMetadata) {
if (!booleanEntityMetadata.getValue()) { if (!booleanEntityMetadata.getValue()) {
propertyManager.add(CREAKING_STATE, "neutral"); STATE_PROPERTY.apply(propertyManager, CreakingState.NEUTRAL);
} }
} }
public void setIsTearingDown(EntityMetadata<Boolean,? extends MetadataType<Boolean>> booleanEntityMetadata) { public void setIsTearingDown(EntityMetadata<Boolean,? extends MetadataType<Boolean>> booleanEntityMetadata) {
if (booleanEntityMetadata.getValue()) { if (booleanEntityMetadata.getValue()) {
propertyManager.add(CREAKING_STATE, "crumbling"); STATE_PROPERTY.apply(propertyManager, CreakingState.CRUMBLING);
updateBedrockEntityProperties(); updateBedrockEntityProperties();
} }
} }
@@ -115,4 +120,12 @@ public class CreakingEntity extends MonsterEntity {
session.sendUpstreamPacket(levelEventGenericPacket); 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 @NonNull
@Override @Override
protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { 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; return InteractiveTag.IGNITE_CREEPER;
} else { } else {
return super.testMobInteraction(hand, itemInHand); return super.testMobInteraction(hand, itemInHand);
@@ -76,7 +76,7 @@ public class CreeperEntity extends MonsterEntity {
@NonNull @NonNull
@Override @Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { 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 // Ignite creeper - as of 1.19.3
session.playSoundEvent(SoundEvent.IGNITE, position); session.playSoundEvent(SoundEvent.IGNITE, position);
return InteractionResult.SUCCESS; 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.entity.EntityDefinition;
import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.session.cache.tags.ItemTag;
import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractionResult;
import org.geysermc.geyser.util.InteractiveTag; 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.metadata.type.BooleanEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes;
import java.util.UUID; import java.util.UUID;
@@ -71,17 +71,16 @@ public class PiglinEntity extends BasePiglinEntity {
@Override @Override
public void setHand(GeyserItemStack stack) { public void setHand(GeyserItemStack stack) {
ItemMapping crossbow = session.getItemMappings().getStoredItems().crossbow(); boolean toCrossbow = stack != null && stack.is(Items.CROSSBOW);
boolean toCrossbow = stack != null && stack.asItem() == crossbow.getJavaItem();
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.BLOCK, session.getBlockMappings().getDefinition(toCrossbow ? 0 : 1));
dirtyMetadata.put(EntityDataTypes.CHARGE_AMOUNT, (byte) 0); dirtyMetadata.put(EntityDataTypes.CHARGE_AMOUNT, (byte) 0);
setFlag(EntityFlag.CHARGED, false); setFlag(EntityFlag.CHARGED, false);
setFlag(EntityFlag.USING_ITEM, false); setFlag(EntityFlag.USING_ITEM, false);
updateBedrockMetadata(); updateBedrockMetadata();
if (this.hand.isValid()) { if (!getMainHandItem().isEmpty()) {
MobEquipmentPacket mobEquipmentPacket = new MobEquipmentPacket(); MobEquipmentPacket mobEquipmentPacket = new MobEquipmentPacket();
mobEquipmentPacket.setRuntimeEntityId(geyserId); mobEquipmentPacket.setRuntimeEntityId(geyserId);
mobEquipmentPacket.setContainerId(ContainerId.INVENTORY); mobEquipmentPacket.setContainerId(ContainerId.INVENTORY);
@@ -96,11 +95,11 @@ public class PiglinEntity extends BasePiglinEntity {
} }
@Override @Override
public void updateMainHand(GeyserSession session) { public void updateMainHand() {
super.updateMainHand(session); super.updateMainHand();
if (this.hand.getDefinition() == session.getItemMappings().getStoredItems().crossbow().getBedrockDefinition()) { if (getMainHandItem().is(Items.CROSSBOW)) {
if (this.hand.getTag() != null && this.hand.getTag().containsKey("chargedItem")) { if (getMainHandItem().getComponent(DataComponentTypes.CHARGED_PROJECTILES) != null) {
dirtyMetadata.put(EntityDataTypes.CHARGE_AMOUNT, Byte.MAX_VALUE); dirtyMetadata.put(EntityDataTypes.CHARGE_AMOUNT, Byte.MAX_VALUE);
setFlag(EntityFlag.CHARGING, false); setFlag(EntityFlag.CHARGING, false);
setFlag(EntityFlag.CHARGED, true); setFlag(EntityFlag.CHARGED, true);
@@ -116,12 +115,12 @@ public class PiglinEntity extends BasePiglinEntity {
} }
@Override @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 // 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.updateBedrockMetadata();
super.updateOffHand(session); super.updateOffHand();
} }
@NonNull @NonNull
@@ -147,6 +146,6 @@ public class PiglinEntity extends BasePiglinEntity {
} }
private boolean canGiveGoldTo(@NonNull GeyserItemStack itemInHand) { 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 @NonNull
@Override @Override
protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
if (itemInHand.asItem() == Items.GOLDEN_APPLE) { if (itemInHand.is(Items.GOLDEN_APPLE)) {
return InteractiveTag.CURE; return InteractiveTag.CURE;
} else { } else {
return super.testMobInteraction(hand, itemInHand); return super.testMobInteraction(hand, itemInHand);
@@ -80,7 +80,7 @@ public class ZombieVillagerEntity extends ZombieEntity {
@NonNull @NonNull
@Override @Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { 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 // The client doesn't know if the entity has weakness as that's not usually sent over the network
return InteractionResult.CONSUME; return InteractionResult.CONSUME;
} else { } 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.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; 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.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.geyser.session.GeyserSession;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; 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; import java.util.UUID;
@@ -49,33 +50,32 @@ public class PillagerEntity extends AbstractIllagerEntity {
} }
@Override @Override
public void updateMainHand(GeyserSession session) { public void updateMainHand() {
updateCrossbow(); updateCrossbow();
super.updateMainHand(session); super.updateMainHand();
} }
@Override @Override
public void updateOffHand(GeyserSession session) { public void updateOffHand() {
updateCrossbow(); 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 * Check for a crossbow in either the mainhand or offhand. If one exists, indicate that the pillager should be posing
*/ */
protected void updateCrossbow() { protected void updateCrossbow() {
ItemMapping crossbow = session.getItemMappings().getStoredItems().crossbow(); GeyserItemStack activeCrossbow = null;
ItemData activeCrossbow = null; if (getMainHandItem().is(Items.CROSSBOW)) {
if (this.hand.getDefinition() == crossbow.getBedrockDefinition()) { activeCrossbow = getMainHandItem();
activeCrossbow = this.hand; } else if (getOffHandItem().is(Items.CROSSBOW)) {
} else if (this.offhand.getDefinition() == crossbow.getBedrockDefinition()) { activeCrossbow = getOffHandItem();
activeCrossbow = this.offhand;
} }
if (activeCrossbow != null) { 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); dirtyMetadata.put(EntityDataTypes.CHARGE_AMOUNT, Byte.MAX_VALUE);
setFlag(EntityFlag.CHARGING, false); setFlag(EntityFlag.CHARGING, false);
setFlag(EntityFlag.CHARGED, true); 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.Getter;
import lombok.Setter; import lombok.Setter;
import net.kyori.adventure.text.Component;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector3f; 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.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityLinkData; 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.SetEntityLinkPacket;
import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket; import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket;
import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity; import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity;
import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.entity.attribute.GeyserAttributeType; import org.geysermc.geyser.entity.attribute.GeyserAttributeType;
import org.geysermc.geyser.entity.type.Entity; 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.entity.type.living.animal.tameable.ParrotEntity;
import org.geysermc.geyser.level.block.Blocks;
import org.geysermc.geyser.session.GeyserSession; 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.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 org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatEntityMetadata;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.OptionalInt;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Getter @Setter @Getter @Setter
public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity { public class PlayerEntity extends AvatarEntity 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;
/** /**
* Saves the parrot currently on the player's left shoulder; otherwise null * 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, 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) { 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); super(session, entityId, geyserId, uuid, EntityDefinitions.PLAYER, position, motion, yaw, pitch, headYaw, username);
this.username = username;
this.nametag = username;
this.texturesProperty = texturesProperty; this.texturesProperty = texturesProperty;
} }
@Override @Override
protected void initializeMetadata() { protected void initializeMetadata() {
super.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 :/ // 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. // The nametag does disappear properly when the player is invisible though.
dirtyMetadata.put(EntityDataTypes.NAMETAG_ALWAYS_SHOW, (byte) 1); dirtyMetadata.put(EntityDataTypes.NAMETAG_ALWAYS_SHOW, (byte) 1);
dirtyMetadata.apply(addPlayerPacket.getMetadata());
setFlagsDirty(false);
valid = true;
session.sendUpstreamPacket(addPlayerPacket);
} }
@Override @Override
@@ -164,12 +88,6 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
this.nametag = username; this.nametag = username;
this.equipment.clear(); 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() { public void resetMetadata() {
@@ -179,8 +97,8 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
this.initializeMetadata(); this.initializeMetadata();
// Explicitly reset all metadata not handled by initializeMetadata // Explicitly reset all metadata not handled by initializeMetadata
setParrot(null, true); setParrot(OptionalInt.empty(), true);
setParrot(null, false); setParrot(OptionalInt.empty(), false);
} }
public void sendPlayer() { public void sendPlayer() {
@@ -192,30 +110,7 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
@Override @Override
public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) { public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) {
setPosition(position); super.moveAbsolute(position, yaw, pitch, headYaw, isOnGround, teleported);
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);
}
if (leftParrot != null) { if (leftParrot != null) {
leftParrot.moveAbsolute(position, yaw, pitch, headYaw, true, teleported); leftParrot.moveAbsolute(position, yaw, pitch, headYaw, true, teleported);
} }
@@ -226,34 +121,7 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
@Override @Override
public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) { public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) {
setYaw(yaw); super.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, isOnGround);
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);
if (leftParrot != null) { if (leftParrot != null) {
leftParrot.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, true); 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) { public void setAbsorptionHearts(FloatEntityMetadata entityMetadata) {
// Extra hearts - is not metadata but an attribute on Bedrock // Extra hearts - is not metadata but an attribute on Bedrock
UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket(); UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket();
@@ -308,19 +140,11 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
session.sendUpstreamPacket(attributesPacket); session.sendUpstreamPacket(attributesPacket);
} }
public void setSkinVisibility(ByteEntityMetadata entityMetadata) { public void setLeftParrot(EntityMetadata<OptionalInt, ?> 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) {
setParrot(entityMetadata.getValue(), true); setParrot(entityMetadata.getValue(), true);
} }
public void setRightParrot(EntityMetadata<NbtMap, ?> entityMetadata) { public void setRightParrot(EntityMetadata<OptionalInt, ?> entityMetadata) {
setParrot(entityMetadata.getValue(), false); 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 * Sets the parrot occupying the shoulder. Bedrock Edition requires a full entity whereas Java Edition just
* spawns it from the NBT data provided * spawns it from the NBT data provided
*/ */
protected void setParrot(NbtMap tag, boolean isLeft) { protected void setParrot(OptionalInt variant, boolean isLeft) {
if (tag != null && !tag.isEmpty()) { if (variant.isPresent()) {
if ((isLeft && leftParrot != null) || (!isLeft && rightParrot != null)) { if ((isLeft && leftParrot != null) || (!isLeft && rightParrot != null)) {
// No need to update a parrot's data when it already exists // No need to update a parrot's data when it already exists
return; 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(), ParrotEntity parrot = new ParrotEntity(session, 0, session.getEntityCache().getNextEntityId().incrementAndGet(),
null, EntityDefinitions.PARROT, position, motion, getYaw(), getPitch(), getHeadYaw()); null, EntityDefinitions.PARROT, position, motion, getYaw(), getPitch(), getHeadYaw());
parrot.spawnEntity(); 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 // Different position whether the parrot is left or right
float offset = isLeft ? 0.4f : -0.4f; float offset = isLeft ? 0.4f : -0.4f;
parrot.getDirtyMetadata().put(EntityDataTypes.SEAT_OFFSET, Vector3f.from(offset, -0.22, -0.1)); 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 @Override
public String teamIdentifier() { public String teamIdentifier() {
return username; return username;
} }
// TODO test mannequins
@Override @Override
protected void setNametag(@Nullable String nametag, boolean fromDisplayName) { protected void setNametag(@Nullable String nametag, boolean fromDisplayName) {
// when fromDisplayName, LivingEntity will call scoreboard code. After that // when fromDisplayName, LivingEntity will call scoreboard code. After that
@@ -394,85 +209,8 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
super.setNametag(nametag, fromDisplayName); super.setNametag(nametag, fromDisplayName);
} }
@Override public void setUsername(String username) {
public void setDisplayNameVisible(BooleanEntityMetadata entityMetadata) { this.username = username;
// 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);
} }
/** /**

View File

@@ -322,9 +322,9 @@ public class SessionPlayerEntity extends PlayerEntity {
protected boolean hasShield(boolean offhand) { protected boolean hasShield(boolean offhand) {
// Must be overridden to point to the player's inventory cache // Must be overridden to point to the player's inventory cache
if (offhand) { if (offhand) {
return session.getPlayerInventory().getOffhand().asItem() == Items.SHIELD; return session.getPlayerInventory().getOffhand().is(Items.SHIELD);
} else { } 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(); Vector3i pos = getPosition().down(EntityDefinitions.PLAYER.offset()).toInt();
BlockState state = session.getGeyser().getWorldManager().blockAt(session, pos); 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; 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 // 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; 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.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.cloudburstmc.protocol.bedrock.packet.AddPlayerPacket; 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.property.Properties;
import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.level.block.type.BlockState;
import org.geysermc.geyser.level.block.type.WallSkullBlock; 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.GeyserSession;
import org.geysermc.geyser.session.cache.SkullCache; import org.geysermc.geyser.session.cache.SkullCache;
import org.geysermc.geyser.skin.SkullSkinManager; import org.geysermc.geyser.skin.SkullSkinManager;
import org.geysermc.geyser.translator.item.ItemTranslator;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; 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 * A wrapper to handle skulls more effectively - skulls have to be treated as entities since there are no
* custom player skulls in Bedrock. * custom player skulls in Bedrock.
*/ */
public class SkullPlayerEntity extends PlayerEntity { public class SkullPlayerEntity extends AvatarEntity {
@Getter @Getter
private UUID skullUUID; private UUID skullUUID;
@@ -59,7 +61,7 @@ public class SkullPlayerEntity extends PlayerEntity {
private Vector3i skullPosition; private Vector3i skullPosition;
public SkullPlayerEntity(GeyserSession session, long geyserId) { 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 @Override
@@ -73,51 +75,20 @@ public class SkullPlayerEntity extends PlayerEntity {
setFlag(EntityFlag.INVISIBLE, true); // Until the skin is loaded 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) { public void updateSkull(SkullCache.Skull skull) {
skullPosition = skull.getPosition(); 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 // Make skull invisible as we change skins
setFlag(EntityFlag.INVISIBLE, true); setFlag(EntityFlag.INVISIBLE, true);
updateBedrockMetadata(); updateBedrockMetadata();
skullUUID = skull.getUuid(); skullUUID = skull.getUuid();
setTexturesProperty(skull.getTexturesProperty()); setSkin(skull.getTexturesProperty(), false, () -> session.scheduleInEventLoop(() -> {
SkullSkinManager.requestAndHandleSkin(this, session, (skin -> session.scheduleInEventLoop(() -> {
// Delay to minimize split-second "player" pop-in // Delay to minimize split-second "player" pop-in
setFlag(EntityFlag.INVISIBLE, false); setFlag(EntityFlag.INVISIBLE, false);
updateBedrockMetadata(); updateBedrockMetadata();
}, 250, TimeUnit.MILLISECONDS))); }, 250, TimeUnit.MILLISECONDS));
} else { } else {
// Just a rotation/position change // Just a rotation/position change
setFlag(EntityFlag.INVISIBLE, false); setFlag(EntityFlag.INVISIBLE, false);

View File

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

View File

@@ -40,11 +40,11 @@ import java.nio.file.Path;
public class GeyserExtensionClassLoader extends URLClassLoader { public class GeyserExtensionClassLoader extends URLClassLoader {
private final GeyserExtensionLoader loader; private final GeyserExtensionLoader loader;
private final ExtensionDescription description; private final GeyserExtensionDescription description;
private final Object2ObjectMap<String, Class<?>> classes = new Object2ObjectOpenHashMap<>(); private final Object2ObjectMap<String, Class<?>> classes = new Object2ObjectOpenHashMap<>();
private boolean warnedForExternalClassAccess; 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); super(new URL[] { path.toUri().toURL() }, parent);
this.loader = loader; this.loader = loader;
this.description = description; 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 // 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 // This is used for classes that are not in the extension, but are in other extensions
if (checkGlobal) { 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. " + 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!"); "This can change at any time and break the extension, additionally to potentially causing unexpected behaviour!");
warnedForExternalClassAccess = true; warnedForExternalClassAccess = true;

View File

@@ -47,7 +47,8 @@ public record GeyserExtensionDescription(@NonNull String id,
int majorApiVersion, int majorApiVersion,
int minorApiVersion, int minorApiVersion,
@NonNull String version, @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())); 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); 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 @NonNull
@@ -116,5 +122,17 @@ public record GeyserExtensionDescription(@NonNull String id,
String version; String version;
String author; String author;
List<String> authors; 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.Path;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -167,6 +172,8 @@ public class GeyserExtensionLoader extends ExtensionLoader {
Map<String, Path> extensions = new LinkedHashMap<>(); Map<String, Path> extensions = new LinkedHashMap<>();
Map<String, GeyserExtensionContainer> loadedExtensions = 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"); Path updateDirectory = extensionsDirectory.resolve("update");
if (Files.isDirectory(updateDirectory)) { 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) -> { this.processExtensionsFolder(extensionsDirectory, (path, description) -> {
String name = description.name();
String id = description.id(); 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) { if (extensions.containsKey(id) || extensionManager.extension(id) != null) {
logger.warning(GeyserLocale.getLocaleStringLog("geyser.extensions.load.duplicate", name, path.toString())); logger.warning(GeyserLocale.getLocaleStringLog("geyser.extensions.load.duplicate", name, path.toString()));
return; return;
@@ -222,20 +345,22 @@ public class GeyserExtensionLoader extends ExtensionLoader {
} }
} }
try {
GeyserExtensionContainer container = this.loadExtension(path, description); GeyserExtensionContainer container = this.loadExtension(path, description);
extensions.put(id, path); extensions.put(id, path);
loadedExtensions.put(id, container); loadedExtensions.put(id, container);
}, (path, e) -> { } catch (Throwable e) {
logger.error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_with_name", path.getFileName(), path.toAbsolutePath()), 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()) { for (GeyserExtensionContainer container : loadedExtensions.values()) {
this.extensionContainers.put(container.extension(), container); this.extensionContainers.put(container.extension(), container);
this.register(container.extension(), extensionManager); this.register(container.extension(), extensionManager);
} }
} catch (IOException ex) { } 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.registry.type.ItemMapping;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.BundleCache; 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.geyser.translator.item.ItemTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; 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.DataComponentType;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; 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.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.EmptySlotDisplay;
import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.ItemSlotDisplay; import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.ItemSlotDisplay;
import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.ItemStackSlotDisplay; import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.ItemStackSlotDisplay;
@@ -115,6 +118,22 @@ public class GeyserItemStack {
return isEmpty() ? 0 : amount; 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. * Returns all components of this item - base and additional components sent over the network.
* These are NOT modifiable! To add components, use {@link #getOrCreateComponents()}. * These are NOT modifiable! To add components, use {@link #getOrCreateComponents()}.
@@ -248,6 +267,10 @@ public class GeyserItemStack {
return new ItemStackSlotDisplay(this.getItemStack()); return new ItemStackSlotDisplay(this.getItemStack());
} }
public int getMaxStackSize() {
return getComponentElseGet(DataComponentTypes.MAX_STACK_SIZE, () -> 1);
}
public int getMaxDamage() { public int getMaxDamage() {
return getComponentElseGet(DataComponentTypes.MAX_DAMAGE, () -> 0); 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; return getComponent(DataComponentTypes.MAX_DAMAGE) != null && getComponent(DataComponentTypes.UNBREAKABLE) == null && getComponent(DataComponentTypes.DAMAGE) != null;
} }
public boolean isDamaged() {
return isDamageable() && getDamage() > 0;
}
public Item asItem() { public Item asItem() {
if (isEmpty()) { if (isEmpty()) {
return Items.AIR; return Items.AIR;

View File

@@ -148,7 +148,7 @@ public abstract class Inventory {
items[slot] = newItem; items[slot] = newItem;
// Lodestone caching // Lodestone caching
if (newItem.asItem() == Items.COMPASS) { if (newItem.is(Items.COMPASS)) {
var tracker = newItem.getComponent(DataComponentTypes.LODESTONE_TRACKER); var tracker = newItem.getComponent(DataComponentTypes.LODESTONE_TRACKER);
if (tracker != null) { if (tracker != null) {
session.getLodestoneCache().cacheInventoryItem(newItem, tracker); 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 * @return If the player is holding the item in either hand
*/ */
public boolean isHolding(@NonNull Item item) { 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) { 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) { public void setItemInHand(@NonNull GeyserItemStack item) {
if (36 + heldItemSlot > this.size) { if (36 + heldItemSlot > this.size) {
GeyserImpl.getInstance().getLogger().debug("Held item slot was larger than expected!"); GeyserImpl.getInstance().getLogger().debug("Held item slot was larger than expected!");

View File

@@ -45,7 +45,7 @@ public class StonecutterContainer extends Container {
@Override @Override
public void setItem(int slot, @NonNull GeyserItemStack newItem, GeyserSession session) { 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 // The pressed stonecutter button output resets whenever the input item changes
this.stonecutterButton = -1; this.stonecutterButton = -1;
} }

View File

@@ -40,37 +40,25 @@ import java.util.Map;
@Getter @Getter
@Accessors(fluent = true) @Accessors(fluent = true)
public class StoredItemMappings { public class StoredItemMappings {
private final ItemMapping banner;
private final ItemMapping barrier; private final ItemMapping barrier;
private final ItemMapping bow;
private final ItemMapping carrotOnAStick;
private final ItemMapping compass; private final ItemMapping compass;
private final ItemMapping crossbow;
private final ItemMapping glassBottle; private final ItemMapping glassBottle;
private final ItemMapping milkBucket; private final ItemMapping milkBucket;
private final ItemMapping powderSnowBucket; private final ItemMapping powderSnowBucket;
private final ItemMapping shield;
private final ItemMapping totem; private final ItemMapping totem;
private final ItemMapping upgradeTemplate; private final ItemMapping upgradeTemplate;
private final ItemMapping warpedFungusOnAStick;
private final ItemMapping wheat; private final ItemMapping wheat;
private final ItemMapping writableBook; private final ItemMapping writableBook;
private final ItemMapping writtenBook; private final ItemMapping writtenBook;
public StoredItemMappings(Map<Item, ItemMapping> itemMappings) { 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.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.compass = load(itemMappings, Items.COMPASS);
this.crossbow = load(itemMappings, Items.CROSSBOW);
this.glassBottle = load(itemMappings, Items.GLASS_BOTTLE); this.glassBottle = load(itemMappings, Items.GLASS_BOTTLE);
this.milkBucket = load(itemMappings, Items.MILK_BUCKET); this.milkBucket = load(itemMappings, Items.MILK_BUCKET);
this.powderSnowBucket = load(itemMappings, Items.POWDER_SNOW_BUCKET); this.powderSnowBucket = load(itemMappings, Items.POWDER_SNOW_BUCKET);
this.shield = load(itemMappings, Items.SHIELD);
this.totem = load(itemMappings, Items.TOTEM_OF_UNDYING); this.totem = load(itemMappings, Items.TOTEM_OF_UNDYING);
this.upgradeTemplate = load(itemMappings, Items.NETHERITE_UPGRADE_SMITHING_TEMPLATE); 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.wheat = load(itemMappings, Items.WHEAT);
this.writableBook = load(itemMappings, Items.WRITABLE_BOOK); this.writableBook = load(itemMappings, Items.WRITABLE_BOOK);
this.writtenBook = load(itemMappings, Items.WRITTEN_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()); return new TrimMaterial(key, color, trimItem.getBedrockIdentifier());
} }
// TODO this is WRONG. this changed. FIXME in 1.21.5
public static TrimPattern readTrimPattern(RegistryEntryContext context) { public static TrimPattern readTrimPattern(RegistryEntryContext context) {
String key = context.id().asMinimalString(); String key = context.id().asMinimalString();
String itemIdentifier = context.data().getString("template_item"); // Not ideal, Java edition also gives us a translatable description... Bedrock wants the template item
ItemMapping itemMapping = context.session().getItemMappings().getMapping(itemIdentifier); String identifier = context.id().asString() + "_armor_trim_smithing_template";
ItemMapping itemMapping = context.session().getItemMappings().getMapping(identifier);
if (itemMapping == null) { if (itemMapping == null) {
// This should never happen so not sure what to do here. // This should never happen so not sure what to do here.
itemMapping = ItemMapping.AIR; itemMapping = ItemMapping.AIR;

View File

@@ -223,7 +223,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater {
if (!material.isEmpty()) { if (!material.isEmpty()) {
totalRepairCost += getRepairCost(material); totalRepairCost += getRepairCost(material);
if (isCombining(input, material)) { if (isCombining(input, material)) {
if (hasDurability(input) && input.getJavaId() == material.getJavaId()) { if (hasDurability(input) && input.isSameItem(material)) {
cost += calcMergeRepairCost(input, material); cost += calcMergeRepairCost(input, material);
} }
@@ -312,7 +312,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater {
for (Object2IntMap.Entry<Enchantment> entry : getEnchantments(session, material).object2IntEntrySet()) { for (Object2IntMap.Entry<Enchantment> entry : getEnchantments(session, material).object2IntEntrySet()) {
Enchantment enchantment = entry.getKey(); 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); List<Enchantment> incompatibleEnchantments = enchantment.exclusiveSet().resolve(session);
for (Enchantment incompatible : incompatibleEnchantments) { for (Enchantment incompatible : incompatibleEnchantments) {
@@ -388,11 +388,11 @@ public class AnvilInventoryUpdater extends InventoryUpdater {
} }
private boolean isEnchantedBook(GeyserItemStack itemStack) { private boolean isEnchantedBook(GeyserItemStack itemStack) {
return itemStack.asItem() == Items.ENCHANTED_BOOK; return itemStack.is(Items.ENCHANTED_BOOK);
} }
private boolean isCombining(GeyserItemStack input, GeyserItemStack material) { 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) { private boolean isRepairing(GeyserItemStack input, GeyserItemStack material, GeyserSession session) {
@@ -401,7 +401,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater {
return false; return false;
} }
return session.getTagCache().isItem(repairable, material.asItem()); return material.is(session, repairable);
} }
private boolean isRenaming(GeyserSession session, AnvilContainer anvilContainer, boolean bedrock) { 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_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 SMOOTH_STONE = register(new BlockItem(builder(), Blocks.SMOOTH_STONE));
public static final Item BRICKS = register(new BlockItem(builder(), Blocks.BRICKS)); 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 BOOKSHELF = register(new BlockItem(builder(), Blocks.BOOKSHELF));
public static final Item CHISELED_BOOKSHELF = register(new BlockItem(builder(), Blocks.CHISELED_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)); 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 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 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 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 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_STONE = register(new BlockItem(builder(), Blocks.INFESTED_STONE));
public static final Item INFESTED_COBBLESTONE = register(new BlockItem(builder(), Blocks.INFESTED_COBBLESTONE)); 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 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 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 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 GLASS_PANE = register(new BlockItem(builder(), Blocks.GLASS_PANE));
public static final Item MELON = register(new BlockItem(builder(), Blocks.MELON)); public static final Item MELON = register(new BlockItem(builder(), Blocks.MELON));
public static final Item VINE = register(new BlockItem(builder(), Blocks.VINE)); 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 TARGET = register(new BlockItem(builder(), Blocks.TARGET));
public static final Item LEVER = register(new BlockItem(builder(), Blocks.LEVER)); 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 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 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 SCULK_SENSOR = register(new BlockItem(builder(), Blocks.SCULK_SENSOR));
public static final Item CALIBRATED_SCULK_SENSOR = register(new BlockItem(builder(), Blocks.CALIBRATED_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_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_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 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_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_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))); 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_CHESTPLATE = register(new DyeableArmorItem("leather_chestplate", builder()));
public static final Item LEATHER_LEGGINGS = register(new DyeableArmorItem("leather_leggings", 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 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_HELMET = register(new ArmorItem("chainmail_helmet", builder()));
public static final Item CHAINMAIL_CHESTPLATE = register(new ArmorItem("chainmail_chestplate", 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())); 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 BLAZE_POWDER = register(new Item("blaze_powder", builder()));
public static final Item MAGMA_CREAM = register(new Item("magma_cream", 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 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 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 GLISTERING_MELON_SLICE = register(new Item("glistering_melon_slice", builder()));
public static final Item ARMADILLO_SPAWN_EGG = register(new SpawnEggItem("armadillo_spawn_egg", 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 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 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 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 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 CREEPER_SPAWN_EGG = register(new SpawnEggItem("creeper_spawn_egg", builder()));
public static final Item DOLPHIN_SPAWN_EGG = register(new SpawnEggItem("dolphin_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_FOOT = register(new Item("rabbit_foot", builder()));
public static final Item RABBIT_HIDE = register(new Item("rabbit_hide", 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 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 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 GOLDEN_HORSE_ARMOR = register(new Item("golden_horse_armor", builder()));
public static final Item DIAMOND_HORSE_ARMOR = register(new Item("diamond_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 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 SHULKER_SHELL = register(new Item("shulker_shell", builder()));
public static final Item IRON_NUGGET = register(new Item("iron_nugget", 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 KNOWLEDGE_BOOK = register(new Item("knowledge_book", builder()));
public static final Item DEBUG_STICK = register(new Item("debug_stick", 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())); 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 BELL = register(new BlockItem(builder(), Blocks.BELL));
public static final Item LANTERN = register(new BlockItem(builder(), Blocks.LANTERN)); 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 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 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 GLOW_BERRIES = register(new BlockItem("glow_berries", builder(), Blocks.CAVE_VINES));
public static final Item CAMPFIRE = register(new BlockItem(builder(), Blocks.CAMPFIRE)); 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_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_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 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_SPAWNER = register(new BlockItem(builder(), Blocks.TRIAL_SPAWNER));
public static final Item TRIAL_KEY = register(new Item("trial_key", builder())); 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())); 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.PotionContents;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.ToolData; 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.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.Unit;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.UseCooldown; import org.geysermc.mcprotocollib.protocol.data.game.item.component.UseCooldown;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.Weapon; 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.TRIM, RegistryHasher.ARMOR_TRIM);
register(DataComponentTypes.DEBUG_STICK_STATE, MinecraftHasher.NBT_MAP); 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.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.INSTRUMENT, RegistryHasher.INSTRUMENT_COMPONENT);
register(DataComponentTypes.PROVIDES_TRIM_MATERIAL, RegistryHasher.PROVIDES_TRIM_MATERIAL); 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) .optional("flight_duration", MinecraftHasher.BYTE, fireworks -> (byte) fireworks.getFlightDuration(), (byte) 0)
.optionalList("explosions", RegistryHasher.FIREWORK_EXPLOSION, Fireworks::getExplosions)); .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.NOTE_BLOCK_SOUND, MinecraftHasher.KEY);
register(DataComponentTypes.BANNER_PATTERNS, RegistryHasher.BANNER_PATTERN_LAYER.list()); register(DataComponentTypes.BANNER_PATTERNS, RegistryHasher.BANNER_PATTERN_LAYER.list());
register(DataComponentTypes.BASE_COLOR, MinecraftHasher.DYE_COLOR); register(DataComponentTypes.BASE_COLOR, MinecraftHasher.DYE_COLOR);

View File

@@ -26,6 +26,8 @@
package org.geysermc.geyser.item.hashing; package org.geysermc.geyser.item.hashing;
import com.google.common.hash.HashCode; import com.google.common.hash.HashCode;
import org.cloudburstmc.nbt.NbtList;
import org.cloudburstmc.nbt.NbtMap;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -67,6 +69,14 @@ public class MapHasher<Type> {
return this; 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. * Adds a constant {@link Value} to the map.
* *
@@ -82,6 +92,25 @@ public class MapHasher<Type> {
return accept(key, hasher.hash(value, encoder)); 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. * 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.auth.GameProfile;
import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; 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.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.Filterable;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.ItemAttributeModifiers; import org.geysermc.mcprotocollib.protocol.data.game.item.component.ItemAttributeModifiers;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.Unit; 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.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Function; import java.util.function.Function;
@@ -131,10 +133,15 @@ public interface MinecraftHasher<Type> {
.accept("value", STRING, GameProfile.Property::getValue) .accept("value", STRING, GameProfile.Property::getValue)
.optionalNullable("signature", STRING, GameProfile.Property::getSignature)); .optionalNullable("signature", STRING, GameProfile.Property::getSignature));
MinecraftHasher<GameProfile> GAME_PROFILE = mapBuilder(builder -> builder MinecraftHasher<ResolvableProfile> RESOLVABLE_PROFILE = mapBuilder(builder -> builder
.optionalNullable("name", STRING, GameProfile::getName) .optionalNullable("name", STRING, resolvableProfile -> resolvableProfile.getProfile().getName())
.optionalNullable("id", UUID, GameProfile::getId) .optionalNullable("id", UUID, resolvableProfile -> resolvableProfile.getProfile().getId())
.optionalList("properties", GAME_PROFILE_PROPERTY, GameProfile::getProperties)); .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); 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 com.google.common.hash.HashCode;
import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Key;
import org.cloudburstmc.nbt.NbtMap;
import org.geysermc.geyser.inventory.item.Potion; import org.geysermc.geyser.inventory.item.Potion;
import org.geysermc.geyser.item.hashing.data.ConsumeEffectType; import org.geysermc.geyser.item.hashing.data.ConsumeEffectType;
import org.geysermc.geyser.item.hashing.data.FireworkExplosionShape; 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.SuspiciousStewEffect;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.ToolData; 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.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.BuiltinSound;
import org.geysermc.mcprotocollib.protocol.data.game.level.sound.CustomSound; import org.geysermc.mcprotocollib.protocol.data.game.level.sound.CustomSound;
import org.geysermc.mcprotocollib.protocol.data.game.level.sound.Sound; 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()); 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<?> ENCHANTMENT = registry(JavaRegistries.ENCHANTMENT);
RegistryHasher<?> ATTRIBUTE = enumIdRegistry(AttributeType.Builtin.values(), AttributeType::getIdentifier); 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)); .optional("has_twinkle", BOOL, Fireworks.FireworkExplosion::isHasTwinkle, false));
MinecraftHasher<BeehiveOccupant> BEEHIVE_OCCUPANT = MinecraftHasher.mapBuilder(builder -> builder 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("ticks_in_hive", INT, BeehiveOccupant::getTicksInHive)
.accept("min_ticks_in_hive", INT, BeehiveOccupant::getMinTicksInHive)); .accept("min_ticks_in_hive", INT, BeehiveOccupant::getMinTicksInHive));

View File

@@ -25,11 +25,22 @@
package org.geysermc.geyser.item.hashing.data; package org.geysermc.geyser.item.hashing.data;
import java.util.Locale;
// Ordered and named by Java ID // Ordered and named by Java ID
public enum FireworkExplosionShape { public enum FireworkExplosionShape {
SMALL_BALL, SMALL_BALL,
LARGE_BALL, LARGE_BALL,
STAR, STAR,
CREEPER, 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()) { if (chargedProjectiles != null && !chargedProjectiles.isEmpty()) {
ItemStack javaProjectile = chargedProjectiles.get(0); ItemStack javaProjectile = chargedProjectiles.get(0);
ItemMapping projectileMapping = session.getItemMappings().getMapping(javaProjectile.getId());
ItemData itemData = ItemTranslator.translateToBedrock(session, javaProjectile); ItemData itemData = ItemTranslator.translateToBedrock(session, javaProjectile);
NbtMapBuilder newProjectile = BedrockItemBuilder.createItemNbt(itemData);
NbtMapBuilder newProjectile = BedrockItemBuilder.createItemNbt(projectileMapping, itemData.getCount(), itemData.getDamage());
builder.putCompound("chargedItem", newProjectile.build()); 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.registry.type.ItemMappings;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.registry.JavaRegistries; 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.ChatColor;
import org.geysermc.geyser.text.MinecraftLocale; import org.geysermc.geyser.text.MinecraftLocale;
import org.geysermc.geyser.translator.item.BedrockItemBuilder; 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.DataComponentType;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; 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.DataComponents;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.ItemEnchantments; import org.geysermc.mcprotocollib.protocol.data.game.item.component.ItemEnchantments;
import org.jetbrains.annotations.UnmodifiableView; import org.jetbrains.annotations.UnmodifiableView;
@@ -101,6 +103,14 @@ public class Item {
return baseComponents.getOrDefault(DataComponentTypes.MAX_STACK_SIZE, 1); 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. * Returns an unmodifiable {@link DataComponents} view containing known data components.
* Optionally, additional components can be provided to replace (or add to) * 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