diff --git a/README.md b/README.md index c6962f65e..50ef6c7ab 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ The ultimate goal of this project is to allow Minecraft: Bedrock Edition users t Special thanks to the DragonProxy project for being a trailblazer in protocol translation and for all the team members who have joined us here! ## Supported Versions -Geyser is currently supporting Minecraft Bedrock 1.21.40 - 1.21.61 and Minecraft Java 1.21.4. For more information, please see [here](https://geysermc.org/wiki/geyser/supported-versions/). +Geyser is currently supporting Minecraft Bedrock 1.21.50 - 1.21.72 and Minecraft Java 1.21.5. For more information, please see [here](https://geysermc.org/wiki/geyser/supported-versions/). ## Setting Up Take a look [here](https://geysermc.org/wiki/geyser/setup/) for how to set up Geyser. diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionJoinEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionJoinEvent.java index ab2088c00..0228214a7 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionJoinEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionJoinEvent.java @@ -31,6 +31,7 @@ import org.geysermc.geyser.api.event.connection.ConnectionEvent; /** * Called when Geyser session connected to a Java remote server and is in a play-ready state. + * @since 2.1.1 */ public final class SessionJoinEvent extends ConnectionEvent { public SessionJoinEvent(@NonNull GeyserConnection connection) { diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java index c2f1cd427..edce76f6a 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java @@ -26,15 +26,20 @@ package org.geysermc.geyser.api.event.bedrock; 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.event.connection.ConnectionEvent; import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.exception.ResourcePackException; +import org.geysermc.geyser.api.pack.option.ResourcePackOption; +import java.util.Collection; import java.util.List; import java.util.UUID; /** - * Called when Geyser initializes a session for a new Bedrock client and is in the process of sending resource packs. + * Called when Geyser initializes a session for a new Bedrock client and is in the process of sending {@link ResourcePack}'s. + * @since 2.1.1 */ public abstract class SessionLoadResourcePacksEvent extends ConnectionEvent { public SessionLoadResourcePacksEvent(@NonNull GeyserConnection connection) { @@ -42,26 +47,70 @@ public abstract class SessionLoadResourcePacksEvent extends ConnectionEvent { } /** - * Gets an unmodifiable list of {@link ResourcePack}s that will be sent to the client. + * Gets the {@link ResourcePack}'s that will be sent to this {@link GeyserConnection}. + * To remove packs, use {@link #unregister(UUID)}, as the list returned + * by this method is unmodifiable. * - * @return an unmodifiable list of resource packs that will be sent to the client. + * @return an unmodifiable list of {@link ResourcePack}'s + * @since 2.1.1 */ public abstract @NonNull List resourcePacks(); /** - * Registers a {@link ResourcePack} to be sent to the client. - * - * @param resourcePack a resource pack that will be sent to the client. - * @return true if the resource pack was added successfully, - * or false if already present + * @deprecated Use {{@link #register(ResourcePack, ResourcePackOption[])}} instead */ - public abstract boolean register(@NonNull ResourcePack resourcePack); + @Deprecated + public abstract boolean register(@NonNull ResourcePack pack); /** - * Unregisters a resource pack from being sent to the client. + * Registers a {@link ResourcePack} to be sent to the client, optionally alongside + * specific {@link ResourcePackOption}'s specifying how it will be applied by the client. * - * @param uuid the UUID of the resource pack - * @return true whether the resource pack was removed from the list of resource packs. + * @param pack the {@link ResourcePack} that will be sent to the client + * @param options {@link ResourcePackOption}'s that specify how the client loads the pack + * @throws ResourcePackException if an issue occurred during pack registration + * @since 2.6.2 + */ + public abstract void register(@NonNull ResourcePack pack, @Nullable ResourcePackOption... options); + + /** + * Sets {@link ResourcePackOption}'s for a {@link ResourcePack}. + * This method can also be used to override options for resource packs already registered in the + * {@link org.geysermc.geyser.api.event.lifecycle.GeyserDefineResourcePacksEvent}. + * + * @param uuid the uuid of the resource pack to register the options for + * @param options the {@link ResourcePackOption}'s to register for the resource pack + * @throws ResourcePackException if an issue occurred during {@link ResourcePackOption} registration + * @since 2.6.2 + */ + public abstract void registerOptions(@NonNull UUID uuid, @NonNull ResourcePackOption... options); + + /** + * Returns a collection of {@link ResourcePackOption}'s for a registered {@link ResourcePack}. + * The collection returned here is not modifiable. + * + * @param uuid the {@link ResourcePack} for which the options are set + * @return a collection of {@link ResourcePackOption}'s + * @throws ResourcePackException if the pack was not registered + * @since 2.6.2 + */ + public abstract Collection> options(@NonNull UUID uuid); + + /** + * Returns the current {@link ResourcePackOption}, or null, for a given {@link ResourcePackOption.Type}. + * + * @param uuid the {@link ResourcePack} for which to query this option type + * @param type the {@link ResourcePackOption.Type} of the option to query + * @throws ResourcePackException if the queried option is invalid or not present on the resource pack + * @since 2.6.2 + */ + public abstract @Nullable ResourcePackOption option(@NonNull UUID uuid, ResourcePackOption.@NonNull Type type); + + /** + * Unregisters a {@link ResourcePack} from the list of packs sent to this {@link GeyserConnection}. + * + * @param uuid the UUID of the {@link ResourcePack} to be removed + * @since 2.1.1 */ public abstract boolean unregister(@NonNull UUID uuid); } diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java new file mode 100644 index 000000000..4128f1c47 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2019-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.pack.ResourcePack; +import org.geysermc.geyser.api.pack.exception.ResourcePackException; +import org.geysermc.geyser.api.pack.option.ResourcePackOption; + +import java.util.Collection; +import java.util.List; +import java.util.UUID; + +/** + * Called when {@link ResourcePack}'s are loaded within Geyser. + * @since 2.6.2 + */ +public abstract class GeyserDefineResourcePacksEvent implements Event { + + /** + * Gets the {@link ResourcePack}'s that will be sent to connecting Bedrock clients. + * To remove packs, use {@link #unregister(UUID)}, as the list returned + * by this method is unmodifiable. + * + * @return an unmodifiable list of {@link ResourcePack}'s + * @since 2.6.2 + */ + public abstract @NonNull List resourcePacks(); + + /** + * Registers a {@link ResourcePack} to be sent to the client, optionally alongside + * {@link ResourcePackOption}'s specifying how it will be applied on clients. + * + * @param pack a resource pack that will be sent to the client + * @param options {@link ResourcePackOption}'s that specify how clients load the pack + * @throws ResourcePackException if an issue occurred during pack registration + * @since 2.6.2 + */ + public abstract void register(@NonNull ResourcePack pack, @Nullable ResourcePackOption... options); + + /** + * Sets {@link ResourcePackOption}'s for a {@link ResourcePack}. + * + * @param uuid the uuid of the resource pack to register the options for + * @param options the {@link ResourcePackOption}'s to register for the resource pack + * @throws ResourcePackException if an issue occurred during {@link ResourcePackOption} registration + * @since 2.6.2 + */ + public abstract void registerOptions(@NonNull UUID uuid, @NonNull ResourcePackOption... options); + + /** + * Returns a collection of {@link ResourcePackOption}'s for a registered {@link ResourcePack}. + * The collection returned here is not modifiable. + * + * @param uuid the uuid of the {@link ResourcePack} for which the options are set + * @return a collection of {@link ResourcePackOption}'s + * @throws ResourcePackException if the pack was not registered + * @since 2.6.2 + */ + public abstract Collection> options(@NonNull UUID uuid); + + /** + * Returns the current option, or null, for a given {@link ResourcePackOption.Type}. + * + * @param uuid the {@link ResourcePack} for which to query this option type + * @param type the {@link ResourcePackOption.Type} of the option to query + * @throws ResourcePackException if the queried option is invalid or not present on the resource pack + * @since 2.6.2 + */ + public abstract @Nullable ResourcePackOption option(@NonNull UUID uuid, ResourcePackOption.@NonNull Type type); + + /** + * Unregisters a {@link ResourcePack} from the list of packs sent to connecting Bedrock clients. + * + * @param uuid the UUID of the {@link ResourcePack} to be removed + * @since 2.6.2 + */ + public abstract void unregister(@NonNull UUID uuid); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.java index e9b283ecb..047f9df57 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.java @@ -32,9 +32,8 @@ import java.nio.file.Path; import java.util.List; /** - * Called when resource packs are loaded within Geyser. - * - * @param resourcePacks a mutable list of the currently listed resource packs + * @deprecated Use the {@link GeyserDefineResourcePacksEvent} instead. */ +@Deprecated public record GeyserLoadResourcePacksEvent(@NonNull List resourcePacks) implements Event { } diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserPreReloadEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserPreReloadEvent.java index 16d5058da..9147ad4b8 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserPreReloadEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserPreReloadEvent.java @@ -33,7 +33,7 @@ import org.geysermc.geyser.api.extension.ExtensionManager; /** * Called when Geyser is about to reload. Primarily aimed at extensions, so they can decide on their own what to reload. - * After this event is fired, some lifecycle events can be fired again - such as the {@link GeyserLoadResourcePacksEvent}. + * After this event is fired, some lifecycle events can be fired again - such as the {@link GeyserDefineResourcePacksEvent}. * * @param extensionManager the extension manager * @param eventBus the event bus diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java b/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java index 884129fa3..b6626aa6a 100644 --- a/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java +++ b/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java @@ -35,6 +35,7 @@ import java.nio.file.Path; /** * Represents a pack codec that can be used * to provide resource packs to clients. + * @since 2.1.1 */ public abstract class PackCodec { @@ -42,6 +43,7 @@ public abstract class PackCodec { * Gets the sha256 hash of the resource pack. * * @return the hash of the resource pack + * @since 2.1.1 */ public abstract byte @NonNull [] sha256(); @@ -49,34 +51,66 @@ public abstract class PackCodec { * Gets the resource pack size. * * @return the resource pack file size + * @since 2.1.1 */ public abstract long size(); /** - * Serializes the given resource pack into a byte buffer. + * @deprecated use {@link #serialize()} instead. + */ + @Deprecated + @NonNull + public SeekableByteChannel serialize(@NonNull ResourcePack resourcePack) throws IOException { + return serialize(); + }; + + /** + * Serializes the given codec into a byte buffer. * - * @param resourcePack the resource pack to serialize * @return the serialized resource pack + * @since 2.6.2 */ @NonNull - public abstract SeekableByteChannel serialize(@NonNull ResourcePack resourcePack) throws IOException; + public abstract SeekableByteChannel serialize() throws IOException; /** * Creates a new resource pack from this codec. * * @return the new resource pack + * @since 2.1.1 */ @NonNull protected abstract ResourcePack create(); + /** + * Creates a new resource pack builder from this codec. + * + * @return the new resource pack builder + * @since 2.6.2 + */ + protected abstract ResourcePack.@NonNull Builder createBuilder(); + /** * Creates a new pack provider from the given path. * * @param path the path to create the pack provider from * @return the new pack provider + * @since 2.1.1 */ @NonNull public static PackCodec path(@NonNull Path path) { return GeyserApi.api().provider(PathPackCodec.class, path); } + + /** + * Creates a new pack provider from the given url. + * + * @param url the url to create the pack provider from + * @return the new pack provider + * @since 2.6.2 + */ + @NonNull + public static PackCodec url(@NonNull String url) { + return GeyserApi.api().provider(UrlPackCodec.class, url); + } } diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/PathPackCodec.java b/api/src/main/java/org/geysermc/geyser/api/pack/PathPackCodec.java index a3770451a..d6d668fb2 100644 --- a/api/src/main/java/org/geysermc/geyser/api/pack/PathPackCodec.java +++ b/api/src/main/java/org/geysermc/geyser/api/pack/PathPackCodec.java @@ -32,6 +32,7 @@ import java.nio.file.Path; /** * Represents a pack codec that creates a resource * pack from a path on the filesystem. + * @since 2.1.1 */ public abstract class PathPackCodec extends PackCodec { @@ -39,7 +40,8 @@ public abstract class PathPackCodec extends PackCodec { * Gets the path of the resource pack. * * @return the path of the resource pack + * @since 2.1.1 */ @NonNull public abstract Path path(); -} \ No newline at end of file +} diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePack.java b/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePack.java index de1beaf65..19b579f26 100644 --- a/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePack.java +++ b/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePack.java @@ -26,12 +26,17 @@ package org.geysermc.geyser.api.pack; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.common.returnsreceiver.qual.This; +import org.geysermc.geyser.api.GeyserApi; + +import java.util.UUID; /** * Represents a resource pack sent to Bedrock clients *

* This representation of a resource pack only contains what * Geyser requires to send it to the client. + * @since 2.1.1 */ public interface ResourcePack { @@ -39,6 +44,7 @@ public interface ResourcePack { * The {@link PackCodec codec} for this pack. * * @return the codec for this pack + * @since 2.1.1 */ @NonNull PackCodec codec(); @@ -47,6 +53,7 @@ public interface ResourcePack { * Gets the resource pack manifest. * * @return the resource pack manifest + * @since 2.1.1 */ @NonNull ResourcePackManifest manifest(); @@ -55,18 +62,83 @@ public interface ResourcePack { * Gets the content key of the resource pack. Lack of a content key is represented by an empty String. * * @return the content key of the resource pack + * @since 2.1.1 */ @NonNull String contentKey(); + /** + * Shortcut for getting the UUID from the {@link ResourcePackManifest}. + * + * @return the resource pack uuid + * @since 2.6.2 + */ + @NonNull + default UUID uuid() { + return manifest().header().uuid(); + } + /** * Creates a resource pack with the given {@link PackCodec}. * * @param codec the pack codec * @return the resource pack + * @since 2.1.1 */ @NonNull static ResourcePack create(@NonNull PackCodec codec) { return codec.create(); } + + /** + * Returns a {@link Builder} for a resource pack. + * It can be used to set a content key. + * + * @param codec the {@link PackCodec} to base the builder on + * @return a {@link Builder} to build a resource pack + * @since 2.6.2 + */ + static Builder builder(@NonNull PackCodec codec) { + return GeyserApi.api().provider(Builder.class, codec); + } + + /** + * A builder for a resource pack. It allows providing a content key manually. + * @since 2.6.2 + */ + interface Builder { + + /** + * @return the {@link ResourcePackManifest} of this resource pack + * @since 2.6.2 + */ + ResourcePackManifest manifest(); + + /** + * @return the {@link PackCodec} of this resource pack + * @since 2.6.2 + */ + PackCodec codec(); + + /** + * @return the current content key, or an empty string if not set + * @since 2.6.2 + */ + String contentKey(); + + /** + * Sets a content key for this resource pack. + * + * @param contentKey the content key + * @return this builder + * @since 2.6.2 + */ + @This Builder contentKey(@NonNull String contentKey); + + /** + * @return the resource pack + * @since 2.6.2 + */ + ResourcePack build(); + } } diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePackManifest.java b/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePackManifest.java index c9ccdd6c5..737682e22 100644 --- a/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePackManifest.java +++ b/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePackManifest.java @@ -26,55 +26,99 @@ package org.geysermc.geyser.api.pack; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.event.bedrock.SessionLoadResourcePacksEvent; +import org.geysermc.geyser.api.event.lifecycle.GeyserDefineResourcePacksEvent; +import org.geysermc.geyser.api.pack.option.SubpackOption; import java.util.Collection; import java.util.UUID; /** - * Represents a resource pack manifest. + * Represents a Bedrock edition resource pack manifest (manifest.json). + * All resource packs are required to have such a file as it identifies the resource pack. + * See + * Microsoft's docs for more info. + * @since 2.1.1 */ public interface ResourcePackManifest { /** * Gets the format version of the resource pack. + *

+ * "1" is used for skin packs, + * "2" is used for resource and behavior packs, and world templates. * * @return the format version + * @since 2.1.1 */ int formatVersion(); /** - * Gets the header of the resource pack. + * Gets the {@link Header} of the resource pack. * - * @return the header + * @return the {@link Header} + * @since 2.1.1 */ @NonNull Header header(); /** - * Gets the modules of the resource pack. + * Gets the {@link Module}'s of the resource pack. * - * @return the modules + * @return a collection of modules + * @since 2.1.1 */ @NonNull Collection modules(); /** - * Gets the dependencies of the resource pack. + * Gets the {@link Dependency}'s of the resource pack. * - * @return the dependencies + * @return a collection of dependencies + * @since 2.6.2 */ @NonNull Collection dependencies(); + /** + * Gets the {@link Subpack}'s of the resource pack. + * See Microsoft's docs on subpacks + * for more information. + * + * @return a collection of subpacks + * @since 2.6.2 + */ + @NonNull + Collection subpacks(); + + /** + * Gets the {@link Setting}'s of the resource pack. + * These are shown to Bedrock client's in the resource pack settings menu (see here) + * to inform users about what the resource pack and sub-packs include. + * + * @return a collection of settings + * @since 2.6.2 + */ + @NonNull + Collection settings(); + /** * Represents the header of a resource pack. + * It contains the main information about the resource pack, such as + * the name, description, or uuid. + * See + * Microsoft's docs for further details on headers. + * @since 2.1.1 */ interface Header { /** - * Gets the UUID of the resource pack. + * Gets the UUID of the resource pack. It is a unique identifier that differentiates this resource pack from any other resource pack. + * Bedrock clients will cache resource packs, and download resource packs when the uuid is new (or the version changes). * * @return the UUID + * @since 2.1.1 */ @NonNull UUID uuid(); @@ -83,6 +127,7 @@ public interface ResourcePackManifest { * Gets the version of the resource pack. * * @return the version + * @since 2.1.1 */ @NonNull Version version(); @@ -91,6 +136,7 @@ public interface ResourcePackManifest { * Gets the name of the resource pack. * * @return the name + * @since 2.1.1 */ @NonNull String name(); @@ -99,6 +145,7 @@ public interface ResourcePackManifest { * Gets the description of the resource pack. * * @return the description + * @since 2.1.1 */ @NonNull String description(); @@ -107,6 +154,7 @@ public interface ResourcePackManifest { * Gets the minimum supported Minecraft version of the resource pack. * * @return the minimum supported Minecraft version + * @since 2.1.1 */ @NonNull Version minimumSupportedMinecraftVersion(); @@ -114,21 +162,29 @@ public interface ResourcePackManifest { /** * Represents a module of a resource pack. + * It contains information about the content type that is + * offered by this resource pack. + * See + * Microsoft's docs for further details on modules. + * @since 2.1.1 */ interface Module { /** * Gets the UUID of the module. + * This should usually be different from the UUID in the {@link Header}. * * @return the UUID + * @since 2.1.1 */ @NonNull UUID uuid(); /** - * Gets the version of the module. + * Gets the {@link Version} of the module. * - * @return the version + * @return the {@link Version} + * @since 2.1.1 */ @NonNull Version version(); @@ -137,6 +193,7 @@ public interface ResourcePackManifest { * Gets the type of the module. * * @return the type + * @since 2.1.1 */ @NonNull String type(); @@ -145,6 +202,7 @@ public interface ResourcePackManifest { * Gets the description of the module. * * @return the description + * @since 2.1.1 */ @NonNull String description(); @@ -152,28 +210,102 @@ public interface ResourcePackManifest { /** * Represents a dependency of a resource pack. + * These are references to other resource packs that must be + * present in order for this resource pack to apply. + * See + * Microsoft's docs for further details on dependencies. + * @since 2.1.1 */ interface Dependency { /** - * Gets the UUID of the dependency. + * Gets the UUID of the resource pack dependency. * * @return the uuid + * @since 2.1.1 */ @NonNull UUID uuid(); /** - * Gets the version of the dependency. + * Gets the {@link Version} of the dependency. * - * @return the version + * @return the {@link Version} + * @since 2.1.1 */ @NonNull Version version(); } + /** + * Represents a subpack of a resource pack. These are often used for "variants" of the resource pack, + * such as lesser details, or additional features either to be determined by player's taste or adapted to the player device's performance. + * See Micoroft's docs for more information. + */ + interface Subpack { + + /** + * Gets the folder name where this sub-pack is placed in. + * + * @return the folder name + * @since 2.6.2 + */ + @NonNull + String folderName(); + + /** + * Gets the name of this subpack. Required for each subpack to be valid. + * To make a Bedrock client load any subpack, register the resource pack + * in the {@link SessionLoadResourcePacksEvent} or {@link GeyserDefineResourcePacksEvent} and specify a + * {@link SubpackOption} with the name of the subpack to load. + * + * @return the subpack name + * @since 2.6.2 + */ + @NonNull + String name(); + + /** + * Gets the memory tier of this Subpack, representing how much RAM a device must have to run it. + * Each memory tier requires 0.25 GB of RAM. For example, a memory tier of 0 is no requirement, + * and a memory tier of 4 requires 1GB of RAM. + * + * @return the memory tier + * @since 2.6.2 + */ + @Nullable + Float memoryTier(); + } + + /** + * Represents a setting that is shown client-side that describe what a pack does. + * Multiple setting entries are shown in separate paragraphs. + * @since 2.6.2 + */ + interface Setting { + + /** + * The type of the setting. Usually just "label". + * + * @return the type + * @since 2.6.2 + */ + @NonNull + String type(); + + /** + * The text shown for the setting. + * + * @return the text content + * @since 2.6.2 + */ + @NonNull + String text(); + } + /** * Represents a version of a resource pack. + * @since 2.1.1 */ interface Version { @@ -181,6 +313,7 @@ public interface ResourcePackManifest { * Gets the major version. * * @return the major version + * @since 2.1.1 */ int major(); @@ -188,6 +321,7 @@ public interface ResourcePackManifest { * Gets the minor version. * * @return the minor version + * @since 2.1.1 */ int minor(); @@ -195,6 +329,7 @@ public interface ResourcePackManifest { * Gets the patch version. * * @return the patch version + * @since 2.1.1 */ int patch(); @@ -202,6 +337,7 @@ public interface ResourcePackManifest { * Gets the version formatted as a String. * * @return the version string + * @since 2.1.1 */ @NonNull String toString(); } diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/UrlPackCodec.java b/api/src/main/java/org/geysermc/geyser/api/pack/UrlPackCodec.java new file mode 100644 index 000000000..e729e3fbb --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/pack/UrlPackCodec.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.pack; + +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Represents a pack codec that creates a resource + * pack from a URL. + *

+ * Due to Bedrock limitations, the URL must: + *

    + *
  • be a direct download link to a .zip or .mcpack resource pack
  • + *
  • use the application type `application/zip` and set a correct content length
  • + *
+ * @since 2.6.2 + */ +public abstract class UrlPackCodec extends PackCodec { + + /** + * Gets the URL to the resource pack location. + * + * @return the URL of the resource pack + * @since 2.6.2 + */ + @NonNull + public abstract String url(); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/exception/ResourcePackException.java b/api/src/main/java/org/geysermc/geyser/api/pack/exception/ResourcePackException.java new file mode 100644 index 000000000..66b3924d5 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/pack/exception/ResourcePackException.java @@ -0,0 +1,97 @@ +/* + * 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.pack.exception; + +import java.io.Serial; + +/** + * Used to indicate an exception that occurred while handling resource pack registration, + * or during resource pack option validation. + * @since 2.6.2 + */ +public class ResourcePackException extends IllegalArgumentException { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * The {@link Cause} of this exception. + */ + private final Cause cause; + + /** + * @param cause the cause of this exception + * @since 2.6.2 + */ + public ResourcePackException(Cause cause) { + super(cause.message()); + this.cause = cause; + } + + /** + * @param cause the cause of this exception + * @param message an additional, more in-depth message about the issue. + * @since 2.6.2 + */ + public ResourcePackException(Cause cause, String message) { + super(message); + this.cause = cause; + } + + /** + * @return the cause of this exception + * @since 2.6.2 + */ + public Cause cause() { + return cause; + } + + /** + * Represents different causes with explanatory messages stating which issue occurred. + * @since 2.6.2 + */ + public enum Cause { + DUPLICATE("A resource pack with this UUID was already registered!"), + INVALID_PACK("This resource pack is not a valid Bedrock edition resource pack!"), + INVALID_PACK_OPTION("Attempted to register an invalid resource pack option!"), + PACK_NOT_FOUND("No resource pack was found!"), + UNKNOWN_IMPLEMENTATION("Use the resource pack codecs to create resource packs."); + + private final String message; + + /** + * @return the message of this cause + * @since 2.6.2 + */ + public String message() { + return message; + } + + Cause(String message) { + this.message = message; + } + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/option/PriorityOption.java b/api/src/main/java/org/geysermc/geyser/api/pack/option/PriorityOption.java new file mode 100644 index 000000000..f1f91e9a9 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/pack/option/PriorityOption.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 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.api.pack.option; + +import org.geysermc.geyser.api.GeyserApi; + +/** + * Allows specifying a pack priority that decides the order on how packs are sent to the client. + * If two resource packs modify the same texture - for example if one removes the pumpkin overlay and + * the other is just making it translucent, one of the packs will override the other. + * Specifically, the pack with the higher priority will override the pack changes of the lower priority. + * @since 2.6.2 + */ +public interface PriorityOption extends ResourcePackOption { + + PriorityOption HIGHEST = PriorityOption.priority(100); + PriorityOption HIGH = PriorityOption.priority(50); + PriorityOption NORMAL = PriorityOption.priority(0); + PriorityOption LOW = PriorityOption.priority(-50); + PriorityOption LOWEST = PriorityOption.priority(-100); + + /** + * Constructs a priority option based on a value between 0 and 10. + * The higher the number, the higher will this pack appear in the resource pack stack. + * + * @param priority an integer that is above 0, but smaller than 10 + * @return the priority option + * @since 2.6.2 + */ + static PriorityOption priority(int priority) { + if (priority < -100 || priority > 100) { + throw new IllegalArgumentException("Priority must be between 0 and 10 inclusive!"); + } + return GeyserApi.api().provider(PriorityOption.class, priority); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/option/ResourcePackOption.java b/api/src/main/java/org/geysermc/geyser/api/pack/option/ResourcePackOption.java new file mode 100644 index 000000000..a86ad88db --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/pack/option/ResourcePackOption.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 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.api.pack.option; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.exception.ResourcePackException; + +/** + * Represents a resource pack option that can be used to specify how a resource + * pack is sent to Bedrock clients. + *

+ * Not all options can be applied to all resource packs. For example, you cannot specify + * a specific subpack to be loaded on resource packs that do not have subpacks. + * To see which limitations apply to specific resource pack options, check the javadocs + * or see the {@link #validate(ResourcePack)} method. + * @since 2.6.2 + */ +public interface ResourcePackOption { + + /** + * @return the option type + * @since 2.6.2 + */ + @NonNull Type type(); + + /** + * @return the value of the option + * @since 2.6.2 + */ + @NonNull T value(); + + /** + * Used to validate a specific options for a pack. + * Some options are not applicable to some packs. + * + * @param pack the resource pack to validate the option for + * @throws ResourcePackException with the {@link ResourcePackException.Cause#INVALID_PACK_OPTION} cause + * @since 2.6.2 + */ + void validate(@NonNull ResourcePack pack); + + /** + * Represents the different types of resource pack options. + * @since 2.6.2 + */ + enum Type { + SUBPACK, + PRIORITY, + FALLBACK + } + +} diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/option/SubpackOption.java b/api/src/main/java/org/geysermc/geyser/api/pack/option/SubpackOption.java new file mode 100644 index 000000000..a0462afa7 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/pack/option/SubpackOption.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 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.api.pack.option; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.api.pack.ResourcePackManifest; + +/** + * Can be used to specify which subpack from a resource pack a player should load. + * Available subpacks can be seen in a resource pack manifest {@link ResourcePackManifest#subpacks()}. + * @since 2.6.2 + */ +public interface SubpackOption extends ResourcePackOption { + + /** + * Creates a subpack option based on a {@link ResourcePackManifest.Subpack}. + * + * @param subpack the chosen subpack + * @return a subpack option specifying that subpack + * @since 2.6.2 + */ + static SubpackOption subpack(ResourcePackManifest.@NonNull Subpack subpack) { + return named(subpack.name()); + } + + /** + * Creates a subpack option based on a subpack name. + * + * @param subpackName the name of the subpack + * @return a subpack option specifying a subpack with that name + * @since 2.6.2 + */ + static SubpackOption named(@NonNull String subpackName) { + return GeyserApi.api().provider(SubpackOption.class, subpackName); + } + + /** + * Creates a subpack option with no subpack specified. + * + * @return a subpack option specifying no subpack + * @since 2.6.2 + */ + static SubpackOption empty() { + return GeyserApi.api().provider(SubpackOption.class, ""); + } + +} diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/option/UrlFallbackOption.java b/api/src/main/java/org/geysermc/geyser/api/pack/option/UrlFallbackOption.java new file mode 100644 index 000000000..9431c8cdd --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/pack/option/UrlFallbackOption.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 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.api.pack.option; + +import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.api.pack.PathPackCodec; +import org.geysermc.geyser.api.pack.UrlPackCodec; + +/** + * Can be used for resource packs created with the {@link UrlPackCodec}. + * When a Bedrock client is unable to download a resource pack from a URL, Geyser will, by default, + * serve the resource pack over raknet (as packs are served with the {@link PathPackCodec}). + * This option can be used to disable that behavior, and disconnect the player instead. + * By default, the {@link UrlFallbackOption#TRUE} option is set. + * @since 2.6.2 + */ +public interface UrlFallbackOption extends ResourcePackOption { + + UrlFallbackOption TRUE = fallback(true); + UrlFallbackOption FALSE = fallback(false); + + /** + * Whether to fall back to serving packs over the raknet connection + * + * @param fallback whether to fall back + * @return a UrlFallbackOption with the specified behavior + * @since 2.6.2 + */ + static UrlFallbackOption fallback(boolean fallback) { + return GeyserApi.api().provider(UrlFallbackOption.class, fallback); + } + +} diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeLogger.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeLogger.java index e8cf7ee39..e9b18acca 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeLogger.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeLogger.java @@ -75,4 +75,11 @@ public class GeyserBungeeLogger implements GeyserLogger { info(message); } } + + @Override + public void debug(String message, Object... arguments) { + if (debug) { + info(String.format(message, arguments)); + } + } } diff --git a/bootstrap/mod/fabric/src/main/resources/fabric.mod.json b/bootstrap/mod/fabric/src/main/resources/fabric.mod.json index a1c786c2f..c0056e5cf 100644 --- a/bootstrap/mod/fabric/src/main/resources/fabric.mod.json +++ b/bootstrap/mod/fabric/src/main/resources/fabric.mod.json @@ -25,6 +25,6 @@ "depends": { "fabricloader": ">=0.16.7", "fabric-api": "*", - "minecraft": ">=1.21.4" + "minecraft": ">=1.21.5" } } diff --git a/bootstrap/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/bootstrap/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml index 56b7d68e1..7958926bc 100644 --- a/bootstrap/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml +++ b/bootstrap/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml @@ -16,12 +16,12 @@ config = "geyser_neoforge.mixins.json" [[dependencies.geyser_neoforge]] modId="neoforge" type="required" - versionRange="[21.0.0-beta,)" + versionRange="[21.5.0-beta,)" ordering="NONE" side="BOTH" [[dependencies.geyser_neoforge]] modId="minecraft" type="required" - versionRange="[1.21,)" + versionRange="[1.21.5,)" ordering="NONE" - side="BOTH" \ No newline at end of file + side="BOTH" diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModLogger.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModLogger.java index 9260288d7..da66b32c3 100644 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModLogger.java +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModLogger.java @@ -84,6 +84,13 @@ public class GeyserModLogger implements GeyserLogger { } } + @Override + public void debug(String message, Object... arguments) { + if (debug) { + logger.info(String.format(message, arguments)); + } + } + @Override public void setDebug(boolean debug) { this.debug = debug; diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/BlockPlaceMixin.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/BlockPlaceMixin.java index 98620588e..a2d76497e 100644 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/BlockPlaceMixin.java +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/BlockPlaceMixin.java @@ -25,15 +25,13 @@ package org.geysermc.geyser.platform.mod.mixin.server; +import com.llamalad7.mixinextras.sugar.Local; import net.minecraft.core.BlockPos; import net.minecraft.world.InteractionResult; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.BlockItem; -import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.context.BlockPlaceContext; -import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.SoundType; import net.minecraft.world.level.block.state.BlockState; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.SoundEvent; @@ -44,13 +42,13 @@ import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -import org.spongepowered.asm.mixin.injection.callback.LocalCapture; @Mixin(BlockItem.class) public class BlockPlaceMixin { - @Inject(method = "place", locals = LocalCapture.CAPTURE_FAILSOFT, at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/Level;playSound(Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/core/BlockPos;Lnet/minecraft/sounds/SoundEvent;Lnet/minecraft/sounds/SoundSource;FF)V")) - private void geyser$hijackPlaySound(BlockPlaceContext blockPlaceContext, CallbackInfoReturnable cir, BlockPlaceContext blockPlaceContext2, BlockState blockState, BlockPos blockPos, Level level, Player player, ItemStack itemStack, BlockState blockState2, SoundType soundType) { + @Inject(method = "place", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/Level;playSound(Lnet/minecraft/world/entity/Entity;Lnet/minecraft/core/BlockPos;Lnet/minecraft/sounds/SoundEvent;Lnet/minecraft/sounds/SoundSource;FF)V")) + private void geyser$hijackPlaySound(BlockPlaceContext blockPlaceContext, CallbackInfoReturnable callbackInfoReturnable, + @Local BlockPos pos, @Local Player player, @Local(ordinal = 1) BlockState placedState) { if (player == null) { return; } @@ -61,16 +59,16 @@ public class BlockPlaceMixin { } Vector3f position = Vector3f.from( - blockPos.getX(), - blockPos.getY(), - blockPos.getZ() + pos.getX(), + pos.getY(), + pos.getZ() ); LevelSoundEventPacket placeBlockSoundPacket = new LevelSoundEventPacket(); placeBlockSoundPacket.setSound(SoundEvent.PLACE); placeBlockSoundPacket.setPosition(position); placeBlockSoundPacket.setBabySound(false); - placeBlockSoundPacket.setExtraData(session.getBlockMappings().getBedrockBlockId(Block.BLOCK_STATE_REGISTRY.getId(blockState2))); + placeBlockSoundPacket.setExtraData(session.getBlockMappings().getBedrockBlockId(Block.BLOCK_STATE_REGISTRY.getId(placedState))); placeBlockSoundPacket.setIdentifier(":"); session.sendUpstreamPacket(placeBlockSoundPacket); session.setLastBlockPlacePosition(null); diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperPingPassthrough.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperPingPassthrough.java index 60d5dee55..84a12b4ad 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperPingPassthrough.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperPingPassthrough.java @@ -37,6 +37,7 @@ import org.geysermc.geyser.ping.GeyserPingInfo; import org.geysermc.geyser.ping.IGeyserPingPassthrough; import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import java.net.InetSocketAddress; /** @@ -44,7 +45,10 @@ import java.net.InetSocketAddress; * applied. */ public final class GeyserPaperPingPassthrough implements IGeyserPingPassthrough { - private static final Constructor OLD_CONSTRUCTOR = ReflectedNames.getOldPaperPingConstructor(); + private static final Constructor EVENT_CONSTRUCTOR = ReflectedNames.paperServerListPingEventConstructor(); + // https://jd.papermc.io/paper/1.19.2/com/destroystokyo/paper/event/server/PaperServerListPingEvent.html + private static final boolean CHAT_PREVIEWS = EVENT_CONSTRUCTOR.getParameters()[2].getType() == boolean.class; + private static final Method MOTD_COMPONENT_GETTER = ReflectedNames.motdGetter(); private final GeyserSpigotLogger logger; @@ -57,18 +61,15 @@ public final class GeyserPaperPingPassthrough implements IGeyserPingPassthrough @Override public GeyserPingInfo getPingInformation(InetSocketAddress inetSocketAddress) { try { - // We'd rather *not* use deprecations here, but unfortunately any Adventure class would be relocated at - // runtime because we still have to shade in our own Adventure class. For now. PaperServerListPingEvent event; - if (OLD_CONSTRUCTOR != null) { - // 1.19, removed in 1.19.4 - event = OLD_CONSTRUCTOR.newInstance(new GeyserStatusClient(inetSocketAddress), - Bukkit.getMotd(), Bukkit.getOnlinePlayers().size(), - Bukkit.getMaxPlayers(), Bukkit.getVersion(), GameProtocol.getJavaProtocolVersion(), null); + if (CHAT_PREVIEWS) { + event = EVENT_CONSTRUCTOR.newInstance(new GeyserStatusClient(inetSocketAddress), + MOTD_COMPONENT_GETTER.invoke(null), false, Bukkit.getOnlinePlayers().size(), + Bukkit.getMaxPlayers(), Bukkit.getVersion(), GameProtocol.getJavaProtocolVersion(), null); } else { - event = new PaperServerListPingEvent(new GeyserStatusClient(inetSocketAddress), - Bukkit.getMotd(), Bukkit.getOnlinePlayers().size(), - Bukkit.getMaxPlayers(), Bukkit.getVersion(), GameProtocol.getJavaProtocolVersion(), null); + event = EVENT_CONSTRUCTOR.newInstance(new GeyserStatusClient(inetSocketAddress), + MOTD_COMPONENT_GETTER.invoke(null), Bukkit.getOnlinePlayers().size(), + Bukkit.getMaxPlayers(), Bukkit.getVersion(), GameProtocol.getJavaProtocolVersion(), null); } Bukkit.getPluginManager().callEvent(event); if (event.isCancelled()) { diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotLogger.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotLogger.java index 5c6101eae..231255fec 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotLogger.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotLogger.java @@ -75,4 +75,11 @@ public class GeyserSpigotLogger implements GeyserLogger { info(message); } } + + @Override + public void debug(String message, Object... arguments) { + if (debug) { + info(String.format(message, arguments)); + } + } } diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/ReflectedNames.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/ReflectedNames.java index 275fec657..5429850de 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/ReflectedNames.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/ReflectedNames.java @@ -26,12 +26,12 @@ package org.geysermc.geyser.platform.spigot; import com.destroystokyo.paper.event.server.PaperServerListPingEvent; -import com.destroystokyo.paper.network.StatusClient; +import org.bukkit.Bukkit; import org.bukkit.event.server.ServerListPingEvent; -import org.bukkit.util.CachedServerIcon; import org.checkerframework.checker.nullness.qual.Nullable; import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import java.net.InetAddress; /** @@ -42,8 +42,9 @@ public final class ReflectedNames { static boolean checkPaperPingEvent() { try { Class.forName("com.destroystokyo.paper.event.server.PaperServerListPingEvent"); + paperServerListPingEventConstructor(); return true; - } catch (ClassNotFoundException e) { + } catch (Throwable ignored) { return false; } } @@ -52,18 +53,27 @@ public final class ReflectedNames { return getConstructor(ServerListPingEvent.class, InetAddress.class, String.class, boolean.class, int.class, int.class) != null; } - static @Nullable Constructor getOldPaperPingConstructor() { - if (getConstructor(PaperServerListPingEvent.class, StatusClient.class, String.class, int.class, - int.class, String.class, int.class, CachedServerIcon.class) != null) { - // @NotNull StatusClient client, @NotNull String motd, int numPlayers, int maxPlayers, - // @NotNull String version, int protocolVersion, @Nullable CachedServerIcon favicon - // New constructor is present - return null; + // Ugly workaround that's necessary due to relocation of adventure components + static Method motdGetter() { + try { + return Bukkit.class.getMethod("motd"); + } catch (Throwable e) { + throw new RuntimeException("Could not find component motd method! Please report this issue.", e); } - // @NotNull StatusClient client, @NotNull String motd, boolean shouldSendChatPreviews, int numPlayers, int maxPlayers, - // @NotNull String version, int protocolVersion, @Nullable CachedServerIcon favicon - return getConstructor(PaperServerListPingEvent.class, StatusClient.class, String.class, boolean.class, int.class, int.class, - String.class, int.class, CachedServerIcon.class); + } + + @SuppressWarnings("unchecked") + static Constructor paperServerListPingEventConstructor() { + var constructors = PaperServerListPingEvent.class.getConstructors(); + for (var constructor : constructors) { + // We want to get the constructor with the adventure component motd, but without referencing the + // component class as that's relocated + if (constructor.getParameters()[1].getType() != String.class) { + return (Constructor) constructor; + } + } + + throw new IllegalStateException("Could not find component motd method!"); } /** diff --git a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneLogger.java b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneLogger.java index 13ece3004..e73e799ba 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneLogger.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneLogger.java @@ -121,6 +121,12 @@ public class GeyserStandaloneLogger extends SimpleTerminalConsole implements Gey log.debug("{}", object); } + @Override + public void debug(String message, Object... arguments) { + // We can't use the debug call that would format for us as we're using Java's string formatting + log.debug(ChatColor.GRAY + String.format(message, arguments)); + } + @Override public void setDebug(boolean debug) { Configurator.setLevel(log.getName(), debug ? Level.DEBUG : Level.INFO); diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityInjector.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityInjector.java index ef56b1bfa..3d44c3393 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityInjector.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityInjector.java @@ -53,6 +53,13 @@ public class GeyserVelocityInjector extends GeyserInjector { @Override @SuppressWarnings("unchecked") protected void initializeLocalChannel0(GeyserBootstrap bootstrap) throws Exception { + // TEMPORARY until Netty 4.2 is implemented with these changes in mind. + try { + Class.forName("io.netty.channel.MultiThreadIoEventLoopGroup"); + return; + } catch (ClassNotFoundException ignored) { + } + Field cm = proxy.getClass().getDeclaredField("cm"); cm.setAccessible(true); Object connectionManager = cm.get(proxy); diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityLogger.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityLogger.java index 4d10e4daf..0331b825e 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityLogger.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityLogger.java @@ -73,4 +73,11 @@ public class GeyserVelocityLogger implements GeyserLogger { info(message); } } -} \ No newline at end of file + + @Override + public void debug(String message, Object... arguments) { + if (debug) { + logger.info(String.format(message, arguments)); + } + } +} diff --git a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyLogger.java b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyLogger.java index 550ee9106..8ed1b5d65 100644 --- a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyLogger.java +++ b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyLogger.java @@ -83,6 +83,13 @@ public class GeyserViaProxyLogger implements GeyserLogger, GeyserCommandSource { } } + @Override + public void debug(String message, Object... arguments) { + if (this.debug) { + this.debug(String.format(message, arguments)); + } + } + @Override public void setDebug(boolean debug) { this.debug = debug; diff --git a/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts b/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts index 04803753b..dca1bcef5 100644 --- a/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts @@ -62,14 +62,11 @@ repositories { name = "viaversion" } - // MinecraftAuth - maven("https://maven.lenni0451.net/snapshots") + // For Adventure snapshots + maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") // Jitpack for e.g. MCPL maven("https://jitpack.io") { content { includeGroupByRegex("com\\.github\\..*") } } - - // For Adventure snapshots - maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") } diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index 51e188d83..a0a56412a 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -84,6 +84,7 @@ import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.network.netty.GeyserServer; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.registry.loader.ResourcePackLoader; import org.geysermc.geyser.registry.provider.ProviderSupplier; import org.geysermc.geyser.scoreboard.ScoreboardUpdater; import org.geysermc.geyser.session.GeyserSession; @@ -703,9 +704,7 @@ public class GeyserImpl implements GeyserApi, EventRegistrar { runIfNonNull(newsHandler, NewsHandler::shutdown); runIfNonNull(erosionUnixListener, UnixSocketClientListener::close); - if (Registries.RESOURCE_PACKS.loaded()) { - Registries.RESOURCE_PACKS.get().clear(); - } + ResourcePackLoader.clear(); this.setEnabled(false); } diff --git a/core/src/main/java/org/geysermc/geyser/GeyserLogger.java b/core/src/main/java/org/geysermc/geyser/GeyserLogger.java index a49391bab..3f042f8e2 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserLogger.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserLogger.java @@ -29,6 +29,7 @@ import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.command.GeyserCommandSource; +import org.geysermc.geyser.session.GeyserSession; import java.util.UUID; @@ -106,6 +107,15 @@ public interface GeyserLogger extends GeyserCommandSource { } } + /** + * Logs and formats a message to console if debug mode is enabled, + * with the provided arguments. + * + * @param message the message to log + * @param arguments the arguments to replace in the message + */ + void debug(String message, Object... arguments); + /** * Sets if the logger should print debug messages * @@ -113,6 +123,15 @@ public interface GeyserLogger extends GeyserCommandSource { */ void setDebug(boolean debug); + /** + * A method to debug information specific to a session. + */ + default void debug(GeyserSession session, String message, Object... arguments) { + if (isDebug()) { + debug("(" + session.bedrockUsername() + ") " + message, arguments); + } + } + /** * If debug is enabled for this logger */ diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancedTooltipsCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancedTooltipsCommand.java index 75b9252da..be2ffd39c 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancedTooltipsCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancedTooltipsCommand.java @@ -51,6 +51,6 @@ public class AdvancedTooltipsCommand extends GeyserCommand { + MinecraftLocale.getLocaleString("debug.prefix", session.locale()) + " " + ChatColor.RESET + MinecraftLocale.getLocaleString("debug.advanced_tooltips." + onOrOff, session.locale())); - session.getInventoryTranslator().updateInventory(session, session.getPlayerInventory()); + session.getPlayerInventoryHolder().updateInventory(); } } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancementsCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancementsCommand.java index 0cba28f33..e82c0b66b 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancementsCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancementsCommand.java @@ -42,6 +42,6 @@ public class AdvancementsCommand extends GeyserCommand { @Override public void execute(CommandContext context) { GeyserSession session = Objects.requireNonNull(context.sender().connection()); - session.getAdvancementsCache().buildAndShowMenuForm(); + session.getAdvancementsCache().buildAndShowForm(); } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java index c8488238d..77030306c 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java @@ -28,7 +28,7 @@ package org.geysermc.geyser.entity; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.factory.EntityFactory; -import org.geysermc.geyser.entity.properties.GeyserEntityProperties; +import org.geysermc.geyser.entity.properties.VanillaEntityProperties; import org.geysermc.geyser.entity.type.AbstractArrowEntity; import org.geysermc.geyser.entity.type.AbstractWindChargeEntity; import org.geysermc.geyser.entity.type.AreaEffectCloudEntity; @@ -37,6 +37,7 @@ import org.geysermc.geyser.entity.type.BoatEntity; import org.geysermc.geyser.entity.type.ChestBoatEntity; import org.geysermc.geyser.entity.type.CommandBlockMinecartEntity; import org.geysermc.geyser.entity.type.DisplayBaseEntity; +import org.geysermc.geyser.entity.type.ThrowableEggEntity; import org.geysermc.geyser.entity.type.EnderCrystalEntity; import org.geysermc.geyser.entity.type.EnderEyeEntity; import org.geysermc.geyser.entity.type.Entity; @@ -80,8 +81,8 @@ import org.geysermc.geyser.entity.type.living.TadpoleEntity; import org.geysermc.geyser.entity.type.living.animal.ArmadilloEntity; import org.geysermc.geyser.entity.type.living.animal.AxolotlEntity; import org.geysermc.geyser.entity.type.living.animal.BeeEntity; -import org.geysermc.geyser.entity.type.living.animal.ChickenEntity; -import org.geysermc.geyser.entity.type.living.animal.CowEntity; +import org.geysermc.geyser.entity.type.living.animal.farm.ChickenEntity; +import org.geysermc.geyser.entity.type.living.animal.farm.CowEntity; import org.geysermc.geyser.entity.type.living.animal.FoxEntity; import org.geysermc.geyser.entity.type.living.animal.FrogEntity; import org.geysermc.geyser.entity.type.living.animal.GoatEntity; @@ -89,7 +90,7 @@ import org.geysermc.geyser.entity.type.living.animal.HoglinEntity; import org.geysermc.geyser.entity.type.living.animal.MooshroomEntity; import org.geysermc.geyser.entity.type.living.animal.OcelotEntity; import org.geysermc.geyser.entity.type.living.animal.PandaEntity; -import org.geysermc.geyser.entity.type.living.animal.PigEntity; +import org.geysermc.geyser.entity.type.living.animal.farm.PigEntity; import org.geysermc.geyser.entity.type.living.animal.PolarBearEntity; import org.geysermc.geyser.entity.type.living.animal.PufferFishEntity; import org.geysermc.geyser.entity.type.living.animal.RabbitEntity; @@ -189,7 +190,7 @@ public final class EntityDefinitions { public static final EntityDefinition DONKEY; public static final EntityDefinition DRAGON_FIREBALL; public static final EntityDefinition DROWNED; - public static final EntityDefinition EGG; + public static final EntityDefinition EGG; public static final EntityDefinition ELDER_GUARDIAN; public static final EntityDefinition ENDERMAN; public static final EntityDefinition ENDERMITE; @@ -250,7 +251,8 @@ public final class EntityDefinitions { public static final EntityDefinition PILLAGER; public static final EntityDefinition PLAYER; public static final EntityDefinition POLAR_BEAR; - public static final EntityDefinition POTION; + public static final EntityDefinition SPLASH_POTION; + public static final EntityDefinition LINGERING_POTION; public static final EntityDefinition PUFFERFISH; public static final EntityDefinition RABBIT; public static final EntityDefinition RAVAGER; @@ -312,7 +314,7 @@ public final class EntityDefinitions { EntityDefinition entityBase = EntityDefinition.builder(Entity::new) .addTranslator(MetadataTypes.BYTE, Entity::setFlags) .addTranslator(MetadataTypes.INT, Entity::setAir) // Air/bubbles - .addTranslator(MetadataTypes.OPTIONAL_CHAT, Entity::setDisplayName) + .addTranslator(MetadataTypes.OPTIONAL_COMPONENT, Entity::setDisplayName) .addTranslator(MetadataTypes.BOOLEAN, Entity::setDisplayNameVisible) .addTranslator(MetadataTypes.BOOLEAN, Entity::setSilent) .addTranslator(MetadataTypes.BOOLEAN, Entity::setGravity) @@ -337,12 +339,13 @@ public final class EntityDefinitions { .type(EntityType.END_CRYSTAL) .heightAndWidth(2.0f) .identifier("minecraft:ender_crystal") - .addTranslator(MetadataTypes.OPTIONAL_POSITION, EnderCrystalEntity::setBlockTarget) + .addTranslator(MetadataTypes.OPTIONAL_BLOCK_POS, EnderCrystalEntity::setBlockTarget) .addTranslator(MetadataTypes.BOOLEAN, (enderCrystalEntity, entityMetadata) -> enderCrystalEntity.setFlag(EntityFlag.SHOW_BOTTOM, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) // There is a base located on the ender crystal .build(); EXPERIENCE_ORB = EntityDefinition.inherited(ExpOrbEntity::new, entityBase) .type(EntityType.EXPERIENCE_ORB) + .addTranslator(null) // int determining xb orb texture .identifier("minecraft:xp_orb") .build(); EVOKER_FANGS = EntityDefinition.inherited(EvokerFangsEntity::new, entityBase) @@ -365,8 +368,8 @@ public final class EntityDefinitions { .type(EntityType.FIREWORK_ROCKET) .heightAndWidth(0.25f) .identifier("minecraft:fireworks_rocket") - .addTranslator(MetadataTypes.ITEM, FireworkEntity::setFireworkItem) - .addTranslator(MetadataTypes.OPTIONAL_VARINT, FireworkEntity::setPlayerGliding) + .addTranslator(MetadataTypes.ITEM_STACK, FireworkEntity::setFireworkItem) + .addTranslator(MetadataTypes.OPTIONAL_UNSIGNED_INT, FireworkEntity::setPlayerGliding) .addTranslator(null) // Shot at angle .build(); FISHING_BOBBER = EntityDefinition.inherited(null, entityBase) @@ -379,7 +382,7 @@ public final class EntityDefinitions { .type(EntityType.ITEM) .heightAndWidth(0.25f) .offset(0.125f) - .addTranslator(MetadataTypes.ITEM, ItemEntity::setItem) + .addTranslator(MetadataTypes.ITEM_STACK, ItemEntity::setItem) .build(); LEASH_KNOT = EntityDefinition.inherited(LeashKnotEntity::new, entityBase) .type(EntityType.LEASH_KNOT) @@ -428,7 +431,7 @@ public final class EntityDefinitions { .type(EntityType.TEXT_DISPLAY) .identifier("minecraft:armor_stand") .offset(-0.5f) - .addTranslator(MetadataTypes.CHAT, TextDisplayEntity::setText) + .addTranslator(MetadataTypes.COMPONENT, TextDisplayEntity::setText) .addTranslator(null) // Line width .addTranslator(null) // Background color .addTranslator(null) // Text opacity @@ -457,11 +460,12 @@ public final class EntityDefinitions { .build(); EntityDefinition throwableItemBase = EntityDefinition.inherited(ThrowableItemEntity::new, entityBase) - .addTranslator(MetadataTypes.ITEM, ThrowableItemEntity::setItem) + .addTranslator(MetadataTypes.ITEM_STACK, ThrowableItemEntity::setItem) .build(); - EGG = EntityDefinition.inherited(ThrowableItemEntity::new, throwableItemBase) + EGG = EntityDefinition.inherited(ThrowableEggEntity::new, throwableItemBase) .type(EntityType.EGG) .heightAndWidth(0.25f) + .properties(VanillaEntityProperties.CLIMATE_VARIANT) .build(); ENDER_PEARL = EntityDefinition.inherited(ThrowableItemEntity::new, throwableItemBase) .type(EntityType.ENDER_PEARL) @@ -472,11 +476,16 @@ public final class EntityDefinitions { .heightAndWidth(0.25f) .identifier("minecraft:xp_bottle") .build(); - POTION = EntityDefinition.inherited(ThrownPotionEntity::new, throwableItemBase) - .type(EntityType.POTION) + SPLASH_POTION = EntityDefinition.inherited(ThrownPotionEntity::new, throwableItemBase) + .type(EntityType.SPLASH_POTION) .heightAndWidth(0.25f) .identifier("minecraft:splash_potion") .build(); + LINGERING_POTION = EntityDefinition.inherited(ThrownPotionEntity::new, throwableItemBase) + .type(EntityType.LINGERING_POTION) + .heightAndWidth(0.25f) + .identifier("minecraft:splash_potion") + .build(); SNOWBALL = EntityDefinition.inherited(ThrowableItemEntity::new, throwableItemBase) .type(EntityType.SNOWBALL) .heightAndWidth(0.25f) @@ -519,7 +528,7 @@ public final class EntityDefinitions { // Item frames are handled differently as they are blocks, not items, in Bedrock ITEM_FRAME = EntityDefinition.inherited(null, entityBase) .type(EntityType.ITEM_FRAME) - .addTranslator(MetadataTypes.ITEM, ItemFrameEntity::setItemInFrame) + .addTranslator(MetadataTypes.ITEM_STACK, ItemFrameEntity::setItemInFrame) .addTranslator(MetadataTypes.INT, ItemFrameEntity::setItemRotation) .build(); GLOW_ITEM_FRAME = EntityDefinition.inherited(ITEM_FRAME.factory(), ITEM_FRAME) @@ -535,9 +544,8 @@ public final class EntityDefinitions { .addTranslator(MetadataTypes.FLOAT, (minecartEntity, entityMetadata) -> // Power in Java, hurt ticks in Bedrock minecartEntity.getDirtyMetadata().put(EntityDataTypes.HURT_TICKS, Math.min((int) ((FloatEntityMetadata) entityMetadata).getPrimitiveValue(), 15))) - .addTranslator(MetadataTypes.INT, MinecartEntity::setCustomBlock) + .addTranslator(MetadataTypes.OPTIONAL_BLOCK_STATE, MinecartEntity::setCustomBlock) .addTranslator(MetadataTypes.INT, MinecartEntity::setCustomBlockOffset) - .addTranslator(MetadataTypes.BOOLEAN, MinecartEntity::setShowCustomBlock) .build(); CHEST_MINECART = EntityDefinition.inherited(MINECART.factory(), MINECART) .type(EntityType.CHEST_MINECART) @@ -545,7 +553,7 @@ public final class EntityDefinitions { COMMAND_BLOCK_MINECART = EntityDefinition.inherited(CommandBlockMinecartEntity::new, MINECART) .type(EntityType.COMMAND_BLOCK_MINECART) .addTranslator(MetadataTypes.STRING, (entity, entityMetadata) -> entity.getDirtyMetadata().put(EntityDataTypes.COMMAND_BLOCK_NAME, entityMetadata.getValue())) - .addTranslator(MetadataTypes.CHAT, (entity, entityMetadata) -> entity.getDirtyMetadata().put(EntityDataTypes.COMMAND_BLOCK_LAST_OUTPUT, MessageTranslator.convertMessage(entityMetadata.getValue()))) + .addTranslator(MetadataTypes.COMPONENT, (entity, entityMetadata) -> entity.getDirtyMetadata().put(EntityDataTypes.COMMAND_BLOCK_LAST_OUTPUT, MessageTranslator.convertMessage(entityMetadata.getValue()))) .build(); FURNACE_MINECART = EntityDefinition.inherited(FurnaceMinecartEntity::new, MINECART) .type(EntityType.FURNACE_MINECART) @@ -621,19 +629,19 @@ public final class EntityDefinitions { (livingEntity, entityMetadata) -> livingEntity.getDirtyMetadata().put(EntityDataTypes.EFFECT_AMBIENCE, (byte) (((BooleanEntityMetadata) entityMetadata).getPrimitiveValue() ? 1 : 0))) .addTranslator(null) // Arrow count .addTranslator(null) // Stinger count - .addTranslator(MetadataTypes.OPTIONAL_POSITION, LivingEntity::setBedPosition) + .addTranslator(MetadataTypes.OPTIONAL_BLOCK_POS, LivingEntity::setBedPosition) .build(); ARMOR_STAND = EntityDefinition.inherited(ArmorStandEntity::new, livingEntityBase) .type(EntityType.ARMOR_STAND) .height(1.975f).width(0.5f) .addTranslator(MetadataTypes.BYTE, ArmorStandEntity::setArmorStandFlags) - .addTranslator(MetadataTypes.ROTATION, ArmorStandEntity::setHeadRotation) - .addTranslator(MetadataTypes.ROTATION, ArmorStandEntity::setBodyRotation) - .addTranslator(MetadataTypes.ROTATION, ArmorStandEntity::setLeftArmRotation) - .addTranslator(MetadataTypes.ROTATION, ArmorStandEntity::setRightArmRotation) - .addTranslator(MetadataTypes.ROTATION, ArmorStandEntity::setLeftLegRotation) - .addTranslator(MetadataTypes.ROTATION, ArmorStandEntity::setRightLegRotation) + .addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setHeadRotation) + .addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setBodyRotation) + .addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setLeftArmRotation) + .addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setRightArmRotation) + .addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setLeftLegRotation) + .addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setRightLegRotation) .build(); PLAYER = EntityDefinition.inherited(null, livingEntityBase) .type(EntityType.PLAYER) @@ -643,8 +651,8 @@ public final class EntityDefinitions { .addTranslator(null) // Player score .addTranslator(MetadataTypes.BYTE, PlayerEntity::setSkinVisibility) .addTranslator(null) // Player main hand - .addTranslator(MetadataTypes.NBT_TAG, PlayerEntity::setLeftParrot) - .addTranslator(MetadataTypes.NBT_TAG, PlayerEntity::setRightParrot) + .addTranslator(MetadataTypes.COMPOUND_TAG, PlayerEntity::setLeftParrot) + .addTranslator(MetadataTypes.COMPOUND_TAG, PlayerEntity::setRightParrot) .build(); EntityDefinition mobEntityBase = EntityDefinition.inherited(MobEntity::new, livingEntityBase) @@ -684,16 +692,8 @@ public final class EntityDefinitions { .addTranslator(MetadataTypes.BOOLEAN, CreakingEntity::setCanMove) .addTranslator(MetadataTypes.BOOLEAN, CreakingEntity::setActive) .addTranslator(MetadataTypes.BOOLEAN, CreakingEntity::setIsTearingDown) - .addTranslator(MetadataTypes.OPTIONAL_POSITION, CreakingEntity::setHomePos) - .properties(new GeyserEntityProperties.Builder() - .addEnum(CreakingEntity.CREAKING_STATE, - "neutral", - "hostile_observed", - "hostile_unobserved", - "twitching", - "crumbling") - .addInt(CreakingEntity.CREAKING_SWAYING_TICKS, 0, 6) - .build()) + .addTranslator(MetadataTypes.OPTIONAL_BLOCK_POS, CreakingEntity::setHomePos) + .properties(VanillaEntityProperties.CREAKING) .build(); CREEPER = EntityDefinition.inherited(CreeperEntity::new, mobEntityBase) .type(EntityType.CREEPER) @@ -946,15 +946,7 @@ public final class EntityDefinitions { ARMADILLO = EntityDefinition.inherited(ArmadilloEntity::new, ageableEntityBase) .type(EntityType.ARMADILLO) .height(0.65f).width(0.7f) - .properties(new GeyserEntityProperties.Builder() - .addEnum( - "minecraft:armadillo_state", - "unrolled", - "rolled_up", - "rolled_up_peeking", - "rolled_up_relaxing", - "rolled_up_unrolling") - .build()) + .properties(VanillaEntityProperties.ARMADILLO) .addTranslator(MetadataTypes.ARMADILLO_STATE, ArmadilloEntity::setArmadilloState) .build(); AXOLOTL = EntityDefinition.inherited(AxolotlEntity::new, ageableEntityBase) @@ -967,19 +959,21 @@ public final class EntityDefinitions { BEE = EntityDefinition.inherited(BeeEntity::new, ageableEntityBase) .type(EntityType.BEE) .heightAndWidth(0.6f) - .properties(new GeyserEntityProperties.Builder() - .addBoolean("minecraft:has_nectar") - .build()) + .properties(VanillaEntityProperties.BEE) .addTranslator(MetadataTypes.BYTE, BeeEntity::setBeeFlags) .addTranslator(MetadataTypes.INT, BeeEntity::setAngerTime) .build(); CHICKEN = EntityDefinition.inherited(ChickenEntity::new, ageableEntityBase) .type(EntityType.CHICKEN) .height(0.7f).width(0.4f) + .properties(VanillaEntityProperties.CLIMATE_VARIANT) + .addTranslator(MetadataTypes.CHICKEN_VARIANT, ChickenEntity::setVariant) .build(); COW = EntityDefinition.inherited(CowEntity::new, ageableEntityBase) .type(EntityType.COW) .height(1.4f).width(0.9f) + .properties(VanillaEntityProperties.CLIMATE_VARIANT) + .addTranslator(MetadataTypes.COW_VARIANT, CowEntity::setVariant) .build(); FOX = EntityDefinition.inherited(FoxEntity::new, ageableEntityBase) .type(EntityType.FOX) @@ -992,8 +986,8 @@ public final class EntityDefinitions { FROG = EntityDefinition.inherited(FrogEntity::new, ageableEntityBase) .type(EntityType.FROG) .heightAndWidth(0.5f) - .addTranslator(MetadataTypes.FROG_VARIANT, FrogEntity::setFrogVariant) - .addTranslator(MetadataTypes.OPTIONAL_VARINT, FrogEntity::setTongueTarget) + .addTranslator(MetadataTypes.FROG_VARIANT, FrogEntity::setVariant) + .addTranslator(MetadataTypes.OPTIONAL_UNSIGNED_INT, FrogEntity::setTongueTarget) .build(); HOGLIN = EntityDefinition.inherited(HoglinEntity::new, ageableEntityBase) .type(EntityType.HOGLIN) @@ -1010,7 +1004,7 @@ public final class EntityDefinitions { MOOSHROOM = EntityDefinition.inherited(MooshroomEntity::new, ageableEntityBase) .type(EntityType.MOOSHROOM) .height(1.4f).width(0.9f) - .addTranslator(MetadataTypes.STRING, MooshroomEntity::setVariant) + .addTranslator(MetadataTypes.INT, MooshroomEntity::setMooshroomVariant) .build(); OCELOT = EntityDefinition.inherited(OcelotEntity::new, ageableEntityBase) .type(EntityType.OCELOT) @@ -1030,8 +1024,9 @@ public final class EntityDefinitions { PIG = EntityDefinition.inherited(PigEntity::new, ageableEntityBase) .type(EntityType.PIG) .heightAndWidth(0.9f) - .addTranslator(MetadataTypes.BOOLEAN, (pigEntity, entityMetadata) -> pigEntity.setFlag(EntityFlag.SADDLED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) + .properties(VanillaEntityProperties.CLIMATE_VARIANT) .addTranslator(MetadataTypes.INT, PigEntity::setBoost) + .addTranslator(MetadataTypes.PIG_VARIANT, PigEntity::setVariant) .build(); POLAR_BEAR = EntityDefinition.inherited(PolarBearEntity::new, ageableEntityBase) .type(EntityType.POLAR_BEAR) @@ -1059,7 +1054,6 @@ public final class EntityDefinitions { .height(1.7f).width(0.9f) .addTranslator(MetadataTypes.INT, StriderEntity::setBoost) .addTranslator(MetadataTypes.BOOLEAN, StriderEntity::setCold) - .addTranslator(MetadataTypes.BOOLEAN, StriderEntity::setSaddled) .build(); TURTLE = EntityDefinition.inherited(TurtleEntity::new, ageableEntityBase) .type(EntityType.TURTLE) @@ -1158,12 +1152,12 @@ public final class EntityDefinitions { EntityDefinition tameableEntityBase = EntityDefinition.inherited(null, ageableEntityBase) // No factory, is abstract .addTranslator(MetadataTypes.BYTE, TameableEntity::setTameableFlags) - .addTranslator(MetadataTypes.OPTIONAL_UUID, TameableEntity::setOwner) + .addTranslator(MetadataTypes.OPTIONAL_LIVING_ENTITY_REFERENCE, TameableEntity::setOwner) .build(); CAT = EntityDefinition.inherited(CatEntity::new, tameableEntityBase) .type(EntityType.CAT) .height(0.35f).width(0.3f) - .addTranslator(MetadataTypes.CAT_VARIANT, CatEntity::setCatVariant) + .addTranslator(MetadataTypes.CAT_VARIANT, CatEntity::setVariant) .addTranslator(MetadataTypes.BOOLEAN, CatEntity::setResting) .addTranslator(null) // "resting state one" //TODO .addTranslator(MetadataTypes.INT, CatEntity::setCollarColor) @@ -1176,11 +1170,12 @@ public final class EntityDefinitions { WOLF = EntityDefinition.inherited(WolfEntity::new, tameableEntityBase) .type(EntityType.WOLF) .height(0.85f).width(0.6f) + .properties(VanillaEntityProperties.WOLF_SOUND_VARIANT) // "Begging" on wiki.vg, "Interested" in Nukkit - the tilt of the head .addTranslator(MetadataTypes.BOOLEAN, (wolfEntity, entityMetadata) -> wolfEntity.setFlag(EntityFlag.INTERESTED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) .addTranslator(MetadataTypes.INT, WolfEntity::setCollarColor) .addTranslator(MetadataTypes.INT, WolfEntity::setWolfAngerTime) - .addTranslator(MetadataTypes.WOLF_VARIANT, WolfEntity::setWolfVariant) + .addTranslator(MetadataTypes.WOLF_VARIANT, WolfEntity::setVariant) .build(); // As of 1.18 these don't track entity data at all diff --git a/core/src/main/java/org/geysermc/geyser/entity/attribute/GeyserAttributeType.java b/core/src/main/java/org/geysermc/geyser/entity/attribute/GeyserAttributeType.java index 10e93810e..80db3354e 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/attribute/GeyserAttributeType.java +++ b/core/src/main/java/org/geysermc/geyser/entity/attribute/GeyserAttributeType.java @@ -25,10 +25,10 @@ package org.geysermc.geyser.entity.attribute; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.cloudburstmc.protocol.bedrock.data.AttributeData; import lombok.AllArgsConstructor; import lombok.Getter; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.protocol.bedrock.data.AttributeData; @Getter @AllArgsConstructor diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityProperties.java b/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityProperties.java index 1729b0583..eaa7b7448 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityProperties.java +++ b/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityProperties.java @@ -162,4 +162,4 @@ public class GeyserEntityProperties { return new GeyserEntityProperties(properties, propertyIndices); } } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/VanillaEntityProperties.java b/core/src/main/java/org/geysermc/geyser/entity/properties/VanillaEntityProperties.java new file mode 100644 index 000000000..305dbf22e --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/properties/VanillaEntityProperties.java @@ -0,0 +1,74 @@ +/* + * 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 WOLF_SOUND_VARIANT = new GeyserEntityProperties.Builder() + .addEnum("minecraft:sound_variant", + "default", + "big", + "cute", + "grumpy", + "mad", + "puglin", + "sad") + .build(); +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/AreaEffectCloudEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/AreaEffectCloudEntity.java index 165495506..546f66700 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/AreaEffectCloudEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/AreaEffectCloudEntity.java @@ -35,7 +35,7 @@ import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.MathUtils; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatEntityMetadata; -import org.geysermc.mcprotocollib.protocol.data.game.level.particle.EntityEffectParticleData; +import org.geysermc.mcprotocollib.protocol.data.game.level.particle.ColorParticleData; import org.geysermc.mcprotocollib.protocol.data.game.level.particle.Particle; import java.util.UUID; @@ -72,7 +72,7 @@ public class AreaEffectCloudEntity extends Entity { Registries.PARTICLES.map(particle.getType(), p -> p.levelEventType() instanceof ParticleType particleType ? particleType : null).ifPresent(type -> dirtyMetadata.put(EntityDataTypes.AREA_EFFECT_CLOUD_PARTICLE, type)); - if (particle.getData() instanceof EntityEffectParticleData effectParticleData) { + if (particle.getData() instanceof ColorParticleData effectParticleData) { dirtyMetadata.put(EntityDataTypes.EFFECT_COLOR, effectParticleData.getColor()); } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java index 7d789fb2a..667b4190b 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java @@ -28,6 +28,7 @@ package org.geysermc.geyser.entity.type; import lombok.Getter; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.packet.AnimatePacket; import org.cloudburstmc.protocol.bedrock.packet.MoveEntityAbsolutePacket; import org.geysermc.geyser.entity.EntityDefinition; @@ -76,16 +77,20 @@ public class BoatEntity extends Entity implements Leashable, Tickable { super(session, entityId, geyserId, uuid, definition, position.add(0d, definition.offset(), 0d), motion, yaw + 90, 0, yaw + 90); this.variant = variant; - // TODO remove once 1.21.40 is dropped - if (variant == BoatVariant.PALE_OAK && GameProtocol.isPreWinterDrop(session)) { - variant = BoatVariant.BIRCH; - } - dirtyMetadata.put(EntityDataTypes.VARIANT, variant.ordinal()); // Required to be able to move on land 1.16.200+ or apply gravity not in the water 1.16.100+ dirtyMetadata.put(EntityDataTypes.IS_BUOYANT, true); - dirtyMetadata.put(EntityDataTypes.BUOYANCY_DATA, BUOYANCY_DATA); + dirtyMetadata.put(EntityDataTypes.BUOYANCY_DATA, BUOYANCY_DATA);; + } + + @Override + protected void initializeMetadata() { + super.initializeMetadata(); + if (GameProtocol.is1_21_70orHigher(session)) { + // Without this flag you cant stand on boats + setFlag(EntityFlag.COLLIDABLE, true); + } } @Override diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/DefaultBlockMinecartEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/DefaultBlockMinecartEntity.java index fd6f17eb8..65b71adcd 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/DefaultBlockMinecartEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/DefaultBlockMinecartEntity.java @@ -25,12 +25,11 @@ package org.geysermc.geyser.entity.type; -import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; -import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import java.util.UUID; @@ -58,9 +57,13 @@ public class DefaultBlockMinecartEntity extends MinecartEntity { @Override public void setCustomBlock(IntEntityMetadata entityMetadata) { customBlock = entityMetadata.getPrimitiveValue(); + showCustomBlock = entityMetadata.getPrimitiveValue() != 0; if (showCustomBlock) { dirtyMetadata.put(EntityDataTypes.DISPLAY_BLOCK_STATE, session.getBlockMappings().getBedrockBlock(customBlock)); + dirtyMetadata.put(EntityDataTypes.DISPLAY_OFFSET, customBlockOffset); + } else { + updateDefaultBlockMetadata(); } } @@ -73,18 +76,6 @@ public class DefaultBlockMinecartEntity extends MinecartEntity { } } - @Override - public void setShowCustomBlock(BooleanEntityMetadata entityMetadata) { - if (entityMetadata.getPrimitiveValue()) { - showCustomBlock = true; - dirtyMetadata.put(EntityDataTypes.DISPLAY_BLOCK_STATE, session.getBlockMappings().getBedrockBlock(customBlock)); - dirtyMetadata.put(EntityDataTypes.DISPLAY_OFFSET, customBlockOffset); - } else { - showCustomBlock = false; - updateDefaultBlockMetadata(); - } - } - public void updateDefaultBlockMetadata() { } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java b/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java index 07b0e7893..a93850c15 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java @@ -200,6 +200,7 @@ public class Entity implements GeyserEntity { addAdditionalSpawnData(addEntityPacket); valid = true; + session.sendUpstreamPacket(addEntityPacket); flagsDirty = false; @@ -372,6 +373,10 @@ public class Entity implements GeyserEntity { flagsDirty = false; } dirtyMetadata.apply(entityDataPacket.getMetadata()); + if (propertyManager != null && propertyManager.hasProperties()) { + propertyManager.applyIntProperties(entityDataPacket.getProperties().getIntProperties()); + propertyManager.applyFloatProperties(entityDataPacket.getProperties().getFloatProperties()); + } session.sendUpstreamPacket(entityDataPacket); } } @@ -430,7 +435,7 @@ public class Entity implements GeyserEntity { } public String teamIdentifier() { - // experience orbs are the only known entities that do not send an uuid (even though they do have one), + // experience orbs were the only known entities that do not send an uuid pre 1.21.5 (even though they do have one), // but to be safe in the future it's done in the entity class itself instead of the entity specific one. // All entities without an uuid cannot show up in the scoreboard! return uuid != null ? uuid.toString() : null; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/ExpOrbEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/ExpOrbEntity.java index 9f61bc961..8cca969b1 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/ExpOrbEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/ExpOrbEntity.java @@ -28,7 +28,6 @@ package org.geysermc.geyser.entity.type; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.geysermc.geyser.entity.EntityDefinition; -import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.session.GeyserSession; import java.util.UUID; @@ -36,12 +35,7 @@ import java.util.UUID; public class ExpOrbEntity extends Entity { public ExpOrbEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition entityDefinition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { - this(session, 1, entityId, geyserId, position); - } - - public ExpOrbEntity(GeyserSession session, int amount, int entityId, long geyserId, Vector3f position) { - super(session, entityId, geyserId, null, EntityDefinitions.EXPERIENCE_ORB, position, Vector3f.ZERO, 0, 0, 0); - - this.dirtyMetadata.put(EntityDataTypes.TRADE_EXPERIENCE, amount); + super(session, entityId, geyserId, uuid, entityDefinition, position, motion, yaw, pitch, headYaw); + this.dirtyMetadata.put(EntityDataTypes.TRADE_EXPERIENCE, 1); } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/FireworkEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/FireworkEntity.java index ebe35320e..d7a9990fe 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/FireworkEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/FireworkEntity.java @@ -31,6 +31,7 @@ import org.cloudburstmc.protocol.bedrock.packet.SetEntityMotionPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.type.player.PlayerEntity; import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.item.BedrockItemBuilder; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; @@ -59,7 +60,8 @@ public class FireworkEntity extends Entity { // TODO this looked the same, so I'm going to assume it is and (keep below comment if true) // Translate using item methods to get firework NBT for Bedrock BedrockItemBuilder builder = new BedrockItemBuilder(); - Items.FIREWORK_ROCKET.translateComponentsToBedrock(session, components, builder); + TooltipOptions tooltip = TooltipOptions.fromComponents(components); + Items.FIREWORK_ROCKET.translateComponentsToBedrock(session, components, tooltip, builder); dirtyMetadata.put(EntityDataTypes.DISPLAY_FIREWORK, builder.build()); } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/InteractionEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/InteractionEntity.java index 06035a47c..967269069 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/InteractionEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/InteractionEntity.java @@ -25,17 +25,24 @@ package org.geysermc.geyser.entity.type; +import net.kyori.adventure.text.Component; import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.packet.AnimatePacket; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.EntityDefinitions; +import org.geysermc.geyser.entity.type.living.ArmorStandEntity; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.InteractionResult; +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.FloatEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundSwingPacket; +import java.util.Optional; import java.util.UUID; public class InteractionEntity extends Entity { @@ -50,6 +57,16 @@ public class InteractionEntity extends Entity { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } + /** + * On Java Edition, interaction entities can have a nametag. Invisibility hides the name on Bedrock. + * By having a second entity, we can still show the nametag while keeping the interaction entity invisible. + */ + private ArmorStandEntity secondEntity = null; + + private boolean isNameTagVisible = false; + + private boolean isVisible = true; + @Override protected void initializeMetadata() { super.initializeMetadata(); @@ -58,6 +75,19 @@ public class InteractionEntity extends Entity { setFlag(EntityFlag.INVISIBLE, true); } + @Override + protected void setInvisible(boolean value) { + // Always invisible; would reveal the armor stand otherwise + isVisible = value; + this.updateNameTag(); + } + + @Override + public void setDisplayNameVisible(BooleanEntityMetadata entityMetadata) { + isNameTagVisible = entityMetadata.getPrimitiveValue(); + this.updateNameTag(); + } + @Override public InteractionResult interact(Hand hand) { // these InteractionResults do mirror the java client @@ -75,6 +105,33 @@ public class InteractionEntity extends Entity { return InteractionResult.CONSUME; } + @Override + public void despawnEntity() { + if (secondEntity != null) { + secondEntity.despawnEntity(); + } + super.despawnEntity(); + } + + @Override + public void setDisplayName(EntityMetadata, ?> entityMetadata) { + super.setDisplayName(entityMetadata); + this.updateNameTag(); + } + + @Override + public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) { + moveAbsolute(position.add(relX, relY, relZ), yaw, pitch, headYaw, isOnGround, false); + } + + @Override + public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) { + if (secondEntity != null) { + secondEntity.moveAbsolute(position.up(getBoundingBoxHeight()), yaw, pitch, headYaw, isOnGround, teleported); + } + super.moveAbsolute(position, yaw, pitch, headYaw, isOnGround, teleported); + } + public void setWidth(FloatEntityMetadata width) { setBoundingBoxWidth(width.getPrimitiveValue()); } @@ -84,9 +141,39 @@ public class InteractionEntity extends Entity { // https://gist.github.com/Owen1212055/f5d59169d3a6a5c32f0c173d57eb199d recommend(s/ed) using the tactic // https://github.com/GeyserMC/Geyser/issues/4688 setBoundingBoxHeight(Math.min(height.getPrimitiveValue(), 64f)); + + if (secondEntity != null) { + secondEntity.moveAbsolute(position.up(getBoundingBoxHeight()), yaw, pitch, onGround, true); + } } public void setResponse(BooleanEntityMetadata response) { this.response = response.getPrimitiveValue(); } + + public void updateNameTag() { + if (this.nametag.isBlank() || !isVisible) { + if (secondEntity != null) { + secondEntity.despawnEntity(); + secondEntity = null; + } + return; + } + + if (this.secondEntity == null) { + secondEntity = new ArmorStandEntity(session, 0, session.getEntityCache().getNextEntityId().incrementAndGet(), null, + EntityDefinitions.ARMOR_STAND, position.up(getBoundingBoxHeight()), motion, getYaw(), getPitch(), getHeadYaw()); + } + secondEntity.getDirtyMetadata().put(EntityDataTypes.NAME, nametag); + secondEntity.getDirtyMetadata().put(EntityDataTypes.NAMETAG_ALWAYS_SHOW, isNameTagVisible ? (byte) 1 : (byte) 0); + // Scale to 0 to show nametag + secondEntity.setScale(0f); + // No bounding box as we don't want to interact with this entity + secondEntity.getDirtyMetadata().put(EntityDataTypes.WIDTH, 0.0f); + secondEntity.getDirtyMetadata().put(EntityDataTypes.HEIGHT, 0.0f); + secondEntity.getDirtyMetadata().put(EntityDataTypes.HITBOX, NbtMap.EMPTY); + if (!secondEntity.valid) { // Spawn the entity once + secondEntity.spawnEntity(); + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java index 8c1ab80f0..928e9b764 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java @@ -44,6 +44,8 @@ import org.geysermc.geyser.entity.attribute.GeyserAttributeType; import org.geysermc.geyser.entity.vehicle.ClientVehicle; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.item.type.Item; +import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.scoreboard.Team; import org.geysermc.geyser.session.GeyserSession; @@ -51,6 +53,7 @@ import org.geysermc.geyser.translator.item.ItemTranslator; import org.geysermc.geyser.util.AttributeUtils; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.MathUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.Attribute; import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; @@ -62,7 +65,9 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.Object import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; 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.level.particle.EntityEffectParticleData; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.Equippable; +import org.geysermc.mcprotocollib.protocol.data.game.level.particle.ColorParticleData; import org.geysermc.mcprotocollib.protocol.data.game.level.particle.Particle; import org.geysermc.mcprotocollib.protocol.data.game.level.particle.ParticleType; @@ -80,6 +85,7 @@ public class LivingEntity extends Entity { 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; @@ -118,10 +124,6 @@ public class LivingEntity extends Entity { this.chestplate = ItemTranslator.translateToBedrock(session, stack); } - public void setBody(ItemStack stack) { - this.body = ItemTranslator.translateToBedrock(session, stack); - } - public void setLeggings(ItemStack stack) { this.leggings = ItemTranslator.translateToBedrock(session, stack); } @@ -130,6 +132,26 @@ public class LivingEntity extends Entity { this.boots = ItemTranslator.translateToBedrock(session, stack); } + public void setBody(ItemStack stack) { + this.body = ItemTranslator.translateToBedrock(session, stack); + } + + public void setSaddle(@Nullable ItemStack stack) { + this.saddle = ItemTranslator.translateToBedrock(session, stack); + + boolean saddled = false; + if (stack != null) { + Item item = Registries.JAVA_ITEMS.get(stack.getId()); + if (item != null) { + DataComponents components = item.gatherComponents(stack.getDataComponentsPatch()); + Equippable equippable = components.get(DataComponentTypes.EQUIPPABLE); + saddled = equippable != null && equippable.slot() == EquipmentSlot.SADDLE; + } + } + + updateSaddled(saddled); + } + public void setHand(ItemStack stack) { this.hand = ItemTranslator.translateToBedrock(session, stack); } @@ -138,6 +160,17 @@ public class LivingEntity extends Entity { this.offhand = ItemTranslator.translateToBedrock(session, stack); } + protected void updateSaddled(boolean saddled) { + setFlag(EntityFlag.SADDLED, saddled); + updateBedrockMetadata(); + + // Update the interactive tag, if necessary + Entity mouseoverEntity = session.getMouseoverEntity(); + if (mouseoverEntity != null && mouseoverEntity.getEntityId() == entityId) { + mouseoverEntity.updateInteractiveTag(); + } + } + public void switchHands() { ItemData offhand = this.offhand; this.offhand = this.hand; @@ -202,7 +235,7 @@ public class LivingEntity extends Entity { continue; } - int color = ((EntityEffectParticleData) particle.getData()).getColor(); + int color = ((ColorParticleData) particle.getData()).getColor(); r += ((color >> 16) & 0xFF) / 255f; g += ((color >> 8) & 0xFF) / 255f; b += ((color) & 0xFF) / 255f; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/MinecartEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/MinecartEntity.java index 6faecb389..1f69d34e7 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/MinecartEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/MinecartEntity.java @@ -37,7 +37,6 @@ import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; import org.geysermc.geyser.util.MathUtils; import org.geysermc.mcprotocollib.protocol.data.game.entity.MinecartStep; -import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.ClientboundMoveMinecartPacket; @@ -65,6 +64,9 @@ public class MinecartEntity extends Entity implements Tickable { } public void setCustomBlock(IntEntityMetadata entityMetadata) { + // Optional block state -> "0" is air, aka none + // Sets whether the custom block should be enabled + dirtyMetadata.put(EntityDataTypes.CUSTOM_DISPLAY, (byte) (entityMetadata.getPrimitiveValue() != 0 ? 1 : 0)); dirtyMetadata.put(EntityDataTypes.DISPLAY_BLOCK_STATE, session.getBlockMappings().getBedrockBlock(entityMetadata.getPrimitiveValue())); } @@ -72,12 +74,6 @@ public class MinecartEntity extends Entity implements Tickable { dirtyMetadata.put(EntityDataTypes.DISPLAY_OFFSET, entityMetadata.getPrimitiveValue()); } - public void setShowCustomBlock(BooleanEntityMetadata entityMetadata) { - // If the custom block should be enabled - // Needs a byte based off of Java's boolean - dirtyMetadata.put(EntityDataTypes.CUSTOM_DISPLAY, (byte) (entityMetadata.getPrimitiveValue() ? 1 : 0)); - } - @Override public void tick() { if (!session.isUsingExperimentalMinecartLogic()) { diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/PaintingEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/PaintingEntity.java index 09c055c84..b7e900365 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/PaintingEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/PaintingEntity.java @@ -30,6 +30,7 @@ import org.cloudburstmc.protocol.bedrock.packet.AddPaintingPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.level.PaintingType; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; import org.geysermc.mcprotocollib.protocol.data.game.Holder; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.PaintingVariant; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ObjectEntityMetadata; @@ -55,10 +56,7 @@ public class PaintingEntity extends Entity { if (!entityMetadata.getValue().isId()) { return; } - PaintingType type = session.getRegistryCache().paintings().byId(entityMetadata.getValue().id()); - if (type == null) { - return; - } + PaintingType type = session.getRegistryCache().registry(JavaRegistries.PAINTING_VARIANT).byId(entityMetadata.getValue().id()); AddPaintingPacket addPaintingPacket = new AddPaintingPacket(); addPaintingPacket.setUniqueEntityId(geyserId); addPaintingPacket.setRuntimeEntityId(geyserId); diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java index ccd93d12a..80e5f3d18 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java @@ -29,6 +29,7 @@ import lombok.Getter; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; @@ -61,7 +62,8 @@ public class TextDisplayEntity extends DisplayBaseEntity { @Override protected void initializeMetadata() { super.initializeMetadata(); - // Remove armor stand body + // Remove armor stand body / hitbox + this.dirtyMetadata.put(EntityDataTypes.HITBOX, NbtMap.EMPTY); this.dirtyMetadata.put(EntityDataTypes.SCALE, 0f); this.dirtyMetadata.put(EntityDataTypes.NAMETAG_ALWAYS_SHOW, (byte) 1); } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEggEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEggEntity.java new file mode 100644 index 000000000..e86265ded --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEggEntity.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.type; + +import net.kyori.adventure.key.Key; +import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.properties.VanillaEntityProperties; +import org.geysermc.geyser.entity.type.living.animal.farm.TemperatureVariantAnimal; +import org.geysermc.geyser.inventory.GeyserItemStack; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.mcprotocollib.protocol.data.game.Holder; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; + +import java.util.Locale; +import java.util.UUID; + +public class ThrowableEggEntity extends ThrowableItemEntity { + public ThrowableEggEntity(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 + public void addAdditionalSpawnData(AddEntityPacket addEntityPacket) { + propertyManager.add(VanillaEntityProperties.CLIMATE_VARIANT_ID, "temperate"); + propertyManager.applyIntProperties(addEntityPacket.getProperties().getIntProperties()); + } + + @Override + public void setItem(EntityMetadata entityMetadata) { + GeyserItemStack stack = GeyserItemStack.from(entityMetadata.getValue()); + propertyManager.add(VanillaEntityProperties.CLIMATE_VARIANT_ID, getVariantOrFallback(session, stack)); + updateBedrockEntityProperties(); + } + + private static String getVariantOrFallback(GeyserSession session, GeyserItemStack stack) { + Holder holder = stack.getComponent(DataComponentTypes.CHICKEN_VARIANT); + if (holder != null) { + Key chickenVariant = holder.getOrCompute(id -> JavaRegistries.CHICKEN_VARIANT.keyFromNetworkId(session, id)); + for (var variant : TemperatureVariantAnimal.BuiltInVariant.values()) { + if (chickenVariant.asMinimalString().equalsIgnoreCase(variant.name())) { + return chickenVariant.asMinimalString().toLowerCase(Locale.ROOT); + } + } + } + + return TemperatureVariantAnimal.BuiltInVariant.TEMPERATE.name().toLowerCase(Locale.ROOT); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEntity.java index 25bbdbd3c..85abc1c40 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEntity.java @@ -120,7 +120,7 @@ public class ThrowableEntity extends Entity implements Tickable { protected float getGravity() { if (getFlag(EntityFlag.HAS_GRAVITY)) { switch (definition.entityType()) { - case POTION: + case LINGERING_POTION, SPLASH_POTION: return 0.05f; case EXPERIENCE_BOTTLE: return 0.07f; @@ -146,7 +146,7 @@ public class ThrowableEntity extends Entity implements Tickable { return 0.8f; } else { switch (definition.entityType()) { - case POTION: + case LINGERING_POTION, SPLASH_POTION: case EXPERIENCE_BOTTLE: case SNOWBALL: case EGG: diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/ThrownPotionEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/ThrownPotionEntity.java index e940b074e..84c2cb731 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/ThrownPotionEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/ThrownPotionEntity.java @@ -31,10 +31,9 @@ import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.inventory.item.Potion; -import org.geysermc.geyser.item.Items; -import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; @@ -73,7 +72,7 @@ public class ThrownPotionEntity extends ThrowableItemEntity { } } - boolean isLingering = Registries.JAVA_ITEMS.get().get(itemStack.getId()) == Items.LINGERING_POTION; + boolean isLingering = definition.entityType() == EntityType.LINGERING_POTION; setFlag(EntityFlag.LINGERING, isLingering); } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FrogEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FrogEntity.java index a0b909b75..8638267c8 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FrogEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FrogEntity.java @@ -33,16 +33,17 @@ import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose; -import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ObjectEntityMetadata; import java.util.OptionalInt; import java.util.UUID; -public class FrogEntity extends AnimalEntity { +public class FrogEntity extends AnimalEntity implements VariantIntHolder { public FrogEntity(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); } @@ -56,13 +57,14 @@ public class FrogEntity extends AnimalEntity { super.setPose(pose); } - public void setFrogVariant(IntEntityMetadata entityMetadata) { - int variant = entityMetadata.getPrimitiveValue(); - dirtyMetadata.put(EntityDataTypes.VARIANT, switch (variant) { - case 1 -> 2; // White - case 2 -> 1; // Green - default -> variant; - }); + @Override + public JavaRegistryKey variantRegistry() { + return JavaRegistries.FROG_VARIANT; + } + + @Override + public void setBedrockVariantId(int bedrockId) { + dirtyMetadata.put(EntityDataTypes.VARIANT, bedrockId); } public void setTongueTarget(ObjectEntityMetadata entityMetadata) { @@ -82,4 +84,12 @@ public class FrogEntity extends AnimalEntity { protected Tag getFoodTag() { return ItemTag.FROG_FOOD; } + + // Ordered by bedrock id + // TODO: are these ordered correctly? + public enum BuiltInVariant implements BuiltIn { + TEMPERATE, + COLD, + WARM + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java index 2c9040b53..3314344cb 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java @@ -28,14 +28,16 @@ package org.geysermc.geyser.entity.type.living.animal; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; +import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.type.living.animal.farm.CowEntity; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; -import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ObjectEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; @@ -47,9 +49,14 @@ public class MooshroomEntity extends CowEntity { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } - public void setVariant(ObjectEntityMetadata entityMetadata) { - isBrown = entityMetadata.getValue().equals("brown"); - dirtyMetadata.put(EntityDataTypes.VARIANT, isBrown ? 1 : 0); + public void setMooshroomVariant(IntEntityMetadata metadata) { + isBrown = metadata.getPrimitiveValue() == 1; + dirtyMetadata.put(EntityDataTypes.VARIANT, metadata.getPrimitiveValue()); + } + + @Override + public void addAdditionalSpawnData(AddEntityPacket addEntityPacket) { + // There are no variants for mooshroom cows, so far } @NonNull diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java index 62318e255..236b22c51 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java @@ -69,10 +69,6 @@ public class StriderEntity extends AnimalEntity implements Tickable, ClientVehic isCold = entityMetadata.getPrimitiveValue(); } - public void setSaddled(BooleanEntityMetadata entityMetadata) { - setFlag(EntityFlag.SADDLED, entityMetadata.getPrimitiveValue()); - } - @Override public void updateBedrockMetadata() { // Make sure they are not shaking when riding another entity diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/TropicalFishEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/TropicalFishEntity.java index b6751bc3f..182bb176f 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/TropicalFishEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/TropicalFishEntity.java @@ -61,6 +61,10 @@ public class TropicalFishEntity extends AbstractFishEntity { dirtyMetadata.put(EntityDataTypes.COLOR_2, getPatternColor(varNumber)); // Pattern color 0-15 } + public static int getPackedVariant(int pattern, int baseColor, int patternColor) { + return pattern & 65535 | (baseColor & 0xFF) << 16 | (patternColor & 0xFF) << 24; + } + public static int getShape(int variant) { return Math.min(variant & 0xFF, 1); } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantHolder.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantHolder.java new file mode 100644 index 000000000..3936ca81a --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantHolder.java @@ -0,0 +1,107 @@ +/* + * 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.animal; + +import net.kyori.adventure.key.Key; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.RegistryCache; +import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; +import org.geysermc.geyser.util.MinecraftKey; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; + +import java.util.Locale; + +/** + * Interface to help set up data-driven entity variants for mobs. + * + *

Data-driven variants are sent as an int ID of their variant registry by Java, but can be a metadata ID or entity property on bedrock. + * This interface helps translate data-driven variants to built-in bedrock ones.

+ * + *

Implementations usually have to implement {@link VariantHolder#variantRegistry()} and {@link VariantHolder#setBedrockVariant(BuiltIn)}, and should also + * have an enum with built-in variants on bedrock (implementing {@link BuiltIn}).

+ * + * @param the enum of Bedrock variants. + */ +public interface VariantHolder { + + default void setVariant(IntEntityMetadata variant) { + setVariantFromJavaId(variant.getPrimitiveValue()); + } + + /** + * Sets the variant of the entity. + */ + default void setVariantFromJavaId(int variant) { + setBedrockVariant(variantRegistry().fromNetworkId(getSession(), variant)); + } + + GeyserSession getSession(); + + /** + * The registry in {@link org.geysermc.geyser.session.cache.registry.JavaRegistries} for this mob's variants. The registry can utilise the {@link VariantHolder#reader(Class, Enum)} method + * to create a reader to be used in {@link org.geysermc.geyser.session.cache.RegistryCache}. + */ + JavaRegistryKey variantRegistry(); + + /** + * Should set the variant for bedrock. + */ + void setBedrockVariant(BedrockVariant bedrockVariant); + + /** + * Creates a registry reader for this mob's variants. + * + *

This reader simply matches the identifiers of registry entries with built-in variants. If no built-in variant matches, the fallback/default is returned.

+ */ + static > RegistryCache.RegistryReader reader(Class clazz, BuiltInVariant fallback) { + BuiltInVariant[] variants = clazz.getEnumConstants(); + if (variants == null) { + throw new IllegalArgumentException("Class is not an enum"); + } + return context -> { + for (BuiltInVariant variant : variants) { + if (((BuiltIn) variant).javaIdentifier().equals(context.id())) { + return variant; + } + } + return fallback; + }; + } + + /** + * Should be implemented on an enum within the entity class. The enum lists vanilla variants that can appear on bedrock. + * + *

The enum constants should be named the same as their Java identifiers.

+ */ + interface BuiltIn { + + String name(); + + default Key javaIdentifier() { + return MinecraftKey.key(name().toLowerCase(Locale.ROOT)); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantIntHolder.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantIntHolder.java new file mode 100644 index 000000000..f1d45a447 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantIntHolder.java @@ -0,0 +1,54 @@ +/* + * 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.animal; + +/** + * Extension to {@link VariantHolder} to make it easier to implement on mobs that use bedrock's metadata system to set their variants, which are quite common. + * + * @see VariantHolder + */ +public interface VariantIntHolder extends VariantHolder { + + @Override + default void setBedrockVariant(BuiltIn variant) { + setBedrockVariantId(variant.ordinal()); + } + + /** + * Should set the variant on bedrock's metadata. The bedrock ID has already been checked and is always valid. + */ + void setBedrockVariantId(int bedrockId); + + /** + * The enum constants should be ordered in the order of their bedrock network ID. + * + * @see org.geysermc.geyser.entity.type.living.animal.VariantHolder.BuiltIn + */ + interface BuiltIn extends VariantHolder.BuiltIn { + + int ordinal(); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/ChickenEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/ChickenEntity.java similarity index 81% rename from core/src/main/java/org/geysermc/geyser/entity/type/living/animal/ChickenEntity.java rename to core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/ChickenEntity.java index 0c8e437c8..4ee7175de 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/ChickenEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/ChickenEntity.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2019-2025 GeyserMC. http://geysermc.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,19 +23,21 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.entity.type.living.animal; +package org.geysermc.geyser.entity.type.living.animal.farm; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.session.cache.tags.Tag; import java.util.UUID; -public class ChickenEntity extends AnimalEntity { +public class ChickenEntity extends TemperatureVariantAnimal { public ChickenEntity(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); @@ -46,4 +48,9 @@ public class ChickenEntity extends AnimalEntity { protected Tag getFoodTag() { return ItemTag.CHICKEN_FOOD; } + + @Override + public JavaRegistryKey variantRegistry() { + return JavaRegistries.CHICKEN_VARIANT; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/CowEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java similarity index 87% rename from core/src/main/java/org/geysermc/geyser/entity/type/living/animal/CowEntity.java rename to core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java index 66210068b..de79e9a52 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/CowEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2019-2025 GeyserMC. http://geysermc.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,7 +23,7 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.entity.type.living.animal; +package org.geysermc.geyser.entity.type.living.animal.farm; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -35,6 +35,8 @@ import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.InteractionResult; @@ -43,7 +45,8 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; -public class CowEntity extends AnimalEntity { +public class CowEntity extends TemperatureVariantAnimal { + public CowEntity(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); } @@ -74,4 +77,9 @@ public class CowEntity extends AnimalEntity { protected Tag getFoodTag() { return ItemTag.COW_FOOD; } + + @Override + public JavaRegistryKey variantRegistry() { + return JavaRegistries.COW_VARIANT; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PigEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java similarity index 92% rename from core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PigEntity.java rename to core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java index b8ba2c94f..d6a8ece7c 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PigEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2019-2025 GeyserMC. http://geysermc.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,7 +23,7 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.entity.type.living.animal; +package org.geysermc.geyser.entity.type.living.animal.farm; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -41,6 +41,8 @@ import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.EntityUtils; @@ -51,7 +53,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; -public class PigEntity extends AnimalEntity implements Tickable, ClientVehicle { +public class PigEntity extends TemperatureVariantAnimal implements Tickable, ClientVehicle { private final BoostableVehicleComponent vehicleComponent = new BoostableVehicleComponent<>(this, 1.0f); public PigEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { @@ -147,4 +149,9 @@ public class PigEntity extends AnimalEntity implements Tickable, ClientVehicle { public boolean isClientControlled() { return getPlayerPassenger() == session.getPlayerEntity() && session.getPlayerInventory().isHolding(Items.CARROT_ON_A_STICK); } + + @Override + public JavaRegistryKey variantRegistry() { + return JavaRegistries.PIG_VARIANT; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java new file mode 100644 index 000000000..7e90e64b6 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java @@ -0,0 +1,66 @@ +/* + * 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.animal.farm; + +import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.properties.VanillaEntityProperties; +import org.geysermc.geyser.entity.type.living.animal.AnimalEntity; +import org.geysermc.geyser.entity.type.living.animal.VariantHolder; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.RegistryCache; + +import java.util.Locale; +import java.util.UUID; + +public abstract class TemperatureVariantAnimal extends AnimalEntity implements VariantHolder { + + public static final RegistryCache.RegistryReader VARIANT_READER = VariantHolder.reader(BuiltInVariant.class, BuiltInVariant.TEMPERATE); + + public TemperatureVariantAnimal(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 + public void addAdditionalSpawnData(AddEntityPacket addEntityPacket) { + propertyManager.add(VanillaEntityProperties.CLIMATE_VARIANT_ID, "temperate"); + propertyManager.applyIntProperties(addEntityPacket.getProperties().getIntProperties()); + } + + @Override + public void setBedrockVariant(BuiltInVariant variant) { + propertyManager.add(VanillaEntityProperties.CLIMATE_VARIANT_ID, variant.name().toLowerCase(Locale.ROOT)); + updateBedrockEntityProperties(); + } + + public enum BuiltInVariant implements BuiltIn { + COLD, + TEMPERATE, + WARM + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java index 100a29299..7b6184579 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java @@ -79,12 +79,17 @@ public class AbstractHorseEntity extends AnimalEntity { session.sendUpstreamPacket(attributesPacket); } + @Override + public void updateSaddled(boolean saddled) { + // Shows the jump meter + setFlag(EntityFlag.CAN_POWER_JUMP, saddled); + super.updateSaddled(saddled); + } + public void setHorseFlags(ByteEntityMetadata entityMetadata) { byte xd = entityMetadata.getPrimitiveValue(); boolean tamed = (xd & 0x02) == 0x02; - boolean saddled = (xd & 0x04) == 0x04; setFlag(EntityFlag.TAMED, tamed); - setFlag(EntityFlag.SADDLED, saddled); setFlag(EntityFlag.EATING, (xd & 0x10) == 0x10); setFlag(EntityFlag.STANDING, (xd & 0x20) == 0x20); @@ -114,9 +119,6 @@ public class AbstractHorseEntity extends AnimalEntity { // Set container type if tamed dirtyMetadata.put(EntityDataTypes.CONTAINER_TYPE, tamed ? (byte) ContainerType.HORSE.getId() : (byte) 0); - - // Shows the jump meter - setFlag(EntityFlag.CAN_POWER_JUMP, saddled); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/CatEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/CatEntity.java index fb53c18ed..e90cb3f1c 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/CatEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/CatEntity.java @@ -31,9 +31,12 @@ import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.type.living.animal.VariantIntHolder; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.InteractionResult; @@ -45,7 +48,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; -public class CatEntity extends TameableEntity { +public class CatEntity extends TameableEntity implements VariantIntHolder { private byte collarColor = 14; // Red - default @@ -81,17 +84,14 @@ public class CatEntity extends TameableEntity { updateCollarColor(); } - public void setCatVariant(IntEntityMetadata entityMetadata) { - // Different colors in Java and Bedrock for some reason - int metadataValue = entityMetadata.getPrimitiveValue(); - int variantColor = switch (metadataValue) { - case 0 -> 8; - case 8 -> 0; - case 9 -> 10; - case 10 -> 9; - default -> metadataValue; - }; - dirtyMetadata.put(EntityDataTypes.VARIANT, variantColor); + @Override + public JavaRegistryKey variantRegistry() { + return JavaRegistries.CAT_VARIANT; + } + + @Override + public void setBedrockVariantId(int bedrockId) { + dirtyMetadata.put(EntityDataTypes.VARIANT, bedrockId); } public void setResting(BooleanEntityMetadata entityMetadata) { @@ -138,4 +138,20 @@ public class CatEntity extends TameableEntity { return !canEat(itemInHand) || health >= maxHealth && tamed ? InteractionResult.PASS : InteractionResult.SUCCESS; } } + + // Ordered by bedrock id + // TODO: are these ordered correctly? + public enum BuiltInVariant implements BuiltIn { + WHITE, + BLACK, + RED, + SIAMESE, + BRITISH_SHORTHAIR, + CALICO, + PERSIAN, + RAGDOLL, + TABBY, + ALL_BLACK, + JELLIE + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java index c8b6a6f58..02edfec3f 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java @@ -30,24 +30,25 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; +import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.type.living.animal.VariantIntHolder; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.enchantment.EnchantmentComponent; import org.geysermc.geyser.item.type.DyeItem; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; import org.geysermc.geyser.util.ItemUtils; -import org.geysermc.mcprotocollib.protocol.data.game.Holder; -import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.WolfVariant; 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.ObjectEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; @@ -55,10 +56,9 @@ import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponen import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet; import java.util.Collections; -import java.util.Locale; import java.util.UUID; -public class WolfEntity extends TameableEntity { +public class WolfEntity extends TameableEntity implements VariantIntHolder { private byte collarColor = 14; // Red - default private HolderSet repairableItems = null; private boolean isCurseOfBinding = false; @@ -67,6 +67,12 @@ public class WolfEntity extends TameableEntity { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } + @Override + public void addAdditionalSpawnData(AddEntityPacket addEntityPacket) { + propertyManager.add("minecraft:sound_variant", "default"); + propertyManager.applyIntProperties(addEntityPacket.getProperties().getIntProperties()); + } + @Override public void setTameableFlags(ByteEntityMetadata entityMetadata) { super.setTameableFlags(entityMetadata); @@ -107,15 +113,14 @@ public class WolfEntity extends TameableEntity { dirtyMetadata.put(EntityDataTypes.COLOR, time != 0 ? (byte) 0 : collarColor); } - // 1.20.5+ - public void setWolfVariant(ObjectEntityMetadata> entityMetadata) { - entityMetadata.getValue().ifId(id -> { - BuiltInWolfVariant wolfVariant = session.getRegistryCache().wolfVariants().byId(id); - if (wolfVariant == null) { - wolfVariant = BuiltInWolfVariant.PALE; - } - dirtyMetadata.put(EntityDataTypes.VARIANT, wolfVariant.ordinal()); - }); + @Override + public JavaRegistryKey variantRegistry() { + return JavaRegistries.WOLF_VARIANT; + } + + @Override + public void setBedrockVariantId(int bedrockId) { + dirtyMetadata.put(EntityDataTypes.VARIANT, bedrockId); } @Override @@ -188,7 +193,7 @@ public class WolfEntity extends TameableEntity { } // Ordered by bedrock id - public enum BuiltInWolfVariant { + public enum BuiltInVariant implements BuiltIn { PALE, ASHEN, BLACK, @@ -197,23 +202,6 @@ public class WolfEntity extends TameableEntity { SNOWY, SPOTTED, STRIPED, - WOODS; - - private static final BuiltInWolfVariant[] VALUES = values(); - - private final String javaIdentifier; - - BuiltInWolfVariant() { - this.javaIdentifier = "minecraft:" + this.name().toLowerCase(Locale.ROOT); - } - - public static @Nullable BuiltInWolfVariant getByJavaIdentifier(String javaIdentifier) { - for (BuiltInWolfVariant wolfVariant : VALUES) { - if (wolfVariant.javaIdentifier.equals(javaIdentifier)) { - return wolfVariant; - } - } - return null; - } + WOODS } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/EndermanEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/EndermanEntity.java index 586ba5cd9..94ff657d2 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/EndermanEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/EndermanEntity.java @@ -30,7 +30,7 @@ import org.cloudburstmc.protocol.bedrock.data.SoundEvent; import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; -import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEvent2Packet; +import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; @@ -57,7 +57,7 @@ public class EndermanEntity extends MonsterEntity { //TODO see if Bedrock controls this differently // Java Edition this controls which ambient sound is used if (entityMetadata.getPrimitiveValue()) { - LevelSoundEvent2Packet packet = new LevelSoundEvent2Packet(); + LevelSoundEventPacket packet = new LevelSoundEventPacket(); packet.setSound(SoundEvent.STARE); packet.setPosition(this.position); packet.setExtraData(-1); diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java index c7060d3e4..e0422036f 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java @@ -36,9 +36,11 @@ import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.packet.MovePlayerPacket; import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket; +import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.attribute.GeyserAttributeType; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.level.BedrockDimension; +import org.geysermc.geyser.level.block.Blocks; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.AttributeUtils; import org.geysermc.geyser.util.DimensionUtils; @@ -88,6 +90,12 @@ public class SessionPlayerEntity extends PlayerEntity { private int lastAirSupply = getMaxAir(); + /** + * The client last tick end velocity, used for calculating player onGround. + */ + @Getter @Setter + private Vector3f lastTickEndVelocity = Vector3f.ZERO; + /** * Determines if our position is currently out-of-sync with the Java server * due to our workaround for the void floor @@ -407,4 +415,17 @@ public class SessionPlayerEntity extends PlayerEntity { movePlayerPacket.setTeleportationCause(MovePlayerPacket.TeleportationCause.BEHAVIOR); session.sendUpstreamPacketImmediately(movePlayerPacket); } + + /** + * Used to calculate player jumping velocity for ground status calculation. + */ + public float getJumpVelocity() { + float velocity = 0.42F; + + if (session.getGeyser().getWorldManager().blockAt(session, this.getPosition().sub(0, EntityDefinitions.PLAYER.offset() + 0.1F, 0).toInt()).is(Blocks.HONEY_BLOCK)) { + velocity *= 0.6F; + } + + return velocity + 0.1F * session.getEffectCache().getJumpPower(); + } } diff --git a/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java b/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java new file mode 100644 index 000000000..2e62a4482 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.event.type; + +import lombok.Getter; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.event.lifecycle.GeyserDefineResourcePacksEvent; +import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.exception.ResourcePackException; +import org.geysermc.geyser.api.pack.option.ResourcePackOption; +import org.geysermc.geyser.pack.GeyserResourcePack; +import org.geysermc.geyser.pack.ResourcePackHolder; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +@Getter +public class GeyserDefineResourcePacksEventImpl extends GeyserDefineResourcePacksEvent { + private final Map packs; + + public GeyserDefineResourcePacksEventImpl(Map packMap) { + this.packs = packMap; + } + + @Override + public @NonNull List resourcePacks() { + return packs.values().stream().map(ResourcePackHolder::resourcePack).toList(); + } + + @Override + public void register(@NonNull ResourcePack resourcePack, @Nullable ResourcePackOption... options) { + Objects.requireNonNull(resourcePack, "resource pack must not be null!"); + if (!(resourcePack instanceof GeyserResourcePack pack)) { + throw new ResourcePackException(ResourcePackException.Cause.UNKNOWN_IMPLEMENTATION); + } + + UUID uuid = resourcePack.uuid(); + if (packs.containsKey(uuid)) { + throw new ResourcePackException(ResourcePackException.Cause.DUPLICATE); + } + + ResourcePackHolder holder = ResourcePackHolder.of(pack); + attemptRegisterOptions(holder, options); + packs.put(uuid, holder); + } + + @Override + public void registerOptions(@NonNull UUID uuid, @NonNull ResourcePackOption... options) { + Objects.requireNonNull(uuid); + Objects.requireNonNull(options); + + ResourcePackHolder holder = packs.get(uuid); + if (holder == null) { + throw new ResourcePackException(ResourcePackException.Cause.PACK_NOT_FOUND); + } + + attemptRegisterOptions(holder, options); + } + + @Override + public Collection> options(@NonNull UUID uuid) { + Objects.requireNonNull(uuid); + ResourcePackHolder packHolder = packs.get(uuid); + if (packHolder == null) { + throw new ResourcePackException(ResourcePackException.Cause.PACK_NOT_FOUND); + } + + return packHolder.optionHolder().immutableValues(); + } + + @Override + public @Nullable ResourcePackOption option(@NonNull UUID uuid, ResourcePackOption.@NonNull Type type) { + Objects.requireNonNull(uuid); + Objects.requireNonNull(type); + + ResourcePackHolder packHolder = packs.get(uuid); + if (packHolder == null) { + throw new ResourcePackException(ResourcePackException.Cause.PACK_NOT_FOUND); + } + + return packHolder.optionHolder().get(type); + } + + @Override + public void unregister(@NonNull UUID uuid) { + packs.remove(uuid); + } + + private void attemptRegisterOptions(@NonNull ResourcePackHolder holder, @Nullable ResourcePackOption... options) { + if (options == null) { + return; + } + + holder.optionHolder().validateAndAdd(holder.pack(), options); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java b/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java index 5bc0dd0bd..a926e5400 100644 --- a/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java +++ b/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java @@ -25,45 +25,200 @@ package org.geysermc.geyser.event.type; +import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import lombok.Getter; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.protocol.bedrock.packet.ResourcePackStackPacket; +import org.cloudburstmc.protocol.bedrock.packet.ResourcePacksInfoPacket; +import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.event.bedrock.SessionLoadResourcePacksEvent; import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.ResourcePackManifest; +import org.geysermc.geyser.api.pack.exception.ResourcePackException; +import org.geysermc.geyser.api.pack.option.PriorityOption; +import org.geysermc.geyser.api.pack.option.ResourcePackOption; +import org.geysermc.geyser.pack.GeyserResourcePack; +import org.geysermc.geyser.pack.ResourcePackHolder; +import org.geysermc.geyser.pack.option.OptionHolder; +import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.session.GeyserSession; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.UUID; public class SessionLoadResourcePacksEventImpl extends SessionLoadResourcePacksEvent { - private final Map packs; + /** + * The packs for this Session. A {@link ResourcePackHolder} may contain resource pack options registered + * during the {@link org.geysermc.geyser.api.event.lifecycle.GeyserDefineResourcePacksEvent}. + */ + @Getter + private final Map packs; - public SessionLoadResourcePacksEventImpl(GeyserSession session, Map packMap) { + /** + * The additional, per-session options for the resource packs of this session. + * These options are prioritized over the "default" options registered + * in the {@link org.geysermc.geyser.api.event.lifecycle.GeyserDefineResourcePacksEvent} + */ + private final Map sessionPackOptionOverrides; + + public SessionLoadResourcePacksEventImpl(GeyserSession session) { super(session); - this.packs = packMap; - } - - public @NonNull Map getPacks() { - return packs; + this.packs = new Object2ObjectLinkedOpenHashMap<>(Registries.RESOURCE_PACKS.get()); + this.sessionPackOptionOverrides = new Object2ObjectOpenHashMap<>(); } @Override public @NonNull List resourcePacks() { - return List.copyOf(packs.values()); + return packs.values().stream().map(ResourcePackHolder::resourcePack).toList(); } @Override public boolean register(@NonNull ResourcePack resourcePack) { - UUID packID = resourcePack.manifest().header().uuid(); - if (packs.containsValue(resourcePack) || packs.containsKey(packID)) { + try { + register(resourcePack, PriorityOption.NORMAL); + } catch (ResourcePackException e) { + GeyserImpl.getInstance().getLogger().error("An exception occurred while registering resource pack: " + e.getMessage(), e); return false; } - packs.put(resourcePack.manifest().header().uuid(), resourcePack); return true; } + @Override + public void register(@NonNull ResourcePack resourcePack, @Nullable ResourcePackOption... options) { + Objects.requireNonNull(resourcePack); + if (!(resourcePack instanceof GeyserResourcePack pack)) { + throw new ResourcePackException(ResourcePackException.Cause.UNKNOWN_IMPLEMENTATION); + } + + UUID uuid = resourcePack.uuid(); + if (packs.containsKey(uuid)) { + throw new ResourcePackException(ResourcePackException.Cause.DUPLICATE); + } + + attemptRegisterOptions(pack, options); + packs.put(uuid, ResourcePackHolder.of(pack)); + } + + @Override + public void registerOptions(@NonNull UUID uuid, @NonNull ResourcePackOption... options) { + Objects.requireNonNull(uuid, "uuid cannot be null"); + Objects.requireNonNull(options, "options cannot be null"); + ResourcePackHolder holder = packs.get(uuid); + if (holder == null) { + throw new ResourcePackException(ResourcePackException.Cause.PACK_NOT_FOUND); + } + + attemptRegisterOptions(holder.pack(), options); + } + + @Override + public Collection> options(@NonNull UUID uuid) { + Objects.requireNonNull(uuid); + ResourcePackHolder packHolder = packs.get(uuid); + if (packHolder == null) { + throw new ResourcePackException(ResourcePackException.Cause.PACK_NOT_FOUND); + } + + OptionHolder optionHolder = sessionPackOptionOverrides.get(uuid); + if (optionHolder == null) { + // No need to create a new session option holder + return packHolder.optionHolder().immutableValues(); + } + + return optionHolder.immutableValues(packHolder.optionHolder()); + } + + @Override + public @Nullable ResourcePackOption option(@NonNull UUID uuid, ResourcePackOption.@NonNull Type type) { + Objects.requireNonNull(uuid); + Objects.requireNonNull(type); + + ResourcePackHolder packHolder = packs.get(uuid); + if (packHolder == null) { + throw new ResourcePackException(ResourcePackException.Cause.PACK_NOT_FOUND); + } + + @Nullable OptionHolder additionalOptions = sessionPackOptionOverrides.get(uuid); + OptionHolder defaultHolder = packHolder.optionHolder(); + Objects.requireNonNull(defaultHolder); // should never be null + + return OptionHolder.optionByType(type, additionalOptions, defaultHolder); + } + @Override public boolean unregister(@NonNull UUID uuid) { + sessionPackOptionOverrides.remove(uuid); return packs.remove(uuid) != null; } + + private void attemptRegisterOptions(@NonNull GeyserResourcePack pack, @Nullable ResourcePackOption... options) { + if (options == null) { + return; + } + + OptionHolder holder = this.sessionPackOptionOverrides.computeIfAbsent(pack.uuid(), $ -> new OptionHolder()); + holder.validateAndAdd(pack, options); + } + + // Methods used internally for e.g. ordered packs, or resource pack entries + + public List orderedPacks() { + return packs.values().stream() + // Map each ResourcePack to a pair of (GeyserResourcePack, Priority) + .map(holder -> new AbstractMap.SimpleEntry<>(holder.pack(), priority(holder.pack()))) + // Sort by priority in descending order + .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())) + // Map the sorted entries to ResourcePackStackPacket.Entry + .map(entry -> { + ResourcePackManifest.Header header = entry.getKey().manifest().header(); + return new ResourcePackStackPacket.Entry( + header.uuid().toString(), + header.version().toString(), + subpackName(entry.getKey()) + ); + }) + .toList(); + } + + public List infoPacketEntries() { + List entries = new ArrayList<>(); + + for (ResourcePackHolder holder : packs.values()) { + GeyserResourcePack pack = holder.pack(); + ResourcePackManifest.Header header = pack.manifest().header(); + entries.add(new ResourcePacksInfoPacket.Entry( + header.uuid(), header.version().toString(), pack.codec().size(), pack.contentKey(), + subpackName(pack), header.uuid().toString(), false, false, false, subpackName(pack)) + ); + } + + return entries; + } + + // Helper methods to get the options for a ResourcePack + + public T value(UUID uuid, ResourcePackOption.Type type, T defaultValue) { + OptionHolder holder = sessionPackOptionOverrides.get(uuid); + OptionHolder defaultHolder = packs.get(uuid).optionHolder(); + Objects.requireNonNull(defaultHolder); // should never be null + + return OptionHolder.valueOrFallback(type, holder, defaultHolder, defaultValue); + } + + private double priority(GeyserResourcePack pack) { + return value(pack.uuid(), ResourcePackOption.Type.PRIORITY, PriorityOption.NORMAL.value()); + } + + private String subpackName(GeyserResourcePack pack) { + return value(pack.uuid(), ResourcePackOption.Type.SUBPACK, ""); + } } diff --git a/core/src/main/java/org/geysermc/geyser/impl/camera/GeyserCameraData.java b/core/src/main/java/org/geysermc/geyser/impl/camera/GeyserCameraData.java index beb10db86..759828744 100644 --- a/core/src/main/java/org/geysermc/geyser/impl/camera/GeyserCameraData.java +++ b/core/src/main/java/org/geysermc/geyser/impl/camera/GeyserCameraData.java @@ -272,7 +272,12 @@ public class GeyserCameraData implements CameraData { elementSet.add(HUD_ELEMENT_VALUES[element.id()]); } - session.sendUpstreamPacket(packet); + if (session.isSentSpawnPacket()) { + session.sendUpstreamPacket(packet); + } else { + // Ensures hidden GUI elements properly hide when we spawn in the spectator gamemode + session.getUpstream().queuePostStartGamePacket(packet); + } } @Override diff --git a/core/src/main/java/org/geysermc/geyser/inventory/AnvilContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/AnvilContainer.java index fe760c75a..cc852d3bf 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/AnvilContainer.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/AnvilContainer.java @@ -62,8 +62,8 @@ public class AnvilContainer extends Container { private int lastTargetSlot = -1; - public AnvilContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType, playerInventory); + public AnvilContainer(GeyserSession session, String title, int id, int size, ContainerType containerType) { + super(session, title, id, size, containerType); } /** diff --git a/core/src/main/java/org/geysermc/geyser/inventory/BeaconContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/BeaconContainer.java index 1b59772fa..09c31d6b6 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/BeaconContainer.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/BeaconContainer.java @@ -25,9 +25,10 @@ package org.geysermc.geyser.inventory; -import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; import lombok.Getter; import lombok.Setter; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; @Getter @Setter @@ -35,7 +36,7 @@ public class BeaconContainer extends Container { private int primaryId; private int secondaryId; - public BeaconContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType, playerInventory); + public BeaconContainer(GeyserSession session, String title, int id, int size, ContainerType containerType) { + super(session, title, id, size, containerType); } } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/CartographyContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/CartographyContainer.java index ace3f93ad..f4a47daa0 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/CartographyContainer.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/CartographyContainer.java @@ -25,10 +25,11 @@ package org.geysermc.geyser.inventory; +import org.geysermc.geyser.session.GeyserSession; import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; public class CartographyContainer extends Container { - public CartographyContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType, playerInventory); + public CartographyContainer(GeyserSession session, String title, int id, int size, ContainerType containerType) { + super(session, title, id, size, containerType); } } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/Container.java b/core/src/main/java/org/geysermc/geyser/inventory/Container.java index f2db415c0..16c5d5344 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/Container.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/Container.java @@ -46,9 +46,9 @@ public class Container extends Inventory { */ private boolean isUsingRealBlock = false; - public Container(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType); - this.playerInventory = playerInventory; + public Container(GeyserSession session, String title, int id, int size, ContainerType containerType) { + super(session, title, id, size, containerType); + this.playerInventory = session.getPlayerInventory(); this.containerSize = this.size + InventoryTranslator.PLAYER_INVENTORY_SIZE; } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/CrafterContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/CrafterContainer.java index fb118252d..664788b11 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/CrafterContainer.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/CrafterContainer.java @@ -48,8 +48,8 @@ public class CrafterContainer extends Container { */ private short disabledSlotsMask = 0; - public CrafterContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType, playerInventory); + public CrafterContainer(GeyserSession session, String title, int id, int size, ContainerType containerType) { + super(session, title, id, size, containerType); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/inventory/EnchantingContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/EnchantingContainer.java index 08397ab44..9c88ab231 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/EnchantingContainer.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/EnchantingContainer.java @@ -25,24 +25,24 @@ package org.geysermc.geyser.inventory; +import org.geysermc.geyser.session.GeyserSession; import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; import org.cloudburstmc.protocol.bedrock.data.inventory.EnchantOptionData; import lombok.Getter; +@Getter public class EnchantingContainer extends Container { /** * A cache of what Bedrock sees */ - @Getter private final EnchantOptionData[] enchantOptions; /** * A mutable cache of what the server sends us */ - @Getter private final GeyserEnchantOption[] geyserEnchantOptions; - public EnchantingContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType, playerInventory); + public EnchantingContainer(GeyserSession session, String title, int id, int size, ContainerType containerType) { + super(session, title, id, size, containerType); enchantOptions = new EnchantOptionData[3]; geyserEnchantOptions = new GeyserEnchantOption[3]; diff --git a/core/src/main/java/org/geysermc/geyser/inventory/Generic3X3Container.java b/core/src/main/java/org/geysermc/geyser/inventory/Generic3X3Container.java index 0b14d1105..f84c8bb91 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/Generic3X3Container.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/Generic3X3Container.java @@ -32,17 +32,17 @@ import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.inventory.Generic3X3InventoryTranslator; import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; +@Getter public class Generic3X3Container extends Container { /** * Whether we need to set the container type as {@link org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType#DROPPER}. *

- * Used at {@link Generic3X3InventoryTranslator#openInventory(GeyserSession, Inventory)} + * Used at {@link Generic3X3InventoryTranslator#openInventory(GeyserSession, Generic3X3Container)} */ - @Getter private boolean isDropper = false; - public Generic3X3Container(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType, playerInventory); + public Generic3X3Container(GeyserSession session, String title, int id, int size, ContainerType containerType) { + super(session, title, id, size, containerType); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/inventory/Generic9X3Container.java b/core/src/main/java/org/geysermc/geyser/inventory/Generic9X3Container.java new file mode 100644 index 000000000..aeea15c70 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/inventory/Generic9X3Container.java @@ -0,0 +1,55 @@ +/* + * 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.inventory; + +import lombok.Getter; +import lombok.Setter; +import org.geysermc.geyser.level.block.Blocks; +import org.geysermc.geyser.level.block.type.Block; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; + +/** + * A "hack" to be able to use existing barrels. + * The only difference to chests appears to be the different ContainerSlotType - this accounts for it. + */ +@Getter @Setter +public class Generic9X3Container extends Container { + + private boolean isBarrel; + + public Generic9X3Container(GeyserSession session, String title, int id, int size, ContainerType containerType) { + super(session, title, id, size, containerType); + } + + @Override + public void setUsingRealBlock(boolean usingRealBlock, Block block) { + super.setUsingRealBlock(usingRealBlock, block); + if (usingRealBlock) { + isBarrel = block == Blocks.BARREL; + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java b/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java index 2c0c2798d..e0e1aa93f 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java @@ -32,6 +32,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.inventory.click.ClickPlan; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.item.ItemTranslator; @@ -46,6 +47,10 @@ public abstract class Inventory { @Getter protected final int javaId; + @Setter + @Getter + private int bedrockId; + /** * The Java inventory state ID from the server. As of Java Edition 1.18.1 this value has one instance per player. * If this is out of sync with the server when a packet containing it is handled, the server will resync items. @@ -55,7 +60,7 @@ public abstract class Inventory { @Setter private int stateId; /** - * See {@link org.geysermc.geyser.inventory.click.ClickPlan#execute(boolean)}; used as a hack + * See {@link ClickPlan#execute(boolean)}; used as a hack */ @Getter private int nextStateId = -1; @@ -75,43 +80,52 @@ public abstract class Inventory { protected final GeyserItemStack[] items; /** - * The location of the inventory block. Will either be a fake block above the player's head, or the actual block location + * The location of the inventory block. Will either be a fake block above the player's head, or the actual block location. */ @Getter @Setter protected Vector3i holderPosition = Vector3i.ZERO; + /** + * The entity id of the entity holding the inventory. + * Either this, or the holder position must be set in order for Bedrock to open inventories. + */ @Getter @Setter protected long holderId = -1; + /** + * Whether this inventory is currently shown to the Bedrock player. + */ @Getter @Setter - private boolean pending = false; + private boolean displayed; - @Getter - @Setter - private boolean displayed = false; - - protected Inventory(int id, int size, ContainerType containerType) { - this("Inventory", id, size, containerType); + protected Inventory(GeyserSession session, int id, int size, ContainerType containerType) { + this(session, "Inventory", id, size, containerType); } - protected Inventory(String title, int javaId, int size, ContainerType containerType) { + protected Inventory(GeyserSession session, String title, int javaId, int size, ContainerType containerType) { this.title = title; this.javaId = javaId; this.size = size; this.containerType = containerType; this.items = new GeyserItemStack[size]; Arrays.fill(items, GeyserItemStack.EMPTY); - } - // This is to prevent conflicts with special bedrock inventory IDs. - // The vanilla java server only sends an ID between 1 and 100 when opening an inventory, - // so this is rarely needed. (certain plugins) - // Example: https://github.com/GeyserMC/Geyser/issues/3254 - public int getBedrockId() { - return javaId <= 100 ? javaId : (javaId % 100) + 1; + // This is to prevent conflicts with special bedrock inventory IDs. + // The vanilla java server only sends an ID between 1 and 100 when opening an inventory, + // so this is rarely needed. (certain plugins) + // Example: https://github.com/GeyserMC/Geyser/issues/3254 + this.bedrockId = javaId <= 100 ? javaId : (javaId % 100) + 1; + + // We occasionally need to re-open inventories with a delay in cases where + // Java wouldn't - e.g. for virtual chest menus that switch pages. + // And, well, we want to avoid reusing Bedrock inventory id's that are currently being used in a closing inventory; + // so to be safe we just deviate in that case as well. + if ((session.getInventoryHolder() != null && session.getInventoryHolder().bedrockId() == bedrockId) || session.isClosingInventory()) { + this.bedrockId += 1; + } } public GeyserItemStack getItem(int slot) { diff --git a/core/src/main/java/org/geysermc/geyser/inventory/InventoryHolder.java b/core/src/main/java/org/geysermc/geyser/inventory/InventoryHolder.java new file mode 100644 index 000000000..8d822749b --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/inventory/InventoryHolder.java @@ -0,0 +1,167 @@ +/* + * 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.inventory; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.ItemStackRequest; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.inventory.InventoryTranslator; + +import java.util.List; + +/** + * A helper class storing the current inventory, translator, and session. + */ +@Accessors(fluent = true) +@Getter +public final class InventoryHolder { + private final GeyserSession session; + private final T inventory; + private final InventoryTranslator translator; + + /** + * Whether this inventory is currently pending. + * It can be pending if this inventory was opened while another inventory was still open, + * or because opening this inventory takes more time (e.g. virtual inventories). + */ + @Setter + private boolean pending; + + /** + * Stores the number of attempts to open virtual inventories. + * Capped at 3, and isn't used in ideal circumstances. + * Used to resolve container closing issues. + */ + @Setter + private int containerOpenAttempts; + + @SuppressWarnings("unchecked") + public InventoryHolder(GeyserSession session, Inventory newInventory, InventoryTranslator newTranslator) { + this.session = session; + this.inventory = (T) newInventory; + this.translator = (InventoryTranslator) newTranslator; + } + + public void markCurrent() { + this.session.setInventoryHolder(this); + } + + public boolean shouldSetPending() { + return session.isClosingInventory() || !session.getUpstream().isInitialized() || session.getPendingOrCurrentBedrockInventoryId() != -1; + } + + public boolean shouldConfirmClose(boolean confirm) { + return confirm && inventory.isDisplayed() && !pending; + } + + public void inheritFromExisting(InventoryHolder existing) { + // Mirror Bedrock id + inventory.setBedrockId(existing.bedrockId()); + + // Also mirror other properties - in case we're e.g. dealing with a pending virtual inventory + Inventory existingInventory = existing.inventory; + this.pending = existing.pending(); + inventory.setDisplayed(existingInventory.isDisplayed()); + inventory.setHolderPosition(existingInventory.getHolderPosition()); + inventory.setHolderId(existingInventory.getHolderId()); + this.markCurrent(); + } + + /* + * Helper methods to avoid using the wrong translator to update specific inventories. + */ + + public void updateInventory() { + this.translator.updateInventory(session, inventory); + } + + public void updateProperty(int rawProperty, int value) { + this.translator.updateProperty(session, inventory, rawProperty, value); + } + + public void updateSlot(int slot) { + this.translator.updateSlot(session, inventory, slot); + } + + public void openInventory() { + this.translator.openInventory(session, inventory); + this.pending = false; + this.inventory.setDisplayed(true); + } + + public void closeInventory(boolean force) { + this.translator.closeInventory(session, inventory, force); + if (session.getContainerOutputFuture() != null) { + session.getContainerOutputFuture().cancel(true); + } + } + + public boolean requiresOpeningDelay() { + return this.translator.requiresOpeningDelay(session, inventory); + } + + public boolean prepareInventory() { + return this.translator.prepareInventory(session, inventory); + } + + public void translateRequests(List requests) { + this.translator.translateRequests(session, inventory, requests); + } + + public GeyserSession session() { + return session; + } + + public T inventory() { + return inventory; + } + + public InventoryTranslator translator() { + return translator; + } + + public void incrementContainerOpenAttempts() { + this.containerOpenAttempts++; + } + + public int javaId() { + return inventory.getJavaId(); + } + + public int bedrockId() { + return inventory.getBedrockId(); + } + + @Override + public String toString() { + return "InventoryHolder[" + + "session=" + session + ", " + + "inventory=" + inventory + ", " + + "translator=" + translator + ']'; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/inventory/LecternContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/LecternContainer.java index 9988188f1..e828698f9 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/LecternContainer.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/LecternContainer.java @@ -27,8 +27,6 @@ package org.geysermc.geyser.inventory; import lombok.Getter; import lombok.Setter; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.nbt.NbtMap; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.protocol.java.inventory.JavaOpenBookTranslator; @@ -40,33 +38,18 @@ public class LecternContainer extends Container { private int currentBedrockPage = 0; @Setter private NbtMap blockEntityTag; - @Setter - private Vector3i position; private boolean isBookInPlayerInventory = false; - public LecternContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType, playerInventory); - } - - /** - * When the Java server asks the client to open a book in their hotbar, we create a fake lectern to show it to the client. - * We can't use the {@link #isUsingRealBlock()} check as we may also be dealing with a real virtual lectern (with its own inventory). - */ - @Override - public void setItem(int slot, @NonNull GeyserItemStack newItem, GeyserSession session) { - if (isBookInPlayerInventory) { - session.getPlayerInventory().setItem(slot, newItem, session); - } else { - super.setItem(slot, newItem, session); - } + public LecternContainer(GeyserSession session, String title, int id, int size, ContainerType containerType) { + super(session, title, id, size, containerType); } /** * This is used ONLY once to set the book of a fake lectern in {@link JavaOpenBookTranslator}. * See {@link LecternContainer#setItem(int, GeyserItemStack, GeyserSession)} as for why this is separate. */ - public void setFakeLecternBook(GeyserItemStack book, GeyserSession session) { + public void setVirtualLecternBook(GeyserItemStack book, GeyserSession session) { this.isBookInPlayerInventory = true; super.setItem(0, book, session); } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/MerchantContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/MerchantContainer.java index 0bfa6d1a7..be0e1cd60 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/MerchantContainer.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/MerchantContainer.java @@ -34,24 +34,26 @@ import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; import org.geysermc.mcprotocollib.protocol.data.game.inventory.VillagerTrade; import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.inventory.ClientboundMerchantOffersPacket; +import java.util.List; + +@Setter public class MerchantContainer extends Container { - @Getter @Setter + @Getter private Entity villager; - @Setter - private VillagerTrade[] villagerTrades; - @Getter @Setter + private List villagerTrades; + @Getter private ClientboundMerchantOffersPacket pendingOffersPacket; - @Getter @Setter + @Getter private int tradeExperience; - public MerchantContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType, playerInventory); + public MerchantContainer(GeyserSession session, String title, int id, int size, ContainerType containerType) { + super(session, title, id, size, containerType); } public void onTradeSelected(GeyserSession session, int slot) { - if (villagerTrades != null && slot >= 0 && slot < villagerTrades.length) { - VillagerTrade trade = villagerTrades[slot]; - setItem(2, GeyserItemStack.from(trade.getOutput()), session); + if (villagerTrades != null && slot >= 0 && slot < villagerTrades.size()) { + VillagerTrade trade = villagerTrades.get(slot); + setItem(2, GeyserItemStack.from(trade.getResult()), session); tradeExperience += trade.getXp(); villager.getDirtyMetadata().put(EntityDataTypes.TRADE_EXPERIENCE, tradeExperience); diff --git a/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java b/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java index 3ea9cd112..a3af293d9 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java @@ -34,21 +34,20 @@ import org.geysermc.geyser.session.GeyserSession; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import org.jetbrains.annotations.Range; +@Getter public class PlayerInventory extends Inventory { /** * Stores the held item slot, starting at index 0. * Add 36 in order to get the network item slot. */ - @Getter @Setter private int heldItemSlot; - @Getter @NonNull private GeyserItemStack cursor = GeyserItemStack.EMPTY; - public PlayerInventory() { - super(0, 46, null); + public PlayerInventory(GeyserSession session) { + super(session, 0, 46, null); heldItemSlot = 0; } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/StonecutterContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/StonecutterContainer.java index 1eb115847..e9d884cdd 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/StonecutterContainer.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/StonecutterContainer.java @@ -31,16 +31,16 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; +@Setter +@Getter public class StonecutterContainer extends Container { /** * The button that has currently been pressed Java-side */ - @Getter - @Setter private int stonecutterButton = -1; - public StonecutterContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { - super(title, id, size, containerType, playerInventory); + public StonecutterContainer(GeyserSession session, String title, int id, int size, ContainerType containerType) { + super(session, title, id, size, containerType); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java b/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java index b5929add3..110da3b0d 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java @@ -32,6 +32,7 @@ import it.unimi.dsi.fastutil.ints.IntSet; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.inventory.SlotType; +import org.geysermc.geyser.item.hashing.DataComponentHashers; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.inventory.BundleInventoryTranslator; import org.geysermc.geyser.translator.inventory.CraftingInventoryTranslator; @@ -41,6 +42,7 @@ import org.geysermc.geyser.util.thirdparty.Fraction; import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerActionType; import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; import org.geysermc.mcprotocollib.protocol.data.game.inventory.MoveToHotbarAction; +import org.geysermc.mcprotocollib.protocol.data.game.item.HashedStack; import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundSelectBundleItemPacket; @@ -58,22 +60,24 @@ public final class ClickPlan { * Used for 1.17.1+ proper packet translation - any non-cursor item that is changed in a single transaction gets sent here. */ private Int2ObjectMap changedItems; + private Int2ObjectMap changedHashedItems; private GeyserItemStack simulatedCursor; private int desiredBundleSlot; private boolean executionBegan; private final GeyserSession session; - private final InventoryTranslator translator; + private final InventoryTranslator translator; private final Inventory inventory; private final int gridSize; - public ClickPlan(GeyserSession session, InventoryTranslator translator, Inventory inventory) { + public ClickPlan(GeyserSession session, InventoryTranslator translator, Inventory inventory) { this.session = session; this.translator = translator; this.inventory = inventory; this.simulatedItems = new Int2ObjectOpenHashMap<>(inventory.getSize()); this.changedItems = null; + this.changedHashedItems = null; this.simulatedCursor = session.getPlayerInventory().getCursor().copy(); this.executionBegan = false; @@ -118,6 +122,7 @@ public final class ClickPlan { } changedItems = new Int2ObjectOpenHashMap<>(); + changedHashedItems = new Int2ObjectOpenHashMap<>(); boolean emulatePost1_16Logic = session.isEmulatePost1_16Logic(); @@ -157,8 +162,8 @@ public final class ClickPlan { action.slot, action.click.actionType, action.click.action, - clickedItemStack, - changedItems + DataComponentHashers.hashStack(session, clickedItemStack), + changedHashedItems ); session.sendDownstreamGamePacket(clickPacket); @@ -175,6 +180,7 @@ public final class ClickPlan { //update geyser inventory after simulation to avoid net id desync resetSimulation(); changedItems = new Int2ObjectOpenHashMap<>(); + changedHashedItems = new Int2ObjectOpenHashMap<>(); for (ClickAction action : plan) { simulateAction(action); } @@ -254,6 +260,7 @@ public final class ClickPlan { private void onSlotItemChange(int slot, GeyserItemStack itemStack) { if (changedItems != null) { changedItems.put(slot, itemStack.getItemStack()); + changedHashedItems.put(slot, DataComponentHashers.hashStack(session, itemStack.getItemStack())); } } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/holder/BlockInventoryHolder.java b/core/src/main/java/org/geysermc/geyser/inventory/holder/BlockInventoryHolder.java index 1bf30bc7e..aac51a225 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/holder/BlockInventoryHolder.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/holder/BlockInventoryHolder.java @@ -40,11 +40,11 @@ import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.util.InventoryUtils; import java.util.Collections; import java.util.HashSet; +import java.util.Objects; import java.util.Set; /** @@ -77,24 +77,34 @@ public class BlockInventoryHolder extends InventoryHolder { } @Override - public boolean prepareInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { - // Check to see if there is an existing block we can use that the player just selected. - // First, verify that the player's position has not changed, so we don't try to select a block wildly out of range. - // (This could be a virtual inventory that the player is opening) - if (checkInteractionPosition(session)) { - // Then, check to see if the interacted block is valid for this inventory by ensuring the block state identifier is valid - // and the bedrock block is vanilla - BlockState state = session.getGeyser().getWorldManager().blockAt(session, session.getLastInteractionBlockPosition()); - if (!BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.get().containsKey(state.javaId())) { - if (isValidBlock(state)) { - // We can safely use this block - inventory.setHolderPosition(session.getLastInteractionBlockPosition()); - ((Container) inventory).setUsingRealBlock(true, state.block()); - setCustomName(session, session.getLastInteractionBlockPosition(), inventory, state); + public boolean canReuseContainer(GeyserSession session, Container container, Container previous) { + // We already ensured that the inventories are using the same type, size, and title - return true; - } - } + // While we could reuse real blocks for virtual inventories, + // it can result in unpleasant visual artifacts with specific plugins. + // Specifically - a few plugins send multiple ClientboundOpenScreen packets + // with different titles; where Geyser needs to re-open the menu fully in order to get + // the correct title to appear. The additional delay added by using virtual blocks masks + // the quick closing of the first packet. + if (previous.isUsingRealBlock()) { + return false; + } + + // Check if we'd be using the same virtual inventory position. + Vector3i position = InventoryUtils.findAvailableWorldSpace(session); + if (Objects.equals(position, previous.getHolderPosition())) { + return true; + } else { + GeyserImpl.getInstance().getLogger().debug(session, "Not reusing inventory due to virtual block holder changing (%s -> %s)!", + previous.getHolderPosition(), position); + return false; + } + } + + @Override + public boolean prepareInventory(GeyserSession session, Container container) { + if (canUseRealBlock(session, container)) { + return true; } Vector3i position = InventoryUtils.findAvailableWorldSpace(session); @@ -108,13 +118,36 @@ public class BlockInventoryHolder extends InventoryHolder { blockPacket.setDefinition(session.getBlockMappings().getVanillaBedrockBlock(defaultJavaBlockState)); blockPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY); session.sendUpstreamPacket(blockPacket); - inventory.setHolderPosition(position); + container.setHolderPosition(position); - setCustomName(session, position, inventory, defaultJavaBlockState); + setCustomName(session, position, container, defaultJavaBlockState); return true; } + protected boolean canUseRealBlock(GeyserSession session, Container container) { + // Check to see if there is an existing block we can use that the player just selected. + // First, verify that the player's position has not changed, so we don't try to select a block wildly out of range. + // (This could be a virtual inventory that the player is opening) + if (checkInteractionPosition(session)) { + // Then, check to see if the interacted block is valid for this inventory by ensuring the block state identifier is valid + // and the bedrock block is vanilla + BlockState state = session.getGeyser().getWorldManager().blockAt(session, session.getLastInteractionBlockPosition()); + if (!BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.get().containsKey(state.javaId())) { + if (isValidBlock(state)) { + // We can safely use this block + container.setHolderPosition(session.getLastInteractionBlockPosition()); + container.setUsingRealBlock(true, state.block()); + setCustomName(session, session.getLastInteractionBlockPosition(), container, state); + + return true; + } + } + } + + return false; + } + /** * Will be overwritten in the beacon inventory translator to remove the check, since virtual inventories can't exist. * @@ -145,57 +178,49 @@ public class BlockInventoryHolder extends InventoryHolder { } @Override - public void openInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { + public void openInventory(GeyserSession session, Container container) { ContainerOpenPacket containerOpenPacket = new ContainerOpenPacket(); - containerOpenPacket.setId((byte) inventory.getBedrockId()); + containerOpenPacket.setId((byte) container.getBedrockId()); containerOpenPacket.setType(containerType); - containerOpenPacket.setBlockPosition(inventory.getHolderPosition()); - containerOpenPacket.setUniqueEntityId(inventory.getHolderId()); + containerOpenPacket.setBlockPosition(container.getHolderPosition()); + containerOpenPacket.setUniqueEntityId(container.getHolderId()); session.sendUpstreamPacket(containerOpenPacket); + + GeyserImpl.getInstance().getLogger().debug(session, containerOpenPacket.toString()); } @Override - public void closeInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory, ContainerType type) { - if (!(inventory instanceof Container container)) { - GeyserImpl.getInstance().getLogger().warning("Tried to close a non-container inventory in a block inventory holder! Please report this error on discord."); - GeyserImpl.getInstance().getLogger().warning("Current inventory translator: " + translator.getClass().getSimpleName()); - GeyserImpl.getInstance().getLogger().warning("Current inventory: " + inventory.getClass().getSimpleName()); - // Try to save ourselves? maybe? - // https://github.com/GeyserMC/Geyser/issues/4141 - // TODO: improve once this issue is pinned down - session.setOpenInventory(null); - session.setInventoryTranslator(InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR); - return; - } + public void closeInventory(GeyserSession session, Container container, ContainerType type) { + if (container.isDisplayed() && !(container instanceof LecternContainer)) { + // No need to reset a block since we didn't change any blocks + // But send a container close packet because we aren't destroying the original. + ContainerClosePacket packet = new ContainerClosePacket(); + packet.setId((byte) container.getBedrockId()); + packet.setServerInitiated(true); + packet.setType(type != null ? type : containerType); + session.sendUpstreamPacket(packet); - // Bedrock broke inventory closing. I wish i was kidding. - // "type" is explicitly passed to keep track of which inventory types can be closed without - // ""workarounds"". yippie. - // Further, Lecterns cannot be closed with any of the two methods below. - if (container.isUsingRealBlock() && !(container instanceof LecternContainer)) { - if (type != null) { - // No need to reset a block since we didn't change any blocks - // But send a container close packet because we aren't destroying the original. - ContainerClosePacket packet = new ContainerClosePacket(); - packet.setId((byte) inventory.getBedrockId()); - packet.setServerInitiated(true); - packet.setType(type); - session.sendUpstreamPacket(packet); - return; + if (container.isUsingRealBlock()) { + // Type being null indicates that the ContainerClosePacket is not effective. + // So we yeet away the block! + if (type == null) { + Vector3i holderPos = container.getHolderPosition(); + UpdateBlockPacket blockPacket = new UpdateBlockPacket(); + blockPacket.setDataLayer(0); + blockPacket.setBlockPosition(holderPos); + blockPacket.setDefinition(session.getBlockMappings().getBedrockAir()); + blockPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY); + session.sendUpstreamPacket(blockPacket); + } else { + // We're using a real block and are able to close the block without destroying it, + // so we can don't need to reset it below. + return; + } } - - // Destroy the block. There's no inventory to view => it gets closed! - Vector3i holderPos = inventory.getHolderPosition(); - UpdateBlockPacket blockPacket = new UpdateBlockPacket(); - blockPacket.setDataLayer(0); - blockPacket.setBlockPosition(holderPos); - blockPacket.setDefinition(session.getBlockMappings().getBedrockAir()); - blockPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY); - session.sendUpstreamPacket(blockPacket); } // Reset to correct block - Vector3i holderPos = inventory.getHolderPosition(); + Vector3i holderPos = container.getHolderPosition(); int realBlock = session.getGeyser().getWorldManager().getBlockAt(session, holderPos.getX(), holderPos.getY(), holderPos.getZ()); UpdateBlockPacket blockPacket = new UpdateBlockPacket(); blockPacket.setDataLayer(0); diff --git a/core/src/main/java/org/geysermc/geyser/inventory/holder/InventoryHolder.java b/core/src/main/java/org/geysermc/geyser/inventory/holder/InventoryHolder.java index d61193c7a..a4b2eddae 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/holder/InventoryHolder.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/holder/InventoryHolder.java @@ -26,12 +26,12 @@ package org.geysermc.geyser.inventory.holder; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType; -import org.geysermc.geyser.inventory.Inventory; +import org.geysermc.geyser.inventory.Container; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.translator.inventory.InventoryTranslator; public abstract class InventoryHolder { - public abstract boolean prepareInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory); - public abstract void openInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory); - public abstract void closeInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory, ContainerType containerType); + public abstract boolean canReuseContainer(GeyserSession session, Container container, Container oldInventory); + public abstract boolean prepareInventory(GeyserSession session, Container container); + public abstract void openInventory(GeyserSession session, Container container); + public abstract void closeInventory(GeyserSession session, Container container, ContainerType containerType); } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/item/BannerPattern.java b/core/src/main/java/org/geysermc/geyser/inventory/item/BannerPattern.java index 743fbdc7e..33935e190 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/item/BannerPattern.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/item/BannerPattern.java @@ -88,13 +88,13 @@ public enum BannerPattern { this.bedrockIdentifier = bedrockIdentifier; } - public static @Nullable BannerPattern getByJavaIdentifier(Key key) { + public static BannerPattern getByJavaIdentifier(Key key) { for (BannerPattern bannerPattern : VALUES) { if (bannerPattern.javaIdentifier.equals(key)) { return bannerPattern; } } - return null; + return BASE; // Default fallback } public static @Nullable BannerPattern getByBedrockIdentifier(String bedrockIdentifier) { diff --git a/core/src/main/java/org/geysermc/geyser/inventory/item/GeyserInstrument.java b/core/src/main/java/org/geysermc/geyser/inventory/item/GeyserInstrument.java index 9983a8e90..dc4a2030d 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/item/GeyserInstrument.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/item/GeyserInstrument.java @@ -29,13 +29,13 @@ import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.nbt.NbtMap; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; import org.geysermc.geyser.session.cache.registry.JavaRegistry; import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.geyser.util.MinecraftKey; import org.geysermc.geyser.util.SoundUtils; -import org.geysermc.mcprotocollib.protocol.data.game.Holder; -import org.geysermc.mcprotocollib.protocol.data.game.item.component.Instrument; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.InstrumentComponent; import org.geysermc.mcprotocollib.protocol.data.game.level.sound.BuiltinSound; import java.util.Locale; @@ -77,7 +77,7 @@ public interface GeyserInstrument { * @return the ID of the Java counterpart for the given Bedrock ID. If an invalid Bedrock ID was given, or there is no counterpart, -1 is returned. */ static int bedrockIdToJava(GeyserSession session, int id) { - JavaRegistry instruments = session.getRegistryCache().instruments(); + JavaRegistry instruments = session.getRegistryCache().registry(JavaRegistries.INSTRUMENT); BedrockInstrument bedrockInstrument = BedrockInstrument.getByBedrockId(id); if (bedrockInstrument != null) { for (int i = 0; i < instruments.values().size(); i++) { @@ -90,34 +90,40 @@ public interface GeyserInstrument { return -1; } - static GeyserInstrument fromHolder(GeyserSession session, Holder holder) { - if (holder.isId()) { - return session.getRegistryCache().instruments().byId(holder.id()); + // TODO test in 1.21.5 + static GeyserInstrument fromComponent(GeyserSession session, InstrumentComponent component) { + if (component.instrumentLocation() != null) { + return session.getRegistryCache().registry(JavaRegistries.INSTRUMENT).byKey(component.instrumentLocation()); + } else if (component.instrumentHolder() != null) { + if (component.instrumentHolder().isId()) { + return session.getRegistryCache().registry(JavaRegistries.INSTRUMENT).byId(component.instrumentHolder().id()); + } + InstrumentComponent.Instrument custom = component.instrumentHolder().custom(); + return new Wrapper(custom, session.locale()); } - Instrument custom = holder.custom(); - return new Wrapper(custom, session.locale()); + throw new IllegalStateException("InstrumentComponent must have either a location or a holder"); } - record Wrapper(Instrument instrument, String locale) implements GeyserInstrument { + record Wrapper(InstrumentComponent.Instrument instrument, String locale) implements GeyserInstrument { @Override public String soundEvent() { - return instrument.getSoundEvent().getName(); + return instrument.soundEvent().getName(); } @Override public float range() { - return instrument.getRange(); + return instrument.range(); } @Override public String description() { - return MessageTranslator.convertMessageForTooltip(instrument.getDescription(), locale); + return MessageTranslator.convertMessageForTooltip(instrument.description(), locale); } @Override public BedrockInstrument bedrockInstrument() { - if (instrument.getSoundEvent() instanceof BuiltinSound) { - return BedrockInstrument.getByJavaIdentifier(MinecraftKey.key(instrument.getSoundEvent().getName())); + if (instrument.soundEvent() instanceof BuiltinSound) { + return BedrockInstrument.getByJavaIdentifier(MinecraftKey.key(instrument.soundEvent().getName())); } // Probably custom return null; diff --git a/core/src/main/java/org/geysermc/geyser/inventory/recipe/TrimRecipe.java b/core/src/main/java/org/geysermc/geyser/inventory/recipe/TrimRecipe.java index b5e76a296..b7a9ac741 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/recipe/TrimRecipe.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/recipe/TrimRecipe.java @@ -26,19 +26,30 @@ package org.geysermc.geyser.inventory.recipe; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.TextColor; import org.cloudburstmc.protocol.bedrock.data.TrimMaterial; import org.cloudburstmc.protocol.bedrock.data.TrimPattern; import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.ItemDescriptorWithCount; import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.ItemTagDescriptor; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.item.type.Item; +import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; import org.geysermc.geyser.translator.text.MessageTranslator; +import org.geysermc.mcprotocollib.protocol.data.game.Holder; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.ArmorTrim; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.ProvidesTrimMaterial; + +import java.util.HashMap; +import java.util.Map; /** * Stores information on trim materials and patterns, including smithing armor hacks for pre-1.20. */ public final class TrimRecipe { + private static final Map trimMaterialProviders = new HashMap<>(); + // For CraftingDataPacket public static final String ID = "minecraft:smithing_armor_trim"; public static final ItemDescriptorWithCount BASE = tagDescriptor("minecraft:trimmable_armors"); @@ -49,22 +60,31 @@ public final class TrimRecipe { String key = context.id().asMinimalString(); // Color is used when hovering over the item - // Find the nearest legacy color from the RGB Java gives us to work with - // Also yes this is a COMPLETE hack but it works ok!!!!! - String colorTag = context.data().getCompound("description").getString("color"); - TextColor color = TextColor.fromHexString(colorTag); - String legacy = MessageTranslator.convertMessage(Component.space().color(color)); + // Find the nearest legacy color from the style Java gives us to work with + Component description = MessageTranslator.componentFromNbtTag(context.data().get("description")); + String legacy = MessageTranslator.convertMessage(Component.space().style(description.style())); - String itemIdentifier = context.data().getString("ingredient"); - ItemMapping itemMapping = context.session().getItemMappings().getMapping(itemIdentifier); - if (itemMapping == null) { - // This should never happen so not sure what to do here. - itemMapping = ItemMapping.AIR; + int networkId = context.getNetworkId(context.id()); + ItemMapping trimItem = null; + for (ProvidesTrimMaterial provider : materialProviders().keySet()) { + Holder materialHolder = provider.materialHolder(); + if (context.id().equals(provider.materialLocation()) || (materialHolder != null && materialHolder.isId() && materialHolder.id() == networkId)) { + trimItem = context.session().getItemMappings().getMapping(materialProviders().get(provider)); + break; + } } + + if (trimItem == null) { + // This happens for custom trim materials, not sure what to do here. + GeyserImpl.getInstance().getLogger().debug("Unable to found trim material item for material " + context.id()); + trimItem = ItemMapping.AIR; + } + // Just pick out the resulting color code, without RESET in front. - return new TrimMaterial(key, legacy.substring(2).trim(), itemMapping.getBedrockIdentifier()); + return new TrimMaterial(key, legacy.substring(2).trim(), trimItem.getBedrockIdentifier()); } + // TODO this is WRONG. this changed. FIXME in 1.21.5 public static TrimPattern readTrimPattern(RegistryEntryContext context) { String key = context.id().asMinimalString(); @@ -81,6 +101,19 @@ public final class TrimRecipe { //no-op } + // Lazy initialise + private static Map materialProviders() { + if (trimMaterialProviders.isEmpty()) { + for (Item item : Registries.JAVA_ITEMS.get()) { + ProvidesTrimMaterial provider = item.getComponent(DataComponentTypes.PROVIDES_TRIM_MATERIAL); + if (provider != null) { + trimMaterialProviders.put(provider, item); + } + } + } + return trimMaterialProviders; + } + private static ItemDescriptorWithCount tagDescriptor(String tag) { return new ItemDescriptorWithCount(new ItemTagDescriptor(tag), 1); } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java index ac9e35909..bea3e0507 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java @@ -41,6 +41,7 @@ import org.geysermc.geyser.inventory.item.BedrockEnchantment; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.enchantment.Enchantment; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode; @@ -59,7 +60,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater { private static final int MAX_LEVEL_COST = 40; @Override - public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { + public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { super.updateInventory(translator, session, inventory); AnvilContainer anvilContainer = (AnvilContainer) inventory; updateInventoryState(session, anvilContainer); @@ -81,7 +82,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater { } @Override - public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { + public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { if (super.updateSlot(translator, session, inventory, javaSlot)) return true; AnvilContainer anvilContainer = (AnvilContainer) inventory; @@ -150,7 +151,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater { return 0; } - private void updateTargetSlot(InventoryTranslator translator, GeyserSession session, AnvilContainer anvilContainer, int slot) { + private void updateTargetSlot(InventoryTranslator translator, GeyserSession session, AnvilContainer anvilContainer, int slot) { ItemData itemData = anvilContainer.getItem(slot).getItemData(session); itemData = hijackRepairCost(session, anvilContainer, itemData); @@ -374,7 +375,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater { if (enchantmentComponent != null) { Object2IntMap enchantments = new Object2IntOpenHashMap<>(); for (Map.Entry entry : enchantmentComponent.getEnchantments().entrySet()) { - Enchantment enchantment = session.getRegistryCache().enchantments().byId(entry.getKey()); + Enchantment enchantment = session.getRegistryCache().registry(JavaRegistries.ENCHANTMENT).byId(entry.getKey()); if (enchantment == null) { GeyserImpl.getInstance().getLogger().debug("Unknown Java enchantment in anvil: " + entry.getKey()); continue; diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/ChestInventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/ChestInventoryUpdater.java index a67f594ab..29e02523b 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/updater/ChestInventoryUpdater.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/ChestInventoryUpdater.java @@ -46,7 +46,7 @@ public class ChestInventoryUpdater extends InventoryUpdater { private final int paddedSize; @Override - public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { + public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { super.updateInventory(translator, session, inventory); List bedrockItems = new ArrayList<>(paddedSize); @@ -65,7 +65,7 @@ public class ChestInventoryUpdater extends InventoryUpdater { } @Override - public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { + public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { if (super.updateSlot(translator, session, inventory, javaSlot)) return true; diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/ContainerInventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/ContainerInventoryUpdater.java index c9f313f2a..5294eba04 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/updater/ContainerInventoryUpdater.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/ContainerInventoryUpdater.java @@ -38,7 +38,7 @@ public class ContainerInventoryUpdater extends InventoryUpdater { public static final ContainerInventoryUpdater INSTANCE = new ContainerInventoryUpdater(); @Override - public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { + public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { super.updateInventory(translator, session, inventory); ItemData[] bedrockItems = new ItemData[translator.size]; @@ -53,7 +53,7 @@ public class ContainerInventoryUpdater extends InventoryUpdater { } @Override - public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { + public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { if (super.updateSlot(translator, session, inventory, javaSlot)) return true; diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/CrafterInventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/CrafterInventoryUpdater.java index 4474d420c..621e32b07 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/updater/CrafterInventoryUpdater.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/CrafterInventoryUpdater.java @@ -44,7 +44,7 @@ public class CrafterInventoryUpdater extends InventoryUpdater { public static final CrafterInventoryUpdater INSTANCE = new CrafterInventoryUpdater(); @Override - public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { + public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { ItemData[] bedrockItems; InventoryContentPacket contentPacket; @@ -74,7 +74,7 @@ public class CrafterInventoryUpdater extends InventoryUpdater { } @Override - public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { + public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { int containerId; if (javaSlot < CrafterInventoryTranslator.GRID_SIZE || javaSlot == CrafterInventoryTranslator.JAVA_RESULT_SLOT) { // Parts of the Crafter UI diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/HorseInventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/HorseInventoryUpdater.java index 7441e66d0..2f2c79ebf 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/updater/HorseInventoryUpdater.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/HorseInventoryUpdater.java @@ -38,7 +38,7 @@ public class HorseInventoryUpdater extends InventoryUpdater { public static final HorseInventoryUpdater INSTANCE = new HorseInventoryUpdater(); @Override - public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { + public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { super.updateInventory(translator, session, inventory); ItemData[] bedrockItems = new ItemData[translator.size]; @@ -53,7 +53,7 @@ public class HorseInventoryUpdater extends InventoryUpdater { } @Override - public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { + public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { if (super.updateSlot(translator, session, inventory, javaSlot)) return true; diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/InventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/InventoryUpdater.java index 68ee334ba..4fd6a6aa3 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/updater/InventoryUpdater.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/InventoryUpdater.java @@ -36,7 +36,7 @@ import org.geysermc.geyser.translator.inventory.InventoryTranslator; import java.util.Arrays; public class InventoryUpdater { - public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { + public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { ItemData[] bedrockItems = new ItemData[36]; for (int i = 0; i < 36; i++) { final int offset = i < 9 ? 27 : -9; @@ -48,7 +48,7 @@ public class InventoryUpdater { session.sendUpstreamPacket(contentPacket); } - public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { + public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { if (javaSlot >= translator.size) { InventorySlotPacket slotPacket = new InventorySlotPacket(); slotPacket.setContainerId(ContainerId.INVENTORY); diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/UIInventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/UIInventoryUpdater.java index a23385b53..b025eecbb 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/updater/UIInventoryUpdater.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/UIInventoryUpdater.java @@ -35,7 +35,7 @@ public class UIInventoryUpdater extends InventoryUpdater { public static final UIInventoryUpdater INSTANCE = new UIInventoryUpdater(); @Override - public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { + public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { super.updateInventory(translator, session, inventory); for (int i = 0; i < translator.size; i++) { @@ -51,7 +51,7 @@ public class UIInventoryUpdater extends InventoryUpdater { } @Override - public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { + public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { if (super.updateSlot(translator, session, inventory, javaSlot)) return true; diff --git a/core/src/main/java/org/geysermc/geyser/item/Items.java b/core/src/main/java/org/geysermc/geyser/item/Items.java index 664c956c3..5fd64c4d4 100644 --- a/core/src/main/java/org/geysermc/geyser/item/Items.java +++ b/core/src/main/java/org/geysermc/geyser/item/Items.java @@ -270,9 +270,13 @@ public final class Items { public static final Item COBWEB = register(new BlockItem(builder(), Blocks.COBWEB)); public static final Item SHORT_GRASS = register(new BlockItem(builder(), Blocks.SHORT_GRASS)); public static final Item FERN = register(new BlockItem(builder(), Blocks.FERN)); + public static final Item BUSH = register(new BlockItem(builder(), Blocks.BUSH)); public static final Item AZALEA = register(new BlockItem(builder(), Blocks.AZALEA)); public static final Item FLOWERING_AZALEA = register(new BlockItem(builder(), Blocks.FLOWERING_AZALEA)); public static final Item DEAD_BUSH = register(new BlockItem(builder(), Blocks.DEAD_BUSH)); + public static final Item FIREFLY_BUSH = register(new BlockItem(builder(), Blocks.FIREFLY_BUSH)); + public static final Item SHORT_DRY_GRASS = register(new BlockItem(builder(), Blocks.SHORT_DRY_GRASS)); + public static final Item TALL_DRY_GRASS = register(new BlockItem(builder(), Blocks.TALL_DRY_GRASS)); public static final Item SEAGRASS = register(new BlockItem(builder(), Blocks.SEAGRASS)); public static final Item SEA_PICKLE = register(new BlockItem(builder(), Blocks.SEA_PICKLE)); public static final Item WHITE_WOOL = register(new BlockItem(builder(), Blocks.WHITE_WOOL)); @@ -321,6 +325,8 @@ public final class Items { public static final Item SUGAR_CANE = register(new BlockItem(builder(), Blocks.SUGAR_CANE)); public static final Item KELP = register(new BlockItem(builder(), Blocks.KELP)); public static final Item PINK_PETALS = register(new BlockItem(builder(), Blocks.PINK_PETALS)); + public static final Item WILDFLOWERS = register(new BlockItem(builder(), Blocks.WILDFLOWERS)); + public static final Item LEAF_LITTER = register(new BlockItem(builder(), Blocks.LEAF_LITTER)); public static final Item MOSS_CARPET = register(new BlockItem(builder(), Blocks.MOSS_CARPET)); public static final Item MOSS_BLOCK = register(new BlockItem(builder(), Blocks.MOSS_BLOCK)); public static final Item PALE_MOSS_CARPET = register(new BlockItem(builder(), Blocks.PALE_MOSS_CARPET)); @@ -389,6 +395,7 @@ public final class Items { public static final Item ICE = register(new BlockItem(builder(), Blocks.ICE)); public static final Item SNOW_BLOCK = register(new BlockItem(builder(), Blocks.SNOW_BLOCK)); public static final Item CACTUS = register(new BlockItem(builder(), Blocks.CACTUS)); + public static final Item CACTUS_FLOWER = register(new BlockItem(builder(), Blocks.CACTUS_FLOWER)); public static final Item CLAY = register(new BlockItem(builder(), Blocks.CLAY)); public static final Item JUKEBOX = register(new BlockItem(builder(), Blocks.JUKEBOX)); public static final Item OAK_FENCE = register(new BlockItem(builder(), Blocks.OAK_FENCE)); @@ -891,6 +898,8 @@ public final class Items { public static final Item BAMBOO_CHEST_RAFT = register(new BoatItem("bamboo_chest_raft", builder())); public static final Item STRUCTURE_BLOCK = register(new BlockItem(builder(), Blocks.STRUCTURE_BLOCK)); public static final Item JIGSAW = register(new BlockItem(builder(), Blocks.JIGSAW)); + public static final Item TEST_BLOCK = register(new BlockItem(builder(), Blocks.TEST_BLOCK)); + public static final Item TEST_INSTANCE_BLOCK = register(new BlockItem(builder(), Blocks.TEST_INSTANCE_BLOCK)); public static final Item TURTLE_HELMET = register(new ArmorItem("turtle_helmet", builder())); public static final Item TURTLE_SCUTE = register(new Item("turtle_scute", builder())); public static final Item ARMADILLO_SCUTE = register(new Item("armadillo_scute", builder())); @@ -1027,6 +1036,8 @@ public final class Items { public static final Item BOOK = register(new Item("book", builder())); public static final Item SLIME_BALL = register(new Item("slime_ball", builder())); public static final Item EGG = register(new Item("egg", builder())); + public static final Item BLUE_EGG = register(new Item("blue_egg", builder())); + public static final Item BROWN_EGG = register(new Item("brown_egg", builder())); public static final Item COMPASS = register(new CompassItem("compass", builder())); public static final Item RECOVERY_COMPASS = register(new Item("recovery_compass", builder())); public static final Item BUNDLE = register(new Item("bundle", builder())); @@ -1120,7 +1131,7 @@ public final class Items { public static final Item BLAZE_POWDER = register(new Item("blaze_powder", builder())); public static final Item MAGMA_CREAM = register(new Item("magma_cream", builder())); public static final Item BREWING_STAND = register(new BlockItem(builder(), Blocks.BREWING_STAND)); - public static final Item CAULDRON = register(new BlockItem(builder(), Blocks.CAULDRON, Blocks.POWDER_SNOW_CAULDRON, Blocks.LAVA_CAULDRON, Blocks.WATER_CAULDRON)); + public static final Item CAULDRON = register(new BlockItem(builder(), Blocks.CAULDRON, Blocks.POWDER_SNOW_CAULDRON, Blocks.WATER_CAULDRON, Blocks.LAVA_CAULDRON)); public static final Item ENDER_EYE = register(new Item("ender_eye", builder())); public static final Item GLISTERING_MELON_SLICE = register(new Item("glistering_melon_slice", builder())); public static final Item ARMADILLO_SPAWN_EGG = register(new SpawnEggItem("armadillo_spawn_egg", builder())); @@ -1210,7 +1221,7 @@ public final class Items { public static final Item WRITABLE_BOOK = register(new WritableBookItem("writable_book", builder())); public static final Item WRITTEN_BOOK = register(new WrittenBookItem("written_book", builder())); public static final Item BREEZE_ROD = register(new Item("breeze_rod", builder())); - public static final Item MACE = register(new Item("mace", builder())); + public static final Item MACE = register(new Item("mace", builder().attackDamage(6.0))); public static final Item ITEM_FRAME = register(new Item("item_frame", builder())); public static final Item GLOW_ITEM_FRAME = register(new Item("glow_item_frame", builder())); public static final Item FLOWER_POT = register(new BlockItem(builder(), Blocks.FLOWER_POT)); diff --git a/core/src/main/java/org/geysermc/geyser/item/TooltipOptions.java b/core/src/main/java/org/geysermc/geyser/item/TooltipOptions.java new file mode 100644 index 000000000..2fa9af789 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/TooltipOptions.java @@ -0,0 +1,59 @@ +/* + * 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; + +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.TooltipDisplay; + +@FunctionalInterface +public interface TooltipOptions { + + TooltipOptions ALL_SHOWN = component -> true; + + TooltipOptions ALL_HIDDEN = component -> false; + + boolean showInTooltip(DataComponentType component); + + static TooltipOptions fromComponents(DataComponents components) { + TooltipDisplay display = components.get(DataComponentTypes.TOOLTIP_DISPLAY); + if (display == null) { + return ALL_SHOWN; + } else if (display.hideTooltip()) { + return ALL_HIDDEN; + } else if (display.hiddenComponents().isEmpty()) { + return ALL_SHOWN; + } + + return component -> !display.hiddenComponents().contains(component); + } + + static boolean hideTooltip(DataComponents components) { + TooltipDisplay display = components.get(DataComponentTypes.TOOLTIP_DISPLAY); + return display != null && display.hideTooltip(); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHasher.java b/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHasher.java new file mode 100644 index 000000000..f8abb46ab --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHasher.java @@ -0,0 +1,167 @@ +/* + * 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.hashing; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.KeybindComponent; +import net.kyori.adventure.text.NBTComponent; +import net.kyori.adventure.text.ScoreComponent; +import net.kyori.adventure.text.SelectorComponent; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.TranslatableComponent; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; + +import java.util.function.Supplier; + +/** + * This interface contains various {@link MinecraftHasher}s used to encode properties of {@link Component}s. Usually, you'll only need {@link ComponentHasher#COMPONENT}. + */ +public interface ComponentHasher { + + MinecraftHasher COMPONENT = MinecraftHasher.lazyInitialize(new Supplier<>() { + @Override + public MinecraftHasher get() { + return ACTUAL_COMPONENT; + } + }); + + MinecraftHasher NAMED_COLOR = MinecraftHasher.STRING.cast(NamedTextColor::toString); + + MinecraftHasher DIRECT_COLOR = MinecraftHasher.STRING.cast(TextColor::asHexString); + + MinecraftHasher COLOR = (value, encoder) -> { + if (value instanceof NamedTextColor named) { + return NAMED_COLOR.hash(named, encoder); + } + return DIRECT_COLOR.hash(value, encoder); + }; + + MinecraftHasher DECORATION_STATE = MinecraftHasher.BOOL.cast(state -> switch (state) { + case NOT_SET -> null; // Should never happen since we're using .optional() with NOT_SET as default value below + case FALSE -> false; + case TRUE -> true; + }); + + MinecraftHasher CLICK_EVENT_ACTION = MinecraftHasher.STRING.cast(ClickEvent.Action::toString); + + MinecraftHasher CLICK_EVENT = CLICK_EVENT_ACTION.dispatch("action", ClickEvent::action, action -> switch (action) { + case OPEN_URL -> builder -> builder.accept("url", MinecraftHasher.STRING, ClickEvent::value); + case OPEN_FILE -> builder -> builder.accept("path", MinecraftHasher.STRING, ClickEvent::value); + case RUN_COMMAND, SUGGEST_COMMAND -> builder -> builder.accept("command", MinecraftHasher.STRING, ClickEvent::value); + case CHANGE_PAGE -> builder -> builder.accept("page", MinecraftHasher.STRING, ClickEvent::value); + case COPY_TO_CLIPBOARD -> builder -> builder.accept("value", MinecraftHasher.STRING, ClickEvent::value); + }); + + MinecraftHasher> HOVER_EVENT_ACTION = MinecraftHasher.STRING.cast(HoverEvent.Action::toString); + + MinecraftHasher> HOVER_EVENT = HOVER_EVENT_ACTION.dispatch("action", HoverEvent::action, action -> { + if (action == HoverEvent.Action.SHOW_TEXT) { + return builder -> builder.accept("value", COMPONENT, event -> (Component) event.value()); + } else if (action == HoverEvent.Action.SHOW_ITEM) { + return builder -> builder + .accept("id", MinecraftHasher.KEY, event -> ((HoverEvent.ShowItem) event.value()).item()) + .accept("count", MinecraftHasher.INT, event -> ((HoverEvent.ShowItem) event.value()).count()); // Data components are probably not possible + } + return builder -> builder + .accept("id", MinecraftHasher.KEY, event -> ((HoverEvent.ShowEntity) event.value()).type()) + .accept("uuid", MinecraftHasher.UUID, event -> ((HoverEvent.ShowEntity) event.value()).id()) + .optionalNullable("name", COMPONENT, event -> ((HoverEvent.ShowEntity) event.value()).name()); + }); + + // TODO shadow colours - needs kyori bump + MapBuilder